@imtbl/wallet 2.12.2 → 2.12.3-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/browser/index.js +24 -32
  2. package/dist/node/index.cjs +47 -55
  3. package/dist/node/index.js +26 -34
  4. package/dist/types/guardian/index.d.ts +1 -2
  5. package/dist/types/magic/magicTEESigner.d.ts +8 -7
  6. package/dist/types/types.d.ts +15 -6
  7. package/dist/types/utils/crypto.d.ts +12 -0
  8. package/dist/types/zkEvm/personalSign.d.ts +4 -3
  9. package/dist/types/zkEvm/relayerClient.d.ts +6 -6
  10. package/dist/types/zkEvm/sequenceCompat.d.ts +60 -30
  11. package/dist/types/zkEvm/signTypedDataV4.d.ts +6 -3
  12. package/dist/types/zkEvm/transactionHelpers.d.ts +15 -4
  13. package/dist/types/zkEvm/types.d.ts +5 -6
  14. package/dist/types/zkEvm/user/registerZkEvmUser.d.ts +4 -3
  15. package/dist/types/zkEvm/walletHelpers.d.ts +15 -14
  16. package/dist/types/zkEvm/zkEvmProvider.d.ts +2 -3
  17. package/package.json +5 -6
  18. package/src/guardian/index.ts +3 -3
  19. package/src/magic/magicTEESigner.ts +11 -24
  20. package/src/types.ts +16 -6
  21. package/src/utils/crypto.ts +85 -0
  22. package/src/utils/string.ts +30 -2
  23. package/src/zkEvm/personalSign.ts +6 -5
  24. package/src/zkEvm/relayerClient.ts +11 -11
  25. package/src/zkEvm/sendDeployTransactionAndPersonalSign.ts +1 -1
  26. package/src/zkEvm/sequenceCompat.ts +26 -24
  27. package/src/zkEvm/sessionActivity/sessionActivity.ts +7 -3
  28. package/src/zkEvm/signEjectionTransaction.ts +1 -1
  29. package/src/zkEvm/signTypedDataV4.test.ts +125 -0
  30. package/src/zkEvm/signTypedDataV4.ts +16 -10
  31. package/src/zkEvm/transactionHelpers.ts +20 -11
  32. package/src/zkEvm/types.ts +5 -6
  33. package/src/zkEvm/user/registerZkEvmUser.ts +9 -8
  34. package/src/zkEvm/walletHelpers.ts +112 -63
  35. package/src/zkEvm/zkEvmProvider.ts +32 -32
@@ -1,15 +1,24 @@
1
1
  import {
2
- BigNumberish, Contract, getBytes, hashMessage,
3
- Interface, keccak256, Signer, solidityPacked, ZeroAddress,
4
- TypedDataEncoder, JsonRpcProvider, AbiCoder,
5
- isError,
6
- } from 'ethers';
2
+ keccak256,
3
+ encodePacked,
4
+ encodeAbiParameters,
5
+ parseAbiParameters,
6
+ getContract,
7
+ encodeFunctionData,
8
+ toBytes,
9
+ hashMessage,
10
+ hashTypedData,
11
+ zeroAddress,
12
+ type PublicClient,
13
+ type Hex,
14
+ } from 'viem';
7
15
  import { MetaTransaction, MetaTransactionNormalised, TypedDataPayload } from './types';
8
16
  import {
9
17
  decodeSequenceSignatureV1,
10
18
  encodeSequenceSignatureV1,
11
19
  walletContracts,
12
20
  } from './sequenceCompat';
21
+ import type { WalletSigner } from '../types';
13
22
 
14
23
  const SIGNATURE_WEIGHT = 1; // Weight of a single signature in the multi-sig
15
24
  const TRANSACTION_SIGNATURE_THRESHOLD = 1; // Total required weight in the multi-sig for a transaction
@@ -17,56 +26,74 @@ const PACKED_SIGNATURE_THRESHOLD = 2; // Total required weight in the multi-sig
17
26
 
18
27
  const ETH_SIGN_FLAG = '02';
19
28
  const ETH_SIGN_PREFIX = '\x19\x01';
20
- const META_TRANSACTIONS_TYPE = `tuple(
21
- bool delegateCall,
22
- bool revertOnError,
23
- uint256 gasLimit,
24
- address target,
25
- uint256 value,
26
- bytes data
27
- )[]`;
29
+
30
+ // ABI parameter type for meta transactions array
31
+ const META_TRANSACTIONS_ABI_TYPE = parseAbiParameters(
32
+ '(bool delegateCall, bool revertOnError, uint256 gasLimit, address target, uint256 value, bytes data)[]',
33
+ );
28
34
 
