Skip to content

Commit f2a4114

Browse files
committed
fix(sdk-core): pass txParams on EdDSA MPCv2 re-sign and PA paths
EdDSA MPCv2 verifyTransaction() compares txParams.recipients to parsed tx outputs before MPC signing. On re-sign and pending-approval flows txParams was never derived, causing a "Number of tx outputs does not match number of txParams recipients" error. Add txParamsFromIntent() which maps the persisted IntentRecipient shape on txRequest.intent into the flat ITransactionRecipient shape expected by verifyTransaction callers. Wire it in two places: - recreateTxRequest: derive txParams after fetching the fresh txRequest so PA approve → auto-sign completes correctly. - signTransactionTss: when buildParams is absent (re-sign via signAndSendTxRequest), fetch the txRequest and derive txParams so UI re-sign completes correctly. Existing buildParams / sendMany paths are unchanged; the new derivation only runs when buildParams is not already present. Ticket: WCI-765 Session-Id: 1c44b40e-24c1-49b1-b454-86318c9a44a2 Task-Id: bad80f2c-73b6-4826-a6ee-49cc4344544c
1 parent 5145634 commit f2a4114

2 files changed

Lines changed: 95 additions & 3 deletions

File tree

modules/sdk-core/src/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
IntentOptionsForMessage,
3232
IntentOptionsForTypedData,
3333
ITssUtils,
34+
PopulatedIntent,
3435
PopulatedIntentForMessageSigning,
3536
PopulatedIntentForTypedDataSigning,
3637
PrebuildTransactionWithIntentOptions,
@@ -579,8 +580,24 @@ export default class BaseTssUtils<KeyShare> extends MpcUtils implements ITssUtil
579580
async recreateTxRequest(txRequestId: string, decryptedPrv: string, reqId: IRequestTracer): Promise<TxRequest> {
580581
await this.deleteSignatureShares(txRequestId, reqId);
581582
// after delete signatures shares get the tx without them
582-
const txRequest = await getTxRequest(this.bitgo, this.wallet.id(), txRequestId, reqId);
583-
return await this.signTxRequest({ txRequest, prv: decryptedPrv, reqId });
583+
const txRequest = await this.getTxRequest(txRequestId, reqId);
584+
// EdDSA MPCv2 re-verifies the transaction against txParams.recipients before DSG starts.
585+
// On the PA path there is no SDK-local buildParams, so derive txParams from the persisted
586+
// intent. Other TSS variants either skip recipient verification or already work without txParams.
587+
let txParams;
588+
if (this.wallet.multisigTypeVersion() === 'MPCv2' && this.baseCoin.getMPCAlgorithm() === 'eddsa') {
589+
const intentRecipients = (txRequest.intent as PopulatedIntent | undefined)?.recipients;
590+
if (intentRecipients?.length) {
591+
txParams = {
592+
recipients: intentRecipients.map((r) => ({
593+
address: r.address.address,
594+
amount: String(r.amount.value),
595+
...(r.amount.symbol && { tokenName: r.amount.symbol }),
596+
})),
597+
};
598+
}
599+
}
600+
return await this.signTxRequest({ txRequest, prv: decryptedPrv, reqId, txParams });
584601
}
585602

