From 22be163c685a47804ef3563cfa173c26e248669d Mon Sep 17 00:00:00 2001 From: Vijay Jagannathan Date: Mon, 17 Nov 2025 20:42:50 +0530 Subject: [PATCH] fix(sdk-coin-vet): fix exit delegation + burn NFT for hayabusa upgrade Ticket: SC-3990 --- .../src/lib/transaction/burnNftTransaction.ts | 22 +++++--- .../src/lib/transaction/exitDelegation.ts | 22 +++++--- .../lib/transactionBuilder/burnNftBuilder.ts | 16 +++--- .../exitDelegationBuilder.ts | 25 +++++---- modules/sdk-coin-vet/test/resources/vet.ts | 6 +++ .../test/transactionBuilder/burnNftBuilder.ts | 53 ++++++++----------- .../exitDelegationBuilder.ts | 53 ++++++++----------- 7 files changed, 106 insertions(+), 91 deletions(-) diff --git a/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts b/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts index f45d1c565d..2038e7be5e 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts @@ -10,6 +10,7 @@ import utils from '../utils'; export class BurnNftTransaction extends Transaction { private _tokenId: string; + private _stakingContractAddress: string; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -24,6 +25,14 @@ export class BurnNftTransaction extends Transaction { this._tokenId = id; } + get stakingContractAddress(): string { + return this._stakingContractAddress; + } + + set stakingContractAddress(address: string) { + this._stakingContractAddress = address; + } + /** @inheritdoc */ async build(): Promise { this.buildClauses(); @@ -34,15 +43,15 @@ export class BurnNftTransaction extends Transaction { /** @inheritdoc */ buildClauses(): void { - if (!this._contract || !this._tokenId) { + if (!this._stakingContractAddress || !this._tokenId) { throw new InvalidTransactionError('Missing required burn NFT parameters'); } - utils.validateStakingContractAddress(this._contract, this._coinConfig); + utils.validateStakingContractAddress(this._stakingContractAddress, this._coinConfig); this._clauses = [ { - to: this._contract, + to: this._stakingContractAddress, value: '0x0', data: this._transactionData || this.getBurnNftData(), }, @@ -50,7 +59,7 @@ export class BurnNftTransaction extends Transaction { this._recipients = [ { - address: this._contract, + address: this._stakingContractAddress, amount: '0', }, ]; @@ -87,7 +96,8 @@ export class BurnNftTransaction extends Transaction { data: this.transactionData || this.getBurnNftData(), value: '0', sender: this.sender, - to: this.contract, + to: this.stakingContractAddress, + tokenId: this.tokenId, }; return json; } @@ -114,7 +124,7 @@ export class BurnNftTransaction extends Transaction { this.nonce = String(body.nonce); // Set data from clauses - this.contract = body.clauses[0]?.to || '0x0'; + this.stakingContractAddress = body.clauses[0]?.to || '0x0'; this.transactionData = body.clauses[0]?.data || '0x0'; this.type = TransactionType.StakingWithdraw; diff --git a/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts b/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts index 13a4fe5e0c..93f8302777 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts @@ -11,6 +11,7 @@ import BigNumber from 'bignumber.js'; export class ExitDelegationTransaction extends Transaction { private _tokenId: string; + private _stakingContractAddress: string; constructor(_coinConfig: Readonly) { super(_coinConfig); @@ -25,6 +26,14 @@ export class ExitDelegationTransaction extends Transaction { this._tokenId = id; } + get stakingContractAddress(): string { + return this._stakingContractAddress; + } + + set stakingContractAddress(address: string) { + this._stakingContractAddress = address; + } + /** @inheritdoc */ async build(): Promise { this.buildClauses(); @@ -35,15 +44,15 @@ export class ExitDelegationTransaction extends Transaction { /** @inheritdoc */ buildClauses(): void { - if (!this._contract || !this._tokenId) { + if (!this._stakingContractAddress || !this._tokenId) { throw new InvalidTransactionError('Missing required unstaking parameters'); } - utils.validateDelegationContractAddress(this._contract, this._coinConfig); + utils.validateStakingContractAddress(this._stakingContractAddress, this._coinConfig); this._clauses = [ { - to: this._contract, + to: this._stakingContractAddress, value: '0x0', data: this._transactionData || this.getExitDelegationData(), }, @@ -51,7 +60,7 @@ export class ExitDelegationTransaction extends Transaction { this._recipients = [ { - address: this._contract, + address: this._stakingContractAddress, amount: '0', }, ]; @@ -88,7 +97,8 @@ export class ExitDelegationTransaction extends Transaction { data: this.transactionData || this.getExitDelegationData(), value: '0', sender: this.sender, - to: this.contract, + to: this.stakingContractAddress, + tokenId: this.tokenId, }; return json; } @@ -115,7 +125,7 @@ export class ExitDelegationTransaction extends Transaction { this.nonce = String(body.nonce); // Set data from clauses - this.contract = body.clauses[0]?.to || '0x0'; + this.stakingContractAddress = body.clauses[0]?.to || '0x0'; this.transactionData = body.clauses[0]?.data || '0x0'; this.type = TransactionType.StakingUnlock; diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts index 1e1badd3f6..ba9b6a0141 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts @@ -92,19 +92,19 @@ export class BurnNftBuilder extends TransactionBuilder { } /** - * Sets the NFT contract address for this burn NFT transaction. + * Sets the staking contract address for this staking tx. * The address must be explicitly provided to ensure the correct contract is used. * - * @param {string} address - The NFT contract address (required) - * @returns {BurnNftBuilder} This transaction builder + * @param {string} address - The staking contract address (required) + * @returns {StakingBuilder} This transaction builder * @throws {Error} If no address is provided */ - nftContract(address: string): this { + stakingContractAddress(address: string): this { if (!address) { - throw new Error('NFT contract address is required and must be explicitly provided'); + throw new Error('Staking contract address is required'); } this.validateAddress({ address }); - this.burnNftTransaction.contract = address; + this.burnNftTransaction.stakingContractAddress = address; return this; } @@ -113,10 +113,10 @@ export class BurnNftBuilder extends TransactionBuilder { if (!transaction) { throw new Error('transaction not defined'); } - assert(transaction.contract, 'NFT contract address is required'); + assert(transaction.stakingContractAddress, 'Staking contract address is required'); assert(transaction.tokenId, 'Token ID is required'); - this.validateAddress({ address: transaction.contract }); + this.validateAddress({ address: transaction.stakingContractAddress }); } /** @inheritdoc */ diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts index 3444191871..93c098f84c 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts @@ -48,7 +48,7 @@ export class ExitDelegationBuilder extends TransactionBuilder { } /** - * Validates the transaction clauses for unstaking. + * Validates the transaction clauses for exit delegation. * @param {TransactionClause[]} clauses - The transaction clauses to validate. * @returns {boolean} - Returns true if the clauses are valid, false otherwise. */ @@ -92,16 +92,19 @@ export class ExitDelegationBuilder extends TransactionBuilder { } /** - * Sets the delegation contract address for this unstaking transaction. - * If not provided, uses the network-appropriate default address. + * Sets the staking contract address for this staking tx. + * The address must be explicitly provided to ensure the correct contract is used. * - * @param {string} address - The delegation contract address - * @returns {ExitDelegationBuilder} This transaction builder + * @param {string} address - The staking contract address (required) + * @returns {StakingBuilder} This transaction builder + * @throws {Error} If no address is provided */ - delegationContract(address?: string): this { - const contractAddress = address || utils.getDefaultDelegationAddress(this._coinConfig); - this.validateAddress({ address: contractAddress }); - this.exitDelegationTransaction.contract = contractAddress; + stakingContractAddress(address: string): this { + if (!address) { + throw new Error('Staking contract address is required'); + } + this.validateAddress({ address }); + this.exitDelegationTransaction.stakingContractAddress = address; return this; } @@ -110,10 +113,10 @@ export class ExitDelegationBuilder extends TransactionBuilder { if (!transaction) { throw new Error('transaction not defined'); } - assert(transaction.contract, 'Delegation contract address is required'); + assert(transaction.stakingContractAddress, 'Staking contract address is required'); assert(transaction.tokenId, 'Token ID is required'); - this.validateAddress({ address: transaction.contract }); + this.validateAddress({ address: transaction.stakingContractAddress }); } /** @inheritdoc */ diff --git a/modules/sdk-coin-vet/test/resources/vet.ts b/modules/sdk-coin-vet/test/resources/vet.ts index 32dd6fb8d3..d77c2f0d24 100644 --- a/modules/sdk-coin-vet/test/resources/vet.ts +++ b/modules/sdk-coin-vet/test/resources/vet.ts @@ -20,6 +20,12 @@ export const STAKE_CLAUSE_TRANSACTION = export const DELEGATION_TRANSACTION = '0xf8fc278801640639091a26ce40f85ef85c941e02b2953adefec225cf0ec49805b1146a4429c180b84408bbb8240000000000000000000000000000000000000000000000000000000000003d2e00000000000000000000000000563ec3cafbbe7e60b04b3190e6eca66579706d8180830464b080830d8b05c101b8821a3cca8e8339456c6055ef796e5d716dda00de45f4cd9431bedf2119ae5de01b1f0a7268690784ba8f5c22b3043d0530ece5303a813ffdd9c0a5ae0ae85deee400b04543d6874f30eca88b3efb927c44934e9eb64a6f2327cce44a0a94faaca13615d153e804ba3fdd02bf5f8e1b6bc8e0f6149a1c7694803ed4fbb549bb79066101'; +export const EXIT_DELEGATION_TRANSACTION = + '0xf8db278801640bf461bc7e1840f83df83b941e02b2953adefec225cf0ec49805b1146a4429c180a469e79b7d0000000000000000000000000000000000000000000000000000000000003d2d81808303525f808305f65ac101b8820cb393317793011b0a205973c77761f5c5c8652c21fe0115f527d2e2f2c1b5fc72a048107b263764312e9323f2ace9f30ce0beed873d7ef7f5432943330d2d5000a4a5f6439503f235ac6a5e17b47ac26c9e0c9e3be9dbd4cec3266fea324eb9bf5f806cedca59ff4144deb0ca18c41d9d6d600a86bf3d4e7b930bcec9b04c2e7301'; + +export const BURN_NFT_TRANSACTION = + '0xf8db278801640bfe6c1ee3e640f83df83b941e02b2953adefec225cf0ec49805b1146a4429c180a42e17de780000000000000000000000000000000000000000000000000000000000003d2d81808305548c808304fe38c101b882044933b92e0fc5517d58205b46211a5ad2403103c8c217ce9682ebe2457e374f655fc6be307c7dfd59f0f4eda2aab7e3a1ac9219923086cde52e6405c34de2d801d692df740f95dd4ac6dbae7eb6e91a712a1e456e1a80e3a2f501ea1e6ed12c4308e65a8a98b0142190812d4484f54121bbc95b6048ae09de5946304affbfba1400'; + export const STAKING_LEVEL_ID = 8; export const STAKING_AUTORENEW = true; export const STAKING_CONTRACT_ADDRESS = '0x1e02b2953adefec225cf0ec49805b1146a4429c1'; diff --git a/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts index 9ff10dd5de..fd9ca04601 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts @@ -8,6 +8,7 @@ import { BURN_NFT_METHOD_ID, STARGATE_CONTRACT_ADDRESS_TESTNET } from '../../src describe('Vet Burn NFT Transaction', () => { const factory = new TransactionBuilderFactory(coins.get('tvet')); + const stakingContractAddress = STARGATE_CONTRACT_ADDRESS_TESTNET; describe('Build and Sign', () => { it('should build a burn NFT transaction', async function () { @@ -16,7 +17,7 @@ describe('Vet Burn NFT Transaction', () => { txBuilder.sender(testData.addresses.validAddresses[0]); txBuilder.tokenId(tokenId); - txBuilder.nftContract(STARGATE_CONTRACT_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.gas(21000); txBuilder.nonce('64248'); txBuilder.blockRef('0x014ead140e77bbc1'); @@ -27,7 +28,7 @@ describe('Vet Burn NFT Transaction', () => { should.equal(tx.sender, testData.addresses.validAddresses[0]); should.equal(tx.tokenId, tokenId); - should.equal(tx.contract, STARGATE_CONTRACT_ADDRESS_TESTNET); + should.equal(tx.stakingContractAddress, STARGATE_CONTRACT_ADDRESS_TESTNET); should.equal(tx.gas, 21000); should.equal(tx.nonce, '64248'); should.equal(tx.expiration, 64); @@ -56,28 +57,6 @@ describe('Vet Burn NFT Transaction', () => { should.equal(tx.outputs[0].coin, 'tvet'); }); - it('should build a burn NFT transaction with custom contract address', async function () { - const tokenId = '123456'; - const customContractAddress = STARGATE_CONTRACT_ADDRESS_TESTNET; // Use the valid testnet NFT address - const txBuilder = factory.getBurnNftBuilder(); - - txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.tokenId(tokenId); - txBuilder.nftContract(customContractAddress); - txBuilder.gas(21000); - txBuilder.nonce('64248'); - txBuilder.blockRef('0x014ead140e77bbc1'); - txBuilder.expiration(64); - txBuilder.gasPriceCoef(128); - - const tx = (await txBuilder.build()) as BurnNftTransaction; - - should.equal(tx.contract, customContractAddress); - should.exist(tx.clauses[0]); - should.exist(tx.clauses[0].to); - tx.clauses[0]?.to?.should.equal(customContractAddress); - }); - it('should deserialize and reserialize a signed burn NFT transaction', async function () { // Create a mock serialized transaction for burn NFT const tokenId = '123456'; @@ -85,7 +64,7 @@ describe('Vet Burn NFT Transaction', () => { txBuilder.sender(testData.addresses.validAddresses[0]); txBuilder.tokenId(tokenId); - txBuilder.nftContract(STARGATE_CONTRACT_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.gas(21000); txBuilder.nonce('64248'); txBuilder.blockRef('0x014ead140e77bbc1'); @@ -101,17 +80,17 @@ describe('Vet Burn NFT Transaction', () => { should.equal(deserializedTx.type, TransactionType.StakingWithdraw); should.equal(deserializedTx.tokenId, tokenId); - should.equal(deserializedTx.contract, STARGATE_CONTRACT_ADDRESS_TESTNET); + should.equal(deserializedTx.stakingContractAddress, STARGATE_CONTRACT_ADDRESS_TESTNET); }); it('should validate the transaction data structure', async function () { const txBuilder = factory.getBurnNftBuilder(); // Should throw error when building without required fields - await should(txBuilder.build()).be.rejectedWith('NFT contract address is required'); + await should(txBuilder.build()).be.rejectedWith('Staking contract address is required'); txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.nftContract(STARGATE_CONTRACT_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(stakingContractAddress); await should(txBuilder.build()).be.rejectedWith('Token ID is required'); // Now add the token ID and it should build successfully @@ -123,18 +102,32 @@ describe('Vet Burn NFT Transaction', () => { const tx = await txBuilder.build(); should.exist(tx); }); + + it('should build from raw signed tx', async function () { + const txBuilder = factory.from(testData.BURN_NFT_TRANSACTION); + const tx = txBuilder.transaction as BurnNftTransaction; + const toJson = tx.toJson(); + toJson.id.should.equal('0xf5e074f2d127fa3ef014873ec193b76823efab51891d43861092bd52b122563e'); + toJson.stakingContractAddress?.should.equal('0x1e02b2953adefec225cf0ec49805b1146a4429c1'); + toJson.nonce.should.equal('327224'); + toJson.gas.should.equal(349324); + toJson.gasPriceCoef.should.equal(128); + toJson.expiration.should.equal(64); + toJson.chainTag.should.equal(39); + toJson.tokenId?.should.equal('15661'); + }); }); describe('Validation', () => { it('should fail with invalid contract address', function () { const txBuilder = factory.getBurnNftBuilder(); - should(() => txBuilder.nftContract('invalid-address')).throwError('Invalid address invalid-address'); + should(() => txBuilder.stakingContractAddress('invalid-address')).throwError('Invalid address invalid-address'); }); it('should fail with invalid token ID', async function () { const txBuilder = factory.getBurnNftBuilder(); txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.nftContract(STARGATE_CONTRACT_ADDRESS_TESTNET); + txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.tokenId(''); await should(txBuilder.build()).be.rejectedWith('Token ID is required'); diff --git a/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts index d8f35dacb2..f2a13c3c6e 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts @@ -8,6 +8,7 @@ import { EXIT_DELEGATION_METHOD_ID, STARGATE_CONTRACT_ADDRESS_TESTNET } from '.. describe('Vet Exit Delegation Transaction', () => { const factory = new TransactionBuilderFactory(coins.get('tvet')); + const stakingContractAddress = STARGATE_CONTRACT_ADDRESS_TESTNET; describe('Build and Sign', () => { it('should build an exit delegation transaction', async function () { @@ -16,7 +17,7 @@ describe('Vet Exit Delegation Transaction', () => { txBuilder.sender(testData.addresses.validAddresses[0]); txBuilder.tokenId(tokenId); - txBuilder.delegationContract(); // Use default address + txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.gas(21000); txBuilder.nonce('64248'); txBuilder.blockRef('0x014ead140e77bbc1'); @@ -27,7 +28,7 @@ describe('Vet Exit Delegation Transaction', () => { should.equal(tx.sender, testData.addresses.validAddresses[0]); should.equal(tx.tokenId, tokenId); - should.equal(tx.contract, STARGATE_CONTRACT_ADDRESS_TESTNET); + should.equal(tx.stakingContractAddress, STARGATE_CONTRACT_ADDRESS_TESTNET); should.equal(tx.gas, 21000); should.equal(tx.nonce, '64248'); should.equal(tx.expiration, 64); @@ -56,28 +57,6 @@ describe('Vet Exit Delegation Transaction', () => { should.equal(tx.outputs[0].coin, 'tvet'); }); - it('should build an exit delegation transaction with custom contract address', async function () { - const tokenId = '123456'; - const customContractAddress = STARGATE_CONTRACT_ADDRESS_TESTNET; // Use the valid testnet delegation address - const txBuilder = factory.getExitDelegationBuilder(); - - txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.tokenId(tokenId); - txBuilder.delegationContract(customContractAddress); - txBuilder.gas(21000); - txBuilder.nonce('64248'); - txBuilder.blockRef('0x014ead140e77bbc1'); - txBuilder.expiration(64); - txBuilder.gasPriceCoef(128); - - const tx = (await txBuilder.build()) as ExitDelegationTransaction; - - should.equal(tx.contract, customContractAddress); - should.exist(tx.clauses[0]); - should.exist(tx.clauses[0].to); - tx.clauses[0]?.to?.should.equal(customContractAddress); - }); - it('should deserialize and reserialize a signed exit delegation transaction', async function () { // Create a mock serialized transaction for exit delegation const tokenId = '123456'; @@ -85,7 +64,7 @@ describe('Vet Exit Delegation Transaction', () => { txBuilder.sender(testData.addresses.validAddresses[0]); txBuilder.tokenId(tokenId); - txBuilder.delegationContract(); + txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.gas(21000); txBuilder.nonce('64248'); txBuilder.blockRef('0x014ead140e77bbc1'); @@ -101,17 +80,17 @@ describe('Vet Exit Delegation Transaction', () => { should.equal(deserializedTx.type, TransactionType.StakingUnlock); should.equal(deserializedTx.tokenId, tokenId); - should.equal(deserializedTx.contract, STARGATE_CONTRACT_ADDRESS_TESTNET); + should.equal(deserializedTx.stakingContractAddress, STARGATE_CONTRACT_ADDRESS_TESTNET); }); it('should validate the transaction data structure', async function () { const txBuilder = factory.getExitDelegationBuilder(); // Should throw error when building without required fields - await should(txBuilder.build()).be.rejectedWith('Delegation contract address is required'); + await should(txBuilder.build()).be.rejectedWith('Staking contract address is required'); txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.delegationContract(); + txBuilder.stakingContractAddress(stakingContractAddress); await should(txBuilder.build()).be.rejectedWith('Token ID is required'); // Now add the token ID and it should build successfully @@ -123,18 +102,32 @@ describe('Vet Exit Delegation Transaction', () => { const tx = await txBuilder.build(); should.exist(tx); }); + + it('should build from raw signed tx', async function () { + const txBuilder = factory.from(testData.EXIT_DELEGATION_TRANSACTION); + const tx = txBuilder.transaction as ExitDelegationTransaction; + const toJson = tx.toJson(); + toJson.id.should.equal('0xeca0ba2c8fa91332a1fe037232aa0af9fe6e939313458a6838c6d4060ede0278'); + toJson.stakingContractAddress?.should.equal('0x1e02b2953adefec225cf0ec49805b1146a4429c1'); + toJson.nonce.should.equal('390746'); + toJson.gas.should.equal(217695); + toJson.gasPriceCoef.should.equal(128); + toJson.expiration.should.equal(64); + toJson.chainTag.should.equal(39); + toJson.tokenId?.should.equal('15661'); + }); }); describe('Validation', () => { it('should fail with invalid contract address', function () { const txBuilder = factory.getExitDelegationBuilder(); - should(() => txBuilder.delegationContract('invalid-address')).throwError('Invalid address invalid-address'); + should(() => txBuilder.stakingContractAddress('invalid-address')).throwError('Invalid address invalid-address'); }); it('should fail with invalid token ID', async function () { const txBuilder = factory.getExitDelegationBuilder(); txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.delegationContract(); + txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.tokenId(''); await should(txBuilder.build()).be.rejectedWith('Token ID is required');