@imtbl/wallet 2.12.5-alpha.8 → 2.12.5

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 (37) hide show
  1. package/dist/browser/index.js +202 -29
  2. package/dist/node/index.cjs +241 -56
  3. package/dist/node/index.js +202 -29
  4. package/dist/types/confirmation/confirmation.d.ts +1 -1
  5. package/dist/types/connectWallet.d.ts +2 -3
  6. package/dist/types/constants.d.ts +7 -0
  7. package/dist/types/index.d.ts +4 -2
  8. package/dist/types/network/chainRegistry.d.ts +13 -0
  9. package/dist/types/{presets.d.ts → network/presets.d.ts} +37 -1
  10. package/dist/types/sequence/sequenceProvider.d.ts +21 -0
  11. package/dist/types/sequence/signer/identityInstrumentSigner.d.ts +15 -0
  12. package/dist/types/sequence/signer/index.d.ts +20 -0
  13. package/dist/types/sequence/signer/privateKeySigner.d.ts +15 -0
  14. package/dist/types/sequence/signer/types.d.ts +14 -0
  15. package/dist/types/sequence/user/index.d.ts +2 -0
  16. package/dist/types/sequence/user/registerUser.d.ts +18 -0
  17. package/dist/types/types.d.ts +22 -1
  18. package/dist/types/zkEvm/types.d.ts +3 -10
  19. package/package.json +9 -4
  20. package/src/confirmation/confirmation.ts +14 -4
  21. package/src/connectWallet.test.ts +62 -3
  22. package/src/connectWallet.ts +82 -45
  23. package/src/constants.ts +10 -0
  24. package/src/guardian/index.ts +2 -0
  25. package/src/index.ts +14 -2
  26. package/src/network/chainRegistry.test.ts +64 -0
  27. package/src/network/chainRegistry.ts +74 -0
  28. package/src/{presets.ts → network/presets.ts} +64 -2
  29. package/src/sequence/sequenceProvider.ts +276 -0
  30. package/src/sequence/signer/identityInstrumentSigner.ts +193 -0
  31. package/src/sequence/signer/index.ts +45 -0
  32. package/src/sequence/signer/privateKeySigner.ts +111 -0
  33. package/src/sequence/signer/types.ts +24 -0
  34. package/src/sequence/user/index.ts +2 -0
  35. package/src/sequence/user/registerUser.ts +100 -0
  36. package/src/types.ts +26 -2
  37. package/src/zkEvm/types.ts +4 -10
