Skip to content
Closed
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ coverage/
logs/
tsconfig.tsbuildinfo
out/
.vscode/
303 changes: 303 additions & 0 deletions src/__tests__/masterBitgoExpress/.wip.md

Large diffs are not rendered by default.

146 changes: 146 additions & 0 deletions src/__tests__/masterBitgoExpress/generateWallet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ describe('POST /api/:coin/wallet/generate', () => {
const enclavedExpressUrl = 'http://enclaved.invalid';
const bitgoApiUrl = Environments.test.uri;
const coin = 'tbtc';
const eddsaCoin = 'tsol';
const accessToken = 'test-token';

before(() => {
Expand Down Expand Up @@ -136,6 +137,151 @@ describe('POST /api/:coin/wallet/generate', () => {
bitgoAddWalletNock.done();
});

it('should generate a TSS wallet by calling the enclaved express service', async () => {
const constantsNock = nock(bitgoApiUrl)
.get('/api/v1/client/constants')
// Not sure why the nock is not matching any headers, but this works
.matchHeader('accept-encoding', 'gzip, deflate')
.matchHeader('bitgo-sdk-version', '48.0.0')
.reply(200, {
constants: {
mpc: {
bitgoPublicKey: 'test-bitgo-public-key',
},
},
});

const userInitNock = nock(enclavedExpressUrl)
.post(`/api/${eddsaCoin}/mpc/initialize`, {
source: 'user',
bitgoGpgKey: 'test-bitgo-public-key',
})
.reply(200, {
encryptedDataKey: 'key',
encryptedData: 'data',
bitgoPayload: {
from: 'user',
to: 'bitgo',
publicShare:
'dcf591bfb22f9764ed382dcb397f591bdb64c69773c6cf2902d14789a13811a0a768fb0eae38f9ebe2b047182e2a95bb49921bfec56bcd96e3075e53396c1775',
privateShare:
'175bdf3264662e1d13de1dacc22c0913b367f165fb15439fe687cbdc1713560ca768fb0eae38f9ebe2b047182e2a95bb49921bfec56bcd96e3075e53396c1775',
privateShareProof:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwTGXYL4mPPKg3u1KkPeXR9lOqqem/i3kgdgQE9P\nIZlvNdZyVcoAyrTos0Negm39jQPzssKbjNYbwmD6oBliJIWDzVUxYzU3NDY3\nNmUwNWM3Zjc0Zjg4YmM5YmEgPHVzZXItMWM1NzQ2NzZlMDVjN2Y3NGY4OGJj\nOWJhQDFjNTc0Njc2ZTA1YzdmNzRmODhiYzliYS5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQ6ylVI/YkWEQDFQgKBBYAAgECGQECmwMCHgEWIQRS2wpzMoJX\nVNidgnnrKVUj9iRYRAAA0kkA/R78hy0CNnUPCMMi2Co6VlYALrx+xFydb0+7\n8Yza5IF2AP93Xc9FKo8OPO5pg5uPnC6fXvsJqVne289iETTtsihaaM5TBGhU\nZoASBSuBBAAKAgME99PyPC8OyvjMb5GMLIvU3UOa8vDHDw4EJxEk9vjP1M8w\n9Uz8BlRby1wYFShcTYrl8lqBmvO9KswHXSLvwyw1QAMBCAfCeAQYEwgAKgWC\naFRmgAmQ6ylVI/YkWEQCmwwWIQRS2wpzMoJXVNidgnnrKVUj9iRYRAAASxsA\n/RbBP5LPbfcAay8osipVWf/oTzw2/tKzER0K3FfAAsImAP0c2ee+qa0Tn5nv\neezRo+XxgIoxw2gT8jYpyzJw+BKBBQ==\n=DYMw\n-----END PGP PUBLIC KEY BLOCK-----\n',
vssProof: '011532df3eceab48fc91c2e17e7accea1d0dd30b8b7562a5f602afb2130ab26a',
userGPGPublicKey:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwTGXYL4mPPKg3u1KkPeXR9lOqqem/i3kgdgQE9P\nIZlvNdZyVcoAyrTos0Negm39jQPzssKbjNYbwmD6oBliJIWDzVUxYzU3NDY3\nNmUwNWM3Zjc0Zjg4YmM5YmEgPHVzZXItMWM1NzQ2NzZlMDVjN2Y3NGY4OGJj\nOWJhQDFjNTc0Njc2ZTA1YzdmNzRmODhiYzliYS5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQ6ylVI/YkWEQDFQgKBBYAAgECGQECmwMCHgEWIQRS2wpzMoJX\nVNidgnnrKVUj9iRYRAAA0kkA/R78hy0CNnUPCMMi2Co6VlYALrx+xFydb0+7\n8Yza5IF2AP93Xc9FKo8OPO5pg5uPnC6fXvsJqVne289iETTtsihaaM5TBGhU\nZoASBSuBBAAKAgME99PyPC8OyvjMb5GMLIvU3UOa8vDHDw4EJxEk9vjP1M8w\n9Uz8BlRby1wYFShcTYrl8lqBmvO9KswHXSLvwyw1QAMBCAfCeAQYEwgAKgWC\naFRmgAmQ6ylVI/YkWEQCmwwWIQRS2wpzMoJXVNidgnnrKVUj9iRYRAAASxsA\n/RbBP5LPbfcAay8osipVWf/oTzw2/tKzER0K3FfAAsImAP0c2ee+qa0Tn5nv\neezRo+XxgIoxw2gT8jYpyzJw+BKBBQ==\n=DYMw\n-----END PGP PUBLIC KEY BLOCK-----\n',
},
});

const backupInitNock = nock(enclavedExpressUrl)
.post(`/api/${eddsaCoin}/mpc/initialize`, {
source: 'backup',
bitgoGpgKey: 'test-bitgo-public-key',
})
.reply(200, {
encryptedDataKey: 'key',
encryptedData: 'data',
bitgoPayload: {
from: 'backup',
to: 'bitgo',
publicShare:
'280b5d3b40899e6e1cac86906602ffdf76b70aefc2def7f311693aba654cca6ecdcb2be051910ebc9bcbae6ac0db3edf707498b19be0f229102ce76dd880ab9b',
privateShare:
'1be0dcb0b3c77bceac11ce77d83b33a5b74ff39f90485d81b2003bc55270b509cdcb2be051910ebc9bcbae6ac0db3edf707498b19be0f229102ce76dd880ab9b',
privateShareProof:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwQbnZsAMbrZ6LnlMT8ZjmCyq4Au+KDEMH9dndk5\nqVpZIgvHzMwZYusZtija5M/erWbg0Iutv1R1olMd9htHSScOzVViMmZlNTRl\nZTI1YzIyOWM0MzJiNzU2MWYgPHVzZXItYjJmZTU0ZWUyNWMyMjljNDMyYjc1\nNjFmQGIyZmU1NGVlMjVjMjI5YzQzMmI3NTYxZi5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQrNctBNmaAcADFQgKBBYAAgECGQECmwMCHgEWIQSJSRD0FwPm\nwraqiESs1y0E2ZoBwAAAqJkBAIhIhHS8i71tbe43TKYThRaOzeo73afL31UE\nbK12huloAQCrjr5GEz+4L84Nl8TcWt5yAI8UF1hi+O5rdP35UL6xKc5TBGhU\nZoASBSuBBAAKAgME+Bm/MFl4fP7CxJsannVVcZ1M+bL8X8kcl30wXaLkiqvg\nZpEunra42o4RwaQcQirsvPX9+di0P2FoFXH/n1+s1wMBCAfCeAQYEwgAKgWC\naFRmgAmQrNctBNmaAcACmwwWIQSJSRD0FwPmwraqiESs1y0E2ZoBwAAAXWoB\nAI9xw2J9mzyPGpnFiIb/qxHRzSbXsNYyPvxUU15rSKiaAP9uy61NJBs3vTT8\nzf33PkAgoxFZsEDLwAsDyOecH/Cilw==\n=0SE8\n-----END PGP PUBLIC KEY BLOCK-----\n',
vssProof: 'f008212df4a14e81b8b7bca268a3b2b19d65220fb2f0b2e1c8f83e0d9286aec2',
backupGPGPublicKey:
'-----BEGIN PGP PUBLIC KEY BLOCK-----\n\nxk8EaFRmgBMFK4EEAAoCAwQbnZsAMbrZ6LnlMT8ZjmCyq4Au+KDEMH9dndk5\nqVpZIgvHzMwZYusZtija5M/erWbg0Iutv1R1olMd9htHSScOzVViMmZlNTRl\nZTI1YzIyOWM0MzJiNzU2MWYgPHVzZXItYjJmZTU0ZWUyNWMyMjljNDMyYjc1\nNjFmQGIyZmU1NGVlMjVjMjI5YzQzMmI3NTYxZi5jb20+wowEEBMIAD4FgmhU\nZoAECwkHCAmQrNctBNmaAcADFQgKBBYAAgECGQECmwMCHgEWIQSJSRD0FwPm\nwraqiESs1y0E2ZoBwAAAqJkBAIhIhHS8i71tbe43TKYThRaOzeo73afL31UE\nbK12huloAQCrjr5GEz+4L84Nl8TcWt5yAI8UF1hi+O5rdP35UL6xKc5TBGhU\nZoASBSuBBAAKAgME+Bm/MFl4fP7CxJsannVVcZ1M+bL8X8kcl30wXaLkiqvg\nZpEunra42o4RwaQcQirsvPX9+di0P2FoFXH/n1+s1wMBCAfCeAQYEwgAKgWC\naFRmgAmQrNctBNmaAcACmwwWIQSJSRD0FwPmwraqiESs1y0E2ZoBwAAAXWoB\nAI9xw2J9mzyPGpnFiIb/qxHRzSbXsNYyPvxUU15rSKiaAP9uy61NJBs3vTT8\nzf33PkAgoxFZsEDLwAsDyOecH/Cilw==\n=0SE8\n-----END PGP PUBLIC KEY BLOCK-----\n',
},
});

const bitgoAddKeychainNock = nock(bitgoApiUrl)
.post(`/api/v2/${eddsaCoin}/key`)
.reply(function (uri, requestBody) {
// Verify request structure
const body = requestBody as any;
body.should.have.properties({
keyType: 'tss',
source: 'bitgo',
enterprise: 'test_enterprise',
});

// Verify key shares structure
body.should.have.property('keyShares').which.is.an.Array().of.length(2);

// Verify user share
const userShare = body.keyShares.find((s: any) => s.from === 'user' && s.to === 'bitgo');
userShare.should.have.properties([
'publicShare',
'privateShare',
'privateShareProof',
'vssProof',
]);
userShare.publicShare.should.be.a.String().and.not.empty();
userShare.privateShare.should.be.a.String().and.not.empty();
userShare.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
userShare.vssProof.should.be.a.String().and.not.empty();

// Verify backup share
const backupShare = body.keyShares.find(
(s: any) => s.from === 'backup' && s.to === 'bitgo',
);
backupShare.should.have.properties([
'publicShare',
'privateShare',
'privateShareProof',
'vssProof',
]);
backupShare.publicShare.should.be.a.String().and.not.empty();
backupShare.privateShare.should.be.a.String().and.not.empty();
backupShare.privateShareProof.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
backupShare.vssProof.should.be.a.String().and.not.empty();

// Verify GPG keys
body.userGPGPublicKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');
body.backupGPGPublicKey.should.startWith('-----BEGIN PGP PUBLIC KEY BLOCK-----');

return [
200,
{
id: 'bitgo-key-id',
commonKeychain:
'4e534a0193c6636a0727079e25601abd6c2853d63582162bc53ae69b152f0ec2c2e096583da8e7ffd36dff6131a17020727f9543001525c172c1e772900359d3',
},
];
});

const response = await agent
.post(`/api/${eddsaCoin}/wallet/generate`)
.set('Authorization', `Bearer ${accessToken}`)
.send({
label: 'test_wallet',
enterprise: 'test_enterprise',
multisigType: 'tss',
});

// Verify response status and structure
response.status.should.equal(500); // TODO: Update to 200 when fully integrated with finalize endpoint
// response.body.should.have.property('bitgoKeychain');
//
// // Verify BitGo keychain properties
// const bitgoKeychain = response.body.bitgoKeychain;
// bitgoKeychain.should.have.property('id').which.is.a.String();
// bitgoKeychain.should.have.property('commonKeychain').which.is.a.String();
// bitgoKeychain.id.should.equal('bitgo-key-id');
// bitgoKeychain.commonKeychain.should.equal(
// '4e534a0193c6636a0727079e25601abd6c2853d63582162bc53ae69b152f0ec2c2e096583da8e7ffd36dff6131a17020727f9543001525c172c1e772900359d3',
// );

// Verify all nock mocks were called
constantsNock.done();
userInitNock.done();
backupInitNock.done();
bitgoAddKeychainNock.done();
});

it('should fail when enclaved express client is not configured', async () => {
// Create a config without enclaved express settings
const invalidConfig: Partial<MasterExpressConfig> = {
Expand Down
109 changes: 109 additions & 0 deletions src/enclavedBitgoExpress/routers/enclavedApiSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,70 @@ import { signMultisigTransaction } from '../../api/enclaved/signMultisigTransact
import { prepareBitGo, responseHandler } from '../../shared/middleware';
import { EnclavedConfig } from '../../types';
import { BitGoRequest } from '../../types/request';
import { NotImplementedError } from 'bitgo';

Copilot AI Jun 19, 2025

Copy link

Choose a reason for hiding this comment

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

[nitpick] Importing NotImplementedError from 'bitgo' may be inconsistent with other modules using @bitgo/sdk-core; align the import source to avoid mismatches.

Suggested change
import { NotImplementedError } from 'bitgo';
import { NotImplementedError } from '@bitgo/sdk-core';

Copilot uses AI. Check for mistakes.

// Request type for /key/independent endpoint
const IndependentKeyRequest = {
source: t.string,
seed: t.union([t.undefined, t.string]),
};

const BitgoPayloadType = t.union([
t.type({
from: t.literal('user'),
to: t.literal('bitgo'),
publicShare: t.string,
privateShare: t.string,
privateShareProof: t.string,
vssProof: t.string,
userGPGPublicKey: t.string,
}),
t.type({
from: t.literal('backup'),
to: t.literal('bitgo'),
publicShare: t.string,
privateShare: t.string,
privateShareProof: t.string,
vssProof: t.string,
backupGPGPublicKey: t.string,
}),
]);

export const InitEddsaKeyGenerationRequest = {
source: t.union([t.literal('user'), t.literal('backup')]),
bitgoGpgKey: t.string,
};

export const InitEddsaKeyGenerationResponse = t.type({
encryptedDataKey: t.string,
encryptedData: t.string,
bitgoPayload: BitgoPayloadType,
});

export type InitEddsaKeyGenerationResponse = t.TypeOf<typeof InitEddsaKeyGenerationResponse>;

// Types for /mpc/finalize endpoint
const BitGoKeychainType = t.type({
id: t.string,
source: t.literal('bitgo'),
type: t.literal('tss'),
commonKeychain: t.string,
verifiedVssProof: t.boolean,
});

const FinalizeKeyGenerationRequest = {
encryptedDataKey: t.string,
encryptedData: t.string,
bitGoKeychain: BitGoKeychainType,
source: t.union([t.literal('user'), t.literal('backup')]),
};

const FinalizeKeyGenerationResponse = t.type({
commonKeychain: t.string,
enclavedExpressKeyId: t.string,
source: t.union([t.literal('user'), t.literal('backup')]),
});

// Response type for /key/independent endpoint
const IndependentKeyResponse: HttpResponse = {
// TODO: Define proper response type
Expand Down Expand Up @@ -123,6 +180,46 @@ export const EnclavedAPiSpec = apiSpec({
description: 'Generate an independent key',
}),
},
'v1.key.mpc.init': {
post: httpRoute({
method: 'POST',
path: '/api/{coin}/mpc/initialize',
request: httpRequest({
params: {
coin: t.string,
},
body: InitEddsaKeyGenerationRequest,
}),
response: {
200: InitEddsaKeyGenerationResponse,
500: t.type({
error: t.string,
details: t.string,
}),
},
description: 'Initialize Eddsa key generation',
}),
},
'v1.mpc.finalize': {
post: httpRoute({
method: 'POST',
path: '/api/{coin}/mpc/finalize',
request: httpRequest({
params: {
coin: t.string,
},
body: FinalizeKeyGenerationRequest,
}),
response: {
200: FinalizeKeyGenerationResponse,
500: t.type({
error: t.string,
details: t.string,
}),
},
description: 'Finalize key generation and confirm commonKeychain',
}),
},
});

export type EnclavedApiSpecRouteHandler<
Expand Down Expand Up @@ -170,5 +267,17 @@ export function createKeyGenRouter(config: EnclavedConfig): WrappedRouter<typeof
}),
]);

router.post('v1.key.mpc.init', [
responseHandler<EnclavedConfig>(async (_req) => {
throw new NotImplementedError('MPC key generation is not implemented yet');
}),
]);

router.post('v1.mpc.finalize', [
responseHandler<EnclavedConfig>(async (_req) => {
throw new NotImplementedError('MPC key finalization is not implemented yet');
}),
]);

return router;
}
Loading