586603
/**

modules/sdk-core/test/unit/bitgo/utils/tss/baseTSSUtils.ts

Lines changed: 76 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import * as sinon from 'sinon';
66
import * as sjcl from '@bitgo/sjcl';
77
import { DklsUtils } from '@bitgo/sdk-lib-mpc';
88

9-
import { BitGoBase, EcdsaMPCv2Utils, IBaseCoin, TxRequest } from '../../../../../src';
9+
import { BitGoBase, EcdsaMPCv2Utils, IBaseCoin, IWallet, RequestTracer, TxRequest } from '../../../../../src';
1010
import BaseTssUtils from '../../../../../src/bitgo/utils/tss/baseTSSUtils';
1111

1212
type BitgoGpgKeyPair = openpgp.SerializedKeyPair<string> & { revocationCertificate: string };
@@ -322,6 +322,81 @@ describe('Base TSS Utils', function () {
322322
});
323323
});
324324

325+
describe('recreateTxRequest', function () {
326+
function makeWallet(multisigTypeVersion: 'MPCv2' | undefined): IWallet {
327+
return {
328+
id: sinon.stub().returns('wallet-id'),
329+
multisigTypeVersion: sinon.stub().returns(multisigTypeVersion),
330+
} as unknown as IWallet;
331+
}
332+
333+
function makeCoin(mpcAlgorithm: 'eddsa' | 'ecdsa'): IBaseCoin {
334+
const coin = {} as IBaseCoin;
335+
coin.getHashFunction = sinon.stub();
336+
coin.getMPCAlgorithm = sinon.stub().returns(mpcAlgorithm);
337+
return coin;
338+
}
339+
340+
it('derives txParams from intent for EdDSA MPCv2 wallets', async function () {
341+
const txRequestId = 'tx-req-id-1';
342+
const reqId = new RequestTracer();
343+
const txRequest = buildTxRequest({
344+
txRequestId,
345+
intent: {
346+
intentType: 'payment',
347+
recipients: [{ address: { address: 'solAddr1' }, amount: { value: '5000000', symbol: 'tsol' } }],
348+
},
349+
});
350+
351+
const utils = new TestBaseTssUtils(mockBitgo, makeCoin('eddsa'), makeWallet('MPCv2'));
352+
sinon.stub(utils, 'deleteSignatureShares').resolves();
353+
sinon.stub(utils, 'getTxRequest').resolves(txRequest);
354+
const signTxRequestStub = sinon.stub(utils, 'signTxRequest').resolves(txRequest);
355+
356+
await utils.recreateTxRequest(txRequestId, 'prv', reqId);
357+
358+
assert.deepStrictEqual(signTxRequestStub.firstCall.args[0].txParams, {
359+
recipients: [{ address: 'solAddr1', amount: '5000000', tokenName: 'tsol' }],
360+
});
361+
});
362+
363+
it('passes undefined txParams for EdDSA MPCv2 when intent has no recipients', async function () {
364+
const txRequestId = 'tx-req-id-2';
365+
const reqId = new RequestTracer();
366+
const txRequest = buildTxRequest({ txRequestId, intent: { intentType: 'enableToken' } });
367+
368+
const utils = new TestBaseTssUtils(mockBitgo, makeCoin('eddsa'), makeWallet('MPCv2'));
369+
sinon.stub(utils, 'deleteSignatureShares').resolves();
370+
sinon.stub(utils, 'getTxRequest').resolves(txRequest);
371+
const signTxRequestStub = sinon.stub(utils, 'signTxRequest').resolves(txRequest);
372+
373+
await utils.recreateTxRequest(txRequestId, 'prv', reqId);
374+
375+
assert.strictEqual(signTxRequestStub.firstCall.args[0].txParams, undefined);
376+
});
377+
378+
it('passes undefined txParams for ECDSA MPCv2 wallets (guard does not apply)', async function () {
379+
const txRequestId = 'tx-req-id-3';
380+
const reqId = new RequestTracer();
381+
const txRequest = buildTxRequest({
382+
txRequestId,
383+
intent: {
384+
intentType: 'payment',
385+
recipients: [{ address: { address: 'ethAddr1' }, amount: { value: '1000000', symbol: 'eth' } }],
386+
},
387+
});
388+
389+
const utils = new TestBaseTssUtils(mockBitgo, makeCoin('ecdsa'), makeWallet('MPCv2'));
390+
sinon.stub(utils, 'deleteSignatureShares').resolves();
391+
sinon.stub(utils, 'getTxRequest').resolves(txRequest);
392+
const signTxRequestStub = sinon.stub(utils, 'signTxRequest').resolves(txRequest);
393+
394+
await utils.recreateTxRequest(txRequestId, 'prv', reqId);
395+
396+
assert.strictEqual(signTxRequestStub.firstCall.args[0].txParams, undefined);
397+
});
398+
});
399+
325400
describe('ECDSA MPC v2 delegated txRequest parsing', function () {
326401
it('getHashStringAndDerivationPath produces the correct hashBuffer and derivationPath', async function () {
327402
const signableHex = 'deadbeef';

0 commit comments

Comments
 (0)