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
6 changes: 3 additions & 3 deletions .github/workflows/docs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,12 @@ jobs:

steps:
- name: Checkout
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Setup Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'
node-version: '22'
enable-global-cache: true

- name: Clone docs repository
Expand Down
12 changes: 6 additions & 6 deletions .github/workflows/e2e-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,17 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
version: '9.15.4'

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'
node-version: '22'
cache: 'pnpm'

- name: Install dependencies
Expand All @@ -52,7 +52,7 @@ jobs:
# 上傳測試報告
- name: Upload test results
if: always()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: playwright-report
path: e2e/playwright-report/
Expand All @@ -61,7 +61,7 @@ jobs:
# 上傳測試截圖
- name: Upload screenshots
if: failure()
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v6
with:
name: test-screenshots
path: e2e/test-results/
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/unit-test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,17 @@ jobs:

steps:
- name: Checkout code
uses: actions/checkout@v4
uses: actions/checkout@v5

- name: Install pnpm
uses: pnpm/action-setup@v4
uses: pnpm/action-setup@v5
with:
version: '9.15.4'

- name: Set up Node.js
uses: actions/setup-node@v4
uses: actions/setup-node@v5
with:
node-version: '20'
node-version: '22'
cache: 'pnpm'

- name: Install dependencies
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"access": "restricted"
},
"engines": {
"node": ">=18.17.0"
"node": ">=22.0.0"
},
"scripts": {
"clean": "pnpm -r run clean",
Expand All @@ -29,7 +29,7 @@
"devDependencies": {
"@playwright/test": "^1.40.0",
"@types/jest": "^29.5.11",
"@types/node": "^20.12.7",
"@types/node": "^22.0.0",
"@typescript-eslint/eslint-plugin": "^7.10.0",
"@typescript-eslint/parser": "^7.10.0",
"eslint": "^8.56.0",
Expand Down
47 changes: 47 additions & 0 deletions packages/core/__tests__/wc-wallet-expiry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
jest.mock('../src/utils/get-wallet-of-type', () => ({}));
jest.mock('../src/wallets/extension-wallet', () => ({
ExtensionWallet: class {},
}));
jest.mock('@walletconnect/universal-provider');

import { WCCosmosWallet } from '../src/wallets/wc-wallets/wc-cosmos-wallet';
import { WCWallet } from '../src/wallets/wc-wallets/wc-wallet';

describe('WCWallet signingRequestExpiry', () => {
const signDoc = {} as any;
const expectedArgs = {
method: 'cosmos_signAmino',
params: { signerAddress: 'test-signer', signDoc },
};

it('passes the configured expiry to WCWallet sign requests', async () => {
const request = jest.fn().mockResolvedValue({ signed: 'signed-doc' });
const wallet = new WCWallet(undefined, undefined, { signingRequestExpiry: 3600 });
wallet.provider = { request } as any;

await wallet.signAmino('test-chain-id', 'test-signer', signDoc);

expect(request).toHaveBeenCalledWith(expectedArgs, 'cosmos:test-chain-id', 3600);
});

it('passes the parent WCWallet expiry to WCCosmosWallet sign requests', async () => {
const request = jest.fn().mockResolvedValue({ signed: 'signed-doc' });
const cosmosWallet = new WCCosmosWallet();
cosmosWallet.setWCProvider({ request } as any);
cosmosWallet.setWCWallet(new WCWallet(undefined, undefined, { signingRequestExpiry: 600 }));

await cosmosWallet.signAmino('test-chain-id', 'test-signer', signDoc);

expect(request).toHaveBeenCalledWith(expectedArgs, 'cosmos:test-chain-id', 600);
});

it('leaves expiry undefined when not configured', async () => {
const request = jest.fn().mockResolvedValue({ signed: 'signed-doc' });
const wallet = new WCWallet();
wallet.provider = { request } as any;

await wallet.signAmino('test-chain-id', 'test-signer', signDoc);

expect(request).toHaveBeenCalledWith(expectedArgs, 'cosmos:test-chain-id', undefined);
});
});
9 changes: 9 additions & 0 deletions packages/core/src/types/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,15 @@ export interface SignOptions {
readonly disableBalanceCheck?: boolean;
}

export interface WCWalletOptions {
/**
* Expiry for WalletConnect signing requests, in seconds.
* Must be between 300 (5 minutes) and 604800 (7 days).
* @see https://specs.walletconnect.com/2.0/specs/clients/sign/rpc-methods#expiry-validation
*/
signingRequestExpiry?: number;
}

export type Algo = 'secp256k1' | 'eth_secp256k1';
export interface AccountData {
/** A printable address (typically bech32 encoded) */
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/wallets/wc-wallets/wc-cosmos-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ export class WCCosmosWallet extends CosmosWallet implements IWCCommon {
signerAddress: signer,
signDoc,
},
}, `cosmos:${chainId}`);
}, `cosmos:${chainId}`, this.wcWallet?.options?.signingRequestExpiry);

