diff --git a/modules/bitgo/test/v2/unit/wallet.ts b/modules/bitgo/test/v2/unit/wallet.ts index e348c23166..7eb54e9de4 100644 --- a/modules/bitgo/test/v2/unit/wallet.ts +++ b/modules/bitgo/test/v2/unit/wallet.ts @@ -2069,6 +2069,148 @@ describe('V2 Wallet:', function () { getSharingKeyNock.isDone().should.be.True(); getKeyNock.isDone().should.be.True(); }); + + describe('OFC Multi-User-Key Wallet Sharing', function () { + const userId = '123'; + const permissions = 'view,spend'; + let ofcWallet: Wallet; + let ofcMultiUserKeyWallet: Wallet; + + before(function () { + const ofcCoin = bitgo.coin('ofc'); + + // Regular OFC wallet without multi-user-key feature + const regularOfcWalletData = { + id: '5b34252f1bf349930e3400c00000000', + coin: 'ofc', + keys: [ + '5b3424f91bf349930e34018100000000', + '5b3424f91bf349930e34018200000000', + '5b3424f91bf349930e34018300000000', + ], + coinSpecific: {}, + multisigType: 'onchain', + type: 'hot', + } as any; + ofcWallet = new Wallet(bitgo, ofcCoin, regularOfcWalletData); + + // OFC wallet with multi-user-key feature + const multiUserKeyWalletData = { + id: '5b34252f1bf349930e3400d00000000', + coin: 'ofc', + keys: [ + '5b3424f91bf349930e34018400000000', + '5b3424f91bf349930e34018500000000', + '5b3424f91bf349930e34018600000000', + ], + coinSpecific: { + features: ['multi-user-key'], + }, + multisigType: 'onchain', + type: 'hot', + } as any; + ofcMultiUserKeyWallet = new Wallet(bitgo, ofcCoin, multiUserKeyWalletData); + }); + + afterEach(function () { + sinon.restore(); + nock.cleanAll(); + }); + + it('should exclude keychain property for multi-user-key wallets in createShare', async function () { + const createShareParams = { + user: userId, + permissions, + }; + + const createShareNock = nock(bgUrl) + .post(`/api/v2/ofc/wallet/${ofcMultiUserKeyWallet.id()}/share`, (body) => { + // Verify that keychain is not included in the request + body.should.not.have.property('keychain'); + body.user.should.equal(userId); + body.permissions.should.equal(permissions); + return true; + }) + .reply(200, {}); + + await ofcMultiUserKeyWallet.createShare(createShareParams); + + createShareNock.isDone().should.be.True(); + }); + + it('should throw error when keychain is provided for multi-user-key wallets in createShare', async function () { + const createShareParams = { + user: userId, + permissions, + keychain: { + pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5', + encryptedPrv: 'encrypted', + fromPubKey: 'fromPub', + toPubKey: 'toPub', + path: 'm/999999/1/1', + }, + }; + + await ofcMultiUserKeyWallet + .createShare(createShareParams) + .should.be.rejectedWith('keychain property must not be provided for multi-user-key wallets'); + }); + + it('should include keychain property for non-multi-user-key OFC wallets', async function () { + const createShareParams = { + user: userId, + permissions, + keychain: { + pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5', + encryptedPrv: 'encrypted', + fromPubKey: 'fromPub', + toPubKey: 'toPub', + path: 'm/999999/1/1', + }, + }; + + const createShareNock = nock(bgUrl) + .post(`/api/v2/ofc/wallet/${ofcWallet.id()}/share`, (body) => { + // Verify that keychain IS included in the request for regular wallets + body.should.have.property('keychain'); + body.keychain.should.have.property('pub'); + body.keychain.should.have.property('encryptedPrv'); + body.keychain.should.have.property('fromPubKey'); + body.keychain.should.have.property('toPubKey'); + body.keychain.should.have.property('path'); + body.user.should.equal(userId); + body.permissions.should.equal(permissions); + return true; + }) + .reply(200, {}); + + await ofcWallet.createShare(createShareParams); + + createShareNock.isDone().should.be.True(); + }); + + it('should handle empty keychain object for multi-user-key wallets', async function () { + const createShareParams = { + user: userId, + permissions, + keychain: {}, + }; + + const createShareNock = nock(bgUrl) + .post(`/api/v2/ofc/wallet/${ofcMultiUserKeyWallet.id()}/share`, (body) => { + // Verify that keychain is not included in the request even if passed as empty + body.should.not.have.property('keychain'); + body.user.should.equal(userId); + body.permissions.should.equal(permissions); + return true; + }) + .reply(200, {}); + + await ofcMultiUserKeyWallet.createShare(createShareParams); + + createShareNock.isDone().should.be.True(); + }); + }); }); describe('Wallet Freezing', function () { diff --git a/modules/sdk-core/src/bitgo/wallet/iWallet.ts b/modules/sdk-core/src/bitgo/wallet/iWallet.ts index 71f58ca54b..80123bff15 100644 --- a/modules/sdk-core/src/bitgo/wallet/iWallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/iWallet.ts @@ -328,6 +328,7 @@ export interface WalletCoinSpecific { walletVersion?: number; hashAlgorithm?: string; pendingEcdsaTssInitialization?: boolean; + features?: string[]; /** * Lightning coin specific data starts */ diff --git a/modules/sdk-core/src/bitgo/wallet/wallet.ts b/modules/sdk-core/src/bitgo/wallet/wallet.ts index 924acdc102..5af79d116c 100644 --- a/modules/sdk-core/src/bitgo/wallet/wallet.ts +++ b/modules/sdk-core/src/bitgo/wallet/wallet.ts @@ -1594,6 +1594,10 @@ export class Wallet implements IWallet { return userPrv; } + private isMultiUserKeyWallet(): boolean { + return this._wallet.coinSpecific?.features?.includes('multi-user-key') ?? false; + } + /** * Send an encrypted wallet share to BitGo. * @param params @@ -1601,6 +1605,16 @@ export class Wallet implements IWallet { async createShare(params: CreateShareOptions = {}): Promise { common.validateParams(params, ['user', 'permissions'], []); + const isMultiUserKeyWallet = this.isMultiUserKeyWallet(); + + if (isMultiUserKeyWallet) { + if (params.keychain && !_.isEmpty(params.keychain)) { + throw new Error('keychain property must not be provided for multi-user-key wallets'); + } + // Remove keychain from params if presents + return await this.bitgo.post(this.url('/share')).send(_.omit(params, 'keychain')).result(); + } + if (params.keychain && !_.isEmpty(params.keychain)) { if ( !params.keychain.pub ||