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 759864db..d1967eb4 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, requestAccountsCode = 200) { globalThis.window = { diff --git a/packages/wallet-extensions/src/onekey/helpers.ts b/packages/wallet-extensions/src/onekey/helpers.ts index 47a3b307..9f960b1c 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, EVMChains, 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 { getTonConnectWallet } from "../helpers/tonConnect"; import { getUtxoScriptTypeParams } from "../helpers/utxoScriptType"; +const SUI_MAINNET = "sui:mainnet"; + type WalletMethodsWithAddress = Record & { address: string }; const TRON_USER_REJECTED = 4000; @@ -86,6 +89,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 (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" }); @@ -217,6 +249,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 }, @@ -66,6 +67,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 c89936b4..f2aca8ca 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 }>; @@ -39,6 +50,7 @@ export type OneKeyProviders = { jsBridge?: OneKeyJsBridge; solana?: SolanaProvider; stellar?: OneKeyStellarProvider; + sui?: OneKeySuiProvider; tonconnect?: TonConnectBridge; tron?: OneKeyTronProvider; };