Skip to content

Commit b88c07a

Browse files
authored
Merge pull request #7137 from BitGo/SC-3324-1
fix: invalid id error for vechain staking txns
2 parents 10a3ca9 + 0493661 commit b88c07a

File tree

4 files changed

+239
-13
lines changed

4 files changed

+239
-13
lines changed

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,11 @@ export class Transaction extends BaseTransaction {
374374
if (
375375
this.type === TransactionType.Send ||
376376
this.type === TransactionType.SendToken ||
377-
this.type === TransactionType.SendNFT
377+
this.type === TransactionType.SendNFT ||
378+
this.type === TransactionType.ContractCall ||
379+
this.type === TransactionType.StakingUnlock ||
380+
this.type === TransactionType.StakingWithdraw ||
381+
this.type === TransactionType.StakingClaim
378382
) {
379383
transactionBody.reserved = {
380384
features: 1, // mark transaction as delegated i.e. will use gas payer

modules/sdk-coin-vet/test/resources/vet.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export const STAKING_TRANSACTION =
1616

1717
export const STAKING_LEVEL_ID = 8;
1818
export const STAKING_AUTORENEW = true;
19-
export const STAKING_CONTRACT_ADDRESS = '0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7';
19+
export const STAKING_CONTRACT_ADDRESS = '0x1ec1d168574603ec35b9d229843b7c2b44bcb770';
2020

2121
export const VALID_TOKEN_SIGNABLE_PAYLOAD =
2222
'f8762788014ead140e77bbc140f85ef85c940000000000000000000000000000456e6572677980b844a9059cbb000000000000000000000000e59f1cea4e0fef511e3d0f4eec44adf19c4cbeec000000000000000000000000000000000000000000000000016345785d8a000081808252088082faf8c101';

modules/sdk-coin-vet/test/transactionBuilder/stakingTransaction.ts

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -143,17 +143,6 @@ describe('VET Staking Transaction', function () {
143143
stakingTx.clauses[0].data.should.equal(expectedData);
144144
});
145145

146-
it('should throw error when stakingContractABI is missing', async function () {
147-
const txBuilder = createBasicTxBuilder();
148-
txBuilder.stakingContractAddress(stakingContractAddress);
149-
txBuilder.amountToStake(amountToStake);
150-
txBuilder.levelId(levelId);
151-
// Not setting stakingContractABI
152-
153-
// Should fail when trying to build without ABI
154-
await txBuilder.build().should.be.rejectedWith('Staking contract ABI is required');
155-
});
156-
157146
it('should build transaction with undefined sender but include it in inputs', async function () {
158147
const txBuilder = factory.getStakingBuilder();
159148
txBuilder.stakingContractAddress(stakingContractAddress);
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
import should from 'should';
2+
import { coins } from '@bitgo/statics';
3+
import { TransactionType } from '@bitgo/sdk-core';
4+
import { TransactionBuilderFactory, StakingTransaction } from '../../src/lib';
5+
import * as testData from '../resources/vet';
6+
7+
describe('VET Staking Flow - End-to-End Test', function () {
8+
const coinConfig = coins.get('tvet');
9+
const factory = new TransactionBuilderFactory(coinConfig);
10+
11+
// Test data
12+
const stakingContractAddress = testData.STAKING_CONTRACT_ADDRESS;
13+
const amountToStake = '1000000000000000000'; // 1 VET in wei
14+
const amountToStakeHex = '0xde0b6b3a7640000'; // Same amount in hex
15+
const levelId = testData.STAKING_LEVEL_ID;
16+
const autorenew = testData.STAKING_AUTORENEW;
17+
const senderAddress = '0x9378c12BD7502A11F770a5C1F223c959B2805dA9';
18+
const feePayerAddress = '0xdc9fef0b84a0ccf3f1bd4b84e41743e3e051a083';
19+
20+
// Mock signatures for testing (these would come from actual signing in real scenario)
21+
const mockSenderSignature = Buffer.from(testData.senderSig, 'hex');
22+
const mockFeePayerSignature = Buffer.from(testData.feePayerSig, 'hex');
23+
24+
describe('Complete Staking Transaction Flow', function () {
25+
it('should build, sign, and serialize a complete staking transaction with fee delegation', async function () {
26+
// Step 1: Build the staking transaction
27+
const stakingBuilder = factory.getStakingBuilder();
28+
29+
stakingBuilder
30+
.stakingContractAddress(stakingContractAddress)
31+
.amountToStake(amountToStake)
32+
.levelId(levelId)
33+
.sender(senderAddress)
34+
.chainTag(0x27) // Testnet chain tag
35+
.blockRef('0x014ead140e77bbc1')
36+
.expiration(64)
37+
.gas(100000)
38+
.gasPriceCoef(128)
39+
.nonce('12345');
40+
41+
stakingBuilder.addFeePayerAddress(feePayerAddress);
42+
43+
const unsignedTx = await stakingBuilder.build();
44+
should.exist(unsignedTx);
45+
unsignedTx.should.be.instanceof(StakingTransaction);
46+
47+
const stakingTx = unsignedTx as StakingTransaction;
48+
49+
// Verify transaction structure
50+
stakingTx.type.should.equal(TransactionType.ContractCall);
51+
stakingTx.stakingContractAddress.should.equal(stakingContractAddress);
52+
stakingTx.amountToStake.should.equal(amountToStake);
53+
stakingTx.levelId.should.equal(levelId);
54+
stakingTx.autorenew.should.equal(autorenew);
55+
56+
should.exist(stakingTx.rawTransaction);
57+
should.exist(stakingTx.rawTransaction.body);
58+
59+
// This is the critical test - ensure reserved.features = 1 for ContractCall type
60+
should.exist(stakingTx.rawTransaction.body.reserved);
61+
stakingTx.rawTransaction.body.reserved!.should.have.property('features', 1);
62+
63+
// Step 3: Add sender signature
64+
stakingTx.addSenderSignature(mockSenderSignature);
65+
should.exist(stakingTx.senderSignature);
66+
Buffer.from(stakingTx.senderSignature!).should.eql(mockSenderSignature);
67+
68+
// Step 4: Add fee payer signature
69+
stakingTx.addFeePayerSignature(mockFeePayerSignature);
70+
should.exist(stakingTx.feePayerSignature);
71+
Buffer.from(stakingTx.feePayerSignature!).should.eql(mockFeePayerSignature);
72+
73+
// Step 5: Generate transaction ID
74+
75+
// This should NOT throw "not signed transaction: id unavailable" error anymore
76+
const transactionId = stakingTx.id;
77+
should.exist(transactionId);
78+
transactionId.should.not.equal('UNAVAILABLE');
79+
80+
// Step 6: Serialize the fully signed transaction
81+
const serializedTx = stakingTx.toBroadcastFormat();
82+
should.exist(serializedTx);
83+
serializedTx.should.be.type('string');
84+
serializedTx.should.startWith('0x');
85+
86+
// Step 7: Verify transaction can be deserialized
87+
const deserializedBuilder = factory.from(serializedTx);
88+
const deserializedTx = deserializedBuilder.transaction as StakingTransaction;
89+
90+
deserializedTx.should.be.instanceof(StakingTransaction);
91+
deserializedTx.stakingContractAddress.should.equal(stakingContractAddress);
92+
// Amount is stored in hex format in deserialized transaction
93+
deserializedTx.amountToStake.should.equal(amountToStakeHex);
94+
deserializedTx.levelId.should.equal(levelId);
95+
deserializedTx.autorenew.should.equal(autorenew);
96+
97+
// Step 8: Verify toJson output
98+
const jsonOutput = stakingTx.toJson();
99+
should.exist(jsonOutput);
100+
jsonOutput.should.have.property('id', transactionId);
101+
jsonOutput.should.have.property('stakingContractAddress', stakingContractAddress);
102+
// JSON output keeps decimal format for amounts
103+
jsonOutput.should.have.property('amountToStake', amountToStake);
104+
jsonOutput.should.have.property('nftTokenId', levelId);
105+
jsonOutput.should.have.property('autorenew', autorenew);
106+
});
107+
108+
it('should handle signature combination in the correct order', async function () {
109+
// This test specifically validates the signature combination flow that was failing
110+
const stakingBuilder = factory.getStakingBuilder();
111+
112+
stakingBuilder
113+
.stakingContractAddress(stakingContractAddress)
114+
.amountToStake(amountToStake)
115+
.levelId(levelId)
116+
.sender(senderAddress)
117+
.chainTag(0x27)
118+
.blockRef('0x014ead140e77bbc1')
119+
.expiration(64)
120+
.gas(100000)
121+
.gasPriceCoef(128)
122+
.nonce('12345');
123+
124+
stakingBuilder.addFeePayerAddress(feePayerAddress);
125+
126+
const tx = (await stakingBuilder.build()) as StakingTransaction;
127+
128+
// Test 1: Only sender signature - should generate half-signed transaction
129+
tx.addSenderSignature(mockSenderSignature);
130+
131+
// Test 2: Add fee payer signature - should generate fully signed transaction
132+
tx.addFeePayerSignature(mockFeePayerSignature);
133+
134+
// Should be able to get transaction ID with both signatures
135+
const fullSignedId = tx.id;
136+
should.exist(fullSignedId);
137+
fullSignedId.should.not.equal('UNAVAILABLE');
138+
139+
// The ID should be consistent
140+
fullSignedId.should.be.type('string');
141+
fullSignedId.length.should.be.greaterThan(10);
142+
});
143+
144+
it('should properly set transaction type for fee delegation validation', async function () {
145+
// This test ensures our fix for TransactionType.ContractCall is working
146+
const stakingBuilder = factory.getStakingBuilder();
147+
148+
stakingBuilder
149+
.stakingContractAddress(stakingContractAddress)
150+
.amountToStake(amountToStake)
151+
.levelId(levelId)
152+
.sender(senderAddress)
153+
.chainTag(0x27)
154+
.blockRef('0x014ead140e77bbc1')
155+
.expiration(64)
156+
.gas(100000)
157+
.gasPriceCoef(128)
158+
.nonce('12345');
159+
160+
stakingBuilder.addFeePayerAddress(feePayerAddress);
161+
162+
const tx = (await stakingBuilder.build()) as StakingTransaction;
163+
164+
// Verify the transaction type is ContractCall
165+
tx.type.should.equal(TransactionType.ContractCall);
166+
167+
// Verify fee delegation is enabled via reserved.features = 1
168+
const rawTxBody = tx.rawTransaction.body;
169+
should.exist(rawTxBody.reserved);
170+
rawTxBody.reserved!.should.have.property('features', 1);
171+
172+
// This proves that ContractCall transactions now get fee delegation support
173+
});
174+
175+
it('should work with pre-built signed transaction from test data', async function () {
176+
// Test using the actual signed transaction from our test data
177+
const txBuilder = factory.from(testData.STAKING_TRANSACTION);
178+
const tx = txBuilder.transaction as StakingTransaction;
179+
180+
// Should be able to get ID without throwing errors
181+
const txId = tx.id;
182+
should.exist(txId);
183+
txId.should.not.equal('UNAVAILABLE');
184+
185+
// Verify all staking properties are preserved
186+
// Note: The test data uses a different contract address in the transaction
187+
// The actual contract address from the parsed transaction is 0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7
188+
tx.stakingContractAddress.should.equal('0x1856c533ac2d94340aaa8544d35a5c1d4a21dee7');
189+
tx.levelId.should.equal(testData.STAKING_LEVEL_ID);
190+
tx.autorenew.should.equal(testData.STAKING_AUTORENEW);
191+
});
192+
});
193+
194+
describe('Fee Delegation Flag Tests', function () {
195+
it('should set fee delegation for all staking-related transaction types', async function () {
196+
const testTypes = [
197+
{ type: TransactionType.ContractCall, name: 'Staking' },
198+
{ type: TransactionType.StakingUnlock, name: 'Exit Delegation' },
199+
{ type: TransactionType.StakingWithdraw, name: 'Burn NFT' },
200+
{ type: TransactionType.StakingClaim, name: 'Claim Rewards' },
201+
];
202+
203+
for (const testCase of testTypes) {
204+
const stakingBuilder = factory.getStakingBuilder();
205+
stakingBuilder
206+
.stakingContractAddress(stakingContractAddress)
207+
.amountToStake(amountToStake)
208+
.levelId(levelId)
209+
.sender(senderAddress)
210+
.chainTag(0x27)
211+
.blockRef('0x014ead140e77bbc1')
212+
.expiration(64)
213+
.gas(100000)
214+
.gasPriceCoef(128)
215+
.nonce('12345');
216+
217+
stakingBuilder.addFeePayerAddress(feePayerAddress);
218+
219+
// Manually set the transaction type to test different types
220+
const tx = (await stakingBuilder.build()) as StakingTransaction;
221+
(tx as any)._type = testCase.type;
222+
223+
// Rebuild the raw transaction to test fee delegation flag
224+
await (tx as any).buildRawTransaction();
225+
226+
// Verify fee delegation is set for all types
227+
const rawTxBody = tx.rawTransaction.body;
228+
should.exist(rawTxBody.reserved);
229+
rawTxBody.reserved!.should.have.property('features', 1);
230+
}
231+
});
232+
});
233+
});

0 commit comments

Comments
 (0)