Skip to content

Commit 0b33fdf

Browse files
authored
refactor(passport): replace ethers with viem in wallet package (#2753)
1 parent ba44b61 commit 0b33fdf

29 files changed

+2638
-1223
lines changed

packages/game-bridge/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
},
1212
"devDependencies": {
1313
"eslint": "^8.40.0",
14-
"parcel": "^2.8.3"
14+
"parcel": "^2.13.3"
1515
},
1616
"scripts": {
1717
"build": "parcel build --no-cache --no-scope-hoist",

packages/internal/toolkit/src/crypto.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
import BN from 'bn.js';
22
import * as encUtils from 'enc-utils';
3-
import { Signer } from 'ethers';
3+
4+
type MessageSigner = {
5+
getAddress(): Promise<string>;
6+
signMessage(message: string | Uint8Array): Promise<string>;
7+
};
48

59
type SignatureOptions = {
610
r: BN;
@@ -40,7 +44,7 @@ function deserializeSignature(sig: string, size = 64): SignatureOptions {
4044

4145
export async function signRaw(
4246
payload: string,
43-
signer: Signer,
47+
signer: MessageSigner,
4448
): Promise<string> {
4549
const signature = deserializeSignature(await signer.signMessage(payload));
4650
return serializeEthSignature(signature);
@@ -52,7 +56,7 @@ type IMXAuthorisationHeaders = {
5256
};
5357

5458
export async function generateIMXAuthorisationHeaders(
55-
ethSigner: Signer,
59+
ethSigner: MessageSigner,
5660
): Promise<IMXAuthorisationHeaders> {
5761
const timestamp = Math.floor(Date.now() / 1000).toString();
5862
const signature = await signRaw(timestamp, ethSigner);
@@ -65,7 +69,7 @@ export async function generateIMXAuthorisationHeaders(
6569

6670
export async function signMessage(
6771
message: string,
68-
signer: Signer,
72+
signer: MessageSigner,
6973
): Promise<{ message: string; ethAddress: string; ethSignature: string }> {
7074
const ethAddress = await signer.getAddress();
7175
const ethSignature = await signRaw(message, signer);
Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
1-
import { Signer } from 'ethers';
21
import {
32
createStarkSigner,
43
generateLegacyStarkPrivateKey,
54
StarkSigner,
65
} from '@imtbl/x-client';
76
import { withPassportError, PassportErrorType } from '../errors/passportError';
87

9-
export const getStarkSigner = async (signer: Signer) => withPassportError<StarkSigner>(async () => {
8+
type StarkMessageSigner = {
9+
getAddress(): Promise<string>;
10+
signMessage(message: string | Uint8Array): Promise<string>;
11+
};
12+
13+
export const getStarkSigner = async (signer: StarkMessageSigner) => withPassportError<StarkSigner>(async () => {
1014
const privateKey = await generateLegacyStarkPrivateKey(signer);
1115
return createStarkSigner(privateKey);
1216
}, PassportErrorType.WALLET_CONNECTION_ERROR);

packages/passport/sdk/src/starkEx/workflows/registerOffchain.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ImxApiClients, imx } from '@imtbl/generated-clients';
2-
import { EthSigner, StarkSigner } from '@imtbl/x-client';
2+
import { MessageSigner, StarkSigner } from '@imtbl/x-client';
33
import { Auth, User } from '@imtbl/auth';
44
import { retryWithDelay } from '@imtbl/wallet';
55
import { PassportErrorType, withPassportError } from '../../errors/passportError';
@@ -25,7 +25,7 @@ async function forceUserRefresh(auth: Auth) {
2525
}
2626

2727
export default async function registerOffchain(
28-
userAdminKeySigner: EthSigner,
28+
userAdminKeySigner: MessageSigner,
2929
starkSigner: StarkSigner,
3030
unregisteredUser: User,
3131
auth: Auth,

packages/passport/sdk/src/starkEx/workflows/registration.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import { signRaw } from '@imtbl/toolkit';
2-
import { WalletConnection } from '@imtbl/x-client';
2+
import { MessageSigner, StarkSigner } from '@imtbl/x-client';
33
import { ImxApiClients, imx } from '@imtbl/generated-clients';
44
import { PassportErrorType, withPassportError } from '../../errors/passportError';
55

6-
export type RegisterPassportParams = WalletConnection & {
6+
export type RegisterPassportParams = {
7+
ethSigner: MessageSigner;
8+
starkSigner: StarkSigner;
79
imxApiClients: ImxApiClients;
810
};
911

packages/wallet/package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,7 @@
3737
"@imtbl/auth": "workspace:*",
3838
"@imtbl/generated-clients": "workspace:*",
3939
"@imtbl/metrics": "workspace:*",
40-
"@imtbl/toolkit": "workspace:*",
41-
"ethers": "^6.13.4"
40+
"viem": "~2.18.0"
4241
},
4342
"devDependencies": {
4443
"@swc/core": "^1.3.36",

packages/wallet/src/guardian/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import * as GeneratedClients from '@imtbl/generated-clients';
2-
import { BigNumberish, ZeroAddress } from 'ethers';
2+
import { zeroAddress } from 'viem';
33
import { Auth, IAuthConfiguration } from '@imtbl/auth';
44
import ConfirmationScreen from '../confirmation/confirmation';
55
import { JsonRpcError, ProviderErrorCode, RpcErrorCode } from '../zkEvm/JsonRpcError';
@@ -37,7 +37,7 @@ const transactionRejectedCrossSdkBridgeError = 'Transaction requires confirmatio
3737
+ ' supported in this environment. Please contact Immutable support if you need to enable this feature.';
3838

3939
export const convertBigNumberishToString = (
40-
value: BigNumberish,
40+
value: bigint,
4141
): string => BigInt(value).toString();
4242

4343
const transformGuardianTransactions = (
@@ -48,7 +48,7 @@ const transformGuardianTransactions = (
4848
delegateCall: t.delegateCall === true,
4949
revertOnError: t.revertOnError === true,
5050
gasLimit: t.gasLimit ? convertBigNumberishToString(t.gasLimit) : '0',
51-
target: t.to ?? ZeroAddress,
51+
target: t.to ?? zeroAddress,
5252
value: t.value ? convertBigNumberishToString(t.value) : '0',
5353
data: t.data ? t.data.toString() : '0x',
5454
}));

packages/wallet/src/magic/magicTEESigner.ts

Lines changed: 11 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
/* eslint-disable no-bitwise */
2-
import { AbstractSigner, Signer } from 'ethers';
32
import { MagicTeeApiClients } from '@imtbl/generated-clients';
43
import { Flow, trackDuration } from '@imtbl/metrics';
54
import { WalletError, WalletErrorType } from '../errors';
65
import { Auth } from '@imtbl/auth';
76
import { withMetricsAsync } from '../utils/metrics';
8-
import { isUserZkEvm, User } from '../types';
7+
import { isUserZkEvm, User, WalletSigner } from '../types';
98
import { isAxiosError } from '../utils/http';
109

1110
const CHAIN_IDENTIFIER = 'ETH';
@@ -58,7 +57,11 @@ const toBase64 = (value: string): string => {
5857
return output;
5958
};
6059

61-
export default class MagicTEESigner extends AbstractSigner {
60+
/**
61+
* MagicTEESigner implements the WalletSigner interface for Magic TEE-based signing.
62+
* This signer delegates cryptographic operations to the Magic TEE service.
63+
*/
64+
export default class MagicTEESigner implements WalletSigner {
6265
private readonly auth: Auth;
6366

6467
private readonly magicTeeApiClient: MagicTeeApiClients;
@@ -68,7 +71,6 @@ export default class MagicTEESigner extends AbstractSigner {
6871
private createWalletPromise: Promise<UserWallet> | null = null;
6972

7073
constructor(auth: Auth, magicTeeApiClient: MagicTeeApiClients) {
71-
super();
7274
this.auth = auth;
7375
this.magicTeeApiClient = magicTeeApiClient;
7476
}
@@ -184,19 +186,19 @@ export default class MagicTEESigner extends AbstractSigner {
184186
};
185187
}
186188

187-
public async getAddress(): Promise<string> {
189+
public async getAddress(): Promise<`0x${string}`> {
188190
const userWallet = await this.getUserWallet();
189-
return userWallet.walletAddress;
191+
return userWallet.walletAddress as `0x${string}`;
190192
}
191193

192-
public async signMessage(message: string | Uint8Array): Promise<string> {
194+
public async signMessage(message: string | Uint8Array): Promise<`0x${string}`> {
193195
// Call getUserWallet to ensure that the createWallet endpoint has been called at least once,
194196
// as this is a prerequisite for signing messages.
195197
await this.getUserWallet();
196198

197199
const messageToSign = message instanceof Uint8Array ? `0x${toHex(message)}` : message;
198200
const user = await this.getUserOrThrow();
199-
const headers = await MagicTEESigner.getHeaders(user);
201+
const headers = MagicTEESigner.getHeaders(user);
200202

201203
return withMetricsAsync(async (flow: Flow) => {
202204
try {
@@ -217,7 +219,7 @@ export default class MagicTEESigner extends AbstractSigner {
217219
Math.round(performance.now() - startTime),
218220
);
219221

220-
return response.data.signature;
222+
return response.data.signature as `0x${string}`;
221223
} catch (error) {
222224
let errorMessage: string = 'MagicTEE: Failed to sign message using EOA';
223225

@@ -235,19 +237,4 @@ export default class MagicTEESigner extends AbstractSigner {
235237
}
236238
}, 'magicSignMessage');
237239
}
238-
239-
// eslint-disable-next-line class-methods-use-this
240-
connect(): Signer {
241-
throw new Error('Method not implemented.');
242-
}
243-
244-
// eslint-disable-next-line class-methods-use-this
245-
signTransaction(): Promise<string> {
246-
throw new Error('Method not implemented.');
247-
}
248-
249-
// eslint-disable-next-line class-methods-use-this
250-
signTypedData(): Promise<string> {
251-
throw new Error('Method not implemented.');
252-
}
253240
}

packages/wallet/src/types.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,19 @@ import { Flow } from '@imtbl/metrics';
22
import {
33
Auth, TypedEventEmitter, type AuthEventMap,
44
} from '@imtbl/auth';
5-
import { BigNumberish } from 'ethers';
65
import { JsonRpcError } from './zkEvm/JsonRpcError';
76

7+
/**
8+
* A viem-compatible signer interface for wallet operations.
9+
* This replaces ethers' AbstractSigner/Signer.
10+
*/
11+
export interface WalletSigner {
12+
/** Get the wallet address */
13+
getAddress(): Promise<`0x${string}`>;
14+
/** Sign a message (EIP-191 personal_sign) */
15+
signMessage(message: string | Uint8Array): Promise<`0x${string}`>;
16+
}
17+
818
// Re-export auth types for convenience
919
export type {
1020
User, UserProfile, UserZkEvm, DirectLoginMethod, AuthEventMap,
@@ -83,20 +93,20 @@ export interface TypedDataPayload {
8393

8494
export interface MetaTransaction {
8595
to: string;
86-
value?: BigNumberish | null;
96+
value?: bigint | null;
8797
data?: string | null;
88-
nonce?: BigNumberish;
89-
gasLimit?: BigNumberish;
98+
nonce?: bigint;
99+
gasLimit?: bigint;
90100
delegateCall?: boolean;
91101
revertOnError?: boolean;
92102
}
93103

94104
export interface MetaTransactionNormalised {
95105
delegateCall: boolean;
96106
revertOnError: boolean;
97-
gasLimit: BigNumberish;
107+
gasLimit: bigint;
98108
target: string;
99-
value: BigNumberish;
109+
value: bigint;
100110
data: string;
101111
}
102112

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
import type { WalletSigner } from '../types';
2+
3+
/**
4+
* Signature components for Ethereum signatures
5+
*/
6+
type SignatureOptions = {
7+
r: bigint;
8+
s: bigint;
9+
recoveryParam: number | null | undefined;
10+
};
11+
12+
/**
13+
* Adds '0x' prefix to a hex string if not present
14+
*/
15+
function addHexPrefix(hex: string): string {
16+
return hex.startsWith('0x') ? hex : `0x${hex}`;
17+
}
18+
19+
/**
20+
* Removes '0x' prefix from a hex string if present
21+
*/
22+
function removeHexPrefix(hex: string): string {
23+
return hex.startsWith('0x') ? hex.slice(2) : hex;
24+
}
25+
26+
/**
27+
* Pads a hex string to a specified length with leading zeros
28+
*/
29+
function padLeft(str: string, length: number): string {
30+
return str.padStart(length, '0');
31+
}
32+
33+
/**
34+
* Serializes Ethereum signature components into a hex string.
35+
* This format is used for IMX registration with golang backend.
36+
* @see https://github.com/ethers-io/ethers.js/issues/823
37+
*/
38+
function serializeEthSignature(sig: SignatureOptions): string {
39+
const rHex = padLeft(sig.r.toString(16), 64);
40+
const sHex = padLeft(sig.s.toString(16), 64);
41+
const vHex = padLeft(sig.recoveryParam?.toString(16) || '', 2);
42+
return addHexPrefix(rHex + sHex + vHex);
43+
}
44+
45+
/**
46+
* Imports recovery parameter from hex string, normalizing v value
47+
*/
48+
function importRecoveryParam(v: string): number | undefined {
49+
if (!v.trim()) return undefined;
50+
51+
const vValue = parseInt(v, 16);
52+
// If v >= 27, subtract 27 to get recovery param (0 or 1)
53+
return vValue >= 27 ? vValue - 27 : vValue;
54+
}
55+
56+
/**
57+
* Deserializes a signature hex string into its components (r, s, v)
58+
*/
59+
function deserializeSignature(sig: string, size = 64): SignatureOptions {
60+
const cleanSig = removeHexPrefix(sig);
61+
return {
62+
r: BigInt(`0x${cleanSig.substring(0, size)}`),
63+
s: BigInt(`0x${cleanSig.substring(size, size * 2)}`),
64+
recoveryParam: importRecoveryParam(cleanSig.substring(size * 2, size * 2 + 2)),
65+
};
66+
}
67+
68+
/**
69+
* Signs a message with the provided signer and returns a serialized signature
70+
* suitable for IMX registration and authorization.
71+
*
72+
* This is inlined from @imtbl/toolkit to avoid ethers dependency.
73+
*
74+
* @param payload - The message to sign
75+
* @param signer - A WalletSigner implementation
76+
* @returns The serialized signature as a hex string
77+
*/
78+
export async function signRaw(
79+
payload: string,
80+
signer: WalletSigner,
81+
): Promise<string> {
82+
const rawSignature = await signer.signMessage(payload);
83+
const signature = deserializeSignature(rawSignature);
84+
return serializeEthSignature(signature);
85+
}

0 commit comments

Comments
 (0)