Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions modules/sdk-coin-ton/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@
"@bitgo/sdk-core": "^36.35.0",
"@bitgo/sdk-lib-mpc": "^10.9.0",
"@bitgo/statics": "^58.31.0",
"@bitgo/wasm-ton": "*",
"bignumber.js": "^9.0.0",
"bn.js": "^5.2.1",
"lodash": "^4.17.21",
Expand Down
73 changes: 73 additions & 0 deletions modules/sdk-coin-ton/src/lib/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/**
* WASM-based TON transaction explanation.
*
* Built on @bitgo/wasm-ton's parseTransaction(). Maps parsed output to the
* BitGoJS TransactionExplanation format with all 7 TON transaction types.
* This is BitGo-specific business logic that lives outside the wasm package.
*/

import {
Transaction as WasmTonTransaction,
parseTransaction,
TonTransactionType,
type ParsedTransaction,
} from '@bitgo/wasm-ton';
import { TransactionRecipient } from '@bitgo/sdk-core';
import { TransactionExplanation } from './iface';

export interface ExplainTonTransactionWasmOptions {
txBase64: string;
}

// =============================================================================
// Main explain function
// =============================================================================

/**
* Explain a TON transaction using the WASM parser.
*
* Parses the transaction via WASM parseTransaction(), then maps to the
* BitGoJS TransactionExplanation format. Supports all 7 TON transaction types:
* Send, SendToken, SingleNominatorWithdraw, TonWhalesDeposit,
* TonWhalesWithdrawal, TonWhalesVestingDeposit, TonWhalesVestingWithdrawal.
*/
export function explainTonTransaction(params: ExplainTonTransactionWasmOptions): TransactionExplanation {
const txBytes = Buffer.from(params.txBase64, 'base64');
const tx = WasmTonTransaction.fromBytes(txBytes);
const parsed: ParsedTransaction = parseTransaction(tx);

// For SendToken, the output is the jetton recipient with the jetton amount.
// For all other types, outputs come from the parsed outputs array.
const outputs: TransactionRecipient[] = [];
let outputAmount: string;

if (parsed.type === TonTransactionType.SendToken && parsed.jettonDestination && parsed.jettonAmount !== undefined) {
outputs.push({
address: parsed.jettonDestination,
amount: String(parsed.jettonAmount),
});
outputAmount = String(parsed.jettonAmount);
} else {
for (const out of parsed.outputs) {
outputs.push({
address: out.address,
amount: String(out.amount),
});
}
outputAmount = String(parsed.outputAmount);
}

const withdrawAmount =
parsed.withdrawAmount !== undefined && parsed.withdrawAmount !== 0n ? String(parsed.withdrawAmount) : undefined;

return {
displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'],
id: '',
outputs,
outputAmount,
changeOutputs: [],
changeAmount: '0',
fee: { fee: 'UNKNOWN' },
withdrawAmount,
};
}
2 changes: 2 additions & 0 deletions modules/sdk-coin-ton/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ export { TransferBuilder } from './transferBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { TonWhalesVestingDepositBuilder } from './tonWhalesVestingDepositBuilder';
export { TonWhalesVestingWithdrawBuilder } from './tonWhalesVestingWithdrawBuilder';
export { explainTonTransaction } from './explainTransactionWasm';
export type { ExplainTonTransactionWasmOptions } from './explainTransactionWasm';
export { Interface, Utils };
18 changes: 3 additions & 15 deletions modules/sdk-coin-ton/src/lib/keyPair.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DefaultKeys, Ed25519KeyPair, KeyPairOptions, toUint8Array } from '@bitgo/sdk-core';
import { encodeAddress } from '@bitgo/wasm-ton';
import utils from './utils';
import * as nacl from 'tweetnacl';

