on master help sign in
Allow for batching of transactions

The Azimuth delayed release contracts only allow for one star to be withdrawn at a time. This change reworks the ethereum transaction logic to allow for a batch of transactions to be executed at once.

commitfd5b44c2fe9a4a3e5134e727357725fb62734def authorLiam Fitzgerald <hastuc-dibtux@gmail.com> dateFri 25 Oct 2019, 16:53 (7 years ago) parentc31b9a7
3 files changed+133−89
src/components/InlineEthereumTransaction.js+74−53 src/lib/explorer.js+5 src/lib/useEthereumTransaction.js+54−36
src/components/InlineEthereumTransaction.js+74−53
@@ -10,8 +10,9 @@ import {
1010 Text,
1111} from 'indigo-react';
1212
13−import { useExploreTxUrl } from 'lib/explorer';
13+import { useExploreTxUrl, useExploreTxUrls } from 'lib/explorer';
1414import { hexify } from 'lib/txn';
15+import pluralize from 'lib/pluralize';
1516
1617import { composeValidator, buildCheckboxValidator } from 'form/validators';
1718import BridgeForm from 'form/BridgeForm';
@@ -38,11 +39,11 @@ export default function InlineEthereumTransaction({
3839 gasPrice,
3940 setGasPrice,
4041 resetGasPrice,
41− txHash,
42+ txHashes,
4243 nonce,
4344 chainId,
4445 needFunds,
45− signedTransaction,
46+ signedTransactions,
4647 confirmationProgress,
4748
4849 // additional from parent
@@ -61,8 +62,6 @@ export default function InlineEthereumTransaction({
6162 // show signed tx only when signing (for offline usage)
6263 const showSignedTx = signed;
6364
64− const exploreTxUrl = useExploreTxUrl(txHash);
65−
6665 const validate = useMemo(
6766 () =>
6867 composeValidator({
@@ -131,9 +130,11 @@ export default function InlineEthereumTransaction({
131130 }
132131 };
133132
134− const serializedTxHex = useMemo(
135− () => signedTransaction && hexify(signedTransaction.serialize()),
136− [signedTransaction]
133+ const serializedTxsHex = useMemo(
134+ () =>
135+ signedTransactions &&
136+ signedTransactions.map(stx => hexify(stx.serialize())),
137+ [signedTransactions]
137138 );
138139
139140 return (
@@ -218,37 +219,11 @@ export default function InlineEthereumTransaction({
218219 disabled={!showSignedTx}
219220 />
220221 <Condition when="viewSigned" is={true}>
221− <Grid.Divider />
222− <Grid.Item
223− full
224− as={Flex}
225− justify="between"
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 />
222+ <SignedTransactionList
223+ serializedTxsHex={serializedTxsHex}
224+ gasPrice={gasPrice}
225+ nonce={nonce}
226+ />
252227 </Condition>
253228 </>
254229 )}
@@ -256,20 +231,8 @@ export default function InlineEthereumTransaction({
256231 {showReceipt && (
257232 <>
258233 <Grid.Divider />
259− <Grid.Item full as={Flex} col className="pv4">
260− <Flex.Item as={Flex} row justify="between">
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>
234+ <HashReceiptList txHashes={txHashes} />
235+
273236 <Grid.Divider />
274237 </>
275238 )}
@@ -288,3 +251,61 @@ export default function InlineEthereumTransaction({
288251 </Grid>
289252 );
290253}
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) {
2020
2121 return `${domain}/tx/${txHash}`;
2222}
23+export function useExploreTxUrls(txHashes = []) {
24+ const domain = useEtherscanDomain();
25+
26+ return txHashes.map(txHash => `${domain}/tx/${txHash}`);
27+}
2328
2429export function useExploreAddressUrl(address) {
2530 const domain = useEtherscanDomain();
src/lib/useEthereumTransaction.js+54−36
@@ -66,14 +66,14 @@ export default function useEthereumTransaction(
6666 const [nonce, setNonce] = useState();
6767 const { gasPrice, setGasPrice, resetGasPrice } = useGasPrice(initialGasPrice);
6868 const [gasLimit] = useState(initialGasLimit);
69− const [unsignedTransaction, setUnsignedTransaction] = useState();
70− const [signedTransaction, setSignedTransaction] = useState();
71− const [txHash, setTxHash] = useState();
69+ const [unsignedTransactions, setUnsignedTransactions] = useState();
70+ const [signedTransactions, setSignedTransactions] = useState();
71+ const [txHashes, setTxHashes] = useState();
7272 const [needFunds, setNeedFunds] = useState();
7373 const [confirmationProgress, setConfirmationProgress] = useState(0.0);
7474
7575 const initializing = nonce === undefined || chainId === undefined;
76− const constructed = !!unsignedTransaction;
76+ const constructed = !!unsignedTransactions;
7777 const isDefaultState = state === STATE.NONE;
7878 const signed = state === STATE.SIGNED;
7979 const broadcasted = state === STATE.BROADCASTED;
@@ -88,31 +88,39 @@ export default function useEthereumTransaction(
8888
8989 const construct = useCallback(
9090 async (...args) =>
91− setUnsignedTransaction(await transactionBuilder(...args)),
91+ setUnsignedTransactions(await transactionBuilder(...args)),
9292 [transactionBuilder]
9393 );
9494
95− const unconstruct = useCallback(() => setUnsignedTransaction(undefined), [
96− setUnsignedTransaction,
95+ const unconstruct = useCallback(() => setUnsignedTransactions(undefined), [
96+ setUnsignedTransactions,
9797 ]);
9898
9999 const generateAndSign = useCallback(async () => {
100100 try {
101101 setError(undefined);
102102
103− const txn = await signTransaction({
104− wallet: _wallet,
105− walletType,
106− walletHdPath,
107− networkType,
108− txn: unsignedTransaction,
109− nonce,
110− chainId,
111− gasPrice: gasPrice.toFixed(0),
112− gasLimit,
113− });
114−
115− setSignedTransaction(txn);
103+ const utxs = Array.isArray(unsignedTransactions)
104+ ? unsignedTransactions
105+ : [unsignedTransactions];
106+
107+ const txns = await Promise.all(
108+ utxs.map((utx, i) =>
109+ signTransaction({
110+ wallet: _wallet,
111+ walletType,
112+ walletHdPath,
113+ networkType,
114+ txn: utx,
115+ nonce: nonce + i,
116+ chainId,
117+ gasPrice: gasPrice.toFixed(0),
118+ gasLimit,
119+ })
120+ )
121+ );
122+
123+ setSignedTransactions(txns);
116124 setState(STATE.SIGNED);
117125 } catch (error) {
118126 setError(error);
@@ -125,7 +133,7 @@ export default function useEthereumTransaction(
125133 networkType,
126134 nonce,
127135 setError,
128− unsignedTransaction,
136+ unsignedTransactions,
129137 walletHdPath,
130138 walletType,
131139 ]);
@@ -135,10 +143,12 @@ export default function useEthereumTransaction(
135143 setConfirmationProgress(0.0);
136144 setError(undefined);
137145
138− const rawTx = hexify(signedTransaction.serialize());
139− const costGwei = toBN(gasLimit).mul(toBN(gasPrice));
140− const cost = toWei(costGwei.toString(), 'gwei');
146+ const rawTxs = signedTransactions.map(stx => hexify(stx.serialize()));
141147
148+ const costGwei = toBN(gasLimit)
149+ .mul(toBN(gasPrice))
150+ .mul(toBN(rawTxs.length));
151+ const cost = toWei(costGwei.toString(), 'gwei');
142152 let usedTank = false;
143153 // if this ethereum transaction is being executed by a specific point
144154 // see if we can use the tank
@@ -148,27 +158,35 @@ export default function useEthereumTransaction(
148158 pointCursor.value,
149159 _wallet.address,
150160 cost,
151− [rawTx],
161+ rawTxs,
152162 (address, minBalance, balance) =>
153163 setNeedFunds({ address, minBalance, balance }),
154164 () => setNeedFunds(undefined)
155165 );
156166 }
157167
158− const txHash = await sendSignedTransaction(
159− _web3,
160− signedTransaction,
161− /* doubtNonceError */ usedTank
168+ const txHashes = await signedTransactions.reduce(
169+ (acc, stx) =>
170+ acc.then(hashes =>
171+ sendSignedTransaction(
172+ _web3,
173+ stx,
174+ /* doubtNonceError */ usedTank
175+ ).then(hash => [...hashes, hash])
176+ ),
177+ Promise.resolve([])
162178 );
163179
180+ setTxHashes(txHashes);
164181 setState(STATE.BROADCASTED);
165− setTxHash(txHash);
166182
167183 await timeout(PROGRESS_ANIMATION_DELAY_MS);
168184
169185 setConfirmationProgress(0.2);
170186
171− await waitForTransactionConfirm(_web3, txHash);
187+ await Promise.all(
188+ txHashes.map(txHash => waitForTransactionConfirm(_web3, txHash))
189+ );
172190
173191 setConfirmationProgress(0.9);
174192
@@ -183,12 +201,12 @@ export default function useEthereumTransaction(
183201 gasPrice,
184202 pointCursor,
185203 setError,
186− signedTransaction,
204+ signedTransactions,
187205 ]);
188206
189207 const reset = useCallback(() => {
190− setTxHash(undefined);
191− setSignedTransaction(undefined);
208+ setTxHashes(undefined);
209+ setSignedTransactions(undefined);
192210 setNonce(undefined);
193211 setChainId(undefined);
194212 resetGasPrice();
@@ -283,8 +301,8 @@ export default function useEthereumTransaction(
283301 error,
284302 inputsLocked,
285303 confirmationProgress,
286− txHash,
287− signedTransaction,
304+ txHashes,
305+ signedTransactions,
288306 gasPrice,
289307 setGasPrice,
290308 resetGasPrice,