Skip to content

Commit aac55ad

Browse files
authored
Merge pull request #6614 from BitGo/SC-2626
feat: build unstaking txns for vechain
2 parents a2a6019 + f939cfe commit aac55ad

File tree

10 files changed

+867
-0
lines changed

10 files changed

+867
-0
lines changed

modules/sdk-coin-vet/src/lib/constants.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,8 @@ export const VET_ADDRESS_LENGTH = 40;
33
export const VET_BLOCK_ID_LENGTH = 64;
44

55
export const TRANSFER_TOKEN_METHOD_ID = '0xa9059cbb';
6+
export const EXIT_DELEGATION_METHOD_ID = '0x32b7006d';
7+
export const BURN_NFT_METHOD_ID = '0x42966c68';
8+
9+
export const STARGATE_NFT_ADDRESS = '0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7';
10+
export const STARGATE_DELEGATION_ADDRESS = '0x4cb1c9ef05b529c093371264fab2c93cc6cddb0e';

modules/sdk-coin-vet/src/lib/iface.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export interface VetTransactionData {
2424
deployedAddress?: string;
2525
to?: string;
2626
tokenAddress?: string;
27+
tokenId?: string; // Added for unstaking and burn NFT transactions
2728
}
2829

2930
export interface VetTransactionExplanation extends BaseTransactionExplanation {
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { Transaction as VetTransaction, Secp256k1 } from '@vechain/sdk-core';
4+
import { Transaction } from './transaction';
5+
import { VetTransactionData } from '../iface';
6+
import { BURN_NFT_METHOD_ID } from '../constants';
7+
import EthereumAbi from 'ethereumjs-abi';
8+
import { addHexPrefix } from 'ethereumjs-util';
9+
10+
export class BurnNftTransaction extends Transaction {
11+
private _tokenId: string;
12+
13+
constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig);
15+
this._type = TransactionType.StakingWithdraw;
16+
}
17+
18+
get tokenId(): string {
19+
return this._tokenId;
20+
}
21+
22+
set tokenId(id: string) {
23+
this._tokenId = id;
24+
}
25+
26+
/** @inheritdoc */
27+
async build(): Promise<void> {
28+
this.buildClauses();
29+
await this.buildRawTransaction();
30+
this.generateTxnIdAndSetSender();
31+
this.loadInputsAndOutputs();
32+
}
33+
34+
/** @inheritdoc */
35+
buildClauses(): void {
36+
if (!this._contract || !this._tokenId) {
37+
throw new InvalidTransactionError('Missing required burn NFT parameters');
38+
}
39+
40+
this._clauses = [
41+
{
42+
to: this._contract,
43+
value: '0x0',
44+
data: this._transactionData || this.getBurnNftData(),
45+
},
46+
];
47+
}
48+
49+
/**
50+
* Generates the transaction data for burning NFT by encoding the burn method call.
51+
*
52+
* @private
53+
* @returns {string} The encoded transaction data as a hex string
54+
*/
55+
private getBurnNftData(): string {
56+
const methodName = 'burn';
57+
const types = ['uint256'];
58+
const params = [this._tokenId];
59+
60+
const method = EthereumAbi.methodID(methodName, types);
61+
const args = EthereumAbi.rawEncode(types, params);
62+
63+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
64+
}
65+
66+
/** @inheritdoc */
67+
toJson(): VetTransactionData {
68+
const json: VetTransactionData = {
69+
id: this.id,
70+
chainTag: this.chainTag,
71+
blockRef: this.blockRef,
72+
expiration: this.expiration,
73+
gasPriceCoef: this.gasPriceCoef,
74+
gas: this.gas,
75+
dependsOn: this.dependsOn,
76+
nonce: this.nonce,
77+
data: this.transactionData || this.getBurnNftData(),
78+
value: '0',
79+
sender: this.sender,
80+
to: this.contract,
81+
};
82+
return json;
83+
}
84+
85+
/** @inheritdoc */
86+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
87+
try {
88+
if (!signedTx || !signedTx.body) {
89+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
90+
}
91+
92+
// Store the raw transaction
93+
this.rawTransaction = signedTx;
94+
95+
// Set transaction body properties
96+
const body = signedTx.body;
97+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
98+
this.blockRef = body.blockRef || '0x0';
99+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
100+
this.clauses = body.clauses || [];
101+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
102+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
103+
this.dependsOn = body.dependsOn || null;
104+
this.nonce = String(body.nonce);
105+
106+
// Set data from clauses
107+
this.contract = body.clauses[0]?.to || '0x0';
108+
this.transactionData = body.clauses[0]?.data || '0x0';
109+
this.type = TransactionType.StakingWithdraw;
110+
111+
// Extract tokenId from transaction data
112+
if (this.transactionData.startsWith(BURN_NFT_METHOD_ID)) {
113+
const tokenIdHex = this.transactionData.slice(BURN_NFT_METHOD_ID.length);
114+
// Convert hex to decimal
115+
this.tokenId = parseInt(tokenIdHex, 16).toString();
116+
}
117+
118+
// Set sender address
119+
if (signedTx.origin) {
120+
this.sender = signedTx.origin.toString().toLowerCase();
121+
}
122+
123+
// Set signatures if present
124+
if (signedTx.signature) {
125+
// First signature is sender's signature
126+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
127+
128+
// If there's additional signature data, it's the fee payer's signature
129+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
130+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
131+
}
132+
}
133+
} catch (e) {
134+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
135+
}
136+
}
137+
}
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { TransactionType, InvalidTransactionError } from '@bitgo/sdk-core';
2+
import { BaseCoin as CoinConfig } from '@bitgo/statics';
3+
import { Transaction as VetTransaction, Secp256k1 } from '@vechain/sdk-core';
4+
import { Transaction } from './transaction';
5+
import { VetTransactionData } from '../iface';
6+
import { EXIT_DELEGATION_METHOD_ID } from '../constants';
7+
import EthereumAbi from 'ethereumjs-abi';
8+
import { addHexPrefix } from 'ethereumjs-util';
9+
10+
export class ExitDelegationTransaction extends Transaction {
11+
private _tokenId: string;
12+
13+
constructor(_coinConfig: Readonly<CoinConfig>) {
14+
super(_coinConfig);
15+
this._type = TransactionType.StakingUnlock;
16+
}
17+
18+
get tokenId(): string {
19+
return this._tokenId;
20+
}
21+
22+
set tokenId(id: string) {
23+
this._tokenId = id;
24+
}
25+
26+
/** @inheritdoc */
27+
async build(): Promise<void> {
28+
this.buildClauses();
29+
await this.buildRawTransaction();
30+
this.generateTxnIdAndSetSender();
31+
this.loadInputsAndOutputs();
32+
}
33+
34+
/** @inheritdoc */
35+
buildClauses(): void {
36+
if (!this._contract || !this._tokenId) {
37+
throw new InvalidTransactionError('Missing required unstaking parameters');
38+
}
39+
40+
this._clauses = [
41+
{
42+
to: this._contract,
43+
value: '0x0',
44+
data: this._transactionData || this.getExitDelegationData(),
45+
},
46+
];
47+
}
48+
49+
/**
50+
* Generates the transaction data for unstaking by encoding the exitDelegation method call.
51+
*
52+
* @private
53+
* @returns {string} The encoded transaction data as a hex string
54+
*/
55+
private getExitDelegationData(): string {
56+
const methodName = 'exitDelegation';
57+
const types = ['uint256'];
58+
const params = [this._tokenId];
59+
60+
const method = EthereumAbi.methodID(methodName, types);
61+
const args = EthereumAbi.rawEncode(types, params);
62+
63+
return addHexPrefix(Buffer.concat([method, args]).toString('hex'));
64+
}
65+
66+
/** @inheritdoc */
67+
toJson(): VetTransactionData {
68+
const json: VetTransactionData = {
69+
id: this.id,
70+
chainTag: this.chainTag,
71+
blockRef: this.blockRef,
72+
expiration: this.expiration,
73+
gasPriceCoef: this.gasPriceCoef,
74+
gas: this.gas,
75+
dependsOn: this.dependsOn,
76+
nonce: this.nonce,
77+
data: this.transactionData || this.getExitDelegationData(),
78+
value: '0',
79+
sender: this.sender,
80+
to: this.contract,
81+
};
82+
return json;
83+
}
84+
85+
/** @inheritdoc */
86+
fromDeserializedSignedTransaction(signedTx: VetTransaction): void {
87+
try {
88+
if (!signedTx || !signedTx.body) {
89+
throw new InvalidTransactionError('Invalid transaction: missing transaction body');
90+
}
91+
92+
// Store the raw transaction
93+
this.rawTransaction = signedTx;
94+
95+
// Set transaction body properties
96+
const body = signedTx.body;
97+
this.chainTag = typeof body.chainTag === 'number' ? body.chainTag : 0;
98+
this.blockRef = body.blockRef || '0x0';
99+
this.expiration = typeof body.expiration === 'number' ? body.expiration : 64;
100+
this.clauses = body.clauses || [];
101+
this.gasPriceCoef = typeof body.gasPriceCoef === 'number' ? body.gasPriceCoef : 128;
102+
this.gas = typeof body.gas === 'number' ? body.gas : Number(body.gas) || 0;
103+
this.dependsOn = body.dependsOn || null;
104+
this.nonce = String(body.nonce);
105+
106+
// Set data from clauses
107+
this.contract = body.clauses[0]?.to || '0x0';
108+
this.transactionData = body.clauses[0]?.data || '0x0';
109+
this.type = TransactionType.StakingUnlock;
110+
111+
// Extract tokenId from transaction data
112+
if (this.transactionData.startsWith(EXIT_DELEGATION_METHOD_ID)) {
113+
const tokenIdHex = this.transactionData.slice(EXIT_DELEGATION_METHOD_ID.length);
114+
// Convert hex to decimal
115+
this.tokenId = parseInt(tokenIdHex, 16).toString();
116+
}
117+
118+
// Set sender address
119+
if (signedTx.origin) {
120+
this.sender = signedTx.origin.toString().toLowerCase();
121+
}
122+
123+
// Set signatures if present
124+
if (signedTx.signature) {
125+
// First signature is sender's signature
126+
this.senderSignature = Buffer.from(signedTx.signature.slice(0, Secp256k1.SIGNATURE_LENGTH));
127+
128+
// If there's additional signature data, it's the fee payer's signature
129+
if (signedTx.signature.length > Secp256k1.SIGNATURE_LENGTH) {
130+
this.feePayerSignature = Buffer.from(signedTx.signature.slice(Secp256k1.SIGNATURE_LENGTH));
131+
}
132+
}
133+
} catch (e) {
134+
throw new InvalidTransactionError(`Failed to deserialize transaction: ${e.message}`);
135+
}
136+
}
137+
}

0 commit comments

Comments
 (0)