Skip to content

Commit 53557cb

Browse files
committed
feat(sdk-core): wire eddsaMPCv2Callbacks into generateWalletWithExternalSigner
Add eddsaMPCv2Callbacks field to GenerateWalletWithExternalSignerOptions and add routing in generateMpcWalletWithExternalSigner to dispatch to EddsaMPCv2Utils.createKeychainsWithExternalSigner when the field is set. Changes: - iWallets.ts: add eddsaMPCv2Callbacks?: EddsaMPCv2KeyGenCallbacks to GenerateWalletWithExternalSignerOptions - wallets.ts: update hasMpcCallbacks to include eddsaMPCv2Callbacks; add an EdDSA MPCv2 branch that calls EddsaMPCv2Utils; guard against passing both eddsaMPCv2Callbacks and eddsaCallbacks simultaneously - walletsExternalSigner.ts: new test block covering routing to EddsaMPCv2Utils, mutual-exclusion rejection, no-callbacks rejection, and MPCv1 path unchanged verification Ticket: WCI-917 Session-Id: 23a5c77a-c909-407f-bf3e-a820c9e88aea Task-Id: 38b68db1-6599-4911-9635-d408663b0d82
1 parent 78ad370 commit 53557cb

3 files changed

Lines changed: 232 additions & 8 deletions

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,7 @@ export interface GenerateWalletWithExternalSignerOptions
272272
keySignatures?: { backup: string; bitgo: string };
273273
ecdsaMPCv2Callbacks?: EcdsaMPCv2KeyGenCallbacks;
274274
eddsaCallbacks?: EddsaKeyGenCallbacks;
275+
eddsaMPCv2Callbacks?: EddsaMPCv2KeyGenCallbacks;
275276
}
276277

277278
export const GenerateLightningWalletOptionsCodec = t.intersection(

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

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -736,7 +736,7 @@ export class Wallets implements IWallets {
736736
params: GenerateWalletWithExternalSignerOptions
737737
): Promise<WalletWithKeychains> {
738738
const hasOnchainCallback = _.isFunction(params.createKeychainCallback);
739-
const hasMpcCallbacks = !!(params.ecdsaMPCv2Callbacks || params.eddsaCallbacks);
739+
const hasMpcCallbacks = !!(params.ecdsaMPCv2Callbacks || params.eddsaCallbacks || params.eddsaMPCv2Callbacks);
740740

741741
if (hasOnchainCallback && hasMpcCallbacks) {
742742
throw new Error('createKeychainCallback cannot be used together with MPC TSS key generation callbacks');
@@ -1802,13 +1802,25 @@ export class Wallets implements IWallets {
18021802
callbacks: params.ecdsaMPCv2Callbacks,
18031803
});
18041804
} else {
1805-
if (!params.eddsaCallbacks) {
1806-
throw new Error('eddsaCallbacks is required for EdDSA TSS wallet generation with external signer');
1805+
if (params.eddsaMPCv2Callbacks && params.eddsaCallbacks) {
1806+
throw new Error(
1807+
'eddsaMPCv2Callbacks and eddsaCallbacks cannot both be provided; use eddsaMPCv2Callbacks for EdDSA MPCv2'
1808+
);
1809+
}
1810+
if (params.eddsaMPCv2Callbacks) {
1811+
keychains = await new EDDSAUtils.EddsaMPCv2Utils(this.bitgo, this.baseCoin).createKeychainsWithExternalSigner({
1812+
enterprise,
1813+
callbacks: params.eddsaMPCv2Callbacks,
1814+
});
1815+
} else {
1816+
if (!params.eddsaCallbacks) {
1817+
throw new Error('eddsaCallbacks is required for EdDSA TSS wallet generation with external signer');
1818+
}
1819+
keychains = await new EDDSAUtils.default(this.bitgo, this.baseCoin).createKeychainsWithExternalSigner({
1820+
enterprise,
1821+
callbacks: params.eddsaCallbacks,
1822+
});
18071823
}
1808-
keychains = await new EDDSAUtils.default(this.bitgo, this.baseCoin).createKeychainsWithExternalSigner({
1809-
enterprise,
1810-
callbacks: params.eddsaCallbacks,
1811-
});
18121824
}
18131825

18141826
const { userKeychain, backupKeychain, bitgoKeychain } = keychains;

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

Lines changed: 212 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ import * as sinon from 'sinon';
33
import 'should';
44

55
import { Wallets } from '../../../../src/bitgo/wallet/wallets';
6-
import { ECDSAUtils } from '../../../../src/bitgo/utils';
6+
import { ECDSAUtils, EDDSAUtils } from '../../../../src/bitgo/utils';
77
import { CoinFeature } from '@bitgo/statics';
88
import {
99
CreateKeychainCallback,
1010
EcdsaMPCv2KeyGenCallbacks,
1111
EddsaKeyGenCallbacks,
12+
EddsaMPCv2KeyGenCallbacks,
1213
} from '../../../../src/bitgo/wallet/iWallets';
1314
import { Wallet } from '../../../../src/bitgo/wallet/wallet';
1415

@@ -713,6 +714,216 @@ describe('Wallets - external signer onchain wallet generation', function () {
713714
});
714715
});
715716

