Skip to content

Commit

Permalink
[ID-2631] Expose zkEvm method for signing ejection transaction (#2338)
Browse files Browse the repository at this point in the history
Co-authored-by: Hayden Fowler <[email protected]>
  • Loading branch information
matthewmuscat and haydenfowler authored Nov 4, 2024
1 parent dce866e commit 2cc70ec
Show file tree
Hide file tree
Showing 6 changed files with 291 additions and 23 deletions.
11 changes: 11 additions & 0 deletions packages/passport/sdk-sample-app/src/components/zkevm/Request.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,17 @@ const EthereumMethods: EthereumMethod[] = [
],
exampleComponents: PersonalSignExamples,
},
{
name: 'im_signEjectionTransaction',
params: [
{
name: 'transaction',
placeholder:
'{ "to": "0x", "data": "", "nonce": "", "chainId": "", "value": "" }',
type: EthereumParamType.object,
},
],
},
{
name: 'eth_signTypedData_v4',
params: [
Expand Down
77 changes: 77 additions & 0 deletions packages/passport/sdk/src/zkEvm/signEjectionTransaction.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { TransactionRequest } from '@ethersproject/providers';
import { Signer } from '@ethersproject/abstract-signer';
import { Flow } from '@imtbl/metrics';
import { BigNumber } from 'ethers';
import { mockUserZkEvm } from '../test/mocks';
import * as transactionHelpers from './transactionHelpers';
import { signEjectionTransaction } from './signEjectionTransaction';
import { JsonRpcError, RpcErrorCode } from './JsonRpcError';

jest.mock('./transactionHelpers');
jest.mock('../network/retry');

describe('im_signEjectionTransaction', () => {
const signedTransactionPayload = {
to: mockUserZkEvm.zkEvm.ethAddress,
data: '123',
chainId: '1',
};

const transactionRequest: TransactionRequest = {
to: mockUserZkEvm.zkEvm.ethAddress,
nonce: BigNumber.from(5),
chainId: 1,
value: BigNumber.from('5'),
};
const ethSigner = {
getAddress: jest.fn(),
} as Partial<Signer> as Signer;
const flow = {
addEvent: jest.fn(),
};

beforeEach(() => {
jest.resetAllMocks();
(transactionHelpers.prepareAndSignEjectionTransaction as jest.Mock).mockResolvedValue(
signedTransactionPayload,
);
});

it('calls prepareAndSignEjectionTransaction with the correct arguments', async () => {
await signEjectionTransaction({
params: [transactionRequest],
ethSigner,
zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress,
flow: flow as unknown as Flow,
});

expect(transactionHelpers.prepareAndSignEjectionTransaction).toHaveBeenCalledWith({
transactionRequest,
ethSigner,
zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress,
flow: flow as unknown as Flow,
});
});

it('calls signEjectionTransaction with invalid params', async () => {
await expect(signEjectionTransaction({
params: [transactionRequest, { test: 'test' }],
ethSigner,
zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress,
flow: flow as unknown as Flow,
})).rejects.toThrow(
new JsonRpcError(RpcErrorCode.INVALID_PARAMS, 'im_signEjectionTransaction requires a singular param (hash)'),
);
});

it('returns the transaction hash', async () => {
const result = await signEjectionTransaction({
params: [transactionRequest],
ethSigner,
zkEvmAddress: mockUserZkEvm.zkEvm.ethAddress,
flow: flow as unknown as Flow,
});

expect(result).toEqual(signedTransactionPayload);
});
});
33 changes: 33 additions & 0 deletions packages/passport/sdk/src/zkEvm/signEjectionTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { TransactionRequest } from '@ethersproject/providers';
import {
prepareAndSignEjectionTransaction,
EjectionTransactionParams,
EjectionTransactionResponse,
} from './transactionHelpers';
import { JsonRpcError, RpcErrorCode } from './JsonRpcError';

type EthSendTransactionEjectionParams = EjectionTransactionParams & {
params: Array<any>;
};

export const signEjectionTransaction = async ({
params,
ethSigner,
zkEvmAddress,
flow,
}: EthSendTransactionEjectionParams): Promise<EjectionTransactionResponse> => {
if (!params || params.length !== 1) {
throw new JsonRpcError(
RpcErrorCode.INVALID_PARAMS,
'im_signEjectionTransaction requires a singular param (hash)',
);
}

const transactionRequest = params[0] as TransactionRequest;
return await prepareAndSignEjectionTransaction({
transactionRequest,
ethSigner,
zkEvmAddress,
flow,
});
};
71 changes: 70 additions & 1 deletion packages/passport/sdk/src/zkEvm/transactionHelpers.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { BigNumber } from 'ethers';
import { BigNumber, BigNumberish } from 'ethers';
import {
StaticJsonRpcProvider,
TransactionRequest,
Expand Down Expand Up @@ -30,6 +30,13 @@ export type TransactionParams = {
flow: Flow;
};

export type EjectionTransactionParams = Pick<TransactionParams, 'ethSigner' | 'zkEvmAddress' | 'flow'>;
export type EjectionTransactionResponse = {
to: string;
data: string;
chainId: string;
};

const getFeeOption = async (
metaTransaction: MetaTransaction,
walletAddress: string,
Expand Down Expand Up @@ -212,3 +219,65 @@ export const prepareAndSignTransaction = async ({

return { signedTransactions, relayerId, nonce };
};

const buildMetaTransactionForEjection = async (
transactionRequest: TransactionRequest,
): Promise<[MetaTransaction, ...MetaTransaction[]]> => {
if (!transactionRequest.to) {
throw new JsonRpcError(
RpcErrorCode.INVALID_PARAMS,
'im_signEjectionTransaction requires a "to" field',
);
}

if (!transactionRequest.nonce) {
throw new JsonRpcError(
RpcErrorCode.INVALID_PARAMS,
'im_signEjectionTransaction requires a "nonce" field',
);
}

if (!transactionRequest.chainId) {
throw new JsonRpcError(
RpcErrorCode.INVALID_PARAMS,
'im_signEjectionTransaction requires a "chainId" field',
);
}

const metaTransaction: MetaTransaction = {
to: transactionRequest.to,
data: transactionRequest.data,
nonce: transactionRequest.nonce,
value: transactionRequest.value,
revertOnError: true,
};

return [metaTransaction];
};

export const prepareAndSignEjectionTransaction = async ({
transactionRequest,
ethSigner,
zkEvmAddress,
flow,
}: EjectionTransactionParams & { transactionRequest: TransactionRequest }): Promise<EjectionTransactionResponse> => {
const metaTransaction = await buildMetaTransactionForEjection(
transactionRequest,
);
flow.addEvent('endBuildMetaTransactions');

const signedTransaction = await signMetaTransactions(
metaTransaction,
transactionRequest.nonce as BigNumberish,
BigNumber.from(transactionRequest.chainId),
zkEvmAddress,
ethSigner,
);
flow.addEvent('endGetSignedMetaTransactions');

return {
to: zkEvmAddress,
data: signedTransaction,
chainId: getEip155ChainId(transactionRequest.chainId as number),
};
};
Loading

0 comments on commit 2cc70ec

Please sign in to comment.