Skip to content

Commit c2dfa59

Browse files
authored
feat(passport): add arbitrum chain config to wallet and passport (#2770)
1 parent c717923 commit c2dfa59

File tree

10 files changed

+274
-33
lines changed

10 files changed

+274
-33
lines changed

packages/passport/sdk/src/Passport.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ jest.mock('@imtbl/auth', () => {
3434
});
3535

3636
jest.mock('@imtbl/wallet', () => {
37+
const actual = jest.requireActual('@imtbl/wallet');
3738
const connectWalletMock = jest.fn();
3839

3940
return {
@@ -43,6 +44,8 @@ jest.mock('@imtbl/wallet', () => {
4344
MagicTEESigner: jest.fn(),
4445
WalletConfiguration: jest.fn(),
4546
ConfirmationScreen: jest.fn(),
47+
EvmChain: actual.EvmChain,
48+
getChainConfig: actual.getChainConfig,
4649
__mocked: {
4750
connectWalletMock,
4851
},
@@ -213,6 +216,32 @@ describe('Passport', () => {
213216
announceProvider: false,
214217
}));
215218
});
219+
220+
it('uses zkEVM chain config by default', async () => {
221+
connectWalletMock.mockResolvedValue({ kind: 'zkEvm' });
222+
const passport = createPassport();
223+
224+
await passport.connectEvm({ announceProvider: true });
225+
226+
expect(connectWalletMock).toHaveBeenCalledWith(expect.objectContaining({
227+
chains: expect.arrayContaining([
228+
expect.objectContaining({
229+
chainId: 13473, // zkEVM testnet for SANDBOX
230+
name: 'Immutable zkEVM Testnet',
231+
}),
232+
]),
233+
feeTokenSymbol: 'IMX',
234+
}));
235+
});
236+
237+
it('throws error for non-zkEVM chains (not yet implemented)', async () => {
238+
const { EvmChain: actualEvmChain } = jest.requireActual('@imtbl/wallet');
239+
const passport = createPassport();
240+
241+
await expect(
242+
passport.connectEvm({ announceProvider: true, chain: actualEvmChain.ARBITRUM_ONE }),
243+
).rejects.toThrow('Chain arbitrum_one is not yet supported. Only ZKEVM is currently available.');
244+
});
216245
});
217246

218247
describe('login flow', () => {

packages/passport/sdk/src/Passport.ts

Lines changed: 76 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ import {
2121
MagicTEESigner,
2222
ChainConfig,
2323
ConfirmationScreen,
24+
EvmChain,
25+
getChainConfig,
2426
} from '@imtbl/wallet';
2527
import type { LinkWalletParams, LinkedWallet } from '@imtbl/wallet';
2628
import {
@@ -200,19 +202,53 @@ export class Passport {
200202
* Uses: Auth + Wallet packages
201203
* @param {Object} options - Configuration options
202204
* @param {boolean} options.announceProvider - Whether to announce the provider via EIP-6963 for wallet discovery (defaults to true)
205+
* @param {EvmChain} options.chain - The EVM chain to connect to (defaults to ZKEVM)
203206
* @returns {Promise<Provider>} The EVM provider instance
204207
*/
205208
public async connectEvm(options: ConnectEvmArguments = { announceProvider: true }): Promise<ZkEvmProvider> {
206209
return withMetricsAsync(async () => {
207-
// Access PassportOverrides from PassportConfiguration
208-
const passportOverrides = this.passportConfig.overrides;
210+
const chain = options?.chain ?? EvmChain.ZKEVM;
209211

210-
// Build complete chain configuration
211-
let chainConfig: ChainConfig;
212+
// TODO: Remove this check once other chains are fully implemented
213+
if (chain !== EvmChain.ZKEVM) {
214+
throw new Error(`Chain ${chain} is not yet supported. Only ZKEVM is currently available.`);
215+
}
216+
217+
// Build chain configuration based on selected chain
218+
const chainConfig = this.buildChainConfig(chain);
219+
220+
// Use fee token from chain config, default to IMX for zkEVM
221+
const feeTokenSymbol = chainConfig.feeTokenSymbol ?? 'IMX';
222+
223+
// Use connectWallet to create the provider (it will create WalletConfiguration internally)
224+
const provider = await connectWallet({
225+
auth: this.auth,
226+
chains: [chainConfig],
227+
crossSdkBridgeEnabled: this.passportConfig.crossSdkBridgeEnabled,
228+
jsonRpcReferrer: this.passportConfig.jsonRpcReferrer,
229+
forceScwDeployBeforeMessageSignature: this.passportConfig.forceScwDeployBeforeMessageSignature,
230+
passportEventEmitter: this.auth.eventEmitter,
231+
feeTokenSymbol,
232+
announceProvider: options?.announceProvider ?? true,
233+
});
212234

235+
return provider;
236+
}, 'connectEvm', false);
237+
}
238+
239+
/**
240+
* Build chain configuration based on selected chain and environment
241+
* @internal
242+
*/
243+
private buildChainConfig(chain: EvmChain): ChainConfig {
244+
// Access PassportOverrides from PassportConfiguration
245+
const passportOverrides = this.passportConfig.overrides;
246+
247+
// For zkEVM chain (default)
248+
if (chain === EvmChain.ZKEVM) {
213249
if (passportOverrides?.zkEvmChainId) {
214250
// Dev environment with custom chain
215-
chainConfig = {
251+
return {
216252
chainId: passportOverrides.zkEvmChainId,
217253
name: passportOverrides.zkEvmChainName || 'Dev Chain',
218254
rpcUrl: this.passportConfig.zkEvmRpcUrl,
@@ -223,9 +259,11 @@ export class Passport {
223259
magicProviderId: this.passportConfig.magicProviderId,
224260
magicTeeBasePath: passportOverrides.magicTeeBasePath || this.passportConfig.magicTeeBasePath,
225261
};
226-
} else if (this.environment === Environment.PRODUCTION) {
262+
}
263+
264+
if (this.environment === Environment.PRODUCTION) {
227265
// Production environment
228-
chainConfig = {
266+
return {
229267
chainId: 13371,
230268
name: 'Immutable zkEVM',
231269
rpcUrl: this.passportConfig.zkEvmRpcUrl,
@@ -236,35 +274,40 @@ export class Passport {
236274
magicProviderId: this.passportConfig.magicProviderId,
237275
magicTeeBasePath: this.passportConfig.magicTeeBasePath,
238276
};
239-
} else {
240-
// Sandbox/testnet environment
241-
chainConfig = {
242-
chainId: 13473,
243-
name: 'Immutable zkEVM Testnet',
244-
rpcUrl: this.passportConfig.zkEvmRpcUrl,
245-
relayerUrl: this.passportConfig.relayerUrl,
246-
apiUrl: this.passportConfig.multiRollupConfig.indexer.basePath || this.passportConfig.passportDomain,
247-
passportDomain: this.passportConfig.passportDomain,
248-
magicPublishableApiKey: this.passportConfig.magicPublishableApiKey,
249-
magicProviderId: this.passportConfig.magicProviderId,
250-
magicTeeBasePath: this.passportConfig.magicTeeBasePath,
251-
};
252277
}
253278

254-
// Use connectWallet to create the provider (it will create WalletConfiguration internally)
255-
const provider = await connectWallet({
256-
auth: this.auth,
257-
chains: [chainConfig],
258-
crossSdkBridgeEnabled: this.passportConfig.crossSdkBridgeEnabled,
259-
jsonRpcReferrer: this.passportConfig.jsonRpcReferrer,
260-
forceScwDeployBeforeMessageSignature: this.passportConfig.forceScwDeployBeforeMessageSignature,
261-
passportEventEmitter: this.auth.eventEmitter,
262-
feeTokenSymbol: 'IMX',
263-
announceProvider: options?.announceProvider ?? true,
264-
});
279+
// Sandbox/testnet environment
280+
return {
281+
chainId: 13473,
282+
name: 'Immutable zkEVM Testnet',
283+
rpcUrl: this.passportConfig.zkEvmRpcUrl,
284+
relayerUrl: this.passportConfig.relayerUrl,
285+
apiUrl: this.passportConfig.multiRollupConfig.indexer.basePath || this.passportConfig.passportDomain,
286+
passportDomain: this.passportConfig.passportDomain,
287+
magicPublishableApiKey: this.passportConfig.magicPublishableApiKey,
288+
magicProviderId: this.passportConfig.magicProviderId,
289+
magicTeeBasePath: this.passportConfig.magicTeeBasePath,
290+
};
291+
}
265292

266-
return provider;
267-
}, 'connectEvm', false);
293+
// For all other chains, use the registry lookup
294+
const chainConfig = getChainConfig(chain, this.environment);
295+
296+
// If dev overrides exist, use dev-specific Passport config
297+
if (passportOverrides) {
298+
return {
299+
...chainConfig,
300+
apiUrl: this.passportConfig.multiRollupConfig.indexer.basePath || chainConfig.apiUrl,
301+
passportDomain: this.passportConfig.passportDomain,
302+
};
303+
}
304+
305+
// Standard SANDBOX/PRODUCTION
306+
return {
307+
...chainConfig,
308+
apiUrl: this.passportConfig.multiRollupConfig.indexer.basePath || chainConfig.apiUrl,
309+
passportDomain: this.passportConfig.passportDomain,
310+
};
268311
}
269312

270313
// ============================================================================

packages/passport/sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,4 +38,5 @@ export type {
3838
} from './types';
3939
export {
4040
MarketingConsentStatus,
41+
EvmChain,
4142
} from './types';

packages/passport/sdk/src/types.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,8 +121,16 @@ export type PKCEData = {
121121
// Re-export wallet linking types from wallet package
122122
export type { LinkWalletParams, LinkedWallet } from '@imtbl/wallet';
123123

124+
// Re-export EvmChain enum for specifying which chain to connect to
125+
export { EvmChain } from '@imtbl/wallet';
126+
124127
export type ConnectEvmArguments = {
125128
announceProvider: boolean;
129+
/**
130+
* The EVM chain to connect to (defaults to ZKEVM)
131+
* @default EvmChain.ZKEVM
132+
*/
133+
chain?: import('@imtbl/wallet').EvmChain;
126134
};
127135

128136
// Export ZkEvmProvider for return type
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { Environment } from '@imtbl/config';
2+
import { getChainConfig } from './chainRegistry';
3+
import { EvmChain } from './types';
4+
import { ARBITRUM_ONE_CHAIN, ARBITRUM_SEPOLIA_CHAIN } from './presets';
5+
6+
describe('chainRegistry', () => {
7+
describe('getChainConfig', () => {
8+
it('returns Arbitrum One mainnet config for PRODUCTION', () => {
9+
const config = getChainConfig(EvmChain.ARBITRUM_ONE, Environment.PRODUCTION);
10+
11+
expect(config).toEqual(ARBITRUM_ONE_CHAIN);
12+
expect(config.chainId).toBe(42161);
13+
expect(config.name).toBe('Arbitrum One');
14+
});
15+
16+
it('returns Arbitrum Sepolia config for SANDBOX', () => {
17+
const config = getChainConfig(EvmChain.ARBITRUM_ONE, Environment.SANDBOX);
18+
19+
expect(config).toEqual(ARBITRUM_SEPOLIA_CHAIN);
20+
expect(config.chainId).toBe(421614);
21+
expect(config.name).toBe('Arbitrum Sepolia');
22+
});
23+
24+
it('throws error for unsupported chain', () => {
25+
expect(() => {
26+
getChainConfig('unsupported_chain' as any, Environment.PRODUCTION);
27+
}).toThrow('Chain unsupported_chain is not supported');
28+
});
29+
});
30+
});
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { Environment } from '@imtbl/config';
2+
import { ChainConfig, EvmChain } from './types';
3+
import {
4+
ARBITRUM_ONE_CHAIN,
5+
ARBITRUM_SEPOLIA_CHAIN,
6+
} from './presets';
7+
8+
/**
9+
* Registry mapping (EvmChain, Environment) to ChainConfig
10+
* Add new chains here - no changes needed in Passport.ts
11+
*/
12+
const CHAIN_REGISTRY: Record<Exclude<EvmChain, EvmChain.ZKEVM>, Record<Environment, ChainConfig>> = {
13+
[EvmChain.ARBITRUM_ONE]: {
14+
[Environment.PRODUCTION]: ARBITRUM_ONE_CHAIN,
15+
[Environment.SANDBOX]: ARBITRUM_SEPOLIA_CHAIN,
16+
},
17+
};
18+
19+
/**
20+
* Get chain config for non-zkEVM chains
21+
* @throws Error if chain is not in registry
22+
*/
23+
export function getChainConfig(
24+
chain: Exclude<EvmChain, EvmChain.ZKEVM>,
25+
environment: Environment,
26+
): ChainConfig {
27+
const envConfigs = CHAIN_REGISTRY[chain];
28+
if (!envConfigs) {
29+
throw new Error(`Chain ${chain} is not supported`);
30+
}
31+
32+
const config = envConfigs[environment];
33+
if (!config) {
34+
throw new Error(`Chain ${chain} is not configured for environment ${environment}`);
35+
}
36+
37+
return config;
38+
}

packages/wallet/src/constants.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,16 @@ export const IMMUTABLE_ZKEVM_MAINNET_CHAIN_ID = 13371;
88
/** Immutable zkEVM Testnet chain ID */
99
export const IMMUTABLE_ZKEVM_TESTNET_CHAIN_ID = 13473;
1010

11+
/**
12+
* Chain ID constants for Arbitrum networks
13+
*/
14+
15+
/** Arbitrum One Mainnet chain ID */
16+
export const ARBITRUM_ONE_CHAIN_ID = 42161;
17+
18+
/** Arbitrum Sepolia Testnet chain ID */
19+
export const ARBITRUM_SEPOLIA_CHAIN_ID = 421614;
20+
1121
/**
1222
* Magic configuration for Immutable networks
1323
* @internal

packages/wallet/src/index.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,18 +5,29 @@ export { connectWallet } from './connectWallet';
55
export {
66
IMMUTABLE_ZKEVM_MAINNET_CHAIN_ID,
77
IMMUTABLE_ZKEVM_TESTNET_CHAIN_ID,
8+
ARBITRUM_ONE_CHAIN_ID,
9+
ARBITRUM_SEPOLIA_CHAIN_ID,
810
} from './constants';
911

1012
// Export presets (public API)
1113
export {
14+
// zkEVM chains
1215
IMMUTABLE_ZKEVM_MAINNET,
1316
IMMUTABLE_ZKEVM_TESTNET,
1417
IMMUTABLE_ZKEVM_MULTICHAIN,
1518
IMMUTABLE_ZKEVM_MAINNET_CHAIN,
1619
IMMUTABLE_ZKEVM_TESTNET_CHAIN,
1720
DEFAULT_CHAINS,
21+
// Arbitrum chains
22+
ARBITRUM_ONE,
23+
ARBITRUM_SEPOLIA,
24+
ARBITRUM_ONE_CHAIN,
25+
ARBITRUM_SEPOLIA_CHAIN,
1826
} from './presets';
1927

28+
// Export chain registry for looking up chain configs
29+
export { getChainConfig } from './chainRegistry';
30+
2031
// Export main wallet provider
2132
export { ZkEvmProvider } from './zkEvm/zkEvmProvider';
2233

0 commit comments

Comments
 (0)