Skip to content

Commit 2737396

Browse files
Feature/aa zero transactions mvp (#468)
* Implement basic Paymaster Transaction flow * Enable flow under FEATURE_PAYMASTER flag
1 parent 8c88250 commit 2737396

File tree

26 files changed

+814
-305
lines changed

26 files changed

+814
-305
lines changed

.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,5 @@ TEST_WALLET_ADDRESS=
77
PROXY_URL=https://proxy.zerion.io/
88
FEATURE_FOOTER_BUG_BUTTON=on
99
FEATURE_SEND_FORM=
10+
FEATURE_PAYMASTER=
1011
MIXPANEL_TOKEN_PUBLIC=

.github/workflows/pr.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ on:
99
default: ''
1010
description: 'Backend env for special features'
1111
required: false
12+
FEATURE_PAYMASTER:
13+
default: 'off'
14+
description: 'Feature flag for paymaster transactions ("on" | "off")'
15+
required: false
1216
DEFI_SDK_API_URL:
1317
default: wss://api-v4.zerion.io/
1418
description: 'Zerion API'
@@ -53,6 +57,7 @@ jobs:
5357
DEFI_SDK_API_URL: ${{ github.event.inputs.DEFI_SDK_API_URL || 'wss://api-v4.zerion.io/' }}
5458
ZERION_API_URL: ${{ github.event.inputs.ZERION_API_URL || 'https://zpi.zerion.io/' }}
5559
BACKEND_ENV: ${{ github.event.inputs.BACKEND_ENV || '' }}
60+
FEATURE_PAYMASTER: ${{ github.event.inputs.FEATURE_PAYMASTER || '' }}
5661
PROXY_URL: ${{ github.event.inputs.PROXY_URL || 'https://proxy.zerion.io/' }}
5762
DEFI_SDK_TRANSACTIONS_API_URL: ${{ github.event.inputs.DEFI_SDK_TRANSACTIONS_API_URL || 'https://transactions.zerion.io' }}
5863
DEFI_SDK_API_TOKEN: Zerion.0JOY6zZTTw6yl5Cvz9sdmXc7d5AhzVMG

package-lock.json

Lines changed: 25 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@
9191
},
9292
"dependencies": {
9393
"@download/blockies": "^1.0.3",
94+
"@ethersproject/abstract-provider": "^5.7.0",
9495
"@react-spring/web": "^9.7.3",
9596
"@store-unit/react": "^1.0.4",
9697
"@tanstack/react-query": "^4.35.3",
@@ -127,6 +128,7 @@
127128
"rlp": "^3.0.0",
128129
"store-unit": "^1.0.3",
129130
"uuid": "^9.0.0",
130-
"webextension-polyfill": "^0.10.0"
131+
"webextension-polyfill": "^0.10.0",
132+
"zksync-ethers": "^5.7.2"
131133
}
132134
}

src/background/Wallet/Wallet.ts

Lines changed: 85 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1-
import type { UnsignedTransaction } from 'ethers';
1+
import type { BigNumberish, UnsignedTransaction } from 'ethers';
22
import { ethers } from 'ethers';
3+
import {
4+
EIP712Signer,
5+
utils as zkSyncUtils,
6+
Provider as ZksProvider,
7+
} from 'zksync-ethers';
38
import type { Emitter } from 'nanoevents';
49
import { createNanoEvents } from 'nanoevents';
510
import { Store } from 'store-unit';
@@ -28,7 +33,10 @@ import {
2833
INTERNAL_ORIGIN_SYMBOL,
2934
} from 'src/background/constants';
3035
import { networksStore } from 'src/modules/networks/networks-store.background';
31-
import type { IncomingTransaction } from 'src/modules/ethereum/types/IncomingTransaction';
36+
import type {
37+
IncomingTransactionAA,
38+
IncomingTransactionWithChainId,
39+
} from 'src/modules/ethereum/types/IncomingTransaction';
3240
import { prepareTransaction } from 'src/modules/ethereum/transactions/prepareTransaction';
3341
import type { Chain } from 'src/modules/networks/Chain';
3442
import { createChain } from 'src/modules/networks/Chain';
@@ -69,6 +77,8 @@ import { backgroundGetBestKnownTransactionCount } from 'src/modules/ethereum/tra
6977
import { toCustomNetworkId } from 'src/modules/ethereum/chains/helpers';
7078
import { normalizeTransactionChainId } from 'src/modules/ethereum/transactions/normalizeTransactionChainId';
7179
import type { ChainId } from 'src/modules/ethereum/transactions/ChainId';
80+
import { FEATURE_PAYMASTER_ENABLED } from 'src/env/config';
81+
import { createTypedData } from 'src/modules/ethereum/account-abstraction/createTypedData';
7282
import type { DaylightEventParams, ScreenViewParams } from '../events';
7383
import { emitter } from '../events';
7484
import type { Credentials, SessionCredentials } from '../account/Credentials';
@@ -92,7 +102,11 @@ import {
92102
ReadonlyAccountContainer,
93103
} from './model/AccountContainer';
94104