717+
describe('generateWalletWithExternalSigner - EdDSA MPCv2 TSS', function () {
718+
let eddsaMPCv2Callbacks: EddsaMPCv2KeyGenCallbacks;
719+
let eddsaMPCv2MockBaseCoin: any;
720+
let eddsaMPCv2Wallets: Wallets;
721+
722+
const commonKeychain = 'eddsampcv2-common-keychain';
723+
724+
beforeEach(function () {
725+
const mpcState = { encryptedData: 'data', encryptedDataKey: 'data-key' };
726+
eddsaMPCv2Callbacks = {
727+
initializeCallback: sinon.stub().resolves({
728+
userGpgPublicKey: 'user-gpg-pub',
729+
backupGpgPublicKey: 'backup-gpg-pub',
730+
userState: mpcState,
731+
backupState: mpcState,
732+
}),
733+
round1Callback: sinon.stub().resolves({
734+
userSignedMsg1: { from: 0, payload: '', signature: '' },
735+
backupSignedMsg1: { from: 1, payload: '', signature: '' },
736+
userState: mpcState,
737+
backupState: mpcState,
738+
}),
739+
round2Callback: sinon.stub().resolves({
740+
userSignedMsg2: { from: 0, payload: '', signature: '' },
741+
backupSignedMsg2: { from: 1, payload: '', signature: '' },
742+
userState: mpcState,
743+
backupState: mpcState,
744+
}),
745+
finalizeCallback: sinon.stub().resolves({ commonKeychain }),
746+
};
747+
748+
const mockEddsaMPCv2BitGo = {
749+
post: sinon.stub().returns({
750+
send: sinon.stub().returns({ result: sinon.stub().resolves({ id: 'eddsampcv2-wallet-id' }) }),
751+
}),
752+
setRequestTracer: sinon.stub(),
753+
};
754+
755+
eddsaMPCv2MockBaseCoin = {
756+
isEVM: sinon.stub().returns(false),
757+
supportsTss: sinon.stub().returns(true),
758+
getMPCAlgorithm: sinon.stub().returns('eddsa'),
759+
getFamily: sinon.stub().returns('sol'),
760+
getChain: sinon.stub().returns('tsol'),
761+
getDefaultMultisigType: sinon.stub().returns('tss'),
762+
keychains: sinon.stub().returns({ add: sinon.stub() }),
763+
url: sinon.stub().returns('/api/v2/tsol/wallet/add'),
764+
getConfig: sinon.stub().returns({ features: [] }),
765+
supplementGenerateWallet: sinon.stub().callsFake((params: any) => Promise.resolve(params)),
766+
};
767+
768+
eddsaMPCv2Wallets = new Wallets(mockEddsaMPCv2BitGo as any, eddsaMPCv2MockBaseCoin);
769+
sinon.stub(eddsaMPCv2Wallets as any, 'generateMpcWalletWithExternalSigner').resolves({
770+
responseType: 'WalletWithKeychains' as const,
771+
wallet: sinon.createStubInstance(Wallet),
772+
userKeychain: { id: 'user-key-id', commonKeychain, type: 'tss' },
773+
backupKeychain: { id: 'backup-key-id', commonKeychain, type: 'tss' },
774+
bitgoKeychain: { id: 'bitgo-key-id', commonKeychain, type: 'tss' },
775+
});
776+
});
777+
778+
it('should route to generateMpcWalletWithExternalSigner for tss EdDSA MPCv2 coin', async function () {
779+
const result = await eddsaMPCv2Wallets.generateWalletWithExternalSigner({
780+
label: 'EdDSA MPCv2 TSS Wallet',
781+
multisigType: 'tss',
782+
enterprise: 'enterprise-id',
783+
eddsaMPCv2Callbacks,
784+
});
785+
786+
result.responseType.should.equal('WalletWithKeychains');
787+
assert.strictEqual((eddsaMPCv2Wallets as any).generateMpcWalletWithExternalSigner.calledOnce, true);
788+
});
789+
790+
it('should route to EddsaMPCv2Utils.createKeychainsWithExternalSigner when eddsaMPCv2Callbacks provided', async function () {
791+
sinon.restore();
792+
eddsaMPCv2MockBaseCoin.getMPCAlgorithm = sinon.stub().returns('eddsa');
793+
794+
const keychainsTriplet = {
795+
userKeychain: { id: 'user-key-id', commonKeychain, type: 'tss' },
796+
backupKeychain: { id: 'backup-key-id', commonKeychain, type: 'tss' },
797+
bitgoKeychain: { id: 'bitgo-key-id', commonKeychain, type: 'tss' },
798+
};
799+
800+
// createKeychainsWithExternalSigner is added by WCI-916; inject a stub here so the
801+
// routing logic can be tested before that PR lands.
802+
const createKeychainsStub = sinon.stub().resolves(keychainsTriplet);
803+
(EDDSAUtils.EddsaMPCv2Utils.prototype as any).createKeychainsWithExternalSigner = createKeychainsStub;
804+
805+
const mockBitGoForIntegration = {
806+
post: sinon.stub().returns({
807+
send: sinon.stub().returns({ result: sinon.stub().resolves({ id: 'eddsampcv2-wallet-id' }) }),
808+
}),
809+
setRequestTracer: sinon.stub(),
810+
};
811+
812+
const integrationWallets = new Wallets(mockBitGoForIntegration as any, eddsaMPCv2MockBaseCoin);
813+
814+
await integrationWallets.generateWalletWithExternalSigner({
815+
label: 'EdDSA MPCv2 TSS Wallet',
816+
multisigType: 'tss',
817+
enterprise: 'enterprise-id',
818+
eddsaMPCv2Callbacks,
819+
});
820+
821+
assert.strictEqual(createKeychainsStub.calledOnce, true);
822+
createKeychainsStub.firstCall.args[0].should.deepEqual({
823+
enterprise: 'enterprise-id',
824+
callbacks: eddsaMPCv2Callbacks,
825+
});
826+
827+
delete (EDDSAUtils.EddsaMPCv2Utils.prototype as any).createKeychainsWithExternalSigner;
828+
});
829+
830+
it('should reject when both eddsaMPCv2Callbacks and eddsaCallbacks are provided', async function () {
831+
sinon.restore();
832+
eddsaMPCv2MockBaseCoin.getMPCAlgorithm = sinon.stub().returns('eddsa');
833+
834+
const eddsaCallbacks: EddsaKeyGenCallbacks = {
835+
initializeCallback: sinon.stub() as any,
836+
finalizeCallback: sinon.stub() as any,
837+
};
838+
839+
await eddsaMPCv2Wallets
840+
.generateWalletWithExternalSigner({
841+
label: 'EdDSA MPCv2 Conflict Wallet',
842+
multisigType: 'tss',
843+
enterprise: 'enterprise-id',
844+
eddsaMPCv2Callbacks,
845+
eddsaCallbacks,
846+
})
847+
.should.be.rejectedWith(
848+
'eddsaMPCv2Callbacks and eddsaCallbacks cannot both be provided; use eddsaMPCv2Callbacks for EdDSA MPCv2'
849+
);
850+
});
851+
852+
it('should reject without any eddsa callbacks for eddsa coin', async function () {
853+
sinon.restore();
854+
eddsaMPCv2MockBaseCoin.getMPCAlgorithm = sinon.stub().returns('eddsa');
855+
856+
await eddsaMPCv2Wallets
857+
.generateWalletWithExternalSigner({
858+
label: 'EdDSA MPCv2 No Callbacks Wallet',
859+
multisigType: 'tss',
860+
enterprise: 'enterprise-id',
861+
})
862+
.should.be.rejectedWith('eddsaCallbacks is required for EdDSA TSS wallet generation with external signer');
863+
});
864+
865+
it('should not call EddsaMPCv2Utils when eddsaCallbacks (MPCv1) is provided for eddsa coin', async function () {
866+
sinon.restore();
867+
eddsaMPCv2MockBaseCoin.getMPCAlgorithm = sinon.stub().returns('eddsa');
868+
869+
const eddsaCallbacks: EddsaKeyGenCallbacks = {
870+
initializeCallback: sinon.stub() as any,
871+
finalizeCallback: sinon.stub() as any,
872+
};
873+
874+
// createKeychainsWithExternalSigner is added by WCI-916; inject a stub here so the
875+
// routing logic can be tested before that PR lands.
876+
const mpcv2Stub = sinon.stub().resolves({
877+
userKeychain: { id: 'user-key-id', commonKeychain, type: 'tss' },
878+
backupKeychain: { id: 'backup-key-id', commonKeychain, type: 'tss' },
879+
bitgoKeychain: { id: 'bitgo-key-id', commonKeychain, type: 'tss' },
880+
});
881+
(EDDSAUtils.EddsaMPCv2Utils.prototype as any).createKeychainsWithExternalSigner = mpcv2Stub;
882+
883+
const mpcv1Stub = sinon.stub(EDDSAUtils.default.prototype, 'createKeychainsWithExternalSigner').resolves({
884+
userKeychain: { id: 'user-key-id', commonKeychain, type: 'tss' },
885+
backupKeychain: { id: 'backup-key-id', commonKeychain, type: 'tss' },
886+
bitgoKeychain: { id: 'bitgo-key-id', commonKeychain, type: 'tss' },
887+
});
888+
889+
const mockBitGoForIntegration = {
890+
post: sinon.stub().returns({
891+
send: sinon.stub().returns({ result: sinon.stub().resolves({ id: 'eddsa-wallet-id' }) }),
892+
}),
893+
setRequestTracer: sinon.stub(),
894+
};
895+
896+
const integrationWallets = new Wallets(mockBitGoForIntegration as any, eddsaMPCv2MockBaseCoin);
897+
898+
await integrationWallets.generateWalletWithExternalSigner({
899+
label: 'EdDSA MPCv1 TSS Wallet',
900+
multisigType: 'tss',
901+
enterprise: 'enterprise-id',
902+
eddsaCallbacks,
903+
});
904+
905+
assert.strictEqual(mpcv2Stub.callCount, 0);
906+
assert.strictEqual(mpcv1Stub.calledOnce, true);
907+
908+
delete (EDDSAUtils.EddsaMPCv2Utils.prototype as any).createKeychainsWithExternalSigner;
909+
});
910+
911+
it('should include eddsaMPCv2Callbacks in hasMpcCallbacks check', async function () {
912+
const hasMpcCallbacks = !!(eddsaMPCv2Callbacks as any);
913+
914+
await eddsaMPCv2Wallets
915+
.generateWalletWithExternalSigner({
916+
label: 'EdDSA MPCv2 No Callbacks Conflict',
917+
enterprise: 'enterprise-id',
918+
createKeychainCallback: sinon.stub() as any,
919+
eddsaMPCv2Callbacks,
920+
})
921+
.should.be.rejectedWith('createKeychainCallback cannot be used together with MPC TSS key generation callbacks');
922+
923+
assert.ok(hasMpcCallbacks);
924+
});
925+
});
926+
716927
describe('generateWallet with createKeychainCallback', function () {
717928
it('should delegate to generateWalletWithExternalSigner', async function () {
718929
const generateWalletWithExternalSignerStub = sinon.stub(wallets, 'generateWalletWithExternalSigner').resolves({

0 commit comments

Comments
 (0)