diff --git a/jest.config.js b/jest.config.js index d5f277a03..ebb978712 100644 --- a/jest.config.js +++ b/jest.config.js @@ -7,10 +7,13 @@ module.exports = { reporters: ["default", "jest-junit"], rootDir: "packages", testMatch: ["/**/*.test.ts"], - transformIgnorePatterns: ["node_modules/(?!(@shapeshiftoss/bitcoinjs-lib|valibot)/.*)"], + transformIgnorePatterns: [ + "node_modules/(?!(@shapeshiftoss/bitcoinjs-lib|valibot|@ton/ton|@ton/core|@ton/crypto|axios)/.*)", + ], moduleNameMapper: { "^@shapeshiftoss/hdwallet-(.*)": "/hdwallet-$1/src", "^valibot$": require.resolve("valibot"), + "^@ton/ton$": "/hdwallet-native/__mocks__/@ton/ton.js", }, globals: { "ts-jest": { diff --git a/packages/hdwallet-core/src/index.ts b/packages/hdwallet-core/src/index.ts index 3a08dddfc..ccd57f57f 100644 --- a/packages/hdwallet-core/src/index.ts +++ b/packages/hdwallet-core/src/index.ts @@ -20,6 +20,7 @@ export * from "./solana"; export * from "./starknet"; export * from "./sui"; export * from "./near"; +export * from "./ton"; export * from "./transport"; export * from "./tron"; export * from "./utils"; diff --git a/packages/hdwallet-core/src/ton.ts b/packages/hdwallet-core/src/ton.ts new file mode 100644 index 000000000..807f251c9 --- /dev/null +++ b/packages/hdwallet-core/src/ton.ts @@ -0,0 +1,100 @@ +import { addressNListToBIP32, slip44ByCoin } from "./utils"; +import { BIP32Path, HDWallet, HDWalletInfo, PathDescription } from "./wallet"; + +export interface TonGetAddress { + addressNList: BIP32Path; + showDisplay?: boolean; +} + +export interface TonRawMessage { + targetAddress: string; + sendAmount: string; + payload: string; + stateInit?: string; +} + +export interface TonSignTx { + addressNList: BIP32Path; + /** Raw message bytes to sign (BOC serialized) - used for simple transfers */ + message?: Uint8Array; + /** Raw messages from external protocols like Stonfi - used for complex swaps */ + rawMessages?: TonRawMessage[]; + /** Sequence number for the wallet */ + seqno?: number; + /** Transaction expiration timestamp */ + expireAt?: number; +} + +export interface TonSignedTx { + signature: string; + serialized: string; +} + +export interface TonGetAccountPaths { + accountIdx: number; +} + +export interface TonAccountPath { + addressNList: BIP32Path; +} + +export interface TonWalletInfo extends HDWalletInfo { + readonly _supportsTonInfo: boolean; + + /** + * Returns a list of bip32 paths for a given account index in preferred order + * from most to least preferred. + */ + tonGetAccountPaths(msg: TonGetAccountPaths): Array; + + /** + * Returns the "next" account path, if any. + */ + tonNextAccountPath(msg: TonAccountPath): TonAccountPath | undefined; +} + +export interface TonWallet extends TonWalletInfo, HDWallet { + readonly _supportsTon: boolean; + + tonGetAddress(msg: TonGetAddress): Promise; + tonSignTx(msg: TonSignTx): Promise; +} + +export function tonDescribePath(path: BIP32Path): PathDescription { + const pathStr = addressNListToBIP32(path); + const unknown: PathDescription = { + verbose: pathStr, + coin: "Ton", + isKnown: false, + }; + + // TON uses a 3-level path like Stellar: m/44'/607'/' + const slip44 = slip44ByCoin("Ton"); + if (slip44 === undefined) return unknown; + if (path.length != 3) return unknown; + if (path[0] != 0x80000000 + 44) return unknown; + if (path[1] != 0x80000000 + slip44) return unknown; + if ((path[2] & 0x80000000) >>> 0 !== 0x80000000) return unknown; + + const index = path[2] & 0x7fffffff; + return { + verbose: `TON Account #${index}`, + accountIdx: index, + wholeAccount: true, + coin: "Ton", + isKnown: true, + }; +} + +// TON uses a 3-level hardened derivation path: m/44'/607'/' +// This follows the same pattern as Stellar (SEP-0005) since TON uses Ed25519 +// https://github.com/satoshilabs/slips/blob/master/slip-0044.md (607 = TON) +export function tonGetAccountPaths(msg: TonGetAccountPaths): Array { + const slip44 = slip44ByCoin("Ton"); + if (slip44 === undefined) return []; + return [ + { + addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx], + }, + ]; +} diff --git a/packages/hdwallet-core/src/utils.ts b/packages/hdwallet-core/src/utils.ts index a12ee3a95..5c4503f9d 100644 --- a/packages/hdwallet-core/src/utils.ts +++ b/packages/hdwallet-core/src/utils.ts @@ -161,6 +161,7 @@ export const slip44Table = Object.freeze({ Tron: 195, Sui: 784, Near: 397, + Ton: 607, // EVM chains all use the same SLIP44 Ethereum: 60, Avalanche: 60, diff --git a/packages/hdwallet-core/src/wallet.ts b/packages/hdwallet-core/src/wallet.ts index c55a629ba..51ddb8ada 100644 --- a/packages/hdwallet-core/src/wallet.ts +++ b/packages/hdwallet-core/src/wallet.ts @@ -17,6 +17,7 @@ import { StarknetWallet, StarknetWalletInfo } from "./starknet"; import { SuiWallet, SuiWalletInfo } from "./sui"; import { TerraWallet, TerraWalletInfo } from "./terra"; import { ThorchainWallet, ThorchainWalletInfo } from "./thorchain"; +import { TonWallet, TonWalletInfo } from "./ton"; import { Transport } from "./transport"; import { TronWallet, TronWalletInfo } from "./tron"; @@ -300,6 +301,14 @@ export function infoSui(info: HDWalletInfo): info is SuiWalletInfo { return isObject(info) && (info as any)._supportsSuiInfo; } +export function supportsTon(wallet: HDWallet): wallet is TonWallet { + return isObject(wallet) && (wallet as any)._supportsTon; +} + +export function infoTon(info: HDWalletInfo): info is TonWalletInfo { + return isObject(info) && (info as any)._supportsTonInfo; +} + export function supportsDebugLink(wallet: HDWallet): wallet is DebugLinkWallet { return isObject(wallet) && (wallet as any)._supportsDebugLink; } diff --git a/packages/hdwallet-native/__mocks__/@ton/ton.js b/packages/hdwallet-native/__mocks__/@ton/ton.js new file mode 100644 index 000000000..b80bde029 --- /dev/null +++ b/packages/hdwallet-native/__mocks__/@ton/ton.js @@ -0,0 +1,10 @@ +module.exports = { + WalletContractV4: { + create: jest.fn().mockReturnValue({ + address: { toString: jest.fn().mockReturnValue("mock-address") }, + init: { code: null, data: null }, + createTransfer: jest.fn(), + }), + }, + TonClient: jest.fn(), +}; diff --git a/packages/hdwallet-native/package.json b/packages/hdwallet-native/package.json index 68fb327f4..a1a278a0f 100644 --- a/packages/hdwallet-native/package.json +++ b/packages/hdwallet-native/package.json @@ -20,6 +20,9 @@ "@shapeshiftoss/bitcoinjs-lib": "7.0.0-shapeshift.2", "@shapeshiftoss/hdwallet-core": "1.62.39", "@shapeshiftoss/proto-tx-builder": "0.10.0", + "@ton/core": "^0.62.1", + "@ton/crypto": "^3.3.0", + "@ton/ton": "^16.1.0", "@zxing/text-encoding": "^0.9.0", "bchaddrjs": "^0.4.9", "bech32": "^1.1.4", diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/index.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/index.ts index 4e77f6c43..66b69dd45 100644 --- a/packages/hdwallet-native/src/crypto/isolation/adapters/index.ts +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/index.ts @@ -10,4 +10,5 @@ export { default as Solana } from "./solana"; export { default as Starknet } from "./starknet"; export { default as Sui } from "./sui"; export { default as Near } from "./near"; +export { default as Ton } from "./ton"; export { default as Tron } from "./tron"; diff --git a/packages/hdwallet-native/src/crypto/isolation/adapters/ton.ts b/packages/hdwallet-native/src/crypto/isolation/adapters/ton.ts new file mode 100644 index 000000000..88283b71b --- /dev/null +++ b/packages/hdwallet-native/src/crypto/isolation/adapters/ton.ts @@ -0,0 +1,214 @@ +import * as core from "@shapeshiftoss/hdwallet-core"; +import { Address, beginCell, Cell, internal, MessageRelaxed, SendMode, storeMessage } from "@ton/core"; +import { WalletContractV4 } from "@ton/ton"; + +import { Isolation } from "../.."; + +const ED25519_PUBLIC_KEY_SIZE = 32; + +export interface TonTransactionParams { + from: string; + to: string; + value: string; + seqno: number; + expireAt: number; + memo?: string; + contractAddress?: string; + type?: "transfer" | "jetton_transfer"; +} + +export class TonAdapter { + protected readonly nodeAdapter: Isolation.Adapters.Ed25519; + + constructor(nodeAdapter: Isolation.Adapters.Ed25519) { + this.nodeAdapter = nodeAdapter; + } + + async getAddress(addressNList: core.BIP32Path): Promise { + const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList)); + const publicKey = await nodeAdapter.getPublicKey(); + + if (publicKey.length !== ED25519_PUBLIC_KEY_SIZE) { + throw new Error(`Invalid Ed25519 public key size: ${publicKey.length}`); + } + + const wallet = WalletContractV4.create({ + workchain: 0, + publicKey: Buffer.from(publicKey), + }); + + return wallet.address.toString({ bounceable: false }); + } + + async getPublicKey(addressNList: core.BIP32Path): Promise { + const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList)); + const publicKey = await nodeAdapter.getPublicKey(); + return Buffer.from(publicKey).toString("hex"); + } + + async createSignedTransferBoc(params: TonTransactionParams, addressNList: core.BIP32Path): Promise { + const derivedNodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList)); + const publicKey = await derivedNodeAdapter.getPublicKey(); + + const wallet = WalletContractV4.create({ + workchain: 0, + publicKey: Buffer.from(publicKey), + }); + + const destination = Address.parse(params.to); + + let internalMessage: MessageRelaxed; + + if (params.type === "jetton_transfer" && params.contractAddress) { + const jettonWalletAddress = Address.parse(params.contractAddress); + const forwardPayload = params.memo + ? beginCell().storeUint(0, 32).storeStringTail(params.memo).endCell() + : beginCell().endCell(); + + const jettonTransferBody = beginCell() + .storeUint(0x0f8a7ea5, 32) + .storeUint(0, 64) + .storeCoins(BigInt(params.value)) + .storeAddress(destination) + .storeAddress(Address.parse(params.from)) + .storeBit(false) + .storeCoins(BigInt(1)) + .storeBit(true) + .storeRef(forwardPayload) + .endCell(); + + internalMessage = internal({ + to: jettonWalletAddress, + value: BigInt(100000000), + bounce: true, + body: jettonTransferBody, + }); + } else { + internalMessage = internal({ + to: destination, + value: BigInt(params.value), + bounce: false, + body: params.memo ? beginCell().storeUint(0, 32).storeStringTail(params.memo).endCell() : beginCell().endCell(), + }); + } + + const signer = async (message: Cell): Promise => { + const hash = message.hash(); + const signature = await derivedNodeAdapter.node.sign(hash); + return Buffer.from(signature); + }; + + const transfer = await wallet.createTransfer({ + seqno: params.seqno, + signer, + messages: [internalMessage], + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, + timeout: params.expireAt, + }); + + const externalMessage = beginCell() + .store( + storeMessage({ + info: { + type: "external-in", + dest: wallet.address, + importFee: BigInt(0), + }, + init: params.seqno === 0 ? wallet.init : null, + body: transfer, + }) + ) + .endCell(); + + return externalMessage.toBoc().toString("base64"); + } + + async signTransaction(message: Uint8Array, addressNList: core.BIP32Path): Promise { + const nodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList)); + const signature = await nodeAdapter.node.sign(message); + return Buffer.from(signature).toString("hex"); + } + + async createSignedRawTransferBoc( + rawMessages: core.TonRawMessage[], + seqno: number, + expireAt: number, + addressNList: core.BIP32Path + ): Promise { + const derivedNodeAdapter = await this.nodeAdapter.derivePath(core.addressNListToHardenedBIP32(addressNList)); + const publicKey = await derivedNodeAdapter.getPublicKey(); + + const wallet = WalletContractV4.create({ + workchain: 0, + publicKey: Buffer.from(publicKey), + }); + + const internalMessages: MessageRelaxed[] = rawMessages.map((msg) => { + const destination = Address.parse(msg.targetAddress); + const value = BigInt(msg.sendAmount); + + let body: Cell; + if (msg.payload && msg.payload.length > 0) { + const payloadBuffer = Buffer.from(msg.payload, "hex"); + body = Cell.fromBoc(payloadBuffer)[0]; + } else { + body = beginCell().endCell(); + } + + let init: { code: Cell; data: Cell } | undefined; + if (msg.stateInit && msg.stateInit.length > 0) { + const stateInitBuffer = Buffer.from(msg.stateInit, "hex"); + const stateInitCell = Cell.fromBoc(stateInitBuffer)[0]; + const stateInitSlice = stateInitCell.beginParse(); + const hasCode = stateInitSlice.loadBit(); + const hasData = stateInitSlice.loadBit(); + if (hasCode && hasData) { + init = { + code: stateInitSlice.loadRef(), + data: stateInitSlice.loadRef(), + }; + } + } + + return internal({ + to: destination, + value, + bounce: true, + body, + init, + }); + }); + + const signer = async (message: Cell): Promise => { + const hash = message.hash(); + const signature = await derivedNodeAdapter.node.sign(hash); + return Buffer.from(signature); + }; + + const transfer = await wallet.createTransfer({ + seqno, + signer, + messages: internalMessages, + sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, + timeout: expireAt, + }); + + const externalMessage = beginCell() + .store( + storeMessage({ + info: { + type: "external-in", + dest: wallet.address, + importFee: BigInt(0), + }, + init: seqno === 0 ? wallet.init : null, + body: transfer, + }) + ) + .endCell(); + + return externalMessage.toBoc().toString("base64"); + } +} + +export default TonAdapter; diff --git a/packages/hdwallet-native/src/crypto/isolation/core/bip39/interfaces.ts b/packages/hdwallet-native/src/crypto/isolation/core/bip39/interfaces.ts index db765accb..7fb69bef1 100644 --- a/packages/hdwallet-native/src/crypto/isolation/core/bip39/interfaces.ts +++ b/packages/hdwallet-native/src/crypto/isolation/core/bip39/interfaces.ts @@ -1,6 +1,11 @@ import { Revocable } from ".."; import * as BIP32 from "../bip32"; +export interface TonSeed extends Partial { + toTonMasterKey(): Promise; +} + export interface Mnemonic extends Partial { toSeed(passphrase?: string): Promise; + toTonSeed?(password?: string): Promise; } diff --git a/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts b/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts index 4d0e0479b..6ff262f5e 100644 --- a/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts +++ b/packages/hdwallet-native/src/crypto/isolation/engines/default/bip32.ts @@ -211,3 +211,28 @@ export class Seed extends Revocable(class {}) implements Core.BIP32.Seed { return out; } } + +export class TonSeed extends Revocable(class {}) { + readonly #seed: Buffer; + + protected constructor(seed: Uint8Array) { + super(); + this.#seed = safeBufferFrom(seed); + this.addRevoker(() => this.#seed.fill(0)); + } + + static async create(seed: Uint8Array): Promise { + const obj = new TonSeed(seed); + return revocable(obj, (x) => obj.addRevoker(x)); + } + + async toTonMasterKey(): Promise { + const hmacKey = safeBufferFrom(new TextEncoder().encode("ed25519 seed")); + const I = safeBufferFrom(bip32crypto.hmacSHA512(hmacKey, this.#seed)); + const IL = I.subarray(0, 32); + const IR = I.subarray(32, 64); + const out = await Ed25519.Node.create(IL, IR); + this.addRevoker(() => out.revoke?.()); + return out; + } +} diff --git a/packages/hdwallet-native/src/crypto/isolation/engines/default/bip39.ts b/packages/hdwallet-native/src/crypto/isolation/engines/default/bip39.ts index f05512ae1..e1d5a70d6 100644 --- a/packages/hdwallet-native/src/crypto/isolation/engines/default/bip39.ts +++ b/packages/hdwallet-native/src/crypto/isolation/engines/default/bip39.ts @@ -1,10 +1,10 @@ /// -import { createSHA512, pbkdf2 } from "hash-wasm"; +import { createHMAC, createSHA512, pbkdf2 } from "hash-wasm"; import type { Seed as SeedType } from "../../core/bip32"; import type { Mnemonic as Bip39Mnemonic } from "../../core/bip39"; -import { Seed } from "./bip32"; +import { Seed, TonSeed } from "./bip32"; import { Revocable, revocable } from "./revocable"; export * from "../../core/bip39"; @@ -41,4 +41,27 @@ export class Mnemonic extends Revocable(class {}) implements Bip39Mnemonic { this.addRevoker(() => out.revoke?.()); return out; } + + async toTonSeed(password?: string): Promise { + const mnemonic = this.#mnemonic; + const passwordBytes = new TextEncoder().encode((password ?? "").normalize("NFKD")); + const mnemonicBytes = new TextEncoder().encode(mnemonic); + + const hmac = await createHMAC(createSHA512(), mnemonicBytes); + hmac.update(passwordBytes); + const entropy = hmac.digest("binary"); + + const seed = await pbkdf2({ + password: entropy, + salt: new TextEncoder().encode("TON default seed"), + iterations: 100000, + hashLength: 64, + hashFunction: createSHA512(), + outputType: "binary", + }); + + const out = await TonSeed.create(Buffer.from(seed)); + this.addRevoker(() => out.revoke?.()); + return out; + } } diff --git a/packages/hdwallet-native/src/crypto/isolation/engines/default/revocable.ts b/packages/hdwallet-native/src/crypto/isolation/engines/default/revocable.ts index 013ac2e72..0fbff8318 100644 --- a/packages/hdwallet-native/src/crypto/isolation/engines/default/revocable.ts +++ b/packages/hdwallet-native/src/crypto/isolation/engines/default/revocable.ts @@ -51,35 +51,43 @@ Proxy handler invariants (per MDN): */ export const revocable = _freeze((x: T, addRevoker: (revoke: () => void) => void) => { - const universalProxyHandler = (pseudoTarget: object) => - new Proxy( - {}, - { - get(_, p) { - return (_t: any, p2: any, r: any) => { - switch (p) { - case "get": { - const out = Reflect.get(pseudoTarget, p2, r); - if (typeof out === "function") return out.bind(x); - return out; - } - case "getOwnPropertyDescriptor": { - const out = Reflect.getOwnPropertyDescriptor(pseudoTarget, p2); - if (out) out.configurable = true; - return out; - } - case "isExtensible": - return true; - case "preventExtensions": - return false; - default: - return (Reflect as any)[p](pseudoTarget, p2, r); - } - }; - }, + const handler: ProxyHandler = { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + get(target, prop, receiver) { + const value = Reflect.get(x, prop, x); + if (typeof value === "function") { + return value.bind(x); } - ); - const { proxy, revoke } = _revocable({} as T, universalProxyHandler(x)); + return value; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + has(target, prop) { + return Reflect.has(x, prop); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + ownKeys(target) { + return Reflect.ownKeys(x); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getOwnPropertyDescriptor(target, prop) { + const desc = Reflect.getOwnPropertyDescriptor(x, prop); + if (desc) desc.configurable = true; + return desc; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + getPrototypeOf(target) { + return Reflect.getPrototypeOf(x); + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isExtensible(target) { + return true; + }, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + preventExtensions(target) { + return false; + }, + }; + const { proxy, revoke } = _revocable({} as T, handler); addRevoker(revoke); return proxy; }); diff --git a/packages/hdwallet-native/src/native.ts b/packages/hdwallet-native/src/native.ts index 8b2965ae9..e71ef0eb1 100644 --- a/packages/hdwallet-native/src/native.ts +++ b/packages/hdwallet-native/src/native.ts @@ -21,6 +21,7 @@ import { MixinNativeStarknetWallet, MixinNativeStarknetWalletInfo } from "./star import { MixinNativeSuiWallet, MixinNativeSuiWalletInfo } from "./sui"; import { MixinNativeTerraWallet, MixinNativeTerraWalletInfo } from "./terra"; import { MixinNativeThorchainWallet, MixinNativeThorchainWalletInfo } from "./thorchain"; +import { MixinNativeTonWallet, MixinNativeTonWalletInfo } from "./ton"; import { MixinNativeTronWallet, MixinNativeTronWalletInfo } from "./tron"; export enum NativeEvents { @@ -134,14 +135,16 @@ class NativeHDWalletInfo MixinNativeSolanaWalletInfo( MixinNativeStarknetWalletInfo( MixinNativeTronWalletInfo( - MixinNativeSuiWalletInfo( - MixinNativeNearWalletInfo( - MixinNativeThorchainWalletInfo( - MixinNativeMayachainWalletInfo( - MixinNativeSecretWalletInfo( - MixinNativeTerraWalletInfo( - MixinNativeKavaWalletInfo( - MixinNativeArkeoWalletInfo(MixinNativeOsmosisWalletInfo(NativeHDWalletBase)) + MixinNativeTonWalletInfo( + MixinNativeSuiWalletInfo( + MixinNativeNearWalletInfo( + MixinNativeThorchainWalletInfo( + MixinNativeMayachainWalletInfo( + MixinNativeSecretWalletInfo( + MixinNativeTerraWalletInfo( + MixinNativeKavaWalletInfo( + MixinNativeArkeoWalletInfo(MixinNativeOsmosisWalletInfo(NativeHDWalletBase)) + ) ) ) ) @@ -165,6 +168,7 @@ class NativeHDWalletInfo core.SolanaWalletInfo, core.StarknetWalletInfo, core.TronWalletInfo, + core.TonWalletInfo, core.SuiWalletInfo, core.NearWalletInfo, core.ThorchainWalletInfo, @@ -231,6 +235,8 @@ class NativeHDWalletInfo case "tron": case "trx": return core.tronDescribePath(msg.path); + case "ton": + return core.tonDescribePath(msg.path); default: throw new Error("Unsupported path"); } @@ -245,13 +251,17 @@ export class NativeHDWallet MixinNativeSolanaWallet( MixinNativeStarknetWallet( MixinNativeTronWallet( - MixinNativeSuiWallet( - MixinNativeNearWallet( - MixinNativeThorchainWallet( - MixinNativeMayachainWallet( - MixinNativeSecretWallet( - MixinNativeTerraWallet( - MixinNativeKavaWallet(MixinNativeOsmosisWallet(MixinNativeArkeoWallet(NativeHDWalletInfo))) + MixinNativeTonWallet( + MixinNativeSuiWallet( + MixinNativeNearWallet( + MixinNativeThorchainWallet( + MixinNativeMayachainWallet( + MixinNativeSecretWallet( + MixinNativeTerraWallet( + MixinNativeKavaWallet( + MixinNativeOsmosisWallet(MixinNativeArkeoWallet(NativeHDWalletInfo)) + ) + ) ) ) ) @@ -274,6 +284,7 @@ export class NativeHDWallet core.SolanaWallet, core.StarknetWallet, core.TronWallet, + core.TonWallet, core.SuiWallet, core.NearWallet, core.ThorchainWallet, @@ -422,6 +433,7 @@ export class NativeHDWallet super.solanaInitializeWallet(ed25519MasterKey), super.suiInitializeWallet(ed25519MasterKey), super.nearInitializeWallet(ed25519MasterKey), + super.tonInitializeWallet(ed25519MasterKey), ]); this.#initialized = true; @@ -466,6 +478,7 @@ export class NativeHDWallet super.solanaWipe(); super.suiWipe(); super.nearWipe(); + super.tonWipe(); super.btcWipe(); super.ethWipe(); super.cosmosWipe(); diff --git a/packages/hdwallet-native/src/ton.ts b/packages/hdwallet-native/src/ton.ts new file mode 100644 index 000000000..7e4871dad --- /dev/null +++ b/packages/hdwallet-native/src/ton.ts @@ -0,0 +1,90 @@ +import * as core from "@shapeshiftoss/hdwallet-core"; + +import { Isolation } from "./crypto"; +import { TonAdapter, TonTransactionParams } from "./crypto/isolation/adapters/ton"; +import { NativeHDWalletBase } from "./native"; + +export function MixinNativeTonWalletInfo>(Base: TBase) { + // eslint-disable-next-line @typescript-eslint/no-shadow + return class MixinNativeTonWalletInfo extends Base implements core.TonWalletInfo { + readonly _supportsTonInfo = true; + + tonGetAccountPaths(msg: core.TonGetAccountPaths): Array { + return core.tonGetAccountPaths(msg); + } + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + tonNextAccountPath(_msg: core.TonAccountPath): core.TonAccountPath | undefined { + throw new Error("Method not implemented"); + } + }; +} + +export function MixinNativeTonWallet>(Base: TBase) { + // eslint-disable-next-line @typescript-eslint/no-shadow + return class MixinNativeTonWallet extends Base { + readonly _supportsTon = true; + + tonAdapter: TonAdapter | undefined; + + async tonInitializeWallet(ed25519MasterKey: Isolation.Core.Ed25519.Node): Promise { + const nodeAdapter = new Isolation.Adapters.Ed25519(ed25519MasterKey); + this.tonAdapter = new TonAdapter(nodeAdapter); + } + + tonWipe() { + this.tonAdapter = undefined; + } + + async tonGetAddress(msg: core.TonGetAddress): Promise { + return this.needsMnemonic(!!this.tonAdapter, () => { + return this.tonAdapter!.getAddress(msg.addressNList); + }); + } + + async tonSignTx(msg: core.TonSignTx): Promise { + return this.needsMnemonic(!!this.tonAdapter, async () => { + if (msg.rawMessages && msg.rawMessages.length > 0) { + const seqno = msg.seqno ?? 0; + const expireAt = msg.expireAt ?? Math.floor(Date.now() / 1000) + 300; + + const bocBase64 = await this.tonAdapter!.createSignedRawTransferBoc( + msg.rawMessages, + seqno, + expireAt, + msg.addressNList + ); + + return { + signature: "", + serialized: bocBase64, + }; + } + + if (!msg.message) { + throw new Error("Either message or rawMessages must be provided"); + } + + const messageJson = new TextDecoder().decode(msg.message); + let txParams: TonTransactionParams; + + try { + txParams = JSON.parse(messageJson) as TonTransactionParams; + } catch (error) { + throw new Error( + `Failed to parse TON transaction message: ${ + error instanceof Error ? error.message : String(error) + }. Message: ${messageJson}` + ); + } + + const bocBase64 = await this.tonAdapter!.createSignedTransferBoc(txParams, msg.addressNList); + + return { + signature: "", + serialized: bocBase64, + }; + }); + } + }; +} diff --git a/yarn.lock b/yarn.lock index fb800baef..cd1ac2367 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4969,6 +4969,38 @@ dependencies: defer-to-connect "^2.0.1" +"@ton/core@^0.62.1": + version "0.62.1" + resolved "https://registry.yarnpkg.com/@ton/core/-/core-0.62.1.tgz#c116088806150d68f69c9d07ab592b668fca2c1c" + integrity sha512-RaEGBo9gCf6ZHyS8SKq1K53pswvYW9E5A6vwUuzFBTRX14g4qMDlB9F+fq4aBE5kN7XyVr8ScQtOJfQj41usCw== + +"@ton/crypto-primitives@2.1.0": + version "2.1.0" + resolved "https://registry.yarnpkg.com/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz#8c9277c250b59aae3c819e0d6bd61e44d998e9ca" + integrity sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow== + dependencies: + jssha "3.2.0" + +"@ton/crypto@^3.3.0": + version "3.3.0" + resolved "https://registry.yarnpkg.com/@ton/crypto/-/crypto-3.3.0.tgz#019103df6540fbc1d8102979b4587bc85ff9779e" + integrity sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA== + dependencies: + "@ton/crypto-primitives" "2.1.0" + jssha "3.2.0" + tweetnacl "1.0.3" + +"@ton/ton@^16.1.0": + version "16.1.0" + resolved "https://registry.yarnpkg.com/@ton/ton/-/ton-16.1.0.tgz#aabdcc08ca8203387cc33584c6732ab868ab25a8" + integrity sha512-vRlMZVJ0/JABFDTFInyLh3C4LRP6AF3VtOl2iwCEcPfqRxdPcHW4r+bJLkKvo5fCknaGS8CEVdBeu6ziXHv2Ig== + dependencies: + axios "^1.6.7" + dataloader "^2.0.0" + symbol.inspect "1.0.1" + teslabot "^1.3.0" + zod "^3.21.4" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -7131,7 +7163,7 @@ axios@^1.0.0: form-data "^4.0.0" proxy-from-env "^1.1.0" -axios@^1.8.4: +axios@^1.6.7, axios@^1.8.4: version "1.13.2" resolved "https://registry.yarnpkg.com/axios/-/axios-1.13.2.tgz#9ada120b7b5ab24509553ec3e40123521117f687" integrity sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA== @@ -9081,6 +9113,11 @@ data-view-byte-offset@^1.0.0: es-errors "^1.3.0" is-data-view "^1.0.1" +dataloader@^2.0.0: + version "2.2.3" + resolved "https://registry.yarnpkg.com/dataloader/-/dataloader-2.2.3.tgz#42d10b4913515f5b37c6acedcb4960d6ae1b1517" + integrity sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA== + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" @@ -13527,6 +13564,11 @@ jsprim@^1.2.2: json-schema "0.4.0" verror "1.10.0" +jssha@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/jssha/-/jssha-3.2.0.tgz#88ec50b866dd1411deaddbe6b3e3692e4c710f16" + integrity sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q== + just-diff-apply@^5.2.0: version "5.5.0" resolved "https://registry.yarnpkg.com/just-diff-apply/-/just-diff-apply-5.5.0.tgz#771c2ca9fa69f3d2b54e7c3f5c1dfcbcc47f9f0f" @@ -18013,6 +18055,11 @@ symbol-tree@^3.2.2, symbol-tree@^3.2.4: resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.4.tgz#430637d248ba77e078883951fb9aa0eed7c63fa2" integrity sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== +symbol.inspect@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/symbol.inspect/-/symbol.inspect-1.0.1.tgz#e13125b8038c4996eb0dfa1567332ad4dcd0763f" + integrity sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ== + system-architecture@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/system-architecture/-/system-architecture-0.1.0.tgz#71012b3ac141427d97c67c56bc7921af6bff122d" @@ -18082,6 +18129,11 @@ terminal-link@^2.0.0: ansi-escapes "^4.2.1" supports-hyperlinks "^2.0.0" +teslabot@^1.3.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/teslabot/-/teslabot-1.5.0.tgz#70f544516699ca5f696d8ae94f3d12cd495d5cd6" + integrity sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg== + test-exclude@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-6.0.0.tgz#04a8698661d805ea6fa293b6cb9e63ac044ef15e" @@ -18427,16 +18479,16 @@ tunnel@^0.0.6: resolved "https://registry.yarnpkg.com/tunnel/-/tunnel-0.0.6.tgz#72f1314b34a5b192db012324df2cc587ca47f92c" integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== +tweetnacl@1.0.3, tweetnacl@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" + integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== + tweetnacl@^0.14.3, tweetnacl@~0.14.0: version "0.14.5" resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-0.14.5.tgz#5ae68177f192d4456269d108afa93ff8743f4f64" integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA== -tweetnacl@^1.0.3: - version "1.0.3" - resolved "https://registry.yarnpkg.com/tweetnacl/-/tweetnacl-1.0.3.tgz#ac0af71680458d8a6378d0d0d050ab1407d35596" - integrity sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw== - type-assertions@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/type-assertions/-/type-assertions-1.1.0.tgz#51c5189fc6c1bdc1c19f48bf5ace6cc619917977" @@ -20077,3 +20129,8 @@ zod@3.22.4: version "3.22.4" resolved "https://registry.yarnpkg.com/zod/-/zod-3.22.4.tgz#f31c3a9386f61b1f228af56faa9255e845cf3fff" integrity sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg== + +zod@^3.21.4: + version "3.25.76" + resolved "https://registry.yarnpkg.com/zod/-/zod-3.25.76.tgz#26841c3f6fd22a6a2760e7ccb719179768471e34" + integrity sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==