Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions src/__tests__/integration/fixtures/bitgo/addKey.backup.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "backup-key-id",
"pub": "xpub_backup",
"keyType": "independent",
"source": "backup",
"type": "independent"
}
10 changes: 10 additions & 0 deletions src/__tests__/integration/fixtures/bitgo/addKey.bitgo.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"id": "bitgo-key-id",
"pub": "xpub_bitgo",
"keyType": "independent",
"source": "bitgo",
"type": "independent",
"isBitGo": true,
"isTrust": false,
"hsmType": "institutional"
}
7 changes: 7 additions & 0 deletions src/__tests__/integration/fixtures/bitgo/addKey.user.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"id": "user-key-id",
"pub": "xpub_user",
"keyType": "independent",
"source": "user",
"type": "independent"
}
42 changes: 42 additions & 0 deletions src/__tests__/integration/fixtures/bitgo/createWallet.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"id": "test-wallet-id",
"coin": "tbtc",
"label": "test-wallet",
"m": 2,
"n": 3,
"keys": ["user-key-id", "backup-key-id", "bitgo-key-id"],
"keySignatures": {},
"type": "advanced",
"multisigType": "onchain",
"isCold": true,
"enterprise": "test-enterprise",
"organization": "test-org",
"bitgoOrg": "BitGo Inc",
"tags": ["test-wallet-id", "test-enterprise"],
"disableTransactionNotifications": false,
"freeze": {},
"deleted": false,
"approvalsRequired": 1,
"coinSpecific": {},
"admin": {},
"allowBackupKeySigning": false,
"clientFlags": [],
"recoverable": false,
"startDate": "2025-01-01T00:00:00.000Z",
"hasLargeNumberOfAddresses": false,
"config": {},
"balanceString": "0",
"confirmedBalanceString": "0",
"spendableBalanceString": "0",
"pendingApprovals": [],
"receiveAddress": {
"id": "addr-id",
"address": "0xexampleaddress",
"chain": 0,
"index": 0,
"coin": "tbtc",
"wallet": "test-wallet-id",
"coinSpecific": {}
},
"users": [{ "user": "user-id", "permissions": ["admin", "spend", "view"] }]
}
120 changes: 120 additions & 0 deletions src/__tests__/integration/generateWallet.integ.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import 'should';
import { startServices, IntegServices } from './helpers/setup';
import { LOCALHOST } from './helpers/servers';
import { SigningMode } from '../../shared/types';
import type { GenerateWalletResponseBody } from '../../masterBitgoExpress/routers/generateWalletRoute';

describe('Generate wallet: LOCAL signing', () => {
let services: IntegServices;

before(async () => {
services = await startServices();
});

after(async () => {
await services.teardown();
});

beforeEach(() => {
services.keyProvider.calls.length = 0;
services.bitgo.calls.length = 0;
});

it('generates a tbtc onchain wallet end-to-end', 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(200);
const body = (await res.json()) as GenerateWalletResponseBody;
body.should.have.property('wallet');
body.wallet.should.have.property('id', 'test-wallet-id');

/** In local mode, AWM stores keys via POST /key — POST /key/generate must NOT be called */
const keyProviderStoreCalls = services.keyProvider.calls.filter((c) => c.path === '/key');
keyProviderStoreCalls.should.have.length(2);

const keyProviderGenerateCalls = services.keyProvider.calls.filter(
(c) => c.path === '/key/generate',
);
keyProviderGenerateCalls.should.have.length(0);

/** BitGo received 3 keychain adds */
const bitgoKeyCalls = services.bitgo.calls.filter(
(c) => c.method === 'POST' && c.path.endsWith('/key'),
);
bitgoKeyCalls.should.have.length(3);

/** and 1 wallet add */
const walletAddCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/wallet/add'));
walletAddCalls.should.have.length(1);
});
});

