diff --git a/README.md b/README.md
index 560d6a7d..874289d3 100644
--- a/README.md
+++ b/README.md
@@ -41,6 +41,17 @@ Key features include:
- **Advanced Wallet Manager** (Port 3080) - An isolated signing server with no internet access that only connects to your key provider API implementation for key operations.
- **Master Express** (Port 3081) - An API gateway providing end-to-end wallet creation and transaction support, integrating [BitGo APIs](https://developers.bitgo.com/reference/overview#/) with secure communication to Advanced Wallet Manager.
+### User and Backup AWM Instances
+
+Master Express supports configuring **separate Advanced Wallet Manager instances** for user and backup key operations. This allows you to isolate the user key and backup key into independent AWM deployments, each connected to its own KMS/HSM, for stronger security and operational separation.
+
+- **User AWM** — Configured via `ADVANCED_WALLET_MANAGER_URL`. This is the primary AWM instance and handles all user key operations. It is always required.
+- **Backup AWM** — Configured via `ADVANCED_WALLET_MANAGER_BACKUP_URL`. This is an optional, separate AWM instance dedicated to backup key operations.
+
+**Fallback behavior:** If `ADVANCED_WALLET_MANAGER_BACKUP_URL` is not set, backup key operations automatically fall back to the user AWM instance. This means a single AWM instance handles both user and backup keys — which is the default behavior and sufficient for most deployments.
+
+When a backup AWM URL **is** provided, dedicated mTLS certificates for the backup AWM are also required (see [Backup AWM mTLS Settings](#backup-awm-mtls-settings) below). The backup AWM will not reuse the primary AWM's certificates.
+
## Installation
### Prerequisites
@@ -185,6 +196,16 @@ curl -X POST http://localhost:3081/ping/advancedWalletManager
| `BITGO_AUTH_VERSION` | BitGo authentication version | `2` | ❌ |
| `BITGO_CUSTOM_BITCOIN_NETWORK` | Custom Bitcoin network | - | ❌ |
+### Backup AWM Settings (Optional)
+
+These settings are only required when you want to use a **separate AWM instance for backup key operations**. If these are not configured, backup operations fall back to the primary (user) AWM instance.
+
+| Variable | Description | Default | Required |
+| ------------------------------------- | ------------------------------------ | ------- | ------------------------------------- |
+| `ADVANCED_WALLET_MANAGER_BACKUP_URL` | Backup AWM URL | - | Only if using a separate backup AWM |
+
+> **Note:** When `ADVANCED_WALLET_MANAGER_BACKUP_URL` is not set, the system uses the primary AWM (`ADVANCED_WALLET_MANAGER_URL`) for both user and backup key operations. This is the simplest configuration and works well when a single AWM instance manages both keys.
+
### Additional Settings
| Variable | Description | Default | Applies To |
@@ -233,6 +254,19 @@ curl -X POST http://localhost:3081/ping/advancedWalletManager
| `AWM_SERVER_CA_CERT` | AWM server CA certificate (alternative) | PEM string |
| `AWM_SERVER_CERT_ALLOW_SELF_SIGNED` | Allow self-signed AWM server certificates | Boolean (default: `false`) |
+**For Master Express → Backup Advanced Wallet Manager (optional):**
+
+These are only required when `ADVANCED_WALLET_MANAGER_BACKUP_URL` is set. If the backup URL is not configured, backup key operations use the primary AWM connection and these settings are ignored.
+
+| Variable | Description | Format |
+| ----------------------------------------- | -------------------------------------------------- | -------------------------- |
+| `AWM_BACKUP_CLIENT_TLS_KEY_PATH` | Backup AWM client private key file path | File path |
+| `AWM_BACKUP_CLIENT_TLS_KEY` | Backup AWM client private key (alternative) | PEM string |
+| `AWM_BACKUP_CLIENT_TLS_CERT_PATH` | Backup AWM client certificate file path | File path |
+| `AWM_BACKUP_CLIENT_TLS_CERT` | Backup AWM client certificate (alternative) | PEM string |
+| `AWM_BACKUP_SERVER_CA_CERT_PATH` | Backup AWM server CA certificate file path | File path |
+| `AWM_BACKUP_SERVER_CA_CERT` | Backup AWM server CA certificate (alternative) | PEM string |
+
**For Advanced Wallet Manager → key provider:**
| Variable | Description | Format |
@@ -245,7 +279,7 @@ curl -X POST http://localhost:3081/ping/advancedWalletManager
| `KEY_PROVIDER_SERVER_CA_CERT` | Key provider server CA certificate (alternative) | PEM string |
| `KEY_PROVIDER_SERVER_CERT_ALLOW_SELF_SIGNED` | Allow self-signed key provider server certificates | Boolean (default: `false`) |
-> **Note:** For security reasons, when `TLS_MODE=mtls`, outbound client certificates are required and cannot reuse server certificates. When `TLS_MODE=disabled`, these certificates aren't required.
+> **Note:** For security reasons, when `TLS_MODE=mtls`, outbound client certificates are required and cannot reuse server certificates. When a backup AWM is configured, it requires its own dedicated set of certificates — it will not reuse the primary AWM's certificates. When `TLS_MODE=disabled`, these certificates aren't required.
## Container Deployment with Podman
diff --git a/src/__tests__/api/master/awmBackupClient.test.ts b/src/__tests__/api/master/awmBackupClient.test.ts
new file mode 100644
index 00000000..4a7b9f6d
--- /dev/null
+++ b/src/__tests__/api/master/awmBackupClient.test.ts
@@ -0,0 +1,131 @@
+import 'should';
+import { AppMode, MasterExpressConfig, TlsMode } from '../../../shared/types';
+import {
+ createAwmClient,
+ createAwmBackupClient,
+} from '../../../masterBitgoExpress/clients/advancedWalletManagerClient';
+
+describe('AWM Backup Client', () => {
+ const baseConfig: MasterExpressConfig = {
+ appMode: AppMode.MASTER_EXPRESS,
+ port: 3081,
+ bind: 'localhost',
+ timeout: 60000,
+ httpLoggerFile: '',
+ env: 'test',
+ disableEnvCheck: true,
+ authVersion: 2,
+ advancedWalletManagerUrl: 'http://primary-awm.invalid',
+ awmServerCaCert: 'dummy-cert',
+ tlsMode: TlsMode.DISABLED,
+ clientCertAllowSelfSigned: true,
+ };
+
+ describe('createAwmBackupClient', () => {
+ it('should return undefined when no backup URL is configured', () => {
+ const result = createAwmBackupClient(baseConfig, 'tbtc');
+ (result === undefined).should.be.true();
+ });
+
+ it('should create a client when backup URL is configured', () => {
+ const config: MasterExpressConfig = {
+ ...baseConfig,
+ advancedWalletManagerBackupUrl: 'http://backup-awm.invalid',
+ };
+ const result = createAwmBackupClient(config, 'tbtc');
+ (result !== undefined).should.be.true();
+ });
+
+ it('should create a client pointing to the backup URL, not the primary', () => {
+ const config: MasterExpressConfig = {
+ ...baseConfig,
+ advancedWalletManagerBackupUrl: 'http://backup-awm.invalid',
+ };
+ const backupClient = createAwmBackupClient(config, 'tbtc');
+ const primaryClient = createAwmClient(config, 'tbtc');
+
+ // Both clients should exist
+ (backupClient !== undefined).should.be.true();
+ (primaryClient !== undefined).should.be.true();
+
+ // They should be different instances
+ (backupClient !== primaryClient).should.be.true();
+ });
+
+ it('should throw when backup URL is set with mTLS but backup server CA cert is missing', () => {
+ const config: MasterExpressConfig = {
+ ...baseConfig,
+ tlsMode: TlsMode.MTLS,
+ advancedWalletManagerBackupUrl: 'https://backup-awm.invalid',
+ awmServerCaCert: 'primary-ca-cert',
+ awmClientTlsKey: 'primary-client-key',
+ awmClientTlsCert: 'primary-client-cert',
+ // No backup-specific certs — should NOT fall back to primary
+ };
+ (() => createAwmBackupClient(config, 'tbtc')).should.throw(
+ /awmBackupServerCaCert is required/,
+ );
+ });
+
+ it('should throw when backup URL is set with mTLS but backup client certs are missing', () => {
+ const config: MasterExpressConfig = {
+ ...baseConfig,
+ tlsMode: TlsMode.MTLS,
+ advancedWalletManagerBackupUrl: 'https://backup-awm.invalid',
+ awmBackupServerCaCert: 'backup-ca-cert',
+ // No backup client certs
+ };
+ (() => createAwmBackupClient(config, 'tbtc')).should.throw(
+ /awmBackupClientTlsKey and awmBackupClientTlsCert are required/,
+ );
+ });
+
+ it('should create a client when all backup-specific certs are provided with mTLS', () => {
+ const config: MasterExpressConfig = {
+ ...baseConfig,
+ tlsMode: TlsMode.MTLS,
+ advancedWalletManagerBackupUrl: 'https://backup-awm.invalid',
+ awmServerCaCert: 'primary-ca-cert',
+ awmClientTlsKey: 'primary-client-key',
+ awmClientTlsCert: 'primary-client-cert',
+ awmBackupServerCaCert: 'backup-ca-cert',
+ awmBackupClientTlsKey: 'backup-client-key',
+ awmBackupClientTlsCert: 'backup-client-cert',
+ };
+ const result = createAwmBackupClient(config, 'tbtc');
+ (result !== undefined).should.be.true();
+ });
+ });
+
+ describe('fallback behavior in middleware', () => {
+ it('should use primary client for both user and backup when no backup URL is set', () => {
+ const primaryClient = createAwmClient(baseConfig, 'tbtc');
+ const backupClient = createAwmBackupClient(baseConfig, 'tbtc');
+
+ (primaryClient !== undefined).should.be.true();
+ // No backup URL → backup client is undefined → middleware falls back to primary
+ (backupClient === undefined).should.be.true();
+
+ // Middleware would do: awmBackupClient = backupClient ?? primaryClient
+ const effectiveBackupClient = backupClient ?? primaryClient;
+ (effectiveBackupClient === primaryClient).should.be.true();
+ });
+
+ it('should use separate client for backup when backup URL is set', () => {
+ const config: MasterExpressConfig = {
+ ...baseConfig,
+ advancedWalletManagerBackupUrl: 'http://backup-awm.invalid',
+ };
+ const primaryClient = createAwmClient(config, 'tbtc');
+ const backupClient = createAwmBackupClient(config, 'tbtc');
+
+ (primaryClient !== undefined).should.be.true();
+ (backupClient !== undefined).should.be.true();
+
+ // Middleware would do: awmBackupClient = backupClient ?? primaryClient
+ const effectiveBackupClient = backupClient ?? primaryClient;
+ (effectiveBackupClient === backupClient).should.be.true();
+ (effectiveBackupClient !== primaryClient).should.be.true();
+ });
+ });
+});
diff --git a/src/__tests__/api/master/generateWallet.test.ts b/src/__tests__/api/master/generateWallet.test.ts
index cc47cf73..5e5aba07 100644
--- a/src/__tests__/api/master/generateWallet.test.ts
+++ b/src/__tests__/api/master/generateWallet.test.ts
@@ -112,6 +112,128 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
sinon.restore();
});
+ 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,
+ };
+
+ sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => {
+ (req as BitGoRequest).bitgo = backupBitgo;
+ (req as BitGoRequest).config = configWithBackup;
+ next();
+ });
+
+ const app = expressApp(configWithBackup);
+ const backupAgent = request.agent(app);
+
+ // User keychain goes to primary AWM
+ const userKeychainNock = nock(advancedWalletManagerUrl)
+ .post(`/api/${coin}/key/independent`, {
+ source: 'user',
+ })
+ .reply(200, {
+ pub: 'xpub_user',
+ source: 'user',
+ type: 'independent',
+ });
+
+ // Backup keychain goes to backup AWM
+ const backupKeychainNock = nock(backupAwmUrl)
+ .post(`/api/${coin}/key/independent`, {
+ source: 'backup',
+ })
+ .reply(200, {
+ pub: 'xpub_backup',
+ source: 'backup',
+ type: 'independent',
+ });
+
+ const bitgoAddUserKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${coin}/key`, {
+ pub: 'xpub_user',
+ keyType: 'independent',
+ source: 'user',
+ })
+ .matchHeader('any', () => true)
+ .reply(200, { id: 'user-key-id', pub: 'xpub_user' });
+
+ const bitgoAddBackupKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${coin}/key`, {
+ pub: 'xpub_backup',
+ keyType: 'independent',
+ source: 'backup',
+ })
+ .matchHeader('any', () => true)
+ .reply(200, { id: 'backup-key-id', pub: 'xpub_backup' });
+
+ const bitgoAddBitGoKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${coin}/key`, {
+ source: 'bitgo',
+ keyType: 'independent',
+ enterprise: 'test_enterprise',
+ })
+ .reply(200, {
+ id: 'bitgo-key-id',
+ pub: 'xpub_bitgo',
+ source: 'bitgo',
+ type: 'independent',
+ isBitGo: true,
+ isTrust: false,
+ hsmType: 'institutional',
+ });
+
+ const bitgoAddWalletNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${coin}/wallet/add`)
+ .matchHeader('any', () => true)
+ .reply(
+ 200,
+ mockWalletResponse('new-wallet-id', coin, {
+ isCold: true,
+ pendingApprovals: [],
+ multisigType: 'onchain',
+ type: 'advanced',
+ }),
+ );
+
+ const response = await backupAgent
+ .post(`/api/v1/${coin}/advancedwallet/generate`)
+ .set('Authorization', `Bearer ${accessToken}`)
+ .send({
+ label: 'test_wallet',
+ enterprise: 'test_enterprise',
+ multisigType: 'onchain',
+ });
+
+ response.status.should.equal(200);
+ response.body.should.have.property('wallet');
+
+ // Verify user keychain went to primary AWM
+ userKeychainNock.done();
+ // Verify backup keychain went to backup AWM (separate HSM)
+ backupKeychainNock.done();
+ bitgoAddUserKeyNock.done();
+ bitgoAddBackupKeyNock.done();
+ bitgoAddBitGoKeyNock.done();
+ bitgoAddWalletNock.done();
+ });
+
it('should generate a wallet by calling the advanced wallet manager service', async () => {
const userKeychainNock = nock(advancedWalletManagerUrl)
.post(`/api/${coin}/key/independent`, {
@@ -226,14 +348,43 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
bitgoAddWalletNock.done();
});
- it('should generate a TSS MPC v1 wallet by calling the advanced wallet manager service', async () => {
- // Mock fetchConstants instead of using nock for URL mocking
- sinon.stub(bitgo, 'fetchConstants').resolves({
+ it('should generate a TSS MPC v1 (EdDSA) wallet with separate backup AWM', async () => {
+ const backupAwmUrl = 'http://backup-awm.invalid';
+
+ 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,
+ };
+
+ sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => {
+ (req as BitGoRequest).bitgo = backupBitgo;
+ (req as BitGoRequest).config = configWithBackup;
+ next();
+ });
+
+ const app = expressApp(configWithBackup);
+ const backupAgent = request.agent(app);
+
+ sinon.stub(backupBitgo, 'fetchConstants').resolves({
mpc: {
bitgoPublicKey: 'test-bitgo-public-key',
},
});
+ // User init goes to primary AWM
const userInitNock = nock(advancedWalletManagerUrl)
.post(`/api/${eddsaCoin}/mpc/key/initialize`, {
source: 'user',
@@ -253,7 +404,8 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
},
});
- const backupInitNock = nock(advancedWalletManagerUrl)
+ // Backup init goes to backup AWM
+ const backupInitNock = nock(backupAwmUrl)
.post(`/api/${eddsaCoin}/mpc/key/initialize`, {
source: 'backup',
bitgoGpgPub: 'test-bitgo-public-key',
@@ -340,6 +492,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
walletHSMGPGPublicKeySigs: 'hsm-sig',
});
+ // User finalize goes to primary AWM
const userFinalizeNock = nock(advancedWalletManagerUrl)
.post(`/api/${eddsaCoin}/mpc/key/finalize`, {
source: 'user',
@@ -399,6 +552,7 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
source: 'user',
commonKeychain: 'commonKeychain',
});
+
const addUserKeyNock = nock(bitgoApiUrl)
.post(`/api/v2/${eddsaCoin}/key`, {
commonKeychain: 'commonKeychain',
@@ -406,12 +560,14 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
type: 'tss',
})
.reply(200, {
- id: 'id',
+ id: 'user-key-id',
source: 'user',
type: 'tss',
commonKeychain: 'commonKeychain',
});
- const backupFinalizeNock = nock(advancedWalletManagerUrl)
+
+ // Backup finalize goes to backup AWM
+ const backupFinalizeNock = nock(backupAwmUrl)
.post(`/api/${eddsaCoin}/mpc/key/finalize`, {
source: 'backup',
encryptedDataKey: 'key',
@@ -469,80 +625,23 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
commonKeychain: 'commonKeychain',
})
.reply(200, {
- id: 'id',
+ id: 'backup-key-id',
source: 'backup',
type: 'tss',
commonKeychain: 'commonKeychain',
});
const addWalletNock = nock(bitgoApiUrl)
- .post(`/api/v2/${eddsaCoin}/wallet/add`, {
- label: 'test_wallet',
- enterprise: 'test_enterprise',
- multisigType: 'tss',
- coin: eddsaCoin,
- m: 2,
- n: 3,
- keys: ['id', 'id', 'id'],
- type: 'advanced',
- })
+ .post(`/api/v2/${eddsaCoin}/wallet/add`)
+ .matchHeader('any', () => true)
.reply(200, {
- id: 'wallet-id',
- users: [
- {
- user: 'user-id',
- permissions: ['admin', 'spend', 'view'],
- },
- ],
- coin: eddsaCoin,
- label: 'test_wallet',
- m: 2,
- n: 3,
- keys: ['id', 'id', 'id'],
- keySignatures: {},
- enterprise: 'test_enterprise',
- organization: 'org-id',
- bitgoOrg: 'BitGo Inc',
- tags: ['wallet-id', 'test_enterprise'],
- disableTransactionNotifications: false,
- freeze: {},
- deleted: false,
- approvalsRequired: 1,
- isCold: true,
- coinSpecific: {
- rootAddress: '74AUHib3F6Fq5eVm2ywP5ik9iQjviwAfZXWnGM9JHhJ4',
- pendingChainInitialization: true,
- minimumFunding: 2447136,
- lastChainIndex: ['Object'],
- nonceExpiresAt: '2025-06-25T23:00:12.019Z',
- trustedTokens: [],
- },
- admin: {},
- pendingApprovals: [],
- allowBackupKeySigning: false,
- clientFlags: [],
- walletFlags: [],
- recoverable: false,
- startDate: '2025-01-01T00:00:00.000Z',
- hasLargeNumberOfAddresses: false,
- config: {},
- balanceString: '0',
- confirmedBalanceString: '0',
- spendableBalanceString: '0',
- receiveAddress: {
- id: 'addr-id',
- address: '93AHaUAExampleRootAddress',
- chain: 0,
- index: 0,
- coin: eddsaCoin,
- wallet: 'wallet-id',
- coinSpecific: {},
- },
- multisigType: 'tss',
- type: 'advanced',
+ ...mockWalletResponse('wallet-id', eddsaCoin, {
+ multisigType: 'tss',
+ type: 'advanced',
+ }),
});
- const response = await agent
+ const response = await backupAgent
.post(`/api/v1/${eddsaCoin}/advancedwallet/generate`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
@@ -551,16 +650,994 @@ describe('POST /api/v1/:coin/advancedwallet/generate', () => {
multisigType: 'tss',
});
- // No need to check constantsNock since we're using sinon stub
+ response.status.should.equal(200);
+ response.body.should.have.property('wallet');
+
+ // Verify user operations went to primary AWM
userInitNock.done();
+ userFinalizeNock.done();
+ // Verify backup operations went to backup AWM
backupInitNock.done();
+ backupFinalizeNock.done();
+ // Verify BitGo API calls
bitgoAddKeychainNock.done();
- userFinalizeNock.done();
addUserKeyNock.done();
- backupFinalizeNock.done();
addBackupKeyNock.done();
addWalletNock.done();
- response.status.should.equal(200);
+ });
+
+ it('should generate a TSS MPC v1 wallet by calling the advanced wallet manager service', async () => {
+ // Mock fetchConstants instead of using nock for URL mocking
+ sinon.stub(bitgo, 'fetchConstants').resolves({
+ mpc: {
+ bitgoPublicKey: 'test-bitgo-public-key',
+ },
+ });
+
+ const userInitNock = nock(advancedWalletManagerUrl)
+ .post(`/api/${eddsaCoin}/mpc/key/initialize`, {
+ source: 'user',
+ bitgoGpgPub: 'test-bitgo-public-key',
+ })
+ .reply(200, {
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ bitgoPayload: {
+ from: 'user',
+ to: 'bitgo',
+ publicShare: 'public-share-user',
+ privateShare: 'private-share-user-to-bitgo',
+ privateShareProof: 'proof',
+ vssProof: 'proof',
+ gpgKey: 'user-key',
+ },
+ });
+
+ const backupInitNock = nock(advancedWalletManagerUrl)
+ .post(`/api/${eddsaCoin}/mpc/key/initialize`, {
+ source: 'backup',
+ bitgoGpgPub: 'test-bitgo-public-key',
+ counterPartyGpgPub: 'user-key',
+ })
+ .reply(200, {
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ bitgoPayload: {
+ from: 'backup',
+ to: 'bitgo',
+ publicShare: 'public-share-backup',
+ privateShare: 'private-share-backup-to-bitgo',
+ privateShareProof: 'proof',
+ vssProof: 'proof',
+ gpgKey: 'backup-key',
+ },
+ counterPartyKeyShare: {
+ from: 'backup',
+ to: 'user',
+ publicShare: 'public-share-backup',
+ privateShare: 'private-share-backup-to-user',
+ privateShareProof: 'proof',
+ vssProof: 'proof',
+ gpgKey: 'backup-key',
+ },
+ });
+
+ const bitgoAddKeychainNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${eddsaCoin}/key`, {
+ keyType: 'tss',
+ source: 'bitgo',
+ enterprise: 'test_enterprise',
+ keyShares: [
+ {
+ from: 'user',
+ to: 'bitgo',
+ publicShare: 'public-share-user',
+ privateShare: 'private-share-user-to-bitgo',
+ privateShareProof: 'proof',
+ vssProof: 'proof',
+ gpgKey: 'user-key',
+ },
+ {
+ from: 'backup',
+ to: 'bitgo',
+ publicShare: 'public-share-backup',
+ privateShare: 'private-share-backup-to-bitgo',
+ privateShareProof: 'proof',
+ vssProof: 'proof',
+ gpgKey: 'backup-key',
+ },
+ ],
+ userGPGPublicKey: 'user-key',
+ backupGPGPublicKey: 'backup-key',
+ })
+ .reply(200, {
+ id: 'id',
+ source: 'bitgo',
+ type: 'tss',
+ commonKeychain: 'commonKeychain',
+ verifiedVssProof: true,
+ isBitGo: true,
+ isTrust: true,
+ hsmType: 'institutional',
+ keyShares: [
+ {
+ from: 'bitgo',
+ to: 'user',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ vssProof: 'true',
+ gpgKey: 'bitgo-key',
+ },
+ {
+ from: 'bitgo',
+ to: 'backup',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ vssProof: 'true',
+ gpgKey: 'bitgo-key',
+ },
+ ],
+ walletHSMGPGPublicKeySigs: 'hsm-sig',
+ });
+
+ const userFinalizeNock = nock(advancedWalletManagerUrl)
+ .post(`/api/${eddsaCoin}/mpc/key/finalize`, {
+ source: 'user',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ counterPartyGpgPub: 'backup-key',
+ bitgoKeyChain: {
+ id: 'id',
+ source: 'bitgo',
+ type: 'tss',
+ commonKeychain: 'commonKeychain',
+ verifiedVssProof: true,
+ isBitGo: true,
+ isTrust: false,
+ hsmType: 'institutional',
+ keyShares: [
+ {
+ from: 'bitgo',
+ to: 'user',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ vssProof: 'true',
+ gpgKey: 'bitgo-key',
+ },
+ {
+ from: 'bitgo',
+ to: 'backup',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ vssProof: 'true',
+ gpgKey: 'bitgo-key',
+ },
+ ],
+ walletHSMGPGPublicKeySigs: 'hsm-sig',
+ },
+ coin: 'tsol',
+ counterPartyKeyShare: {
+ from: 'backup',
+ to: 'user',
+ publicShare: 'public-share-backup',
+ privateShare: 'private-share-backup-to-user',
+ privateShareProof: 'proof',
+ vssProof: 'proof',
+ gpgKey: 'backup-key',
+ },
+ })
+ .reply(200, {
+ counterpartyKeyShare: {
+ from: 'user',
+ to: 'backup',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ privateShareProof: 'privateShareProof',
+ vssProof: 'vssProof',
+ gpgKey: 'user-key',
+ },
+ source: 'user',
+ commonKeychain: 'commonKeychain',
+ });
+ const addUserKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${eddsaCoin}/key`, {
+ commonKeychain: 'commonKeychain',
+ source: 'user',
+ type: 'tss',
+ })
+ .reply(200, {
+ id: 'id',
+ source: 'user',
+ type: 'tss',
+ commonKeychain: 'commonKeychain',
+ });
+ const backupFinalizeNock = nock(advancedWalletManagerUrl)
+ .post(`/api/${eddsaCoin}/mpc/key/finalize`, {
+ source: 'backup',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ counterPartyGpgPub: 'user-key',
+ bitgoKeyChain: {
+ id: 'id',
+ source: 'bitgo',
+ type: 'tss',
+ commonKeychain: 'commonKeychain',
+ verifiedVssProof: true,
+ isBitGo: true,
+ isTrust: false,
+ hsmType: 'institutional',
+ keyShares: [
+ {
+ from: 'bitgo',
+ to: 'user',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ vssProof: 'true',
+ gpgKey: 'bitgo-key',
+ },
+ {
+ from: 'bitgo',
+ to: 'backup',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ vssProof: 'true',
+ gpgKey: 'bitgo-key',
+ },
+ ],
+ walletHSMGPGPublicKeySigs: 'hsm-sig',
+ },
+ coin: 'tsol',
+ counterPartyKeyShare: {
+ from: 'user',
+ to: 'backup',
+ publicShare: 'publicShare',
+ privateShare: 'privateShare',
+ privateShareProof: 'privateShareProof',
+ vssProof: 'vssProof',
+ gpgKey: 'user-key',
+ },
+ })
+ .reply(200, {
+ source: 'backup',
+ commonKeychain: 'commonKeychain',
+ });
+
+ const addBackupKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${eddsaCoin}/key`, {
+ source: 'backup',
+ type: 'tss',
+ commonKeychain: 'commonKeychain',
+ })
+ .reply(200, {
+ id: 'id',
+ source: 'backup',
+ type: 'tss',
+ commonKeychain: 'commonKeychain',
+ });
+
+ const addWalletNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${eddsaCoin}/wallet/add`, {
+ label: 'test_wallet',
+ enterprise: 'test_enterprise',
+ multisigType: 'tss',
+ coin: eddsaCoin,
+ m: 2,
+ n: 3,
+ keys: ['id', 'id', 'id'],
+ type: 'advanced',
+ })
+ .reply(200, {
+ id: 'wallet-id',
+ users: [
+ {
+ user: 'user-id',
+ permissions: ['admin', 'spend', 'view'],
+ },
+ ],
+ coin: eddsaCoin,
+ label: 'test_wallet',
+ m: 2,
+ n: 3,
+ keys: ['id', 'id', 'id'],
+ keySignatures: {},
+ enterprise: 'test_enterprise',
+ organization: 'org-id',
+ bitgoOrg: 'BitGo Inc',
+ tags: ['wallet-id', 'test_enterprise'],
+ disableTransactionNotifications: false,
+ freeze: {},
+ deleted: false,
+ approvalsRequired: 1,
+ isCold: true,
+ coinSpecific: {
+ rootAddress: '74AUHib3F6Fq5eVm2ywP5ik9iQjviwAfZXWnGM9JHhJ4',
+ pendingChainInitialization: true,
+ minimumFunding: 2447136,
+ lastChainIndex: ['Object'],
+ nonceExpiresAt: '2025-06-25T23:00:12.019Z',
+ trustedTokens: [],
+ },
+ admin: {},
+ pendingApprovals: [],
+ allowBackupKeySigning: false,
+ clientFlags: [],
+ walletFlags: [],
+ recoverable: false,
+ startDate: '2025-01-01T00:00:00.000Z',
+ hasLargeNumberOfAddresses: false,
+ config: {},
+ balanceString: '0',
+ confirmedBalanceString: '0',
+ spendableBalanceString: '0',
+ receiveAddress: {
+ id: 'addr-id',
+ address: '93AHaUAExampleRootAddress',
+ chain: 0,
+ index: 0,
+ coin: eddsaCoin,
+ wallet: 'wallet-id',
+ coinSpecific: {},
+ },
+ multisigType: 'tss',
+ type: 'advanced',
+ });
+
+ const response = await agent
+ .post(`/api/v1/${eddsaCoin}/advancedwallet/generate`)
+ .set('Authorization', `Bearer ${accessToken}`)
+ .send({
+ label: 'test_wallet',
+ enterprise: 'test_enterprise',
+ multisigType: 'tss',
+ });
+
+ // No need to check constantsNock since we're using sinon stub
+ userInitNock.done();
+ backupInitNock.done();
+ bitgoAddKeychainNock.done();
+ userFinalizeNock.done();
+ addUserKeyNock.done();
+ backupFinalizeNock.done();
+ addBackupKeyNock.done();
+ addWalletNock.done();
+ response.status.should.equal(200);
+ });
+
+ it('should generate a TSS MPC v2 (ECDSA) wallet with separate backup AWM', async () => {
+ const backupAwmUrl = 'http://backup-awm.invalid';
+
+ 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,
+ };
+
+ sinon.stub(middleware, 'prepareBitGo').callsFake(() => (req, res, next) => {
+ (req as BitGoRequest).bitgo = backupBitgo;
+ (req as BitGoRequest).config = configWithBackup;
+ next();
+ });
+
+ const app = expressApp(configWithBackup);
+ const backupAgent = request.agent(app);
+
+ sinon.stub(backupBitgo, 'fetchConstants').resolves({
+ mpc: {
+ bitgoMPCv2PublicKey: 'test-bitgo-public-key',
+ },
+ });
+
+ // Init: user goes to primary AWM, backup goes to backup AWM
+ const userInitNock = nock(advancedWalletManagerUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { source: 'user' })
+ .reply(200, {
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ gpgPub: 'test-user-public-key',
+ });
+
+ const backupInitNock = nock(backupAwmUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/initialize`, { source: 'backup' })
+ .reply(200, {
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ gpgPub: 'test-backup-public-key',
+ });
+
+ // Round 1: user goes to primary, backup goes to backup AWM
+ const userRound1Nock = nock(advancedWalletManagerUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'user',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 1,
+ bitgoGpgPub: 'test-bitgo-public-key',
+ counterPartyGpgPub: 'test-backup-public-key',
+ })
+ .reply(200, {
+ round: 2,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ broadcastMessage: {
+ from: 0,
+ payload: { message: 'test-broadcast-message-user-1', signature: 'test-signature-user-1' },
+ },
+ });
+
+ const backupRound1Nock = nock(backupAwmUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'backup',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 1,
+ bitgoGpgPub: 'test-bitgo-public-key',
+ counterPartyGpgPub: 'test-user-public-key',
+ })
+ .reply(200, {
+ round: 2,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ broadcastMessage: {
+ from: 1,
+ payload: {
+ message: 'test-broadcast-message-backup-1',
+ signature: 'test-signature-backup-1',
+ },
+ },
+ });
+
+ // BitGo round 1 & 2
+ const bitgoRound1And2Nock = nock(bitgoApiUrl)
+ .post(`/api/v2/mpc/generatekey`, {
+ enterprise: 'test-enterprise',
+ type: 'MPCv2',
+ round: 'MPCv2-R1',
+ payload: {
+ userGpgPublicKey: 'test-user-public-key',
+ backupGpgPublicKey: 'test-backup-public-key',
+ userMsg1: {
+ from: 0,
+ message: 'test-broadcast-message-user-1',
+ signature: 'test-signature-user-1',
+ },
+ backupMsg1: {
+ from: 1,
+ message: 'test-broadcast-message-backup-1',
+ signature: 'test-signature-backup-1',
+ },
+ walletId: undefined,
+ },
+ })
+ .reply(200, {
+ walletGpgPubKeySigs: 'test-wallet-gpg-pub-key-sigs',
+ sessionId: 'test-session-id',
+ bitgoMsg1: {
+ from: 2,
+ message: 'test-broadcast-message-bitgo-1',
+ signature: 'test-signature-bitgo-1',
+ },
+ bitgoToUserMsg2: {
+ from: 2,
+ to: 0,
+ encryptedMessage: 'test-p2p-message-bitgo-to-user-2',
+ signature: 'test-signature-bitgo-to-user-2',
+ },
+ bitgoToBackupMsg2: {
+ from: 2,
+ to: 1,
+ encryptedMessage: 'test-p2p-message-bitgo-to-backup-2',
+ signature: 'test-signature-bitgo-to-backup-2',
+ },
+ });
+
+ // Round 2: user to primary, backup to backup AWM
+ const userRound2Nock = nock(advancedWalletManagerUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'user',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 2,
+ broadcastMessages: {
+ bitgo: {
+ from: 2,
+ payload: {
+ message: 'test-broadcast-message-bitgo-1',
+ signature: 'test-signature-bitgo-1',
+ },
+ },
+ counterParty: {
+ from: 1,
+ payload: {
+ message: 'test-broadcast-message-backup-1',
+ signature: 'test-signature-backup-1',
+ },
+ },
+ },
+ })
+ .reply(200, {
+ round: 3,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ p2pMessages: {
+ bitgo: {
+ from: 0,
+ to: 2,
+ payload: {
+ encryptedMessage: 'test-p2p-message-user-to-bitgo-2',
+ signature: 'test-signature-user-to-bitgo-2',
+ },
+ commitment: 'test-commitment-user-2',
+ },
+ counterParty: {
+ from: 0,
+ to: 1,
+ payload: {
+ encryptedMessage: 'test-p2p-message-user-to-backup-2',
+ signature: 'test-signature-user-to-backup-2',
+ },
+ commitment: 'test-commitment-user-2',
+ },
+ },
+ });
+
+ const backupRound2Nock = nock(backupAwmUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'backup',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 2,
+ broadcastMessages: {
+ bitgo: {
+ from: 2,
+ payload: {
+ message: 'test-broadcast-message-bitgo-1',
+ signature: 'test-signature-bitgo-1',
+ },
+ },
+ counterParty: {
+ from: 0,
+ payload: {
+ message: 'test-broadcast-message-user-1',
+ signature: 'test-signature-user-1',
+ },
+ },
+ },
+ })
+ .reply(200, {
+ round: 3,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ p2pMessages: {
+ bitgo: {
+ from: 1,
+ to: 2,
+ payload: {
+ encryptedMessage: 'test-p2p-message-backup-to-bitgo-2',
+ signature: 'test-signature-backup-to-bitgo-2',
+ },
+ commitment: 'test-commitment-backup-2',
+ },
+ counterParty: {
+ from: 1,
+ to: 0,
+ payload: {
+ encryptedMessage: 'test-p2p-message-backup-to-user-2',
+ signature: 'test-signature-backup-to-user-2',
+ },
+ commitment: 'test-commitment-backup-2',
+ },
+ },
+ });
+
+ // Round 3: user to primary, backup to backup AWM
+ const userRound3Nock = nock(advancedWalletManagerUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'user',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 3,
+ p2pMessages: {
+ bitgo: {
+ from: 2,
+ to: 0,
+ payload: {
+ encryptedMessage: 'test-p2p-message-bitgo-to-user-2',
+ signature: 'test-signature-bitgo-to-user-2',
+ },
+ },
+ counterParty: {
+ from: 1,
+ to: 0,
+ payload: {
+ encryptedMessage: 'test-p2p-message-backup-to-user-2',
+ signature: 'test-signature-backup-to-user-2',
+ },
+ commitment: 'test-commitment-backup-2',
+ },
+ },
+ })
+ .reply(200, {
+ round: 4,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ p2pMessages: {
+ bitgo: {
+ from: 0,
+ to: 2,
+ payload: {
+ encryptedMessage: 'test-p2p-message-user-to-bitgo-3',
+ signature: 'test-signature-user-to-bitgo-3',
+ },
+ commitment: 'test-commitment-user-3',
+ },
+ counterParty: {
+ from: 0,
+ to: 1,
+ payload: {
+ encryptedMessage: 'test-p2p-message-user-to-backup-3',
+ signature: 'test-signature-user-to-backup-3',
+ },
+ commitment: 'test-commitment-user-3',
+ },
+ },
+ });
+
+ const backupRound3Nock = nock(backupAwmUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'backup',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 3,
+ p2pMessages: {
+ bitgo: {
+ from: 2,
+ to: 1,
+ payload: {
+ encryptedMessage: 'test-p2p-message-bitgo-to-backup-2',
+ signature: 'test-signature-bitgo-to-backup-2',
+ },
+ },
+ counterParty: {
+ from: 0,
+ to: 1,
+ payload: {
+ encryptedMessage: 'test-p2p-message-user-to-backup-2',
+ signature: 'test-signature-user-to-backup-2',
+ },
+ commitment: 'test-commitment-user-2',
+ },
+ },
+ })
+ .reply(200, {
+ round: 4,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ p2pMessages: {
+ bitgo: {
+ from: 1,
+ to: 2,
+ payload: {
+ encryptedMessage: 'test-p2p-message-backup-to-bitgo-3',
+ signature: 'test-signature-backup-to-bitgo-3',
+ },
+ commitment: 'test-commitment-backup-3',
+ },
+ counterParty: {
+ from: 1,
+ to: 0,
+ payload: {
+ encryptedMessage: 'test-p2p-message-backup-to-user-3',
+ signature: 'test-signature-backup-to-user-3',
+ },
+ commitment: 'test-commitment-backup-3',
+ },
+ },
+ });
+
+ // BitGo round 3
+ const bitgoRound3Nock = nock(bitgoApiUrl)
+ .post(`/api/v2/mpc/generatekey`, {
+ enterprise: 'test-enterprise',
+ type: 'MPCv2',
+ round: 'MPCv2-R2',
+ payload: {
+ sessionId: 'test-session-id',
+ userMsg2: {
+ from: 0,
+ to: 2,
+ encryptedMessage: 'test-p2p-message-user-to-bitgo-2',
+ signature: 'test-signature-user-to-bitgo-2',
+ },
+ userCommitment2: 'test-commitment-user-2',
+ backupMsg2: {
+ from: 1,
+ to: 2,
+ encryptedMessage: 'test-p2p-message-backup-to-bitgo-2',
+ signature: 'test-signature-backup-to-bitgo-2',
+ },
+ backupCommitment2: 'test-commitment-backup-2',
+ },
+ })
+ .reply(200, {
+ sessionId: 'test-session-id',
+ bitgoCommitment2: 'test-commitment-bitgo-2',
+ bitgoToUserMsg3: {
+ from: 2,
+ to: 0,
+ encryptedMessage: 'test-p2p-message-bitgo-to-user-3',
+ signature: 'test-signature-bitgo-to-user-3',
+ },
+ bitgoToBackupMsg3: {
+ from: 2,
+ to: 1,
+ encryptedMessage: 'test-p2p-message-bitgo-to-backup-3',
+ signature: 'test-signature-bitgo-to-backup-3',
+ },
+ });
+
+ // Round 4: user to primary, backup to backup AWM
+ const userRound4Nock = nock(advancedWalletManagerUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'user',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 4,
+ p2pMessages: {
+ bitgo: {
+ from: 2,
+ to: 0,
+ payload: {
+ encryptedMessage: 'test-p2p-message-bitgo-to-user-3',
+ signature: 'test-signature-bitgo-to-user-3',
+ },
+ commitment: 'test-commitment-bitgo-2',
+ },
+ counterParty: {
+ from: 1,
+ to: 0,
+ payload: {
+ encryptedMessage: 'test-p2p-message-backup-to-user-3',
+ signature: 'test-signature-backup-to-user-3',
+ },
+ commitment: 'test-commitment-backup-3',
+ },
+ },
+ })
+ .reply(200, {
+ round: 5,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ broadcastMessage: {
+ from: 0,
+ payload: { message: 'test-broadcast-message-user-4', signature: 'test-signature-user-4' },
+ },
+ });
+
+ const backupRound4Nock = nock(backupAwmUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/round`, {
+ source: 'backup',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ round: 4,
+ p2pMessages: {
+ bitgo: {
+ from: 2,
+ to: 1,
+ payload: {
+ encryptedMessage: 'test-p2p-message-bitgo-to-backup-3',
+ signature: 'test-signature-bitgo-to-backup-3',
+ },
+ commitment: 'test-commitment-bitgo-2',
+ },
+ counterParty: {
+ from: 0,
+ to: 1,
+ payload: {
+ encryptedMessage: 'test-p2p-message-user-to-backup-3',
+ signature: 'test-signature-user-to-backup-3',
+ },
+ commitment: 'test-commitment-user-3',
+ },
+ },
+ })
+ .reply(200, {
+ round: 5,
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ broadcastMessage: {
+ from: 1,
+ payload: {
+ message: 'test-broadcast-message-backup-4',
+ signature: 'test-signature-backup-4',
+ },
+ },
+ });
+
+ // BitGo round 4
+ const bitgoRound4Nock = nock(bitgoApiUrl)
+ .post(`/api/v2/mpc/generatekey`, {
+ enterprise: 'test-enterprise',
+ type: 'MPCv2',
+ round: 'MPCv2-R3',
+ payload: {
+ sessionId: 'test-session-id',
+ userMsg3: {
+ from: 0,
+ to: 2,
+ encryptedMessage: 'test-p2p-message-user-to-bitgo-3',
+ signature: 'test-signature-user-to-bitgo-3',
+ },
+ backupMsg3: {
+ from: 1,
+ to: 2,
+ encryptedMessage: 'test-p2p-message-backup-to-bitgo-3',
+ signature: 'test-signature-backup-to-bitgo-3',
+ },
+ userMsg4: {
+ from: 0,
+ message: 'test-broadcast-message-user-4',
+ signature: 'test-signature-user-4',
+ },
+ backupMsg4: {
+ from: 1,
+ message: 'test-broadcast-message-backup-4',
+ signature: 'test-signature-backup-4',
+ },
+ },
+ })
+ .reply(200, {
+ sessionId: 'test-session-id',
+ commonKeychain: 'commonKeychain',
+ bitgoMsg4: {
+ from: 2,
+ message: 'test-broadcast-message-bitgo-4',
+ signature: 'test-signature-bitgo-4',
+ },
+ });
+
+ // Finalize: user to primary, backup to backup AWM
+ const userFinalizeNock = nock(advancedWalletManagerUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/finalize`, {
+ source: 'user',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ broadcastMessages: {
+ bitgo: {
+ from: 2,
+ payload: {
+ message: 'test-broadcast-message-bitgo-4',
+ signature: 'test-signature-bitgo-4',
+ },
+ },
+ counterParty: {
+ from: 1,
+ payload: {
+ message: 'test-broadcast-message-backup-4',
+ signature: 'test-signature-backup-4',
+ },
+ },
+ },
+ bitgoCommonKeychain: 'commonKeychain',
+ })
+ .reply(200, { source: 'user', commonKeychain: 'commonKeychain' });
+
+ const backupFinalizeNock = nock(backupAwmUrl)
+ .post(`/api/${ecdsaCoin}/mpcv2/finalize`, {
+ source: 'backup',
+ encryptedDataKey: 'key',
+ encryptedData: 'data',
+ broadcastMessages: {
+ bitgo: {
+ from: 2,
+ payload: {
+ message: 'test-broadcast-message-bitgo-4',
+ signature: 'test-signature-bitgo-4',
+ },
+ },
+ counterParty: {
+ from: 0,
+ payload: {
+ message: 'test-broadcast-message-user-4',
+ signature: 'test-signature-user-4',
+ },
+ },
+ },
+ bitgoCommonKeychain: 'commonKeychain',
+ })
+ .reply(200, { source: 'backup', commonKeychain: 'commonKeychain' });
+
+ // Key creation on BitGo
+ const bitgoAddUserKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${ecdsaCoin}/key`, {
+ commonKeychain: 'commonKeychain',
+ source: 'user',
+ type: 'tss',
+ isMPCv2: true,
+ })
+ .reply(200, { id: 'user-key-id', source: 'user', type: 'tss' });
+
+ const bitgoAddBackupKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${ecdsaCoin}/key`, {
+ commonKeychain: 'commonKeychain',
+ source: 'backup',
+ type: 'tss',
+ isMPCv2: true,
+ })
+ .reply(200, { id: 'backup-key-id', source: 'backup', type: 'tss' });
+
+ const bitgoAddBitGoKeyNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${ecdsaCoin}/key`, {
+ commonKeychain: 'commonKeychain',
+ source: 'bitgo',
+ type: 'tss',
+ isMPCv2: true,
+ })
+ .reply(200, {
+ id: 'bitgo-key-id',
+ source: 'bitgo',
+ type: 'tss',
+ commonKeychain: 'commonKeychain',
+ isBitGo: true,
+ isTrust: false,
+ hsmType: 'institutional',
+ });
+
+ const bitgoAddWalletNock = nock(bitgoApiUrl)
+ .post(`/api/v2/${ecdsaCoin}/wallet/add`)
+ .matchHeader('any', () => true)
+ .reply(200, {
+ ...mockWalletResponse('new-wallet-id', ecdsaCoin, {
+ multisigType: 'tss',
+ type: 'advanced',
+ }),
+ });
+
+ const response = await backupAgent
+ .post(`/api/v1/${ecdsaCoin}/advancedwallet/generate`)
+ .set('Authorization', `Bearer ${accessToken}`)
+ .send({
+ label: 'test-wallet',
+ enterprise: 'test-enterprise',
+ multisigType: 'tss',
+ });
+
+ response.status.should.equal(200);
+ response.body.should.have.property('wallet');
+
+ // Verify user operations went to primary AWM
+ userInitNock.done();
+ userRound1Nock.done();
+ userRound2Nock.done();
+ userRound3Nock.done();
+ userRound4Nock.done();
+ userFinalizeNock.done();
+ // Verify backup operations went to backup AWM
+ backupInitNock.done();
+ backupRound1Nock.done();
+ backupRound2Nock.done();
+ backupRound3Nock.done();
+ backupRound4Nock.done();
+ backupFinalizeNock.done();
+ // Verify BitGo API calls
+ bitgoRound1And2Nock.done();
+ bitgoRound3Nock.done();
+ bitgoRound4Nock.done();
+ bitgoAddUserKeyNock.done();
+ bitgoAddBackupKeyNock.done();
+ bitgoAddBitGoKeyNock.done();
+ bitgoAddWalletNock.done();
});
it('should generate a TSS MPC v2 wallet by calling the advanced wallet manager service', async () => {
diff --git a/src/__tests__/api/master/nonRecovery.test.ts b/src/__tests__/api/master/nonRecovery.test.ts
index c8706072..0814299e 100644
--- a/src/__tests__/api/master/nonRecovery.test.ts
+++ b/src/__tests__/api/master/nonRecovery.test.ts
@@ -61,7 +61,7 @@ describe('Non Recovery Tests', () => {
beforeEach(() => {
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
(req as BitGoRequest).params = { coin };
- (req as BitGoRequest).awmClient = new AdvancedWalletManagerClient(
+ (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient(
config,
coin,
);
diff --git a/src/__tests__/api/master/recoveryWallet.test.ts b/src/__tests__/api/master/recoveryWallet.test.ts
index f2de4360..7c8e0f08 100644
--- a/src/__tests__/api/master/recoveryWallet.test.ts
+++ b/src/__tests__/api/master/recoveryWallet.test.ts
@@ -99,7 +99,7 @@ describe('Recovery Tests', () => {
// Setup coin middleware
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
(req as BitGoRequest).params = { coin };
- (req as BitGoRequest).awmClient = new AdvancedWalletManagerClient(
+ (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient(
config,
coin,
);
@@ -288,7 +288,7 @@ describe('Recovery Tests', () => {
// Setup coin middleware for ETH coin
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
(req as BitGoRequest).params = { coin: ethCoinId };
- (req as BitGoRequest).awmClient = new AdvancedWalletManagerClient(
+ (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient(
config,
ethCoinId,
);
@@ -381,7 +381,7 @@ describe('Recovery Tests', () => {
// Setup coin middleware for Solana coin
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
(req as BitGoRequest).params = { coin: solCoinId };
- (req as BitGoRequest).awmClient = new AdvancedWalletManagerClient(
+ (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient(
config,
solCoinId,
);
@@ -527,7 +527,7 @@ describe('Recovery Tests', () => {
// Setup coin middleware for Sui coin
sinon.stub(masterMiddleware, 'validateMasterExpressConfig').callsFake((req, res, next) => {
(req as BitGoRequest).params = { coin: suiCoinId };
- (req as BitGoRequest).awmClient = new AdvancedWalletManagerClient(
+ (req as BitGoRequest).awmUserClient = new AdvancedWalletManagerClient(
config,
suiCoinId,
);
diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts
index 747ce578..1ee549bf 100644
--- a/src/__tests__/config.test.ts
+++ b/src/__tests__/config.test.ts
@@ -54,6 +54,12 @@ describe('Configuration', () => {
delete process.env.AWM_CLIENT_TLS_CERT_PATH;
delete process.env.KEY_PROVIDER_SERVER_CA_CERT_PATH;
delete process.env.RECOVERY_MODE;
+ delete process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL;
+ delete process.env.AWM_BACKUP_SERVER_CA_CERT_PATH;
+ delete process.env.AWM_BACKUP_CLIENT_TLS_KEY_PATH;
+ delete process.env.AWM_BACKUP_CLIENT_TLS_CERT_PATH;
+ delete process.env.AWM_BACKUP_CLIENT_TLS_KEY;
+ delete process.env.AWM_BACKUP_CLIENT_TLS_CERT;
});
after(() => {
@@ -462,5 +468,187 @@ describe('Configuration', () => {
cfg.httpLoggerFile.should.equal('/tmp/test-http-access.log');
}
});
+
+ it('should not set backup URL when ADVANCED_WALLET_MANAGER_BACKUP_URL is not set', () => {
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ (cfg.advancedWalletManagerBackupUrl === undefined).should.be.true();
+ }
+ });
+
+ it('should read and protocol-process backup URL when configured', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_KEY = mockClientTlsKey;
+ process.env.AWM_BACKUP_CLIENT_TLS_CERT = mockClientTlsCert;
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ cfg.advancedWalletManagerBackupUrl!.should.equal('https://backup-awm.example.com:3080');
+ }
+ });
+
+ it('should use http protocol for backup URL when TLS is disabled', () => {
+ process.env.TLS_MODE = 'disabled';
+ delete process.env.AWM_SERVER_CA_CERT_PATH;
+ delete process.env.SERVER_TLS_KEY_PATH;
+ delete process.env.SERVER_TLS_CERT_PATH;
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ cfg.advancedWalletManagerBackupUrl!.should.equal('http://backup-awm.example.com:3080');
+ }
+ });
+
+ it('should throw error when backup URL is set without AWM_BACKUP_SERVER_CA_CERT_PATH in MTLS mode', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'https://backup-awm.example.com:3080';
+ (() => initConfig()).should.throw(
+ 'AWM_BACKUP_SERVER_CA_CERT_PATH environment variable is required for MTLS mode when provisioning a backup AWM URL.',
+ );
+ });
+
+ it('should load backup server CA cert from file', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_KEY = mockClientTlsKey;
+ process.env.AWM_BACKUP_CLIENT_TLS_CERT = mockClientTlsCert;
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ cfg.awmBackupServerCaCert!.should.be.a.String();
+ cfg.awmBackupServerCaCert!.length.should.be.greaterThan(0);
+ }
+ });
+
+ it('should not require backup cert paths when backup URL is not set in MTLS mode', () => {
+ // This verifies backward compatibility — no backup URL means no backup cert validation
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ (cfg.advancedWalletManagerBackupUrl === undefined).should.be.true();
+ (cfg.awmBackupServerCaCert === undefined).should.be.true();
+ }
+ });
+
+ it('should load backup client TLS key from file path', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_KEY_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/client.key',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_CERT = mockClientTlsCert;
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ cfg.awmBackupClientTlsKey!.should.be.a.String();
+ cfg.awmBackupClientTlsKey!.length.should.be.greaterThan(0);
+ }
+ });
+
+ it('should load backup client TLS cert from file path', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_KEY = mockClientTlsKey;
+ process.env.AWM_BACKUP_CLIENT_TLS_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/client.crt',
+ );
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ cfg.awmBackupClientTlsCert!.should.be.a.String();
+ cfg.awmBackupClientTlsCert!.length.should.be.greaterThan(0);
+ }
+ });
+
+ it('should throw error when backup client TLS key path points to a nonexistent file', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_KEY_PATH = '/nonexistent/path/client.key';
+ (() => initConfig()).should.throw(/Failed to read AWM backup client key/);
+ });
+
+ it('should throw error when backup client TLS cert path points to a nonexistent file', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_CERT_PATH = '/nonexistent/path/client.crt';
+ (() => initConfig()).should.throw(/Failed to read AWM backup client cert/);
+ });
+
+ it('should throw error when backup URL is set in mTLS mode but no backup or primary client certs are available', () => {
+ // Remove primary client certs so fallback also fails
+ delete process.env.AWM_CLIENT_TLS_KEY;
+ delete process.env.AWM_CLIENT_TLS_CERT;
+ delete process.env.AWM_CLIENT_TLS_KEY_PATH;
+ delete process.env.AWM_CLIENT_TLS_CERT_PATH;
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ // Primary cert validation fires first since both primary and backup certs are missing
+ (() => initConfig()).should.throw(
+ /AWM_CLIENT_TLS_KEY_PATH and AWM_CLIENT_TLS_CERT_PATH.*are required for outbound mTLS connections to Advanced Wallet Manager/,
+ );
+ });
+
+ it('should succeed when backup URL is set in mTLS mode and dedicated backup client certs are provided', () => {
+ process.env.ADVANCED_WALLET_MANAGER_BACKUP_URL = 'backup-awm.example.com:3080';
+ process.env.AWM_BACKUP_SERVER_CA_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/advanced-wallet-manager-cert.pem',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_KEY = mockClientTlsKey;
+ process.env.AWM_BACKUP_CLIENT_TLS_CERT = mockClientTlsCert;
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ cfg.advancedWalletManagerBackupUrl!.should.be.a.String();
+ }
+ });
+
+ it('should not load backup client certs from file when backup URL is not set', () => {
+ process.env.TLS_MODE = 'disabled';
+ delete process.env.AWM_SERVER_CA_CERT_PATH;
+ delete process.env.SERVER_TLS_KEY_PATH;
+ delete process.env.SERVER_TLS_CERT_PATH;
+ process.env.AWM_BACKUP_CLIENT_TLS_KEY_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/client.key',
+ );
+ process.env.AWM_BACKUP_CLIENT_TLS_CERT_PATH = path.resolve(
+ __dirname,
+ 'mocks/certs/client.crt',
+ );
+ const cfg = initConfig();
+ isMasterExpressConfig(cfg).should.be.true();
+ if (isMasterExpressConfig(cfg)) {
+ // Paths are stored in config but files are not loaded without a backup URL
+ (cfg.advancedWalletManagerBackupUrl === undefined).should.be.true();
+ (cfg.awmBackupClientTlsKey === undefined).should.be.true();
+ (cfg.awmBackupClientTlsCert === undefined).should.be.true();
+ }
+ });
});
});
diff --git a/src/initConfig.ts b/src/initConfig.ts
index 8e9c8a86..08cabe9d 100644
--- a/src/initConfig.ts
+++ b/src/initConfig.ts
@@ -31,6 +31,38 @@ function readEnvVar(name: string): string | undefined {
}
}
+function readCertFile(filePath: string, label: string): string {
+ try {
+ const content = fs.readFileSync(filePath, 'utf-8');
+ logger.info(`✓ ${label} loaded from file: ${filePath}`);
+ return content;
+ } catch (e) {
+ const err = e instanceof Error ? e : new Error(String(e));
+ throw new Error(`Failed to read ${label} from ${filePath}: ${err.message}`);
+ }
+}
+
+/**
+ * Loads a certificate/key value from either an environment variable or a file path.
+ * If the value is already set (from env), it logs and returns it.
+ * If a path is provided, it reads the file.
+ * Returns undefined if neither is set.
+ */
+function loadCert(
+ value: string | undefined,
+ path: string | undefined,
+ label: string,
+): string | undefined {
+ if (value) {
+ logger.info(`✓ ${label} loaded from environment variable`);
+ return value;
+ }
+ if (path) {
+ return readCertFile(path, label);
+ }
+ return undefined;
+}
+
function determineAppMode(): AppMode {
const mode = readEnvVar('APP_MODE') || readEnvVar('BITGO_APP_MODE');
if (!mode) {
@@ -182,7 +214,7 @@ function mergeAkmConfigs(
};
}
-function configureAdvancedWalletManagaerMode(): AdvancedWalletManagerConfig {
+function configureAdvancedWalletManagerMode(): AdvancedWalletManagerConfig {
const env = advancedWalletManagerEnvConfig();
let config = mergeAkmConfigs(env);
@@ -191,87 +223,29 @@ function configureAdvancedWalletManagaerMode(): AdvancedWalletManagerConfig {
// Only load certificates if TLS is enabled
if (config.tlsMode !== TlsMode.DISABLED) {
- // Handle file loading for TLS certificates
- if (!config.serverTlsKey && config.serverTlsKeyPath) {
- try {
- config = { ...config, serverTlsKey: fs.readFileSync(config.serverTlsKeyPath, 'utf-8') };
- logger.info(`✓ TLS private key loaded from file: ${config.serverTlsKeyPath}`);
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(`Failed to read TLS key from serverTlsKeyPath: ${err.message}`);
- }
- } else if (config.serverTlsKey) {
- logger.info('✓ TLS private key loaded from environment variable');
- }
-
- if (!config.serverTlsCert && config.serverTlsCertPath) {
- try {
- config = {
- ...config,
- serverTlsCert: fs.readFileSync(config.serverTlsCertPath, 'utf-8'),
- };
- logger.info(`✓ TLS certificate loaded from file: ${config.serverTlsCertPath}`);
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(`Failed to read TLS certificate from serverTlsCertPath: ${err.message}`);
- }
- } else if (config.serverTlsCert) {
- logger.info('✓ TLS certificate loaded from environment variable');
- }
-
if (!config.keyProviderServerCaCertPath) {
throw new Error('KEY_PROVIDER_SERVER_CA_CERT_PATH is required when TLS mode is MTLS');
}
- if (config.keyProviderServerCaCertPath) {
- try {
- config.keyProviderServerCaCert = fs.readFileSync(
- config.keyProviderServerCaCertPath,
- 'utf-8',
- );
- logger.info(
- `✓ key provider server CA certificate loaded from file: ${config.keyProviderServerCaCertPath}`,
- );
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(
- `Failed to read key provider TLS certificate from keyProviderTlsCert: ${err.message}`,
- );
- }
- }
- if (config.keyProviderClientTlsKeyPath) {
- try {
- config.keyProviderClientTlsKey = fs.readFileSync(
- config.keyProviderClientTlsKeyPath,
- 'utf-8',
- );
- logger.info(
- `✓ key provider client key loaded from file: ${config.keyProviderClientTlsKeyPath}`,
- );
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(
- `Failed to read key provider client key from keyProviderClientTlsKeyPath: ${err.message}`,
- );
- }
- }
-
- if (config.keyProviderClientTlsCertPath) {
- try {
- config.keyProviderClientTlsCert = fs.readFileSync(
- config.keyProviderClientTlsCertPath,
- 'utf-8',
- );
- logger.info(
- `✓ key provider client certificate loaded from file: ${config.keyProviderClientTlsCertPath}`,
- );
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(
- `Failed to read key provider client cert from keyProviderClientTlsCertPath: ${err.message}`,
- );
- }
- }
+ config = {
+ ...config,
+ serverTlsKey: loadCert(config.serverTlsKey, config.serverTlsKeyPath, 'TLS private key'),
+ serverTlsCert: loadCert(config.serverTlsCert, config.serverTlsCertPath, 'TLS certificate'),
+ keyProviderServerCaCert: readCertFile(
+ config.keyProviderServerCaCertPath,
+ 'Key provider server CA certificate',
+ ),
+ keyProviderClientTlsKey: loadCert(
+ config.keyProviderClientTlsKey,
+ config.keyProviderClientTlsKeyPath,
+ 'Key provider client key',
+ ),
+ keyProviderClientTlsCert: loadCert(
+ config.keyProviderClientTlsCert,
+ config.keyProviderClientTlsCertPath,
+ 'Key provider client certificate',
+ ),
+ };
// Validate that client certificates are provided for outbound mTLS connections
if (config.tlsMode === TlsMode.MTLS) {
@@ -321,7 +295,9 @@ function determineProtocol(url: string, tlsMode: TlsMode, isBitGo = false): stri
function masterExpressEnvConfig(): Partial {
const advancedWalletManagerUrl = readEnvVar('ADVANCED_WALLET_MANAGER_URL');
+ const advancedWalletManagerBackupUrl = readEnvVar('ADVANCED_WALLET_MANAGER_BACKUP_URL');
const awmServerCaCertPath = readEnvVar('AWM_SERVER_CA_CERT_PATH');
+ const awmBackupServerCaCertPath = readEnvVar('AWM_BACKUP_SERVER_CA_CERT_PATH');
const awmServerCertAllowSelfSigned = readEnvVar('AWM_SERVER_CERT_ALLOW_SELF_SIGNED') === 'true';
const tlsMode = determineTlsMode();
@@ -335,6 +311,12 @@ function masterExpressEnvConfig(): Partial {
throw new Error('AWM_SERVER_CA_CERT_PATH environment variable is required for MTLS mode.');
}
+ if (advancedWalletManagerBackupUrl && tlsMode === TlsMode.MTLS && !awmBackupServerCaCertPath) {
+ throw new Error(
+ 'AWM_BACKUP_SERVER_CA_CERT_PATH environment variable is required for MTLS mode when provisioning a backup AWM URL.',
+ );
+ }
+
// Debug mTLS environment variables
const clientCertAllowSelfSignedRaw = readEnvVar('CLIENT_CERT_ALLOW_SELF_SIGNED');
const clientCertAllowSelfSigned = clientCertAllowSelfSignedRaw === 'true';
@@ -354,11 +336,17 @@ function masterExpressEnvConfig(): Partial {
disableEnvCheck: readEnvVar('BITGO_DISABLE_ENV_CHECK') === 'true',
authVersion: Number(readEnvVar('BITGO_AUTH_VERSION')),
advancedWalletManagerUrl: advancedWalletManagerUrl,
+ advancedWalletManagerBackupUrl: advancedWalletManagerBackupUrl,
awmServerCaCertPath: awmServerCaCertPath,
awmClientTlsKeyPath: readEnvVar('AWM_CLIENT_TLS_KEY_PATH'),
awmClientTlsCertPath: readEnvVar('AWM_CLIENT_TLS_CERT_PATH'),
awmClientTlsKey: readEnvVar('AWM_CLIENT_TLS_KEY'),
awmClientTlsCert: readEnvVar('AWM_CLIENT_TLS_CERT'),
+ awmBackupServerCaCertPath: awmBackupServerCaCertPath,
+ awmBackupClientTlsKeyPath: readEnvVar('AWM_BACKUP_CLIENT_TLS_KEY_PATH'),
+ awmBackupClientTlsCertPath: readEnvVar('AWM_BACKUP_CLIENT_TLS_CERT_PATH'),
+ awmBackupClientTlsKey: readEnvVar('AWM_BACKUP_CLIENT_TLS_KEY'),
+ awmBackupClientTlsCert: readEnvVar('AWM_BACKUP_CLIENT_TLS_CERT'),
awmServerCertAllowSelfSigned,
customBitcoinNetwork: readEnvVar('BITGO_CUSTOM_BITCOIN_NETWORK'),
// mTLS server settings
@@ -398,12 +386,20 @@ function mergeMasterExpressConfigs(
disableEnvCheck: get('disableEnvCheck'),
authVersion: get('authVersion'),
advancedWalletManagerUrl: get('advancedWalletManagerUrl'),
+ advancedWalletManagerBackupUrl: get('advancedWalletManagerBackupUrl'),
awmServerCaCertPath: get('awmServerCaCertPath'),
awmServerCaCert: get('awmServerCaCert'),
awmClientTlsKeyPath: get('awmClientTlsKeyPath'),
awmClientTlsCertPath: get('awmClientTlsCertPath'),
awmClientTlsKey: get('awmClientTlsKey'),
awmClientTlsCert: get('awmClientTlsCert'),
+ // Backup AWM configs
+ awmBackupServerCaCertPath: get('awmBackupServerCaCertPath'),
+ awmBackupServerCaCert: get('awmBackupServerCaCert'),
+ awmBackupClientTlsKeyPath: get('awmBackupClientTlsKeyPath'),
+ awmBackupClientTlsCertPath: get('awmBackupClientTlsCertPath'),
+ awmBackupClientTlsKey: get('awmBackupClientTlsKey'),
+ awmBackupClientTlsCert: get('awmBackupClientTlsCert'),
awmServerCertAllowSelfSigned: get('awmServerCertAllowSelfSigned'),
customBitcoinNetwork: get('customBitcoinNetwork'),
serverTlsKeyPath: get('serverTlsKeyPath'),
@@ -433,6 +429,13 @@ export function configureMasterExpressMode(): MasterExpressConfig {
false,
);
}
+ if (config.advancedWalletManagerBackupUrl) {
+ updates.advancedWalletManagerBackupUrl = determineProtocol(
+ config.advancedWalletManagerBackupUrl,
+ config.tlsMode,
+ false,
+ );
+ }
config = { ...config, ...updates };
// Certificate Loading Section
@@ -440,79 +443,52 @@ export function configureMasterExpressMode(): MasterExpressConfig {
// Only load certificates if TLS is enabled
if (config.tlsMode !== TlsMode.DISABLED) {
- // Handle file loading for TLS certificates
- if (!config.serverTlsKey && config.serverTlsKeyPath) {
- try {
- config = { ...config, serverTlsKey: fs.readFileSync(config.serverTlsKeyPath, 'utf-8') };
- logger.info(`✓ TLS private key loaded from file: ${config.serverTlsKeyPath}`);
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(`Failed to read TLS key from serverTlsKeyPath: ${err.message}`);
- }
- } else if (config.serverTlsKey) {
- logger.info('✓ TLS private key loaded from environment variable');
- }
-
- if (!config.serverTlsCert && config.serverTlsCertPath) {
- try {
- config = {
- ...config,
- serverTlsCert: fs.readFileSync(config.serverTlsCertPath, 'utf-8'),
- };
- logger.info(`✓ TLS certificate loaded from file: ${config.serverTlsCertPath}`);
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(`Failed to read TLS certificate from serverTlsCertPath: ${err.message}`);
- }
- } else if (config.serverTlsCert) {
- logger.info('✓ TLS certificate loaded from environment variable');
- }
+ config = {
+ ...config,
+ serverTlsKey: loadCert(config.serverTlsKey, config.serverTlsKeyPath, 'TLS private key'),
+ serverTlsCert: loadCert(config.serverTlsCert, config.serverTlsCertPath, 'TLS certificate'),
+ };
// Validate that certificates are properly loaded when TLS is enabled
validateTlsCertificates(config);
}
// Handle cert loading for Advanced Wallet Manager (always required for Master Express)
- if (config.awmServerCaCertPath) {
- try {
- if (fs.existsSync(config.awmServerCaCertPath)) {
- config = {
- ...config,
- awmServerCaCert: fs.readFileSync(config.awmServerCaCertPath, 'utf-8'),
- };
- logger.info(
- `✓ AWM server CA certificate loaded from file: ${config.awmServerCaCertPath?.substring(
- 0,
- 50,
- )}...`,
- );
- } else {
- throw new Error(`Certificate file not found: ${config.awmServerCaCertPath}`);
- }
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(`Failed to read advanced wallet manager cert: ${err.message}`);
- }
- }
-
- if (config.awmClientTlsKeyPath) {
- try {
- config.awmClientTlsKey = fs.readFileSync(config.awmClientTlsKeyPath, 'utf-8');
- logger.info(`✓ AWM client key loaded from file: ${config.awmClientTlsKeyPath}`);
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(`Failed to read AWM client key from awmClientTlsKeyPath: ${err.message}`);
- }
- }
+ config = {
+ ...config,
+ awmServerCaCert: loadCert(
+ config.awmServerCaCert,
+ config.awmServerCaCertPath,
+ 'AWM server CA certificate',
+ ),
+ awmClientTlsKey: loadCert(config.awmClientTlsKey, config.awmClientTlsKeyPath, 'AWM client key'),
+ awmClientTlsCert: loadCert(
+ config.awmClientTlsCert,
+ config.awmClientTlsCertPath,
+ 'AWM client certificate',
+ ),
+ };
- if (config.awmClientTlsCertPath) {
- try {
- config.awmClientTlsCert = fs.readFileSync(config.awmClientTlsCertPath, 'utf-8');
- logger.info(`✓ AWM client certificate loaded from file: ${config.awmClientTlsCertPath}`);
- } catch (e) {
- const err = e instanceof Error ? e : new Error(String(e));
- throw new Error(`Failed to read AWM client cert from awmClientTlsCertPath: ${err.message}`);
- }
+ // Handle cert loading for backup AWM (only when backup URL is configured)
+ if (config.advancedWalletManagerBackupUrl) {
+ config = {
+ ...config,
+ awmBackupServerCaCert: loadCert(
+ config.awmBackupServerCaCert,
+ config.awmBackupServerCaCertPath,
+ 'AWM backup server CA certificate',
+ ),
+ awmBackupClientTlsKey: loadCert(
+ config.awmBackupClientTlsKey,
+ config.awmBackupClientTlsKeyPath,
+ 'AWM backup client key',
+ ),
+ awmBackupClientTlsCert: loadCert(
+ config.awmBackupClientTlsCert,
+ config.awmBackupClientTlsCertPath,
+ 'AWM backup client certificate',
+ ),
+ };
}
logger.info('==========================');
@@ -524,6 +500,20 @@ export function configureMasterExpressMode(): MasterExpressConfig {
'AWM_CLIENT_TLS_KEY_PATH and AWM_CLIENT_TLS_CERT_PATH (or AWM_CLIENT_TLS_KEY and AWM_CLIENT_TLS_CERT) are required for outbound mTLS connections to Advanced Wallet Manager. Client certificates cannot reuse server certificates for security reasons.',
);
}
+
+ // Validate that dedicated backup certificates are provided when a backup AWM URL is configured
+ if (config.advancedWalletManagerBackupUrl) {
+ if (!config.awmBackupServerCaCert) {
+ throw new Error(
+ 'AWM_BACKUP_SERVER_CA_CERT_PATH is required for mTLS communication with the backup Advanced Wallet Manager.',
+ );
+ }
+ if (!config.awmBackupClientTlsKey || !config.awmBackupClientTlsCert) {
+ throw new Error(
+ 'AWM_BACKUP_CLIENT_TLS_KEY_PATH and AWM_BACKUP_CLIENT_TLS_CERT_PATH (or AWM_BACKUP_CLIENT_TLS_KEY and AWM_BACKUP_CLIENT_TLS_CERT) are required for mTLS communication with the backup Advanced Wallet Manager.',
+ );
+ }
+ }
}
// Validate Master Express configuration
@@ -540,7 +530,7 @@ export function initConfig(): Config {
const appMode = determineAppMode();
if (appMode === AppMode.ADVANCED_WALLET_MANAGER) {
- return configureAdvancedWalletManagaerMode();
+ return configureAdvancedWalletManagerMode();
} else if (appMode === AppMode.MASTER_EXPRESS) {
return configureMasterExpressMode();
} else {
diff --git a/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts b/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts
index e17bc4be..10352446 100644
--- a/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts
+++ b/src/masterBitgoExpress/clients/advancedWalletManagerClient.ts
@@ -811,7 +811,7 @@ export class AdvancedWalletManagerClient {
/**
* Create an advanced wallet manager client if the configuration is present
*/
-export function createawmClient(
+export function createAwmClient(
cfg: MasterExpressConfig,
coin?: string,
): AdvancedWalletManagerClient | undefined {
@@ -823,3 +823,50 @@ export function createawmClient(
return undefined;
}
}
+
+/**
+ * Create a backup AWM client when a separate backup URL is configured.
+ * Requires dedicated backup certs when mTLS is enabled — does not fall back to primary certs.
+ * Returns undefined if no backup URL is configured (same-HSM mode).
+ */
+export function createAwmBackupClient(
+ cfg: MasterExpressConfig,
+ coin?: string,
+): AdvancedWalletManagerClient | undefined {
+ if (!cfg.advancedWalletManagerBackupUrl) {
+ return undefined;
+ }
+
+ if (cfg.tlsMode === TlsMode.MTLS) {
+ if (!cfg.awmBackupServerCaCert) {
+ throw new Error(
+ 'awmBackupServerCaCert is required for mTLS communication with the backup AWM. ' +
+ 'Set AWM_BACKUP_SERVER_CA_CERT_PATH to the backup AWM server CA certificate.',
+ );
+ }
+ if (!cfg.awmBackupClientTlsKey || !cfg.awmBackupClientTlsCert) {
+ throw new Error(
+ 'awmBackupClientTlsKey and awmBackupClientTlsCert are required for mTLS communication with the backup AWM. ' +
+ 'Set AWM_BACKUP_CLIENT_TLS_KEY_PATH and AWM_BACKUP_CLIENT_TLS_CERT_PATH to the backup AWM client credentials.',
+ );
+ }
+ }
+
+ try {
+ const backupConfig: MasterExpressConfig = {
+ ...cfg,
+ advancedWalletManagerUrl: cfg.advancedWalletManagerBackupUrl,
+ awmServerCaCert: cfg.awmBackupServerCaCert,
+ awmClientTlsKey: cfg.awmBackupClientTlsKey,
+ awmClientTlsCert: cfg.awmBackupClientTlsCert,
+ };
+ return new AdvancedWalletManagerClient(backupConfig, coin);
+ } catch (error) {
+ const err = error as Error;
+ throw new Error(
+ `Failed to create backup advanced wallet manager client: ${err.message}. ` +
+ `Backup AWM URL is configured (${cfg.advancedWalletManagerBackupUrl}) but the client could not be initialized. ` +
+ `Please verify your backup AWM TLS/mTLS configuration.`,
+ );
+ }
+}
diff --git a/src/masterBitgoExpress/handlers/ecdsa.ts b/src/masterBitgoExpress/handlers/ecdsa.ts
index 0fb81d42..74e10baa 100644
--- a/src/masterBitgoExpress/handlers/ecdsa.ts
+++ b/src/masterBitgoExpress/handlers/ecdsa.ts
@@ -107,6 +107,7 @@ interface OrchestrateEcdsaKeyGenParams {
bitgo: BitGoBase;
baseCoin: BaseCoin;
awmClient: AdvancedWalletManagerClient;
+ awmBackupClient: AdvancedWalletManagerClient;
enterprise: string;
walletParams: SupplementGenerateWalletOptions;
}
@@ -115,6 +116,7 @@ export async function orchestrateEcdsaKeyGen({
bitgo,
baseCoin,
awmClient,
+ awmBackupClient,
enterprise,
walletParams,
}: OrchestrateEcdsaKeyGenParams) {
@@ -135,7 +137,7 @@ export async function orchestrateEcdsaKeyGen({
) {
throw new Error('Missing required fields in user init response');
}
- const backupInitResponse = await awmClient.initEcdsaMpcV2KeyGenMpcV2({
+ const backupInitResponse = await awmBackupClient.initEcdsaMpcV2KeyGenMpcV2({
source: 'backup',
});
if (
@@ -155,7 +157,7 @@ export async function orchestrateEcdsaKeyGen({
bitgoGpgPub: constants.mpc.bitgoMPCv2PublicKey,
counterPartyGpgPub: backupInitResponse.gpgPub,
});
- const backupRound1Promise = awmClient.roundEcdsaMPCv2KeyGen({
+ const backupRound1Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({
source: 'backup',
encryptedData: backupInitResponse.encryptedData,
encryptedDataKey: backupInitResponse.encryptedDataKey,
@@ -200,7 +202,7 @@ export async function orchestrateEcdsaKeyGen({
counterParty: backupRound1Response.broadcastMessage,
},
});
- const backupRound2Promise = awmClient.roundEcdsaMPCv2KeyGen({
+ const backupRound2Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({
source: 'backup',
encryptedData: backupRound1Response.encryptedData,
encryptedDataKey: backupRound1Response.encryptedDataKey,
@@ -232,7 +234,7 @@ export async function orchestrateEcdsaKeyGen({
counterParty: backupRound2Response.p2pMessages?.counterParty,
},
});
- const backupRound3Promise = awmClient.roundEcdsaMPCv2KeyGen({
+ const backupRound3Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({
source: 'backup',
encryptedData: backupRound2Response.encryptedData,
encryptedDataKey: backupRound2Response.encryptedDataKey,
@@ -281,7 +283,7 @@ export async function orchestrateEcdsaKeyGen({
counterParty: backupRound3Response.p2pMessages?.counterParty,
},
});
- const backupRound4Promise = awmClient.roundEcdsaMPCv2KeyGen({
+ const backupRound4Promise = awmBackupClient.roundEcdsaMPCv2KeyGen({
source: 'backup',
encryptedData: backupRound3Response.encryptedData,
encryptedDataKey: backupRound3Response.encryptedDataKey,
@@ -328,7 +330,7 @@ export async function orchestrateEcdsaKeyGen({
},
bitgoCommonKeychain,
});
- const backupFinalizePromise = awmClient.finalizeEcdsaMPCv2KeyGen({
+ const backupFinalizePromise = awmBackupClient.finalizeEcdsaMPCv2KeyGen({
source: 'backup',
encryptedData: backupRound4Response.encryptedData,
encryptedDataKey: backupRound4Response.encryptedDataKey,
diff --git a/src/masterBitgoExpress/handlers/eddsa.ts b/src/masterBitgoExpress/handlers/eddsa.ts
index 0e0c4b54..5b91eec0 100644
--- a/src/masterBitgoExpress/handlers/eddsa.ts
+++ b/src/masterBitgoExpress/handlers/eddsa.ts
@@ -120,6 +120,7 @@ interface OrchestrateEddsaKeyGenParams {
bitgo: BitGoBase;
baseCoin: BaseCoin;
awmClient: AdvancedWalletManagerClient;
+ awmBackupClient: AdvancedWalletManagerClient;
enterprise: string;
walletParams: any;
}
@@ -128,6 +129,7 @@ export async function orchestrateEddsaKeyGen({
bitgo,
baseCoin,
awmClient,
+ awmBackupClient,
enterprise,
walletParams,
}: OrchestrateEddsaKeyGenParams) {
@@ -140,7 +142,7 @@ export async function orchestrateEddsaKeyGen({
source: 'user',
bitgoGpgKey: constants.mpc.bitgoPublicKey,
});
- const backupInitResponse = await awmClient.initMpcKeyGeneration({
+ const backupInitResponse = await awmBackupClient.initMpcKeyGeneration({
source: 'backup',
bitgoGpgKey: constants.mpc.bitgoPublicKey,
userGpgKey: userInitResponse.bitgoPayload.gpgKey,
@@ -197,7 +199,7 @@ export async function orchestrateEddsaKeyGen({
source: 'user',
type: 'tss',
});
- const backupKeychainPromise = await awmClient.finalizeMpcKeyGeneration({
+ const backupKeychainPromise = await awmBackupClient.finalizeMpcKeyGeneration({
source: 'backup',
coin: baseCoin.getFamily(),
encryptedDataKey: backupInitResponse.encryptedDataKey,
diff --git a/src/masterBitgoExpress/handlers/handleAccelerate.ts b/src/masterBitgoExpress/handlers/handleAccelerate.ts
index 2dda8b77..6057236b 100644
--- a/src/masterBitgoExpress/handlers/handleAccelerate.ts
+++ b/src/masterBitgoExpress/handlers/handleAccelerate.ts
@@ -6,7 +6,7 @@ import { getWalletAndSigningKeychain, makeCustomSigningFunction } from './utils/
export async function handleAccelerate(
req: MasterApiSpecRouteRequest<'v1.wallet.accelerate', 'post'>,
) {
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
const reqId = new RequestTracer();
const bitgo = req.bitgo;
const params = req.decoded;
diff --git a/src/masterBitgoExpress/handlers/handleConsolidate.ts b/src/masterBitgoExpress/handlers/handleConsolidate.ts
index 872b013b..9eedd5a1 100644
--- a/src/masterBitgoExpress/handlers/handleConsolidate.ts
+++ b/src/masterBitgoExpress/handlers/handleConsolidate.ts
@@ -12,7 +12,7 @@ import { signAndSendTxRequests } from './transactionRequests';
export async function handleConsolidate(
req: MasterApiSpecRouteRequest<'v1.wallet.consolidate', 'post'>,
) {
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
const reqId = new RequestTracer();
const bitgo = req.bitgo;
const params = req.decoded;
diff --git a/src/masterBitgoExpress/handlers/handleConsolidateUnspents.ts b/src/masterBitgoExpress/handlers/handleConsolidateUnspents.ts
index 484d1908..0842c8e0 100644
--- a/src/masterBitgoExpress/handlers/handleConsolidateUnspents.ts
+++ b/src/masterBitgoExpress/handlers/handleConsolidateUnspents.ts
@@ -6,7 +6,7 @@ import { getWalletAndSigningKeychain, makeCustomSigningFunction } from './utils/
export async function handleConsolidateUnspents(
req: MasterApiSpecRouteRequest<'v1.wallet.consolidateunspents', 'post'>,
) {
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
const reqId = new RequestTracer();
const bitgo = req.bitgo;
const params = req.decoded;
diff --git a/src/masterBitgoExpress/handlers/handleGenerateWallet.ts b/src/masterBitgoExpress/handlers/handleGenerateWallet.ts
index 8824827b..6c8c3b05 100644
--- a/src/masterBitgoExpress/handlers/handleGenerateWallet.ts
+++ b/src/masterBitgoExpress/handlers/handleGenerateWallet.ts
@@ -44,7 +44,8 @@ async function handleGenerateOnChainWallet(
const baseCoin = await coinFactory.getCoin(req.params.coin, bitgo);
// The awmClient is now available from the request
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
+ const awmBackupClient = req.awmBackupClient;
const reqId = new RequestTracer();
@@ -86,7 +87,7 @@ async function handleGenerateOnChainWallet(
};
const backupKeychainPromise = async (): Promise => {
- const backupKeychain = await awmClient.createIndependentKeychain({
+ const backupKeychain = await awmBackupClient.createIndependentKeychain({
source: 'backup',
coin: req.params.coin,
type: 'independent',
@@ -145,7 +146,8 @@ async function handleGenerateMpcWallet(
) {
const bitgo = req.bitgo;
const baseCoin = await coinFactory.getCoin(req.decoded.coin, bitgo);
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
+ const awmBackupClient = req.awmBackupClient;
if (!baseCoin.supportsTss()) {
throw new BadRequestError(
@@ -185,6 +187,7 @@ async function handleGenerateMpcWallet(
bitgo,
baseCoin,
awmClient,
+ awmBackupClient,
enterprise,
walletParams,
});
@@ -194,6 +197,7 @@ async function handleGenerateMpcWallet(
bitgo,
baseCoin,
awmClient,
+ awmBackupClient,
walletParams,
enterprise,
});
diff --git a/src/masterBitgoExpress/handlers/handleRecoveryConsolidations.ts b/src/masterBitgoExpress/handlers/handleRecoveryConsolidations.ts
index 35b88bff..4bd75667 100644
--- a/src/masterBitgoExpress/handlers/handleRecoveryConsolidations.ts
+++ b/src/masterBitgoExpress/handlers/handleRecoveryConsolidations.ts
@@ -41,7 +41,7 @@ export async function handleRecoveryConsolidations(
const bitgo = req.bitgo;
const coin = req.decoded.coin;
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
const isMPC = req.decoded.multisigType === 'tss';
diff --git a/src/masterBitgoExpress/handlers/handleSendMany.ts b/src/masterBitgoExpress/handlers/handleSendMany.ts
index 12462b44..6159c8de 100644
--- a/src/masterBitgoExpress/handlers/handleSendMany.ts
+++ b/src/masterBitgoExpress/handlers/handleSendMany.ts
@@ -72,7 +72,7 @@ async function createMPCSendParamsWithCustomSigningFns(
}
export async function handleSendMany(req: MasterApiSpecRouteRequest<'v1.wallet.sendMany', 'post'>) {
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
const reqId = new RequestTracer();
const bitgo = req.bitgo;
const baseCoin = await coinFactory.getCoin(req.params.coin, bitgo);
diff --git a/src/masterBitgoExpress/handlers/handleSignAndSendTxRequest.ts b/src/masterBitgoExpress/handlers/handleSignAndSendTxRequest.ts
index 15509c58..3a93fa4a 100644
--- a/src/masterBitgoExpress/handlers/handleSignAndSendTxRequest.ts
+++ b/src/masterBitgoExpress/handlers/handleSignAndSendTxRequest.ts
@@ -7,7 +7,7 @@ import coinFactory from '../../shared/coinFactory';
export async function handleSignAndSendTxRequest(
req: MasterApiSpecRouteRequest<'v1.wallet.txrequest.signAndSend', 'post'>,
) {
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
const reqId = new RequestTracer();
const bitgo = req.bitgo;
const baseCoin = await coinFactory.getCoin(req.params.coin, bitgo);
diff --git a/src/masterBitgoExpress/handlers/recoveryWallet.ts b/src/masterBitgoExpress/handlers/recoveryWallet.ts
index 87e59e80..581aa2ea 100644
--- a/src/masterBitgoExpress/handlers/recoveryWallet.ts
+++ b/src/masterBitgoExpress/handlers/recoveryWallet.ts
@@ -222,7 +222,7 @@ export async function handleRecoveryWallet(
const bitgo = req.bitgo;
const coin = req.decoded.coin;
- const awmClient = req.awmClient;
+ const awmClient = req.awmUserClient;
const { recoveryDestinationAddress, coinSpecificParams } = req.decoded;
const sdkCoin = await coinFactory.getCoin(coin, bitgo);
@@ -350,7 +350,7 @@ export async function handleRecoveryWallet(
}
if (isUtxoCoin(sdkCoin)) {
- return handleUtxoLikeRecovery(sdkCoin, req.awmClient, {
+ return handleUtxoLikeRecovery(sdkCoin, req.awmUserClient, {
userKey: userPub,
backupKey: backupPub,
bitgoKey: bitgoPub,
diff --git a/src/masterBitgoExpress/middleware/middleware.ts b/src/masterBitgoExpress/middleware/middleware.ts
index 7c2aea11..6d298d25 100644
--- a/src/masterBitgoExpress/middleware/middleware.ts
+++ b/src/masterBitgoExpress/middleware/middleware.ts
@@ -1,6 +1,6 @@
import { Request, Response, NextFunction } from 'express';
import { isMasterExpressConfig } from '../../shared/types';
-import { createawmClient } from '../clients/advancedWalletManagerClient';
+import { createAwmClient, createAwmBackupClient } from '../clients/advancedWalletManagerClient';
import { BitGoRequest } from '../../types/request';
/**
@@ -18,7 +18,7 @@ export function validateMasterExpressConfig(req: Request, res: Response, next: N
}
// Validate advanced wallet manager client
- const awmClient = createawmClient(bitgoReq.config, bitgoReq.params?.coin);
+ const awmClient = createAwmClient(bitgoReq.config, bitgoReq.params?.coin);
if (!awmClient) {
return res.status(500).json({
error: 'Please configure advanced wallet manager configs.',
@@ -27,6 +27,19 @@ export function validateMasterExpressConfig(req: Request, res: Response, next: N
}
// Attach the client to the request for use in route handlers
- bitgoReq.awmClient = awmClient;
+ bitgoReq.awmUserClient = awmClient;
+
+ // Create backup client if backup URL is configured; falls back to primary client
+ try {
+ const awmBackupClient = createAwmBackupClient(bitgoReq.config, bitgoReq.params?.coin);
+ bitgoReq.awmBackupClient = awmBackupClient ?? awmClient;
+ } catch (error) {
+ const err = error as Error;
+ return res.status(500).json({
+ error: 'Failed to initialize backup advanced wallet manager client.',
+ details: err.message,
+ });
+ }
+
next();
}
diff --git a/src/shared/types/index.ts b/src/shared/types/index.ts
index d961aaf9..d07d20da 100644
--- a/src/shared/types/index.ts
+++ b/src/shared/types/index.ts
@@ -61,6 +61,7 @@ export interface MasterExpressConfig extends BaseConfig {
authVersion?: number;
// AWM client settings
advancedWalletManagerUrl: string;
+ advancedWalletManagerBackupUrl?: string;
awmServerCaCertPath?: string;
awmServerCaCert?: string;
awmClientTlsKeyPath?: string;
@@ -68,6 +69,13 @@ export interface MasterExpressConfig extends BaseConfig {
awmClientTlsKey?: string;
awmClientTlsCert?: string;
awmServerCertAllowSelfSigned?: boolean;
+ // AWM backup client settings (separate HSM for backup key)
+ awmBackupServerCaCertPath?: string;
+ awmBackupServerCaCert?: string;
+ awmBackupClientTlsKeyPath?: string;
+ awmBackupClientTlsCertPath?: string;
+ awmBackupClientTlsKey?: string;
+ awmBackupClientTlsCert?: string;
customBitcoinNetwork?: string;
// mTLS server settings
serverTlsKeyPath?: string;
diff --git a/src/types/request.ts b/src/types/request.ts
index e4d61d5e..de0203ae 100644
--- a/src/types/request.ts
+++ b/src/types/request.ts
@@ -7,7 +7,8 @@ import { AdvancedWalletManagerClient } from '../masterBitgoExpress/clients/advan
export interface BitGoRequest extends express.Request {
bitgo: BitGoAPI;
config: T;
- awmClient: AdvancedWalletManagerClient;
+ awmUserClient: AdvancedWalletManagerClient;
+ awmBackupClient: AdvancedWalletManagerClient;
}
export function isBitGoRequest(req: express.Request): req is BitGoRequest {