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
1 change: 1 addition & 0 deletions modules/sdk-coin-vet/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ 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 INCREASE_STAKE_METHOD_ID = '0x43b0de9a';
export const EXIT_DELEGATION_METHOD_ID = '0x69e79b7d';
export const BURN_NFT_METHOD_ID = '0x2e17de78';
export const TRANSFER_NFT_METHOD_ID = '0x23b872dd';
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 @@ -15,6 +15,7 @@ export { BurnNftTransaction } from './transaction/burnNftTransaction';
export { ClaimRewardsTransaction } from './transaction/claimRewards';
export { NFTTransaction } from './transaction/nftTransaction';
export { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
export { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
export { TransferBuilder } from './transactionBuilder/transferBuilder';
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
Expand All @@ -27,5 +28,6 @@ export { BurnNftBuilder } from './transactionBuilder/burnNftBuilder';
export { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilder';
export { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
export { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
export { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { Constants, Utils, Interface };
178 changes: 178 additions & 0 deletions modules/sdk-coin-vet/src/lib/transaction/increaseStakeTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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 } from 'ethereumjs-util';

export class IncreaseStakeTransaction extends Transaction {
private _stakingContractAddress: string;
private _validator: string;
private _amountToStake: string;

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

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

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

get amountToStake(): string {
return this._amountToStake;
}

set amountToStake(amount: string) {
this._amountToStake = amount;
}

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');
}

utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
const increaseStakeData = this.getIncreaseStakeClauseData(this.validator);
this._transactionData = increaseStakeData;
// Create the clause for increase stake
this._clauses = [
{
to: this.stakingContractAddress,
value: this.amountToStake,
data: increaseStakeData,
},
];

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

/**
* Encodes increaseStake transaction data using ethereumjs-abi for increaseStake method
* @param {string} validator - address of the validator
* @returns {string} - The encoded transaction data
*/
getIncreaseStakeClauseData(validator: string): string {
const methodName = 'increaseStake';
const types = ['address'];
const params = [validator];

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: this.amountToStake,
sender: this.sender,
to: this.stakingContractAddress,
stakingContractAddress: this.stakingContractAddress,
amountToStake: this.amountToStake,
validatorAddress: this.validator,
};

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 increase stake-specific properties
if (body.clauses.length > 0) {
const increaseStakeClause = body.clauses[0];
if (increaseStakeClause.to) {
this.stakingContractAddress = increaseStakeClause.to;
}

if (increaseStakeClause.value) {
this.amountToStake = new BigNumber(increaseStakeClause.value).toFixed();
}

// Extract validator from increaseStake data
if (increaseStakeClause.data) {
this.transactionData = increaseStakeClause.data;
const decoded = utils.decodeIncreaseStakeData(increaseStakeClause.data);
this.validator = decoded.validator;
}
}

// 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}`);
}
}
}
3 changes: 2 additions & 1 deletion modules/sdk-coin-vet/src/lib/transaction/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -394,7 +394,8 @@ export class Transaction extends BaseTransaction {
this.type === TransactionType.StakingUnlock ||
this.type === TransactionType.StakingWithdraw ||
this.type === TransactionType.StakingClaim ||
this.type === TransactionType.StakingLock
this.type === TransactionType.StakingLock ||
this.type === TransactionType.StakingAdd
) {
transactionBody.reserved = {
features: 1, // mark transaction as delegated i.e. will use gas payer
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import assert from 'assert';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
import { TransactionType } from '@bitgo/sdk-core';
import { TransactionClause } from '@vechain/sdk-core';
import BigNumber from 'bignumber.js';

import { TransactionBuilder } from './transactionBuilder';
import { Transaction } from '../transaction/transaction';
import { IncreaseStakeTransaction } from '../transaction/increaseStakeTransaction';
import utils from '../utils';

export class IncreaseStakeBuilder extends TransactionBuilder {
/**
* Creates a new increase stake txn instance.
*
* @param {Readonly<CoinConfig>} _coinConfig - The coin configuration object
*/
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new IncreaseStakeTransaction(_coinConfig);
}

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

/**
* Gets the increase stake transaction instance.
*
* @returns {IncreaseStakeTransaction} The increase stake transaction
*/
get increaseStakeTransaction(): IncreaseStakeTransaction {
return this._transaction as IncreaseStakeTransaction;
}

/**
* Gets the transaction type for increase stake.
*
* @returns {TransactionType} The transaction type
*/
protected get transactionType(): TransactionType {
return TransactionType.StakingAdd;
}

/**
* Validates the transaction clauses for increase stake 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 increase stake tx.
* The address must be explicitly provided to ensure the correct contract is used.
*
* @param {string} address - The contract address (required)
* @returns {IncreaseStakeBuilder} 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.increaseStakeTransaction.stakingContractAddress = address;
return this;
}

/**
* Sets the amount to stake for this increase stake tx (VET amount being sent).
*
* @param {string} amount - The amount to stake in wei
* @returns {IncreaseStakeBuilder} This transaction builder
*/
amountToStake(amount: string): this {
this.increaseStakeTransaction.amountToStake = amount;
return this;
}

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

/**
* Sets the transaction data for this increase stake tx.
*
* @param {string} data - The transaction data
* @returns {IncreaseStakeBuilder} This transaction builder
*/
transactionData(data: string): this {
this.increaseStakeTransaction.transactionData = data;
return this;
}

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

// Validate staking amount is greater than 0
const amount = new BigNumber(transaction.amountToStake);
if (amount.isLessThanOrEqualTo(0)) {
throw new Error('Staking amount must be greater than 0');
}

this.validateAddress({ address: transaction.stakingContractAddress });
}

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this.transaction.type = this.transactionType;
await this.increaseStakeTransaction.build();
return this.transaction;
}
}
10 changes: 10 additions & 0 deletions modules/sdk-coin-vet/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ import { DelegateTxnBuilder } from './transactionBuilder/delegateTxnBuilder';
import { DelegateClauseTransaction } from './transaction/delegateClauseTransaction';
import { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
import { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
import { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
import { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down Expand Up @@ -87,6 +89,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
const validatorRegistrationTx = new ValidatorRegistrationTransaction(this._coinConfig);
validatorRegistrationTx.fromDeserializedSignedTransaction(signedTx);
return this.getValidatorRegistrationBuilder(validatorRegistrationTx);
case TransactionType.StakingAdd:
const increaseStakeTx = new IncreaseStakeTransaction(this._coinConfig);
increaseStakeTx.fromDeserializedSignedTransaction(signedTx);
return this.getIncreaseStakeBuilder(increaseStakeTx);
default:
throw new InvalidTransactionError('Invalid transaction type');
}
Expand Down Expand Up @@ -124,6 +130,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new ValidatorRegistrationBuilder(this._coinConfig));
}

getIncreaseStakeBuilder(tx?: IncreaseStakeTransaction): IncreaseStakeBuilder {
return this.initializeBuilder(tx, new IncreaseStakeBuilder(this._coinConfig));
}

getStakingActivateBuilder(tx?: StakeClauseTransaction): StakeClauseTxnBuilder {
return this.initializeBuilder(tx, new StakeClauseTxnBuilder(this._coinConfig));
}
Expand Down
Loading