From 207d06ddf68ca3875896b0695a8b3571d53429a9 Mon Sep 17 00:00:00 2001 From: Vijay Jagannathan Date: Wed, 12 Nov 2025 00:31:23 +0530 Subject: [PATCH] fix(sdk-coin-vet): update stake and delegate builders for hayabusa Ticket: SC-3989 --- modules/sdk-coin-vet/src/lib/constants.ts | 2 +- modules/sdk-coin-vet/src/lib/iface.ts | 2 + .../transaction/delegateClauseTransaction.ts | 44 +++++++++--------- .../lib/transaction/stakeClauseTransaction.ts | 2 +- .../transactionBuilder/delegateTxnBuilder.ts | 20 +++++++-- modules/sdk-coin-vet/src/lib/utils.ts | 12 ++--- modules/sdk-coin-vet/test/resources/vet.ts | 2 +- .../delegateClauseTxnBuilder.ts | 45 ++++++++++++------- .../stakeClauseTransactionBuilder.ts | 6 +-- 9 files changed, 85 insertions(+), 50 deletions(-) diff --git a/modules/sdk-coin-vet/src/lib/constants.ts b/modules/sdk-coin-vet/src/lib/constants.ts index 2d7873733c..7663dab707 100644 --- a/modules/sdk-coin-vet/src/lib/constants.ts +++ b/modules/sdk-coin-vet/src/lib/constants.ts @@ -17,7 +17,7 @@ export const STARGATE_NFT_ADDRESS = '0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7' export const STARGATE_DELEGATION_ADDRESS = '0x4cb1c9ef05b529c093371264fab2c93cc6cddb0e'; export const STARGATE_NFT_ADDRESS_TESTNET = '0x1ec1d168574603ec35b9d229843b7c2b44bcb770'; -export const STARGATE_DELEGATION_ADDRESS_TESTNET = '0x7240e3bc0d26431512d5b67dbd26d199205bffe8'; +export const STARGATE_CONTRACT_ADDRESS_TESTNET = '0x1E02B2953AdEfEC225cF0Ec49805b1146a4429C1'; export const AVG_GAS_UNITS = '21000'; export const EXPIRATION = 400; diff --git a/modules/sdk-coin-vet/src/lib/iface.ts b/modules/sdk-coin-vet/src/lib/iface.ts index 0541f4dc4c..b927e21035 100644 --- a/modules/sdk-coin-vet/src/lib/iface.ts +++ b/modules/sdk-coin-vet/src/lib/iface.ts @@ -28,10 +28,12 @@ export interface VetTransactionData { tokenId?: string; // Added for unstaking and burn NFT transactions stakingContractAddress?: string; amountToStake?: string; + levelId?: number; // NFT tier level nftTokenId?: number; // Used as tier level (levelId) for stakeAndDelegate method (not the actual NFT token ID) autorenew?: boolean; // Autorenew flag for stakeAndDelegate method nftCollectionId?: string; claimRewardsData?: ClaimRewardsData; + validatorAddress?: string; } export interface VetTransactionExplanation extends BaseTransactionExplanation { diff --git a/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts b/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts index c588e16010..2bb2ac271e 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts @@ -6,19 +6,27 @@ import { VetTransactionData } from '../iface'; import EthereumAbi from 'ethereumjs-abi'; import utils from '../utils'; import BigNumber from 'bignumber.js'; -import { addHexPrefix } from 'ethereumjs-util'; +import { addHexPrefix, BN } from 'ethereumjs-util'; import { ZERO_VALUE_AMOUNT } from '../constants'; export class DelegateClauseTransaction extends Transaction { private _stakingContractAddress: string; - private _tokenId: number; - private _delegateForever = true; + private _tokenId: string; + private _validator: string; constructor(_coinConfig: Readonly) { super(_coinConfig); this._type = TransactionType.StakingDelegate; } + get validator(): string { + return this._validator; + } + + set validator(address: string) { + this._validator = address; + } + get stakingContractAddress(): string { return this._stakingContractAddress; } @@ -27,22 +35,14 @@ export class DelegateClauseTransaction extends Transaction { this._stakingContractAddress = address; } - get tokenId(): number { + get tokenId(): string { return this._tokenId; } - set tokenId(tokenId: number) { + set tokenId(tokenId: string) { this._tokenId = tokenId; } - get delegateForever(): boolean { - return this._delegateForever; - } - - set delegateForever(delegateForever: boolean) { - this._delegateForever = delegateForever; - } - buildClauses(): void { if (!this.stakingContractAddress) { throw new Error('Staking contract address is not set'); @@ -54,7 +54,11 @@ export class DelegateClauseTransaction extends Transaction { throw new Error('Token ID is not set'); } - const data = this.getDelegateData(this.tokenId, this.delegateForever); + if (this.validator === undefined || this.validator === null) { + throw new Error('Validator address is not set'); + } + + const data = this.getDelegateData(this.tokenId, this.validator); this._transactionData = data; // Create the clause for delegation @@ -80,10 +84,10 @@ export class DelegateClauseTransaction extends Transaction { * @param {number} tokenId - The Token ID for delegation * @returns {string} - The encoded transaction data */ - getDelegateData(levelId: number, delegateForever = true): string { + getDelegateData(tokenId: string, validatorAddress: string): string { const methodName = 'delegate'; - const types = ['uint256', 'bool']; - const params = [levelId, delegateForever]; + const types = ['uint256', 'address']; + const params = [new BN(tokenId), validatorAddress]; const method = EthereumAbi.methodID(methodName, types); const args = EthereumAbi.rawEncode(types, params); @@ -107,8 +111,8 @@ export class DelegateClauseTransaction extends Transaction { to: this.stakingContractAddress, stakingContractAddress: this.stakingContractAddress, amountToStake: ZERO_VALUE_AMOUNT, - nftTokenId: this.tokenId, - autorenew: this.delegateForever, + tokenId: this.tokenId, + validatorAddress: this.validator, }; return json; @@ -144,7 +148,7 @@ export class DelegateClauseTransaction extends Transaction { this.transactionData = clause.data; const decoded = utils.decodeDelegateClauseData(clause.data); this.tokenId = decoded.tokenId; - this.delegateForever = decoded.delegateForever; + this.validator = decoded.validator; } } diff --git a/modules/sdk-coin-vet/src/lib/transaction/stakeClauseTransaction.ts b/modules/sdk-coin-vet/src/lib/transaction/stakeClauseTransaction.ts index 4fb2aafca1..e9afc713f4 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/stakeClauseTransaction.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/stakeClauseTransaction.ts @@ -110,7 +110,7 @@ export class StakeClauseTransaction extends Transaction { to: this.stakingContractAddress, stakingContractAddress: this.stakingContractAddress, amountToStake: this.amountToStake, - nftTokenId: this.levelId, + levelId: this.levelId, }; return json; diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/delegateTxnBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/delegateTxnBuilder.ts index 137c23758e..81c6dcdcab 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/delegateTxnBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/delegateTxnBuilder.ts @@ -88,14 +88,28 @@ export class DelegateTxnBuilder extends TransactionBuilder { /** * Sets the token ID for this delegate tx. * - * @param {number} levelId - The level ID for staking + * @param {number} levelId - The NFT token ID * @returns {DelegateTxnBuilder} This transaction builder */ - tokenId(tokenId: number): this { + tokenId(tokenId: string): this { this.delegateTransaction.tokenId = tokenId; return this; } + /** + * Sets the validator address for this delegate tx. + * @param {string} address - The validator address + * @returns {DelegateTxnBuilder} This transaction builder + */ + validator(address: string): this { + if (!address) { + throw new Error('Validator address is required'); + } + this.validateAddress({ address }); + this.delegateTransaction.validator = address; + return this; + } + /** * Sets the transaction data for this delegate tx. * @@ -115,7 +129,7 @@ export class DelegateTxnBuilder extends TransactionBuilder { assert(transaction.stakingContractAddress, 'Staking contract address is required'); assert(transaction.tokenId, 'Token ID is required'); - assert(transaction.delegateForever, 'delegate forever flag is required'); + assert(transaction.validator, 'validator address is required'); this.validateAddress({ address: transaction.stakingContractAddress }); } diff --git a/modules/sdk-coin-vet/src/lib/utils.ts b/modules/sdk-coin-vet/src/lib/utils.ts index 90d1858af0..3b0fed778a 100644 --- a/modules/sdk-coin-vet/src/lib/utils.ts +++ b/modules/sdk-coin-vet/src/lib/utils.ts @@ -179,21 +179,21 @@ export class Utils implements BaseUtils { } /** - * Decodes delegate transaction data to extract tokenId and delegateForever + * Decodes delegate transaction data to extract tokenId and validatorAddress * * @param {string} data - The encoded transaction data - * @returns {object} - Object containing tokenId and delegateForever + * @returns {object} - Object containing levelId and validator address */ - decodeDelegateClauseData(data: string): { tokenId: number; delegateForever: boolean } { + decodeDelegateClauseData(data: string): { tokenId: string; validator: string } { try { const parameters = data.slice(10); // Decode using ethereumjs-abi directly - const decoded = EthereumAbi.rawDecode(['uint256', 'bool'], Buffer.from(parameters, 'hex')); + const decoded = EthereumAbi.rawDecode(['uint256', 'address'], Buffer.from(parameters, 'hex')); return { - tokenId: Number(decoded[0]), - delegateForever: Boolean(decoded[1]), + tokenId: String(decoded[0]), + validator: String(decoded[1]), }; } catch (error) { throw new Error(`Failed to decode delegation data: ${error.message}`); diff --git a/modules/sdk-coin-vet/test/resources/vet.ts b/modules/sdk-coin-vet/test/resources/vet.ts index fb7051113e..f36ad7e46e 100644 --- a/modules/sdk-coin-vet/test/resources/vet.ts +++ b/modules/sdk-coin-vet/test/resources/vet.ts @@ -15,7 +15,7 @@ export const STAKING_TRANSACTION = '0xf901032788015d55fcf2457e7c40f866f864941856c533ac2d94340aaa8544d35a5c1d4a21dee7880de0b6b3a7640000b844d8da3bbf0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000181808265848083094c53c101b882efcb9ea88e908d1a142db96c1b44dd056ea194f1ad45670c100a8c52348cc7b20387741260ebe7fe9b7594f96693c88662fa60edba5992332728222b0bdd8a30008535368bd901319eb4513d16bebc428dc8454d32a19eeb76372849a6134ebbba79f1eeceea1f6546574b945c05489222cb451f5b0e2901b0c687b750e833aeb800'; export const STAKE_CLAUSE_TRANSACTION = - '0xf8e3278801618b7b1354c4ca40f845f843941ec1d168574603ec35b9d229843b7c2b44bcb770880de0b6b3a7640000a4604f2177000000000000000000000000000000000000000000000000000000000000000881808305fd5a808307b278c101b882b380970580d957b8e7989aa9aa9281e57245fa3835cb2aaae6475b4062bb4f1c2dd2ca694df6503d5dfd654579130b3484bee75420247cf8f6b6b4b76b7939f101db6e6cef5a27375274741f3c0aba4be13a9e086337c3290866afe049efcdaa2d3227c9e12b52627c4d71f5b667821f9d33adcc4c97fdc28b93c34013d32e242300'; + '0xf8e5278801638298c53767ac40f847f845941e02b2953adefec225cf0ec49805b1146a4429c18a021e19e0c9bab2400000a4604f2177000000000000000000000000000000000000000000000000000000000000000881808306338a80830f340bc101b882b4970b0c160552162de719b9ed1fa6268bbfe9b36fd4e4a5c13e956cf539e60478f69179e8db4a3106fdbe775e3d923510b16f48da56c66076d1f66ffd822abf01ebc040c795b1e17cb0ca5ba747e3a181b4eefea7db5378f64f82361bdb7745da45aba2ace3db9822b675474552f13849052987c431cd4867813c2cf635302b1101'; export const DELEGATION_TRANSACTION = '0xf8fb278801618aa3e0a55fc940f85ef85c947240e3bc0d26431512d5b67dbd26d199205bffe880b8443207555d00000000000000000000000000000000000000000000000000000000000187690000000000000000000000000000000000000000000000000000000000000001818082e43a808306af07c101b882fb8030f6e2ef6563ff3b0e7e2a2292c1db5fc41c7ab9f598bad370c5cfd3dc32286ae8d709e941c0312c8cd33a3505156b44d1639c73980ffa66bc72f37820f2001c0e6b6e76a6a4d806c377a0a279053eb6ea4356bd235f4396585bb071d70f992c639d45c53431a3c1493a52a136203905e42c671dd384ee5f5ead0a70cb607001'; diff --git a/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts index 799d8d1f16..f5eb7a6319 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts @@ -1,14 +1,15 @@ import { coins } from '@bitgo/statics'; import { TransactionBuilderFactory, Transaction, DelegateClauseTransaction } from '../../src/lib'; import should from 'should'; -import { DELEGATE_CLAUSE_METHOD_ID, STARGATE_DELEGATION_ADDRESS_TESTNET } from '../../src/lib/constants'; +import { DELEGATE_CLAUSE_METHOD_ID, STARGATE_CONTRACT_ADDRESS_TESTNET } from '../../src/lib/constants'; import EthereumAbi from 'ethereumjs-abi'; import * as testData from '../resources/vet'; +import { BN } from 'ethereumjs-util'; describe('VET Delegation Transaction', function () { const factory = new TransactionBuilderFactory(coins.get('tvet')); - const tokenId = 100201; // Test level ID - const delegateForever = true; // Test delegateForever flag + const tokenId = '100201'; // Test token ID + const validatorAddress = '0x9a7aFCACc88c106f3bbD6B213CD0821D9224d945'; // Helper function to create a basic transaction builder with common properties const createBasicTxBuilder = () => { @@ -20,12 +21,13 @@ describe('VET Delegation Transaction', function () { txBuilder.gas(100000); txBuilder.gasPriceCoef(0); txBuilder.nonce('12345'); + txBuilder.validator(validatorAddress); return txBuilder; }; it('should build a delegate transaction', async function () { const txBuilder = factory.getStakingDelegateBuilder(); - txBuilder.stakingContractAddress(STARGATE_DELEGATION_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(STARGATE_CONTRACT_ADDRESS_TESTNET); txBuilder.tokenId(tokenId); txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9'); txBuilder.chainTag(0x27); // Testnet chain tag @@ -34,6 +36,7 @@ describe('VET Delegation Transaction', function () { txBuilder.gas(100000); txBuilder.gasPriceCoef(0); txBuilder.nonce('12345'); + txBuilder.validator(validatorAddress); const tx = await txBuilder.build(); should.exist(tx); @@ -41,14 +44,14 @@ describe('VET Delegation Transaction', function () { tx.should.be.instanceof(DelegateClauseTransaction); const delegationTx = tx as DelegateClauseTransaction; - delegationTx.stakingContractAddress.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); + delegationTx.stakingContractAddress.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); delegationTx.tokenId.should.equal(tokenId); - delegationTx.delegateForever.should.equal(delegateForever); + delegationTx.validator.should.equal(validatorAddress); // Verify clauses delegationTx.clauses.length.should.equal(1); should.exist(delegationTx.clauses[0].to); - delegationTx.clauses[0].to?.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); + delegationTx.clauses[0].to?.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); // Verify transaction data is correctly encoded using ethereumABI should.exist(delegationTx.clauses[0].data); @@ -57,8 +60,8 @@ describe('VET Delegation Transaction', function () { // Verify the encoded data matches what we expect from ethereumABI const methodName = 'delegate'; - const types = ['uint256', 'bool']; - const params = [tokenId, delegateForever]; + const types = ['uint256', 'address']; + const params = [new BN(tokenId), validatorAddress]; const method = EthereumAbi.methodID(methodName, types); const args = EthereumAbi.rawEncode(types, params); @@ -68,24 +71,33 @@ describe('VET Delegation Transaction', function () { // Verify recipients delegationTx.recipients.length.should.equal(1); - delegationTx.recipients[0].address.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); + delegationTx.recipients[0].address.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); }); describe('Failure scenarios', function () { it('should throw error when stakingContractAddress is missing', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.tokenId(tokenId); + txBuilder.validator(validatorAddress); await txBuilder.build().should.be.rejectedWith('Staking contract address is required'); }); it('should throw error when tokenId is missing', async function () { const txBuilder = createBasicTxBuilder(); - txBuilder.stakingContractAddress(STARGATE_DELEGATION_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(STARGATE_CONTRACT_ADDRESS_TESTNET); await txBuilder.build().should.be.rejectedWith('Token ID is required'); }); + it('should throw error when validator address is missing', async function () { + const txBuilder = createBasicTxBuilder(); + txBuilder.stakingContractAddress(STARGATE_CONTRACT_ADDRESS_TESTNET); + txBuilder.tokenId(tokenId); + + await txBuilder.build().should.be.rejectedWith('Validator address is required'); + }); + it('should throw error when stakingContractAddress is invalid', async function () { const txBuilder = createBasicTxBuilder(); @@ -97,7 +109,7 @@ describe('VET Delegation Transaction', function () { it('should build transaction with undefined sender but include it in inputs', async function () { const txBuilder = factory.getStakingDelegateBuilder(); - txBuilder.stakingContractAddress(STARGATE_DELEGATION_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(STARGATE_CONTRACT_ADDRESS_TESTNET); txBuilder.tokenId(tokenId); txBuilder.chainTag(0x27); txBuilder.blockRef('0x0000000000000000'); @@ -105,6 +117,7 @@ describe('VET Delegation Transaction', function () { txBuilder.gas(100000); txBuilder.gasPriceCoef(0); txBuilder.nonce('12345'); + txBuilder.validator(validatorAddress); // Not setting sender const tx = await txBuilder.build(); @@ -117,12 +130,12 @@ describe('VET Delegation Transaction', function () { // Verify the transaction has the correct output delegationTx.outputs.length.should.equal(1); - delegationTx.outputs[0].address.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); + delegationTx.outputs[0].address.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); }); it('should use network default chainTag when not explicitly set', async function () { const txBuilder = factory.getStakingDelegateBuilder(); - txBuilder.stakingContractAddress(STARGATE_DELEGATION_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(STARGATE_CONTRACT_ADDRESS_TESTNET); txBuilder.tokenId(tokenId); // Not setting chainTag txBuilder.blockRef('0x0000000000000000'); @@ -131,6 +144,7 @@ describe('VET Delegation Transaction', function () { txBuilder.gasPriceCoef(0); txBuilder.nonce('12345'); txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9'); + txBuilder.validator(validatorAddress); const tx = await txBuilder.build(); tx.should.be.instanceof(DelegateClauseTransaction); @@ -140,6 +154,7 @@ describe('VET Delegation Transaction', function () { delegationTx.chainTag.should.equal(39); }); + // TODO(SC-3926): this test needs to be updated with delegate txn data once its done in coins sandbox it('should build a signed tx and validate its toJson', async function () { const txBuilder = factory.from(testData.DELEGATION_TRANSACTION); const tx = txBuilder.transaction as DelegateClauseTransaction; @@ -153,7 +168,7 @@ describe('VET Delegation Transaction', function () { toJson.chainTag.should.equal(39); // in delegate txn, nftTokenId indicates the tokenId toJson.nftTokenId?.should.equal(tokenId); - toJson.autorenew?.should.equal(true); + toJson.validatorAddress?.should.equal(validatorAddress); }); }); }); diff --git a/modules/sdk-coin-vet/test/transactionBuilder/stakeClauseTransactionBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/stakeClauseTransactionBuilder.ts index ee0fecfe9c..9c5a5dd593 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/stakeClauseTransactionBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/stakeClauseTransactionBuilder.ts @@ -8,7 +8,7 @@ import * as testData from '../resources/vet'; describe('VET Staking Transaction', function () { const factory = new TransactionBuilderFactory(coins.get('tvet')); const stakingContractAddress = STARGATE_NFT_ADDRESS_TESTNET; - const amountToStake = '1000000000000000000'; // 1 VET in wei + const amountToStake = '10000000000000000000000'; // 10000 VET in wei const levelId = 8; // Test level ID // Helper function to create a basic transaction builder with common properties @@ -240,8 +240,8 @@ describe('VET Staking Transaction', function () { const txBuilder = factory.from(testData.STAKE_CLAUSE_TRANSACTION); const tx = txBuilder.transaction as StakeClauseTransaction; const toJson = tx.toJson(); - toJson.id.should.equal('0x2f96e4c16d70bd3e2dabec29a07eb3d6066691ba5b812d6e897676f6ebc0a798'); - toJson.stakingContractAddress?.should.equal('0x1ec1d168574603ec35b9d229843b7c2b44bcb770'); + toJson.id.should.equal('0x7148f62b42ecfdcb7ee62ac0654514e4b5f65f2fe5fdee79d4d29f56ab1722eb'); + toJson.stakingContractAddress?.should.equal('0x1E02B2953AdEfEC225cF0Ec49805b1146a4429C1'); toJson.amountToStake?.should.equal('0xde0b6b3a7640000'); toJson.nonce.should.equal('504440'); toJson.gas.should.equal(392538);