diff --git a/modules/sdk-coin-vet/src/lib/iface.ts b/modules/sdk-coin-vet/src/lib/iface.ts index b927e21035..f3be344a03 100644 --- a/modules/sdk-coin-vet/src/lib/iface.ts +++ b/modules/sdk-coin-vet/src/lib/iface.ts @@ -32,7 +32,6 @@ export interface VetTransactionData { 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; } diff --git a/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts b/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts index 6641e00e85..b7b3dad1e9 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts @@ -5,24 +5,32 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { Transaction as VetTransaction, Secp256k1, TransactionClause } from '@vechain/sdk-core'; import { Transaction } from './transaction'; import { VetTransactionData } from '../iface'; -import { ClaimRewardsData } from '../types'; -import { CLAIM_BASE_REWARDS_METHOD_ID, CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants'; +import { CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants'; import utils from '../utils'; export class ClaimRewardsTransaction extends Transaction { - private _claimRewardsData: ClaimRewardsData; + private _stakingContractAddress: string; + private _tokenId: string; constructor(_coinConfig: Readonly) { super(_coinConfig); this._type = TransactionType.StakingClaim; } - get claimRewardsData(): ClaimRewardsData { - return this._claimRewardsData; + get stakingContractAddress(): string { + return this._stakingContractAddress; } - set claimRewardsData(data: ClaimRewardsData) { - this._claimRewardsData = data; + set stakingContractAddress(address: string) { + this._stakingContractAddress = address; + } + + get tokenId(): string { + return this._tokenId; + } + + set tokenId(tokenId: string) { + this._tokenId = tokenId; } /** @inheritdoc */ @@ -51,65 +59,37 @@ export class ClaimRewardsTransaction extends Transaction { /** @inheritdoc */ buildClauses(): void { - if (!this._claimRewardsData) { - throw new InvalidTransactionError('Missing claim rewards data'); + if (!this.stakingContractAddress) { + throw new Error('Staking contract address is not set'); } - const clauses: TransactionClause[] = []; + utils.validateStakingContractAddress(this.stakingContractAddress, this._coinConfig); - // Add clause for claiming base rewards if requested - const shouldClaimBaseRewards = this.claimRewardsData.claimBaseRewards !== false; // Default true - if (shouldClaimBaseRewards) { - clauses.push(this.buildClaimBaseRewardsClause()); + if (this.tokenId === undefined || this.tokenId === null) { + throw new Error('Token ID is not set'); } - // Add clause for claiming staking rewards if requested - const shouldClaimStakingRewards = this.claimRewardsData.claimStakingRewards !== false; // Default true - if (shouldClaimStakingRewards) { - clauses.push(this.buildClaimStakingRewardsClause()); - } + const data = this.encodeClaimRewardsMethod(this.tokenId); + this._transactionData = data; - if (clauses.length === 0) { - throw new InvalidTransactionError('At least one type of rewards must be claimed'); - } - - this.clauses = clauses; + // Create the clause for claim rewards + this._clauses = [ + { + to: this.stakingContractAddress, + value: '0x0', + data: this._transactionData, + }, + ]; // Set recipients as empty since claim rewards doesn't send value this.recipients = []; } - /** - * Build clause for claiming base rewards (claimVetGeneratedVtho) - */ - private buildClaimBaseRewardsClause(): TransactionClause { - const methodData = this.encodeClaimRewardsMethod(CLAIM_BASE_REWARDS_METHOD_ID, this._claimRewardsData.tokenId); - - return { - to: utils.getDefaultStakingAddress(this._coinConfig), - value: '0x0', - data: methodData, - }; - } - - /** - * Build clause for claiming staking rewards (claimRewards) - */ - private buildClaimStakingRewardsClause(): TransactionClause { - const methodData = this.encodeClaimRewardsMethod(CLAIM_STAKING_REWARDS_METHOD_ID, this._claimRewardsData.tokenId); - - return { - to: utils.getDefaultDelegationAddress(this._coinConfig), - value: '0x0', - data: methodData, - }; - } - /** * Encode the claim rewards method call data */ - private encodeClaimRewardsMethod(methodId: string, tokenId: string): string { - const methodName = methodId === CLAIM_BASE_REWARDS_METHOD_ID ? 'claimVetGeneratedVtho' : 'claimRewards'; + private encodeClaimRewardsMethod(tokenId: string): string { + const methodName = 'claimRewards'; const types = ['uint256']; const params = [tokenId]; @@ -133,7 +113,8 @@ export class ClaimRewardsTransaction extends Transaction { sender: this.sender, feePayer: this.feePayerAddress, recipients: this.recipients, - claimRewardsData: this._claimRewardsData, + tokenId: this.tokenId, + stakingContractAddress: this.stakingContractAddress, }; return json; } @@ -159,8 +140,14 @@ export class ClaimRewardsTransaction extends Transaction { this.dependsOn = body.dependsOn || null; this.nonce = String(body.nonce); - // Parse claim rewards data from clauses - this.parseClaimRewardsDataFromClauses(body.clauses); + if (body.clauses.length === 1) { + const clause = body.clauses[0]; + if (clause.data && clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)) { + // claimRewards should go to STARGATE_DELEGATION_ADDRESS + this.tokenId = utils.decodeClaimRewardsData(clause.data); + this.stakingContractAddress = clause.to || '0x0'; + } + } // Set recipients as empty for claim rewards this.recipients = []; @@ -185,59 +172,4 @@ export class ClaimRewardsTransaction extends Transaction { throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`); } } - - /** - * Parse claim rewards data from transaction clauses - */ - private parseClaimRewardsDataFromClauses(clauses: TransactionClause[]): void { - if (!clauses || clauses.length === 0) { - throw new InvalidTransactionError('No clauses found in transaction'); - } - - let claimBaseRewards = false; - let claimStakingRewards = false; - let tokenId = ''; - let delegationContractAddress = ''; - let stargateNftAddress = ''; - - for (const clause of clauses) { - if (clause.data) { - if (clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)) { - // claimVetGeneratedVtho should go to STARGATE_NFT_ADDRESS - claimBaseRewards = true; - 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 (!tokenId) { - tokenId = utils.decodeClaimRewardsData(clause.data); - } - if (!delegationContractAddress && clause.to) { - delegationContractAddress = clause.to; - } - } - } - } - - if (!claimBaseRewards && !claimStakingRewards) { - throw new InvalidTransactionError('Transaction does not contain claim rewards clauses'); - } - - this._claimRewardsData = { - tokenId, - delegationContractAddress: - delegationContractAddress && !utils.isDelegationContractAddress(delegationContractAddress) - ? delegationContractAddress - : undefined, - stargateNftAddress: - stargateNftAddress && !utils.isNftContractAddress(stargateNftAddress) ? stargateNftAddress : undefined, - claimBaseRewards, - claimStakingRewards, - }; - } } diff --git a/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts b/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts index 2bb2ac271e..66dae13ab5 100644 --- a/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts +++ b/modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts @@ -48,7 +48,7 @@ export class DelegateClauseTransaction extends Transaction { throw new Error('Staking contract address is not set'); } - utils.validateDelegationContractAddress(this.stakingContractAddress, this._coinConfig); + utils.validateStakingContractAddress(this.stakingContractAddress, this._coinConfig); if (this.tokenId === undefined || this.tokenId === null) { throw new Error('Token ID is not set'); diff --git a/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts b/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts index 4489b13f3f..ebb96d2702 100644 --- a/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts +++ b/modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts @@ -6,8 +6,7 @@ import assert from 'assert'; 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 } from '../constants'; +import { CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants'; import utils from '../utils'; export class ClaimRewardsBuilder extends TransactionBuilder { @@ -49,92 +48,61 @@ export class ClaimRewardsBuilder extends TransactionBuilder { } /** - * Validates the transaction clauses for claim rewards transaction. - * @param {TransactionClause[]} clauses - The transaction clauses to validate. - * @returns {boolean} - Returns true if the clauses are valid, false otherwise. + * Sets the staking contract address for this claim tx. + * The address must be explicitly provided to ensure the correct contract is used. + * + * @param {string} address - The staking contract address (required) + * @returns {ClaimRewardsBuilder} This transaction builder + * @throws {Error} If no address is provided */ - protected isValidTransactionClauses(clauses: TransactionClause[]): boolean { - try { - if (!clauses || !Array.isArray(clauses) || clauses.length === 0) { - return false; - } - - let hasValidClaimClause = false; - - for (const clause of clauses) { - if (!clause.to || !utils.isValidAddress(clause.to)) { - return false; - } - - // For claim rewards transactions, value must be '0x0' or 0 - if (clause.value !== '0x0' && clause.value !== 0 && clause.value !== '0') { - return false; - } - - 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) && isNftContract) || - (clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) && isDelegationContract) - ) { - hasValidClaimClause = true; - } - } - } - - return hasValidClaimClause; - } catch (e) { - return false; + stakingContractAddress(address: string): this { + if (!address) { + throw new Error('Staking contract address is required'); } + this.validateAddress({ address }); + this.claimRewardsTransaction.stakingContractAddress = address; + return this; } /** - * Sets the claim rewards data for this transaction. + * Sets the token ID for this claim tx. * - * @param {ClaimRewardsData} data - The claim rewards data - * @returns {ClaimRewardsBuilder} This transaction builder + * @param {number} levelId - The NFT token ID + * @returns {DelegateTxnBuilder} This transaction builder */ - claimRewardsData(data: ClaimRewardsData): this { - this.validateClaimRewardsData(data); - this.claimRewardsTransaction.claimRewardsData = data; + tokenId(tokenId: string): this { + this.claimRewardsTransaction.tokenId = tokenId; return this; } /** - * Validates the claim rewards data. - * - * @param {ClaimRewardsData} data - The claim rewards data to validate + * Validates the transaction clauses for claim rewards transaction. + * @param {TransactionClause[]} clauses - The transaction clauses to validate. + * @returns {boolean} - Returns true if the clauses are valid, false otherwise. */ - private validateClaimRewardsData(data: ClaimRewardsData): void { - if (!data) { - throw new Error('Claim rewards data is required'); - } - - if (!data.tokenId) { - throw new Error('Token ID is required'); - } - - // Validate tokenId is a valid number string - if (!/^\d+$/.test(data.tokenId)) { - throw new Error('Token ID must be a valid number string'); - } + protected isValidTransactionClauses(clauses: TransactionClause[]): boolean { + try { + if (!clauses || !Array.isArray(clauses) || clauses.length === 0) { + return false; + } - if (data.claimBaseRewards !== undefined && typeof data.claimBaseRewards !== 'boolean') { - throw new Error('claimBaseRewards must be a boolean'); - } + const clause = clauses[0]; + if (!clause.to || !utils.isValidAddress(clause.to)) { + return false; + } - if (data.claimStakingRewards !== undefined && typeof data.claimStakingRewards !== 'boolean') { - throw new Error('claimStakingRewards must be a boolean'); - } + // Ensure value is '0x0', '0', or 0 + if (!['0x0', '0', 0].includes(clause.value)) { + return false; + } - // At least one type of rewards must be claimed (both default to true if undefined) - const claimBase = data.claimBaseRewards !== false; - const claimStaking = data.claimStakingRewards !== false; + if (clause.data && clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)) { + return true; + } - if (!claimBase && !claimStaking) { - throw new Error('At least one type of rewards (base or staking) must be claimed'); + return false; + } catch (e) { + return false; } } @@ -144,14 +112,10 @@ export class ClaimRewardsBuilder extends TransactionBuilder { throw new Error('transaction not defined'); } - const claimData = transaction.claimRewardsData; - assert(claimData, 'Claim rewards data is required'); - assert(claimData.tokenId, 'Token ID is required'); + assert(transaction.tokenId, 'Token ID is required'); + assert(transaction.stakingContractAddress, 'Staking contract address is required'); - // Validate tokenId is a valid number string - if (!/^\d+$/.test(claimData.tokenId)) { - throw new Error('Token ID must be a valid number string'); - } + this.validateAddress({ address: transaction.stakingContractAddress }); } /** @inheritdoc */ diff --git a/modules/sdk-coin-vet/src/lib/types.ts b/modules/sdk-coin-vet/src/lib/types.ts index 0795d8043c..c4147cfc51 100644 --- a/modules/sdk-coin-vet/src/lib/types.ts +++ b/modules/sdk-coin-vet/src/lib/types.ts @@ -10,10 +10,7 @@ export interface VetParseTransactionOptions extends ParseTransactionOptions { export interface ClaimRewardsData { tokenId: string; - delegationContractAddress?: string; - stargateNftAddress?: string; - claimBaseRewards?: boolean; - claimStakingRewards?: boolean; + stakingContractAddress?: string; } export type RecoverOptions = { diff --git a/modules/sdk-coin-vet/test/resources/vet.ts b/modules/sdk-coin-vet/test/resources/vet.ts index d77c2f0d24..1f6bfd3099 100644 --- a/modules/sdk-coin-vet/test/resources/vet.ts +++ b/modules/sdk-coin-vet/test/resources/vet.ts @@ -26,6 +26,9 @@ export const EXIT_DELEGATION_TRANSACTION = export const BURN_NFT_TRANSACTION = '0xf8db278801640bfe6c1ee3e640f83df83b941e02b2953adefec225cf0ec49805b1146a4429c180a42e17de780000000000000000000000000000000000000000000000000000000000003d2d81808305548c808304fe38c101b882044933b92e0fc5517d58205b46211a5ad2403103c8c217ce9682ebe2457e374f655fc6be307c7dfd59f0f4eda2aab7e3a1ac9219923086cde52e6405c34de2d801d692df740f95dd4ac6dbae7eb6e91a712a1e456e1a80e3a2f501ea1e6ed12c4308e65a8a98b0142190812d4484f54121bbc95b6048ae09de5946304affbfba1400'; +export const CLAIM_REWARDS_TRANSACTION = + '0xf8db278801641640d3222ab440f83df83b941e02b2953adefec225cf0ec49805b1146a4429c180a40962ef790000000000000000000000000000000000000000000000000000000000003d2e818083024d5e80830ed95ec101b882bfa94a4f13af900edaae0bf3000f3b2aa35b3f5f6404d61f84921ef5184d0b0175bf4ab3567c80c6479b58bf028dcca9c8ecc76525633fa4efa490486e85ee950120cbcd1b63200604f1369eb5f1394db45cc40302e790ff704893ca7f4861c3062b8c4771a2a583a9e0c7eed2bbd5fddfe7800101b94dec9fb855b7534104735b01'; + 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/claimRewardsBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/claimRewardsBuilder.ts index a2328b8352..037d84d660 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/claimRewardsBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/claimRewardsBuilder.ts @@ -1,17 +1,14 @@ import { coins } from '@bitgo/statics'; import { TransactionBuilderFactory, Transaction, ClaimRewardsTransaction } from '../../src/lib'; -import { ClaimRewardsData } from '../../src/lib/types'; import should from 'should'; -import { - CLAIM_BASE_REWARDS_METHOD_ID, - CLAIM_STAKING_REWARDS_METHOD_ID, - STARGATE_CONTRACT_ADDRESS_TESTNET, -} from '../../src/lib/constants'; +import { STARGATE_CONTRACT_ADDRESS_TESTNET } from '../../src/lib/constants'; +import * as testData from '../resources/vet'; import { TransactionType } from '@bitgo/sdk-core'; describe('VET Claim Rewards Transaction', function () { const factory = new TransactionBuilderFactory(coins.get('tvet')); const tokenId = '12345'; + const stakingContractAddress = STARGATE_CONTRACT_ADDRESS_TESTNET; // Helper function to create a basic transaction builder with common properties const createBasicTxBuilder = () => { @@ -19,6 +16,7 @@ describe('VET Claim Rewards Transaction', function () { txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9'); txBuilder.chainTag(0x27); // Testnet chain tag txBuilder.blockRef('0x0000000000000000'); + txBuilder.stakingContractAddress(stakingContractAddress); txBuilder.expiration(64); txBuilder.gas(100000); txBuilder.gasPriceCoef(0); @@ -28,11 +26,8 @@ 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({ - tokenId, - claimBaseRewards: true, - claimStakingRewards: true, - }); + txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.tokenId(tokenId); txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9'); txBuilder.chainTag(0x27); // Testnet chain tag txBuilder.blockRef('0x0000000000000000'); @@ -47,23 +42,14 @@ describe('VET Claim Rewards Transaction', function () { tx.should.be.instanceof(ClaimRewardsTransaction); const claimTx = tx as ClaimRewardsTransaction; - 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.tokenId.should.equal(tokenId); + claimTx.stakingContractAddress.should.equal(stakingContractAddress); - // 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_CONTRACT_ADDRESS_TESTNET); - baseRewardsClause?.value.should.equal('0x0'); + // Verify clauses - should have ONLY 1 clause (delegation rewards) + claimTx.clauses.length.should.equal(1); - // Find staking rewards clause (claimRewards) - const stakingRewardsClause = claimTx.clauses.find((clause) => - clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) - ); + // staking rewards clause (claimRewards) + const stakingRewardsClause = claimTx.clauses[0]; should.exist(stakingRewardsClause); stakingRewardsClause?.to?.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); stakingRewardsClause?.value.should.equal('0x0'); @@ -72,233 +58,104 @@ describe('VET Claim Rewards Transaction', function () { claimTx.recipients.length.should.equal(0); }); - it('should build a claim rewards transaction with only base rewards', async function () { - const txBuilder = createBasicTxBuilder(); - txBuilder.claimRewardsData({ - tokenId, - claimBaseRewards: true, - claimStakingRewards: false, - }); + it('should build transaction with undefined sender but include it in inputs', async function () { + const txBuilder = factory.getClaimRewardsBuilder(); + txBuilder.tokenId(tokenId); + txBuilder.stakingContractAddress(stakingContractAddress); + txBuilder.chainTag(0x27); + txBuilder.blockRef('0x0000000000000000'); + txBuilder.expiration(64); + txBuilder.gas(100000); + txBuilder.gasPriceCoef(0); + txBuilder.nonce('12345'); + // Not setting sender const tx = await txBuilder.build(); - const claimTx = tx as ClaimRewardsTransaction; + tx.should.be.instanceof(ClaimRewardsTransaction); - // 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_CONTRACT_ADDRESS_TESTNET); - claimTx.clauses[0].value.should.equal('0x0'); + const claimTx = tx as ClaimRewardsTransaction; + // Verify the transaction has inputs but with undefined address + claimTx.inputs.length.should.equal(1); + should.not.exist(claimTx.inputs[0].address); - claimTx.claimRewardsData.claimBaseRewards?.should.be.true(); - claimTx.claimRewardsData.claimStakingRewards?.should.be.false(); + // Verify the transaction has no outputs (claim rewards doesn't transfer value) + claimTx.outputs.length.should.equal(0); }); - it('should build a claim rewards transaction with only staking rewards', async function () { + it('should serialize and explain transaction correctly', async function () { const txBuilder = createBasicTxBuilder(); - txBuilder.claimRewardsData({ - tokenId, - claimBaseRewards: false, - claimStakingRewards: true, - }); + txBuilder.tokenId(tokenId); + txBuilder.stakingContractAddress(stakingContractAddress); const tx = await txBuilder.build(); const claimTx = tx as ClaimRewardsTransaction; - // 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_CONTRACT_ADDRESS_TESTNET); - claimTx.clauses[0].value.should.equal('0x0'); - - claimTx.claimRewardsData.claimBaseRewards?.should.be.false(); - claimTx.claimRewardsData.claimStakingRewards?.should.be.true(); + // Test serialization + const serialized = claimTx.toBroadcastFormat(); + serialized.should.be.String(); + serialized.should.startWith('0x'); + + // Test explanation + const explanation = claimTx.explainTransaction(); + explanation.type?.should.equal(TransactionType.StakingClaim); + should.exist(explanation.fee); + explanation.outputAmount.should.equal('0'); + explanation.changeAmount.should.equal('0'); + + // Test toJson + const txJson = claimTx.toJson(); + should.exist(txJson); + txJson.tokenId?.should.equal(tokenId); + txJson.stakingContractAddress?.should.equal(stakingContractAddress); }); - describe('Failure scenarios', function () { - it('should throw error when claim rewards data is missing', async function () { - const txBuilder = createBasicTxBuilder(); - // Not setting claimRewardsData - - await txBuilder.build().should.be.rejectedWith('Claim rewards data is required'); - }); - - it('should throw error when token ID is missing', async function () { - const txBuilder = createBasicTxBuilder(); - - should(() => { - txBuilder.claimRewardsData({} as ClaimRewardsData); - }).throw('Token ID is required'); - }); - - it('should throw error when token ID is invalid', async function () { - const txBuilder = createBasicTxBuilder(); - - should(() => { - txBuilder.claimRewardsData({ - tokenId: 'invalid-tokenid', - }); - }).throw('Token ID must be a valid number string'); - }); - - it('should throw error when both reward flags are false', async function () { - const txBuilder = createBasicTxBuilder(); - - should(() => { - txBuilder.claimRewardsData({ - tokenId, - claimBaseRewards: false, - claimStakingRewards: false, - }); - }).throw('At least one type of rewards (base or staking) must be claimed'); - }); - - it('should throw error when claimBaseRewards is not boolean', async function () { - const txBuilder = createBasicTxBuilder(); - - should(() => { - txBuilder.claimRewardsData({ - tokenId, - claimBaseRewards: 'true' as unknown as boolean, - }); - }).throw('claimBaseRewards must be a boolean'); - }); - - it('should throw error when claimStakingRewards is not boolean', async function () { - const txBuilder = createBasicTxBuilder(); - - should(() => { - txBuilder.claimRewardsData({ - tokenId, - claimStakingRewards: 1 as unknown as boolean, - }); - }).throw('claimStakingRewards must be a boolean'); - }); - - it('should default to claiming both rewards when flags are undefined', async function () { - const txBuilder = createBasicTxBuilder(); - txBuilder.claimRewardsData({ - tokenId, - }); - - const tx = await txBuilder.build(); - const claimTx = tx as ClaimRewardsTransaction; - - // Should have 2 clauses by default - claimTx.clauses.length.should.equal(2); - - 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) - ); - - should.exist(baseRewardsClause); - should.exist(stakingRewardsClause); - baseRewardsClause?.to?.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); - stakingRewardsClause?.to?.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); - }); - - it('should build transaction with undefined sender but include it in inputs', async function () { - const txBuilder = factory.getClaimRewardsBuilder(); - txBuilder.claimRewardsData({ - tokenId, - }); - txBuilder.chainTag(0x27); - txBuilder.blockRef('0x0000000000000000'); - txBuilder.expiration(64); - txBuilder.gas(100000); - txBuilder.gasPriceCoef(0); - txBuilder.nonce('12345'); - // Not setting sender - - const tx = await txBuilder.build(); - tx.should.be.instanceof(ClaimRewardsTransaction); - - const claimTx = tx as ClaimRewardsTransaction; - // Verify the transaction has inputs but with undefined address - claimTx.inputs.length.should.equal(1); - should.not.exist(claimTx.inputs[0].address); - - // Verify the transaction has no outputs (claim rewards doesn't transfer value) - claimTx.outputs.length.should.equal(0); - }); + it('should use network default chainTag when not explicitly set', async function () { + const txBuilder = factory.getClaimRewardsBuilder(); + txBuilder.tokenId(tokenId); + txBuilder.stakingContractAddress(stakingContractAddress); + // Not setting chainTag + txBuilder.blockRef('0x0000000000000000'); + txBuilder.expiration(64); + txBuilder.gas(100000); + txBuilder.gasPriceCoef(0); + txBuilder.nonce('12345'); + txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9'); - it('should use network default chainTag when not explicitly set', async function () { - const txBuilder = factory.getClaimRewardsBuilder(); - txBuilder.claimRewardsData({ - tokenId, - }); - // Not setting chainTag - txBuilder.blockRef('0x0000000000000000'); - txBuilder.expiration(64); - txBuilder.gas(100000); - txBuilder.gasPriceCoef(0); - txBuilder.nonce('12345'); - txBuilder.sender('0x9378c12BD7502A11F770a5C1F223c959B2805dA9'); + const tx = await txBuilder.build(); + tx.should.be.instanceof(ClaimRewardsTransaction); - const tx = await txBuilder.build(); - tx.should.be.instanceof(ClaimRewardsTransaction); + const claimTx = tx as ClaimRewardsTransaction; + // Verify the chainTag is set to the testnet default (39) + claimTx.chainTag.should.equal(39); + }); - const claimTx = tx as ClaimRewardsTransaction; - // Verify the chainTag is set to the testnet default (39) - claimTx.chainTag.should.equal(39); - }); + it('should build a signed tx and validate its toJson', async function () { + const tokenIdForClaimTxn = '15662'; + const txBuilder = factory.from(testData.CLAIM_REWARDS_TRANSACTION); + const tx = txBuilder.transaction as ClaimRewardsTransaction; + const toJson = tx.toJson(); + toJson.id.should.equal('0x841b388ee325838eb1e3efad661c2ae3266e950b8fc86b8bb484571bdfa27c6d'); + toJson.stakingContractAddress?.should.equal('0x1e02b2953adefec225cf0ec49805b1146a4429c1'); + toJson.nonce.should.equal('973150'); + toJson.gas.should.equal(150878); + toJson.gasPriceCoef.should.equal(128); + toJson.expiration.should.equal(64); + toJson.chainTag.should.equal(39); + toJson.tokenId?.should.equal(tokenIdForClaimTxn); + }); - it('should serialize and explain transaction correctly', async function () { + describe('Failure scenarios', function () { + it('should throw error when token ID is missing', async function () { const txBuilder = createBasicTxBuilder(); - txBuilder.claimRewardsData({ - tokenId, - }); - - const tx = await txBuilder.build(); - const claimTx = tx as ClaimRewardsTransaction; - - // Test serialization - const serialized = claimTx.toBroadcastFormat(); - serialized.should.be.String(); - serialized.should.startWith('0x'); - // Test explanation - const explanation = claimTx.explainTransaction(); - explanation.type?.should.equal(TransactionType.StakingClaim); - should.exist(explanation.fee); - explanation.outputAmount.should.equal('0'); - explanation.changeAmount.should.equal('0'); - - // Test toJson - const json = claimTx.toJson(); - should.exist(json.claimRewardsData); - json.claimRewardsData?.tokenId.should.equal(tokenId); + await txBuilder.build().should.be.rejectedWith('Token ID is required'); }); - it('should correctly handle custom contract addresses when building transactions', async function () { - const customNftAddress = '0x1234567890123456789012345678901234567890'; - const customDelegationAddress = '0x0987654321098765432109876543210987654321'; - + it('should throw error when staking contract address is missing', async function () { 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) - ); + txBuilder.tokenId(tokenId); - baseRewardsClause?.to?.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); - stakingRewardsClause?.to?.should.equal(STARGATE_CONTRACT_ADDRESS_TESTNET); + await txBuilder.build().should.be.rejectedWith('Staking contract address is required'); }); }); }); diff --git a/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts b/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts index bded9eb271..5816cc7b7b 100644 --- a/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts +++ b/modules/sdk-coin-vet/test/transactionBuilder/delegateClauseTxnBuilder.ts @@ -165,7 +165,6 @@ describe('VET Delegation Transaction', function () { toJson.gasPriceCoef.should.equal(128); toJson.expiration.should.equal(64); toJson.chainTag.should.equal(39); - // in delegate txn, nftTokenId indicates the tokenId toJson.tokenId?.should.equal(tokenIdForDelegateTxn); toJson.validatorAddress?.should.equal('00563ec3cafbbe7e60b04b3190e6eca66579706d'); });