@@ -0,0 +1,193 @@
1
+ import { hashMessage, toHex, concat } from 'viem';
2
+ import { Identity } from '@0xsequence/wallet-wdk';
3
+ import { IdentityInstrument, IdTokenChallenge } from '@0xsequence/identity-instrument';
4
+ import { WalletError, WalletErrorType } from '../../errors';
5
+ import { Auth, User, decodeJwtPayload } from '@imtbl/auth';
6
+ import { Hex, Address } from 'ox';
7
+ import {
8
+ Payload,
9
+ Signature as SequenceSignature,
10
+ } from '@0xsequence/wallet-primitives';
11
+ import { SequenceSigner } from './types';
12
+
13
+ interface IdTokenPayload {
14
+ iss: string;
15
+ aud: string;
16
+ sub: string;
17
+ }
18
+
19
+ interface AuthKey {
20
+ address: string;
21
+ privateKey: CryptoKey;
22
+ identitySigner: string;
23
+ expiresAt: Date;
24
+ }
25
+
26
+ interface UserWallet {
27
+ userIdentifier: string;
28
+ signerAddress: string;
29
+ authKey: AuthKey;
30
+ identityInstrument: IdentityInstrument;
31
+ }
32
+
33
+ export interface IdentityInstrumentSignerConfig {
34
+ /** Sequence Identity Instrument endpoint URL */
35
+ identityInstrumentEndpoint: string;
36
+ }
37
+
38
+ export class IdentityInstrumentSigner implements SequenceSigner {
39
+ readonly #auth: Auth;
40
+
41
+ readonly #config: IdentityInstrumentSignerConfig;
42
+
43
+ #userWallet: UserWallet | null = null;
44
+
45
+ #createWalletPromise: Promise<UserWallet> | null = null;
46
+
47
+ constructor(auth: Auth, config: IdentityInstrumentSignerConfig) {
48
+ this.#auth = auth;
49
+ this.#config = config;
50
+ }
51
+
52
+ async #getUserOrThrow(): Promise<User> {
53
+ const user = await this.#auth.getUser();
54
+ if (!user) {
55
+ throw new WalletError(
56
+ 'User not authenticated',
57
+ WalletErrorType.NOT_LOGGED_IN_ERROR,
58
+ );
59
+ }
60
+ return user;
61
+ }
62
+
63
+ async #getUserWallet(): Promise<UserWallet> {
64
+ let userWallet = this.#userWallet;
65
+ if (!userWallet) {
66
+ userWallet = await this.#createWallet();
67
+ }
68
+
69
+ const user = await this.#getUserOrThrow();
70
+ if (user.profile.sub !== userWallet.userIdentifier) {
71
+ userWallet = await this.#createWallet(user);
72
+ }
73
+
74
+ return userWallet;
75
+ }
76
+
77
+ async #createWallet(user?: User): Promise<UserWallet> {
78
+ if (this.#createWalletPromise) return this.#createWalletPromise;
79
+
80
+ this.#createWalletPromise = (async () => {
81
+ try {
82
+ this.#userWallet = null;
83
+ await this.#auth.forceUserRefresh(); // TODO shouldn't have to refresh all the time
84
+
85
+ const authenticatedUser = user || await this.#getUserOrThrow();
86
+
87
+ if (!authenticatedUser.idToken) {
88
+ throw new WalletError(
89
+ 'User idToken not available',
90
+ WalletErrorType.NOT_LOGGED_IN_ERROR,
91
+ );
92
+ }
93
+
94
+ const { idToken } = authenticatedUser;
95
+ const decoded = decodeJwtPayload<IdTokenPayload>(idToken);
96
+ const issuer = decoded.iss;
97
+ const audience = decoded.aud;
98
+
99
+ const keyPair = await window.crypto.subtle.generateKey(
100
+ { name: 'ECDSA', namedCurve: 'P-256' },
101
+ false,
102
+ ['sign', 'verify'],
103
+ );
104
+
105
+ const publicKey = await window.crypto.subtle.exportKey('raw', keyPair.publicKey);
106
+ const authKey: AuthKey = {
107
+ address: Hex.fromBytes(new Uint8Array(publicKey)),
108
+ privateKey: keyPair.privateKey,
109
+ identitySigner: '',
110
+ expiresAt: new Date(Date.now() + 3600000),
111
+ };
112
+
113
+ const identityInstrument = new IdentityInstrument(
114
+ this.#config.identityInstrumentEndpoint,
115
+ '@14:test',
116
+ );
117
+ const challenge = new IdTokenChallenge(issuer, audience, idToken);
118
+
119
+ await identityInstrument.commitVerifier(
120
+ Identity.toIdentityAuthKey(authKey),
121
+ challenge,
122
+ );
123
+
124
+ const result = await identityInstrument.completeAuth(
125
+ Identity.toIdentityAuthKey(authKey),
126
+ challenge,
127
+ );
128
+
129
+ const signerAddress = result.signer.address;
130
+ authKey.identitySigner = signerAddress;
131
+
132
+ this.#userWallet = {
133
+ userIdentifier: authenticatedUser.profile.sub,
134
+ signerAddress,
135
+ authKey,
136
+ identityInstrument,
137
+ };
138
+
139
+ return this.#userWallet;
140
+ } catch (error) {
141
+ const errorMessage = `Identity Instrument: Failed to create signer: ${(error as Error).message}`;
142
+ throw new WalletError(errorMessage, WalletErrorType.WALLET_CONNECTION_ERROR);
143
+ } finally {
144
+ this.#createWalletPromise = null;
145
+ }
146
+ })();
147
+
148
+ return this.#createWalletPromise;
149
+ }
150
+
151
+ async getAddress(): Promise<string> {
152
+ const wallet = await this.#getUserWallet();
153
+ return wallet.signerAddress;
154
+ }
155
+
156
+ async signPayload(
157
+ walletAddress: Address.Address,
158
+ chainId: number,
159
+ payload: Payload.Parented,
160
+ ): Promise<SequenceSignature.SignatureOfSignerLeaf> {
161
+ const wallet = await this.#getUserWallet();
162
+
163
+ const signer = new Identity.IdentitySigner(
164
+ wallet.identityInstrument,
165
+ wallet.authKey,
166
+ );
167
+
168
+ return signer.sign(walletAddress, chainId, payload);
169
+ }
170
+
171
+ async signMessage(message: string | Uint8Array): Promise<string> {
172
+ const wallet = await this.#getUserWallet();
173
+
174
+ const signer = new Identity.IdentitySigner(
175
+ wallet.identityInstrument,
176
+ wallet.authKey,
177
+ );
178
+
179
+ const messageBytes = typeof message === 'string'
180
+ ? new TextEncoder().encode(message)
181
+ : message;
182
+ const digest = hashMessage({ raw: messageBytes });
183
+
184
+ const signature = await signer.signDigest(Hex.toBytes(digest));
185
+
186
+ // Format signature: r (32 bytes) + s (32 bytes) + v (1 byte)
187
+ const r = toHex(signature.r, { size: 32 });
188
+ const s = toHex(signature.s, { size: 32 });
189
+ const v = toHex(signature.yParity + 27, { size: 1 });
190
+
191
+ return concat([r, s, v]);
192
+ }
193
+ }
@@ -0,0 +1,45 @@
1
+ import { Auth, IAuthConfiguration } from '@imtbl/auth';
2
+ import { SequenceSigner } from './types';
3
+ import { IdentityInstrumentSigner } from './identityInstrumentSigner';
4
+ import { PrivateKeySigner } from './privateKeySigner';
5
+
6
+ export type { SequenceSigner } from './types';
7
+ export { IdentityInstrumentSigner } from './identityInstrumentSigner';
8
+ export type { IdentityInstrumentSignerConfig } from './identityInstrumentSigner';
9
+ export { PrivateKeySigner } from './privateKeySigner';
10
+
11
+ const DEV_AUTH_DOMAIN = 'https://auth.dev.immutable.com';
12
+
13
+ export interface CreateSequenceSignerConfig {
14
+ /** Identity Instrument endpoint (required for prod/sandbox) */
15
+ identityInstrumentEndpoint?: string;
16
+ }
17
+
18
+ /**
19
+ * Create the appropriate signer based on environment.
20
+ * - Dev environment (behind VPN): uses PrivateKeySigner
21
+ * - Prod/Sandbox: uses IdentityInstrumentSigner
22
+ *
23
+ * @param auth - Auth instance
24
+ * @param authConfig - Auth configuration (to determine environment)
25
+ * @param config - Signer configuration
26
+ */
27
+ export function createSequenceSigner(
28
+ auth: Auth,
29
+ authConfig: IAuthConfiguration,
30
+ config: CreateSequenceSignerConfig = {},
31
+ ): SequenceSigner {
32
+ const isDevEnvironment = authConfig.authenticationDomain === DEV_AUTH_DOMAIN;
33
+
34
+ if (isDevEnvironment) {
35
+ return new PrivateKeySigner(auth);
36
+ }
37
+
38
+ if (!config.identityInstrumentEndpoint) {
39
+ throw new Error('identityInstrumentEndpoint is required for non-dev environments');
40
+ }
41
+
42
+ return new IdentityInstrumentSigner(auth, {
43
+ identityInstrumentEndpoint: config.identityInstrumentEndpoint,
44
+ });
45
+ }
@@ -0,0 +1,111 @@
1
+ import { keccak256, toBytes } from 'viem';
2
+ import { privateKeyToAccount } from 'viem/accounts';
3
+ import { WalletError, WalletErrorType } from '../../errors';
4
+ import { Auth, User } from '@imtbl/auth';
5
+ import { Signers } from '@0xsequence/wallet-core';
6
+ import {
7
+ Payload,
8
+ Signature as SequenceSignature,
9
+ } from '@0xsequence/wallet-primitives';
10
+ import { SequenceSigner } from './types';
11
+ import { Address } from 'ox';
12
+
13
+ interface PrivateKeyWallet {
14
+ userIdentifier: string;
15
+ signerAddress: string;
16
+ signer: Signers.Pk.Pk;
17
+ privateKey: `0x${string}`;
18
+ }
19
+
20
+ /**
21
+ * Private key signer for dev environments (behind VPN).
22
+ * Uses a deterministic private key derived from the user's sub.
23
+ */
24
+ export class PrivateKeySigner implements SequenceSigner {
25
+ readonly #auth: Auth;
26
+
27
+ #privateKeyWallet: PrivateKeyWallet | null = null;
28
+
29
+ #createWalletPromise: Promise<PrivateKeyWallet> | null = null;
30
+
31
+ constructor(auth: Auth) {
32
+ this.#auth = auth;
33
+ }
34
+
35
+ async #getUserOrThrow(): Promise<User> {
36
+ const user = await this.#auth.getUser();
37
+ if (!user) {
38
+ throw new WalletError('User not authenticated', WalletErrorType.NOT_LOGGED_IN_ERROR);
39
+ }
40
+ return user;
41
+ }
42
+
43
+ async #getWalletInstance(): Promise<PrivateKeyWallet> {
44
+ let privateKeyWallet = this.#privateKeyWallet;
45
+ if (!privateKeyWallet) {
46
+ privateKeyWallet = await this.#createWallet();
47
+ }
48
+
49
+ const user = await this.#getUserOrThrow();
50
+ if (user.profile.sub !== privateKeyWallet.userIdentifier) {
51
+ privateKeyWallet = await this.#createWallet(user);
52
+ }
53
+
54
+ return privateKeyWallet;
55
+ }
56
+
57
+ async #createWallet(user?: User): Promise<PrivateKeyWallet> {
58
+ if (this.#createWalletPromise) return this.#createWalletPromise;
59
+
60
+ this.#createWalletPromise = (async () => {
61
+ try {
62
+ this.#privateKeyWallet = null;
63
+ const authenticatedUser = user || (await this.#getUserOrThrow());
64
+
65
+ const privateKeyHash = keccak256(toBytes(`${authenticatedUser.profile.sub}-sequence`));
66
+ const signer = new Signers.Pk.Pk(privateKeyHash);
67
+ const signerAddress = signer.address;
68
+
69
+ this.#privateKeyWallet = {
70
+ userIdentifier: authenticatedUser.profile.sub,
71
+ signerAddress,
72
+ signer,
73
+ privateKey: privateKeyHash,
74
+ };
75
+
76
+ return this.#privateKeyWallet;
77
+ } catch (error) {
78
+ const errorMessage = `Failed to create private key wallet: ${(error as Error).message}`;
79
+ throw new WalletError(errorMessage, WalletErrorType.WALLET_CONNECTION_ERROR);
80
+ } finally {
81
+ this.#createWalletPromise = null;
82
+ }
83
+ })();
84
+
85
+ return this.#createWalletPromise;
86
+ }
87
+
88
+ async getAddress(): Promise<string> {
89
+ const wallet = await this.#getWalletInstance();
90
+ return wallet.signerAddress;
91
+ }
92
+
93
+ async signPayload(
94
+ walletAddress: Address.Address,
95
+ chainId: number,
96
+ payload: Payload.Parented,
97
+ ): Promise<SequenceSignature.SignatureOfSignerLeaf> {
98
+ const pkWallet = await this.#getWalletInstance();
99
+ return pkWallet.signer.sign(walletAddress, chainId, payload);
100
+ }
101
+
102
+ async signMessage(message: string | Uint8Array): Promise<string> {
103
+ const pkWallet = await this.#getWalletInstance();
104
+
105
+ // Use viem's account to sign
106
+ const account = privateKeyToAccount(pkWallet.privateKey);
107
+ const messageToSign = typeof message === 'string' ? message : { raw: message };
108
+
109
+ return await account.signMessage({ message: messageToSign });
110
+ }
111
+ }
@@ -0,0 +1,24 @@
1
+ import { Address } from 'ox';
2
+ import {
3
+ Payload,
4
+ Signature as SequenceSignature,
5
+ } from '@0xsequence/wallet-primitives';
6
+
7
+ /**
8
+ * Signer interface for Sequence wallet operations.
9
+ * Used by non-zkEVM chains (e.g., Arbitrum).
10
+ */
11
+ export interface SequenceSigner {
12
+ /** Get the signer's address */
13
+ getAddress(): Promise<string>;
14
+
15
+ /** Sign a Sequence payload (for transactions) */
16
+ signPayload(
17
+ walletAddress: Address.Address,
18
+ chainId: number,
19
+ payload: Payload.Parented,
20
+ ): Promise<SequenceSignature.SignatureOfSignerLeaf>;
21
+
22
+ /** Sign a message (EIP-191 personal_sign) */
23
+ signMessage(message: string | Uint8Array): Promise<string>;
24
+ }
@@ -0,0 +1,2 @@
1
+ export { registerUser } from './registerUser';
2
+ export type { RegisterUserInput } from './registerUser';
@@ -0,0 +1,100 @@
1
+ import { MultiRollupApiClients } from '@imtbl/generated-clients';
2
+ import { Flow } from '@imtbl/metrics';
3
+ import type { PublicClient } from 'viem';
4
+ import { getEip155ChainId } from '../../zkEvm/walletHelpers';
5
+ import { Auth } from '@imtbl/auth';
6
+ import { JsonRpcError, RpcErrorCode } from '../../zkEvm/JsonRpcError';
7
+ import { SequenceSigner } from '../signer';
8
+
9
+ export type RegisterUserInput = {
10
+ auth: Auth;
11
+ ethSigner: SequenceSigner;
12
+ multiRollupApiClients: MultiRollupApiClients;
13
+ accessToken: string;
14
+ rpcProvider: PublicClient;
15
+ flow: Flow;
16
+ };
17
+
18
+ const MESSAGE_TO_SIGN = 'Only sign this message from Immutable Passport';
19
+
20
+ /**
21
+ * Format the signature for the registration API.
22
+ * Converts v value from 27/28 format to 0/1 recovery param format.
23
+ */
24
+ function formatSignature(signature: string): string {
25
+ const sig = signature.startsWith('0x') ? signature.slice(2) : signature;
26
+ const r = sig.substring(0, 64);
27
+ const s = sig.substring(64, 128);
28
+ const v = sig.substring(128, 130);
29
+
30
+ const vNum = parseInt(v, 16);
31
+ const recoveryParam = vNum >= 27 ? vNum - 27 : vNum;
32
+ const vHex = recoveryParam.toString(16).padStart(2, '0');
33
+
34
+ return `0x${r}${s}${vHex}`;
35
+ }
36
+
37
+ /**
38
+ * Register a user for a non-zkEVM chain (e.g., Arbitrum).
39
+ * Creates a counterfactual address for the user on the specified chain.
40
+ */
41
+ export async function registerUser({
42
+ auth,
43
+ ethSigner,
44
+ multiRollupApiClients,
45
+ accessToken,
46
+ rpcProvider,
47
+ flow,
48
+ }: RegisterUserInput): Promise<string> {
49
+ // Parallelize the operations that can happen concurrently
50
+ const getAddressPromise = ethSigner.getAddress();
51
+ getAddressPromise.then(() => flow.addEvent('endGetAddress'));
52
+
53
+ const signMessagePromise = ethSigner.signMessage(MESSAGE_TO_SIGN).then(formatSignature);
54
+ signMessagePromise.then(() => flow.addEvent('endSignRaw'));
55
+
56
+ const detectNetworkPromise = rpcProvider.getChainId();
57
+ detectNetworkPromise.then(() => flow.addEvent('endDetectNetwork'));
58
+
59
+ const listChainsPromise = multiRollupApiClients.chainsApi.listChains();
60
+ listChainsPromise.then(() => flow.addEvent('endListChains'));
61
+
62
+ const [ethereumAddress, ethereumSignature, chainId, chainListResponse] = await Promise.all([
63
+ getAddressPromise,
64
+ signMessagePromise,
65
+ detectNetworkPromise,
66
+ listChainsPromise,
67
+ ]);
68
+
69
+ const eipChainId = getEip155ChainId(Number(chainId));
70
+ const chainName = chainListResponse.data?.result?.find((chain) => chain.id === eipChainId)?.name;
71
+ if (!chainName) {
72
+ throw new JsonRpcError(
73
+ RpcErrorCode.INTERNAL_ERROR,
74
+ `Chain name does not exist for chain id ${chainId}`,
75
+ );
76
+ }
77
+
78
+ try {
79
+ const registrationResponse = await multiRollupApiClients.passportApi.createCounterfactualAddressV2({
80
+ chainName,
81
+ createCounterfactualAddressRequest: {
82
+ ethereum_address: ethereumAddress,
83
+ ethereum_signature: ethereumSignature,
84
+ },
85
+ }, {
86
+ headers: { Authorization: `Bearer ${accessToken}` },
87
+ });
88
+ flow.addEvent('endCreateCounterfactualAddress');
89
+
90
+ auth.forceUserRefreshInBackground();
91
+
92
+ return registrationResponse.data.counterfactual_address;
93
+ } catch (error) {
94
+ flow.addEvent('errorRegisteringUser');
95
+ throw new JsonRpcError(
96
+ RpcErrorCode.INTERNAL_ERROR,
97
+ `Failed to create counterfactual address: ${error}`,
98
+ );
99
+ }
100
+ }
package/src/types.ts CHANGED
@@ -4,6 +4,11 @@ import {
4
4
  } from '@imtbl/auth';