return result as AminoSignResponse;
} catch (error) {
Expand Down Expand Up @@ -152,7 +152,7 @@ export class WCCosmosWallet extends CosmosWallet implements IWCCommon {
const result = await (this.provider.request({
method: 'cosmos_signDirect',
params: signDocValue,
}, `cosmos:${chainId}`) as Promise<WCDirectSignResponse>);
}, `cosmos:${chainId}`, this.wcWallet?.options?.signingRequestExpiry) as Promise<WCDirectSignResponse>);

return {
signed: {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/wallets/wc-wallets/wc-ethereum-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ export class WCEthereumWallet extends EthereumWallet implements IWCCommon {
const result = await this.provider.request({
method: 'eth_sendTransaction',
params: [transactionParameters]
}) as string;
}, undefined, this.wcWallet?.options?.signingRequestExpiry) as string;
return result;
} catch (error) {
console.log('send transaction error:', error);
Expand All @@ -126,7 +126,7 @@ export class WCEthereumWallet extends EthereumWallet implements IWCCommon {
const result = await this.provider.request({
method: 'personal_sign',
params: [message]
}) as string;
}, undefined, this.wcWallet?.options?.signingRequestExpiry) as string;
return result;
} catch (error) {
console.log('sign message error:', error);
Expand Down
11 changes: 6 additions & 5 deletions packages/core/src/wallets/wc-wallets/wc-solana-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export class WCSolanaWallet extends SolanaWallet implements IWCCommon {
const result = await this.provider.request({
method: 'solana_signAllTransactions',
params: { transactions }
}) as any[];
}, undefined, this.wcWallet?.options?.signingRequestExpiry) as any[];
return result;
} catch (error) {
console.log('sign all transactions error:', error);
Expand All @@ -135,7 +135,7 @@ export class WCSolanaWallet extends SolanaWallet implements IWCCommon {
const result = await this.provider.request({
method: 'solana_signAndSendAllTransactions',
params: { transactions }
});
}, undefined, this.wcWallet?.options?.signingRequestExpiry);
return result;
} catch (error) {
console.log('sign and send all transactions error:', error);
Expand All @@ -154,7 +154,7 @@ export class WCSolanaWallet extends SolanaWallet implements IWCCommon {
params: {
transaction: transaction.serialize({ verifySignatures: false }).toString('base64')
}
}) as string;
}, undefined, this.wcWallet?.options?.signingRequestExpiry) as string;
return result;
} catch (error) {
console.log('sign and send transaction error:', error);
Expand Down Expand Up @@ -190,7 +190,7 @@ export class WCSolanaWallet extends SolanaWallet implements IWCCommon {
const result = await this.provider.request({
method: 'solana_signMessage',
params: { message: base58.encode(message), pubkey: address }
}) as Uint8Array;
}, undefined, this.wcWallet?.options?.signingRequestExpiry) as Uint8Array;
return result;
} catch (error) {
console.log('sign message error:', error);
Expand All @@ -217,7 +217,8 @@ export class WCSolanaWallet extends SolanaWallet implements IWCCommon {
...transaction,
transaction: transaction.serialize({ verifySignatures: false }).toString('base64')
}
}
},
expiry: this.wcWallet?.options?.signingRequestExpiry,
});

const account = await this.getAccount('solana');
Expand Down
11 changes: 7 additions & 4 deletions packages/core/src/wallets/wc-wallets/wc-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { fromByteArray, toByteArray } from 'base64-js';
import { BroadcastMode } from 'interchainjs';

import { WalletConnectIcon } from '../../constant';
import { DirectSignDoc, SignType, Wallet, WalletAccount, WCDirectSignResponse, WcEventTypes, WcProviderEventType } from '../../types';
import { DirectSignDoc, SignType, Wallet, WalletAccount, WCDirectSignResponse, WcEventTypes, WcProviderEventType, WCWalletOptions } from '../../types';
import { MultiChainWallet } from '../multichain-wallet';
import { isWCCommon } from './wc-common';

