From 612580764da038a11ed23ac32c80e8d464c4d027 Mon Sep 17 00:00:00 2001 From: Gautam2305 Date: Sat, 27 Sep 2025 18:58:30 +0530 Subject: [PATCH] fix: correct methods and abi for vechain Ticket: SC-3296 --- modules/sdk-coin-vet/src/lib/constants.ts | 13 +- modules/sdk-coin-vet/src/lib/iface.ts | 2 + .../src/lib/transaction/burnNftTransaction.ts | 9 +- .../src/lib/transaction/claimRewards.ts | 117 ++++-------- .../src/lib/transaction/exitDelegation.ts | 9 +- .../src/lib/transaction/stakingTransaction.ts | 51 +++++- .../lib/transactionBuilder/burnNftBuilder.ts | 31 +--- .../transactionBuilder/claimRewardsBuilder.ts | 42 ++--- .../exitDelegationBuilder.ts | 32 +--- .../lib/transactionBuilder/stakingBuilder.ts | 35 +++- modules/sdk-coin-vet/src/lib/types.ts | 4 +- modules/sdk-coin-vet/src/lib/utils.ts | 167 ++++++++++++++++-- modules/sdk-coin-vet/test/resources/vet.ts | 6 +- .../test/transactionBuilder/burnNftBuilder.ts | 18 +- .../transactionBuilder/claimRewardsBuilder.ts | 146 ++++++++------- .../exitDelegationBuilder.ts | 10 +- .../transactionBuilder/stakingTransaction.ts | 68 ++++--- 17 files changed, 463 insertions(+), 297 deletions(-) diff --git a/modules/sdk-coin-vet/src/lib/constants.ts b/modules/sdk-coin-vet/src/lib/constants.ts index 0a82d04818..bb2bf298ca 100644 --- a/modules/sdk-coin-vet/src/lib/constants.ts +++ b/modules/sdk-coin-vet/src/lib/constants.ts @@ -3,12 +3,15 @@ export const VET_ADDRESS_LENGTH = 40; export const VET_BLOCK_ID_LENGTH = 64; export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb'; -export const STAKING_METHOD_ID = '0xa694fc3a'; -export const EXIT_DELEGATION_METHOD_ID = '0x3fb7a871'; -export const BURN_NFT_METHOD_ID = '0x42966c68'; +export const STAKING_METHOD_ID = '0xd8da3bbf'; +export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d'; +export const BURN_NFT_METHOD_ID = '0x2e17de78'; export const TRANSFER_NFT_METHOD_ID = '0x23b872dd'; -export const CLAIM_BASE_REWARDS_METHOD_ID = '0x037402d3'; -export const CLAIM_STAKING_REWARDS_METHOD_ID = '0xeb2767fa'; +export const CLAIM_BASE_REWARDS_METHOD_ID = '0x858d50e8'; // claimVetGeneratedVtho(uint256) +export const CLAIM_STAKING_REWARDS_METHOD_ID = '0x0962ef79'; // claimRewards(uint256) 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'; diff --git a/modules/sdk-coin-vet/src/lib/iface.ts b/modules/sdk-coin-vet/src/lib/iface.ts index 27280a3c04..0541f4dc4c 100644 --- a/modules/sdk-coin-vet/src/lib/iface.ts +++ b/modules/sdk-coin-vet/src/lib/iface.ts @@ -28,6 +28,8 @@ export interface VetTransactionData { tokenId?: string; // Added for unstaking and burn NFT transactions stakingContractAddress?: string; amountToStake?: string; + 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; } diff --git a/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts b/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts index 86232e7f61..5a7004b125 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts @@ -6,6 +6,7 @@ import { VetTransactionData } from '../iface'; import { BURN_NFT_METHOD_ID } from '../constants'; import EthereumAbi from 'ethereumjs-abi'; import { addHexPrefix } from 'ethereumjs-util'; +import utils from '../utils'; export class BurnNftTransaction extends Transaction { private _tokenId: string; @@ -37,6 +38,8 @@ export class BurnNftTransaction extends Transaction { throw new InvalidTransactionError('Missing required burn NFT parameters'); } + utils.validateStakingContractAddress(this._contract, this._coinConfig); + this._clauses = [ { to: this._contract, @@ -53,7 +56,7 @@ export class BurnNftTransaction extends Transaction { * @returns {string} The encoded transaction data as a hex string */ private getBurnNftData(): string { - const methodName = 'burn'; + const methodName = 'unstake'; const types = ['uint256']; const params = [this._tokenId]; @@ -110,9 +113,7 @@ export class BurnNftTransaction extends Transaction { // Extract tokenId from transaction data if (this.transactionData.startsWith(BURN_NFT_METHOD_ID)) { - const tokenIdHex = this.transactionData.slice(BURN_NFT_METHOD_ID.length); - // Convert hex to decimal - this.tokenId = parseInt(tokenIdHex, 16).toString(); + this.tokenId = utils.decodeBurnNftData(this.transactionData); } // Set sender address diff --git a/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts b/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts index 14b2f4e190..6641e00e85 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts @@ -6,11 +6,8 @@ import { Transaction as VetTransaction, Secp256k1, TransactionClause } from '@ve import { Transaction } from './transaction'; import { VetTransactionData } from '../iface'; import { ClaimRewardsData } from '../types'; -import { - CLAIM_BASE_REWARDS_METHOD_ID, - CLAIM_STAKING_REWARDS_METHOD_ID, - STARGATE_DELEGATION_ADDRESS, -} from '../constants'; +import { CLAIM_BASE_REWARDS_METHOD_ID, CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants'; +import utils from '../utils'; export class ClaimRewardsTransaction extends Transaction { private _claimRewardsData: ClaimRewardsData; @@ -83,42 +80,26 @@ export class ClaimRewardsTransaction extends Transaction { } /** - * Get the delegation contract address to use for claims - * Uses the address from claimRewardsData if provided, otherwise falls back to default - */ - private getDelegationAddress(): string { - return this._claimRewardsData.delegationContractAddress || STARGATE_DELEGATION_ADDRESS; - } - - /** - * Build clause for claiming base rewards + * Build clause for claiming base rewards (claimVetGeneratedVtho) */ private buildClaimBaseRewardsClause(): TransactionClause { - const methodData = this.encodeClaimRewardsMethod( - CLAIM_BASE_REWARDS_METHOD_ID, - this._claimRewardsData.validatorAddress, - this._claimRewardsData.delegatorAddress - ); + const methodData = this.encodeClaimRewardsMethod(CLAIM_BASE_REWARDS_METHOD_ID, this._claimRewardsData.tokenId); return { - to: this.getDelegationAddress(), + to: utils.getDefaultStakingAddress(this._coinConfig), value: '0x0', data: methodData, }; } /** - * Build clause for claiming staking rewards + * Build clause for claiming staking rewards (claimRewards) */ private buildClaimStakingRewardsClause(): TransactionClause { - const methodData = this.encodeClaimRewardsMethod( - CLAIM_STAKING_REWARDS_METHOD_ID, - this._claimRewardsData.validatorAddress, - this._claimRewardsData.delegatorAddress - ); + const methodData = this.encodeClaimRewardsMethod(CLAIM_STAKING_REWARDS_METHOD_ID, this._claimRewardsData.tokenId); return { - to: this.getDelegationAddress(), + to: utils.getDefaultDelegationAddress(this._coinConfig), value: '0x0', data: methodData, }; @@ -127,10 +108,10 @@ export class ClaimRewardsTransaction extends Transaction { /** * Encode the claim rewards method call data */ - private encodeClaimRewardsMethod(methodId: string, validatorAddress: string, delegatorAddress: string): string { - const methodName = methodId === CLAIM_BASE_REWARDS_METHOD_ID ? 'claimBaseRewards' : 'claimStakingRewards'; - const types = ['address', 'address']; - const params = [validatorAddress, delegatorAddress]; + private encodeClaimRewardsMethod(methodId: string, tokenId: string): string { + const methodName = methodId === CLAIM_BASE_REWARDS_METHOD_ID ? 'claimVetGeneratedVtho' : 'claimRewards'; + const types = ['uint256']; + const params = [tokenId]; const method = EthereumAbi.methodID(methodName, types); const args = EthereumAbi.rawEncode(types, params); @@ -215,35 +196,29 @@ export class ClaimRewardsTransaction extends Transaction { let claimBaseRewards = false; let claimStakingRewards = false; - let validatorAddress = ''; - let delegatorAddress = ''; + let tokenId = ''; let delegationContractAddress = ''; + let stargateNftAddress = ''; for (const clause of clauses) { - // Check if this is a claim rewards clause by looking at the method ID in data - if ( - clause.data && - (clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID) || - clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)) - ) { - // Store the contract address (could be different from default) - if (!delegationContractAddress) { - delegationContractAddress = clause.to || ''; - } - + if (clause.data) { if (clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)) { + // claimVetGeneratedVtho should go to STARGATE_NFT_ADDRESS claimBaseRewards = true; - if (!validatorAddress || !delegatorAddress) { - const addresses = this.parseAddressesFromClaimData(clause.data); - validatorAddress = addresses.validator; - delegatorAddress = addresses.delegator; + if (!tokenId) { + tokenId = utils.decodeClaimRewardsData(clause.data); + } + if (!stargateNftAddress && clause.to) { + stargateNftAddress = clause.to; } } else if (clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)) { + // claimRewards should go to STARGATE_DELEGATION_ADDRESS claimStakingRewards = true; - if (!validatorAddress || !delegatorAddress) { - const addresses = this.parseAddressesFromClaimData(clause.data); - validatorAddress = addresses.validator; - delegatorAddress = addresses.delegator; + if (!tokenId) { + tokenId = utils.decodeClaimRewardsData(clause.data); + } + if (!delegationContractAddress && clause.to) { + delegationContractAddress = clause.to; } } } @@ -254,41 +229,15 @@ export class ClaimRewardsTransaction extends Transaction { } this._claimRewardsData = { - validatorAddress, - delegatorAddress, + tokenId, delegationContractAddress: - delegationContractAddress !== STARGATE_DELEGATION_ADDRESS ? delegationContractAddress : undefined, + delegationContractAddress && !utils.isDelegationContractAddress(delegationContractAddress) + ? delegationContractAddress + : undefined, + stargateNftAddress: + stargateNftAddress && !utils.isNftContractAddress(stargateNftAddress) ? stargateNftAddress : undefined, claimBaseRewards, claimStakingRewards, }; } - - /** - * Parse validator and delegator addresses from claim rewards method data. - * - * The method data follows Ethereum ABI encoding where each parameter occupies 32 bytes (64 hex chars). - * After the 4-byte method ID, the parameters are laid out as: - * - Bytes 0-31 (chars 0-63): First address parameter (validator) - right-padded, actual address in bytes 12-31 - * - Bytes 32-63 (chars 64-127): Second address parameter (delegator) - right-padded, actual address in bytes 44-63 - * - * @param data The encoded method call data including method ID and parameters - * @returns Object containing the extracted validator and delegator addresses - */ - private parseAddressesFromClaimData(data: string): { validator: string; delegator: string } { - // Remove method ID (first 10 characters: '0x' + 4-byte method ID) - const methodData = data.slice(10); - - // Extract validator address from first parameter (bytes 12-31 of first 32-byte slot) - // Slice 24-64: Skip first 12 bytes of padding (24 hex chars), take next 20 bytes (40 hex chars) - const validatorAddress = '0x' + methodData.slice(24, 64); - - // Extract delegator address from second parameter (bytes 44-63 of second 32-byte slot) - // Slice 88-128: Skip to second slot + 12 bytes padding (88 hex chars), take next 20 bytes (40 hex chars) - const delegatorAddress = '0x' + methodData.slice(88, 128); - - return { - validator: validatorAddress, - delegator: delegatorAddress, - }; - } } diff --git a/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts b/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts index f98f4fb639..740c4eab25 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts @@ -6,6 +6,7 @@ import { VetTransactionData } from '../iface'; import { EXIT_DELEGATION_METHOD_ID } from '../constants'; import EthereumAbi from 'ethereumjs-abi'; import { addHexPrefix } from 'ethereumjs-util'; +import utils from '../utils'; export class ExitDelegationTransaction extends Transaction { private _tokenId: string; @@ -37,6 +38,8 @@ export class ExitDelegationTransaction extends Transaction { throw new InvalidTransactionError('Missing required unstaking parameters'); } + utils.validateDelegationContractAddress(this._contract, this._coinConfig); + this._clauses = [ { to: this._contract, @@ -53,7 +56,7 @@ export class ExitDelegationTransaction extends Transaction { * @returns {string} The encoded transaction data as a hex string */ private getExitDelegationData(): string { - const methodName = 'exitDelegation'; + const methodName = 'requestDelegationExit'; const types = ['uint256']; const params = [this._tokenId]; @@ -110,9 +113,7 @@ export class ExitDelegationTransaction extends Transaction { // Extract tokenId from transaction data if (this.transactionData.startsWith(EXIT_DELEGATION_METHOD_ID)) { - const tokenIdHex = this.transactionData.slice(EXIT_DELEGATION_METHOD_ID.length); - // Convert hex to decimal - this.tokenId = parseInt(tokenIdHex, 16).toString(); + this.tokenId = utils.decodeExitDelegationData(this.transactionData); } // Set sender address diff --git a/modules/sdk-coin-vet/src/lib/transaction/stakingTransaction.ts b/modules/sdk-coin-vet/src/lib/transaction/stakingTransaction.ts index ef72d7dd0b..f3a1230b2d 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/stakingTransaction.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/stakingTransaction.ts @@ -6,15 +6,19 @@ import { VetTransactionData } from '../iface'; import EthereumAbi from 'ethereumjs-abi'; import utils from '../utils'; import BigNumber from 'bignumber.js'; +import { addHexPrefix } from 'ethereumjs-util'; export class StakingTransaction extends Transaction { private _stakingContractAddress: string; + private _levelId: number; + private _autorenew = true; private _amountToStake: string; private _stakingContractABI: EthereumAbi; constructor(_coinConfig: Readonly) { super(_coinConfig); this._type = TransactionType.ContractCall; + this._autorenew = true; } get stakingContractAddress(): string { @@ -25,6 +29,22 @@ export class StakingTransaction extends Transaction { this._stakingContractAddress = address; } + get levelId(): number { + return this._levelId; + } + + set levelId(levelId: number) { + this._levelId = levelId; + } + + get autorenew(): boolean { + return this._autorenew; + } + + set autorenew(autorenew: boolean) { + this._autorenew = autorenew; + } + get amountToStake(): string { return this._amountToStake; } @@ -46,12 +66,17 @@ export class StakingTransaction extends Transaction { throw new Error('Staking contract address is not set'); } + utils.validateStakingContractAddress(this.stakingContractAddress, this._coinConfig); + + if (this.levelId === undefined || this.levelId === null) { + throw new Error('Level ID is not set'); + } + if (!this.amountToStake) { throw new Error('Amount to stake is not set'); } - // Generate transaction data using ethereumjs-abi - const data = utils.getStakingData(this.amountToStake); + const data = this.getStakingData(this.levelId, this.autorenew); this._transactionData = data; // Create the clause for staking @@ -71,6 +96,23 @@ export class StakingTransaction extends Transaction { }, ]; } + /** + * Encodes staking transaction data using ethereumjs-abi for stakeAndDelegate method + * + * @param {number} levelId - The level ID for staking + * @param {boolean} autorenew - Whether to enable autorenew + * @returns {string} - The encoded transaction data + */ + getStakingData(levelId: number, autorenew = true): string { + const methodName = 'stakeAndDelegate'; + const types = ['uint8', 'bool']; + const params = [levelId, autorenew]; + + const method = EthereumAbi.methodID(methodName, types); + const args = EthereumAbi.rawEncode(types, params); + + return addHexPrefix(Buffer.concat([method, args]).toString('hex')); + } toJson(): VetTransactionData { const json: VetTransactionData = { @@ -88,6 +130,8 @@ export class StakingTransaction extends Transaction { to: this.stakingContractAddress, stakingContractAddress: this.stakingContractAddress, amountToStake: this.amountToStake, + nftTokenId: this.levelId, + autorenew: this.autorenew, }; return json; @@ -124,6 +168,9 @@ export class StakingTransaction extends Transaction { } if (clause.data) { this.transactionData = clause.data; + const decoded = utils.decodeStakingData(clause.data); + this.levelId = decoded.levelId; + this.autorenew = decoded.autorenew; } } diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts index 1a9bc493a7..1e1badd3f6 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/burnNftBuilder.ts @@ -2,14 +2,12 @@ import assert from 'assert'; import { TransactionClause } from '@vechain/sdk-core'; import { TransactionType } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import EthereumAbi from 'ethereumjs-abi'; -import { addHexPrefix } from 'ethereumjs-util'; import { TransactionBuilder } from './transactionBuilder'; import { BurnNftTransaction } from '../transaction/burnNftTransaction'; import { Transaction } from '../transaction/transaction'; import utils from '../utils'; -import { BURN_NFT_METHOD_ID, STARGATE_NFT_ADDRESS } from '../constants'; +import { BURN_NFT_METHOD_ID } from '../constants'; export class BurnNftBuilder extends TransactionBuilder { /** @@ -95,12 +93,16 @@ export class BurnNftBuilder extends TransactionBuilder { /** * Sets the NFT contract address for this burn NFT transaction. - * If not provided, uses the default address from constants. + * The address must be explicitly provided to ensure the correct contract is used. * - * @param {string} address - The NFT contract address + * @param {string} address - The NFT contract address (required) * @returns {BurnNftBuilder} This transaction builder + * @throws {Error} If no address is provided */ - nftContract(address: string = STARGATE_NFT_ADDRESS): this { + nftContract(address: string): this { + if (!address) { + throw new Error('NFT contract address is required and must be explicitly provided'); + } this.validateAddress({ address }); this.burnNftTransaction.contract = address; return this; @@ -123,21 +125,4 @@ export class BurnNftBuilder extends TransactionBuilder { await this.burnNftTransaction.build(); return this.transaction; } - - /** - * Generates the transaction data for burning NFT by encoding the burn method call. - * - * @private - * @returns {string} The encoded transaction data as a hex string - */ - private getBurnNftData(): string { - const methodName = 'burn'; - const types = ['uint256']; - const params = [this.burnNftTransaction.tokenId]; - - const method = EthereumAbi.methodID(methodName, types); - const args = EthereumAbi.rawEncode(types, params); - - return addHexPrefix(Buffer.concat([method, args]).toString('hex')); - } } diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts index 82c1de51ad..4489b13f3f 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts @@ -7,11 +7,7 @@ import { TransactionBuilder } from './transactionBuilder'; import { ClaimRewardsTransaction } from '../transaction/claimRewards'; import { Transaction } from '../transaction/transaction'; import { ClaimRewardsData } from '../types'; -import { - CLAIM_BASE_REWARDS_METHOD_ID, - CLAIM_STAKING_REWARDS_METHOD_ID, - STARGATE_DELEGATION_ADDRESS, -} from '../constants'; +import { CLAIM_BASE_REWARDS_METHOD_ID, CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants'; import utils from '../utils'; export class ClaimRewardsBuilder extends TransactionBuilder { @@ -75,11 +71,13 @@ export class ClaimRewardsBuilder extends TransactionBuilder { return false; } - // Check if the clause is for claim rewards operations - if (clause.to.toLowerCase() === STARGATE_DELEGATION_ADDRESS.toLowerCase() && clause.data) { + const isDelegationContract = utils.isDelegationContractAddress(clause.to); + const isNftContract = utils.isNftContractAddress(clause.to); + + if (clause.data && (isDelegationContract || isNftContract)) { if ( - clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID) || - clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) + (clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID) && isNftContract) || + (clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) && isDelegationContract) ) { hasValidClaimClause = true; } @@ -114,20 +112,13 @@ export class ClaimRewardsBuilder extends TransactionBuilder { throw new Error('Claim rewards data is required'); } - if (!data.validatorAddress) { - throw new Error('Validator address is required'); - } - - if (!data.delegatorAddress) { - throw new Error('Delegator address is required'); + if (!data.tokenId) { + throw new Error('Token ID is required'); } - if (!utils.isValidAddress(data.validatorAddress)) { - throw new Error('Invalid validator address format'); - } - - if (!utils.isValidAddress(data.delegatorAddress)) { - throw new Error('Invalid delegator address format'); + // Validate tokenId is a valid number string + if (!/^\d+$/.test(data.tokenId)) { + throw new Error('Token ID must be a valid number string'); } if (data.claimBaseRewards !== undefined && typeof data.claimBaseRewards !== 'boolean') { @@ -155,11 +146,12 @@ export class ClaimRewardsBuilder extends TransactionBuilder { const claimData = transaction.claimRewardsData; assert(claimData, 'Claim rewards data is required'); - assert(claimData.validatorAddress, 'Validator address is required'); - assert(claimData.delegatorAddress, 'Delegator address is required'); + assert(claimData.tokenId, 'Token ID is required'); - this.validateAddress({ address: claimData.validatorAddress }); - this.validateAddress({ address: claimData.delegatorAddress }); + // Validate tokenId is a valid number string + if (!/^\d+$/.test(claimData.tokenId)) { + throw new Error('Token ID must be a valid number string'); + } } /** @inheritdoc */ diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts index e7dba3fe0a..3444191871 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/exitDelegationBuilder.ts @@ -2,14 +2,12 @@ import assert from 'assert'; import { TransactionClause } from '@vechain/sdk-core'; import { TransactionType } from '@bitgo/sdk-core'; import { BaseCoin as CoinConfig } from '@bitgo/statics'; -import EthereumAbi from 'ethereumjs-abi'; -import { addHexPrefix } from 'ethereumjs-util'; import { TransactionBuilder } from './transactionBuilder'; import { ExitDelegationTransaction } from '../transaction/exitDelegation'; import { Transaction } from '../transaction/transaction'; import utils from '../utils'; -import { EXIT_DELEGATION_METHOD_ID, STARGATE_DELEGATION_ADDRESS } from '../constants'; +import { EXIT_DELEGATION_METHOD_ID } from '../constants'; export class ExitDelegationBuilder extends TransactionBuilder { /** @@ -95,14 +93,15 @@ export class ExitDelegationBuilder extends TransactionBuilder { /** * Sets the delegation contract address for this unstaking transaction. - * If not provided, uses the default address from constants. + * If not provided, uses the network-appropriate default address. * * @param {string} address - The delegation contract address * @returns {ExitDelegationBuilder} This transaction builder */ - delegationContract(address: string = STARGATE_DELEGATION_ADDRESS): this { - this.validateAddress({ address }); - this.exitDelegationTransaction.contract = address; + delegationContract(address?: string): this { + const contractAddress = address || utils.getDefaultDelegationAddress(this._coinConfig); + this.validateAddress({ address: contractAddress }); + this.exitDelegationTransaction.contract = contractAddress; return this; } @@ -120,26 +119,7 @@ export class ExitDelegationBuilder extends TransactionBuilder { /** @inheritdoc */ protected async buildImplementation(): Promise { this.transaction.type = this.transactionType; - // Set the transaction data before building - this.exitDelegationTransaction.transactionData = this.getExitDelegationData(); await this.exitDelegationTransaction.build(); return this.transaction; } - - /** - * Generates the transaction data for exit delegation by encoding the exitDelegation method call. - * - * @private - * @returns {string} The encoded transaction data as a hex string - */ - private getExitDelegationData(): string { - const methodName = 'exitDelegation'; - const types = ['uint256']; - const params = [this.exitDelegationTransaction.tokenId]; - - const method = EthereumAbi.methodID(methodName, types); - const args = EthereumAbi.rawEncode(types, params); - - return addHexPrefix(Buffer.concat([method, args]).toString('hex')); - } } diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/stakingBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/stakingBuilder.ts index 65b0d46a0e..26c7eded83 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/stakingBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/stakingBuilder.ts @@ -77,18 +77,34 @@ export class StakingBuilder extends TransactionBuilder { /** * 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 staking contract address + * @param {string} address - The staking contract address (required) * @returns {StakingBuilder} This transaction builder + * @throws {Error} If no address is provided */ stakingContractAddress(address: string): this { + if (!address) { + throw new Error('Staking contract address is required'); + } this.validateAddress({ address }); this.stakingTransaction.stakingContractAddress = address; return this; } /** - * Sets the amount to stake for this staking tx. + * Sets the level ID for this staking tx. + * + * @param {number} levelId - The level ID for staking + * @returns {StakingBuilder} This transaction builder + */ + levelId(levelId: number): this { + this.stakingTransaction.levelId = levelId; + return this; + } + + /** + * Sets the amount to stake for this staking tx (VET amount being sent). * * @param {string} amount - The amount to stake in wei * @returns {StakingBuilder} This transaction builder @@ -127,7 +143,22 @@ export class StakingBuilder extends TransactionBuilder { } assert(transaction.stakingContractAddress, 'Staking contract address is required'); assert(transaction.amountToStake, 'Amount to stake is required'); + + // Validate amount is a valid number string + if (transaction.amountToStake) { + try { + const bn = new (require('bignumber.js'))(transaction.amountToStake); + if (!bn.isFinite() || bn.isNaN()) { + throw new Error('Invalid character'); + } + } catch (e) { + throw new Error('Invalid character'); + } + } + assert(transaction.stakingContractABI, 'Staking contract ABI is required'); + assert(transaction.levelId, 'Level ID is required'); + assert(transaction.autorenew, 'Autorenew flag is required'); this.validateAddress({ address: transaction.stakingContractAddress }); } diff --git a/modules/sdk-coin-vet/src/lib/types.ts b/modules/sdk-coin-vet/src/lib/types.ts index 9a8eb2bb0d..6cd98044dc 100644 --- a/modules/sdk-coin-vet/src/lib/types.ts +++ b/modules/sdk-coin-vet/src/lib/types.ts @@ -9,9 +9,9 @@ export interface VetParseTransactionOptions extends ParseTransactionOptions { } export interface ClaimRewardsData { - validatorAddress: string; - delegatorAddress: string; + tokenId: string; delegationContractAddress?: string; + stargateNftAddress?: string; claimBaseRewards?: boolean; claimStakingRewards?: boolean; } diff --git a/modules/sdk-coin-vet/src/lib/utils.ts b/modules/sdk-coin-vet/src/lib/utils.ts index 953530cace..6f8e9f6eb6 100644 --- a/modules/sdk-coin-vet/src/lib/utils.ts +++ b/modules/sdk-coin-vet/src/lib/utils.ts @@ -19,8 +19,13 @@ import { TRANSFER_NFT_METHOD_ID, CLAIM_BASE_REWARDS_METHOD_ID, CLAIM_STAKING_REWARDS_METHOD_ID, + STARGATE_NFT_ADDRESS, + STARGATE_NFT_ADDRESS_TESTNET, + STARGATE_DELEGATION_ADDRESS, + STARGATE_DELEGATION_ADDRESS_TESTNET, } from './constants'; import { KeyPair } from './keyPair'; +import { BaseCoin as CoinConfig } from '@bitgo/statics'; export class Utils implements BaseUtils { isValidAddress(address: string): boolean { @@ -125,20 +130,25 @@ export class Utils implements BaseUtils { } /** - * Encodes staking transaction data using ethereumjs-abi + * Decodes staking transaction data to extract levelId and autorenew * - * @param {string} stakingAmount - The amount to stake in wei - * @returns {string} - The encoded transaction data + * @param {string} data - The encoded transaction data + * @returns {object} - Object containing levelId and autorenew */ - getStakingData(stakingAmount: string): string { - const methodName = 'stake'; - const types = ['uint256']; - const params = [new BN(stakingAmount)]; + decodeStakingData(data: string): { levelId: number; autorenew: boolean } { + try { + const parameters = data.slice(10); - const method = EthereumAbi.methodID(methodName, types); - const args = EthereumAbi.rawEncode(types, params); + // Decode using ethereumjs-abi directly + const decoded = EthereumAbi.rawDecode(['uint8', 'bool'], Buffer.from(parameters, 'hex')); - return addHexPrefix(Buffer.concat([method, args]).toString('hex')); + return { + levelId: Number(decoded[0]), + autorenew: Boolean(decoded[1]), + }; + } catch (error) { + throw new Error(`Failed to decode staking data: ${error.message}`); + } } decodeTransferTokenData(data: string): TransactionRecipient { @@ -175,6 +185,143 @@ export class Utils implements BaseUtils { tokenId, }; } + + /** + * Decodes claim rewards transaction data to extract tokenId + * + * @param {string} data - The encoded claim rewards method call data + * @returns {string} - The tokenId as a string + */ + decodeClaimRewardsData(data: string): string { + try { + // Remove method ID (first 10 characters: '0x' + 4-byte method ID) + const methodData = data.slice(10); + + // Extract tokenId from first 32-byte slot + // The tokenId is a uint256, so we take the full 64 hex characters + const tokenIdHex = methodData.slice(0, 64); + + // Convert hex to decimal string + return BigInt('0x' + tokenIdHex).toString(); + } catch (error) { + throw new Error(`Failed to decode claim rewards data: ${error.message}`); + } + } + + /** + * Decodes exit delegation transaction data to extract tokenId + * + * @param {string} data - The encoded exit delegation method call data + * @returns {string} - The tokenId as a string + */ + decodeExitDelegationData(data: string): string { + try { + if (!data.startsWith(EXIT_DELEGATION_METHOD_ID)) { + throw new Error('Invalid exit delegation method data'); + } + + const tokenIdHex = data.slice(EXIT_DELEGATION_METHOD_ID.length); + // Convert hex to decimal (matching original parseInt logic) + return parseInt(tokenIdHex, 16).toString(); + } catch (error) { + throw new Error(`Failed to decode exit delegation data: ${error.message}`); + } + } + + /** + * Decodes burn NFT transaction data to extract tokenId + * + * @param {string} data - The encoded burn NFT method call data + * @returns {string} - The tokenId as a string + */ + decodeBurnNftData(data: string): string { + try { + if (!data.startsWith(BURN_NFT_METHOD_ID)) { + throw new Error('Invalid burn NFT method data'); + } + + const tokenIdHex = data.slice(BURN_NFT_METHOD_ID.length); + // Convert hex to decimal (matching original parseInt logic) + return parseInt(tokenIdHex, 16).toString(); + } catch (error) { + throw new Error(`Failed to decode burn NFT data: ${error.message}`); + } + } + + /** + * Get the network-appropriate delegation contract address + * @param {CoinConfig} coinConfig - The coin configuration object + * @returns {string} The delegation contract address for the network + */ + getDefaultDelegationAddress(coinConfig: Readonly): string { + const isTestnet = coinConfig.network.type === 'testnet'; + return isTestnet ? STARGATE_DELEGATION_ADDRESS_TESTNET : STARGATE_DELEGATION_ADDRESS; + } + + /** + * Get the network-appropriate staking contract address + * @param {CoinConfig} coinConfig - The coin configuration object + * @returns {string} The staking contract address for the network + */ + getDefaultStakingAddress(coinConfig: Readonly): string { + const isTestnet = coinConfig.network.type === 'testnet'; + return isTestnet ? STARGATE_NFT_ADDRESS_TESTNET : STARGATE_NFT_ADDRESS; + } + + /** + * Check if an address is a valid delegation contract address for any network + * @param {string} address - The address to check + * @returns {boolean} True if the address is a delegation contract address + */ + isDelegationContractAddress(address: string): boolean { + const lowerAddress = address.toLowerCase(); + return ( + lowerAddress === STARGATE_DELEGATION_ADDRESS.toLowerCase() || + lowerAddress === STARGATE_DELEGATION_ADDRESS_TESTNET.toLowerCase() + ); + } + + /** + * Check if an address is a valid NFT contract address for any network + * @param {string} address - The address to check + * @returns {boolean} True if the address is an NFT contract address + */ + isNftContractAddress(address: string): boolean { + const lowerAddress = address.toLowerCase(); + return ( + lowerAddress === STARGATE_NFT_ADDRESS.toLowerCase() || lowerAddress === STARGATE_NFT_ADDRESS_TESTNET.toLowerCase() + ); + } + + /** + * Validate that a contract address matches the expected NFT/staking contract for the network + * @param {string} address - The contract address to validate + * @param {CoinConfig} coinConfig - The coin configuration object + * @throws {Error} If the address doesn't match the expected NFT contract address + */ + validateStakingContractAddress(address: string, coinConfig: Readonly): void { + const expectedAddress = this.getDefaultStakingAddress(coinConfig); + if (address.toLowerCase() !== expectedAddress.toLowerCase()) { + throw new Error( + `Invalid staking contract address. Expected ${expectedAddress} for ${coinConfig.network.type}, got ${address}` + ); + } + } + + /** + * Validate that a contract address matches the expected delegation contract for the network + * @param {string} address - The contract address to validate + * @param {CoinConfig} coinConfig - The coin configuration object + * @throws {Error} If the address doesn't match the expected delegation contract address + */ + validateDelegationContractAddress(address: string, coinConfig: Readonly): void { + const expectedAddress = this.getDefaultDelegationAddress(coinConfig); + if (address.toLowerCase() !== expectedAddress.toLowerCase()) { + throw new Error( + `Invalid delegation contract address. Expected ${expectedAddress} for ${coinConfig.network.type}, got ${address}` + ); + } + } } const utils = new Utils(); diff --git a/modules/sdk-coin-vet/test/resources/vet.ts b/modules/sdk-coin-vet/test/resources/vet.ts index 5e5b0b2e42..929ab03b0e 100644 --- a/modules/sdk-coin-vet/test/resources/vet.ts +++ b/modules/sdk-coin-vet/test/resources/vet.ts @@ -12,7 +12,11 @@ export const SPONSORED_NFT_TRANSACTION = '0xf9011a2788014ead140e77bbc140f87ef87c941ec1d168574603ec35b9d229843b7c2b44bcb77080b86423b872dd000000000000000000000000c0716f386a0a78361962d1f64ce2256a2c871bb50000000000000000000000007ca00e3bc8a836026c2917c6c7c6d049e52099dd000000000000000000000000000000000000000000000000000000000001872381808252088082faf8c101b882212d212effea03773f4228cd049efe42c3efe54f18378def52f0ad349637248d67d0eec6fd9bc7ddc5ab4d98f22c4fc8de485e2403f90a68b2613e214245c0a401758c9d00ada40ebc36890bbf8cf1c84070e0950dcbb6cad94a4c9a4c0ffe16c7486c5b674207c4d916f7ed7cb501c73daaafc49cfe7685cf1e0f8054a7fa460300'; export const STAKING_TRANSACTION = - '0xf8e22788015a07263f9d6fc140f845f843941ec1d168574603ec35b9d229843b7c2b44bcb770880de0b6b3a7640000a4a694fc3a0000000000000000000000000000000000000000000001000000000000000000818082b9cd808306da0dc101b882dfcfaea89c500ac28fff75a1fa83b84dc157f23021e81383518550bf69f626ca184d58060663e013e4dd6f73b64000c5133204ffa6948eaea51b4f1bfc371683019e7476b5df49d75d969894b38e4913767107ab3d0d12313f5c1c398bdaf31a2d1df47ac763a1de1d07008e5e800fe65c4e867cf7ca57fbaf709cddab2a648bca01'; + '0xf901032788015d55fcf2457e7c40f866f864941856c533ac2d94340aaa8544d35a5c1d4a21dee7880de0b6b3a7640000b844d8da3bbf0000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000181808265848083094c53c101b882efcb9ea88e908d1a142db96c1b44dd056ea194f1ad45670c100a8c52348cc7b20387741260ebe7fe9b7594f96693c88662fa60edba5992332728222b0bdd8a30008535368bd901319eb4513d16bebc428dc8454d32a19eeb76372849a6134ebbba79f1eeceea1f6546574b945c05489222cb451f5b0e2901b0c687b750e833aeb800'; + +export const STAKING_LEVEL_ID = 8; +export const STAKING_AUTORENEW = true; +export const STAKING_CONTRACT_ADDRESS = '0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7'; export const VALID_TOKEN_SIGNABLE_PAYLOAD = 'f8762788014ead140e77bbc140f85ef85c940000000000000000000000000000456e6572677980b844a9059cbb000000000000000000000000e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec000000000000000000000000000000000000000000000000016345785d8a000081808252088082faf8c101'; diff --git a/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts index 501b63e686..585a6f2d34 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/burnNftBuilder.ts @@ -4,7 +4,7 @@ import { coins } from '@bitgo/statics'; import { TransactionBuilderFactory } from '../../src'; import { BurnNftTransaction } from '../../src/lib/transaction/burnNftTransaction'; import * as testData from '../resources/vet'; -import { BURN_NFT_METHOD_ID, STARGATE_NFT_ADDRESS } from '../../src/lib/constants'; +import { BURN_NFT_METHOD_ID, STARGATE_NFT_ADDRESS_TESTNET } from '../../src/lib/constants'; describe('Vet Burn NFT Transaction', () => { const factory = new TransactionBuilderFactory(coins.get('tvet')); @@ -16,7 +16,7 @@ describe('Vet Burn NFT Transaction', () => { txBuilder.sender(testData.addresses.validAddresses[0]); txBuilder.tokenId(tokenId); - txBuilder.nftContract(); // Use default address + txBuilder.nftContract(STARGATE_NFT_ADDRESS_TESTNET); txBuilder.gas(21000); txBuilder.nonce('64248'); txBuilder.blockRef('0x014ead140e77bbc1'); @@ -27,7 +27,7 @@ describe('Vet Burn NFT Transaction', () => { should.equal(tx.sender, testData.addresses.validAddresses[0]); should.equal(tx.tokenId, tokenId); - should.equal(tx.contract, STARGATE_NFT_ADDRESS); + should.equal(tx.contract, STARGATE_NFT_ADDRESS_TESTNET); should.equal(tx.gas, 21000); should.equal(tx.nonce, '64248'); should.equal(tx.expiration, 64); @@ -38,13 +38,13 @@ describe('Vet Burn NFT Transaction', () => { // Verify the transaction has the correct structure tx.clauses.length.should.equal(1); - should.equal(tx.clauses[0].to, STARGATE_NFT_ADDRESS); + should.equal(tx.clauses[0].to, STARGATE_NFT_ADDRESS_TESTNET); should.equal(tx.clauses[0].value, '0x0'); }); it('should build a burn NFT transaction with custom contract address', async function () { const tokenId = '123456'; - const customContractAddress = '0x1234567890123456789012345678901234567890'; + const customContractAddress = STARGATE_NFT_ADDRESS_TESTNET; // Use the valid testnet NFT address const txBuilder = factory.getBurnNftBuilder(); txBuilder.sender(testData.addresses.validAddresses[0]); @@ -69,7 +69,7 @@ describe('Vet Burn NFT Transaction', () => { txBuilder.sender(testData.addresses.validAddresses[0]); txBuilder.tokenId(tokenId); - txBuilder.nftContract(); + txBuilder.nftContract(STARGATE_NFT_ADDRESS_TESTNET); txBuilder.gas(21000); txBuilder.nonce('64248'); txBuilder.blockRef('0x014ead140e77bbc1'); @@ -85,7 +85,7 @@ describe('Vet Burn NFT Transaction', () => { should.equal(deserializedTx.type, TransactionType.StakingWithdraw); should.equal(deserializedTx.tokenId, tokenId); - should.equal(deserializedTx.contract, STARGATE_NFT_ADDRESS); + should.equal(deserializedTx.contract, STARGATE_NFT_ADDRESS_TESTNET); }); it('should validate the transaction data structure', async function () { @@ -95,7 +95,7 @@ describe('Vet Burn NFT Transaction', () => { await should(txBuilder.build()).be.rejectedWith('NFT contract address is required'); txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.nftContract(); + txBuilder.nftContract(STARGATE_NFT_ADDRESS_TESTNET); await should(txBuilder.build()).be.rejectedWith('Token ID is required'); // Now add the token ID and it should build successfully @@ -118,7 +118,7 @@ describe('Vet Burn NFT Transaction', () => { it('should fail with invalid token ID', async function () { const txBuilder = factory.getBurnNftBuilder(); txBuilder.sender(testData.addresses.validAddresses[0]); - txBuilder.nftContract(); + txBuilder.nftContract(STARGATE_NFT_ADDRESS_TESTNET); txBuilder.tokenId(''); await should(txBuilder.build()).be.rejectedWith('Token ID is required'); diff --git a/modules/sdk-coin-vet/test/transactionBuilder/claimRewardsBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/claimRewardsBuilder.ts index c89eca059e..0e3bb634cf 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/claimRewardsBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/claimRewardsBuilder.ts @@ -5,14 +5,14 @@ import should from 'should'; import { CLAIM_BASE_REWARDS_METHOD_ID, CLAIM_STAKING_REWARDS_METHOD_ID, - STARGATE_DELEGATION_ADDRESS, + STARGATE_DELEGATION_ADDRESS_TESTNET, + STARGATE_NFT_ADDRESS_TESTNET, } from '../../src/lib/constants'; import { TransactionType } from '@bitgo/sdk-core'; describe('VET Claim Rewards Transaction', function () { const factory = new TransactionBuilderFactory(coins.get('tvet')); - const validatorAddress = '0x7567d83b7b8d80addcb281a71d54fc7b3364ffed'; - const delegatorAddress = '0x625476eab2e75c5b9c6f8a9d7f1b2c5e6f8e9a7b'; + const tokenId = '12345'; // Helper function to create a basic transaction builder with common properties const createBasicTxBuilder = () => { @@ -30,8 +30,7 @@ describe('VET Claim Rewards Transaction', function () { it('should build a claim rewards transaction with both reward types', async function () { const txBuilder = factory.getClaimRewardsBuilder(); txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, claimBaseRewards: true, claimStakingRewards: true, }); @@ -49,26 +48,26 @@ describe('VET Claim Rewards Transaction', function () { tx.should.be.instanceof(ClaimRewardsTransaction); const claimTx = tx as ClaimRewardsTransaction; - claimTx.claimRewardsData.validatorAddress.should.equal(validatorAddress); - claimTx.claimRewardsData.delegatorAddress.should.equal(delegatorAddress); + claimTx.claimRewardsData.tokenId.should.equal(tokenId); claimTx.claimRewardsData.claimBaseRewards?.should.be.true(); claimTx.claimRewardsData.claimStakingRewards?.should.be.true(); // Verify clauses - should have 2 clauses for both reward types claimTx.clauses.length.should.equal(2); - claimTx.clauses.forEach((clause) => { - should.exist(clause.to); - clause.to?.should.equal(STARGATE_DELEGATION_ADDRESS); - clause.value.should.equal('0x0'); - should.exist(clause.data); - }); - // Verify method IDs - const hasBaseRewards = claimTx.clauses.some((clause) => clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)); - const hasStakingRewards = claimTx.clauses.some((clause) => clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)); + // Find base rewards clause (claimVetGeneratedVtho) + const baseRewardsClause = claimTx.clauses.find((clause) => clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)); + should.exist(baseRewardsClause); + baseRewardsClause?.to?.should.equal(STARGATE_NFT_ADDRESS_TESTNET); + baseRewardsClause?.value.should.equal('0x0'); - hasBaseRewards.should.be.true(); - hasStakingRewards.should.be.true(); + // Find staking rewards clause (claimRewards) + const stakingRewardsClause = claimTx.clauses.find((clause) => + clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) + ); + should.exist(stakingRewardsClause); + stakingRewardsClause?.to?.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); + stakingRewardsClause?.value.should.equal('0x0'); // Verify recipients should be empty for claim rewards claimTx.recipients.length.should.equal(0); @@ -77,8 +76,7 @@ describe('VET Claim Rewards Transaction', function () { it('should build a claim rewards transaction with only base rewards', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, claimBaseRewards: true, claimStakingRewards: false, }); @@ -89,7 +87,7 @@ describe('VET Claim Rewards Transaction', function () { // Should have only 1 clause for base rewards claimTx.clauses.length.should.equal(1); claimTx.clauses[0].data.should.startWith(CLAIM_BASE_REWARDS_METHOD_ID); - claimTx.clauses[0].to?.should.equal(STARGATE_DELEGATION_ADDRESS); + claimTx.clauses[0].to?.should.equal(STARGATE_NFT_ADDRESS_TESTNET); claimTx.clauses[0].value.should.equal('0x0'); claimTx.claimRewardsData.claimBaseRewards?.should.be.true(); @@ -99,8 +97,7 @@ describe('VET Claim Rewards Transaction', function () { it('should build a claim rewards transaction with only staking rewards', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, claimBaseRewards: false, claimStakingRewards: true, }); @@ -111,7 +108,7 @@ describe('VET Claim Rewards Transaction', function () { // Should have only 1 clause for staking rewards claimTx.clauses.length.should.equal(1); claimTx.clauses[0].data.should.startWith(CLAIM_STAKING_REWARDS_METHOD_ID); - claimTx.clauses[0].to?.should.equal(STARGATE_DELEGATION_ADDRESS); + claimTx.clauses[0].to?.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); claimTx.clauses[0].value.should.equal('0x0'); claimTx.claimRewardsData.claimBaseRewards?.should.be.false(); @@ -126,46 +123,22 @@ describe('VET Claim Rewards Transaction', function () { await txBuilder.build().should.be.rejectedWith('Claim rewards data is required'); }); - it('should throw error when validator address is missing', async function () { - const txBuilder = createBasicTxBuilder(); - - should(() => { - txBuilder.claimRewardsData({ - delegatorAddress, - } as Partial as ClaimRewardsData); - }).throw('Validator address is required'); - }); - - it('should throw error when delegator address is missing', async function () { + it('should throw error when token ID is missing', async function () { const txBuilder = createBasicTxBuilder(); should(() => { - txBuilder.claimRewardsData({ - validatorAddress, - } as Partial as ClaimRewardsData); - }).throw('Delegator address is required'); - }); - - it('should throw error when validator address is invalid', async function () { - const txBuilder = createBasicTxBuilder(); - - should(() => { - txBuilder.claimRewardsData({ - validatorAddress: 'invalid-address', - delegatorAddress, - }); - }).throw(/Invalid validator address format/); + txBuilder.claimRewardsData({} as ClaimRewardsData); + }).throw('Token ID is required'); }); - it('should throw error when delegator address is invalid', async function () { + it('should throw error when token ID is invalid', async function () { const txBuilder = createBasicTxBuilder(); should(() => { txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress: '0xinvalid', + tokenId: 'invalid-tokenid', }); - }).throw(/Invalid delegator address format/); + }).throw('Token ID must be a valid number string'); }); it('should throw error when both reward flags are false', async function () { @@ -173,8 +146,7 @@ describe('VET Claim Rewards Transaction', function () { should(() => { txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, claimBaseRewards: false, claimStakingRewards: false, }); @@ -186,8 +158,7 @@ describe('VET Claim Rewards Transaction', function () { should(() => { txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, claimBaseRewards: 'true' as unknown as boolean, }); }).throw('claimBaseRewards must be a boolean'); @@ -198,8 +169,7 @@ describe('VET Claim Rewards Transaction', function () { should(() => { txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, claimStakingRewards: 1 as unknown as boolean, }); }).throw('claimStakingRewards must be a boolean'); @@ -208,8 +178,7 @@ describe('VET Claim Rewards Transaction', function () { it('should default to claiming both rewards when flags are undefined', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, }); const tx = await txBuilder.build(); @@ -218,20 +187,21 @@ describe('VET Claim Rewards Transaction', function () { // Should have 2 clauses by default claimTx.clauses.length.should.equal(2); - const hasBaseRewards = claimTx.clauses.some((clause) => clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)); - const hasStakingRewards = claimTx.clauses.some((clause) => + const baseRewardsClause = claimTx.clauses.find((clause) => clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)); + const stakingRewardsClause = claimTx.clauses.find((clause) => clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) ); - hasBaseRewards.should.be.true(); - hasStakingRewards.should.be.true(); + should.exist(baseRewardsClause); + should.exist(stakingRewardsClause); + baseRewardsClause?.to?.should.equal(STARGATE_NFT_ADDRESS_TESTNET); + stakingRewardsClause?.to?.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); }); it('should build transaction with undefined sender but include it in inputs', async function () { const txBuilder = factory.getClaimRewardsBuilder(); txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, }); txBuilder.chainTag(0x27); txBuilder.blockRef('0x0000000000000000'); @@ -256,8 +226,7 @@ describe('VET Claim Rewards Transaction', function () { it('should use network default chainTag when not explicitly set', async function () { const txBuilder = factory.getClaimRewardsBuilder(); txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, }); // Not setting chainTag txBuilder.blockRef('0x0000000000000000'); @@ -278,8 +247,7 @@ describe('VET Claim Rewards Transaction', function () { it('should serialize and explain transaction correctly', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.claimRewardsData({ - validatorAddress, - delegatorAddress, + tokenId, }); const tx = await txBuilder.build(); @@ -300,8 +268,38 @@ describe('VET Claim Rewards Transaction', function () { // Test toJson const json = claimTx.toJson(); should.exist(json.claimRewardsData); - json.claimRewardsData?.validatorAddress.should.equal(validatorAddress); - json.claimRewardsData?.delegatorAddress.should.equal(delegatorAddress); + json.claimRewardsData?.tokenId.should.equal(tokenId); + }); + + it('should correctly handle custom contract addresses when building transactions', async function () { + const customNftAddress = '0x1234567890123456789012345678901234567890'; + const customDelegationAddress = '0x0987654321098765432109876543210987654321'; + + const txBuilder = createBasicTxBuilder(); + txBuilder.claimRewardsData({ + tokenId, + delegationContractAddress: customDelegationAddress, + stargateNftAddress: customNftAddress, + }); + + const tx = await txBuilder.build(); + const claimTx = tx as ClaimRewardsTransaction; + + // Verify that custom addresses are stored in claimRewardsData when they differ from defaults + should.exist(claimTx.claimRewardsData.delegationContractAddress); + claimTx.claimRewardsData.delegationContractAddress?.should.equal(customDelegationAddress); + + should.exist(claimTx.claimRewardsData.stargateNftAddress); + claimTx.claimRewardsData.stargateNftAddress?.should.equal(customNftAddress); + + // Verify clauses still use the default addresses (as builders use defaults) + const baseRewardsClause = claimTx.clauses.find((clause) => clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)); + const stakingRewardsClause = claimTx.clauses.find((clause) => + clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) + ); + + baseRewardsClause?.to?.should.equal(STARGATE_NFT_ADDRESS_TESTNET); + stakingRewardsClause?.to?.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); }); }); }); diff --git a/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts index 0f180fac43..89a09db12d 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/exitDelegationBuilder.ts @@ -4,7 +4,7 @@ import { coins } from '@bitgo/statics'; import { TransactionBuilderFactory } from '../../src'; import { ExitDelegationTransaction } from '../../src/lib/transaction/exitDelegation'; import * as testData from '../resources/vet'; -import { EXIT_DELEGATION_METHOD_ID, STARGATE_DELEGATION_ADDRESS } from '../../src/lib/constants'; +import { EXIT_DELEGATION_METHOD_ID, STARGATE_DELEGATION_ADDRESS_TESTNET } from '../../src/lib/constants'; describe('Vet Exit Delegation Transaction', () => { const factory = new TransactionBuilderFactory(coins.get('tvet')); @@ -27,7 +27,7 @@ describe('Vet Exit Delegation Transaction', () => { should.equal(tx.sender, testData.addresses.validAddresses[0]); should.equal(tx.tokenId, tokenId); - should.equal(tx.contract, STARGATE_DELEGATION_ADDRESS); + should.equal(tx.contract, STARGATE_DELEGATION_ADDRESS_TESTNET); should.equal(tx.gas, 21000); should.equal(tx.nonce, '64248'); should.equal(tx.expiration, 64); @@ -40,14 +40,14 @@ describe('Vet Exit Delegation Transaction', () => { tx.clauses.length.should.equal(1); should.exist(tx.clauses[0]); should.exist(tx.clauses[0].to); - tx.clauses[0]?.to?.should.equal(STARGATE_DELEGATION_ADDRESS); + tx.clauses[0]?.to?.should.equal(STARGATE_DELEGATION_ADDRESS_TESTNET); should.exist(tx.clauses[0].value); tx.clauses[0].value.should.equal('0x0'); }); it('should build an exit delegation transaction with custom contract address', async function () { const tokenId = '123456'; - const customContractAddress = '0x1234567890123456789012345678901234567890'; + const customContractAddress = STARGATE_DELEGATION_ADDRESS_TESTNET; // Use the valid testnet delegation address const txBuilder = factory.getExitDelegationBuilder(); txBuilder.sender(testData.addresses.validAddresses[0]); @@ -90,7 +90,7 @@ describe('Vet Exit Delegation Transaction', () => { should.equal(deserializedTx.type, TransactionType.StakingUnlock); should.equal(deserializedTx.tokenId, tokenId); - should.equal(deserializedTx.contract, STARGATE_DELEGATION_ADDRESS); + should.equal(deserializedTx.contract, STARGATE_DELEGATION_ADDRESS_TESTNET); }); it('should validate the transaction data structure', async function () { diff --git a/modules/sdk-coin-vet/test/transactionBuilder/stakingTransaction.ts b/modules/sdk-coin-vet/test/transactionBuilder/stakingTransaction.ts index f43628b992..59d636db29 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/stakingTransaction.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/stakingTransaction.ts @@ -1,15 +1,16 @@ import { coins } from '@bitgo/statics'; import { TransactionBuilderFactory, Transaction, StakingTransaction } from '../../src/lib'; import should from 'should'; -import { STAKING_METHOD_ID } from '../../src/lib/constants'; +import { STAKING_METHOD_ID, STARGATE_NFT_ADDRESS_TESTNET } from '../../src/lib/constants'; import EthereumAbi from 'ethereumjs-abi'; -import { BN } from 'ethereumjs-util'; import * as testData from '../resources/vet'; describe('VET Staking Transaction', function () { const factory = new TransactionBuilderFactory(coins.get('tvet')); - const stakingContractAddress = '0x1EC1D168574603ec35b9d229843B7C2b44bCB770'; + const stakingContractAddress = STARGATE_NFT_ADDRESS_TESTNET; const amountToStake = '1000000000000000000'; // 1 VET in wei + const levelId = 8; // Test level ID + const autorenew = true; // Test autorenew flag // Helper function to create a basic transaction builder with common properties const createBasicTxBuilder = () => { @@ -28,6 +29,7 @@ describe('VET Staking Transaction', function () { const txBuilder = factory.getStakingBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.amountToStake(amountToStake); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9'); txBuilder.chainTag(0x27); // Testnet chain tag @@ -45,6 +47,8 @@ describe('VET Staking Transaction', function () { const stakingTx = tx as StakingTransaction; stakingTx.stakingContractAddress.should.equal(stakingContractAddress); stakingTx.amountToStake.should.equal(amountToStake); + stakingTx.levelId.should.equal(levelId); + stakingTx.autorenew.should.equal(autorenew); stakingTx.stakingContractABI.should.deepEqual(EthereumAbi); // Verify clauses @@ -59,9 +63,9 @@ describe('VET Staking Transaction', function () { txData.should.startWith(STAKING_METHOD_ID); // Verify the encoded data matches what we expect from ethereumABI - const methodName = 'stake'; - const types = ['uint256']; - const params = [new BN(amountToStake)]; + const methodName = 'stakeAndDelegate'; + const types = ['uint8', 'bool']; + const params = [levelId, autorenew]; const method = EthereumAbi.methodID(methodName, types); const args = EthereumAbi.rawEncode(types, params); @@ -79,14 +83,25 @@ describe('VET Staking Transaction', function () { it('should throw error when stakingContractAddress is missing', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.amountToStake(amountToStake); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); await txBuilder.build().should.be.rejectedWith('Staking contract address is required'); }); + it('should throw error when levelId is missing', async function () { + const txBuilder = createBasicTxBuilder(); + txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.amountToStake(amountToStake); + txBuilder.stakingContractABI(EthereumAbi); + + await txBuilder.build().should.be.rejectedWith('Level ID is required'); + }); + it('should throw error when amountToStake is missing', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); await txBuilder.build().should.be.rejectedWith('Amount to stake is required'); @@ -104,20 +119,21 @@ describe('VET Staking Transaction', function () { it('should throw error when amountToStake is not a valid number string', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); // Invalid amount (not a number) - should(() => { - txBuilder.amountToStake('not-a-number'); - }).not.throw(); // The setter doesn't validate - // But it should fail when building the transaction - await txBuilder.build().should.be.rejectedWith(/Invalid character/); + txBuilder.amountToStake('not-a-number'); + + // Should fail when building the transaction due to invalid amount + await txBuilder.build().should.be.rejected(); }); it('should pass validation with any ABI object but may fail during build', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.amountToStake(amountToStake); + txBuilder.levelId(levelId); // Set an invalid ABI object const invalidAbi = {}; @@ -138,6 +154,7 @@ describe('VET Staking Transaction', function () { it('should allow zero amountToStake but encode it properly', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); txBuilder.amountToStake('0'); @@ -148,8 +165,10 @@ describe('VET Staking Transaction', function () { // Verify the amount is correctly set to zero stakingTx.amountToStake.should.equal('0'); - // Verify the transaction data is correctly encoded with zero amount - const expectedData = '0xa694fc3a0000000000000000000000000000000000000000000000000000000000000000'; + // Verify the transaction data is correctly encoded with levelId and autorenew for stakeAndDelegate + // Expected data for stakeAndDelegate(8, true) where 8 is levelId and true is autorenew + const expectedData = + '0xd8da3bbf00000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001'; stakingTx.clauses[0].data.should.equal(expectedData); }); @@ -157,6 +176,7 @@ describe('VET Staking Transaction', function () { const txBuilder = createBasicTxBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.amountToStake(amountToStake); + txBuilder.levelId(levelId); // Not setting stakingContractABI // Should fail when trying to build without ABI @@ -167,6 +187,7 @@ describe('VET Staking Transaction', function () { const txBuilder = factory.getStakingBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.amountToStake(amountToStake); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); txBuilder.chainTag(0x27); txBuilder.blockRef('0x0000000000000000'); @@ -194,6 +215,7 @@ describe('VET Staking Transaction', function () { const txBuilder = factory.getStakingBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.amountToStake(amountToStake); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); // Not setting chainTag txBuilder.blockRef('0x0000000000000000'); @@ -214,6 +236,7 @@ describe('VET Staking Transaction', function () { it('should verify ABI encoding matches expected output for different amounts', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); // Test with a different amount @@ -223,10 +246,10 @@ describe('VET Staking Transaction', function () { const tx = await txBuilder.build(); const stakingTx = tx as StakingTransaction; - // Manually encode the expected data - const methodName = 'stake'; - const types = ['uint256']; - const params = [new BN(differentAmount)]; + // Manually encode the expected data for stakeAndDelegate method + const methodName = 'stakeAndDelegate'; + const types = ['uint8', 'bool']; + const params = [levelId, autorenew]; const method = EthereumAbi.methodID(methodName, types); const args = EthereumAbi.rawEncode(types, params); @@ -240,6 +263,7 @@ describe('VET Staking Transaction', function () { it('should handle extremely large stake amounts correctly', async function () { const txBuilder = createBasicTxBuilder(); txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.levelId(levelId); txBuilder.stakingContractABI(EthereumAbi); // Test with a very large amount (near uint256 max) @@ -263,14 +287,16 @@ describe('VET Staking Transaction', function () { const txBuilder = factory.from(testData.STAKING_TRANSACTION); const tx = txBuilder.transaction as StakingTransaction; const toJson = tx.toJson(); - toJson.id.should.equal('0x4fd543eb5ac4e4b1a3eeda7335cd8ba449e5aef6dff243a55d83daf480526e11'); - toJson.stakingContractAddress?.should.equal('0x1ec1d168574603ec35b9d229843b7c2b44bcb770'); + toJson.id.should.equal('0x99325b39cd04bd1821f6f6af7b679c247e6425a4eb95eb429fa8dff477298d0e'); + toJson.stakingContractAddress?.should.equal('0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7'); toJson.amountToStake?.should.equal('0xde0b6b3a7640000'); - toJson.nonce.should.equal('449037'); - toJson.gas.should.equal(47565); + toJson.nonce.should.equal('609363'); + toJson.gas.should.equal(25988); toJson.gasPriceCoef.should.equal(128); toJson.expiration.should.equal(64); toJson.chainTag.should.equal(39); + toJson.nftTokenId?.should.equal(8); + toJson.autorenew?.should.equal(true); }); }); });