describe('Generate wallet: EXTERNAL signing', () => {
let services: IntegServices;

before(async () => {
services = await startServices({ signingMode: SigningMode.EXTERNAL });
});

after(async () => {
await services.teardown();
});

beforeEach(() => {
services.keyProvider.calls.length = 0;
services.bitgo.calls.length = 0;
});

it('generates a tbtc onchain wallet — key provider generates keys (external signing mode)', 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(200);
const body = (await res.json()) as GenerateWalletResponseBody;
body.should.have.property('wallet');
body.wallet.should.have.property('id', 'test-wallet-id');

/**
* In external mode, AWM delegates key generation to the key provider.
*/
const keyProviderGenerateCalls = services.keyProvider.calls.filter(
(c) => c.path === '/key/generate',
);
keyProviderGenerateCalls.should.have.length(2);

/** POST /key should NOT be called — AWM never generates keys locally in external mode */
const keyProviderStoreCalls = services.keyProvider.calls.filter((c) => c.path === '/key');
keyProviderStoreCalls.should.have.length(0);

/** BitGo receives 3 keychain adds */
const bitgoKeyCalls = services.bitgo.calls.filter(
(c) => c.method === 'POST' && c.path.endsWith('/key'),
);
bitgoKeyCalls.should.have.length(3);

/** and 1 wallet add */
const walletAddCalls = services.bitgo.calls.filter((c) => c.path.endsWith('/wallet/add'));
walletAddCalls.should.have.length(1);
});
});
18 changes: 10 additions & 8 deletions src/__tests__/integration/health.integ.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import * as http from 'http';
import { app as awmApp } from '../../advancedWalletManagerApp';
import { app as mbeApp } from '../../masterBitGoExpressApp';
import { AppMode, TlsMode, SigningMode } from '../../shared/types';
import { listen, close } from './helpers/servers';
import { listen, close, LOCALHOST } from './helpers/servers';

describe('integration — health checks', () => {
describe('Integration Test — health checks', () => {
let awmServer: http.Server;
let mbeServer: http.Server;
let awmPort: number;
Expand All @@ -18,10 +18,10 @@ describe('integration — health checks', () => {
tlsMode: TlsMode.DISABLED,
signingMode: SigningMode.LOCAL,
port: 0,
bind: '127.0.0.1',
bind: LOCALHOST,
timeout: 30000,
httpLoggerFile: '',
keyProviderUrl: 'http://127.0.0.1:3082',
keyProviderUrl: `http://${LOCALHOST}:3082`,
}),
);
awmPort = await listen(awmServer);
Expand All @@ -31,12 +31,12 @@ describe('integration — health checks', () => {
appMode: AppMode.MASTER_EXPRESS,
tlsMode: TlsMode.DISABLED,
port: 0,
bind: '127.0.0.1',
bind: LOCALHOST,
timeout: 30000,
httpLoggerFile: '',
env: 'test',
disableEnvCheck: true,
advancedWalletManagerUrl: `http://127.0.0.1:${awmPort}`,
advancedWalletManagerUrl: `http://${LOCALHOST}:${awmPort}`,
awmServerCertAllowSelfSigned: true,
}),
);
Expand All @@ -49,12 +49,14 @@ describe('integration — health checks', () => {
});

it('AWM /ping returns 200', async () => {
const res = await fetch(`http://127.0.0.1:${awmPort}/ping`, { method: 'POST' });
const res = await fetch(`http://${LOCALHOST}:${awmPort}/ping`, { method: 'POST' });
res.status.should.equal(200);
});

it('MBE /advancedwallet/ping returns 200', async () => {
const res = await fetch(`http://127.0.0.1:${mbePort}/advancedwallet/ping`, { method: 'POST' });
const res = await fetch(`http://${LOCALHOST}:${mbePort}/advancedwallet/ping`, {
method: 'POST',
});
res.status.should.equal(200);
});
});
61 changes: 61 additions & 0 deletions src/__tests__/integration/helpers/mockBitgoServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import * as http from 'http';
import * as path from 'path';
import express from 'express';
import { listen, close } from './servers';

export interface MockBitgoCall {
method: string;
path: string;
body: unknown;
}

export interface MockBitgoServer {
port: number;
calls: MockBitgoCall[];
close(): Promise<void>;
}