Expand All @@ -35,11 +35,13 @@ export class WCWallet extends MultiChainWallet {

accountToRestore: WalletAccount | null = null;

options?: WCWalletOptions;

setAccountToRestore(account: WalletAccount) {
this.accountToRestore = account;
}

constructor(option?: Wallet, walletConnectOption?: SignClientTypes.Options) {
constructor(option?: Wallet, walletConnectOption?: SignClientTypes.Options, wcWalletOptions?: WCWalletOptions) {
const defaultWalletConnectOption: Wallet = {
name: 'WalletConnect',
prettyName: 'Wallet Connect',
Expand All @@ -50,6 +52,7 @@ export class WCWallet extends MultiChainWallet {
super({ ...defaultWalletConnectOption, ...option });

this.walletConnectOption = walletConnectOption;
this.options = wcWalletOptions;
}

async init(): Promise<void> {
Expand Down Expand Up @@ -276,7 +279,7 @@ export class WCWallet extends MultiChainWallet {
signerAddress: signer,
signDoc,
},
}, `cosmos:${chainId}`);
}, `cosmos:${chainId}`, this.options?.signingRequestExpiry);

return result as AminoSignResponse;
} catch (error) {
Expand Down Expand Up @@ -314,7 +317,7 @@ export class WCWallet extends MultiChainWallet {
const result = await (this.provider.request({
method: 'cosmos_signDirect',
params: signDocValue,
}, `cosmos:${chainId}`) as Promise<WCDirectSignResponse>);
}, `cosmos:${chainId}`, this.options?.signingRequestExpiry) as Promise<WCDirectSignResponse>);

return {
signed: {
Expand Down
43 changes: 37 additions & 6 deletions packages/store/__tests__/local-storage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,18 +126,18 @@ describe('LocalStorage', () => {
localStorageMock.setItem('interchain-kit-store', JSON.stringify(data));

const loadedData = localStorage.load();
// Note: Uint8Array gets serialized to plain object in JSON
const expectedData = {
...data,
chainWalletStates: data.chainWalletStates?.map(state => ({
...state,
account: state.account ? {
...state.account,
pubkey: { 0: 1, 1: 2, 2: 3 }
pubkey: new Uint8Array([1, 2, 3])
} : undefined
}))
};
expect(loadedData).toEqual(expectedData);
expect(loadedData.chainWalletStates?.[0]?.account?.pubkey).toBeInstanceOf(Uint8Array);
});

it('should return empty object when no data exists', () => {
Expand Down Expand Up @@ -199,18 +199,19 @@ describe('LocalStorage', () => {
localStorage.save(complexData);
const loadedData = localStorage.load();

// Note: Uint8Array gets serialized to plain object in JSON
const expectedData = {
...complexData,
chainWalletStates: complexData.chainWalletStates?.map(state => ({
...state,
account: state.account ? {
...state.account,
pubkey: { 0: 1, 1: 2, 2: 3 }
pubkey: new Uint8Array([1, 2, 3])
} : undefined
}))
};
expect(loadedData).toEqual(expectedData);
expect(loadedData.chainWalletStates?.[0]?.account?.pubkey).toBeInstanceOf(Uint8Array);
expect(loadedData.chainWalletStates?.[1]?.account?.pubkey).toBeInstanceOf(Uint8Array);
});
});

Expand Down Expand Up @@ -240,18 +241,18 @@ describe('LocalStorage', () => {
localStorage.save(originalData);
const loadedData = localStorage.load();

// Note: Uint8Array gets serialized to plain object in JSON
const expectedData = {
...originalData,
chainWalletStates: originalData.chainWalletStates?.map(state => ({
...state,
account: state.account ? {
...state.account,
pubkey: { 0: 1, 1: 2, 2: 3 }
pubkey: new Uint8Array([1, 2, 3])
} : undefined
}))
};
expect(loadedData).toEqual(expectedData);
expect(loadedData.chainWalletStates?.[0]?.account?.pubkey).toBeInstanceOf(Uint8Array);
});

it('should handle multiple save/load cycles', () => {
Expand All @@ -272,5 +273,35 @@ describe('LocalStorage', () => {
localStorage.save(data2);
expect(localStorage.load()).toEqual(data2);
});

it('should revive Uint8Array pubkeys after save/load round-trip', () => {
const originalData: Partial<InterchainStoreType> = {
currentWalletName: 'keplr',
currentChainName: 'cosmoshub',
isReady: true,
chainWalletStates: [
{
chainName: 'cosmoshub',
walletName: 'keplr',
walletState: 'Connected' as any,
rpcEndpoint: 'https://rpc.cosmos.network',
errorMessage: '',
account: {
address: 'cosmos1abc123',
pubkey: new Uint8Array([1, 2, 3]),
isNanoLedger: false,
algo: 'secp256k1' as Algo,
},
},
],
};

localStorage.save(originalData);
const loadedData = localStorage.load();
const pubkey = loadedData.chainWalletStates?.[0]?.account?.pubkey;

expect(pubkey).toBeInstanceOf(Uint8Array);
expect(Array.from(pubkey as Uint8Array)).toEqual([1, 2, 3]);
});
});
});
Loading
Loading