Skip to content

Commit

Permalink
feat: only send 1 meta transaction when sponsored [ID-1152] (#1080)
Browse files Browse the repository at this point in the history
  • Loading branch information
yundifu authored Oct 30, 2023
1 parent abc4a2d commit b9a3940
Show file tree
Hide file tree
Showing 2 changed files with 101 additions and 30 deletions.
49 changes: 49 additions & 0 deletions packages/passport/sdk/src/zkEvm/sendTransaction.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,55 @@ describe('sendTransaction', () => {
);
});

it('calls relayerClient.ethSendTransaction with sponsored meta transaction', async () => {
(retryWithDelay as jest.Mock).mockResolvedValue({
status: RelayerTransactionStatus.SUCCESSFUL,
hash: transactionHash,
} as RelayerTransaction);

const mockImxFeeOption = {
tokenPrice: '0',
tokenSymbol: 'IMX',
tokenDecimals: 18,
tokenAddress: '0x1337',
recipientAddress: '0x7331',
};

relayerClient.imGetFeeOptions.mockResolvedValue([mockImxFeeOption]);

const result = await sendTransaction({
params: [transactionRequest],
magicProvider,
jsonRpcProvider: jsonRpcProvider as JsonRpcProvider,
relayerClient: relayerClient as unknown as RelayerClient,
user: mockUserZkEvm,
guardianClient: guardianClient as unknown as GuardianClient,
});

expect(result).toEqual(transactionHash);
expect(guardianClient.validateEVMTransaction).toHaveBeenCalledWith(
{
chainId: chainIdEip155,
nonce,
user: mockUserZkEvm,
metaTransactions: [
{
data: transactionRequest.data,
revertOnError: true,
to: mockUserZkEvm.zkEvm.ethAddress,
value: '0x00',
nonce,
},
],
},
);

expect(relayerClient.ethSendTransaction).toHaveBeenCalledWith(
mockUserZkEvm.zkEvm.ethAddress,
signedTransactions,
);
});

it('calls guardian.evaluateTransaction with the correct arguments', async () => {
(retryWithDelay as jest.Mock).mockResolvedValue({
status: RelayerTransactionStatus.SUCCESSFUL,
Expand Down
82 changes: 52 additions & 30 deletions packages/passport/sdk/src/zkEvm/sendTransaction.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {
ExternalProvider, JsonRpcProvider, TransactionRequest, Web3Provider,
ExternalProvider, JsonRpcProvider, JsonRpcSigner, TransactionRequest, Web3Provider,
} from '@ethersproject/providers';
import { BigNumber } from 'ethers';
import { BigNumber, BigNumberish } from 'ethers';
import { getEip155ChainId, getNonce, getSignedMetaTransactions } from './walletHelpers';
import { MetaTransaction, RelayerTransactionStatus } from './types';
import { JsonRpcError, RpcErrorCode } from './JsonRpcError';
Expand All @@ -22,6 +22,51 @@ export type EthSendTransactionParams = {
params: Array<any>;
};

const getMetaTransactions = async (
metaTransaction: MetaTransaction,
nonce: BigNumberish,
chainId: BigNumber,
walletAddress: string,
signer: JsonRpcSigner,
relayerClient: RelayerClient,
): Promise<MetaTransaction[]> => {
// NOTE: We sign the transaction before getting the fee options because
// accurate estimation of a transaction gas cost is only possible if the smart
// wallet contract can actually execute it (in a simulated environment) - and
// it can only execute signed transactions.
const signedTransaction = await getSignedMetaTransactions(
[metaTransaction],
nonce,
chainId,
walletAddress,
signer,
);

// TODO: ID-698 Add support for non-native gas payments (e.g ERC20, feeTransaction initialisation must change)

// NOTE: "Fee Options" represent the multiple ways we could pay for the gas
// used in this transaction. Each fee option has a "recipientAddress" we
// should transfer the payment to, an amount and a currency. We choose one
// option and build a transaction that sends the expected currency amount for
// that option to the specified address.
const feeOptions = await relayerClient.imGetFeeOptions(walletAddress, signedTransaction);
const imxFeeOption = feeOptions.find((feeOption) => feeOption.tokenSymbol === 'IMX');
if (!imxFeeOption) {
throw new Error('Failed to retrieve fees for IMX token');
}

const feeMetaTransaction: MetaTransaction = {
nonce,
to: imxFeeOption.recipientAddress,
value: imxFeeOption.tokenPrice,
revertOnError: true,
};
if (BigNumber.from(feeMetaTransaction.value).isZero()) {
return [metaTransaction];
}
return [metaTransaction, feeMetaTransaction];
};

export const sendTransaction = ({
params,
magicProvider,
Expand Down Expand Up @@ -50,49 +95,26 @@ export const sendTransaction = ({
revertOnError: true,
};

// NOTE: We sign the transaction before getting the fee options because
// accurate estimation of a transaction gas cost is only possible if the smart
// wallet contract can actually execute it (in a simulated environment) - and
// it can only execute signed transactions.
const signedTransaction = await getSignedMetaTransactions(
[metaTransaction],
const metaTransactions = await getMetaTransactions(
metaTransaction,
nonce,
chainIdBigNumber,
user.zkEvm.ethAddress,
signer,
relayerClient,
);

// TODO: ID-698 Add support for non-native gas payments (e.g ERC20, feeTransaction initialisation must change)

// NOTE: "Fee Options" represent the multiple ways we could pay for the gas
// used in this transaction. Each fee option has a "recipientAddress" we
// should transfer the payment to, an amount and a currency. We choose one
// option and build a transaction that sends the expected currency amount for
// that option to the specified address.
const feeOptions = await relayerClient.imGetFeeOptions(user.zkEvm.ethAddress, signedTransaction);
const imxFeeOption = feeOptions.find((feeOption) => feeOption.tokenSymbol === 'IMX');
if (!imxFeeOption) {
throw new Error('Failed to retrieve fees for IMX token');
}

const feeMetaTransaction: MetaTransaction = {
nonce,
to: imxFeeOption.recipientAddress,
value: imxFeeOption.tokenPrice,
revertOnError: true,
};

await guardianClient.validateEVMTransaction({
chainId: getEip155ChainId(chainId),
nonce: convertBigNumberishToString(nonce),
user,
metaTransactions: [metaTransaction, feeMetaTransaction],
metaTransactions,
});

// NOTE: We sign again because we now are adding the fee transaction, so the
// whole payload is different and needs a new signature.
const signedTransactions = await getSignedMetaTransactions(
[metaTransaction, feeMetaTransaction],
metaTransactions,
nonce,
chainIdBigNumber,
user.zkEvm.ethAddress,
Expand Down

0 comments on commit b9a3940

Please sign in to comment.