Skip to content

Commit

Permalink
Merge pull request #4884 from BitGo/CS-3699
Browse files Browse the repository at this point in the history
feat(sdk-core): add bulkShareWallet method
  • Loading branch information
SoumalyaBh authored Sep 12, 2024
2 parents ad94814 + 577d631 commit 7bb4f8f
Show file tree
Hide file tree
Showing 3 changed files with 412 additions and 1 deletion.
249 changes: 249 additions & 0 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as nock from 'nock';
import * as sinon from 'sinon';
import * as should from 'should';
import * as _ from 'lodash';

import { TestBitGo } from '@bitgo/sdk-test';
import {
BlsUtils,
Expand All @@ -17,10 +18,14 @@ import {
GenerateWalletOptions,
Wallet,
isWalletWithKeychains,
BulkWalletShareOptions,
OptionalKeychainEncryptedKey,
KeychainWithEncryptedPrv,
} from '@bitgo/sdk-core';
import { BitGo } from '../../../src';
import { afterEach } from 'mocha';
import { TssSettings } from '@bitgo/public-types';
import * as moduleBitgo from '@bitgo/sdk-core';

describe('V2 Wallets:', function () {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
Expand Down Expand Up @@ -1426,4 +1431,248 @@ describe('V2 Wallets:', function () {
acceptShareNock.done();
});
});

describe('createBulkKeyShares tests', () => {
const walletData = {
id: '5b34252f1bf349930e34020a00000000',
coin: 'tbtc',
keys: [
'5b3424f91bf349930e34017500000000',
'5b3424f91bf349930e34017600000000',
'5b3424f91bf349930e34017700000000',
],
coinSpecific: {},
multisigType: 'onchain',
type: 'hot',
};
const tsol = bitgo.coin('tsol');
const wallet = new Wallet(bitgo, tsol, walletData);
before(function () {
nock('https://bitgo.fakeurl').persist().get('/api/v1/client/constants').reply(200, { ttl: 3600, constants: {} });
bitgo.initializeTestVars();
});
beforeEach(() => {
sinon.createSandbox();
});
after(function () {
nock.cleanAll();
nock.pendingMocks().length.should.equal(0);
});
afterEach(function () {
sinon.restore();
});

it('should throw an error if shareOptions is empty', async () => {
try {
await wallet.createBulkKeyShares([]);
assert.fail('Expected error not thrown');
} catch (error) {
assert.strictEqual(error.message, 'shareOptions cannot be empty');
}
});

it('should skip shareoption if keychain parameters are missing', async () => {
const params = [
{
user: '[email protected]',
permissions: ['spend'],
keychain: { pub: 'pubkey', encryptedPrv: '', fromPubKey: '', toPubKey: '', path: '' },
},
];

try {
await wallet.createBulkKeyShares(params);
assert.fail('Expected error not thrown');
} catch (error) {
// Shareoptions with invalid keychains are skipped
assert.strictEqual(error.message, 'shareOptions cannot be empty');
}
});

it('should send the correct data to BitGo API if shareOptions are valid', async () => {
const params = {
shareOptions: [
{
user: '[email protected]',
permissions: ['spend'],
keychain: {
pub: 'pubkey',
encryptedPrv: 'encryptedPrv',
fromPubKey: 'fromPubKey',
toPubKey: 'toPubKey',
path: 'm/0/0',
},
},
],
};
const paramsToSend = [
{
user: '[email protected]',
permissions: ['spend'],
keychain: {
pub: 'pubkey',
encryptedPrv: 'encryptedPrv',
fromPubKey: 'fromPubKey',
toPubKey: 'toPubKey',
path: 'm/0/0',
},
},
];
nock(bgUrl)
.post(`/api/v2/wallet/${walletData.id}/walletshares`, params)
.reply(200, {
shares: [
{
id: 'userId',
coin: walletData.coin,
wallet: walletData.id,
fromUser: 'fromUserId',
toUser: 'toUserId',
permissions: ['view', 'spend'],
keychain: {
pub: 'dummyPub',
encryptedPrv: 'dummyEncryptedPrv',
fromPubKey: 'dummyFromPubKey',
toPubKey: 'dummyToPubKey',
path: 'dummyPath',
},
},
],
});
const result = await wallet.createBulkKeyShares(paramsToSend);
assert.strictEqual(result.shares[0].id, 'userId', 'The share ID should match');
assert.strictEqual(result.shares[0].coin, walletData.coin, 'The coin should match');
assert.strictEqual(result.shares[0].wallet, walletData.id, 'The wallet ID should match');
assert(result.shares[0].keychain);
assert.strictEqual(result.shares[0].keychain.pub, 'dummyPub', 'The keychain pub should match');
assert.strictEqual(result.shares[0].permissions.includes('view'), true, 'The permissions should include "view"');
assert.strictEqual(
result.shares[0].permissions.includes('spend'),
true,
'The permissions should include "spend"'
);
});
});

describe('createBulkWalletShare tests', () => {
const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });

const walletData = {
id: '5b34252f1bf349930e34020a00000000',
coin: 'tbtc',
keys: [
'5b3424f91bf349930e34017500000000',
'5b3424f91bf349930e34017600000000',
'5b3424f91bf349930e34017700000000',
],
coinSpecific: {},
multisigType: 'onchain',
type: 'hot',
};
const tsol = bitgo.coin('tsol');
const wallet = new Wallet(bitgo, tsol, walletData);
before(function () {
nock('https://bitgo.fakeurl').persist().get('/api/v1/client/constants').reply(200, { ttl: 3600, constants: {} });
bitgo.initializeTestVars();
});

after(function () {
nock.cleanAll();
nock.pendingMocks().length.should.equal(0);
});

afterEach(function () {
sinon.restore();
});

it('should throw an error if no share options are provided', async () => {
try {
await wallet.createBulkWalletShare({ walletPassphrase: 'Test', keyShareOptions: [] });
assert.fail('Expected error not thrown');
} catch (error) {
assert.strictEqual(error.message, 'shareOptions cannot be empty');
}
});

it('should correctly process share options and call createBulkKeyShares', async () => {
const userId = '[email protected]';
const permissions = ['view', 'spend'];
const path = 'm/999999/1/1';
const walletPassphrase = 'bitgo1234';
const pub = 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQk';
nock(bgUrl)
.get(`/api/v2/tbtc/key/${wallet.keyIds()[0]}`)
.reply(200, {
id: wallet.keyIds()[0],
pub,
source: 'user',
encryptedPrv: bitgo.encrypt({ input: 'xprv1', password: walletPassphrase }),
coinSpecific: {},
});
const params: BulkWalletShareOptions = {
walletPassphrase,
keyShareOptions: [
{
userId: userId,
permissions: permissions,
pubKey: '02705a6d33a2459feb537e7abe36aaad8c11532cdbffa3a2e4e58868467d51f532',
path: path,
},
],
};

const prv1 = Math.random().toString();
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: prv1, password: walletPassphrase }),
};

