diff --git a/examples/ts/apt/stake-apt.ts b/examples/ts/apt/stake-apt.ts new file mode 100644 index 0000000000..e5f9571557 --- /dev/null +++ b/examples/ts/apt/stake-apt.ts @@ -0,0 +1,97 @@ +/** + * Performs delegated staking with Aptos. + * + * Copyright 2025, BitGo, Inc. All Rights Reserved. + */ +import { BitGoAPI } from '@bitgo/sdk-api'; +import { coins } from '@bitgo/statics'; +import { Tapt, TransactionBuilderFactory, Utils } from '@bitgo/sdk-coin-apt'; +import { Network, Aptos, AptosConfig, Account, Ed25519PrivateKey, SimpleTransaction } from '@aptos-labs/ts-sdk'; + +require('dotenv').config({ path: '../../.env' }); + +const AMOUNT_OCTAS = 11 * 100_000_000; +const NETWORK = Network.TESTNET; + +const aptosConfig = new AptosConfig({ network: NETWORK }); +const aptos = new Aptos(aptosConfig); + +const bitgo = new BitGoAPI({ + accessToken: process.env.TESTNET_ACCESS_TOKEN, + env: 'test', +}); +const coin = coins.get('tapt'); +bitgo.register(coin.name, Tapt.createInstance); + +const broadcastToSimple = (serializedTx: string) => + new SimpleTransaction(Utils.default.deserializeSignedTransaction(serializedTx).raw_txn); + +async function main() { + const account = getAccount(); + const delegationPoolAddress = getDelegationPoolAddress(); + + // Account should have sufficient balance + const accountBalance = await aptos.getAccountAPTAmount({ accountAddress: account.accountAddress }); + if (accountBalance < AMOUNT_OCTAS) { + console.info(`Balance of ${account.accountAddress} is ${accountBalance} octas, requesting funds.`); + const txn = await aptos.fundAccount({ accountAddress: account.accountAddress, amount: AMOUNT_OCTAS }); + await aptos.waitForTransaction({ transactionHash: txn.hash }); + console.info(`Funding successful: ${txn.hash}`); + } + const { sequence_number } = await aptos.getAccountInfo({ accountAddress: account.accountAddress }); + + // Use BitGoAPI to build instruction + const txBuilder = new TransactionBuilderFactory(coin).getDelegationPoolAddStakeTransactionBuilder(); + txBuilder + .sender(account.accountAddress.toString()) + .recipients([{ address: delegationPoolAddress, amount: `${AMOUNT_OCTAS}` }]) + .sequenceNumber(Number(sequence_number)); + const unsignedTx = await txBuilder.build(); + const serializedUnsignedTx = unsignedTx.toBroadcastFormat(); + + // Sign transaction. Signing is flexible, let's use Aptos libs + const authenticator = aptos.sign({ + signer: account, + transaction: broadcastToSimple(serializedUnsignedTx), + }); + if (!authenticator.isEd25519()) { + throw new Error('Example only supports Ed25519'); + } + txBuilder.addSenderSignature( + { pub: account.publicKey.toString() }, + Buffer.from(authenticator.signature.toUint8Array()) + ); + const tx = await txBuilder.build(); + const serializedTx = tx.toBroadcastFormat(); + console.info(`Transaction ${serializedTx} and JSON:\n${JSON.stringify(tx.toJson(), undefined, 2)}`); + + // Submit transaction + const submittedTxn = await aptos.transaction.submit.simple({ + transaction: broadcastToSimple(serializedTx), + senderAuthenticator: authenticator, + }); + console.log(`Success: ${submittedTxn.hash}`); +} + +const getAccount = () => { + const privateKey = process.env.APTOS_PRIVATE_KEY; + if (privateKey === undefined) { + const { privateKey } = Account.generate(); + console.log('# Here is a new account to save into your .env file.'); + console.log(`APTOS_PRIVATE_KEY=${privateKey.toAIP80String()}`); + throw new Error('Missing account information'); + } + return Account.fromPrivateKey({ privateKey: new Ed25519PrivateKey(privateKey) }); +}; + +const getDelegationPoolAddress = () => { + const address = process.env.APTOS_DELEGATION_POOL_ADDRESS; + if (!address) { + console.log('# Provide a delegation pool.'); + console.log(`APTOS_DELEGATION_POOL_ADDRESS=`); + throw new Error('Missing delegation pool address'); + } + return address; +}; + +main().catch((e) => console.error(e)); diff --git a/modules/sdk-coin-apt/src/lib/constants.ts b/modules/sdk-coin-apt/src/lib/constants.ts index 52ae9d6916..3828e94f0f 100644 --- a/modules/sdk-coin-apt/src/lib/constants.ts +++ b/modules/sdk-coin-apt/src/lib/constants.ts @@ -15,6 +15,7 @@ export const FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch export const COIN_TRANSFER_FUNCTION = '0x1::aptos_account::transfer_coins'; export const COIN_BATCH_TRANSFER_FUNCTION = '0x1::aptos_account::batch_transfer_coins'; export const DIGITAL_ASSET_TRANSFER_FUNCTION = '0x1::object::transfer'; +export const DELEGATION_POOL_ADD_STAKE_FUNCTION = '0x1::delegation_pool::add_stake'; export const APTOS_COIN = '0x1::aptos_coin::AptosCoin'; export const FUNGIBLE_ASSET_TYPE_ARGUMENT = '0x1::fungible_asset::Metadata'; diff --git a/modules/sdk-coin-apt/src/lib/transaction/delegationPoolAddStakeTransaction.ts b/modules/sdk-coin-apt/src/lib/transaction/delegationPoolAddStakeTransaction.ts new file mode 100644 index 0000000000..4676671765 --- /dev/null +++ b/modules/sdk-coin-apt/src/lib/transaction/delegationPoolAddStakeTransaction.ts @@ -0,0 +1,55 @@ +import { Transaction } from './transaction'; +import { InvalidTransactionError, TransactionType } from '@bitgo/sdk-core'; +import { + AccountAddress, + EntryFunctionABI, + InputGenerateTransactionPayloadData, + TransactionPayload, + TransactionPayloadEntryFunction, + TypeTagAddress, + TypeTagU64, +} from '@aptos-labs/ts-sdk'; + +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { APTOS_COIN, DELEGATION_POOL_ADD_STAKE_FUNCTION } from '../constants'; +import utils from '../utils'; + +export class DelegationPoolAddStakeTransaction extends Transaction { + constructor(coinConfig: Readonly) { + super(coinConfig); + this._type = TransactionType.StakingDelegate; + this._assetId = APTOS_COIN; + } + + protected parseTransactionPayload(payload: TransactionPayload): void { + if (!this.isValidPayload(payload)) { + throw new InvalidTransactionError('Invalid transaction payload'); + } + const { entryFunction } = payload; + const addressArg = entryFunction.args[0]; + const amountArg = entryFunction.args[1]; + this.recipients = utils.parseRecipients(addressArg, amountArg); + } + + protected getTransactionPayloadData(): InputGenerateTransactionPayloadData { + return { + function: DELEGATION_POOL_ADD_STAKE_FUNCTION, + typeArguments: [], + functionArguments: [AccountAddress.fromString(this.recipients[0].address), this.recipients[0].amount], + abi: this.abi, + }; + } + + private isValidPayload(payload: TransactionPayload): payload is TransactionPayloadEntryFunction { + return ( + payload instanceof TransactionPayloadEntryFunction && + payload.entryFunction.args.length === 2 && + payload.entryFunction.type_args.length === 0 + ); + } + + private abi: EntryFunctionABI = { + typeParameters: [], + parameters: [new TypeTagAddress(), new TypeTagU64()], + }; +} diff --git a/modules/sdk-coin-apt/src/lib/transaction/transaction.ts b/modules/sdk-coin-apt/src/lib/transaction/transaction.ts index c00019f529..51e10d3b70 100644 --- a/modules/sdk-coin-apt/src/lib/transaction/transaction.ts +++ b/modules/sdk-coin-apt/src/lib/transaction/transaction.ts @@ -304,7 +304,6 @@ export abstract class Transaction extends BaseTransaction { try { signedTxn = utils.deserializeSignedTransaction(rawTransaction); } catch (e) { - console.error('invalid raw transaction', e); throw new Error('invalid raw transaction'); } this.fromDeserializedSignedTransaction(signedTxn); @@ -318,7 +317,6 @@ export abstract class Transaction extends BaseTransaction { try { return utils.deserializeSignedTransaction(signedRawTransaction); } catch (e) { - console.error('invalid raw transaction', e); throw new Error('invalid raw transaction'); } } diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilder/delegationPoolAddStakeTransactionBuilder.ts b/modules/sdk-coin-apt/src/lib/transactionBuilder/delegationPoolAddStakeTransactionBuilder.ts new file mode 100644 index 0000000000..46ab0890e9 --- /dev/null +++ b/modules/sdk-coin-apt/src/lib/transactionBuilder/delegationPoolAddStakeTransactionBuilder.ts @@ -0,0 +1,44 @@ +import { TransactionBuilder } from './transactionBuilder'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; +import { TransactionType } from '@bitgo/sdk-core'; +import utils from '../utils'; +import { TransactionPayload, TransactionPayloadEntryFunction } from '@aptos-labs/ts-sdk'; +import { DelegationPoolAddStakeTransaction } from '../transaction/delegationPoolAddStakeTransaction'; + +export class DelegationPoolAddStakeTransactionBuilder extends TransactionBuilder { + constructor(_coinConfig: Readonly) { + super(_coinConfig); + this.transaction = new DelegationPoolAddStakeTransaction(_coinConfig); + } + + protected get transactionType(): TransactionType { + return TransactionType.StakingDelegate; + } + + assetId(_assetId: string): TransactionBuilder { + this.transaction.assetId = _assetId; + return this; + } + + protected isValidTransactionPayload(payload: TransactionPayload): boolean { + try { + if (!this.isValidPayload(payload)) { + return false; + } + const { entryFunction } = payload; + const addressArg = entryFunction.args[0]; + const amountArg = entryFunction.args[1]; + return utils.fetchAndValidateRecipients(addressArg, amountArg).isValid; + } catch (e) { + return false; + } + } + + private isValidPayload(payload: TransactionPayload): payload is TransactionPayloadEntryFunction { + return ( + payload instanceof TransactionPayloadEntryFunction && + payload.entryFunction.args.length === 2 && + payload.entryFunction.type_args.length === 0 + ); + } +} diff --git a/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts index f2f09e8f46..6f31f40e75 100644 --- a/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-apt/src/lib/transactionBuilderFactory.ts @@ -12,6 +12,8 @@ import { DigitalAssetTransfer } from './transaction/digitalAssetTransfer'; import { DigitalAssetTransferBuilder } from './transactionBuilder/digitalAssetTransferBuilder'; import { CustomTransaction } from './transaction/customTransaction'; import { CustomTransactionBuilder } from './transactionBuilder/customTransactionBuilder'; +import { DelegationPoolAddStakeTransaction } from './transaction/delegationPoolAddStakeTransaction'; +import { DelegationPoolAddStakeTransactionBuilder } from './transactionBuilder/delegationPoolAddStakeTransactionBuilder'; export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { constructor(_coinConfig: Readonly) { @@ -37,6 +39,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { const digitalAssetTransferTx = new DigitalAssetTransfer(this._coinConfig); digitalAssetTransferTx.fromDeserializedSignedTransaction(signedTxn); return this.getDigitalAssetTransactionBuilder(digitalAssetTransferTx); + case TransactionType.StakingDelegate: + const delegateTx = new DelegationPoolAddStakeTransaction(this._coinConfig); + delegateTx.fromDeserializedSignedTransaction(signedTxn); + return this.getDelegationPoolAddStakeTransactionBuilder(delegateTx); case TransactionType.CustomTx: const customTx = new CustomTransaction(this._coinConfig); if (abi) { @@ -72,6 +78,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { return this.initializeBuilder(tx, new DigitalAssetTransferBuilder(this._coinConfig)); } + getDelegationPoolAddStakeTransactionBuilder(tx?: Transaction): DelegationPoolAddStakeTransactionBuilder { + return this.initializeBuilder(tx, new DelegationPoolAddStakeTransactionBuilder(this._coinConfig)); + } + /** * Get a custom transaction builder * diff --git a/modules/sdk-coin-apt/src/lib/utils.ts b/modules/sdk-coin-apt/src/lib/utils.ts index cb4afb908a..9bb201efe1 100644 --- a/modules/sdk-coin-apt/src/lib/utils.ts +++ b/modules/sdk-coin-apt/src/lib/utils.ts @@ -28,6 +28,7 @@ import { APT_TRANSACTION_ID_LENGTH, COIN_BATCH_TRANSFER_FUNCTION, COIN_TRANSFER_FUNCTION, + DELEGATION_POOL_ADD_STAKE_FUNCTION, DIGITAL_ASSET_TRANSFER_FUNCTION, FUNGIBLE_ASSET_BATCH_TRANSFER_FUNCTION, FUNGIBLE_ASSET_TRANSFER_FUNCTION, @@ -97,6 +98,8 @@ export class Utils implements BaseUtils { return TransactionType.SendToken; case DIGITAL_ASSET_TRANSFER_FUNCTION: return TransactionType.SendNFT; + case DELEGATION_POOL_ADD_STAKE_FUNCTION: + return TransactionType.StakingDelegate; default: // For any other function calls, treat as a custom transaction return TransactionType.CustomTx; diff --git a/modules/sdk-coin-apt/test/resources/apt.ts b/modules/sdk-coin-apt/test/resources/apt.ts index cd962711c2..a2baad7dc1 100644 --- a/modules/sdk-coin-apt/test/resources/apt.ts +++ b/modules/sdk-coin-apt/test/resources/apt.ts @@ -83,6 +83,13 @@ export const digitalTokenRecipients: Recipient[] = [ }, ]; +export const delegationPoolAddStakeRecipients: Recipient[] = [ + { + address: addresses.validAddresses[0], + amount: AMOUNT.toString(), + }, +]; + export const invalidRecipients: Recipient[] = [ { address: addresses.invalidAddresses[0], @@ -135,3 +142,9 @@ export const FUNGIBLE_BATCH_RAW_TX_HEX = export const FUNGIBLE_BATCH_SIGNABLE_PAYLOAD = '5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010d6170746f735f6163636f756e741e62617463685f7472616e736665725f66756e6769626c655f617373657473000320d5d0d561493ea2b9410f67da804653ae44e793c2423707d4f11edb2e381920504102dd52c0b72a73696b867d6571a308c413e43bff8f44956a5991abc4d50db0b8492a81760d52db9a96df2609860218214b6d1012e77e84a3fed5145a9a65bf6932110201000000000000000100000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2'; + +export const DELEGATION_POOL_ADD_STAKE_TX_HEX = + '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d670000000002030020000000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2002000000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'; + +export const DELEGATION_POOL_ADD_STAKE_TX_HEX_SIGNABLE_PAYLOAD = + '5efa3c4f02f83a0f4b2d69fc95c607cc02825cc4e7be536ef0992df050d9e67c011aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a3724490e000000000000000200000000000000000000000000000000000000000000000000000000000000010f64656c65676174696f6e5f706f6f6c096164645f7374616b65000220f7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad908e803000000000000400d03000000000064000000000000008b037d67000000000200dbc87a1c816d9bcd06b683c37e80c7162e4d48da7812198b830e4d5d8e0629f2'; diff --git a/modules/sdk-coin-apt/test/unit/apt.ts b/modules/sdk-coin-apt/test/unit/apt.ts index 54cade0f1c..f7d8d03ff6 100644 --- a/modules/sdk-coin-apt/test/unit/apt.ts +++ b/modules/sdk-coin-apt/test/unit/apt.ts @@ -19,6 +19,7 @@ import { } from '@aptos-labs/ts-sdk'; import utils from '../../src/lib/utils'; import { AptCoin, coins, GasTankAccountCoin } from '@bitgo/statics'; +import { DelegationPoolAddStakeTransaction } from '../../src/lib/transaction/delegationPoolAddStakeTransaction'; describe('APT:', function () { let bitgo: TestBitGoAPI; @@ -229,6 +230,39 @@ describe('APT:', function () { }); }); + it('should explain a staking delegate transaction', async function () { + const rawTx = testData.DELEGATION_POOL_ADD_STAKE_TX_HEX; + const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt')); + transaction.fromRawTransaction(rawTx); + const explainedTx = transaction.explainTransaction(); + explainedTx.should.deepEqual({ + displayOrder: [ + 'id', + 'outputs', + 'outputAmount', + 'changeOutputs', + 'changeAmount', + 'fee', + 'withdrawAmount', + 'sender', + 'type', + ], + id: '0xc5b960d1bec149c77896344774352c61441307af564eaa8c84f857208e411bf3', + outputs: [ + { + address: '0xf7405c28a02cf5bab4ea4498240bb3579db45951794eb1c843bef0534c093ad9', + amount: '1000', + }, + ], + outputAmount: '1000', + changeOutputs: [], + changeAmount: '0', + fee: { fee: '0' }, + sender: '0x1aed808916ab9b1b30b07abb53561afd46847285ce28651221d406173a372449', + type: 30, + }); + }); + it('should fail to explain a invalid raw transaction', async function () { const rawTx = 'invalidRawTransaction'; const transaction = new TransferTransaction(coins.get('tapt')); diff --git a/modules/sdk-coin-apt/test/unit/transactionBuilder/delegationPoolAddStakeTransactionBuilder.ts b/modules/sdk-coin-apt/test/unit/transactionBuilder/delegationPoolAddStakeTransactionBuilder.ts new file mode 100644 index 0000000000..330a2f7911 --- /dev/null +++ b/modules/sdk-coin-apt/test/unit/transactionBuilder/delegationPoolAddStakeTransactionBuilder.ts @@ -0,0 +1,152 @@ +import { getBuilderFactory } from '../getBuilderFactory'; +import { coins } from '@bitgo/statics'; +import * as testData from '../../resources/apt'; +import { TransactionType } from '@bitgo/sdk-core'; +import should from 'should'; +import { DelegationPoolAddStakeTransaction } from '../../../src/lib/transaction/delegationPoolAddStakeTransaction'; + +describe('Apt Token Transfer Builder', () => { + const factory = getBuilderFactory('tapt'); + + describe('Succeed', () => { + it('should build a staking delegate transaction', async function () { + const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt')); + const txBuilder = factory.getDelegationPoolAddStakeTransactionBuilder(transaction); + txBuilder.sender(testData.sender.address); + txBuilder.recipients(testData.delegationPoolAddStakeRecipients); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction; + should.equal(tx.sender, testData.sender.address); + should.equal(tx.recipients[0].address, testData.delegationPoolAddStakeRecipients[0].address); + should.equal(tx.recipients[0].amount, testData.delegationPoolAddStakeRecipients[0].amount); + should.equal(tx.maxGasAmount, 200000); + should.equal(tx.gasUnitPrice, 100); + should.equal(tx.sequenceNumber, 14); + should.equal(tx.expirationTime, 1736246155); + should.equal(tx.type, TransactionType.StakingDelegate); + should.deepEqual(tx.inputs, [ + { + address: testData.sender.address, + value: testData.delegationPoolAddStakeRecipients[0].amount, + coin: 'tapt', + }, + ]); + should.deepEqual(tx.outputs, [ + { + address: testData.delegationPoolAddStakeRecipients[0].address, + value: testData.delegationPoolAddStakeRecipients[0].amount, + coin: 'tapt', + }, + ]); + const rawTx = tx.toBroadcastFormat(); + should.equal(txBuilder.isValidRawTransaction(rawTx), true); + rawTx.should.equal(testData.DELEGATION_POOL_ADD_STAKE_TX_HEX); + }); + + it('should build and send a signed tx', async function () { + const txBuilder = factory.from(testData.DELEGATION_POOL_ADD_STAKE_TX_HEX); + const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction; + tx.inputs.should.deepEqual([ + { + address: testData.sender.address, + value: testData.delegationPoolAddStakeRecipients[0].amount, + coin: 'tapt', + }, + ]); + tx.outputs.should.deepEqual([ + { + address: testData.delegationPoolAddStakeRecipients[0].address, + value: testData.delegationPoolAddStakeRecipients[0].amount, + coin: 'tapt', + }, + ]); + should.equal(tx.id, '0xc5b960d1bec149c77896344774352c61441307af564eaa8c84f857208e411bf3'); + should.equal(tx.maxGasAmount, 200000); + should.equal(tx.gasUnitPrice, 100); + should.equal(tx.sequenceNumber, 14); + should.equal(tx.expirationTime, 1736246155); + should.equal(tx.type, TransactionType.StakingDelegate); + const rawTx = tx.toBroadcastFormat(); + should.equal(txBuilder.isValidRawTransaction(rawTx), true); + should.equal(rawTx, testData.DELEGATION_POOL_ADD_STAKE_TX_HEX); + }); + + it('should succeed to validate a valid signablePayload', async function () { + const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt')); + const txBuilder = factory.getDelegationPoolAddStakeTransactionBuilder(transaction); + txBuilder.sender(testData.sender.address); + txBuilder.recipients([testData.delegationPoolAddStakeRecipients[0]]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction; + const signablePayload = tx.signablePayload; + should.equal(signablePayload.toString('hex'), testData.DELEGATION_POOL_ADD_STAKE_TX_HEX_SIGNABLE_PAYLOAD); + }); + + it('should build a unsigned tx and validate its toJson', async function () { + const transaction = new DelegationPoolAddStakeTransaction(coins.get('tapt')); + const txBuilder = factory.getDelegationPoolAddStakeTransactionBuilder(transaction); + txBuilder.sender(testData.sender.address); + txBuilder.recipients([testData.delegationPoolAddStakeRecipients[0]]); + txBuilder.gasData({ + maxGasAmount: 200000, + gasUnitPrice: 100, + }); + txBuilder.sequenceNumber(14); + txBuilder.expirationTime(1736246155); + txBuilder.assetId(testData.fungibleTokenAddress.usdt); + txBuilder.addFeePayerAddress(testData.feePayer.address); + const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction; + const toJson = tx.toJson(); + should.equal(toJson.sender, testData.sender.address); + should.deepEqual(toJson.recipients, [ + { + address: testData.delegationPoolAddStakeRecipients[0].address, + amount: testData.delegationPoolAddStakeRecipients[0].amount, + }, + ]); + should.deepEqual(toJson.recipient, { + address: testData.delegationPoolAddStakeRecipients[0].address, + amount: testData.delegationPoolAddStakeRecipients[0].amount, + }); + should.equal(toJson.sequenceNumber, 14); + should.equal(toJson.maxGasAmount, 200000); + should.equal(toJson.gasUnitPrice, 100); + should.equal(toJson.expirationTime, 1736246155); + should.equal(toJson.feePayer, testData.feePayer.address); + }); + + it('should build a signed tx and validate its toJson', async function () { + const txBuilder = factory.from(testData.DELEGATION_POOL_ADD_STAKE_TX_HEX); + const tx = (await txBuilder.build()) as DelegationPoolAddStakeTransaction; + const toJson = tx.toJson(); + should.equal(toJson.id, '0xc5b960d1bec149c77896344774352c61441307af564eaa8c84f857208e411bf3'); + should.equal(toJson.sender, testData.sender.address); + should.deepEqual(toJson.recipients, [ + { + address: testData.delegationPoolAddStakeRecipients[0].address, + amount: testData.delegationPoolAddStakeRecipients[0].amount.toString(), + }, + ]); + should.deepEqual(toJson.recipient, { + address: testData.delegationPoolAddStakeRecipients[0].address, + amount: testData.delegationPoolAddStakeRecipients[0].amount.toString(), + }); + should.equal(toJson.maxGasAmount, 200000); + should.equal(toJson.gasUnitPrice, 100); + should.equal(toJson.sequenceNumber, 14); + should.equal(toJson.expirationTime, 1736246155); + }); + }); +});