Skip to content
Draft
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
Binary file not shown.
2 changes: 1 addition & 1 deletion wallets/rn_cli_wallet/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ This is a production-ready reference implementation for developers building wall
- **@walletconnect/pay**: WalletConnect Pay integration

### Blockchain Libraries
- **ethers** (5.8.0): Ethereum/EVM interactions
- **ethers** (6.13.5): Ethereum/EVM interactions
- **@mysten/sui**: Sui blockchain support
- **@ton/core**, **@ton/crypto**, **@ton/ton**: TON blockchain
- **tronweb**: Tron blockchain
Expand Down
51 changes: 51 additions & 0 deletions wallets/rn_cli_wallet/__tests__/SettingsStore.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// Regression test for the ethers v6 + valtio interaction: setWallet() must
// ref() the wallet, else valtio's proxy corrupts ethers v6's private
// #signingKey and breaks signing from the shared eip155Wallets instance.

// SettingsStore transitively pulls in react-native-mmkv (native). Stub it.
jest.mock('react-native-mmkv', () => ({
MMKV: class {
getString() {
return undefined;
}
set() {}
delete() {}
getAllKeys() {
return [];
}
},
}));

import EIP155Lib from '../src/lib/EIP155Lib';
import SettingsStore from '../src/store/SettingsStore';

const TYPED_DATA = {
domain: {
name: 'Test',
version: '1',
chainId: 1,
verifyingContract: '0x0000000000000000000000000000000000000001',
},
types: { Msg: [{ name: 'value', type: 'string' }] },
message: { value: 'hello' },
};

describe('SettingsStore wallet storage (ethers v6 + valtio)', () => {
it('keeps the EVM wallet usable for signing after setWallet()', async () => {
const lib = EIP155Lib.init({});

// Mirrors useInitializeWalletKit: the same instance lives in the
// eip155Wallets map AND is stored in SettingsStore.
SettingsStore.setWallet(lib);

// PaymentStore signs using the instance from the raw map. Without ref()
// this throws "Cannot read private member #signingKey".
const signature = await lib._signTypedData(
TYPED_DATA.domain,
TYPED_DATA.types,
TYPED_DATA.message,
);

expect(signature).toMatch(/^0x[0-9a-fA-F]+$/);
});
});
4 changes: 1 addition & 3 deletions wallets/rn_cli_wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
"crc-32": "1.2.2",
"dayjs": "1.11.21",
"ed25519-hd-key": "1.3.0",
"ethers": "5.8.0",
"ethers": "6.13.5",
"expo": "56.0.12",
Comment on lines 60 to 62
"expo-application": "~56.0.3",
"expo-clipboard": "~56.0.4",
Expand Down Expand Up @@ -153,8 +153,6 @@
"joi": "17.13.4",
"uuid": "11.1.1",
"@ton/crypto-primitives@2.1.0": "patch:@ton/crypto-primitives@npm%3A2.1.0#./.yarn/patches/@ton-crypto-primitives-npm-2.1.0-3d1ea62176.patch",
"@ethersproject/pbkdf2@5.8.0": "patch:@ethersproject/pbkdf2@npm%3A5.8.0#./.yarn/patches/@ethersproject-pbkdf2-npm-5.8.0-b720e81bcc.patch",
"@ethersproject/pbkdf2@^5.8.0": "patch:@ethersproject/pbkdf2@npm%3A5.8.0#./.yarn/patches/@ethersproject-pbkdf2-npm-5.8.0-b720e81bcc.patch",
"bip39@3.1.0": "patch:bip39@npm%3A3.1.0#./.yarn/patches/bip39-npm-3.1.0-03958ed434.patch",
Comment on lines 153 to 156
"glob": "10.5.0",
"valibot": "1.2.0",
Expand Down
25 changes: 15 additions & 10 deletions wallets/rn_cli_wallet/src/lib/EIP155Lib.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { providers, Wallet } from 'ethers';
import {
HDNodeWallet,
JsonRpcProvider,
TransactionRequest,
Wallet,
} from 'ethers';