5
5
  import { JsonRpcError } from './zkEvm/JsonRpcError';
6
6
 
7
+ export enum EvmChain {
8
+ ZKEVM = 'zkevm',
9
+ ARBITRUM_ONE = 'arbitrum_one',
10
+ }
11
+
7
12
  /**
8
13
  * A viem-compatible signer interface for wallet operations.
9
14
  * This replaces ethers' AbstractSigner/Signer.
@@ -41,8 +46,16 @@ export interface PassportEventMap extends AuthEventMap {
41
46
  [WalletEvents.ACCOUNTS_REQUESTED]: [AccountsRequestedEvent];
42
47
  }
43
48
 
44
- // Re-export zkEVM Provider type for public API
45
- export type { Provider } from './zkEvm/types';
49
+ /**
50
+ * EIP-1193 Provider Interface
51
+ * Standard Ethereum provider interface for all chain types
52
+ */
53
+ export type Provider = {
54
+ request: (request: RequestArguments) => Promise<any>;
55
+ on: (event: string, listener: (...args: any[]) => void) => void;
56
+ removeListener: (event: string, listener: (...args: any[]) => void) => void;
57
+ isPassport: boolean;
58
+ };
46
59
 
47
60
  export interface RequestArguments {
48
61
  method: string;
@@ -191,6 +204,17 @@ export interface ChainConfig {
191
204
  * Defaults to 'https://tee.express.magiclabs.com'
192
205
  */
193
206
  magicTeeBasePath?: string;
207
+
208
+ /** Preferred token symbol for relayer fees (default: 'IMX') */
209
+ feeTokenSymbol?: string;
210
+
211
+ /** Sequence RPC node URL TODO: check if this can be removed and only use rpcUrl */
212
+ nodeUrl?: string;
213
+
214
+ /**
215
+ * Sequence Identity Instrument endpoint (for non-zkEVM chains in prod/sandbox)
216
+ */
217
+ sequenceIdentityInstrumentEndpoint?: string;
194
218
  }
195
219
 
196
220
  /**
@@ -1,4 +1,5 @@
1
1
  import { JsonRpcError } from './JsonRpcError';
2
+ import type { Provider as ProviderType } from '../types';
2
3
 
3
4
  export enum RelayerTransactionStatus {
4
5
  PENDING = 'PENDING',
@@ -91,16 +92,9 @@ export interface JsonRpcResponsePayload {
91
92
  id?: string | number;
92
93
  }
93
94
 
94
- /**
95
- * EIP-1193 Provider Interface
96
- * Standard Ethereum provider interface
97
- */
98
- export type Provider = {
99
- request: (request: RequestArguments) => Promise<any>;
100
- on: (event: string, listener: (...args: any[]) => void) => void;
101
- removeListener: (event: string, listener: (...args: any[]) => void) => void;
102
- isPassport: boolean;
103
- };
95
+ export type { Provider } from '../types';
96
+
97
+ type Provider = ProviderType;
104
98
 
105
99
  export enum ProviderEvent {
106
100
  ACCOUNTS_CHANGED = 'accountsChanged',