Skip to content

Commit 95dba4d

Browse files
Merge pull request #7737 from BitGo/SC-4409
feat(sdk-coin-vet): add support for validator registration
2 parents cc0e1b6 + b8a4f43 commit 95dba4d

File tree

9 files changed

+606
-2
lines changed

9 files changed

+606
-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: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
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.StakingLock;
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+
if (!this.stakingPeriod) {
56+
throw new Error('Staking period is not set');
57+
}
58+
59+
utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
60+
const addValidationData = this.getAddValidationClauseData(this.validator, this.stakingPeriod);
61+
this._transactionData = addValidationData;
62+
// Create the clause for delegation
63+
this._clauses = [
64+
{
65+
to: this.stakingContractAddress,
66+
value: ZERO_VALUE_AMOUNT,
67+
data: addValidationData,
68+
},
69+
];
70+
71+
// Set recipients based on the clauses
72+
this._recipients = [
73+
{
74+
address: this.stakingContractAddress,
75+
amount: ZERO_VALUE_AMOUNT,
76+
},
77+
];
78+
}
79+
80+
/**
81+
* Encodes addValidation transaction data using ethereumjs-abi for addValidation method
82+
* @param {string} validator - address of the validator
83+
* @param {number} period - staking period, denoted in blocks, that the Validator commits to hard
84+
locking their VET into the built-in staker contract. Allowed values are 60480 (7 days),
85+
129600 (15 days) or 259200 (30 days)
86+
* @returns {string} - The encoded transaction data
87+
*/
88+
getAddValidationClauseData(validator: string, period: number): string {
89+
const methodName = 'addValidation';
90+
const types = ['address', 'uint32'];
91+
const params = [validator, new BN(period)];
92+
93+
const method = EthereumAbi.methodID(methodName, types);
94+
const args = EthereumAbi.rawEncode(types, params);
95+
96+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
97+
}
98+
99+
toJson(): VetTransactionData {
100+
const json: VetTransactionData = {
101+
id: this.id,
102+
chainTag: this.chainTag,
103+
blockRef: this.blockRef,
104+
expiration: this.expiration,
105+
gasPriceCoef: this.gasPriceCoef,
106+
gas: this.gas,
107+
dependsOn: this.dependsOn,
108+
nonce: this.nonce,
109+
data: this.transactionData,
110+
value: ZERO_VALUE_AMOUNT,
111+
sender: this.sender,
112+
to: this.stakingContractAddress,
113+
stakingContractAddress: this.stakingContractAddress,
114+
amountToStake: ZERO_VALUE_AMOUNT,
115+
validatorAddress: this.validator,
116+
stakingPeriod: this.stakingPeriod,
117+
};
118+
119+
return json;
120+
}
121+
122+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
123+
try {
124+
if (!signedTx || !signedTx.body) {
125+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
126+
}
127+
128+
// Store the raw transaction
129+
this.rawTransaction = signedTx;
130+
131+
// Set transaction body properties
132+
const body = signedTx.body;
133+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
134+
this.blockRef = body.blockRef || '0x0';
135+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
136+
this.clauses = body.clauses || [];
137+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
138+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
139+
this.dependsOn = body.dependsOn || null;
140+
this.nonce = String(body.nonce);
141+
142+
// Set validator registration-specific properties
143+
if (body.clauses.length > 0) {
144+
// Get the addValidation clause
145+
const addValidationClause = body.clauses[0];
146+
if (addValidationClause.to) {
147+
this.stakingContractAddress = addValidationClause.to;
148+
}
149+
150+
// Extract validator and period from addValidation data
151+
if (addValidationClause.data) {
152+
this.transactionData = addValidationClause.data;
153+
const decoded = utils.decodeAddValidationData(addValidationClause.data);
154+
this.validator = decoded.validator;
155+
this.stakingPeriod = decoded.period;
156+
}
157+
}
158+
159+
// Set recipients from clauses
160+
this.recipients = body.clauses.map((clause) => ({
161+
address: (clause.to || '0x0').toString().toLowerCase(),
162+
amount: new BigNumber(clause.value || 0).toString(),
163+
}));
164+
this.loadInputsAndOutputs();
165+
166+
// Set sender address
167+
if (signedTx.signature && signedTx.origin) {
168+
this.sender = signedTx.origin.toString().toLowerCase();
169+
}
170+
171+
// Set signatures if present
172+
if (signedTx.signature) {
173+
// First signature is sender's signature
174+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
175+
176+
// If there's additional signature data, it's the fee payer's signature
177+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
178+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
179+
}
180+
}
181+
} catch (e) {
182+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
183+
}
184+
}
185+
}
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.StakingLock;
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+
}

0 commit comments

Comments
 (0)