on master help sign in
Allow for batching of transactions
fd5b44cLiam Fitzgerald
src/components/InlineEthereumTransaction.js+74−53
@@ -10,8 +10,9 @@ import {
10 Text,
10 Text,
11} from 'indigo-react';
11} from 'indigo-react';
12
12
13−import { useExploreTxUrl } from 'lib/explorer';
13+import { useExploreTxUrl, useExploreTxUrls } from 'lib/explorer';
14import { hexify } from 'lib/txn';
14import { hexify } from 'lib/txn';
15+import pluralize from 'lib/pluralize';
15
16
16import { composeValidator, buildCheckboxValidator } from 'form/validators';
17import { composeValidator, buildCheckboxValidator } from 'form/validators';
17import BridgeForm from 'form/BridgeForm';
18import BridgeForm from 'form/BridgeForm';
@@ -38,11 +39,11 @@ export default function InlineEthereumTransaction({
38 gasPrice,
39 gasPrice,
39 setGasPrice,
40 setGasPrice,
40 resetGasPrice,
41 resetGasPrice,
41− txHash,
42+ txHashes,
42 nonce,
43 nonce,
43 chainId,
44 chainId,
44 needFunds,
45 needFunds,
45− signedTransaction,
46+ signedTransactions,
46 confirmationProgress,
47 confirmationProgress,
47
48
48 // additional from parent
49 // additional from parent
@@ -61,8 +62,6 @@ export default function InlineEthereumTransaction({
61 // show signed tx only when signing (for offline usage)
62 // show signed tx only when signing (for offline usage)
62 const showSignedTx = signed;
63 const showSignedTx = signed;
63
64
64− const exploreTxUrl = useExploreTxUrl(txHash);
65−
66 const validate = useMemo(
65 const validate = useMemo(
67 () =>
66 () =>
68 composeValidator({
67 composeValidator({
@@ -131,9 +130,11 @@ export default function InlineEthereumTransaction({
131 }
130 }
132 };
131 };
133
132
134− const serializedTxHex = useMemo(
133+ const serializedTxsHex = useMemo(
135− () => signedTransaction && hexify(signedTransaction.serialize()),
134+ () =>
136− [signedTransaction]
135+ signedTransactions &&
136+ signedTransactions.map(stx => hexify(stx.serialize())),
137+ [signedTransactions]
137 );
138 );
138
139
139 return (
140 return (
@@ -218,37 +219,11 @@ export default function InlineEthereumTransaction({
218 disabled={!showSignedTx}
219 disabled={!showSignedTx}
219 />
220 />
220 <Condition when="viewSigned" is={true}>
221 <Condition when="viewSigned" is={true}>
221− <Grid.Divider />
222+ <SignedTransactionList
222− <Grid.Item
223+ serializedTxsHex={serializedTxsHex}
223− full
224+ gasPrice={gasPrice}
224− as={Flex}
225+ nonce={nonce}
225− justify="between"
226+ />
226− className="pv4 black f5">
227− <Flex.Item>Nonce</Flex.Item>
228− <Flex.Item>{nonce}</Flex.Item>
229− </Grid.Item>
230− <Grid.Divider />
231− <Grid.Item
232− full
233− as={Flex}
234− justify="between"
235− className="pv4 black f5">
236− <Flex.Item>Gas Price</Flex.Item>
237− <Flex.Item>{gasPrice.toFixed()} Gwei</Flex.Item>
238− </Grid.Item>
239− <Grid.Divider />
240− <Grid.Item
241− full
242− as={Flex}
243− justify="between"
244− className="mt3 mb2">
245− <Flex.Item as={H5}>Signed Transaction Hex</Flex.Item>
246− <Flex.Item as={CopyButton} text={serializedTxHex} />
247− </Grid.Item>
248− <Grid.Item full as="code" className="mb4 f6 mono gray4 wrap">
249− {serializedTxHex}
250− </Grid.Item>
251− <Grid.Divider />
252 </Condition>
227 </Condition>
253 </>
228 </>
254 )}
229 )}
@@ -256,20 +231,8 @@ export default function InlineEthereumTransaction({
256 {showReceipt && (
231 {showReceipt && (
257 <>
232 <>
258 <Grid.Divider />
233 <Grid.Divider />
259− <Grid.Item full as={Flex} col className="pv4">
234+ <HashReceiptList txHashes={txHashes} />
260− <Flex.Item as={Flex} row justify="between">
235+
261− <Flex.Item as={H5}>Transaction Hash</Flex.Item>
262− <Flex.Item as={LinkButton} href={exploreTxUrl}>
263− Etherscan↗
264− </Flex.Item>
265− </Flex.Item>
266− <Flex.Item as={Flex}>
267− <Flex.Item flex as="code" className="f6 mono gray4 wrap">
268− {txHash}
269− </Flex.Item>
270− <Flex.Item flex />
271− </Flex.Item>
272− </Grid.Item>
273 <Grid.Divider />
236 <Grid.Divider />
274 </>
237 </>
275 )}
238 )}
@@ -288,3 +251,61 @@ export default function InlineEthereumTransaction({
288 </Grid>
251 </Grid>
289 );
252 );
290}
253}
254+
255+function SignedTransactionList({ serializedTxsHex, nonce, gasPrice }) {
256+ return serializedTxsHex.map((serializedTxHex, i) => (
257+ <React.Fragment key="i">
258+ <Grid.Divider />
259+ <Grid.Item full as={Flex} justify="between" className="pv4 black f5">
260+ <Flex.Item>Nonce</Flex.Item>
261+ <Flex.Item>{nonce + i}</Flex.Item>
262+ </Grid.Item>
263+ <Grid.Divider />
264+ <Grid.Item full as={Flex} justify="between" className="pv4 black f5">
265+ <Flex.Item>Gas Price</Flex.Item>
266+ <Flex.Item>{gasPrice.toFixed()} Gwei</Flex.Item>
267+ </Grid.Item>
268+ <Grid.Divider />
269+ <Grid.Item full as={Flex} justify="between" className="mt3 mb2">
270+ <Flex.Item as={H5}>Signed Transaction Hex</Flex.Item>
271+ <Flex.Item as={CopyButton} text={serializedTxHex} />
272+ </Grid.Item>
273+ <Grid.Item full as="code" className="mb4 f6 mono gray4 wrap">
274+ {serializedTxHex}
275+ </Grid.Item>
276+ <Grid.Divider />
277+ </React.Fragment>
278+ ));
279+}
280+
281+function HashReceiptList({ txHashes }) {
282+ const txUrls = useExploreTxUrls(txHashes);
283+ const header = pluralize(txHashes.length, 'Hash', 'Hashes');
284+ return (
285+ <>
286+ <Grid.Divider />
287+ <Grid.Item full as={Flex} col className="pv4">
288+ <Flex.Item as={H5}>Transaction {header}</Flex.Item>
289+
290+ {txHashes &&
291+ txHashes.map((txHash, i) => (
292+ <Flex.Item as={Flex}>
293+ <>
294+ <Flex.Item
295+ key={i}
296+ flex
297+ as="code"
298+ className="f6 mono gray4 wrap">
299+ {txHash}
300+ </Flex.Item>
301+ <Flex.Item as={LinkButton} href={txUrls[i]}>
302+ Etherscan↗
303+ </Flex.Item>
304+ </>
305+ </Flex.Item>
306+ ))}
307+ </Grid.Item>
308+ <Grid.Divider />
309+ </>
310+ );
311+}
src/lib/explorer.js+5
@@ -20,6 +20,11 @@ export function useExploreTxUrl(txHash) {
20
20
21 return `${domain}/tx/${txHash}`;
21 return `${domain}/tx/${txHash}`;
22}
22}
23+export function useExploreTxUrls(txHashes = []) {
24+ const domain = useEtherscanDomain();
25+
26+ return txHashes.map(txHash => `${domain}/tx/${txHash}`);
27+}
23
28
24export function useExploreAddressUrl(address) {
29export function useExploreAddressUrl(address) {
25 const domain = useEtherscanDomain();
30 const domain = useEtherscanDomain();
src/lib/useEthereumTransaction.js+54−36
@@ -66,14 +66,14 @@ export default function useEthereumTransaction(
66 const [nonce, setNonce] = useState();
66 const [nonce, setNonce] = useState();
67 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
67 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
68 const [gasLimit] = useState(initialGasLimit);
68 const [gasLimit] = useState(initialGasLimit);
69− const [unsignedTransaction, setUnsignedTransaction] = useState();
69+ const [unsignedTransactions, setUnsignedTransactions] = useState();
70− const [signedTransaction, setSignedTransaction] = useState();
70+ const [signedTransactions, setSignedTransactions] = useState();
71− const [txHash, setTxHash] = useState();
71+ const [txHashes, setTxHashes] = useState();
72 const [needFunds, setNeedFunds] = useState();
72 const [needFunds, setNeedFunds] = useState();
73 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
73 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
74
74
75 const initializing = nonce === undefined || chainId === undefined;
75 const initializing = nonce === undefined || chainId === undefined;
76− const constructed = !!unsignedTransaction;
76+ const constructed = !!unsignedTransactions;
77 const isDefaultState = state === STATE.NONE;
77 const isDefaultState = state === STATE.NONE;
78 const signed = state === STATE.SIGNED;
78 const signed = state === STATE.SIGNED;
79 const broadcasted = state === STATE.BROADCASTED;
79 const broadcasted = state === STATE.BROADCASTED;
@@ -88,31 +88,39 @@ export default function useEthereumTransaction(
88
88
89 const construct = useCallback(
89 const construct = useCallback(
90 async (...args) =>
90 async (...args) =>
91− setUnsignedTransaction(await transactionBuilder(...args)),
91+ setUnsignedTransactions(await transactionBuilder(...args)),
92 [transactionBuilder]
92 [transactionBuilder]
93 );
93 );
94
94
95− const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
95+ const unconstruct = useCallback(() => setUnsignedTransactions(undefined), [
96− setUnsignedTransaction,
96+ setUnsignedTransactions,
97 ]);
97 ]);
98
98
99 const generateAndSign = useCallback(async () => {
99 const generateAndSign = useCallback(async () => {
100 try {
100 try {
101 setError(undefined);
101 setError(undefined);
102
102
103+ const utxs = Array.isArray(unsignedTransactions)
104+ ? unsignedTransactions
105+ : [unsignedTransactions];
106+
107+ const txns = await Promise.all(
108+ utxs.map((utx, i) =>
103− const txn = await signTransaction({
109+ signTransaction({
104− wallet: _wallet,
110+ wallet: _wallet,
105− walletType,
111+ walletType,
106− walletHdPath,
112+ walletHdPath,
107− networkType,
113+ networkType,
108− txn: unsignedTransaction,
114+ txn: utx,
109− nonce,
115+ nonce: nonce + i,
110− chainId,
116+ chainId,
111− gasPrice: gasPrice.toFixed(0),
117+ gasPrice: gasPrice.toFixed(0),
112− gasLimit,
118+ gasLimit,
113− });
119+ })
114−
120+ )
115− setSignedTransaction(txn);
121+ );
122+
123+ setSignedTransactions(txns);
116 setState(STATE.SIGNED);
124 setState(STATE.SIGNED);
117 } catch (error) {
125 } catch (error) {
118 setError(error);
126 setError(error);
@@ -125,7 +133,7 @@ export default function useEthereumTransaction(
125 networkType,
133 networkType,
126 nonce,
134 nonce,
127 setError,
135 setError,
128− unsignedTransaction,
136+ unsignedTransactions,
129 walletHdPath,
137 walletHdPath,
130 walletType,
138 walletType,
131 ]);
139 ]);
@@ -135,10 +143,12 @@ export default function useEthereumTransaction(
135 setConfirmationProgress(0.0);
143 setConfirmationProgress(0.0);
136 setError(undefined);
144 setError(undefined);
137
145
138− const rawTx = hexify(signedTransaction.serialize());
146+ const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
139− const costGwei = toBN(gasLimit).mul(toBN(gasPrice));
140− const cost = toWei(costGwei.toString(), 'gwei');
141
147
148+ const costGwei = toBN(gasLimit)
149+ .mul(toBN(gasPrice))
150+ .mul(toBN(rawTxs.length));
151+ const cost = toWei(costGwei.toString(), 'gwei');
142 let usedTank = false;
152 let usedTank = false;
143 // if this ethereum transaction is being executed by a specific point
153 // if this ethereum transaction is being executed by a specific point
144 // see if we can use the tank
154 // see if we can use the tank
@@ -148,27 +158,35 @@ export default function useEthereumTransaction(
148 pointCursor.value,
158 pointCursor.value,
149 _wallet.address,
159 _wallet.address,
150 cost,
160 cost,
151− [rawTx],
161+ rawTxs,
152 (address, minBalance, balance) =>
162 (address, minBalance, balance) =>
153 setNeedFunds({ address, minBalance, balance }),
163 setNeedFunds({ address, minBalance, balance }),
154 () => setNeedFunds(undefined)
164 () => setNeedFunds(undefined)
155 );
165 );
156 }
166 }
157
167
168+ const txHashes = await signedTransactions.reduce(
169+ (acc, stx) =>
170+ acc.then(hashes =>
158− const txHash = await sendSignedTransaction(
171+ sendSignedTransaction(
159− _web3,
172+ _web3,
160− signedTransaction,
173+ stx,
161− /* doubtNonceError */ usedTank
174+ /* doubtNonceError */ usedTank
175+ ).then(hash => [...hashes, hash])
176+ ),
177+ Promise.resolve([])
162 );
178 );
163
179
180+ setTxHashes(txHashes);
164 setState(STATE.BROADCASTED);
181 setState(STATE.BROADCASTED);
165− setTxHash(txHash);
166
182
167 await timeout(PROGRESS_ANIMATION_DELAY_MS);
183 await timeout(PROGRESS_ANIMATION_DELAY_MS);
168
184
169 setConfirmationProgress(0.2);
185 setConfirmationProgress(0.2);
170
186
187+ await Promise.all(
171− await waitForTransactionConfirm(_web3, txHash);
188+ txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
189+ );
172
190
173 setConfirmationProgress(0.9);
191 setConfirmationProgress(0.9);
174
192
@@ -183,12 +201,12 @@ export default function useEthereumTransaction(
183 gasPrice,
201 gasPrice,
184 pointCursor,
202 pointCursor,
185 setError,
203 setError,
186− signedTransaction,
204+ signedTransactions,
187 ]);
205 ]);
188
206
189 const reset = useCallback(() => {
207 const reset = useCallback(() => {
190− setTxHash(undefined);
208+ setTxHashes(undefined);
191− setSignedTransaction(undefined);
209+ setSignedTransactions(undefined);
192 setNonce(undefined);
210 setNonce(undefined);
193 setChainId(undefined);
211 setChainId(undefined);
194 resetGasPrice();
212 resetGasPrice();
@@ -283,8 +301,8 @@ export default function useEthereumTransaction(
283 error,
301 error,
284 inputsLocked,
302 inputsLocked,
285 confirmationProgress,
303 confirmationProgress,
286− txHash,
304+ txHashes,
287− signedTransaction,
305+ signedTransactions,
288 gasPrice,
306 gasPrice,
289 setGasPrice,
307 setGasPrice,
290 resetGasPrice,
308 resetGasPrice,