/**
* Types
Expand All @@ -12,19 +17,19 @@ interface IInitArgs {
* Library
*/
export default class EIP155Lib {
wallet: Wallet;
wallet: Wallet | HDNodeWallet;

constructor(wallet: Wallet) {
constructor(wallet: Wallet | HDNodeWallet) {
this.wallet = wallet;
}

static init({ mnemonic, privateKey }: IInitArgs) {
let wallet: Wallet;
let wallet: Wallet | HDNodeWallet;

if (privateKey) {
wallet = new Wallet(privateKey);
} else if (mnemonic) {
wallet = Wallet.fromMnemonic(mnemonic);
wallet = HDNodeWallet.fromPhrase(mnemonic);
} else {
wallet = Wallet.createRandom();
}
Expand All @@ -33,15 +38,15 @@ export default class EIP155Lib {
}

getMnemonic() {
return this.wallet.mnemonic?.phrase ?? '';
return 'mnemonic' in this.wallet ? this.wallet.mnemonic?.phrase ?? '' : '';
}

getPrivateKey() {
return this.wallet.privateKey;
}

hasMnemonic() {
return !!this.wallet.mnemonic?.phrase;
return 'mnemonic' in this.wallet && !!this.wallet.mnemonic?.phrase;
}

getAddress() {
Expand All @@ -53,14 +58,14 @@ export default class EIP155Lib {
}

_signTypedData(domain: any, types: any, data: any) {
return this.wallet._signTypedData(domain, types, data);
return this.wallet.signTypedData(domain, types, data);
}

connect(provider: providers.JsonRpcProvider) {
connect(provider: JsonRpcProvider) {
return this.wallet.connect(provider);
}

signTransaction(transaction: providers.TransactionRequest) {
signTransaction(transaction: TransactionRequest) {
return this.wallet.signTransaction(transaction);
}
}
7 changes: 6 additions & 1 deletion wallets/rn_cli_wallet/src/modals/SessionSignCantonModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
rejectCantonRequest,
} from '@/utils/CantonRequestHandlerUtil';
import { walletKit } from '@/utils/WalletKitUtil';
import { handleRedirect } from '@/utils/LinkingUtils';
import { RequestModal } from './RequestModal';
import { useCallback, useState } from 'react';
import { StyleSheet, View } from 'react-native';
Expand Down Expand Up @@ -44,6 +45,10 @@ export default function SessionSignCantonModal() {
response,
});
haptics.requestResponse();
handleRedirect({
peerRedirect: requestSession?.peer?.metadata?.redirect,
isLinkMode,
});
} catch (e) {
// Respond with JSON-RPC error so the dapp doesn't hang
try {
Expand All @@ -68,7 +73,7 @@ export default function SessionSignCantonModal() {
setIsLoadingApprove(false);
ModalStore.close();
}
}, [requestEvent, topic]);
}, [requestEvent, topic, requestSession, isLinkMode]);

const onReject = useCallback(async () => {
if (!requestEvent) {
Expand Down
7 changes: 6 additions & 1 deletion wallets/rn_cli_wallet/src/modals/SessionSignTronModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
rejectTronRequest,
} from '@/utils/TronRequestHandlerUtil';
import { walletKit } from '@/utils/WalletKitUtil';
import { handleRedirect } from '@/utils/LinkingUtils';
import { RequestModal } from './RequestModal';
import { useCallback, useState } from 'react';
import { StyleSheet, View } from 'react-native';
Expand Down Expand Up @@ -54,6 +55,10 @@ export default function SessionSignTronModal() {
response,
});
haptics.requestResponse();
handleRedirect({
peerRedirect: requestSession?.peer?.metadata?.redirect,
isLinkMode,
});
}
} catch (e) {
LogStore.error((e as Error).message, 'SessionSignTronModal', 'onApprove');
Expand All @@ -65,7 +70,7 @@ export default function SessionSignTronModal() {
setIsLoadingApprove(false);
ModalStore.close();
}
}, [requestEvent, topic]);
}, [requestEvent, topic, requestSession, isLinkMode]);

