bitgo
Version:
BitGo JavaScript SDK
1,049 lines • 145 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const sodium = require("libsodium-wrappers-sumo");
const _ = require("lodash");
const nock = require("nock");
const openpgp = require("openpgp");
const should = require("should");
const sinon = require("sinon");
const sdk_test_1 = require("@bitgo/sdk-test");
const src_1 = require("../../../../../src");
const sdk_core_1 = require("@bitgo/sdk-core");
const helpers_1 = require("../../tss/helpers");
const common_1 = require("./common");
openpgp.config.rejectCurves = new Set();
describe('TSS Utils:', async function () {
let sandbox;
let MPC;
let bgUrl;
let tssUtils;
let userGpgKey;
let backupGpgKey;
let bitgoGpgKey;
let bitgo;
let baseCoin;
let wallet;
let bitgoKeyShare;
const reqId = new sdk_core_1.RequestTracer();
const coinName = 'tsol';
const validUserSigningMaterial = {
uShare: {
i: 1,
t: 2,
n: 3,
y: '093c8603ad86c41d5ee25a814b88185b435dd3a9ceccf9c9fd691a465ac4a8b0',
seed: 'ca40c789813250c334ddd2ba19050f6ed20b5a08853ceca492358f2711ad4b15',
chaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca',
},
commonChaincode: '596d5404a7eb918ee78247b952d06539619884091fdd9e0ff5a665f349e32fca',
bitgoYShare: {
i: 1,
j: 3,
y: '59d8000ba5e85fa402f39382960e7d5ede82b1b6e22b146a18b7df238c3a3225',
v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517',
u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b',
chaincode: 'd21dbd8eae5d4789292ecea2efa53e0165b2439d57f5158eb4dd57dc26b59236',
},
backupYShare: {
i: 1,
j: 2,
y: 'e0ae75077715686a121acb41b29a55bde426971154f40a41fc317f7f774a9424',
v: 'f76ef629dfc15ab5e4531e532b5d67f2176637ca752b195876b7e3172459c969',
u: 'fe6b89fb6acfcd7392c35c084f58bde0846b888c4df57e466caf0a3271b06a05',
chaincode: '1c34e5dfbbd4a870f4479caaa5e6a46e3438f976ad5aefd4905b8fe8bca1101e',
},
};
const validUserSignShare = {
xShare: {
i: 1,
y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296',
u: '1e159d6a0ae3a8dccc74615113e7c3e25d3080e5e0ffeb0ae04dd6a967268102',
r: 'c8f64cc48926216c3f60e1d8ff1e24eba060d7c1ff020d0fc1d735d4564efd03',
R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c',
},
rShares: {
3: {
i: 3,
j: 1,
u: 'd675f9099fbef03aa9fcdca4009286f435e56369c374d0042f03cc60b49e690a',
v: '3c090e88ed42da0dd0bade35c8d6b88bc050284536b98e5b27d33ff45da9755b',
r: '7f16224dbf5b02adb6c21380fcb2a8ee00323daae62cac3575a4d328fd23a905',
R: '9be2208ee28cd4b2577a9a66f6aab1ed8b08a300969eeb9b203a52aa54d2c23c',
commitment: '445c8cb1dee0166b6bdd5ad1d0a53fbfe86c4d3a470f184745530a863eedff28',
},
},
};
const validBitgoToUserSignShare = {
xShare: {
i: 3,
y: '4d9343988e68191aac945a6963031dddde3490f9020d0571a6e6c6e15cca0296',
u: '1315dbe18069825b4a27188b813eae7ff2917a614499ed553e70d65d4fa4820b',
r: 'd0539375e6566f2fe540cba48c5e56bd1cdf68cfe1f0d527d2b730fe4e879809',
R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32',
},
rShares: {
1: {
i: 1,
j: 3,
u: '9ce3204a8c9757738967f3f81b463d87267bf6f2c0e5eaf2843167537b872b0b',
v: '01ea3f425b1adf8aec6cfe4fc8f9b94755c34657965f32397655dcd784f1b517',
r: '0375e8c5a5691a73c21df00d49d423e3f83fe08d7b5d5af33c5c6aa9cae59d0a',
R: 'c883fe2ae9b8da1764cc36a526cfa1a21f81d604320b209867f8de9223f1de32',
commitment: '62b21f98bf885841ad469145192d4df0697b3f42c581e3e926394eae0b101ecb',
},
},
};
const txRequest = {
txRequestId: 'randomId',
unsignedTxs: [{ signableHex: 'MPC on a Friday night', serializedTxHex: 'MPC on a Friday night' }],
signatureShares: [
{
from: 'bitgo',
to: 'user',
share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
},
],
};
beforeEach(function () {
sandbox = sinon.createSandbox();
});
afterEach(function () {
sandbox.restore();
});
before('initializes mpc', async function () {
const hdTree = await sdk_core_1.Ed25519BIP32.initialize();
MPC = await sdk_core_1.Eddsa.initialize(hdTree);
});
before(async function () {
bitgoKeyShare = await MPC.keyShare(3, 2, 3);
userGpgKey = await openpgp.generateKey({
userIDs: [
{
name: 'test',
email: 'test@test.com',
},
],
curve: 'secp256k1',
});
backupGpgKey = await openpgp.generateKey({
userIDs: [
{
name: 'testBackup',
email: 'testBackup@test.com',
},
],
curve: 'secp256k1',
});
bitgoGpgKey = await openpgp.generateKey({
userIDs: [
{
name: 'bitgo',
email: 'bitgo@test.com',
},
],
curve: 'secp256k1',
});
const constants = {
mpc: {
bitgoPublicKey: bitgoGpgKey.publicKey,
},
};
bitgo = sdk_test_1.TestBitGo.decorate(src_1.BitGo, { env: 'mock' });
bitgo.initializeTestVars();
baseCoin = bitgo.coin(coinName);
bgUrl = sdk_core_1.common.Environments[bitgo.getEnv()].uri;
// TODO(WP-346): sdk-test mocks conflict so we can't use persist
nock(bgUrl).get('/api/v1/client/constants').times(23).reply(200, { ttl: 3600, constants });
const walletData = {
id: '5b34252f1bf349930e34020a00000000',
coin: 'tsol',
keys: [
'5b3424f91bf349930e34017500000000',
'5b3424f91bf349930e34017600000000',
'5b3424f91bf349930e34017700000000',
],
coinSpecific: {},
multisigType: 'tss',
};
wallet = new sdk_core_1.Wallet(bitgo, baseCoin, walletData);
tssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, wallet);
});
after(function () {
nock.cleanAll();
});
describe('TSS key chains:', async function () {
it('should generate TSS key chains', async function () {
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);
const nockedBitGoKeychain = await nockBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
const nockedUserKeychain = await nockUserKeychain({ coin: coinName });
await nockBackupKeychain({ coin: coinName });
const bitgoKeychain = await tssUtils.createBitgoKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
});
const userKeychain = await tssUtils.createUserKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase: 'passphrase',
});
const backupKeychain = await tssUtils.createBackupKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase: 'passphrase',
});
bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
userKeychain.should.deepEqual(nockedUserKeychain);
// unencrypted `prv` property should exist on backup keychain
JSON.stringify({
uShare: backupKeyShare.uShare,
bitgoYShare: bitgoKeyShare.yShares[2],
userYShare: userKeyShare.yShares[2],
}).should.equal(backupKeychain.prv);
should.exist(backupKeychain.encryptedPrv);
});
it('should generate TSS key chains without passphrase', async function () {
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);
const nockedBitGoKeychain = await nockBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
// reusing the user gpg key as the backup gpg key, i.e. the user is their own the backup provider
backupGpgKey,
bitgoGpgKey,
});
const nockedUserKeychain = await nockUserKeychain({ coin: coinName });
await nockBackupKeychain({ coin: coinName });
const bitgoKeychain = await tssUtils.createBitgoKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
});
const userKeychain = await tssUtils.createUserKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
});
const backupKeychain = await tssUtils.createBackupKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
});
bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
userKeychain.should.deepEqual(nockedUserKeychain);
// unencrypted `prv` property should exist on backup keychain
JSON.stringify({
uShare: backupKeyShare.uShare,
bitgoYShare: bitgoKeyShare.yShares[2],
userYShare: userKeyShare.yShares[2],
}).should.equal(backupKeychain.prv);
});
it('should generate TSS key chains with optional params', async function () {
const enterprise = 'enterprise';
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);
const nockedBitGoKeychain = await nockBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
const nockedUserKeychain = await nockUserKeychain({ coin: coinName });
await nockBackupKeychain({ coin: coinName });
const bitgoKeychain = await tssUtils.createBitgoKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
enterprise,
});
const userKeychain = await tssUtils.createUserKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase: 'passphrase',
originalPasscodeEncryptionCode: 'originalPasscodeEncryptionCode',
});
const backupKeychain = await tssUtils.createBackupKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain,
passphrase: 'passphrase',
});
bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
userKeychain.should.deepEqual(nockedUserKeychain);
// unencrypted `prv` property should exist on backup keychain
JSON.stringify({
uShare: backupKeyShare.uShare,
bitgoYShare: bitgoKeyShare.yShares[2],
userYShare: userKeyShare.yShares[2],
}).should.equal(backupKeychain.prv);
should.exist(backupKeychain.encryptedPrv);
});
it('should fail to generate TSS keychains when received invalid number of wallet signatures', async function () {
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);
const bitgoKeychain = await generateBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
const certsString = await (0, sdk_core_1.createSharedDataProof)(bitgoGpgKey.privateKey, userGpgKey.publicKey, []);
const certsKey = await openpgp.readKey({ armoredKey: certsString });
const finalKey = new openpgp.PacketList();
certsKey.toPacketList().forEach((packet) => finalKey.push(packet));
// the underlying function only requires two arguments but the according .d.ts file for openpgp has the further
// arguments marked as mandatory as well.
// Once the following PR has been merged and released we no longer need the ts-ignore:
// https://github.com/openpgpjs/openpgpjs/pull/1576
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
bitgoKeychain.walletHSMGPGPublicKeySigs = openpgp.armor(openpgp.enums.armor.publicKey, finalKey.write());
await tssUtils
.verifyWalletSignatures(userGpgKey.publicKey, backupGpgKey.publicKey, bitgoKeychain, '', 1)
.should.be.rejectedWith('Invalid wallet signatures');
});
it('should fail to generate TSS keychains when wallet signature fingerprints do not match passed user/backup fingerprints', async function () {
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);
const bitgoKeychain = await generateBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
// using the backup gpg here instead of the user gpg key to simulate that the first signature has a different
// fingerprint from the passed in first gpg key
await tssUtils
.verifyWalletSignatures(backupGpgKey.publicKey, backupGpgKey.publicKey, bitgoKeychain, '', 1)
.should.be.rejectedWith(`first wallet signature's fingerprint does not match passed user gpg key's fingerprint`);
// using the user gpg here instead of the backup gpg key to simulate that the second signature has a different
// fingerprint from the passed in second gpg key
await tssUtils
.verifyWalletSignatures(userGpgKey.publicKey, userGpgKey.publicKey, bitgoKeychain, '', 1)
.should.be.rejectedWith(`second wallet signature's fingerprint does not match passed backup gpg key's fingerprint`);
});
it('should fail to generate TSS keychains when wallet signature is for different key share', async function () {
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);
const customBitgoKeyShare = MPC.keyShare(3, 2, 3);
const bitgoKeychain1 = await generateBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
const bitgoKeychain2 = await generateBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare: customBitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
// using the other bitgo keychains common keychain and walletHSMGPGPublicKeySigs so that the verification of the
// commmon keychain passes but fails for the bitgo to user/ backup shares
bitgoKeychain1.commonKeychain = bitgoKeychain2.commonKeychain;
bitgoKeychain1.walletHSMGPGPublicKeySigs = bitgoKeychain2.walletHSMGPGPublicKeySigs;
await tssUtils
.createUserKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain: bitgoKeychain1,
})
.should.be.rejectedWith('bitgo share mismatch');
await tssUtils
.createBackupKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
bitgoKeychain: bitgoKeychain1,
})
.should.be.rejectedWith('bitgo share mismatch');
});
it('should fail to generate TSS key chains when common keychains do not match', async function () {
const userKeyShare = MPC.keyShare(1, 2, 3);
const backupKeyShare = MPC.keyShare(2, 2, 3);
const nockedBitGoKeychain = await nockBitgoKeychain({
coin: coinName,
userKeyShare,
backupKeyShare,
bitgoKeyShare,
userGpgKey,
backupGpgKey,
bitgoGpgKey,
});
const bitgoKeychain = await tssUtils.createBitgoKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare,
});
bitgoKeychain.should.deepEqual(nockedBitGoKeychain);
await tssUtils
.createUserKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare: MPC.keyShare(2, 2, 3),
bitgoKeychain,
passphrase: 'passphrase',
})
.should.be.rejectedWith('Failed to create user keychain - commonKeychains do not match.');
await tssUtils
.createUserKeychain({
userGpgKey,
backupGpgKey,
userKeyShare: MPC.keyShare(1, 2, 3),
backupKeyShare,
bitgoKeychain,
passphrase: 'passphrase',
})
.should.be.rejectedWith('Failed to create user keychain - commonKeychains do not match.');
await tssUtils
.createBackupKeychain({
userGpgKey,
backupGpgKey,
userKeyShare: MPC.keyShare(1, 2, 3),
backupKeyShare,
bitgoKeychain,
passphrase: 'passphrase',
})
.should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.');
await tssUtils
.createBackupKeychain({
userGpgKey,
backupGpgKey,
userKeyShare,
backupKeyShare: MPC.keyShare(2, 2, 3),
bitgoKeychain,
passphrase: 'passphrase',
})
.should.be.rejectedWith('Failed to create backup keychain - commonKeychains do not match.');
});
});
describe('signTxRequest:', function () {
const txRequestId = 'randomid';
const txRequest = {
txRequestId,
transactions: [],
unsignedTxs: [
{
serializedTxHex: 'MPC on a Friday night',
signableHex: 'MPC on a Friday night',
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
};
beforeEach(async function () {
const userSignShare = validUserSignShare;
const rShare = userSignShare.rShares[3];
const signatureShare = {
from: sdk_core_1.SignatureShareType.USER,
to: sdk_core_1.SignatureShareType.BITGO,
share: rShare.r + rShare.R,
};
await (0, common_1.nockSendSignatureShare)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
signatureShare,
});
const signatureShare2 = {
from: sdk_core_1.SignatureShareType.BITGO,
to: sdk_core_1.SignatureShareType.USER,
share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
};
const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] };
await (0, common_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response });
const bitgoToUserCommitmentShare = {
from: sdk_core_1.SignatureShareType.BITGO,
to: sdk_core_1.SignatureShareType.USER,
type: sdk_core_1.CommitmentType.COMMITMENT,
share: validBitgoToUserSignShare.rShares[1].commitment,
};
const exchangeCommitResponse = { commitmentShare: bitgoToUserCommitmentShare };
await (0, common_1.nockExchangeCommitments)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
response: exchangeCommitResponse,
});
});
it('signTxRequest should succeed with txRequest object as input', async function () {
const signedTxRequest = await tssUtils.signTxRequest({
txRequest,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
});
signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);
sandbox.verifyAndRestore();
});
it('signTxRequest should succeed with txRequest id as input', async function () {
const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest');
getTxRequest.resolves(txRequest);
getTxRequest.calledWith(txRequestId);
const signedTxRequest = await tssUtils.signTxRequest({
txRequest: txRequestId,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
});
signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);
sandbox.verifyAndRestore();
});
});
describe('signTxRequest With Commitment:', function () {
const txRequestId = 'randomid';
const txRequest = {
txRequestId,
transactions: [],
unsignedTxs: [
{
serializedTxHex: 'MPC on a Friday night',
signableHex: 'MPC on a Friday night',
derivationPath: 'm/0',
},
],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
};
beforeEach(async function () {
const userSignShare = validUserSignShare;
const rShare = userSignShare.rShares[3];
const signatureShare = {
from: sdk_core_1.SignatureShareType.USER,
to: sdk_core_1.SignatureShareType.BITGO,
share: rShare.r + rShare.R,
};
await (0, common_1.nockSendSignatureShare)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
signatureShare,
});
const signatureShare2 = {
from: sdk_core_1.SignatureShareType.BITGO,
to: sdk_core_1.SignatureShareType.USER,
share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
};
const response = { txRequests: [{ ...txRequest, signatureShares: [signatureShare2] }] };
await (0, common_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response });
const bitgoToUserCommitmentShare = {
from: sdk_core_1.SignatureShareType.BITGO,
to: sdk_core_1.SignatureShareType.USER,
type: sdk_core_1.CommitmentType.COMMITMENT,
share: validBitgoToUserSignShare.rShares[1].commitment,
};
const exchangeCommitResponse = { commitmentShare: bitgoToUserCommitmentShare };
await (0, common_1.nockExchangeCommitments)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
response: exchangeCommitResponse,
});
});
it('signTxRequest should succeed with txRequest object as input', async function () {
const signedTxRequest = await tssUtils.signTxRequest({
txRequest,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
});
signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);
sandbox.verifyAndRestore();
});
it('signTxRequest should succeed with txRequest id as input', async function () {
const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest');
getTxRequest.resolves(txRequest);
getTxRequest.calledWith(txRequestId);
const signedTxRequest = await tssUtils.signTxRequest({
txRequest: txRequestId,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
});
signedTxRequest.unsignedTxs.should.deepEqual(txRequest.unsignedTxs);
sandbox.verifyAndRestore();
});
});
describe('signTxRequestForMessage:', function () {
const txRequestId = 'randomid-abc';
const messageRaw = 'hello world';
const messageEncoded = Buffer.from(`${messageRaw}`).toString('hex');
const bufferToSign = Buffer.from(messageEncoded, 'hex');
const txRequest = {
txRequestId,
transactions: [],
messages: [
{
state: 'pendingSignature',
signatureShares: [],
messageRaw,
messageEncoded,
derivationPath: 'm/0',
},
],
unsignedTxs: [],
date: new Date().toISOString(),
intent: {
intentType: 'payment',
},
latest: true,
state: 'pendingUserSignature',
walletType: 'hot',
walletId: 'walletId',
policiesChecked: true,
version: 1,
userId: 'userId',
apiVersion: 'full',
};
beforeEach(async function () {
const rShare = validUserSignShare.rShares[3];
const signatureShare = {
from: sdk_core_1.SignatureShareType.USER,
to: sdk_core_1.SignatureShareType.BITGO,
share: rShare.r + rShare.R,
};
await (0, common_1.nockSendSignatureShare)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
signatureShare,
requestType: sdk_core_1.RequestType.message,
apiMode: 'full',
});
const signatureShare2 = {
from: sdk_core_1.SignatureShareType.BITGO,
to: sdk_core_1.SignatureShareType.USER,
share: validBitgoToUserSignShare.rShares[1].r + validBitgoToUserSignShare.rShares[1].R,
};
txRequest.messages[0].signatureShares.push(signatureShare2);
const response = { txRequests: [{ ...txRequest, apiVersion: 'full' }] };
await (0, common_1.nockGetTxRequest)({ walletId: wallet.id(), txRequestId: txRequest.txRequestId, response });
const bitgoToUserCommitmentShare = {
from: sdk_core_1.SignatureShareType.BITGO,
to: sdk_core_1.SignatureShareType.USER,
type: sdk_core_1.CommitmentType.COMMITMENT,
share: validBitgoToUserSignShare.rShares[1].commitment,
};
const exchangeCommitResponse = { commitmentShare: bitgoToUserCommitmentShare };
await (0, common_1.nockExchangeCommitments)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
response: exchangeCommitResponse,
apiMode: 'full',
});
});
afterEach(async function () {
txRequest.messages[0].signatureShares = [];
});
it('signTxRequest should succeed with txRequest object as input', async function () {
const signedTxRequest = await tssUtils.signTxRequestForMessage({
messageRaw,
bufferToSign,
txRequest,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
});
signedTxRequest.messages.should.deepEqual(txRequest.messages);
sandbox.verifyAndRestore();
});
it('signTxRequest should succeed with txRequest id as input', async function () {
const getTxRequest = sandbox.stub(tssUtils, 'getTxRequest');
getTxRequest.resolves(txRequest);
getTxRequest.calledWith(txRequestId);
const signedTxRequest = await tssUtils.signTxRequestForMessage({
txRequest: txRequestId,
prv: JSON.stringify(validUserSigningMaterial),
reqId,
messageRaw,
bufferToSign,
});
signedTxRequest.messages.should.deepEqual(txRequest.messages);
sandbox.verifyAndRestore();
});
});
describe('prebuildTxWithIntent:', async function () {
it('should build single recipient tx', async function () {
const nockedCreateTx = await (0, common_1.nockCreateTxRequest)({
walletId: wallet.id(),
requestBody: {
apiVersion: 'lite',
intent: {
intentType: 'payment',
recipients: [
{
address: {
address: 'recipient',
},
amount: {
value: '10000',
symbol: 'tsol',
},
},
],
},
},
// don't care about the actual response - just need to make sure request body matches
response: {},
});
await tssUtils.prebuildTxWithIntent({
reqId,
recipients: [
{
address: 'recipient',
amount: '10000',
},
],
intentType: 'payment',
});
nockedCreateTx.isDone().should.be.true();
});
it('should build multiple recipients with memo tx', async function () {
const nockedCreateTx = await (0, common_1.nockCreateTxRequest)({
walletId: wallet.id(),
requestBody: {
apiVersion: 'lite',
intent: {
intentType: 'payment',
recipients: [
{
address: {
address: 'recipient1',
},
amount: {
value: '10000',
symbol: 'tsol',
},
},
{
address: {
address: 'recipient2',
},
amount: {
value: '20000',
symbol: 'tsol',
},
},
],
memo: 'memo',
},
},
// don't care about the actual response - just need to make sure request body matches
response: {},
});
await tssUtils.prebuildTxWithIntent({
reqId,
recipients: [
{
address: 'recipient1',
amount: '10000',
},
{
address: 'recipient2',
amount: '20000',
},
],
memo: {
value: 'memo',
type: 'text',
},
intentType: 'payment',
});
nockedCreateTx.isDone().should.be.true();
});
});
describe('delete SignatureShare:', async function () {
it('should succeed to delete Signature Share', async function () {
const signatureShare = { from: 'user', to: 'bitgo', share: '128bytestring' };
const nock = await (0, common_1.nockDeleteSignatureShare)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
signatureShare,
});
const response = await tssUtils.deleteSignatureShares(txRequest.txRequestId);
response.should.deepEqual([signatureShare]);
response.should.length(1);
nock.isDone().should.equal(true);
});
it('should call setRequestTracer', async function () {
const signatureShare = { from: 'user', to: 'bitgo', share: '128bytestring' };
const nock = await (0, common_1.nockDeleteSignatureShare)({
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
signatureShare,
});
const reqId = new sdk_core_1.RequestTracer();
const setRequestTracerSpy = sinon.spy(bitgo, 'setRequestTracer');
setRequestTracerSpy.withArgs(reqId);
const response = await tssUtils.deleteSignatureShares(txRequest.txRequestId, reqId);
response.should.deepEqual([signatureShare]);
response.should.length(1);
nock.isDone().should.equal(true);
sinon.assert.calledOnce(setRequestTracerSpy);
setRequestTracerSpy.restore();
});
});
describe('sendTxRequest:', async function () {
it('should succeed to send tx request', async function () {
const nock = await (0, common_1.nockSendTxRequest)({
coin: coinName,
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
});
await tssUtils.sendTxRequest(txRequest.txRequestId).should.be.fulfilled();
nock.isDone().should.equal(true);
});
it('should call setRequestTracer', async function () {
const nock = await (0, common_1.nockSendTxRequest)({
coin: coinName,
walletId: wallet.id(),
txRequestId: txRequest.txRequestId,
});
const reqId = new sdk_core_1.RequestTracer();
const setRequestTracerSpy = sinon.spy(bitgo, 'setRequestTracer');
setRequestTracerSpy.withArgs(reqId);
await tssUtils.sendTxRequest(txRequest.txRequestId, reqId).should.be.fulfilled();
nock.isDone().should.equal(true);
sinon.assert.calledOnce(setRequestTracerSpy);
setRequestTracerSpy.restore();
});
});
describe('createUserToBitgoCommitmentShare', function () {
it('should create a valid commitmentShare', async function () {
const value = 'randomstring';
const validUserToBitgoCommitmentShare = {
from: sdk_core_1.SignatureShareType.USER,
to: sdk_core_1.SignatureShareType.BITGO,
type: sdk_core_1.CommitmentType.COMMITMENT,
share: value,
};
const commitmentShare = tssUtils.createUserToBitgoCommitmentShare(value);
commitmentShare.should.deepEqual(validUserToBitgoCommitmentShare);
});
});
describe('createUserToBitgoEncryptedSignerShare', function () {
it('should create a valid encryptedSignerShare', async function () {
const value = 'randomstring';
const validUserToBitgoEncryptedSignerShare = {
from: sdk_core_1.SignatureShareType.USER,
to: sdk_core_1.SignatureShareType.BITGO,
type: sdk_core_1.EncryptedSignerShareType.ENCRYPTED_SIGNER_SHARE,
share: value,
};
const encryptedSignerShare = tssUtils.createUserToBitgoEncryptedSignerShare(value);
encryptedSignerShare.should.deepEqual(validUserToBitgoEncryptedSignerShare);
});
});
describe('supportedTxRequestVersions', function () {
it('should return full for custodial wallets', async function () {
const custodialWallet = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'custodial' });
const custodialTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, custodialWallet);
custodialTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
});
it('should return full for cold wallets', async function () {
const coldWallet = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'cold' });
const coldWalletTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, coldWallet);
coldWalletTssUtils.supportedTxRequestVersions().should.deepEqual(['full']);
});
it('should return full and lite for hot wallets', async function () {
const hotWallet = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'hot' });
const hotTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, hotWallet);
const supportedTxRequestVersions = hotTssUtils.supportedTxRequestVersions();
supportedTxRequestVersions.should.deepEqual(['lite', 'full']);
});
it('should return empty for trading wallets', function () {
const tradingWallets = new sdk_core_1.Wallet(bitgo, baseCoin, { multisigType: 'tss', type: 'trading' });
const tradingWalletTssUtils = new sdk_core_1.TssUtils(bitgo, baseCoin, tradingWallets);
const supportedTxRequestVersions = tradingWalletTssUtils.supportedTxRequestVersions();
supportedTxRequestVersions.should.deepEqual([]);
});
it('should return empty for non-tss wallets', function () {
const nonTssWalletData = { coin: 'btc', multisigType: 'onchain' };
const btcCoin = bitgo.coin('tbtc');
const nonTssWallet = new sdk_core_1.Wallet(bitgo, btcCoin, nonTssWalletData);
const nonTssWalletTssUtils = new sdk_core_1.TssUtils(bitgo, btcCoin, nonTssWallet);
nonTssWalletTssUtils.supportedTxRequestVersions().should.deepEqual([]);
});
});
describe('isPendingApprovalTxRequestFull', () => {
it('should return true for full apiVersion and pendingApproval state', async () => {
const txRequest = {
apiVersion: 'full',
state: 'pendingApproval',
};
const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest);
result.should.be.true();
});
it('should return false for non-full apiVersion', async () => {
const txRequest = {
apiVersion: 'lite',
state: 'pendingApproval',
};
const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest);
result.should.be.false();
});
it('should return false for non-pendingApproval state', async () => {
const txRequest = {
apiVersion: 'full',
state: 'pendingDelivery',
};
const result = await tssUtils.isPendingApprovalTxRequestFull(txRequest);
result.should.be.false();
});
});
// #region Nock helpers
async function generateBitgoKeychain(params) {
const bitgoCombined = MPC.keyCombine(params.bitgoKeyShare.uShare, [
params.userKeyShare.yShares[3],
params.backupKeyShare.yShares[3],
]);
const userGpgKeyActual = await openpgp.readKey({ armoredKey: params.userGpgKey.publicKey });
const backupGpgKeyActual = await openpgp.readKey({ armoredKey: params.backupGpgKey.publicKey });
const bitgoToUserMessage = await openpgp.createMessage({
text: Buffer.concat([
Buffer.from(params.bitgoKeyShare.yShares[1].u, 'hex'),
Buffer.from(params.bitgoKeyShare.yShares[1].chaincode, 'hex'),
]).toString('hex'),
});
const encryptedBitgoToUserMessage = await openpgp.encrypt({
message: bitgoToUserMessage,
encryptionKeys: [userGpgKeyActual.toPublic()],
format: 'armored',
});
const bitgoToBackupMessage = await openpgp.createMessage({
text: Buffer.concat([
Buffer.from(params.bitgoKeyShare.yShares[2].u, 'hex'),
Buffer.from(params.bitgoKeyShare.yShares[2].chaincode, 'hex'),
]).toString('hex'),
});
const encryptedBitgoToBackupMessage = await openpgp.encrypt({
message: bitgoToBackupMessage,
encryptionKeys: [backupGpgKeyActual.toPublic()],
format: 'armored',
});
const bitgoKeychain = {
id: '3',
pub: '',
commonKeychain: bitgoCombined.pShare.y + bitgoCombined.pShare.chaincode,
keyShares: [
{
from: 'bitgo',
to: 'user',
publicShare: params.bitgoKeyShare.yShares[1].y + params.bitgoKeyShare.yShares[1].chaincode,
privateShare: encryptedBitgoToUserMessage.toString(),
vssProof: params.bitgoKeyShare.yShares[1].v,
},
{
from: 'bitgo',
to: 'backup',
publicShare: params.bitgoKeyShare.yShares[2].y + params.bitgoKeyShare.yShares[2].chaincode,
privateShare: encryptedBitgoToBackupMessage.toString(),
vssProof: params.bitgoKeyShare.yShares[2].v,
},
],
type: 'tss',
};
const userKeyId = userGpgKeyActual.keyPacket.getFingerprint();
const backupKeyId = backupGpgKeyActual.keyPacket.getFingerprint();
const bitgoToUserPublicShare = Buffer.from(await sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(params.bitgoKeyShare.yShares[1].u, 'hex'))).toString('hex') + params.bitgoKeyShare.yShares[1].chaincode;
const bitgoToBackupPublicShare = Buffer.from(await sodium.crypto_scalarmult_ed25519_base_noclamp(Buffer.from(params.bitgoKeyShare.yShares[2].u, 'hex'))).toString('hex') + params.bitgoKeyShare.yShares[2].chaincode;
bitgoKeychain.walletHSMGPGPublicKeySigs = await (0, helpers_1.createWalletSignatures)(params.bitgoGpgKey.privateKey, params.userGpgKey.publicKey, params.backupGpgKey.publicKey, [
{ name: 'commonKeychain', value: bitgoCombined.pShare.y + bitgoCombined.pShare.chaincode },
{ name: 'userKeyId', value: userKeyId },
{ name: 'backupKeyId', value: backupKeyId },
{ name: 'bitgoToUserPublicShare', value: bitgoToUserPublicShare },
{ name: 'bitgoToBackupPublicShare', value: bitgoToBackupPublicShare },
]);
return bitgoKeychain;
}
async function nockBitgoKeychain(params) {
const bitgoKeychain = await generateBitgoKeychain(params);
nock(bgUrl)
.post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'bitgo' }))
.reply(200, bitgoKeychain);
return bitgoKeychain;
}
async function nockUserKeychain(params) {
const userKeychain = {
id: '1',
pub: '',
type: 'tss',
};
nock('https://bitgo.fakeurl')
.post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'user' }))
.reply(200, userKeychain);
return userKeychain;
}
async function nockBackupKeychain(params) {
const backupKeychain = {
id: '2',
pub: '',
type: 'tss',
};
nock('https://bitgo.fakeurl')
.post(`/api/v2/${params.coin}/key`, _.matches({ keyType: 'tss', source: 'backup' }))
.reply(200, backupKeychain);
return backupKeychain;
}
// #endregion Nock helpers
});
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZWRkc2EuanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi8uLi8uLi8uLi90ZXN0L3YyL3VuaXQvaW50ZXJuYWwvdHNzVXRpbHMvZWRkc2EudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6Ijs7QUFBQSxrREFBa0Q7QUFDbEQsNEJBQTRCO0FBQzVCLDZCQUE4QjtBQUM5QixtQ0FBbUM7QUFDbkMsaUNBQWtDO0FBQ2xDLCtCQUErQjtBQUUvQiw4Q0FBd0Q7QUFDeEQsNENBQTJDO0FBQzNDLDhDQW1CeUI7QUFDekIsK0NBQTJEO0FBQzNELHFDQU9rQjtBQUVsQixPQUFPLENBQUMsTUFBTSxDQUFDLFlBQVksR0FBRyxJQUFJLEdBQUcsRUFBRSxDQUFDO0FBRXhDLFFBQVEsQ0FBQyxZQUFZLEVBQUUsS0FBSztJQUMxQixJQUFJLE9BQTJCLENBQUM7SUFDaEMsSUFBSSxHQUFVLENBQUM7SUFDZixJQUFJLEtBQWEsQ0FBQztJQUNsQixJQUFJLFFBQWtCLENBQUM7SUFDdkIsSUFBSSxVQUFVLENBQUM7SUFDZixJQUFJLFlBQVksQ0FBQztJQUNqQixJQUFJLFdBQVcsQ0FBQztJQUNoQixJQUFJLEtBQXlCLENBQUM7SUFDOUIsSUFBSSxRQUFrQixDQUFDO0lBQ3ZCLElBQUksTUFBYyxDQUFDO0lBQ25CLElBQUksYUFBYSxDQUFDO0lBQ2xCLE1BQU0sS0FBSyxHQUFHLElBQUksd0JBQWEsRUFBRSxDQUFDO0lBQ2xDLE1BQU0sUUFBUSxHQUFHLE1BQU0sQ0FBQztJQUN4QixNQUFNLHdCQUF3QixHQUFHO1FBQy9CLE1BQU0sRUFBRTtZQUNOLENBQUMsRUFBRSxDQUFDO1lBQ0osQ0FBQyxFQUFFLENBQUM7WUFDSixDQUFDLEVBQUUsQ0FBQztZQUNKLENBQUMsRUFBRSxrRUFBa0U7WUFDckUsSUFBSSxFQUFFLGtFQUFrRTtZQUN4RSxTQUFTLEVBQUUsa0VBQWtFO1NBQzlFO1FBQ0QsZUFBZSxFQUFFLGtFQUFrRTtRQUNuRixXQUFXLEVBQUU7WUFDWCxDQUFD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