PHP WebShell

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

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

//
// Tests for Wallet
//

import * as should from 'should';
import * as sinon from 'sinon';
import '../lib/asserts';
import * as nock from 'nock';
import * as _ from 'lodash';

import {
  common,
  CustomSigningFunction,
  ECDSAUtils,
  EDDSAUtils,
  RequestTracer,
  TokenType,
  TssUtils,
  TxRequest,
  Wallet,
  SignatureShareType,
  Ecdsa,
  Keychains,
  TypedData,
  TypedMessage,
  MessageTypes,
  SignTypedDataVersion,
  GetUserPrvOptions,
  ManageUnspentsOptions,
  SignedMessage,
  BaseTssUtils,
  KeyType,
  SendManyOptions,
  PopulatedIntent,
  TxRequestVersion,
  WalletSignMessageOptions,
  WalletSignTypedDataOptions,
  PrebuildTransactionWithIntentOptions,
} from '@bitgo/sdk-core';

import { TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../../src';
import * as utxoLib from '@bitgo/utxo-lib';
import { randomBytes } from 'crypto';
import { getDefaultWalletKeys, toKeychainObjects } from './coins/utxo/util';
import { Tsol } from '@bitgo/sdk-coin-sol';
import { Teth } from '@bitgo/sdk-coin-eth';

import { nftResponse, unsupportedNftResponse } from '../fixtures/nfts/nftResponses';

require('should-sinon');

nock.disableNetConnect();

type CreateTxRequestBody = {
  intent: PopulatedIntent;
  apiversion: TxRequestVersion;
  preview?: boolean;
};

describe('V2 Wallet:', function () {
  const reqId = new RequestTracer();
  const bitgo = TestBitGo.decorate(BitGo, { env: 'test' });
  bitgo.initializeTestVars();
  const basecoin: any = bitgo.coin('tbtc');
  const walletData = {
    id: '5b34252f1bf349930e34020a00000000',
    coin: 'tbtc',
    keys: ['5b3424f91bf349930e34017500000000', '5b3424f91bf349930e34017600000000', '5b3424f91bf349930e34017700000000'],
    coinSpecific: {},
    multisigType: 'onchain',
    type: 'hot',
  };
  const coldWalletData = {
    id: '65774419fb4d9690847fbe4b00000000',
    coin: 'tbtc',
    keys: ['65774412e54b7516393c9df800000000', '6577442428664ffe791af7ea00000000', '6577442b7317a945756c2fd900000000'],
    coinSpecific: {},
    multisigType: 'onchain',
    type: 'cold',
  };
  const wallet = new Wallet(bitgo, basecoin, walletData);
  const coldWallet = new Wallet(bitgo, basecoin, coldWalletData);
  const bgUrl = common.Environments[bitgo.getEnv()].uri;
  const address1 = '0x174cfd823af8ce27ed0afee3fcf3c3ba259116be';
  const address2 = '0x7e85bdc27c050e3905ebf4b8e634d9ad6edd0de6';
  const tbtcHotWalletDefaultParams = {
    txFormat: 'psbt',
    changeAddressType: ['p2trMusig2', 'p2wsh', 'p2shP2wsh', 'p2sh', 'p2tr'],
  };

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

  describe('Wallet transfers', function () {
    it('should search in wallet for a transfer', async function () {
      const params = { limit: 1, searchLabel: 'test' };

      const scope = nock(bgUrl)
        .get(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/transfer`)
        .query(params)
        .reply(200, {
          coin: 'tbch',
          transfers: [
            {
              wallet: wallet.id(),
              comment: 'tests',
            },
          ],
        });

      try {
        await wallet.transfers(params);
      } catch (e) {
        // test is successful if nock is consumed, HMAC errors expected
      }

      scope.isDone().should.be.True();
    });

    it('should forward all valid parameters', async function () {
      const params = {
        limit: 1,
        address: ['address1', 'address2'],
        dateGte: 'dateString0',
        dateLt: 'dateString1',
        valueGte: 0,
        valueLt: 300000000,
        allTokens: true,
        searchLabel: 'abc',
        includeHex: true,
        type: 'transfer_type',
        state: 'transfer_state',
      };

      // The actual api request will only send strings, but the SDK function expects numbers for some values
      const apiParams = _.mapValues(params, (param) => (Array.isArray(param) ? param : String(param)));

      const scope = nock(bgUrl)
        .get(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/transfer`)
        .query(_.matches(apiParams))
        .reply(200);

      await wallet.transfers(params);
      scope.isDone().should.be.True();
    });

    it('should accept a string argument for address', async function () {
      const params = {
        limit: 1,
        address: 'stringAddress',
      };

      const apiParams = {
        limit: '1',
        address: 'stringAddress',
      };

      const scope = nock(bgUrl)
        .get(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/transfer`)
        .query(_.matches(apiParams))
        .reply(200);

      try {
        await wallet.transfers(params);
      } catch (e) {
        // test is successful if nock is consumed, HMAC errors expected
      }

      scope.isDone().should.be.True();
    });

    it('should throw errors for invalid expected parameters', async function () {
      await wallet
        // @ts-expect-error checking type mismatch
        .transfers({ address: 13375 })
        .should.be.rejectedWith('invalid address argument, expecting string or array');

      await wallet
        // @ts-expect-error checking type mismatch
        .transfers({ address: [null] })
        .should.be.rejectedWith('invalid address argument, expecting array of address strings');

      await wallet
        // @ts-expect-error checking type mismatch
        .transfers({ dateGte: 20101904 })
        .should.be.rejectedWith('invalid dateGte argument, expecting string');

      // @ts-expect-error checking type mismatch
      await wallet.transfers({ dateLt: 20101904 }).should.be.rejectedWith('invalid dateLt argument, expecting string');

      await wallet
        // @ts-expect-error checking type mismatch
        .transfers({ valueGte: '10230005' })
        .should.be.rejectedWith('invalid valueGte argument, expecting number');

      // @ts-expect-error checking type mismatch
      await wallet.transfers({ valueLt: '-5e8' }).should.be.rejectedWith('invalid valueLt argument, expecting number');

      await wallet
        // @ts-expect-error checking type mismatch
        .transfers({ includeHex: '123' })
        .should.be.rejectedWith('invalid includeHex argument, expecting boolean');

      await wallet
        // @ts-expect-error checking type mismatch
        .transfers({ state: 123 })
        .should.be.rejectedWith('invalid state argument, expecting string or array');

      await wallet
        // @ts-expect-error checking type mismatch
        .transfers({ state: [123, 456] })
        .should.be.rejectedWith('invalid state argument, expecting array of state strings');

      // @ts-expect-error checking type mismatch
      await wallet.transfers({ type: 123 }).should.be.rejectedWith('invalid type argument, expecting string');
    });
  });

  describe('Wallet addresses', function () {
    it('should search in wallet addresses', async function () {
      const params = { limit: 1, labelContains: 'test' };

      const scope = nock(bgUrl)
        .get(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/addresses`)
        .query(params)
        .reply(200, {
          coin: 'tbch',
          transfers: [
            {
              wallet: wallet.id(),
              comment: 'tests',
            },
          ],
        });

      try {
        await wallet.addresses(params);
      } catch (e) {
        // test is successful if nock is consumed, HMAC errors expected
      }

      scope.isDone().should.be.True();
    });
  });

  it('should verify bch cashaddr format as valid', async function () {
    const coin = bitgo.coin('tbch');
    const valid = coin.isValidAddress('bchtest:pzfkxv532t0q5zchck2mhmmf2y02cdejyssq5qrz7a');
    valid.should.be.True();
  });

  it('should verify bch legacy format as valid', async function () {
    const coin = bitgo.coin('tbch');
    const valid = coin.isValidAddress('2N6gY9r9iuXQQzZiSyngWJeoUuL5mC1x4Ac');
    valid.should.be.True();
  });

  describe('TETH Wallet Addresses', function () {
    let ethWallet;

    before(async function () {
      const walletData = {
        id: '598f606cd8fc24710d2ebadb1d9459bb',
        coin: 'teth',
        keys: [
          '598f606cd8fc24710d2ebad89dce86c2',
          '598f606cc8e43aef09fcb785221d9dd2',
          '5935d59cf660764331bafcade1855fd7',
        ],
      };
      ethWallet = new Wallet(bitgo, bitgo.coin('teth'), walletData);
    });

    it('search list addresses should return success', async function () {
      const params = {
        includeBalances: true,
        includeTokens: true,
        returnBalancesForToken: 'gterc6dp',
        pendingDeployment: false,
        includeTotalAddressCount: true,
      };

      const scope = nock(bgUrl)
        .get(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/addresses`)
        .query(params)
        .reply(200);
      try {
        await wallet.addresses(params);
        throw '';
      } catch (error) {
        // test is successful if nock is consumed, HMAC errors expected
      }
      scope.isDone().should.be.True();
    });

    it('should throw errors for invalid expected parameters', async function () {
      await ethWallet
        .addresses({ includeBalances: true, returnBalancesForToken: 1 })
        .should.be.rejectedWith('invalid returnBalancesForToken argument, expecting string');

      await ethWallet
        .addresses({ pendingDeployment: 1 })
        .should.be.rejectedWith('invalid pendingDeployment argument, expecting boolean');

      await ethWallet
        .addresses({ includeBalances: 1 })
        .should.be.rejectedWith('invalid includeBalances argument, expecting boolean');

      await ethWallet
        .addresses({ includeTokens: 1 })
        .should.be.rejectedWith('invalid includeTokens argument, expecting boolean');

      await ethWallet
        .addresses({ includeTotalAddressCount: 1 })
        .should.be.rejectedWith('invalid includeTotalAddressCount argument, expecting boolean');
    });

    it('get forwarder balance', async function () {
      const forwarders = [
        {
          address: '0xbfbcc0fe2b865de877134246af09378e9bc3c91d',
          balance: '200000',
        },
        {
          address: '0xe59524ed8b47165f4cb0850c9428069a6002e5eb',
          balance: '10000000000000000',
        },
      ];

      nock(bgUrl).get(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/forwarders/balances`).reply(200, {
        forwarders,
      });

      const forwarderBalance = await ethWallet.getForwarderBalance();
      forwarderBalance.forwarders[0].address.should.eql(forwarders[0].address);
      forwarderBalance.forwarders[0].balance.should.eql(forwarders[0].balance);
      forwarderBalance.forwarders[1].address.should.eql(forwarders[1].address);
      forwarderBalance.forwarders[1].balance.should.eql(forwarders[1].balance);
    });
  });

  describe('Get User Prv', () => {
    const prv =
      'xprv9s21ZrQH143K3hekyNj7TciR4XNYe1kMj68W2ipjJGNHETWP7o42AjDnSPgKhdZ4x8NBAvaL72RrXjuXNdmkMqLERZza73oYugGtbLFXG8g';
    const derivedPrv =
      'xprv9yoG67Td11uwjXwbV8zEmrySVXERu5FZAsLD9suBeEJbgJqANs8Yng5dEJoii7hag5JermK6PbfxgDmSzW7ewWeLmeJEkmPfmZUSLdETtHx';
    it('should use the cold derivation seed to derive the proper user private key', async () => {
      const userPrvOptions = {
        prv,
        coldDerivationSeed: '123',
      };
      wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
    });

    it('should use the user keychain derivedFromParentWithSeed as the cold derivation seed if none is provided', async () => {
      const userPrvOptions: GetUserPrvOptions = {
        prv,
        keychain: {
          derivedFromParentWithSeed: '123',
          id: '456',
          pub: '789',
          type: 'independent',
        },
      };
      wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
    });

    it('should prefer the explicit cold derivation seed to the user keychain derivedFromParentWithSeed', async () => {
      const userPrvOptions: GetUserPrvOptions = {
        prv,
        coldDerivationSeed: '123',
        keychain: {
          derivedFromParentWithSeed: '456',
          id: '789',
          pub: '012',
          type: 'independent',
        },
      };
      wallet.getUserPrv(userPrvOptions).should.eql(derivedPrv);
    });

    it('should return the prv provided for TSS SMC', async () => {
      const tssWalletData = {
        id: '5b34252f1bf349930e34020a00000000',
        coin: 'tsol',
        keys: [
          '5b3424f91bf349930e34017500000000',
          '5b3424f91bf349930e34017600000000',
          '5b3424f91bf349930e34017700000000',
        ],
        coinSpecific: {},
        multisigType: 'tss',
      };
      const tsolcoin: any = bitgo.coin('tsol');
      const wallet = new Wallet(bitgo, tsolcoin, tssWalletData);
      const prv = 'longstringifiedjson';
      const keychain = {
        derivedFromParentWithSeed: 'random seed',
        id: '123',
        commonKeychain: 'longstring',
        type: 'tss' as KeyType,
      };
      const userPrvOptions = {
        prv,
        keychain,
      };
      wallet.getUserPrv(userPrvOptions).should.eql(prv);
    });
  });

  describe('UTXO Custom Signer Function', function () {
    const recipients = [
      { address: 'abc', amount: 123 },
      { address: 'def', amount: 456 },
    ];
    const rootWalletKey = getDefaultWalletKeys();
    let customSigningFunction: CustomSigningFunction;
    let stubs: sinon.SinonStub[];

    beforeEach(function () {
      customSigningFunction = sinon.stub().returns({
        txHex: 'this-is-a-tx',
      });
      stubs = [
        sinon.stub(wallet.baseCoin, 'postProcessPrebuild').returnsArg(0),
        sinon.stub(wallet.baseCoin, 'verifyTransaction').resolves(true),
        sinon.stub(wallet.baseCoin, 'signTransaction').resolves({ txHex: 'this-is-a-tx' }),
      ];
    });

    function nocks(txPrebuild: { txHex: string }) {
      return nock(bgUrl)
        .post(wallet.url('/tx/build').replace(bgUrl, ''))
        .reply(200, txPrebuild)
        .get(wallet.baseCoin.url('/public/block/latest').replace(bgUrl, ''))
        .reply(200)
        .get(wallet.baseCoin.url(`/key/${wallet.keyIds()[0]}`).replace(bgUrl, ''))
        .reply(200, { pub: 'pub' })
        .get(wallet.baseCoin.url(`/key/${wallet.keyIds()[1]}`).replace(bgUrl, ''))
        .reply(200, { pub: 'pub' })
        .get(wallet.baseCoin.url(`/key/${wallet.keyIds()[2]}`).replace(bgUrl, ''))
        .reply(200, { pub: 'pub' })
        .post(wallet.url('/tx/send').replace(bgUrl, ''))
        .reply(200, { ok: true });
    }

    it('should use a custom signing function if provided for PSBT with taprootKeyPathSpend input', async function () {
      const psbt = utxoLib.testutil.constructPsbt(
        [{ scriptType: 'taprootKeyPathSpend', value: BigInt(1000) }],
        [{ scriptType: 'p2sh', value: BigInt(900) }],
        basecoin.network,
        rootWalletKey,
        'unsigned'
      );
      const scope = nocks({ txHex: psbt.toHex() });
      const result = await wallet.sendMany({ recipients, customSigningFunction });

      result.should.have.property('ok', true);
      customSigningFunction.should.have.been.calledTwice();
      scope.done();
      stubs.forEach((s) => s.restore());
    });

    it('should use a custom signing function if provided for PSBT without taprootKeyPathSpend input', async function () {
      const psbt = utxoLib.testutil.constructPsbt(
        [{ scriptType: 'p2wsh', value: BigInt(1000) }],
        [{ scriptType: 'p2sh', value: BigInt(900) }],
        basecoin.network,
        rootWalletKey,
        'unsigned'
      );
      const scope = nocks({ txHex: psbt.toHex() });
      const result = await wallet.sendMany({ recipients, customSigningFunction });

      result.should.have.property('ok', true);
      customSigningFunction.should.have.been.calledOnce();
      scope.done();
      stubs.forEach((s) => s.restore());
    });

    it('should use a custom signing function if provided for Tx without taprootKeyPathSpend input', async function () {
      const tx = utxoLib.testutil.constructTxnBuilder(
        [{ scriptType: 'p2wsh', value: BigInt(1000) }],
        [{ scriptType: 'p2sh', value: BigInt(900) }],
        basecoin.network,
        rootWalletKey,
        'unsigned'
      );
      const scope = nocks({ txHex: tx.buildIncomplete().toHex() });
      const result = await wallet.sendMany({ recipients, customSigningFunction });

      result.should.have.property('ok', true);
      customSigningFunction.should.have.been.calledOnce();
      scope.done();
      stubs.forEach((s) => s.restore());
    });
  });

  describe('TETH Wallet Transactions', function () {
    let ethWallet;

    before(async function () {
      const walletData = {
        id: '598f606cd8fc24710d2ebadb1d9459bb',
        coin: 'teth',
        keys: [
          '598f606cd8fc24710d2ebad89dce86c2',
          '598f606cc8e43aef09fcb785221d9dd2',
          '5935d59cf660764331bafcade1855fd7',
        ],
        multisigType: 'onchain',
      };
      ethWallet = new Wallet(bitgo, bitgo.coin('teth'), walletData);
    });

    afterEach(async function () {
      nock.cleanAll();
    });

    it('should error eip1559 and gasPrice are passed', async function () {
      const params = {
        gasPrice: 100,
        eip1559: {
          maxPriorityFeePerGas: 10,
          maxFeePerGas: 10,
        },
        amount: 10,
        address: TestBitGo.V2.TEST_WALLET1_ADDRESS,
        walletPassphrase: TestBitGo.V2.TEST_WALLET1_PASSCODE,
      };
      await ethWallet.send(params).should.be.rejected();
    });

    it('should search for pending transaction correctly', async function () {
      const params = { walletId: wallet.id() };

      const scope = nock(bgUrl).get(`/api/v2/${wallet.coin()}/tx/pending/first`).query(params).reply(200);
      try {
        await wallet.getFirstPendingTransaction();
        throw '';
      } catch (error) {
        // test is successful if nock is consumed, HMAC errors expected
      }
      scope.isDone().should.be.True();
    });

    it('should try to change the fee correctly', async function () {
      const params = { txid: '0xffffffff', fee: '10000000' };

      const scope = nock(bgUrl).post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/changeFee`, params).reply(200);

      try {
        await wallet.changeFee({ txid: '0xffffffff', fee: '10000000' });
        throw '';
      } catch (error) {
        // test is successful if nock is consumed, HMAC errors expected
      }
      scope.isDone().should.be.True();
    });

    it('should try to change the fee correctly using eip1559', async function () {
      const params = {
        txid: '0xffffffff',
        eip1559: {
          maxPriorityFeePerGas: '1000000000',
          maxFeePerGas: '25000000000',
        },
      };

      const scope = nock(bgUrl).post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/changeFee`, params).reply(200);

      try {
        await wallet.changeFee(params);
        throw '';
      } catch (error) {
        // test is successful if nock is consumed, HMAC errors expected
      }
      scope.isDone().should.be.True();
    });

    it('should pass data parameter and amount: 0 when using sendTransaction', async function () {
      const path = `/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/tx/build`;
      const recipientAddress = '0x7db562c4dd465cc895761c56f83b6af0e32689ba';
      const recipients = [
        {
          address: recipientAddress,
          amount: 0,
          data: '0x00110011',
        },
      ];
      const response = nock(bgUrl)
        .post(path, _.matches({ recipients })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      const nockKeyChain = nock(bgUrl).get(`/api/v2/${ethWallet.coin()}/key/${ethWallet.keyIds()[0]}`).reply(200, {});

      try {
        await ethWallet.send({
          address: recipients[0].address,
          data: recipients[0].data,
          amount: recipients[0].amount,
        });
      } catch (e) {
        // test is successful if nock is consumed, HMAC errors expected
      }
      response.isDone().should.be.true();
      nockKeyChain.isDone().should.be.true();
    });

    it('should pass data parameter and amount: 0 when using sendMany', async function () {
      const path = `/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/tx/build`;
      const recipientAddress = '0x7db562c4dd465cc895761c56f83b6af0e32689ba';
      const recipients = [
        {
          address: recipientAddress,
          amount: 0,
          data: '0x00110011',
        },
      ];
      const response = nock(bgUrl)
        .post(path, _.matches({ recipients })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      const nockKeyChain = nock(bgUrl).get(`/api/v2/${ethWallet.coin()}/key/${ethWallet.keyIds()[0]}`).reply(200, {});

      try {
        await ethWallet.sendMany({ recipients });
      } catch (e) {
        // test is successful if nock is consumed, HMAC errors expected
      }
      response.isDone().should.be.true();
      nockKeyChain.isDone().should.be.true();
    });

    it('should not pass recipients in sendMany when transaction type is fillNonce', async function () {
      const recipientAddress = '0x7db562c4dd465cc895761c56f83b6af0e32689ba';
      const recipients = [
        {
          address: recipientAddress,
          amount: 0,
        },
      ];
      const sendManyParams = { recipients, type: 'fillNonce', isTss: true, nonce: '13' };

      try {
        await ethWallet.sendMany(sendManyParams);
      } catch (e) {
        e.message.should.equal('cannot provide recipients for transaction type fillNonce');
        // test is successful if nock is consumed, HMAC errors expected
      }
    });

    it('should not pass receiveAddress in sendMany when TSS transaction type is transfer or transferToken', async function () {
      const recipientAddress = '0x7db562c4dd465cc895761c56f83b6af0e32689ba';
      const recipients = [
        {
          address: recipientAddress,
          amount: 0,
        },
      ];
      const errorMessage = 'cannot use receive address for TSS transactions of type transfer';
      const sendManyParamsReceiveAddressError = {
        receiveAddress: 'throw',
        recipients,
        type: 'transfer',
        isTss: true,
        nonce: '13',
      };
      const sendManyParams = { recipients, type: 'transfer', isTss: true, nonce: '13' };

      try {
        await ethWallet.sendMany(sendManyParamsReceiveAddressError);
      } catch (e) {
        e.message.should.equal(errorMessage);
      }

      try {
        await ethWallet.sendMany(sendManyParams);
      } catch (e) {
        e.message.should.not.equal(errorMessage);
      }
    });

    it('should throw error early if password is wrong', async function () {
      const recipientAddress = '0x7db562c4dd465cc895761c56f83b6af0e32689ba';
      const recipients = [
        {
          address: recipientAddress,
          amount: 0,
        },
      ];
      const errorMessage = `unable to decrypt keychain with the given wallet passphrase`;
      const sendManyParamsCorrectPassPhrase = {
        recipients,
        type: 'transfer',
        isTss: true,
        nonce: '13',
        walletPassphrase: TestBitGo.V2.TEST_ETH_WALLET_PASSPHRASE,
      };
      const nockKeychain = nock(bgUrl)
        .get(`/api/v2/${ethWallet.coin()}/key/${ethWallet.keyIds()[0]}`)
        .times(2)
        .reply(200, {
          id: '598f606cd8fc24710d2ebad89dce86c2',
          pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5',
          ethAddress: '0x26a163ba9739529720c0914c583865dec0d37278',
          source: 'user',
          encryptedPrv:
            '{"iv":"15FsbDVI1zG9OggD8YX+Hg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"hHbNH3Sz/aU=","ct":"WoNVKz7afiRxXI2w/YkzMdMyoQg/B15u1Q8aQgi96jJZ9wk6TIaSEc6bXFH3AHzD9MdJCWJQUpRhoQc/rgytcn69scPTjKeeyVMElGCxZdFVS/psQcNE+lue3//2Zlxj+6t1NkvYO+8yAezSMRBK5OdftXEjNQI="}',
          coinSpecific: {},
        });

      await ethWallet
        .sendMany({ ...sendManyParamsCorrectPassPhrase, walletPassphrase: 'wrongPassphrase' })
        .should.be.rejectedWith(errorMessage);

      try {
        const customSigningFunction = () => {
          return 'mock';
        };
        // Should not validate passphrase if custom signing function is provided
        await ethWallet.sendMany({
          ...sendManyParamsCorrectPassPhrase,
          walletPassphrase: 'wrongPassphrase',
          customSigningFunction,
        });
      } catch (e) {
        e.message.should.not.equal(errorMessage);
      }
      try {
        await ethWallet.sendMany({ ...sendManyParamsCorrectPassPhrase });
      } catch (e) {
        e.message.should.not.equal(errorMessage);
      }
      nockKeychain.isDone().should.be.true();
    });
  });

  describe('OFC Create Address', () => {
    let ofcWallet: Wallet;
    let nocks;
    before(async function () {
      const walletDataOfc = {
        id: '5b34252f1bf349930e3400b00000000',
        coin: 'ofc',
        keys: [
          '5b3424f91bf349930e34017800000000',
          '5b3424f91bf349930e34017900000000',
          '5b3424f91bf349930e34018000000000',
        ],
        coinSpecific: {},
        multisigType: 'onchain',
      };
      ofcWallet = new Wallet(bitgo, bitgo.coin('ofc'), walletDataOfc);
    });

    beforeEach(async function () {
      nocks = [
        nock(bgUrl).get(`/api/v2/ofc/key/${ofcWallet.keyIds()[0]}`).reply(200, {
          id: ofcWallet.keyIds()[0],
          pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5',
          source: 'user',
          encryptedPrv:
            '{"iv":"15FsbDVI1zG9OggD8YX+Hg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"hHbNH3Sz/aU=","ct":"WoNVKz7afiRxXI2w/YkzMdMyoQg/B15u1Q8aQgi96jJZ9wk6TIaSEc6bXFH3AHzD9MdJCWJQUpRhoQc/rgytcn69scPTjKeeyVMElGCxZdFVS/psQcNE+lue3//2Zlxj+6t1NkvYO+8yAezSMRBK5OdftXEjNQI="}',
          coinSpecific: {},
        }),

        nock(bgUrl).get(`/api/v2/ofc/key/${ofcWallet.keyIds()[1]}`).reply(200, {
          id: ofcWallet.keyIds()[1],
          pub: 'xpub661MyMwAqRbcGhSaXikpuTC9KU88Xx9LrjKSw1JKsvXNgabpTdgjy7LSovh9ZHhcqhAHQu7uthu7FguNGdcC4aXTKK5gqTcPe4WvLYRbCSG',
          source: 'backup',
          coinSpecific: {},
        }),

        nock(bgUrl).get(`/api/v2/ofc/key/${ofcWallet.keyIds()[2]}`).reply(200, {
          id: ofcWallet.keyIds()[2],
          pub: 'xpub661MyMwAqRbcFsXShW8R3hJsHNTYTUwzcejnLkY7KCtaJbDqcGkcBF99BrEJSjNZHeHveiYUrsAdwnjUMGwpgmEbiKcZWRuVA9HxnRaA3r3',
          source: 'bitgo',
          coinSpecific: {},
        }),
      ];
    });

    afterEach(async function () {
      nock.cleanAll();
      nocks.forEach((scope) => scope.isDone().should.be.true());
    });

    it('should correctly validate arguments to create address on OFC wallet', async function () {
      await ofcWallet.createAddress().should.be.rejectedWith('onToken is a mandatory parameter for OFC wallets');
      // @ts-expect-error test passing invalid number argument
      await ofcWallet.createAddress({ onToken: 42 }).should.be.rejectedWith('onToken has to be a string');
    });

    it('address creation with valid onToken argument succeeds', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/ofc/wallet/${ofcWallet.id()}/address`, { onToken: 'ofctbtc' })
        .reply(200, {
          id: '638a48c6c3dba40007a3497fa49a080c',
          address: 'generated address',
          chain: 0,
          index: 1,
          coin: 'tbtc',
          wallet: ofcWallet.id,
        });
      const address = await ofcWallet.createAddress({ onToken: 'ofctbtc' });
      address.address.should.equal('generated address');
      scope.isDone().should.be.true();
    });
  });

  describe('TETH Create Address', () => {
    let ethWallet, nocks;
    const walletData = {
      id: '598f606cd8fc24710d2ebadb1d9459bb',
      coinSpecific: {
        baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
      },
      coin: 'teth',
      keys: [
        '598f606cd8fc24710d2ebad89dce86c2',
        '598f606cc8e43aef09fcb785221d9dd2',
        '5935d59cf660764331bafcade1855fd7',
      ],
    };

    beforeEach(async function () {
      ethWallet = new Wallet(bitgo, bitgo.coin('teth'), walletData);
      nocks = [
        nock(bgUrl).get(`/api/v2/${ethWallet.coin()}/key/${ethWallet.keyIds()[0]}`).reply(200, {
          id: '598f606cd8fc24710d2ebad89dce86c2',
          pub: 'xpub661MyMwAqRbcFXDcWD2vxuebcT1ZpTF4Vke6qmMW8yzddwNYpAPjvYEEL5jLfyYXW2fuxtAxY8TgjPUJLcf1C8qz9N6VgZxArKX4EwB8rH5',
          ethAddress: '0x26a163ba9739529720c0914c583865dec0d37278',
          source: 'user',
          encryptedPrv:
            '{"iv":"15FsbDVI1zG9OggD8YX+Hg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"hHbNH3Sz/aU=","ct":"WoNVKz7afiRxXI2w/YkzMdMyoQg/B15u1Q8aQgi96jJZ9wk6TIaSEc6bXFH3AHzD9MdJCWJQUpRhoQc/rgytcn69scPTjKeeyVMElGCxZdFVS/psQcNE+lue3//2Zlxj+6t1NkvYO+8yAezSMRBK5OdftXEjNQI="}',
          coinSpecific: {},
        }),

        nock(bgUrl).get(`/api/v2/${ethWallet.coin()}/key/${ethWallet.keyIds()[1]}`).reply(200, {
          id: '598f606cc8e43aef09fcb785221d9dd2',
          pub: 'xpub661MyMwAqRbcGhSaXikpuTC9KU88Xx9LrjKSw1JKsvXNgabpTdgjy7LSovh9ZHhcqhAHQu7uthu7FguNGdcC4aXTKK5gqTcPe4WvLYRbCSG',
          ethAddress: '0xa1a88a502274073b1bc4fe06ea0f5fe77e151b91',
          source: 'backup',
          coinSpecific: {},
        }),

        nock(bgUrl).get(`/api/v2/${ethWallet.coin()}/key/${ethWallet.keyIds()[2]}`).reply(200, {
          id: '5935d59cf660764331bafcade1855fd7',
          pub: 'xpub661MyMwAqRbcFsXShW8R3hJsHNTYTUwzcejnLkY7KCtaJbDqcGkcBF99BrEJSjNZHeHveiYUrsAdwnjUMGwpgmEbiKcZWRuVA9HxnRaA3r3',
          ethAddress: '0x032821b7ea40ea5d446f47c29a0f777ee035aa10',
          source: 'bitgo',
          coinSpecific: {},
        }),
      ];
    });

    afterEach(async function () {
      nock.cleanAll();
      nocks.forEach((scope) => scope.isDone().should.be.true());
    });

    it('should correctly validate arguments to create address', async function () {
      let message = 'gasPrice has to be an integer or numeric string';
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ gasPrice: {} }).should.be.rejectedWith(message);
      await wallet.createAddress({ gasPrice: 'abc' }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ gasPrice: null }).should.be.rejectedWith(message);

      message = 'chain has to be an integer';
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ chain: {} }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ chain: 'abc' }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ chain: null }).should.be.rejectedWith(message);

      message = 'count has to be a number between 1 and 250';
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ count: {} }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ count: 'abc' }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ count: null }).should.be.rejectedWith(message);
      await wallet.createAddress({ count: -1 }).should.be.rejectedWith(message);
      await wallet.createAddress({ count: 0 }).should.be.rejectedWith(message);
      await wallet.createAddress({ count: 251 }).should.be.rejectedWith(message);

      message = 'baseAddress has to be a string';
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ baseAddress: {} }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ baseAddress: 123 }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ baseAddress: null }).should.be.rejectedWith(message);

      message = 'allowSkipVerifyAddress has to be a boolean';
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ allowSkipVerifyAddress: {} }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ allowSkipVerifyAddress: 123 }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ allowSkipVerifyAddress: 'abc' }).should.be.rejectedWith(message);
      // @ts-expect-error checking type mismatch
      await wallet.createAddress({ allowSkipVerifyAddress: null }).should.be.rejectedWith(message);

      message = 'forwarderVersion has to be an integer 0, 1, 2, 3 or 4';
      await wallet.createAddress({ forwarderVersion: 5 }).should.be.rejectedWith(message);
      await wallet.createAddress({ forwarderVersion: -1 }).should.be.rejectedWith(message);
    });

    it('address creation with forwarder version 3 succeeds', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 3 })
        .reply(200, {
          id: '638a48c6c3dba40007a3497fa49a080c',
          address: '0x5e61b64f38f1b5f85078fb84b27394830b4c8e80',
          chain: 0,
          index: 1,
          coin: 'tpolygon',
          lastNonce: 0,
          wallet: '63785f95af7c760007cfae068c2f31ae',
          coinSpecific: {
            nonce: -1,
            updateTime: '2022-12-02T18:49:42.348Z',
            txCount: 0,
            pendingChainInitialization: false,
            creationFailure: [],
            salt: '0x1',
            pendingDeployment: true,
            forwarderVersion: 3,
            isTss: true,
          },
        });
      const address = await ethWallet.createAddress({ chain: 0, forwarderVersion: 3 });
      address.coinSpecific.forwarderVersion.should.equal(3);
      scope.isDone().should.be.true();
    });

    it('address creation with forwarder version 3 fails due invalid address', async function () {
      const address = '0x5e61b6'; // invalid address
      nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 3 })
        .reply(200, {
          id: '638a48c6c3dba40007a3497fa49a080c',
          address: address,
          chain: 0,
          index: 1,
          coin: 'tpolygon',
          lastNonce: 0,
          wallet: '63785f95af7c760007cfae068c2f31ae',
          coinSpecific: {
            nonce: -1,
            updateTime: '2022-12-02T18:49:42.348Z',
            txCount: 0,
            pendingChainInitialization: false,
            creationFailure: [],
            salt: '0x1',
            pendingDeployment: true,
            forwarderVersion: 3,
            isTss: true,
          },
        });
      await ethWallet
        .createAddress({ chain: 0, forwarderVersion: 3 })
        .should.be.rejectedWith(`invalid address: ${address}`);
    });

    it('address creation with forwarder version 2 succeeds', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 2 })
        .reply(200, {
          id: '638a48c6c3dba40007a3497fa49a080c',
          address: '0x5e61b64f38f1b5f85078fb84b27394830b4c8e80',
          chain: 0,
          index: 1,
          coin: 'tpolygon',
          lastNonce: 0,
          wallet: '63785f95af7c760007cfae068c2f31ae',
          coinSpecific: {
            nonce: -1,
            updateTime: '2022-12-02T18:49:42.348Z',
            txCount: 0,
            pendingChainInitialization: true,
            creationFailure: [],
            salt: '0x1',
            pendingDeployment: true,
            forwarderVersion: 2,
            isTss: true,
          },
        });
      const address = await ethWallet.createAddress({ chain: 0, forwarderVersion: 2 });
      address.coinSpecific.forwarderVersion.should.equal(2);
      scope.isDone().should.be.true();
    });

    it('verify address when pendingChainInitialization is true in case of eth v1 forwarder', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 1 })
        .reply(200, {
          id: '615c643a98a2a100068e023c639c0f74',
          address: '0x8c13cd0bb198858f628d5631ba4b2293fc08df49',
          baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
          chain: 0,
          index: 3179,
          coin: 'teth',
          lastNonce: 0,
          wallet: '598f606cd8fc24710d2ebadb1d9459bb',
          coinSpecific: {
            nonce: -1,
            updateTime: '2021-10-05T14:42:02.399Z',
            txCount: 0,
            pendingChainInitialization: true,
            creationFailure: [],
            salt: '0xc6b',
            pendingDeployment: true,
            forwarderVersion: 1,
          },
        });
      await ethWallet
        .createAddress({ chain: 0, forwarderVersion: 1 })
        .should.be.rejectedWith(
          'address validation failure: expected 0x32a226cda14e352a47bf4b1658648d8037736f80 but got 0x8c13cd0bb198858f628d5631ba4b2293fc08df49'
        );
      scope.isDone().should.be.true();
    });

    it('verify address when invalid baseAddress is passed', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 1 })
        .reply(200, {
          id: '615c643a98a2a100068e023c639c0f74',
          address: '0x32a226cda14e352a47bf4b1658648d8037736f80',
          baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
          chain: 0,
          index: 3179,
          coin: 'teth',
          lastNonce: 0,
          wallet: '598f606cd8fc24710d2ebadb1d9459bb',
          coinSpecific: {
            nonce: -1,
            updateTime: '2021-10-05T14:42:02.399Z',
            txCount: 0,
            pendingChainInitialization: true,
            creationFailure: [],
            salt: '0xc6b',
            pendingDeployment: true,
            forwarderVersion: 1,
          },
        });
      await ethWallet
        .createAddress({ chain: 0, forwarderVersion: 1, baseAddress: 'asgf' })
        .should.be.rejectedWith('invalid base address');
      scope.isDone().should.be.true();
    });

    it('verify address when incorrect baseAddress is passed', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 1 })
        .reply(200, {
          id: '615c643a98a2a100068e023c639c0f74',
          address: '0x32a226cda14e352a47bf4b1658648d8037736f80',
          baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
          chain: 0,
          index: 3179,
          coin: 'teth',
          lastNonce: 0,
          wallet: '598f606cd8fc24710d2ebadb1d9459bb',
          coinSpecific: {
            nonce: -1,
            updateTime: '2021-10-05T14:42:02.399Z',
            txCount: 0,
            pendingChainInitialization: true,
            creationFailure: [],
            salt: '0xc6b',
            pendingDeployment: true,
            forwarderVersion: 1,
          },
        });
      // incorrect address is generated while validating due to incorrect baseAddress
      await ethWallet
        .createAddress({ chain: 0, forwarderVersion: 1, baseAddress: '0x8c13cd0bb198858f628d5631ba4b2293fc08df49' })
        .should.be.rejectedWith(
          'address validation failure: expected 0x36748926007790e7ee416c6485b32e00cfb177a3 but got 0x32a226cda14e352a47bf4b1658648d8037736f80'
        );
      scope.isDone().should.be.true();
    });

    it('verify address when pendingChainInitialization is true  and allowSkipVerifyAddress is false in case of eth v0 forwarder', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 0 })
        .reply(200, {
          id: '615c643a98a2a100068e023c639c0f74',
          address: '0x32a26cda14e352a47bf4b1658648d8037736f80',
          baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
          chain: 0,
          index: 3179,
          coin: 'teth',
          lastNonce: 0,
          wallet: '598f606cd8fc24710d2ebadb1d9459bb',
          coinSpecific: {
            nonce: -1,
            updateTime: '2021-10-05T14:42:02.399Z',
            txCount: 0,
            pendingChainInitialization: true,
            creationFailure: [],
            salt: '0xc6b',
            pendingDeployment: true,
            forwarderVersion: 1,
          },
        });
      await ethWallet
        .createAddress({ chain: 0, forwarderVersion: 0, allowSkipVerifyAddress: false })
        .should.be.rejectedWith('address verification skipped for count = 1');
      scope.isDone().should.be.true();
    });

    it('verify address with allowSkipVerifyAddress set to false and eth v1 forwarder', async function () {
      const scope = nock(bgUrl)
        .post(`/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/address`, { chain: 0, forwarderVersion: 1 })
        .reply(200, {
          id: '615c643a98a2a100068e023c639c0f74',
          address: '0x32a226cda14e352a47bf4b1658648d8037736f80',
          baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
          chain: 0,
          index: 3179,
          coin: 'teth',
          lastNonce: 0,
          wallet: '598f606cd8fc24710d2ebadb1d9459bb',
          coinSpecific: {
            nonce: -1,
            updateTime: '2021-10-05T14:42:02.399Z',
            txCount: 0,
            pendingChainInitialization: true,
            creationFailure: [],
            salt: '0xc6b',
            pendingDeployment: true,
            forwarderVersion: 0,
          },
        });
      const newAddress = await ethWallet.createAddress({
        chain: 0,
        forwarderVersion: 1,
        allowSkipVerifyAddress: false,
      });
      newAddress.index.should.equal(3179);
      scope.isDone().should.be.true();
    });
  });

  describe('Algorand tests', () => {
    let algoWallet: Wallet;

    before(async () => {
      // This is not a real TALGO wallet
      const walletData = {
        id: '650204cf43d8b40007cd9e11a872ce65',
        coin: 'talgo',
        keys: [
          '650204b78a75c90007790bce979ae34d',
          '650204b766c56a00072956c08fb9cdf1',
          '650204b8ccf1370007b32bb8155dfbec',
        ],
        coinSpecific: {
          rootAddress: '2ULRGE64U7LTMT5M6REB7ORHX5GLJYWHTIV5EAXVLWQTTATVJDGM5KJMII',
        },
      };
      algoWallet = new Wallet(bitgo, bitgo.coin('talgo'), walletData);
    });

    it('Should build token enablement transactions', async () => {
      const params = {
        enableTokens: [
          {
            name: 'talgo:USDt-180447',
          },
        ],
      };
      const txRequestNock = nock(bgUrl)
        .post(`/api/v2/${algoWallet.coin()}/wallet/${algoWallet.id()}/tx/build`)
        .reply((uri, body) => {
          const params = body as any;
          params.recipients.length.should.equal(1);
          params.recipients[0].tokenName.should.equal('talgo:USDt-180447');
          params.type.should.equal('enabletoken');
          should.not.exist(params.enableTokens);
          return [200, params];
        });
      await algoWallet.buildTokenEnablements(params);
      txRequestNock.isDone().should.equal(true);
    });

    afterEach(() => {
      nock.cleanAll();
    });
  });

  describe('Hedera tests', () => {
    let hbarWallet: Wallet;

    before(async () => {
      // This is not a real THBAR wallet
      const walletData = {
        id: '598f606cd8fc24710d2ebadb1d9459bb',
        coin: 'thbar',
        keys: [
          '598f606cd8fc24710d2ebad89dce86c2',
          '598f606cc8e43aef09fcb785221d9dd2',
          '5935d59cf660764331bafcade1855fd7',
        ],
        coinSpecific: {
          baseAddress: '0.0.47841511',
        },
      };
      hbarWallet = new Wallet(bitgo, bitgo.coin('thbar'), walletData);
    });

    it('Should build token enablement transactions', async () => {
      const params = {
        enableTokens: [
          {
            name: 'thbar:usdc',
          },
        ],
      };
      const txRequestNock = nock(bgUrl)
        .post(`/api/v2/${hbarWallet.coin()}/wallet/${hbarWallet.id()}/tx/build`)
        .reply((uri, body) => {
          const params = body as any;
          params.recipients.length.should.equal(1);
          params.recipients[0].tokenName.should.equal('thbar:usdc');
          params.type.should.equal('enabletoken');
          should.not.exist(params.enableTokens);
          return [200, params];
        });
      await hbarWallet.buildTokenEnablements(params);
      txRequestNock.isDone().should.equal(true);
    });

    afterEach(() => {
      nock.cleanAll();
    });
  });

  describe('Solana tests: ', () => {
    let solWallet: Wallet;
    const passphrase = '#Bondiola1234';
    const solBitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
    solBitgo.initializeTestVars();
    const walletData = {
      id: '598f606cd8fc24710d2ebadb1d9459bb',
      coinSpecific: {
        baseAddress: '5f8WmC2uW9SAk7LMX2r4G1Bx8MMwx8sdgpotyHGodiZo',
        pendingChainInitialization: false,
        minimumFunding: 2447136,
        lastChainIndex: { 0: 0 },
      },
      coin: 'tsol',
      keys: [
        '598f606cd8fc24710d2ebad89dce86c2',
        '598f606cc8e43aef09fcb785221d9dd2',
        '5935d59cf660764331bafcade1855fd7',
      ],
      multisigType: 'tss',
    };

    before(async function () {
      solWallet = new Wallet(bitgo, bitgo.coin('tsol'), walletData);
      nock(bgUrl).get(`/api/v2/${solWallet.coin()}/key/${solWallet.keyIds()[0]}`).times(3).reply(200, {
        id: '598f606cd8fc24710d2ebad89dce86c2',
        pub: '5f8WmC2uW9SAk7LMX2r4G1Bx8MMwx8sdgpotyHGodiZo',
        source: 'user',
        encryptedPrv:
          '{"iv":"hNK3rg82P1T94MaueXFAbA==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"cV4wU4EzPjs=","ct":"9VZX99Ztsb6p75Cxl2lrcXBplmssIAQ9k7ZA81vdDYG4N5dZ36BQNWVfDoelj9O31XyJ+Xri0XKIWUzl0KKLfUERplmtNoOCn5ifJcZwCrOxpHZQe3AJ700o8Wmsrk5H"}',
        coinSpecific: {},
      });

      nock(bgUrl).get(`/api/v2/${solWallet.coin()}/key/${solWallet.keyIds()[1]}`).times(2).reply(200, {
        id: '598f606cc8e43aef09fcb785221d9dd2',
        pub: 'G1s43JTzNZzqhUn4aNpwgcc6wb9FUsZQD5JjffG6isyd',
        encryptedPrv:
          '{"iv":"UFrt/QlIUR1XeQafPBaAlw==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"7VPBYaJXPm8=","ct":"ajFKv2y8yaIBXQ39sAbBWcnbiEEzbjS4AoQtp5cXYqjeDRxt3aCxemPm22pnkJaCijFjJrMHbkmsNhNYzHg5aHFukN+nEAVssyNwHbzlhSnm8/BVN50yAdAAtWreh8cp"}',
        source: 'backup',
        coinSpecific: {},
      });

      nock(bgUrl).get(`/api/v2/${solWallet.coin()}/key/${solWallet.keyIds()[2]}`).times(2).reply(200, {
        id: '5935d59cf660764331bafcade1855fd7',
        pub: 'GH1LV1e9FdqGe8U2c8PMEcma3fDeh1ktcGVBrD3AuFqx',
        encryptedPrv:
          '{"iv":"iIuWOHIOErEDdiJn6g46mg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Rzh7RRJksj0=","ct":"rcNICUfp9FakT53l+adB6XKzS1vNTc0Qq9jAtqnxA+ScssiS4Q0l3sgG/0gDy5DaZKtXryKBDUvGsi7b/fYaFCUpAoZn/VZTOhOUN/mo7ZHb4OhOXL29YPPkiryAq9Cr"}',
        source: 'bitgo',
        coinSpecific: {},
      });
    });

    after(async function () {
      nock.cleanAll();
    });

    describe('prebuildAndSignTransaction: ', function () {
      // TODO (STLX-15018): fix test
      xit('should successfully sign a consolidation transfer', async function () {
        const txParams = {
          prebuildTx: {
            walletId: walletData.id,
            txHex:
              'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAIE9MWWV2ct01mg5Gm4EqcJ9SAn2XuD+FuAHcHFTkc1Tgut3DgTsiSgTQ0dmzj5JJg6qYTpn8FxOYPFCFTMoZi46gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABUpTWpkpIQZNJOhxYNo4fHw1td28kruB5B+oQEEFRI0Qc+q0Zg6OOpV8eCDVLfYziox7YBA7+QPLX4IRhDCSKwICAgABDAIAAACghgEAAAAAAAMAFVRlc3QgaW50ZWdyYXRpb24gbWVtbw==',
            txInfo: {
              feePayer: 'HUVE5NfJyGfU1djZsVLA6fxSTS1E2iRqcTRVNC9K2z7c',
              lamportsPerSignature: 5000,
              nonce: '27E3MXFvXMUNYeMJeX1pAbERGsJfUbkaZTfgMgpmNN5g',
              numSignatures: 0,
              instructionsData: [
                {
                  type: 'Transfer',
                  params: {
                    fromAddress: 'HUVE5NfJyGfU1djZsVLA6fxSTS1E2iRqcTRVNC9K2z7c',
                    toAddress: 'ChgJ5tgDwBUsk9RNMm2iLiwP8RodwgZ6uqrC5paJsXVT',
                    amount: '100000',
                  },
                },
                {
                  type: 'Memo',
                  params: {
                    memo: 'Test integration memo',
                  },
                },
              ],
            },
            buildParams: {
              memo: {
                type: 'Memo',
                value: 'Test integration memo',
              },
              recipients: [
                {
                  address: 'ChgJ5tgDwBUsk9RNMm2iLiwP8RodwgZ6uqrC5paJsXVT',
                  amount: '100000',
                },
              ],
              type: 'transfer',
            },
            consolidateId: '1234',
            consolidationDetails: {
              senderAddressIndex: 1,
            },
          },
          walletPassphrase: passphrase,
        };
        // Build and sign the transaction
        const preBuiltSignedTx = await solWallet.prebuildAndSignTransaction(txParams);
        preBuiltSignedTx.should.have.property('txHex');
      });
    });

    it('Should build token enablement transactions correctly', async function () {
      const params = {
        enableTokens: [{ name: 'tsol:usdc' }, { name: 'tsol:srm' }, { name: 'tsol:gmt' }],
      };
      const txRequestNock = nock(bgUrl)
        .post(`/api/v2/wallet/${solWallet.id()}/txrequests`)
        .reply((url, body) => {
          const bodyParams = body as any;
          bodyParams.intent.intentType.should.equal('enableToken');
          bodyParams.intent.recipients.length.should.equal(0);
          bodyParams.intent.enableTokens.should.deepEqual(params.enableTokens);
          return [
            200,
            {
              apiVersion: 'full',
              transactions: [
                {
                  unsignedTx: {
                    serializedTxHex: 'fake transaction',
                    feeInfo: 'fake fee info',
                  },
                },
              ],
            },
          ];
        });
      await solWallet.buildTokenEnablements(params);
      txRequestNock.isDone().should.equal(true);
    });
  });

  describe('Accelerate Transaction', function () {
    it('fails if acceleration ids are not passed', async function () {
      await wallet.accelerateTransaction({}).should.be.rejectedWith({ code: 'cpfptxids_or_rbftxids_required' });
    });

    it('fails if cpfpTxIds is not an array', async function () {
      // @ts-expect-error checking type mismatch
      await wallet.accelerateTransaction({ cpfpTxIds: {} }).should.be.rejectedWith({ code: 'cpfptxids_not_array' });
    });

    it('fails if cpfpTxIds is not of length 1', async function () {
      await wallet.accelerateTransaction({ cpfpTxIds: [] }).should.be.rejectedWith({ code: 'cpfptxids_not_array' });
      await wallet
        .accelerateTransaction({ cpfpTxIds: ['id1', 'id2'] })
        .should.be.rejectedWith({ code: 'cpfptxids_not_array' });
    });

    it('fails if cpfpFeeRate is not passed and neither is noCpfpFeeRate', async function () {
      await wallet.accelerateTransaction({ cpfpTxIds: ['id'] }).should.be.rejectedWith({ code: 'cpfpfeerate_not_set' });
    });

    it('fails if cpfpFeeRate is not an integer', async function () {
      await wallet
        // @ts-expect-error checking type mismatch
        .accelerateTransaction({ cpfpTxIds: ['id'], cpfpFeeRate: 'one' })
        .should.be.rejectedWith({ code: 'cpfpfeerate_not_nonnegative_integer' });
    });

    it('fails if cpfpFeeRate is negative', async function () {
      await wallet
        .accelerateTransaction({ cpfpTxIds: ['id'], cpfpFeeRate: -1 })
        .should.be.rejectedWith({ code: 'cpfpfeerate_not_nonnegative_integer' });
    });

    it('fails if maxFee is not passed and neither is noMaxFee', async function () {
      await wallet
        .accelerateTransaction({ cpfpTxIds: ['id'], noCpfpFeeRate: true })
        .should.be.rejectedWith({ code: 'maxfee_not_set' });
    });

    it('fails if maxFee is not an integer', async function () {
      await wallet
        // @ts-expect-error checking type mismatch
        .accelerateTransaction({ cpfpTxIds: ['id'], noCpfpFeeRate: true, maxFee: 'one' })
        .should.be.rejectedWith({ code: 'maxfee_not_nonnegative_integer' });
    });

    it('fails if maxFee is negative', async function () {
      await wallet
        .accelerateTransaction({ cpfpTxIds: ['id'], noCpfpFeeRate: true, maxFee: -1 })
        .should.be.rejectedWith({ code: 'maxfee_not_nonnegative_integer' });
    });

    it('fails if both rbfTxids and cpfpTxids is set', async function () {
      await wallet
        .accelerateTransaction({ cpfpTxIds: ['id1'], rbfTxIds: ['id2'] })
        .should.be.rejectedWith({ code: 'cannot_specify_both_cpfp_and_rbf_txids' });
    });

    it('fails if rbfTxIds is set but feeMultiplier is missing', async function () {
      await wallet
        .accelerateTransaction({ rbfTxIds: ['id'] })
        .should.be.rejectedWith({ code: 'feemultiplier_not_set' });
    });

    it('fails if fee multiplier is less than or equal to 1', async function () {
      await wallet
        .accelerateTransaction({ rbfTxIds: ['id'], feeMultiplier: 1 })
        .should.be.rejectedWith({ code: 'feemultiplier_greater_than_one' });

      await wallet
        .accelerateTransaction({ rbfTxIds: ['id2'], feeMultiplier: 0.5 })
        .should.be.rejectedWith({ code: 'feemultiplier_greater_than_one' });
    });

    it('submits a transaction with all cpfp specific parameters', async function () {
      const params = {
        cpfpTxIds: ['id'],
        cpfpFeeRate: 1,
        maxFee: 1,
      };

      const prebuildReturn = Object.assign({ txHex: '123' }, params);
      const prebuildStub = sinon.stub(wallet, 'prebuildAndSignTransaction').resolves(prebuildReturn);

      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`;
      nock(bgUrl).post(path, _.matches(prebuildReturn)).reply(200);

      await wallet.accelerateTransaction(params);

      prebuildStub.should.have.been.calledOnceWith(params);

      sinon.restore();
    });
  });

  describe('fanout input maxNumInputsToUse and unspents verification', function () {
    const address = '5b34252f1bf349930e34020a';
    const maxNumInputsToUse = 2;
    const unspents = [
      'cc30565750e2aeb818625aaedaf89db5c614e5977b9645cee1d7289f616fb1d8:0',
      '8c45164787a954ab07864af9b05b34fbde3a8e430a8c65b0e60e4e543d8e1b6c:2',
    ];
    let basecoin;
    let wallet;

    before(async function () {
      basecoin = bitgo.coin('tbtc');
      const walletData = {
        id: '5b34252f1bf349930e34020a',
        coin: 'tbtc',
        keys: ['5b3424f91bf349930e340175'],
      };
      wallet = new Wallet(bitgo, basecoin, walletData);
    });

    it('should pass maxNumInputsToUse parameter when calling fanout unspents', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/fanoutUnspents`;
      const response = nock(bgUrl)
        .post(path, _.matches({ maxNumInputsToUse })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      try {
        await wallet.fanoutUnspents({ address, maxNumInputsToUse });
      } catch (e) {
        // the fanoutUnspents method will probably throw an exception for not having all of the correct nocks
        // we only care about /fanoutUnspents and whether maxNumInputsToUse is an allowed parameter
      }

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

    it('should pass unspents parameter when calling fanout unspents', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/fanoutUnspents`;
      const response = nock(bgUrl)
        .post(path, _.matches({ unspents })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      try {
        await wallet.fanoutUnspents({ address, unspents });
      } catch (e) {
        // the fanoutUnspents method will probably throw an exception for not having all of the correct nocks
        // we only care about /fanoutUnspents and whether unspents is an allowed parameter
      }

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

    it('should only build tx (not sign/send) while fanning out unspents', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/fanoutUnspents`;
      const response = nock(bgUrl).post(path, _.matches({ unspents })).reply(200);

      const unusedNocks = nock(bgUrl);
      unusedNocks.get(`/api/v2/${wallet.coin()}/key/${wallet.keyIds()[0]}`).reply(200);
      unusedNocks.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`).reply(200);

      try {
        await wallet.fanoutUnspents({ address, unspents }, ManageUnspentsOptions.BUILD_ONLY);
      } catch (e) {
        // the fanoutUnspents method will probably throw an exception for not having all of the correct nocks
        // we only care about /fanoutUnspents and whether unspents is an allowed parameter
      }

      response.isDone().should.be.true();
      unusedNocks.pendingMocks().length.should.eql(2);
      nock.cleanAll();
    });
  });

  describe('manage unspents', function () {
    let rootWalletKey;
    let walletPassphrase;
    let basecoin;
    let wallet;
    let keysObj;

    before(async function () {
      rootWalletKey = getDefaultWalletKeys();
      walletPassphrase = 'fixthemoneyfixtheworld';
      keysObj = toKeychainObjects(rootWalletKey, walletPassphrase);
      basecoin = bitgo.coin('tbtc');
      const walletData = {
        id: '5b34252f1bf349930e34020a',
        coin: 'tbtc',
        keys: keysObj.map((k) => k.id),
      };
      wallet = new Wallet(bitgo, basecoin, walletData);
    });

    it('should pass for bulk consolidating unspents', async function () {
      const psbts = (['p2wsh', 'p2shP2wsh'] as const).map((scriptType) =>
        utxoLib.testutil.constructPsbt(
          [{ scriptType, value: BigInt(1000) }],
          [{ scriptType, value: BigInt(900) }],
          basecoin.network,
          rootWalletKey,
          'unsigned'
        )
      );
      const txHexes = psbts.map((psbt) => ({ txHex: psbt.toHex() }));

      const nocks: nock.Scope[] = [];
      nocks.push(
        nock(bgUrl).post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/consolidateUnspents`).reply(200, txHexes)
      );

      nocks.push(
        ...keysObj.map((k, i) => nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${wallet.keyIds()[i]}`).reply(200, k))
      );

      nocks.push(
        ...psbts.map((psbt) =>
          nock(bgUrl)
            .post(
              `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`,
              _.matches({ txHex: psbt.signAllInputsHD(rootWalletKey.user).toHex() })
            )
            .reply(200)
        )
      );

      await wallet.consolidateUnspents({ bulk: true, walletPassphrase });

      nocks.forEach((n) => {
        console.log(n);
        n.isDone().should.be.true();
      });
    });

    it('should pass for single consolidating unspents', async function () {
      const psbt = utxoLib.testutil.constructPsbt(
        [{ scriptType: 'p2wsh', value: BigInt(1000) }],
        [{ scriptType: 'p2shP2wsh', value: BigInt(900) }],
        basecoin.network,
        rootWalletKey,
        'unsigned'
      );

      const nocks: nock.Scope[] = [];
      nocks.push(
        nock(bgUrl)
          .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/consolidateUnspents`)
          .reply(200, { txHex: psbt.toHex() })
      );

      nocks.push(
        ...keysObj.map((k, i) => nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${wallet.keyIds()[i]}`).reply(200, k))
      );

      nocks.push(
        nock(bgUrl)
          .post(
            `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`,
            _.matches({ txHex: psbt.signAllInputsHD(rootWalletKey.user).toHex() })
          )
          .reply(200)
      );

      await wallet.consolidateUnspents({ walletPassphrase });

      nocks.forEach((n) => {
        n.isDone().should.be.true();
      });
    });
  });
  describe('max recipient', function () {
    const address = '5b34252f1bf349930e34020a';
    const recipients = [
      {
        address,
        amount: 'max',
      },
    ];
    let basecoin;
    let wallet;

    before(async function () {
      basecoin = bitgo.coin('tbtc');
      const walletData = {
        id: '5b34252f1bf349930e34020a',
        coin: 'tbtc',
        keys: ['5b3424f91bf349930e340175'],
      };
      wallet = new Wallet(bitgo, basecoin, walletData);
    });

    it('should pass maxFeeRate parameter when building transactions', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`;
      const response = nock(bgUrl)
        .post(
          path,
          _.matches({
            recipients,
          })
        ) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      try {
        await wallet.prebuildTransaction({ recipients });
      } catch (e) {
        // the prebuildTransaction method will probably throw an exception for not having all of the correct nocks
        // we only care about /tx/build and whether maxFeeRate is an allowed parameter
      }

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

  describe('maxFeeRate verification', function () {
    const address = '5b34252f1bf349930e34020a';
    const recipients = [
      {
        address,
        amount: 0,
      },
    ];
    const maxFeeRate = 10000;
    let basecoin;
    let wallet;

    before(async function () {
      basecoin = bitgo.coin('tbtc');
      const walletData = {
        id: '5b34252f1bf349930e34020a',
        coin: 'tbtc',
        keys: ['5b3424f91bf349930e340175'],
      };
      wallet = new Wallet(bitgo, basecoin, walletData);
    });

    it('should pass maxFeeRate parameter when building transactions', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`;
      const response = nock(bgUrl)
        .post(path, _.matches({ recipients, maxFeeRate })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      try {
        await wallet.prebuildTransaction({ recipients, maxFeeRate });
      } catch (e) {
        // the prebuildTransaction method will probably throw an exception for not having all of the correct nocks
        // we only care about /tx/build and whether maxFeeRate is an allowed parameter
      }

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

    it('should pass maxFeeRate parameter when consolidating unspents', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/consolidateUnspents`;
      const response = nock(bgUrl)
        .post(path, _.matches({ maxFeeRate })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      nock(bgUrl).get(`/api/v2/${wallet.coin()}/key/${wallet.keyIds()[0]}`).reply(200);

      try {
        await wallet.consolidateUnspents({ recipients, maxFeeRate });
      } catch (e) {
        // the consolidateUnspents method will probably throw an exception for not having all of the correct nocks
        // we only care about /consolidateUnspents and whether maxFeeRate is an allowed parameter
      }

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

    it('should only build tx (not sign/send) while consolidating unspents', async function () {
      const toBeUsedNock = nock(bgUrl);
      toBeUsedNock.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/consolidateUnspents`).reply(200);

      const unusedNocks = nock(bgUrl);
      unusedNocks.get(`/api/v2/${wallet.coin()}/key/${wallet.keyIds()[0]}`).reply(200);
      unusedNocks.post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/send`).reply(200);

      await wallet.consolidateUnspents({ recipients }, ManageUnspentsOptions.BUILD_ONLY);

      toBeUsedNock.isDone().should.be.true();
      unusedNocks.pendingMocks().length.should.eql(2);
      nock.cleanAll();
    });

    it('should pass maxFeeRate parameter when calling sweep wallets', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/sweepWallet`;
      const response = nock(bgUrl)
        .post(path, _.matches({ address, maxFeeRate })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      try {
        await wallet.sweep({ address, maxFeeRate });
      } catch (e) {
        // the sweep method will probably throw an exception for not having all of the correct nocks
        // we only care about /sweepWallet and whether maxFeeRate is an allowed parameter
      }

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

    it('should pass maxFeeRate parameter when calling fanout unspents', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/fanoutUnspents`;
      const response = nock(bgUrl)
        .post(path, _.matches({ maxFeeRate })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      try {
        await wallet.fanoutUnspents({ address, maxFeeRate });
      } catch (e) {
        // the fanoutUnspents method will probably throw an exception for not having all of the correct nocks
        // we only care about /fanoutUnspents and whether maxFeeRate is an allowed parameter
      }

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

  describe('allowPartialSweep verification', function () {
    const address = '5b34252f1bf349930e34020a';
    const allowPartialSweep = true;
    let basecoin;
    let wallet;

    before(async function () {
      basecoin = bitgo.coin('tbtc');
      const walletData = {
        id: '5b34252f1bf349930e34020a',
        coin: 'tbtc',
        keys: ['5b3424f91bf349930e340175'],
      };
      wallet = new Wallet(bitgo, basecoin, walletData);
    });

    it('should pass allowPartialSweep parameter when calling sweep wallets', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/sweepWallet`;
      const response = nock(bgUrl)
        .post(path, _.matches({ address, allowPartialSweep })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);

      try {
        await wallet.sweep({ address, allowPartialSweep });
      } catch (e) {
        // the sweep method will probably throw an exception for not having all of the correct nocks
        // we only care about /sweepWallet and whether allowPartialSweep is an allowed parameter
      }

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

  describe('sweep wallet', function () {
    let basecoin;
    let wallet;

    before(async function () {
      basecoin = bitgo.coin('ttrx');
      const walletData = {
        id: '5b34252f1bf349930e34020a',
        coin: 'ttrx',
        keys: ['5b3424f91bf349930e340175'],
      };
      wallet = new Wallet(bitgo, basecoin, walletData);
    });

    it('should use maximum spendable balance of wallet to sweep funds ', async function () {
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/maximumSpendable`;
      const response = nock(bgUrl).get(path).reply(200, {
        coin: 'ttrx',
        maximumSpendable: 65000,
      });
      const body = {
        coin: 'ttrx',
        address: '2MwvR24yqym2CgHMp7zwvdeqBa4F8KTqunS',
      };
      try {
        await wallet.sweep(body);
      } catch (e) {
        // the sweep method will probably throw an exception for not having all of the correct nocks
        // we only care about maximum spendable balance being used to sweep funds
      }

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

  describe('Transaction prebuilds', function () {
    let ethWallet;

    before(async function () {
      const walletData = {
        id: '598f606cd8fc24710d2ebadb1d9459bb',
        coin: 'teth',
        keys: [
          '598f606cd8fc24710d2ebad89dce86c2',
          '598f606cc8e43aef09fcb785221d9dd2',
          '5935d59cf660764331bafcade1855fd7',
        ],
      };
      ethWallet = new Wallet(bitgo, bitgo.coin('teth'), walletData);
    });

    it('should return reqId if it was passed in the params', async function () {
      const params = { offlineVerification: true };
      const scope = nock(bgUrl)
        .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`, tbtcHotWalletDefaultParams)
        .query(params)
        .reply(200, {});
      const blockHeight = 100;
      sinon.stub(basecoin, 'getLatestBlockHeight').resolves(blockHeight);
      sinon.stub(basecoin, 'postProcessPrebuild').resolves({});
      const txRequest = await wallet.prebuildTransaction({ ...params, reqId: reqId });
      txRequest.reqId?.should.containEql(reqId);
      scope.done();
    });

    it('should pass offlineVerification=true query param if passed truthy value', async function () {
      const params = { offlineVerification: true };
      const scope = nock(bgUrl)
        .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`, tbtcHotWalletDefaultParams)
        .query(params)
        .reply(200, {});
      const blockHeight = 100;
      const blockHeightStub = sinon.stub(basecoin, 'getLatestBlockHeight').resolves(blockHeight);
      const postProcessStub = sinon.stub(basecoin, 'postProcessPrebuild').resolves({});
      await wallet.prebuildTransaction(params);
      blockHeightStub.should.have.been.calledOnce();
      postProcessStub.should.have.been.calledOnceWith({
        blockHeight: 100,
        wallet: wallet,
        buildParams: tbtcHotWalletDefaultParams,
      });
      scope.done();
      blockHeightStub.restore();
      postProcessStub.restore();
    });

    it('should not pass the offlineVerification query param if passed a falsey value', async function () {
      const params = { offlineVerification: false };
      nock(bgUrl)
        .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`, tbtcHotWalletDefaultParams)
        .query({})
        .reply(200, {});
      const blockHeight = 100;
      const blockHeightStub = sinon.stub(basecoin, 'getLatestBlockHeight').resolves(blockHeight);
      const postProcessStub = sinon.stub(basecoin, 'postProcessPrebuild').resolves({});
      await wallet.prebuildTransaction(params);
      blockHeightStub.should.have.been.calledOnce();
      postProcessStub.should.have.been.calledOnceWith({
        blockHeight: 100,
        wallet: wallet,
        buildParams: tbtcHotWalletDefaultParams,
      });
      blockHeightStub.restore();
      postProcessStub.restore();
    });

    it('should pass script outputs with the proper structure to wallet platform', async function () {
      const script = '6a11223344556677889900';
      nock(bgUrl)
        .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`, {
          ...tbtcHotWalletDefaultParams,
          recipients: [{ script, amount: 1e6 }],
        })
        .query({})
        .reply(200, {});

      const blockHeight = 100;
      const blockHeightStub = sinon.stub(basecoin, 'getLatestBlockHeight').resolves(blockHeight);
      const postProcessStub = sinon.stub(basecoin, 'postProcessPrebuild').resolves({});
      await wallet.prebuildTransaction({ recipients: [{ address: `scriptPubKey:${script}`, amount: 1e6 }] });
      blockHeightStub.should.have.been.calledOnce();
      postProcessStub.should.have.been.calledOnceWith({
        blockHeight: 100,
        wallet: wallet,
        buildParams: { ...tbtcHotWalletDefaultParams, recipients: [{ script, amount: 1e6 }] },
      });
      blockHeightStub.restore();
      postProcessStub.restore();
    });

    it('prebuild should call build and getLatestBlockHeight for utxo coins', async function () {
      const params = {};
      nock(bgUrl)
        .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`, tbtcHotWalletDefaultParams)
        .query(params)
        .reply(200, {});
      const blockHeight = 100;
      const blockHeightStub = sinon.stub(basecoin, 'getLatestBlockHeight').resolves(blockHeight);
      const postProcessStub = sinon.stub(basecoin, 'postProcessPrebuild').resolves({});
      await wallet.prebuildTransaction(params);
      blockHeightStub.should.have.been.calledOnce();
      postProcessStub.should.have.been.calledOnceWith({
        blockHeight: 100,
        wallet: wallet,
        buildParams: tbtcHotWalletDefaultParams,
      });
      blockHeightStub.restore();
      postProcessStub.restore();
    });

    it('prebuild should not have changeAddressType array in post body when changeAddressType is defined', async function () {
      const expectedBuildPostBodyParams = {
        changeAddressType: 'p2trMusig2',
        txFormat: 'psbt',
      };

      nock(bgUrl)
        .post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`, expectedBuildPostBodyParams)
        .query({})
        .reply(200, {});
      const blockHeight = 100;
      const blockHeightStub = sinon.stub(basecoin, 'getLatestBlockHeight').resolves(blockHeight);
      const postProcessStub = sinon.stub(basecoin, 'postProcessPrebuild').resolves({});
      await wallet.prebuildTransaction({ changeAddressType: 'p2trMusig2' });
      blockHeightStub.should.have.been.calledOnce();
      postProcessStub.should.have.been.calledOnceWith({
        blockHeight: 100,
        wallet: wallet,
        buildParams: expectedBuildPostBodyParams,
      });
      blockHeightStub.restore();
      postProcessStub.restore();
    });

    it('prebuild should call build but not getLatestBlockHeight for account coins', async function () {
      ['txrp', 'txlm', 'teth'].forEach(async function (coin) {
        const accountcoin = bitgo.coin(coin);
        const walletData = {
          id: '5b34252f1bf349930e34021a',
          coin,
          keys: ['5b3424f91bf349930e340175'],
        };
        const accountWallet = new Wallet(bitgo, accountcoin, walletData);
        const params = {};
        nock(bgUrl)
          .post(`/api/v2/${accountWallet.coin()}/wallet/${accountWallet.id()}/tx/build`)
          .query(params)
          .reply(200, {});
        const postProcessStub = sinon.stub(accountcoin, 'postProcessPrebuild').resolves({});
        await accountWallet.prebuildTransaction(params);
        postProcessStub.should.have.been.calledOnceWith({
          wallet: accountWallet,
          buildParams: {},
        });
        postProcessStub.restore();
      });
    });

    it('should have isBatch = true in the txPrebuild if txParams has more than one recipient', async function () {
      const txParams = {
        recipients: [
          { amount: '1000000000000000', address: address1 },
          { amount: '1000000000000000', address: address2 },
        ],
        walletContractAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
        walletPassphrase: 'moon',
      };

      const totalAmount = '2000000000000000';

      nock(bgUrl)
        .post(
          `/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/tx/build`,
          _.matches({ recipients: txParams.recipients })
        )
        .reply(200, {
          recipients: [
            {
              address: '0xc0aaf2649e7b0f3950164681eca2b1a8f654a478',
              amount: '2000000000000000',
              data: '0xc00c4e9e000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000000000002000000000000000000000000174cfd823af8ce27ed0afee3fcf3c3ba259116be0000000000000000000000007e85bdc27c050e3905ebf4b8e634d9ad6edd0de6000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000038d7ea4c6800000000000000000000000000000000000000000000000000000038d7ea4c68000',
            },
          ],
          nextContractSequenceId: 10896,
          gasPrice: 20000000000,
          gasLimit: 500000,
          isBatch: true,
          coin: 'teth',
        });

      const txPrebuild = await ethWallet.prebuildTransaction(txParams);
      txPrebuild.isBatch.should.equal(true);
      txPrebuild.recipients[0].address.should.equal(
        (bitgo.coin('teth') as any).staticsCoin.network.batcherContractAddress
      );
      txPrebuild.recipients[0].amount.should.equal(totalAmount);
    });

    it('should have isBatch = false and hopTransaction field should not be there in the txPrebuild  for normal eth tx', async function () {
      const txParams = {
        recipients: [{ amount: '1000000000000000', address: address1 }],
        walletContractAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
        walletPassphrase: 'moon',
      };

      nock(bgUrl)
        .post(
          `/api/v2/${ethWallet.coin()}/wallet/${ethWallet.id()}/tx/build`,
          _.matches({ recipients: txParams.recipients })
        )
        .reply(200, {
          recipients: [
            {
              amount: '1000000000000000',
              address: '0x174cfd823af8ce27ed0afee3fcf3c3ba259116be',
            },
          ],
          nextContractSequenceId: 10897,
          gasPrice: 20000000000,
          gasLimit: 500000,
          isBatch: false,
          coin: 'teth',
        });

      const txPrebuild = await ethWallet.prebuildTransaction(txParams);
      txPrebuild.isBatch.should.equal(false);
      txPrebuild.should.not.have.property('hopTransaction');
      txPrebuild.recipients[0].address.should.equal(address1);
      txPrebuild.recipients[0].amount.should.equal('1000000000000000');
    });

    it('should pass unspent reservation parameter through when building transactions', async function () {
      const reservation = {
        expireTime: '2029-08-12',
      };
      const recipients = [
        {
          address: 'aaa',
          amount: '1000',
        },
      ];
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`;
      const response = nock(bgUrl)
        .post(path, _.matches({ recipients, reservation })) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);
      try {
        await wallet.prebuildTransaction({ recipients, reservation });
      } catch (e) {
        // the prebuildTransaction method will probably throw an exception for not having all of the correct nocks
        // we only care about /tx/build and whether reservation is an allowed parameter
      }

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

    it('should pass gas limit parameter through when building transaction for sui', async function () {
      const params = { gasLimit: 100 };
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/build`;
      const response = nock(bgUrl)
        .post(path, _.matches(params)) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200);
      try {
        await wallet.prebuildTransaction(params);
      } catch (e) {
        // the prebuildTransaction method will probably throw an exception for not having all of the correct nocks
        // we only care about /tx/build and whether reservation is an allowed parameter
      }

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

  describe('Maximum Spendable', function maximumSpendable() {
    let bgUrl;

    before(async function () {
      nock.pendingMocks().should.be.empty();
      bgUrl = common.Environments[bitgo.getEnv()].uri;
    });

    it('arguments', async function () {
      const optionalParams = {
        limit: 25,
        minValue: '0',
        maxValue: '9999999999999',
        minHeight: 0,
        minConfirms: 2,
        enforceMinConfirmsForChange: false,
        feeRate: 10000,
        maxFeeRate: 100000,
        recipientAddress: '2NCUFDLiUz9CVnmdVqQe9acVonoM89e76df',
      };

      // The actual api request will only send strings, but the SDK function expects numbers for some values
      const apiParams = _.mapValues(optionalParams, (param) => String(param));

      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/maximumSpendable`;
      const response = nock(bgUrl)
        .get(path)
        .query(_.matches(apiParams)) // use _.matches to do a partial match on request body object instead of strict matching
        .reply(200, {
          coin: 'tbch',
          maximumSpendable: 65000,
        });

      try {
        await wallet.maximumSpendable(optionalParams);
      } catch (e) {
        // test is successful if nock is consumed
      }

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

  describe('Wallet Sharing', function () {
    it('should share to cold wallet without passing skipKeychain', async function () {
      const userId = '123';
      const email = 'shareto@sdktest.com';
      const permissions = 'view,spend';

      const getSharingKeyNock = nock(bgUrl).post('/api/v1/user/sharingkey', { email }).reply(200, { userId });

      const getKeyNock = nock(bgUrl)
        .get(`/api/v2/tbtc/key/${coldWallet.keyIds()[0]}`)
        .reply(200, {})
        .get(`/api/v2/tbtc/key/${coldWallet.keyIds()[1]}`)
        .reply(200, {})
        .get(`/api/v2/tbtc/key/${coldWallet.keyIds()[2]}`)
        .reply(200, {});

      const createShareNock = nock(bgUrl)
        .post(`/api/v2/tbtc/wallet/${coldWallet.id()}/share`, {
          user: userId,
          permissions,
          skipKeychain: true,
        })
        .reply(200, {});

      await coldWallet.shareWallet({ email, permissions });

      getSharingKeyNock.isDone().should.be.True();
      getKeyNock.isDone().should.be.True();
      createShareNock.isDone().should.be.True();
    });

    describe('Hot Wallet Sharing', function () {
      const userId = '123';
      const email = 'shareto@sdktest.com';
      const permissions = 'view,spend';
      const toKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef02deadbeef02deadbeef02deadbeef02', 'hex'));
      const path = 'm/999999/1/1';
      const pubkey = toKeychain.derivePath(path).publicKey.toString('hex');
      const walletPassphrase = 'bitgo1234';
      const pub = 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQk';

      const lightningCoin: any = bitgo.coin('tlnbtc');
      const lightningWalletData = {
        id: '5b34252f1bf349930e34020a00000001',
        coin: 'tlnbtc',
        keys: ['5b3424f91bf349930e34017500000001'],
        coinSpecific: { keys: ['5b3424f91bf349930e34017600000000', '5b3424f91bf349930e34017700000000'] },
        type: 'hot',
      };
      const lightningWallet = new Wallet(bitgo, lightningCoin, lightningWalletData);

      for (const hotWallet of [wallet, lightningWallet] as const) {
        it(`should use keychain pub to share ${hotWallet.coin()} hot wallet`, async function () {
          const getSharingKeyNock = nock(bgUrl)
            .post('/api/v1/user/sharingkey', { email })
            .reply(200, { userId, pubkey, path });

          const getKeyNocks: nock.Scope[] = [];
          if (hotWallet.baseCoin.getFamily() === 'lnbtc') {
            for (let i = 0; i < 2; i++) {
              const keyId = lightningWalletData.coinSpecific.keys[i];
              const getKeyNock = nock(bgUrl)
                .get(`/api/v2/tlnbtc/key/${keyId}`)
                .reply(200, {
                  id: keyId,
                  pub: i === 0 ? pub : 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQm',
                  source: 'user',
                  encryptedPrv: bitgo.encrypt({ input: 'xprv' + i, password: walletPassphrase }),
                  coinSpecific:
                    i === 0
                      ? { [hotWallet.baseCoin.getChain()]: { purpose: 'userAuth' } }
                      : { [hotWallet.baseCoin.getChain()]: { purpose: 'nodeAuth' } },
                });
              getKeyNocks.push(getKeyNock);
            }
          } else {
            const getKeyNock = 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: {},
              });
            getKeyNocks.push(getKeyNock);
          }

          const stub = sinon.stub(hotWallet, 'createShare').callsFake(async (options) => {
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            options!.keychain!.pub!.should.not.be.undefined();
            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
            options!.keychain!.pub!.should.equal(pub);
            return undefined;
          });
          await hotWallet.shareWallet({ email, permissions, walletPassphrase });

          stub.calledOnce.should.be.true();
          getSharingKeyNock.isDone().should.be.True();
          getKeyNocks.every((v) => v.isDone().should.be.True());
        });
      }
    });

    it('should provide skipKeychain to wallet share api for hot wallet', async function () {
      const userId = '123';
      const email = 'shareto@sdktest.com';
      const permissions = 'view,spend';
      const toKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef02deadbeef02deadbeef02deadbeef02', 'hex'));
      const path = 'm/999999/1/1';
      const pubkey = toKeychain.derivePath(path).publicKey.toString('hex');

      const getSharingKeyNock = nock(bgUrl)
        .post('/api/v1/user/sharingkey', { email })
        .reply(200, { userId, pubkey, path });
      const createShareNock = nock(bgUrl)
        .post(`/api/v2/tbtc/wallet/${wallet.id()}/share`, {
          user: userId,
          permissions,
          skipKeychain: true,
        })
        .reply(200, {});

      await wallet.shareWallet({ email, permissions, skipKeychain: true });

      createShareNock.isDone().should.be.True();
      getSharingKeyNock.isDone().should.be.True();
    });

    it('should decrypt webauthn encryptedPrv for wallet share (spend)', async function () {
      const userId = '123';
      const email = 'shareto@sdktest.com';
      const permissions = 'view,spend';
      const toKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef02deadbeef02deadbeef02deadbeef02', 'hex'));
      const path = 'm/999999/1/1';
      const pubkey = toKeychain.derivePath(path).publicKey.toString('hex');
      const privateKey = 'xprv1';
      const walletPassphrase1 = 'bitgo1234';
      const walletPassphrase2 = 'bitgo5678';

      const getSharingKeyNock = nock(bgUrl)
        .post('/api/v1/user/sharingkey', { email })
        .reply(200, { userId, pubkey, path });

      const pub = 'Zo1ggzTUKMY5bYnDvT5mtVeZxzf2FaLTbKkmvGUhUQk';
      const getKeyNock = nock(bgUrl)
        .get(`/api/v2/tbtc/key/${wallet.keyIds()[0]}`)
        .reply(200, {
          id: wallet.keyIds()[0],
          pub,
          source: 'user',
          encryptedPrv: bitgo.encrypt({ input: privateKey, password: walletPassphrase1 }),
          webauthnDevices: [
            {
              otpDeviceId: '123',
              authenticatorInfo: {
                credID: 'credID',
                fmt: 'packed',
                publicKey: 'some value',
              },
              prfSalt: '456',
              encryptedPrv: bitgo.encrypt({ input: privateKey, password: walletPassphrase2 }),
            },
          ],
          coinSpecific: {},
        });

      const stub = sinon.stub(wallet, 'createShare').callsFake(async (options) => {
        options!.keychain!.encryptedPrv!.should.not.be.undefined();
        return undefined;
      });
      await wallet.shareWallet({ email, permissions, walletPassphrase: walletPassphrase2 });
      stub.calledOnce.should.be.true();
      getSharingKeyNock.isDone().should.be.True();
      getKeyNock.isDone().should.be.True();
    });
  });

  describe('Wallet Freezing', function () {
    it('should freeze wallet for specified duration in seconds', async function () {
      const params = { duration: 60 };
      const scope = nock(bgUrl).post(`/api/v2/${wallet.coin()}/wallet/${wallet.id()}/freeze`, params).reply(200, {});
      await wallet.freeze(params);
      scope.isDone().should.be.True();
    });
  });

  describe('TSS Wallets', function () {
    const sandbox = sinon.createSandbox();
    const tsol = bitgo.coin('tsol');
    const walletData = {
      id: '5b34252f1bf349930e34020a00000000',
      coin: 'tsol',
      keys: [
        '598f606cd8fc24710d2ebad89dce86c2',
        '598f606cc8e43aef09fcb785221d9dd2',
        '5935d59cf660764331bafcade1855fd7',
      ],
      coinSpecific: {},
      multisigType: 'tss',
    };

    const ethWalletData = {
      id: '598f606cd8fc24710d2ebadb1d9459bb',
      coin: 'teth',
      keys: [
        '598f606cd8fc24710d2ebad89dce86c2',
        '598f606cc8e43aef09fcb785221d9dd2',
        '5935d59cf660764331bafcade1855fd7',
      ],
      multisigType: 'tss',
      coinSpecific: { addressVersion: 1 },
      type: 'hot',
    };

    const polygonWalletData = {
      id: '632826520ee1e5000729017354acaeab',
      coin: 'tpolygon',
      keys: [
        '598f606cd8fc24710d2ebad89dce86c2',
        '598f606cc8e43aef09fcb785221d9dd2',
        '5935d59cf660764331bafcade1855fd7',
      ],
      multisigType: 'tss',
    };

    const tssSolWallet = new Wallet(bitgo, tsol, walletData);

    let tssEthWallet = new Wallet(bitgo, bitgo.coin('teth'), ethWalletData);
    const tssPolygonWallet = new Wallet(bitgo, bitgo.coin('tpolygon'), polygonWalletData);
    const custodialTssSolWallet = new Wallet(bitgo, tsol, {
      ...walletData,
      type: 'custodial',
    });

    const txRequest: TxRequest = {
      txRequestId: 'id',
      transactions: [],
      intent: {
        intentType: 'payment',
      },
      date: new Date().toISOString(),
      latest: true,
      state: 'pendingUserSignature',
      userId: 'userId',
      walletType: 'hot',
      policiesChecked: false,
      version: 1,
      walletId: 'walletId',
      unsignedTxs: [
        {
          serializedTxHex: 'ababcdcd',
          signableHex: 'deadbeef',
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
          derivationPath: 'm/0',
        },
      ],
    };

    const txRequestFull: TxRequest = {
      txRequestId: 'id',
      intent: {
        intentType: 'payment',
      },
      date: new Date().toISOString(),
      latest: true,
      state: 'pendingUserSignature',
      userId: 'userId',
      walletId: 'walletId',
      signatureShares: [],
      version: 1,
      policiesChecked: false,
      walletType: 'hot',
      transactions: [
        {
          state: 'pendingSignature',
          unsignedTx: {
            serializedTxHex: 'ababcdcd',
            signableHex: 'deadbeef',
            feeInfo: {
              fee: 5000,
              feeString: '5000',
            },
            derivationPath: 'm/0',
          },
          signatureShares: [],
          commitmentShares: [],
        },
      ],
      unsignedTxs: [],
      apiVersion: 'full',
    };

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

    describe('preBuildAndSignTransaction', async function () {
      const params = {
        walletPassphrase: 'passphrase12345',
        prebuildTx: { walletId: tssEthWallet.id(), txRequestId: 'randomId' },
        type: 'transfer',
      };

      ['eddsa', 'ecdsa'].forEach((keyCurve: string) => {
        describe(keyCurve, () => {
          const wallet = keyCurve === 'eddsa' ? tssSolWallet : tssEthWallet;

          beforeEach(function () {
            sandbox
              .stub(Keychains.prototype, 'getKeysForSigning')
              .resolves([{ commonKeychain: 'test', id: '', pub: '', type: 'independent' }]);
            if (keyCurve === 'eddsa') {
              sandbox.stub(Tsol.prototype, 'verifyTransaction').resolves(true);
            } else {
              sandbox.stub(Teth.prototype, 'verifyTransaction').resolves(true);
            }
          });

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

          it('it should succeed but not sign if the txRequest is pending approval', async function () {
            const getTxRequestStub = sandbox.stub(BaseTssUtils.default.prototype, 'getTxRequest').resolves({
              ...txRequestFull,
              state: 'pendingApproval',
            });

            const signTransactionSpy = sandbox.spy(Wallet.prototype, 'signTransaction');

            const result = (await wallet.prebuildAndSignTransaction(params)) as TxRequest;
            result.should.have.property('state');
            result.state.should.equal('pendingApproval');
            getTxRequestStub.calledOnce.should.be.true();
            signTransactionSpy.notCalled.should.be.true();
          });

          it('it should succeed and sign if the txRequest is not pending approval', async function () {
            const getTxRequestStub = sandbox.stub(BaseTssUtils.default.prototype, 'getTxRequest');
            getTxRequestStub.resolves(txRequestFull);

            const signTransactionStub = sandbox.stub(Wallet.prototype, 'signTransaction');
            signTransactionStub.resolves({ ...txRequestFull, state: 'signed' });

            const result = (await wallet.prebuildAndSignTransaction(params)) as TxRequest;
            result.should.have.property('state');
            result.state.should.equal('signed');
            getTxRequestStub.calledOnce.should.be.true();
            signTransactionStub.calledOnce.should.be.true();
          });
        });
      });
    });

    describe('Transaction prebuilds', function () {
      it('should build a single recipient transfer transaction', async function () {
        const recipients = [
          {
            address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
            amount: '1000',
          },
        ];

        const prebuildTxWithIntent = sandbox.stub(TssUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequest);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke prebuildTransaction
        prebuildTxWithIntent.calledOnceWithExactly({
          reqId,
          recipients,
          intentType: 'payment',
        });

        const txPrebuild = await tssSolWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'transfer',
        });

        txPrebuild.should.deepEqual({
          walletId: tssSolWallet.id(),
          wallet: tssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          buildParams: {
            recipients,
            type: 'transfer',
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });

      it('should build a single recipient transfer with pending approval id if transaction is having one', async function () {
        const recipients = [
          {
            address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
            amount: '1000',
          },
        ];

        const prebuildTxWithIntent = sandbox.stub(TssUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves({ ...txRequest, state: 'pendingApproval', pendingApprovalId: 'some-id' });
        prebuildTxWithIntent.calledOnceWithExactly({
          reqId,
          recipients,
          intentType: 'payment',
        });

        const txPrebuild = await custodialTssSolWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'transfer',
        });

        txPrebuild.should.deepEqual({
          walletId: custodialTssSolWallet.id(),
          wallet: custodialTssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          pendingApprovalId: 'some-id',
          buildParams: {
            recipients,
            type: 'transfer',
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });

      it('should build a multiple recipient transfer transaction with memo', async function () {
        const recipients = [
          {
            address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
            amount: '1000',
          },
          {
            address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
            amount: '2000',
          },
        ];

        const prebuildTxWithIntent = sandbox.stub(TssUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequest);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke prebuildTransaction
        prebuildTxWithIntent.calledOnceWithExactly({
          reqId,
          recipients,
          intentType: 'payment',
          memo: {
            type: 'type',
            value: 'test memo',
          },
        });

        const txPrebuild = await tssSolWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'transfer',
          memo: {
            type: 'type',
            value: 'test memo',
          },
        });

        txPrebuild.should.deepEqual({
          walletId: tssSolWallet.id(),
          wallet: tssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          buildParams: {
            recipients,
            memo: {
              type: 'type',
              value: 'test memo',
            },
            type: 'transfer',
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });

      it('should build an enable token transaction', async function () {
        const recipients = [];
        const tokenName = 'tcoin:tokenName';
        const prebuildTxWithIntent = sandbox.stub(TssUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequest);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke prebuildTransaction
        prebuildTxWithIntent.calledOnceWithExactly({
          reqId,
          recipients,
          intentType: 'createAccount',
          memo: {
            type: 'type',
            value: 'test memo',
          },
          tokenName,
        });

        const txPrebuild = await tssSolWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'enabletoken',
          memo: {
            type: 'type',
            value: 'test memo',
          },
          tokenName,
        });

        txPrebuild.should.deepEqual({
          walletId: tssSolWallet.id(),
          wallet: tssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          buildParams: {
            recipients,
            memo: {
              type: 'type',
              value: 'test memo',
            },
            type: 'enabletoken',
            tokenName,
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });

      it('should build an enable token transaction for cold wallets', async function () {
        const recipients = [];
        const tokenName = 'tcoin:tokenName';
        const prebuildTxWithIntent = sandbox.stub(TssUtils.prototype, 'prebuildTxWithIntent');
        txRequest.walletType = 'cold';
        prebuildTxWithIntent.resolves(txRequest);
        prebuildTxWithIntent.calledOnceWithExactly({
          reqId,
          recipients,
          intentType: 'createAccount',
          memo: {
            type: 'type',
            value: 'test memo',
          },
          tokenName,
        });

        const txPrebuild = await tssSolWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'enabletoken',
          memo: {
            type: 'type',
            value: 'test memo',
          },
          tokenName,
        });

        txPrebuild.should.deepEqual({
          walletId: tssSolWallet.id(),
          wallet: tssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          buildParams: {
            recipients,
            memo: {
              type: 'type',
              value: 'test memo',
            },
            type: 'enabletoken',
            tokenName,
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });

      it('should fail for non-transfer transaction types', async function () {
        await tssSolWallet
          .prebuildTransaction({
            reqId,
            recipients: [
              {
                address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
                amount: '1000',
              },
            ],
            type: 'stake',
          })
          .should.be.rejectedWith('transaction type not supported: stake');
      });

      it('should fail for full api version compatibility', async function () {
        await custodialTssSolWallet
          .prebuildTransaction({
            reqId,
            apiVersion: 'lite',
            recipients: [
              {
                address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
                amount: '1000',
              },
            ],
            type: 'transfer',
          })
          .should.be.rejectedWith('For non self-custodial (hot) tss wallets, parameter `apiVersion` must be `full`.');
      });

      it('should build a single recipient transfer transaction for full', async function () {
        const recipients = [
          {
            address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
            amount: '1000',
          },
        ];

        const prebuildTxWithIntent = sandbox.stub(TssUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke prebuildTransaction
        prebuildTxWithIntent.calledOnceWithExactly(
          {
            reqId,
            recipients,
            intentType: 'payment',
          },
          'full'
        );

        const txPrebuild = await custodialTssSolWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'transfer',
        });

        txPrebuild.should.deepEqual({
          walletId: tssSolWallet.id(),
          wallet: custodialTssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          buildParams: {
            recipients,
            type: 'transfer',
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });

      it('should call prebuildTxWithIntent with the correct params for eth transfers', async function () {
        const recipients = [
          {
            address: '0xAB100912e133AA06cEB921459aaDdBd62381F5A3',
            amount: '1000',
          },
        ];

        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };

        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);

        await tssEthWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'transfer',
          feeOptions,
        });

        sinon.assert.calledOnce(prebuildTxWithIntent);
        const args = prebuildTxWithIntent.args[0];
        args[0]!.recipients!.should.deepEqual(recipients);
        args[0]!.feeOptions!.should.deepEqual(feeOptions);
        args[0]!.intentType.should.equal('payment');
        args[1]!.should.equal('full');
      });

      it('should call prebuildTxWithIntent with the correct params for eth transfertokens', async function () {
        const recipients = [
          {
            address: '0xAB100912e133AA06cEB921459aaDdBd62381F5A3',
            amount: '1000',
            tokenName: 'gterc18dp',
          },
        ];

        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };

        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);

        await tssEthWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'transfertoken',
          isTss: true,
          feeOptions,
        });

        sinon.assert.calledOnce(prebuildTxWithIntent);
        const args = prebuildTxWithIntent.args[0];
        args[0]!.recipients!.should.deepEqual(recipients);
        args[0]!.feeOptions!.should.deepEqual(feeOptions);
        args[0]!.isTss!.should.equal(true);
        args[0]!.intentType.should.equal('transferToken');
        args[1]!.should.equal('full');
      });

      it('should call prebuildTxWithIntent with the correct params for eth accelerations', async function () {
        const recipients = [
          {
            address: '0xAB100912e133AA06cEB921459aaDdBd62381F5A3',
            amount: '1000',
            tokenName: 'gterc18dp',
          },
        ];

        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };

        const lowFeeTxid = '0x6ea07f9420f4676be6478ab1660eb92444a7c663e0e24bece929f715e882e0cf';

        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);

        await tssEthWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'acceleration',
          feeOptions,
          lowFeeTxid,
        });

        sinon.assert.calledOnce(prebuildTxWithIntent);
        const args = prebuildTxWithIntent.args[0];
        args[0]!.should.not.have.property('recipients');
        args[0]!.feeOptions!.should.deepEqual(feeOptions);
        args[0]!.lowFeeTxid!.should.equal(lowFeeTxid);
        args[0]!.intentType.should.equal('acceleration');
        args[1]!.should.equal('full');
      });

      it('should call prebuildTxWithIntent with the correct params for eth accelerations for receive address', async function () {
        const recipients = [
          {
            address: '0xAB100912e133AA06cEB921459aaDdBd62381F5A3',
            amount: '1000',
            tokenName: 'gterc18dp',
          },
        ];

        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };

        const lowFeeTxid = '0x6ea07f9420f4676be6478ab1660eb92444a7c663e0e24bece929f715e882e0cf';
        const receiveAddress = '0x062176bc9345da3e8ee90361b0cf6ff883ba7206';

        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);

        await tssEthWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'acceleration',
          feeOptions,
          lowFeeTxid,
          receiveAddress,
        });

        sinon.assert.calledOnce(prebuildTxWithIntent);
        const args = prebuildTxWithIntent.args[0];
        args[0]!.should.not.have.property('recipients');
        args[0]!.feeOptions!.should.deepEqual(feeOptions);
        args[0]!.lowFeeTxid!.should.equal(lowFeeTxid);
        args[0]!.receiveAddress!.should.equal(receiveAddress);
        args[0]!.intentType.should.equal('acceleration');
        args[1]!.should.equal('full');
      });

      it('should call prebuildTxWithIntent with the correct params for eth fillNonce', async function () {
        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };

        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);

        const nonce = '1';
        const comment = 'fillNonce comment';

        await tssEthWallet.prebuildTransaction({
          reqId,
          type: 'fillNonce',
          feeOptions,
          nonce,
          comment,
        });

        sinon.assert.calledOnce(prebuildTxWithIntent);
        const args = prebuildTxWithIntent.args[0];
        args[0]!.should.not.have.property('recipients');
        args[0]!.feeOptions!.should.deepEqual(feeOptions);
        args[0]!.nonce!.should.equal(nonce);
        args[0]!.intentType.should.equal('fillNonce');
        args[0]!.comment!.should.equal(comment);
        args[1]!.should.equal('full');
      });

      it('should call prebuildTxWithIntent with the correct params for eth fillNonce for receive address nonce filling tx', async function () {
        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };

        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);

        const nonce = '1';
        const comment = 'fillNonce comment';
        const receiveAddress = '0x062176bc9345da3e8ee90361b0cf6ff883ba7206';

        await tssEthWallet.prebuildTransaction({
          reqId,
          type: 'fillNonce',
          feeOptions,
          nonce,
          receiveAddress,
          comment,
        });

        sinon.assert.calledOnce(prebuildTxWithIntent);
        const args = prebuildTxWithIntent.args[0];
        args[0]!.should.not.have.property('recipients');
        args[0]!.feeOptions!.should.deepEqual(feeOptions);
        args[0]!.nonce!.should.equal(nonce);
        args[0]!.intentType.should.equal('fillNonce');
        args[0]!.comment!.should.equal(comment);
        args[0]!.receiveAddress!.should.equal(receiveAddress);
        args[1]!.should.equal('full');
      });

      it('should call prebuildTxWithIntent with the correct feeOptions when passing using the legacy format', async function () {
        const recipients = [
          {
            address: '0xAB100912e133AA06cEB921459aaDdBd62381F5A3',
            amount: '1000',
          },
        ];

        const expectedFeeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
          gasLimit: undefined,
        };

        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);

        await tssEthWallet.prebuildTransaction({
          reqId,
          recipients,
          type: 'transfer',
          eip1559: {
            maxFeePerGas: expectedFeeOptions.maxFeePerGas.toString(),
            maxPriorityFeePerGas: expectedFeeOptions.maxPriorityFeePerGas.toString(),
          },
        });

        sinon.assert.calledOnce(prebuildTxWithIntent);
        const args = prebuildTxWithIntent.args[0];
        args[0]!.feeOptions!.should.deepEqual(expectedFeeOptions);
      });

      it('populate intent should return valid eth acceleration intent', async function () {
        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));

        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };
        const lowFeeTxid = '0x6ea07f9420f4676be6478ab1660eb92444a7c663e0e24bece929f715e882e0cf';

        const intent = mpcUtils.populateIntent(bitgo.coin('hteth'), {
          reqId,
          intentType: 'acceleration',
          lowFeeTxid,
          feeOptions,
        });

        intent.should.have.property('recipients', undefined);
        intent.feeOptions!.should.deepEqual(feeOptions);
        intent.txid!.should.equal(lowFeeTxid);
        intent.intentType.should.equal('acceleration');
      });

      it('populate intent should return valid eth acceleration intent for receive address', async function () {
        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));

        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };
        const lowFeeTxid = '0x6ea07f9420f4676be6478ab1660eb92444a7c663e0e24bece929f715e882e0cf';
        const receiveAddress = '0x062176bc9345da3e8ee90361b0cf6ff883ba7206';

        const intent = mpcUtils.populateIntent(bitgo.coin('hteth'), {
          reqId,
          intentType: 'acceleration',
          lowFeeTxid,
          receiveAddress,
          feeOptions,
        });

        intent.should.have.property('recipients', undefined);
        intent.feeOptions!.should.deepEqual(feeOptions);
        intent.txid!.should.equal(lowFeeTxid);
        intent.receiveAddress!.should.equal(receiveAddress);
        intent.intentType.should.equal('acceleration');
      });

      it('populate intent should return valid eth fillNonce intent', async function () {
        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));
        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };
        const nonce = '1';

        const intent = mpcUtils.populateIntent(bitgo.coin('hteth'), {
          reqId,
          intentType: 'fillNonce',
          nonce,
          feeOptions,
        });

        intent.should.have.property('recipients', undefined);
        intent.feeOptions!.should.deepEqual(feeOptions);
        intent.nonce!.should.equal(nonce);
        intent.intentType.should.equal('fillNonce');
      });

      it('populate intent should return valid eth fillNonce intent for receive address nonce filling tx', async function () {
        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));
        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };
        const nonce = '1';
        const receiveAddress = '0x062176bc9345da3e8ee90361b0cf6ff883ba7206';

        const intent = mpcUtils.populateIntent(bitgo.coin('hteth'), {
          reqId,
          intentType: 'fillNonce',
          nonce,
          receiveAddress,
          feeOptions,
        });

        intent.should.have.property('recipients', undefined);
        intent.feeOptions!.should.deepEqual(feeOptions);
        intent.nonce!.should.equal(nonce);
        intent.receiveAddress!.should.equal(receiveAddress);
        intent.intentType.should.equal('fillNonce');
      });

      it('should populate intent with custodianTransactionId', async function () {
        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));
        const feeOptions = {
          maxFeePerGas: 3000000000,
          maxPriorityFeePerGas: 2000000000,
        };
        const nonce = '1';

        const intent = mpcUtils.populateIntent(bitgo.coin('hteth'), {
          custodianTransactionId: 'unittest',
          reqId,
          intentType: 'fillNonce',
          nonce,
          feeOptions,
          isTss: true,
        });

        intent.custodianTransactionId!.should.equal('unittest');
        intent.should.have.property('recipients', undefined);
        intent.feeOptions!.should.deepEqual(feeOptions);
        intent.nonce!.should.equal(nonce);
        intent.isTss!.should.equal(true);
        intent.intentType.should.equal('fillNonce');
      });

      it('should build a single recipient transfer transaction providing apiVersion parameter as "full" ', async function () {
        const recipients = [
          {
            address: '6DadkZcx9JZgeQUDbHh12cmqCpaqehmVxv6sGy49jrah',
            amount: '1000',
          },
        ];

        const prebuildTxWithIntent = sandbox.stub(TssUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFull);
        prebuildTxWithIntent.calledOnceWithExactly(
          {
            reqId,
            recipients,
            intentType: 'payment',
          },
          'full'
        );

        const txPrebuild = await custodialTssSolWallet.prebuildTransaction({
          reqId,
          apiVersion: 'full',
          recipients,
          type: 'transfer',
        });

        txPrebuild.should.deepEqual({
          walletId: tssSolWallet.id(),
          wallet: custodialTssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          buildParams: {
            apiVersion: 'full',
            recipients,
            type: 'transfer',
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });
    });

    describe('Transaction signing', function () {
      it('should sign transaction', async function () {
        const signTxRequest = sandbox.stub(TssUtils.prototype, 'signTxRequest');
        signTxRequest.resolves(txRequest);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke signTransaction
        signTxRequest.calledOnceWithExactly({ txRequest, prv: 'secretKey', reqId });

        const txPrebuild = {
          walletId: tssSolWallet.id(),
          wallet: tssSolWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
        };
        const signedTransaction = await tssSolWallet.signTransaction({
          reqId,
          txPrebuild,
          prv: 'sercretKey',
        });
        signedTransaction.should.deepEqual(txRequest);
      });

      it('should fail to sign transaction without txRequestId', async function () {
        const txPrebuild = {
          walletId: tssSolWallet.id(),
          wallet: tssSolWallet,
          txHex: 'ababcdcd',
        };
        await tssSolWallet
          .signTransaction({
            reqId,
            txPrebuild,
            prv: 'sercretKey',
          })
          .should.be.rejectedWith('txRequestId required to sign transactions with TSS');
      });
    });

    describe('getUserKeyAndSignTssTransaction', function () {
      ['eddsa', 'ecdsa'].forEach((keyCurve: string) => {
        describe(keyCurve, () => {
          const wallet = keyCurve === 'eddsa' ? tssSolWallet : tssEthWallet;
          let getKeysStub: sinon.SinonStub;
          let signTransactionStub: sinon.SinonStub;
          beforeEach(function () {
            getKeysStub = sandbox.stub(Keychains.prototype, 'getKeysForSigning');

            signTransactionStub = sandbox
              .stub(Wallet.prototype, 'signTransaction')
              .resolves({ ...txRequestFull, state: 'signed' });
          });

          afterEach(function () {
            sandbox.verifyAndRestore();
          });
          it('should sign transaction', async function () {
            getKeysStub.resolves([
              {
                commonKeychain: 'test',
                id: '',
                pub: '',
                type: 'tss',
                encryptedPrv:
                  '{"iv":"15FsbDVI1zG9OggD8YX+Hg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"hHbNH3Sz/aU=","ct":"WoNVKz7afiRxXI2w/YkzMdMyoQg/B15u1Q8aQgi96jJZ9wk6TIaSEc6bXFH3AHzD9MdJCWJQUpRhoQc/rgytcn69scPTjKeeyVMElGCxZdFVS/psQcNE+lue3//2Zlxj+6t1NkvYO+8yAezSMRBK5OdftXEjNQI="}',
              },
            ]);
            const params = {
              walletPassphrase: TestBitGo.V2.TEST_ETH_WALLET_PASSPHRASE as string,
              txRequestId: 'id',
            };

            const response = await wallet.getUserKeyAndSignTssTransaction(params);
            response.should.deepEqual({ ...txRequestFull, state: 'signed' });

            getKeysStub.calledOnce.should.be.true();
            signTransactionStub.calledOnce.should.be.true();
          });

          it('should throw if the keychain doesnt have the encryptedKey', async function () {
            getKeysStub.resolves([{ commonKeychain: 'test', id: '', pub: '', type: 'tss' }]);
            const params = {
              walletPassphrase: TestBitGo.V2.TEST_ETH_WALLET_PASSPHRASE as string,
              txRequestId: 'id',
            };

            await wallet
              .getUserKeyAndSignTssTransaction(params)
              .should.be.rejectedWith('the user keychain does not have property encryptedPrv');

            getKeysStub.calledOnce.should.be.true();
            signTransactionStub.notCalled.should.be.true();
          });

          it('should throw if password is invalid', async function () {
            getKeysStub.resolves([
              {
                commonKeychain: 'test',
                id: '',
                pub: '',
                type: 'tss',
                encryptedPrv:
                  '{"iv":"15FsbDVI1zG9OggD8YX+Hg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"hHbNH3Sz/aU=","ct":"WoNVKz7afiRxXI2w/YkzMdMyoQg/B15u1Q8aQgi96jJZ9wk6TIaSEc6bXFH3AHzD9MdJCWJQUpRhoQc/rgytcn69scPTjKeeyVMElGCxZdFVS/psQcNE+lue3//2Zlxj+6t1NkvYO+8yAezSMRBK5OdftXEjNQI="}',
              },
            ]);
            const params = {
              walletPassphrase: 'randompass',
              txRequestId: 'id',
            };

            await wallet
              .getUserKeyAndSignTssTransaction(params)
              .should.be.rejectedWith(`unable to decrypt keychain with the given wallet passphrase`);

            getKeysStub.calledOnce.should.be.true();
            signTransactionStub.notCalled.should.be.true();
          });
        });
      });
    });

    describe('signAndSendTxRequest', function () {
      const exampleSignedTx = {
        txHex: '0x123',
        txid: '0x456',
        status: 'signed',
      };

      afterEach(async function () {
        sandbox.restore();
      });

      it('should sign lite transaction', async function () {
        const getUserKeyAndSignTssTxSpy = sandbox.stub(tssSolWallet, 'getUserKeyAndSignTssTransaction');
        getUserKeyAndSignTssTxSpy.resolves(exampleSignedTx);
        const submitTxSpy = sandbox.stub(tssSolWallet, 'submitTransaction');
        submitTxSpy.resolves(exampleSignedTx);

        const signedTx = await tssSolWallet.signAndSendTxRequest({
          walletPassphrase: 'passphrase',
          txRequestId: 'id',
          isTxRequestFull: false,
        });

        sandbox.assert.calledOnce(getUserKeyAndSignTssTxSpy);
        sandbox.assert.calledOnce(submitTxSpy);
        signedTx.should.deepEqual(exampleSignedTx);
      });

      it('should sign full transaction', async function () {
        const deleteSignatureSharesSpy = sandbox.stub(TssUtils.prototype, 'deleteSignatureShares');
        const getUserKeyAndSignTssTxSpy = sandbox.stub(tssSolWallet, 'getUserKeyAndSignTssTransaction');
        getUserKeyAndSignTssTxSpy.resolves(exampleSignedTx);

        const signedTx = await tssSolWallet.signAndSendTxRequest({
          walletPassphrase: 'passphrase',
          txRequestId: 'id',
          isTxRequestFull: true,
        });

        sandbox.assert.calledOnce(deleteSignatureSharesSpy);
        sandbox.assert.calledOnce(getUserKeyAndSignTssTxSpy);
        signedTx.should.deepEqual(exampleSignedTx);
      });
    });

    describe('Message Signing', function () {
      const txHash = '0xrrrsss1b';
      const txRequestForMessageSigning: TxRequest = {
        txRequestId: reqId.toString(),
        transactions: [],
        intent: {
          intentType: 'signMessage',
        },
        date: new Date().toISOString(),
        latest: true,
        state: 'pendingUserSignature',
        userId: 'userId',
        walletType: 'hot',
        policiesChecked: false,
        version: 1,
        walletId: 'walletId',
        unsignedTxs: [],
        unsignedMessages: [],
        messages: [
          {
            state: 'signed',
            messageRaw: 'hello world',
            derivationPath: 'm/0',
            signatureShares: [{ from: SignatureShareType.USER, to: SignatureShareType.USER, share: '' }],
            combineSigShare: '0:rrr:sss:3',
            txHash,
          },
        ],
      };
      let signTxRequestForMessage;
      const messageSigningCoins = ['teth', 'tpolygon'];
      const messageRaw = 'test';
      const expected: SignedMessage = {
        txRequestId: reqId.toString(),
        txHash,
        signature: txHash,
        messageRaw,
        coin: 'teth',
        messageEncoded: Buffer.from('\u0019Ethereum Signed Message:\n4test').toString('hex'),
      };

      beforeEach(async function () {
        signTxRequestForMessage = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'signTxRequestForMessage');
        signTxRequestForMessage.resolves(txRequestForMessageSigning);
        sandbox
          .stub(Keychains.prototype, 'getKeysForSigning')
          .resolves([{ commonKeychain: 'test', id: '', pub: '', type: 'independent' }]);
        sinon.stub(Ecdsa.prototype, 'verify').resolves(true);
      });

      afterEach(async function () {
        sinon.restore();
        nock.cleanAll();
      });

      it('should throw error for unsupported coins', async function () {
        await tssSolWallet
          .signMessage({
            reqId,
            message: { messageRaw },
            prv: 'secretKey',
          })
          .should.be.rejectedWith('Message signing not supported for Testnet Solana');
      });

      messageSigningCoins.map((coinName) => {
        const expectedWithCoinField = { ...expected, coin: 'teth' };

        tssEthWallet = new Wallet(bitgo, bitgo.coin(coinName), ethWalletData);
        const txRequestId = txRequestForMessageSigning.txRequestId;

        it('should sign message', async function () {
          const signMessageTssSpy = sinon.spy(tssEthWallet, 'signMessageTss' as any);
          nock(bgUrl)
            .get(
              `/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${
                txRequestForMessageSigning.txRequestId
              }&latest=true`
            )
            .reply(200, { txRequests: [txRequestForMessageSigning] });

          const signMessage = await tssEthWallet.signMessage({
            reqId,
            message: { messageRaw, txRequestId },
            prv: 'secretKey',
          });
          signMessage.should.deepEqual(expectedWithCoinField);
          const actualArg = signMessageTssSpy.getCalls()[0].args[0] as WalletSignMessageOptions;
          actualArg.message?.messageEncoded?.should.equal(
            Buffer.from(`\u0019Ethereum Signed Message:\n${messageRaw.length}${messageRaw}`).toString('hex')
          );
        });

        it('should sign message when custodianMessageId is provided', async function () {
          const signMessageTssSpy = sinon.spy(tssEthWallet, 'signMessageTss' as any);
          nock(bgUrl).post(`/api/v2/wallet/${tssEthWallet.id()}/txrequests`).reply(200, txRequestForMessageSigning);

          const signMessage = await tssEthWallet.signMessage({
            custodianMessageId: 'unittest',
            reqId,
            message: { messageRaw },
            prv: 'secretKey',
          });
          signMessage.should.deepEqual(expectedWithCoinField);
          const actualArg = signMessageTssSpy.getCalls()[0].args[0] as WalletSignMessageOptions;
          actualArg.message?.messageEncoded?.should.equal(
            Buffer.from(`\u0019Ethereum Signed Message:\n${messageRaw.length}${messageRaw}`).toString('hex')
          );
        });

        it('should sign message when txRequestId not provided', async function () {
          const signMessageTssSpy = sinon.spy(tssEthWallet, 'signMessageTss' as any);
          nock(bgUrl).post(`/api/v2/wallet/${tssEthWallet.id()}/txrequests`).reply(200, txRequestForMessageSigning);

          const signMessage = await tssEthWallet.signMessage({
            reqId,
            message: { messageRaw },
            prv: 'secretKey',
          });
          signMessage.should.deepEqual(expectedWithCoinField);
          const actualArg = signMessageTssSpy.getCalls()[0].args[0] as WalletSignMessageOptions;
          actualArg.message?.messageEncoded?.should.equal(
            Buffer.from(`\u0019Ethereum Signed Message:\n${messageRaw.length}${messageRaw}`).toString('hex')
          );
        });

        it('should fail to sign message with empty prv', async function () {
          await tssEthWallet
            .signMessage({
              reqId,
              message: { messageRaw, txRequestId },
              prv: '',
            })
            .should.be.rejectedWith('keychain does not have property encryptedPrv');
        });
      });
    });

    describe('Typed Data Signing', function () {
      const txHash =
        '1901493fbf2ae1c27c3ced26a89070c6ab5d3fbf37ed778de9378e7703b7d1f116b3883077a61826129b98b622e54fc68c5008d1b1c16552e1eda6916f870d719220';
      const txRequestForTypedDataSigning: TxRequest = {
        txRequestId: reqId.toString(),
        transactions: [],
        intent: {
          intentType: 'signMessage',
        },
        date: new Date().toISOString(),
        latest: true,
        state: 'pendingUserSignature',
        userId: 'userId',
        walletType: 'hot',
        policiesChecked: false,
        version: 1,
        walletId: 'walletId',
        unsignedTxs: [],
        unsignedMessages: [],
        messages: [
          {
            state: 'signed',
            messageRaw: 'hello world',
            derivationPath: 'm/0',
            signatureShares: [{ from: SignatureShareType.USER, to: SignatureShareType.USER, share: '' }],
            combineSigShare: '0:rrr:sss:3',
            txHash,
          },
        ],
      };
      let signTxRequestForMessage;
      const messageSigningCoins = ['teth', 'tpolygon'];
      const types: MessageTypes = {
        EIP712Domain: [
          {
            name: 'name',
            type: 'string',
          },
          {
            name: 'version',
            type: 'string',
          },
          {
            name: 'chainId',
            type: 'uint256',
          },
          {
            name: 'verifyingContract',
            type: 'address',
          },
        ],
        Message: [{ name: 'data', type: 'string' }],
      };
      const typedMessage: TypedMessage<MessageTypes> = {
        domain: {
          name: 'bitgo',
          version: '1',
          chainId: 1,
          verifyingContract: '0x0000000000000000000000000000000000000000',
        },
        primaryType: 'Message',
        types,
        message: { data: 'bitgo says hello!' },
      };
      const typedDataBase: TypedData = {
        typedDataRaw: JSON.stringify(typedMessage),
        version: SignTypedDataVersion.V3,
      };

      beforeEach(async function () {
        signTxRequestForMessage = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'signTxRequestForMessage');
        signTxRequestForMessage.resolves(txRequestForTypedDataSigning);
        sandbox
          .stub(Keychains.prototype, 'getKeysForSigning')
          .resolves([{ commonKeychain: 'test', id: '', pub: '', type: 'independent' }]);
        sinon.stub(Ecdsa.prototype, 'verify').resolves(true);
      });

      afterEach(async function () {
        sinon.restore();
        nock.cleanAll();
      });

      it('should throw error for unsupported coins', async function () {
        await tssSolWallet
          .signTypedData({
            reqId,
            typedData: typedDataBase,
            prv: 'secretKey',
          })
          .should.be.rejectedWith('Sign typed data not supported for Testnet Solana');
      });

      it('should throw error for sign typed data V1', async function () {
        const typedData = { ...typedDataBase };
        typedData.version = SignTypedDataVersion.V1;
        nock(bgUrl)
          .get(
            `/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${
              txRequestForTypedDataSigning.txRequestId
            }&latest=true`
          )
          .reply(200, { txRequests: [txRequestForTypedDataSigning] });

        await tssEthWallet
          .signTypedData({
            reqId,
            typedData,
            prv: 'secretKey',
          })
          .should.be.rejectedWith('SignTypedData v1 is not supported due to security concerns');
      });
      messageSigningCoins.map((coinName) => {
        tssEthWallet = new Wallet(bitgo, bitgo.coin(coinName), ethWalletData);
        const txRequestId = txRequestForTypedDataSigning.txRequestId;
        typedDataBase.txRequestId = txRequestId;
        const expected: SignedMessage = {
          txRequestId,
          messageRaw: JSON.stringify(typedMessage),
          signature: txHash,
          txHash,
          coin: 'teth',
          messageEncoded: txHash,
        };

        describe(`sign typed data V3 for ${coinName}`, async function () {
          const typedData = { ...typedDataBase };
          typedData.version = SignTypedDataVersion.V3;

          it('should sign typed data V3', async function () {
            const signTypedDataTssSpy = sinon.spy(tssEthWallet, 'signTypedDataTss' as any);
            nock(bgUrl)
              .get(
                `/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${
                  txRequestForTypedDataSigning.txRequestId
                }&latest=true`
              )
              .reply(200, { txRequests: [txRequestForTypedDataSigning] });

            const signedTypedData = await tssEthWallet.signTypedData({
              reqId,
              typedData,
              prv: 'secretKey',
            });
            signedTypedData.should.deepEqual(expected);
            const actualArg = signTypedDataTssSpy.getCalls()[0].args[0] as WalletSignTypedDataOptions;
            actualArg.typedData?.typedDataEncoded?.toString('hex').should.equal(txHash);
          });

          it('should sign typed data V3 when custodianMessageID is provided', async function () {
            typedData.txRequestId = txRequestId;
            const signTypedDataTssSpy = sinon.spy(tssEthWallet, 'signTypedDataTss' as any);
            nock(bgUrl)
              .get(
                `/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${
                  txRequestForTypedDataSigning.txRequestId
                }&latest=true`
              )
              .reply(200, { txRequests: [txRequestForTypedDataSigning] });

            const signedTypedData = await tssEthWallet.signTypedData({
              custodianMessageId: 'unittest',
              reqId,
              typedData,
              prv: 'secretKey',
            });
            signedTypedData.should.deepEqual(expected);
            const actualArg = signTypedDataTssSpy.getCalls()[0].args[0] as WalletSignTypedDataOptions;
            actualArg.typedData?.typedDataEncoded?.toString('hex').should.equal(txHash);
          });

          it('should fail to sign typed data V3 with empty prv', async function () {
            await tssEthWallet
              .signTypedData({
                reqId,
                typedData: typedDataBase,
                prv: '',
              })
              .should.be.rejectedWith('keychain does not have property encryptedPrv');
          });

          it('should sign typed data V3 when txRequestId not provided ', async function () {
            delete typedData.txRequestId;
            const signedTypedDataTssSpy = sinon.spy(tssEthWallet, 'signTypedDataTss' as any);
            nock(bgUrl).post(`/api/v2/wallet/${tssEthWallet.id()}/txrequests`).reply(200, txRequestForTypedDataSigning);

            const signedTypedData = await tssEthWallet.signTypedData({
              reqId,
              typedData,
              prv: 'secretKey',
            });
            signedTypedData.should.deepEqual(expected);
            const actualArg = signedTypedDataTssSpy.getCalls()[0].args[0] as WalletSignTypedDataOptions;
            actualArg.typedData?.typedDataEncoded?.toString('hex').should.equal(txHash);
          });
        });

        describe(`sign typed data V4 for ${coinName}`, async function () {
          const typedData = { ...typedDataBase };
          typedData.version = SignTypedDataVersion.V4;
          it('should sign typed data V4', async function () {
            typedData.txRequestId = txRequestId;
            nock(bgUrl)
              .get(
                `/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${
                  txRequestForTypedDataSigning.txRequestId
                }&latest=true`
              )
              .reply(200, { txRequests: [txRequestForTypedDataSigning] });

            const signedTypedData = await tssEthWallet.signTypedData({
              reqId,
              typedData,
              prv: 'secretKey',
            });
            signedTypedData.should.deepEqual(expected);
          });

          it('should sign typed data V4 when custodianMessageID is provided', async function () {
            typedData.txRequestId = txRequestId;
            nock(bgUrl)
              .get(
                `/api/v2/wallet/${tssEthWallet.id()}/txrequests?txRequestIds=${
                  txRequestForTypedDataSigning.txRequestId
                }&latest=true`
              )
              .reply(200, { txRequests: [txRequestForTypedDataSigning] });

            const signedTypedData = await tssEthWallet.signTypedData({
              custodianMessageId: 'unittest',
              reqId,
              typedData,
              prv: 'secretKey',
            });
            signedTypedData.should.deepEqual(expected);
          });

          it('should fail to sign typed data V4 with empty prv', async function () {
            await tssEthWallet
              .signTypedData({
                reqId,
                typedData: typedDataBase,
                prv: '',
              })
              .should.be.rejectedWith('keychain does not have property encryptedPrv');
          });

          it('should sign typed data V4 when txRequestId not provided ', async function () {
            delete typedData.txRequestId;
            const signedTypedDataTssSpy = sinon.spy(tssEthWallet, 'signTypedDataTss' as any);
            nock(bgUrl).post(`/api/v2/wallet/${tssEthWallet.id()}/txrequests`).reply(200, txRequestForTypedDataSigning);

            const signedTypedData = await tssEthWallet.signTypedData({
              reqId,
              typedData,
              prv: 'secretKey',
            });
            signedTypedData.should.deepEqual(expected);
            const actualArg = signedTypedDataTssSpy.getCalls()[0].args[0] as WalletSignTypedDataOptions;
            actualArg.typedData?.typedDataEncoded?.toString('hex').should.equal(txHash);
          });
        });
      });
    });

    describe('Send Many', function () {
      const sendManyInput = {
        type: 'transfer',
        recipients: [
          {
            address: 'address',
            amount: '1000',
          },
        ],
        reqId: new RequestTracer(),
      };

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

      it('should send many', async function () {
        const signedTransaction = {
          txRequestId: 'txRequestId',
        };

        const prebuildAndSignTransaction = sandbox.stub(tssSolWallet, 'prebuildAndSignTransaction');
        prebuildAndSignTransaction.resolves(signedTransaction);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke sendMany
        prebuildAndSignTransaction.calledOnceWithExactly(sendManyInput);

        const sendTxRequest = sandbox.stub(TssUtils.prototype, 'sendTxRequest');
        sendTxRequest.resolves('sendTxResponse');
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke sendMany
        sendTxRequest.calledOnceWithExactly(signedTransaction.txRequestId);

        const sendMany = await tssSolWallet.sendMany(sendManyInput);
        sendMany.should.deepEqual('sendTxResponse');
      });

      it('should send many and call setRequestTracer', async function () {
        const signedTransaction = {
          txRequestId: 'txRequestId',
        };

        const prebuildAndSignTransaction = sandbox.stub(tssSolWallet, 'prebuildAndSignTransaction');
        prebuildAndSignTransaction.resolves(signedTransaction);
        prebuildAndSignTransaction.calledOnceWithExactly(sendManyInput);

        const sendTxRequest = sandbox.stub(TssUtils.prototype, 'sendTxRequest');
        sendTxRequest.resolves('sendTxResponse');
        sendTxRequest.calledOnceWithExactly(signedTransaction.txRequestId);

        const setRequestTracerSpy = sinon.spy(bitgo, 'setRequestTracer');
        setRequestTracerSpy.withArgs(sendManyInput.reqId);

        const sendMany = await tssSolWallet.sendMany(sendManyInput);
        sendMany.should.deepEqual('sendTxResponse');
        sinon.assert.calledOnce(setRequestTracerSpy);
        setRequestTracerSpy.restore();
      });

      it('should return transfer from sendMany for apiVersion=full', async function () {
        const wallet = new Wallet(bitgo, tsol, {
          ...walletData,
          type: 'custodial',
        });
        const signedTxResult = {
          txRequestId: 'txRequestId',
        };
        const txRequest: TxRequest = {
          date: new Date().toString(),
          intent: 'payment',
          latest: false,
          policiesChecked: false,
          state: 'delivered',
          unsignedTxs: [],
          userId: 'unit-test',
          version: 0,
          walletId: wallet.id(),
          walletType: wallet.type() ?? 'hot',
          txRequestId: signedTxResult.txRequestId,
          transactions: [
            {
              state: 'delivered',
              signedTx: {
                id: 'txid',
                tx: 'tx',
              },
              unsignedTx: 'something' as any,
              signatureShares: [],
            },
          ],
        };
        const transfer = {
          id: 'transferId',
          state: 'signed',
          txid: 'txid',
        };

        const prebuildAndSignTransaction = sandbox.stub(wallet, 'prebuildAndSignTransaction').resolves(signedTxResult);

        const txRequestNock = nock(bgUrl)
          .persist()
          .get(`/api/v2/wallet/${walletData.id}/txrequests?txRequestIds=${signedTxResult.txRequestId}&latest=true`)
          .reply(200, { txRequests: [txRequest] });

        const createTransferNock = nock(bgUrl)
          .persist()
          .post(`/api/v2/wallet/${walletData.id}/txrequests/${signedTxResult.txRequestId}/transfers`)
          .reply(200, transfer);

        const input: SendManyOptions = {
          type: 'transfer',
          recipients: [
            {
              address: 'address',
              amount: '1000',
            },
          ],
          apiVersion: 'full',
        };
        const sendManyResult = await wallet.sendMany(input);
        prebuildAndSignTransaction.calledOnceWithExactly(input);
        txRequestNock.isDone().should.be.true();
        createTransferNock.isDone().should.be.true();

        sendManyResult.should.deepEqual({
          txRequest,
          transfer,
          txid: 'txid',
          tx: 'tx',
          status: 'signed',
        });
      });

      it('should return pendingApproval from sendMany for apiVersion=full', async function () {
        const wallet = new Wallet(bitgo, tsol, {
          ...walletData,
          type: 'hot',
        });
        const signedTxResult = {
          txRequestId: 'txRequestId',
        };
        const txRequest: TxRequest = {
          txRequestId: signedTxResult.txRequestId,
          date: new Date().toString(),
          intent: 'payment',
          latest: false,
          policiesChecked: false,
          state: 'pendingApproval',
          unsignedTxs: [],
          userId: 'unit-test',
          version: 0,
          walletId: wallet.id(),
          walletType: wallet.type() ?? 'hot',
          pendingApprovalId: 'some-pending-approval-id',
          transactions: [
            {
              state: 'initialized',
              unsignedTx: 'something' as any,
              signatureShares: [],
            },
          ],
        };
        const transfer = {
          id: 'transferId',
          state: 'signed',
          txid: 'txid',
        };
        const pendingApproval = {
          id: 'some-pending-approval-id',
          wallet: wallet.id(),
          info: {
            type: 'transactionRequestFull',
          },
          txRequestId: txRequest.txRequestId,
        };

        const prebuildAndSignTransaction = sandbox.stub(wallet, 'prebuildAndSignTransaction').resolves(signedTxResult);

        const txRequestNock = nock(bgUrl)
          .persist()
          .get(`/api/v2/wallet/${walletData.id}/txrequests?txRequestIds=${txRequest.txRequestId}&latest=true`)
          .reply(200, { txRequests: [txRequest] });

        const createTransferNock = nock(bgUrl)
          .persist()
          .post(`/api/v2/wallet/${walletData.id}/txrequests/${txRequest.txRequestId}/transfers`)
          .reply(200, transfer);

        const getPendingApprovalNock = nock(bgUrl)
          .persist()
          .get(`/api/v2/${wallet.coin()}/pendingapprovals/${txRequest.pendingApprovalId}`)
          .reply(200, pendingApproval);

        const input: SendManyOptions = {
          type: 'transfer',
          recipients: [
            {
              address: 'address',
              amount: '1000',
            },
          ],
          apiVersion: 'full',
        };
        const sendManyResult = await wallet.sendMany(input);
        prebuildAndSignTransaction.calledOnceWithExactly(input);
        txRequestNock.isDone().should.be.true();
        createTransferNock.isDone().should.be.true();
        getPendingApprovalNock.isDone().should.be.true();

        sendManyResult.should.deepEqual({ pendingApproval, txRequest });
      });

      it('should fail if txRequestId is missing from prebuild', async function () {
        const signedTransaction = {
          txHex: 'deadbeef',
        };

        const prebuildAndSignTransaction = sandbox.stub(tssSolWallet, 'prebuildAndSignTransaction');
        prebuildAndSignTransaction.resolves(signedTransaction);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke sendMany
        prebuildAndSignTransaction.calledOnceWithExactly(sendManyInput);

        await tssSolWallet
          .sendMany(sendManyInput)
          .should.be.rejectedWith('txRequestId missing from signed transaction');
      });
    });

    describe('Submit transaction', function () {
      it('should submit transaction with txRequestId', async function () {
        const nockSendTx = nock(bgUrl)
          .persist(false)
          .post(tssSolWallet.url('/tx/send').replace(bgUrl, ''))
          .reply(200, { message: 'success' });

        const submittedTx = await tssSolWallet.submitTransaction({
          txRequestId: 'id',
        });
        submittedTx.should.deepEqual({ message: 'success' });
        nockSendTx.isDone().should.be.true();
      });

      it('should fail when txRequestId and txHex are both provided', async function () {
        await tssSolWallet
          .submitTransaction({
            txRequestId: 'id',
            txHex: 'beef',
          })
          .should.be.rejectedWith('must supply exactly one of txRequestId, txHex, or halfSigned');
      });

      it('should fail when txRequestId and halfSigned are both provided', async function () {
        await tssSolWallet
          .submitTransaction({
            txRequestId: 'id',
            halfSigned: {
              txHex: 'beef',
            },
          })
          .should.be.rejectedWith('must supply exactly one of txRequestId, txHex, or halfSigned');
      });

      it('should fail when txHex and halfSigned are both provided', async function () {
        await tssSolWallet
          .submitTransaction({
            txHex: 'beef',
            halfSigned: {
              txHex: 'beef',
            },
          })
          .should.be.rejectedWith('must supply either txHex or halfSigned, but not both');
      });
    });

    describe('Transfer tokens', function () {
      const recipients = [
        {
          address: '0x101c3928946b2e1d99759e8e5d34b5e94c1a8e2f',
          amount: '0',
          tokenData: {
            tokenName: 'erc721:bitgoerc721',
            tokenContractAddress: '0x8397b091514c1f7bebb9dea6ac267ea23b570605',
            tokenId: '38',
            tokenQuantity: '1',
            decimalPlaces: 0,
            tokenType: TokenType.ERC721,
          },
        },
      ];

      const feeOptions = {
        maxFeePerGas: 2000000000,
        maxPriorityFeePerGas: 1000000000,
      };

      it('calling prebuildxTransaction should execute prebuildTxWithIntent with proper params', async function () {
        const txRequestFullTokenTransfer = { ...txRequestFull, intent: 'transferToken' };
        const prebuildTxWithIntent = sandbox.stub(ECDSAUtils.EcdsaUtils.prototype, 'prebuildTxWithIntent');
        prebuildTxWithIntent.resolves(txRequestFullTokenTransfer);
        // TODO(BG-59686): this is not doing anything if we don't check the return value, we should also move this check to happen after we invoke prebuildTransaction
        prebuildTxWithIntent.calledOnceWithExactly(
          {
            reqId,
            recipients,
            intentType: 'transferToken',
            feeOptions,
          },
          'full'
        );

        const txPrebuild = await tssPolygonWallet.prebuildTransaction({
          isTss: true,
          recipients,
          type: 'transfertoken',
          walletPassphrase: 'passphrase12345',
          feeOptions,
        });

        txPrebuild.should.deepEqual({
          walletId: tssPolygonWallet.id(),
          wallet: tssPolygonWallet,
          txRequestId: 'id',
          txHex: 'ababcdcd',
          buildParams: {
            recipients,
            type: 'transfertoken',
          },
          feeInfo: {
            fee: 5000,
            feeString: '5000',
          },
        });
      });

      it('should populate intent with EVM-like params', async function () {
        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('tpolygon'));
        // @ts-expect-error only pass in params being tested
        const intent = mpcUtils.populateIntent(bitgo.coin('tpolygon'), {
          intentType: 'transferToken',
          recipients,
          feeOptions,
        });
        intent.should.have.property('feeOptions');
        intent.feeOptions!.should.have.property('maxFeePerGas', 2000000000);
        intent.feeOptions!.should.have.property('maxPriorityFeePerGas', 1000000000);
        intent.should.have.property('recipients');
        intent.recipients!.should.have.property('length', 1);
        intent.recipients![0].should.have.property('tokenData');
        intent.recipients![0].tokenData!.should.have.property('tokenQuantity', recipients[0].tokenData.tokenQuantity);
        intent.recipients![0].tokenData!.should.have.property('tokenType', recipients[0].tokenData.tokenType);
        intent.recipients![0].tokenData!.should.have.property('tokenName', recipients[0].tokenData.tokenName);
        intent.recipients![0].tokenData!.should.have.property(
          'tokenContractAddress',
          recipients[0].tokenData.tokenContractAddress
        );
        intent.recipients![0].tokenData!.should.have.property('tokenId', recipients[0].tokenData.tokenId);
        intent.recipients![0].tokenData!.should.have.property('decimalPlaces', recipients[0].tokenData.decimalPlaces);
      });

      it('should populate intent with calldata', async function () {
        const recipients = [
          {
            address: '0x101c3928946b2e1d99759e8e5d34b5e94c1a8e2f',
            amount: '0',
            data: '0x000011112222',
          },
        ];

        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('hteth'));
        // @ts-expect-error only pass in params being tested
        const intent = mpcUtils.populateIntent(bitgo.coin('hteth'), {
          intentType: 'payment',
          recipients,
          feeOptions,
        });

        intent.should.have.property('feeOptions');
        intent.feeOptions!.should.have.property('maxFeePerGas', 2000000000);
        intent.feeOptions!.should.have.property('maxPriorityFeePerGas', 1000000000);
        intent.should.have.property('recipients');
        intent.recipients!.should.have.property('length', 1);
        intent.recipients![0].data!.should.equal('0x000011112222');
      });

      it('should not populate intent with tokenData if certain params are undefined', async function () {
        const mpcUtils = new ECDSAUtils.EcdsaUtils(bitgo, bitgo.coin('tpolygon'));
        const recipients = [
          {
            address: '0x101c3928946b2e1d99759e8e5d34b5e94c1a8e2f',
            amount: '0',
            tokenData: {
              tokenName: 'erc721:bitgoerc721',
              tokenContractAddress: '0x8397b091514c1f7bebb9dea6ac267ea23b570605',
              tokenId: '38',
              tokenQuantity: '1',
              decimalPlaces: 0,
            },
          },
        ];
        let intent;
        try {
          intent = mpcUtils.populateIntent(bitgo.coin('tpolygon'), {
            intentType: 'transferToken',
            // @ts-expect-error only pass in params be tested for
            recipients,
            feeOptions,
          });
          intent.should.equal(undefined);
        } catch (e: any) {
          e.message.should.equal(
            'token type and quantity is required to request a transaction with intent to transfer a token'
          );
        }
      });
    });

    describe('Transfer NFTs', function () {
      it('should populate intent with NFT token details', async function () {
        const params: PrebuildTransactionWithIntentOptions = {
          reqId,
          intentType: 'payment',
          recipients: [
            {
              address: '0x9fef749050644625012a2c866973775e7123753b3eef0a1a4037453ac26d79bf',
              amount: '1',
              tokenData: {
                tokenType: TokenType.DIGITAL_ASSET,
                tokenQuantity: '1',
                tokenContractAddress: '0xbbc561fbfa5d105efd8dfb06ae3e7e5be46331165b99d518f094c701e40603b5',
                tokenId: '0x675b053d72c24dcc6bc7f38cdd45b4843cfb7af69a25ad21d002c376357e9d69',
              },
            },
          ],
        };

        const baseCoin = bitgo.coin('tapt');
        const mpcUtils = new EDDSAUtils.default(bitgo, baseCoin);
        const intent = mpcUtils.populateIntent(baseCoin, params);

        intent.should.have.property('intentType', 'payment');
        intent.recipients!.should.deepEqual([
          {
            address: {
              address: '0x9fef749050644625012a2c866973775e7123753b3eef0a1a4037453ac26d79bf',
            },
            amount: {
              value: '1',
              symbol: 'tapt:nftcollection1',
            },
            tokenData: {
              tokenType: 'Digital Asset',
              tokenQuantity: '1',
              tokenContractAddress: '0xbbc561fbfa5d105efd8dfb06ae3e7e5be46331165b99d518f094c701e40603b5',
              tokenId: '0x675b053d72c24dcc6bc7f38cdd45b4843cfb7af69a25ad21d002c376357e9d69',
              tokenName: 'tapt:nftcollection1',
            },
          },
        ]);
      });
    });

    describe('Wallet Sharing', function () {
      it('should use keychain pub to share tss wallet', async function () {
        const userId = '123';
        const email = 'shareto@sdktest.com';
        const permissions = 'view,spend';
        const toKeychain = utxoLib.bip32.fromSeed(Buffer.from('deadbeef02deadbeef02deadbeef02deadbeef02', 'hex'));
        const path = 'm/999999/1/1';
        const pubkey = toKeychain.derivePath(path).publicKey.toString('hex');
        const walletPassphrase = 'bitgo1234';

        const getSharingKeyNock = nock(bgUrl)
          .post('/api/v1/user/sharingkey', { email })
          .reply(200, { userId, pubkey, path });

        // commonPub + commonChaincode
        const commonKeychain = randomBytes(32).toString('hex') + randomBytes(32).toString('hex');
        const getKeyNock = nock(bgUrl)
          .get(`/api/v2/tsol/key/${tssSolWallet.keyIds()[0]}`)
          .reply(200, {
            id: tssSolWallet.keyIds()[0],
            commonKeychain: commonKeychain,
            source: 'user',
            encryptedPrv: bitgo.encrypt({ input: 'xprv1', password: walletPassphrase }),
            coinSpecific: {},
          });

        const stub = sinon.stub(tssSolWallet, 'createShare').callsFake(async (options) => {
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          options!.keychain!.pub!.should.not.be.undefined();
          // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
          options!.keychain!.pub!.should.equal(TssUtils.getPublicKeyFromCommonKeychain(commonKeychain));
          return undefined;
        });
        await tssSolWallet.shareWallet({ email, permissions, walletPassphrase });

        stub.calledOnce.should.be.true();
        getSharingKeyNock.isDone().should.be.True();
        getKeyNock.isDone().should.be.True();
      });
    });
  });

  describe('AVAX tests', function () {
    let bgUrl;
    let basecoin;
    let walletData;
    let wallet;

    before(async function () {
      nock.pendingMocks().should.be.empty();
      bgUrl = common.Environments[bitgo.getEnv()].uri;
      walletData = {
        id: '5b34252f1bf349930e34020a00000000',
        keys: [
          '5b3424f91bf349930e34017500000000',
          '5b3424f91bf349930e34017600000000',
          '5b3424f91bf349930e34017700000000',
        ],
        coinSpecific: {},
      };
    });

    it('should fetch cross-chain utxos', async function () {
      basecoin = bitgo.coin('tavaxp');
      walletData.coin = 'tavaxp';
      wallet = new Wallet(bitgo, basecoin, walletData);

      const params = { sourceChain: 'C' };
      const path = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/crossChainUnspents`;
      const scope = nock(bgUrl)
        .get(path)
        .query(params)
        .reply(200, {
          unspent: {
            outputID: 7,
            amount: '10000000',
            txid: 'V3UBZTQj364zNWqt8uMHD5NjxxX8T8qkbeZXURmjnVmLEqzab',
            threshold: 2,
            addresses: [
              'C-fuji199fluegrthqs4tvz40zajfrsx5m7dvy75ajfm6',
              'C-fuji1gk3m444893ynl0gfvxahjgw3vftnn8sptyd9g5',
              'C-fuji1ujfzjgwzfygl60qp2l8rmglg3lnm7w4059nca5',
            ],
            outputidx: '1111XiaYg',
            locktime: '0',
          },
          fromWallet: '635092fd4ff3316142df6e6b7a078b92',
          toWallet: '635092fd4ff3316142df6e891f6a7ee6',
          toAddress: '0x125c4451c870f753265b0b1af3cf6ab88ffe4657',
        });

      try {
        await wallet.fetchCrossChainUTXOs(params);
      } catch (e) {
        // test is successful if nock is consumed, HMAC errors expected
      }

      scope.isDone().should.be.True();
    });

    it('sendMany should work for C > P export with custodial wallet', async function () {
      basecoin = bitgo.coin('tavaxc');
      walletData.coin = 'tavaxc';
      walletData.type = 'custodial';
      wallet = new Wallet(bitgo, basecoin, walletData);

      const address =
        'P-fuji1e56pc4966qsevzhwgkym5l0jfma9llkqnrr4gh~P-fuji1kq05zm9nmlq8p3ld55k79dl3qay6c0e3atj56v~P-fuji1rp46z30qg457xc3dpffyxcgzpflxc85mhkjme3';
      const initiateTxParams = {
        recipients: [
          {
            amount: '10000000000000000', // 0.01 AVAX
            address,
          },
        ],
        hop: true,
        type: 'Export',
      };

      const initiateTxPath = `/api/v2/${wallet.coin()}/wallet/${wallet.id()}/tx/initiate`;
      let initiateTxBody;
      const response = nock(bgUrl)
        .post(initiateTxPath, (body) => {
          initiateTxBody = body;
          return true;
        })
        .reply(200);

      const feeEstimationPath = `/api/v2/${wallet.coin()}/tx/fee?hop=true&recipient=${address}&amount=10000000000000000&type=Export`;
      nock(bgUrl).get(feeEstimationPath).reply(200, {
        feeEstimate: '718750000000000',
        gasLimitEstimate: 500000,
      });

      try {
        await wallet.sendMany(initiateTxParams);
      } catch (e) {
        console.log(e);
        // test is successful if nock is consumed, HMAC errors expected
      }
      _.isMatch(initiateTxBody, {
        hopParams: {
          gasPriceMax: 7187500000,
          gasLimit: 500000,
        },
        type: 'Export',
        recipients: [
          {
            amount: '10000000000000000',
            address,
          },
        ],
      }).should.be.true();

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

  describe('NFT Tests', function () {
    let ethWallet: Wallet;

    before(async function () {
      const walletData = {
        id: '598f606cd8fc24710d2ebadb1d9459bb',
        coin: 'hteth',
        keys: [
          '598f606cd8fc24710d2ebad89dce86c2',
          '598f606cc8e43aef09fcb785221d9dd2',
          '5935d59cf660764331bafcade1855fd7',
        ],
        multisigType: 'onchain',
        coinSpecific: {
          baseAddress: '0xdf07117705a9f8dc4c2a78de66b7f1797dba9d4e',
        },
      };
      ethWallet = new Wallet(bitgo, bitgo.coin('hteth'), walletData);
    });

    afterEach(async function () {
      nock.cleanAll();
    });

    it('Should return all nfts in the wallet', async function () {
      const getTokenBalanceNock = nock(bgUrl)
        .get(`/api/v2/hteth/wallet/${ethWallet.id()}?allTokens=true`)
        .reply(200, {
          ...walletData,
          ...nftResponse,
        });
      const nfts = await ethWallet.getNftBalances();
      getTokenBalanceNock.isDone().should.be.true();

      nfts.should.length(5);
      nfts.should.containEql({
        type: 'ERC721',
        metadata: {
          name: 'terc721:bitgoerc721',
          tokenContractAddress: '0x8397b091514c1f7bebb9dea6ac267ea23b570605',
        },
        collections: {},
        balanceString: '0',
        confirmedBalanceString: '0',
        spendableBalanceString: '0',
        transferCount: 0,
      });
    });

    it('Should throw when attempting to transfer a nft collection not in the wallet', async function () {
      const getTokenBalanceNock = nock(bgUrl)
        .get(`/api/v2/hteth/wallet/${ethWallet.id()}?allTokens=true`)
        .reply(200, {
          ...walletData,
          ...nftResponse,
        });

      await ethWallet
        .sendNft(
          {
            walletPassphrase: '123abc',
            otp: '000000',
          },
          {
            tokenId: '123',
            type: 'ERC721',
            tokenContractAddress: '0x123badaddress',
            recipientAddress: '0xc15acc27ee41f266877c8f0c61df5bcbc7997df6',
          }
        )
        .should.be.rejectedWith('Collection not found for token contract 0x123badaddress');
      getTokenBalanceNock.isDone().should.be.true();
    });

    it('Should throw when attempting to transfer a ERC-721 nft not owned by the wallet', async function () {
      const getTokenBalanceNock = nock(bgUrl)
        .get(`/api/v2/hteth/wallet/${ethWallet.id()}?allTokens=true`)
        .reply(200, {
          ...walletData,
          ...nftResponse,
          ...unsupportedNftResponse,
        });

      await ethWallet
        .sendNft(
          {
            walletPassphrase: '123abc',
            otp: '000000',
          },
          {
            tokenId: '123',
            type: 'ERC721',
            tokenContractAddress: '0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b',
            recipientAddress: '0xc15acc27ee41f266877c8f0c61df5bcbc7997df6',
          }
        )
        .should.be.rejectedWith(
          'Token 123 not found in collection 0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b or does not have a spendable balance'
        );
      getTokenBalanceNock.isDone().should.be.true();
    });

    it('Should throw when attempting to transfer ERC-1155 tokens when the amount transferred is more than the spendable balance', async function () {
      const getTokenBalanceNock = nock(bgUrl)
        .get(`/api/v2/hteth/wallet/${ethWallet.id()}?allTokens=true`)
        .reply(200, {
          ...walletData,
          ...{
            unsupportedNfts: {
              '0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b': {
                type: 'ERC1155',
                collections: {
                  1186703: '9',
                  1186705: '1',
                  1294856: '1',
                  1294857: '1',
                  1294858: '1',
                  1294859: '1',
                  1294860: '1',
                },
                metadata: {
                  name: 'MultiFaucet NFT',
                  tokenContractAddress: '0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b',
                },
              },
            },
          },
        });

      await ethWallet
        .sendNft(
          {
            walletPassphrase: '123abc',
            otp: '000000',
          },
          {
            entries: [
              {
                amount: 10,
                tokenId: '1186703',
              },
              {
                amount: 1,
                tokenId: '1186705',
              },
            ],
            type: 'ERC1155',
            tokenContractAddress: '0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b',
            recipientAddress: '0xc15acc27ee41f266877c8f0c61df5bcbc7997df6',
          }
        )
        .should.be.rejectedWith('Amount 10 exceeds spendable balance of 9 for token 1186703');
      getTokenBalanceNock.isDone().should.be.true();
    });
  });

  describe('Ada tests: ', () => {
    let adaWallet: Wallet;
    const adaBitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
    adaBitgo.initializeTestVars();
    const walletData = {
      id: '598f606cd8fc24710d2ebadb1d9459bb',
      coinSpecific: {
        baseAddress:
          'addr_test1q9faa5q3zr38wkd4kd3u8jfshx97jxvwsyvjg3tac826502nmmgpzy8zwavmtvmrc0ynpwvtayvcaqgey3zhmsw44g7shrfrh9',
        pendingChainInitialization: false,
        minimumFunding: 1000000,
        lastChainIndex: { 0: 0 },
      },
      coin: 'tada',
      keys: [
        '598f606cd8fc24710d2ebad89dce86c2',
        '598f606cc8e43aef09fcb785221d9dd2',
        '5935d59cf660764331bafcade1855fd7',
      ],
      multisigType: 'tss',
    };

    before(async function () {
      adaWallet = new Wallet(bitgo, bitgo.coin('tada'), walletData);
      nock(bgUrl).get(`/api/v2/${adaWallet.coin()}/key/${adaWallet.keyIds()[0]}`).times(3).reply(200, {
        id: '598f606cd8fc24710d2ebad89dce86c2',
        pub: '5f8WmC2uW9SAk7LMX2r4G1Bx8MMwx8sdgpotyHGodiZo',
        source: 'user',
        encryptedPrv:
          '{"iv":"hNK3rg82P1T94MaueXFAbA==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"cV4wU4EzPjs=","ct":"9VZX99Ztsb6p75Cxl2lrcXBplmssIAQ9k7ZA81vdDYG4N5dZ36BQNWVfDoelj9O31XyJ+Xri0XKIWUzl0KKLfUERplmtNoOCn5ifJcZwCrOxpHZQe3AJ700o8Wmsrk5H"}',
        coinSpecific: {},
      });

      nock(bgUrl).get(`/api/v2/${adaWallet.coin()}/key/${adaWallet.keyIds()[1]}`).times(2).reply(200, {
        id: '598f606cc8e43aef09fcb785221d9dd2',
        pub: 'G1s43JTzNZzqhUn4aNpwgcc6wb9FUsZQD5JjffG6isyd',
        encryptedPrv:
          '{"iv":"UFrt/QlIUR1XeQafPBaAlw==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"7VPBYaJXPm8=","ct":"ajFKv2y8yaIBXQ39sAbBWcnbiEEzbjS4AoQtp5cXYqjeDRxt3aCxemPm22pnkJaCijFjJrMHbkmsNhNYzHg5aHFukN+nEAVssyNwHbzlhSnm8/BVN50yAdAAtWreh8cp"}',
        source: 'backup',
        coinSpecific: {},
      });

      nock(bgUrl).get(`/api/v2/${adaWallet.coin()}/key/${adaWallet.keyIds()[2]}`).times(2).reply(200, {
        id: '5935d59cf660764331bafcade1855fd7',
        pub: 'GH1LV1e9FdqGe8U2c8PMEcma3fDeh1ktcGVBrD3AuFqx',
        encryptedPrv:
          '{"iv":"iIuWOHIOErEDdiJn6g46mg==","v":1,"iter":10000,"ks":256,"ts":64,"mode":"ccm","adata":"","cipher":"aes","salt":"Rzh7RRJksj0=","ct":"rcNICUfp9FakT53l+adB6XKzS1vNTc0Qq9jAtqnxA+ScssiS4Q0l3sgG/0gDy5DaZKtXryKBDUvGsi7b/fYaFCUpAoZn/VZTOhOUN/mo7ZHb4OhOXL29YPPkiryAq9Cr"}',
        source: 'bitgo',
        coinSpecific: {},
      });
    });

    after(async function () {
      sinon.restore();
      nock.cleanAll();
    });

    it('Should send unspents in payment intent when using sendmany', async function () {
      const sendManyParams = {
        type: 'transfer',
        recipients: [
          {
            address: 'address',
            amount: '1000',
          },
        ],
        unspents: ['unspent1', 'unspent2'],
      };

      nock(bgUrl)
        .post(`/api/v2/wallet/${adaWallet.id()}/txrequests`)
        .reply((url, body: any) => {
          // validate that the populated intent has unspents
          body.intent.intentType.should.equal('payment');
          body.intent.unspents.should.deepEqual(['unspent1', 'unspent2']);

          return [
            200,
            {
              apiVersion: 'lite',
              unsignedTxs: [
                {
                  unsignedTx: {
                    serializedTxHex: 'serializedTxHex',
                    feeInfo: 'fee info',
                  },
                },
              ],
            },
          ];
        });

      // stub all steps after txrequest creation
      sinon.stub(adaWallet.baseCoin, 'verifyTransaction').resolves(true);
      sinon.stub(adaWallet, 'signTransaction').resolves({ txRequestId: 'txRequestId' });
      sinon.stub(BaseTssUtils.default.prototype, 'sendTxRequest').resolves('sendTxResponse');
      await adaWallet.sendMany(sendManyParams);
    });

    it('Should send senderAddress in payment intent when using sendmany', async function () {
      const sendManyParams = {
        type: 'transfer',
        recipients: [
          {
            address: 'address',
            amount: '1000',
          },
        ],
        senderAddress: 'senderAddr1',
      };

      nock(bgUrl)
        .post(`/api/v2/wallet/${adaWallet.id()}/txrequests`)
        .reply((url, body: nock.Body) => {
          const createTxRequestBody = body as CreateTxRequestBody;
          createTxRequestBody.intent.intentType.should.equal('payment');
          createTxRequestBody.intent.senderAddress?.should.equal('senderAddr1');

          return [
            200,
            {
              apiVersion: 'lite',
              unsignedTxs: [
                {
                  unsignedTx: {
                    serializedTxHex: 'serializedTxHex',
                    feeInfo: 'fee info',
                  },
                },
              ],
            },
          ];
        });

      sinon.stub(adaWallet.baseCoin, 'verifyTransaction').resolves(true);
      sinon.stub(adaWallet, 'signTransaction').resolves({ txRequestId: 'txRequestId' });
      sinon.stub(BaseTssUtils.default.prototype, 'sendTxRequest').resolves('sendTxResponse');
      await adaWallet.sendMany(sendManyParams).should.be.resolved();
    });
  });

  describe('ERC20 Token Approval', function () {
    let wallet;
    const walletId = '5b34252f1bf349930e34020a00000000';
    let topethCoin;
    const tokenName = 'topeth:terc18dp';
    const coin = 'topeth';

    beforeEach(function () {
      topethCoin = bitgo.coin('topeth');
      wallet = new Wallet(bitgo, topethCoin, {
        id: walletId,
        coin: coin,
        keys: ['keyid1', 'keyid2', 'keyid3'],
      });
    });

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

    it('should successfully approve a token', async function () {
      const walletPassphrase = 'password123';
      const expectedApprovalBuild = {
        txHex: '0x123456',
        feeInfo: {
          fee: '1000000000',
        },
      };

      const expectedSignedTx = {
        txHex: '0x123456signed',
      };

      const expectedSendResult = {
        txid: '0xabcdef',
        tx: '0x123456signed',
        status: 'signed',
      };

      // Mock the token approval build API
      const ethUrl = common.Environments[bitgo.getEnv()].uri;
      nock(ethUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);

      // Mock the getKeychainsAndValidatePassphrase method
      sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').resolves([
        {
          id: 'keyid1',
          pub: 'pub1',
          encryptedPrv: 'encryptedPrv',
        },
      ]);

      // Mock the sign transaction method
      sinon.stub(wallet, 'signTransaction').resolves(expectedSignedTx);

      // Mock the send transaction method
      sinon.stub(wallet, 'sendTransaction').resolves(expectedSendResult);

      const result = await wallet.approveErc20Token(walletPassphrase, tokenName);

      should.exist(result);
      result.should.deepEqual(expectedSendResult);

      // Verify the parameters passed to signTransaction
      const signParams = wallet.signTransaction.firstCall.args[0];
      signParams.should.have.property('txPrebuild', expectedApprovalBuild);
      signParams.should.have.property('keychain');
      signParams.should.have.property('walletPassphrase', walletPassphrase);
    });

    it('should handle token approval build API errors', async function () {
      const walletPassphrase = 'password123';
      const errorMessage = 'token approval build failed';

      // Mock the token approval build API to return an error
      nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).replyWithError(errorMessage);

      await wallet.approveErc20Token(walletPassphrase, tokenName).should.be.rejectedWith(errorMessage);
    });

    it('should handle wallet passphrase validation errors', async function () {
      const walletPassphrase = 'wrong-password';
      const expectedApprovalBuild = {
        txHex: '0x123456',
        feeInfo: {
          fee: '1000000000',
        },
      };

      // Mock the token approval build API
      nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);

      // Mock the getKeychainsAndValidatePassphrase method to throw an error
      const error = new Error('unable to decrypt keychain with the given wallet passphrase');
      error.name = 'wallet_passphrase_incorrect';
      sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').rejects(error);

      await wallet
        .approveErc20Token(walletPassphrase, tokenName)
        .should.be.rejectedWith('unable to decrypt keychain with the given wallet passphrase');
    });

    it('should handle signing errors', async function () {
      const walletPassphrase = 'password123';
      const expectedApprovalBuild = {
        txHex: '0x123456',
        feeInfo: {
          fee: '1000000000',
        },
      };
      const signingError = new Error('signing failed');

      // Mock the token approval build API
      nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);

      // Mock the getKeychainsAndValidatePassphrase method
      sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').resolves([
        {
          id: 'keyid1',
          pub: 'pub1',
          encryptedPrv: 'encryptedPrv',
        },
      ]);

      // Mock the sign transaction method to throw an error
      sinon.stub(wallet, 'signTransaction').rejects(signingError);

      await wallet.approveErc20Token(walletPassphrase, tokenName).should.be.rejectedWith(signingError);
    });

    it('should handle send transaction errors', async function () {
      const walletPassphrase = 'password123';
      const expectedApprovalBuild = {
        txHex: '0x123456',
        feeInfo: {
          fee: '1000000000',
        },
      };
      const expectedSignedTx = {
        txHex: '0x123456signed',
      };
      const sendError = new Error('send failed');

      // Mock the token approval build API
      nock(bgUrl).post(`/api/v2/${coin}/wallet/${walletId}/token/approval/build`).reply(200, expectedApprovalBuild);

      // Mock the getKeychainsAndValidatePassphrase method
      sinon.stub(wallet, 'getKeychainsAndValidatePassphrase').resolves([
        {
          id: 'keyid1',
          pub: 'pub1',
          encryptedPrv: 'encryptedPrv',
        },
      ]);

      // Mock the sign transaction method
      sinon.stub(wallet, 'signTransaction').resolves(expectedSignedTx);

      // Mock the send transaction method to throw an error
      sinon.stub(wallet, 'sendTransaction').rejects(sendError);

      await wallet.approveErc20Token(walletPassphrase, tokenName).should.be.rejectedWith(sendError);
    });
  });
});

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


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