diff --git a/src/__tests__/api/master/generateWallet.test.ts b/src/__tests__/api/master/generateWallet.test.ts index 206c2a6b..b3dbe218 100644 --- a/src/__tests__/api/master/generateWallet.test.ts +++ b/src/__tests__/api/master/generateWallet.test.ts @@ -6,7 +6,7 @@ import nock from 'nock'; import sinon from 'sinon'; import { app as expressApp } from '../../../masterBitGoExpressApp'; import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types'; -import { BitgoMpcGpgPubKeys, Environments } from '@bitgo-beta/sdk-core'; +import { BitgoMpcGpgPubKeys, Environments, Wallets } from '@bitgo-beta/sdk-core'; import { BitGoAPI } from '@bitgo-beta/sdk-api'; import * as middleware from '../../../shared/middleware'; import { BitGoRequest } from '../../../types/request'; @@ -348,6 +348,13 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { }); it('should generate a TSS MPC v1 (EdDSA) wallet with separate backup AWM', async () => { + // No MPCv2 setting for the family: the server still wants MPCv1 + nock(bitgoApiUrl) + .persist() + .get('/api/v2/tss/settings') + .query(true) + .reply(200, { coinSettings: {} }); + const backupAwmUrl = 'http://backup-awm.invalid'; sinon.restore(); @@ -652,6 +659,13 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { }); it('should generate a TSS MPC v1 wallet by calling the advanced wallet manager service', async () => { + // No MPCv2 setting for the family: the server still wants MPCv1 + nock(bitgoApiUrl) + .persist() + .get('/api/v2/tss/settings') + .query(true) + .reply(200, { coinSettings: {} }); + nock(bitgoApiUrl) .persist() .get('/api/v1/client/constants') @@ -2330,6 +2344,80 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { } }); + describe('EdDSA key generation routing by the tss settings multiSigTypeVersion', () => { + let generateStub: sinon.SinonStub; + let routingAgent: request.SuperAgentTest; + + beforeEach(() => { + sinon.restore(); + const configWithBackup = makeConfig({ + advancedWalletManagerBackupUrl: 'http://backup-awm.invalid', + }); + sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { + (req as BitGoRequest).bitgo = bitgo; + (req as BitGoRequest).config = configWithBackup; + next(); + }); + routingAgent = request.agent(expressApp(configWithBackup)); + generateStub = sinon + .stub(Wallets.prototype, 'generateWalletWithExternalSigner') + .resolves({ wallet: { toJSON: () => ({ id: 'wallet-id' }) } } as never); + }); + + const mockTssSettings = (family: string, walletCreationSettings: object) => + nock(bitgoApiUrl) + .get('/api/v2/tss/settings') + .query({ enterprise: 'test_enterprise' }) + .reply(200, { coinSettings: { [family]: { walletCreationSettings } } }); + + // Only the options handed to the SDK are asserted: the stubbed result is not a full wallet + // response, so the route's response encoding is expected to fail after the SDK call. + const generate = (c = eddsaCoin) => + routingAgent + .post(`/api/v1/${c}/advancedwallet/generate`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ label: 'test_wallet', enterprise: 'test_enterprise', multisigType: 'tss' }); + + it('uses the EdDSA MPCv2 callbacks when the setting is MPCv2', async () => { + mockTssSettings('sol', { multiSigTypeVersion: 'MPCv2' }); + await generate(); + + generateStub.calledOnce.should.be.true(); + const options = generateStub.firstCall.args[0]; + options.eddsaMPCv2Callbacks.should.have.keys( + 'initializeCallback', + 'round1Callback', + 'round2Callback', + 'finalizeCallback', + ); + options.should.not.have.property('eddsaCallbacks'); + }); + + it('keeps the EdDSA MPCv1 callbacks when the setting is absent', async () => { + mockTssSettings('sol', {}); + await generate(); + + const options = generateStub.firstCall.args[0]; + options.should.have.property('eddsaCallbacks'); + options.should.not.have.property('eddsaMPCv2Callbacks'); + }); + + it('uses the setting of the coin family only', async () => { + mockTssSettings('eth', { multiSigTypeVersion: 'MPCv2' }); + await generate(); + + generateStub.firstCall.args[0].should.have.property('eddsaCallbacks'); + }); + + it('does not read the setting for ECDSA key generation', async () => { + await generate(ecdsaCoin); + + const options = generateStub.firstCall.args[0]; + options.should.have.property('ecdsaMPCv2Callbacks'); + options.should.not.have.property('eddsaMPCv2Callbacks'); + }); + }); + it('should fail when multisig type is invalid / not provided', async () => { const response = await agent .post(`/api/v1/${coin}/advancedwallet/generate`) diff --git a/src/masterBitgoExpress/handlers/handleGenerateWallet.ts b/src/masterBitgoExpress/handlers/handleGenerateWallet.ts index c7dcfaaa..9ec30964 100644 --- a/src/masterBitgoExpress/handlers/handleGenerateWallet.ts +++ b/src/masterBitgoExpress/handlers/handleGenerateWallet.ts @@ -1,5 +1,6 @@ import assert from 'assert'; import { GenerateWalletWithExternalSignerOptions, MPCAlgorithm } from '@bitgo-beta/sdk-core'; +import { TssSettings } from '@bitgo/public-types'; import { MasterApiSpecRouteRequest } from '../routers/masterBitGoExpressApiSpec'; import coinFactory from '../../shared/coinFactory'; import { BadRequestError } from '../../shared/errors'; @@ -8,6 +9,7 @@ import { submitJobViaBridgeClient } from './utils/asyncUtils'; import { createEcdsaMPCv2KeyGenCallbacks, createEddsaKeyGenCallbacks, + createEddsaMPCv2KeyGenCallbacks, createOnchainKeyGenCallback, } from './walletGenerationCallbacks'; @@ -54,22 +56,41 @@ export async function handleGenerateWallet( ...req.decoded, type: 'advanced', multisigType: isTss ? 'tss' : 'onchain', - ...keyGenCallbacks(req, isTss ? baseCoin.getMPCAlgorithm() : undefined), + ...(await keyGenCallbacks(req, isTss ? baseCoin.getMPCAlgorithm() : undefined)), }); return { ...result, wallet: result.wallet.toJSON() }; } +/** + * The MPC version BitGo currently wants for new wallets, read from the same server-side setting + * BitGoJS uses (`/api/v2/tss/settings`, per enterprise and coin family), so moving to MPCv2 only + * is a server-side change. + */ +async function walletCreationMultisigTypeVersion( + req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>, +): Promise { + const baseCoin = await coinFactory.getCoin(req.decoded.coin, req.bitgo); + const tssSettings: TssSettings = await req.bitgo + .get(req.bitgo.microservicesUrl('/api/v2/tss/settings')) + .query({ enterprise: req.decoded.enterprise }) + .result(); + return tssSettings.coinSettings[baseCoin.getFamily()]?.walletCreationSettings + ?.multiSigTypeVersion; +} + /** * Picks the AWM key generation callbacks the SDK should drive, based on the coin's MPC algorithm. * An undefined algorithm means an onchain multisig wallet. */ -function keyGenCallbacks( +async function keyGenCallbacks( req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>, algorithm: MPCAlgorithm | undefined, -): Pick< - GenerateWalletWithExternalSignerOptions, - 'createKeychainCallback' | 'ecdsaMPCv2Callbacks' | 'eddsaCallbacks' +): Promise< + Pick< + GenerateWalletWithExternalSignerOptions, + 'createKeychainCallback' | 'ecdsaMPCv2Callbacks' | 'eddsaCallbacks' | 'eddsaMPCv2Callbacks' + > > { const { awmUserClient, awmBackupClient } = req; assert(awmUserClient, 'User AWM client not initialized'); @@ -85,6 +106,11 @@ function keyGenCallbacks( ecdsaMPCv2Callbacks: createEcdsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient), }; case 'eddsa': + if ((await walletCreationMultisigTypeVersion(req)) === 'MPCv2') { + return { + eddsaMPCv2Callbacks: createEddsaMPCv2KeyGenCallbacks(awmUserClient, awmBackupClient), + }; + } return { eddsaCallbacks: createEddsaKeyGenCallbacks(awmUserClient, awmBackupClient) }; default: throw new BadRequestError(