Skip to content

Commit c71f68f

Browse files
committed
feat(vet): add increaseStake builder for staking
Ticket: SC-5966
1 parent 349c847 commit c71f68f

8 files changed

Lines changed: 572 additions & 1 deletion

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export const STAKING_METHOD_ID = '0xd8da3bbf';
88
export const STAKE_CLAUSE_METHOD_ID = '0x604f2177';
99
export const DELEGATE_CLAUSE_METHOD_ID = '0x08bbb824';
1010
export const ADD_VALIDATION_METHOD_ID = '0xc3c4b138';
11+
export const INCREASE_STAKE_METHOD_ID = '0x43b0de9a';
1112
export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d';
1213
export const BURN_NFT_METHOD_ID = '0x2e17de78';
1314
export const TRANSFER_NFT_METHOD_ID = '0x23b872dd';

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ export { BurnNftTransaction } from './transaction/burnNftTransaction';
1515
export { ClaimRewardsTransaction } from './transaction/claimRewards';
1616
export { NFTTransaction } from './transaction/nftTransaction';
1717
export { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
18+
export { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
1819
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
1920
export { TransferBuilder } from './transactionBuilder/transferBuilder';
2021
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
@@ -27,5 +28,6 @@ export { BurnNftBuilder } from './transactionBuilder/burnNftBuilder';
2728
export { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilder';
2829
export { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
2930
export { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
31+
export { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
3032
export { TransactionBuilderFactory } from './transactionBuilderFactory';
3133
export { Constants, Utils, Interface };
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { Transaction as VetTransaction, Secp256k1 } from '@vechain/sdk-core';
4+
import { Transaction } from './transaction';
5+
import { VetTransactionData } from '../iface';
6+
import EthereumAbi from 'ethereumjs-abi';
7+
import utils from '../utils';
8+
import BigNumber from 'bignumber.js';
9+
import { addHexPrefix } from 'ethereumjs-util';
10+
11+
export class IncreaseStakeTransaction extends Transaction {
12+
private _stakingContractAddress: string;
13+
private _validator: string;
14+
private _amountToStake: string;
15+
16+
constructor(_coinConfig: Readonly<CoinConfig>) {
17+
super(_coinConfig);
18+
this._type = TransactionType.StakingAdd;
19+
}
20+
21+
get validator(): string {
22+
return this._validator;
23+
}
24+
25+
set validator(address: string) {
26+
this._validator = address;
27+
}
28+
29+
get amountToStake(): string {
30+
return this._amountToStake;
31+
}
32+
33+
set amountToStake(amount: string) {
34+
this._amountToStake = amount;
35+
}
36+
37+
get stakingContractAddress(): string {
38+
return this._stakingContractAddress;
39+
}
40+
41+
set stakingContractAddress(address: string) {
42+
this._stakingContractAddress = address;
43+
}
44+
45+
buildClauses(): void {
46+
if (!this.stakingContractAddress) {
47+
throw new Error('Staking contract address is not set');
48+
}
49+
50+
if (!this.validator) {
51+
throw new Error('Validator address is not set');
52+
}
53+
54+
utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
55+
const increaseStakeData = this.getIncreaseStakeClauseData(this.validator);
56+
this._transactionData = increaseStakeData;
57+
// Create the clause for increase stake
58+
this._clauses = [
59+
{
60+
to: this.stakingContractAddress,
61+
value: this.amountToStake,
62+
data: increaseStakeData,
63+
},
64+
];
65+
66+
// Set recipients based on the clauses
67+
this._recipients = [
68+
{
69+
address: this.stakingContractAddress,
70+
amount: this.amountToStake,
71+
},
72+
];
73+
}
74+
75+
/**
76+
* Encodes increaseStake transaction data using ethereumjs-abi for increaseStake method
77+
* @param {string} validator - address of the validator
78+
* @returns {string} - The encoded transaction data
79+
*/
80+
getIncreaseStakeClauseData(validator: string): string {
81+
const methodName = 'increaseStake';
82+
const types = ['address'];
83+
const params = [validator];
84+
85+
const method = EthereumAbi.methodID(methodName, types);
86+
const args = EthereumAbi.rawEncode(types, params);
87+
88+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
89+
}
90+
91+
toJson(): VetTransactionData {
92+
const json: VetTransactionData = {
93+
id: this.id,
94+
chainTag: this.chainTag,
95+
blockRef: this.blockRef,
96+
expiration: this.expiration,
97+
gasPriceCoef: this.gasPriceCoef,
98+
gas: this.gas,
99+
dependsOn: this.dependsOn,
100+
nonce: this.nonce,
101+
data: this.transactionData,
102+
value: this.amountToStake,
103+
sender: this.sender,
104+
to: this.stakingContractAddress,
105+
stakingContractAddress: this.stakingContractAddress,
106+
amountToStake: this.amountToStake,
107+
validatorAddress: this.validator,
108+
};
109+
110+
return json;
111+
}
112+
113+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
114+
try {
115+
if (!signedTx || !signedTx.body) {
116+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
117+
}
118+
119+
// Store the raw transaction
120+
this.rawTransaction = signedTx;
121+
122+
// Set transaction body properties
123+
const body = signedTx.body;
124+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
125+
this.blockRef = body.blockRef || '0x0';
126+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
127+
this.clauses = body.clauses || [];
128+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
129+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
130+
this.dependsOn = body.dependsOn || null;
131+
this.nonce = String(body.nonce);
132+
133+
// Set increase stake-specific properties
134+
if (body.clauses.length > 0) {
135+
const increaseStakeClause = body.clauses[0];
136+
if (increaseStakeClause.to) {
137+
this.stakingContractAddress = increaseStakeClause.to;
138+
}
139+
140+
if (increaseStakeClause.value) {
141+
this.amountToStake = new BigNumber(increaseStakeClause.value).toFixed();
142+
}
143+
144+
// Extract validator from increaseStake data
145+
if (increaseStakeClause.data) {
146+
this.transactionData = increaseStakeClause.data;
147+
const decoded = utils.decodeIncreaseStakeData(increaseStakeClause.data);
148+
this.validator = decoded.validator;
149+
}
150+
}
151+
152+
// Set recipients from clauses
153+
this.recipients = body.clauses.map((clause) => ({
154+
address: (clause.to || '0x0').toString().toLowerCase(),
155+
amount: new BigNumber(clause.value || 0).toString(),
156+
}));
157+
this.loadInputsAndOutputs();
158+
159+
// Set sender address
160+
if (signedTx.signature && signedTx.origin) {
161+
this.sender = signedTx.origin.toString().toLowerCase();
162+
}
163+
164+
// Set signatures if present
165+
if (signedTx.signature) {
166+
// First signature is sender's signature
167+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
168+
169+
// If there's additional signature data, it's the fee payer's signature
170+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
171+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
172+
}
173+
}
174+
} catch (e) {
175+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
176+
}
177+
}
178+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -394,7 +394,8 @@ export class Transaction extends BaseTransaction {
394394
this.type === TransactionType.StakingUnlock ||
395395
this.type === TransactionType.StakingWithdraw ||
396396
this.type === TransactionType.StakingClaim ||
397-
this.type === TransactionType.StakingLock
397+
this.type === TransactionType.StakingLock ||
398+
this.type === TransactionType.StakingAdd
398399
) {
399400
transactionBody.reserved = {
400401
features: 1, // mark transaction as delegated i.e. will use gas payer
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
import assert from 'assert';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { TransactionType } from '@bitgo/sdk-core';
4+
import { TransactionClause } from '@vechain/sdk-core';
5+
import BigNumber from 'bignumber.js';
6+
7+
import { TransactionBuilder } from './transactionBuilder';
8+
import { Transaction } from '../transaction/transaction';
9+
import { IncreaseStakeTransaction } from '../transaction/increaseStakeTransaction';
10+
import utils from '../utils';
11+
12+
export class IncreaseStakeBuilder extends TransactionBuilder {
13+
/**
14+
* Creates a new increase stake txn instance.
15+
*
16+
* @param {Readonly<CoinConfig>} _coinConfig - The coin configuration object
17+
*/
18+
constructor(_coinConfig: Readonly<CoinConfig>) {
19+
super(_coinConfig);
20+
this._transaction = new IncreaseStakeTransaction(_coinConfig);
21+
}
22+
23+
/**
24+
* Initializes the builder with an existing increase stake txn.
25+
*
26+
* @param {IncreaseStakeTransaction} tx - The transaction to initialize the builder with
27+
*/
28+
initBuilder(tx: IncreaseStakeTransaction): void {
29+
this._transaction = tx;
30+
}
31+
32+
/**
33+
* Gets the increase stake transaction instance.
34+
*
35+
* @returns {IncreaseStakeTransaction} The increase stake transaction
36+
*/
37+
get increaseStakeTransaction(): IncreaseStakeTransaction {
38+
return this._transaction as IncreaseStakeTransaction;
39+
}
40+
41+
/**
42+
* Gets the transaction type for increase stake.
43+
*
44+
* @returns {TransactionType} The transaction type
45+
*/
46+
protected get transactionType(): TransactionType {
47+
return TransactionType.StakingAdd;
48+
}
49+
50+
/**
51+
* Validates the transaction clauses for increase stake transaction.
52+
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
53+
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
54+
*/
55+
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
56+
try {
57+
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
58+
return false;
59+
}
60+
61+
const clause = clauses[0];
62+
if (!clause.to || !utils.isValidAddress(clause.to)) {
63+
return false;
64+
}
65+
66+
return true;
67+
} catch (e) {
68+
return false;
69+
}
70+
}
71+
72+
/**
73+
* Sets the staking contract address for this increase stake tx.
74+
* The address must be explicitly provided to ensure the correct contract is used.
75+
*
76+
* @param {string} address - The contract address (required)
77+
* @returns {IncreaseStakeBuilder} This transaction builder
78+
* @throws {Error} If no address is provided
79+
*/
80+
stakingContractAddress(address: string): this {
81+
if (!address) {
82+
throw new Error('Staking contract address is required');
83+
}
84+
this.validateAddress({ address });
85+
this.increaseStakeTransaction.stakingContractAddress = address;
86+
return this;
87+
}
88+
89+
/**
90+
* Sets the amount to stake for this increase stake tx (VET amount being sent).
91+
*
92+
* @param {string} amount - The amount to stake in wei
93+
* @returns {IncreaseStakeBuilder} This transaction builder
94+
*/
95+
amountToStake(amount: string): this {
96+
this.increaseStakeTransaction.amountToStake = amount;
97+
return this;
98+
}
99+
100+
/**
101+
* Sets the validator address for this increase stake tx.
102+
* @param {string} address - The validator address
103+
* @returns {IncreaseStakeBuilder} This transaction builder
104+
*/
105+
validator(address: string): this {
106+
if (!address) {
107+
throw new Error('Validator address is required');
108+
}
109+
this.validateAddress({ address });
110+
this.increaseStakeTransaction.validator = address;
111+
return this;
112+
}
113+
114+
/**
115+
* Sets the transaction data for this increase stake tx.
116+
*
117+
* @param {string} data - The transaction data
118+
* @returns {IncreaseStakeBuilder} This transaction builder
119+
*/
120+
transactionData(data: string): this {
121+
this.increaseStakeTransaction.transactionData = data;
122+
return this;
123+
}
124+
125+
/** @inheritdoc */
126+
validateTransaction(transaction?: IncreaseStakeTransaction): void {
127+
if (!transaction) {
128+
throw new Error('transaction not defined');
129+
}
130+
assert(transaction.stakingContractAddress, 'Staking contract address is required');
131+
assert(transaction.validator, 'Validator address is required');
132+
assert(transaction.amountToStake, 'Staking amount is required');
133+
134+
// Validate staking amount is greater than 0
135+
const amount = new BigNumber(transaction.amountToStake);
136+
if (amount.isLessThanOrEqualTo(0)) {
137+
throw new Error('Staking amount must be greater than 0');
138+
}
139+
140+
this.validateAddress({ address: transaction.stakingContractAddress });
141+
}
142+
143+
/** @inheritdoc */
144+
protected async buildImplementation(): Promise<Transaction> {
145+
this.transaction.type = this.transactionType;
146+
await this.increaseStakeTransaction.build();
147+
return this.transaction;
148+
}
149+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ import { DelegateTxnBuilder } from './transactionBuilder/delegateTxnBuilder';
2727
import { DelegateClauseTransaction } from './transaction/delegateClauseTransaction';
2828
import { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
2929
import { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
30+
import { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
31+
import { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
3032

3133
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3234
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -87,6 +89,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
8789
const validatorRegistrationTx = new ValidatorRegistrationTransaction(this._coinConfig);
8890
validatorRegistrationTx.fromDeserializedSignedTransaction(signedTx);
8991
return this.getValidatorRegistrationBuilder(validatorRegistrationTx);
92+
case TransactionType.StakingAdd:
93+
const increaseStakeTx = new IncreaseStakeTransaction(this._coinConfig);
94+
increaseStakeTx.fromDeserializedSignedTransaction(signedTx);
95+
return this.getIncreaseStakeBuilder(increaseStakeTx);
9096
default:
9197
throw new InvalidTransactionError('Invalid transaction type');
9298
}
@@ -124,6 +130,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
124130
return this.initializeBuilder(tx, new ValidatorRegistrationBuilder(this._coinConfig));
125131
}
126132

133+
getIncreaseStakeBuilder(tx?: IncreaseStakeTransaction): IncreaseStakeBuilder {
134+
return this.initializeBuilder(tx, new IncreaseStakeBuilder(this._coinConfig));
135+
}
136+
127137
getStakingActivateBuilder(tx?: StakeClauseTransaction): StakeClauseTxnBuilder {
128138
return this.initializeBuilder(tx, new StakeClauseTxnBuilder(this._coinConfig));
129139
}

0 commit comments

Comments
 (0)