PHP WebShell

Текущая директория: /opt/BitGoJS/modules/bitgo/test/v2/unit

Просмотр файла: wallets.ts

//
// Tests for Wallets
//
import * as assert from 'assert';
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 {
  common,
  TssUtils,
  Wallets,
  ECDSAUtils,
  KeychainsTriplet,
  GenerateWalletOptions,
  Wallet,
  isWalletWithKeychains,
  OptionalKeychainEncryptedKey,
  decryptKeychainPrivateKey,
  makeRandomKey,
  getSharedSecret,
  BulkWalletShareOptions,
  KeychainWithEncryptedPrv,
  WalletWithKeychains,
  multisigTypes,
  IncorrectPasswordError,
} 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' });
  let wallets;
  let bgUrl;

  before(function () {
    nock('https://bitgo.fakeurl').persist().get('/api/v1/client/constants').reply(200, { ttl: 3600, constants: {} });

    bitgo.initializeTestVars();

    const basecoin = bitgo.coin('tbtc');
    wallets = basecoin.wallets();
    bgUrl = common.Environments[bitgo.getEnv()].uri;
  });

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

  describe('Add Wallet:', function () {
    it('throws on invalid arguments', async function () {
      // isCustodial flag is not a boolean
      await wallets
        .add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, isCustodial: 1 })
        .should.be.rejectedWith('invalid argument for isCustodial - boolean expected');

      // type is not a string
      await wallets
        .add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, type: 1 })
        .should.be.rejectedWith('Expecting parameter string: type but found number');

      // Address is an invalid address
      await wallets
        .add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, address: '$' })
        .should.be.rejectedWith('invalid argument for address - valid address string expected');

      // gasPrice is a number
      await wallets
        .add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, gasPrice: '17' })
        .should.be.rejectedWith('invalid argument for gasPrice - number expected');

      // walletVersion is a number
      await wallets
        .add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, walletVersion: '1' })
        .should.be.rejectedWith('invalid argument for walletVersion - number expected');
    });

    it('creates a paired custodial wallet', async function () {
      nock(bgUrl)
        .post('/api/v2/tbtc/wallet/add', function (body) {
          body.isCustodial.should.be.true();
          body.should.have.property('keys');
          body.m.should.equal(2);
          body.n.should.equal(3);
          return true;
        })
        .reply(200, {});
      await wallets.add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, isCustodial: true });
    });

    it('creates an eos wallet with custom address', async function () {
      const eosBitGo = TestBitGo.decorate(BitGo, { env: 'mock' });
      eosBitGo.initializeTestVars();
      const eosWallets = eosBitGo.coin('teos').wallets();
      const address = 'testeosaddre';
      nock(bgUrl)
        .post('/api/v2/teos/wallet/add', function (body) {
          body.should.have.property('keys');
          body.m.should.equal(2);
          body.n.should.equal(3);
          body.address.should.equal(address);
          return true;
        })
        .reply(200, {});
      await eosWallets.add({ label: 'label', enterprise: 'enterprise', keys: [], m: 2, n: 3, address } as any);
    });

    it('creates a single custodial wallet', async function () {
      nock(bgUrl)
        .post('/api/v2/tbtc/wallet/add', function (body) {
          body.type.should.equal('custodial');
          body.should.not.have.property('keys');
          body.should.not.have.property('m');
          body.should.not.have.property('n');
          return true;
        })
        .reply(200, {});
      await wallets.add({ label: 'label', enterprise: 'enterprise', type: 'custodial' });
    });

    it('creates a wallet with custom gasPrice', async function () {
      const ethBitGo = TestBitGo.decorate(BitGo, { env: 'mock' });
      ethBitGo.initializeTestVars();
      const ethWallets = ethBitGo.coin('teth').wallets();
      nock(bgUrl)
        .post('/api/v2/teth/wallet/add', function (body) {
          body.type.should.equal('custodial');
          body.gasPrice.should.equal(20000000000);
          body.should.not.have.property('keys');
          body.should.not.have.property('m');
          body.should.not.have.property('n');
          return true;
        })
        .reply(200, {});
      await ethWallets.add({
        label: 'label',
        enterprise: 'enterprise',
        type: 'custodial',
        gasPrice: 20000000000,
      } as any);
    });

    it('creates a new wallet with walletVersion', async function () {
      const ethBitGo = TestBitGo.decorate(BitGo, { env: 'mock' });
      ethBitGo.initializeTestVars();
      const ethWallets = ethBitGo.coin('teth').wallets();
      nock(bgUrl)
        .post('/api/v2/teth/wallet/add', function (body) {
          body.type.should.equal('custodial');
          body.walletVersion.should.equal(1);
          body.should.not.have.property('keys');
          body.should.not.have.property('m');
          body.should.not.have.property('n');
          return true;
        })
        .reply(200, {});
      await ethWallets.add({ label: 'label', enterprise: 'enterprise', type: 'custodial', walletVersion: 1 } as any);
    });

    it('creates a new hot wallet with userKey', async function () {
      nock(bgUrl)
        .post('/api/v2/tbtc/wallet/add', function (body) {
          body.type.should.equal('hot');
          body.should.have.property('keys');
          body.should.have.property('m');
          body.should.have.property('n');
          return true;
        })
        .reply(200, {});
      await wallets.add({
        label: 'label',
        enterprise: 'enterprise',
        type: 'hot',
        keys: [],
        m: 2,
        n: 3,
        userKey: 'test123',
      });
    });
  });

  describe('Generate wallet:', function () {
    const sandbox = sinon.createSandbox();
    it('should validate parameters', async function () {
      let params = {};
      await wallets.generateWallet(params).should.be.rejectedWith('Missing parameter: label');

      params = {
        label: 'abc',
        backupXpub: 'backup',
        backupXpubProvider: 'provider',
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith('Cannot provide more than one backupXpub or backupXpubProvider flag');

      params = {
        label: 'abc',
        passcodeEncryptionCode: 123,
      };
      await wallets.generateWallet(params).should.be.rejectedWith('passcodeEncryptionCode must be a string');

      params = {
        label: 'abc',
        enterprise: 1234,
      };
      await wallets.generateWallet(params).should.be.rejectedWith('invalid enterprise argument, expecting string');

      params = {
        label: 'abc',
        disableTransactionNotifications: 'string',
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith('invalid disableTransactionNotifications argument, expecting boolean');

      params = {
        label: 'abc',
        gasPrice: 'string',
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith('invalid gas price argument, expecting number or number as string');

      params = {
        label: 'abc',
        gasPrice: true,
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith('invalid gas price argument, expecting number or number as string');

      params = {
        label: 'abc',
        gasPrice: 123,
        eip1559: {
          maxFeePerGas: 1234,
          maxPriorityFeePerGas: 123,
        },
      };

      await wallets.generateWallet(params).should.be.rejectedWith('can not use both eip1559 and gasPrice values');

      params = {
        label: 'abc',
        eip1559: {
          maxFeePerGas: 'q1234',
          maxPriorityFeePerGas: '123',
        },
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith('invalid max fee argument, expecting number or number as string');

      params = {
        label: 'abc',
        eip1559: {
          maxFeePerGas: 1234,
          maxPriorityFeePerGas: '123a',
        },
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith('invalid priority fee argument, expecting number or number as string');

      params = {
        label: 'abc',
        disableKRSEmail: 'string',
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith('invalid disableKRSEmail argument, expecting boolean');

      params = {
        label: 'abc',
        krsSpecific: {
          malicious: {
            javascript: {
              code: 'bad.js',
            },
          },
        },
      };

      await wallets
        .generateWallet(params)
        .should.be.rejectedWith(
          'krsSpecific object contains illegal values. values must be strings, booleans, or numbers'
        );
    });

    it('should correctly disable krs emails when creating backup keychains', async function () {
      const params = {
        label: 'my_wallet',
        disableKRSEmail: true,
        backupXpubProvider: 'test',
        passphrase: 'test123',
        userKey: 'xpub123',
      };

      // bitgo key
      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'bitgo' }))
        .reply(200);

      // user key
      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.conforms({ pub: (p) => p.startsWith('xpub') }))
        .reply(200);

      // backup key
      nock(bgUrl)
        .post(
          '/api/v2/tbtc/key',
          _.matches({
            source: 'backup',
            provider: params.backupXpubProvider,
            disableKRSEmail: true,
          })
        )
        .reply(200);

      // wallet
      nock(bgUrl).post('/api/v2/tbtc/wallet/add').reply(200);

      await wallets.generateWallet(params);
    });

    it('should correctly pass through the krsSpecific param when creating backup keychains', async function () {
      const params = {
        label: 'my_wallet',
        backupXpubProvider: 'test',
        passphrase: 'test123',
        userKey: 'xpub123',
        krsSpecific: { coverage: 'insurance', expensive: true, howExpensive: 25 },
      };

      // bitgo key
      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'bitgo' }))
        .reply(200);

      // user key
      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.conforms({ pub: (p) => p.startsWith('xpub') }))
        .reply(200);

      // backup key
      nock(bgUrl)
        .post(
          '/api/v2/tbtc/key',
          _.matches({
            source: 'backup',
            provider: params.backupXpubProvider,
            krsSpecific: { coverage: 'insurance', expensive: true, howExpensive: 25 },
          })
        )
        .reply(200);

      // wallet
      nock(bgUrl).post('/api/v2/tbtc/wallet/add').reply(200);

      await wallets.generateWallet(params);
    });

    it('should send the cold derivation seed for a user key', async () => {
      const params = {
        label: 'my-cold-wallet',
        passphrase: 'test123',
        userKey:
          'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
        coldDerivationSeed: '123',
      };

      // bitgo key
      const bitgoKeyNock = nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'bitgo' }))
        .reply(200);

      // user key
      const userKeyNock = nock(bgUrl)
        .post(
          '/api/v2/tbtc/key',
          _.matches({
            derivedFromParentWithSeed: params.coldDerivationSeed,
          })
        )
        .reply(200);

      // backup key
      const backupKeyNock = nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'backup' }))
        .reply(200);

      // wallet
      const walletNock = nock(bgUrl).post('/api/v2/tbtc/wallet/add').reply(200);

      await wallets.generateWallet(params);
      for (const scope of [bitgoKeyNock, userKeyNock, backupKeyNock, walletNock]) {
        scope.done();
      }
    });

    it('should generate custodial onchain wallet without passing m, n, keys, keySignatures', async () => {
      const params: GenerateWalletOptions = {
        label: 'test wallet',
        enterprise: 'myenterprise',
        type: 'custodial',
      };

      const walletNock = nock(bgUrl)
        .post('/api/v2/tbtc/wallet/add', function (body) {
          body.type.should.equal('custodial');
          should.not.exist(body.m);
          should.not.exist(body.n);
          should.not.exist(body.keys);
          should.not.exist(body.keySignatures);
          return true;
        })
        .reply(200, { id: '123', baseCoin: bitgo.coin('tbtc'), keys: ['123', '456', '789'] });

      nock(bgUrl).get('/api/v2/tbtc/key/123').reply(200, { pub: 'bitgoPub', id: '789' });
      nock(bgUrl).get('/api/v2/tbtc/key/456', _.matches({})).reply(200);
      nock(bgUrl).get('/api/v2/tbtc/key/789').reply(200, { pub: 'backupPub', id: '789' });

      const response = await wallets.generateWallet(params);

      walletNock.isDone().should.be.true();

      assert.ok(response.encryptedWalletPassphrase === undefined);
      assert.ok(response.wallet);
    });

    it('should generate hot onchain wallet', async () => {
      const params: GenerateWalletOptions = {
        label: 'test wallet',
        passphrase: 'multisig password',
        enterprise: 'enterprise',
        passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
      };

      const walletNock = nock(bgUrl)
        .post('/api/v2/tbtc/wallet/add', function (body) {
          body.type.should.equal('hot');
          return true;
        })
        .reply(200);

      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'bitgo' }))
        .reply(200, { pub: 'bitgoPub' });
      nock(bgUrl).post('/api/v2/tbtc/key', _.matches({})).reply(200);
      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'backup' }))
        .reply(200, { pub: 'backupPub' });

      const response = await wallets.generateWallet(params);

      walletNock.isDone().should.be.true();

      assert.ok(response.encryptedWalletPassphrase);
      assert.ok(response.wallet);
      assert.equal(
        bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
        params.passphrase
      );
    });

    it('should generate hot onchain wallet without passing multisig type', async () => {
      const params: GenerateWalletOptions = {
        label: 'test wallet',
        passphrase: 'multisig password',
        enterprise: 'enterprise',
        passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
      };

      const walletNock = nock(bgUrl)
        .post('/api/v2/tbtc/wallet/add', function (body) {
          body.type.should.equal('hot');
          return true;
        })
        .reply(200);

      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'bitgo' }))
        .reply(200, { pub: 'bitgoPub' });
      nock(bgUrl).post('/api/v2/tbtc/key', _.matches({})).reply(200);
      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'backup' }))
        .reply(200, { pub: 'backupPub' });

      const generateWalletSpy = sandbox.spy(wallets, 'generateWallet');
      const response = await wallets.generateWallet(params);
      walletNock.isDone().should.be.true();
      sinon.assert.calledOnce(generateWalletSpy);
      assert.equal(generateWalletSpy.firstCall?.args[0]?.multisigType, multisigTypes.onchain);
      assert.ok(response.encryptedWalletPassphrase);
      assert.ok(response.wallet);
      assert.equal(
        bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
        params.passphrase
      );
    });
  });

  describe('Generate TSS wallet:', function () {
    const tsol = bitgo.coin('tsol');
    const sandbox = sinon.createSandbox();

    beforeEach(function () {
      nock('https://bitgo.fakeurl')
        .get(`/api/v2/tss/settings`)
        .times(2)
        .reply(200, {
          coinSettings: {
            eth: {
              walletCreationSettings: {},
            },
            bsc: {
              walletCreationSettings: {},
            },
            polygon: {
              walletCreationSettings: {},
            },
          },
        });
    });

    afterEach(function () {
      nock.cleanAll();
      sandbox.verifyAndRestore();
    });

    it('should create a new TSS wallet', async function () {
      const stubbedKeychainsTriplet: KeychainsTriplet = {
        userKeychain: {
          id: '1',
          pub: 'userPub',
          type: 'independent',
          source: 'user',
        },
        backupKeychain: {
          id: '2',
          pub: 'userPub',
          type: 'independent',
          source: 'backup',
        },
        bitgoKeychain: {
          id: '3',
          pub: 'userPub',
          type: 'independent',
          source: 'bitgo',
        },
      };
      sandbox.stub(TssUtils.prototype, 'createKeychains').resolves(stubbedKeychainsTriplet);

      const walletNock = nock('https://bitgo.fakeurl').post('/api/v2/tsol/wallet/add').reply(200);

      const wallets = new Wallets(bitgo, tsol);

      const params = {
        label: 'tss wallet',
        passphrase: 'tss password',
        multisigType: 'tss' as any,
        enterprise: 'enterprise',
        passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
      };

      const response = await wallets.generateWallet(params);

      walletNock.isDone().should.be.true();

      assert.ok(response.encryptedWalletPassphrase);
      assert.ok(response.wallet);
      assert.equal(
        bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
        params.passphrase
      );
    });

    it('should create a new TSS wallet without passing multisig type', async function () {
      const stubbedKeychainsTriplet: KeychainsTriplet = {
        userKeychain: {
          id: '1',
          pub: 'userPub',
          type: 'independent',
          source: 'user',
        },
        backupKeychain: {
          id: '2',
          pub: 'userPub',
          type: 'independent',
          source: 'backup',
        },
        bitgoKeychain: {
          id: '3',
          pub: 'userPub',
          type: 'independent',
          source: 'bitgo',
        },
      };
      sandbox.stub(TssUtils.prototype, 'createKeychains').resolves(stubbedKeychainsTriplet);

      const walletNock = nock('https://bitgo.fakeurl')
        .post('/api/v2/tsol/wallet/add', function (body) {
          body.multisigType.should.equal(multisigTypes.tss);
          return true;
        })
        .reply(200);

      const wallets = new Wallets(bitgo, tsol);

      const params = {
        label: 'tss wallet',
        passphrase: 'tss password',
        enterprise: 'enterprise',
        passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
      };

      const generateWalletSpy = sandbox.spy(wallets, 'generateWallet');
      const response = await wallets.generateWallet(params);
      walletNock.isDone().should.be.true();
      sinon.assert.calledOnce(generateWalletSpy);
      assert.equal(generateWalletSpy.firstCall?.args[0]?.multisigType, multisigTypes.tss);
      assert.ok(response.encryptedWalletPassphrase);
      assert.ok(response.wallet);
      assert.equal(
        bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
        params.passphrase
      );
    });

    it('should create a new TSS wallet without providing passcodeEncryptionCode', async function () {
      const stubbedKeychainsTriplet: KeychainsTriplet = {
        userKeychain: {
          id: '1',
          pub: 'userPub',
          type: 'independent',
          source: 'user',
        },
        backupKeychain: {
          id: '2',
          pub: 'userPub',
          type: 'independent',
          source: 'backup',
        },
        bitgoKeychain: {
          id: '3',
          pub: 'userPub',
          type: 'independent',
          source: 'bitgo',
        },
      };
      sandbox.stub(TssUtils.prototype, 'createKeychains').resolves(stubbedKeychainsTriplet);

      const walletNock = nock('https://bitgo.fakeurl').post('/api/v2/tsol/wallet/add').reply(200);

      const wallets = new Wallets(bitgo, tsol);

      const response = await wallets.generateWallet({
        label: 'tss wallet',
        passphrase: 'tss password',
        multisigType: 'tss',
        enterprise: 'enterprise',
      });

      walletNock.isDone().should.be.true();

      assert.ok(response.wallet);
      assert.ok(response.encryptedWalletPassphrase === undefined);
    });

    it('should fail to create TSS wallet with invalid inputs', async function () {
      const tbtc = bitgo.coin('tbtc');
      const params = {
        label: 'my-cold-wallet',
        passphrase: 'test123',
        userKey:
          'xpub661MyMwAqRbcFtXgS5sYJABqqG9YLmC4Q1Rdap9gSE8NqtwybGhePY2gZ29ESFjqJoCu1Rupje8YtGqsefD265TMg7usUDFdp6W1EGMcet8',
        coldDerivationSeed: '123',
      };
      const wallets = new Wallets(bitgo, tbtc);

      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'bitgo' }))
        .reply(200);

      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ derivedFromParentWithSeed: params.coldDerivationSeed }))
        .reply(200);
      nock(bgUrl)
        .post('/api/v2/tbtc/key', _.matches({ source: 'backup' }))
        .reply(200);

      nock(bgUrl).post('/api/v2/tbtc/wallet/add').reply(200);

      // create a non tss wallet for coin that doesn't support tss even though multisigType is set to tss
      await wallets.generateWallet({ ...params, multisigType: 'tss' });

      const tsolWallets = new Wallets(bitgo, tsol);

      await tsolWallets
        .generateWallet({
          label: 'tss cold wallet',
          passphrase: 'passphrase',
          userKey: 'user key',
          multisigType: 'tss',
        })
        .should.be.rejectedWith('enterprise is required for TSS wallet');

      await tsolWallets
        .generateWallet({
          label: 'tss cold wallet',
          userKey: 'user key',
          multisigType: 'tss',
          enterprise: 'enterpriseId',
        })
        .should.be.rejectedWith('cannot generate TSS keys without passphrase');
    });

    it('should create a new TSS custodial wallet', async function () {
      const keys = ['1', '2', '3'];

      const walletParams: GenerateWalletOptions = {
        label: 'tss wallet',
        multisigType: 'tss',
        enterprise: 'enterprise',
        type: 'custodial',
      };

      const walletNock = nock('https://bitgo.fakeurl')
        .post('/api/v2/tsol/wallet/add')
        .times(1)
        .reply(200, { ...walletParams, keys });

      const wallets = new Wallets(bitgo, tsol);

      const res = await wallets.generateWallet(walletParams);
      if (!isWalletWithKeychains(res)) {
        throw new Error('wallet missing required keychains');
      }
      res.wallet.label().should.equal(walletParams.label);
      should.equal(res.wallet.type(), walletParams.type);
      res.wallet.toJSON().enterprise.should.equal(walletParams.enterprise);
      res.wallet.multisigType().should.equal(walletParams.multisigType);
      res.userKeychain.id.should.equal(keys[0]);
      res.backupKeychain.id.should.equal(keys[1]);
      res.bitgoKeychain.id.should.equal(keys[2]);

      walletNock.isDone().should.be.true();
    });

    it('should create a new TSS SMC wallet', async function () {
      const commonKeychain = 'longstring';
      const seed = 'seed';
      const keys: KeychainsTriplet = {
        userKeychain: {
          id: '1',
          commonKeychain,
          type: 'tss',
          derivedFromParentWithSeed: seed,
          source: 'user',
        },
        backupKeychain: {
          id: '2',
          commonKeychain,
          type: 'tss',
          derivedFromParentWithSeed: seed,
          source: 'backup',
        },
        bitgoKeychain: {
          id: '3',
          commonKeychain,
          type: 'tss',
          source: 'bitgo',
        },
      };

      const bitgoKeyNock = nock('https://bitgo.fakeurl').get('/api/v2/tsol/key/3').reply(200, keys.bitgoKeychain);
      const userKeyExpectedBody = {
        source: 'user',
        keyType: 'tss',
        commonKeychain,
        derivedFromParentWithSeed: seed,
      };
      const userKeyNock = nock('https://bitgo.fakeurl')
        .post('/api/v2/tsol/key', userKeyExpectedBody)
        .reply(200, keys.userKeychain);
      const backupKeyExpectedBody = {
        source: 'backup',
        keyType: 'tss',
        commonKeychain,
        derivedFromParentWithSeed: seed,
      };
      const backupKeyNock = nock('https://bitgo.fakeurl')
        .post('/api/v2/tsol/key', backupKeyExpectedBody)
        .reply(200, keys.backupKeychain);

      const walletParams: GenerateWalletOptions = {
        label: 'tss wallet',
        multisigType: 'tss',
        enterprise: 'enterprise',
        type: 'cold',
        bitgoKeyId: keys.bitgoKeychain.id,
        commonKeychain,
        coldDerivationSeed: seed,
      };

      const walletNockExpected = {
        label: walletParams.label,
        m: 2,
        n: 3,
        keys: [keys.userKeychain.id, keys.backupKeychain.id, keys.bitgoKeychain.id],
        type: walletParams.type,
        multisigType: walletParams.multisigType,
        enterprise: walletParams.enterprise,
        walletVersion: undefined,
      };

      const walletNock = nock('https://bitgo.fakeurl')
        .post('/api/v2/tsol/wallet/add', walletNockExpected)
        .reply(200, { ...walletNockExpected, responseType: 'WalletWithKeychains' });

      const wallets = new Wallets(bitgo, tsol);

      const res = await wallets.generateWallet(walletParams);
      if (!isWalletWithKeychains(res)) {
        throw new Error('wallet missing required keychains');
      }
      res.responseType.should.equal('WalletWithKeychains');
      res.wallet.label().should.equal(walletParams.label);
      should.equal(res.wallet.type(), walletParams.type);
      res.wallet.toJSON().enterprise.should.equal(walletParams.enterprise);
      res.wallet.multisigType().should.equal(walletParams.multisigType);
      res.userKeychain.should.deepEqual(keys.userKeychain);
      res.backupKeychain.should.deepEqual(keys.backupKeychain);
      res.bitgoKeychain.should.deepEqual(keys.bitgoKeychain);

      bitgoKeyNock.isDone().should.be.true();
      userKeyNock.isDone().should.be.true();
      backupKeyNock.isDone().should.be.true();
      walletNock.isDone().should.be.true();
    });

    it('should throw an error for TSS SMC wallet if the bitgoKeyId is not a bitgo key ', async function () {
      const commonKeychain = 'longstring';
      const seed = 'seed';
      const keys: KeychainsTriplet = {
        userKeychain: {
          id: '1',
          commonKeychain,
          type: 'tss',
          derivedFromParentWithSeed: seed,
          source: 'user',
        },
        backupKeychain: {
          id: '2',
          commonKeychain,
          type: 'tss',
          derivedFromParentWithSeed: seed,
          source: 'backup',
        },
        bitgoKeychain: {
          id: '3',
          commonKeychain,
          type: 'tss',
          source: 'bitgo',
        },
      };

      const bitgoKeyNock = nock('https://bitgo.fakeurl').get('/api/v2/tsol/key/1').reply(200, keys.userKeychain);

      const walletParams: GenerateWalletOptions = {
        label: 'tss wallet',
        multisigType: 'tss',
        enterprise: 'enterprise',
        type: 'cold',
        bitgoKeyId: keys.userKeychain.id,
        commonKeychain,
        coldDerivationSeed: seed,
      };

      const wallets = new Wallets(bitgo, tsol);

      await wallets
        .generateWallet(walletParams)
        .should.be.rejectedWith('The provided bitgoKeyId is not a BitGo keychain');

      bitgoKeyNock.isDone().should.be.true();
    });
  });

  describe('Generate TSS MPCv2 wallet:', async function () {
    const sandbox = sinon.createSandbox();

    beforeEach(function () {
      const tssSettings: TssSettings = {
        coinSettings: {
          eth: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
          bsc: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
          polygon: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
          atom: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
          tia: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
          bera: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
          arbeth: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
          opeth: {
            walletCreationSettings: {
              multiSigTypeVersion: 'MPCv2',
              coldMultiSigTypeVersion: 'MPCv2',
              custodialMultiSigTypeVersion: 'MPCv2',
            },
          },
        },
      };
      nock('https://bitgo.fakeurl').get(`/api/v2/tss/settings`).times(2).reply(200, tssSettings);
    });

    afterEach(function () {
      nock.cleanAll();
      sandbox.verifyAndRestore();
    });

    ['hteth', 'tbsc', 'tpolygon', 'ttia', 'tatom', 'tbera', 'tarbeth', 'topeth'].forEach((coin) => {
      it(`should create a new ${coin} TSS MPCv2 hot wallet`, async function () {
        const testCoin = bitgo.coin(coin);
        const stubbedKeychainsTriplet: KeychainsTriplet = {
          userKeychain: {
            id: '1',
            commonKeychain: 'userPub',
            type: 'tss',
            source: 'user',
          },
          backupKeychain: {
            id: '2',
            commonKeychain: 'userPub',
            type: 'tss',
            source: 'backup',
          },
          bitgoKeychain: {
            id: '3',
            commonKeychain: 'userPub',
            type: 'tss',
            source: 'bitgo',
          },
        };
        const stubCreateKeychains = sandbox
          .stub(ECDSAUtils.EcdsaMPCv2Utils.prototype, 'createKeychains')
          .resolves(stubbedKeychainsTriplet);

        const walletNock = nock('https://bitgo.fakeurl').post(`/api/v2/${coin}/wallet/add`).reply(200);

        const wallets = new Wallets(bitgo, testCoin);

        const params = {
          label: 'tss wallet',
          passphrase: 'tss password',
          multisigType: 'tss' as const,
          enterprise: 'enterprise',
          passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
          walletVersion: 3,
        };

        const response = await wallets.generateWallet(params);

        walletNock.isDone().should.be.true();
        stubCreateKeychains.calledOnce.should.be.true();

        assert.ok(response.encryptedWalletPassphrase);
        assert.ok(response.wallet);
        assert.equal(
          bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
          params.passphrase
        );
      });

      it(`should create a new ${coin} TSS MPCv2 hot wallet without passing multisig type`, async function () {
        const testCoin = bitgo.coin(coin);
        const stubbedKeychainsTriplet: KeychainsTriplet = {
          userKeychain: {
            id: '1',
            commonKeychain: 'userPub',
            type: 'tss',
            source: 'user',
          },
          backupKeychain: {
            id: '2',
            commonKeychain: 'userPub',
            type: 'tss',
            source: 'backup',
          },
          bitgoKeychain: {
            id: '3',
            commonKeychain: 'userPub',
            type: 'tss',
            source: 'bitgo',
          },
        };
        const stubCreateKeychains = sandbox
          .stub(ECDSAUtils.EcdsaMPCv2Utils.prototype, 'createKeychains')
          .resolves(stubbedKeychainsTriplet);

        const walletNock = nock('https://bitgo.fakeurl')
          .post(`/api/v2/${coin}/wallet/add`, function (body) {
            body.multisigType.should.equal(multisigTypes.tss);
            return true;
          })
          .reply(200);

        const wallets = new Wallets(bitgo, testCoin);

        const params = {
          label: 'tss wallet',
          passphrase: 'tss password',
          enterprise: 'enterprise',
          passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
          walletVersion: 3,
        };

        const response = await wallets.generateWallet(params);

        walletNock.isDone().should.be.true();
        stubCreateKeychains.calledOnce.should.be.true();

        assert.ok(response.encryptedWalletPassphrase);
        assert.ok(response.wallet);
        assert.equal(
          bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
          params.passphrase
        );
      });

      it(`should create a new ${coin} TSS MPCv2 cold wallet`, async function () {
        const testCoin = bitgo.coin(coin);
        const bitgoKeyId = 'key123';
        const commonKeychain = '0xabc';

        const bitgoKeyNock = nock('https://bitgo.fakeurl')
          .get(`/api/v2/${coin}/key/${bitgoKeyId}`)
          .times(1)
          .reply(200, {
            id: 'key123',
            pub: 'bitgoPub',
            type: 'tss',
            source: 'bitgo',
            commonKeychain,
          });

        const userKeyNock = nock('https://bitgo.fakeurl')
          .post(`/api/v2/${coin}/key`, {
            source: 'user',
            keyType: 'tss',
            commonKeychain,
            derivedFromParentWithSeed: '37',
            isMPCv2: true,
          })
          .times(1)
          .reply(200, {
            id: 'userKey123',
            pub: 'userPub',
            type: 'tss',
            source: 'user',
          });

        const backupKeyNock = nock('https://bitgo.fakeurl')
          .post(`/api/v2/${coin}/key`, {
            source: 'backup',
            keyType: 'tss',
            commonKeychain,
            derivedFromParentWithSeed: '37',
            isMPCv2: true,
          })
          .times(1)
          .reply(200, {
            id: 'backupKey123',
            pub: 'backupPub',
            type: 'tss',
            source: 'backup',
          });

        const walletNock = nock('https://bitgo.fakeurl')
          .post(`/api/v2/${coin}/wallet/add`, {
            label: 'tss wallet',
            m: 2,
            n: 3,
            keys: ['userKey123', 'backupKey123', 'key123'],
            type: 'cold',
            multisigType: 'tss',
            enterprise: 'enterprise',
            walletVersion: 5,
          })
          .reply(200);

        const wallets = new Wallets(bitgo, testCoin);

        const params: GenerateWalletOptions = {
          label: 'tss wallet',
          multisigType: 'tss' as const,
          enterprise: 'enterprise',
          passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
          walletVersion: 5,
          type: 'cold',
          bitgoKeyId: 'key123',
          commonKeychain: '0xabc',
          coldDerivationSeed: '37',
        };

        const response = (await wallets.generateWallet(params)) as WalletWithKeychains;

        bitgoKeyNock.isDone().should.be.true();
        userKeyNock.isDone().should.be.true();
        backupKeyNock.isDone().should.be.true();
        walletNock.isDone().should.be.true();

        should.exist(response.wallet);
        should.exist(response.userKeychain);
        should.exist(response.backupKeychain);
        should.exist(response.bitgoKeychain);
        response.responseType.should.equal('WalletWithKeychains');
        response.userKeychain.id.should.equal('userKey123');
        response.backupKeychain.id.should.equal('backupKey123');
        response.bitgoKeychain.id.should.equal('key123');
      });

      it(`should create a new ${coin} TSS MPCv2 custody wallet`, async function () {
        const testCoin = bitgo.coin(coin);
        const keys = ['userKey', 'backupKey', 'bitgoKey'];

        const params: GenerateWalletOptions = {
          label: 'tss wallet',
          passphrase: 'tss password',
          multisigType: 'tss' as const,
          enterprise: 'enterprise',
          passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
          walletVersion: 5,
          type: 'custodial',
        };

        const walletNock = nock('https://bitgo.fakeurl')
          .post(`/api/v2/${coin}/wallet/add`)
          .times(1)
          .reply(200, { ...params, keys });

        const wallets = new Wallets(bitgo, testCoin);

        const response = (await wallets.generateWallet(params)) as WalletWithKeychains;

        walletNock.isDone().should.be.true();
        should.exist(response.wallet);
        should.exist(response.userKeychain);
        should.exist(response.backupKeychain);
        should.exist(response.bitgoKeychain);
        response.responseType.should.equal('WalletWithKeychains');
        response.userKeychain.id.should.equal(keys[0]);
        response.backupKeychain.id.should.equal(keys[1]);
        response.bitgoKeychain.id.should.equal(keys[2]);
      });
    });

    it(`should create a new hteth TSS MPCv2 wallet with walletVersion 6`, async function () {
      const testCoin = bitgo.coin('hteth');
      const stubbedKeychainsTriplet: KeychainsTriplet = {
        userKeychain: {
          id: '1',
          commonKeychain: 'userPub',
          type: 'tss',
          source: 'user',
        },
        backupKeychain: {
          id: '2',
          commonKeychain: 'userPub',
          type: 'tss',
          source: 'backup',
        },
        bitgoKeychain: {
          id: '3',
          commonKeychain: 'userPub',
          type: 'tss',
          source: 'bitgo',
        },
      };
      const stubCreateKeychains = sandbox
        .stub(ECDSAUtils.EcdsaMPCv2Utils.prototype, 'createKeychains')
        .resolves(stubbedKeychainsTriplet);

      const walletNock = nock('https://bitgo.fakeurl')
        .post(`/api/v2/hteth/wallet/add`, (body) => {
          body.walletVersion.should.equal(6);
          return true;
        })
        .reply(200);

      const wallets = new Wallets(bitgo, testCoin);

      const params = {
        label: 'tss wallet',
        passphrase: 'tss password',
        multisigType: 'tss' as const,
        enterprise: 'enterprise',
        passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
        walletVersion: 6,
      };

      const response = await wallets.generateWallet(params);

      walletNock.isDone().should.be.true();
      stubCreateKeychains.calledOnce.should.be.true();

      assert.ok(response.encryptedWalletPassphrase);
      assert.ok(response.wallet);
      assert.equal(
        bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
        params.passphrase
      );
    });

    it(`should create a new MPCv2 wallet with version 5 if walletVersion passed is not 5 or 6`, async function () {
      const testCoin = bitgo.coin('hteth');
      const stubbedKeychainsTriplet: KeychainsTriplet = {
        userKeychain: {
          id: '1',
          commonKeychain: 'userPub',
          type: 'tss',
          source: 'user',
        },
        backupKeychain: {
          id: '2',
          commonKeychain: 'userPub',
          type: 'tss',
          source: 'backup',
        },
        bitgoKeychain: {
          id: '3',
          commonKeychain: 'userPub',
          type: 'tss',
          source: 'bitgo',
        },
      };
      const stubCreateKeychains = sandbox
        .stub(ECDSAUtils.EcdsaMPCv2Utils.prototype, 'createKeychains')
        .resolves(stubbedKeychainsTriplet);

      const walletNock = nock('https://bitgo.fakeurl')
        .post(`/api/v2/hteth/wallet/add`, (body) => {
          body.walletVersion.should.equal(5);
          return true;
        })
        .reply(200);

      const wallets = new Wallets(bitgo, testCoin);

      const params = {
        label: 'tss wallet',
        passphrase: 'tss password',
        multisigType: 'tss' as const,
        enterprise: 'enterprise',
        passcodeEncryptionCode: 'originalPasscodeEncryptionCode',
        walletVersion: 3,
      };

      const response = await wallets.generateWallet(params);

      walletNock.isDone().should.be.true();
      stubCreateKeychains.calledOnce.should.be.true();

      assert.ok(response.encryptedWalletPassphrase);
      assert.ok(response.wallet);
      assert.equal(
        bitgo.decrypt({ input: response.encryptedWalletPassphrase, password: params.passcodeEncryptionCode }),
        params.passphrase
      );
    });
  });

  describe('Sharing', () => {
    describe('Wallet share where keychainOverrideRequired is set true', () => {
      const sandbox = sinon.createSandbox();

      afterEach(function () {
        sandbox.verifyAndRestore();
      });

      it('when password not provived we should receive validation error', async function () {
        const shareId = 'test_case_1';

        const walletShareNock = nock(bgUrl)
          .get(`/api/v2/tbtc/walletshare/${shareId}`)
          .reply(200, {
            keychainOverrideRequired: true,
            permissions: ['admin', 'spend', 'view'],
          });

        // Validate accept share case
        await wallets
          .acceptShare({ walletShareId: shareId })
          .should.be.rejectedWith('userPassword param must be provided to decrypt shared key');
        walletShareNock.done();
      });

      it('when we accept share and failed to make changes, reshare should not be called', async function () {
        const shareId = 'test_case_2';
        const keychainId = 'test_case_2';
        const userPassword = 'test_case_2';
        // create a user key
        const keyChainNock = nock(bgUrl)
          .post('/api/v2/tbtc/key', _.conforms({ pub: (p) => p.startsWith('xpub') }))
          .reply(200, (uri, requestBody) => {
            return { id: keychainId, encryptedPrv: requestBody['encryptedPrv'], pub: requestBody['pub'] };
          });

        const walletShareInfoNock = nock(bgUrl)
          .get(`/api/v2/tbtc/walletshare/${shareId}`)
          .reply(200, {
            keychainOverrideRequired: true,
            permissions: ['admin', 'spend', 'view'],
          });

        const acceptShareNock = nock(bgUrl)
          .post(`/api/v2/tbtc/walletshare/${shareId}`, (body: any) => {
            if (body.walletShareId !== shareId || body.state !== 'accepted' || body.keyId !== keychainId) {
              return false;
            }
            return true;
          })
          .reply(200, { changed: false });

        // Stub wallet share wallet method
        const walletShareStub = sandbox.stub(Wallet.prototype, 'shareWallet').onCall(0).resolves('success');

        const res = await wallets.acceptShare({ walletShareId: shareId, userPassword });
        should.equal(res.changed, false);
        keyChainNock.done();
        walletShareInfoNock.done();
        acceptShareNock.done();
        should.equal(walletShareStub.called, false);
      });

      it('when we accept share but state is not valid, reshare should not be called', async function () {
        const shareId = 'test_case_3';
        const keychainId = 'test_case_3';
        const userPassword = 'test_case_3';

        // create a user key
        const keyChainNock = nock(bgUrl)
          .post('/api/v2/tbtc/key', _.conforms({ pub: (p) => p.startsWith('xpub') }))
          .reply(200, (uri, requestBody) => {
            return { id: keychainId, encryptedPrv: requestBody['encryptedPrv'], pub: requestBody['pub'] };
          });

        const walletShareInfoNock = nock(bgUrl)
          .get(`/api/v2/tbtc/walletshare/${shareId}`)
          .reply(200, {
            keychainOverrideRequired: true,
            permissions: ['admin', 'spend', 'view'],
          });

        const acceptShareNock = nock(bgUrl)
          .post(`/api/v2/tbtc/walletshare/${shareId}`, (body: any) => {
            if (body.walletShareId !== shareId || body.state !== 'accepted' || body.keyId !== keychainId) {
              return false;
            }
            return true;
          })
          .reply(200, { changed: true, state: 'not_accepted' });

        // Stub wallet share wallet method
        const walletShareStub = sandbox.stub(Wallet.prototype, 'shareWallet').onCall(0).resolves('success');

        const res = await wallets.acceptShare({ walletShareId: shareId, userPassword });
        should.equal(res.changed, true);
        should.equal(res.state, 'not_accepted');
        keyChainNock.done();
        walletShareInfoNock.done();
        acceptShareNock.done();
        should.equal(walletShareStub.called, false);
      });

      it('when we get a correct resposne from accept share method, but failed to reshare wallet with spenders', async function () {
        const shareId = 'test_case_6';
        const keychainId = 'test_case_6';
        const spenderUserOne = {
          payload: {
            permissions: ['spend', 'view'],
            user: 'test_case_6',
          },
          email: { email: 'test_case_6' },
          id: 'test_case_6',
          coin: 'ofc',
        };
        const spenderUserTwo = {
          payload: {
            permissions: ['spend', 'view'],
            user: 'test_case_9',
          },
          email: { email: 'test_case_9' },
          id: 'test_case_9',
          coin: 'ofc',
        };
        const adminUser = {
          payload: {
            permissions: ['admin', 'spend', 'view'],
            user: 'test_case_7',
          },
          email: { email: 'test_case_7' },
          id: 'test_case_7',
          coin: 'ofc',
        };
        const viewerUser = {
          payload: {
            permissions: ['view'],
            user: 'test_case_8',
          },
          email: { email: 'test_case_8' },
          id: 'test_case_8',
          coin: 'ofc',
        };
        const userPassword = 'test_case_6';
        const walletId = 'test_case_6';
        const enterpriseId = 'test_case_6';

        const walletShareNock = nock(bgUrl)
          .get(`/api/v2/tbtc/walletshare/${shareId}`)
          .reply(200, {
            keychainOverrideRequired: true,
            permissions: ['admin', 'spend', 'view'],
            wallet: walletId,
          });

        // create a user key
        const keyChainCreateNock = nock(bgUrl)
          .post('/api/v2/tbtc/key', _.conforms({ pub: (p) => p.startsWith('xpub') }))
          .reply(200, (uri, requestBody) => {
            return { id: keychainId, encryptedPrv: requestBody['encryptedPrv'], pub: requestBody['pub'] };
          });

        const acceptShareNock = nock(bgUrl)
          .post(`/api/v2/tbtc/walletshare/${shareId}`, (body: any) => {
            if (body.walletShareId !== shareId || body.state !== 'accepted' || body.keyId !== keychainId) {
              return false;
            }
            return true;
          })
          .reply(200, { changed: true, state: 'accepted' });

        const walletInfoNock = nock(bgUrl)
          .get(`/api/v2/tbtc/wallet/${walletId}`)
          .reply(200, {
            users: [spenderUserOne.payload, spenderUserTwo.payload, adminUser.payload, viewerUser.payload],
            enterprise: enterpriseId,
            coin: spenderUserOne.coin,
            id: walletId,
            keys: [{}],
          });

        const enterpriseUserNock = nock(bgUrl)
          .get(`/api/v1/enterprise/${enterpriseId}/user`)
          .reply(200, {
            adminUsers: [
              { id: spenderUserOne.id, email: spenderUserOne.email },
              { id: spenderUserTwo.id, email: spenderUserTwo.email },
              { id: adminUser.id, email: adminUser.email },
              { id: viewerUser.id, email: viewerUser.email },
            ],
            nonAdminUsers: [],
          });

        const walletShareStub = sandbox
          .stub(Wallet.prototype, 'shareWallet')
          .returns(new Promise((_resolve, reject) => reject(new Error('Failed to reshare wallet'))));

        const shareParamsOne = {
          walletId: walletId,
          user: spenderUserOne.id,
          permissions: spenderUserOne.payload.permissions.join(','),
          walletPassphrase: userPassword,
          email: spenderUserOne.email.email,
          reshare: true,
          skipKeychain: false,
        };

        const shareParamsTwo = {
          walletId: walletId,
          user: spenderUserTwo.id,
          permissions: spenderUserTwo.payload.permissions.join(','),
          walletPassphrase: userPassword,
          email: spenderUserTwo.email.email,
          reshare: true,
          skipKeychain: false,
        };

        const res = await wallets.acceptShare({ walletShareId: shareId, userPassword });
        should.equal(res.changed, true);
        should.equal(res.state, 'accepted');
        keyChainCreateNock.done();
        walletShareNock.done();
        walletInfoNock.done();
        acceptShareNock.done();
        enterpriseUserNock.done();
        should.equal(walletShareStub.calledOnce, true);
        should.equal(walletShareStub.calledWith(shareParamsOne), true);
        should.equal(walletShareStub.calledWith(shareParamsTwo), false);
      });

      it('when we get a correct resposne from accept share method and reshare wallet with spenders', async function () {
        const shareId = 'test_case_6';
        const keychainId = 'test_case_6';
        const spenderUserOne = {
          payload: {
            permissions: ['spend', 'view'],
            user: 'test_case_6',
          },
          email: { email: 'test_case_6' },
          id: 'test_case_6',
          coin: 'ofc',
        };
        const spenderUserTwo = {
          payload: {
            permissions: ['spend', 'view'],
            user: 'test_case_9',
          },
          email: { email: 'test_case_9' },
          id: 'test_case_9',
          coin: 'ofc',
        };
        const adminUser = {
          payload: {
            permissions: ['admin', 'spend', 'view'],
            user: 'test_case_7',
          },
          email: { email: 'test_case_7' },
          id: 'test_case_7',
          coin: 'ofc',
        };
        const viewerUser = {
          payload: {
            permissions: ['view'],
            user: 'test_case_8',
          },
          email: { email: 'test_case_8' },
          id: 'test_case_8',
          coin: 'ofc',
        };
        const userPassword = 'test_case_6';
        const walletId = 'test_case_6';
        const enterpriseId = 'test_case_6';

        const walletShareNock = nock(bgUrl)
          .get(`/api/v2/tbtc/walletshare/${shareId}`)
          .reply(200, {
            keychainOverrideRequired: true,
            permissions: ['admin', 'spend', 'view'],
            wallet: walletId,
          });

        // create a user key
        const keyChainCreateNock = nock(bgUrl)
          .post('/api/v2/tbtc/key', _.conforms({ pub: (p) => p.startsWith('xpub') }))
          .reply(200, (uri, requestBody) => {
            return { id: keychainId, encryptedPrv: requestBody['encryptedPrv'], pub: requestBody['pub'] };
          });

        const acceptShareNock = nock(bgUrl)
          .post(`/api/v2/tbtc/walletshare/${shareId}`, (body: any) => {
            if (body.walletShareId !== shareId || body.state !== 'accepted' || body.keyId !== keychainId) {
              return false;
            }
            return true;
          })
          .reply(200, { changed: true, state: 'accepted' });

        const walletInfoNock = nock(bgUrl)
          .get(`/api/v2/tbtc/wallet/${walletId}`)
          .reply(200, {
            users: [spenderUserOne.payload, spenderUserTwo.payload, adminUser.payload, viewerUser.payload],
            enterprise: enterpriseId,
            coin: spenderUserOne.coin,
            id: walletId,
            keys: [{}],
          });

        const enterpriseUserNock = nock(bgUrl)
          .get(`/api/v1/enterprise/${enterpriseId}/user`)
          .reply(200, {
            adminUsers: [
              { id: spenderUserOne.id, email: spenderUserOne.email },
              { id: spenderUserTwo.id, email: spenderUserTwo.email },
              { id: adminUser.id, email: adminUser.email },
              { id: viewerUser.id, email: viewerUser.email },
            ],
            nonAdminUsers: [],
          });

        const walletShareStub = sandbox
          .stub(Wallet.prototype, 'shareWallet')
          .returns(new Promise((resolve, _reject) => resolve('success')));

        const shareParamsOne = {
          walletId: walletId,
          user: spenderUserOne.id,
          permissions: spenderUserOne.payload.permissions.join(','),
          walletPassphrase: userPassword,
          email: spenderUserOne.email.email,
          reshare: true,
          skipKeychain: false,
        };

        const shareParamsTwo = {
          walletId: walletId,
          user: spenderUserTwo.id,
          permissions: spenderUserTwo.payload.permissions.join(','),
          walletPassphrase: userPassword,
          email: spenderUserTwo.email.email,
          reshare: true,
          skipKeychain: false,
        };

        const res = await wallets.acceptShare({ walletShareId: shareId, userPassword });
        should.equal(res.changed, true);
        should.equal(res.state, 'accepted');
        keyChainCreateNock.done();
        walletShareNock.done();
        walletInfoNock.done();
        acceptShareNock.done();
        enterpriseUserNock.done();
        should.equal(walletShareStub.calledTwice, true);
        should.equal(walletShareStub.calledWith(shareParamsOne), true);
        should.equal(walletShareStub.calledWith(shareParamsTwo), true);
      });
    });

    it('should share a wallet to viewer', async function () {
      const shareId = '12311';

      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, {});

      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('bulkUpdateWalletShare', function () {
      afterEach(function () {
        nock.cleanAll();
        nock.pendingMocks().length.should.equal(0);
        sinon.restore();
      });

      it('should throw validation error for missing shares parameter', async () => {
        await wallets.bulkUpdateWalletShare({}).should.be.rejectedWith('Missing parameter: shares');
      });

      it('should throw validation error for non-array shares parameter', async () => {
        await wallets
          .bulkUpdateWalletShare({ shares: 'not-an-array' })
          .should.be.rejectedWith('Expecting parameter array: shares but found string');
      });

      it('should throw validation error for missing walletShareId in share', async () => {
        await wallets
          .bulkUpdateWalletShare({ shares: [{ status: 'accept' }] })
          .should.be.rejectedWith('Missing walletShareId in share');
      });

      it('should throw validation error for missing status in share', async () => {
        await wallets
          .bulkUpdateWalletShare({ shares: [{ walletShareId: '123' }] })
          .should.be.rejectedWith('Missing status in share');
      });

      it('should throw validation error for invalid status in share', async () => {
        await wallets
          .bulkUpdateWalletShare({ shares: [{ walletShareId: '123', status: 'invalid' }] })
          .should.be.rejectedWith('Invalid status in share: invalid. Must be either "accept" or "reject"');
      });

      it('should throw validation error for non-string walletShareId', async () => {
        await wallets
          .bulkUpdateWalletShare({ shares: [{ walletShareId: 123, status: 'accept' }] })
          .should.be.rejectedWith('Expecting walletShareId to be a string but found number');
      });

      it('should throw validation error for non-string userLoginPassword', async () => {
        await wallets
          .bulkUpdateWalletShare({ shares: [{ walletShareId: '123', status: 'accept' }], userLoginPassword: 123 })
          .should.be.rejectedWith('Expecting parameter string: userLoginPassword but found number');
      });

      it('should throw validation error for non-string newWalletPassphrase', async () => {
        await wallets
          .bulkUpdateWalletShare({
            shares: [{ walletShareId: '123', status: 'accept' }],
            userLoginPassword: 'password',
            newWalletPassphrase: 123,
          })
          .should.be.rejectedWith('Expecting parameter string: newWalletPassphrase but found number');
      });

      it('should throw assertion error for empty shares array', async () => {
        await wallets.bulkUpdateWalletShare({ shares: [] }).should.be.rejectedWith('no shares 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
          .bulkUpdateWalletShare({
            shares: [{ walletShareId: '66a229dbdccdcfb95b44fc2745a60bd1', status: 'accept' }],
            userLoginPassword: 'dummy@123',
          })
          .should.be.rejectedWith('invalid wallet share provided: 66a229dbdccdcfb95b44fc2745a60bd1');
      });

      it('should throw error for ecdh keychain undefined when accepting share requiring decryption', 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
          .bulkUpdateWalletShare({
            shares: [{ walletShareId: '66a229dbdccdcfb95b44fc2745a60bd4', status: 'accept' }],
            userLoginPassword: 'dummy@123',
          })
          .should.be.rejectedWith('encryptedXprv was not found on sharing keychain');
      });

      it('should successfully update shares with both accept and reject statuses', async () => {
        const walletPassphrase = 'bitgo1234';

        // Mock listSharesV2 to return two shares
        sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
          incoming: [
            {
              id: 'share1',
              coin: 'tsol',
              walletLabel: 'testing',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet1',
              permissions: ['spend'],
              state: 'active',
            },
            {
              id: 'share2',
              coin: 'tsol',
              walletLabel: 'testing2',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet2',
              permissions: ['spend'],
              state: 'active',
            },
          ],
          outgoing: [],
        });

        // Mock bulkUpdateWalletShareRequest
        const bulkUpdateStub = sinon.stub(Wallets.prototype, 'bulkUpdateWalletShareRequest').resolves({
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: ['share2'],
          walletShareUpdateErrors: [],
        });

        const result = await wallets.bulkUpdateWalletShare({
          shares: [
            { walletShareId: 'share1', status: 'accept' },
            { walletShareId: 'share2', status: 'reject' },
          ],
          userLoginPassword: walletPassphrase,
        });

        // Verify the result
        assert.deepEqual(result, {
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: ['share2'],
          walletShareUpdateErrors: [],
        });

        // Verify bulkUpdateWalletShareRequest was called with correct parameters
        bulkUpdateStub.calledOnce.should.be.true();
        const updateParams = bulkUpdateStub.firstCall.args[0];
        updateParams.should.have.lengthOf(2);

        // Second param should be for share2 with reject status
        updateParams.should.containDeep([
          {
            walletShareId: 'share2',
            status: 'reject',
          },
        ]);
      });

      it('should handle special override cases and reshare with spenders', async () => {
        const walletPassphrase = 'bitgo1234';

        // Mock listSharesV2 to return a share with keychainOverrideRequired
        sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
          incoming: [
            {
              id: 'share1',
              coin: 'tsol',
              walletLabel: 'testing',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet1',
              permissions: ['admin', 'spend'],
              state: 'active',
              keychainOverrideRequired: true,
            },
          ],
          outgoing: [],
        });

        // Setup for the baseCoin.keychains().createUserKeychain
        const userKeychain = {
          id: 'key1',
          pub: 'pubKey',
          encryptedPrv: 'encryptedPrivateKey',
        };
        sinon.stub(wallets.baseCoin.keychains(), 'createUserKeychain').resolves(userKeychain);

        // Mock decrypt and signMessage
        sinon.stub(bitgo, 'decrypt').returns('decryptedPrivateKey');
        sinon.stub(wallets.baseCoin, 'signMessage').resolves(Buffer.from('signature'));

        // Mock getECDHKeychain
        sinon.stub(bitgo, 'getECDHKeychain').resolves({
          encryptedXprv: 'encryptedXprv',
        });

        // Mock bulkUpdateWalletShareRequest
        const bulkUpdateStub = sinon.stub(Wallets.prototype, 'bulkUpdateWalletShareRequest').resolves({
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: [],
          walletShareUpdateErrors: [],
        });

        // Mock reshareWalletWithSpenders
        const reshareStub = sinon.stub(Wallets.prototype, 'reshareWalletWithSpenders').resolves();

        const result = await wallets.bulkUpdateWalletShare({
          shares: [{ walletShareId: 'share1', status: 'accept' }],
          userLoginPassword: walletPassphrase,
        });

        // Verify the result
        assert.deepEqual(result, {
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: [],
          walletShareUpdateErrors: [],
        });

        // Verify bulkUpdateWalletShareRequest was called
        bulkUpdateStub.calledOnce.should.be.true();

        // Verify reshareWalletWithSpenders was called with correct parameters
        reshareStub.calledOnce.should.be.true();
        reshareStub.firstCall.args[0].should.equal('wallet1');
        reshareStub.firstCall.args[1].should.equal(walletPassphrase);
      });

      it('should handle shares with no keychain to decrypt', async () => {
        const walletPassphrase = 'bitgo1234';

        // Mock listSharesV2 to return a share with no keychain
        sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
          incoming: [
            {
              id: 'share1',
              coin: 'tsol',
              walletLabel: 'testing',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet1',
              permissions: ['view'],
              state: 'active',
            },
          ],
          outgoing: [],
        });

        // Mock bulkUpdateWalletShareRequest
        const bulkUpdateStub = sinon.stub(Wallets.prototype, 'bulkUpdateWalletShareRequest').resolves({
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: [],
          walletShareUpdateErrors: [],
        });

        const result = await wallets.bulkUpdateWalletShare({
          shares: [{ walletShareId: 'share1', status: 'accept' }],
          userLoginPassword: walletPassphrase,
        });

        // Verify the result
        assert.deepEqual(result, {
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: [],
          walletShareUpdateErrors: [],
        });

        // Verify bulkUpdateWalletShareRequest was called with correct parameters
        bulkUpdateStub.calledOnce.should.be.true();
        const updateParams = bulkUpdateStub.firstCall.args[0];
        updateParams.should.have.lengthOf(1);

        // Param should be for share1 with accept status and no additional fields
        updateParams.should.containDeep([
          {
            walletShareId: 'share1',
            status: 'accept',
          },
        ]);
      });

      it('should handle shares with keychain requiring decryption', async () => {
        const walletPassphrase = 'bitgo1234';
        const path = 'm/999999/1/1';

        // Create test keychains
        const fromKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef01deadbeef01deadbeef01deadbeef01', 'hex'));
        const toKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef02deadbeef02deadbeef02deadbeef02', 'hex'));

        const fromPubKey = fromKeychain.publicKey.toString('hex');
        const toPubKey = toKeychain.derivePath(path).publicKey.toString('hex');

        // Create encrypted private key
        const originalPrivKey = 'originalPrivateKey';
        const sharedSecret = getSharedSecret(fromKeychain, Buffer.from(toPubKey, 'hex')).toString('hex');

        const encryptedPrv = bitgo.encrypt({
          password: sharedSecret,
          input: originalPrivKey,
        });

        // Mock listSharesV2 to return a share with keychain
        sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
          incoming: [
            {
              id: 'share1',
              coin: 'tsol',
              walletLabel: 'testing',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet1',
              permissions: ['spend'],
              state: 'active',
              keychain: {
                pub: toPubKey,
                toPubKey: toPubKey,
                fromPubKey: fromPubKey,
                encryptedPrv: encryptedPrv,
                path: path,
              },
            },
          ],
          outgoing: [],
        });

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

        // Setup decrypt and encrypt stubs
        const decryptStub = sinon.stub(bitgo, 'decrypt');
        decryptStub.onFirstCall().returns(myEcdhKeychain.xprv); // For sharing keychain
        decryptStub.onSecondCall().returns(originalPrivKey); // For wallet keychain

        const encryptStub = sinon.stub(bitgo, 'encrypt').returns('newEncryptedPrv');

        // Mock getSharedSecret
        sinon.stub(moduleBitgo, 'getSharedSecret').returns(Buffer.from(sharedSecret));

        // Mock bulkUpdateWalletShareRequest
        const bulkUpdateStub = sinon.stub(Wallets.prototype, 'bulkUpdateWalletShareRequest').resolves({
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: [],
          walletShareUpdateErrors: [],
        });

        const result = await wallets.bulkUpdateWalletShare({
          shares: [{ walletShareId: 'share1', status: 'accept' }],
          userLoginPassword: walletPassphrase,
          newWalletPassphrase: 'newPassphrase',
        });

        // Verify the result
        assert.deepEqual(result, {
          acceptedWalletShares: ['share1'],
          rejectedWalletShares: [],
          walletShareUpdateErrors: [],
        });

        // Verify bulkUpdateWalletShareRequest was called with correct parameters
        bulkUpdateStub.calledOnce.should.be.true();
        const updateParams = bulkUpdateStub.firstCall.args[0];
        updateParams.should.have.lengthOf(1);

        // Param should be for share1 with accept status and encryptedPrv
        updateParams.should.containDeep([
          {
            walletShareId: 'share1',
            status: 'accept',
            encryptedPrv: 'newEncryptedPrv',
          },
        ]);

        // Verify encrypt was called with correct parameters
        encryptStub.calledOnce.should.be.true();
        encryptStub.firstCall.args[0].should.have.property('password', 'newPassphrase');
        encryptStub.firstCall.args[0].should.have.property('input', originalPrivKey);
      });

      it('should handle rejected promises and add them to walletShareUpdateErrors', async () => {
        const walletPassphrase = 'bitgo1234';

        // Mock listSharesV2 to return two shares
        sinon.stub(Wallets.prototype, 'listSharesV2').resolves({
          incoming: [
            {
              id: 'share1',
              coin: 'tsol',
              walletLabel: 'testing',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet1',
              permissions: ['spend'],
              state: 'active',
            },
            {
              id: 'share2',
              coin: 'tsol',
              walletLabel: 'testing2',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet2',
              permissions: ['spend'],
              state: 'active',
            },
            {
              id: 'share3',
              coin: 'tsol',
              walletLabel: 'testing3',
              fromUser: 'dummyFromUser',
              toUser: 'dummyToUser',
              wallet: 'wallet3',
              permissions: ['spend'],
              state: 'active',
            },
          ],
          outgoing: [],
        });

        // Stub processAcceptShare to throw an error for share2
        // Using 'as any' to bypass TypeScript's private method restriction
        const processAcceptShareStub = sinon.stub(Wallets.prototype as any, 'processAcceptShare');
        processAcceptShareStub
          .withArgs('share1', sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any)
          .resolves([{ walletShareId: 'share1', status: 'accept' }]);
        processAcceptShareStub
          .withArgs('share2', sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any)
          .rejects(new Error('Failed to process share2'));
        processAcceptShareStub
          .withArgs('share3', sinon.match.any, sinon.match.any, sinon.match.any, sinon.match.any)
          .resolves([{ walletShareId: 'share3', status: 'accept' }]);

        // Mock bulkUpdateWalletShareRequest to return a response
        const bulkUpdateStub = sinon.stub(Wallets.prototype, 'bulkUpdateWalletShareRequest').resolves({
          acceptedWalletShares: ['share1', 'share3'],
          rejectedWalletShares: [],
          walletShareUpdateErrors: [], // Empty array that should be populated
        });

        const result = await wallets.bulkUpdateWalletShare({
          shares: [
            { walletShareId: 'share1', status: 'accept' },
            { walletShareId: 'share2', status: 'accept' },
            { walletShareId: 'share3', status: 'accept' },
          ],
          userLoginPassword: walletPassphrase,
        });

        // Verify bulkUpdateWalletShareRequest was called with only the successful share
        bulkUpdateStub.calledOnce.should.be.true();
        const updateParams = bulkUpdateStub.firstCall.args[0];
        updateParams.should.have.lengthOf(2);
        updateParams[0].walletShareId.should.equal('share1');
        updateParams[0].status.should.equal('accept');
        updateParams[1].walletShareId.should.equal('share3');
        updateParams[1].status.should.equal('accept');

        // Verify the result contains the error information
        result.should.have.property('walletShareUpdateErrors');
        result.walletShareUpdateErrors.should.be.an.Array();
        result.walletShareUpdateErrors.should.have.lengthOf(1);
        result.walletShareUpdateErrors[0].should.have.property('walletShareId', 'share2');
        result.walletShareUpdateErrors[0].should.have.property('reason', 'Failed to process share2');
      });
    });
  });

  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: 'testuser@example.com',
          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: 'testuser@example.com',
            permissions: ['spend'],
            keychain: {
              pub: 'pubkey',
              encryptedPrv: 'encryptedPrv',
              fromPubKey: 'fromPubKey',
              toPubKey: 'toPubKey',
              path: 'm/0/0',
            },
          },
        ],
      };
      const paramsToSend = [
        {
          user: 'testuser@example.com',
          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 = 'user@example.com';
      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,
        pub,
      } 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',
            },
          },
        ],
      });
    });

    it('should throw error in processing share options when wallet password is incorrect', async () => {
      const userId = 'user@example.com';
      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: 'wrong password',
        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,
        pub,
      } as KeychainWithEncryptedPrv);

      try {
        await wallet.createBulkWalletShare(params);
        assert.fail('Expected error not thrown');
      } catch (error) {
        assert(error instanceof IncorrectPasswordError);
        assert.equal(error.message, 'Password shared is incorrect for this wallet');
      }
    });
  });

  describe('List Wallets:', function () {
    it('should list wallets with skipReceiveAddress = true', async function () {
      const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
      const basecoin = bitgo.coin('tbtc');
      const wallets = basecoin.wallets();
      const bgUrl = common.Environments[bitgo.getEnv()].uri;

      nock(bgUrl)
        .get('/api/v2/tbtc/wallet')
        .query({ skipReceiveAddress: true })
        .reply(200, {
          wallets: [
            { id: 'wallet1', label: 'Test Wallet 1' },
            { id: 'wallet2', label: 'Test Wallet 2' },
          ],
        });

      const result = await wallets.list({ skipReceiveAddress: true });
      result.wallets.should.have.length(2);
      should.not.exist(result.wallets[0].receiveAddress());
      should.not.exist(result.wallets[1].receiveAddress());
    });

    it('should list wallets without skipReceiveAddress', async function () {
      const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
      const basecoin = bitgo.coin('tbtc');
      const wallets = basecoin.wallets();
      const bgUrl = common.Environments[bitgo.getEnv()].uri;

      nock(bgUrl)
        .get('/api/v2/tbtc/wallet')
        .query({})
        .reply(200, {
          wallets: [
            { id: 'wallet1', label: 'Test Wallet 1', receiveAddress: { address: 'address1' } },
            { id: 'wallet2', label: 'Test Wallet 2', receiveAddress: { address: 'address2' } },
          ],
        });

      const result = await wallets.list();
      result.wallets.should.have.length(2);
      should.exist(result.wallets[0].receiveAddress());
      should.exist(result.wallets[1].receiveAddress());
    });
  });
});

Выполнить команду


Для локальной разработки. Не используйте в интернете!