@imtbl/wallet 2.10.7-alpha.2
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.
- package/.eslintrc.cjs +18 -0
- package/LICENSE.md +176 -0
- package/dist/browser/index.mjs +21 -0
- package/dist/node/index.js +71 -0
- package/dist/node/index.mjs +22 -0
- package/dist/types/config.d.ts +13 -0
- package/dist/types/errors.d.ts +14 -0
- package/dist/types/guardian/index.d.ts +57 -0
- package/dist/types/index.d.ts +12 -0
- package/dist/types/magic/index.d.ts +1 -0
- package/dist/types/magic/magicTEESigner.d.ts +24 -0
- package/dist/types/network/chains.d.ts +32 -0
- package/dist/types/network/constants.d.ts +3 -0
- package/dist/types/network/retry.d.ts +8 -0
- package/dist/types/provider/eip6963.d.ts +3 -0
- package/dist/types/types.d.ts +163 -0
- package/dist/types/utils/metrics.d.ts +3 -0
- package/dist/types/utils/string.d.ts +1 -0
- package/dist/types/utils/typedEventEmitter.d.ts +6 -0
- package/dist/types/zkEvm/JsonRpcError.d.ts +25 -0
- package/dist/types/zkEvm/index.d.ts +2 -0
- package/dist/types/zkEvm/personalSign.d.ts +15 -0
- package/dist/types/zkEvm/provider/eip6963.d.ts +3 -0
- package/dist/types/zkEvm/relayerClient.d.ts +60 -0
- package/dist/types/zkEvm/sendDeployTransactionAndPersonalSign.d.ts +6 -0
- package/dist/types/zkEvm/sendTransaction.d.ts +6 -0
- package/dist/types/zkEvm/sessionActivity/errorBoundary.d.ts +1 -0
- package/dist/types/zkEvm/sessionActivity/request.d.ts +15 -0
- package/dist/types/zkEvm/sessionActivity/sessionActivity.d.ts +2 -0
- package/dist/types/zkEvm/signEjectionTransaction.d.ts +6 -0
- package/dist/types/zkEvm/signTypedDataV4.d.ts +14 -0
- package/dist/types/zkEvm/transactionHelpers.d.ts +31 -0
- package/dist/types/zkEvm/types.d.ts +120 -0
- package/dist/types/zkEvm/user/index.d.ts +1 -0
- package/dist/types/zkEvm/user/registerZkEvmUser.d.ts +13 -0
- package/dist/types/zkEvm/walletHelpers.d.ts +33 -0
- package/dist/types/zkEvm/zkEvmProvider.d.ts +25 -0
- package/package.json +55 -0
- package/src/config.ts +51 -0
- package/src/errors.ts +33 -0
- package/src/guardian/index.ts +358 -0
- package/src/index.ts +27 -0
- package/src/magic/index.ts +1 -0
- package/src/magic/magicTEESigner.ts +214 -0
- package/src/network/chains.ts +33 -0
- package/src/network/constants.ts +28 -0
- package/src/network/retry.ts +37 -0
- package/src/provider/eip6963.ts +25 -0
- package/src/types.ts +192 -0
- package/src/utils/metrics.ts +57 -0
- package/src/utils/string.ts +12 -0
- package/src/utils/typedEventEmitter.ts +26 -0
- package/src/zkEvm/JsonRpcError.ts +33 -0
- package/src/zkEvm/index.ts +2 -0
- package/src/zkEvm/personalSign.ts +62 -0
- package/src/zkEvm/provider/eip6963.ts +25 -0
- package/src/zkEvm/relayerClient.ts +216 -0
- package/src/zkEvm/sendDeployTransactionAndPersonalSign.ts +44 -0
- package/src/zkEvm/sendTransaction.ts +34 -0
- package/src/zkEvm/sessionActivity/errorBoundary.ts +33 -0
- package/src/zkEvm/sessionActivity/request.ts +62 -0
- package/src/zkEvm/sessionActivity/sessionActivity.ts +140 -0
- package/src/zkEvm/signEjectionTransaction.ts +33 -0
- package/src/zkEvm/signTypedDataV4.ts +103 -0
- package/src/zkEvm/transactionHelpers.ts +295 -0
- package/src/zkEvm/types.ts +136 -0
- package/src/zkEvm/user/index.ts +1 -0
- package/src/zkEvm/user/registerZkEvmUser.ts +75 -0
- package/src/zkEvm/walletHelpers.ts +243 -0
- package/src/zkEvm/zkEvmProvider.ts +453 -0
- package/tsconfig.json +15 -0
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { walletContracts } from '@0xsequence/abi';
|
|
2
|
+
import { v1 as sequenceCoreV1 } from '@0xsequence/core';
|
|
3
|
+
import {
|
|
4
|
+
BigNumberish, Contract, getBytes, hashMessage,
|
|
5
|
+
Interface, keccak256, Signer, solidityPacked, ZeroAddress,
|
|
6
|
+
TypedDataEncoder, JsonRpcProvider, AbiCoder,
|
|
7
|
+
isError,
|
|
8
|
+
} from 'ethers';
|
|
9
|
+
import { MetaTransaction, MetaTransactionNormalised, TypedDataPayload } from './types';
|
|
10
|
+
|
|
11
|
+
const SIGNATURE_WEIGHT = 1; // Weight of a single signature in the multi-sig
|
|
12
|
+
const TRANSACTION_SIGNATURE_THRESHOLD = 1; // Total required weight in the multi-sig for a transaction
|
|
13
|
+
const PACKED_SIGNATURE_THRESHOLD = 2; // Total required weight in the multi-sig for data signing
|
|
14
|
+
|
|
15
|
+
const ETH_SIGN_FLAG = '02';
|
|
16
|
+
const ETH_SIGN_PREFIX = '\x19\x01';
|
|
17
|
+
const META_TRANSACTIONS_TYPE = `tuple(
|
|
18
|
+
bool delegateCall,
|
|
19
|
+
bool revertOnError,
|
|
20
|
+
uint256 gasLimit,
|
|
21
|
+
address target,
|
|
22
|
+
uint256 value,
|
|
23
|
+
bytes data
|
|
24
|
+
)[]`;
|
|
25
|
+
|
|
26
|
+
export const getNormalisedTransactions = (txs: MetaTransaction[]): MetaTransactionNormalised[] => txs.map((t) => ({
|
|
27
|
+
delegateCall: t.delegateCall === true,
|
|
28
|
+
revertOnError: t.revertOnError === true,
|
|
29
|
+
gasLimit: t.gasLimit ?? BigInt(0),
|
|
30
|
+
target: t.to ?? ZeroAddress,
|
|
31
|
+
value: t.value ?? BigInt(0),
|
|
32
|
+
data: t.data ?? '0x',
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
export const digestOfTransactionsAndNonce = (
|
|
36
|
+
nonce: BigNumberish,
|
|
37
|
+
normalisedTransactions: MetaTransactionNormalised[],
|
|
38
|
+
): string => {
|
|
39
|
+
const packMetaTransactionsNonceData = AbiCoder.defaultAbiCoder().encode(
|
|
40
|
+
['uint256', META_TRANSACTIONS_TYPE],
|
|
41
|
+
[nonce, normalisedTransactions],
|
|
42
|
+
);
|
|
43
|
+
return keccak256(packMetaTransactionsNonceData);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export const encodedTransactions = (
|
|
47
|
+
normalisedTransactions: MetaTransactionNormalised[],
|
|
48
|
+
): string => AbiCoder.defaultAbiCoder().encode(
|
|
49
|
+
[META_TRANSACTIONS_TYPE],
|
|
50
|
+
[normalisedTransactions],
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* This helper function is used to coerce the type <BigNumber | undefined> to BigNumber for the
|
|
55
|
+
* getNonce function above.
|
|
56
|
+
* @param {BigNumber} nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space.
|
|
57
|
+
* @returns {BigNumber} The passed in nonceSpace or instead initialises the nonce to 0.
|
|
58
|
+
*/
|
|
59
|
+
export const coerceNonceSpace = (nonceSpace?: bigint): bigint => nonceSpace || 0n;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* This helper function is used to encode the nonce into a 256 bit value where the space is encoded into
|
|
63
|
+
* the first 160 bits, and the nonce the remaining 96 bits.
|
|
64
|
+
* @param {BigNumber} nonceSpace - An unsigned 256 bit value that can be used to encode a nonce into a distinct space.
|
|
65
|
+
* @param nonce {BigNumber} nonce - Sequential number starting at 0, and incrementing in single steps e.g. 0,1,2,...
|
|
66
|
+
* @returns {BigNumber} The encoded value where the space is left shifted 96 bits, and the nonce is in the first 96 bits.
|
|
67
|
+
*/
|
|
68
|
+
export const encodeNonce = (nonceSpace: bigint, nonce: bigint): bigint => {
|
|
69
|
+
const shiftedSpace = BigInt(nonceSpace) * (2n ** 96n);
|
|
70
|
+
return BigInt(nonce) + (shiftedSpace);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* When we retrieve a nonce for a smart contract wallet we can retrieve the nonce in a given 256 bit space.
|
|
75
|
+
* Nonces in each 256 bit space need to be sequential per wallet address. Nonces across 256 bit spaces per
|
|
76
|
+
* wallet address do not. This function overload can be used to invoke transactions in parallel per smart
|
|
77
|
+
* contract wallet if required.
|
|
78
|
+
*/
|
|
79
|
+
export const getNonce = async (
|
|
80
|
+
rpcProvider: JsonRpcProvider,
|
|
81
|
+
smartContractWalletAddress: string,
|
|
82
|
+
nonceSpace?: bigint,
|
|
83
|
+
): Promise<bigint> => {
|
|
84
|
+
try {
|
|
85
|
+
const contract = new Contract(
|
|
86
|
+
smartContractWalletAddress,
|
|
87
|
+
walletContracts.mainModule.abi,
|
|
88
|
+
rpcProvider,
|
|
89
|
+
);
|
|
90
|
+
const space: bigint = coerceNonceSpace(nonceSpace); // Default nonce space is 0
|
|
91
|
+
const result = await contract.readNonce(space);
|
|
92
|
+
if (typeof result === 'bigint') {
|
|
93
|
+
return encodeNonce(space, result);
|
|
94
|
+
}
|
|
95
|
+
throw new Error('Unexpected result from contract.nonce() call.');
|
|
96
|
+
} catch (error) {
|
|
97
|
+
if (isError(error, 'BAD_DATA')) {
|
|
98
|
+
// The most likely reason for a BAD_DATA error is that the smart contract wallet
|
|
99
|
+
// has not been deployed yet, so we should default to a nonce of 0.
|
|
100
|
+
return BigInt(0);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
export const encodeMessageSubDigest = (chainId: bigint, walletAddress: string, digest: string): string => (
|
|
108
|
+
solidityPacked(
|
|
109
|
+
['string', 'uint256', 'address', 'bytes32'],
|
|
110
|
+
[ETH_SIGN_PREFIX, chainId, walletAddress, digest],
|
|
111
|
+
)
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
export const signMetaTransactions = async (
|
|
115
|
+
metaTransactions: MetaTransaction[],
|
|
116
|
+
nonce: BigNumberish,
|
|
117
|
+
chainId: bigint,
|
|
118
|
+
walletAddress: string,
|
|
119
|
+
signer: Signer,
|
|
120
|
+
): Promise<string> => {
|
|
121
|
+
const normalisedMetaTransactions = getNormalisedTransactions(metaTransactions);
|
|
122
|
+
|
|
123
|
+
// Get the hash
|
|
124
|
+
const digest = digestOfTransactionsAndNonce(nonce, normalisedMetaTransactions);
|
|
125
|
+
const completePayload = encodeMessageSubDigest(chainId, walletAddress, digest);
|
|
126
|
+
|
|
127
|
+
const hash = keccak256(completePayload);
|
|
128
|
+
|
|
129
|
+
// Sign the digest
|
|
130
|
+
const hashArray = getBytes(hash);
|
|
131
|
+
const ethsigNoType = await signer.signMessage(hashArray);
|
|
132
|
+
const signedDigest = `${ethsigNoType}${ETH_SIGN_FLAG}`;
|
|
133
|
+
|
|
134
|
+
// Add metadata
|
|
135
|
+
const encodedSignature = sequenceCoreV1.signature.encodeSignature({
|
|
136
|
+
version: 1,
|
|
137
|
+
threshold: TRANSACTION_SIGNATURE_THRESHOLD,
|
|
138
|
+
signers: [
|
|
139
|
+
{
|
|
140
|
+
isDynamic: false,
|
|
141
|
+
unrecovered: true,
|
|
142
|
+
weight: SIGNATURE_WEIGHT,
|
|
143
|
+
signature: signedDigest,
|
|
144
|
+
},
|
|
145
|
+
],
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
// Encode the transaction;
|
|
149
|
+
const walletInterface = new Interface(walletContracts.mainModule.abi);
|
|
150
|
+
return walletInterface.encodeFunctionData(walletInterface.getFunction('execute') ?? '', [
|
|
151
|
+
normalisedMetaTransactions,
|
|
152
|
+
nonce,
|
|
153
|
+
encodedSignature,
|
|
154
|
+
]);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const decodeRelayerSignature = (relayerSignature: string) => {
|
|
158
|
+
const signatureWithThreshold = `0x0000${relayerSignature}`;
|
|
159
|
+
return sequenceCoreV1.signature.decodeSignature(signatureWithThreshold);
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export const packSignatures = (
|
|
163
|
+
EOASignature: string,
|
|
164
|
+
EOAAddress: string,
|
|
165
|
+
relayerSignature: string,
|
|
166
|
+
): string => {
|
|
167
|
+
const signedDigest = `${EOASignature}${ETH_SIGN_FLAG}`;
|
|
168
|
+
|
|
169
|
+
// Combine the relayer and user signatures; sort by address to match the imageHash order
|
|
170
|
+
const { signers: relayerSigners } = decodeRelayerSignature(relayerSignature);
|
|
171
|
+
const combinedSigners = [
|
|
172
|
+
...relayerSigners,
|
|
173
|
+
{
|
|
174
|
+
isDynamic: false,
|
|
175
|
+
unrecovered: true,
|
|
176
|
+
weight: SIGNATURE_WEIGHT,
|
|
177
|
+
signature: signedDigest,
|
|
178
|
+
address: EOAAddress,
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
const sortedSigners = combinedSigners.sort((a, b) => {
|
|
182
|
+
const bigA = BigInt(a.address ?? 0);
|
|
183
|
+
const bigB = BigInt(b.address ?? 0);
|
|
184
|
+
|
|
185
|
+
if (bigA <= bigB) {
|
|
186
|
+
return -1;
|
|
187
|
+
} if (bigA === bigB) {
|
|
188
|
+
return 0;
|
|
189
|
+
}
|
|
190
|
+
return 1;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
return sequenceCoreV1.signature.encodeSignature({
|
|
194
|
+
version: 1,
|
|
195
|
+
threshold: PACKED_SIGNATURE_THRESHOLD,
|
|
196
|
+
signers: sortedSigners,
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
export const signAndPackTypedData = async (
|
|
201
|
+
typedData: TypedDataPayload,
|
|
202
|
+
relayerSignature: string,
|
|
203
|
+
chainId: bigint,
|
|
204
|
+
walletAddress: string,
|
|
205
|
+
signer: Signer,
|
|
206
|
+
): Promise<string> => {
|
|
207
|
+
// Ethers auto-generates the EIP712Domain type in the TypedDataEncoder, and so it needs to be removed
|
|
208
|
+
const types = { ...typedData.types };
|
|
209
|
+
// @ts-ignore
|
|
210
|
+
delete types.EIP712Domain;
|
|
211
|
+
|
|
212
|
+
// Hash the EIP712 payload and generate the complete payload
|
|
213
|
+
const typedDataHash = TypedDataEncoder.hash(typedData.domain, types, typedData.message);
|
|
214
|
+
const messageSubDigest = encodeMessageSubDigest(chainId, walletAddress, typedDataHash);
|
|
215
|
+
const hash = keccak256(messageSubDigest);
|
|
216
|
+
|
|
217
|
+
// Sign the sub digest
|
|
218
|
+
// https://github.com/immutable/wallet-contracts/blob/7824b5f24b2e0eb2dc465ecb5cd71f3984556b73/src/contracts/modules/commons/ModuleAuth.sol#L155
|
|
219
|
+
const hashArray = getBytes(hash);
|
|
220
|
+
const eoaSignature = await signer.signMessage(hashArray);
|
|
221
|
+
const eoaAddress = await signer.getAddress();
|
|
222
|
+
|
|
223
|
+
return packSignatures(eoaSignature, eoaAddress, relayerSignature);
|
|
224
|
+
};
|
|
225
|
+
|
|
226
|
+
export const signERC191Message = async (
|
|
227
|
+
chainId: bigint,
|
|
228
|
+
payload: string,
|
|
229
|
+
signer: Signer,
|
|
230
|
+
walletAddress: string,
|
|
231
|
+
): Promise<string> => {
|
|
232
|
+
// Generate digest
|
|
233
|
+
const digest = hashMessage(payload);
|
|
234
|
+
|
|
235
|
+
// Generate subDigest
|
|
236
|
+
const subDigest = encodeMessageSubDigest(chainId, walletAddress, digest);
|
|
237
|
+
const subDigestHash = keccak256(subDigest);
|
|
238
|
+
const subDigestHashArray = getBytes(subDigestHash);
|
|
239
|
+
|
|
240
|
+
return signer.signMessage(subDigestHashArray);
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
export const getEip155ChainId = (chainId: number) => `eip155:${chainId}`;
|
|
@@ -0,0 +1,453 @@
|
|
|
1
|
+
import { MultiRollupApiClients } from '@imtbl/generated-clients';
|
|
2
|
+
import {
|
|
3
|
+
Flow, identify, trackError, trackFlow,
|
|
4
|
+
} from '@imtbl/metrics';
|
|
5
|
+
import {
|
|
6
|
+
JsonRpcProvider, Signer, toBeHex,
|
|
7
|
+
} from 'ethers';
|
|
8
|
+
import {
|
|
9
|
+
Provider,
|
|
10
|
+
ProviderEvent,
|
|
11
|
+
ProviderEventMap,
|
|
12
|
+
RequestArguments,
|
|
13
|
+
} from './types';
|
|
14
|
+
import { AuthManager } from '@imtbl/auth';
|
|
15
|
+
import TypedEventEmitter from '../utils/typedEventEmitter';
|
|
16
|
+
import { WalletConfiguration } from '../config';
|
|
17
|
+
import {
|
|
18
|
+
PassportEventMap, PassportEvents, User, UserZkEvm,
|
|
19
|
+
} from '../types';
|
|
20
|
+
import { RelayerClient } from './relayerClient';
|
|
21
|
+
import { JsonRpcError, ProviderErrorCode, RpcErrorCode } from './JsonRpcError';
|
|
22
|
+
import { registerZkEvmUser } from './user';
|
|
23
|
+
import { sendTransaction } from './sendTransaction';
|
|
24
|
+
import GuardianClient from '../guardian';
|
|
25
|
+
import { signTypedDataV4 } from './signTypedDataV4';
|
|
26
|
+
import { personalSign } from './personalSign';
|
|
27
|
+
import { trackSessionActivity } from './sessionActivity/sessionActivity';
|
|
28
|
+
import { getNonce } from './walletHelpers';
|
|
29
|
+
import { sendDeployTransactionAndPersonalSign } from './sendDeployTransactionAndPersonalSign';
|
|
30
|
+
import { signEjectionTransaction } from './signEjectionTransaction';
|
|
31
|
+
|
|
32
|
+
export type ZkEvmProviderInput = {
|
|
33
|
+
authManager: AuthManager;
|
|
34
|
+
config: WalletConfiguration;
|
|
35
|
+
multiRollupApiClients: MultiRollupApiClients;
|
|
36
|
+
passportEventEmitter: TypedEventEmitter<PassportEventMap>;
|
|
37
|
+
guardianClient: GuardianClient;
|
|
38
|
+
ethSigner: Signer;
|
|
39
|
+
user: User | null;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const isZkEvmUser = (user: User): user is UserZkEvm => 'zkEvm' in user;
|
|
43
|
+
|
|
44
|
+
export class ZkEvmProvider implements Provider {
|
|
45
|
+
readonly #authManager: AuthManager;
|
|
46
|
+
|
|
47
|
+
readonly #config: WalletConfiguration;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* intended to emit EIP-1193 events
|
|
51
|
+
*/
|
|
52
|
+
readonly #providerEventEmitter: TypedEventEmitter<ProviderEventMap>;
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* intended to emit internal Passport events
|
|
56
|
+
*/
|
|
57
|
+
readonly #passportEventEmitter: TypedEventEmitter<ProviderEventMap>;
|
|
58
|
+
|
|
59
|
+
readonly #guardianClient: GuardianClient;
|
|
60
|
+
|
|
61
|
+
readonly #rpcProvider: JsonRpcProvider; // Used for read
|
|
62
|
+
|
|
63
|
+
readonly #multiRollupApiClients: MultiRollupApiClients;
|
|
64
|
+
|
|
65
|
+
readonly #relayerClient: RelayerClient;
|
|
66
|
+
|
|
67
|
+
readonly #ethSigner: Signer;
|
|
68
|
+
|
|
69
|
+
public readonly isPassport: boolean = true;
|
|
70
|
+
|
|
71
|
+
constructor({
|
|
72
|
+
authManager,
|
|
73
|
+
config,
|
|
74
|
+
multiRollupApiClients,
|
|
75
|
+
passportEventEmitter,
|
|
76
|
+
guardianClient,
|
|
77
|
+
ethSigner,
|
|
78
|
+
user,
|
|
79
|
+
}: ZkEvmProviderInput) {
|
|
80
|
+
this.#authManager = authManager;
|
|
81
|
+
this.#config = config;
|
|
82
|
+
this.#guardianClient = guardianClient;
|
|
83
|
+
this.#passportEventEmitter = passportEventEmitter;
|
|
84
|
+
this.#ethSigner = ethSigner;
|
|
85
|
+
|
|
86
|
+
this.#rpcProvider = new JsonRpcProvider(this.#config.zkEvmRpcUrl, undefined, {
|
|
87
|
+
staticNetwork: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
this.#relayerClient = new RelayerClient({
|
|
91
|
+
config: this.#config,
|
|
92
|
+
rpcProvider: this.#rpcProvider,
|
|
93
|
+
authManager: this.#authManager,
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
this.#multiRollupApiClients = multiRollupApiClients;
|
|
97
|
+
this.#providerEventEmitter = new TypedEventEmitter<ProviderEventMap>();
|
|
98
|
+
|
|
99
|
+
if (user && isZkEvmUser(user)) {
|
|
100
|
+
this.#callSessionActivity(user.zkEvm.ethAddress);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
passportEventEmitter.on(PassportEvents.LOGGED_IN, (loggedInUser: User) => {
|
|
104
|
+
if (isZkEvmUser(loggedInUser)) {
|
|
105
|
+
this.#callSessionActivity(loggedInUser.zkEvm.ethAddress);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
passportEventEmitter.on(PassportEvents.LOGGED_OUT, this.#handleLogout);
|
|
109
|
+
passportEventEmitter.on(
|
|
110
|
+
PassportEvents.ACCOUNTS_REQUESTED,
|
|
111
|
+
trackSessionActivity,
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
#handleLogout = () => {
|
|
116
|
+
this.#providerEventEmitter.emit(ProviderEvent.ACCOUNTS_CHANGED, []);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
async #callSessionActivity(zkEvmAddress: string, clientId?: string) {
|
|
120
|
+
// SessionActivity requests are processed in nonce space 1, where as all
|
|
121
|
+
// other sendTransaction requests are processed in nonce space 0. This means
|
|
122
|
+
// we can submit a session activity request per SCW in parallel without a SCW
|
|
123
|
+
// INVALID_NONCE error.
|
|
124
|
+
const nonceSpace: bigint = BigInt(1);
|
|
125
|
+
const sendTransactionClosure = async (params: Array<any>, flow: Flow) => await sendTransaction({
|
|
126
|
+
params,
|
|
127
|
+
ethSigner: this.#ethSigner,
|
|
128
|
+
guardianClient: this.#guardianClient,
|
|
129
|
+
rpcProvider: this.#rpcProvider,
|
|
130
|
+
relayerClient: this.#relayerClient,
|
|
131
|
+
zkEvmAddress,
|
|
132
|
+
flow,
|
|
133
|
+
nonceSpace,
|
|
134
|
+
isBackgroundTransaction: true,
|
|
135
|
+
});
|
|
136
|
+
this.#passportEventEmitter.emit(PassportEvents.ACCOUNTS_REQUESTED, {
|
|
137
|
+
environment: this.#config.environment,
|
|
138
|
+
sendTransaction: sendTransactionClosure,
|
|
139
|
+
walletAddress: zkEvmAddress,
|
|
140
|
+
passportClient: clientId || 'wallet',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Used to get the registered zkEvm address from the User session
|
|
145
|
+
async #getZkEvmAddress() {
|
|
146
|
+
try {
|
|
147
|
+
const user = await this.#authManager.getUser();
|
|
148
|
+
if (user && isZkEvmUser(user)) {
|
|
149
|
+
return user.zkEvm.ethAddress;
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
} catch {
|
|
153
|
+
return undefined;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async #performRequest(request: RequestArguments): Promise<any> {
|
|
158
|
+
// This is required for sending session activity events
|
|
159
|
+
|
|
160
|
+
switch (request.method) {
|
|
161
|
+
case 'eth_requestAccounts': {
|
|
162
|
+
const zkEvmAddress = await this.#getZkEvmAddress();
|
|
163
|
+
if (zkEvmAddress) return [zkEvmAddress];
|
|
164
|
+
|
|
165
|
+
const flow = trackFlow('passport', 'ethRequestAccounts');
|
|
166
|
+
|
|
167
|
+
try {
|
|
168
|
+
const user = await this.#authManager.getUserOrLogin();
|
|
169
|
+
flow.addEvent('endGetUserOrLogin');
|
|
170
|
+
|
|
171
|
+
let userZkEvmEthAddress: string | undefined;
|
|
172
|
+
|
|
173
|
+
if (!isZkEvmUser(user)) {
|
|
174
|
+
flow.addEvent('startUserRegistration');
|
|
175
|
+
|
|
176
|
+
userZkEvmEthAddress = await registerZkEvmUser({
|
|
177
|
+
ethSigner: this.#ethSigner,
|
|
178
|
+
authManager: this.#authManager,
|
|
179
|
+
multiRollupApiClients: this.#multiRollupApiClients,
|
|
180
|
+
accessToken: user.accessToken,
|
|
181
|
+
rpcProvider: this.#rpcProvider,
|
|
182
|
+
flow,
|
|
183
|
+
});
|
|
184
|
+
flow.addEvent('endUserRegistration');
|
|
185
|
+
} else {
|
|
186
|
+
userZkEvmEthAddress = user.zkEvm.ethAddress;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
this.#providerEventEmitter.emit(ProviderEvent.ACCOUNTS_CHANGED, [
|
|
190
|
+
userZkEvmEthAddress,
|
|
191
|
+
]);
|
|
192
|
+
identify({
|
|
193
|
+
passportId: user.profile.sub,
|
|
194
|
+
});
|
|
195
|
+
this.#callSessionActivity(userZkEvmEthAddress);
|
|
196
|
+
return [userZkEvmEthAddress];
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (error instanceof Error) {
|
|
199
|
+
trackError('passport', 'ethRequestAccounts', error, { flowId: flow.details.flowId });
|
|
200
|
+
} else {
|
|
201
|
+
flow.addEvent('errored');
|
|
202
|
+
}
|
|
203
|
+
throw error;
|
|
204
|
+
} finally {
|
|
205
|
+
flow.addEvent('End');
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
case 'eth_sendTransaction': {
|
|
209
|
+
const zkEvmAddress = await this.#getZkEvmAddress();
|
|
210
|
+
if (!zkEvmAddress) {
|
|
211
|
+
throw new JsonRpcError(
|
|
212
|
+
ProviderErrorCode.UNAUTHORIZED,
|
|
213
|
+
'Unauthorised - call eth_requestAccounts first',
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const flow = trackFlow('passport', 'ethSendTransaction');
|
|
218
|
+
|
|
219
|
+
try {
|
|
220
|
+
return await this.#guardianClient.withConfirmationScreen({
|
|
221
|
+
width: 480,
|
|
222
|
+
height: 720,
|
|
223
|
+
})(async () => await sendTransaction({
|
|
224
|
+
params: request.params || [],
|
|
225
|
+
ethSigner: this.#ethSigner,
|
|
226
|
+
guardianClient: this.#guardianClient,
|
|
227
|
+
rpcProvider: this.#rpcProvider,
|
|
228
|
+
relayerClient: this.#relayerClient,
|
|
229
|
+
zkEvmAddress,
|
|
230
|
+
flow,
|
|
231
|
+
}));
|
|
232
|
+
} catch (error) {
|
|
233
|
+
if (error instanceof Error) {
|
|
234
|
+
trackError('passport', 'eth_sendTransaction', error, { flowId: flow.details.flowId });
|
|
235
|
+
} else {
|
|
236
|
+
flow.addEvent('errored');
|
|
237
|
+
}
|
|
238
|
+
throw error;
|
|
239
|
+
} finally {
|
|
240
|
+
flow.addEvent('End');
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
case 'eth_accounts': {
|
|
244
|
+
const zkEvmAddress = await this.#getZkEvmAddress();
|
|
245
|
+
return zkEvmAddress ? [zkEvmAddress] : [];
|
|
246
|
+
}
|
|
247
|
+
case 'personal_sign': {
|
|
248
|
+
const zkEvmAddress = await this.#getZkEvmAddress();
|
|
249
|
+
if (!zkEvmAddress) {
|
|
250
|
+
throw new JsonRpcError(
|
|
251
|
+
ProviderErrorCode.UNAUTHORIZED,
|
|
252
|
+
'Unauthorised - call eth_requestAccounts first',
|
|
253
|
+
);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const flow = trackFlow('passport', 'personalSign');
|
|
257
|
+
|
|
258
|
+
try {
|
|
259
|
+
return await this.#guardianClient.withConfirmationScreen({
|
|
260
|
+
width: 480,
|
|
261
|
+
height: 720,
|
|
262
|
+
})(async () => {
|
|
263
|
+
if (this.#config.forceScwDeployBeforeMessageSignature) {
|
|
264
|
+
// Check if the smart contract wallet has been deployed
|
|
265
|
+
const nonce = await getNonce(this.#rpcProvider, zkEvmAddress);
|
|
266
|
+
if (!(nonce > BigInt(0))) {
|
|
267
|
+
// If the smart contract wallet has not been deployed,
|
|
268
|
+
// submit a transaction before signing the message
|
|
269
|
+
return await sendDeployTransactionAndPersonalSign({
|
|
270
|
+
params: request.params || [],
|
|
271
|
+
zkEvmAddress,
|
|
272
|
+
ethSigner: this.#ethSigner,
|
|
273
|
+
rpcProvider: this.#rpcProvider,
|
|
274
|
+
guardianClient: this.#guardianClient,
|
|
275
|
+
relayerClient: this.#relayerClient,
|
|
276
|
+
flow,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
return await personalSign({
|
|
282
|
+
params: request.params || [],
|
|
283
|
+
zkEvmAddress,
|
|
284
|
+
ethSigner: this.#ethSigner,
|
|
285
|
+
rpcProvider: this.#rpcProvider,
|
|
286
|
+
guardianClient: this.#guardianClient,
|
|
287
|
+
relayerClient: this.#relayerClient,
|
|
288
|
+
flow,
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error instanceof Error) {
|
|
293
|
+
trackError('passport', 'personal_sign', error, { flowId: flow.details.flowId });
|
|
294
|
+
} else {
|
|
295
|
+
flow.addEvent('errored');
|
|
296
|
+
}
|
|
297
|
+
throw error;
|
|
298
|
+
} finally {
|
|
299
|
+
flow.addEvent('End');
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
case 'eth_signTypedData':
|
|
303
|
+
case 'eth_signTypedData_v4': {
|
|
304
|
+
const zkEvmAddress = await this.#getZkEvmAddress();
|
|
305
|
+
if (!zkEvmAddress) {
|
|
306
|
+
throw new JsonRpcError(
|
|
307
|
+
ProviderErrorCode.UNAUTHORIZED,
|
|
308
|
+
'Unauthorised - call eth_requestAccounts first',
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
const flow = trackFlow('passport', 'ethSignTypedDataV4');
|
|
313
|
+
|
|
314
|
+
try {
|
|
315
|
+
return await this.#guardianClient.withConfirmationScreen({
|
|
316
|
+
width: 480,
|
|
317
|
+
height: 720,
|
|
318
|
+
})(async () => await signTypedDataV4({
|
|
319
|
+
method: request.method,
|
|
320
|
+
params: request.params || [],
|
|
321
|
+
ethSigner: this.#ethSigner,
|
|
322
|
+
rpcProvider: this.#rpcProvider,
|
|
323
|
+
relayerClient: this.#relayerClient,
|
|
324
|
+
guardianClient: this.#guardianClient,
|
|
325
|
+
flow,
|
|
326
|
+
}));
|
|
327
|
+
} catch (error) {
|
|
328
|
+
if (error instanceof Error) {
|
|
329
|
+
trackError('passport', 'eth_signTypedData', error, { flowId: flow.details.flowId });
|
|
330
|
+
} else {
|
|
331
|
+
flow.addEvent('errored');
|
|
332
|
+
}
|
|
333
|
+
throw error;
|
|
334
|
+
} finally {
|
|
335
|
+
flow.addEvent('End');
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
case 'eth_chainId': {
|
|
339
|
+
// Call detect network to fetch the chainId so to take advantage of
|
|
340
|
+
// the caching layer provided by StaticJsonRpcProvider.
|
|
341
|
+
// In case Passport is changed from StaticJsonRpcProvider to a
|
|
342
|
+
// JsonRpcProvider, this function will still work as expected given
|
|
343
|
+
// that detectNetwork call _uncachedDetectNetwork which will force
|
|
344
|
+
// the provider to re-fetch the chainId from remote.
|
|
345
|
+
const { chainId } = await this.#rpcProvider.getNetwork();
|
|
346
|
+
return toBeHex(chainId);
|
|
347
|
+
}
|
|
348
|
+
// Pass through methods
|
|
349
|
+
case 'eth_getBalance':
|
|
350
|
+
case 'eth_getCode':
|
|
351
|
+
case 'eth_getTransactionCount': {
|
|
352
|
+
const [address, blockNumber] = request.params || [];
|
|
353
|
+
return this.#rpcProvider.send(request.method, [
|
|
354
|
+
address,
|
|
355
|
+
blockNumber || 'latest',
|
|
356
|
+
]);
|
|
357
|
+
}
|
|
358
|
+
case 'eth_getStorageAt': {
|
|
359
|
+
const [address, storageSlot, blockNumber] = request.params || [];
|
|
360
|
+
return this.#rpcProvider.send(request.method, [
|
|
361
|
+
address,
|
|
362
|
+
storageSlot,
|
|
363
|
+
blockNumber || 'latest',
|
|
364
|
+
]);
|
|
365
|
+
}
|
|
366
|
+
case 'eth_call':
|
|
367
|
+
case 'eth_estimateGas': {
|
|
368
|
+
const [transaction, blockNumber] = request.params || [];
|
|
369
|
+
return this.#rpcProvider.send(request.method, [
|
|
370
|
+
transaction,
|
|
371
|
+
blockNumber || 'latest',
|
|
372
|
+
]);
|
|
373
|
+
}
|
|
374
|
+
case 'eth_gasPrice':
|
|
375
|
+
case 'eth_blockNumber':
|
|
376
|
+
case 'eth_getBlockByHash':
|
|
377
|
+
case 'eth_getBlockByNumber':
|
|
378
|
+
case 'eth_getTransactionByHash':
|
|
379
|
+
case 'eth_getTransactionReceipt': {
|
|
380
|
+
return this.#rpcProvider.send(request.method, request.params || []);
|
|
381
|
+
}
|
|
382
|
+
case 'im_signEjectionTransaction': {
|
|
383
|
+
const zkEvmAddress = await this.#getZkEvmAddress();
|
|
384
|
+
if (!zkEvmAddress) {
|
|
385
|
+
throw new JsonRpcError(
|
|
386
|
+
ProviderErrorCode.UNAUTHORIZED,
|
|
387
|
+
'Unauthorised - call eth_requestAccounts first',
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const flow = trackFlow('passport', 'imSignEjectionTransaction');
|
|
392
|
+
|
|
393
|
+
try {
|
|
394
|
+
return await signEjectionTransaction({
|
|
395
|
+
params: request.params || [],
|
|
396
|
+
ethSigner: this.#ethSigner,
|
|
397
|
+
zkEvmAddress,
|
|
398
|
+
flow,
|
|
399
|
+
});
|
|
400
|
+
} catch (error) {
|
|
401
|
+
if (error instanceof Error) {
|
|
402
|
+
trackError('passport', 'imSignEjectionTransaction', error, { flowId: flow.details.flowId });
|
|
403
|
+
} else {
|
|
404
|
+
flow.addEvent('errored');
|
|
405
|
+
}
|
|
406
|
+
throw error;
|
|
407
|
+
} finally {
|
|
408
|
+
flow.addEvent('End');
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
case 'im_addSessionActivity': {
|
|
412
|
+
const [clientId] = request.params || [];
|
|
413
|
+
const zkEvmAddress = await this.#getZkEvmAddress();
|
|
414
|
+
if (zkEvmAddress) {
|
|
415
|
+
this.#callSessionActivity(zkEvmAddress, clientId);
|
|
416
|
+
}
|
|
417
|
+
return null;
|
|
418
|
+
}
|
|
419
|
+
default: {
|
|
420
|
+
throw new JsonRpcError(
|
|
421
|
+
ProviderErrorCode.UNSUPPORTED_METHOD,
|
|
422
|
+
'Method not supported',
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
public async request(request: RequestArguments): Promise<any> {
|
|
429
|
+
try {
|
|
430
|
+
return this.#performRequest(request);
|
|
431
|
+
} catch (error: unknown) {
|
|
432
|
+
if (error instanceof JsonRpcError) {
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
if (error instanceof Error) {
|
|
436
|
+
throw new JsonRpcError(RpcErrorCode.INTERNAL_ERROR, error.message);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
throw new JsonRpcError(RpcErrorCode.INTERNAL_ERROR, 'Internal error');
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
public on(event: string, listener: (...args: any[]) => void): void {
|
|
444
|
+
this.#providerEventEmitter.on(event, listener);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
public removeListener(
|
|
448
|
+
event: string,
|
|
449
|
+
listener: (...args: any[]) => void,
|
|
450
|
+
): void {
|
|
451
|
+
this.#providerEventEmitter.removeListener(event, listener);
|
|
452
|
+
}
|
|
453
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "./dist",
|
|
5
|
+
"rootDirs": ["src"],
|
|
6
|
+
"customConditions": ["development"],
|
|
7
|
+
"types": ["node"]
|
|
8
|
+
},
|
|
9
|
+
"include": ["src", "src/types.ts"],
|
|
10
|
+
"exclude": [
|
|
11
|
+
"node_modules",
|
|
12
|
+
"dist",
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
|