From a87b65ea3eb724593ca680e5211ffe56865c16e9 Mon Sep 17 00:00:00 2001 From: Ravneet Sandhu Date: Wed, 10 Apr 2024 20:09:11 +0530 Subject: [PATCH] feat: modify accept share method Ticket: PX-3296 We are modifying the accept share method for go account wallet invitations --- modules/bitgo/test/v2/unit/wallets.ts | 4 +- modules/sdk-core/package.json | 1 + modules/sdk-core/src/bitgo/wallet/iWallet.ts | 1 + modules/sdk-core/src/bitgo/wallet/iWallets.ts | 1 + modules/sdk-core/src/bitgo/wallet/wallets.ts | 112 ++++++++++++++++-- 5 files changed, 107 insertions(+), 12 deletions(-) diff --git a/modules/bitgo/test/v2/unit/wallets.ts b/modules/bitgo/test/v2/unit/wallets.ts index 80b676ce4e..11a1d7261d 100644 --- a/modules/bitgo/test/v2/unit/wallets.ts +++ b/modules/bitgo/test/v2/unit/wallets.ts @@ -789,7 +789,9 @@ describe('V2 Wallets:', function () { nock(bgUrl).get(`/api/v2/tbtc/walletshare/${shareId}`).reply(200, {}); const acceptShareNock = nock(bgUrl) .post(`/api/v2/tbtc/walletshare/${shareId}`, { walletShareId: shareId, state: 'accepted' }) - .reply(200, {}); + .reply(200, { + result: () => ({ changed: true }), + }); await wallets.acceptShare({ walletShareId: shareId }); acceptShareNock.done(); diff --git a/modules/sdk-core/package.json b/modules/sdk-core/package.json index 85b9a2d98d..d008b3b2c9 100644 --- a/modules/sdk-core/package.json +++ b/modules/sdk-core/package.json @@ -40,6 +40,7 @@ "@bitgo/bls-dkg": "^1.3.1", "@bitgo/public-types": "2.1.0", "@bitgo/sdk-lib-mpc": "^9.2.0", + "@bitgo/sjcl": "^1.0.1", "@bitgo/statics": "^48.5.0", "@bitgo/utxo-lib": "^9.35.0", "@noble/secp256k1": "1.6.3", diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index a4ce4b1a0c..58f16ae6dd 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -609,6 +609,7 @@ export interface WalletData { tokens?: Record[]; nfts?: { [contractAddressOrToken: string]: NftBalance }; unsupportedNfts?: { [contractAddress: string]: NftBalance }; + users?: any[]; } export interface RecoverTokenOptions { diff --git a/modules/sdk-core/src/bitgo/wallet/iWallets.ts b/modules/sdk-core/src/bitgo/wallet/iWallets.ts index 7ce6121bbd..3d4e95f8db 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallets.ts @@ -72,6 +72,7 @@ export interface UpdateShareOptions { walletShareId?: string; state?: string; encryptedPrv?: string; + keyId?: string; } export interface AcceptShareOptions { diff --git a/modules/sdk-core/src/bitgo/wallet/wallets.ts b/modules/sdk-core/src/bitgo/wallet/wallets.ts index e4b67ec5f1..42a15b888a 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallets.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallets.ts @@ -27,6 +27,8 @@ import { WalletWithKeychains, } from './iWallets'; import { Wallet } from './wallet'; +import * as sjcl from '@bitgo/sjcl'; +import * as bs58 from 'bs58'; export class Wallets implements IWallets { private readonly bitgo: BitGoBase; @@ -506,10 +508,7 @@ export class Wallets implements IWallets { async updateShare(params: UpdateShareOptions = {}): Promise { common.validateParams(params, ['walletShareId'], []); - return await this.bitgo - .post(this.baseCoin.url('/walletshare/' + params.walletShareId)) - .send(params) - .result(); + return await this.bitgo.post(this.baseCoin.url('/walletshare/' + params.walletShareId)).send(params); } /** @@ -538,6 +537,77 @@ export class Wallets implements IWallets { .result(); } + /** + * Re-share wallet with existing spenders of the wallet + * @param walletId + * @param userPassword + */ + async reshareOfcAccountWithSpenders(walletId: string, userPassword: string): Promise { + const wallet = await this.bitgo.coin('ofc').wallets().get({ id: walletId }); + const enterpriseUsersResponse = (await this.bitgo + .get(`/api/v2/enterprise/${wallet?._wallet?.enterprise}/user`) + .result()) as any; + // create a map of users for easy lookup - we need the user email id to share the wallet + const usersMap = new Map( + [...enterpriseUsersResponse?.adminUsers, ...enterpriseUsersResponse?.nonAdminUsers].map((obj) => [obj.id, obj]) + ); + wallet?._wallet?.users?.forEach(async (user) => { + try { + // user should be a spender and not an admin + if (user.permissions.includes('spend') && !user.permissions.includes('admin')) { + const userObject = usersMap.get(user.user); + const shareParams = { + walletId: walletId, + user: user.user, + permissions: user.permissions.join(','), + walletPassphrase: userPassword, + email: userObject.email.email, + coin: wallet.coin, + reshare: true, + }; + wallet.shareWallet(shareParams); + } + } catch (e) { + // TODO: gracefully handle this error + console.error(e); + } + }); + } + + /** + * Generate a random password + * @param {Number} numWords Number of 32-bit words + * @returns {String} base58 random password + */ + generateRandomPassword(numWords = 5): string { + const bytes = sjcl.codec.bytes.fromBits(sjcl.random.randomWords(numWords)); + return bs58.encode(bytes); + } + + /** + * Create keychain for ofc wallet using the password + * @param userPassword + * @returns + */ + async createKeychain(userPassword: string): Promise { + const sdkCoin = await this.bitgo.coin('ofc'); + const keychains = sdkCoin.keychains(); + const newKeychain = keychains.create(); + const originalPasscodeEncryptionCode = this.generateRandomPassword(); + + const encryptedPrv = this.bitgo.encrypt({ + password: userPassword, + input: newKeychain.prv, + }); + + const walletKeychain = await keychains.add({ + encryptedPrv, + originalPasscodeEncryptionCode, + pub: newKeychain.pub, + source: 'user', + }); + return walletKeychain; + } /** * Accepts a wallet share, adding the wallet to the user's list * Needs a user's password to decrypt the shared key @@ -554,15 +624,36 @@ export class Wallets implements IWallets { common.validateParams(params, ['walletShareId'], ['overrideEncryptedPrv', 'userPassword', 'newWalletPassphrase']); let encryptedPrv = params.overrideEncryptedPrv; - const walletShare = (await this.getShare({ walletShareId: params.walletShareId })) as any; - - // Return right away if there is no keychain to decrypt, or if explicit encryptedPrv was provided - if (!walletShare.keychain || !walletShare.keychain.encryptedPrv || encryptedPrv) { - return this.updateShare({ + if (walletShare.keychainOverrideRequired && walletShare.permissions.indexOf('admin') !== -1) { + if (_.isUndefined(params.userPassword)) { + throw new Error('userPassword param must be provided to decrypt shared key'); + } + const walletKeychain = await this.createKeychain(params.userPassword); + const response = await this.updateShare({ walletShareId: params.walletShareId, state: 'accepted', + keyId: walletKeychain.id, }); + // If the wallet share was accepted successfully (changed=true), reshare the wallet with the spenders + if (response.statusCode === 200 && response.result().changed) { + try { + await this.reshareOfcAccountWithSpenders(walletShare.wallet, params.userPassword); + } catch (e) { + // TODO: gracefully handle this error + console.error(e); + } + } + return response; + } + // Return right away if there is no keychain to decrypt, or if explicit encryptedPrv was provided + if (!walletShare.keychain || !walletShare.keychain.encryptedPrv || encryptedPrv) { + return ( + this.updateShare({ + walletShareId: params.walletShareId, + state: 'accepted', + }) as any + ).result(); } // More than viewing was requested, so we need to process the wallet keys using the shared ecdh scheme @@ -606,8 +697,7 @@ export class Wallets implements IWallets { if (encryptedPrv) { updateParams.encryptedPrv = encryptedPrv; } - - return this.updateShare(updateParams); + return (this.updateShare(updateParams) as any).result(); } /**