const FIXTURES_DIR = path.resolve(__dirname, '../fixtures/bitgo');

function loadFixture(name: string): unknown {
return require(`${FIXTURES_DIR}/${name}.json`);
}

export async function startMockBitgoServer(): Promise<MockBitgoServer> {
const calls: MockBitgoCall[] = [];

const app = express();
app.use(express.json());

app.use((req, _res, next) => {
calls.push({ method: req.method, path: req.path, body: req.body });
next();
});

/** SDK calls this on every BitGo instance initialisation */
app.get('/api/v1/client/constants', (_req, res) => {
res.json({ ttl: 3600, constants: {} });
});

/** Add keychain — source distinguishes user / backup / bitgo */
app.post('/api/v2/:coin/key', (req, res) => {
const { coin } = req.params;
const source = req.body?.source;
const fixtureName =
source === 'user' ? 'addKey.user' : source === 'backup' ? 'addKey.backup' : 'addKey.bitgo';
const fixture = loadFixture(fixtureName) as Record<string, unknown>;
return res.json({ ...fixture, coin });
});

/** Create wallet */
app.post('/api/v2/:coin/wallet/add', (req, res) => {
const { coin } = req.params;
const fixture = loadFixture('createWallet') as Record<string, unknown>;
res.json({ ...fixture, coin });
});

const server = http.createServer(app);
const port = await listen(server);

return { port, calls, close: () => close(server) };
}
74 changes: 74 additions & 0 deletions src/__tests__/integration/helpers/mockKeyProviderServer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import * as http from 'http';
import express from 'express';
import { BitGoAPI } from '@bitgo-beta/sdk-api';
import { Hteth } from '@bitgo-beta/sdk-coin-eth';
import { Tbtc } from '@bitgo-beta/sdk-coin-btc';
import { listen, close } from './servers';

export interface MockKeyProviderCall {
method: string;
path: string;
body: unknown;
}

export interface MockKeyProviderServer {
port: number;
calls: MockKeyProviderCall[];
close(): Promise<void>;
}

interface StoredKey {
prv: string;
source: string;
type: string;
}

/**
* @returns BitGo Instance with coins registered
*/
function createBitgoInstance(): BitGoAPI {
const instance = new BitGoAPI({ env: 'test' });
instance.register('hteth', Hteth.createInstance);
instance.register('tbtc', Tbtc.createInstance);
return instance;
}

function generateKeypair(coin: string): { pub: string; prv: string } {
const keychain = createBitgoInstance().coin(coin).keychains().create();
if (!keychain.pub || !keychain.prv)
throw new Error(`Failed to generate keypair for coin: ${coin}`);
return { pub: keychain.pub, prv: keychain.prv };
}

export async function startMockKeyProviderServer(): Promise<MockKeyProviderServer> {
const calls: MockKeyProviderCall[] = [];
const keyStore = new Map<string, StoredKey>();

const app = express();
app.use(express.json());

app.use((req, _res, next) => {
calls.push({ method: req.method, path: req.path, body: req.body });
next();
});

/** External signing mode — key provider generates the key */
app.post('/key/generate', (req, res) => {
const { coin, source, type } = req.body;
const { pub, prv } = generateKeypair(coin);
keyStore.set(pub, { prv, source, type });
res.json({ pub, coin, source, type });
});

/** Local signing mode — AWM generates the key and sends it here for storage */
app.post('/key', (req, res) => {
const { pub, prv, coin, source, type } = req.body;
keyStore.set(pub, { prv, source, type });
res.json({ pub, coin, source, type });
});

const server = http.createServer(app);
const port = await listen(server);

return { port, calls, close: () => close(server) };
}
4 changes: 3 additions & 1 deletion src/__tests__/integration/helpers/servers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import * as http from 'http';
import * as net from 'net';

export const LOCALHOST = '127.0.0.1';

export function listen(server: http.Server): Promise<number> {
return new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
server.listen(0, LOCALHOST, () => {
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 here means use whatever port is available!

resolve((server.address() as net.AddressInfo).port);
});
});
Expand Down
Loading
Loading