Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion packages/game-bridge/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
},
"devDependencies": {
"eslint": "^8.40.0",
"parcel": "^2.8.3"
"parcel": "^2.13.3"
},
"scripts": {
"build": "parcel build --no-cache --no-scope-hoist",
Expand Down
12 changes: 8 additions & 4 deletions packages/internal/toolkit/src/crypto.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import BN from 'bn.js';
import * as encUtils from 'enc-utils';
import { Signer } from 'ethers';

type MessageSigner = {
getAddress(): Promise<string>;
signMessage(message: string | Uint8Array): Promise<string>;
};

type SignatureOptions = {
r: BN;
Expand Down Expand Up @@ -40,7 +44,7 @@ function deserializeSignature(sig: string, size = 64): SignatureOptions {

export async function signRaw(
payload: string,
signer: Signer,
signer: MessageSigner,
): Promise<string> {
const signature = deserializeSignature(await signer.signMessage(payload));
return serializeEthSignature(signature);
Expand All @@ -52,7 +56,7 @@ type IMXAuthorisationHeaders = {
};

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

export async function signMessage(
message: string,
signer: Signer,
signer: MessageSigner,
): Promise<{ message: string; ethAddress: string; ethSignature: string }> {
const ethAddress = await signer.getAddress();
const ethSignature = await signRaw(message, signer);
Expand Down
8 changes: 6 additions & 2 deletions packages/passport/sdk/src/starkEx/getStarkSigner.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
import { Signer } from 'ethers';
import {
createStarkSigner,
generateLegacyStarkPrivateKey,
StarkSigner,
} from '@imtbl/x-client';
import { withPassportError, PassportErrorType } from '../errors/passportError';

export const getStarkSigner = async (signer: Signer) => withPassportError<StarkSigner>(async () => {
type StarkMessageSigner = {
getAddress(): Promise<string>;
signMessage(message: string | Uint8Array): Promise<string>;
};

export const getStarkSigner = async (signer: StarkMessageSigner) => withPassportError<StarkSigner>(async () => {
const privateKey = await generateLegacyStarkPrivateKey(signer);
return createStarkSigner(privateKey);
}, PassportErrorType.WALLET_CONNECTION_ERROR);
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { ImxApiClients, imx } from '@imtbl/generated-clients';
import { EthSigner, StarkSigner } from '@imtbl/x-client';
import { MessageSigner, StarkSigner } from '@imtbl/x-client';
import { Auth, User } from '@imtbl/auth';
import { retryWithDelay } from '@imtbl/wallet';
import { PassportErrorType, withPassportError } from '../../errors/passportError';
Expand All @@ -25,7 +25,7 @@ async function forceUserRefresh(auth: Auth) {
}

export default async function registerOffchain(
userAdminKeySigner: EthSigner,
userAdminKeySigner: MessageSigner,
starkSigner: StarkSigner,
unregisteredUser: User,
auth: Auth,
Expand Down
6 changes: 4 additions & 2 deletions packages/passport/sdk/src/starkEx/workflows/registration.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { signRaw } from '@imtbl/toolkit';
import { WalletConnection } from '@imtbl/x-client';
import { MessageSigner, StarkSigner } from '@imtbl/x-client';
import { ImxApiClients, imx } from '@imtbl/generated-clients';
import { PassportErrorType, withPassportError } from '../../errors/passportError';

export type RegisterPassportParams = WalletConnection & {
export type RegisterPassportParams = {
ethSigner: MessageSigner;
starkSigner: StarkSigner;
imxApiClients: ImxApiClients;
};

Expand Down
3 changes: 1 addition & 2 deletions packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,7 @@
"@imtbl/auth": "workspace:*",
"@imtbl/generated-clients": "workspace:*",
"@imtbl/metrics": "workspace:*",
"@imtbl/toolkit": "workspace:*",
"ethers": "^6.13.4"
"viem": "~2.18.0"
},
"devDependencies": {
"@swc/core": "^1.3.36",
Expand Down
6 changes: 3 additions & 3 deletions packages/wallet/src/guardian/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import * as GeneratedClients from '@imtbl/generated-clients';
import { BigNumberish, ZeroAddress } from 'ethers';
import { zeroAddress } from 'viem';
import { Auth, IAuthConfiguration } from '@imtbl/auth';
import ConfirmationScreen from '../confirmation/confirmation';
import { JsonRpcError, ProviderErrorCode, RpcErrorCode } from '../zkEvm/JsonRpcError';
Expand Down Expand Up @@ -37,7 +37,7 @@ const transactionRejectedCrossSdkBridgeError = 'Transaction requires confirmatio
+ ' supported in this environment. Please contact Immutable support if you need to enable this feature.';

export const convertBigNumberishToString = (
value: BigNumberish,
value: bigint,
): string => BigInt(value).toString();

const transformGuardianTransactions = (
Expand All @@ -48,7 +48,7 @@ const transformGuardianTransactions = (
delegateCall: t.delegateCall === true,
revertOnError: t.revertOnError === true,
gasLimit: t.gasLimit ? convertBigNumberishToString(t.gasLimit) : '0',
target: t.to ?? ZeroAddress,
target: t.to ?? zeroAddress,
value: t.value ? convertBigNumberishToString(t.value) : '0',
data: t.data ? t.data.toString() : '0x',
}));
Expand Down
35 changes: 11 additions & 24 deletions packages/wallet/src/magic/magicTEESigner.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/* eslint-disable no-bitwise */
import { AbstractSigner, Signer } from 'ethers';
import { MagicTeeApiClients } from '@imtbl/generated-clients';
import { Flow, trackDuration } from '@imtbl/metrics';
import { WalletError, WalletErrorType } from '../errors';
import { Auth } from '@imtbl/auth';
import { withMetricsAsync } from '../utils/metrics';
import { isUserZkEvm, User } from '../types';
import { isUserZkEvm, User, WalletSigner } from '../types';
import { isAxiosError } from '../utils/http';

const CHAIN_IDENTIFIER = 'ETH';
Expand Down Expand Up @@ -58,7 +57,11 @@ const toBase64 = (value: string): string => {
return output;
};

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

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

constructor(auth: Auth, magicTeeApiClient: MagicTeeApiClients) {
super();
this.auth = auth;
this.magicTeeApiClient = magicTeeApiClient;
}
Expand Down Expand Up @@ -184,19 +186,19 @@ export default class MagicTEESigner extends AbstractSigner {
};
}

public async getAddress(): Promise<string> {
public async getAddress(): Promise<`0x${string}`> {
const userWallet = await this.getUserWallet();
return userWallet.walletAddress;
return userWallet.walletAddress as `0x${string}`;
}

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

const messageToSign = message instanceof Uint8Array ? `0x${toHex(message)}` : message;
const user = await this.getUserOrThrow();
const headers = await MagicTEESigner.getHeaders(user);
const headers = MagicTEESigner.getHeaders(user);

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

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

Expand All @@ -235,19 +237,4 @@ export default class MagicTEESigner extends AbstractSigner {
}
}, 'magicSignMessage');
}

// eslint-disable-next-line class-methods-use-this
connect(): Signer {
throw new Error('Method not implemented.');
}

// eslint-disable-next-line class-methods-use-this
signTransaction(): Promise<string> {
throw new Error('Method not implemented.');
}

// eslint-disable-next-line class-methods-use-this
signTypedData(): Promise<string> {
throw new Error('Method not implemented.');
}
}
22 changes: 16 additions & 6 deletions packages/wallet/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,19 @@ import { Flow } from '@imtbl/metrics';
import {
Auth, TypedEventEmitter, type AuthEventMap,
} from '@imtbl/auth';
import { BigNumberish } from 'ethers';
import { JsonRpcError } from './zkEvm/JsonRpcError';

/**
* A viem-compatible signer interface for wallet operations.
* This replaces ethers' AbstractSigner/Signer.
*/
export interface WalletSigner {
/** Get the wallet address */
getAddress(): Promise<`0x${string}`>;
/** Sign a message (EIP-191 personal_sign) */
signMessage(message: string | Uint8Array): Promise<`0x${string}`>;
}

// Re-export auth types for convenience
export type {
User, UserProfile, UserZkEvm, DirectLoginMethod, AuthEventMap,
Expand Down Expand Up @@ -83,20 +93,20 @@ export interface TypedDataPayload {

export interface MetaTransaction {
to: string;
value?: BigNumberish | null;
value?: bigint | null;
data?: string | null;
nonce?: BigNumberish;
gasLimit?: BigNumberish;
nonce?: bigint;
gasLimit?: bigint;
delegateCall?: boolean;
revertOnError?: boolean;
}

export interface MetaTransactionNormalised {
delegateCall: boolean;
revertOnError: boolean;
gasLimit: BigNumberish;
gasLimit: bigint;
target: string;
value: BigNumberish;
value: bigint;
data: string;
}

Expand Down
85 changes: 85 additions & 0 deletions packages/wallet/src/utils/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { WalletSigner } from '../types';

/**
* Signature components for Ethereum signatures
*/
type SignatureOptions = {
r: bigint;
s: bigint;
recoveryParam: number | null | undefined;
};

/**
* Adds '0x' prefix to a hex string if not present
*/
function addHexPrefix(hex: string): string {
return hex.startsWith('0x') ? hex : `0x${hex}`;
}

/**
* Removes '0x' prefix from a hex string if present
*/
function removeHexPrefix(hex: string): string {
return hex.startsWith('0x') ? hex.slice(2) : hex;
}

/**
* Pads a hex string to a specified length with leading zeros
*/
function padLeft(str: string, length: number): string {
return str.padStart(length, '0');
}

/**
* Serializes Ethereum signature components into a hex string.
* This format is used for IMX registration with golang backend.
* @see https://github.com/ethers-io/ethers.js/issues/823
*/
function serializeEthSignature(sig: SignatureOptions): string {
const rHex = padLeft(sig.r.toString(16), 64);
const sHex = padLeft(sig.s.toString(16), 64);
const vHex = padLeft(sig.recoveryParam?.toString(16) || '', 2);
return addHexPrefix(rHex + sHex + vHex);
}

/**
* Imports recovery parameter from hex string, normalizing v value
*/
function importRecoveryParam(v: string): number | undefined {
if (!v.trim()) return undefined;

const vValue = parseInt(v, 16);
// If v >= 27, subtract 27 to get recovery param (0 or 1)
return vValue >= 27 ? vValue - 27 : vValue;
}

/**
* Deserializes a signature hex string into its components (r, s, v)
*/
function deserializeSignature(sig: string, size = 64): SignatureOptions {
const cleanSig = removeHexPrefix(sig);
return {
r: BigInt(`0x${cleanSig.substring(0, size)}`),
s: BigInt(`0x${cleanSig.substring(size, size * 2)}`),
recoveryParam: importRecoveryParam(cleanSig.substring(size * 2, size * 2 + 2)),
};
}

/**
* Signs a message with the provided signer and returns a serialized signature
* suitable for IMX registration and authorization.
*
* This is inlined from @imtbl/toolkit to avoid ethers dependency.
*
* @param payload - The message to sign
* @param signer - A WalletSigner implementation
* @returns The serialized signature as a hex string
*/
export async function signRaw(
payload: string,
signer: WalletSigner,
): Promise<string> {
const rawSignature = await signer.signMessage(payload);
const signature = deserializeSignature(rawSignature);
return serializeEthSignature(signature);
}
32 changes: 30 additions & 2 deletions packages/wallet/src/utils/string.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,38 @@
import { getBytes, stripZerosLeft, toUtf8String } from 'ethers';
import { toBytes, type Hex } from 'viem';

/**
* Strip leading zero bytes from a Uint8Array
*/
const stripZerosLeft = (bytes: Uint8Array): Uint8Array => {
let start = 0;
while (start < bytes.length && bytes[start] === 0) {
start++;
}
return bytes.slice(start);
};

/**
* Convert UTF-8 bytes to string
*/
const toUtf8String = (bytes: Uint8Array): string => {
if (typeof TextDecoder !== 'undefined') {
return new TextDecoder('utf-8').decode(bytes);
}

// Fallback for environments without TextDecoder
let result = '';
for (let i = 0; i < bytes.length; i++) {
result += String.fromCharCode(bytes[i]);
}
return decodeURIComponent(escape(result));
};

export const hexToString = (hex: string) => {
if (!hex) return hex;

try {
const stripped = stripZerosLeft(getBytes(hex));
const bytes = toBytes(hex as Hex);
const stripped = stripZerosLeft(bytes);
return toUtf8String(stripped);
} catch (e) {
return hex;
Expand Down
Loading
Loading