Skip to content

Commit c38ded9

Browse files
authored
sendTransactionBatch method (#91)
1 parent 8f2f468 commit c38ded9

File tree

3 files changed

+131
-4
lines changed

3 files changed

+131
-4
lines changed

packages/0xsequence/README.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,6 @@ terminals, and then run a specific test directly from your browser instance by v
2727

2828
1. `yarn test:server` -- in one terminal, to start the webpack server compiling typescript
2929
2. `yarn start:hardhat` -- in another terminal, to start hardhat local ethereum test node
30-
3. open browser to `http://localhost:9999/{browser-test-dir}/{test-filename}.test.html` for example,
30+
3. `yarn start:hardhat2` -- (2nd chain) in another terminal, to start hardhat2 local ethereum test node
31+
4. open browser to `http://localhost:9999/{browser-test-dir}/{test-filename}.test.html` for example,
3132
http://localhost:9999/wallet-provider/dapp.test.html

packages/0xsequence/tests/browser/wallet-provider/dapp.test.ts

+58
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { sequenceContext, WalletContext, JsonRpcSender, JsonRpcRequest, JsonRpcR
66
import { isValidSignature, packMessageData, recoverConfig } from '@0xsequence/wallet'
77
import { addressOf } from '@0xsequence/config'
88
import { testAccounts, getEOAWallet, deployWalletContext, testWalletContext, sendETH } from '../testutils'
9+
import { Transaction, TransactionRequest } from '@0xsequence/transactions'
910

1011
export const tests = async () => {
1112

@@ -361,6 +362,63 @@ export const tests = async () => {
361362
}
362363
})
363364

365+
await test('sendTransaction batch', async () => {
366+
const testAccount = getEOAWallet(testAccounts[1].privateKey)
367+
368+
const ethAmount1 = ethers.utils.parseEther('1.234')
369+
const ethAmount2 = ethers.utils.parseEther('0.456')
370+
371+
const tx1: TransactionRequest = {
372+
to: testAccount.address,
373+
value: ethAmount1,
374+
data: '0x'
375+
}
376+
const tx2: TransactionRequest = {
377+
to: testAccount.address,
378+
value: ethAmount2,
379+
data: '0x'
380+
}
381+
const txBatched = {
382+
...tx1,
383+
auxiliary: [tx2]
384+
}
385+
386+
const toBalanceBefore = await provider.getBalance(testAccount.address)
387+
const txnResp = await signer.sendTransaction(txBatched) // error
388+
389+
await txnResp.wait()
390+
391+
const toBalanceAfter = await provider.getBalance(testAccount.address)
392+
assert.true(toBalanceAfter.sub(toBalanceBefore).mul(1).eq(ethAmount1.add(ethAmount2)), `wallet sent ${ethAmount1} + ${ethAmount2} eth`)
393+
})
394+
395+
await test('sendTransaction batch format 2', async () => {
396+
const testAccount = getEOAWallet(testAccounts[1].privateKey)
397+
398+
const ethAmount1 = ethers.utils.parseEther('1.234')
399+
const ethAmount2 = ethers.utils.parseEther('0.456')
400+
401+
const tx1: TransactionRequest = {
402+
to: testAccount.address,
403+
value: ethAmount1,
404+
data: '0x'
405+
}
406+
407+
const tx2: TransactionRequest = {
408+
to: testAccount.address,
409+
value: ethAmount2,
410+
data: '0x'
411+
}
412+
413+
const toBalanceBefore = await provider.getBalance(testAccount.address)
414+
const txnResp = await signer.sendTransactionBatch([tx1, tx2])
415+
416+
await txnResp.wait()
417+
418+
const toBalanceAfter = await provider.getBalance(testAccount.address)
419+
assert.true(toBalanceAfter.sub(toBalanceBefore).mul(1).eq(ethAmount1.add(ethAmount2)), `wallet sent ${ethAmount1} + ${ethAmount2} eth`)
420+
})
421+
364422
await test('sendETH from the sequence smart wallet (authChain)', async () => {
365423
// multi-chain to send eth on an alternative chain, in this case the authChain
366424
//

packages/provider/src/provider.ts

+71-3
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { BytesLike } from '@ethersproject/bytes'
33
import { Web3Provider as EthersWeb3Provider, ExternalProvider, JsonRpcProvider, Networkish } from "@ethersproject/providers"
44
import { TypedDataDomain, TypedDataField, TypedDataSigner } from '@ethersproject/abstract-signer'
55
import { NetworkConfig, Networks, WalletContext, ChainId, JsonRpcHandler, JsonRpcHandlerFunc, JsonRpcFetchFunc, JsonRpcRequest, JsonRpcResponseCallback, JsonRpcResponse, maybeNetworkId, JsonRpcVersion, JsonRpcSender, isJsonRpcProvider } from '@0xsequence/network'
6-
import { Signer } from '@0xsequence/wallet'
6+
import { resolveArrayProperties, Signer } from '@0xsequence/wallet'
77
import { WalletConfig, WalletState } from '@0xsequence/config'
88
import { Relayer } from '@0xsequence/relayer'
99
import { Deferrable, shallowCopy, resolveProperties } from '@0xsequence/utils'
10-
import { TransactionRequest, TransactionResponse, Transactionish, SignedTransactions } from '@0xsequence/transactions'
10+
import { Transaction, TransactionRequest, TransactionResponse, Transactionish, SignedTransactions } from '@0xsequence/transactions'
1111
import { WalletRequestHandler } from './transports/wallet-request-handler'
1212

1313
// naming..?
@@ -235,6 +235,29 @@ export class Web3Signer extends Signer implements TypedDataSigner {
235235
})
236236
}
237237

238+
// sendTransactionBatch is a convience method to call sendTransaction in a batch format, allowing you to
239+
// send multiple transaction as a single payload and just one on-chain transaction.
240+
async sendTransactionBatch(
241+
transactions: Deferrable<TransactionRequest[] | Transaction[]>,
242+
chainId?: ChainId,
243+
allSigners?: boolean
244+
): Promise<TransactionResponse> {
245+
const batch = await resolveArrayProperties<TransactionRequest[] | Transaction[]>(transactions)
246+
if (!batch || batch.length === 0) {
247+
throw new Error('cannot send empty batch')
248+
}
249+
250+
const tx: TransactionRequest = {
251+
...batch[0],
252+
auxiliary: []
253+
}
254+
if (batch.length > 1) {
255+
tx.auxiliary = batch.splice(1)
256+
}
257+
258+
return this.sendTransaction(tx, chainId, allSigners)
259+
}
260+
238261
signTransactions(transaction: Deferrable<TransactionRequest>, chainId?: ChainId, allSigners?: boolean): Promise<SignedTransactions> {
239262
transaction = shallowCopy(transaction)
240263
// TODO: transaction argument..? make sure to resolve any properties and serialize property before sending over
@@ -320,7 +343,7 @@ export class Web3Signer extends Signer implements TypedDataSigner {
320343
tx.from = sender;
321344
}
322345

323-
const hexTx = (<any>this.provider.constructor).hexlifyTransaction(tx, { from: true })
346+
const hexTx = hexlifyTransaction(tx)
324347

325348
return provider.send('eth_sendTransaction', [hexTx]).then((hash) => {
326349
return hash
@@ -340,3 +363,48 @@ export class Web3Signer extends Signer implements TypedDataSigner {
340363
return this.provider.send("personal_unlockAccount", [address.toLowerCase(), password, null])
341364
}
342365
}
366+
367+
368+
// NOTE: method has been copied + modified from ethers.js JsonRpcProvider
369+
// Convert an ethers.js transaction into a JSON-RPC transaction
370+
371+
const allowedTransactionKeys: { [ key: string ]: boolean } = {
372+
chainId: true, data: true, gasLimit: true, gasPrice:true, nonce: true, to: true, value: true,
373+
from: true, auxiliary: true, expiration: true, afterNonce: true
374+
}
375+
376+
const hexlifyTransaction = (transaction: TransactionRequest, allowExtra?: { [key: string]: boolean }): { [key: string]: string } => {
377+
// Check only allowed properties are given
378+
const allowed = shallowCopy(allowedTransactionKeys)
379+
if (allowExtra) {
380+
for (const key in allowExtra) {
381+
if (allowExtra[key]) { allowed[key] = true; }
382+
}
383+
}
384+
ethers.utils.checkProperties(transaction, allowed)
385+
386+
const result: { [key: string]: any } = {};
387+
388+
// Some nodes (INFURA ropsten; INFURA mainnet is fine) do not like leading zeros.
389+
['gasLimit', 'gasPrice', 'nonce', 'value'].forEach(key => {
390+
if (!(<any>transaction)[key]) { return }
391+
const value = ethers.utils.hexValue((<any>transaction)[key])
392+
if (key === 'gasLimit') { key = 'gas' }
393+
result[key] = value
394+
});
395+
396+
['from', 'to', 'data'].forEach(key => {
397+
if (!(<any>transaction)[key]) { return }
398+
result[key] = ethers.utils.hexlify((<any>transaction)[key])
399+
})
400+
401+
const auxiliary = <any>transaction['auxiliary']
402+
if (auxiliary && auxiliary.length > 0) {
403+
result['auxiliary'] = []
404+
auxiliary.forEach(a => {
405+
result['auxiliary'].push(hexlifyTransaction(a))
406+
})
407+
}
408+
409+
return result
410+
}

0 commit comments

Comments
 (0)