Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions modules/sdk-coin-vet/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb';
export const STAKING_METHOD_ID = '0xd8da3bbf';
export const STAKE_CLAUSE_METHOD_ID = '0x604f2177';
export const DELEGATE_CLAUSE_METHOD_ID = '0x08bbb824';
export const ADD_VALIDATION_METHOD_ID = '0xc3c4b138';
export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d';
export const BURN_NFT_METHOD_ID = '0x2e17de78';
export const TRANSFER_NFT_METHOD_ID = '0x23b872dd';
Expand All @@ -20,6 +21,9 @@ export const STARGATE_DELEGATION_ADDRESS_TESTNET = '0x7240e3bc0d26431512d5b67dbd
export const STARGATE_NFT_ADDRESS_TESTNET = '0x887d9102f0003f1724d8fd5d4fe95a11572fcd77';
export const STARGATE_CONTRACT_ADDRESS_TESTNET = '0x1e02b2953adefec225cf0ec49805b1146a4429c1';

export const VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET = '0x00000000000000000000000000005374616B6572';
export const VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_MAINNET = '0x00000000000000000000000000005374616B6572';

export const AVG_GAS_UNITS = '21000';
export const EXPIRATION = 400;
export const GAS_PRICE_COEF = '128';
Expand Down
1 change: 1 addition & 0 deletions modules/sdk-coin-vet/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export interface VetTransactionData {
autorenew?: boolean; // Autorenew flag for stakeAndDelegate method
nftCollectionId?: string;
validatorAddress?: string;
stakingPeriod?: number;
}

