Skip to content

Commit ed88cd9

Browse files
committed
feat(sdk-coin-vet): add decreaseStake builder for validators
Ticket: SC-6237
1 parent 80c470e commit ed88cd9

8 files changed

Lines changed: 444 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
@@ -9,6 +9,7 @@ export const STAKE_CLAUSE_METHOD_ID = '0x604f2177';
99
export const DELEGATE_CLAUSE_METHOD_ID = '0x08bbb824';
1010
export const ADD_VALIDATION_METHOD_ID = '0xc3c4b138';
1111
export const INCREASE_STAKE_METHOD_ID = '0x43b0de9a';
12+
export const DECREASE_STAKE_METHOD_ID = '0x1a73ba01';
1213
export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d';
1314
export const BURN_NFT_METHOD_ID = '0x2e17de78';
1415
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
@@ -16,6 +16,7 @@ export { ClaimRewardsTransaction } from './transaction/claimRewards';
1616
export { NFTTransaction } from './transaction/nftTransaction';
1717
export { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
1818
export { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
19+
export { DecreaseStakeTransaction } from './transaction/decreaseStakeTransaction';
1920
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
2021
export { TransferBuilder } from './transactionBuilder/transferBuilder';
2122
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
@@ -29,5 +30,6 @@ export { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilde
2930
export { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
3031
export { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
3132
export { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
33+
export { DecreaseStakeBuilder } from './transactionBuilder/decreaseStakeBuilder';
3234
export { TransactionBuilderFactory } from './transactionBuilderFactory';
3335
export { Constants, Utils, Interface };
Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,162 @@
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, BN } from 'ethereumjs-util';
10+
import { ZERO_VALUE_AMOUNT } from '../constants';
11+
12+
export class DecreaseStakeTransaction extends Transaction {
13+
private _stakingContractAddress: string;
14+
private _validator: string;
15+
private _amount: string;
16+
17+
constructor(_coinConfig: Readonly<CoinConfig>) {
18+
super(_coinConfig);
19+
this._type = TransactionType.StakingDeactivate;
20+
}
21+
22+
get validator(): string {
23+
return this._validator;
24+
}
25+
26+
set validator(address: string) {
27+
this._validator = address;
28+
}
29+
30+
get amount(): string {
31+
return this._amount;
32+
}
33+
34+
set amount(value: string) {
35+
this._amount = value;
36+
}
37+
38+
get stakingContractAddress(): string {
39+
return this._stakingContractAddress;
40+
}
41+
42+
set stakingContractAddress(address: string) {
43+
this._stakingContractAddress = address;
44+
}
45+
46+
buildClauses(): void {
47+
if (!this.stakingContractAddress) {
48+
throw new Error('Staking contract address is not set');
49+
}
50+
51+
if (!this.validator) {
52+
throw new Error('Validator address is not set');
53+
}
54+
55+
if (!this.amount) {
56+
throw new Error('Amount is not set');
57+
}
58+
59+
utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
60+
const decreaseStakeData = this.getDecreaseStakeClauseData(this.validator, this.amount);
61+
this._transactionData = decreaseStakeData;
62+
this._clauses = [
63+
{
64+
to: this.stakingContractAddress,
65+
value: ZERO_VALUE_AMOUNT,
66+
data: decreaseStakeData,
67+
},
68+
];
69+
70+
this._recipients = [
71+
{
72+
address: this.stakingContractAddress,
73+
amount: ZERO_VALUE_AMOUNT,
74+
},
75+
];
76+
}
77+
78+
getDecreaseStakeClauseData(validator: string, amount: string): string {
79+
const methodName = 'decreaseStake';
80+
const types = ['address', 'uint256'];
81+
const params = [validator, new BN(amount)];
82+
83+
const method = EthereumAbi.methodID(methodName, types);
84+
const args = EthereumAbi.rawEncode(types, params);
85+
86+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
87+
}
88+
89+
toJson(): VetTransactionData {
90+
return {
91+
id: this.id,
92+
chainTag: this.chainTag,
93+
blockRef: this.blockRef,
94+
expiration: this.expiration,
95+
gasPriceCoef: this.gasPriceCoef,
96+
gas: this.gas,
97+
dependsOn: this.dependsOn,
98+
nonce: this.nonce,
99+
data: this.transactionData,
100+
value: ZERO_VALUE_AMOUNT,
101+
sender: this.sender,
102+
to: this.stakingContractAddress,
103+
stakingContractAddress: this.stakingContractAddress,
104+
amountToStake: this.amount,
105+
validatorAddress: this.validator,
106+
};
107+
}
108+
109+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
110+
try {
111+
if (!signedTx || !signedTx.body) {
112+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
113+
}
114+
115+
this.rawTransaction = signedTx;
116+
117+
const body = signedTx.body;
118+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
119+
this.blockRef = body.blockRef || '0x0';
120+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
121+
this.clauses = body.clauses || [];
122+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
123+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
124+
this.dependsOn = body.dependsOn || null;
125+
this.nonce = String(body.nonce);
126+
127+
if (body.clauses.length > 0) {
128+
const clause = body.clauses[0];
129+
if (clause.to) {
130+
this.stakingContractAddress = clause.to;
131+
}
132+
133+
if (clause.data) {
134+
this.transactionData = clause.data;
135+
const decoded = utils.decodeDecreaseStakeData(clause.data);
136+
this.validator = decoded.validator;
137+
this.amount = decoded.amount;
138+
}
139+
}
140+
141+
this.recipients = body.clauses.map((clause) => ({
142+
address: (clause.to || '0x0').toString().toLowerCase(),
143+
amount: new BigNumber(clause.value || 0).toString(),
144+
}));
145+
this.loadInputsAndOutputs();
146+
147+
if (signedTx.signature && signedTx.origin) {
148+
this.sender = signedTx.origin.toString().toLowerCase();
149+
}
150+
151+
if (signedTx.signature) {
152+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
153+
154+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
155+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
156+
}
157+
}
158+
} catch (e) {
159+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
160+
}
161+
}
162+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,8 @@ export class Transaction extends BaseTransaction {
395395
this.type === TransactionType.StakingWithdraw ||
396396
this.type === TransactionType.StakingClaim ||
397397
this.type === TransactionType.StakingLock ||
398-
this.type === TransactionType.StakingAdd
398+
this.type === TransactionType.StakingAdd ||
399+
this.type === TransactionType.StakingDeactivate
399400
) {
400401
transactionBody.reserved = {
401402
features: 1, // mark transaction as delegated i.e. will use gas payer
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
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 { DecreaseStakeTransaction } from '../transaction/decreaseStakeTransaction';
10+
import utils from '../utils';
11+
12+
export class DecreaseStakeBuilder extends TransactionBuilder {
13+
constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig);
15+
this._transaction = new DecreaseStakeTransaction(_coinConfig);
16+
}
17+
18+
initBuilder(tx: DecreaseStakeTransaction): void {
19+
this._transaction = tx;
20+
}
21+
22+
get decreaseStakeTransaction(): DecreaseStakeTransaction {
23+
return this._transaction as DecreaseStakeTransaction;
24+
}
25+
26+
protected get transactionType(): TransactionType {
27+
return TransactionType.StakingDeactivate;
28+
}
29+
30+
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
31+
try {
32+
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
33+
return false;
34+
}
35+
36+
const clause = clauses[0];
37+
if (!clause.to || !utils.isValidAddress(clause.to)) {
38+
return false;
39+
}
40+
41+
return true;
42+
} catch (e) {
43+
return false;
44+
}
45+
}
46+
47+
stakingContractAddress(address: string): this {
48+
if (!address) {
49+
throw new Error('Staking contract address is required');
50+
}
51+
this.validateAddress({ address });
52+
this.decreaseStakeTransaction.stakingContractAddress = address;
53+
return this;
54+
}
55+
56+
amount(value: string): this {
57+
this.decreaseStakeTransaction.amount = value;
58+
return this;
59+
}
60+
61+
validator(address: string): this {
62+
if (!address) {
63+
throw new Error('Validator address is required');
64+
}
65+
this.validateAddress({ address });
66+
this.decreaseStakeTransaction.validator = address;
67+
return this;
68+
}
69+
70+
transactionData(data: string): this {
71+
this.decreaseStakeTransaction.transactionData = data;
72+
return this;
73+
}
74+
75+
/** @inheritdoc */
76+
validateTransaction(transaction?: DecreaseStakeTransaction): void {
77+
if (!transaction) {
78+
throw new Error('transaction not defined');
79+
}
80+
assert(transaction.stakingContractAddress, 'Staking contract address is required');
81+
assert(transaction.validator, 'Validator address is required');
82+
assert(transaction.amount, 'Amount is required');
83+
84+
const amt = new BigNumber(transaction.amount);
85+
if (amt.isLessThanOrEqualTo(0)) {
86+
throw new Error('Amount must be greater than 0');
87+
}
88+
89+
this.validateAddress({ address: transaction.stakingContractAddress });
90+
}
91+
92+
/** @inheritdoc */
93+
protected async buildImplementation(): Promise<Transaction> {
94+
this.transaction.type = this.transactionType;
95+
await this.decreaseStakeTransaction.build();
96+
return this.transaction;
97+
}
98+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ import { ValidatorRegistrationTransaction } from './transaction/validatorRegistr
2929
import { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
3030
import { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
3131
import { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
32+
import { DecreaseStakeTransaction } from './transaction/decreaseStakeTransaction';
33+
import { DecreaseStakeBuilder } from './transactionBuilder/decreaseStakeBuilder';
3234

3335
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3436
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -93,6 +95,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
9395
const increaseStakeTx = new IncreaseStakeTransaction(this._coinConfig);
9496
increaseStakeTx.fromDeserializedSignedTransaction(signedTx);
9597
return this.getIncreaseStakeBuilder(increaseStakeTx);
98+
case TransactionType.StakingDeactivate:
99+
const decreaseStakeTx = new DecreaseStakeTransaction(this._coinConfig);
100+
decreaseStakeTx.fromDeserializedSignedTransaction(signedTx);
101+
return this.getDecreaseStakeBuilder(decreaseStakeTx);
96102
default:
97103
throw new InvalidTransactionError('Invalid transaction type');
98104
}
@@ -134,6 +140,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
134140
return this.initializeBuilder(tx, new IncreaseStakeBuilder(this._coinConfig));
135141
}
136142

143+
getDecreaseStakeBuilder(tx?: DecreaseStakeTransaction): DecreaseStakeBuilder {
144+
return this.initializeBuilder(tx, new DecreaseStakeBuilder(this._coinConfig));
145+
}
146+
137147
getStakingActivateBuilder(tx?: StakeClauseTransaction): StakeClauseTxnBuilder {
138148
return this.initializeBuilder(tx, new StakeClauseTxnBuilder(this._coinConfig));
139149
}

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

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import {
2929
VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET,
3030
ADD_VALIDATION_METHOD_ID,
3131
INCREASE_STAKE_METHOD_ID,
32+
DECREASE_STAKE_METHOD_ID,
3233
} from './constants';
3334
import { KeyPair } from './keyPair';
3435
import { BaseCoin as CoinConfig } from '@bitgo/statics';
@@ -107,6 +108,8 @@ export class Utils implements BaseUtils {
107108
return TransactionType.StakingLock;
108109
} else if (clauses[0].data.startsWith(INCREASE_STAKE_METHOD_ID)) {
109110
return TransactionType.StakingAdd;
111+
} else if (clauses[0].data.startsWith(DECREASE_STAKE_METHOD_ID)) {
112+
return TransactionType.StakingDeactivate;
110113
} else if (clauses[0].data.startsWith(BURN_NFT_METHOD_ID)) {
111114
return TransactionType.StakingWithdraw;
112115
} else if (
@@ -308,6 +311,27 @@ export class Utils implements BaseUtils {
308311
}
309312
}
310313

314+
/**
315+
* Decodes decreaseStake transaction data to extract validator address and amount
316+
*
317+
* @param {string} data - The encoded transaction data
318+
* @returns {object} - Object containing validator address and amount
319+
*/
320+
decodeDecreaseStakeData(data: string): { validator: string; amount: string } {
321+
try {
322+
const parameters = data.slice(10);
323+
324+
const decoded = EthereumAbi.rawDecode(['address', 'uint256'], Buffer.from(parameters, 'hex'));
325+
326+
return {
327+
validator: addHexPrefix(decoded[0].toString()).toLowerCase(),
328+
amount: decoded[1].toString(),
329+
};
330+
} catch (error) {
331+
throw new Error(`Failed to decode decrease stake data: ${error.message}`);
332+
}
333+
}
334+
311335
/**
312336
* Decodes exit delegation transaction data to extract tokenId
313337
*

0 commit comments

Comments
 (0)