From db62a43c8912eb3fef4b819d7777490bef6b22ed Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Wed, 28 Oct 2026 17:54:18 +0000 Subject: [PATCH 01/12] feat(onekey): support Sui, Stellar, TON, TRON, Cardano and NEAR Connect each chain through the provider the OneKey extension injects, checked against OneKey's own provider and background sources. Bitcoin moves to the Unisat-style $onekey.btc API and Solana to $onekey.solana: the previous sats-connect and $onekey.sol paths never matched what the extension exposes, so both chains failed to connect. Sui, TRON and Stellar signatures are checked against the requested transaction before they are returned. NEAR and TON can only sign and send, so they go through the toolbox's sign-and-send path. The unused EVM signer is removed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .changeset/onekey-multichain-support.md | 5 + bun.lock | 2 + packages/wallet-extensions/package.json | 2 + .../src/onekey/__tests__/onekey.test.ts | 193 ++++++++++++ .../wallet-extensions/src/onekey/evmSigner.ts | 113 -------- .../wallet-extensions/src/onekey/helpers.ts | 274 ++++++++++++++++++ .../wallet-extensions/src/onekey/index.ts | 146 +--------- packages/wallet-extensions/src/onekey/ton.ts | 127 ++++++++ .../wallet-extensions/src/onekey/types.ts | 76 +++++ packages/wallet-extensions/src/types.ts | 2 +- 10 files changed, 696 insertions(+), 244 deletions(-) create mode 100644 .changeset/onekey-multichain-support.md create mode 100644 packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts delete mode 100644 packages/wallet-extensions/src/onekey/evmSigner.ts create mode 100644 packages/wallet-extensions/src/onekey/helpers.ts create mode 100644 packages/wallet-extensions/src/onekey/ton.ts create mode 100644 packages/wallet-extensions/src/onekey/types.ts diff --git a/.changeset/onekey-multichain-support.md b/.changeset/onekey-multichain-support.md new file mode 100644 index 00000000..6a168b10 --- /dev/null +++ b/.changeset/onekey-multichain-support.md @@ -0,0 +1,5 @@ +--- +"@swapkit/wallet-extensions": minor +--- + +OneKey now connects Sui, Stellar, TON, TRON, Cardano and NEAR on mainnet, alongside its EVM chains. Bitcoin and Solana connections work again: they were looking for wallet providers the OneKey extension never exposes, so connecting either chain always failed. A Sui, TRON or Stellar signature from OneKey is rejected if it covers a different transaction than the one requested. diff --git a/bun.lock b/bun.lock index 1b941695..25b11996 100644 --- a/bun.lock +++ b/bun.lock @@ -58,6 +58,7 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "~2.2.0", "@solana/web3.js": "~1.98.4", + "@stellar/stellar-sdk": "~16.1.0", "@swapkit/helpers": "^6.3.0", "@swapkit/toolboxes": "^6.4.1", "@swapkit/utxo-signer": "^3.1.1", @@ -80,6 +81,7 @@ "@near-js/transactions": "2.5.0", "@scure/base": "2.2.0", "@solana/web3.js": "1.98.4", + "@stellar/stellar-sdk": "16.1.0", "@ton/core": "0.63.1", "@wallet-standard/app": "1.1.1", "cosmjs-types": "0.10.1", diff --git a/packages/wallet-extensions/package.json b/packages/wallet-extensions/package.json index e09c3478..7f1d538f 100644 --- a/packages/wallet-extensions/package.json +++ b/packages/wallet-extensions/package.json @@ -11,6 +11,7 @@ "@near-js/transactions": "~2.5.0", "@scure/base": "~2.2.0", "@solana/web3.js": "~1.98.4", + "@stellar/stellar-sdk": "~16.1.0", "@swapkit/helpers": "^6.3.0", "@swapkit/toolboxes": "^6.4.1", "@swapkit/utxo-signer": "^3.1.1", @@ -34,6 +35,7 @@ "@near-js/transactions": "2.5.0", "@scure/base": "2.2.0", "@solana/web3.js": "1.98.4", + "@stellar/stellar-sdk": "16.1.0", "@ton/core": "0.63.1", "@wallet-standard/app": "1.1.1", "cosmjs-types": "0.10.1", diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts new file mode 100644 index 00000000..669052ab --- /dev/null +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -0,0 +1,193 @@ +// @ts-nocheck - Test file with intentional mocking of browser globals +import { afterEach, describe, expect, test } from "bun:test"; +import { createHash } from "node:crypto"; +import { KeyPair } from "@near-js/crypto"; +import { actionCreators, createTransaction } from "@near-js/transactions"; +import { base64 } from "@scure/base"; +import { Account, Asset, Keypair, Networks, Operation, TransactionBuilder } from "@stellar/stellar-sdk"; +import { Chain } from "@swapkit/helpers"; + +import { getWalletMethods } from "../helpers"; + +const SUI_ADDRESS = `0x${"ab".repeat(32)}`; +const TRON_ADDRESS = "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"; + +afterEach(() => { + globalThis.window = undefined; +}); + +function tronTransaction(rawDataHex: string) { + return { + raw_data: { contract: [], expiration: 0, ref_block_bytes: "", ref_block_hash: "", timestamp: 0 }, + raw_data_hex: rawDataHex, + txID: createHash("sha256").update(Buffer.from(rawDataHex, "hex")).digest("hex"), + }; +} + +function stellarPayment(source: Keypair, amount: string) { + return new TransactionBuilder(new Account(source.publicKey(), "1"), { + fee: "100", + networkPassphrase: Networks.PUBLIC, + }) + .addOperation(Operation.payment({ amount, asset: Asset.native(), destination: Keypair.random().publicKey() })) + .setTimeout(30) + .build(); +} + +describe("OneKey Sui signer", () => { + function mockSui(signTransaction) { + globalThis.window = { + $onekey: { sui: { getAccounts: async () => [{ address: SUI_ADDRESS, publicKey: "" }], signTransaction } }, + }; + } + + test("sends the exact transaction bytes and returns the wallet signature", async () => { + const txBytes = new Uint8Array([1, 2, 3, 4]); + let request: Record | undefined; + mockSui((input) => { + request = input; + return Promise.resolve({ bytes: input.transaction.serialize(), signature: "wallet-signature" }); + }); + + const wallet = await getWalletMethods(Chain.Sui); + const signed = await wallet.signTransaction(txBytes); + + expect(request.chain).toBe("sui:mainnet"); + expect(request.account.address).toBe(SUI_ADDRESS); + expect(base64.decode(request.transaction.serialize())).toEqual(txBytes); + expect(signed).toEqual({ bytes: base64.encode(txBytes), signature: "wallet-signature" }); + }); + + test("rejects a signature over different bytes", async () => { + mockSui(async () => ({ bytes: base64.encode(new Uint8Array([9, 9])), signature: "wallet-signature" })); + + const wallet = await getWalletMethods(Chain.Sui); + + await expect(wallet.signTransaction(new Uint8Array([1, 2, 3, 4]))).rejects.toThrow("different transaction"); + }); +}); + +describe("OneKey Tron signer", () => { + function mockTron(sign) { + globalThis.window = { + $onekey: { + tron: { + request: async () => ({ code: 200, message: "ok" }), + tronWeb: { defaultAddress: { base58: TRON_ADDRESS }, trx: { sign } }, + }, + }, + }; + } + + test("returns the original transaction with the wallet signature", async () => { + const transaction = tronTransaction("0a02abcd"); + mockTron(async (tx) => ({ ...tx, raw_data: { ...tx.raw_data, fee_limit: 1 }, signature: ["sig"] })); + + const wallet = await getWalletMethods(Chain.Tron); + const signed = await wallet.signTransaction(transaction); + + expect(wallet.address).toBe(TRON_ADDRESS); + expect(signed).toEqual({ ...transaction, signature: ["sig"] }); + }); + + test("rejects a signature over a different transaction", async () => { + const other = tronTransaction("0a02ffff"); + mockTron(async () => ({ ...other, signature: ["sig"] })); + + const wallet = await getWalletMethods(Chain.Tron); + + await expect(wallet.signTransaction(tronTransaction("0a02abcd"))).rejects.toThrow("different transaction"); + }); +}); + +describe("OneKey Stellar signer", () => { + function mockStellar(signer: Keypair, signTransaction) { + globalThis.window = { + $onekey: { + stellar: { + getAddress: async () => ({ address: signer.publicKey() }), + getNetwork: async () => ({ network: "mainnet", networkPassphrase: Networks.PUBLIC }), + signTransaction, + }, + }, + }; + } + + test("adds the wallet signature to the requested transaction", async () => { + const keypair = Keypair.random(); + const transaction = stellarPayment(keypair, "1"); + mockStellar(keypair, (xdr) => { + const tx = TransactionBuilder.fromXDR(xdr, Networks.PUBLIC); + tx.sign(keypair); + return Promise.resolve({ signedTxXdr: tx.toXDR() }); + }); + + const wallet = await getWalletMethods(Chain.Stellar); + const signed = await wallet.signTransaction(transaction); + + expect(signed.hash()).toEqual(transaction.hash()); + expect(signed.signatures).toHaveLength(1); + expect(keypair.verify(transaction.hash(), signed.signatures[0].signature())).toBe(true); + }); + + test("rejects a signature over a different transaction", async () => { + const keypair = Keypair.random(); + mockStellar(keypair, () => { + const tx = stellarPayment(keypair, "100"); + tx.sign(keypair); + return Promise.resolve({ signedTxXdr: tx.toXDR() }); + }); + + const wallet = await getWalletMethods(Chain.Stellar); + + await expect(wallet.signTransaction(stellarPayment(keypair, "1"))).rejects.toThrow("different transaction"); + }); +}); + +describe("OneKey Near signer", () => { + test("sends the encoded transaction to OneKey and returns its hash", async () => { + const keyPair = KeyPair.fromRandom("ed25519"); + const transaction = createTransaction( + "alice.near", + keyPair.getPublicKey(), + "bob.near", + 1n, + [actionCreators.transfer(1n)], + new Uint8Array(32), + ); + const requests = []; + globalThis.window = { + $onekey: { + jsBridge: { + request: ({ data, scope }) => { + requests.push({ ...data, scope }); + if (data.method === "near_requestAccounts") { + const accounts = [{ accountId: "alice.near", publicKey: keyPair.getPublicKey().toString() }]; + return Promise.resolve({ result: { accounts } }); + } + if (data.method === "near_network") return Promise.resolve({ result: { networkId: "mainnet" } }); + return Promise.resolve({ result: { transactionHashes: ["near-hash"] } }); + }, + }, + }, + }; + + const wallet = await getWalletMethods(Chain.Near); + const hash = await wallet.signAndBroadcastTransaction(transaction); + const signRequest = requests.find(({ method }) => method === "near_requestSignTransactions"); + + expect(hash).toBe("near-hash"); + expect(signRequest.scope).toBe("near"); + expect(signRequest.params[0].send).toBe(true); + expect(signRequest.params[0].transactions).toEqual([base64.encode(transaction.encode())]); + }); +}); + +test("connects Solana through the provider OneKey injects", async () => { + const address = "4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T"; + globalThis.window = { $onekey: { solana: { connect: async () => ({ publicKey: { toString: () => address } }) } } }; + + const wallet = await getWalletMethods(Chain.Solana); + + expect(wallet.address).toBe(address); +}); diff --git a/packages/wallet-extensions/src/onekey/evmSigner.ts b/packages/wallet-extensions/src/onekey/evmSigner.ts deleted file mode 100644 index 00501ff2..00000000 --- a/packages/wallet-extensions/src/onekey/evmSigner.ts +++ /dev/null @@ -1,113 +0,0 @@ -import type { Chain, DerivationPathArray } from "@swapkit/helpers"; -import { SwapKitError, WalletOption } from "@swapkit/helpers"; -import type { JsonRpcProvider, Provider, Signer, TypedDataDomain, TypedDataField } from "ethers"; - -type OneKeyEVMSignerParams = { - chain: Chain; - derivationPath: DerivationPathArray; - provider: Provider | JsonRpcProvider; -}; - -export async function getEVMSigner({ chain, derivationPath, provider }: OneKeyEVMSignerParams) { - const { AbstractSigner, BrowserProvider } = await import("ethers"); - - class OneKeySigner extends AbstractSigner { - address: string; - chain: Chain; - derivationPath: DerivationPathArray; - readonly provider: Provider | JsonRpcProvider; - - constructor({ chain, derivationPath, provider }: OneKeyEVMSignerParams) { - super(provider); - - this.address = ""; - this.chain = chain; - this.derivationPath = derivationPath; - this.provider = provider; - } - - connect(provider: null | Provider): Signer { - if (!provider) { - throw new SwapKitError({ - errorKey: "wallet_provider_not_found", - info: { chain: this.chain, wallet: WalletOption.ONEKEY }, - }); - } - - return new OneKeySigner({ chain: this.chain, derivationPath: this.derivationPath, provider }); - } - - signTypedData( - domain: TypedDataDomain, - types: Record, - value: Record, - _primaryType?: string, - ): Promise { - if (!window.$onekey?.ethereum) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: this.chain } }); - } - - return this.getAddress().then(async (_address) => { - try { - const signer = await new BrowserProvider(window.$onekey.ethereum).getSigner(); - return signer.signTypedData(domain, types, value); - } catch (error) { - throw new SwapKitError({ - errorKey: "core_wallet_sign_message_not_supported", - info: { error, wallet: WalletOption.ONEKEY }, - }); - } - }); - } - - getAddress = async () => { - if (this.address) return this.address; - - if (!window.$onekey?.ethereum) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: this.chain } }); - } - - const signer = await new BrowserProvider(window.$onekey.ethereum).getSigner(); - this.address = await signer.getAddress(); - return this.address; - }; - - signMessage = async (message: string) => { - if (!window.$onekey?.ethereum) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: this.chain } }); - } - - const signer = await new BrowserProvider(window.$onekey.ethereum).getSigner(); - return signer.signMessage(message); - }; - - signTransaction = async (transaction: any) => { - if (!window.$onekey?.ethereum) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: this.chain } }); - } - - const signer = await new BrowserProvider(window.$onekey.ethereum).getSigner(); - const { to, value, gasLimit, gasPrice, data, maxFeePerGas, maxPriorityFeePerGas } = transaction; - - if (!to) throw new SwapKitError("toolbox_evm_no_to_address"); - if (!gasLimit) throw new SwapKitError("toolbox_evm_no_gas_price"); - - const tx: any = { data, gasLimit, to }; - - if (value) { - tx.value = value; - } - - if (maxFeePerGas && maxPriorityFeePerGas) { - tx.maxFeePerGas = maxFeePerGas; - tx.maxPriorityFeePerGas = maxPriorityFeePerGas; - } else if (gasPrice) { - tx.gasPrice = gasPrice; - } - - return signer.signTransaction(tx); - }; - } - - return new OneKeySigner({ chain, derivationPath, provider }); -} diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts new file mode 100644 index 00000000..c298eb07 --- /dev/null +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -0,0 +1,274 @@ +import { base64, hex } from "@scure/base"; +import { Chain, type EVMChain, prepareNetworkSwitch, SwapKitError, WalletOption } from "@swapkit/helpers"; +import type { NearSigner } from "@swapkit/toolboxes/near"; +import type { StellarSigner } from "@swapkit/toolboxes/stellar"; +import type { SuiChainSigner } from "@swapkit/toolboxes/sui"; +import type { TronSigner } from "@swapkit/toolboxes/tron"; +import { Transaction } from "@swapkit/utxo-signer"; +import { getUtxoScriptTypeParams } from "../helpers/utxoScriptType"; +import { getOneKeyTonWallet } from "./ton"; + +const SUI_MAINNET = "sui:mainnet"; + +type WalletMethodsWithAddress = Record & { address: string }; + +const ONEKEY_EVM_CHAINS = [ + Chain.Arbitrum, + Chain.Aurora, + Chain.Avalanche, + Chain.Base, + Chain.BinanceSmartChain, + Chain.Ethereum, + Chain.Gnosis, + Chain.Optimism, + Chain.Polygon, + Chain.XLayer, +] as const; + +function getProvider>(key: Key, chain: Chain) { + const provider = window.$onekey?.[key]; + if (!provider) { + throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain } }); + } + + return provider as NonNullable[Key]>; +} + +function signingError(chain: Chain, reason: string) { + return new SwapKitError({ errorKey: "wallet_onekey_sign_transaction_error", info: { chain, reason } }); +} + +function mainnetOnlyError(chain: Chain) { + return new SwapKitError({ + errorKey: "wallet_chain_not_supported", + info: { chain, message: "Mainnet only", wallet: WalletOption.ONEKEY }, + }); +} + +function connectionRejectedError(chain: Chain) { + return new SwapKitError("wallet_connection_rejected_by_user", { chain, wallet: WalletOption.ONEKEY }); +} + +async function getBitcoinWalletMethods() { + const wallet = getProvider("btc", Chain.Bitcoin); + const [address] = await wallet.requestAccounts(); + if (!address) throw connectionRejectedError(Chain.Bitcoin); + if ((await wallet.getNetwork()) !== "livenet") throw mainnetOnlyError(Chain.Bitcoin); + + const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); + + async function signTransaction(tx: InstanceType) { + const signedPsbtHex = await wallet.signPsbt(hex.encode(tx.toPSBT(0)), { autoFinalized: false }); + return Transaction.fromPSBT(hex.decode(signedPsbtHex)); + } + + const signer = { getAddress: () => Promise.resolve(address), signTransaction }; + const toolbox = await getUtxoToolbox(Chain.Bitcoin, { + ...(await getUtxoScriptTypeParams({ address, chain: Chain.Bitcoin })), + signer, + }); + + return { ...toolbox, address }; +} + +async function getSolanaWalletMethods() { + const provider = getProvider("solana", Chain.Solana); + const { getSolanaToolbox } = await import("@swapkit/toolboxes/solana"); + const { publicKey } = await provider.connect(); + + return { ...getSolanaToolbox({ signer: provider }), address: publicKey.toString() }; +} + +async function getEvmWalletMethods(chain: EVMChain) { + const ethereum = getProvider("ethereum", chain); + const { getProvider: getEvmProvider, getEvmToolboxAsync } = await import("@swapkit/toolboxes/evm"); + const { BrowserProvider } = await import("ethers"); + const provider = new BrowserProvider(ethereum, "any"); + + await provider.send("eth_requestAccounts", []); + const signer = await provider.getSigner(); + const address = await signer.getAddress(); + const toolbox = await getEvmToolboxAsync(chain, { provider: await getEvmProvider(chain), signer }); + + return { address, ...prepareNetworkSwitch({ chain, provider, toolbox }) }; +} + +async function getSuiWalletMethods() { + const provider = getProvider("sui", Chain.Sui); + const [account] = await provider.getAccounts(); + if (!account?.address) throw connectionRejectedError(Chain.Sui); + + const { getSuiToolbox } = await import("@swapkit/toolboxes/sui"); + + const signer: SuiChainSigner = { + getAddress: () => Promise.resolve(account.address), + signTransaction: async (txBytes: Uint8Array) => { + const serialized = base64.encode(txBytes); + const result = await provider.signTransaction({ + account, + chain: SUI_MAINNET, + transaction: { serialize: () => serialized }, + }); + + if (base64.encode(base64.decode(result.bytes)) !== serialized) { + throw signingError(Chain.Sui, "OneKey signed a different transaction than the one requested"); + } + + return { bytes: serialized, signature: result.signature }; + }, + toSuiAddress: () => account.address, + }; + + return { ...getSuiToolbox({ signer }), address: account.address }; +} + +async function getTronWalletMethods() { + const provider = getProvider("tron", Chain.Tron); + const response = await provider.request({ method: "tron_requestAccounts" }); + if (response.code !== 200) throw connectionRejectedError(Chain.Tron); + + const tronWeb = provider.tronWeb; + const address = tronWeb?.defaultAddress?.base58; + if (!(tronWeb && address)) throw connectionRejectedError(Chain.Tron); + + const { getTronToolbox } = await import("@swapkit/toolboxes/tron"); + + const signer: TronSigner = { + getAddress: () => Promise.resolve(address), + signTransaction: async (transaction) => { + const { signature, txID } = await tronWeb.trx.sign(transaction); + + if (txID !== transaction.txID) { + throw signingError(Chain.Tron, "OneKey signed a different transaction than the one requested"); + } + if (!signature?.length) throw signingError(Chain.Tron, "OneKey returned no signature"); + + return { ...transaction, signature }; + }, + }; + + return { ...getTronToolbox({ signer }), address }; +} + +async function getCardanoWalletMethods() { + const api = await getProvider("cardano", Chain.Cardano).enable(); + if ((await api.getNetworkId()) !== 1) throw mainnetOnlyError(Chain.Cardano); + + const { addCardanoVkeyWitnesses, decodeCardanoWitnessSet, getCardanoAddressFromBytes, getCardanoToolbox } = + await import("@swapkit/toolboxes/cardano"); + + const changeAddress = await api.getChangeAddress(); + const address = changeAddress.startsWith("addr") + ? changeAddress + : await getCardanoAddressFromBytes(hex.decode(changeAddress)); + + const signer = { + address, + connect: () => Promise.resolve({ address }), + disconnect: () => Promise.resolve(), + getAddress: () => Promise.resolve(address), + signTransaction: async (transaction: string) => + addCardanoVkeyWitnesses(transaction, await decodeCardanoWitnessSet(await api.signTx(transaction))), + }; + + return { ...getCardanoToolbox({ signer }), address }; +} + +async function getStellarWalletMethods() { + const provider = getProvider("stellar", Chain.Stellar); + const { address } = await provider.getAddress(); + const { Networks, TransactionBuilder } = await import("@stellar/stellar-sdk"); + if ((await provider.getNetwork()).networkPassphrase !== Networks.PUBLIC) throw mainnetOnlyError(Chain.Stellar); + + const { getStellarToolbox } = await import("@swapkit/toolboxes/stellar"); + + const signer: StellarSigner = { + getAddress: () => address, + signTransaction: async (transaction) => { + const { signedTxXdr } = await provider.signTransaction(transaction.toXDR(), { + networkPassphrase: Networks.PUBLIC, + }); + const signed = TransactionBuilder.fromXDR(signedTxXdr, Networks.PUBLIC); + + if (!signed.hash().equals(transaction.hash())) { + throw signingError(Chain.Stellar, "OneKey signed a different transaction than the one requested"); + } + + const existing = new Set(transaction.signatures.map((entry) => entry.signature().toString("base64"))); + const added = signed.signatures.filter((entry) => !existing.has(entry.signature().toString("base64"))); + if (!added.length) throw signingError(Chain.Stellar, "OneKey returned no signature"); + + for (const entry of added) transaction.addDecoratedSignature(entry); + return transaction; + }, + }; + + return { ...getStellarToolbox({ signer }), address }; +} + +async function requestNear(method: string, params?: unknown) { + const bridge = getProvider("jsBridge", Chain.Near); + const response = await bridge.request({ data: { method, params }, scope: "near" }); + return response?.result as Result | undefined; +} + +async function getNearWalletMethods() { + const result = await requestNear<{ accounts?: { accountId: string; publicKey: string }[] }>("near_requestAccounts"); + const account = result?.accounts?.[0]; + if (!account?.accountId) throw connectionRejectedError(Chain.Near); + + const network = await requestNear<{ networkId: string }>("near_network"); + if (network?.networkId !== "mainnet") throw mainnetOnlyError(Chain.Near); + + const { getNearToolbox } = await import("@swapkit/toolboxes/near"); + + const notSupported = (method: string) => + Promise.reject(new SwapKitError("wallet_near_method_not_supported", { method, wallet: WalletOption.ONEKEY })); + + // OneKey only signs and sends NEAR transactions, so the toolbox falls back to signAndSendTransactions + const signer = { + getAddress: () => Promise.resolve(account.accountId), + async getPublicKey() { + const { PublicKey } = await import("@near-js/crypto"); + return PublicKey.fromString(account.publicKey); + }, + async signAndSendTransactions({ transactions }: { transactions: { encode: () => Uint8Array }[] }) { + const response = await requestNear<{ transactionHashes?: string[] }>("near_requestSignTransactions", [ + { meta: {}, send: true, transactions: transactions.map((transaction) => base64.encode(transaction.encode())) }, + ]); + + if (!response?.transactionHashes?.length) throw signingError(Chain.Near, "OneKey returned no transaction hash"); + return response.transactionHashes; + }, + signDelegateAction: () => notSupported("signDelegateAction"), + signNep413Message: () => notSupported("signNep413Message"), + signTransaction: () => notSupported("signTransaction"), + }; + + return { ...getNearToolbox({ signer: signer as unknown as NearSigner }), address: account.accountId }; +} + +export function getWalletMethods(chain: Chain): Promise { + if ((ONEKEY_EVM_CHAINS as readonly Chain[]).includes(chain)) return getEvmWalletMethods(chain as EVMChain); + + switch (chain) { + case Chain.Bitcoin: + return getBitcoinWalletMethods(); + case Chain.Cardano: + return getCardanoWalletMethods(); + case Chain.Near: + return getNearWalletMethods(); + case Chain.Solana: + return getSolanaWalletMethods(); + case Chain.Stellar: + return getStellarWalletMethods(); + case Chain.Sui: + return getSuiWalletMethods(); + case Chain.Ton: + return getOneKeyTonWallet(); + case Chain.Tron: + return getTronWalletMethods(); + default: + throw new SwapKitError({ errorKey: "wallet_chain_not_supported", info: { chain, wallet: WalletOption.ONEKEY } }); + } +} diff --git a/packages/wallet-extensions/src/onekey/index.ts b/packages/wallet-extensions/src/onekey/index.ts index 873d1a1e..90ac8b33 100644 --- a/packages/wallet-extensions/src/onekey/index.ts +++ b/packages/wallet-extensions/src/onekey/index.ts @@ -1,130 +1,7 @@ -import { base64 } from "@scure/base"; -import { Chain, filterSupportedChains, prepareNetworkSwitch, SwapKitError, WalletOption } from "@swapkit/helpers"; -import { Transaction } from "@swapkit/utxo-signer"; +import { Chain, filterSupportedChains, SwapKitError, WalletOption } from "@swapkit/helpers"; import { createWallet, getWalletSupportedChains } from "@swapkit/wallet-core"; -import type { BitcoinProvider, GetAddressOptions, GetAddressResponse, SignTransactionOptions } from "sats-connect"; -import { getUtxoScriptTypeParams } from "../helpers/utxoScriptType"; import type { ExtensionWallet } from "../walletTypes"; - -async function getWalletMethodsForExtension(chain: Chain) { - switch (chain) { - case Chain.Bitcoin: { - if (!window.$onekey?.btc) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain } }); - } - - const { getUtxoToolbox } = await import("@swapkit/toolboxes/utxo"); - const { - signTransaction: satsSignTransaction, - getAddress, - AddressPurpose, - BitcoinNetworkType, - } = await import("sats-connect"); - - let address = ""; - - const getProvider = () => new Promise((res) => res(window.$onekey?.btc)); - - const getAddressOptions: GetAddressOptions = { - getProvider, - onCancel: () => { - throw new SwapKitError("wallet_connection_rejected_by_user"); - }, - onFinish: (response: GetAddressResponse) => { - if (response.addresses[0]?.address) { - address = response.addresses[0].address; - } - }, - payload: { - message: "Address for receiving and sending payments", - network: { type: BitcoinNetworkType.Mainnet }, - purposes: [AddressPurpose.Payment], - }, - }; - - await getAddress(getAddressOptions); - - async function signTransaction(tx: InstanceType) { - let signedTx: InstanceType | undefined; - const psbtBytes = tx.toPSBT(0); - const psbtBase64 = base64.encode(psbtBytes); - const inputCount = tx.inputsLength; - - const signPsbtOptions: SignTransactionOptions = { - getProvider, - onCancel: () => { - throw new SwapKitError("wallet_connection_rejected_by_user"); - }, - onFinish: (response) => { - signedTx = Transaction.fromPSBT(base64.decode(response.psbtBase64)); - }, - payload: { - broadcast: false, - inputsToSign: [{ address, signingIndexes: Array.from({ length: inputCount }, (_, i) => i) }], - message: "Sign transaction", - network: { type: BitcoinNetworkType.Mainnet }, - psbtBase64, - }, - }; - - await satsSignTransaction(signPsbtOptions); - if (!signedTx) throw new SwapKitError("wallet_onekey_sign_transaction_error"); - return signedTx; - } - - const signer = { getAddress: () => Promise.resolve(address), signTransaction }; - - const toolbox = await getUtxoToolbox(chain, { ...(await getUtxoScriptTypeParams({ address, chain })), signer }); - - return { ...toolbox, address }; - } - - case Chain.Solana: { - if (!window.$onekey?.sol) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain } }); - } - - const { getSolanaToolbox } = await import("@swapkit/toolboxes/solana"); - - const signer = window.$onekey.sol; - const address = await signer.getAddress(); - const toolbox = await getSolanaToolbox({ signer }); - - return { ...toolbox, address }; - } - - case Chain.Arbitrum: - case Chain.Aurora: - case Chain.Avalanche: - case Chain.Base: - case Chain.BinanceSmartChain: - case Chain.Ethereum: - case Chain.Gnosis: - case Chain.Optimism: - case Chain.Polygon: - case Chain.XLayer: { - const { getProvider, getEvmToolboxAsync } = await import("@swapkit/toolboxes/evm"); - if (!window.$onekey?.ethereum) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain } }); - } - - const { BrowserProvider } = await import("ethers"); - const provider = new BrowserProvider(window.$onekey.ethereum, "any"); - - await provider.send("eth_requestAccounts", []); - const jsonRpcProvider = await getProvider(chain); - const signer = await provider.getSigner(); - const address = await signer.getAddress(); - - const toolbox = await getEvmToolboxAsync(chain, { provider: jsonRpcProvider, signer }); - - return { address, ...prepareNetworkSwitch({ chain, provider, toolbox }) }; - } - - default: - throw new SwapKitError({ errorKey: "wallet_chain_not_supported", info: { chain, wallet: WalletOption.ONEKEY } }); - } -} +import { getWalletMethods } from "./helpers"; export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet({ connect: ({ addChain, walletType, supportedChains }) => @@ -137,11 +14,8 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet await Promise.all( filteredChains.map(async (chain) => { - const walletMethods = await getWalletMethodsForExtension(chain); - - const address = (await walletMethods.getAddress()) || "F"; - - addChain({ ...walletMethods, address, chain, walletType }); + const walletMethods = await getWalletMethods(chain); + addChain({ ...walletMethods, chain, walletType }); }), ); @@ -155,11 +29,17 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet [Chain.Base]: { direct: true, typedData: true }, [Chain.BinanceSmartChain]: { direct: true, typedData: true }, [Chain.Bitcoin]: { direct: true }, + [Chain.Cardano]: { direct: true }, [Chain.Ethereum]: { direct: true, typedData: true }, [Chain.Gnosis]: { direct: true, typedData: true }, + [Chain.Near]: { direct: true }, [Chain.Optimism]: { direct: true, typedData: true }, [Chain.Polygon]: { direct: true, typedData: true }, [Chain.Solana]: { direct: true }, + [Chain.Stellar]: { direct: true }, + [Chain.Sui]: { direct: true }, + [Chain.Ton]: { direct: true }, + [Chain.Tron]: { direct: true }, [Chain.XLayer]: { direct: true, typedData: true }, }, supportedChains: [ @@ -169,11 +49,17 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet Chain.Base, Chain.BinanceSmartChain, Chain.Bitcoin, + Chain.Cardano, Chain.Ethereum, Chain.Gnosis, + Chain.Near, Chain.Optimism, Chain.Polygon, Chain.Solana, + Chain.Stellar, + Chain.Sui, + Chain.Ton, + Chain.Tron, Chain.XLayer, ], walletType: WalletOption.ONEKEY, diff --git a/packages/wallet-extensions/src/onekey/ton.ts b/packages/wallet-extensions/src/onekey/ton.ts new file mode 100644 index 00000000..62389d6b --- /dev/null +++ b/packages/wallet-extensions/src/onekey/ton.ts @@ -0,0 +1,127 @@ +import { AssetValue, Chain, type GenericTransferParams, SKConfig, SwapKitError, WalletOption } from "@swapkit/helpers"; +import type { TONTransactionInput } from "@swapkit/toolboxes/ton"; +import type { OneKeyTonConnectBridge } from "./types"; + +const TON_MAINNET = "-239"; +const SUPPORTED_SEND_MODE = 3; +const VALID_SECONDS = 300; + +export async function getOneKeyTonWallet() { + const bridge = getBridge(); + const rawAddress = await connect(bridge); + const { Address, Cell } = await import("@ton/core"); + const address = Address.parse(rawAddress).toString({ bounceable: false, urlSafe: true }); + + const { getTONToolbox } = await import("@swapkit/toolboxes/ton"); + const toolbox = getTONToolbox(); + + async function signAndBroadcastTransaction(transaction: TONTransactionInput) { + const messages = Array.isArray(transaction) ? transaction : transaction.messages; + const sendMode = Array.isArray(transaction) ? transaction[0]?.sendMode : transaction.sendMode; + + if (sendMode !== undefined && sendMode !== SUPPORTED_SEND_MODE) { + throw new SwapKitError({ + errorKey: "core_swap_invalid_params", + info: { reason: "OneKey does not support this TON send mode", sendMode }, + }); + } + + // OneKey rejects raw `0:` addresses, so destinations are sent in user-friendly form + const payload = { + from: rawAddress, + messages: messages.map(({ address: destination, amount, payload, stateInit }) => ({ + address: Address.isFriendly(destination) + ? destination + : Address.parse(destination).toString({ bounceable: true, urlSafe: true }), + amount, + payload, + stateInit, + })), + network: TON_MAINNET, + valid_until: Math.floor(Date.now() / 1000) + VALID_SECONDS, + }; + + const response = await bridge.send({ + id: Date.now().toString(), + method: "sendTransaction", + params: [JSON.stringify(payload)], + }); + + if (!response?.result) { + throw new SwapKitError("core_transaction_failed", { error: response?.error }); + } + + return Cell.fromBase64(response.result).hash().toString("hex"); + } + + async function transfer({ assetValue, recipient, memo, sweep }: GenericTransferParams) { + if (sweep && assetValue.isGasAsset) { + throw new SwapKitError({ + errorKey: "core_swap_invalid_params", + info: { reason: "OneKey does not support sweeping the TON balance" }, + }); + } + + const transaction = await toolbox.createTransaction({ assetValue, memo, recipient, sender: address, sweep }); + return signAndBroadcastTransaction(transaction); + } + + // Same budget as TON Connect: the signerless toolbox underestimates jetton fees + function estimateTransactionFee({ assetValue }: GenericTransferParams) { + return Promise.resolve(AssetValue.from({ chain: Chain.Ton, value: assetValue.isGasAsset ? "0.01" : "0.06" })); + } + + return { + ...toolbox, + address, + estimateTransactionFee, + getAddress: () => address, + getBalance: () => toolbox.getBalance(address), + signAndBroadcastTransaction, + transfer, + }; +} + +function getBridge(): OneKeyTonConnectBridge { + const bridge = window.$onekey?.tonconnect; + if (!bridge) { + throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: Chain.Ton } }); + } + + return bridge; +} + +async function connect(bridge: OneKeyTonConnectBridge) { + const restored = await bridge.restoreConnection().catch(() => undefined); + const event = restored?.event === "connect" ? restored : await requestConnection(bridge); + + if (event.event !== "connect") { + throw new SwapKitError("wallet_connection_rejected_by_user", { + message: event.payload.message, + wallet: WalletOption.ONEKEY, + }); + } + + const account = event.payload.items.find((item) => item.name === "ton_addr"); + if (!account?.address) { + throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: Chain.Ton } }); + } + + if (account.network !== TON_MAINNET) { + throw new SwapKitError({ + errorKey: "wallet_chain_not_supported", + info: { chain: Chain.Ton, message: "Mainnet only", wallet: WalletOption.ONEKEY }, + }); + } + + return account.address; +} + +function requestConnection(bridge: OneKeyTonConnectBridge) { + const manifestUrl = SKConfig.get("integrations").tonConnect?.manifestUrl; + if (!manifestUrl) { + throw new SwapKitError("wallet_missing_params", { param: "manifestUrl", wallet: WalletOption.ONEKEY }); + } + + return bridge.connect(2, { items: [{ name: "ton_addr" }], manifestUrl }); +} diff --git a/packages/wallet-extensions/src/onekey/types.ts b/packages/wallet-extensions/src/onekey/types.ts new file mode 100644 index 00000000..dc9da645 --- /dev/null +++ b/packages/wallet-extensions/src/onekey/types.ts @@ -0,0 +1,76 @@ +import type { SolanaProvider } from "@swapkit/toolboxes/solana"; +import type { TronSignedTransaction, TronTransaction } from "@swapkit/toolboxes/tron"; +import type { Eip1193Provider } from "ethers"; +import type { OkxTonConnectEvent } from "../types"; + +export type OneKeyJsBridge = { + request: (payload: { + data: { method: string; params?: unknown }; + scope: string; + }) => Promise<{ result?: unknown } | undefined>; +}; + +export type OneKeyBtcProvider = { + getNetwork: () => Promise; + requestAccounts: () => Promise; + signPsbt: (psbtHex: string, options: { autoFinalized: boolean }) => Promise; +}; + +export type OneKeySuiAccount = { address: string; publicKey: string }; + +export type OneKeySuiProvider = { + getAccounts: () => Promise; + signTransaction: (input: { + account: OneKeySuiAccount; + chain: string; + transaction: { serialize: () => string }; + }) => Promise<{ bytes: string; signature: string }>; +}; + +export type OneKeyTronProvider = { + request: (args: { method: string }) => Promise<{ code: number; message: string }>; + tronWeb?: { + defaultAddress?: { base58?: string | false }; + trx: { sign: (transaction: TronTransaction) => Promise }; + }; +}; + +export type OneKeyTonConnectBridge = { + connect: ( + protocolVersion: number, + request: { manifestUrl: string; items: { name: string }[] }, + ) => Promise; + restoreConnection: () => Promise; + send: (request: { + id: string; + method: string; + params: string[]; + }) => Promise<{ id: string; result?: string; error?: { code: number; message: string } }>; +}; + +export type OneKeyCardanoApi = { + getChangeAddress: () => Promise; + getNetworkId: () => Promise; + signTx: (transaction: string, partialSign?: boolean) => Promise; +}; + +export type OneKeyStellarProvider = { + getAddress: () => Promise<{ address: string }>; + getNetwork: () => Promise<{ network: string; networkPassphrase: string }>; + signTransaction: ( + xdr: string, + options: { networkPassphrase: string }, + ) => Promise<{ signedTxXdr: string; signerAddress?: string }>; +}; + +export type OneKeyProviders = { + btc?: OneKeyBtcProvider; + cardano?: { enable: () => Promise }; + ethereum?: Eip1193Provider; + jsBridge?: OneKeyJsBridge; + solana?: SolanaProvider; + stellar?: OneKeyStellarProvider; + sui?: OneKeySuiProvider; + tonconnect?: OneKeyTonConnectBridge; + tron?: OneKeyTronProvider; +}; diff --git a/packages/wallet-extensions/src/types.ts b/packages/wallet-extensions/src/types.ts index b1b5cf31..094c02e2 100644 --- a/packages/wallet-extensions/src/types.ts +++ b/packages/wallet-extensions/src/types.ts @@ -64,7 +64,7 @@ declare global { interface Window { injectedWeb3?: SubstrateInjectedExtension; talismanEth?: EthereumWindowProvider; - $onekey?: any; + $onekey?: import("./onekey/types").OneKeyProviders; braveSolana: any; coinbaseWalletExtension: EthereumWindowProvider; ethereum: EthereumWindowProvider; From 65fad9ed730d87f7b888c296855a5f96bba52dfc Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Thu, 29 Oct 2026 17:22:36 +0000 Subject: [PATCH 02/12] refactor(wallet-extensions): share the TON Connect flow between OKX and OneKey Both wallets talk to the same TON Connect bridge, so the connect, send and transfer logic moves to one helper. Each wallet passes its own provider and errors, so OKX keeps its existing error keys. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .../src/helpers/tonConnect.ts | 118 ++++++++++++++++ packages/wallet-extensions/src/okx/ton.ts | 123 ++-------------- packages/wallet-extensions/src/onekey/ton.ts | 131 ++---------------- .../wallet-extensions/src/onekey/types.ts | 17 +-- packages/wallet-extensions/src/types.ts | 10 +- 5 files changed, 146 insertions(+), 253 deletions(-) create mode 100644 packages/wallet-extensions/src/helpers/tonConnect.ts diff --git a/packages/wallet-extensions/src/helpers/tonConnect.ts b/packages/wallet-extensions/src/helpers/tonConnect.ts new file mode 100644 index 00000000..ec19c761 --- /dev/null +++ b/packages/wallet-extensions/src/helpers/tonConnect.ts @@ -0,0 +1,118 @@ +import { AssetValue, Chain, type GenericTransferParams, SKConfig, SwapKitError } from "@swapkit/helpers"; +import type { TONTransactionInput } from "@swapkit/toolboxes/ton"; +import type { TonConnectBridge } from "../types"; + +const TON_MAINNET = "-239"; +const SUPPORTED_SEND_MODE = 3; +const VALID_SECONDS = 300; + +type TonConnectWalletParams = { + bridge: TonConnectBridge; + wallet: string; + noAccountError: () => SwapKitError; + mainnetOnlyError: () => SwapKitError; +}; + +export async function getTonConnectWallet({ + bridge, + wallet, + noAccountError, + mainnetOnlyError, +}: TonConnectWalletParams) { + const rawAddress = await connect({ bridge, mainnetOnlyError, noAccountError, wallet }); + const { Address, Cell } = await import("@ton/core"); + const address = Address.parse(rawAddress).toString({ bounceable: false, urlSafe: true }); + + const { getTONToolbox } = await import("@swapkit/toolboxes/ton"); + const toolbox = getTONToolbox(); + + async function signAndBroadcastTransaction(transaction: TONTransactionInput) { + const messages = Array.isArray(transaction) ? transaction : transaction.messages; + const sendMode = Array.isArray(transaction) ? transaction[0]?.sendMode : transaction.sendMode; + + if (sendMode !== undefined && sendMode !== SUPPORTED_SEND_MODE) { + throw new SwapKitError({ + errorKey: "core_swap_invalid_params", + info: { reason: `${wallet} does not support this TON send mode`, sendMode }, + }); + } + + // TON Connect wallets reject raw `0:` addresses, so destinations are sent in user-friendly form + const payload = { + from: rawAddress, + messages: messages.map(({ address: destination, amount, payload, stateInit }) => ({ + address: Address.isFriendly(destination) + ? destination + : Address.parse(destination).toString({ bounceable: true, urlSafe: true }), + amount, + payload, + stateInit, + })), + network: TON_MAINNET, + valid_until: Math.floor(Date.now() / 1000) + VALID_SECONDS, + }; + + const response = await bridge.send({ + id: Date.now().toString(), + method: "sendTransaction", + params: [JSON.stringify(payload)], + }); + + if (!response.result) { + throw new SwapKitError("core_transaction_failed", { error: response.error }); + } + + return Cell.fromBase64(response.result).hash().toString("hex"); + } + + async function transfer({ assetValue, recipient, memo, sweep }: GenericTransferParams) { + if (sweep && assetValue.isGasAsset) { + throw new SwapKitError({ + errorKey: "core_swap_invalid_params", + info: { reason: `${wallet} does not support sweeping the TON balance` }, + }); + } + + const transaction = await toolbox.createTransaction({ assetValue, memo, recipient, sender: address, sweep }); + return signAndBroadcastTransaction(transaction); + } + + // Same budget as TON Connect: the signerless toolbox underestimates jetton fees + function estimateTransactionFee({ assetValue }: GenericTransferParams) { + return Promise.resolve(AssetValue.from({ chain: Chain.Ton, value: assetValue.isGasAsset ? "0.01" : "0.06" })); + } + + return { + ...toolbox, + address, + estimateTransactionFee, + getAddress: () => address, + getBalance: () => toolbox.getBalance(address), + signAndBroadcastTransaction, + transfer, + }; +} + +async function connect({ bridge, wallet, noAccountError, mainnetOnlyError }: TonConnectWalletParams) { + const restored = await bridge.restoreConnection().catch(() => undefined); + const event = restored?.event === "connect" ? restored : await requestConnection(bridge, wallet); + + if (event.event !== "connect") { + throw new SwapKitError("wallet_connection_rejected_by_user", { message: event.payload.message, wallet }); + } + + const account = event.payload.items.find((item) => item.name === "ton_addr"); + if (!account?.address) throw noAccountError(); + if (account.network !== TON_MAINNET) throw mainnetOnlyError(); + + return account.address; +} + +function requestConnection(bridge: TonConnectBridge, wallet: string) { + const manifestUrl = SKConfig.get("integrations").tonConnect?.manifestUrl; + if (!manifestUrl) { + throw new SwapKitError("wallet_missing_params", { param: "manifestUrl", wallet }); + } + + return bridge.connect(2, { items: [{ name: "ton_addr" }], manifestUrl }); +} diff --git a/packages/wallet-extensions/src/okx/ton.ts b/packages/wallet-extensions/src/okx/ton.ts index 9fcc3deb..99e73e8f 100644 --- a/packages/wallet-extensions/src/okx/ton.ts +++ b/packages/wallet-extensions/src/okx/ton.ts @@ -1,122 +1,17 @@ -import { AssetValue, Chain, type GenericTransferParams, SKConfig, SwapKitError } from "@swapkit/helpers"; -import type { TONTransactionInput } from "@swapkit/toolboxes/ton"; -import type { OkxTonConnectEvent } from "../types"; - -const TON_MAINNET = "-239"; -const SUPPORTED_SEND_MODE = 3; -const VALID_SECONDS = 300; - -type OkxTonConnectBridge = NonNullable["tonconnect"]; +import { Chain, SwapKitError } from "@swapkit/helpers"; +import { getTonConnectWallet } from "../helpers/tonConnect"; export async function getOkxTonWallet() { - const bridge = getBridge(); - const rawAddress = await connect(bridge); - const { Address, Cell } = await import("@ton/core"); - const address = Address.parse(rawAddress).toString({ bounceable: false, urlSafe: true }); - - const { getTONToolbox } = await import("@swapkit/toolboxes/ton"); - const toolbox = getTONToolbox(); - - async function signAndBroadcastTransaction(transaction: TONTransactionInput) { - const messages = Array.isArray(transaction) ? transaction : transaction.messages; - const sendMode = Array.isArray(transaction) ? transaction[0]?.sendMode : transaction.sendMode; - - if (sendMode !== undefined && sendMode !== SUPPORTED_SEND_MODE) { - throw new SwapKitError({ - errorKey: "core_swap_invalid_params", - info: { reason: "OKX Wallet does not support this TON send mode", sendMode }, - }); - } - - const payload = { - from: rawAddress, - messages: messages.map(({ address: destination, amount, payload, stateInit }) => ({ - address: Address.isFriendly(destination) - ? destination - : Address.parse(destination).toString({ bounceable: true, urlSafe: true }), - amount, - payload, - stateInit, - })), - network: TON_MAINNET, - valid_until: Math.floor(Date.now() / 1000) + VALID_SECONDS, - }; - - const response = await bridge.send({ - id: Date.now().toString(), - method: "sendTransaction", - params: [JSON.stringify(payload)], - }); - - if (!response.result) { - throw new SwapKitError("core_transaction_failed", { error: response.error }); - } - - return Cell.fromBase64(response.result).hash().toString("hex"); - } - - async function transfer({ assetValue, recipient, memo, sweep }: GenericTransferParams) { - if (sweep && assetValue.isGasAsset) { - throw new SwapKitError({ - errorKey: "core_swap_invalid_params", - info: { reason: "OKX Wallet does not support sweeping the TON balance" }, - }); - } - - const transaction = await toolbox.createTransaction({ assetValue, memo, recipient, sender: address, sweep }); - return signAndBroadcastTransaction(transaction); - } - - // Same budget as TON Connect: the signerless toolbox underestimates jetton fees - function estimateTransactionFee({ assetValue }: GenericTransferParams) { - return Promise.resolve(AssetValue.from({ chain: Chain.Ton, value: assetValue.isGasAsset ? "0.01" : "0.06" })); - } - - return { - ...toolbox, - address, - estimateTransactionFee, - getAddress: () => address, - getBalance: () => toolbox.getBalance(address), - signAndBroadcastTransaction, - transfer, - }; -} - -function getBridge(): OkxTonConnectBridge { const bridge = window.okxTonWallet?.tonconnect; if (!bridge) { throw new SwapKitError("wallet_okx_not_found", { chain: Chain.Ton }); } - return bridge; -} - -async function connect(bridge: OkxTonConnectBridge) { - const restored = await bridge.restoreConnection().catch(() => undefined); - const event = restored?.event === "connect" ? restored : await requestConnection(bridge); - - if (event.event !== "connect") { - throw new SwapKitError("wallet_connection_rejected_by_user", { message: event.payload.message, wallet: "OKX" }); - } - - const account = event.payload.items.find((item) => item.name === "ton_addr"); - if (!account?.address) { - throw new SwapKitError("wallet_okx_no_accounts", { chain: Chain.Ton }); - } - - if (account.network !== TON_MAINNET) { - throw new SwapKitError("wallet_okx_chain_not_supported", { chain: Chain.Ton, message: "Mainnet only" }); - } - - return account.address; -} - -function requestConnection(bridge: OkxTonConnectBridge): Promise { - const manifestUrl = SKConfig.get("integrations").tonConnect?.manifestUrl; - if (!manifestUrl) { - throw new SwapKitError("wallet_missing_params", { param: "manifestUrl", wallet: "OKX" }); - } - - return bridge.connect(2, { items: [{ name: "ton_addr" }], manifestUrl }); + return await getTonConnectWallet({ + bridge, + mainnetOnlyError: () => + new SwapKitError("wallet_okx_chain_not_supported", { chain: Chain.Ton, message: "Mainnet only" }), + noAccountError: () => new SwapKitError("wallet_okx_no_accounts", { chain: Chain.Ton }), + wallet: "OKX", + }); } diff --git a/packages/wallet-extensions/src/onekey/ton.ts b/packages/wallet-extensions/src/onekey/ton.ts index 62389d6b..422218b9 100644 --- a/packages/wallet-extensions/src/onekey/ton.ts +++ b/packages/wallet-extensions/src/onekey/ton.ts @@ -1,127 +1,20 @@ -import { AssetValue, Chain, type GenericTransferParams, SKConfig, SwapKitError, WalletOption } from "@swapkit/helpers"; -import type { TONTransactionInput } from "@swapkit/toolboxes/ton"; -import type { OneKeyTonConnectBridge } from "./types"; - -const TON_MAINNET = "-239"; -const SUPPORTED_SEND_MODE = 3; -const VALID_SECONDS = 300; +import { Chain, SwapKitError, WalletOption } from "@swapkit/helpers"; +import { getTonConnectWallet } from "../helpers/tonConnect"; export async function getOneKeyTonWallet() { - const bridge = getBridge(); - const rawAddress = await connect(bridge); - const { Address, Cell } = await import("@ton/core"); - const address = Address.parse(rawAddress).toString({ bounceable: false, urlSafe: true }); - - const { getTONToolbox } = await import("@swapkit/toolboxes/ton"); - const toolbox = getTONToolbox(); - - async function signAndBroadcastTransaction(transaction: TONTransactionInput) { - const messages = Array.isArray(transaction) ? transaction : transaction.messages; - const sendMode = Array.isArray(transaction) ? transaction[0]?.sendMode : transaction.sendMode; - - if (sendMode !== undefined && sendMode !== SUPPORTED_SEND_MODE) { - throw new SwapKitError({ - errorKey: "core_swap_invalid_params", - info: { reason: "OneKey does not support this TON send mode", sendMode }, - }); - } - - // OneKey rejects raw `0:` addresses, so destinations are sent in user-friendly form - const payload = { - from: rawAddress, - messages: messages.map(({ address: destination, amount, payload, stateInit }) => ({ - address: Address.isFriendly(destination) - ? destination - : Address.parse(destination).toString({ bounceable: true, urlSafe: true }), - amount, - payload, - stateInit, - })), - network: TON_MAINNET, - valid_until: Math.floor(Date.now() / 1000) + VALID_SECONDS, - }; - - const response = await bridge.send({ - id: Date.now().toString(), - method: "sendTransaction", - params: [JSON.stringify(payload)], - }); - - if (!response?.result) { - throw new SwapKitError("core_transaction_failed", { error: response?.error }); - } - - return Cell.fromBase64(response.result).hash().toString("hex"); - } - - async function transfer({ assetValue, recipient, memo, sweep }: GenericTransferParams) { - if (sweep && assetValue.isGasAsset) { - throw new SwapKitError({ - errorKey: "core_swap_invalid_params", - info: { reason: "OneKey does not support sweeping the TON balance" }, - }); - } - - const transaction = await toolbox.createTransaction({ assetValue, memo, recipient, sender: address, sweep }); - return signAndBroadcastTransaction(transaction); - } - - // Same budget as TON Connect: the signerless toolbox underestimates jetton fees - function estimateTransactionFee({ assetValue }: GenericTransferParams) { - return Promise.resolve(AssetValue.from({ chain: Chain.Ton, value: assetValue.isGasAsset ? "0.01" : "0.06" })); - } - - return { - ...toolbox, - address, - estimateTransactionFee, - getAddress: () => address, - getBalance: () => toolbox.getBalance(address), - signAndBroadcastTransaction, - transfer, - }; -} - -function getBridge(): OneKeyTonConnectBridge { const bridge = window.$onekey?.tonconnect; if (!bridge) { throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: Chain.Ton } }); } - return bridge; -} - -async function connect(bridge: OneKeyTonConnectBridge) { - const restored = await bridge.restoreConnection().catch(() => undefined); - const event = restored?.event === "connect" ? restored : await requestConnection(bridge); - - if (event.event !== "connect") { - throw new SwapKitError("wallet_connection_rejected_by_user", { - message: event.payload.message, - wallet: WalletOption.ONEKEY, - }); - } - - const account = event.payload.items.find((item) => item.name === "ton_addr"); - if (!account?.address) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: Chain.Ton } }); - } - - if (account.network !== TON_MAINNET) { - throw new SwapKitError({ - errorKey: "wallet_chain_not_supported", - info: { chain: Chain.Ton, message: "Mainnet only", wallet: WalletOption.ONEKEY }, - }); - } - - return account.address; -} - -function requestConnection(bridge: OneKeyTonConnectBridge) { - const manifestUrl = SKConfig.get("integrations").tonConnect?.manifestUrl; - if (!manifestUrl) { - throw new SwapKitError("wallet_missing_params", { param: "manifestUrl", wallet: WalletOption.ONEKEY }); - } - - return bridge.connect(2, { items: [{ name: "ton_addr" }], manifestUrl }); + return await getTonConnectWallet({ + bridge, + mainnetOnlyError: () => + new SwapKitError({ + errorKey: "wallet_chain_not_supported", + info: { chain: Chain.Ton, message: "Mainnet only", wallet: WalletOption.ONEKEY }, + }), + noAccountError: () => new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: Chain.Ton } }), + wallet: WalletOption.ONEKEY, + }); } diff --git a/packages/wallet-extensions/src/onekey/types.ts b/packages/wallet-extensions/src/onekey/types.ts index dc9da645..b1b228f0 100644 --- a/packages/wallet-extensions/src/onekey/types.ts +++ b/packages/wallet-extensions/src/onekey/types.ts @@ -1,7 +1,7 @@ import type { SolanaProvider } from "@swapkit/toolboxes/solana"; import type { TronSignedTransaction, TronTransaction } from "@swapkit/toolboxes/tron"; import type { Eip1193Provider } from "ethers"; -import type { OkxTonConnectEvent } from "../types"; +import type { TonConnectBridge } from "../types"; export type OneKeyJsBridge = { request: (payload: { @@ -35,19 +35,6 @@ export type OneKeyTronProvider = { }; }; -export type OneKeyTonConnectBridge = { - connect: ( - protocolVersion: number, - request: { manifestUrl: string; items: { name: string }[] }, - ) => Promise; - restoreConnection: () => Promise; - send: (request: { - id: string; - method: string; - params: string[]; - }) => Promise<{ id: string; result?: string; error?: { code: number; message: string } }>; -}; - export type OneKeyCardanoApi = { getChangeAddress: () => Promise; getNetworkId: () => Promise; @@ -71,6 +58,6 @@ export type OneKeyProviders = { solana?: SolanaProvider; stellar?: OneKeyStellarProvider; sui?: OneKeySuiProvider; - tonconnect?: OneKeyTonConnectBridge; + tonconnect?: TonConnectBridge; tron?: OneKeyTronProvider; }; diff --git a/packages/wallet-extensions/src/types.ts b/packages/wallet-extensions/src/types.ts index 094c02e2..613f4621 100644 --- a/packages/wallet-extensions/src/types.ts +++ b/packages/wallet-extensions/src/types.ts @@ -22,16 +22,16 @@ export type OkxCardanoApi = { signTx: (transaction: string, partialSign?: boolean) => Promise; }; -export type OkxTonConnectEvent = +export type TonConnectEvent = | { event: "connect"; payload: { items: { name: string; address?: string; network?: string }[] } } | { event: "connect_error"; payload: { code: number; message: string } }; -type OkxTonConnectBridge = { +export type TonConnectBridge = { connect: ( protocolVersion: number, request: { manifestUrl: string; items: { name: string }[] }, - ) => Promise; - restoreConnection: () => Promise; + ) => Promise; + restoreConnection: () => Promise; send: (request: { id: string; method: string; @@ -183,6 +183,6 @@ declare global { } | EthereumWindowProvider; tronLink?: TronLinkWindow; - okxTonWallet?: { tonconnect: OkxTonConnectBridge }; + okxTonWallet?: { tonconnect: TonConnectBridge }; } } From 64941e05e1495b8e4c1255e342768ce9067ef0c5 Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 10:17:05 +0000 Subject: [PATCH 03/12] fix(onekey): connect one network at a time and stop when the user cancels OneKey queues every pending approval, so requesting all networks at once kept opening windows after the user cancelled one. Networks are now requested in order; a cancel stops the loop and keeps the ones already approved, as OKX does. Other failures skip that network with a warning. The TON Connect helper keeps the wallet's error code on a rejected connection, so a manifest error is no longer mistaken for a cancel. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .changeset/onekey-multichain-support.md | 2 + .../src/helpers/tonConnect.ts | 5 +- .../src/onekey/__tests__/onekey.test.ts | 60 ++++++++++++++++++- .../wallet-extensions/src/onekey/index.ts | 35 +++++++++-- 4 files changed, 96 insertions(+), 6 deletions(-) diff --git a/.changeset/onekey-multichain-support.md b/.changeset/onekey-multichain-support.md index 6a168b10..b8052f5b 100644 --- a/.changeset/onekey-multichain-support.md +++ b/.changeset/onekey-multichain-support.md @@ -3,3 +3,5 @@ --- OneKey now connects Sui, Stellar, TON, TRON, Cardano and NEAR on mainnet, alongside its EVM chains. Bitcoin and Solana connections work again: they were looking for wallet providers the OneKey extension never exposes, so connecting either chain always failed. A Sui, TRON or Stellar signature from OneKey is rejected if it covers a different transaction than the one requested. + +Connecting OneKey now asks for one network at a time. If the user cancels an approval, no further approval windows open; networks approved before the cancel stay connected. A network OneKey refuses to connect for a reason other than a cancel, such as Bitcoin with multiple addresses enabled or a TON Connect manifest for another domain, is skipped and the rest still connect. diff --git a/packages/wallet-extensions/src/helpers/tonConnect.ts b/packages/wallet-extensions/src/helpers/tonConnect.ts index ec19c761..123eb2f0 100644 --- a/packages/wallet-extensions/src/helpers/tonConnect.ts +++ b/packages/wallet-extensions/src/helpers/tonConnect.ts @@ -98,7 +98,10 @@ async function connect({ bridge, wallet, noAccountError, mainnetOnlyError }: Ton const event = restored?.event === "connect" ? restored : await requestConnection(bridge, wallet); if (event.event !== "connect") { - throw new SwapKitError("wallet_connection_rejected_by_user", { message: event.payload.message, wallet }); + throw new SwapKitError({ + errorKey: "wallet_connection_rejected_by_user", + info: { code: event.payload.code, message: event.payload.message, wallet }, + }); } const account = event.payload.items.find((item) => item.name === "ton_addr"); diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index 669052ab..e007a515 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -5,9 +5,10 @@ import { KeyPair } from "@near-js/crypto"; import { actionCreators, createTransaction } from "@near-js/transactions"; import { base64 } from "@scure/base"; import { Account, Asset, Keypair, Networks, Operation, TransactionBuilder } from "@stellar/stellar-sdk"; -import { Chain } from "@swapkit/helpers"; +import { Chain, SKConfig } from "@swapkit/helpers"; import { getWalletMethods } from "../helpers"; +import { onekeyWallet } from "../index"; const SUI_ADDRESS = `0x${"ab".repeat(32)}`; const TRON_ADDRESS = "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"; @@ -191,3 +192,60 @@ test("connects Solana through the provider OneKey injects", async () => { expect(wallet.address).toBe(address); }); + +describe("connectOnekeyWallet", () => { + const SOLANA_ADDRESS = "4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T"; + const solana = { connect: () => Promise.resolve({ publicKey: { toString: () => SOLANA_ADDRESS } }) }; + + function connect(chains: Chain[]) { + const added = []; + const result = onekeyWallet.connectOnekeyWallet.connectWallet({ addChain: (wallet) => added.push(wallet) })(chains); + return { added, result }; + } + + test("stops asking for more chains once the user rejects one", async () => { + const rejection = Object.assign(new Error("User rejected the request."), { code: 4001 }); + let suiRequested = false; + globalThis.window = { + $onekey: { + solana: { connect: () => Promise.reject(rejection) }, + sui: { + getAccounts: () => { + suiRequested = true; + return Promise.resolve([{ address: SUI_ADDRESS, publicKey: "" }]); + }, + }, + }, + }; + + await expect(connect([Chain.Solana, Chain.Sui]).result).rejects.toBe(rejection); + expect(suiRequested).toBe(false); + }); + + test("skips a chain OneKey refuses to connect and keeps the rest", async () => { + const freshAddressError = Object.assign(new Error("Connection requires single address mode."), { code: -99999 }); + globalThis.window = { $onekey: { btc: { requestAccounts: () => Promise.reject(freshAddressError) }, solana } }; + const { added, result } = connect([Chain.Bitcoin, Chain.Solana]); + + expect(await result).toBe(true); + expect(added.map(({ chain }) => chain)).toEqual([Chain.Solana]); + }); + + test("does not treat a TON Connect manifest error as a user rejection", async () => { + SKConfig.set({ integrations: { tonConnect: { manifestUrl: "https://app.example/manifest.json" } } }); + globalThis.window = { + $onekey: { + solana, + tonconnect: { + connect: () => + Promise.resolve({ event: "connect_error", payload: { code: 3, message: "App manifest content error" } }), + restoreConnection: () => Promise.reject(new Error("No session")), + }, + }, + }; + const { added, result } = connect([Chain.Ton, Chain.Solana]); + + expect(await result).toBe(true); + expect(added.map(({ chain }) => chain)).toEqual([Chain.Solana]); + }); +}); diff --git a/packages/wallet-extensions/src/onekey/index.ts b/packages/wallet-extensions/src/onekey/index.ts index 90ac8b33..b349ac78 100644 --- a/packages/wallet-extensions/src/onekey/index.ts +++ b/packages/wallet-extensions/src/onekey/index.ts @@ -12,12 +12,24 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet const filteredChains = filterSupportedChains({ chains, supportedChains, walletType }); - await Promise.all( - filteredChains.map(async (chain) => { + const failedChains: unknown[] = []; + + // OneKey queues every pending approval, so asking one chain at a time lets a cancel stop the rest + for (const chain of filteredChains) { + try { const walletMethods = await getWalletMethods(chain); addChain({ ...walletMethods, chain, walletType }); - }), - ); + } catch (error) { + if (isUserRejection(error)) throw error; + failedChains.push(error); + } + } + + if (failedChains.length > 0 && failedChains.length === filteredChains.length) throw failedChains[0]; + + for (const error of failedChains) { + console.warn("connectOnekeyWallet: skipping a chain OneKey cannot connect", error); + } return true; }, @@ -66,3 +78,18 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet }); export const ONEKEY_WALLET_SUPPORTED_CHAINS = getWalletSupportedChains(onekeyWallet); + +// TON Connect reports a rejected approval as 300 and manifest problems with other codes +const TON_CONNECT_USER_REJECTS_ERROR = 300; + +function isUserRejection(error: unknown) { + if (error instanceof SwapKitError) { + const code = error.info?.code; + return ( + error.errorKey === "wallet_connection_rejected_by_user" && + (code === undefined || code === TON_CONNECT_USER_REJECTS_ERROR) + ); + } + + return (error as { code?: unknown } | null)?.code === 4001; +} From 5de1d6611e86b6de202e7352015f8f77bd9613c6 Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 10:25:19 +0000 Subject: [PATCH 04/12] test(onekey): reset the TON Connect manifest after the manifest error test The test left a manifestUrl in the shared config, so the OKX test that expects no manifest failed whenever it ran afterwards. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .../wallet-extensions/src/onekey/__tests__/onekey.test.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index e007a515..6c9281e8 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -245,7 +245,11 @@ describe("connectOnekeyWallet", () => { }; const { added, result } = connect([Chain.Ton, Chain.Solana]); - expect(await result).toBe(true); - expect(added.map(({ chain }) => chain)).toEqual([Chain.Solana]); + try { + expect(await result).toBe(true); + expect(added.map(({ chain }) => chain)).toEqual([Chain.Solana]); + } finally { + SKConfig.set({ integrations: { tonConnect: undefined } }); + } }); }); From 3aa83bdb41bc3fa74f2e19b2a66598ab714dc11e Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 10:40:42 +0000 Subject: [PATCH 05/12] fix(onekey): read the TRON address from OneKey instead of its injected tronWeb When a site was already connected, tron_requestAccounts answered at once but the injected tronWeb still had no default address, so TRON reported a user rejection and stopped the whole connection. The address now comes from tron_accounts and signing goes through the provider's sign method. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .../wallet-extensions/src/onekey/__tests__/onekey.test.ts | 7 +++++-- packages/wallet-extensions/src/onekey/helpers.ts | 8 ++++---- packages/wallet-extensions/src/onekey/types.ts | 8 ++++---- 3 files changed, 13 insertions(+), 10 deletions(-) diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index 6c9281e8..a3dc76c6 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -73,8 +73,11 @@ describe("OneKey Tron signer", () => { globalThis.window = { $onekey: { tron: { - request: async () => ({ code: 200, message: "ok" }), - tronWeb: { defaultAddress: { base58: TRON_ADDRESS }, trx: { sign } }, + request: ({ method }) => + Promise.resolve(method === "tron_accounts" ? [TRON_ADDRESS] : { code: 200, message: "ok" }), + sign, + // OneKey's injected tronWeb can still have no address right after an existing connection + tronWeb: { defaultAddress: { base58: false } }, }, }, }; diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index c298eb07..6ae849a9 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -127,16 +127,16 @@ async function getTronWalletMethods() { const response = await provider.request({ method: "tron_requestAccounts" }); if (response.code !== 200) throw connectionRejectedError(Chain.Tron); - const tronWeb = provider.tronWeb; - const address = tronWeb?.defaultAddress?.base58; - if (!(tronWeb && address)) throw connectionRejectedError(Chain.Tron); + // The injected tronWeb can lag behind an existing connection, so the address comes from OneKey itself + const [address] = await provider.request({ method: "tron_accounts" }); + if (!address) throw connectionRejectedError(Chain.Tron); const { getTronToolbox } = await import("@swapkit/toolboxes/tron"); const signer: TronSigner = { getAddress: () => Promise.resolve(address), signTransaction: async (transaction) => { - const { signature, txID } = await tronWeb.trx.sign(transaction); + const { signature, txID } = await provider.sign(transaction); if (txID !== transaction.txID) { throw signingError(Chain.Tron, "OneKey signed a different transaction than the one requested"); diff --git a/packages/wallet-extensions/src/onekey/types.ts b/packages/wallet-extensions/src/onekey/types.ts index b1b228f0..e157cc71 100644 --- a/packages/wallet-extensions/src/onekey/types.ts +++ b/packages/wallet-extensions/src/onekey/types.ts @@ -28,11 +28,11 @@ export type OneKeySuiProvider = { }; export type OneKeyTronProvider = { - request: (args: { method: string }) => Promise<{ code: number; message: string }>; - tronWeb?: { - defaultAddress?: { base58?: string | false }; - trx: { sign: (transaction: TronTransaction) => Promise }; + request: { + (args: { method: "tron_requestAccounts" }): Promise<{ code: number; message: string }>; + (args: { method: "tron_accounts" }): Promise; }; + sign: (transaction: TronTransaction) => Promise; }; export type OneKeyCardanoApi = { From aabb2f047d019d406834536b2083d657d2bd7899 Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 11:31:41 +0000 Subject: [PATCH 06/12] refactor(onekey): move Sui support to its own pull request Sui cannot be tested against the OneKey extension yet, so it leaves this change and returns in a follow-up. The rejection test now uses Stellar as the chain that must not be requested. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .changeset/onekey-multichain-support.md | 2 +- .../src/onekey/__tests__/onekey.test.ts | 48 +++---------------- .../wallet-extensions/src/onekey/helpers.ts | 34 ------------- .../wallet-extensions/src/onekey/index.ts | 2 - .../wallet-extensions/src/onekey/types.ts | 12 ----- 5 files changed, 8 insertions(+), 90 deletions(-) diff --git a/.changeset/onekey-multichain-support.md b/.changeset/onekey-multichain-support.md index b8052f5b..102497e8 100644 --- a/.changeset/onekey-multichain-support.md +++ b/.changeset/onekey-multichain-support.md @@ -2,6 +2,6 @@ "@swapkit/wallet-extensions": minor --- -OneKey now connects Sui, Stellar, TON, TRON, Cardano and NEAR on mainnet, alongside its EVM chains. Bitcoin and Solana connections work again: they were looking for wallet providers the OneKey extension never exposes, so connecting either chain always failed. A Sui, TRON or Stellar signature from OneKey is rejected if it covers a different transaction than the one requested. +OneKey now connects Stellar, TON, TRON, Cardano and NEAR on mainnet, alongside its EVM chains. Bitcoin and Solana connections work again: they were looking for wallet providers the OneKey extension never exposes, so connecting either chain always failed. A TRON or Stellar signature from OneKey is rejected if it covers a different transaction than the one requested. Connecting OneKey now asks for one network at a time. If the user cancels an approval, no further approval windows open; networks approved before the cancel stay connected. A network OneKey refuses to connect for a reason other than a cancel, such as Bitcoin with multiple addresses enabled or a TON Connect manifest for another domain, is skipped and the rest still connect. diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index a3dc76c6..267aa60f 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -10,7 +10,6 @@ import { Chain, SKConfig } from "@swapkit/helpers"; import { getWalletMethods } from "../helpers"; import { onekeyWallet } from "../index"; -const SUI_ADDRESS = `0x${"ab".repeat(32)}`; const TRON_ADDRESS = "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"; afterEach(() => { @@ -35,39 +34,6 @@ function stellarPayment(source: Keypair, amount: string) { .build(); } -describe("OneKey Sui signer", () => { - function mockSui(signTransaction) { - globalThis.window = { - $onekey: { sui: { getAccounts: async () => [{ address: SUI_ADDRESS, publicKey: "" }], signTransaction } }, - }; - } - - test("sends the exact transaction bytes and returns the wallet signature", async () => { - const txBytes = new Uint8Array([1, 2, 3, 4]); - let request: Record | undefined; - mockSui((input) => { - request = input; - return Promise.resolve({ bytes: input.transaction.serialize(), signature: "wallet-signature" }); - }); - - const wallet = await getWalletMethods(Chain.Sui); - const signed = await wallet.signTransaction(txBytes); - - expect(request.chain).toBe("sui:mainnet"); - expect(request.account.address).toBe(SUI_ADDRESS); - expect(base64.decode(request.transaction.serialize())).toEqual(txBytes); - expect(signed).toEqual({ bytes: base64.encode(txBytes), signature: "wallet-signature" }); - }); - - test("rejects a signature over different bytes", async () => { - mockSui(async () => ({ bytes: base64.encode(new Uint8Array([9, 9])), signature: "wallet-signature" })); - - const wallet = await getWalletMethods(Chain.Sui); - - await expect(wallet.signTransaction(new Uint8Array([1, 2, 3, 4]))).rejects.toThrow("different transaction"); - }); -}); - describe("OneKey Tron signer", () => { function mockTron(sign) { globalThis.window = { @@ -208,21 +174,21 @@ describe("connectOnekeyWallet", () => { test("stops asking for more chains once the user rejects one", async () => { const rejection = Object.assign(new Error("User rejected the request."), { code: 4001 }); - let suiRequested = false; + let stellarRequested = false; globalThis.window = { $onekey: { solana: { connect: () => Promise.reject(rejection) }, - sui: { - getAccounts: () => { - suiRequested = true; - return Promise.resolve([{ address: SUI_ADDRESS, publicKey: "" }]); + stellar: { + getAddress: () => { + stellarRequested = true; + return Promise.resolve({ address: Keypair.random().publicKey() }); }, }, }, }; - await expect(connect([Chain.Solana, Chain.Sui]).result).rejects.toBe(rejection); - expect(suiRequested).toBe(false); + await expect(connect([Chain.Solana, Chain.Stellar]).result).rejects.toBe(rejection); + expect(stellarRequested).toBe(false); }); test("skips a chain OneKey refuses to connect and keeps the rest", async () => { diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index 6ae849a9..27ab43c7 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -2,14 +2,11 @@ import { base64, hex } from "@scure/base"; import { Chain, type EVMChain, prepareNetworkSwitch, SwapKitError, WalletOption } from "@swapkit/helpers"; import type { NearSigner } from "@swapkit/toolboxes/near"; import type { StellarSigner } from "@swapkit/toolboxes/stellar"; -import type { SuiChainSigner } from "@swapkit/toolboxes/sui"; import type { TronSigner } from "@swapkit/toolboxes/tron"; import { Transaction } from "@swapkit/utxo-signer"; import { getUtxoScriptTypeParams } from "../helpers/utxoScriptType"; import { getOneKeyTonWallet } from "./ton"; -const SUI_MAINNET = "sui:mainnet"; - type WalletMethodsWithAddress = Record & { address: string }; const ONEKEY_EVM_CHAINS = [ @@ -93,35 +90,6 @@ async function getEvmWalletMethods(chain: EVMChain) { return { address, ...prepareNetworkSwitch({ chain, provider, toolbox }) }; } -async function getSuiWalletMethods() { - const provider = getProvider("sui", Chain.Sui); - const [account] = await provider.getAccounts(); - if (!account?.address) throw connectionRejectedError(Chain.Sui); - - const { getSuiToolbox } = await import("@swapkit/toolboxes/sui"); - - const signer: SuiChainSigner = { - getAddress: () => Promise.resolve(account.address), - signTransaction: async (txBytes: Uint8Array) => { - const serialized = base64.encode(txBytes); - const result = await provider.signTransaction({ - account, - chain: SUI_MAINNET, - transaction: { serialize: () => serialized }, - }); - - if (base64.encode(base64.decode(result.bytes)) !== serialized) { - throw signingError(Chain.Sui, "OneKey signed a different transaction than the one requested"); - } - - return { bytes: serialized, signature: result.signature }; - }, - toSuiAddress: () => account.address, - }; - - return { ...getSuiToolbox({ signer }), address: account.address }; -} - async function getTronWalletMethods() { const provider = getProvider("tron", Chain.Tron); const response = await provider.request({ method: "tron_requestAccounts" }); @@ -262,8 +230,6 @@ export function getWalletMethods(chain: Chain): Promise = createWallet [Chain.Polygon]: { direct: true, typedData: true }, [Chain.Solana]: { direct: true }, [Chain.Stellar]: { direct: true }, - [Chain.Sui]: { direct: true }, [Chain.Ton]: { direct: true }, [Chain.Tron]: { direct: true }, [Chain.XLayer]: { direct: true, typedData: true }, @@ -69,7 +68,6 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet Chain.Polygon, Chain.Solana, Chain.Stellar, - Chain.Sui, Chain.Ton, Chain.Tron, Chain.XLayer, diff --git a/packages/wallet-extensions/src/onekey/types.ts b/packages/wallet-extensions/src/onekey/types.ts index e157cc71..b4a3f2b8 100644 --- a/packages/wallet-extensions/src/onekey/types.ts +++ b/packages/wallet-extensions/src/onekey/types.ts @@ -16,17 +16,6 @@ export type OneKeyBtcProvider = { signPsbt: (psbtHex: string, options: { autoFinalized: boolean }) => Promise; }; -export type OneKeySuiAccount = { address: string; publicKey: string }; - -export type OneKeySuiProvider = { - getAccounts: () => Promise; - signTransaction: (input: { - account: OneKeySuiAccount; - chain: string; - transaction: { serialize: () => string }; - }) => Promise<{ bytes: string; signature: string }>; -}; - export type OneKeyTronProvider = { request: { (args: { method: "tron_requestAccounts" }): Promise<{ code: number; message: string }>; @@ -57,7 +46,6 @@ export type OneKeyProviders = { jsBridge?: OneKeyJsBridge; solana?: SolanaProvider; stellar?: OneKeyStellarProvider; - sui?: OneKeySuiProvider; tonconnect?: TonConnectBridge; tron?: OneKeyTronProvider; }; From 968de86dd5c70d07a95173bc7e65d17c81689f46 Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 11:33:52 +0000 Subject: [PATCH 07/12] feat(onekey): support Sui Connect Sui through the provider the OneKey extension injects and sign with its signTransaction. The signed bytes are compared with the requested transaction before the signature is returned. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .changeset/onekey-sui-support.md | 5 +++ .../src/onekey/__tests__/onekey.test.ts | 34 +++++++++++++++++++ .../wallet-extensions/src/onekey/helpers.ts | 34 +++++++++++++++++++ .../wallet-extensions/src/onekey/index.ts | 2 ++ .../wallet-extensions/src/onekey/types.ts | 12 +++++++ 5 files changed, 87 insertions(+) create mode 100644 .changeset/onekey-sui-support.md diff --git a/.changeset/onekey-sui-support.md b/.changeset/onekey-sui-support.md new file mode 100644 index 00000000..728afaaf --- /dev/null +++ b/.changeset/onekey-sui-support.md @@ -0,0 +1,5 @@ +--- +"@swapkit/wallet-extensions": minor +--- + +OneKey now connects Sui on mainnet. A Sui signature from OneKey is rejected if it covers a different transaction than the one requested. diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index 267aa60f..543710db 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -10,6 +10,7 @@ import { Chain, SKConfig } from "@swapkit/helpers"; import { getWalletMethods } from "../helpers"; import { onekeyWallet } from "../index"; +const SUI_ADDRESS = `0x${"ab".repeat(32)}`; const TRON_ADDRESS = "TJRabPrwbZy45sbavfcjinPJC18kjpRTv8"; afterEach(() => { @@ -34,6 +35,39 @@ function stellarPayment(source: Keypair, amount: string) { .build(); } +describe("OneKey Sui signer", () => { + function mockSui(signTransaction) { + globalThis.window = { + $onekey: { sui: { getAccounts: async () => [{ address: SUI_ADDRESS, publicKey: "" }], signTransaction } }, + }; + } + + test("sends the exact transaction bytes and returns the wallet signature", async () => { + const txBytes = new Uint8Array([1, 2, 3, 4]); + let request: Record | undefined; + mockSui((input) => { + request = input; + return Promise.resolve({ bytes: input.transaction.serialize(), signature: "wallet-signature" }); + }); + + const wallet = await getWalletMethods(Chain.Sui); + const signed = await wallet.signTransaction(txBytes); + + expect(request.chain).toBe("sui:mainnet"); + expect(request.account.address).toBe(SUI_ADDRESS); + expect(base64.decode(request.transaction.serialize())).toEqual(txBytes); + expect(signed).toEqual({ bytes: base64.encode(txBytes), signature: "wallet-signature" }); + }); + + test("rejects a signature over different bytes", async () => { + mockSui(async () => ({ bytes: base64.encode(new Uint8Array([9, 9])), signature: "wallet-signature" })); + + const wallet = await getWalletMethods(Chain.Sui); + + await expect(wallet.signTransaction(new Uint8Array([1, 2, 3, 4]))).rejects.toThrow("different transaction"); + }); +}); + describe("OneKey Tron signer", () => { function mockTron(sign) { globalThis.window = { diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index 27ab43c7..6ae849a9 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -2,11 +2,14 @@ import { base64, hex } from "@scure/base"; import { Chain, type EVMChain, prepareNetworkSwitch, SwapKitError, WalletOption } from "@swapkit/helpers"; import type { NearSigner } from "@swapkit/toolboxes/near"; import type { StellarSigner } from "@swapkit/toolboxes/stellar"; +import type { SuiChainSigner } from "@swapkit/toolboxes/sui"; import type { TronSigner } from "@swapkit/toolboxes/tron"; import { Transaction } from "@swapkit/utxo-signer"; import { getUtxoScriptTypeParams } from "../helpers/utxoScriptType"; import { getOneKeyTonWallet } from "./ton"; +const SUI_MAINNET = "sui:mainnet"; + type WalletMethodsWithAddress = Record & { address: string }; const ONEKEY_EVM_CHAINS = [ @@ -90,6 +93,35 @@ async function getEvmWalletMethods(chain: EVMChain) { return { address, ...prepareNetworkSwitch({ chain, provider, toolbox }) }; } +async function getSuiWalletMethods() { + const provider = getProvider("sui", Chain.Sui); + const [account] = await provider.getAccounts(); + if (!account?.address) throw connectionRejectedError(Chain.Sui); + + const { getSuiToolbox } = await import("@swapkit/toolboxes/sui"); + + const signer: SuiChainSigner = { + getAddress: () => Promise.resolve(account.address), + signTransaction: async (txBytes: Uint8Array) => { + const serialized = base64.encode(txBytes); + const result = await provider.signTransaction({ + account, + chain: SUI_MAINNET, + transaction: { serialize: () => serialized }, + }); + + if (base64.encode(base64.decode(result.bytes)) !== serialized) { + throw signingError(Chain.Sui, "OneKey signed a different transaction than the one requested"); + } + + return { bytes: serialized, signature: result.signature }; + }, + toSuiAddress: () => account.address, + }; + + return { ...getSuiToolbox({ signer }), address: account.address }; +} + async function getTronWalletMethods() { const provider = getProvider("tron", Chain.Tron); const response = await provider.request({ method: "tron_requestAccounts" }); @@ -230,6 +262,8 @@ export function getWalletMethods(chain: Chain): Promise = createWallet [Chain.Polygon]: { direct: true, typedData: true }, [Chain.Solana]: { direct: true }, [Chain.Stellar]: { direct: true }, + [Chain.Sui]: { direct: true }, [Chain.Ton]: { direct: true }, [Chain.Tron]: { direct: true }, [Chain.XLayer]: { direct: true, typedData: true }, @@ -68,6 +69,7 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet Chain.Polygon, Chain.Solana, Chain.Stellar, + Chain.Sui, Chain.Ton, Chain.Tron, Chain.XLayer, diff --git a/packages/wallet-extensions/src/onekey/types.ts b/packages/wallet-extensions/src/onekey/types.ts index b4a3f2b8..e157cc71 100644 --- a/packages/wallet-extensions/src/onekey/types.ts +++ b/packages/wallet-extensions/src/onekey/types.ts @@ -16,6 +16,17 @@ export type OneKeyBtcProvider = { signPsbt: (psbtHex: string, options: { autoFinalized: boolean }) => Promise; }; +export type OneKeySuiAccount = { address: string; publicKey: string }; + +export type OneKeySuiProvider = { + getAccounts: () => Promise; + signTransaction: (input: { + account: OneKeySuiAccount; + chain: string; + transaction: { serialize: () => string }; + }) => Promise<{ bytes: string; signature: string }>; +}; + export type OneKeyTronProvider = { request: { (args: { method: "tron_requestAccounts" }): Promise<{ code: number; message: string }>; @@ -46,6 +57,7 @@ export type OneKeyProviders = { jsBridge?: OneKeyJsBridge; solana?: SolanaProvider; stellar?: OneKeyStellarProvider; + sui?: OneKeySuiProvider; tonconnect?: TonConnectBridge; tron?: OneKeyTronProvider; }; From f210ab9929426ee2fdae4e39d20a18fdc04fef2e Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 14:56:12 +0000 Subject: [PATCH 08/12] refactor(onekey): move Cardano support to its own pull request Signing hangs in OneKey's confirmation screen, so Cardano leaves this change until that is resolved and returns in a follow-up. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .changeset/onekey-multichain-support.md | 2 +- .../wallet-extensions/src/onekey/helpers.ts | 26 ------------------- .../wallet-extensions/src/onekey/index.ts | 2 -- .../wallet-extensions/src/onekey/types.ts | 7 ----- 4 files changed, 1 insertion(+), 36 deletions(-) diff --git a/.changeset/onekey-multichain-support.md b/.changeset/onekey-multichain-support.md index 102497e8..29e9dae9 100644 --- a/.changeset/onekey-multichain-support.md +++ b/.changeset/onekey-multichain-support.md @@ -2,6 +2,6 @@ "@swapkit/wallet-extensions": minor --- -OneKey now connects Stellar, TON, TRON, Cardano and NEAR on mainnet, alongside its EVM chains. Bitcoin and Solana connections work again: they were looking for wallet providers the OneKey extension never exposes, so connecting either chain always failed. A TRON or Stellar signature from OneKey is rejected if it covers a different transaction than the one requested. +OneKey now connects Stellar, TON, TRON and NEAR on mainnet, alongside its EVM chains. Bitcoin and Solana connections work again: they were looking for wallet providers the OneKey extension never exposes, so connecting either chain always failed. A TRON or Stellar signature from OneKey is rejected if it covers a different transaction than the one requested. Connecting OneKey now asks for one network at a time. If the user cancels an approval, no further approval windows open; networks approved before the cancel stay connected. A network OneKey refuses to connect for a reason other than a cancel, such as Bitcoin with multiple addresses enabled or a TON Connect manifest for another domain, is skipped and the rest still connect. diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index 27ab43c7..7966c861 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -118,30 +118,6 @@ async function getTronWalletMethods() { return { ...getTronToolbox({ signer }), address }; } -async function getCardanoWalletMethods() { - const api = await getProvider("cardano", Chain.Cardano).enable(); - if ((await api.getNetworkId()) !== 1) throw mainnetOnlyError(Chain.Cardano); - - const { addCardanoVkeyWitnesses, decodeCardanoWitnessSet, getCardanoAddressFromBytes, getCardanoToolbox } = - await import("@swapkit/toolboxes/cardano"); - - const changeAddress = await api.getChangeAddress(); - const address = changeAddress.startsWith("addr") - ? changeAddress - : await getCardanoAddressFromBytes(hex.decode(changeAddress)); - - const signer = { - address, - connect: () => Promise.resolve({ address }), - disconnect: () => Promise.resolve(), - getAddress: () => Promise.resolve(address), - signTransaction: async (transaction: string) => - addCardanoVkeyWitnesses(transaction, await decodeCardanoWitnessSet(await api.signTx(transaction))), - }; - - return { ...getCardanoToolbox({ signer }), address }; -} - async function getStellarWalletMethods() { const provider = getProvider("stellar", Chain.Stellar); const { address } = await provider.getAddress(); @@ -222,8 +198,6 @@ export function getWalletMethods(chain: Chain): Promise = createWallet [Chain.Base]: { direct: true, typedData: true }, [Chain.BinanceSmartChain]: { direct: true, typedData: true }, [Chain.Bitcoin]: { direct: true }, - [Chain.Cardano]: { direct: true }, [Chain.Ethereum]: { direct: true, typedData: true }, [Chain.Gnosis]: { direct: true, typedData: true }, [Chain.Near]: { direct: true }, @@ -60,7 +59,6 @@ export const onekeyWallet: ExtensionWallet<"connectOnekeyWallet"> = createWallet Chain.Base, Chain.BinanceSmartChain, Chain.Bitcoin, - Chain.Cardano, Chain.Ethereum, Chain.Gnosis, Chain.Near, diff --git a/packages/wallet-extensions/src/onekey/types.ts b/packages/wallet-extensions/src/onekey/types.ts index b4a3f2b8..e3f9b5d6 100644 --- a/packages/wallet-extensions/src/onekey/types.ts +++ b/packages/wallet-extensions/src/onekey/types.ts @@ -24,12 +24,6 @@ export type OneKeyTronProvider = { sign: (transaction: TronTransaction) => Promise; }; -export type OneKeyCardanoApi = { - getChangeAddress: () => Promise; - getNetworkId: () => Promise; - signTx: (transaction: string, partialSign?: boolean) => Promise; -}; - export type OneKeyStellarProvider = { getAddress: () => Promise<{ address: string }>; getNetwork: () => Promise<{ network: string; networkPassphrase: string }>; @@ -41,7 +35,6 @@ export type OneKeyStellarProvider = { export type OneKeyProviders = { btc?: OneKeyBtcProvider; - cardano?: { enable: () => Promise }; ethereum?: Eip1193Provider; jsBridge?: OneKeyJsBridge; solana?: SolanaProvider; From 160f5eaf55bffea2f1387c6439eb6d5ed938b812 Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 15:42:40 +0000 Subject: [PATCH 09/12] fix(onekey): stop the connection on EVM cancels and only on real rejections Cancelling the EVM approval now stops the remaining approval windows: ethers reports OneKey's rejection as ACTION_REJECTED, which was not recognised. TRON and TON only count a declined approval as a rejection and skip the chain on any other failure. The TON Connect helper decides this itself, so OKX also reports manifest errors as connection failures. TON moves into the OneKey helpers and EVM chains are matched with EVMChains instead of a separate list. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .../src/helpers/tonConnect.ts | 12 +++-- .../src/onekey/__tests__/onekey.test.ts | 33 ++++++++++++++ .../wallet-extensions/src/onekey/helpers.ts | 44 +++++++++++-------- .../wallet-extensions/src/onekey/index.ts | 15 ++----- packages/wallet-extensions/src/onekey/ton.ts | 20 --------- 5 files changed, 71 insertions(+), 53 deletions(-) delete mode 100644 packages/wallet-extensions/src/onekey/ton.ts diff --git a/packages/wallet-extensions/src/helpers/tonConnect.ts b/packages/wallet-extensions/src/helpers/tonConnect.ts index 123eb2f0..edde20be 100644 --- a/packages/wallet-extensions/src/helpers/tonConnect.ts +++ b/packages/wallet-extensions/src/helpers/tonConnect.ts @@ -5,6 +5,8 @@ import type { TonConnectBridge } from "../types"; const TON_MAINNET = "-239"; const SUPPORTED_SEND_MODE = 3; const VALID_SECONDS = 300; +// TON Connect reports a declined approval with this code; manifest and request problems use others +const TON_CONNECT_USER_REJECTS_ERROR = 300; type TonConnectWalletParams = { bridge: TonConnectBridge; @@ -98,10 +100,12 @@ async function connect({ bridge, wallet, noAccountError, mainnetOnlyError }: Ton const event = restored?.event === "connect" ? restored : await requestConnection(bridge, wallet); if (event.event !== "connect") { - throw new SwapKitError({ - errorKey: "wallet_connection_rejected_by_user", - info: { code: event.payload.code, message: event.payload.message, wallet }, - }); + const info = { code: event.payload.code, message: event.payload.message, wallet }; + const errorKey = + event.payload.code === TON_CONNECT_USER_REJECTS_ERROR + ? "wallet_connection_rejected_by_user" + : "core_wallet_connection_failed"; + throw new SwapKitError({ errorKey, info }); } const account = event.payload.items.find((item) => item.name === "ton_addr"); diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index 267aa60f..f2d4f8e7 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -191,6 +191,39 @@ describe("connectOnekeyWallet", () => { expect(stellarRequested).toBe(false); }); + test("stops asking for more chains once the user rejects the EVM approval", async () => { + let stellarRequested = false; + globalThis.window = { + $onekey: { + ethereum: { + request: ({ method }) => + method === "eth_requestAccounts" + ? Promise.reject(Object.assign(new Error("User rejected the request."), { code: 4001 })) + : Promise.resolve("0x1"), + }, + stellar: { + getAddress: () => { + stellarRequested = true; + return Promise.resolve({ address: Keypair.random().publicKey() }); + }, + }, + }, + }; + + await expect(connect([Chain.Ethereum, Chain.Stellar]).result).rejects.toMatchObject({ code: "ACTION_REJECTED" }); + expect(stellarRequested).toBe(false); + }); + + test("skips TRON when OneKey refuses for a reason other than a rejection", async () => { + globalThis.window = { + $onekey: { solana, tron: { request: () => Promise.resolve({ code: 4001, message: "in the request queue" }) } }, + }; + const { added, result } = connect([Chain.Tron, Chain.Solana]); + + expect(await result).toBe(true); + expect(added.map(({ chain }) => chain)).toEqual([Chain.Solana]); + }); + test("skips a chain OneKey refuses to connect and keeps the rest", async () => { const freshAddressError = Object.assign(new Error("Connection requires single address mode."), { code: -99999 }); globalThis.window = { $onekey: { btc: { requestAccounts: () => Promise.reject(freshAddressError) }, solana } }; diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index 7966c861..f406fb68 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -1,26 +1,15 @@ import { base64, hex } from "@scure/base"; -import { Chain, type EVMChain, prepareNetworkSwitch, SwapKitError, WalletOption } from "@swapkit/helpers"; +import { Chain, type EVMChain, EVMChains, prepareNetworkSwitch, SwapKitError, WalletOption } from "@swapkit/helpers"; import type { NearSigner } from "@swapkit/toolboxes/near"; import type { StellarSigner } from "@swapkit/toolboxes/stellar"; import type { TronSigner } from "@swapkit/toolboxes/tron"; import { Transaction } from "@swapkit/utxo-signer"; +import { getTonConnectWallet } from "../helpers/tonConnect"; import { getUtxoScriptTypeParams } from "../helpers/utxoScriptType"; -import { getOneKeyTonWallet } from "./ton"; type WalletMethodsWithAddress = Record & { address: string }; -const ONEKEY_EVM_CHAINS = [ - Chain.Arbitrum, - Chain.Aurora, - Chain.Avalanche, - Chain.Base, - Chain.BinanceSmartChain, - Chain.Ethereum, - Chain.Gnosis, - Chain.Optimism, - Chain.Polygon, - Chain.XLayer, -] as const; +const TRON_USER_REJECTED = 4000; function getProvider>(key: Key, chain: Chain) { const provider = window.$onekey?.[key]; @@ -46,6 +35,13 @@ function connectionRejectedError(chain: Chain) { return new SwapKitError("wallet_connection_rejected_by_user", { chain, wallet: WalletOption.ONEKEY }); } +function connectionFailedError(chain: Chain, info?: Record) { + return new SwapKitError({ + errorKey: "core_wallet_connection_failed", + info: { chain, wallet: WalletOption.ONEKEY, ...info }, + }); +} + async function getBitcoinWalletMethods() { const wallet = getProvider("btc", Chain.Bitcoin); const [address] = await wallet.requestAccounts(); @@ -93,11 +89,14 @@ async function getEvmWalletMethods(chain: EVMChain) { async function getTronWalletMethods() { const provider = getProvider("tron", Chain.Tron); const response = await provider.request({ method: "tron_requestAccounts" }); - if (response.code !== 200) throw connectionRejectedError(Chain.Tron); + // OneKey answers 4000 when the user declines and other codes, such as a request already queued, otherwise + if (response.code === TRON_USER_REJECTED) throw connectionRejectedError(Chain.Tron); + if (response.code !== 200) + throw connectionFailedError(Chain.Tron, { code: response.code, message: response.message }); // The injected tronWeb can lag behind an existing connection, so the address comes from OneKey itself const [address] = await provider.request({ method: "tron_accounts" }); - if (!address) throw connectionRejectedError(Chain.Tron); + if (!address) throw connectionFailedError(Chain.Tron); const { getTronToolbox } = await import("@swapkit/toolboxes/tron"); @@ -192,8 +191,17 @@ async function getNearWalletMethods() { return { ...getNearToolbox({ signer: signer as unknown as NearSigner }), address: account.accountId }; } +function getTonWalletMethods() { + return getTonConnectWallet({ + bridge: getProvider("tonconnect", Chain.Ton), + mainnetOnlyError: () => mainnetOnlyError(Chain.Ton), + noAccountError: () => connectionFailedError(Chain.Ton), + wallet: WalletOption.ONEKEY, + }); +} + export function getWalletMethods(chain: Chain): Promise { - if ((ONEKEY_EVM_CHAINS as readonly Chain[]).includes(chain)) return getEvmWalletMethods(chain as EVMChain); + if (EVMChains.includes(chain as EVMChain)) return getEvmWalletMethods(chain as EVMChain); switch (chain) { case Chain.Bitcoin: @@ -205,7 +213,7 @@ export function getWalletMethods(chain: Chain): Promise = createWallet export const ONEKEY_WALLET_SUPPORTED_CHAINS = getWalletSupportedChains(onekeyWallet); -// TON Connect reports a rejected approval as 300 and manifest problems with other codes -const TON_CONNECT_USER_REJECTS_ERROR = 300; - function isUserRejection(error: unknown) { - if (error instanceof SwapKitError) { - const code = error.info?.code; - return ( - error.errorKey === "wallet_connection_rejected_by_user" && - (code === undefined || code === TON_CONNECT_USER_REJECTS_ERROR) - ); - } + if (error instanceof SwapKitError) return error.errorKey === "wallet_connection_rejected_by_user"; - return (error as { code?: unknown } | null)?.code === 4001; + // ethers wraps the EIP-1193 4001 rejection of the EVM provider as ACTION_REJECTED + const code = (error as { code?: unknown } | null)?.code; + return code === 4001 || code === "ACTION_REJECTED"; } diff --git a/packages/wallet-extensions/src/onekey/ton.ts b/packages/wallet-extensions/src/onekey/ton.ts deleted file mode 100644 index 422218b9..00000000 --- a/packages/wallet-extensions/src/onekey/ton.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { Chain, SwapKitError, WalletOption } from "@swapkit/helpers"; -import { getTonConnectWallet } from "../helpers/tonConnect"; - -export async function getOneKeyTonWallet() { - const bridge = window.$onekey?.tonconnect; - if (!bridge) { - throw new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: Chain.Ton } }); - } - - return await getTonConnectWallet({ - bridge, - mainnetOnlyError: () => - new SwapKitError({ - errorKey: "wallet_chain_not_supported", - info: { chain: Chain.Ton, message: "Mainnet only", wallet: WalletOption.ONEKEY }, - }), - noAccountError: () => new SwapKitError({ errorKey: "wallet_onekey_not_found", info: { chain: Chain.Ton } }), - wallet: WalletOption.ONEKEY, - }); -} From 3e95b2248a96fff30842194dae8c8435a70bb60c Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 16:12:01 +0000 Subject: [PATCH 10/12] fix(onekey): connect TRON after OneKey's first approval and sign without its sign() OneKey can answer 4000 to tron_requestAccounts right after the user approves, so the connected account from tron_accounts now decides whether TRON connected. Signing calls tron_signTransaction directly: OneKey's injected sign() calls a tronWeb.utils.isString its TronWeb no longer has, which made every TRON signature fail. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .../src/onekey/__tests__/onekey.test.ts | 29 +++++++++++++++---- .../wallet-extensions/src/onekey/helpers.ts | 14 ++++----- .../wallet-extensions/src/onekey/types.ts | 2 +- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index f2d4f8e7..51eadc3d 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -35,20 +35,31 @@ function stellarPayment(source: Keypair, amount: string) { } describe("OneKey Tron signer", () => { - function mockTron(sign) { + function mockTron(sign, requestAccountsCode = 200) { globalThis.window = { $onekey: { tron: { - request: ({ method }) => - Promise.resolve(method === "tron_accounts" ? [TRON_ADDRESS] : { code: 200, message: "ok" }), - sign, - // OneKey's injected tronWeb can still have no address right after an existing connection + request: ({ method, params }) => { + if (method === "tron_signTransaction") return sign(params); + if (method === "tron_accounts") return Promise.resolve([TRON_ADDRESS]); + return Promise.resolve({ code: requestAccountsCode, message: "" }); + }, + // OneKey's own sign() is broken and its tronWeb can still have no address after an existing connection + sign: () => Promise.reject(new TypeError("tronWeb.utils.isString is not a function")), tronWeb: { defaultAddress: { base58: false } }, }, }, }; } + test("connects when OneKey answers 4000 to an approval that did connect the account", async () => { + mockTron(async () => ({}), 4000); + + const wallet = await getWalletMethods(Chain.Tron); + + expect(wallet.address).toBe(TRON_ADDRESS); + }); + test("returns the original transaction with the wallet signature", async () => { const transaction = tronTransaction("0a02abcd"); mockTron(async (tx) => ({ ...tx, raw_data: { ...tx.raw_data, fee_limit: 1 }, signature: ["sig"] })); @@ -216,7 +227,13 @@ describe("connectOnekeyWallet", () => { test("skips TRON when OneKey refuses for a reason other than a rejection", async () => { globalThis.window = { - $onekey: { solana, tron: { request: () => Promise.resolve({ code: 4001, message: "in the request queue" }) } }, + $onekey: { + solana, + tron: { + request: ({ method }) => + Promise.resolve(method === "tron_accounts" ? [] : { code: 4001, message: "in the request queue" }), + }, + }, }; const { added, result } = connect([Chain.Tron, Chain.Solana]); diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index f406fb68..c60ea133 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -89,21 +89,21 @@ async function getEvmWalletMethods(chain: EVMChain) { async function getTronWalletMethods() { const provider = getProvider("tron", Chain.Tron); const response = await provider.request({ method: "tron_requestAccounts" }); - // OneKey answers 4000 when the user declines and other codes, such as a request already queued, otherwise - if (response.code === TRON_USER_REJECTED) throw connectionRejectedError(Chain.Tron); - if (response.code !== 200) - throw connectionFailedError(Chain.Tron, { code: response.code, message: response.message }); - // The injected tronWeb can lag behind an existing connection, so the address comes from OneKey itself + // OneKey can answer 4000 right after an approval, so the connected account decides; tronWeb can lag behind too const [address] = await provider.request({ method: "tron_accounts" }); - if (!address) throw connectionFailedError(Chain.Tron); + if (!address) { + if (response.code === TRON_USER_REJECTED) throw connectionRejectedError(Chain.Tron); + throw connectionFailedError(Chain.Tron, { code: response.code, message: response.message }); + } const { getTronToolbox } = await import("@swapkit/toolboxes/tron"); const signer: TronSigner = { getAddress: () => Promise.resolve(address), signTransaction: async (transaction) => { - const { signature, txID } = await provider.sign(transaction); + // OneKey's injected sign() calls a tronWeb.utils.isString that its TronWeb no longer has + const { signature, txID } = await provider.request({ method: "tron_signTransaction", params: transaction }); if (txID !== transaction.txID) { throw signingError(Chain.Tron, "OneKey signed a different transaction than the one requested"); diff --git a/packages/wallet-extensions/src/onekey/types.ts b/packages/wallet-extensions/src/onekey/types.ts index e3f9b5d6..c89936b4 100644 --- a/packages/wallet-extensions/src/onekey/types.ts +++ b/packages/wallet-extensions/src/onekey/types.ts @@ -20,8 +20,8 @@ export type OneKeyTronProvider = { request: { (args: { method: "tron_requestAccounts" }): Promise<{ code: number; message: string }>; (args: { method: "tron_accounts" }): Promise; + (args: { method: "tron_signTransaction"; params: TronTransaction }): Promise; }; - sign: (transaction: TronTransaction) => Promise; }; export type OneKeyStellarProvider = { From 24d1522e6de09107aa0c5975886c745616ddf34e Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Tue, 3 Nov 2026 16:40:21 +0000 Subject: [PATCH 11/12] fix(onekey): keep TRON's signature check on the transaction that was requested OneKey received the same transaction object the check and the result used, so a provider that rewrote it in place passed the check. It now gets a copy, and the txID check and the returned transaction come from the requested one. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Y7T5QKkbKMCDEvXWUuh6hv --- .../src/onekey/__tests__/onekey.test.ts | 12 ++++++++++++ packages/wallet-extensions/src/onekey/helpers.ts | 11 ++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts index 51eadc3d..759864db 100644 --- a/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts +++ b/packages/wallet-extensions/src/onekey/__tests__/onekey.test.ts @@ -79,6 +79,18 @@ describe("OneKey Tron signer", () => { await expect(wallet.signTransaction(tronTransaction("0a02abcd"))).rejects.toThrow("different transaction"); }); + + test("rejects a signature when the provider rewrites the transaction it was given", async () => { + const other = tronTransaction("0a02ffff"); + mockTron(async (tx) => Object.assign(tx, other, { signature: ["sig"] })); + const transaction = tronTransaction("0a02abcd"); + const original = structuredClone(transaction); + + const wallet = await getWalletMethods(Chain.Tron); + + await expect(wallet.signTransaction(transaction)).rejects.toThrow("different transaction"); + expect(transaction).toEqual(original); + }); }); describe("OneKey Stellar signer", () => { diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index c60ea133..47a3b307 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -102,15 +102,20 @@ async function getTronWalletMethods() { const signer: TronSigner = { getAddress: () => Promise.resolve(address), signTransaction: async (transaction) => { + // The provider only sees a copy, so the check and the result use the transaction that was requested + const requested = structuredClone(transaction); // OneKey's injected sign() calls a tronWeb.utils.isString that its TronWeb no longer has - const { signature, txID } = await provider.request({ method: "tron_signTransaction", params: transaction }); + const { signature, txID } = await provider.request({ + method: "tron_signTransaction", + params: structuredClone(transaction), + }); - if (txID !== transaction.txID) { + if (txID !== requested.txID) { throw signingError(Chain.Tron, "OneKey signed a different transaction than the one requested"); } if (!signature?.length) throw signingError(Chain.Tron, "OneKey returned no signature"); - return { ...transaction, signature }; + return { ...requested, signature }; }, }; From 0af4606462e6a5f5d8717160789da34e4fad994a Mon Sep 17 00:00:00 2001 From: Hippocampus Date: Wed, 4 Nov 2026 17:28:50 +0000 Subject: [PATCH 12/12] refactor(onekey): compare the Sui bytes OneKey signed directly The base64 decoder is strict, so decoding and re-encoding the wallet's bytes could only ever return the same string or throw a raw decode error. A direct comparison keeps the same check and reports malformed bytes through the OneKey signing error like the other signers. Co-Authored-By: Claude Fable 5.1 Claude-Session: https://claude.ai/code/session_01Y7T5QNFUSnoUdjpgbvcx2V --- packages/wallet-extensions/src/onekey/helpers.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index bee41fc1..9f960b1c 100644 --- a/packages/wallet-extensions/src/onekey/helpers.ts +++ b/packages/wallet-extensions/src/onekey/helpers.ts @@ -106,7 +106,7 @@ async function getSuiWalletMethods() { transaction: { serialize: () => serialized }, }); - if (base64.encode(base64.decode(result.bytes)) !== serialized) { + if (result.bytes !== serialized) { throw signingError(Chain.Sui, "OneKey signed a different transaction than the one requested"); }