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 @@ -9,6 +9,7 @@ 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 DECREASE_STAKE_METHOD_ID = '0x1a73ba01';
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 @@ -16,6 +16,7 @@ export { ClaimRewardsTransaction } from './transaction/claimRewards';
export { NFTTransaction } from './transaction/nftTransaction';
export { ValidatorRegistrationTransaction } from './transaction/validatorRegistrationTransaction';
export { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
export { DecreaseStakeTransaction } from './transaction/decreaseStakeTransaction';
export { TransactionBuilder } from './transactionBuilder/transactionBuilder';
export { TransferBuilder } from './transactionBuilder/transferBuilder';
export { AddressInitializationBuilder } from './transactionBuilder/addressInitializationBuilder';
Expand All @@ -29,5 +30,6 @@ export { ExitDelegationBuilder } from './transactionBuilder/exitDelegationBuilde
export { ClaimRewardsBuilder } from './transactionBuilder/claimRewardsBuilder';
export { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
export { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
export { DecreaseStakeBuilder } from './transactionBuilder/decreaseStakeBuilder';
export { TransactionBuilderFactory } from './transactionBuilderFactory';
export { Constants, Utils, Interface };
162 changes: 162 additions & 0 deletions modules/sdk-coin-vet/src/lib/transaction/decreaseStakeTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
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 DecreaseStakeTransaction extends Transaction {
private _stakingContractAddress: string;
private _validator: string;
private _amount: string;

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

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

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

get amount(): string {
return this._amount;
}

set amount(value: string) {
this._amount = value;
}

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.amount) {
throw new Error('Amount is not set');
}

utils.validateContractAddressForValidatorRegistration(this.stakingContractAddress, this._coinConfig);
const decreaseStakeData = this.getDecreaseStakeClauseData(this.validator, this.amount);
this._transactionData = decreaseStakeData;
this._clauses = [
{
to: this.stakingContractAddress,
value: ZERO_VALUE_AMOUNT,
data: decreaseStakeData,
},
];

this._recipients = [
{
address: this.stakingContractAddress,
amount: ZERO_VALUE_AMOUNT,
},
];
}

getDecreaseStakeClauseData(validator: string, amount: string): string {
const methodName = 'decreaseStake';
const types = ['address', 'uint256'];
const params = [validator, new BN(amount)];

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

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

toJson(): VetTransactionData {
return {
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: this.amount,
validatorAddress: this.validator,
};
}

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

this.rawTransaction = signedTx;

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

if (body.clauses.length > 0) {
const clause = body.clauses[0];
if (clause.to) {
this.stakingContractAddress = clause.to;
}

if (clause.data) {
this.transactionData = clause.data;
const decoded = utils.decodeDecreaseStakeData(clause.data);
this.validator = decoded.validator;
this.amount = decoded.amount;
}
}

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

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

if (signedTx.signature) {
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));

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 @@ -395,7 +395,8 @@ export class Transaction extends BaseTransaction {
this.type === TransactionType.StakingWithdraw ||
this.type === TransactionType.StakingClaim ||
this.type === TransactionType.StakingLock ||
this.type === TransactionType.StakingAdd
this.type === TransactionType.StakingAdd ||
this.type === TransactionType.StakingDeactivate
) {
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,98 @@
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 { DecreaseStakeTransaction } from '../transaction/decreaseStakeTransaction';
import utils from '../utils';

export class DecreaseStakeBuilder extends TransactionBuilder {
constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
this._transaction = new DecreaseStakeTransaction(_coinConfig);
}

initBuilder(tx: DecreaseStakeTransaction): void {
this._transaction = tx;
}

get decreaseStakeTransaction(): DecreaseStakeTransaction {
return this._transaction as DecreaseStakeTransaction;
}

protected get transactionType(): TransactionType {
return TransactionType.StakingDeactivate;
}

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

stakingContractAddress(address: string): this {
if (!address) {
throw new Error('Staking contract address is required');
}
this.validateAddress({ address });
this.decreaseStakeTransaction.stakingContractAddress = address;
return this;
}

amount(value: string): this {
this.decreaseStakeTransaction.amount = value;
return this;
}

validator(address: string): this {
if (!address) {
throw new Error('Validator address is required');
}
this.validateAddress({ address });
this.decreaseStakeTransaction.validator = address;
return this;
}

transactionData(data: string): this {
this.decreaseStakeTransaction.transactionData = data;
return this;
}

/** @inheritdoc */
validateTransaction(transaction?: DecreaseStakeTransaction): 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.amount, 'Amount is required');

const amt = new BigNumber(transaction.amount);
if (amt.isLessThanOrEqualTo(0)) {
throw new Error('Amount must be greater than 0');
}

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

/** @inheritdoc */
protected async buildImplementation(): Promise<Transaction> {
this.transaction.type = this.transactionType;
await this.decreaseStakeTransaction.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 @@ -29,6 +29,8 @@ import { ValidatorRegistrationTransaction } from './transaction/validatorRegistr
import { ValidatorRegistrationBuilder } from './transactionBuilder/validatorRegistrationBuilder';
import { IncreaseStakeTransaction } from './transaction/increaseStakeTransaction';
import { IncreaseStakeBuilder } from './transactionBuilder/increaseStakeBuilder';
import { DecreaseStakeTransaction } from './transaction/decreaseStakeTransaction';
import { DecreaseStakeBuilder } from './transactionBuilder/decreaseStakeBuilder';

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
constructor(_coinConfig: Readonly<CoinConfig>) {
Expand Down Expand Up @@ -93,6 +95,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
const increaseStakeTx = new IncreaseStakeTransaction(this._coinConfig);
increaseStakeTx.fromDeserializedSignedTransaction(signedTx);
return this.getIncreaseStakeBuilder(increaseStakeTx);
case TransactionType.StakingDeactivate:
const decreaseStakeTx = new DecreaseStakeTransaction(this._coinConfig);
decreaseStakeTx.fromDeserializedSignedTransaction(signedTx);
return this.getDecreaseStakeBuilder(decreaseStakeTx);
default:
throw new InvalidTransactionError('Invalid transaction type');
}
Expand Down Expand Up @@ -134,6 +140,10 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
return this.initializeBuilder(tx, new IncreaseStakeBuilder(this._coinConfig));
}

getDecreaseStakeBuilder(tx?: DecreaseStakeTransaction): DecreaseStakeBuilder {
return this.initializeBuilder(tx, new DecreaseStakeBuilder(this._coinConfig));
}

getStakingActivateBuilder(tx?: StakeClauseTransaction): StakeClauseTxnBuilder {
return this.initializeBuilder(tx, new StakeClauseTxnBuilder(this._coinConfig));
}
Expand Down
24 changes: 24 additions & 0 deletions modules/sdk-coin-vet/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import {
VALIDATOR_REGISTRATION_STAKER_CONTRACT_ADDRESS_TESTNET,
ADD_VALIDATION_METHOD_ID,
INCREASE_STAKE_METHOD_ID,
DECREASE_STAKE_METHOD_ID,
} from './constants';
import { KeyPair } from './keyPair';
import { BaseCoin as CoinConfig } from '@bitgo/statics';
Expand Down Expand Up @@ -107,6 +108,8 @@ export class Utils implements BaseUtils {
return TransactionType.StakingLock;
} else if (clauses[0].data.startsWith(INCREASE_STAKE_METHOD_ID)) {
return TransactionType.StakingAdd;
} else if (clauses[0].data.startsWith(DECREASE_STAKE_METHOD_ID)) {
return TransactionType.StakingDeactivate;
} else if (clauses[0].data.startsWith(BURN_NFT_METHOD_ID)) {
return TransactionType.StakingWithdraw;
} else if (
Expand Down Expand Up @@ -308,6 +311,27 @@ export class Utils implements BaseUtils {
}
}

/**
* Decodes decreaseStake transaction data to extract validator address and amount
*
* @param {string} data - The encoded transaction data
* @returns {object} - Object containing validator address and amount
*/
decodeDecreaseStakeData(data: string): { validator: string; amount: string } {
try {
const parameters = data.slice(10);

const decoded = EthereumAbi.rawDecode(['address', 'uint256'], Buffer.from(parameters, 'hex'));

return {
validator: addHexPrefix(decoded[0].toString()).toLowerCase(),
amount: decoded[1].toString(),
};
} catch (error) {
throw new Error(`Failed to decode decrease stake data: ${error.message}`);
}
}

/**
* Decodes exit delegation transaction data to extract tokenId
*
Expand Down
Loading
Loading