// Handle reject action
const onReject = useCallback(async () => {
Expand Down
6 changes: 3 additions & 3 deletions wallets/rn_cli_wallet/src/services/ERC20BalanceService.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Contract, providers, utils } from 'ethers';
import { Contract, JsonRpcProvider, formatUnits } from 'ethers';
import { ENV } from '@/utils/env';
import { TokenBalance } from '@/utils/BalanceTypes';
import LogStore, { serializeError } from '@/store/LogStore';
Expand Down Expand Up @@ -58,14 +58,14 @@ async function fetchSingleERC20Balance(
}

try {
const provider = new providers.JsonRpcProvider(rpcUrl);
const provider = new JsonRpcProvider(rpcUrl);
const contract = new Contract(
token.address,
ERC20_BALANCE_OF_ABI,
provider,
);
const rawBalance = await contract.balanceOf(walletAddress);
const numeric = utils.formatUnits(rawBalance, token.decimals);
const numeric = formatUnits(rawBalance, token.decimals);

return {
name: token.name,
Expand Down
10 changes: 5 additions & 5 deletions wallets/rn_cli_wallet/src/shims/quick-crypto.web.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,11 @@ export function getRandomValues<T extends ArrayBufferView | null>(array: T): T {
export const subtle = webCrypto?.subtle;

// Node's `crypto.pbkdf2Sync`, implemented in pure JS via @noble/hashes. The
// `@ethersproject/pbkdf2` patch aliases its `pbkdf2` export to
// `crypto.pbkdf2Sync` (the fast native path via quick-crypto on device); on web
// we provide it here so ethers' `Wallet.fromMnemonic` (mnemonic -> seed
// derivation) works instead of throwing
// "_ethersprojectPbkdf.pbkdf2 is not a function".
// `bip39` patch aliases its seed derivation (`mnemonicToSeedSync`) to
// `crypto.pbkdf2Sync` the fast native path via quick-crypto on device; on web
// we provide it here so bip39-based mnemonic -> seed derivation (Solana, Sui,
// Ton) works instead of throwing "pbkdf2 is not a function". (ethers v6 derives
// its own seed via @noble/hashes and no longer routes through `crypto`.)
type Hashish = typeof sha512;
const HASHES: Record<string, Hashish> = {sha512, sha256, sha1};

Expand Down
4 changes: 2 additions & 2 deletions wallets/rn_cli_wallet/src/store/PaymentStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
PaymentOptionsResponse,
PaymentOption,
} from '@walletconnect/pay';
import { providers } from 'ethers';
import type { TransactionRequest } from 'ethers';
import { Platform } from 'react-native';

import { ENV } from '@/utils/env';
Expand Down Expand Up @@ -620,7 +620,7 @@ const PaymentStore = {

const tx = await sendTransactionWithFreshFees({
chainId,
baseTx: { ...(txPayload as providers.TransactionRequest) },
baseTx: { ...(txPayload as TransactionRequest) },
wallet: evmWallet,
logContext: 'approvePayment',
});
Expand Down
17 changes: 10 additions & 7 deletions wallets/rn_cli_wallet/src/store/SettingsStore.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { proxy } from 'valtio';
import { proxy, ref } from 'valtio';
import { Appearance } from 'react-native';
import { Verify, SessionTypes } from '@walletconnect/types';

Expand Down Expand Up @@ -99,7 +99,10 @@ const SettingsStore = {
},

setWallet(wallet: EIP155Lib) {
state.wallet = wallet;
// ref() keeps the wallet out of valtio's proxy: ethers v6's private
// #signingKey throws through a Proxy and valtio would corrupt the shared
// eip155Wallets instance.
state.wallet = ref(wallet);
},

setActiveChainId(value: string) {
Expand Down Expand Up @@ -146,39 +149,39 @@ const SettingsStore = {
},

setSuiWallet(suiWallet: SuiLib) {
state.suiWallet = suiWallet;
state.suiWallet = ref(suiWallet);
},

setTonAddress(tonAddress: string) {
state.tonAddress = tonAddress;
},

setTonWallet(tonWallet: TonLib) {
state.tonWallet = tonWallet;
state.tonWallet = ref(tonWallet);
},

setTronAddress(tronAddress: string) {
state.tronAddress = tronAddress;
},

setTronWallet(tronWallet: TronLib) {
state.tronWallet = tronWallet;
state.tronWallet = ref(tronWallet);
},

setCantonAddress(cantonAddress: string) {
state.cantonAddress = cantonAddress;
},

setCantonWallet(cantonWallet: CantonLib) {
state.cantonWallet = cantonWallet;
state.cantonWallet = ref(cantonWallet);
},

setSolanaAddress(solanaAddress: string) {
state.solanaAddress = solanaAddress;
},

setSolanaWallet(solanaWallet: SolanaLib) {
state.solanaWallet = solanaWallet;
state.solanaWallet = ref(solanaWallet);
},

setThemeMode(value: 'light' | 'dark') {
Expand Down
18 changes: 15 additions & 3 deletions wallets/rn_cli_wallet/src/utils/EIP155RequestHandlerUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { providers } from 'ethers';
import { JsonRpcProvider } from 'ethers';
import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils';
import { SignClientTypes } from '@walletconnect/types';
import { getSdkError } from '@walletconnect/utils';
Expand All @@ -23,8 +23,20 @@ export async function approveEIP155Request(requestEvent: RequestEventArgs) {
eip155Wallets[getWalletAddressFromParams(eip155Addresses, params)];

switch (request.method) {
case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
// eth_sign signs arbitrary opaque bytes (no message prefix) — a phishing
// vector — so reject it and steer dApps to personal_sign.
case EIP155_SIGNING_METHODS.ETH_SIGN:
LogStore.warn(
'Rejected eth_sign request (disabled for security)',
'EIP155RequestHandler',
'ethSign',
);
return formatJsonRpcError(
id,
'eth_sign is disabled for security reasons. Use personal_sign instead.',
);

case EIP155_SIGNING_METHODS.PERSONAL_SIGN:
try {
const message = getSignParamsMessage(request.params);

Expand Down Expand Up @@ -63,7 +75,7 @@ export async function approveEIP155Request(requestEvent: RequestEventArgs) {
case EIP155_SIGNING_METHODS.ETH_SEND_TRANSACTION:
try {
const chainData = PresetsUtil.getChainDataById(chainId);
const provider = new providers.JsonRpcProvider(chainData?.rpcUrl);
const provider = new JsonRpcProvider(chainData?.rpcUrl);
const sendTransaction = request.params[0];
const connectedWallet = wallet.connect(provider);
Comment on lines 77 to 80
const { hash } = await connectedWallet.sendTransaction(sendTransaction);
Expand Down
4 changes: 2 additions & 2 deletions wallets/rn_cli_wallet/src/utils/EIP155WalletUtil.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { utils } from 'ethers';
import { Mnemonic } from 'ethers';

import { ENV } from './env';

Expand Down Expand Up @@ -68,7 +68,7 @@ export function loadEIP155Wallet(input: string): {
`Mnemonic must be 12, 15, 18, 21, or 24 words (got ${words.length})`,
);
}
if (!utils.isValidMnemonic(trimmedInput)) {
if (!Mnemonic.isValidMnemonic(trimmedInput)) {
throw new Error('Invalid mnemonic phrase');
}
}
Expand Down
Loading