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
5 changes: 5 additions & 0 deletions modules/sdk-coin-vet/src/lib/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,8 @@ export const VET_ADDRESS_LENGTH = 40;
export const VET_BLOCK_ID_LENGTH = 64;

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

export const STARGATE_NFT_ADDRESS = '0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7';
export const STARGATE_DELEGATION_ADDRESS = '0x4cb1c9ef05b529c093371264fab2c93cc6cddb0e';
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 @@ -24,6 +24,7 @@ export interface VetTransactionData {
deployedAddress?: string;
to?: string;
tokenAddress?: string;
tokenId?: string; // Added for unstaking and burn NFT transactions
}

export interface VetTransactionExplanation extends BaseTransactionExplanation {
Expand Down
137 changes: 137 additions & 0 deletions modules/sdk-coin-vet/src/lib/transaction/burnNftTransaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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 { BURN_NFT_METHOD_ID } from '../constants';
import EthereumAbi from 'ethereumjs-abi';
import { addHexPrefix } from 'ethereumjs-util';

export class BurnNftTransaction extends Transaction {
private _tokenId: string;

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

get tokenId(): string {
return this._tokenId;
}

set tokenId(id: string) {
this._tokenId = id;
}

/** @inheritdoc */
async build(): Promise<void> {
this.buildClauses();
await this.buildRawTransaction();
this.generateTxnIdAndSetSender();
this.loadInputsAndOutputs();
}

/** @inheritdoc */
buildClauses(): void {
if (!this._contract || !this._tokenId) {
throw new InvalidTransactionError('Missing required burn NFT parameters');
}

this._clauses = [
{
to: this._contract,
value: '0x0',
data: this._transactionData || this.getBurnNftData(),
},
];
}

/**
* Generates the transaction data for burning NFT by encoding the burn method call.
*
* @private
* @returns {string} The encoded transaction data as a hex string
*/
private getBurnNftData(): string {
const methodName = 'burn';
const types = ['uint256'];
const params = [this._tokenId];

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

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

/** @inheritdoc */
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 || this.getBurnNftData(),
value: '0',
sender: this.sender,
to: this.contract,
};
return json;
}

/** @inheritdoc */
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 data from clauses
this.contract = body.clauses[0]?.to || '0x0';
this.transactionData = body.clauses[0]?.data || '0x0';
this.type = TransactionType.StakingWithdraw;

// Extract tokenId from transaction data
if (this.transactionData.startsWith(BURN_NFT_METHOD_ID)) {
const tokenIdHex = this.transactionData.slice(BURN_NFT_METHOD_ID.length);
// Convert hex to decimal
this.tokenId = parseInt(tokenIdHex, 16).toString();
}

// Set sender address
if (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}`);
}
}
}
137 changes: 137 additions & 0 deletions modules/sdk-coin-vet/src/lib/transaction/exitDelegation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
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 { EXIT_DELEGATION_METHOD_ID } from '../constants';
import EthereumAbi from 'ethereumjs-abi';
import { addHexPrefix } from 'ethereumjs-util';

export class ExitDelegationTransaction extends Transaction {
private _tokenId: string;

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

get tokenId(): string {
return this._tokenId;
}

set tokenId(id: string) {
this._tokenId = id;
}

/** @inheritdoc */
async build(): Promise<void> {
this.buildClauses();
await this.buildRawTransaction();
this.generateTxnIdAndSetSender();
this.loadInputsAndOutputs();
}

/** @inheritdoc */
buildClauses(): void {
if (!this._contract || !this._tokenId) {
throw new InvalidTransactionError('Missing required unstaking parameters');
}

this._clauses = [
{
to: this._contract,
value: '0x0',
data: this._transactionData || this.getExitDelegationData(),
},
];
}

/**
* Generates the transaction data for unstaking by encoding the exitDelegation method call.
*
* @private
* @returns {string} The encoded transaction data as a hex string
*/
private getExitDelegationData(): string {
const methodName = 'exitDelegation';
const types = ['uint256'];
const params = [this._tokenId];

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

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

/** @inheritdoc */
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 || this.getExitDelegationData(),
value: '0',
sender: this.sender,
to: this.contract,
};
return json;
}

/** @inheritdoc */
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 data from clauses
this.contract = body.clauses[0]?.to || '0x0';
this.transactionData = body.clauses[0]?.data || '0x0';
this.type = TransactionType.StakingUnlock;

// Extract tokenId from transaction data
if (this.transactionData.startsWith(EXIT_DELEGATION_METHOD_ID)) {
const tokenIdHex = this.transactionData.slice(EXIT_DELEGATION_METHOD_ID.length);
// Convert hex to decimal
this.tokenId = parseInt(tokenIdHex, 16).toString();
}

// Set sender address
if (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}`);
}
}
}
Loading