From 5f01a83ddadbf0b3286a1b05790795565bcea49d Mon Sep 17 00:00:00 2001 From: Pranish Nepal Date: Wed, 10 Jun 2026 16:04:04 -0400 Subject: [PATCH] feat: implement async mode for generateWallet When asyncModeConfig.enabled, handleGenerateOnChainWallet submits directly to the bridge and returns { jobId, status: 'pending' } (202) without touching AWM or BitGo. We're only supporting multisig wallets for now. Ticket: WCN-886 --- .../api/master/generateWallet.test.ts | 112 +++++++++--------- .../integration/generateWallet.integ.test.ts | 50 ++++++++ .../integration/helpers/mockBridgeServer.ts | 36 ++++++ src/__tests__/integration/helpers/setup.ts | 16 ++- .../handlers/handleGenerateWallet.ts | 20 ++++ .../handlers/utils/asyncUtils.ts | 23 ++++ .../middleware/middleware.ts | 8 ++ .../routers/accelerateRoute.ts | 21 ++-- .../routers/consolidateUnspentsRoute.ts | 11 +- .../routers/generateWalletRoute.ts | 8 +- .../routers/masterBitGoExpressApiSpec.ts | 13 +- .../routers/recoveryRoute.ts | 13 +- src/types/request.ts | 2 + 13 files changed, 246 insertions(+), 87 deletions(-) create mode 100644 src/__tests__/integration/helpers/mockBridgeServer.ts create mode 100644 src/masterBitgoExpress/handlers/utils/asyncUtils.ts diff --git a/src/__tests__/api/master/generateWallet.test.ts b/src/__tests__/api/master/generateWallet.test.ts index 10ecf73d..4f0619e2 100644 --- a/src/__tests__/api/master/generateWallet.test.ts +++ b/src/__tests__/api/master/generateWallet.test.ts @@ -66,16 +66,10 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { let bitgo: BitGoAPI; - before(() => { - nock.disableNetConnect(); - nock.enableNetConnect('127.0.0.1'); - - // Create a BitGo instance that we'll use for stubbing - bitgo = new BitGoAPI({ env: 'test' }); - - const config: MasterExpressConfig = { + function makeConfig(overrides: Partial = {}): MasterExpressConfig { + return { appMode: AppMode.MASTER_EXPRESS, - port: 0, // Let OS assign a free port + port: 0, bind: 'localhost', timeout: 60000, httpLoggerFile: '', @@ -87,7 +81,17 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { tlsMode: TlsMode.DISABLED, clientCertAllowSelfSigned: true, asyncModeConfig: DEFAULT_ASYNC_MODE_CONFIG, + ...overrides, }; + } + + before(() => { + nock.disableNetConnect(); + nock.enableNetConnect('127.0.0.1'); + + bitgo = new BitGoAPI({ env: 'test' }); + + const config = makeConfig(); // Setup middleware stubs before creating app sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { @@ -109,25 +113,9 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { it('should generate an onchain wallet with separate backup AWM (separate-HSM mode)', async () => { const backupAwmUrl = 'http://backup-awm.invalid'; - // Override middleware to inject a separate backup client sinon.restore(); const backupBitgo = new BitGoAPI({ env: 'test' }); - const configWithBackup: MasterExpressConfig = { - appMode: AppMode.MASTER_EXPRESS, - port: 0, - bind: 'localhost', - timeout: 60000, - httpLoggerFile: '', - env: 'test', - disableEnvCheck: true, - authVersion: 2, - advancedWalletManagerUrl: advancedWalletManagerUrl, - advancedWalletManagerBackupUrl: backupAwmUrl, - awmServerCaCert: 'dummy-cert', - tlsMode: TlsMode.DISABLED, - clientCertAllowSelfSigned: true, - asyncModeConfig: DEFAULT_ASYNC_MODE_CONFIG, - }; + const configWithBackup = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl }); sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { (req as BitGoRequest).bitgo = backupBitgo; @@ -354,22 +342,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .get('/api/v1/client/constants') .reply(200, { constants: { mpc: { bitgoPublicKey: 'test-bitgo-public-key' } } }); const backupBitgo = new BitGoAPI({ env: 'test' }); - const configWithBackup: MasterExpressConfig = { - appMode: AppMode.MASTER_EXPRESS, - port: 0, - bind: 'localhost', - timeout: 60000, - httpLoggerFile: '', - env: 'test', - disableEnvCheck: true, - authVersion: 2, - advancedWalletManagerUrl: advancedWalletManagerUrl, - advancedWalletManagerBackupUrl: backupAwmUrl, - awmServerCaCert: 'dummy-cert', - tlsMode: TlsMode.DISABLED, - clientCertAllowSelfSigned: true, - asyncModeConfig: DEFAULT_ASYNC_MODE_CONFIG, - }; + const configWithBackup = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl }); sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { (req as BitGoRequest).bitgo = backupBitgo; @@ -1007,22 +980,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { .get('/api/v1/client/constants') .reply(200, { constants: { mpc: { bitgoMPCv2PublicKey: 'test-bitgo-public-key' } } }); const backupBitgo = new BitGoAPI({ env: 'test' }); - const configWithBackup: MasterExpressConfig = { - appMode: AppMode.MASTER_EXPRESS, - port: 0, - bind: 'localhost', - timeout: 60000, - httpLoggerFile: '', - env: 'test', - disableEnvCheck: true, - authVersion: 2, - advancedWalletManagerUrl: advancedWalletManagerUrl, - advancedWalletManagerBackupUrl: backupAwmUrl, - awmServerCaCert: 'dummy-cert', - tlsMode: TlsMode.DISABLED, - clientCertAllowSelfSigned: true, - asyncModeConfig: DEFAULT_ASYNC_MODE_CONFIG, - }; + const configWithBackup = makeConfig({ advancedWalletManagerBackupUrl: backupAwmUrl }); sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { (req as BitGoRequest).bitgo = backupBitgo; @@ -2420,6 +2378,44 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => { bitgoKeyNock.done(); }); + it('should return 202 with jobId when async mode is enabled for onchain wallet', async () => { + const bridgeUrl = 'http://bridge.invalid'; + const jobId = 'test-job-id-123'; + + sinon.restore(); + const asyncBitgo = new BitGoAPI({ env: 'test' }); + const asyncConfig = makeConfig({ + asyncModeConfig: { + enabled: true, + awmAsyncUrl: bridgeUrl, + pollIntervalInMs: 30000, + jobTtlInSeconds: 3600, + jobTtlMpcInSeconds: 7200, + }, + }); + + sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => { + (req as BitGoRequest).bitgo = asyncBitgo; + (req as BitGoRequest).config = asyncConfig; + next(); + }); + + const asyncApp = expressApp(asyncConfig); + const asyncAgent = request.agent(asyncApp); + + const bridgeNock = nock(bridgeUrl).post(`/api/${coin}/key/independent`).reply(202, { jobId }); + + const response = await asyncAgent + .post(`/api/v1/${coin}/advancedwallet/generate`) + .set('Authorization', `Bearer ${accessToken}`) + .send({ label: 'test_wallet', enterprise: 'test_enterprise', multisigType: 'onchain' }); + + response.status.should.equal(202); + response.body.should.have.property('jobId', jobId); + response.body.should.have.property('status', 'pending'); + bridgeNock.done(); + }); + it('should fail when evmKeyRingReferenceWalletId is provided for a non-EVM coin', async () => { const response = await agent .post(`/api/v1/${coin}/advancedwallet/generate`) diff --git a/src/__tests__/integration/generateWallet.integ.test.ts b/src/__tests__/integration/generateWallet.integ.test.ts index afd0ebcd..79a180a3 100644 --- a/src/__tests__/integration/generateWallet.integ.test.ts +++ b/src/__tests__/integration/generateWallet.integ.test.ts @@ -1,4 +1,5 @@ import 'should'; +import assert from 'assert'; import { startServices, IntegServices } from './helpers/setup'; import { LOCALHOST } from './helpers/servers'; import { SigningMode } from '../../shared/types'; @@ -118,3 +119,52 @@ describe('Generate wallet: EXTERNAL signing', () => { walletAddCalls.should.have.length(1); }); }); + +describe('Generate wallet: ASYNC mode', () => { + let services: IntegServices; + + before(async () => { + services = await startServices({ asyncMode: true }); + }); + + after(async () => { + await services.teardown(); + }); + + beforeEach(() => { + assert(services.bridge, 'bridge must be defined in async mode'); + services.bridge.calls.length = 0; + services.bitgo.calls.length = 0; + services.keyProvider.calls.length = 0; + }); + + it('submits onchain wallet generation to bridge and returns 202 with jobId', async () => { + const res = await fetch( + `http://${LOCALHOST}:${services.mbePort}/api/v1/tbtc/advancedwallet/generate`, + { + method: 'POST', + headers: { 'Content-Type': 'application/json', Authorization: 'Bearer test-token' }, + body: JSON.stringify({ + enterprise: 'test-enterprise', + label: 'test-wallet', + multisigType: 'onchain', + }), + }, + ); + + res.status.should.equal(202); + const body = await res.json(); + body.should.have.property('jobId', 'test-job-id'); + body.should.have.property('status', 'pending'); + + /** Bridge received exactly 1 submit call */ + assert(services.bridge, 'bridge must be defined in async mode'); + services.bridge.calls.should.have.length(1); + services.bridge.calls[0].path.should.equal('/api/tbtc/key/independent'); + + /** AWM and BitGo were never called — bridge owns the full job */ + services.keyProvider.calls.should.have.length(0); + services.bitgo.calls.filter((c) => c.path.endsWith('/key')).should.have.length(0); + services.bitgo.calls.filter((c) => c.path.endsWith('/wallet/add')).should.have.length(0); + }); +}); diff --git a/src/__tests__/integration/helpers/mockBridgeServer.ts b/src/__tests__/integration/helpers/mockBridgeServer.ts new file mode 100644 index 00000000..41c53246 --- /dev/null +++ b/src/__tests__/integration/helpers/mockBridgeServer.ts @@ -0,0 +1,36 @@ +import * as http from 'http'; +import express from 'express'; +import { listen, close } from './servers'; + +export interface MockBridgeCall { + method: string; + path: string; + body: unknown; +} + +export interface MockBridgeServer { + port: number; + calls: MockBridgeCall[]; + close(): Promise; +} + +export async function startMockBridgeServer(): Promise { + const calls: MockBridgeCall[] = []; + + const app = express(); + app.use(express.json()); + + app.use((req, _res, next) => { + calls.push({ method: req.method, path: req.path, body: req.body }); + next(); + }); + + app.post('*', (_req, res) => { + res.status(202).json({ jobId: 'test-job-id' }); + }); + + const server = http.createServer(app); + const port = await listen(server); + + return { port, calls, close: () => close(server) }; +} diff --git a/src/__tests__/integration/helpers/setup.ts b/src/__tests__/integration/helpers/setup.ts index 26d6cd5b..392d8ac7 100644 --- a/src/__tests__/integration/helpers/setup.ts +++ b/src/__tests__/integration/helpers/setup.ts @@ -6,17 +6,20 @@ import { DEFAULT_ASYNC_MODE_CONFIG } from '../../api/master/testUtils'; import { listen, close, LOCALHOST } from './servers'; import { startMockKeyProviderServer, MockKeyProviderServer } from './mockKeyProviderServer'; import { startMockBitgoServer, MockBitgoServer } from './mockBitgoServer'; +import { startMockBridgeServer, MockBridgeServer } from './mockBridgeServer'; export interface IntegServices { mbePort: number; keyProvider: MockKeyProviderServer; bitgo: MockBitgoServer; + bridge?: MockBridgeServer; teardown(): Promise; } export interface StartServicesOptions { signingMode?: SigningMode; recoveryMode?: boolean; + asyncMode?: boolean; } export async function startServices(opts: StartServicesOptions = {}): Promise { @@ -25,6 +28,7 @@ export async function startServices(opts: StartServicesOptions = {}): Promise, ) { + const asyncResult = await submitJobViaBridgeClient(req, { + path: `/api/${req.params.coin}/key/independent`, + body: req.decoded, + sources: [KeySource.USER, KeySource.BACKUP], + operationType: 'multisig_keygen', + }); + if (asyncResult) { + return asyncResult; + } + const bitgo = req.bitgo; const baseCoin = await coinFactory.getCoin(req.params.coin, bitgo); @@ -144,6 +156,10 @@ async function handleGenerateOnChainWallet( async function handleGenerateMpcWallet( req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>, ) { + if (req.config.asyncModeConfig.enabled) { + throw new BadRequestError('Async mode is not yet supported for TSS wallet generation'); + } + const bitgo = req.bitgo; const baseCoin = await coinFactory.getCoin(req.decoded.coin, bitgo); const awmClient = req.awmUserClient; @@ -227,6 +243,10 @@ async function handleGenerateMpcWallet( async function handleGenerateEvmKeyRingWallet( req: MasterApiSpecRouteRequest<'v1.wallet.generate', 'post'>, ) { + if (req.config.asyncModeConfig.enabled) { + throw new BadRequestError('Async mode is not yet supported for EVM keyring wallet generation'); + } + const bitgo = req.bitgo; const baseCoin = await coinFactory.getCoin(req.decoded.coin, bitgo); if (!baseCoin.isEVM()) { diff --git a/src/masterBitgoExpress/handlers/utils/asyncUtils.ts b/src/masterBitgoExpress/handlers/utils/asyncUtils.ts new file mode 100644 index 00000000..74b2a871 --- /dev/null +++ b/src/masterBitgoExpress/handlers/utils/asyncUtils.ts @@ -0,0 +1,23 @@ +import { SubmitParams } from '../../clients/bridgeClient.types'; +import { BitGoRequest } from '../../../types/request'; +import { MasterExpressConfig } from '../../../shared/types'; + +export const ASYNC_JOB_SUBMITTED_STATUS = 'pending' as const; +export type AsyncJobSubmittedStatus = typeof ASYNC_JOB_SUBMITTED_STATUS; +export type AsyncJobResponse = { jobId: string; status: AsyncJobSubmittedStatus }; + +/** + * Submits a signing or keygen job to the bridge and returns { jobId, status: 'pending' }. + * Returns null when async mode is off — callers must fall through to the sync path in that case. + */ +export async function submitJobViaBridgeClient( + req: BitGoRequest, + params: SubmitParams, +): Promise { + if (!req.config.asyncModeConfig.enabled) return null; + if (!req.bridgeClient) { + throw new Error('bridgeClient is required when async mode is enabled'); + } + const { jobId } = await req.bridgeClient.submit(params); + return { jobId, status: ASYNC_JOB_SUBMITTED_STATUS }; +} diff --git a/src/masterBitgoExpress/middleware/middleware.ts b/src/masterBitgoExpress/middleware/middleware.ts index 6d298d25..637d8afa 100644 --- a/src/masterBitgoExpress/middleware/middleware.ts +++ b/src/masterBitgoExpress/middleware/middleware.ts @@ -1,6 +1,7 @@ import { Request, Response, NextFunction } from 'express'; import { isMasterExpressConfig } from '../../shared/types'; import { createAwmClient, createAwmBackupClient } from '../clients/advancedWalletManagerClient'; +import { OsoBridgeClient } from '../clients/bridgeClient'; import { BitGoRequest } from '../../types/request'; /** @@ -41,5 +42,12 @@ export function validateMasterExpressConfig(req: Request, res: Response, next: N }); } + if (bitgoReq.config.asyncModeConfig.enabled) { + bitgoReq.bridgeClient = new OsoBridgeClient( + bitgoReq.config.asyncModeConfig.awmAsyncUrl, + bitgoReq.config.timeout, + ); + } + next(); } diff --git a/src/masterBitgoExpress/routers/accelerateRoute.ts b/src/masterBitgoExpress/routers/accelerateRoute.ts index f1ccd582..b1d3c766 100644 --- a/src/masterBitgoExpress/routers/accelerateRoute.ts +++ b/src/masterBitgoExpress/routers/accelerateRoute.ts @@ -52,26 +52,19 @@ export const AccelerateRequest = { feeMultiplier: optional(t.number), }; +export const AccelerateResponseCodec = t.type({ + txid: t.string, + tx: t.string, +}); +export type AccelerateResponseBody = t.TypeOf; + const AccelerateResponse: HttpResponse = { /** * Successful acceleration response. * @returns The signed transaction and its ID * @example { "txid": "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234", "tx": "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" } */ - 200: t.type({ - /** - * The transaction ID (hash) of the acceleration transaction. - * This can be used to track the transaction on a block explorer. - * @example "abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234" - */ - txid: t.string, - /** - * The full signed transaction in hexadecimal format. - * This transaction can be broadcast to the network. - * @example "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" - */ - tx: t.string, - }), + 200: AccelerateResponseCodec, ...ErrorResponses, }; diff --git a/src/masterBitgoExpress/routers/consolidateUnspentsRoute.ts b/src/masterBitgoExpress/routers/consolidateUnspentsRoute.ts index 1970aefb..fdb12d52 100644 --- a/src/masterBitgoExpress/routers/consolidateUnspentsRoute.ts +++ b/src/masterBitgoExpress/routers/consolidateUnspentsRoute.ts @@ -65,11 +65,14 @@ export const ConsolidateUnspentsRequest = { targetAddress: optional(t.string), }; +export const ConsolidateUnspentsResponseCodec = t.type({ + tx: t.string, + txid: t.string, +}); +export type ConsolidateUnspentsResponseBody = t.TypeOf; + export const ConsolidateUnspentsResponse: HttpResponse = { - 200: t.type({ - tx: t.string, - txid: t.string, - }), + 200: ConsolidateUnspentsResponseCodec, ...ErrorResponses, }; diff --git a/src/masterBitgoExpress/routers/generateWalletRoute.ts b/src/masterBitgoExpress/routers/generateWalletRoute.ts index a1fb2c8a..6dca7986 100644 --- a/src/masterBitgoExpress/routers/generateWalletRoute.ts +++ b/src/masterBitgoExpress/routers/generateWalletRoute.ts @@ -1,7 +1,7 @@ import { httpRequest, HttpResponse, httpRoute, optional } from '@api-ts/io-ts-http'; - import * as t from 'io-ts'; import { ErrorResponses } from '../../shared/errors'; +import { ASYNC_JOB_SUBMITTED_STATUS } from '../handlers/utils/asyncUtils'; const WalletType = t.intersection([ t.type({ @@ -275,8 +275,14 @@ const GenerateWalletResponseCodec = t.type({ export type GenerateWalletResponseBody = t.TypeOf; +export const AsyncJobResponseCodec = t.type({ + jobId: t.string, + status: t.literal(ASYNC_JOB_SUBMITTED_STATUS), +}); + const GenerateWalletResponse: HttpResponse = { 200: GenerateWalletResponseCodec, + 202: AsyncJobResponseCodec, ...ErrorResponses, }; diff --git a/src/masterBitgoExpress/routers/masterBitGoExpressApiSpec.ts b/src/masterBitgoExpress/routers/masterBitGoExpressApiSpec.ts index 1496e59f..0e03a64e 100644 --- a/src/masterBitgoExpress/routers/masterBitGoExpressApiSpec.ts +++ b/src/masterBitgoExpress/routers/masterBitGoExpressApiSpec.ts @@ -37,6 +37,15 @@ export function parseBody(req: express.Request, res: express.Response, next: exp return express.json({ limit: '20mb' })(req, res, next); } +/** + * TODO: union with other handler response types as they are async-ified (WCN-887 through WCN-893) + */ +type MasterBitGoAPIHandlerResponses = Awaited>; + +function toApiResponse(result: MasterBitGoAPIHandlerResponses) { + return 'jobId' in result ? Response.accepted(result) : Response.ok(result); +} + // API Specification export const MasterBitGoExpressApiSpec = apiSpec({ 'v1.wallet.generate': { @@ -75,7 +84,7 @@ export type MasterApiSpecRouteHandler< export type MasterApiSpecRouteRequest< ApiName extends keyof MasterApiSpec, Method extends keyof MasterApiSpec[ApiName] & HttpMethod, -> = BitGoRequest & Parameters>[0]; +> = BitGoRequest & Parameters>[0]; export type GenericMasterApiSpecRouteRequest = MasterApiSpecRouteRequest; @@ -97,7 +106,7 @@ export function createMasterApiRouter( responseHandler(async (req: express.Request) => { const typedReq = req as GenericMasterApiSpecRouteRequest; const result = await handleGenerateWallet(typedReq); - return Response.ok(result); + return toApiResponse(result); }), ]); diff --git a/src/masterBitgoExpress/routers/recoveryRoute.ts b/src/masterBitgoExpress/routers/recoveryRoute.ts index f7be5727..0e1aa947 100644 --- a/src/masterBitgoExpress/routers/recoveryRoute.ts +++ b/src/masterBitgoExpress/routers/recoveryRoute.ts @@ -191,19 +191,18 @@ export type CoinSpecificParamsUnion = /** * Response type for the wallet recovery endpoint. Returns the signed recovery transaction that can be broadcast to the network */ +export const RecoveryWalletResponseCodec = t.type({ + txHex: t.string, +}); +export type RecoveryWalletResponseBody = t.TypeOf; + const RecoveryWalletResponse: HttpResponse = { /** * Successful recovery response. * @returns The signed transaction in hex format * @example { "txHex": "01000000000101edd7a5d948a6c79f273ce686a6a8f2e96ed8c2583b5e77b866aa2a1b3426fbed0100000000ffffffff02102700000000000017a914192f23283c2a9e6c5d11562db0eb5d4eb47f460287b9bc2c000000000017a9145c139b242ab3701f321d2399d3a11b028b3b361e870247304402206ac9477fece38d96688c6c3719cb27396c0563ead0567457e7e884b406b6da8802201992d1cfa1b55a67ce8acb482e9957812487d2555f5f54fb0286ecd3095d78e4012103c92564575197c4d6e3d9792280e7548b3ba52a432101c62de2186c4e2fa7fc580000000000" } */ - 200: t.type({ - /** - * The full signed transaction in hexadecimal format. - * This transaction can be broadcast to the network to complete the recovery. - */ - txHex: t.string, - }), + 200: RecoveryWalletResponseCodec, ...ErrorResponses, }; diff --git a/src/types/request.ts b/src/types/request.ts index de0203ae..644b50f9 100644 --- a/src/types/request.ts +++ b/src/types/request.ts @@ -2,6 +2,7 @@ import express from 'express'; import { type BitGoAPI } from '@bitgo-beta/sdk-api'; import { Config } from '../shared/types'; import { AdvancedWalletManagerClient } from '../masterBitgoExpress/clients/advancedWalletManagerClient'; +import { BridgeClient } from '../masterBitgoExpress/clients/bridgeClient.types'; // Extended request type for BitGo Express export interface BitGoRequest extends express.Request { @@ -9,6 +10,7 @@ export interface BitGoRequest extends express.Request config: T; awmUserClient: AdvancedWalletManagerClient; awmBackupClient: AdvancedWalletManagerClient; + bridgeClient?: BridgeClient; } export function isBitGoRequest(req: express.Request): req is BitGoRequest {