Skip to content

Commit 5fd77f6

Browse files
feat(sdk-coin-vet): add support for validator registration
Ticket: SC-4409
1 parent e8ea6be commit 5fd77f6

File tree

10 files changed

+610
-2
lines changed

10 files changed

+610
-2
lines changed

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb';
77
export const STAKING_METHOD_ID = '0xd8da3bbf';
88
export const STAKE_CLAUSE_METHOD_ID = '0x604f2177';
99
export const DELEGATE_CLAUSE_METHOD_ID = '0x08bbb824';
10+
export const ADD_VALIDATION_METHOD_ID = '0xc3c4b138';
1011
export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d';
1112
export const BURN_NFT_METHOD_ID = '0x2e17de78';
1213
export const TRANSFER_NFT_METHOD_ID = '0x23b872dd';
@@ -20,6 +21,9 @@ export const STARGATE_DELEGATION_ADDRESS_TESTNET = '0x7240e3bc0d26431512d5b67dbd
2021
export const STARGATE_NFT_ADDRESS_TESTNET = '0x887d9102f0003f1724d8fd5d4fe95a11572fcd77';
2122
export const STARGATE_CONTRACT_ADDRESS_TESTNET = '0x1e02b2953adefec225cf0ec49805b1146a4429c1';
2223

24+
export const VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET = '0x00000000000000000000000000005374616B6572';
25+
export const VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_MAINNET = '0x00000000000000000000000000005374616B6572';
26+
2327
export const AVG_GAS_UNITS = '21000';
2428
export const EXPIRATION = 400;
2529
export const GAS_PRICE_COEF = '128';

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export interface VetTransactionData {
3333
autorenew?: boolean; // Autorenew flag for stakeAndDelegate method
3434
nftCollectionId?: string;
3535
validatorAddress?: string;
36+
stakingPeriod?: number;
3637
}
3738

