Skip to content

Commit f928cdb

Browse files
committed
fix: txHex needs to be set for the txn in case of TSS
TICKET: CHALO-349
1 parent e1eed7b commit f928cdb

2 files changed

Lines changed: 153 additions & 0 deletions

File tree

modules/sdk-core/src/bitgo/wallet/wallet.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3526,6 +3526,19 @@ export class Wallet implements IWallet {
35263526
delete prebuild.wallet;
35273527
delete prebuild.buildParams;
35283528

3529+
// For TSS wallets the build endpoint returns only { txRequestId, stakingParams } — no txHex.
3530+
// Fetch the full txRequest to obtain serializedTxHex and populate txHex so that
3531+
// verifyTransaction (called inside prebuildAndSignTransaction) has the transaction bytes
3532+
// it needs. This mirrors what prebuildTransactionTxRequests does for other tx types.
3533+
if (this._wallet.multisigType === 'tss' && !prebuild.txHex && prebuild.txRequestId) {
3534+
const txRequest = await getTxRequest(this.bitgo, this.id(), prebuild.txRequestId, params.reqId);
3535+
const unsignedTx =
3536+
txRequest.apiVersion === 'full' ? txRequest.transactions?.[0]?.unsignedTx : txRequest.unsignedTxs?.[0];
3537+
if (unsignedTx?.serializedTxHex) {
3538+
prebuild = _.extend({}, prebuild, { txHex: unsignedTx.serializedTxHex });
3539+
}
3540+
}
3541+
35293542
prebuild = _.extend({}, prebuild, { walletId: this.id() });
35303543
debug('final resource management transaction prebuild: %O', prebuild);
35313544
prebuilds.push(prebuild);

modules/sdk-core/test/unit/bitgo/wallet/resourceManagement.ts

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,146 @@ describe('Wallet - resource management', function () {
113113
bodyArg.should.have.property('delegations');
114114
bodyArg.should.not.have.property('walletPassphrase');
115115
});
116+
117+
describe('TSS wallet — txHex population from full txRequest', function () {
118+
function stubTxRequestFetch(txRequest: any) {
119+
const resultStub = sinon.stub().resolves({ txRequests: [txRequest] });
120+
const retryStub = sinon.stub().returns({ result: resultStub });
121+
const queryStub = sinon.stub().returns({ retry: retryStub });
122+
mockBitGo.get = sinon.stub().returns({ query: queryStub });
123+
mockBitGo.url = sinon.stub().returns('/mock-api/v2/wallet/test-wallet-id/txrequests');
124+
return { resultStub };
125+
}
126+
127+
beforeEach(function () {
128+
mockWalletData = {
129+
id: 'test-wallet-id',
130+
keys: ['user-key', 'backup-key', 'bitgo-key'],
131+
type: 'hot',
132+
multisigType: 'tss',
133+
};
134+
wallet = new Wallet(mockBitGo, mockBaseCoin, mockWalletData);
135+
});
136+
137+
it('should fetch full txRequest and map serializedTxHex to txHex when apiVersion is full', async function () {
138+
stubPost({ transactions: [{ txRequestId: 'req-1', stakingParams: {} }], errors: [] });
139+
stubTxRequestFetch({
140+
txRequestId: 'req-1',
141+
apiVersion: 'full',
142+
transactions: [{ unsignedTx: { serializedTxHex: 'serialized-hex-aaa' } }],
143+
});
144+
145+
const result = await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
146+
147+
result.prebuilds.should.have.length(1);
148+
result.prebuilds[0]!.txHex!.should.equal('serialized-hex-aaa');
149+
sinon.assert.calledOnce(mockBitGo.get);
150+
});
151+
152+
it('should fetch full txRequest and map serializedTxHex to txHex when apiVersion is lite', async function () {
153+
stubPost({ transactions: [{ txRequestId: 'req-2', stakingParams: {} }], errors: [] });
154+
stubTxRequestFetch({
155+
txRequestId: 'req-2',
156+
apiVersion: 'lite',
157+
unsignedTxs: [{ serializedTxHex: 'serialized-hex-bbb' }],
158+
});
159+
160+
const result = await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
161+
162+
result.prebuilds.should.have.length(1);
163+
result.prebuilds[0]!.txHex!.should.equal('serialized-hex-bbb');
164+
});
165+
166+
it('should leave txHex undefined when txRequest has no serializedTxHex', async function () {
167+
stubPost({ transactions: [{ txRequestId: 'req-3', stakingParams: {} }], errors: [] });
168+
stubTxRequestFetch({
169+
txRequestId: 'req-3',
170+
apiVersion: 'full',
171+
transactions: [{ unsignedTx: {} }],
172+
});
173+
174+
const result = await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
175+
176+
result.prebuilds.should.have.length(1);
177+
(result.prebuilds[0].txHex === undefined).should.be.true();
178+
});
179+
180+
it('should NOT fetch txRequest when txHex is already present in the build response', async function () {
181+
stubPost({ transactions: [{ txRequestId: 'req-4', txHex: 'already-present' }], errors: [] });
182+
mockBitGo.get = sinon.stub();
183+
184+
const result = await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
185+
186+
result.prebuilds[0]!.txHex!.should.equal('already-present');
187+
sinon.assert.notCalled(mockBitGo.get);
188+
});
189+
190+
it('should NOT fetch txRequest when build response has no txRequestId', async function () {
191+
stubPost({ transactions: [{ stakingParams: {} }], errors: [] });
192+
mockBitGo.get = sinon.stub();
193+
194+
await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
195+
196+
sinon.assert.notCalled(mockBitGo.get);
197+
});
198+
199+
it('should fetch txRequest once per delegation for bulk delegations', async function () {
200+
stubPost({
201+
transactions: [
202+
{ txRequestId: 'req-bulk-1', stakingParams: {} },
203+
{ txRequestId: 'req-bulk-2', stakingParams: {} },
204+
],
205+
errors: [],
206+
});
207+
const resultStub = sinon
208+
.stub()
209+
.onFirstCall()
210+
.resolves({
211+
txRequests: [
212+
{
213+
txRequestId: 'req-bulk-1',
214+
apiVersion: 'full',
215+
transactions: [{ unsignedTx: { serializedTxHex: 'hex-bulk-1' } }],
216+
},
217+
],
218+
})
219+
.onSecondCall()
220+
.resolves({
221+
txRequests: [
222+
{
223+
txRequestId: 'req-bulk-2',
224+
apiVersion: 'full',
225+
transactions: [{ unsignedTx: { serializedTxHex: 'hex-bulk-2' } }],
226+
},
227+
],
228+
});
229+
const retryStub = sinon.stub().returns({ result: resultStub });
230+
const queryStub = sinon.stub().returns({ retry: retryStub });
231+
mockBitGo.get = sinon.stub().returns({ query: queryStub });
232+
mockBitGo.url = sinon.stub().returns('/mock-api/v2/wallet/test-wallet-id/txrequests');
233+
234+
const result = await wallet.buildResourceDelegations({ delegations });
235+
236+
result.prebuilds.should.have.length(2);
237+
result.prebuilds[0]!.txHex!.should.equal('hex-bulk-1');
238+
result.prebuilds[1]!.txHex!.should.equal('hex-bulk-2');
239+
sinon.assert.calledTwice(mockBitGo.get);
240+
});
241+
});
242+
});
243+
244+
// ---------------------------------------------------------------------------
245+
// buildResourceDelegations — non-TSS wallet, no txRequest fetch
246+
// ---------------------------------------------------------------------------
247+
describe('buildResourceDelegations non-TSS wallet — no txRequest fetch', function () {
248+
it('should NOT call getTxRequest for non-TSS wallet even when txRequestId is present', async function () {
249+
stubPost({ transactions: [{ txRequestId: 'req-hot', stakingParams: {} }], errors: [] });
250+
mockBitGo.get = sinon.stub();
251+
252+
await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
253+
254+
sinon.assert.notCalled(mockBitGo.get);
255+
});
116256
});
117257

118258
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)