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 @@ -3,6 +3,7 @@ export const VET_ADDRESS_LENGTH = 40;
export const VET_BLOCK_ID_LENGTH = 64;

export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb';
export const STAKING_METHOD_ID = '0xa694fc3a';
export const EXIT_DELEGATION_METHOD_ID = '0x32b7006d';
export const BURN_NFT_METHOD_ID = '0x42966c68';

Expand Down
36 changes: 36 additions & 0 deletions modules/sdk-coin-vet/src/lib/iface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,40 @@ import {
TransactionRecipient,
} from '@bitgo/sdk-core';

/**
* Interface for ABI input parameter
*/
export interface AbiInput {
internalType: string;
name: string;
type: string;
}

/**
* Interface for ABI output parameter
*/
export interface AbiOutput {
internalType?: string;
name?: string;
type: string;
}

/**
* Interface for ABI function definition
*/
export interface AbiFunction {
inputs: AbiInput[];
name: string;
outputs: AbiOutput[];
stateMutability: string;
type: string;
}

/**
* Type for contract ABI
*/
export type ContractAbi = AbiFunction[];

/**
* The transaction data returned from the toJson() function of a transaction
*/
Expand All @@ -25,6 +59,8 @@ export interface VetTransactionData {
to?: string;
tokenAddress?: string;
tokenId?: string; // Added for unstaking and burn NFT transactions
stakingContractAddress?: string;
amountToStake?: string;
}

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 @@ -7,9 +7,11 @@ export { Transaction } from './transaction/transaction';
export { AddressInitializationTransaction } from './transaction/addressInitializationTransaction';
export { FlushTokenTransaction } from './transaction/flushTokenTransaction';
export { TokenTransaction } from './transaction/tokenTransaction';
export { StakingTransaction } from './transaction/stakingTransaction';
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
export { TransferBuilder } from './transactionBuilder/transferBuilder';
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
export { FlushTokenTransactionBuilder } from './transactionBuilder/flushTokenTransactionBuilder';
export { StakingBuilder } from './transactionBuilder/stakingBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { Constants, Utils, Interface };
154 changes: 154 additions & 0 deletions modules/sdk-coin-vet/src/lib/transaction/stakingTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
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, ContractAbi } from '../iface';
import utils from '../utils';

export class StakingTransaction extends Transaction {
private _stakingContractAddress: string;
private _amountToStake: string;
private _stakingContractABI: ContractAbi;

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

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

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

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

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

get stakingContractABI(): ContractAbi {
return this._stakingContractABI;
}

set stakingContractABI(abi: ContractAbi) {
this._stakingContractABI = abi;
}

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

if (!this.amountToStake) {
throw new Error('Amount to stake is not set');
}

// Generate transaction data using ethereumjs-abi
const data = utils.getStakingData(this.amountToStake);
this._transactionData = data;

// Create the clause for staking
this._clauses = [
{
to: this.stakingContractAddress,
value: this.amountToStake,
data: this._transactionData,
},
];

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

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,
};

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 staking-specific properties
if (body.clauses.length > 0) {
const clause = body.clauses[0];
if (clause.to) {
this.stakingContractAddress = clause.to;
}
if (clause.value) {
this.amountToStake = String(clause.value);
}
if (clause.data) {
this.transactionData = clause.data;
}
}

// Set recipients from clauses
this.recipients = body.clauses.map((clause) => ({
address: (clause.to || '0x0').toString().toLowerCase(),
amount: String(clause.value || '0'),
}));
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}`);
}
}
}
140 changes: 140 additions & 0 deletions modules/sdk-coin-vet/src/lib/transactionBuilder/stakingBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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 { StakingTransaction } from '../transaction/stakingTransaction';
import { ContractAbi } from '../iface';
import utils from '../utils';

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

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

/**
* Gets the staking transaction instance.
*
* @returns {StakingTransaction} The staking transaction
*/
get stakingTransaction(): StakingTransaction {
return this._transaction as StakingTransaction;
}

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

/**
* Validates the transaction clauses for staking 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;
}

// For staking transactions, value must be greater than 0
if (!clause.value || clause.value === '0x0' || clause.value === '0') {
return false;
}

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

/**
* Sets the staking contract address for this staking tx.
*
* @param {string} address - The staking contract address
* @returns {StakingBuilder} This transaction builder
*/
stakingContractAddress(address: string): this {
this.validateAddress({ address });
this.stakingTransaction.stakingContractAddress = address;
return this;
}

/**
* Sets the amount to stake for this staking tx.
*
* @param {string} amount - The amount to stake in wei
* @returns {StakingBuilder} This transaction builder
*/
amountToStake(amount: string): this {
this.stakingTransaction.amountToStake = amount;
return this;
}

/**
* Sets the staking contract ABI for this staking tx.
*
* @param {ContractAbi} abi - The staking contract ABI
* @returns {StakingBuilder} This transaction builder
*/
stakingContractABI(abi: ContractAbi): this {
this.stakingTransaction.stakingContractABI = abi;
return this;
}

/**
* Sets the transaction data for this staking tx.
*
* @param {string} data - The transaction data
* @returns {StakingBuilder} This transaction builder
*/
transactionData(data: string): this {
this.stakingTransaction.transactionData = data;
return this;
}

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

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

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this.transaction.type = this.transactionType;
await this.stakingTransaction.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 @@ -15,6 +15,8 @@ import { ExitDelegationTransaction } from './transaction/exitDelegation';
import { BurnNftTransaction } from './transaction/burnNftTransaction';
import { TokenTransactionBuilder } from './transactionBuilder/tokenTransactionBuilder';
import { TokenTransaction } from './transaction/tokenTransaction';
import { StakingBuilder } from './transactionBuilder/stakingBuilder';
import { StakingTransaction } from './transaction/stakingTransaction';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down Expand Up @@ -43,6 +45,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
const tokenTransferTx = new TokenTransaction(this._coinConfig);
tokenTransferTx.fromDeserializedSignedTransaction(signedTx);
return this.getTokenTransactionBuilder(tokenTransferTx);
case TransactionType.ContractCall:
const stakingTx = new StakingTransaction(this._coinConfig);
stakingTx.fromDeserializedSignedTransaction(signedTx);
return this.getStakingBuilder(stakingTx);
case TransactionType.StakingUnlock:
const exitDelegationTx = new ExitDelegationTransaction(this._coinConfig);
exitDelegationTx.fromDeserializedSignedTransaction(signedTx);
Expand Down Expand Up @@ -76,6 +82,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new TokenTransactionBuilder(this._coinConfig));
}

getStakingBuilder(tx?: StakingTransaction): StakingBuilder {
return this.initializeBuilder(tx, new StakingBuilder(this._coinConfig));
}

/**
* Gets an exit delegation transaction builder.
*
Expand Down
Loading