Skip to content

Commit

Permalink
Merge pull request #4897 from BitGo/feature/CS-3707-bulk-accept-walle…
Browse files Browse the repository at this point in the history
…tShare

feat(sdk-core): add bulkAcceptShare function
  • Loading branch information
sdoshibitgo authored Sep 15, 2024
2 parents a05dd50 + fac2743 commit a10dac8
Show file tree
Hide file tree
Showing 4 changed files with 328 additions and 3 deletions.
33 changes: 33 additions & 0 deletions examples/ts/bulk-accept-shares.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Accept multiple wallet shares.
* This makes use of the convenience function wallets().bulkAcceptShare()
*
* This tool will help you see how to use the BitGo API to easily list your
* BitGo wallets.
*
* Copyright 2022, BitGo, Inc. All Rights Reserved.
*/
import { BitGoAPI } from '@bitgo/sdk-api';
import { Tltc } from '@bitgo/sdk-coin-ltc';
require('dotenv').config({ path: '../../.env' });

const bitgo = new BitGoAPI({
accessToken: process.env.TESTNET_ACCESS_TOKEN,
env: 'test',
});

const coin = 'tltc';
bitgo.register(coin, Tltc.createInstance);

const walletShareIds = ['']; // add the shareIds which needs to be accepted
const userLoginPassword = ''; // add the user login password

async function main() {
const acceptShare = await bitgo.coin(coin).wallets().bulkAcceptShare({
walletShareIds: walletShareIds,
userLoginPassword: userLoginPassword,
});
console.dir(acceptShare);
}

main().catch((e) => console.error(e));
181 changes: 179 additions & 2 deletions modules/bitgo/test/v2/unit/wallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import * as nock from 'nock';
import * as sinon from 'sinon';
import * as should from 'should';
import * as _ from 'lodash';

import * as utxoLib from '@bitgo/utxo-lib';
import { TestBitGo } from '@bitgo/sdk-test';
import {
BlsUtils,
Expand All @@ -18,8 +18,11 @@ import {
GenerateWalletOptions,
Wallet,
isWalletWithKeychains,
BulkWalletShareOptions,
OptionalKeychainEncryptedKey,
decryptKeychainPrivateKey,
makeRandomKey,
getSharedSecret,
BulkWalletShareOptions,
KeychainWithEncryptedPrv,
} from '@bitgo/sdk-core';
import { BitGo } from '../../../src';
Expand Down Expand Up @@ -1430,6 +1433,180 @@ describe('V2 Wallets:', function () {
await wallets.acceptShare({ walletShareId: shareId });
acceptShareNock.done();
});

describe('bulkAcceptShare', function () {
afterEach(function () {
nock.cleanAll();
nock.pendingMocks().length.should.equal(0);
sinon.restore();
});

it('should throw validation error for userPassword empty string', async () => {
await wallets
.bulkAcceptShare({ walletShareIds: [], userLoginPassword: '' })
.should.rejectedWith('Missing parameter: userLoginPassword');
});

it('should throw assertion error for empty walletShareIds', async () => {
await wallets
.bulkAcceptShare({ walletShareIds: [], userLoginPassword: 'dummy@123' })
.should.rejectedWith('no walletShareIds are passed');
});

it('should throw error for no valid wallet shares', async () => {
sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
incoming: [
{
id: '66a229dbdccdcfb95b44fc2745a60bd4',
coin: 'tsol',
walletLabel: 'testing',
fromUser: 'dummyFromUser',
toUser: 'dummyToUser',
wallet: 'dummyWalletId',
permissions: ['spend'],
state: 'active',
},
],
outgoing: [],
});
await wallets
.bulkAcceptShare({
walletShareIds: ['66a229dbdccdcfb95b44fc2745a60bd1'],
userLoginPassword: 'dummy@123',
})
.should.rejectedWith('invalid wallet shares provided');
});

it('should throw error for no valid walletShares with keychain', async () => {
sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
incoming: [
{
id: '66a229dbdccdcfb95b44fc2745a60bd4',
coin: 'tsol',
walletLabel: 'testing',
fromUser: 'dummyFromUser',
toUser: 'dummyToUser',
wallet: 'dummyWalletId',
permissions: ['spend'],
state: 'active',
},
],
outgoing: [],
});

await wallets
.bulkAcceptShare({
walletShareIds: ['66a229dbdccdcfb95b44fc2745a60bd4'],
userLoginPassword: 'dummy@123',
})
.should.rejectedWith('invalid wallet shares provided');
});

it('should throw error for ecdh keychain undefined', async () => {
sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
incoming: [
{
id: '66a229dbdccdcfb95b44fc2745a60bd4',
coin: 'tsol',
walletLabel: 'testing',
fromUser: 'dummyFromUser',
toUser: 'dummyToUser',
wallet: 'dummyWalletId',
permissions: ['spend'],
state: 'active',
keychain: {
pub: 'pub',
toPubKey: 'toPubKey',
fromPubKey: 'fromPubKey',
encryptedPrv: 'encryptedPrv',
path: 'path',
},
},
],
outgoing: [],
});
sinon.stub(bitgo, 'getECDHKeychain').resolves({
prv: 'private key',
});

