Skip to content

Commit

Permalink
🗞️ Batched transaction sending for Gnosis Safe (LayerZero-Labs#621)
Browse files Browse the repository at this point in the history
  • Loading branch information
janjakubnanista authored May 30, 2024
1 parent c13cee5 commit 8935369
Show file tree
Hide file tree
Showing 7 changed files with 438 additions and 65 deletions.
7 changes: 7 additions & 0 deletions .changeset/brave-days-vanish.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@layerzerolabs/devtools-evm": patch
"@layerzerolabs/devtools": patch
"@layerzerolabs/toolbox-hardhat": patch
---

Add experimental support for batched sending
14 changes: 14 additions & 0 deletions EXPERIMENTAL.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,20 @@ By default, the RPC calls that check the current state of your contracts are exe

`LZ_ENABLE_EXPERIMENTAL_RETRY=`

## Batched transaction sending <a id="batched-send"></a>

Some signers might support batched transaction sending (e.g. Gnosis Safe signer). If turned on, this feature flag will make use of the batched sending. If this feature flag is on and batched sending is not available, regular sending will be used instead.

If the signer used does not support batch sending, <a href="#batched-wait">batched awaiting</a> feature flag will be used to determine which signing strategy to use.

### To enable

`LZ_ENABLE_EXPERIMENTAL_BATCHED_SEND=1`

### To disable

`LZ_ENABLE_EXPERIMENTAL_BATCHED_SEND=`

## Batched transaction awaiting <a id="batched-wait"></a>

By default, the transactions are submitted and awaited one by one. This means a transaction will only be submitted once the previous transaction has been mined (which results in transactions being mined in consecutive blocks, one transaction per block).
Expand Down
82 changes: 45 additions & 37 deletions packages/devtools-evm/src/signer/sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { TransactionReceipt, TransactionRequest } from '@ethersproject/abst
import type { Signer } from '@ethersproject/abstract-signer'
import Safe, { ConnectSafeConfig, EthersAdapter } from '@safe-global/protocol-kit'
import SafeApiKit from '@safe-global/api-kit'
import { MetaTransactionData, OperationType } from '@safe-global/safe-core-sdk-types'
import { MetaTransactionData, OperationType, SafeTransaction } from '@safe-global/safe-core-sdk-types'
import type { EndpointId } from '@layerzerolabs/lz-definitions'
import {
formatEid,
Expand Down Expand Up @@ -67,8 +67,8 @@ export class OmniSignerEVM extends OmniSignerEVMBase {
data: transaction.data,

// optional
...(transaction.gasLimit && { gasLimit: transaction.gasLimit }),
...(transaction.value && { value: transaction.value }),
...(transaction.gasLimit != null && { gasLimit: transaction.gasLimit }),
...(transaction.value != null && { value: transaction.value }),
}
}
}
Expand All @@ -77,43 +77,56 @@ export class OmniSignerEVM extends OmniSignerEVMBase {
* Implements an OmniSigner interface for EVM-compatible chains using Gnosis Safe.
*/
export class GnosisOmniSignerEVM<TSafeConfig extends ConnectSafeConfig> extends OmniSignerEVMBase {
protected safeSdk: Safe | undefined
protected apiKit: SafeApiKit | undefined

constructor(
eid: EndpointId,
signer: Signer,
protected readonly safeUrl: string,
protected readonly safeConfig: TSafeConfig
protected readonly safeConfig: TSafeConfig,
protected readonly ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: signer,
}),
protected readonly apiKit = new SafeApiKit({ txServiceUrl: safeUrl, ethAdapter }),
protected readonly safeSdkPromise: Safe | Promise<Safe> = Safe.create({
ethAdapter,
safeAddress: safeConfig.safeAddress!,
contractNetworks: safeConfig.contractNetworks,
})
) {
super(eid, signer)
}

async sign(_transaction: OmniTransaction): Promise<string> {
throw new Error('Method not implemented.')
async sign(_: OmniTransaction): Promise<string> {
throw new Error(`Signing transactions with safe is currently not supported, use signAndSend instead`)
}

async signAndSend(transaction: OmniTransaction): Promise<OmniTransactionResponse> {
this.assertTransaction(transaction)
const { safeSdk, apiKit } = await this.#initSafe()
return this.signAndSendBatch([transaction])
}

async signAndSendBatch(transactions: OmniTransaction[]): Promise<OmniTransactionResponse> {
assert(transactions.length > 0, `signAndSendBatch received 0 transactions`)

const safeTransaction = await this.#createSafeTransaction(transactions)

return this.#proposeSafeTransaction(safeTransaction)
}

async #proposeSafeTransaction(safeTransaction: SafeTransaction): Promise<OmniTransactionResponse> {
const safeSdk = await this.safeSdkPromise
const safeAddress = await safeSdk.getAddress()
const safeTransaction = await safeSdk.createTransaction({
safeTransactionData: [this.#serializeTransaction(transaction)],
options: {
nonce: await apiKit.getNextNonce(safeAddress),
},
})
const safeTxHash = await safeSdk.getTransactionHash(safeTransaction)
const senderSignature = await safeSdk.signTransactionHash(safeTxHash)
const senderAddress = await this.signer.getAddress()

await apiKit.proposeTransaction({
await this.apiKit.proposeTransaction({
senderSignature: senderSignature.data,
safeAddress,
safeTransactionData: safeTransaction.data,
safeTxHash,
senderAddress,
})

return {
transactionHash: safeTxHash,
wait: async (_confirmations?: number) => {
Expand All @@ -124,30 +137,25 @@ export class GnosisOmniSignerEVM<TSafeConfig extends ConnectSafeConfig> extends
}
}

async #createSafeTransaction(transactions: OmniTransaction[]): Promise<SafeTransaction> {
transactions.forEach((transaction) => this.assertTransaction(transaction))

const safeSdk = await this.safeSdkPromise
const safeAddress = await safeSdk.getAddress()
const nonce = await this.apiKit.getNextNonce(safeAddress)

return safeSdk.createTransaction({
safeTransactionData: transactions.map((transaction) => this.#serializeTransaction(transaction)),
options: { nonce },
})
}

#serializeTransaction(transaction: OmniTransaction): MetaTransactionData {
return {
to: transaction.point.address,
data: transaction.data,
value: '0',
value: String(transaction.value ?? 0),
operation: OperationType.Call,
}
}

async #initSafe() {
if (!this.safeSdk || !this.apiKit) {
const ethAdapter = new EthersAdapter({
ethers,
signerOrProvider: this.signer,
})
this.apiKit = new SafeApiKit({ txServiceUrl: this.safeUrl, ethAdapter })

this.safeSdk = await Safe.create({
ethAdapter,
safeAddress: this.safeConfig.safeAddress!,
contractNetworks: this.safeConfig.contractNetworks,
})
}

return { safeSdk: this.safeSdk, apiKit: this.apiKit }
}
}
Loading

0 comments on commit 8935369

Please sign in to comment.