sinon.stub(wallet, 'getEncryptedUserKeychain').resolves({
encryptedPrv: keychainTest.encryptedPrv,
} as KeychainWithEncryptedPrv);

sinon.stub(moduleBitgo, 'getSharedSecret').resolves('fakeSharedSecret');

sinon.stub(wallet, 'createBulkKeyShares').resolves({
shares: [
{
id: userId,
coin: walletData.coin,
wallet: walletData.id,
fromUser: userId,
toUser: userId,
permissions: ['view', 'spend'],
keychain: {
pub: 'dummyPub',
encryptedPrv: 'dummyEncryptedPrv',
fromPubKey: 'dummyFromPubKey',
toPubKey: 'dummyToPubKey',
path: 'dummyPath',
},
},
],
});

const result = await wallet.createBulkWalletShare(params);

assert.deepStrictEqual(result, {
shares: [
{
id: userId,
coin: walletData.coin,
wallet: walletData.id,
fromUser: userId,
toUser: userId,
permissions: ['view', 'spend'],
keychain: {
pub: 'dummyPub',
encryptedPrv: 'dummyEncryptedPrv',
fromPubKey: 'dummyFromPubKey',
toPubKey: 'dummyToPubKey',
path: 'dummyPath',
},
},
],
});
});
});
});
48 changes: 48 additions & 0 deletions modules/sdk-core/src/bitgo/wallet/iWallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,6 +485,52 @@ export interface ShareWalletOptions {
disableEmail?: boolean;
}

export interface BulkCreateShareOption {
user: string;
permissions: string[];
keychain: BulkWalletShareKeychain;
}

export interface BulkWalletShareOptions {
walletPassphrase: string;
keyShareOptions: Array<{
userId: string;
pubKey: string;
path: string;
permissions: string[];
}>;
}

export type WalletShareState = 'active' | 'accepted' | 'canceled' | 'rejected' | 'pendingapproval';

export interface BulkWalletShareKeychain {
pub: string;
encryptedPrv: string;
fromPubKey: string;
toPubKey: string;
path: string;
}

export interface WalletShare {
id: string;
coin: string;
wallet: string;
walletLabel?: string;
fromUser: string;
toUser: string;
permissions: string[];
state?: WalletShareState;
enterprise?: string;
message?: string;
pendingApprovalId?: string;
keychainOverrideRequired?: boolean;
isUMSInitiated?: boolean;
keychain?: BulkWalletShareKeychain;
}

export interface CreateBulkWalletShareListResponse {
shares: WalletShare[];
}
export interface RemoveUserOptions {
userId?: string;
}
Expand Down Expand Up @@ -753,6 +799,8 @@ export interface IWallet {
getPrv(params?: GetPrvOptions): Promise<any>;
createShare(params?: CreateShareOptions): Promise<any>;
shareWallet(params?: ShareWalletOptions): Promise<any>;
createBulkKeyShares(params?: BulkCreateShareOption[]): Promise<CreateBulkWalletShareListResponse>;
createBulkWalletShare(params?: BulkWalletShareOptions): Promise<CreateBulkWalletShareListResponse>;
removeUser(params?: RemoveUserOptions): Promise<any>;
prebuildTransaction(params?: PrebuildTransactionOptions): Promise<PrebuildTransactionResult>;
signTransaction(params?: WalletSignTransactionOptions): Promise<SignedTransaction>;
Expand Down
Loading

0 comments on commit 7bb4f8f

Please sign in to comment.