await wallets
.bulkAcceptShare({
walletShareIds: ['66a229dbdccdcfb95b44fc2745a60bd4'],
userLoginPassword: 'dummy@123',
})
.should.rejectedWith('encryptedXprv was not found on sharing keychain');
});

it('should successfully accept share', async () => {
const fromUserPrv = Math.random();
const walletPassphrase = 'bitgo1234';
const keychainTest: OptionalKeychainEncryptedKey = {
encryptedPrv: bitgo.encrypt({ input: fromUserPrv.toString(), password: walletPassphrase }),
};
const userPrv = decryptKeychainPrivateKey(bitgo, keychainTest, walletPassphrase);
if (!userPrv) {
throw new Error('Unable to decrypt user keychain');
}

const toKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef02deadbeef02deadbeef02deadbeef02', 'hex'));
const path = 'm/999999/1/1';
const pubkey = toKeychain.derivePath(path).publicKey.toString('hex');

const eckey = makeRandomKey();
const secret = getSharedSecret(eckey, Buffer.from(pubkey, 'hex')).toString('hex');
const newEncryptedPrv = bitgo.encrypt({ password: secret, input: userPrv });
nock(bgUrl)
.get('/api/v2/walletshares')
.reply(200, {
incoming: [
{
id: '66a229dbdccdcfb95b44fc2745a60bd4',
isUMSInitiated: true,
keychain: {
path: path,
fromPubKey: eckey.publicKey.toString('hex'),
encryptedPrv: newEncryptedPrv,
toPubKey: pubkey,
pub: pubkey,
},
},
],
});
nock(bgUrl)
.put('/api/v2/walletshares/accept')
.reply(200, {
acceptedWalletShares: [
{
walletShareId: '66a229dbdccdcfb95b44fc2745a60bd4',
},
],
});

const myEcdhKeychain = await bitgo.keychains().create();
sinon.stub(bitgo, 'getECDHKeychain').resolves({
encryptedXprv: bitgo.encrypt({ input: myEcdhKeychain.xprv, password: walletPassphrase }),
});

const prvKey = bitgo.decrypt({
password: walletPassphrase,
input: bitgo.encrypt({ input: myEcdhKeychain.xprv, password: walletPassphrase }),
});
sinon.stub(bitgo, 'decrypt').returns(prvKey);
sinon.stub(moduleBitgo, 'getSharedSecret').resolves('fakeSharedSecret');

const share = await wallets.bulkAcceptShare({
walletShareIds: ['66a229dbdccdcfb95b44fc2745a60bd4'],
userLoginPassword: walletPassphrase,
});
assert.deepEqual(share, {
acceptedWalletShares: [
{
walletShareId: '66a229dbdccdcfb95b44fc2745a60bd4',
},
],
});
});
});
});

describe('createBulkKeyShares tests', () => {
Expand Down
24 changes: 23 additions & 1 deletion modules/sdk-core/src/bitgo/wallet/iWallets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as t from 'io-ts';

import { IRequestTracer } from '../../api';
import { KeychainsTriplet, LightningKeychainsTriplet } from '../baseCoin';
import { IWallet, PaginationOptions } from './iWallet';
import { IWallet, PaginationOptions, WalletShare } from './iWallet';
import { Wallet } from './wallet';

export interface WalletWithKeychains extends KeychainsTriplet {
Expand Down Expand Up @@ -107,6 +107,17 @@ export interface AcceptShareOptions {
newWalletPassphrase?: string;
}

export interface BulkAcceptShareOptions {
walletShareIds: string[];
userLoginPassword: string;
newWalletPassphrase?: string;
}

export interface AcceptShareOptionsRequest {
walletShareId: string;
encryptedPrv: string;
}

export interface AddWalletOptions {
coinSpecific?: any;
enterprise?: string;
Expand Down Expand Up @@ -157,6 +168,15 @@ export interface ListWalletOptions extends PaginationOptions {
allTokens?: boolean;
}

export interface WalletShares {
incoming: WalletShare[]; // WalletShares that the user has to accept
outgoing: WalletShare[]; // WalletShares that the user has created
}

export interface AcceptShareResponse {
walletShareId: string;
}

export interface IWallets {
get(params?: GetWalletOptions): Promise<Wallet>;
list(params?: ListWalletOptions): Promise<{ wallets: IWallet[] }>;
Expand All @@ -171,4 +191,6 @@ export interface IWallets {
getWallet(params?: GetWalletOptions): Promise<IWallet>;
getWalletByAddress(params?: GetWalletByAddressOptions): Promise<IWallet>;
getTotalBalances(params?: Record<string, never>): Promise<any>;
bulkAcceptShare(params: BulkAcceptShareOptions): Promise<AcceptShareResponse[]>;
listSharesV2(): Promise<WalletShares>;
}
Loading

0 comments on commit a10dac8

Please sign in to comment.