29
35
  export const getNormalisedTransactions = (txs: MetaTransaction[]): MetaTransactionNormalised[] => txs.map((t) => ({
30
36
  delegateCall: t.delegateCall === true,
31
37
  revertOnError: t.revertOnError === true,
32
38
  gasLimit: t.gasLimit ?? BigInt(0),
33
- target: t.to ?? ZeroAddress,
39
+ target: (t.to ?? zeroAddress) as `0x${string}`,
34
40
  value: t.value ?? BigInt(0),
35
- data: t.data ?? '0x',
41
+ data: (t.data ?? '0x') as `0x${string}`,
36
42
  }));
37
43
 
38
44
  export const digestOfTransactionsAndNonce = (
39
- nonce: BigNumberish,
45
+ nonce: bigint,
40
46
  normalisedTransactions: MetaTransactionNormalised[],
41
- ): string => {
42
- const packMetaTransactionsNonceData = AbiCoder.defaultAbiCoder().encode(
43
- ['uint256', META_TRANSACTIONS_TYPE],
44
- [nonce, normalisedTransactions],
47
+ ): Hex => {
48
+ // Convert normalised transactions to the format expected by encodeAbiParameters
49
+ const txsForEncoding = normalisedTransactions.map((t) => ({
50
+ delegateCall: t.delegateCall,
51
+ revertOnError: t.revertOnError,
52
+ gasLimit: t.gasLimit,
53
+ target: t.target as `0x${string}`,
54
+ value: t.value,
55
+ data: t.data as `0x${string}`,
56
+ }));
57
+
58
+ const packMetaTransactionsNonceData = encodeAbiParameters(
59
+ [{ type: 'uint256' }, ...META_TRANSACTIONS_ABI_TYPE],
60
+ [nonce, txsForEncoding],
45
61
  );
46
62
  return keccak256(packMetaTransactionsNonceData);
47
63
  };
48
64
 
49
65
  export const encodedTransactions = (
50
66
  normalisedTransactions: MetaTransactionNormalised[],
51
- ): string => AbiCoder.defaultAbiCoder().encode(
52
- [META_TRANSACTIONS_TYPE],
53
- [normalisedTransactions],
54
- );
67
+ ): Hex => {
68
+ const txsForEncoding = normalisedTransactions.map((t) => ({
69
+ delegateCall: t.delegateCall,
70
+ revertOnError: t.revertOnError,
71
+ gasLimit: t.gasLimit,
72
+ target: t.target as `0x${string}`,
73
+ value: t.value,
74
+ data: t.data as `0x${string}`,
75
+ }));
76
+
77
+ return encodeAbiParameters(
78
+ META_TRANSACTIONS_ABI_TYPE,
79
+ [txsForEncoding],
80
+ );
81
+ };
55
82
 
56
83
  /**
57
- * This helper function is used to coerce the type <BigNumber | undefined> to BigNumber for the
84
+ * This helper function is used to coerce the type <bigint | undefined> to bigint for the
58
85
  * getNonce function above.
59
- * @param {BigNumber} nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space.
60
- * @returns {BigNumber} The passed in nonceSpace or instead initialises the nonce to 0.
86
+ * @param nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space.
87
+ * @returns The passed in nonceSpace or instead initialises the nonce to 0.
61
88
  */
62
89
  export const coerceNonceSpace = (nonceSpace?: bigint): bigint => nonceSpace || 0n;
63
90
 
64
91
  /**
65
92
  * This helper function is used to encode the nonce into a 256 bit value where the space is encoded into
66
93
  * the first 160 bits, and the nonce the remaining 96 bits.
67
- * @param {BigNumber} nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space.
68
- * @param nonce {BigNumber} nonce - Sequential number starting at 0, and incrementing in single steps e.g. 0,1,2,...
69
- * @returns {BigNumber} The encoded value where the space is left shifted 96 bits, and the nonce is in the first 96 bits.
94
+ * @param nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space.
95
+ * @param nonce - Sequential number starting at 0, and incrementing in single steps e.g. 0,1,2,...
96
+ * @returns The encoded value where the space is left shifted 96 bits, and the nonce is in the first 96 bits.
70
97
  */