export interface VetTransactionExplanation extends BaseTransactionExplanation {
Expand Down
2 changes: 2 additions & 0 deletions modules/sdk-coin-vet/src/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { ExitDelegationTransaction } from './transaction/exitDelegation';
export { BurnNftTransaction } from './transaction/burnNftTransaction';
export { ClaimRewardsTransaction } from './transaction/claimRewards';
export { NFTTransaction } from './transaction/nftTransaction';
export { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
export { TransferBuilder } from './transactionBuilder/transferBuilder';
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
Expand All @@ -25,5 +26,6 @@ export { NFTTransactionBuilder } from './transactionBuilder/nftTransactionBuilde
export { BurnNftBuilder } from './transactionBuilder/burnNftBuilder';
export { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilder';
export { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
export { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { Constants, Utils, Interface };
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { Transaction as VetTransaction, Secp256k1 } from '@vechain/sdk-core';
import { Transaction } from './transaction';
import { VetTransactionData } from '../iface';
import EthereumAbi from 'ethereumjs-abi';
import utils from '../utils';
import BigNumber from 'bignumber.js';
import { addHexPrefix, BN } from 'ethereumjs-util';
import { ZERO_VALUE_AMOUNT } from '../constants';

export class ValidatorRegistrationTransaction extends Transaction {
private _stakingContractAddress: string;
private _validator: string;
private _stakingPeriod: number;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._type = TransactionType.StakingLock;
}

get validator(): string {
return this._validator;
}

set validator(address: string) {
this._validator = address;
}

get stakingPeriod(): number {
return this._stakingPeriod;
}

set stakingPeriod(period: number) {
this._stakingPeriod = period;
}

get stakingContractAddress(): string {
return this._stakingContractAddress;
}

set stakingContractAddress(address: string) {
this._stakingContractAddress = address;
}

buildClauses(): void {
if (!this.stakingContractAddress) {
throw new Error('Staking contract address is not set');
}

if (!this.validator) {
throw new Error('Validator address is not set');
}

if (!this.stakingPeriod) {
throw new Error('Staking period is not set');
}

utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
const addValidationData = this.getAddValidationClauseData(this.validator, this.stakingPeriod);
this._transactionData = addValidationData;
// Create the clause for delegation
this._clauses = [
{
to: this.stakingContractAddress,
value: ZERO_VALUE_AMOUNT,
data: addValidationData,
},
];

// Set recipients based on the clauses
this._recipients = [
{
address: this.stakingContractAddress,
amount: ZERO_VALUE_AMOUNT,
},
];
}

/**
* Encodes addValidation transaction data using ethereumjs-abi for addValidation method
* @param {string} validator - address of the validator
* @param {number} period - staking period, denoted in blocks, that the Validator commits to hard
locking their VET into the built-in staker contract. Allowed values are 60480 (7 days),
129600 (15 days) or 259200 (30 days)
* @returns {string} - The encoded transaction data
*/
getAddValidationClauseData(validator: string, period: number): string {
const methodName = 'addValidation';
const types = ['address', 'uint32'];
const params = [validator, new BN(period)];

const method = EthereumAbi.methodID(methodName, types);
const args = EthereumAbi.rawEncode(types, params);

return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
}

toJson(): VetTransactionData {
const json: VetTransactionData = {
id: this.id,
chainTag: this.chainTag,
blockRef: this.blockRef,
expiration: this.expiration,
gasPriceCoef: this.gasPriceCoef,
gas: this.gas,
dependsOn: this.dependsOn,
nonce: this.nonce,
data: this.transactionData,
value: ZERO_VALUE_AMOUNT,
sender: this.sender,
to: this.stakingContractAddress,
stakingContractAddress: this.stakingContractAddress,
amountToStake: ZERO_VALUE_AMOUNT,
validatorAddress: this.validator,
stakingPeriod: this.stakingPeriod,
};

return json;
}

fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
try {
if (!signedTx || !signedTx.body) {
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
}

// Store the raw transaction
this.rawTransaction = signedTx;

// Set transaction body properties
const body = signedTx.body;
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
this.blockRef = body.blockRef || '0x0';
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
this.clauses = body.clauses || [];
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
this.dependsOn = body.dependsOn || null;
this.nonce = String(body.nonce);

// Set validator registration-specific properties
if (body.clauses.length > 0) {
// Get the addValidation clause
const addValidationClause = body.clauses[0];
if (addValidationClause.to) {
this.stakingContractAddress = addValidationClause.to;
}

// Extract validator and period from addValidation data
if (addValidationClause.data) {
this.transactionData = addValidationClause.data;
const decoded = utils.decodeAddValidationData(addValidationClause.data);
this.validator = decoded.validator;
this.stakingPeriod = decoded.period;
}
}

// Set recipients from clauses
this.recipients = body.clauses.map((clause) => ({
address: (clause.to || '0x0').toString().toLowerCase(),
amount: new BigNumber(clause.value || 0).toString(),
}));
this.loadInputsAndOutputs();

// Set sender address
if (signedTx.signature && signedTx.origin) {
this.sender = signedTx.origin.toString().toLowerCase();
}

// Set signatures if present
if (signedTx.signature) {
// First signature is sender's signature
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));

// If there's additional signature data, it's the fee payer's signature
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
}
}
} catch (e) {
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import assert from 'assert';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionType } from '@bitgo/sdk-core';
import { TransactionClause } from '@vechain/sdk-core';

import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from '../transaction/transaction';
import { ValidatorRegistrationTransaction } from '../transaction/validatorRegistrationTransaction';
import utils from '../utils';

export class ValidatorRegistrationBuilder extends TransactionBuilder {
/**
* Creates a new add validation Clause txn instance.
*
* @param {Readonly<CoinConfig>} _coinConfig - The coin configuration object
*/
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new ValidatorRegistrationTransaction(_coinConfig);
}

/**
* Initializes the builder with an existing validation registration txn.
*
* @param {ValidatorRegistrationTransaction} tx - The transaction to initialize the builder with
*/
initBuilder(tx: ValidatorRegistrationTransaction): void {
this._transaction = tx;
}

/**
* Gets the staking transaction instance.
*
* @returns {ValidatorRegistrationTransaction} The validator registration transaction
*/
get validatorRegistrationTransaction(): ValidatorRegistrationTransaction {
return this._transaction as ValidatorRegistrationTransaction;
}

/**
* Gets the transaction type for validator registration.
*
* @returns {TransactionType} The transaction type
*/
protected get transactionType(): TransactionType {
return TransactionType.StakingLock;
}

/**
* Validates the transaction clauses for validator registration transaction.
* @param {TransactionClause[]} clauses - The transaction clauses to validate.
* @returns {boolean} - Returns true if the clauses are valid, false otherwise.
*/
protected isValidTransactionClauses(clauses: TransactionClause[]): boolean {
try {
if (!clauses || !Array.isArray(clauses) || clauses.length === 0) {
return false;
}

const clause = clauses[0];
if (!clause.to || !utils.isValidAddress(clause.to)) {
return false;
}

return true;
} catch (e) {
return false;
}
}

/**
* Sets the staking contract address for this validator registration tx.
* The address must be explicitly provided to ensure the correct contract is used.
*
* @param {string} address - The contract address (required)
* @returns {ValidatorRegistrationBuilder} This transaction builder
* @throws {Error} If no address is provided
*/
stakingContractAddress(address: string): this {
if (!address) {
throw new Error('Staking contract address is required');
}
this.validateAddress({ address });
this.validatorRegistrationTransaction.stakingContractAddress = address;
return this;
}

/**
* Sets the staking period for this validator registration tx.
*
* @param {number} period - The staking period
* @returns {ValidatorRegistrationBuilder} This transaction builder
*/
stakingPeriod(period: number): this {
this.validatorRegistrationTransaction.stakingPeriod = period;
return this;
}

/**
* Sets the validator address for this validator registration tx.
* @param {string} address - The validator address
* @returns {ValidatorRegistrationBuilder} This transaction builder
*/
validator(address: string): this {
if (!address) {
throw new Error('Validator address is required');
}
this.validateAddress({ address });
this.validatorRegistrationTransaction.validator = address;
return this;
}

/**
* Sets the transaction data for this validator registration tx.
*
* @param {string} data - The transaction data
* @returns {ValidatorRegistrationBuilder} This transaction builder
*/
transactionData(data: string): this {
this.validatorRegistrationTransaction.transactionData = data;
return this;
}

/** @inheritdoc */
validateTransaction(transaction?: ValidatorRegistrationTransaction): void {
if (!transaction) {
throw new Error('transaction not defined');
}
assert(transaction.stakingContractAddress, 'Staking contract address is required');

assert(transaction.stakingPeriod, 'Staking period is required');
assert(transaction.validator, 'Validator address is required');
this.validateAddress({ address: transaction.stakingContractAddress });
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this.transaction.type = this.transactionType;
await this.validatorRegistrationTransaction.build();
return this.transaction;
}
}
Loading