3839
export interface VetTransactionExplanation extends BaseTransactionExplanation {

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ export { ExitDelegationTransaction } from './transaction/exitDelegation';
1414
export { BurnNftTransaction } from './transaction/burnNftTransaction';
1515
export { ClaimRewardsTransaction } from './transaction/claimRewards';
1616
export { NFTTransaction } from './transaction/nftTransaction';
17+
export { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
1718
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
1819
export { TransferBuilder } from './transactionBuilder/transferBuilder';
1920
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
@@ -25,5 +26,6 @@ export { NFTTransactionBuilder } from './transactionBuilder/nftTransactionBuilde
2526
export { BurnNftBuilder } from './transactionBuilder/burnNftBuilder';
2627
export { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilder';
2728
export { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
29+
export { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
2830
export { TransactionBuilderFactory } from './transactionBuilderFactory';
2931
export { Constants, Utils, Interface };
Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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 ValidatorRegistrationTransaction extends Transaction {
13+
private _stakingContractAddress: string;
14+
private _validator: string;
15+
private _stakingPeriod: number;
16+
17+
constructor(_coinConfig: Readonly<CoinConfig>) {
18+
super(_coinConfig);
19+
this._type = TransactionType.StakingDelegate;
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 stakingPeriod(): number {
31+
return this._stakingPeriod;
32+
}
33+
34+
set stakingPeriod(period: number) {
35+
this._stakingPeriod = period;
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+
utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
56+
const addValidationData = this.getAddValidationClauseData(this.validator, this.stakingPeriod);
57+
this._transactionData = addValidationData;
58+
// Create the clause for delegation
59+
this._clauses = [
60+
{
61+
to: this.stakingContractAddress,
62+
value: ZERO_VALUE_AMOUNT,
63+
data: addValidationData,
64+
},
65+
];
66+
67+
// Set recipients based on the clauses
68+
this._recipients = [
69+
{
70+
address: this.stakingContractAddress,
71+
amount: ZERO_VALUE_AMOUNT,
72+
},
73+
];
74+
}
75+
76+
/**
77+
* Encodes addValidation transaction data using ethereumjs-abi for addValidation method
78+
* @param {string} validator - address of the validator
79+
* @param {number} period - staking period, denoted in blocks, that the Validator commits to hard
80+
locking their VET into the built-in staker contract. Allowed values are 60480 (7 days),
81+
129600 (15 days) or 259200 (30 days)
82+
* @returns {string} - The encoded transaction data
83+
*/
84+
getAddValidationClauseData(validator: string, period: number): string {
85+
const methodName = 'addValidation';
86+
const types = ['address', 'uint32'];
87+
const params = [validator, new BN(period)];
88+
89+
const method = EthereumAbi.methodID(methodName, types);
90+
const args = EthereumAbi.rawEncode(types, params);
91+
92+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
93+
}
94+
95+
toJson(): VetTransactionData {
96+
const json: VetTransactionData = {
97+
id: this.id,
98+
chainTag: this.chainTag,
99+
blockRef: this.blockRef,
100+
expiration: this.expiration,
101+
gasPriceCoef: this.gasPriceCoef,
102+
gas: this.gas,
103+
dependsOn: this.dependsOn,
104+
nonce: this.nonce,
105+
data: this.transactionData,
106+
value: ZERO_VALUE_AMOUNT,
107+
sender: this.sender,
108+
to: this.stakingContractAddress,
109+
stakingContractAddress: this.stakingContractAddress,
110+
amountToStake: ZERO_VALUE_AMOUNT,
111+
validatorAddress: this.validator,
112+
stakingPeriod: this.stakingPeriod,
113+
};
114+
115+
return json;
116+
}
117+
118+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
119+
try {
120+
if (!signedTx || !signedTx.body) {
121+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
122+
}
123+
124+
// Store the raw transaction
125+
this.rawTransaction = signedTx;
126+
127+
// Set transaction body properties
128+
const body = signedTx.body;
129+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
130+
this.blockRef = body.blockRef || '0x0';
131+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
132+
this.clauses = body.clauses || [];
133+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
134+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
135+
this.dependsOn = body.dependsOn || null;
136+
this.nonce = String(body.nonce);
137+
138+
// Set validator registration-specific properties
139+
if (body.clauses.length > 0) {
140+
// Get the addValidation clause
141+
const addValidationClause = body.clauses[0];
142+
if (addValidationClause.to) {
143+
this.stakingContractAddress = addValidationClause.to;
144+
}
145+
146+
// Extract validator and period from addValidation data
147+
if (addValidationClause.data) {
148+
this.transactionData = addValidationClause.data;
149+
const decoded = utils.decodeAddValidationData(addValidationClause.data);
150+
this.validator = decoded.validator;
151+
this.stakingPeriod = decoded.period;
152+
}
153+
}
154+
155+
// Set recipients from clauses
156+
this.recipients = body.clauses.map((clause) => ({
157+
address: (clause.to || '0x0').toString().toLowerCase(),
158+
amount: new BigNumber(clause.value || 0).toString(),
159+
}));
160+
this.loadInputsAndOutputs();
161+
162+
// Set sender address
163+
if (signedTx.signature && signedTx.origin) {
164+
this.sender = signedTx.origin.toString().toLowerCase();
165+
}
166+
167+
// Set signatures if present
168+
if (signedTx.signature) {
169+
// First signature is sender's signature
170+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
171+
172+
// If there's additional signature data, it's the fee payer's signature
173+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
174+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
175+
}
176+
}
177+
} catch (e) {
178+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
179+
}
180+
}
181+
}
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
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+
6+
import { TransactionBuilder } from './transactionBuilder';
7+
import { Transaction } from '../transaction/transaction';
8+
import { ValidatorRegistrationTransaction } from '../transaction/validatorRegistrationTransaction';
9+
import utils from '../utils';
10+
11+
export class ValidatorRegistrationBuilder extends TransactionBuilder {
12+
/**
13+
* Creates a new add validation Clause txn instance.
14+
*
15+
* @param {Readonly<CoinConfig>} _coinConfig - The coin configuration object
16+
*/
17+
constructor(_coinConfig: Readonly<CoinConfig>) {
18+
super(_coinConfig);
19+
this._transaction = new ValidatorRegistrationTransaction(_coinConfig);
20+
}
21+
22+
/**
23+
* Initializes the builder with an existing validation registration txn.
24+
*
25+
* @param {ValidatorRegistrationTransaction} tx - The transaction to initialize the builder with
26+
*/
27+
initBuilder(tx: ValidatorRegistrationTransaction): void {
28+
this._transaction = tx;
29+
}
30+
31+
/**
32+
* Gets the staking transaction instance.
33+
*
34+
* @returns {ValidatorRegistrationTransaction} The validator registration transaction
35+
*/
36+
get validatorRegistrationTransaction(): ValidatorRegistrationTransaction {
37+
return this._transaction as ValidatorRegistrationTransaction;
38+
}
39+
40+
/**
41+
* Gets the transaction type for validator registration.
42+
*
43+
* @returns {TransactionType} The transaction type
44+
*/
45+
protected get transactionType(): TransactionType {
46+
return TransactionType.ValidatorRegistration;
47+
}
48+
49+
/**
50+
* Validates the transaction clauses for validator registration transaction.
51+
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
52+
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
53+
*/
54+
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
55+
try {
56+
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
57+
return false;
58+
}
59+
60+
const clause = clauses[0];
61+
if (!clause.to || !utils.isValidAddress(clause.to)) {
62+
return false;
63+
}
64+
65+
return true;
66+
} catch (e) {
67+
return false;
68+
}
69+
}
70+
71+
/**
72+
* Sets the staking contract address for this validator registration tx.
73+
* The address must be explicitly provided to ensure the correct contract is used.
74+
*
75+
* @param {string} address - The contract address (required)
76+
* @returns {ValidatorRegistrationBuilder} This transaction builder
77+
* @throws {Error} If no address is provided
78+
*/
79+
stakingContractAddress(address: string): this {
80+
if (!address) {
81+
throw new Error('Staking contract address is required');
82+
}
83+
this.validateAddress({ address });
84+
this.validatorRegistrationTransaction.stakingContractAddress = address;
85+
return this;
86+
}
87+
88+
/**
89+
* Sets the staking period for this validator registration tx.
90+
*
91+
* @param {number} period - The staking period
92+
* @returns {ValidatorRegistrationBuilder} This transaction builder
93+
*/
94+
stakingPeriod(period: number): this {
95+
this.validatorRegistrationTransaction.stakingPeriod = period;
96+
return this;
97+
}
98+
99+
/**
100+
* Sets the validator address for this validator registration tx.
101+
* @param {string} address - The validator address
102+
* @returns {ValidatorRegistrationBuilder} This transaction builder
103+
*/
104+
validator(address: string): this {
105+
if (!address) {
106+
throw new Error('Validator address is required');
107+
}
108+
this.validateAddress({ address });
109+
this.validatorRegistrationTransaction.validator = address;
110+
return this;
111+
}
112+
113+
/**
114+
* Sets the transaction data for this validator registration tx.
115+
*
116+
* @param {string} data - The transaction data
117+
* @returns {ValidatorRegistrationBuilder} This transaction builder
118+
*/
119+
transactionData(data: string): this {
120+
this.validatorRegistrationTransaction.transactionData = data;
121+
return this;
122+
}
123+
124+
/** @inheritdoc */
125+
validateTransaction(transaction?: ValidatorRegistrationTransaction): void {
126+
if (!transaction) {
127+
throw new Error('transaction not defined');
128+
}
129+
assert(transaction.stakingContractAddress, 'Staking contract address is required');
130+
131+
assert(transaction.stakingPeriod, 'Staking period is required');
132+
assert(transaction.validator, 'Validator address is required');
133+
this.validateAddress({ address: transaction.stakingContractAddress });
134+
}
135+
136+
/** @inheritdoc */
137+
protected async buildImplementation(): Promise<Transaction> {
138+
this.transaction.type = this.transactionType;
139+
await this.validatorRegistrationTransaction.build();
140+
return this.transaction;
141+
}
142+
}

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

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ import { StakeClauseTransaction } from './transaction/stakeClauseTransaction';
2525
import { StakeClauseTxnBuilder } from './transactionBuilder/stakeClauseTxnBuilder';
2626
import { DelegateTxnBuilder } from './transactionBuilder/delegateTxnBuilder';
2727
import { DelegateClauseTransaction } from './transaction/delegateClauseTransaction';
28+
import { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
29+
import { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
2830

2931
export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
3032
constructor(_coinConfig: Readonly<CoinConfig>) {
@@ -81,6 +83,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
8183
const claimRewardsTx = new ClaimRewardsTransaction(this._coinConfig);
8284
claimRewardsTx.fromDeserializedSignedTransaction(signedTx);
8385
return this.getClaimRewardsBuilder(claimRewardsTx);
86+
case TransactionType.ValidatorRegistration:
87+
const validatorRegistrationTx = new ValidatorRegistrationTransaction(this._coinConfig);
88+
validatorRegistrationTx.fromDeserializedSignedTransaction(signedTx);
89+
return this.getValidatorRegistrationBuilder(validatorRegistrationTx);
8490
default:
8591
throw new InvalidTransactionError('Invalid transaction type');
8692
}
@@ -114,6 +120,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
114120
return this.initializeBuilder(tx, new DelegateTxnBuilder(this._coinConfig));
115121
}
116122

123+
getValidatorRegistrationBuilder(tx?: ValidatorRegistrationTransaction): ValidatorRegistrationBuilder {
124+
return this.initializeBuilder(tx, new ValidatorRegistrationBuilder(this._coinConfig));
125+
}
126+
117127
getStakingActivateBuilder(tx?: StakeClauseTransaction): StakeClauseTxnBuilder {
118128
return this.initializeBuilder(tx, new StakeClauseTxnBuilder(this._coinConfig));
119129
}

0 commit comments

Comments
 (0)