95-
async function prepareNonce<T extends { nonce?: number; from?: string }>(
105+
if (FEATURE_PAYMASTER_ENABLED) {
106+
Object.assign(globalThis, { EIP712Signer, zkSyncUtils });
107+
}
108+
109+
async function prepareNonce<T extends { nonce?: BigNumberish; from?: string }>(
96110
transaction: T,
97111
networks: Networks,
98112
chain: string
@@ -899,10 +913,26 @@ export class Wallet {
899913
this.emitter.emit('chainChanged', chain, origin);
900914
}
901915

916+
/** A helper for interpretation in UI */
917+
async uiGetEip712Transaction({
918+
params: { transaction },
919+
context,
920+
}: WalletMethodParams<{ transaction: IncomingTransactionWithChainId }>) {
921+
this.verifyInternalOrigin(context);
922+
923+
const prepared = prepareTransaction(transaction);
924+
const typedData = createTypedData(prepared);
925+
return typedData;
926+
}
927+
902928
private async getProvider(chainId: ChainId) {
903929
const networks = await networksStore.loadNetworksWithChainId(chainId);
904930
const nodeUrl = networks.getRpcUrlInternal(networks.getChainById(chainId));
905-
return new ethers.providers.JsonRpcProvider(nodeUrl);
931+
if (FEATURE_PAYMASTER_ENABLED) {
932+
return new ZksProvider(nodeUrl);
933+
} else {
934+
return new ethers.providers.JsonRpcProvider(nodeUrl);
935+
}
906936
}
907937

908938
private async getSigner(chainId: ChainId) {
@@ -927,7 +957,7 @@ export class Wallet {
927957
context,
928958
...transactionContextParams
929959
}: {
930-
transaction: IncomingTransaction;
960+
transaction: IncomingTransactionAA;
931961
context: Partial<ChannelContext> | undefined;
932962
} & TransactionContextParams): Promise<ethers.providers.TransactionResponse> {
933963
this.verifyInternalOrigin(context);
@@ -968,31 +998,66 @@ export class Wallet {
968998
invariant(chainId, 'Must resolve chainId first');
969999

9701000
const networks = await networksStore.loadNetworksWithChainId(chainId);
971-
const signer = await this.getSigner(chainId);
9721001
const prepared = prepareTransaction(incomingTransaction);
9731002
const txWithFee = await prepareGasAndNetworkFee(prepared, networks);
9741003
const transaction = await prepareNonce(txWithFee, networks, chain);
9751004

976-
try {
977-
const transactionResponse = await signer.sendTransaction({
978-
...transaction,
979-
type: transaction.type || undefined, // to exclude null
980-
});
981-
const safeTx = removeSignature(transactionResponse);
982-
emitter.emit('transactionSent', {
983-
transaction: safeTx,
984-
...transactionContextParams,
985-
});
986-
return safeTx;
987-
} catch (error) {
988-
throw getEthersError(error);
1005+
const paymasterEligible =
1006+
FEATURE_PAYMASTER_ENABLED &&
1007+
Boolean(transaction.customData?.paymasterParams);
1008+
1009+
if (paymasterEligible) {
1010+
console.log('paymasterEligible', { transaction });
1011+
try {
1012+
const { chainId } = transaction;
1013+
invariant(chainId, 'ChainId missing from TransactionRequest');
1014+
const typedData = createTypedData(transaction);
1015+
console.log('will sign typedData:', { typedData });
1016+
const signature = await this.signTypedData_v4({
1017+
context,
1018+
params: { typedData, ...transactionContextParams },
1019+
});
1020+
console.log('will serialize transaction + signature', {
1021+
transaction,
1022+
signature,
1023+
});
1024+
const rawTransaction = zkSyncUtils.serialize({
1025+
...transaction,
1026+
customData: { ...transaction.customData, customSignature: signature },
1027+
});
1028+
1029+
console.log({ rawTransaction });
1030+
return await this.sendSignedTransaction({
1031+
context,
1032+
params: { serialized: rawTransaction, ...transactionContextParams },
1033+
});
1034+
} catch (error) {
1035+
console.log('paymaster tx error', error);
1036+
throw getEthersError(error);
1037+
}
1038+
} else {
1039+
try {
1040+
const signer = await this.getSigner(chainId);
1041+
const transactionResponse = await signer.sendTransaction({
1042+
...transaction,
1043+
type: transaction.type || undefined, // to exclude null
1044+
});
1045+
const safeTx = removeSignature(transactionResponse);
1046+
emitter.emit('transactionSent', {
1047+
transaction: safeTx,
1048+
...transactionContextParams,
1049+
});
1050+
return safeTx;
1051+
} catch (error) {
1052+
throw getEthersError(error);
1053+
}
9891054
}
9901055
}
9911056

9921057
async signAndSendTransaction({
9931058
params,
9941059
context,
995-
}: WalletMethodParams<[IncomingTransaction, TransactionContextParams]>) {
1060+
}: WalletMethodParams<[IncomingTransactionAA, TransactionContextParams]>) {
9961061
this.verifyInternalOrigin(context);
9971062
this.ensureStringOrigin(context);
9981063
const [transaction, transactionContextParams] = params;

src/env/config.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const SOCIAL_API_URL = process.env.SOCIAL_API_URL;
88
export const BACKEND_ENV = process.env.BACKEND_ENV;
99
export const MIXPANEL_TOKEN_PUBLIC = process.env.MIXPANEL_TOKEN_PUBLIC;
1010
export const SLOW_MODE = false;
11+
export const FEATURE_PAYMASTER_ENABLED = process.env.FEATURE_PAYMASTER === 'on';
1112

1213
if (!PROXY_URL) {
1314
throw new Error('PROXY_URL must be defined in ENV');
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { types as zkSyncTypes } from 'zksync-ethers';
2+
import { EIP712Signer, utils as zkSyncUtils } from 'zksync-ethers';
3+
import { invariant } from 'src/shared/invariant';
4+
import { normalizeChainId } from 'src/shared/normalizeChainId';
5+
6+
/**
7+
* Creates an "EIP-712 transaction": a TypedData object
8+
* that the user signs. The signature will be used when serializing the TransactionRequest
9+
*/
10+
export function createTypedData(transaction: zkSyncTypes.TransactionRequest) {
11+
invariant(
12+
Boolean(transaction.customData),
13+
'createTypedData expects a transaction with paymaster data'
14+
);
15+
const { chainId } = transaction;
16+
invariant(chainId, 'ChainId missing from TransactionRequest');
17+
const chainIdAsIntString = String(parseInt(normalizeChainId(chainId)));
18+
return {
19+
types: {
20+
Transaction: zkSyncUtils.EIP712_TYPES.Transaction,
21+
EIP712Domain: [
22+
{ name: 'name', type: 'string' },
23+
{ name: 'version', type: 'string' },
24+
{ name: 'chainId', type: 'uint256' },
25+
],
26+
},
27+
domain: {
28+
name: 'zkSync',
29+
version: '2',
30+
chainId: chainIdAsIntString,
31+
},
32+
primaryType: 'Transaction',
33+
message: EIP712Signer.getSignInput(transaction),
34+
};
35+
}

src/modules/ethereum/transactions/addressAction/creators.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
} from 'src/modules/defi-sdk/queries';
88
import type { Networks } from 'src/modules/networks/Networks';
99
import type { Chain } from 'src/modules/networks/Chain';
10+
import type { BigNumberish } from 'ethers';
1011
import { ethers } from 'ethers';
1112
import { UnsupportedNetwork } from 'src/modules/networks/errors';
1213
import { normalizeChainId } from 'src/shared/normalizeChainId';
@@ -171,7 +172,7 @@ export async function pendingTransactionToAddressAction(
171172
export async function incomingTxToIncomingAddressAction(
172173
transactionObject: {
173174
transaction: IncomingTransactionWithChainId & {
174-
nonce?: number;
175+
nonce?: BigNumberish;
175176
from: string;
176177
};
177178
} & Pick<TransactionObject, 'hash' | 'receipt' | 'timestamp' | 'dropped'>,
@@ -191,7 +192,10 @@ export async function incomingTxToIncomingAddressAction(
191192
chain: chain.toString(),
192193
status: 'pending',
193194
fee: null,
194-
nonce: transaction.nonce ?? -1,
195+
// nonce can be "BigNumberish" due to
196+
// ethers types: {import("@ethersproject/abstract-provider").TransactionRequest}
197+
// Converting bignumber to number cannot be safe, but can nonce be really > MAX_SAFE_INTEGER?
198+
nonce: (transaction.nonce as number) ?? -1,
195199
},
196200
datetime: new Date(timestamp ?? Date.now()).toISOString(),
197201
label,

src/modules/ethereum/transactions/fetchAndAssignGasPrice.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ async function fetchGasPriceForTransaction(
4747
return fetchGasPrice(chain, networks);
4848
}
4949

50-
function hasGasEstimation(transaction: IncomingTransaction) {
50+
export function hasGasEstimation(transaction: IncomingTransaction) {
5151
const gas = getGas(transaction);
5252
return gas && !ethers.BigNumber.from(gas).isZero();
5353
}

src/modules/ethereum/transactions/prepareTransaction.ts

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import { valueToHex } from 'src/shared/units/valueToHex';
2-
import type { UnsignedTransaction } from '../types/UnsignedTransaction';
3-
import type { IncomingTransaction } from '../types/IncomingTransaction';
2+
import type { types as zkSyncTypes } from 'zksync-ethers';
3+
import type { TransactionRequest } from '@ethersproject/abstract-provider';
4+
import type { IncomingTransactionAA } from '../types/IncomingTransaction';
45

5-
const knownFields: Array<keyof UnsignedTransaction> = [
6+
const knownFields: Array<
7+
| (keyof TransactionRequest & zkSyncTypes.TransactionRequest)
8+
| 'gasPerPubdataByteLimit'
9+
> = [
610
'from',
711
'to',
812
'nonce',
@@ -15,14 +19,15 @@ const knownFields: Array<keyof UnsignedTransaction> = [
1519
'gasPrice',
1620
'maxPriorityFeePerGas',
1721
'maxFeePerGas',
22+
'customData',
23+
'gasPerPubdataByteLimit',
1824
];
1925

20-
export function prepareTransaction(incomingTransaction: IncomingTransaction) {
21-
const transaction: UnsignedTransaction = {};
26+
export function prepareTransaction(incomingTransaction: IncomingTransactionAA) {
27+
const transaction: zkSyncTypes.TransactionRequest = {};
2228
for (const field of knownFields) {
23-
const knownField = field as keyof UnsignedTransaction;
29+
const knownField = field as keyof TransactionRequest;
2430
if (incomingTransaction[knownField] !== undefined) {
25-
// TODO: convert using `valueToHex` each value?
2631
// @ts-ignore
2732
transaction[knownField] = incomingTransaction[knownField];
2833
}

0 commit comments

Comments
 (0)