Expand Down Expand Up @@ -37,21 +38,8 @@ export class KeyPair extends Ed25519KeyPair {

/** @inheritdoc */
getAddress(): string {
throw new Error('Method not implemented.');

// this is the async way to get the address using tonweb library
// but we cannot use it as it is aysnc, there is a getAddressfromPublicKey in utlis.ts
/*
const tonweb = new TonWeb(new TonWeb.HttpProvider(''));

const WalletClass = tonweb.wallet.all['v4R2'];
const wallet = new WalletClass(tonweb.provider, {
publicKey: Buffer.from(this.keyPair.pub),
wc: 0
});
const address = await wallet.getAddress();
return address.toString(true, true, true);
*/
const pubKeyBytes = Buffer.from(this.keyPair.pub, 'hex');
return encodeAddress(pubKeyBytes, true);
}

/**
Expand Down
17 changes: 8 additions & 9 deletions modules/sdk-coin-ton/src/lib/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import TonWeb from 'tonweb';
import { BN } from 'bn.js';
import { BaseUtils, isValidEd25519PublicKey } from '@bitgo/sdk-core';
import { encodeAddress as wasmEncodeAddress, toRaw as wasmToRaw } from '@bitgo/wasm-ton';
import { VESTING_CONTRACT_CODE_B64 } from './constants';
import { VestingContractParams } from './iface';
export class Utils implements BaseUtils {
Expand Down Expand Up @@ -50,15 +51,13 @@ export class Utils implements BaseUtils {
}
}

async getAddressFromPublicKey(publicKey: string, bounceable = true, isUserFriendly = true): Promise<string> {
const tonweb = new TonWeb(new TonWeb.HttpProvider(''));
const WalletClass = tonweb.wallet.all['v4R2'];
const wallet = new WalletClass(tonweb.provider, {
publicKey: TonWeb.utils.hexToBytes(publicKey),
wc: 0,
});
const address = await wallet.getAddress();
return address.toString(isUserFriendly, true, bounceable);
getAddressFromPublicKey(publicKey: string, bounceable = true, isUserFriendly = true): string {
const pubKeyBytes = Buffer.from(publicKey, 'hex');
const userFriendlyAddr = wasmEncodeAddress(pubKeyBytes, bounceable);
if (!isUserFriendly) {
return wasmToRaw(userFriendlyAddr);
}
return userFriendlyAddr;
}

getAddress(address: string, bounceable = true): string {
Expand Down
35 changes: 11 additions & 24 deletions modules/sdk-coin-ton/src/ton.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,13 @@ import {
AuditDecryptedKeyParams,
extractCommonKeychain,
} from '@bitgo/sdk-core';
import { Transaction as WasmTonTransaction } from '@bitgo/wasm-ton';
import { auditEddsaPrivateKey, getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
import { KeyPair as TonKeyPair } from './lib/keyPair';
import { TransactionBuilderFactory, Utils, TransferBuilder, TokenTransferBuilder, TransactionBuilder } from './lib';
import { getFeeEstimate } from './lib/utils';
import { explainTonTransaction } from './lib/explainTransactionWasm';

export interface TonParseTransactionOptions extends ParseTransactionOptions {
txHex: string;
Expand Down Expand Up @@ -117,10 +119,8 @@ export class Ton extends BaseCoin {
throw new Error('missing required tx prebuild property txHex');
}

const txBuilder = this.getBuilder().from(Buffer.from(rawTx, 'hex').toString('base64'));
const transaction = await txBuilder.build();

const explainedTx = transaction.explainTransaction();
const txBase64 = Buffer.from(rawTx, 'hex').toString('base64');
const explainedTx = explainTonTransaction({ txBase64 });
if (txParams.recipients !== undefined) {
const filteredRecipients = txParams.recipients?.map((recipient) => {
const destination = this.getAddressDetails(recipient.address);
Expand Down Expand Up @@ -168,7 +168,7 @@ export class Ton extends BaseCoin {
const MPC = await EDDSAMethods.getInitializedMpcInstance();
const derivationPath = 'm/' + index;
const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
const expectedAddress = await Utils.default.getAddressFromPublicKey(derivedPublicKey);
const expectedAddress = Utils.default.getAddressFromPublicKey(derivedPublicKey);

return address === expectedAddress;
}
Expand Down Expand Up @@ -235,29 +235,16 @@ export class Ton extends BaseCoin {

/** @inheritDoc */
async getSignablePayload(serializedTx: string): Promise<Buffer> {
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
const rebuiltTransaction = await factory.from(serializedTx).build();
return rebuiltTransaction.signablePayload;
const txBytes = Buffer.from(serializedTx, 'base64');
const wasmTx = WasmTonTransaction.fromBytes(txBytes);
return Buffer.from(wasmTx.signablePayload());
}

/** @inheritDoc */
async explainTransaction(params: Record<string, any>): Promise<TransactionExplanation> {
try {
const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
const transactionBuilder = factory.from(Buffer.from(params.txHex, 'hex').toString('base64'));

const { toAddressBounceable, fromAddressBounceable } = params;

if (typeof toAddressBounceable === 'boolean') {
transactionBuilder.toAddressBounceable(toAddressBounceable);
}

if (typeof fromAddressBounceable === 'boolean') {
transactionBuilder.fromAddressBounceable(fromAddressBounceable);
}

const rebuiltTransaction = await transactionBuilder.build();
return rebuiltTransaction.explainTransaction();
const txBase64 = Buffer.from(params.txHex, 'hex').toString('base64');
return explainTonTransaction({ txBase64 });
} catch {
throw new Error('Invalid transaction');
}
Expand Down Expand Up @@ -293,7 +280,7 @@ export class Ton extends BaseCoin {
const index = params.index || 0;
const currPath = params.seed ? getDerivationPath(params.seed) + `/${index}` : `m/${index}`;
const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64);
const senderAddr = await Utils.default.getAddressFromPublicKey(accountId);
const senderAddr = Utils.default.getAddressFromPublicKey(accountId);
const balance = await tonweb.getBalance(senderAddr);

const jettonBalances: { minterAddress?: string; walletAddress: string; balance: string }[] = [];
Expand Down
49 changes: 49 additions & 0 deletions modules/sdk-coin-ton/test/unit/explainTransactionWasm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import should from 'should';
import { TransactionType } from '@bitgo/sdk-core';
import { explainTonTransaction } from '../../src/lib/explainTransactionWasm';
import * as testData from '../resources/ton';

describe('TON WASM Explain Transaction', () => {
it('should explain a Send transaction', () => {
const result = explainTonTransaction({ txBase64: testData.signedSendTransaction.tx });
result.outputs.length.should.equal(1);
result.outputs[0].address.should.be.a.String();
result.outputs[0].amount.should.equal(testData.signedSendTransaction.recipient.amount);
result.changeOutputs.should.deepEqual([]);
result.changeAmount.should.equal('0');
result.fee.fee.should.equal('UNKNOWN');
should.not.exist(result.withdrawAmount);
});

it('should explain a SingleNominatorWithdraw transaction', () => {
const result = explainTonTransaction({ txBase64: testData.signedSingleNominatorWithdrawTransaction.tx });
result.outputs.length.should.equal(1);
result.outputs[0].amount.should.equal(testData.signedSingleNominatorWithdrawTransaction.recipient.amount);
});

it('should explain a SendToken transaction', () => {
const result = explainTonTransaction({ txBase64: testData.signedTokenSendTransaction.tx });
result.outputs.length.should.equal(1);
result.outputs[0].amount.should.equal(testData.signedTokenSendTransaction.recipient.amount);
});

it('should explain a TonWhalesDeposit transaction', () => {
const result = explainTonTransaction({ txBase64: testData.signedTonWhalesDepositTransaction.tx });
result.outputs.length.should.equal(1);
result.outputs[0].amount.should.equal(testData.signedTonWhalesDepositTransaction.recipient.amount);
});

it('should explain a TonWhalesWithdrawal transaction', () => {
const result = explainTonTransaction({ txBase64: testData.signedTonWhalesWithdrawalTransaction.tx });
result.outputs.length.should.equal(1);
result.outputs[0].amount.should.equal(testData.signedTonWhalesWithdrawalTransaction.recipient.amount);
should.exist(result.withdrawAmount);
result.withdrawAmount!.should.equal(testData.signedTonWhalesWithdrawalTransaction.withdrawAmount);
});

it('should explain a v3-compatible (vesting) Send transaction', () => {
const result = explainTonTransaction({ txBase64: testData.v3CompatibleSignedSendTransaction.txBounceable });
result.outputs.length.should.equal(1);
result.outputs[0].amount.should.equal(testData.v3CompatibleSignedSendTransaction.recipient.amount);
});
});
40 changes: 40 additions & 0 deletions modules/sdk-coin-ton/test/unit/wasmAddress.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import should from 'should';
import { encodeAddress, validateAddress } from '@bitgo/wasm-ton';
import utils from '../../src/lib/utils';
import * as testData from '../resources/ton';

describe('TON WASM Address', () => {
it('should derive address from public key using WASM encodeAddress', () => {
const address = utils.getAddressFromPublicKey(testData.sender.publicKey);
address.should.be.a.String();
address.length.should.equal(48);
validateAddress(address).should.be.true();
});

it('should derive non-bounceable address from public key', () => {
const bounceable = utils.getAddressFromPublicKey(testData.sender.publicKey, true);
const nonBounceable = utils.getAddressFromPublicKey(testData.sender.publicKey, false);
bounceable.should.not.equal(nonBounceable);
validateAddress(bounceable).should.be.true();
validateAddress(nonBounceable).should.be.true();
});

it('should derive consistent addresses via encodeAddress directly', () => {
const pubKeyBytes = Buffer.from(testData.sender.publicKey, 'hex');
const address = encodeAddress(pubKeyBytes, true);
const addressFromUtils = utils.getAddressFromPublicKey(testData.sender.publicKey);
address.should.equal(addressFromUtils);
});

it('should validate known valid addresses', () => {
for (const addr of testData.addresses.validAddresses) {
validateAddress(addr).should.be.true();
}
});

it('should reject known invalid addresses', () => {
for (const addr of testData.addresses.invalidAddresses) {
validateAddress(addr).should.be.false();
}
});
});
69 changes: 69 additions & 0 deletions modules/sdk-coin-ton/test/unit/wasmSigning.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import should from 'should';
import * as nacl from 'tweetnacl';
import { Transaction as WasmTonTransaction } from '@bitgo/wasm-ton';
import * as testData from '../resources/ton';

describe('TON WASM Signing Flow', () => {
it('should get signable payload from a transaction', () => {
const txBytes = Buffer.from(testData.signedSendTransaction.tx, 'base64');
const tx = WasmTonTransaction.fromBytes(txBytes);
const payload = tx.signablePayload();
payload.should.be.instanceOf(Uint8Array);
payload.length.should.equal(32);
});

it('should produce consistent signable payload', () => {
const txBytes = Buffer.from(testData.signedSendTransaction.tx, 'base64');
const tx1 = WasmTonTransaction.fromBytes(txBytes);
const tx2 = WasmTonTransaction.fromBytes(txBytes);
const payload1 = tx1.signablePayload();
const payload2 = tx2.signablePayload();
Buffer.from(payload1).toString('hex').should.equal(Buffer.from(payload2).toString('hex'));
});

it('should perform fromBytes -> signablePayload -> addSignature -> toBytes roundtrip', () => {
const txBytes = Buffer.from(testData.signedSendTransaction.tx, 'base64');
const tx = WasmTonTransaction.fromBytes(txBytes);
const payload = tx.signablePayload();
payload.length.should.equal(32);

// Sign with a test key
const keyPair = nacl.sign.keyPair.fromSeed(Buffer.from(testData.privateKeys.prvKey1, 'hex'));
const signature = nacl.sign.detached(payload, keyPair.secretKey);
signature.length.should.equal(64);

tx.addSignature(signature);

const outputBytes = tx.toBytes();
outputBytes.should.be.instanceOf(Uint8Array);
outputBytes.length.should.be.greaterThan(0);

const broadcastFormat = tx.toBroadcastFormat();
broadcastFormat.should.be.a.String();
broadcastFormat.length.should.be.greaterThan(0);
});

it('should roundtrip a token transaction', () => {
const txBytes = Buffer.from(testData.signedTokenSendTransaction.tx, 'base64');
const tx = WasmTonTransaction.fromBytes(txBytes);
const payload = tx.signablePayload();
payload.length.should.equal(32);

const keyPair = nacl.sign.keyPair.fromSeed(Buffer.from(testData.privateKeys.prvKey1, 'hex'));
const signature = nacl.sign.detached(payload, keyPair.secretKey);
tx.addSignature(signature);

const broadcastFormat = tx.toBroadcastFormat();
broadcastFormat.should.be.a.String();
broadcastFormat.length.should.be.greaterThan(0);
});

it('should read transaction properties', () => {
const txBytes = Buffer.from(testData.signedSendTransaction.tx, 'base64');
const tx = WasmTonTransaction.fromBytes(txBytes);
tx.seqno.should.be.a.Number();
tx.walletId.should.be.a.Number();
tx.expireTime.should.be.a.Number();
(typeof tx.hasStateInit).should.equal('boolean');
});
});
Loading