From f89be5262524a00803f7e31e89866bf58a613b30 Mon Sep 17 00:00:00 2001 From: Dylan Xiao Date: Fri, 20 Sep 2024 11:21:37 -0400 Subject: [PATCH] feat(sdk-coin-ton): add single nominator withdraw txn (Ticket: SC-370) --- modules/sdk-coin-ton/src/lib/iface.ts | 6 + .../src/lib/singleNominatorWithdrawBuilder.ts | 27 ++ modules/sdk-coin-ton/src/lib/transaction.ts | 62 ++- .../src/lib/transactionBuilder.ts | 13 + .../src/lib/transactionBuilderFactory.ts | 33 +- .../src/lib/transactionExplanation.ts | 14 + modules/sdk-coin-ton/test/resources/ton.ts | 20 +- .../unit/singleNominatorWithdrawBuilder.ts | 229 ++++++++++ modules/sdk-coin-ton/test/unit/ton.ts | 395 +++++++++++------- .../sdk-coin-ton/test/unit/transferBuilder.ts | 20 +- .../sdk-core/src/account-lib/baseCoin/enum.ts | 1 + 11 files changed, 634 insertions(+), 186 deletions(-) create mode 100644 modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts create mode 100644 modules/sdk-coin-ton/src/lib/transactionExplanation.ts create mode 100644 modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts diff --git a/modules/sdk-coin-ton/src/lib/iface.ts b/modules/sdk-coin-ton/src/lib/iface.ts index be4418a284..033616488f 100644 --- a/modules/sdk-coin-ton/src/lib/iface.ts +++ b/modules/sdk-coin-ton/src/lib/iface.ts @@ -1,3 +1,6 @@ +import { TransactionFee } from '@bitgo/sdk-core'; +import { ITransactionExplanation } from './transactionExplanation'; + /** * The transaction data returned from the toJson() function of a transaction */ @@ -7,9 +10,12 @@ export interface TxData { destination: string; destinationAlias: string; amount: string; + withdrawAmount: string; seqno: number; expirationTime: number; publicKey: string; signature: string; bounceable: boolean; } + +export type TransactionExplanation = ITransactionExplanation; diff --git a/modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts b/modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts new file mode 100644 index 0000000000..14ebc58e80 --- /dev/null +++ b/modules/sdk-coin-ton/src/lib/singleNominatorWithdrawBuilder.ts @@ -0,0 +1,27 @@ +import { TransactionBuilder } from './transactionBuilder'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { Recipient, TransactionType } from '@bitgo/sdk-core'; + +export class SingleNominatorWithdrawBuilder extends TransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.SingleNominatorWithdraw; + } + + setWithdrawAmount(amount: string): SingleNominatorWithdrawBuilder { + this.transaction.withdrawAmount = amount; + return this; + } + + send(recipient: Recipient): SingleNominatorWithdrawBuilder { + this.transaction.recipient = recipient; + return this; + } + + setMessage(msg: string): SingleNominatorWithdrawBuilder { + throw new Error('Method not implemented.'); + } +} diff --git a/modules/sdk-coin-ton/src/lib/transaction.ts b/modules/sdk-coin-ton/src/lib/transaction.ts index 393a9ce332..87babc023f 100644 --- a/modules/sdk-coin-ton/src/lib/transaction.ts +++ b/modules/sdk-coin-ton/src/lib/transaction.ts @@ -1,13 +1,5 @@ -import { - BaseKey, - BaseTransaction, - Entry, - Recipient, - TransactionRecipient, - TransactionType, - TransactionExplanation, -} from '@bitgo/sdk-core'; -import { TxData } from './iface'; +import { BaseKey, BaseTransaction, Entry, Recipient, TransactionRecipient, TransactionType } from '@bitgo/sdk-core'; +import { TxData, TransactionExplanation } from './iface'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import TonWeb from 'tonweb'; import { BN } from 'bn.js'; @@ -20,7 +12,8 @@ export class Transaction extends BaseTransaction { public bounceable: boolean; public fromAddressBounceable: boolean; public toAddressBounceable: boolean; - public message: string; + public message: string | Cell; + public withdrawAmount: string; seqno: number; expireTime: number; sender: string; @@ -55,6 +48,7 @@ export class Transaction extends BaseTransaction { destination: this.recipient.address, destinationAlias: otherFormat, amount: this.recipient.amount, + withdrawAmount: this.withdrawAmount, seqno: this.seqno, expirationTime: this.expireTime, publicKey: this.publicKey, @@ -67,8 +61,15 @@ export class Transaction extends BaseTransaction { return Buffer.from(this.unsignedMessage, 'hex'); } + /** + * Set the transaction type. + * @param {TransactionType} transactionType The transaction type to be set. + */ + set transactionType(transactionType: TransactionType) { + this._type = transactionType; + } + async build(): Promise { - this._type = TransactionType.Send; const signingMessage = this.createSigningMessage(WALLET_ID, this.seqno, this.expireTime); const sendMode = 3; signingMessage.bits.writeUint8(sendMode); @@ -118,7 +119,7 @@ export class Transaction extends BaseTransaction { true, this.bounceable ); - return TonWeb.Contract.createCommonMsgInfo(orderHeader, undefined, payloadCell); + return TonWeb.Contract.createCommonMsgInfo(orderHeader, undefined, payloadCell); // compare with commonmsg tonweb and ton } async createExternalMessage(signingMessage: Cell, seqno: number, signature: string): Promise { @@ -164,12 +165,13 @@ export class Transaction extends BaseTransaction { try { const cell = TonWeb.boc.Cell.oneFromBoc(TonWeb.utils.base64ToBytes(rawTransaction)); - const parsed = this.parseTransfer(cell); + const parsed = this.parseTransaction(cell); parsed.value = parsed.value.toString(); parsed.fromAddress = parsed.fromAddress.toString(true, true, this.fromAddressBounceable); parsed.toAddress = parsed.toAddress.toString(true, true, this.toAddressBounceable); this.sender = parsed.fromAddress; this.recipient = { address: parsed.toAddress, amount: parsed.value }; + this.withdrawAmount = parsed.withdrawAmount; this.seqno = parsed.seqno; this.publicKey = parsed.publicKey as string; this.expireTime = parsed.expireAt; @@ -183,10 +185,11 @@ export class Transaction extends BaseTransaction { /** @inheritDoc */ explainTransaction(): TransactionExplanation { - const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee']; + const displayOrder = ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount']; const outputs: TransactionRecipient[] = [this.recipient]; const outputAmount = this.recipient.amount; + const withdrawAmount = this.withdrawAmount; return { displayOrder, id: this.id, @@ -195,10 +198,11 @@ export class Transaction extends BaseTransaction { changeOutputs: [], changeAmount: '0', fee: { fee: 'UNKNOWN' }, + withdrawAmount, }; } - private parseTransfer(cell: Cell): any { + private parseTransaction(cell: Cell): any { const slice = (cell as any).beginParse(); // header @@ -239,11 +243,11 @@ export class Transaction extends BaseTransaction { return { fromAddress: externalDestAddress, publicKey, - ...this.parseTransferBody(bodySlice), + ...this.parseTransactionBody(bodySlice), }; } - private parseTransferBody(slice: any): any { + private parseTransactionBody(slice: any): any { const signature = Buffer.from(slice.loadBits(512)).toString('hex'); // signing message @@ -288,16 +292,29 @@ export class Transaction extends BaseTransaction { // order body let payload; - + let withdrawAmount; + this.transactionType = TransactionType.Send; if (order.getFreeBits() > 0) { if (order.loadBit()) { order = order.loadRef(); } if (order.getFreeBits() > 32) { - const op = order.loadUint(32); - const payloadBytes = order.loadBits(order.getFreeBits()); - payload = op.eq(new BN(0)) ? new TextDecoder().decode(payloadBytes) : ''; + const opcode = order.loadUint(32).toNumber(); + if (opcode === 0) { + const payloadBytes = order.loadBits(order.getFreeBits()); + payload = new TextDecoder().decode(payloadBytes); + } else if (opcode === 4096) { + const queryId = order.loadUint(64).toNumber(); + withdrawAmount = (order.loadCoins().toNumber() / 1e9).toString(); + payload = new TonWeb.boc.Cell(); + payload.bits.writeUint(opcode, 32); + payload.bits.writeUint(queryId, 64); + payload.bits.writeCoins(TonWeb.utils.toNano(withdrawAmount)); + this.transactionType = TransactionType.SingleNominatorWithdraw; + } else { + payload = ''; + } } } return { @@ -305,6 +322,7 @@ export class Transaction extends BaseTransaction { value, bounce, seqno, + withdrawAmount, expireAt, payload, signature, diff --git a/modules/sdk-coin-ton/src/lib/transactionBuilder.ts b/modules/sdk-coin-ton/src/lib/transactionBuilder.ts index af932ebef9..30321b3037 100644 --- a/modules/sdk-coin-ton/src/lib/transactionBuilder.ts +++ b/modules/sdk-coin-ton/src/lib/transactionBuilder.ts @@ -14,6 +14,8 @@ import BigNumber from 'bignumber.js'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; import TonWeb from 'tonweb'; +const WITHDRAW_OPCODE = 4096; + export abstract class TransactionBuilder extends BaseTransactionBuilder { protected _transaction: Transaction; private _signatures: Signature[] = []; @@ -106,12 +108,14 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { /** @inheritdoc */ protected fromImplementation(rawTransaction: string): Transaction { + this.transaction.transactionType = this.transactionType; this.transaction.fromRawTransaction(rawTransaction); return this.transaction; } /** @inheritdoc */ protected async buildImplementation(): Promise { + this.transaction.transactionType = this.transactionType; await this.transaction.build(); this.transaction.loadInputsAndOutputs(); return this.transaction; @@ -156,6 +160,15 @@ export abstract class TransactionBuilder extends BaseTransactionBuilder { return this; } + setWithdrawMessage(): TransactionBuilder { + const message = new TonWeb.boc.Cell(); + message.bits.writeUint(WITHDRAW_OPCODE, 32); + message.bits.writeUint(0, 64); + message.bits.writeCoins(TonWeb.utils.toNano(this.transaction.withdrawAmount)); + this.transaction.message = message; + return this; + } + sequenceNumber(number: number): TransactionBuilder { this.transaction.seqno = number; return this; diff --git a/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts index aa15641e24..0081e1e3cd 100644 --- a/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-ton/src/lib/transactionBuilderFactory.ts @@ -1,7 +1,9 @@ -import { BaseTransactionBuilderFactory } from '@bitgo/sdk-core'; +import { BaseTransactionBuilderFactory, InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; import { TransactionBuilder } from './transactionBuilder'; import { TransferBuilder } from './transferBuilder'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { SingleNominatorWithdrawBuilder } from './singleNominatorWithdrawBuilder'; +import { Transaction } from './transaction'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { constructor(_coinConfig: Readonly) { @@ -9,9 +11,25 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { } /** @inheritdoc */ from(raw: string): TransactionBuilder { - const builder = new TransferBuilder(this._coinConfig); - builder.from(raw); - return builder; + let builder: TransactionBuilder; + const tx = new Transaction(this._coinConfig); + tx.fromRawTransaction(raw); + try { + switch (tx.type) { + case TransactionType.Send: + builder = this.getTransferBuilder(); + break; + case TransactionType.SingleNominatorWithdraw: + builder = this.getSingleNominatorWithdrawBuilder(); + break; + default: + throw new InvalidTransactionError('unsupported transaction'); + } + builder.from(raw); + return builder; + } catch (e) { + throw e; + } } /** @inheritdoc */ @@ -19,6 +37,13 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return new TransferBuilder(this._coinConfig); } + /** + * Returns a specific builder to create a TON withdraw transaction + */ + getSingleNominatorWithdrawBuilder(): SingleNominatorWithdrawBuilder { + return new SingleNominatorWithdrawBuilder(this._coinConfig); + } + /** @inheritdoc */ getWalletInitializationBuilder(): void { throw new Error('Method not implemented.'); diff --git a/modules/sdk-coin-ton/src/lib/transactionExplanation.ts b/modules/sdk-coin-ton/src/lib/transactionExplanation.ts new file mode 100644 index 0000000000..2d35f69693 --- /dev/null +++ b/modules/sdk-coin-ton/src/lib/transactionExplanation.ts @@ -0,0 +1,14 @@ +import { ITransactionRecipient } from '@bitgo/sdk-core'; + +export interface ITransactionExplanation { + displayOrder: string[]; + id: string; + outputs: ITransactionRecipient[]; + outputAmount: TAmount; + changeOutputs: ITransactionRecipient[]; + changeAmount: TAmount; + fee: TFee; + proxy?: string; + producers?: string[]; + withdrawAmount?: string; +} diff --git a/modules/sdk-coin-ton/test/resources/ton.ts b/modules/sdk-coin-ton/test/resources/ton.ts index dfbf6b322f..5a57307837 100644 --- a/modules/sdk-coin-ton/test/resources/ton.ts +++ b/modules/sdk-coin-ton/test/resources/ton.ts @@ -34,7 +34,7 @@ export const recipients: Recipient[] = [ }, ]; -export const signedTransaction = { +export const signedSendTransaction = { tx: 'te6cckEBAgEAqQAB4YgBJAxo7vqHF++LJ4bC/kJ8A1uVRskrKlrKJZ8rIB0tF+gCadlSX+hPo2mmhZyi0p3zTVUYVRkcmrCm97cSUFSa2vzvCArM3APg+ww92r3IcklNjnzfKOgysJVQXiCvj9SAaU1NGLsotvRwAAAAMAAcAQBmQgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr5zEtAAAAAAAAAAAAAAAAAAAdfZO7w==', txBounceable: 'te6cckEBAgEAqQAB4YgBJAxo7vqHF++LJ4bC/kJ8A1uVRskrKlrKJZ8rIB0tF+gCadlSX+hPo2mmhZyi0p3zTVUYVRkcmrCm97cSUFSa2vzvCArM3APg+ww92r3IcklNjnzfKOgysJVQXiCvj9SAaU1NGLsotvRwAAAAMAAcAQBmYgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr5zEtAAAAAAAAAAAAAAAAAAAYubM0w==', @@ -51,3 +51,21 @@ export const signedTransaction = { amount: '10000000', }, }; + +export const signedTransferTransaction = { + tx: 'te6cckEBAgEAugAB4YgANk3QfSfW3PrGRiBaolJGaRREphqOMSBd5rAGj1yJ88gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU1NGLsotvRwAAAAMAAcAQCHQgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr5zEtAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAQ7msoAg2I8aq', + txBounceable: + 'te6cckEBAgEAugAB4YgANk3QfSfW3PrGRiBaolJGaRREphqOMSBd5rAGj1yJ88gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAU1NGLsotvRwAAAAMAAcAQCHYgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr5zEtAAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAQ7msoAhFP9Yn', + txId: 'GUD-auBCZ3PJFfAPkSfeqe8rj2OiHMTXudH4IEWdDgo=', + txIdBounceable: 'GUD-auBCZ3PJFfAPkSfeqe8rj2OiHMTXudH4IEWdDgo=', + signable: 'cX/GEZo6PX0rrw35kBsOk91u0CyWEzASSjM0yzfAHp4=', + bounceableSignable: 'cX/GEZo6PX0rrw35kBsOk91u0CyWEzASSjM0yzfAHp4=', + recipient: { + address: 'EQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBXwtG', + amount: '10000000', + }, + recipientBounceable: { + address: 'UQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBX1aD', + amount: '10000000', + }, +}; diff --git a/modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts b/modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts new file mode 100644 index 0000000000..df8caedd9b --- /dev/null +++ b/modules/sdk-coin-ton/test/unit/singleNominatorWithdrawBuilder.ts @@ -0,0 +1,229 @@ +import should from 'should'; +import { TransactionType } from '@bitgo/sdk-core'; +import { TransactionBuilderFactory } from '../../src/lib/transactionBuilderFactory'; +import { coins } from '@bitgo/statics'; +import * as testData from '../resources/ton'; +import { KeyPair } from '../../src/lib/keyPair'; +import * as utils from '../../src/lib/utils'; +import TonWeb from 'tonweb'; + +describe('Ton Single Nominator Withdraw Builder', () => { + const factory = new TransactionBuilderFactory(coins.get('tton')); + it('should build a unsigned withdraw tx', async function () { + const txId = 'wxaFRG6b7Fhw91Al3JY-zbvl9Mjr-P0NuLvoGYyS5FI='.replace(/\//g, '_').replace(/\+/g, '-'); + const txBuilder = factory.getSingleNominatorWithdrawBuilder(); + txBuilder.sender(testData.sender.address); + txBuilder.sequenceNumber(0); + txBuilder.publicKey(testData.sender.publicKey); + txBuilder.expireTime(1234567890); + txBuilder.send(testData.recipients[0]); + txBuilder.setWithdrawAmount('1'); + txBuilder.setWithdrawMessage(); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.SingleNominatorWithdraw); + should.equal(tx.toJson().bounceable, false); + should.equal(tx.toJson().withdrawAmount, '1'); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender.address, + value: testData.recipients[0].amount, + coin: 'tton', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.recipients[0].address, + value: testData.recipients[0].amount, + coin: 'tton', + }); + tx.id.should.equal(txId); + const rawTx = tx.toBroadcastFormat(); + console.log(rawTx); + rawTx.should.equal( + 'te6cckECGAEAA8AAAuGIADZN0H0n1tz6xkYgWqJSRmkURKYajjEgXeawBo9cifPIGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmpoxdJlgLSAAAAAAADgEXAgE0AhYBFP8A9KQT9LzyyAsDAgEgBBECAUgFCALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQYHAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAkQAgEgCg8CAVgLDAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA0OABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xITFBUAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF8DDudwJkyEh7jUbJEjFCjriVxsSlRJFyF872V1eegb4QACJQgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr6A613oAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAEO5rKAIFYXIKQ==' + ); + }); + + it('should build a unsigned withdraw tx with bounceable flag', async function () { + const txId = '2RHws_jhf1eEKQnl1VjepktDI_Y40to2GqFwEwsdneU='.replace(/\//g, '_').replace(/\+/g, '-'); + const txBuilder = factory.getSingleNominatorWithdrawBuilder(); + txBuilder.sender(testData.sender.address); + txBuilder.sequenceNumber(0); + txBuilder.publicKey(testData.sender.publicKey); + txBuilder.expireTime(1234567890); + txBuilder.send(testData.recipients[0]); + txBuilder.setWithdrawAmount('1'); + txBuilder.setWithdrawMessage(); + txBuilder.bounceable(true); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.SingleNominatorWithdraw); + should.equal(tx.toJson().bounceable, true); + should.equal(tx.toJson().withdrawAmount, '1'); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender.address, + value: testData.recipients[0].amount, + coin: 'tton', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address: testData.recipients[0].address, + value: testData.recipients[0].amount, + coin: 'tton', + }); + tx.id.should.equal(txId); + const rawTx = tx.toBroadcastFormat(); + console.log(rawTx); + rawTx.should.equal( + 'te6cckECGAEAA8AAAuGIADZN0H0n1tz6xkYgWqJSRmkURKYajjEgXeawBo9cifPIGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmpoxdJlgLSAAAAAAADgEXAgE0AhYBFP8A9KQT9LzyyAsDAgEgBBECAUgFCALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQYHAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAkQAgEgCg8CAVgLDAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA0OABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xITFBUAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF8DDudwJkyEh7jUbJEjFCjriVxsSlRJFyF872V1eegb4QACJYgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr6A613oAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAEO5rKAI8DKNSw==' + ); + }); + + it('should build a withdraw from rawTx', async function () { + const txBuilder = factory.from(testData.signedTransferTransaction.tx); + const builtTx = await txBuilder.build(); + const jsonTx = builtTx.toJson(); + should.equal(builtTx.type, TransactionType.SingleNominatorWithdraw); + should.equal(builtTx.signablePayload.toString('base64'), testData.signedTransferTransaction.signable); + should.equal(builtTx.id, testData.signedTransferTransaction.txId); + const builder2 = factory.from(builtTx.toBroadcastFormat()); + const builtTx2 = await builder2.build(); + should.equal(builtTx2.type, TransactionType.SingleNominatorWithdraw); + should.equal(builtTx.toBroadcastFormat(), testData.signedTransferTransaction.tx); + builtTx.inputs.length.should.equal(1); + builtTx.outputs.length.should.equal(1); + jsonTx.sender.should.equal('EQAbJug-k-tufWMjEC1RKSM0iiJTDUcYkC7zWANHrkT55Fol'); + jsonTx.destination.should.equal('EQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBXwtG'); + jsonTx.amount.should.equal('10000000'); + jsonTx.seqno.should.equal(6); + jsonTx.expirationTime.should.equal(1695997582); + jsonTx.withdrawAmount.should.equal('1'); + + const builtTx3 = await txBuilder.bounceable(false).fromAddressBounceable(false).toAddressBounceable(false).build(); + txBuilder.from(testData.signedTransferTransaction.tx); + const jsonTx3 = builtTx3.toJson(); + should.equal(jsonTx3.bounceable, false); + should.equal(builtTx3.signablePayload.toString('base64'), testData.signedTransferTransaction.bounceableSignable); + should.equal(builtTx3.id, testData.signedTransferTransaction.txIdBounceable); + should.equal(builtTx3.toBroadcastFormat(), testData.signedTransferTransaction.tx); + jsonTx3.sender.should.equal('UQAbJug-k-tufWMjEC1RKSM0iiJTDUcYkC7zWANHrkT55Afg'); + jsonTx3.destination.should.equal('UQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBX1aD'); + jsonTx3.amount.should.equal('10000000'); + jsonTx3.seqno.should.equal(6); + jsonTx3.expirationTime.should.equal(1695997582); + jsonTx.withdrawAmount.should.equal('1'); + }); + + it('should parse a raw transaction and set flags', async function () { + const factory = new TransactionBuilderFactory(coins.get('tton')); + const txBuilder = factory.from(testData.signedTransferTransaction.tx); + const txBuilderBounceable = factory.from(testData.signedTransferTransaction.txBounceable); + + const tx = await txBuilder.build(); + const txBounceable = await txBuilderBounceable.build(); + tx.toJson().bounceable.should.equal(false); + should.equal(tx.toJson().withdrawAmount, '1'); + txBounceable.toJson().bounceable.should.equal(true); + should.equal(txBounceable.toJson().withdrawAmount, '1'); + }); + + xit('should build a signed withdraw tx and submit onchain', async function () { + const tonweb = new TonWeb(new TonWeb.HttpProvider('https://testnet.toncenter.com/api/v2/jsonRPC')); + const keyPair = new KeyPair({ prv: testData.privateKeys.prvKey1 }); + const publicKey = keyPair.getKeys().pub; + const address = await utils.default.getAddressFromPublicKey(publicKey); + const txBuilder = factory.getSingleNominatorWithdrawBuilder(); + txBuilder.sender(address); + + const WalletClass = tonweb.wallet.all['v4R2']; + const wallet = new WalletClass(tonweb.provider, { + publicKey: tonweb.utils.hexToBytes(publicKey), + wc: 0, + }); + const seqno = await wallet.methods.seqno().call(); + txBuilder.sequenceNumber(seqno as number); + txBuilder.publicKey(publicKey); + const expireAt = Math.floor(Date.now() / 1e3) + 60 * 60 * 24 * 7; // 7 days + txBuilder.expireTime(expireAt); + txBuilder.send({ + address: 'kf9vYg5iDMPiOpv9ypv9_B7pwYee4wjcQ0SHLpN9V65YF_Ig', + amount: '100000000', + }); + txBuilder.setWithdrawAmount('1'); + txBuilder.setWithdrawMessage(); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.SingleNominatorWithdraw); + const signable = tx.signablePayload; + const signature = keyPair.signMessageinUint8Array(signable); + const signedTx = await txBuilder.build(); + const builder2 = factory.from(signedTx.toBroadcastFormat()); + builder2.addSignature(keyPair.getKeys(), Buffer.from(signature)); + const tx2 = await builder2.build(); + const signature2 = keyPair.signMessageinUint8Array(tx2.signablePayload); + should.equal(Buffer.from(signature).toString('hex'), Buffer.from(signature2).toString('hex')); + await new Promise((resolve) => setTimeout(resolve, 2000)); + const result = await tonweb.provider.sendBoc(tx2.toBroadcastFormat()); + console.log(JSON.stringify(result)); + }); + + it('should build a signed withdraw tx using add signature', async function () { + const keyPair = new KeyPair({ prv: testData.privateKeys.prvKey1 }); + const publicKey = keyPair.getKeys().pub; + const address = await utils.default.getAddressFromPublicKey(publicKey); + const txBuilder = factory.getSingleNominatorWithdrawBuilder(); + txBuilder.sender(address); + txBuilder.sequenceNumber(0); + txBuilder.publicKey(publicKey); + const expireAt = Math.floor(Date.now() / 1e3) + 60 * 60 * 24 * 7; + txBuilder.expireTime(expireAt); + txBuilder.send(testData.recipients[0]); + txBuilder.setWithdrawAmount('1'); + txBuilder.setWithdrawMessage(); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.SingleNominatorWithdraw); + const signable = tx.signablePayload; + const signature = keyPair.signMessageinUint8Array(signable); + txBuilder.addSignature(keyPair.getKeys(), Buffer.from(signature)); + const signedTx = await txBuilder.build(); + const builder2 = factory.from(signedTx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const signature2 = keyPair.signMessageinUint8Array(tx2.signablePayload); + should.equal(Buffer.from(signature).toString('hex'), Buffer.from(signature2).toString('hex')); + should.equal(tx.toBroadcastFormat(), tx2.toBroadcastFormat()); + }); + + it('should build withdraw tx for non-bounceable address', async function () { + const txBuilder = factory.getSingleNominatorWithdrawBuilder(); + txBuilder.sender(testData.sender.address); + txBuilder.sequenceNumber(0); + txBuilder.publicKey(testData.sender.publicKey); + txBuilder.expireTime(1234567890); + const address = 'EQAWzEKcdnykvXfUNouqdS62tvrp32bCxuKS6eQrS6ISgcLo'; + const otherFormat = 'UQAWzEKcdnykvXfUNouqdS62tvrp32bCxuKS6eQrS6ISgZ8t'; + const amount = '100000000'; + txBuilder.send({ address, amount }); + txBuilder.setWithdrawAmount('1'); + txBuilder.setWithdrawMessage(); + const tx = await txBuilder.build(); + should.equal(tx.type, TransactionType.SingleNominatorWithdraw); + tx.inputs.length.should.equal(1); + tx.inputs[0].should.deepEqual({ + address: testData.sender.address, + value: amount, + coin: 'tton', + }); + tx.outputs.length.should.equal(1); + tx.outputs[0].should.deepEqual({ + address, + value: amount, + coin: 'tton', + }); + const txJson = tx.toJson(); + txJson.destination.should.equal(address); + should.equal(tx.toJson().withdrawAmount, '1'); + const builder2 = factory.from(tx.toBroadcastFormat()); + const tx2 = await builder2.build(); + const txJson2 = tx2.toJson(); + txJson2.destinationAlias.should.equal(otherFormat); + should.equal(tx.toJson().withdrawAmount, '1'); + }); +}); diff --git a/modules/sdk-coin-ton/test/unit/ton.ts b/modules/sdk-coin-ton/test/unit/ton.ts index ab9ad9c783..e496eb0e9e 100644 --- a/modules/sdk-coin-ton/test/unit/ton.ts +++ b/modules/sdk-coin-ton/test/unit/ton.ts @@ -14,24 +14,46 @@ describe('TON:', function () { bitgo.safeRegister('ton', Ton.createInstance); bitgo.safeRegister('tton', Tton.createInstance); bitgo.initializeTestVars(); - const txPrebuild = { - txHex: Buffer.from(testData.signedTransaction.tx, 'base64').toString('hex'), - txInfo: {}, - }; - const txPrebuildBounceable = { - txHex: Buffer.from(testData.signedTransaction.txBounceable, 'base64').toString('hex'), - txInfo: {}, - }; - - const txParams = { - recipients: [testData.signedTransaction.recipient], - }; - - const txParamsBounceable = { - recipients: [testData.signedTransaction.recipientBounceable], - }; - - it('should retun the right info', function () { + const txPrebuildList = [ + { + txHex: Buffer.from(testData.signedSendTransaction.tx, 'base64').toString('hex'), + txInfo: {}, + }, + { + txHex: Buffer.from(testData.signedTransferTransaction.tx, 'base64').toString('hex'), + txInfo: {}, + }, + ]; + const txPrebuildBounceableList = [ + { + txHex: Buffer.from(testData.signedSendTransaction.txBounceable, 'base64').toString('hex'), + txInfo: {}, + }, + { + txHex: Buffer.from(testData.signedTransferTransaction.txBounceable, 'base64').toString('hex'), + txInfo: {}, + }, + ]; + + const txParamsList = [ + { + recipients: [testData.signedSendTransaction.recipient], + }, + { + recipients: [testData.signedTransferTransaction.recipient], + }, + ]; + + const txParamsBounceableList = [ + { + recipients: [testData.signedSendTransaction.recipientBounceable], + }, + { + recipients: [testData.signedTransferTransaction.recipientBounceable], + }, + ]; + + it('should return the right info', function () { const ton = bitgo.coin('ton'); const tton = bitgo.coin('tton'); @@ -48,97 +70,104 @@ describe('TON:', function () { describe('Verify transaction: ', () => { const basecoin = bitgo.coin('tton'); - it('should succeed to verify transaction', async function () { - const verification = {}; - const isTransactionVerified = await basecoin.verifyTransaction({ - txParams, - txPrebuild, - verification, - } as any); - isTransactionVerified.should.equal(true); - - const isBounceableTransactionVerified = await basecoin.verifyTransaction({ - txParams: txParamsBounceable, - txPrebuild: txPrebuildBounceable, - verification: {}, - } as any); - isBounceableTransactionVerified.should.equal(true); - }); - - it('should succeed to verify transaction when recipients amount are numbers', async function () { - const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); - txParamsWithNumberAmounts.recipients[0].amount = 20000000; - const verification = {}; - await basecoin - .verifyTransaction({ - txParams: txParamsWithNumberAmounts, + txParamsList.forEach((_, index) => { + const txParams = txParamsList[index]; + const txPrebuild = txPrebuildList[index]; + const txParamsBounceable = txParamsBounceableList[index]; + const txPrebuildBounceable = txPrebuildBounceableList[index]; + + it('should succeed to verify transaction', async function () { + const verification = {}; + const isTransactionVerified = await basecoin.verifyTransaction({ + txParams, txPrebuild, verification, - } as any) - .should.rejectedWith('Tx outputs does not match with expected txParams recipients'); - }); + } as any); + isTransactionVerified.should.equal(true); + + const isBounceableTransactionVerified = await basecoin.verifyTransaction({ + txParams: txParamsBounceable, + txPrebuild: txPrebuildBounceable, + verification: {}, + } as any); + isBounceableTransactionVerified.should.equal(true); + }); - it('should succeed to verify transaction when recipients amount are strings', async function () { - const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); - txParamsWithNumberAmounts.recipients[0].amount = '20000000'; - const verification = {}; - await basecoin - .verifyTransaction({ - txParams: txParamsWithNumberAmounts, - txPrebuild, - verification, - } as any) - .should.rejectedWith('Tx outputs does not match with expected txParams recipients'); - }); + it('should succeed to verify transaction when recipients amount are numbers', async function () { + const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); + txParamsWithNumberAmounts.recipients[0].amount = 20000000; + const verification = {}; + await basecoin + .verifyTransaction({ + txParams: txParamsWithNumberAmounts, + txPrebuild, + verification, + } as any) + .should.rejectedWith('Tx outputs does not match with expected txParams recipients'); + }); - it('should succeed to verify transaction when recipients amounts are number and amount is same', async function () { - const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); - txParamsWithNumberAmounts.recipients[0].amount = 10000000; - const verification = {}; - await basecoin - .verifyTransaction({ - txParams: txParamsWithNumberAmounts, - txPrebuild, - verification, - } as any) - .should.resolvedWith(true); - }); + it('should succeed to verify transaction when recipients amount are strings', async function () { + const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); + txParamsWithNumberAmounts.recipients[0].amount = '20000000'; + const verification = {}; + await basecoin + .verifyTransaction({ + txParams: txParamsWithNumberAmounts, + txPrebuild, + verification, + } as any) + .should.rejectedWith('Tx outputs does not match with expected txParams recipients'); + }); + + it('should succeed to verify transaction when recipients amounts are number and amount is same', async function () { + const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); + txParamsWithNumberAmounts.recipients[0].amount = 10000000; + const verification = {}; + await basecoin + .verifyTransaction({ + txParams: txParamsWithNumberAmounts, + txPrebuild, + verification, + } as any) + .should.resolvedWith(true); + }); + + it('should succeed to verify transaction when recipients amounts are string and amount is same', async function () { + const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); + txParamsWithNumberAmounts.recipients[0].amount = '10000000'; + const verification = {}; + await basecoin + .verifyTransaction({ + txParams: txParamsWithNumberAmounts, + txPrebuild, + verification, + } as any) + .should.resolvedWith(true); + }); - it('should succeed to verify transaction when recipients amounts are string and amount is same', async function () { - const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); - txParamsWithNumberAmounts.recipients[0].amount = '10000000'; - const verification = {}; - await basecoin - .verifyTransaction({ + it('should succeed to verify transaction when recipient address are non bounceable', async function () { + const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); + txParamsWithNumberAmounts.recipients[0].address = new Tonweb.Address( + txParamsWithNumberAmounts.recipients[0].address + ).toString(true, true, false); + const verification = {}; + const isVerified = await basecoin.verifyTransaction({ txParams: txParamsWithNumberAmounts, txPrebuild, verification, - } as any) - .should.resolvedWith(true); - }); - - it('should succeed to verify transaction when recipient address are non bouncable', async function () { - const txParamsWithNumberAmounts = JSON.parse(JSON.stringify(txParams)); - txParamsWithNumberAmounts.recipients[0].address = new Tonweb.Address( - txParamsWithNumberAmounts.recipients[0].address - ).toString(true, true, false); - const verification = {}; - const isVerified = await basecoin.verifyTransaction({ - txParams: txParamsWithNumberAmounts, - txPrebuild, - verification, - } as any); - isVerified.should.equal(true); - }); + } as any); + isVerified.should.equal(true); + }); - it('should fail to verify transaction with invalid param', async function () { - const txPrebuild = {}; - await basecoin - .verifyTransaction({ - txParams, - txPrebuild, - } as any) - .should.rejectedWith('missing required tx prebuild property txHex'); + it('should fail to verify transaction with invalid param', async function () { + const txPrebuild = {}; + await basecoin + .verifyTransaction({ + txParams, + txPrebuild, + } as any) + .should.rejectedWith('missing required tx prebuild property txHex'); + }); }); }); @@ -146,43 +175,89 @@ describe('TON:', function () { const basecoin = bitgo.coin('tton'); it('should explain a transfer transaction', async function () { const explainedTransaction = (await basecoin.explainTransaction({ - txHex: Buffer.from(testData.signedTransaction.tx, 'base64').toString('hex'), + txHex: Buffer.from(testData.signedSendTransaction.tx, 'base64').toString('hex'), })) as TransactionExplanation; explainedTransaction.should.deepEqual({ - displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee'], + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'], id: 'tuyOkyFUMv_neV_FeNBH24Nd4cML2jUgDP4zjGkuOFI=', outputs: [ { - address: testData.signedTransaction.recipient.address, - amount: testData.signedTransaction.recipient.amount, + address: testData.signedSendTransaction.recipient.address, + amount: testData.signedSendTransaction.recipient.amount, }, ], - outputAmount: testData.signedTransaction.recipient.amount, + outputAmount: testData.signedSendTransaction.recipient.amount, changeOutputs: [], changeAmount: '0', fee: { fee: 'UNKNOWN' }, + withdrawAmount: undefined, }); }); it('should explain a non-bounceable transfer transaction', async function () { const explainedTransaction = (await basecoin.explainTransaction({ - txHex: Buffer.from(testData.signedTransaction.tx, 'base64').toString('hex'), + txHex: Buffer.from(testData.signedSendTransaction.tx, 'base64').toString('hex'), toAddressBounceable: false, fromAddressBounceable: false, })) as TransactionExplanation; explainedTransaction.should.deepEqual({ - displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee'], + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'], id: 'tuyOkyFUMv_neV_FeNBH24Nd4cML2jUgDP4zjGkuOFI=', outputs: [ { - address: testData.signedTransaction.recipientBounceable.address, - amount: testData.signedTransaction.recipientBounceable.amount, + address: testData.signedSendTransaction.recipientBounceable.address, + amount: testData.signedSendTransaction.recipientBounceable.amount, + }, + ], + outputAmount: testData.signedSendTransaction.recipientBounceable.amount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: 'UNKNOWN' }, + withdrawAmount: undefined, + }); + }); + + it('should explain a single nominator withdraw transaction', async function () { + const explainedTransaction = (await basecoin.explainTransaction({ + txHex: Buffer.from(testData.signedTransferTransaction.tx, 'base64').toString('hex'), + })) as TransactionExplanation; + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'], + id: 'GUD-auBCZ3PJFfAPkSfeqe8rj2OiHMTXudH4IEWdDgo=', + outputs: [ + { + address: testData.signedTransferTransaction.recipient.address, + amount: testData.signedTransferTransaction.recipient.amount, + }, + ], + outputAmount: testData.signedTransferTransaction.recipient.amount, + changeOutputs: [], + changeAmount: '0', + fee: { fee: 'UNKNOWN' }, + withdrawAmount: '1', + }); + }); + + it('should explain a non-bounceable single nominator withdraw transaction', async function () { + const explainedTransaction = (await basecoin.explainTransaction({ + txHex: Buffer.from(testData.signedTransferTransaction.tx, 'base64').toString('hex'), + toAddressBounceable: false, + fromAddressBounceable: false, + })) as TransactionExplanation; + explainedTransaction.should.deepEqual({ + displayOrder: ['id', 'outputs', 'outputAmount', 'changeOutputs', 'changeAmount', 'fee', 'withdrawAmount'], + id: 'GUD-auBCZ3PJFfAPkSfeqe8rj2OiHMTXudH4IEWdDgo=', + outputs: [ + { + address: testData.signedTransferTransaction.recipientBounceable.address, + amount: testData.signedTransferTransaction.recipientBounceable.amount, }, ], - outputAmount: testData.signedTransaction.recipientBounceable.amount, + outputAmount: testData.signedTransferTransaction.recipientBounceable.amount, changeOutputs: [], changeAmount: '0', fee: { fee: 'UNKNOWN' }, + withdrawAmount: '1', }); }); @@ -206,65 +281,87 @@ describe('TON:', function () { describe('Parse Transactions: ', () => { const basecoin = bitgo.coin('tton'); - const transferInputsResponse = [ - { - address: 'EQCSBjR3fUOL98WTw2F_IT4BrcqjZJWVLWUSz5WQDpaL9Jpl', - amount: '10000000', - }, + const transactionsList = [testData.signedSendTransaction.tx, testData.signedTransferTransaction.tx]; + + const transactionInputsResponseList = [ + [ + { + address: 'EQCSBjR3fUOL98WTw2F_IT4BrcqjZJWVLWUSz5WQDpaL9Jpl', + amount: '10000000', + }, + ], + [ + { + address: 'EQAbJug-k-tufWMjEC1RKSM0iiJTDUcYkC7zWANHrkT55Fol', + amount: '10000000', + }, + ], ]; - const transferInputsResponseBounceable = [ - { - address: 'UQCSBjR3fUOL98WTw2F_IT4BrcqjZJWVLWUSz5WQDpaL9Meg', - amount: '10000000', - }, + const transactionInputsResponseBounceableList = [ + [ + { + address: 'UQCSBjR3fUOL98WTw2F_IT4BrcqjZJWVLWUSz5WQDpaL9Meg', + amount: '10000000', + }, + ], + [ + { + address: 'UQAbJug-k-tufWMjEC1RKSM0iiJTDUcYkC7zWANHrkT55Afg', + amount: '10000000', + }, + ], ]; - const transferOutputsResponse = [ + const transactionOutputsResponse = [ { address: 'EQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBXwtG', amount: '10000000', }, ]; - const transferOutputsResponseBounceable = [ + const transactionOutputsResponseBounceable = [ { address: 'UQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBX1aD', amount: '10000000', }, ]; - it('should parse a transfer transaction', async function () { - const parsedTransaction = await basecoin.parseTransaction({ - txHex: Buffer.from(testData.signedTransaction.tx, 'base64').toString('hex'), - }); + transactionsList.forEach((_, index) => { + const transaction = transactionsList[index]; + const transactionInputsResponse = transactionInputsResponseList[index]; + const transactionInputsResponseBounceable = transactionInputsResponseBounceableList[index]; - parsedTransaction.should.deepEqual({ - inputs: transferInputsResponse, - outputs: transferOutputsResponse, - }); - }); + it('should parse a TON transaction', async function () { + const parsedTransaction = await basecoin.parseTransaction({ + txHex: Buffer.from(transaction, 'base64').toString('hex'), + }); - it('should parse a non-bounceable transfer transaction', async function () { - const parsedTransaction = await basecoin.parseTransaction({ - txHex: Buffer.from(testData.signedTransaction.tx, 'base64').toString('hex'), - toAddressBounceable: false, - fromAddressBounceable: false, - } as TonParseTransactionOptions); + parsedTransaction.should.deepEqual({ + inputs: transactionInputsResponse, + outputs: transactionOutputsResponse, + }); + }); - parsedTransaction.should.deepEqual({ - inputs: transferInputsResponseBounceable, - outputs: transferOutputsResponseBounceable, + it('should parse a non-bounceable TON transaction', async function () { + const parsedTransaction = await basecoin.parseTransaction({ + txHex: Buffer.from(transaction, 'base64').toString('hex'), + toAddressBounceable: false, + fromAddressBounceable: false, + } as TonParseTransactionOptions); + + parsedTransaction.should.deepEqual({ + inputs: transactionInputsResponseBounceable, + outputs: transactionOutputsResponseBounceable, + }); }); - }); - it('should fail to parse a transfer transaction when explainTransaction response is undefined', async function () { - const stub = sinon.stub(Ton.prototype, 'explainTransaction'); - stub.resolves(undefined); - await basecoin - .parseTransaction({ txHex: testData.signedTransaction.tx }) - .should.be.rejectedWith('invalid raw transaction'); - stub.restore(); + it('should fail to parse a TON transaction when explainTransaction response is undefined', async function () { + const stub = sinon.stub(Ton.prototype, 'explainTransaction'); + stub.resolves(undefined); + await basecoin.parseTransaction({ txHex: transaction }).should.be.rejectedWith('invalid raw transaction'); + stub.restore(); + }); }); }); @@ -392,7 +489,7 @@ describe('TON:', function () { ); }); - it('should derive non-bouncable address when requested', async function () { + it('should derive non-bounceable address when requested', async function () { (await utils.getAddressFromPublicKey(derivedPublicKey, false)).should.equal( 'UQDVeyUJOx3AnZGWLtE0l-Vxv7c7uTnD8OXtCFhaO-nvaqn8' ); diff --git a/modules/sdk-coin-ton/test/unit/transferBuilder.ts b/modules/sdk-coin-ton/test/unit/transferBuilder.ts index bdf4e961f2..926c1a7802 100644 --- a/modules/sdk-coin-ton/test/unit/transferBuilder.ts +++ b/modules/sdk-coin-ton/test/unit/transferBuilder.ts @@ -75,16 +75,16 @@ describe('Ton Transfer Builder', () => { }); it('should build a send from rawTx', async function () { - const txBuilder = factory.from(testData.signedTransaction.tx); + const txBuilder = factory.from(testData.signedSendTransaction.tx); const builtTx = await txBuilder.build(); const jsonTx = builtTx.toJson(); should.equal(builtTx.type, TransactionType.Send); - should.equal(builtTx.signablePayload.toString('base64'), testData.signedTransaction.signable); - should.equal(builtTx.id, testData.signedTransaction.txId); + should.equal(builtTx.signablePayload.toString('base64'), testData.signedSendTransaction.signable); + should.equal(builtTx.id, testData.signedSendTransaction.txId); const builder2 = factory.from(builtTx.toBroadcastFormat()); const builtTx2 = await builder2.build(); should.equal(builtTx2.type, TransactionType.Send); - should.equal(builtTx.toBroadcastFormat(), testData.signedTransaction.tx); + should.equal(builtTx.toBroadcastFormat(), testData.signedSendTransaction.tx); builtTx.inputs.length.should.equal(1); builtTx.outputs.length.should.equal(1); jsonTx.sender.should.equal('EQCSBjR3fUOL98WTw2F_IT4BrcqjZJWVLWUSz5WQDpaL9Jpl'); @@ -94,12 +94,12 @@ describe('Ton Transfer Builder', () => { jsonTx.expirationTime.should.equal(1695997582); const builtTx3 = await txBuilder.bounceable(false).fromAddressBounceable(false).toAddressBounceable(false).build(); - txBuilder.from(testData.signedTransaction.tx); + txBuilder.from(testData.signedSendTransaction.tx); const jsonTx3 = builtTx3.toJson(); should.equal(jsonTx3.bounceable, false); - should.equal(builtTx3.signablePayload.toString('base64'), testData.signedTransaction.bounceableSignable); - should.equal(builtTx3.id, testData.signedTransaction.txIdBounceable); - should.equal(builtTx3.toBroadcastFormat(), testData.signedTransaction.tx); + should.equal(builtTx3.signablePayload.toString('base64'), testData.signedSendTransaction.bounceableSignable); + should.equal(builtTx3.id, testData.signedSendTransaction.txIdBounceable); + should.equal(builtTx3.toBroadcastFormat(), testData.signedSendTransaction.tx); jsonTx3.sender.should.equal('UQCSBjR3fUOL98WTw2F_IT4BrcqjZJWVLWUSz5WQDpaL9Meg'); jsonTx3.destination.should.equal('UQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBX1aD'); jsonTx3.amount.should.equal('10000000'); @@ -109,8 +109,8 @@ describe('Ton Transfer Builder', () => { it('should parse a raw transaction and set flags', async function () { const factory = new TransactionBuilderFactory(coins.get('tton')); - const txBuilder = factory.from(testData.signedTransaction.tx); - const txBuilderBounceable = factory.from(testData.signedTransaction.txBounceable); + const txBuilder = factory.from(testData.signedSendTransaction.tx); + const txBuilderBounceable = factory.from(testData.signedSendTransaction.txBounceable); const tx = await txBuilder.build(); const txBounceable = await txBuilderBounceable.build(); diff --git a/modules/sdk-core/src/account-lib/baseCoin/enum.ts b/modules/sdk-core/src/account-lib/baseCoin/enum.ts index 132392c107..8486f24d2b 100644 --- a/modules/sdk-core/src/account-lib/baseCoin/enum.ts +++ b/modules/sdk-core/src/account-lib/baseCoin/enum.ts @@ -69,6 +69,7 @@ export enum TransactionType { AddPermissionlessValidator, // Closing an associated token account (e.g. SOL) CloseAssociatedTokenAccount, + SingleNominatorWithdraw, } /**