Skip to content

Commit 8121753

Browse files
fix(sdk-core): validate unsigned staking transaction before sign
Ticket: WP-3967 TICKET: WP-3967
1 parent 174f7eb commit 8121753

File tree

5 files changed

+384
-3
lines changed

5 files changed

+384
-3
lines changed
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
export const newStakingRequest = {
2+
id: '75de4133-6bea-4507-841a-b19f7672e1f7',
3+
requestingUserId: '5cb525d7e1b1328103076788fd6a07f9',
4+
type: 'STAKE',
5+
enterpriseId: '61c0ff41174c4e0007ae78b2a0bea1ec',
6+
walletId: '674f61528414869237480e6997ea7954',
7+
walletType: 'hot',
8+
withdrawalAddress: '0x1d1a245741bd7d603747a23d30f4c91682a29926',
9+
coin: 'topeth:wct',
10+
status: 'NEW',
11+
statusModifiedDate: '2025-03-25T13:43:07.808717Z',
12+
createdDate: '2025-03-25T13:43:07.808708Z',
13+
delegations: [],
14+
transactions: [],
15+
totalStaked: '0',
16+
amount: '1000000000000000000',
17+
durationSeconds: '1209600',
18+
};
19+
20+
export const stakingReadyToSign = {
21+
id: '75de4133-6bea-4507-841a-b19f7672e1f7',
22+
requestingUserId: '5cb525d7e1b1328103076788fd6a07f9',
23+
type: 'STAKE',
24+
enterpriseId: '61c0ff41174c4e0007ae78b2a0bea1ec',
25+
walletId: '674f61528414869237480e6997ea7954',
26+
walletType: 'hot',
27+
withdrawalAddress: '0x1d1a245741bd7d603747a23d30f4c91682a29926',
28+
coin: 'topeth:wct',
29+
status: 'READY',
30+
statusModifiedDate: '2025-03-25T13:43:08.883527Z',
31+
createdDate: '2025-03-25T13:43:07.808708Z',
32+
delegations: [
33+
{
34+
id: '27d726f9-3061-4f73-b40c-703962449b24',
35+
delegationAddress: '0x1d1a245741bd7d603747a23d30f4c91682a29926',
36+
provider: 'Unknown',
37+
withdrawalAddress: '0x1d1a245741bd7d603747a23d30f4c91682a29926',
38+
delegated: '0',
39+
coin: 'topeth:wct',
40+
walletId: '674f61528414869237480e6997ea7954',
41+
properties: {
42+
type: 'WCT',
43+
},
44+
status: 'PENDING',
45+
rewards: '0',
46+
lockedRewards: '0',
47+
pendingUnstake: '0',
48+
pendingStake: '1000000000000000000',
49+
reserved: '0',
50+
pendingReserved: '0',
51+
apy: '0',
52+
unstakeable: false,
53+
pendingClaimRewards: '0',
54+
permissionAttributes: {
55+
staking: {
56+
enabled: false,
57+
disabledReason: 'Delegation is not active.',
58+
allowClientToUseOwnValidator: false,
59+
},
60+
unstaking: {
61+
enabled: false,
62+
disabledReason: 'Delegation is not active.',
63+
},
64+
},
65+
},
66+
],
67+
transactions: [
68+
{
69+
id: '52676686-0d86-4ae6-8216-3465cb228615',
70+
stakingRequestId: '75de4133-6bea-4507-841a-b19f7672e1f7',
71+
delegationId: '27d726f9-3061-4f73-b40c-703962449b24',
72+
createdDate: '2025-03-25T13:43:07.942063Z',
73+
transactionType: 'authorize',
74+
status: 'READY',
75+
statusModifiedDate: '2025-03-25T13:43:07.942063Z',
76+
amount: '1000000000000000000',
77+
},
78+
{
79+
id: 'ac56e1fc-3077-487f-a4f4-78061aa74300',
80+
stakingRequestId: '75de4133-6bea-4507-841a-b19f7672e1f7',
81+
delegationId: '27d726f9-3061-4f73-b40c-703962449b24',
82+
createdDate: '2025-03-25T13:43:07.942138Z',
83+
transactionType: 'delegate',
84+
status: 'WAITING',
85+
statusModifiedDate: '2025-03-25T13:43:07.942138Z',
86+
amount: '1000000000000000000',
87+
},
88+
],
89+
totalStaked: '0',
90+
amount: '1000000000000000000',
91+
durationSeconds: '1209600',
92+
fiatValue: 20.63776884676185,
93+
};
94+
95+
export const updatedStakingRequest = {
96+
id: '52676686-0d86-4ae6-8216-3465cb228615',
97+
stakingRequestId: '75de4133-6bea-4507-841a-b19f7672e1f7',
98+
delegationId: '27d726f9-3061-4f73-b40c-703962449b24',
99+
createdDate: '2025-03-25T13:43:07.942063Z',
100+
transactionType: 'authorize',
101+
status: 'READY',
102+
statusModifiedDate: '2025-03-25T13:43:07.942063Z',
103+
amount: '1000000000000000000',
104+
buildParams: {
105+
recipients: [
106+
{
107+
amount: '0',
108+
address: '0x75bb6dcA2cD6F9a0189c478bBb8F7EE2fEF07C78',
109+
data: '0x095ea7b3000000000000000000000000140d63efb5b24314f6f62dbadb383dba2e49d7ee0000000000000000000000000000000000000000000000000de0b6b3a7640000',
110+
},
111+
],
112+
stakingParams: {
113+
requestId: '75de4133-6bea-4507-841a-b19f7672e1f7',
114+
amount: '1000000000000000000',
115+
coin: 'topeth:wct',
116+
validator: '0x1d1a245741bd7d603747a23d30f4c91682a29926',
117+
actionType: 'wct_authorize',
118+
},
119+
},
120+
};
121+
122+
export const unsignedStakingTransaction = {
123+
txHex:
124+
'0x02f9019083aa37dc718206a882089e83030d40941d1a245741bd7d603747a23d30f4c91682a2992680b901643912521500000000000000000000000075bb6dca2cd6f9a0189c478bbb8f7ee2fef07c78000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000067ebedc3000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000140d63efb5b24314f6f62dbadb383dba2e49d7ee0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0808080',
125+
txInfo: {
126+
nonce: 113,
127+
gasLimit: '200000',
128+
value: '0',
129+
data: '0x3912521500000000000000000000000075bb6dca2cd6f9a0189c478bbb8f7ee2fef07c78000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000067ebedc3000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000140d63efb5b24314f6f62dbadb383dba2e49d7ee0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000',
130+
id: '0xdddb88e48548a0c326c19b876886c694c3d88e64f7eafa11bbd274257728ce12',
131+
to: '0x1d1a245741bd7d603747a23d30f4c91682a29926',
132+
chainId: '0xaa37dc',
133+
_type: 'EIP1559',
134+
maxFeePerGas: '2206',
135+
maxPriorityFeePerGas: '1704',
136+
},
137+
feeInfo: {
138+
date: '2025-03-25T13:44:35.058Z',
139+
gasPrice: '1000251',
140+
baseFee: '251',
141+
gasUsedRatio: '0.00073025',
142+
safeLowMinerTip: '1136',
143+
normalMinerTip: '1136',
144+
standardMinerTip: '1136',
145+
fastestMinerTip: '1136',
146+
ludicrousMinerTip: '1010002',
147+
},
148+
eip1559: {
149+
maxPriorityFeePerGas: '1704',
150+
maxFeePerGas: '2206',
151+
},
152+
recipients: [
153+
{
154+
amount: '0',
155+
address: '0x75bb6dcA2cD6F9a0189c478bBb8F7EE2fEF07C78',
156+
data: '0x095ea7b3000000000000000000000000140d63efb5b24314f6f62dbadb383dba2e49d7ee0000000000000000000000000000000000000000000000000de0b6b3a7640000',
157+
},
158+
],
159+
nextContractSequenceId: 6,
160+
gasLimit: 200000,
161+
isBatch: false,
162+
stakingParams: {
163+
requestId: '75de4133-6bea-4507-841a-b19f7672e1f7',
164+
amount: '1000000000000000000',
165+
coin: 'topeth:wct',
166+
validator: '0x1d1a245741bd7d603747a23d30f4c91682a29926',
167+
actionType: 'wct_authorize',
168+
},
169+
coin: 'topeth',
170+
};

