diff --git a/wallets/rn_cli_wallet/package.json b/wallets/rn_cli_wallet/package.json
index 8b1cbefd..1b0ea469 100644
--- a/wallets/rn_cli_wallet/package.json
+++ b/wallets/rn_cli_wallet/package.json
@@ -7,7 +7,7 @@
"postinstall": "./scripts/copy-sample-files.sh",
"prebuild": "expo prebuild",
"postprebuild": "node scripts/setup-secrets.js",
- "android": "expo run:android",
+ "android": "expo run:android --app-id com.walletconnect.web3wallet.rnsample.debug",
"android:debug": "expo run:android --app-id com.walletconnect.web3wallet.rnsample.debug",
"android:internal": "expo run:android --variant internal --app-id com.walletconnect.web3wallet.rnsample.internal",
"android:prod": "expo run:android --variant release --app-id com.walletconnect.web3wallet.rnsample",
@@ -31,6 +31,7 @@
"permit2:revoke": "node ./scripts/revoke-permit2-approval.js"
},
"dependencies": {
+ "@bitcoinerlab/secp256k1": "1.2.0",
"@bottom-tabs/react-navigation": "1.3.1",
"@craftzdog/react-native-buffer": "6.1.1",
"@expo/metro-runtime": "~56.0.15",
@@ -39,6 +40,7 @@
"@mhpdev/react-native-haptics": "1.1.0",
"@mysten/sui": "1.45.2",
"@noble/hashes": "1.8.0",
+ "@noble/secp256k1": "3.0.0",
"@react-native-async-storage/async-storage": "2.2.0",
"@react-native-community/netinfo": "12.0.1",
"@react-navigation/bottom-tabs": "7.18.3",
@@ -53,10 +55,14 @@
"@ton/crypto": "3.3.0",
"@ton/ton": "15.4.0",
"@walletconnect/react-native-compat": "2.23.10",
+ "bip32": "4.0.0",
"bip39": "3.1.0",
+ "bitcoinjs-lib": "6.1.7",
+ "bitcoinjs-message": "2.2.0",
"bs58": "6.0.0",
"crc-32": "1.2.2",
"dayjs": "1.11.21",
+ "ecpair": "2.1.0",
"ed25519-hd-key": "1.3.0",
"ethers": "6.13.5",
"expo": "56.0.12",
diff --git a/wallets/rn_cli_wallet/src/assets/chains/bitcoin.png b/wallets/rn_cli_wallet/src/assets/chains/bitcoin.png
new file mode 100644
index 00000000..b991e4b7
Binary files /dev/null and b/wallets/rn_cli_wallet/src/assets/chains/bitcoin.png differ
diff --git a/wallets/rn_cli_wallet/src/components/Modal.tsx b/wallets/rn_cli_wallet/src/components/Modal.tsx
index 01112829..ab126ba3 100644
--- a/wallets/rn_cli_wallet/src/components/Modal.tsx
+++ b/wallets/rn_cli_wallet/src/components/Modal.tsx
@@ -23,6 +23,9 @@ import SessionSignTronModal from '@/modals/SessionSignTronModal';
import SessionSignCantonModal from '@/modals/SessionSignCantonModal';
import SessionSolanaSignMessageModal from '@/modals/SessionSolanaSignMessageModal';
import SessionSolanaSignTransactionModal from '@/modals/SessionSolanaSignTransactionModal';
+import SessionBitcoinSignMessageModal from '@/modals/SessionBitcoinSignMessageModal';
+import SessionBitcoinSendTransactionModal from '@/modals/SessionBitcoinSendTransactionModal';
+import SessionBitcoinGetAddressesModal from '@/modals/SessionBitcoinGetAddressesModal';
import PaymentOptionsModal from '@/modals/PaymentOptionsModal';
import ImportWalletModal from '@/modals/ImportWalletModal';
import SessionDetailModal from '@/modals/SessionDetailModal';
@@ -79,6 +82,12 @@ export default function Modal() {
return ;
case 'SessionSolanaSignTransactionModal':
return ;
+ case 'SessionBitcoinSignMessageModal':
+ return ;
+ case 'SessionBitcoinSendTransactionModal':
+ return ;
+ case 'SessionBitcoinGetAddressesModal':
+ return ;
case 'PaymentOptionsModal':
return ;
case 'ImportWalletModal':
diff --git a/wallets/rn_cli_wallet/src/components/NetworkDropdown.tsx b/wallets/rn_cli_wallet/src/components/NetworkDropdown.tsx
new file mode 100644
index 00000000..e0b59210
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/components/NetworkDropdown.tsx
@@ -0,0 +1,176 @@
+import { useState } from 'react';
+import { Image, ScrollView, StyleSheet, View } from 'react-native';
+
+import { useTheme } from '@/hooks/useTheme';
+import { PresetsUtil } from '@/utils/PresetsUtil';
+import { Spacing, BorderRadius } from '@/utils/ThemeUtil';
+import { Text } from '@/components/Text';
+import { Button } from '@/components/Button';
+import CaretUpDown from '@/assets/CaretUpDown';
+import Checkmark from '@/assets/Checkmark';
+
+export interface NetworkDropdownOption {
+ label: T;
+ chainId: string;
+}
+
+interface NetworkDropdownProps {
+ options: readonly NetworkDropdownOption[];
+ selected: T;
+ onSelect: (label: T) => void;
+ // Optional controlled open state. When provided, the parent owns whether the
+ // list is expanded (e.g. to close it when another field gains focus).
+ isOpen?: boolean;
+ onOpenChange?: (open: boolean) => void;
+}
+
+export function NetworkDropdown({
+ options,
+ selected,
+ onSelect,
+ isOpen,
+ onOpenChange,
+}: NetworkDropdownProps) {
+ const Theme = useTheme();
+ const [internalOpen, setInternalOpen] = useState(false);
+
+ const expanded = isOpen ?? internalOpen;
+ const setExpanded = (open: boolean) => {
+ if (onOpenChange) {
+ onOpenChange(open);
+ } else {
+ setInternalOpen(open);
+ }
+ };
+
+ const selectedOption =
+ options.find(option => option.label === selected) ?? options[0];
+
+ const handleSelect = (label: T) => {
+ onSelect(label);
+ setExpanded(false);
+ };
+
+ return (
+
+
+
+ {expanded && (
+
+
+ {options.map(option => {
+ const isSelected = option.label === selected;
+ return (
+
+ );
+ })}
+
+
+ )}
+
+ );
+}
+
+const styles = StyleSheet.create({
+ wrapper: {
+ width: '100%',
+ },
+ control: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ borderWidth: 1,
+ borderRadius: BorderRadius[3],
+ paddingVertical: Spacing[3],
+ paddingHorizontal: Spacing[4],
+ },
+ list: {
+ borderWidth: 1,
+ borderRadius: BorderRadius[3],
+ marginTop: Spacing[2],
+ paddingHorizontal: Spacing[4],
+ paddingVertical: Spacing[2],
+ },
+ scroll: {
+ maxHeight: 240,
+ },
+ listContent: {
+ gap: Spacing[1],
+ },
+ row: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ justifyContent: 'space-between',
+ paddingVertical: Spacing[2],
+ },
+ chainInfo: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: Spacing[2],
+ },
+ chainLogo: {
+ width: 24,
+ height: 24,
+ borderRadius: BorderRadius.full,
+ },
+});
diff --git a/wallets/rn_cli_wallet/src/constants/Bitcoin.ts b/wallets/rn_cli_wallet/src/constants/Bitcoin.ts
new file mode 100644
index 00000000..325c181a
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/constants/Bitcoin.ts
@@ -0,0 +1,50 @@
+// Bitcoin (bip122)
+import Bitcoin from '@/assets/chains/bitcoin.png';
+import { Chain } from '@/utils/TypesUtil';
+
+/**
+ * Methods
+ */
+export const BIP122_SIGNING_METHODS = {
+ BIP122_SIGN_MESSAGE: 'signMessage',
+ BIP122_GET_ACCOUNT_ADDRESSES: 'getAccountAddresses',
+ BIP122_SEND_TRANSACTION: 'sendTransfer',
+ BIP122_SIGN_PSBT: 'signPsbt',
+};
+
+/**
+ * Events
+ */
+export const BIP122_EVENTS = {
+ BIP122_ADDRESSES_CHANGED: 'bip122_addressesChanged',
+};
+
+export const BIP122_NAMESPACE = 'bip122';
+
+// Mainnet genesis block hash (CAIP-2 reference). Testnet is intentionally not
+// supported here — the RN wallet exposes only mainnet chains.
+export const BIP122_MAINNET_ID = '000000000019d6689c085ae165831e93';
+export const BIP122_MAINNET_CAIP2 = `${BIP122_NAMESPACE}:${BIP122_MAINNET_ID}`;
+
+export type IBip122ChainId = typeof BIP122_MAINNET_CAIP2;
+
+// BIP44 coin type for Bitcoin mainnet (used for HD derivation).
+export const BIP122_MAINNET_COIN_TYPE = '0';
+export const BIP122_MAINNET_RPC = 'https://mempool.space/api';
+
+export const BIP122_MAINNET = {
+ [BIP122_MAINNET_CAIP2]: {
+ chainId: BIP122_MAINNET_ID,
+ namespace: BIP122_NAMESPACE,
+ name: 'Bitcoin',
+ rpcUrl: BIP122_MAINNET_RPC,
+ },
+};
+
+export const BIP122_NETWORKS_IMAGES = {
+ [BIP122_MAINNET_CAIP2]: Bitcoin,
+};
+
+export const BIP122_CHAINS: Record = {
+ ...BIP122_MAINNET,
+};
diff --git a/wallets/rn_cli_wallet/src/hooks/useInitializeWalletKit.ts b/wallets/rn_cli_wallet/src/hooks/useInitializeWalletKit.ts
index b750ee51..0161b4c2 100644
--- a/wallets/rn_cli_wallet/src/hooks/useInitializeWalletKit.ts
+++ b/wallets/rn_cli_wallet/src/hooks/useInitializeWalletKit.ts
@@ -10,6 +10,7 @@ import { createOrRestoreTonWallet } from '@/utils/TonWalletUtil';
import { createOrRestoreTronWallet } from '@/utils/TronWalletUtil';
import { createOrRestoreCantonWallet } from '@/utils/CantonWalletUtil';
import { createOrRestoreSolanaWallet } from '@/utils/SolanaWalletUtil';
+import { createOrRestoreBitcoinWallet } from '@/utils/BitcoinWalletUtil';
export default function useInitializeWalletKit() {
const [initialized, setInitialized] = useState(false);
@@ -28,6 +29,8 @@ export default function useInitializeWalletKit() {
await createOrRestoreCantonWallet();
const { solanaAddress, solanaWallet } =
await createOrRestoreSolanaWallet();
+ const { bitcoinAddress, bitcoinWallet } =
+ await createOrRestoreBitcoinWallet();
SettingsStore.setEIP155Address(eip155Addresses[0]);
SettingsStore.setWallet(eip155Wallets[eip155Addresses[0]]);
@@ -41,6 +44,8 @@ export default function useInitializeWalletKit() {
SettingsStore.setCantonWallet(cantonWallet);
SettingsStore.setSolanaAddress(solanaAddress);
SettingsStore.setSolanaWallet(solanaWallet);
+ SettingsStore.setBitcoinAddress(bitcoinAddress);
+ SettingsStore.setBitcoinWallet(bitcoinWallet);
await createWalletKit(relayerRegionURL);
setInitialized(true);
SettingsStore.state.initPromiseResolver?.resolve(undefined);
diff --git a/wallets/rn_cli_wallet/src/hooks/useWalletKitEventsManager.ts b/wallets/rn_cli_wallet/src/hooks/useWalletKitEventsManager.ts
index 3d542c87..663410b5 100644
--- a/wallets/rn_cli_wallet/src/hooks/useWalletKitEventsManager.ts
+++ b/wallets/rn_cli_wallet/src/hooks/useWalletKitEventsManager.ts
@@ -16,6 +16,7 @@ import { TRON_SIGNING_METHODS } from '@/constants/Tron';
import { CANTON_SIGNING_METHODS } from '@/constants/Canton';
import { approveCantonRequest } from '@/utils/CantonRequestHandlerUtil';
import { SOLANA_SIGNING_METHODS } from '@/constants/Solana';
+import { BIP122_SIGNING_METHODS } from '@/constants/Bitcoin';
export default function useWalletKitEventsManager(initialized: boolean) {
/******************************************************************************
@@ -183,6 +184,22 @@ export default function useWalletKitEventsManager(initialized: boolean) {
requestEvent,
requestSession,
});
+ case BIP122_SIGNING_METHODS.BIP122_SIGN_MESSAGE:
+ return ModalStore.open('SessionBitcoinSignMessageModal', {
+ requestEvent,
+ requestSession,
+ });
+ case BIP122_SIGNING_METHODS.BIP122_GET_ACCOUNT_ADDRESSES:
+ return ModalStore.open('SessionBitcoinGetAddressesModal', {
+ requestEvent,
+ requestSession,
+ });
+ case BIP122_SIGNING_METHODS.BIP122_SEND_TRANSACTION:
+ case BIP122_SIGNING_METHODS.BIP122_SIGN_PSBT:
+ return ModalStore.open('SessionBitcoinSendTransactionModal', {
+ requestEvent,
+ requestSession,
+ });
default:
return ModalStore.open('SessionUnsuportedMethodModal', {
requestEvent,
diff --git a/wallets/rn_cli_wallet/src/lib/BitcoinLib.ts b/wallets/rn_cli_wallet/src/lib/BitcoinLib.ts
new file mode 100644
index 00000000..1333edd6
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/lib/BitcoinLib.ts
@@ -0,0 +1,476 @@
+// Buffer is polyfilled globally by '@walletconnect/react-native-compat' (loaded
+// in index.js). We intentionally use the global (Node-typed) Buffer here rather
+// than @craftzdog/react-native-buffer so the types line up with bitcoinjs-lib.
+import * as bip39 from 'bip39';
+import * as bitcoin from 'bitcoinjs-lib';
+// tiny-secp256k1 (used by the web sample) is a WASM module and does not run
+// under Hermes/React Native. @bitcoinerlab/secp256k1 is a pure-JS drop-in
+// implementing the same interface (built on @noble), so bitcoinjs-lib, bip32
+// and ecpair all accept it via their factories.
+import * as ecc from '@bitcoinerlab/secp256k1';
+import ECPairFactory from 'ecpair';
+import BIP32Factory, { BIP32Interface } from 'bip32';
+import bitcoinMessage from 'bitcoinjs-message';
+import { schnorr, hashes as nobleHashes } from '@noble/secp256k1';
+import { sha256 as nobleSha256 } from '@noble/hashes/sha256';
+import { hmac as nobleHmac } from '@noble/hashes/hmac';
+
+import LogStore from '@/store/LogStore';
+import {
+ BIP122_MAINNET_CAIP2,
+ BIP122_MAINNET_COIN_TYPE,
+ IBip122ChainId,
+} from '@/constants/Bitcoin';
+
+bitcoin.initEccLib(ecc);
+
+// @noble/secp256k1 v3 leaves the synchronous hash functions unset; its sync API
+// (schnorr.sign, used for the ordinals/taproot path) throws "hashes.sha256 not
+// set" until we provide them. Wire them from @noble/hashes (pure-JS, Hermes-safe)
+// so we don't depend on WebCrypto being available.
+nobleHashes.sha256 = (message: Uint8Array) => nobleSha256(message);
+nobleHashes.hmacSha256 = (key: Uint8Array, message: Uint8Array) =>
+ nobleHmac(nobleSha256, key, message);
+
+const ECPair = ECPairFactory(ecc);
+const bip32 = BIP32Factory(ecc);
+
+// The RN wallet only exposes Bitcoin mainnet.
+const NETWORK = bitcoin.networks.bitcoin;
+// Number of receiving addresses to derive per chain.
+const ADDRESS_COUNT = 20;
+
+interface IInitArguments {
+ mnemonic?: string;
+}
+
+interface IUTXO {
+ txid: string;
+ vout: number;
+ value: number;
+ status: {
+ confirmed: boolean;
+ block_height: number;
+ block_hash: string;
+ block_time: number;
+ };
+}
+
+interface ICreateTransaction {
+ network: bitcoin.Network;
+ recipientAddress: string;
+ amount: number;
+ changeAddress: string;
+ memo?: string;
+ utxos: IUTXO[];
+ privateKeyWIF: string;
+ feeRate: number;
+}
+
+interface IAddressData {
+ address: string;
+ path: string;
+ publicKey: string;
+}
+
+interface IPsbtInput {
+ address: string;
+ index: number;
+ sighashTypes: number[];
+}
+
+interface ISignPsbt {
+ account: string;
+ psbt: string;
+ signInputs: IPsbtInput[];
+ broadcast: boolean;
+ chainId: IBip122ChainId;
+}
+
+const validator = (
+ pubkey: Buffer,
+ msghash: Buffer,
+ signature: Buffer,
+): boolean => {
+ return ECPair.fromPublicKey(pubkey).verify(msghash, signature);
+};
+
+/**
+ * Library
+ */
+export default class BitcoinLib {
+ private account: BIP32Interface;
+ private mnemonic: string;
+ private addresses = {} as Record>;
+ private ordinals = {} as Record>;
+ private keys = {} as Record<
+ IBip122ChainId,
+ Map
+ >;
+
+ constructor({ mnemonic }: IInitArguments) {
+ this.keys[BIP122_MAINNET_CAIP2] = new Map();
+ this.addresses[BIP122_MAINNET_CAIP2] = new Map();
+ this.ordinals[BIP122_MAINNET_CAIP2] = new Map();
+
+ this.mnemonic = mnemonic ? mnemonic : bip39.generateMnemonic();
+ const seed = bip39.mnemonicToSeedSync(this.mnemonic);
+ const root = bip32.fromSeed(seed);
+ this.account = bip32.fromBase58(root.toBase58());
+ this.loadAddresses();
+ LogStore.info('Bitcoin wallet initialized', 'BitcoinLib', 'constructor', {
+ address: this.getAddress(BIP122_MAINNET_CAIP2),
+ });
+ }
+
+ static async init({ mnemonic }: IInitArguments) {
+ return new BitcoinLib({ mnemonic });
+ }
+
+ public getAddress(chainId: IBip122ChainId) {
+ return Array.from(this.addresses[chainId].values())[0].address;
+ }
+
+ public getOrdinalsAddress(chainId: IBip122ChainId) {
+ return Array.from(this.ordinals[chainId].values())[0].address;
+ }
+
+ public getMnemonic() {
+ return this.mnemonic;
+ }
+
+ public getAddresses(chainId: IBip122ChainId, intentions?: string[]) {
+ if (intentions && intentions[0] === 'ordinal') {
+ return this.ordinals[chainId];
+ }
+ return this.addresses[chainId];
+ }
+
+ public async signMessage({
+ message,
+ address,
+ protocol,
+ chainId,
+ }: {
+ message: string;
+ address: string;
+ protocol?: string;
+ chainId: IBip122ChainId;
+ }) {
+ if (protocol && protocol !== 'ecdsa') {
+ throw new Error(`Supported signing protocols: ecdsa, received: ${protocol}`);
+ }
+ const addressData = this.getAddressData(address, chainId);
+ if (!addressData) {
+ throw new Error(`Unknown address: ${address}`);
+ }
+ const keyData = this.keys[chainId].get(address)!;
+ const keyPair = ECPair.fromWIF(keyData.wif);
+ const privateKey = keyPair.privateKey!;
+
+ let signature: Buffer;
+ if (this.isOrdinal(address, chainId)) {
+ const messageHash = bitcoin.crypto.sha256(Buffer.from(message));
+ const sig = await schnorr.sign(messageHash, privateKey);
+ signature = Buffer.from(sig);
+ } else {
+ signature = bitcoinMessage.sign(message, privateKey, keyPair.compressed, {
+ segwitType: 'p2wpkh',
+ });
+ }
+
+ LogStore.log('Bitcoin message signed', 'BitcoinLib', 'signMessage');
+ return {
+ signature: signature.toString('hex').replace('0x', ''),
+ address,
+ };
+ }
+
+ public async sendTransfer(params: {
+ account: string;
+ recipientAddress: string;
+ amount: string;
+ changeAddress?: string;
+ memo?: string;
+ chainId: IBip122ChainId;
+ }) {
+ const { account, recipientAddress, amount, changeAddress, memo, chainId } =
+ params;
+ const satoshis = parseInt(amount, 10);
+
+ const addressData = this.getAddressData(account, chainId);
+ if (!addressData) {
+ throw new Error(`Unknown address: ${account}`);
+ }
+
+ if (satoshis < 0) {
+ throw new Error(`Invalid amount: ${amount}`);
+ }
+
+ const utxos = (await this.getUTXOs(account)) as IUTXO[];
+ if (!utxos || utxos.length === 0) {
+ throw new Error(`No UTXOs found for address: ${account}`);
+ }
+
+ let utxosValue = 0;
+ const utxosToSpend: IUTXO[] = [];
+ // Greedily select UTXOs until the target amount is covered. A `return`
+ // inside forEach only skips to the next item (not break), which would spend
+ // every UTXO — use for...of + break so we stop once we have enough.
+ for (const utxo of utxos) {
+ utxosValue += utxo.value;
+ utxosToSpend.push(utxo);
+ if (utxosValue >= satoshis) {
+ break;
+ }
+ }
+
+ const keyData = this.keys[chainId].get(account)!;
+ const transaction = await this.createTransaction({
+ network: keyData.network,
+ recipientAddress,
+ amount: satoshis,
+ changeAddress: changeAddress || account,
+ utxos: utxosToSpend,
+ privateKeyWIF: keyData.wif,
+ memo,
+ feeRate: await this.getFeeRate(),
+ });
+ const txid = await this.broadcastTransaction(transaction);
+ LogStore.log('Bitcoin transfer sent', 'BitcoinLib', 'sendTransfer', {
+ txid,
+ });
+ return txid;
+ }
+
+ public async signPsbt({
+ account,
+ psbt,
+ signInputs,
+ broadcast = false,
+ chainId,
+ }: ISignPsbt) {
+ const keyData = this.keys[chainId].get(account)!;
+ const keyPair = ECPair.fromWIF(keyData.wif);
+ const transaction = bitcoin.Psbt.fromBase64(psbt, {
+ network: keyData.network,
+ });
+ signInputs.forEach(({ address, index, sighashTypes }) => {
+ let keyPairToSignWith = keyPair;
+ if (address !== account) {
+ const inputKeyData = this.keys[chainId].get(address)!;
+ keyPairToSignWith = ECPair.fromWIF(inputKeyData.wif);
+ }
+ transaction.signInput(index, keyPairToSignWith, sighashTypes);
+ });
+ // Validate the inputs we actually signed. Hardcoding index 0 would throw
+ // when the dApp asks to sign inputs that don't include index 0.
+ signInputs.forEach(({ index }) => {
+ transaction.validateSignaturesOfInput(index, validator);
+ });
+ transaction.finalizeAllInputs();
+
+ if (!broadcast) {
+ LogStore.log('Bitcoin PSBT signed', 'BitcoinLib', 'signPsbt');
+ return {
+ psbt: transaction.toBase64(),
+ };
+ }
+
+ const tx = transaction.extractTransaction();
+ const txid = await this.broadcastTransaction(tx.toHex());
+ LogStore.log('Bitcoin PSBT signed and broadcast', 'BitcoinLib', 'signPsbt', {
+ txid,
+ });
+ return {
+ psbt: transaction.toBase64(),
+ txid,
+ };
+ }
+
+ async getUTXOs(address: string): Promise {
+ return await (
+ await fetch(`https://mempool.space/api/address/${address}/utxo`)
+ ).json();
+ }
+
+ async broadcastTransaction(transaction: string) {
+ const result = await fetch('https://mempool.space/api/tx', {
+ method: 'POST',
+ body: transaction,
+ });
+
+ if (result.ok) {
+ return await result.text();
+ }
+ throw new Error('Error broadcasting transaction: ' + (await result.text()));
+ }
+
+ getAvailableBalance(utxos: IUTXO[]) {
+ return utxos.reduce((acc, { value }) => acc + value, 0);
+ }
+
+ private async getFeeRate() {
+ const defaultFeeRate = 2;
+ try {
+ const response = await fetch(
+ 'https://mempool.space/api/v1/fees/recommended',
+ );
+ if (response.ok) {
+ const data = await response.json();
+ return parseInt(data?.economyFee ?? defaultFeeRate, 10);
+ }
+ } catch (e) {
+ LogStore.error(
+ (e as Error).message,
+ 'BitcoinLib',
+ 'getFeeRate',
+ );
+ }
+ return defaultFeeRate;
+ }
+
+ private generateAddress({
+ index,
+ coinType,
+ chainId,
+ change = false,
+ taproot = false,
+ }: {
+ index: number;
+ coinType: string;
+ chainId: IBip122ChainId;
+ change?: boolean;
+ taproot?: boolean;
+ }) {
+ // BIP86 (m/86') is the standard derivation for single-key P2TR (taproot/
+ // ordinals); BIP84 (m/84') is for P2WPKH. Using the right purpose per
+ // address type keeps addresses interoperable with other BIP86/BIP84 wallets.
+ const purpose = taproot ? 86 : 84;
+ const path = `m/${purpose}'/${coinType}'/0'/${change ? 1 : 0}/${index}`;
+ const child = this.account.derivePath(path);
+ let address: string;
+ if (taproot) {
+ address = bitcoin.payments.p2tr({
+ pubkey: child.publicKey.slice(1),
+ network: NETWORK,
+ }).address!;
+ } else {
+ address = bitcoin.payments.p2wpkh({
+ pubkey: child.publicKey,
+ network: NETWORK,
+ }).address!;
+ }
+ const wif = child.toWIF();
+ this.keys[chainId].set(address, { wif, network: NETWORK });
+ return { address, path, publicKey: child.publicKey.toString('hex') };
+ }
+
+ private loadAddresses() {
+ const chainId: IBip122ChainId = BIP122_MAINNET_CAIP2;
+ const coinType = BIP122_MAINNET_COIN_TYPE;
+
+ for (let i = 0; i < ADDRESS_COUNT; i++) {
+ const addressParams = { index: i, chainId, coinType };
+ // payment addresses (P2WPKH)
+ const addressData = this.generateAddress(addressParams);
+ this.addresses[chainId].set(addressData.address, addressData);
+ // ordinals / taproot addresses (P2TR)
+ const taprootAddress = this.generateAddress({
+ ...addressParams,
+ taproot: true,
+ });
+ this.ordinals[chainId].set(taprootAddress.address, taprootAddress);
+ }
+ }
+
+ private async createTransaction({
+ network,
+ recipientAddress,
+ amount,
+ changeAddress,
+ memo,
+ utxos,
+ privateKeyWIF,
+ feeRate,
+ }: ICreateTransaction) {
+ const psbt = new bitcoin.Psbt({ network });
+ const keyPair = ECPair.fromWIF(privateKeyWIF);
+ const payment = bitcoin.payments.p2wpkh({
+ pubkey: keyPair.publicKey,
+ network,
+ });
+
+ utxos.forEach(utxo => {
+ psbt.addInput({
+ hash: utxo.txid,
+ index: utxo.vout,
+ witnessUtxo: {
+ script: Buffer.from(payment.output?.toString('hex')!, 'hex'),
+ value: utxo.value,
+ },
+ });
+ });
+
+ psbt.addOutput({
+ address: recipientAddress,
+ value: amount,
+ });
+ const change = this.calculateChange(utxos, amount, feeRate);
+
+ if (change > 0) {
+ psbt.addOutput({
+ address: changeAddress,
+ value: change,
+ });
+ }
+
+ if (memo) {
+ const data = Buffer.from(memo, 'utf8');
+ const embed = bitcoin.payments.embed({ data: [data] });
+ psbt.addOutput({
+ script: embed.output!,
+ value: 0,
+ });
+ }
+
+ psbt.signAllInputs(keyPair);
+ // All inputs are signed with our key, so validate every one (not just
+ // index 0) before finalizing.
+ psbt.validateSignaturesOfAllInputs(validator);
+ psbt.finalizeAllInputs();
+
+ const tx = psbt.extractTransaction();
+ return tx.toHex();
+ }
+
+ // Helper function to calculate change
+ private calculateChange(
+ utxos: IUTXO[],
+ amount: number,
+ feeRate: number,
+ ): number {
+ const inputSum = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
+ /**
+ * 10 bytes: estimated fixed transaction overhead.
+ * 148 bytes: average size of each input (UTXO).
+ * 34 bytes: size of each output (x2 for recipient + change).
+ */
+ const estimatedSize = 10 + 148 * utxos.length + 34 * 2;
+ const fee = estimatedSize * feeRate;
+ const change = inputSum - amount - fee;
+ return change;
+ }
+
+ private getAddressData(address: string, chainId: IBip122ChainId) {
+ const addressData = this.addresses[chainId].get(address);
+ if (addressData) {
+ return addressData;
+ }
+ return this.ordinals[chainId].get(address);
+ }
+
+ private isOrdinal(address: string, chainId: IBip122ChainId) {
+ return this.ordinals[chainId].has(address);
+ }
+}
diff --git a/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx b/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx
index bb8622b4..a1c8b175 100644
--- a/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx
+++ b/wallets/rn_cli_wallet/src/modals/ImportWalletModal.tsx
@@ -19,21 +19,43 @@ import { loadTronWallet } from '@/utils/TronWalletUtil';
import { loadSuiWallet } from '@/utils/SuiWalletUtil';
import { loadCantonWallet } from '@/utils/CantonWalletUtil';
import { loadSolanaWallet } from '@/utils/SolanaWalletUtil';
+import { loadBitcoinWallet } from '@/utils/BitcoinWalletUtil';
import { Text } from '@/components/Text';
import { ModalCloseButton } from '@/components/ModalCloseButton';
-import { SegmentedControl } from '@/components/SegmentedControl';
+import {
+ NetworkDropdown,
+ NetworkDropdownOption,
+} from '@/components/NetworkDropdown';
import { Spacing, BorderRadius, FontFamily } from '@/utils/ThemeUtil';
import { ActionButton } from '@/components/ActionButton';
+import { TON_CHAINS } from '@/constants/Ton';
+import { TRON_CHAINS } from '@/constants/Tron';
+import { SUI_MAINNET_CAIP2 } from '@/constants/Sui';
+import { CANTON_CHAINS } from '@/constants/Canton';
+import { SOLANA_MAINNET_CAIP2 } from '@/constants/Solana';
+import { BIP122_MAINNET_CAIP2 } from '@/constants/Bitcoin';
+
+type ChainOption =
+ | 'Ethereum'
+ | 'Ton'
+ | 'Tron'
+ | 'Sui'
+ | 'Canton'
+ | 'Solana'
+ | 'Bitcoin';
-const CHAIN_OPTIONS = [
- 'Ethereum',
- 'Ton',
- 'Tron',
- 'Sui',
- 'Canton',
- 'Solana',
+// Representative chainId per option, used to render the network icon in the
+// dropdown. Derived from each namespace's chain map so it stays correct if the
+// underlying CAIP-2 ids change.
+const CHAIN_OPTIONS: readonly NetworkDropdownOption[] = [
+ { label: 'Ethereum', chainId: 'eip155:1' },
+ { label: 'Ton', chainId: Object.keys(TON_CHAINS)[0] },
+ { label: 'Tron', chainId: Object.keys(TRON_CHAINS)[0] },
+ { label: 'Sui', chainId: SUI_MAINNET_CAIP2 },
+ { label: 'Canton', chainId: Object.keys(CANTON_CHAINS)[0] },
+ { label: 'Solana', chainId: SOLANA_MAINNET_CAIP2 },
+ { label: 'Bitcoin', chainId: BIP122_MAINNET_CAIP2 },
] as const;
-type ChainOption = (typeof CHAIN_OPTIONS)[number];
const PLACEHOLDER_TEXT: Record = {
Ethereum: 'Mnemonic or private key (0x…)',
@@ -42,6 +64,7 @@ const PLACEHOLDER_TEXT: Record = {
Sui: 'Mnemonic phrase (12–24 words)',
Canton: 'Secret key (128 hex chars)',
Solana: 'Mnemonic (12–24 words) or base58 secret key',
+ Bitcoin: 'Mnemonic phrase (12–24 words)',
};
const EMPTY_INPUT_ERROR: Record = {
@@ -51,6 +74,7 @@ const EMPTY_INPUT_ERROR: Record = {
Sui: 'Enter a mnemonic phrase.',
Canton: 'Enter an Ed25519 secret key.',
Solana: 'Enter a mnemonic or base58 secret key.',
+ Bitcoin: 'Enter a mnemonic phrase.',
};
export default function ImportWalletModal() {
@@ -58,6 +82,7 @@ export default function ImportWalletModal() {
const [selectedChain, setSelectedChain] = useState('Ethereum');
const [input, setInput] = useState('');
const [isLoading, setIsLoading] = useState(false);
+ const [isNetworkDropdownOpen, setIsNetworkDropdownOpen] = useState(false);
const handleChainChange = (chain: ChainOption) => {
setSelectedChain(chain);
@@ -150,6 +175,23 @@ export default function ImportWalletModal() {
address = result.address;
break;
}
+ case 'Bitcoin': {
+ const result = await loadBitcoinWallet(sanitizedInput);
+ address = result.address;
+ // Refetch balances with the new Bitcoin address
+ WalletStore.fetchBalances(
+ {
+ eip155Address: SettingsStore.state.eip155Address,
+ tonAddress: SettingsStore.state.tonAddress,
+ tronAddress: SettingsStore.state.tronAddress,
+ suiAddress: SettingsStore.state.suiAddress,
+ solanaAddress: SettingsStore.state.solanaAddress,
+ bitcoinAddress: address,
+ },
+ { force: true },
+ );
+ break;
+ }
case 'Solana': {
const result = await loadSolanaWallet(sanitizedInput);
address = result.address;
@@ -225,10 +267,12 @@ export default function ImportWalletModal() {
-
@@ -245,6 +289,7 @@ export default function ImportWalletModal() {
placeholderTextColor={Theme['text-secondary']}
value={input}
onChangeText={setInput}
+ onFocus={() => setIsNetworkDropdownOpen(false)}
multiline
numberOfLines={4}
autoCapitalize="none"
diff --git a/wallets/rn_cli_wallet/src/modals/SessionBitcoinGetAddressesModal.tsx b/wallets/rn_cli_wallet/src/modals/SessionBitcoinGetAddressesModal.tsx
new file mode 100644
index 00000000..32e495fe
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/modals/SessionBitcoinGetAddressesModal.tsx
@@ -0,0 +1,163 @@
+import { useSnapshot } from 'valtio';
+import { useCallback, useMemo, useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { SignClientTypes } from '@walletconnect/types';
+import { showToast } from '@/utils/ToastUtil';
+
+import { Message } from '@/components/Modal/Message';
+import { AppInfoCard } from '@/components/AppInfoCard';
+import { NetworkInfoCard } from '@/components/NetworkInfoCard';
+
+import { walletKit } from '@/utils/WalletKitUtil';
+import { handleRedirect } from '@/utils/LinkingUtils';
+import LogStore from '@/store/LogStore';
+import ModalStore from '@/store/ModalStore';
+import SettingsStore from '@/store/SettingsStore';
+import { RequestModal } from './RequestModal';
+import {
+ approveBitcoinRequest,
+ rejectBitcoinRequest,
+} from '@/utils/BitcoinRequestHandlerUtil';
+import { bitcoinAddresses } from '@/utils/BitcoinWalletUtil';
+import { Text } from '@/components/Text';
+import { Spacing } from '@/utils/ThemeUtil';
+import { haptics } from '@/utils/haptics';
+
+export default function SessionBitcoinGetAddressesModal() {
+ const { data } = useSnapshot(ModalStore.state);
+ const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state);
+ const requestEvent = data?.requestEvent;
+ const session = data?.requestSession;
+ const isLinkMode = session?.transportType === 'link_mode';
+
+ const [isLoadingApprove, setIsLoadingApprove] = useState(false);
+ const [isLoadingReject, setIsLoadingReject] = useState(false);
+
+ const { validation, isScam } = currentRequestVerifyContext?.verified || {};
+
+ const { topic, params } = requestEvent!;
+ const { request } = params;
+ const peerMetadata = session?.peer?.metadata as SignClientTypes.Metadata;
+
+ const displayAddresses = useMemo(() => {
+ const intentions = request.params?.intentions as string[] | undefined;
+ const isOrdinal = intentions && intentions[0] === 'ordinal';
+ const list = bitcoinAddresses
+ ? isOrdinal
+ ? [bitcoinAddresses[1]]
+ : [bitcoinAddresses[0]]
+ : [];
+ return list.join('\n');
+ }, [request.params]);
+
+ const onApprove = useCallback(async () => {
+ if (requestEvent) {
+ setIsLoadingApprove(true);
+ try {
+ const response = await approveBitcoinRequest(requestEvent);
+ await walletKit.respondSessionRequest({ topic, response });
+ haptics.requestResponse();
+ // The handler returns a JSON-RPC error response (rather than throwing)
+ // on failure. Surface it so the wallet doesn't look successful.
+ const errorMessage =
+ 'error' in response ? response.error.message : undefined;
+ if (errorMessage) {
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t share addresses',
+ text2: errorMessage,
+ });
+ }
+ handleRedirect({
+ peerRedirect: peerMetadata?.redirect,
+ isLinkMode: isLinkMode,
+ error: errorMessage,
+ });
+ } catch (e) {
+ LogStore.error(
+ (e as Error).message,
+ 'SessionBitcoinGetAddressesModal',
+ 'onApprove',
+ );
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t share addresses',
+ text2: (e as Error).message,
+ });
+ } finally {
+ setIsLoadingApprove(false);
+ ModalStore.close();
+ }
+ }
+ }, [requestEvent, peerMetadata, topic, isLinkMode]);
+
+ const onReject = useCallback(async () => {
+ if (requestEvent) {
+ setIsLoadingReject(true);
+ try {
+ const response = rejectBitcoinRequest(requestEvent);
+ await walletKit.respondSessionRequest({ topic, response });
+ haptics.requestResponse();
+ handleRedirect({
+ peerRedirect: peerMetadata?.redirect,
+ isLinkMode: isLinkMode,
+ error: 'User rejected Bitcoin addresses request',
+ });
+ } catch (e) {
+ LogStore.error(
+ (e as Error).message,
+ 'SessionBitcoinGetAddressesModal',
+ 'onReject',
+ );
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t reject request',
+ text2: (e as Error).message,
+ });
+ } finally {
+ setIsLoadingReject(false);
+ ModalStore.close();
+ }
+ }
+ }, [requestEvent, topic, peerMetadata, isLinkMode]);
+
+ if (!requestEvent || !session) {
+ return (
+
+ Missing request data
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ marginVertical: Spacing[2],
+ paddingHorizontal: Spacing[4],
+ rowGap: Spacing[2],
+ },
+});
diff --git a/wallets/rn_cli_wallet/src/modals/SessionBitcoinSendTransactionModal.tsx b/wallets/rn_cli_wallet/src/modals/SessionBitcoinSendTransactionModal.tsx
new file mode 100644
index 00000000..b13937b6
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/modals/SessionBitcoinSendTransactionModal.tsx
@@ -0,0 +1,166 @@
+import { useSnapshot } from 'valtio';
+import { useCallback, useMemo, useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { SignClientTypes } from '@walletconnect/types';
+import { showToast } from '@/utils/ToastUtil';
+
+import { Message } from '@/components/Modal/Message';
+import { AppInfoCard } from '@/components/AppInfoCard';
+import { NetworkInfoCard } from '@/components/NetworkInfoCard';
+
+import { walletKit } from '@/utils/WalletKitUtil';
+import { handleRedirect } from '@/utils/LinkingUtils';
+import LogStore from '@/store/LogStore';
+import ModalStore from '@/store/ModalStore';
+import SettingsStore from '@/store/SettingsStore';
+import { RequestModal } from './RequestModal';
+import {
+ approveBitcoinRequest,
+ rejectBitcoinRequest,
+} from '@/utils/BitcoinRequestHandlerUtil';
+import { BIP122_SIGNING_METHODS } from '@/constants/Bitcoin';
+import { Text } from '@/components/Text';
+import { Spacing } from '@/utils/ThemeUtil';
+import { haptics } from '@/utils/haptics';
+
+export default function SessionBitcoinSendTransactionModal() {
+ const { data } = useSnapshot(ModalStore.state);
+ const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state);
+ const requestEvent = data?.requestEvent;
+ const session = data?.requestSession;
+ const isLinkMode = session?.transportType === 'link_mode';
+
+ const [isLoadingApprove, setIsLoadingApprove] = useState(false);
+ const [isLoadingReject, setIsLoadingReject] = useState(false);
+
+ const { validation, isScam } = currentRequestVerifyContext?.verified || {};
+
+ const { topic, params } = requestEvent!;
+ const { request } = params;
+ const peerMetadata = session?.peer?.metadata as SignClientTypes.Metadata;
+
+ const displayPayload = useMemo(() => {
+ try {
+ return JSON.stringify(request.params, null, 2);
+ } catch {
+ return String(request.params);
+ }
+ }, [request.params]);
+
+ const intention =
+ request.method === BIP122_SIGNING_METHODS.BIP122_SIGN_PSBT
+ ? 'Sign a transaction for'
+ : 'Sign & send a transaction for';
+
+ const onApprove = useCallback(async () => {
+ if (requestEvent) {
+ setIsLoadingApprove(true);
+ try {
+ const response = await approveBitcoinRequest(requestEvent);
+ await walletKit.respondSessionRequest({ topic, response });
+ haptics.requestResponse();
+ // The handler returns a JSON-RPC error response (rather than throwing)
+ // when the transfer/PSBT fails, e.g. no UTXOs. Surface it so the wallet
+ // doesn't look like it succeeded.
+ const errorMessage =
+ 'error' in response ? response.error.message : undefined;
+ if (errorMessage) {
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t sign transaction',
+ text2: errorMessage,
+ });
+ }
+ handleRedirect({
+ peerRedirect: peerMetadata?.redirect,
+ isLinkMode: isLinkMode,
+ error: errorMessage,
+ });
+ } catch (e) {
+ LogStore.error(
+ (e as Error).message,
+ 'SessionBitcoinSendTransactionModal',
+ 'onApprove',
+ );
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t sign transaction',
+ text2: (e as Error).message,
+ });
+ } finally {
+ setIsLoadingApprove(false);
+ ModalStore.close();
+ }
+ }
+ }, [requestEvent, peerMetadata, topic, isLinkMode]);
+
+ const onReject = useCallback(async () => {
+ if (requestEvent) {
+ setIsLoadingReject(true);
+ try {
+ const response = rejectBitcoinRequest(requestEvent);
+ await walletKit.respondSessionRequest({ topic, response });
+ haptics.requestResponse();
+ handleRedirect({
+ peerRedirect: peerMetadata?.redirect,
+ isLinkMode: isLinkMode,
+ error: 'User rejected Bitcoin transaction request',
+ });
+ } catch (e) {
+ LogStore.error(
+ (e as Error).message,
+ 'SessionBitcoinSendTransactionModal',
+ 'onReject',
+ );
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t reject request',
+ text2: (e as Error).message,
+ });
+ } finally {
+ setIsLoadingReject(false);
+ ModalStore.close();
+ }
+ }
+ }, [requestEvent, topic, peerMetadata, isLinkMode]);
+
+ if (!requestEvent || !session) {
+ return (
+
+ Missing request data
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ marginVertical: Spacing[2],
+ paddingHorizontal: Spacing[4],
+ rowGap: Spacing[2],
+ },
+});
diff --git a/wallets/rn_cli_wallet/src/modals/SessionBitcoinSignMessageModal.tsx b/wallets/rn_cli_wallet/src/modals/SessionBitcoinSignMessageModal.tsx
new file mode 100644
index 00000000..fd6c8e4d
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/modals/SessionBitcoinSignMessageModal.tsx
@@ -0,0 +1,153 @@
+import { useSnapshot } from 'valtio';
+import { useCallback, useState } from 'react';
+import { View, StyleSheet } from 'react-native';
+import { SignClientTypes } from '@walletconnect/types';
+import { showToast } from '@/utils/ToastUtil';
+
+import { Message } from '@/components/Modal/Message';
+import { AppInfoCard } from '@/components/AppInfoCard';
+import { NetworkInfoCard } from '@/components/NetworkInfoCard';
+
+import { walletKit } from '@/utils/WalletKitUtil';
+import { handleRedirect } from '@/utils/LinkingUtils';
+import LogStore from '@/store/LogStore';
+import ModalStore from '@/store/ModalStore';
+import SettingsStore from '@/store/SettingsStore';
+import { RequestModal } from './RequestModal';
+import {
+ approveBitcoinRequest,
+ rejectBitcoinRequest,
+} from '@/utils/BitcoinRequestHandlerUtil';
+import { Text } from '@/components/Text';
+import { Spacing } from '@/utils/ThemeUtil';
+import { haptics } from '@/utils/haptics';
+
+export default function SessionBitcoinSignMessageModal() {
+ const { data } = useSnapshot(ModalStore.state);
+ const { currentRequestVerifyContext } = useSnapshot(SettingsStore.state);
+ const requestEvent = data?.requestEvent;
+ const session = data?.requestSession;
+ const isLinkMode = session?.transportType === 'link_mode';
+
+ const [isLoadingApprove, setIsLoadingApprove] = useState(false);
+ const [isLoadingReject, setIsLoadingReject] = useState(false);
+
+ const { validation, isScam } = currentRequestVerifyContext?.verified || {};
+
+ const { topic, params } = requestEvent!;
+ const { request } = params;
+ const peerMetadata = session?.peer?.metadata as SignClientTypes.Metadata;
+
+ const displayMessage = request.params?.message || '';
+
+ const onApprove = useCallback(async () => {
+ if (requestEvent) {
+ setIsLoadingApprove(true);
+ try {
+ const response = await approveBitcoinRequest(requestEvent);
+ await walletKit.respondSessionRequest({ topic, response });
+ haptics.requestResponse();
+ // The handler returns a JSON-RPC error response (rather than throwing)
+ // when signing fails. Surface it so the wallet doesn't look successful.
+ const errorMessage =
+ 'error' in response ? response.error.message : undefined;
+ if (errorMessage) {
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t sign message',
+ text2: errorMessage,
+ });
+ }
+ handleRedirect({
+ peerRedirect: peerMetadata?.redirect,
+ isLinkMode: isLinkMode,
+ error: errorMessage,
+ });
+ } catch (e) {
+ LogStore.error(
+ (e as Error).message,
+ 'SessionBitcoinSignMessageModal',
+ 'onApprove',
+ );
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t sign message',
+ text2: (e as Error).message,
+ });
+ } finally {
+ setIsLoadingApprove(false);
+ ModalStore.close();
+ }
+ }
+ }, [requestEvent, peerMetadata, topic, isLinkMode]);
+
+ const onReject = useCallback(async () => {
+ if (requestEvent) {
+ setIsLoadingReject(true);
+ try {
+ const response = rejectBitcoinRequest(requestEvent);
+ await walletKit.respondSessionRequest({ topic, response });
+ haptics.requestResponse();
+ handleRedirect({
+ peerRedirect: peerMetadata?.redirect,
+ isLinkMode: isLinkMode,
+ error: 'User rejected Bitcoin message request',
+ });
+ } catch (e) {
+ LogStore.error(
+ (e as Error).message,
+ 'SessionBitcoinSignMessageModal',
+ 'onReject',
+ );
+ showToast({
+ type: 'error',
+ text1: 'Couldn’t reject request',
+ text2: (e as Error).message,
+ });
+ } finally {
+ setIsLoadingReject(false);
+ ModalStore.close();
+ }
+ }
+ }, [requestEvent, topic, peerMetadata, isLinkMode]);
+
+ if (!requestEvent || !session) {
+ return (
+
+ Missing request data
+
+ );
+ }
+
+ return (
+
+
+
+
+
+
+
+ );
+}
+
+const styles = StyleSheet.create({
+ container: {
+ width: '100%',
+ marginVertical: Spacing[2],
+ paddingHorizontal: Spacing[4],
+ rowGap: Spacing[2],
+ },
+});
diff --git a/wallets/rn_cli_wallet/src/modals/SessionProposalModal.tsx b/wallets/rn_cli_wallet/src/modals/SessionProposalModal.tsx
index bf12cdd7..63dfb4c0 100644
--- a/wallets/rn_cli_wallet/src/modals/SessionProposalModal.tsx
+++ b/wallets/rn_cli_wallet/src/modals/SessionProposalModal.tsx
@@ -32,6 +32,12 @@ import {
SOLANA_EVENTS,
SOLANA_SIGNING_METHODS,
} from '@/constants/Solana';
+import { bitcoinAddresses } from '@/utils/BitcoinWalletUtil';
+import {
+ BIP122_CHAINS,
+ BIP122_EVENTS,
+ BIP122_SIGNING_METHODS,
+} from '@/constants/Bitcoin';
import { AccordionCard } from '@/components/AccordionCard';
import { AppInfoCard } from '@/components/AppInfoCard';
import { NetworkSelector } from '@/components/NetworkSelector';
@@ -90,6 +96,10 @@ export default function SessionProposalModal() {
const solanaMethods = Object.values(SOLANA_SIGNING_METHODS);
const solanaEvents = Object.values(SOLANA_EVENTS);
+ const bip122Chains = Object.keys(BIP122_CHAINS);
+ const bip122Methods = Object.values(BIP122_SIGNING_METHODS);
+ const bip122Events = Object.values(BIP122_EVENTS);
+
return {
eip155: {
chains: eip155Chains,
@@ -135,6 +145,17 @@ export default function SessionProposalModal() {
? solanaChains.map(chain => `${chain}:${solanaAddresses[0]}`)
: [],
},
+ bip122: {
+ chains: bip122Chains,
+ methods: bip122Methods,
+ events: bip122Events,
+ // Expose both the payment (P2WPKH) and ordinals (P2TR) addresses.
+ accounts: bitcoinAddresses?.[0]
+ ? bip122Chains.flatMap(chain =>
+ bitcoinAddresses.map(address => `${chain}:${address}`),
+ )
+ : [],
+ },
};
}, []);
diff --git a/wallets/rn_cli_wallet/src/screens/SecretPhrase/index.tsx b/wallets/rn_cli_wallet/src/screens/SecretPhrase/index.tsx
index 52866c97..9b9753de 100644
--- a/wallets/rn_cli_wallet/src/screens/SecretPhrase/index.tsx
+++ b/wallets/rn_cli_wallet/src/screens/SecretPhrase/index.tsx
@@ -121,6 +121,7 @@ export default function SecretPhrase() {
tronWallet,
cantonWallet,
solanaWallet,
+ bitcoinWallet,
} = useSnapshot(SettingsStore.state);
const Theme = useTheme();
@@ -145,6 +146,9 @@ export default function SecretPhrase() {
const solanaMnemonic = solanaWallet?.getMnemonic?.() || null;
const solanaBase58SecretKey = solanaWallet?.getSecretKey?.() ?? null;
+ // Get Bitcoin mnemonic
+ const bitcoinMnemonic = bitcoinWallet?.getMnemonic?.() ?? null;
+
return (
)}
+
+
);
}
diff --git a/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx b/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx
index 818609dc..ce5d139a 100644
--- a/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx
+++ b/wallets/rn_cli_wallet/src/screens/Wallets/components/TokenBalanceCard.tsx
@@ -101,7 +101,9 @@ export const TokenBalanceCard = React.memo(function TokenBalanceCard({
- {formatBalance(balance.quantity.numeric, balance.symbol)}
+ {balance.balanceUnavailable
+ ? `~ ${balance.symbol}`
+ : formatBalance(balance.quantity.numeric, balance.symbol)}
{truncateAddress(walletAddress)}
diff --git a/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx b/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx
index 1753450d..3e410799 100644
--- a/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx
+++ b/wallets/rn_cli_wallet/src/screens/Wallets/index.tsx
@@ -28,13 +28,22 @@ function getAddressForChain(
if (chainId.startsWith('solana:')) {
return addresses.solanaAddress || '';
}
+ if (chainId.startsWith('bip122:')) {
+ return addresses.bitcoinAddress || '';
+ }
// Default to EIP155 address for all EVM chains
return addresses.eip155Address || '';
}
export default function Wallets() {
- const { eip155Address, tonAddress, tronAddress, suiAddress, solanaAddress } =
- useSnapshot(SettingsStore.state);
+ const {
+ eip155Address,
+ tonAddress,
+ tronAddress,
+ suiAddress,
+ solanaAddress,
+ bitcoinAddress,
+ } = useSnapshot(SettingsStore.state);
const { balances, isLoading } = useSnapshot(WalletStore.state);
const Theme = useTheme();
@@ -45,8 +54,16 @@ export default function Wallets() {
tronAddress,
suiAddress,
solanaAddress,
+ bitcoinAddress,
}),
- [eip155Address, tonAddress, tronAddress, suiAddress, solanaAddress],
+ [
+ eip155Address,
+ tonAddress,
+ tronAddress,
+ suiAddress,
+ solanaAddress,
+ bitcoinAddress,
+ ],
);
const fetchBalances = useCallback(() => {
@@ -55,7 +72,8 @@ export default function Wallets() {
addresses.tonAddress ||
addresses.tronAddress ||
addresses.suiAddress ||
- addresses.solanaAddress
+ addresses.solanaAddress ||
+ addresses.bitcoinAddress
) {
WalletStore.fetchBalances(addresses);
}
diff --git a/wallets/rn_cli_wallet/src/services/BalanceService.ts b/wallets/rn_cli_wallet/src/services/BalanceService.ts
index 86c642ee..e72b31d5 100644
--- a/wallets/rn_cli_wallet/src/services/BalanceService.ts
+++ b/wallets/rn_cli_wallet/src/services/BalanceService.ts
@@ -5,6 +5,19 @@ import LogStore, { serializeError } from '@/store/LogStore';
const BALANCE_API_PATH = '/v1/account';
+// Thrown when the balance API returns a non-OK HTTP status. Carries the status
+// so callers can branch on it (e.g. 400 = chain/address unsupported) without
+// parsing the error message. Failures already logged here at the appropriate
+// level, so the aggregator doesn't re-log them.
+class BalanceFetchError extends Error {
+ status: number;
+ constructor(chainId: string, status: number) {
+ super(`Failed to fetch balance for ${chainId}: ${status}`);
+ this.name = 'BalanceFetchError';
+ this.status = status;
+ }
+}
+
/**
* Fetches token balances for a single chain
* @param address - Wallet address
@@ -64,13 +77,25 @@ async function fetchBalanceForChain(
if (!response.ok) {
const errorText = await response.text();
- LogStore.error('Error response', 'BalanceService', 'fetchBalanceForChain', {
- status: response.status,
- body: errorText,
- });
- throw new Error(
- `Failed to fetch balance for ${chainId}: ${response.status}`,
- );
+ // A 400 means the balance API doesn't support this chain/address format
+ // (e.g. bip122/Bitcoin, sui) — this is expected and handled by the caller
+ // (fails silently per-chain), so log it as a warning rather than an error.
+ if (response.status === 400) {
+ LogStore.warn(
+ 'Chain not supported by balance API',
+ 'BalanceService',
+ 'fetchBalanceForChain',
+ { chainId, status: response.status, body: errorText },
+ );
+ } else {
+ LogStore.error(
+ 'Error response',
+ 'BalanceService',
+ 'fetchBalanceForChain',
+ { chainId, status: response.status, body: errorText },
+ );
+ }
+ throw new BalanceFetchError(chainId, response.status);
}
const data: BalanceResponse = await response.json();
@@ -130,14 +155,15 @@ export async function fetchBalancesForChains(
},
);
allBalances.push(...result.value);
- } else {
+ } else if (!(result.reason instanceof BalanceFetchError)) {
+ // HTTP failures are already logged (at the right level) by
+ // fetchBalanceForChain; only surface unexpected errors here, e.g. a
+ // network failure that threw before a response was received.
LogStore.error(
`Chain ${chainId} failed`,
'BalanceService',
'fetchBalancesForChains',
- {
- error: serializeError(result.reason),
- },
+ { error: serializeError(result.reason) },
);
}
});
diff --git a/wallets/rn_cli_wallet/src/store/ModalStore.ts b/wallets/rn_cli_wallet/src/store/ModalStore.ts
index 2d9a2f23..58f0cc23 100644
--- a/wallets/rn_cli_wallet/src/store/ModalStore.ts
+++ b/wallets/rn_cli_wallet/src/store/ModalStore.ts
@@ -36,6 +36,9 @@ interface State {
| 'SessionSignCantonModal'
| 'SessionSolanaSignMessageModal'
| 'SessionSolanaSignTransactionModal'
+ | 'SessionBitcoinSignMessageModal'
+ | 'SessionBitcoinSendTransactionModal'
+ | 'SessionBitcoinGetAddressesModal'
| 'PaymentOptionsModal'
| 'ImportWalletModal'
| 'SessionDetailModal'
diff --git a/wallets/rn_cli_wallet/src/store/SettingsStore.ts b/wallets/rn_cli_wallet/src/store/SettingsStore.ts
index b90b8869..12e28253 100644
--- a/wallets/rn_cli_wallet/src/store/SettingsStore.ts
+++ b/wallets/rn_cli_wallet/src/store/SettingsStore.ts
@@ -9,6 +9,7 @@ import TonLib from '../lib/TonLib';
import TronLib from '../lib/TronLib';
import CantonLib from '../lib/CantonLib';
import SolanaLib from '../lib/SolanaLib';
+import BitcoinLib from '../lib/BitcoinLib';
import { MMKV } from 'react-native-mmkv';
function getInitialThemeMode(): 'light' | 'dark' {
@@ -41,6 +42,8 @@ interface State {
cantonWallet: CantonLib | null;
solanaAddress: string;
solanaWallet: SolanaLib | null;
+ bitcoinAddress: string;
+ bitcoinWallet: BitcoinLib | null;
relayerRegionURL: string;
activeChainId: string;
currentRequestVerifyContext?: Verify.Context;
@@ -75,6 +78,8 @@ const state = proxy({
cantonWallet: null,
solanaAddress: '',
solanaWallet: null,
+ bitcoinAddress: '',
+ bitcoinWallet: null,
relayerRegionURL: '',
sessions: [],
wallet: null,
@@ -184,6 +189,14 @@ const SettingsStore = {
state.solanaWallet = ref(solanaWallet);
},
+ setBitcoinAddress(bitcoinAddress: string) {
+ state.bitcoinAddress = bitcoinAddress;
+ },
+
+ setBitcoinWallet(bitcoinWallet: BitcoinLib) {
+ state.bitcoinWallet = ref(bitcoinWallet);
+ },
+
setThemeMode(value: 'light' | 'dark') {
state.themeMode = value;
Appearance.setColorScheme?.(value);
diff --git a/wallets/rn_cli_wallet/src/store/WalletStore.ts b/wallets/rn_cli_wallet/src/store/WalletStore.ts
index 8db04bc5..604a8f62 100644
--- a/wallets/rn_cli_wallet/src/store/WalletStore.ts
+++ b/wallets/rn_cli_wallet/src/store/WalletStore.ts
@@ -6,6 +6,7 @@ import { fetchERC20Balances } from '@/services/ERC20BalanceService';
import { EIP155_CHAINS } from '@/constants/Eip155';
import { isSpamToken } from '@/utils/SpamFilter';
import { SOLANA_MAINNET_CAIP2 } from '@/constants/Solana';
+import { BIP122_MAINNET_CAIP2 } from '@/constants/Bitcoin';
import LogStore, { serializeError } from '@/store/LogStore';
const mmkv = new MMKV();
@@ -16,6 +17,7 @@ const TON_SUPPORTED_CHAINS = ['ton:-239'];
const TRON_SUPPORTED_CHAINS = ['tron:0x2b6653dc'];
const SUI_SUPPORTED_CHAINS = ['sui:mainnet'];
const SOLANA_SUPPORTED_CHAINS = [SOLANA_MAINNET_CAIP2];
+const BITCOIN_SUPPORTED_CHAINS = [BIP122_MAINNET_CAIP2];
export interface WalletAddresses {
eip155Address?: string;
@@ -23,6 +25,7 @@ export interface WalletAddresses {
tronAddress?: string;
suiAddress?: string;
solanaAddress?: string;
+ bitcoinAddress?: string;
}
interface WalletState {
@@ -78,6 +81,7 @@ const MAINNET_NATIVE_TOKENS = {
'tron:0x2b6653dc': { name: 'TRON', symbol: 'TRX', decimals: '6' },
'sui:mainnet': { name: 'Sui', symbol: 'SUI', decimals: '9' },
[SOLANA_MAINNET_CAIP2]: { name: 'Solana', symbol: 'SOL', decimals: '9' },
+ [BIP122_MAINNET_CAIP2]: { name: 'Bitcoin', symbol: 'BTC', decimals: '8' },
};
/**
@@ -85,9 +89,21 @@ const MAINNET_NATIVE_TOKENS = {
* 1. Filters out tokens with 0 value (except mainnet native tokens)
* 2. Ensures mainnet native tokens are always present for address visibility
*/
+// Per-namespace flag: true when that chain's balance request failed, so the
+// synthesized native row should read "~" (unknown) instead of a confirmed 0.
+interface BalanceUnavailableFlags {
+ eip155?: boolean;
+ ton?: boolean;
+ tron?: boolean;
+ sui?: boolean;
+ solana?: boolean;
+ bitcoin?: boolean;
+}
+
function processBalances(
apiBalances: TokenBalance[],
addresses: WalletAddresses,
+ unavailable: BalanceUnavailableFlags = {},
): TokenBalance[] {
const mainnetChainIds = Object.keys(MAINNET_NATIVE_TOKENS);
@@ -118,6 +134,7 @@ function processBalances(
price: 0,
quantity: { decimals: '18', numeric: '0' },
iconUrl: undefined,
+ balanceUnavailable: unavailable.eip155,
});
}
}
@@ -136,6 +153,7 @@ function processBalances(
price: 0,
quantity: { decimals: '9', numeric: '0' },
iconUrl: undefined,
+ balanceUnavailable: unavailable.ton,
});
}
}
@@ -154,6 +172,7 @@ function processBalances(
price: 0,
quantity: { decimals: '6', numeric: '0' },
iconUrl: undefined,
+ balanceUnavailable: unavailable.tron,
});
}
}
@@ -172,6 +191,7 @@ function processBalances(
price: 0,
quantity: { decimals: '9', numeric: '0' },
iconUrl: undefined,
+ balanceUnavailable: unavailable.sui,
});
}
}
@@ -190,6 +210,26 @@ function processBalances(
price: 0,
quantity: { decimals: '9', numeric: '0' },
iconUrl: undefined,
+ balanceUnavailable: unavailable.solana,
+ });
+ }
+ }
+
+ // BTC on mainnet
+ if (addresses.bitcoinAddress) {
+ const hasBtcMainnet = result.some(
+ b => b.chainId === BIP122_MAINNET_CAIP2 && !b.address,
+ );
+ if (!hasBtcMainnet) {
+ result.push({
+ name: 'Bitcoin',
+ symbol: 'BTC',
+ chainId: BIP122_MAINNET_CAIP2,
+ value: 0,
+ price: 0,
+ quantity: { decimals: '8', numeric: '0' },
+ iconUrl: undefined,
+ balanceUnavailable: unavailable.bitcoin,
});
}
}
@@ -220,7 +260,8 @@ const WalletStore = {
!addresses.tonAddress &&
!addresses.tronAddress &&
!addresses.suiAddress &&
- !addresses.solanaAddress
+ !addresses.solanaAddress &&
+ !addresses.bitcoinAddress
) {
return;
}
@@ -237,6 +278,7 @@ const WalletStore = {
tronResult,
suiResult,
solanaResult,
+ bitcoinResult,
erc20Balances,
] = await Promise.all([
// EIP155 balances (or empty result if no address)
@@ -277,6 +319,16 @@ const WalletStore = {
balances: [] as TokenBalance[],
anySuccess: false,
}),
+ // Bitcoin balances (or empty result if no address)
+ addresses.bitcoinAddress
+ ? fetchBalancesForChains(
+ addresses.bitcoinAddress,
+ BITCOIN_SUPPORTED_CHAINS,
+ )
+ : Promise.resolve({
+ balances: [] as TokenBalance[],
+ anySuccess: false,
+ }),
// On-chain ERC-20 balances (EURC etc.)
addresses.eip155Address
? fetchERC20Balances(addresses.eip155Address)
@@ -289,7 +341,8 @@ const WalletStore = {
tonResult.anySuccess ||
tronResult.anySuccess ||
suiResult.anySuccess ||
- solanaResult.anySuccess;
+ solanaResult.anySuccess ||
+ bitcoinResult.anySuccess;
if (!anySuccess) {
return;
@@ -302,6 +355,7 @@ const WalletStore = {
...tronResult.balances,
...suiResult.balances,
...solanaResult.balances,
+ ...bitcoinResult.balances,
];
// Merge on-chain ERC-20 balances (only non-zero) unless the API already returned them
@@ -334,8 +388,20 @@ const WalletStore = {
}
}
+ // A group that has an address but didn't return any successful response
+ // means the balance is unknown (e.g. the API rejects bip122/sui) — mark
+ // it so the synthesized native row shows "~" instead of a confirmed 0.
+ const unavailable = {
+ eip155: !!addresses.eip155Address && !eip155Result.anySuccess,
+ ton: !!addresses.tonAddress && !tonResult.anySuccess,
+ tron: !!addresses.tronAddress && !tronResult.anySuccess,
+ sui: !!addresses.suiAddress && !suiResult.anySuccess,
+ solana: !!addresses.solanaAddress && !solanaResult.anySuccess,
+ bitcoin: !!addresses.bitcoinAddress && !bitcoinResult.anySuccess,
+ };
+
// Filter 0-balance tokens and ensure mainnet natives are present
- const allBalances = processBalances(apiBalances, addresses);
+ const allBalances = processBalances(apiBalances, addresses, unavailable);
// Sort: tokens with value first, then by chain
allBalances.sort((a, b) => {
diff --git a/wallets/rn_cli_wallet/src/utils/BalanceTypes.ts b/wallets/rn_cli_wallet/src/utils/BalanceTypes.ts
index 8b3d2c97..4943442e 100644
--- a/wallets/rn_cli_wallet/src/utils/BalanceTypes.ts
+++ b/wallets/rn_cli_wallet/src/utils/BalanceTypes.ts
@@ -10,6 +10,9 @@ export interface TokenBalance {
numeric: string;
};
iconUrl?: string;
+ // True when the balance API request for this chain failed, so the amount is
+ // unknown (shown as "~") rather than a confirmed 0.
+ balanceUnavailable?: boolean;
}
export interface BalanceResponse {
diff --git a/wallets/rn_cli_wallet/src/utils/BitcoinRequestHandlerUtil.ts b/wallets/rn_cli_wallet/src/utils/BitcoinRequestHandlerUtil.ts
new file mode 100644
index 00000000..93472871
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/utils/BitcoinRequestHandlerUtil.ts
@@ -0,0 +1,104 @@
+import { formatJsonRpcError, formatJsonRpcResult } from '@json-rpc-tools/utils';
+import { SignClientTypes } from '@walletconnect/types';
+import { getSdkError } from '@walletconnect/utils';
+
+import LogStore, { serializeError } from '@/store/LogStore';
+import { getWallet } from './BitcoinWalletUtil';
+import { BIP122_SIGNING_METHODS, IBip122ChainId } from '@/constants/Bitcoin';
+
+type RequestEventArgs = Omit<
+ SignClientTypes.EventArguments['session_request'],
+ 'verifyContext'
+>;
+
+export async function approveBitcoinRequest(requestEvent: RequestEventArgs) {
+ const { params, id } = requestEvent;
+ const { chainId, request } = params;
+ const bip122ChainId = chainId as IBip122ChainId;
+
+ const wallet = await getWallet();
+ if (!wallet) {
+ LogStore.error(
+ 'Bitcoin wallet not initialized',
+ 'BitcoinRequestHandler',
+ 'approveBitcoinRequest',
+ );
+ return formatJsonRpcError(id, 'Bitcoin wallet not initialized');
+ }
+
+ const account = wallet.getAddress(bip122ChainId);
+
+ switch (request.method) {
+ case BIP122_SIGNING_METHODS.BIP122_SIGN_MESSAGE:
+ try {
+ const result = await wallet.signMessage({
+ message: request.params.message,
+ address: request.params.address || account,
+ protocol: request.params.protocol,
+ chainId: bip122ChainId,
+ });
+ return formatJsonRpcResult(id, result);
+ } catch (error: any) {
+ LogStore.error(error.message, 'BitcoinRequestHandler', 'signMessage', {
+ error: serializeError(error),
+ });
+ return formatJsonRpcError(id, error.message);
+ }
+ case BIP122_SIGNING_METHODS.BIP122_GET_ACCOUNT_ADDRESSES:
+ try {
+ const addresses = wallet.getAddresses(
+ bip122ChainId,
+ request.params?.intentions,
+ );
+ return formatJsonRpcResult(id, Array.from(addresses.values()));
+ } catch (error: any) {
+ LogStore.error(
+ error.message,
+ 'BitcoinRequestHandler',
+ 'getAccountAddresses',
+ { error: serializeError(error) },
+ );
+ return formatJsonRpcError(id, error.message);
+ }
+ case BIP122_SIGNING_METHODS.BIP122_SEND_TRANSACTION:
+ try {
+ const txid = await wallet.sendTransfer({
+ account: request.params.account || account,
+ recipientAddress: request.params.recipientAddress,
+ amount: request.params.amount,
+ changeAddress: request.params.changeAddress,
+ memo: request.params.memo,
+ chainId: bip122ChainId,
+ });
+ return formatJsonRpcResult(id, { txid });
+ } catch (error: any) {
+ LogStore.error(error.message, 'BitcoinRequestHandler', 'sendTransfer', {
+ error: serializeError(error),
+ });
+ return formatJsonRpcError(id, error.message);
+ }
+ case BIP122_SIGNING_METHODS.BIP122_SIGN_PSBT:
+ try {
+ const result = await wallet.signPsbt({
+ account: request.params.account || account,
+ psbt: request.params.psbt,
+ signInputs: request.params.signInputs,
+ broadcast: request.params.broadcast ?? false,
+ chainId: bip122ChainId,
+ });
+ return formatJsonRpcResult(id, result);
+ } catch (error: any) {
+ LogStore.error(error.message, 'BitcoinRequestHandler', 'signPsbt', {
+ error: serializeError(error),
+ });
+ return formatJsonRpcError(id, error.message);
+ }
+ default:
+ throw new Error(getSdkError('INVALID_METHOD').message);
+ }
+}
+
+export function rejectBitcoinRequest(request: RequestEventArgs) {
+ const { id } = request;
+ return formatJsonRpcError(id, getSdkError('USER_REJECTED').message);
+}
diff --git a/wallets/rn_cli_wallet/src/utils/BitcoinWalletUtil.ts b/wallets/rn_cli_wallet/src/utils/BitcoinWalletUtil.ts
new file mode 100644
index 00000000..66000a1c
--- /dev/null
+++ b/wallets/rn_cli_wallet/src/utils/BitcoinWalletUtil.ts
@@ -0,0 +1,85 @@
+import * as bip39 from 'bip39';
+
+import BitcoinLib from '../lib/BitcoinLib';
+import { storage } from './storage';
+import SettingsStore from '@/store/SettingsStore';
+import { BIP122_MAINNET_CAIP2 } from '@/constants/Bitcoin';
+
+const BITCOIN_MNEMONIC_KEY = 'BITCOIN_MNEMONIC_1';
+
+export let wallet1: BitcoinLib;
+// [0] = mainnet payment (P2WPKH) address, [1] = mainnet ordinals (P2TR) address
+export let bitcoinAddresses: string[];
+
+function isMnemonic(input: string): boolean {
+ const words = input.split(/\s+/).filter(Boolean);
+ if (![12, 15, 18, 21, 24].includes(words.length)) {
+ return false;
+ }
+ return bip39.validateMnemonic(input);
+}
+
+/**
+ * Utilities
+ */
+export async function createOrRestoreBitcoinWallet() {
+ const mnemonic1 = await storage.getItem(BITCOIN_MNEMONIC_KEY);
+
+ if (mnemonic1) {
+ wallet1 = await BitcoinLib.init({ mnemonic: mnemonic1 });
+ } else {
+ wallet1 = await BitcoinLib.init({});
+ // Don't store private keys in local storage in a production project!
+ await storage.setItem(BITCOIN_MNEMONIC_KEY, wallet1.getMnemonic());
+ }
+
+ bitcoinAddresses = [
+ wallet1.getAddress(BIP122_MAINNET_CAIP2),
+ wallet1.getOrdinalsAddress(BIP122_MAINNET_CAIP2),
+ ];
+
+ return {
+ bitcoinWallet: wallet1,
+ bitcoinAddress: bitcoinAddresses[0],
+ bitcoinAddresses,
+ };
+}
+
+export const getWallet = async () => {
+ return wallet1;
+};
+
+export async function loadBitcoinWallet(input: string): Promise<{
+ address: string;
+ wallet: BitcoinLib;
+}> {
+ const trimmedInput = input.trim();
+
+ if (!isMnemonic(trimmedInput)) {
+ throw new Error('Invalid input. Provide a 12-24 word BIP39 mnemonic.');
+ }
+
+ const newWallet = await BitcoinLib.init({ mnemonic: trimmedInput });
+ await storage.setItem(BITCOIN_MNEMONIC_KEY, trimmedInput);
+
+ const newAddress = newWallet.getAddress(BIP122_MAINNET_CAIP2);
+
+ // Update module-level exports
+ wallet1 = newWallet;
+ bitcoinAddresses = [
+ newAddress,
+ newWallet.getOrdinalsAddress(BIP122_MAINNET_CAIP2),
+ ];
+
+ if (__DEV__) {
+ console.warn(
+ '[SECURITY] Bitcoin key material stored unencrypted. Use secure enclave in production.',
+ );
+ }
+
+ // Update store
+ SettingsStore.setBitcoinAddress(newAddress);
+ SettingsStore.setBitcoinWallet(newWallet);
+
+ return { address: newAddress, wallet: newWallet };
+}
diff --git a/wallets/rn_cli_wallet/src/utils/PresetsUtil.ts b/wallets/rn_cli_wallet/src/utils/PresetsUtil.ts
index a7dc9dd3..6036dc6e 100644
--- a/wallets/rn_cli_wallet/src/utils/PresetsUtil.ts
+++ b/wallets/rn_cli_wallet/src/utils/PresetsUtil.ts
@@ -5,6 +5,7 @@ import { EIP155_CHAINS, EIP155_NETWORK_IMAGES } from '@/constants/Eip155';
import { TRON_CHAINS, TRON_NETWORKS_IMAGES } from '@/constants/Tron';
import { CANTON_CHAINS, CANTON_NETWORKS_IMAGES } from '@/constants/Canton';
import { SOLANA_CHAINS, SOLANA_NETWORKS_IMAGES } from '@/constants/Solana';
+import { BIP122_CHAINS, BIP122_NETWORKS_IMAGES } from '@/constants/Bitcoin';
const NetworkImages: Record = {
...EIP155_NETWORK_IMAGES,
@@ -13,6 +14,7 @@ const NetworkImages: Record = {
...TRON_NETWORKS_IMAGES,
...CANTON_NETWORKS_IMAGES,
...SOLANA_NETWORKS_IMAGES,
+ ...BIP122_NETWORKS_IMAGES,
};
export const ALL_CHAINS = {
@@ -22,6 +24,7 @@ export const ALL_CHAINS = {
...TRON_CHAINS,
...CANTON_CHAINS,
...SOLANA_CHAINS,
+ ...BIP122_CHAINS,
};
export const PresetsUtil = {
diff --git a/wallets/rn_cli_wallet/src/utils/misc.ts b/wallets/rn_cli_wallet/src/utils/misc.ts
index 1accbbeb..645fc5c0 100644
--- a/wallets/rn_cli_wallet/src/utils/misc.ts
+++ b/wallets/rn_cli_wallet/src/utils/misc.ts
@@ -1,3 +1,5 @@
+import { Platform } from 'react-native';
+
import { getBundleId } from '@/utils/AppInfo';
type Environment = 'debug' | 'internal' | 'production';
@@ -6,7 +8,12 @@ export function getEnvironment(): Environment {
try {
const bundleId = getBundleId();
if (!bundleId || typeof bundleId !== 'string') {
- console.warn('Invalid bundle ID detected:', bundleId);
+ // On web there's no native bundle ID (expo-application returns null), so
+ // an empty value is expected — only warn on native, where it's a real
+ // signal that something is misconfigured.
+ if (Platform.OS !== 'web') {
+ console.warn('Invalid bundle ID detected:', bundleId);
+ }
return 'production';
}
if (bundleId.endsWith('.debug')) return 'debug';
diff --git a/wallets/rn_cli_wallet/yarn.lock b/wallets/rn_cli_wallet/yarn.lock
index 0d31062d..8fcece50 100644
--- a/wallets/rn_cli_wallet/yarn.lock
+++ b/wallets/rn_cli_wallet/yarn.lock
@@ -2066,6 +2066,15 @@ __metadata:
languageName: node
linkType: hard
+"@bitcoinerlab/secp256k1@npm:1.2.0":
+ version: 1.2.0
+ resolution: "@bitcoinerlab/secp256k1@npm:1.2.0"
+ dependencies:
+ "@noble/curves": ^1.7.0
+ checksum: c95eda77bc271f178d19a9c48cce53e5a303f6710dbd650aa24405416d87b52ab03038b7e28cc55150a0cdf0bd463d2b299791777c332c1556c86d03159e965a
+ languageName: node
+ linkType: hard
+
"@bottom-tabs/react-navigation@npm:1.3.1":
version: 1.3.1
resolution: "@bottom-tabs/react-navigation@npm:1.3.1"
@@ -3276,7 +3285,7 @@ __metadata:
languageName: node
linkType: hard
-"@noble/curves@npm:1.9.7, @noble/curves@npm:^1.4.2":
+"@noble/curves@npm:1.9.7, @noble/curves@npm:^1.4.2, @noble/curves@npm:^1.7.0":
version: 1.9.7
resolution: "@noble/curves@npm:1.9.7"
dependencies:
@@ -3322,6 +3331,13 @@ __metadata:
languageName: node
linkType: hard
+"@noble/secp256k1@npm:3.0.0":
+ version: 3.0.0
+ resolution: "@noble/secp256k1@npm:3.0.0"
+ checksum: f5c3be6fa1f27bd00606051cba9de62031ee67c4c519b3df9232c7343a740b805ca092f7c7a8d2ac9d9db99e4cc7ae8a8929842db64911a80aa2a071317a4749
+ languageName: node
+ linkType: hard
+
"@nodable/entities@npm:^2.1.0":
version: 2.1.0
resolution: "@nodable/entities@npm:2.1.0"
@@ -4054,7 +4070,7 @@ __metadata:
languageName: node
linkType: hard
-"@scure/base@npm:1.2.6, @scure/base@npm:^1.2.6, @scure/base@npm:~1.2.5":
+"@scure/base@npm:1.2.6, @scure/base@npm:^1.1.1, @scure/base@npm:^1.2.6, @scure/base@npm:~1.2.5":
version: 1.2.6
resolution: "@scure/base@npm:1.2.6"
checksum: 1058cb26d5e4c1c46c9cc0ae0b67cc66d306733baf35d6ebdd8ddaba242b80c3807b726e3b48cb0411bb95ec10d37764969063ea62188f86ae9315df8ea6b325
@@ -5186,6 +5202,7 @@ __metadata:
"@babel/core": ^7.25.2
"@babel/preset-env": ^7.25.3
"@babel/runtime": ^7.25.0
+ "@bitcoinerlab/secp256k1": 1.2.0
"@bottom-tabs/react-navigation": 1.3.1
"@craftzdog/react-native-buffer": 6.1.1
"@expo/metro-runtime": ~56.0.15
@@ -5194,6 +5211,7 @@ __metadata:
"@mhpdev/react-native-haptics": 1.1.0
"@mysten/sui": 1.45.2
"@noble/hashes": 1.8.0
+ "@noble/secp256k1": 3.0.0
"@react-native-async-storage/async-storage": 2.2.0
"@react-native-community/cli": ^20.0.0
"@react-native-community/cli-platform-android": ^20.0.0
@@ -5222,10 +5240,14 @@ __metadata:
"@walletconnect/react-native-compat": 2.23.10
babel-plugin-module-resolver: ^5.0.0
babel-preset-expo: 56.0.15
+ bip32: 4.0.0
bip39: 3.1.0
+ bitcoinjs-lib: 6.1.7
+ bitcoinjs-message: 2.2.0
bs58: 6.0.0
crc-32: 1.2.2
dayjs: 1.11.21
+ ecpair: 2.1.0
ed25519-hd-key: 1.3.0
eslint: ^8.19.0
eslint-plugin-ft-flow: ^2.0.1
@@ -5967,6 +5989,13 @@ __metadata:
languageName: node
linkType: hard
+"base-x@npm:^4.0.0":
+ version: 4.0.1
+ resolution: "base-x@npm:4.0.1"
+ checksum: c9061e576f7376b2bc6b69eca131254bb16ebe1445b535a3f0d68f27524e724965b6c191dffd255bf80f9bdf5eb9d1c8d0320903e83116f2c3e09f81b5ecb6a2
+ languageName: node
+ linkType: hard
+
"base-x@npm:^5.0.0":
version: 5.0.1
resolution: "base-x@npm:5.0.1"
@@ -5999,6 +6028,20 @@ __metadata:
languageName: node
linkType: hard
+"bech32@npm:^1.1.3":
+ version: 1.1.4
+ resolution: "bech32@npm:1.1.4"
+ checksum: 0e98db619191548390d6f09ff68b0253ba7ae6a55db93dfdbb070ba234c1fd3308c0606fbcc95fad50437227b10011e2698b89f0181f6e7f845c499bd14d0f4b
+ languageName: node
+ linkType: hard
+
+"bech32@npm:^2.0.0":
+ version: 2.0.0
+ resolution: "bech32@npm:2.0.0"
+ checksum: fa15acb270b59aa496734a01f9155677b478987b773bf701f465858bf1606c6a970085babd43d71ce61895f1baa594cb41a2cd1394bd2c6698f03cc2d811300e
+ languageName: node
+ linkType: hard
+
"big-integer@npm:1.6.x":
version: 1.6.52
resolution: "big-integer@npm:1.6.52"
@@ -6013,6 +6056,34 @@ __metadata:
languageName: node
linkType: hard
+"bindings@npm:^1.5.0":
+ version: 1.5.0
+ resolution: "bindings@npm:1.5.0"
+ dependencies:
+ file-uri-to-path: 1.0.0
+ checksum: 65b6b48095717c2e6105a021a7da4ea435aa8d3d3cd085cb9e85bcb6e5773cf318c4745c3f7c504412855940b585bdf9b918236612a1c7a7942491de176f1ae7
+ languageName: node
+ linkType: hard
+
+"bip174@npm:^2.1.1":
+ version: 2.1.1
+ resolution: "bip174@npm:2.1.1"
+ checksum: bc5b99e7d1acd9484aec117dc4d931a8b6d3a77ffb84e9672a9e8be2e41c22a3d41b4dad307cbe84091c6a30ee2ceaf8e1b3036b91201d4767d0772485ecb225
+ languageName: node
+ linkType: hard
+
+"bip32@npm:4.0.0":
+ version: 4.0.0
+ resolution: "bip32@npm:4.0.0"
+ dependencies:
+ "@noble/hashes": ^1.2.0
+ "@scure/base": ^1.1.1
+ typeforce: ^1.11.5
+ wif: ^2.0.6
+ checksum: a36253ac164db50a25e88effce84aec1eaba2ec2fbd7a19bc1f836f9b1c934fa148c3a01ddece7f136596330074d98c6b2a441e51914c302d130f56f0f893d0d
+ languageName: node
+ linkType: hard
+
"bip39@npm:3.1.0":
version: 3.1.0
resolution: "bip39@npm:3.1.0"
@@ -6031,6 +6102,43 @@ __metadata:
languageName: node
linkType: hard
+"bip66@npm:^1.1.5":
+ version: 1.1.5
+ resolution: "bip66@npm:1.1.5"
+ dependencies:
+ safe-buffer: ^5.0.1
+ checksum: 956cff6e51d7206571ef8ce875bc5fa61b5c181589790b9155799b7edcae4b20dbb3eed43b188ff3eec27cdbe98e0b7e0ec9f1cb2e4f5370c119028b248ad859
+ languageName: node
+ linkType: hard
+
+"bitcoinjs-lib@npm:6.1.7":
+ version: 6.1.7
+ resolution: "bitcoinjs-lib@npm:6.1.7"
+ dependencies:
+ "@noble/hashes": ^1.2.0
+ bech32: ^2.0.0
+ bip174: ^2.1.1
+ bs58check: ^3.0.1
+ typeforce: ^1.11.3
+ varuint-bitcoin: ^1.1.2
+ checksum: 2fbac2bffc2fe0e1d5441fc09f092fa8a83a4572b10fd2601c960e19a60cdcab81b6bff8cefcefda80b952fe8795387c97219fc4676995bf554f26f3d9aadc58
+ languageName: node
+ linkType: hard
+
+"bitcoinjs-message@npm:2.2.0":
+ version: 2.2.0
+ resolution: "bitcoinjs-message@npm:2.2.0"
+ dependencies:
+ bech32: ^1.1.3
+ bs58check: ^2.1.2
+ buffer-equals: ^1.0.3
+ create-hash: ^1.1.2
+ secp256k1: ^3.0.1
+ varuint-bitcoin: ^1.0.1
+ checksum: 2b851d7b976487118f34507739aabf5bca4b3c14ba997f46b932c71a83104a66a9476a808688a319e3a772c026d65381939852f091affc9dbae0f174eb6b03a1
+ languageName: node
+ linkType: hard
+
"bl@npm:^4.0.3, bl@npm:^4.1.0":
version: 4.1.0
resolution: "bl@npm:4.1.0"
@@ -6136,6 +6244,13 @@ __metadata:
languageName: node
linkType: hard
+"brorand@npm:^1.1.0":
+ version: 1.1.0
+ resolution: "brorand@npm:1.1.0"
+ checksum: 8a05c9f3c4b46572dec6ef71012b1946db6cae8c7bb60ccd4b7dd5a84655db49fe043ecc6272e7ef1f69dc53d6730b9e2a3a03a8310509a3d797a618cbee52be
+ languageName: node
+ linkType: hard
+
"brotli@npm:1.3.3":
version: 1.3.3
resolution: "brotli@npm:1.3.3"
@@ -6145,6 +6260,20 @@ __metadata:
languageName: node
linkType: hard
+"browserify-aes@npm:^1.0.6":
+ version: 1.2.0
+ resolution: "browserify-aes@npm:1.2.0"
+ dependencies:
+ buffer-xor: ^1.0.3
+ cipher-base: ^1.0.0
+ create-hash: ^1.1.0
+ evp_bytestokey: ^1.0.3
+ inherits: ^2.0.1
+ safe-buffer: ^5.0.1
+ checksum: 4a17c3eb55a2aa61c934c286f34921933086bf6d67f02d4adb09fcc6f2fc93977b47d9d884c25619144fccd47b3b3a399e1ad8b3ff5a346be47270114bcf7104
+ languageName: node
+ linkType: hard
+
"browserslist@npm:^4.24.0, browserslist@npm:^4.28.0, browserslist@npm:^4.28.1":
version: 4.28.1
resolution: "browserslist@npm:4.28.1"
@@ -6193,6 +6322,36 @@ __metadata:
languageName: node
linkType: hard
+"bs58@npm:^5.0.0":
+ version: 5.0.0
+ resolution: "bs58@npm:5.0.0"
+ dependencies:
+ base-x: ^4.0.0
+ checksum: 2475cb0684e07077521aac718e604a13e0f891d58cff923d437a2f7e9e28703ab39fce9f84c7c703ab369815a675f11e3bd394d38643bfe8969fbe42e6833d45
+ languageName: node
+ linkType: hard
+
+"bs58check@npm:<3.0.0, bs58check@npm:^2.1.2":
+ version: 2.1.2
+ resolution: "bs58check@npm:2.1.2"
+ dependencies:
+ bs58: ^4.0.0
+ create-hash: ^1.1.0
+ safe-buffer: ^5.1.2
+ checksum: 43bdf08a5dd04581b78f040bc4169480e17008da482ffe2a6507327bbc4fc5c28de0501f7faf22901cfe57fbca79cbb202ca529003fedb4cb8dccd265b38e54d
+ languageName: node
+ linkType: hard
+
+"bs58check@npm:^3.0.1":
+ version: 3.0.1
+ resolution: "bs58check@npm:3.0.1"
+ dependencies:
+ "@noble/hashes": ^1.2.0
+ bs58: ^5.0.0
+ checksum: dbbecc7a09f3836e821149266c864c4bbd545539cea43c35f23f4c3c46b54c86c52b65d224b9ea2e916fa6d93bd2ce9fac5b6c6bfcf19621a9c209a5602f71c8
+ languageName: node
+ linkType: hard
+
"bser@npm:2.1.1":
version: 2.1.1
resolution: "bser@npm:2.1.1"
@@ -6202,6 +6361,13 @@ __metadata:
languageName: node
linkType: hard
+"buffer-equals@npm:^1.0.3":
+ version: 1.0.4
+ resolution: "buffer-equals@npm:1.0.4"
+ checksum: 392a2f82acdaad46392aec7ce54a8ff0b2a650b5802ccb0c77072050bbc7fd4e101f38460c7e88cdc7e130421882977f595d5c1a3d3343611562ecf7c684a70f
+ languageName: node
+ linkType: hard
+
"buffer-from@npm:^1.0.0":
version: 1.1.2
resolution: "buffer-from@npm:1.1.2"
@@ -6209,6 +6375,13 @@ __metadata:
languageName: node
linkType: hard
+"buffer-xor@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "buffer-xor@npm:1.0.3"
+ checksum: 10c520df29d62fa6e785e2800e586a20fc4f6dfad84bcdbd12e1e8a83856de1cb75c7ebd7abe6d036bbfab738a6cf18a3ae9c8e5a2e2eb3167ca7399ce65373a
+ languageName: node
+ linkType: hard
+
"buffer@npm:6.0.3, buffer@npm:^6.0.3, buffer@npm:~6.0.3":
version: 6.0.3
resolution: "buffer@npm:6.0.3"
@@ -6448,7 +6621,7 @@ __metadata:
languageName: node
linkType: hard
-"cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3":
+"cipher-base@npm:^1.0.0, cipher-base@npm:^1.0.1, cipher-base@npm:^1.0.3":
version: 1.0.7
resolution: "cipher-base@npm:1.0.7"
dependencies:
@@ -6794,7 +6967,7 @@ __metadata:
languageName: node
linkType: hard
-"create-hash@npm:^1.1.0":
+"create-hash@npm:^1.1.0, create-hash@npm:^1.1.2, create-hash@npm:^1.2.0":
version: 1.2.0
resolution: "create-hash@npm:1.2.0"
dependencies:
@@ -6807,7 +6980,7 @@ __metadata:
languageName: node
linkType: hard
-"create-hmac@npm:1.1.7":
+"create-hmac@npm:1.1.7, create-hmac@npm:^1.1.4":
version: 1.1.7
resolution: "create-hmac@npm:1.1.7"
dependencies:
@@ -7274,6 +7447,17 @@ __metadata:
languageName: node
linkType: hard
+"drbg.js@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "drbg.js@npm:1.0.1"
+ dependencies:
+ browserify-aes: ^1.0.6
+ create-hash: ^1.1.2
+ create-hmac: ^1.1.4
+ checksum: f8df5cdd4fb792e548d6187cbc446fbd0afd8f1ef7fa486e1c286c2adee55a687183ce48ab178e9f24965c2deabb6e2ba7a7ee2d675264b951356480eb042476
+ languageName: node
+ linkType: hard
+
"dunder-proto@npm:^1.0.0, dunder-proto@npm:^1.0.1":
version: 1.0.1
resolution: "dunder-proto@npm:1.0.1"
@@ -7292,6 +7476,17 @@ __metadata:
languageName: node
linkType: hard
+"ecpair@npm:2.1.0":
+ version: 2.1.0
+ resolution: "ecpair@npm:2.1.0"
+ dependencies:
+ randombytes: ^2.1.0
+ typeforce: ^1.18.0
+ wif: ^2.0.6
+ checksum: 924a776808f91d2fdd33a7033f84e3bbfe668ae98c0b9764c7b923e018accd8de57012bf20e419e0a5ef73ec3ec3738a654e71abe12f537b2fd7bcf02b93659f
+ languageName: node
+ linkType: hard
+
"ed25519-hd-key@npm:1.3.0":
version: 1.3.0
resolution: "ed25519-hd-key@npm:1.3.0"
@@ -7323,6 +7518,21 @@ __metadata:
languageName: node
linkType: hard
+"elliptic@npm:6.6.1":
+ version: 6.6.1
+ resolution: "elliptic@npm:6.6.1"
+ dependencies:
+ bn.js: ^4.11.9
+ brorand: ^1.1.0
+ hash.js: ^1.0.0
+ hmac-drbg: ^1.0.1
+ inherits: ^2.0.4
+ minimalistic-assert: ^1.0.1
+ minimalistic-crypto-utils: ^1.0.1
+ checksum: 27b14a52f68bbbc0720da259f712cb73e953f6d2047958cd02fb0d0ade2e83849dc39fb4af630889c67df8817e24237428cf59c4f4c07700f755b401149a7375
+ languageName: node
+ linkType: hard
+
"emittery@npm:^0.13.1":
version: 0.13.1
resolution: "emittery@npm:0.13.1"
@@ -7989,6 +8199,17 @@ __metadata:
languageName: node
linkType: hard
+"evp_bytestokey@npm:^1.0.3":
+ version: 1.0.3
+ resolution: "evp_bytestokey@npm:1.0.3"
+ dependencies:
+ md5.js: ^1.3.4
+ node-gyp: latest
+ safe-buffer: ^5.1.1
+ checksum: ad4e1577f1a6b721c7800dcc7c733fe01f6c310732bb5bf2240245c2a5b45a38518b91d8be2c610611623160b9d1c0e91f1ce96d639f8b53e8894625cf20fa45
+ languageName: node
+ linkType: hard
+
"execa@npm:^5.0.0":
version: 5.1.1
resolution: "execa@npm:5.1.1"
@@ -8414,6 +8635,13 @@ __metadata:
languageName: node
linkType: hard
+"file-uri-to-path@npm:1.0.0":
+ version: 1.0.0
+ resolution: "file-uri-to-path@npm:1.0.0"
+ checksum: b648580bdd893a008c92c7ecc96c3ee57a5e7b6c4c18a9a09b44fb5d36d79146f8e442578bc0e173dc027adf3987e254ba1dfd6e3ec998b7c282873010502144
+ languageName: node
+ linkType: hard
+
"fill-range@npm:^7.1.1":
version: 7.1.1
resolution: "fill-range@npm:7.1.1"
@@ -8926,6 +9154,16 @@ __metadata:
languageName: node
linkType: hard
+"hash.js@npm:^1.0.0, hash.js@npm:^1.0.3":
+ version: 1.1.7
+ resolution: "hash.js@npm:1.1.7"
+ dependencies:
+ inherits: ^2.0.3
+ minimalistic-assert: ^1.0.1
+ checksum: e350096e659c62422b85fa508e4b3669017311aa4c49b74f19f8e1bc7f3a54a584fdfd45326d4964d6011f2b2d882e38bea775a96046f2a61b7779a979629d8f
+ languageName: node
+ linkType: hard
+
"hasown@npm:^2.0.2":
version: 2.0.2
resolution: "hasown@npm:2.0.2"
@@ -9008,6 +9246,17 @@ __metadata:
languageName: node
linkType: hard
+"hmac-drbg@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "hmac-drbg@npm:1.0.1"
+ dependencies:
+ hash.js: ^1.0.3
+ minimalistic-assert: ^1.0.0
+ minimalistic-crypto-utils: ^1.0.1
+ checksum: bd30b6a68d7f22d63f10e1888aee497d7c2c5c0bb469e66bbdac99f143904d1dfe95f8131f95b3e86c86dd239963c9d972fcbe147e7cffa00e55d18585c43fe0
+ languageName: node
+ linkType: hard
+
"hoist-non-react-statics@npm:^3.3.0":
version: 3.3.2
resolution: "hoist-non-react-statics@npm:3.3.2"
@@ -11219,6 +11468,20 @@ __metadata:
languageName: node
linkType: hard
+"minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "minimalistic-assert@npm:1.0.1"
+ checksum: cc7974a9268fbf130fb055aff76700d7e2d8be5f761fb5c60318d0ed010d839ab3661a533ad29a5d37653133385204c503bfac995aaa4236f4e847461ea32ba7
+ languageName: node
+ linkType: hard
+
+"minimalistic-crypto-utils@npm:^1.0.1":
+ version: 1.0.1
+ resolution: "minimalistic-crypto-utils@npm:1.0.1"
+ checksum: 6e8a0422b30039406efd4c440829ea8f988845db02a3299f372fceba56ffa94994a9c0f2fd70c17f9969eedfbd72f34b5070ead9656a34d3f71c0bd72583a0ed
+ languageName: node
+ linkType: hard
+
"minimatch@npm:5.1.8":
version: 5.1.8
resolution: "minimatch@npm:5.1.8"
@@ -11364,6 +11627,15 @@ __metadata:
languageName: node
linkType: hard
+"nan@npm:^2.14.0":
+ version: 2.28.0
+ resolution: "nan@npm:2.28.0"
+ dependencies:
+ node-gyp: latest
+ checksum: a6b29036ae9825ca0c7e2b4956b0ce7e54a66457af6300bf2a5550ab95c878c75c63373c07f9dc0e5cd2edbfb049961ff817485f59136eabe48bdd3fd6cc92e4
+ languageName: node
+ linkType: hard
+
"nanoid@npm:3.3.8":
version: 3.3.8
resolution: "nanoid@npm:3.3.8"
@@ -12402,6 +12674,15 @@ __metadata:
languageName: node
linkType: hard
+"randombytes@npm:^2.1.0":
+ version: 2.1.0
+ resolution: "randombytes@npm:2.1.0"
+ dependencies:
+ safe-buffer: ^5.1.0
+ checksum: d779499376bd4cbb435ef3ab9a957006c8682f343f14089ed5f27764e4645114196e75b7f6abf1cbd84fd247c0cb0651698444df8c9bf30e62120fbbc52269d6
+ languageName: node
+ linkType: hard
+
"range-parser@npm:~1.2.1":
version: 1.2.1
resolution: "range-parser@npm:1.2.1"
@@ -13296,7 +13577,7 @@ __metadata:
languageName: node
linkType: hard
-"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0":
+"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0":
version: 5.2.1
resolution: "safe-buffer@npm:5.2.1"
checksum: b99c4b41fdd67a6aaf280fcd05e9ffb0813654894223afb78a31f14a19ad220bba8aba1cb14eddce1fcfb037155fe6de4e861784eb434f7d11ed58d1e70dd491
@@ -13368,6 +13649,23 @@ __metadata:
languageName: node
linkType: hard
+"secp256k1@npm:^3.0.1":
+ version: 3.8.1
+ resolution: "secp256k1@npm:3.8.1"
+ dependencies:
+ bindings: ^1.5.0
+ bip66: ^1.1.5
+ bn.js: ^4.11.8
+ create-hash: ^1.2.0
+ drbg.js: ^1.0.1
+ elliptic: ^6.5.7
+ nan: ^2.14.0
+ node-gyp: latest
+ safe-buffer: ^5.1.2
+ checksum: 6e06fd1a362cd42add8780f352c666b393c4b8e6d316f3527d50d354ac43bb9bfe089a434a61a9e96b03729d3f2f026be8f6dd6c6094c914305ac7da34d65c09
+ languageName: node
+ linkType: hard
+
"semver@npm:7.7.1":
version: 7.7.1
resolution: "semver@npm:7.7.1"
@@ -14600,6 +14898,13 @@ __metadata:
languageName: node
linkType: hard
+"typeforce@npm:^1.11.3, typeforce@npm:^1.11.5, typeforce@npm:^1.18.0":
+ version: 1.18.0
+ resolution: "typeforce@npm:1.18.0"
+ checksum: e3b21e27e76cb05f32285bef7c30a29760e79c622cfe4aa3c179ce49d1c7895b7154c8deedb9fe4599b1fd0428d35860d43e0776da1c04861168f3ad7ed99c70
+ languageName: node
+ linkType: hard
+
"typescript@npm:~6.0.3":
version: 6.0.3
resolution: "typescript@npm:6.0.3"
@@ -14981,6 +15286,15 @@ __metadata:
languageName: node
linkType: hard
+"varuint-bitcoin@npm:^1.0.1, varuint-bitcoin@npm:^1.1.2":
+ version: 1.1.2
+ resolution: "varuint-bitcoin@npm:1.1.2"
+ dependencies:
+ safe-buffer: ^5.1.1
+ checksum: 1c900bf08f2408ae33a6094dc5d809bdb6673eaf6039062d88c230155873e51e29c760053611f93ccd024854d04ebd92ed95c744720e94a79ca4e1150fcce071
+ languageName: node
+ linkType: hard
+
"vary@npm:~1.1.2":
version: 1.1.2
resolution: "vary@npm:1.1.2"
@@ -15201,6 +15515,15 @@ __metadata:
languageName: node
linkType: hard
+"wif@npm:^2.0.6":
+ version: 2.0.6
+ resolution: "wif@npm:2.0.6"
+ dependencies:
+ bs58check: <3.0.0
+ checksum: 8c3147ef98d56f394d66f0477f699fba7fc18dd0d1c2c5d0f8408be41acffed589fa82447d80eae5afc9a3cbd943bc3eebb337b9f114955adeaad02a244f4f9a
+ languageName: node
+ linkType: hard
+
"word-wrap@npm:^1.2.5":
version: 1.2.5
resolution: "word-wrap@npm:1.2.5"