Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
1d05ec9
feat: add TON blockchain support
NeOMakinG Jan 12, 2026
04b8246
chore: version packages to 1.62.39-ton-chain.0
NeOMakinG Jan 12, 2026
1dd1af1
chore: version packages to 1.62.39-ton-chain.1
NeOMakinG Jan 12, 2026
bc76c37
chore: version packages to 1.62.39-ton-chain.2
NeOMakinG Jan 12, 2026
d380fef
fix: add isMnemonicInterface check for TON in loadDevice
NeOMakinG Jan 12, 2026
727b41b
fix: add toTonSeed to core BIP39 interface and properly check before …
NeOMakinG Jan 12, 2026
a0cada9
fix: use typeof check for toTonSeed method
NeOMakinG Jan 12, 2026
9b7d75b
fix: remove toTonSeed check, just call it
NeOMakinG Jan 12, 2026
a56079f
debug: add TON loadDevice logging
NeOMakinG Jan 12, 2026
6e8816e
fix: make TON initialization graceful - don't block wallet connection…
NeOMakinG Jan 12, 2026
7c3f34c
fix: simplify revocable proxy to properly forward prototype methods l…
NeOMakinG Jan 12, 2026
7870d70
chore: version packages to 1.62.39-ton-chain.10
NeOMakinG Jan 12, 2026
30ec48c
fix: use standard BIP39+SLIP-10 for TON (like Solana) and fix StateIn…
NeOMakinG Jan 12, 2026
fb7842c
fix: add depth to cell repr for correct TON address derivation
NeOMakinG Jan 12, 2026
bb74251
fix(ton): correct V4R2 code cell depth from 1 to 7
NeOMakinG Jan 12, 2026
9e7dde5
chore: version packages to 1.62.39-ton-chain.14
NeOMakinG Jan 13, 2026
4fe7eff
chore: version packages to 1.62.39-ton-chain.15
NeOMakinG Jan 13, 2026
c0058da
chore: version packages to 1.62.39-ton-chain.16 and fix BOC serializa…
NeOMakinG Jan 13, 2026
d9fa99f
fix: BOC deserializer sizeBytes should not add 1
NeOMakinG Jan 13, 2026
805f824
fix: use correct wallet V4R2 code BOC from ton-core
NeOMakinG Jan 13, 2026
d70eb22
feat: use @ton/ton signer interface for proper signing without exposi…
NeOMakinG Jan 13, 2026
351ebee
chore: version packages to 1.62.39-ton-chain.20
NeOMakinG Jan 13, 2026
52c2f8a
fix(ton): use storeMessage for correct external message format
NeOMakinG Jan 13, 2026
6a7a36d
debug: add TON transaction logging
NeOMakinG Jan 13, 2026
a2ef893
fix(ton): set bounce=false, add more debug logging
NeOMakinG Jan 13, 2026
51f1525
fix(ton): use storeMessage for external messages, set bounce=false
NeOMakinG Jan 13, 2026
556c7be
chore: version packages to 1.62.39-ton-chain.25
NeOMakinG Jan 13, 2026
7b5118a
fix: remove unused import and add eslint-disable for ProxyHandler params
NeOMakinG Jan 15, 2026
6778dac
chore: reset package versions to 1.62.38 (remove verdaccio test versi…
NeOMakinG Jan 15, 2026
b1c7805
chore: merge origin/master and update versions to 1.62.39
NeOMakinG Jan 15, 2026
3c97ebe
fix: reset yarn.lock to remove verdaccio references
NeOMakinG Jan 15, 2026
f8747e6
fix: add jest mock for @ton/ton to fix ESM import issues in tests
NeOMakinG Jan 15, 2026
081af70
chore: version packages to 1.62.40-ton-chain.0
NeOMakinG Jan 15, 2026
0252ea6
fix: Apply CodeRabbit feedback for TON chain integration
NeOMakinG Jan 15, 2026
e37daf1
chore: version packages to 1.62.40-coderabbit-fix.0
NeOMakinG Jan 15, 2026
e52367b
feat: revert versions
NeOMakinG Jan 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,13 @@ module.exports = {
reporters: ["default", "jest-junit"],
rootDir: "packages",
testMatch: ["<rootDir>/**/*.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-(.*)": "<rootDir>/hdwallet-$1/src",
"^valibot$": require.resolve("valibot"),
"^@ton/ton$": "<rootDir>/hdwallet-native/__mocks__/@ton/ton.js",
},
globals: {
"ts-jest": {
Expand Down
1 change: 1 addition & 0 deletions packages/hdwallet-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
100 changes: 100 additions & 0 deletions packages/hdwallet-core/src/ton.ts
Original file line number Diff line number Diff line change
@@ -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<TonAccountPath>;

/**
* 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<string | null>;
tonSignTx(msg: TonSignTx): Promise<TonSignedTx | null>;
}

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'/<account>'
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'/<account>'
// 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<TonAccountPath> {
const slip44 = slip44ByCoin("Ton");
if (slip44 === undefined) return [];
return [
{
addressNList: [0x80000000 + 44, 0x80000000 + slip44, 0x80000000 + msg.accountIdx],
},
];
}
1 change: 1 addition & 0 deletions packages/hdwallet-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
9 changes: 9 additions & 0 deletions packages/hdwallet-core/src/wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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;
}
Expand Down
10 changes: 10 additions & 0 deletions packages/hdwallet-native/__mocks__/@ton/ton.js
Original file line number Diff line number Diff line change
@@ -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(),
};
3 changes: 3 additions & 0 deletions packages/hdwallet-native/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
214 changes: 214 additions & 0 deletions packages/hdwallet-native/src/crypto/isolation/adapters/ton.ts
Original file line number Diff line number Diff line change
@@ -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<string> {
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<string> {
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<string> {
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<Buffer> => {
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<string> {
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<string> {
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<Buffer> => {
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;
Loading