modules/bitgo/test/v2/unit/staking/stakingWalletNonTSS.ts

Lines changed: 107 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,20 @@ import * as _ from 'lodash';
22

33
import * as nock from 'nock';
44
import fixtures from '../../fixtures/staking/stakingWallet';
5-
6-
import { Enterprise, Environments, Keychain, Keychains, StakingWallet, Wallet } from '@bitgo/sdk-core';
5+
import * as opethFixtures from '../../fixtures/staking/topethStakingFixtures';
6+
7+
import {
8+
Enterprise,
9+
Environments,
10+
Keychain,
11+
Keychains,
12+
PrebuildTransactionResult,
13+
StakingTransaction,
14+
StakingWallet,
15+
Wallet,
16+
WalletCoinSpecific,
17+
WalletData,
18+
} from '@bitgo/sdk-core';
719
import { TestBitGo } from '@bitgo/sdk-test';
820
import { BitGo } from '../../../../src';
921
import * as sinon from 'sinon';
@@ -14,13 +26,16 @@ describe('non-TSS Staking Wallet', function () {
1426
let ethBaseCoin;
1527
let maticBaseCoin;
1628
let btcBaseCoin;
29+
let topethWctBaseCoin;
1730
let enterprise;
1831
let ethWalletData: any;
1932
let btcWalletData: any;
33+
let topethWctStakingWalletData: WalletData;
2034
let btcDescriptorWalletData: any;
2135
let ethStakingWallet: StakingWallet;
2236
let maticStakingWallet: StakingWallet;
2337
let btcStakingWallet: StakingWallet;
38+
let topethWctStakingWallet: StakingWallet;
2439

2540
before(function () {
2641
bitgo = TestBitGo.decorate(BitGo, { env: 'mock', microservicesUri } as any);
@@ -31,6 +46,7 @@ describe('non-TSS Staking Wallet', function () {
3146
maticBaseCoin.keychains();
3247
btcBaseCoin = bitgo.coin('btc');
3348
btcBaseCoin.keychains();
49+
topethWctBaseCoin = bitgo.coin('topeth:wct');
3450

3551
enterprise = new Enterprise(bitgo, ethBaseCoin, {
3652
id: '5cf940949449412d00f53b3d92dbcaa3',
@@ -63,9 +79,30 @@ describe('non-TSS Staking Wallet', function () {
6379
keys: ['5b3424f91bf349930e340175'],
6480
coinSpecific: {},
6581
};
82+
83+
topethWctStakingWalletData = {
84+
approvalsRequired: 0,
85+
balance: 0,
86+
balanceString: '',
87+
coinSpecific: {} as WalletCoinSpecific,
88+
confirmedBalance: 0,
89+
confirmedBalanceString: '',
90+
keys: [],
91+
label: '',
92+
multisigType: 'onchain',
93+
pendingApprovals: [],
94+
spendableBalance: 0,
95+
spendableBalanceString: '',
96+
id: 'topethWctStakingWalletId',
97+
coin: 'topeth:wct',
98+
enterprise: enterprise.id,
99+
};
100+
66101
const ethWallet = new Wallet(bitgo, ethBaseCoin, ethWalletData);
67102
const maticWallet = new Wallet(bitgo, maticBaseCoin, maticWalletData);
68103
const btcWallet = new Wallet(bitgo, btcBaseCoin, btcWalletData);
104+
topethWctStakingWallet = new Wallet(bitgo, topethWctBaseCoin, topethWctStakingWalletData).toStakingWallet();
105+
69106
ethStakingWallet = ethWallet.toStakingWallet();
70107
maticStakingWallet = maticWallet.toStakingWallet();
71108
btcStakingWallet = btcWallet.toStakingWallet();
@@ -132,12 +169,16 @@ describe('non-TSS Staking Wallet', function () {
132169
)
133170
.reply(200, transaction);
134171

172+
// skipping validation because mock data is not a valid transaction
173+
sinon.stub(StakingWallet.prototype, <any>'validateBuiltStakingTransaction').resolves();
174+
135175
const stakingTransaction = await ethStakingWallet.buildSignAndSend(
136176
{ walletPassphrase: walletPassphrase },
137177
transaction
138178
);
139179

140180
stakingTransaction.should.deepEqual(transaction);
181+
sinon.restore();
141182
});
142183

143184
it('should throw error when buildParams are not expanded', async function () {
@@ -221,12 +262,15 @@ describe('non-TSS Staking Wallet', function () {
221262
)
222263
.reply(200, transaction);
223264

265+
// skipping validation because mock data is not a valid transaction
266+
sinon.stub(StakingWallet.prototype, <any>'validateBuiltStakingTransaction').resolves();
224267
const stakingTransaction = await maticStakingWallet.buildSignAndSend(
225268
{ walletPassphrase: walletPassphrase },
226269
transaction
227270
);
228271

229272
stakingTransaction.should.deepEqual(transaction);
273+
sinon.restore();
230274
});
231275
});
232276

@@ -266,4 +310,65 @@ describe('non-TSS Staking Wallet', function () {
266310
prebuildTransaction.calledOnceWithExactly(transaction.buildParams).should.be.true;
267311
});
268312
});
313+
314+
describe('Opeth:WCT Staking', function () {
315+
it('should build and validate transaction', async function () {
316+
const unsignedTransaction: PrebuildTransactionResult = {
317+
walletId: topethWctStakingWallet.walletId,
318+
...opethFixtures.unsignedStakingTransaction,
319+
} as PrebuildTransactionResult;
320+
const stakingTransaction: StakingTransaction = opethFixtures.updatedStakingRequest;
321+
322+
nock(microservicesUri)
323+
.get(
324+
`/api/staking/v1/${topethWctStakingWallet.coin}/wallets/${topethWctStakingWallet.walletId}/requests/${stakingTransaction.stakingRequestId}/transactions/${stakingTransaction.id}`
325+
)
326+
.query({ expandBuildParams: true })
327+
.reply(200, stakingTransaction);
328+
329+
nock(microservicesUri)
330+
.get(`/api/v2/topeth/wallet/${topethWctStakingWallet.walletId}`)
331+
.reply(200, topethWctStakingWalletData);
332+
333+
nock(microservicesUri)
334+
.post(`/api/v2/topeth/wallet/${topethWctStakingWallet.walletId}/tx/build`)
335+
.reply(200, unsignedTransaction);
336+
337+
// tx validation happens before signing, so we can skip it
338+
sinon.stub(topethWctStakingWallet, 'sign').resolves();
339+
340+
await topethWctStakingWallet.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction);
341+
});
342+
343+
it('should fail to validate transaction if unsigned transaction does not match the staking transaction', async function () {
344+
const unsignedTransaction: PrebuildTransactionResult = {
345+
walletId: topethWctStakingWallet.walletId,
346+
...opethFixtures.unsignedStakingTransaction,
347+
txHex:
348+
'0x02f9019083aa37dc718206a882089e83030d40941d1a245741bd7d603747a23d30f4c91682a2992680b901643912521500000000000000000000000086bb6dca2cd6f9a0189c478bbb8f7ee2fef07c89000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000067ebedc3000000000000000000000000000000000000000000000000000000000000000600000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000044095ea7b3000000000000000000000000140d63efb5b24314f6f62dbadb383dba2e49d7ee0000000000000000000000000000000000000000000000000de0b6b3a7640000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000c0808080',
349+
} as PrebuildTransactionResult;
350+
const stakingTransaction: StakingTransaction = opethFixtures.updatedStakingRequest;
351+
352+
nock(microservicesUri)
353+
.get(
354+
`/api/staking/v1/${topethWctStakingWallet.coin}/wallets/${topethWctStakingWallet.walletId}/requests/${stakingTransaction.stakingRequestId}/transactions/${stakingTransaction.id}`
355+
)
356+
.query({ expandBuildParams: true })
357+
.reply(200, stakingTransaction);
358+
359+
nock(microservicesUri)
360+
.get(`/api/v2/topeth/wallet/${topethWctStakingWallet.walletId}`)
361+
.reply(200, topethWctStakingWalletData);
362+
363+
nock(microservicesUri)
364+
.post(`/api/v2/topeth/wallet/${topethWctStakingWallet.walletId}/tx/build`)
365+
.reply(200, unsignedTransaction);
366+
367+
await topethWctStakingWallet
368+
.buildAndSign({ walletPassphrase: 'passphrase' }, stakingTransaction)
369+
.should.be.rejectedWith(
370+
'Invalid recipient address: 0x86bb6dca2cd6f9a0189c478bbb8f7ee2fef07c89, Missing recipient address(es): 0x75bb6dca2cd6f9a0189c478bbb8f7ee2fef07c78'
371+
);
372+
});
373+
});
269374
});

modules/sdk-core/src/bitgo/staking/iStakingWallet.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,9 +243,14 @@ export interface StakingTransaction {
243243
gasPrice?: string;
244244
}
245245

246+
export type StakingTxRequestPrebuildTransactionResult = {
247+
walletId: string;
248+
txRequestId: string;
249+
};
250+
246251
export interface StakingPrebuildTransactionResult {
247252
transaction: StakingTransaction;
248-
result: PrebuildTransactionResult;
253+
result: PrebuildTransactionResult | StakingTxRequestPrebuildTransactionResult;
249254
}
250255

251256
export interface StakingSignedTransaction {

0 commit comments

Comments
 (0)