Skip to content

Commit 579b957

Browse files
fix(sdk-coin-vet): fix claim rewards builder for hayabusa
Ticket: SC-3991
1 parent 704a646 commit 579b957

File tree

8 files changed

+177
-426
lines changed

8 files changed

+177
-426
lines changed

modules/sdk-coin-vet/src/lib/iface.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,6 @@ export interface VetTransactionData {
3232
nftTokenId?: number; // Used as tier level (levelId) for stakeAndDelegate method (not the actual NFT token ID)
3333
autorenew?: boolean; // Autorenew flag for stakeAndDelegate method
3434
nftCollectionId?: string;
35-
claimRewardsData?: ClaimRewardsData;
3635
validatorAddress?: string;
3736
}
3837

modules/sdk-coin-vet/src/lib/transaction/claimRewards.ts

Lines changed: 42 additions & 110 deletions
Original file line numberDiff line numberDiff line change
@@ -5,24 +5,32 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics';
55
import { Transaction as VetTransaction, Secp256k1, TransactionClause } from '@vechain/sdk-core';
66
import { Transaction } from './transaction';
77
import { VetTransactionData } from '../iface';
8-
import { ClaimRewardsData } from '../types';
9-
import { CLAIM_BASE_REWARDS_METHOD_ID, CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants';
8+
import { CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants';
109
import utils from '../utils';
1110

1211
export class ClaimRewardsTransaction extends Transaction {
13-
private _claimRewardsData: ClaimRewardsData;
12+
private _stakingContractAddress: string;
13+
private _tokenId: string;
1414

1515
constructor(_coinConfig: Readonly<CoinConfig>) {
1616
super(_coinConfig);
1717
this._type = TransactionType.StakingClaim;
1818
}
1919

20-
get claimRewardsData(): ClaimRewardsData {
21-
return this._claimRewardsData;
20+
get stakingContractAddress(): string {
21+
return this._stakingContractAddress;
2222
}
2323

24-
set claimRewardsData(data: ClaimRewardsData) {
25-
this._claimRewardsData = data;
24+
set stakingContractAddress(address: string) {
25+
this._stakingContractAddress = address;
26+
}
27+
28+
get tokenId(): string {
29+
return this._tokenId;
30+
}
31+
32+
set tokenId(tokenId: string) {
33+
this._tokenId = tokenId;
2634
}
2735

2836
/** @inheritdoc */
@@ -51,65 +59,37 @@ export class ClaimRewardsTransaction extends Transaction {
5159

5260
/** @inheritdoc */
5361
buildClauses(): void {
54-
if (!this._claimRewardsData) {
55-
throw new InvalidTransactionError('Missing claim rewards data');
62+
if (!this.stakingContractAddress) {
63+
throw new Error('Staking contract address is not set');
5664
}
5765

58-
const clauses: TransactionClause[] = [];
66+
utils.validateStakingContractAddress(this.stakingContractAddress, this._coinConfig);
5967

60-
// Add clause for claiming base rewards if requested
61-
const shouldClaimBaseRewards = this.claimRewardsData.claimBaseRewards !== false; // Default true
62-
if (shouldClaimBaseRewards) {
63-
clauses.push(this.buildClaimBaseRewardsClause());
68+
if (this.tokenId === undefined || this.tokenId === null) {
69+
throw new Error('Token ID is not set');
6470
}
6571

66-
// Add clause for claiming staking rewards if requested
67-
const shouldClaimStakingRewards = this.claimRewardsData.claimStakingRewards !== false; // Default true
68-
if (shouldClaimStakingRewards) {
69-
clauses.push(this.buildClaimStakingRewardsClause());
70-
}
72+
const data = this.encodeClaimRewardsMethod(this.tokenId);
73+
this._transactionData = data;
7174

72-
if (clauses.length === 0) {
73-
throw new InvalidTransactionError('At least one type of rewards must be claimed');
74-
}
75-
76-
this.clauses = clauses;
75+
// Create the clause for claim rewards
76+
this._clauses = [
77+
{
78+
to: this.stakingContractAddress,
79+
value: '0x0',
80+
data: this._transactionData,
81+
},
82+
];
7783

7884
// Set recipients as empty since claim rewards doesn't send value
7985
this.recipients = [];
8086
}
8187

82-
/**
83-
* Build clause for claiming base rewards (claimVetGeneratedVtho)
84-
*/
85-
private buildClaimBaseRewardsClause(): TransactionClause {
86-
const methodData = this.encodeClaimRewardsMethod(CLAIM_BASE_REWARDS_METHOD_ID, this._claimRewardsData.tokenId);
87-
88-
return {
89-
to: utils.getDefaultStakingAddress(this._coinConfig),
90-
value: '0x0',
91-
data: methodData,
92-
};
93-
}
94-
95-
/**
96-
* Build clause for claiming staking rewards (claimRewards)
97-
*/
98-
private buildClaimStakingRewardsClause(): TransactionClause {
99-
const methodData = this.encodeClaimRewardsMethod(CLAIM_STAKING_REWARDS_METHOD_ID, this._claimRewardsData.tokenId);
100-
101-
return {
102-
to: utils.getDefaultDelegationAddress(this._coinConfig),
103-
value: '0x0',
104-
data: methodData,
105-
};
106-
}
107-
10888
/**
10989
* Encode the claim rewards method call data
11090
*/
111-
private encodeClaimRewardsMethod(methodId: string, tokenId: string): string {
112-
const methodName = methodId === CLAIM_BASE_REWARDS_METHOD_ID ? 'claimVetGeneratedVtho' : 'claimRewards';
91+
private encodeClaimRewardsMethod(tokenId: string): string {
92+
const methodName = 'claimRewards';
11393
const types = ['uint256'];
11494
const params = [tokenId];
11595

@@ -133,7 +113,8 @@ export class ClaimRewardsTransaction extends Transaction {
133113
sender: this.sender,
134114
feePayer: this.feePayerAddress,
135115
recipients: this.recipients,
136-
claimRewardsData: this._claimRewardsData,
116+
tokenId: this.tokenId,
117+
stakingContractAddress: this.stakingContractAddress,
137118
};
138119
return json;
139120
}
@@ -159,8 +140,14 @@ export class ClaimRewardsTransaction extends Transaction {
159140
this.dependsOn = body.dependsOn || null;
160141
this.nonce = String(body.nonce);
161142

162-
// Parse claim rewards data from clauses
163-
this.parseClaimRewardsDataFromClauses(body.clauses);
143+
if (body.clauses.length === 1) {
144+
const clause = body.clauses[0];
145+
if (clause.data && clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)) {
146+
// claimRewards should go to STARGATE_DELEGATION_ADDRESS
147+
this.tokenId = utils.decodeClaimRewardsData(clause.data);
148+
this.stakingContractAddress = clause.to || '0x0';
149+
}
150+
}
164151

165152
// Set recipients as empty for claim rewards
166153
this.recipients = [];
@@ -185,59 +172,4 @@ export class ClaimRewardsTransaction extends Transaction {
185172
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
186173
}
187174
}
188-
189-
/**
190-
* Parse claim rewards data from transaction clauses
191-
*/
192-
private parseClaimRewardsDataFromClauses(clauses: TransactionClause[]): void {
193-
if (!clauses || clauses.length === 0) {
194-
throw new InvalidTransactionError('No clauses found in transaction');
195-
}
196-
197-
let claimBaseRewards = false;
198-
let claimStakingRewards = false;
199-
let tokenId = '';
200-
let delegationContractAddress = '';
201-
let stargateNftAddress = '';
202-
203-
for (const clause of clauses) {
204-
if (clause.data) {
205-
if (clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID)) {
206-
// claimVetGeneratedVtho should go to STARGATE_NFT_ADDRESS
207-
claimBaseRewards = true;
208-
if (!tokenId) {
209-
tokenId = utils.decodeClaimRewardsData(clause.data);
210-
}
211-
if (!stargateNftAddress && clause.to) {
212-
stargateNftAddress = clause.to;
213-
}
214-
} else if (clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)) {
215-
// claimRewards should go to STARGATE_DELEGATION_ADDRESS
216-
claimStakingRewards = true;
217-
if (!tokenId) {
218-
tokenId = utils.decodeClaimRewardsData(clause.data);
219-
}
220-
if (!delegationContractAddress && clause.to) {
221-
delegationContractAddress = clause.to;
222-
}
223-
}
224-
}
225-
}
226-
227-
if (!claimBaseRewards && !claimStakingRewards) {
228-
throw new InvalidTransactionError('Transaction does not contain claim rewards clauses');
229-
}
230-
231-
this._claimRewardsData = {
232-
tokenId,
233-
delegationContractAddress:
234-
delegationContractAddress && !utils.isDelegationContractAddress(delegationContractAddress)
235-
? delegationContractAddress
236-
: undefined,
237-
stargateNftAddress:
238-
stargateNftAddress && !utils.isNftContractAddress(stargateNftAddress) ? stargateNftAddress : undefined,
239-
claimBaseRewards,
240-
claimStakingRewards,
241-
};
242-
}
243175
}

modules/sdk-coin-vet/src/lib/transaction/delegateClauseTransaction.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ export class DelegateClauseTransaction extends Transaction {
4848
throw new Error('Staking contract address is not set');
4949
}
5050

51-
utils.validateDelegationContractAddress(this.stakingContractAddress, this._coinConfig);
51+
utils.validateStakingContractAddress(this.stakingContractAddress, this._coinConfig);
5252

5353
if (this.tokenId === undefined || this.tokenId === null) {
5454
throw new Error('Token ID is not set');

modules/sdk-coin-vet/src/lib/transactionBuilder/claimRewardsBuilder.ts

Lines changed: 43 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,7 @@ import assert from 'assert';
66
import { TransactionBuilder } from './transactionBuilder';
77
import { ClaimRewardsTransaction } from '../transaction/claimRewards';
88
import { Transaction } from '../transaction/transaction';
9-
import { ClaimRewardsData } from '../types';
10-
import { CLAIM_BASE_REWARDS_METHOD_ID, CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants';
9+
import { CLAIM_STAKING_REWARDS_METHOD_ID } from '../constants';
1110
import utils from '../utils';
1211

1312
export class ClaimRewardsBuilder extends TransactionBuilder {
@@ -49,92 +48,61 @@ export class ClaimRewardsBuilder extends TransactionBuilder {
4948
}
5049

5150
/**
52-
* Validates the transaction clauses for claim rewards transaction.
53-
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
54-
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
51+
* Sets the staking contract address for this claim tx.
52+
* The address must be explicitly provided to ensure the correct contract is used.
53+
*
54+
* @param {string} address - The staking contract address (required)
55+
* @returns {ClaimRewardsBuilder} This transaction builder
56+
* @throws {Error} If no address is provided
5557
*/
56-
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
57-
try {
58-
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
59-
return false;
60-
}
61-
62-
let hasValidClaimClause = false;
63-
64-
for (const clause of clauses) {
65-
if (!clause.to || !utils.isValidAddress(clause.to)) {
66-
return false;
67-
}
68-
69-
// For claim rewards transactions, value must be '0x0' or 0
70-
if (clause.value !== '0x0' && clause.value !== 0 && clause.value !== '0') {
71-
return false;
72-
}
73-
74-
const isDelegationContract = utils.isDelegationContractAddress(clause.to);
75-
const isNftContract = utils.isNftContractAddress(clause.to);
76-
77-
if (clause.data && (isDelegationContract || isNftContract)) {
78-
if (
79-
(clause.data.startsWith(CLAIM_BASE_REWARDS_METHOD_ID) && isNftContract) ||
80-
(clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID) && isDelegationContract)
81-
) {
82-
hasValidClaimClause = true;
83-
}
84-
}
85-
}
86-
87-
return hasValidClaimClause;
88-
} catch (e) {
89-
return false;
58+
stakingContractAddress(address: string): this {
59+
if (!address) {
60+
throw new Error('Staking contract address is required');
9061
}
62+
this.validateAddress({ address });
63+
this.claimRewardsTransaction.stakingContractAddress = address;
64+
return this;
9165
}
9266

9367
/**
94-
* Sets the claim rewards data for this transaction.
68+
* Sets the token ID for this claim tx.
9569
*
96-
* @param {ClaimRewardsData} data - The claim rewards data
97-
* @returns {ClaimRewardsBuilder} This transaction builder
70+
* @param {number} levelId - The NFT token ID
71+
* @returns {DelegateTxnBuilder} This transaction builder
9872
*/
99-
claimRewardsData(data: ClaimRewardsData): this {
100-
this.validateClaimRewardsData(data);
101-
this.claimRewardsTransaction.claimRewardsData = data;
73+
tokenId(tokenId: string): this {
74+
this.claimRewardsTransaction.tokenId = tokenId;
10275
return this;
10376
}
10477

10578
/**
106-
* Validates the claim rewards data.
107-
*
108-
* @param {ClaimRewardsData} data - The claim rewards data to validate
79+
* Validates the transaction clauses for claim rewards transaction.
80+
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
81+
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
10982
*/
110-
private validateClaimRewardsData(data: ClaimRewardsData): void {
111-
if (!data) {
112-
throw new Error('Claim rewards data is required');
113-
}
114-
115-
if (!data.tokenId) {
116-
throw new Error('Token ID is required');
117-
}
118-
119-
// Validate tokenId is a valid number string
120-
if (!/^\d+$/.test(data.tokenId)) {
121-
throw new Error('Token ID must be a valid number string');
122-
}
83+
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
84+
try {
85+
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
86+
return false;
87+
}
12388

124-
if (data.claimBaseRewards !== undefined && typeof data.claimBaseRewards !== 'boolean') {
125-
throw new Error('claimBaseRewards must be a boolean');
126-
}
89+
const clause = clauses[0];
90+
if (!clause.to || !utils.isValidAddress(clause.to)) {
91+
return false;
92+
}
12793

128-
if (data.claimStakingRewards !== undefined && typeof data.claimStakingRewards !== 'boolean') {
129-
throw new Error('claimStakingRewards must be a boolean');
130-
}
94+
// Ensure value is '0x0', '0', or 0
95+
if (!['0x0', '0', 0].includes(clause.value)) {
96+
return false;
97+
}
13198

132-
// At least one type of rewards must be claimed (both default to true if undefined)
133-
const claimBase = data.claimBaseRewards !== false;
134-
const claimStaking = data.claimStakingRewards !== false;
99+
if (clause.data && clause.data.startsWith(CLAIM_STAKING_REWARDS_METHOD_ID)) {
100+
return true;
101+
}
135102

136-
if (!claimBase && !claimStaking) {
137-
throw new Error('At least one type of rewards (base or staking) must be claimed');
103+
return false;
104+
} catch (e) {
105+
return false;
138106
}
139107
}
140108

@@ -144,14 +112,10 @@ export class ClaimRewardsBuilder extends TransactionBuilder {
144112
throw new Error('transaction not defined');
145113
}
146114

147-
const claimData = transaction.claimRewardsData;
148-
assert(claimData, 'Claim rewards data is required');
149-
assert(claimData.tokenId, 'Token ID is required');
115+
assert(transaction.tokenId, 'Token ID is required');
116+
assert(transaction.stakingContractAddress, 'Staking contract address is required');
150117

151-
// Validate tokenId is a valid number string
152-
if (!/^\d+$/.test(claimData.tokenId)) {
153-
throw new Error('Token ID must be a valid number string');
154-
}
118+
this.validateAddress({ address: transaction.stakingContractAddress });
155119
}
156120

157121
/** @inheritdoc */

modules/sdk-coin-vet/src/lib/types.ts

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@ export interface VetParseTransactionOptions extends ParseTransactionOptions {
1010

1111
export interface ClaimRewardsData {
1212
tokenId: string;
13-
delegationContractAddress?: string;
14-
stargateNftAddress?: string;
15-
claimBaseRewards?: boolean;
16-
claimStakingRewards?: boolean;
13+
stakingContractAddress?: string;
1714
}
1815

1916
export type RecoverOptions = {

0 commit comments

Comments
 (0)