Skip to content

Commit 5f53e4a

Browse files
authored
Merge pull request #8789 from BitGo/CHALO-349-txHex-fixes
fix: txHex needs to be set for the txn in case of TSS
2 parents 8b304dd + 11e1a85 commit 5f53e4a

2 files changed

Lines changed: 155 additions & 0 deletions

File tree

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

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3526,6 +3526,22 @@ 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+
throw new Error(
3539+
`Expected serializedTxHex on TSS resource management prebuild for txRequestId ${prebuild.txRequestId}`
3540+
);
3541+
}
3542+
prebuild = _.extend({}, prebuild, { txHex: unsignedTx.serializedTxHex });
3543+
}
3544+
35293545
prebuild = _.extend({}, prebuild, { walletId: this.id() });
35303546
debug('final resource management transaction prebuild: %O', prebuild);
35313547
prebuilds.push(prebuild);

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

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,145 @@ 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 throw 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+
await (wallet.buildResourceDelegations({ delegations: [delegations[0]] }) as any).should.be.rejectedWith(
175+
/Expected serializedTxHex/
176+
);
177+
});
178+
179+
it('should NOT fetch txRequest when txHex is already present in the build response', async function () {
180+
stubPost({ transactions: [{ txRequestId: 'req-4', txHex: 'already-present' }], errors: [] });
181+
mockBitGo.get = sinon.stub();
182+
183+
const result = await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
184+
185+
result.prebuilds[0]!.txHex!.should.equal('already-present');
186+
sinon.assert.notCalled(mockBitGo.get);
187+
});
188+
189+
it('should NOT fetch txRequest when build response has no txRequestId', async function () {
190+
stubPost({ transactions: [{ stakingParams: {} }], errors: [] });
191+
mockBitGo.get = sinon.stub();
192+
193+
await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
194+
195+
sinon.assert.notCalled(mockBitGo.get);
196+
});
197+
198+
it('should fetch txRequest once per delegation for bulk delegations', async function () {
199+
stubPost({
200+
transactions: [
201+
{ txRequestId: 'req-bulk-1', stakingParams: {} },
202+
{ txRequestId: 'req-bulk-2', stakingParams: {} },
203+
],
204+
errors: [],
205+
});
206+
const resultStub = sinon
207+
.stub()
208+
.onFirstCall()
209+
.resolves({
210+
txRequests: [
211+
{
212+
txRequestId: 'req-bulk-1',
213+
apiVersion: 'full',
214+
transactions: [{ unsignedTx: { serializedTxHex: 'hex-bulk-1' } }],
215+
},
216+
],
217+
})
218+
.onSecondCall()
219+
.resolves({
220+
txRequests: [
221+
{
222+
txRequestId: 'req-bulk-2',
223+
apiVersion: 'full',
224+
transactions: [{ unsignedTx: { serializedTxHex: 'hex-bulk-2' } }],
225+
},
226+
],
227+
});
228+
const retryStub = sinon.stub().returns({ result: resultStub });
229+
const queryStub = sinon.stub().returns({ retry: retryStub });
230+
mockBitGo.get = sinon.stub().returns({ query: queryStub });
231+
mockBitGo.url = sinon.stub().returns('/mock-api/v2/wallet/test-wallet-id/txrequests');
232+
233+
const result = await wallet.buildResourceDelegations({ delegations });
234+
235+
result.prebuilds.should.have.length(2);
236+
result.prebuilds[0]!.txHex!.should.equal('hex-bulk-1');
237+
result.prebuilds[1]!.txHex!.should.equal('hex-bulk-2');
238+
sinon.assert.calledTwice(mockBitGo.get);
239+
});
240+
});
241+
});
242+
243+
// ---------------------------------------------------------------------------
244+
// buildResourceDelegations — non-TSS wallet, no txRequest fetch
245+
// ---------------------------------------------------------------------------
246+
describe('buildResourceDelegations non-TSS wallet — no txRequest fetch', function () {
247+
it('should NOT call getTxRequest for non-TSS wallet even when txRequestId is present', async function () {
248+
stubPost({ transactions: [{ txRequestId: 'req-hot', stakingParams: {} }], errors: [] });
249+
mockBitGo.get = sinon.stub();
250+
251+
await wallet.buildResourceDelegations({ delegations: [delegations[0]] });
252+
253+
sinon.assert.notCalled(mockBitGo.get);
254+
});
116255
});
117256

118257
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)