71
98
  export const encodeNonce = (nonceSpace: bigint, nonce: bigint): bigint => {
72
99
  const shiftedSpace = BigInt(nonceSpace) * (2n ** 96n);
@@ -80,25 +107,31 @@ export const encodeNonce = (nonceSpace: bigint, nonce: bigint): bigint => {
80
107
  * contract wallet if required.
81
108
  */
82
109
  export const getNonce = async (
83
- rpcProvider: JsonRpcProvider,
110
+ rpcProvider: PublicClient,
84
111
  smartContractWalletAddress: string,
85
112
  nonceSpace?: bigint,
86
113
  ): Promise<bigint> => {
87
114
  try {
88
- const contract = new Contract(
89
- smartContractWalletAddress,
90
- walletContracts.mainModule.abi,
91
- rpcProvider,
92
- );
115
+ const contract = getContract({
116
+ address: smartContractWalletAddress as `0x${string}`,
117
+ abi: walletContracts.mainModule.abi,
118
+ client: rpcProvider,
119
+ });
93
120
  const space: bigint = coerceNonceSpace(nonceSpace); // Default nonce space is 0
94
- const result = await contract.readNonce(space);
121
+ const result = await contract.read.readNonce([space]);
95
122
  if (typeof result === 'bigint') {
96
123
  return encodeNonce(space, result);
97
124
  }
98
- throw new Error('Unexpected result from contract.nonce() call.');
125
+ throw new Error('Unexpected result from contract.readNonce() call.');
99
126
  } catch (error) {
100
- if (isError(error, 'BAD_DATA')) {
101
- // The most likely reason for a BAD_DATA error is that the smart contract wallet
127
+ // Check if the error is due to contract not being deployed (similar to ethers BAD_DATA)
128
+ // In viem, this typically manifests as a ContractFunctionExecutionError with empty return data
129
+ if (error instanceof Error && (
130
+ error.message.includes('returned no data')
131
+ || error.message.includes('execution reverted')
132
+ || error.message.includes('ContractFunctionExecutionError')
133
+ )) {
134
+ // The most likely reason for this error is that the smart contract wallet
102
135
  // has not been deployed yet, so we should default to a nonce of 0.
103
136
  return BigInt(0);
104
137
  }
@@ -107,19 +140,19 @@ export const getNonce = async (
107
140
  }
108
141
  };
109
142
 
110
- export const encodeMessageSubDigest = (chainId: bigint, walletAddress: string, digest: string): string => (
111
- solidityPacked(
143
+ export const encodeMessageSubDigest = (chainId: bigint, walletAddress: string, digest: string): Hex => (
144
+ encodePacked(
112
145
  ['string', 'uint256', 'address', 'bytes32'],
113
- [ETH_SIGN_PREFIX, chainId, walletAddress, digest],
146
+ [ETH_SIGN_PREFIX, chainId, walletAddress as `0x${string}`, digest as `0x${string}`],
114
147
  )
115
148
  );
116
149
 
117
150
  export const signMetaTransactions = async (
118
151
  metaTransactions: MetaTransaction[],
119
- nonce: BigNumberish,
152
+ nonce: bigint,
120
153
  chainId: bigint,
121
154
  walletAddress: string,
122
- signer: Signer,
155
+ signer: WalletSigner,
123
156
  ): Promise<string> => {
124
157
  const normalisedMetaTransactions = getNormalisedTransactions(metaTransactions);
125
158
 
@@ -130,7 +163,7 @@ export const signMetaTransactions = async (
130
163
  const hash = keccak256(completePayload);
131
164
 
132
165
  // Sign the digest
133
- const hashArray = getBytes(hash);
166
+ const hashArray = toBytes(hash);
134
167
  const ethsigNoType = await signer.signMessage(hashArray);
135
168
  const signedDigest = `${ethsigNoType}${ETH_SIGN_FLAG}`;
136
169
 
@@ -148,13 +181,22 @@ export const signMetaTransactions = async (
148
181
  ],
149
182
  });
150
183
 
151
- // Encode the transaction;
152
- const walletInterface = new Interface(walletContracts.mainModule.abi);
153
- return walletInterface.encodeFunctionData(walletInterface.getFunction('execute') ?? '', [
154
- normalisedMetaTransactions,
155
- nonce,
156
- encodedSignature,
157
- ]);
184
+ // Encode the transaction using viem's encodeFunctionData
185
+ // Convert normalised transactions to tuple format for encoding
186
+ const txsForEncoding = normalisedMetaTransactions.map((t) => ({
187
+ delegateCall: t.delegateCall,
188
+ revertOnError: t.revertOnError,
189
+ gasLimit: t.gasLimit,
190
+ target: t.target as `0x${string}`,
191
+ value: t.value,
192
+ data: t.data as `0x${string}`,
193
+ }));
194
+
195
+ return encodeFunctionData({
196
+ abi: walletContracts.mainModule.abi,
197
+ functionName: 'execute',
198
+ args: [txsForEncoding, nonce, encodedSignature as `0x${string}`],
199
+ });
158
200
  };
159
201
 
160
202
  const decodeRelayerSignature = (relayerSignature: string) => {
@@ -205,21 +247,28 @@ export const signAndPackTypedData = async (
205
247
  relayerSignature: string,
206
248
  chainId: bigint,
207
249
  walletAddress: string,
208
- signer: Signer,
250
+ signer: WalletSigner,
209
251
  ): Promise<string> => {
210
- // Ethers auto-generates the EIP712Domain type in the TypedDataEncoder, and so it needs to be removed
252
+ // viem's hashTypedData handles EIP712Domain automatically
211
253
  const types = { ...typedData.types };
212
- // @ts-ignore
213
- delete types.EIP712Domain;
254
+ // Remove EIP712Domain from types as viem handles it
255
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
256
+ const { EIP712Domain, ...typesWithoutDomain } = types;
257
+
258
+ // Hash the EIP712 payload
259
+ const typedDataHash = hashTypedData({
260
+ domain: typedData.domain as Parameters<typeof hashTypedData>[0]['domain'],
261
+ types: typesWithoutDomain,
262
+ primaryType: typedData.primaryType,
263
+ message: typedData.message,
264
+ });
214
265
 
215
- // Hash the EIP712 payload and generate the complete payload
216
- const typedDataHash = TypedDataEncoder.hash(typedData.domain, types, typedData.message);
217
266
  const messageSubDigest = encodeMessageSubDigest(chainId, walletAddress, typedDataHash);
218
267
  const hash = keccak256(messageSubDigest);
219
268
 
220
269
  // Sign the sub digest
221
270
  // https://github.com/immutable/wallet-contracts/blob/7824b5f24b2e0eb2dc465ecb5cd71f3984556b73/src/contracts/modules/commons/ModuleAuth.sol#L155
222
- const hashArray = getBytes(hash);
271
+ const hashArray = toBytes(hash);
223
272
  const eoaSignature = await signer.signMessage(hashArray);
224
273
  const eoaAddress = await signer.getAddress();
225
274
 
@@ -229,16 +278,16 @@ export const signAndPackTypedData = async (
229
278
  export const signERC191Message = async (
230
279
  chainId: bigint,
231
280
  payload: string,
232
- signer: Signer,
281
+ signer: WalletSigner,
233
282
  walletAddress: string,
234
283
  ): Promise<string> => {
235
- // Generate digest
284
+ // Generate digest using viem's hashMessage
236
285
  const digest = hashMessage(payload);
237
286
 
238
287
  // Generate subDigest
239
288
  const subDigest = encodeMessageSubDigest(chainId, walletAddress, digest);
240
289
  const subDigestHash = keccak256(subDigest);
241
- const subDigestHashArray = getBytes(subDigestHash);
290
+ const subDigestHashArray = toBytes(subDigestHash);
242
291
 
243
292
  return signer.signMessage(subDigestHashArray);
244
293
  };
@@ -3,8 +3,11 @@ import {
3
3
  Flow, identify, trackError, trackFlow,
4
4
  } from '@imtbl/metrics';
5
5
  import {
6
- JsonRpcProvider, Signer, toBeHex,
7
- } from 'ethers';
6
+ createPublicClient,
7
+ http,
8
+ toHex,
9
+ type PublicClient,
10
+ } from 'viem';
8
11
  import {
9
12
  Provider,
10
13
  ProviderEvent,
@@ -14,7 +17,7 @@ import {
14
17
  import { Auth, TypedEventEmitter } from '@imtbl/auth';
15
18
  import { WalletConfiguration } from '../config';
16
19
  import {
17
- PassportEventMap, AuthEvents, WalletEvents, User, UserZkEvm,
20
+ PassportEventMap, AuthEvents, WalletEvents, User, UserZkEvm, WalletSigner,
18
21
  } from '../types';
19
22
  import { RelayerClient } from './relayerClient';
20
23
  import { JsonRpcError, ProviderErrorCode, RpcErrorCode } from './JsonRpcError';
@@ -34,7 +37,7 @@ export type ZkEvmProviderInput = {
34
37
  multiRollupApiClients: MultiRollupApiClients;
35
38
  passportEventEmitter: TypedEventEmitter<PassportEventMap>;
36
39
  guardianClient: GuardianClient;
37
- ethSigner: Signer;
40
+ ethSigner: WalletSigner;
38
41
  user: User | null;
39
42
  sessionActivityApiUrl: string | null;
40
43
  };
@@ -60,13 +63,13 @@ export class ZkEvmProvider implements Provider {
60
63
 
61
64
  readonly #guardianClient: GuardianClient;
62
65
 
63
- readonly #rpcProvider: JsonRpcProvider; // Used for read
66
+ readonly #rpcProvider: PublicClient; // Used for read
64
67
 
65
68
  readonly #multiRollupApiClients: MultiRollupApiClients;
66
69
 
67
70
  readonly #relayerClient: RelayerClient;
68
71
 
69
- readonly #ethSigner: Signer;
72
+ readonly #ethSigner: WalletSigner;
70
73
 
71
74
  public readonly isPassport: boolean = true;
72
75
 
@@ -87,9 +90,9 @@ export class ZkEvmProvider implements Provider {
87
90
  this.#sessionActivityApiUrl = sessionActivityApiUrl;
88
91
  this.#ethSigner = ethSigner;
89
92
 
90
- // Create JsonRpcProvider for reading from the chain
91
- this.#rpcProvider = new JsonRpcProvider(this.#config.zkEvmRpcUrl, undefined, {
92
- staticNetwork: true,
93
+ // Create PublicClient for reading from the chain using viem
94
+ this.#rpcProvider = createPublicClient({
95
+ transport: http(this.#config.zkEvmRpcUrl),
93
96
  });
94
97
 
95
98
  // Create RelayerClient for transaction submission
@@ -347,40 +350,34 @@ export class ZkEvmProvider implements Provider {
347
350
  }
348
351
  }
349
352
  case 'eth_chainId': {
350
- // Call detect network to fetch the chainId so to take advantage of
351
- // the caching layer provided by StaticJsonRpcProvider.
352
- // In case Passport is changed from StaticJsonRpcProvider to a
353
- // JsonRpcProvider, this function will still work as expected given
354
- // that detectNetwork call _uncachedDetectNetwork which will force
355
- // the provider to re-fetch the chainId from remote.
356
- const { chainId } = await this.#rpcProvider.getNetwork();
357
- return toBeHex(chainId);
353
+ // Get chain ID using viem's PublicClient
354
+ const chainId = await this.#rpcProvider.getChainId();
355
+ return toHex(chainId);
358
356
  }
359
- // Pass through methods
357
+ // Pass through methods - use viem's request method for raw RPC calls
360
358
  case 'eth_getBalance':
361
359
  case 'eth_getCode':
362
360
  case 'eth_getTransactionCount': {
363
361
  const [address, blockNumber] = request.params || [];
364
- return this.#rpcProvider.send(request.method, [
365
- address,
366
- blockNumber || 'latest',
367
- ]);
362
+ return this.#rpcProvider.request({
363
+ method: request.method as any,
364
+ params: [address, blockNumber || 'latest'],
365
+ });
368
366
  }
369
367
  case 'eth_getStorageAt': {
370
368
  const [address, storageSlot, blockNumber] = request.params || [];
371
- return this.#rpcProvider.send(request.method, [
372
- address,
373
- storageSlot,
374
- blockNumber || 'latest',
375
- ]);
369
+ return this.#rpcProvider.request({
370
+ method: 'eth_getStorageAt',
371
+ params: [address, storageSlot, blockNumber || 'latest'],
372
+ });
376
373
  }
377
374
  case 'eth_call':
378
375
  case 'eth_estimateGas': {
379
376
  const [transaction, blockNumber] = request.params || [];
380
- return this.#rpcProvider.send(request.method, [
381
- transaction,
382
- blockNumber || 'latest',
383
- ]);
377
+ return this.#rpcProvider.request({
378
+ method: request.method as any,
379
+ params: [transaction, blockNumber || 'latest'],
380
+ });
384
381
  }
385
382
  case 'eth_gasPrice':
386
383
  case 'eth_blockNumber':
@@ -388,7 +385,10 @@ export class ZkEvmProvider implements Provider {
388
385
  case 'eth_getBlockByNumber':
389
386
  case 'eth_getTransactionByHash':
390
387
  case 'eth_getTransactionReceipt': {
391
- return this.#rpcProvider.send(request.method, request.params || []);
388
+ return this.#rpcProvider.request({
389
+ method: request.method as any,
390
+ params: (request.params || []) as any,
391
+ });
392
392
  }
393
393
  case 'im_signEjectionTransaction': {
394
394
  const zkEvmAddress = await this.#getZkEvmAddress();