PHP WebShell

Текущая директория: /opt/BitGoJS/modules/sdk-coin-sol/test/unit

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

import { BitGoAPI } from '@bitgo/sdk-api';
import { MPCSweepTxs, MPCTx, MPCTxs, TssUtils, TxRequest, Wallet } from '@bitgo/sdk-core';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { coins } from '@bitgo/statics';
import assert from 'assert';
import * as _ from 'lodash';
import * as should from 'should';
import * as sinon from 'sinon';
import { KeyPair, Sol, Tsol } from '../../src';
import { Transaction } from '../../src/lib';
import { AtaInit, InstructionParams, TokenTransfer } from '../../src/lib/iface';
import { getAssociatedTokenAccountAddress } from '../../src/lib/utils';
import * as testData from '../fixtures/sol';
import * as resources from '../resources/sol';
import { getBuilderFactory } from './getBuilderFactory';

describe('SOL:', function () {
  let bitgo: TestBitGoAPI;
  let basecoin: Sol;
  let keyPair;
  let newTxPrebuild;
  let newTxPrebuildTokenTransfer;
  let newTxParams;
  let newTxParamsWithError;
  let newTxParamsWithExtraData;
  let newTxParamsTokenTransfer;
  const badAddresses = resources.addresses.invalidAddresses;
  const goodAddresses = resources.addresses.validAddresses;

  const keypair = {
    pub: resources.accountWithSeed.publicKey,
    prv: resources.accountWithSeed.privateKey.base58,
  };
  const txPrebuild = {
    recipients: [
      {
        address: 'lionteste212',
        amount: '1000',
      },
    ],
    txBase64: resources.TRANSFER_UNSIGNED_TX_WITH_MEMO_AND_DURABLE_NONCE,
    txInfo: {
      feePayer: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe',
      nonce: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
    },
    txid: '586c5b59b10b134d04c16ac1b273fe3c5529f34aef75db4456cd469c5cdac7e2',
    isVotingTransaction: false,
    coin: 'tsol',
  };
  const txParams = {
    txPrebuild,
    recipients: [
      {
        address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
        amount: '300000',
      },
    ],
  };
  const memo = { value: 'test memo' };
  const durableNonce = {
    walletNonceAddress: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
    authWalletAddress: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe',
  };
  const errorDurableNonce = {
    walletNonceAddress: '8YM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
    authWalletAddress: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe',
  };
  const txParamsWithError = {
    txPrebuild,
    recipients: [
      {
        address: 'CP5Dpaa42mMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
        amount: '300000',
      },
    ],
  };
  const txParamsWithExtraData = {
    txPrebuild,
    recipients: [
      {
        address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
        amount: '300000',
        data: undefined,
      },
    ],
  };
  const txPrebuildTokenTransfer = {
    recipients: [
      {
        address: 'AF5H6vBkFnJuVqChRPgPQ4JRcQ5Gk25HBFhQQkyojmvg',
        amount: '1',
      },
    ],
    txHex: resources.TOKEN_TRANSFER_TO_NATIVE_UNSIGNED_TX_HEX,
    txInfo: {
      feePayer: '4DujymUFbQ8GBKtAwAZrQ6QqpvtBEivL48h4ta2oJGd2',
      nonce: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
    },
    txid: '586c5b59b10b134d04c16ac1b273fe3c5529f34aef75db4456cd469c5cdac7e2',
    isVotingTransaction: false,
    coin: 'tsol',
  };
  const txParamsTokenTransfer = {
    txPrebuild,
    recipients: [
      {
        address: 'AF5H6vBkFnJuVqChRPgPQ4JRcQ5Gk25HBFhQQkyojmvg',
        amount: '1',
      },
    ],
  };
  const errorMemo = { value: 'different memo' };
  const errorFeePayer = '5hr5fisPi6DXCuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe';
  const factory = getBuilderFactory('tsol');
  const wallet = new KeyPair(resources.authAccount).getKeys();
  const stakeAccount = new KeyPair(resources.stakeAccount).getKeys();
  const blockHash = resources.blockHashes.validBlockHashes[0];
  const amount = '10000';
  const validator = resources.validator;

  before(function () {
    bitgo = TestBitGo.decorate(BitGoAPI, { env: 'mock' });
    bitgo.safeRegister('sol', Tsol.createInstance);
    bitgo.safeRegister('tsol', Tsol.createInstance);
    bitgo.initializeTestVars();
    basecoin = bitgo.coin('tsol') as Tsol;
    keyPair = basecoin.generateKeyPair(resources.accountWithSeed.seed);
    newTxPrebuild = () => {
      return _.cloneDeep(txPrebuild);
    };
    newTxPrebuildTokenTransfer = () => {
      return _.cloneDeep(txPrebuildTokenTransfer);
    };
    newTxParams = () => {
      return _.cloneDeep(txParams);
    };
    newTxParamsWithError = () => {
      return _.cloneDeep(txParamsWithError);
    };
    newTxParamsWithExtraData = () => {
      return _.cloneDeep(txParamsWithExtraData);
    };
    newTxParamsTokenTransfer = () => {
      return _.cloneDeep(txParamsTokenTransfer);
    };
  });

  it('should instantiate the coin', async function () {
    let localBasecoin = bitgo.coin('tsol');
    localBasecoin.should.be.an.instanceof(Tsol);

    localBasecoin = bitgo.coin('sol');
    localBasecoin.should.be.an.instanceof(Sol);
  });

  it('should retun the right info', function () {
    basecoin.getChain().should.equal('tsol');
    basecoin.getFamily().should.equal('sol');
    basecoin.getFullName().should.equal('Testnet Solana');
    basecoin.getBaseFactor().should.equal(1000000000);
  });
  describe('verify transactions', () => {
    const walletData = {
      id: '5b34252f1bf349930e34020a00000000',
      coin: 'tsol',
      keys: [
        '5b3424f91bf349930e34017500000000',
        '5b3424f91bf349930e34017600000000',
        '5b3424f91bf349930e34017700000000',
      ],
      coinSpecific: {
        rootAddress: wallet.pub,
      },
      multisigType: 'tss',
    };
    const walletObj = new Wallet(bitgo, basecoin, walletData);

    it('should verify transactions', async function () {
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        durableNonce,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should verify consolidate transaction', async function () {
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      txPrebuild.consolidateId = 'consolidateId';

      const walletData = {
        id: '5b34252f1bf349930e34020a00000000',
        coin: 'tsol',
        keys: [
          '5b3424f91bf349930e34017500000000',
          '5b3424f91bf349930e34017600000000',
          '5b3424f91bf349930e34017700000000',
        ],
        coinSpecific: {
          rootAddress: stakeAccount.pub,
        },
        multisigType: 'tss',
      };
      const walletWithDifferentAddress = new Wallet(bitgo, basecoin, walletData);

      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        durableNonce,
        wallet: walletWithDifferentAddress,
      } as any);
      validTransaction.should.be.true();
    });

    it('should handle txBase64 and txHex interchangeably', async function () {
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      txPrebuild.txHex = txPrebuild.txBase64;
      txPrebuild.txBase64 = undefined;
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        durableNonce,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should convert serialized hex string to base64', async function () {
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      txPrebuild.txBase64 = Buffer.from(txPrebuild.txBase64, 'base64').toString('hex');
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        durableNonce,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should verify when input `recipients` is absent', async function () {
      const txParams = newTxParams();
      txParams.recipients = undefined;
      const txPrebuild = newTxPrebuild();
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        durableNonce,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should fail verify transactions when have different memo', async function () {
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      await basecoin
        .verifyTransaction({ txParams, txPrebuild, memo: errorMemo, wallet: walletObj } as any)
        .should.be.rejectedWith('Tx memo does not match with expected txParams recipient memo');
    });

    it('should pass if we pass PDA address', async function () {
      const walletData = {
        id: '67f8ddff4c9b8b57a2e16acffac9a3b5',
        coin: 'tsol',
        keys: [
          '5b3424f91bf349930e34017500000000',
          '5b3424f91bf349930e34017600000000',
          '5b3424f91bf349930e34017700000000',
        ],
        coinSpecific: {
          rootAddress: '8zbsJA5c8HPR7BPjZkrSVrus2uMuXqCfzksGwB3Uscjb',
        },
        multisigType: 'tss',
      };
      const walletObj = new Wallet(bitgo, basecoin, walletData);
      const txPrebuild = {
        recipients: [
          {
            address: '11111111111111111111111111111112',
            amount: '1000000000',
            tokenName: 'tsol:usdc',
          },
        ],
        txBase64:
          '02000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000006ec1adcc89bb564f1f8225821140a9723efa80e8d506765770b7e201d66d8200d4f690e9a8163291b69f8c3827aad96cfd2105eee3aae76cbca38fcad2bf7f0a0201070c76c356cb069b66c2b35a8638b4d4afca75b303f29f0deeb4bff8528299a9c9d21c96172044f1217c3784e8f02f49e2c8fc3591e81294ab54394f9d22fd7b7a8f60129e6ecb20309c27dcba5fc6c441438d33a1568004a1860e22c16f071976a7d2e2008bd34b53a08aa9c8ec04eb2196745fc6029224447417e2fb0fced601240cabba4ce534c02fc154ba559ed2a02ac971e3385acb426ff63bb1040e2c2435000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000018c97258f4e2489f1bb3d1029148e0d830b5a1399daff1084048e7bd8dbe9f859d10389fbcee528f208611dccc734b31092540cb2b8d58d100f2eaa2cedb4da5e06a7d517192c568ee08a845f73d29788cf035c3145b21ab344d8062ea940000006a7d517192c5c51218cc94c3d4af17f58daee089ba1fd44e3dbd98a0000000006ddf6e1d765a193d9cbe146ceeb79ac1cb485ed5f5b37913a8cf5857eff00a9e680634533882f880a3e7dfa999dfb864b88968d242a0c9a90b5df149e42da050305030209010404000000070700030608050b0a000b04040803000a0c00ca9a3b0000000009',
        txInfo: {
          feePayer: '8zbsJA5c8HPR7BPjZkrSVrus2uMuXqCfzksGwB3Uscjb',
          nonce: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
        },
        txid: '586c5b59b10b134d04c16ac1b273fe3c5529f34aef75db4456cd469c5cdac7e2',
        isVotingTransaction: false,
        coin: 'tsol',
      };
      const txParams = {
        txPrebuild,
        recipients: [
          {
            address: '11111111111111111111111111111112',
            amount: '1000000000',
            tokenName: 'tsol:usdc',
          },
        ],
      };
      const memo = {
        value: undefined,
      };
      const verifyTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo: memo,
        wallet: walletObj,
      } as any);
      verifyTransaction.should.equal(true);
    });

    it('should fail verify transactions when have different durableNonce', async function () {
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      await basecoin
        .verifyTransaction({ txParams, txPrebuild, memo, durableNonce: errorDurableNonce, wallet: walletObj } as any)
        .should.be.rejectedWith('Tx durableNonce does not match with param durableNonce');
    });

    it('should fail verify transactions when have different feePayer', async function () {
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      const walletData = {
        id: '5b34252f1bf349930e34020a00000000',
        coin: 'tsol',
        keys: [
          '5b3424f91bf349930e34017500000000',
          '5b3424f91bf349930e34017600000000',
          '5b3424f91bf349930e34017700000000',
        ],
        coinSpecific: {
          rootAddress: stakeAccount.pub,
        },
        multisigType: 'tss',
      };
      const walletWithDifferentAddress = new Wallet(bitgo, basecoin, walletData);

      await basecoin
        .verifyTransaction({ txParams, txPrebuild, memo, wallet: walletWithDifferentAddress } as any)
        .should.be.rejectedWith('Tx fee payer is not the wallet root address');
    });

    it('should fail verify transactions when have different recipients', async function () {
      const txParams = newTxParamsWithError();
      const txPrebuild = newTxPrebuild();
      await basecoin
        .verifyTransaction({ txParams, txPrebuild, memo, errorFeePayer, wallet: walletObj } as any)
        .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
    });

    it('should succeed to verify token transaction with native address recipient', async function () {
      const txParams = newTxParamsTokenTransfer();
      const address = 'AF5H6vBkFnJuVqChRPgPQ4JRcQ5Gk25HBFhQQkyojmvg'; // Native SOL address
      txParams.recipients = [{ address, amount: '1', tokenName: 'tsol:usdc' }];
      const txPrebuild = newTxPrebuildTokenTransfer();
      const feePayerWalletData = {
        id: '5b34252f1bf349930e34020a00000000',
        coin: 'tsol',
        keys: [
          '5b3424f91bf349930e34017500000000',
          '5b3424f91bf349930e34017600000000',
          '5b3424f91bf349930e34017700000000',
        ],
        coinSpecific: {
          rootAddress: '4DujymUFbQ8GBKtAwAZrQ6QqpvtBEivL48h4ta2oJGd2',
        },
        multisigType: 'tss',
      };
      const feePayerWallet = new Wallet(bitgo, basecoin, feePayerWalletData);
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        wallet: feePayerWallet,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should succeed to verify token transaction with leading zero recipient amount', async function () {
      const txParams = newTxParamsTokenTransfer();
      const address = 'AF5H6vBkFnJuVqChRPgPQ4JRcQ5Gk25HBFhQQkyojmvg'; // Native SOL address
      txParams.recipients = [{ address, amount: '0001', tokenName: 'tsol:usdc' }];
      const txPrebuild = newTxPrebuildTokenTransfer();
      const feePayerWalletData = {
        id: '5b34252f1bf349930e34020a00000000',
        coin: 'tsol',
        keys: [
          '5b3424f91bf349930e34017500000000',
          '5b3424f91bf349930e34017600000000',
          '5b3424f91bf349930e34017700000000',
        ],
        coinSpecific: {
          rootAddress: '4DujymUFbQ8GBKtAwAZrQ6QqpvtBEivL48h4ta2oJGd2',
        },
        multisigType: 'tss',
      };
      const feePayerWallet = new Wallet(bitgo, basecoin, feePayerWalletData);
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        wallet: feePayerWallet,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should fail to verify token transaction with different recipient tokenName', async function () {
      const txParams = newTxParamsTokenTransfer();
      const address = 'AF5H6vBkFnJuVqChRPgPQ4JRcQ5Gk25HBFhQQkyojmvg'; // Native SOL address
      txParams.recipients = [{ address, amount: '1', tokenName: 'tsol:usdt' }]; // Different tokenName, should fail to verify tx
      const txPrebuild = newTxPrebuildTokenTransfer();
      await basecoin
        .verifyTransaction({
          txParams,
          txPrebuild,
          wallet: walletObj,
        } as any)
        .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
    });

    it('should fail to verify token transaction with different recipient amounts', async function () {
      const txParams = newTxParamsTokenTransfer();
      const address = 'AF5H6vBkFnJuVqChRPgPQ4JRcQ5Gk25HBFhQQkyojmvg'; // Native SOL address
      txParams.recipients = [{ address, amount: '2', tokenName: 'tsol:usdt' }];
      const txPrebuild = newTxPrebuildTokenTransfer();
      await basecoin
        .verifyTransaction({
          txParams,
          txPrebuild,
          wallet: walletObj,
        } as any)
        .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
    });

    it('should fail to verify token transaction with different native address', async function () {
      const txParams = newTxParamsTokenTransfer();
      const address = 'AF5H6vBkFnJuVqChRPgPQ4JRcQ5Gk25HBFhQQkyojmvX'; // Native SOL address, different than tx recipients
      txParams.recipients = [{ address, amount: '1', tokenName: 'tsol:usdc' }];
      const txPrebuild = newTxPrebuildTokenTransfer();
      await basecoin
        .verifyTransaction({
          txParams,
          txPrebuild,
          wallet: walletObj,
        } as any)
        .should.be.rejectedWith('Tx outputs does not match with expected txParams recipients');
    });

    it('should succeed to verify transactions when recipients has extra data', async function () {
      const txParams = newTxParamsWithExtraData();
      const txPrebuild = newTxPrebuild();
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        durableNonce,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should verify activate staking transaction', async function () {
      const tx = await factory
        .getStakingActivateBuilder()
        .stakingAddress(stakeAccount.pub)
        .sender(wallet.pub)
        .nonce(blockHash)
        .amount(amount)
        .validator(validator.pub)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      txPrebuild.txBase64 = txToBroadcastFormat;
      txPrebuild.txInfo.nonce = '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen';
      txParams.recipients = [
        {
          address: '7dRuGFbU2y2kijP6o1LYNzVyz4yf13MooqoionCzv5Za',
          amount: amount,
        },
      ];
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should verify withdraw staking transaction', async function () {
      const tx = await factory
        .getStakingWithdrawBuilder()
        .stakingAddress(stakeAccount.pub)
        .sender(wallet.pub)
        .nonce(blockHash)
        .amount(amount)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      txPrebuild.txBase64 = txToBroadcastFormat;
      txPrebuild.txInfo.nonce = '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen';
      txParams.recipients = [
        {
          address: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe',
          amount: amount,
        },
      ];
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should verify deactivate staking transaction', async function () {
      const tx = await factory
        .getStakingDeactivateBuilder()
        .stakingAddress(stakeAccount.pub)
        .sender(wallet.pub)
        .nonce(blockHash)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      txPrebuild.txBase64 = txToBroadcastFormat;
      txPrebuild.txInfo.nonce = '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen';
      txParams.recipients = [];
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });

    it('should verify create associated token account transaction', async function () {
      const tx = await factory
        .getAtaInitializationBuilder()
        .mint('tsol:usdc')
        .sender(wallet.pub)
        .nonce(blockHash)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const txParams = newTxParams();
      const txPrebuild = newTxPrebuild();
      txPrebuild.txBase64 = txToBroadcastFormat;
      txPrebuild.txInfo.nonce = blockHash;
      txParams.recipients = [];
      const validTransaction = await basecoin.verifyTransaction({
        txParams,
        txPrebuild,
        memo,
        wallet: walletObj,
      } as any);
      validTransaction.should.equal(true);
    });
  });

  it('should accept valid address', function () {
    goodAddresses.forEach((addr) => {
      basecoin.isValidAddress(addr).should.equal(true);
    });
  });

  it('should reject invalid address', function () {
    badAddresses.forEach((addr) => {
      basecoin.isValidAddress(addr).should.equal(false);
    });
  });

  it('should check valid pub keys', function () {
    keyPair.should.have.property('pub');
    basecoin.isValidPub(keyPair.pub).should.equal(true);
  });

  it('should check an invalid pub keys', function () {
    const badPubKey = keyPair.pub.slice(0, keyPair.pub.length - 1) + '-';
    basecoin.isValidPub(badPubKey).should.equal(false);
  });

  it('should check valid prv keys', function () {
    keyPair.should.have.property('prv');
    basecoin.isValidPrv(keyPair.prv).should.equal(true);
  });

  it('should check an invalid prv keys', function () {
    const badPrvKey = keyPair.prv ? keyPair.prv.slice(0, keyPair.prv.length - 1) + '-' : undefined;
    basecoin.isValidPrv(badPrvKey as string).should.equal(false);
  });

  describe('Parse Transactions:', () => {
    it('should parse an unsigned transfer transaction', async function () {
      const parsedTransaction = await basecoin.parseTransaction({
        txBase64: testData.rawTransactions.transfer.unsigned,
        feeInfo: {
          fee: '5000',
        },
      });

      parsedTransaction.should.deepEqual({
        inputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: 305000,
          },
        ],
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
          },
        ],
      });
    });

    it('should parse a signed transfer transaction', async function () {
      const parsedTransaction = await basecoin.parseTransaction({
        txBase64: testData.rawTransactions.transfer.signed,
        feeInfo: {
          fee: '5000',
        },
      });

      parsedTransaction.should.deepEqual({
        inputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: 305000,
          },
        ],
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
          },
        ],
      });
    });

    it('should parse an unsigned wallet init transaction', async function () {
      const parsedTransaction = await basecoin.parseTransaction({
        txBase64: testData.rawTransactions.walletInit.unsigned,
        feeInfo: {
          fee: '5000',
        },
      });

      parsedTransaction.should.deepEqual({
        inputs: [
          {
            address: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
            amount: 310000,
          },
        ],
        outputs: [
          {
            address: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
            amount: '300000',
          },
        ],
      });
    });

    it('should parse a signed wallet init transaction', async function () {
      const parsedTransaction = await basecoin.parseTransaction({
        txBase64: testData.rawTransactions.walletInit.signed,
        feeInfo: {
          fee: '5000',
        },
      });

      parsedTransaction.should.deepEqual({
        inputs: [
          {
            address: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
            amount: 310000,
          },
        ],
        outputs: [
          {
            address: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
            amount: '300000',
          },
        ],
      });
    });

    it('should parse an unsigned transfer token transaction', async function () {
      const parsedTransaction = await basecoin.parseTransaction({
        txBase64: testData.rawTransactions.transferToken.unsigned,
        feeInfo: {
          fee: '5000',
        },
      });

      parsedTransaction.should.deepEqual({
        inputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: 5000,
          },
        ],
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
            tokenName: 'tsol:usdc',
          },
        ],
      });
    });

    it('should parse a signed transfer token transaction', async function () {
      const parsedTransaction = await basecoin.parseTransaction({
        txBase64: testData.rawTransactions.transferToken.signed,
        feeInfo: {
          fee: '5000',
        },
      });

      parsedTransaction.should.deepEqual({
        inputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: 5000,
          },
        ],
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
            tokenName: 'tsol:usdc',
          },
        ],
      });
    });
  });

  describe('Explain Transactions:', () => {
    it('should explain an unsigned transfer transaction', async function () {
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: testData.rawTransactions.transfer.unsigned,
        feeInfo: {
          fee: '5000',
        },
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'Send',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '300000',
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
          },
        ],
        fee: {
          fee: '5000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
        durableNonce: {
          authWalletAddress: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe',
          walletNonceAddress: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
        },
      });
    });

    it('should explain a signed transfer transaction', async function () {
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: testData.rawTransactions.transfer.signed,
        feeInfo: {
          fee: '5000',
        },
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: '2XFxGfXddKWnqGaMAsfNL8HgXqDvjBL2Ae28KWrRvg9bQBmCrpHYVDacuZFeAUyYwjXG6ey2jTARX5VQCnj7SF4L',
        type: 'Send',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '300000',
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
          },
        ],
        fee: {
          fee: '5000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
        durableNonce: {
          authWalletAddress: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe',
          walletNonceAddress: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
        },
      });
    });

    it('should explain an unsigned wallet init transaction', async function () {
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: testData.rawTransactions.walletInit.unsigned,
        feeInfo: {
          fee: '5000',
        },
      });

      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'WalletInitialization',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '300000',
        outputs: [
          {
            address: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
            amount: '300000',
          },
        ],
        fee: {
          fee: '10000',
          feeRate: 5000,
        },
        blockhash: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
        durableNonce: undefined,
        memo: undefined,
      });
    });

    it('should explain a signed wallet init transaction', async function () {
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: testData.rawTransactions.walletInit.signed,
        feeInfo: {
          fee: '5000',
        },
      });

      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: '7TkU8wLgXDeLFbVydtg6mqMsp9GatsetitSngysgjxFhofKSUcLPBoKPHciLeGEfJFMsqezpZmGRSFQTBy7ZDsg',
        type: 'WalletInitialization',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '300000',
        outputs: [
          {
            address: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
            amount: '300000',
          },
        ],
        fee: {
          fee: '10000',
          feeRate: 5000,
        },
        blockhash: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
        durableNonce: undefined,
        memo: undefined,
      });
    });

    it('should explain an unsigned token transfer transaction', async function () {
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: testData.rawTransactions.transferToken.unsigned,
        feeInfo: {
          fee: '5000',
        },
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'Send',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '0',
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
            tokenName: 'tsol:usdc',
          },
        ],
        fee: {
          fee: '5000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
        durableNonce: {
          authWalletAddress: '12f6D3WubGVeQoH2m8kTvvcrasWdXWwtVzUCyRNDZxA2',
          walletNonceAddress: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
        },
      });
    });

    it('should explain a signed token transfer transaction', async function () {
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: testData.rawTransactions.transferToken.signed,
        feeInfo: {
          fee: '5000',
        },
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: '2ticU4ZkEqdTHULr6LobTgWBhim6E7wSscDhM4gzyuGUmQyUwLYhoqaifuvwmNzzEf1T5aefVcgMQkSHdJ5nsrfZ',
        type: 'Send',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '0',
        outputs: [
          {
            address: 'CP5Dpaa42RtJmMuKqCQsLwma5Yh3knuvKsYDFX85F41S',
            amount: '300000',
            tokenName: 'tsol:usdc',
          },
        ],
        fee: {
          fee: '5000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: 'GHtXQBsoZHVnNFa9YevAzFr17DJjgHXk3ycTKD5xD3Zi',
        durableNonce: {
          authWalletAddress: '12f6D3WubGVeQoH2m8kTvvcrasWdXWwtVzUCyRNDZxA2',
          walletNonceAddress: '8Y7RM6JfcX4ASSNBkrkrmSbRu431YVi9Y3oLFnzC2dCh',
        },
      });
    });

    it('should explain activate staking transaction', async function () {
      const tx = await factory
        .getStakingActivateBuilder()
        .stakingAddress(stakeAccount.pub)
        .sender(wallet.pub)
        .nonce(blockHash)
        .amount(amount)
        .validator(validator.pub)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: txToBroadcastFormat,
        feeInfo: {
          fee: '5000',
        },
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'StakingActivate',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '10000',
        outputs: [
          {
            address: '7dRuGFbU2y2kijP6o1LYNzVyz4yf13MooqoionCzv5Za',
            amount: '10000',
          },
        ],
        fee: {
          fee: '10000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen',
        durableNonce: undefined,
      });
    });

    it('should explain deactivate staking transaction', async function () {
      const tx = await factory
        .getStakingDeactivateBuilder()
        .stakingAddress(stakeAccount.pub)
        .sender(wallet.pub)
        .nonce(blockHash)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: txToBroadcastFormat,
        feeInfo: {
          fee: '5000',
        },
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'StakingDeactivate',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '0',
        outputs: [],
        fee: {
          fee: '5000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen',
        durableNonce: undefined,
      });
    });

    it('should explain withdraw staking transaction', async function () {
      const tx = await factory
        .getStakingWithdrawBuilder()
        .stakingAddress(stakeAccount.pub)
        .sender(wallet.pub)
        .nonce(blockHash)
        .amount(amount)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: txToBroadcastFormat,
        feeInfo: {
          fee: '5000',
        },
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'StakingWithdraw',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '10000',
        outputs: [
          {
            address: '5hr5fisPi6DXNuuRpm5XUbzpiEnmdyxXuBDTwzwZj5Pe',
            amount: '10000',
          },
        ],
        fee: {
          fee: '5000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen',
        durableNonce: undefined,
      });
    });

    it('should explain create ATA transaction', async function () {
      const tokenName = 'tsol:usdc';
      const rentExemptAmount = '3000000';
      const tx = await factory
        .getAtaInitializationBuilder()
        .sender(wallet.pub)
        .nonce(blockHash)
        .mint(tokenName)
        .rentExemptAmount(rentExemptAmount)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: txToBroadcastFormat,
        feeInfo: {
          fee: '5000',
        },
        tokenAccountRentExemptAmount: rentExemptAmount,
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'AssociatedTokenAccountInitialization',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '0',
        outputs: [],
        fee: {
          fee: '3005000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen',
        durableNonce: undefined,
      });
    });

    it('should explain create multi ATA transaction', async function () {
      const recipients = [
        {
          ownerAddress: wallet.pub,
          tokenName: 'tsol:usdc',
        },
        {
          ownerAddress: durableNonce.walletNonceAddress,
          tokenName: 'tsol:ray',
        },
      ];
      const rentExemptAmount = '3000000';
      const tx = await factory
        .getAtaInitializationBuilder()
        .sender(wallet.pub)
        .nonce(blockHash)
        .enableToken(recipients[0])
        .enableToken(recipients[1])
        .rentExemptAmount(rentExemptAmount)
        .memo('test memo')
        .fee({ amount: 5000 })
        .build();
      const txToBroadcastFormat = tx.toBroadcastFormat();
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: txToBroadcastFormat,
        feeInfo: {
          fee: '5000',
        },
        tokenAccountRentExemptAmount: rentExemptAmount,
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'AssociatedTokenAccountInitialization',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '0',
        outputs: [],
        fee: {
          fee: '6005000',
          feeRate: 5000,
        },
        memo: 'test memo',
        blockhash: '5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen',
        durableNonce: undefined,
      });
    });

    it('should explain an unsigned token transfer with ATA creation transaction', async function () {
      const explainedTransaction = await basecoin.explainTransaction({
        txBase64: testData.rawTransactions.tokenTransferWithAtaCreation.unsigned,
        feeInfo: {
          fee: '5000',
        },
        tokenAccountRentExemptAmount: '3000000',
      });
      explainedTransaction.should.deepEqual({
        displayOrder: [
          'id',
          'type',
          'blockhash',
          'durableNonce',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'memo',
        ],
        id: 'UNAVAILABLE',
        type: 'Send',
        changeOutputs: [],
        changeAmount: '0',
        outputAmount: '0',
        outputs: [
          {
            address: '2eKjVtzV3oPTXFdtRSDj3Em9k1MV7k8WjKkBszQUwizS',
            amount: '10000',
            tokenName: 'tsol:usdc',
          },
        ],
        fee: { fee: '3005000', feeRate: 5000 },
        memo: undefined,
        blockhash: '27E3MXFvXMUNYeMJeX1pAbERGsJfUbkaZTfgMgpmNN5g',
        durableNonce: undefined,
      });
    });
  });

  describe('Keypair:', () => {
    it('should generate a keypair from random seed', function () {
      should.throws(() => basecoin.generateKeyPair('placeholder' as any), 'generateKeyPair method not implemented');
    });

    it('should generate a keypair from a seed', function () {
      should.throws(() => basecoin.generateKeyPair('placeholder' as any), 'generateKeyPair method not implemented');
    });
  });

  describe('Sign transaction:', () => {
    it('should sign transaction', async function () {
      const signed: any = await basecoin.signTransaction({
        txPrebuild: {
          txBase64: resources.RAW_TX_UNSIGNED,
          keys: [resources.accountWithSeed.publicKey.toString()],
        },
        prv: resources.accountWithSeed.privateKey.base58,
      } as any);
      signed.txHex.should.equal(resources.RAW_TX_SIGNED);
    });

    it('should handle txHex and txBase64 interchangeably', async function () {
      const signed: any = await basecoin.signTransaction({
        txPrebuild: {
          txHex: resources.RAW_TX_UNSIGNED,
          keys: [resources.accountWithSeed.publicKey.toString()],
        },
        prv: resources.accountWithSeed.privateKey.base58,
      } as any);
      signed.txHex.should.equal(resources.RAW_TX_SIGNED);
    });

    it('should throw invalid transaction when sign with public key', async function () {
      await basecoin
        .signTransaction({
          txPrebuild: {
            txBase64: resources.RAW_TX_UNSIGNED,
            keys: [resources.accountWithSeed.publicKey.toString()],
          },
          prv: resources.accountWithSeed.publicKey,
        } as any)
        .should.be.rejectedWith('Invalid key');
    });
  });

  describe('Sign message', () => {
    it('should sign message', async function () {
      const signed = await basecoin.signMessage(keypair, 'signed message');
      signed
        .toString('base64')
        .should.equal('s+7d/8aW/twfM/0wLSKOGxd9+LhDIiz/g0FfJ39ylJhQIkjK0RYPm/Y+gdeJ5DIy6K6h6gCXXESDomlv12DBBQ==');
    });
    it('shouldnt sign message when message is undefined', async function () {
      await basecoin
        .signMessage(keypair, undefined as any)
        .should.be.rejectedWith(
          'The first argument must be of type string or an instance of Buffer, ArrayBuffer, or Array or an Array-like Object. Received undefined'
        );
    });
  });

  describe('Get Signing Payload', () => {
    it('should return a valid signing payload', async function () {
      const factory = getBuilderFactory(basecoin.getChain());
      const rebuiltSignablePayload = (await factory.from(resources.TRANSFER_UNSIGNED_TX_WITH_MEMO).build())
        .signablePayload;
      const signingPayload = await basecoin.getSignablePayload(resources.TRANSFER_UNSIGNED_TX_WITH_MEMO);
      signingPayload.should.be.deepEqual(rebuiltSignablePayload);
    });

    it('should build CloseAssociatedTokenAccount txn builder from raw txn', async function () {
      const factory = getBuilderFactory(basecoin.getChain());
      const txnBuilder = factory.from(resources.TRANSFER_UNSIGNED_TX_CLOSE_ATA);
      assert.ok(txnBuilder);
    });
  });

  describe('Presign transaction', () => {
    const txRequestId = 'txRequestId';
    let sandbox: sinon.SinonSandbox;

    beforeEach(() => {
      sandbox = sinon.createSandbox();
    });

    afterEach(() => {
      sandbox.verifyAndRestore();
    });

    it('should rebuild tx request for hot wallets', async () => {
      const rebuiltTx: TxRequest = {
        txRequestId,
        unsignedTxs: [
          {
            serializedTxHex: 'deadbeef',
            signableHex: 'serializedTxHex',
            derivationPath: 'm/0',
          },
        ],
        transactions: [],
        date: new Date().toISOString(),
        intent: {
          intentType: 'payment',
        },
        latest: true,
        state: 'pendingUserSignature',
        walletType: 'hot',
        walletId: 'walletId',
        policiesChecked: true,
        version: 1,
        userId: 'userId',
      };

      const stubTssUtils = sandbox.createStubInstance(TssUtils);
      stubTssUtils.deleteSignatureShares.resolves([]);
      stubTssUtils.getTxRequest.resolves(rebuiltTx);

      const hotWallet = {
        type: 'hot',
      };
      const presignedTransaction: any = await basecoin.presignTransaction({
        walletData: hotWallet,
        tssUtils: stubTssUtils,
        txPrebuild: {
          txRequestId,
        },
      } as any);

      presignedTransaction.walletData.should.deepEqual(hotWallet);
      presignedTransaction.txPrebuild.should.deepEqual(rebuiltTx);
      presignedTransaction.txHex.should.equal(rebuiltTx.unsignedTxs[0].serializedTxHex);
    });

    it('should do nothing for non-hot wallets', async () => {
      const coldWallet = {
        type: 'cold',
      };

      const presignedTransaction = await basecoin.presignTransaction({
        walletData: coldWallet,
      } as any);
      presignedTransaction.should.deepEqual({
        walletData: coldWallet,
      });
    });

    it('should error if txRequestId is missing', async () => {
      const hotWallet = {
        type: 'hot',
      };
      await basecoin
        .presignTransaction({
          walletData: hotWallet,
          txPrebuild: {},
        } as any)
        .should.rejectedWith('Missing txRequestId');
    });
  });

  describe('Recover Transactions:', () => {
    const sandBox = sinon.createSandbox();
    const coin = coins.get('tsol');
    const usdtMintAddress = '9cgpBeNZ2HnLda7NWaaU1i3NyTstk2c4zCMUcoAGsi9C';
    let callBack;

    beforeEach(() => {
      callBack = sandBox.stub(Sol.prototype, 'getDataFromNode' as keyof Sol);

      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getLatestBlockhash',
            params: [
              {
                commitment: 'finalized',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getBlockhashResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getFeeForMessage',
            params: [
              sinon.match.string,
              {
                commitment: 'finalized',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getFeesForMessageResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getMinimumBalanceForRentExemption',
            params: [165],
          },
        })
        .resolves(testData.SolResponses.getMinimumBalanceForRentExemptionResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.accountInfo.bs58EncodedPublicKey],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.accountInfo.bs58EncodedPublicKeyNoFunds],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponseNoFunds);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.accountInfo.bs58EncodedPublicKeyM1Derivation],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponseM1Derivation);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.accountInfo.bs58EncodedPublicKeyM2Derivation],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponseM2Derivation);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.accountInfo.bs58EncodedPublicKeyWithManyTokens],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponseM2Derivation);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.closeATAkeys.closeAtaAddress],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponseM2Derivation);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.closeATAkeys.bs58EncodedPublicKey],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponseM2Derivation);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getAccountInfo',
            params: [
              testData.closeATAkeys.closeAtaAddress,
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenInfoResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getAccountInfo',
            params: [
              testData.keys.durableNoncePubKey,
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getAccountInfoResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.keys.destinationPubKey,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponseNoAccounts);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.accountInfo.bs58EncodedPublicKey,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponseNoAccounts);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.keys.destinationPubKey2,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.wrwUser.walletAddress0,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponse2);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.wrwUser.walletAddress4,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponse3);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.wrwUser.walletAddress0],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'sendTransaction',
            params: sinon.match.array,
          },
        })
        .onCall(0)
        .resolves(testData.SolResponses.broadcastTransactionResponse)
        .onCall(1)
        .resolves(testData.SolResponses.broadcastTransactionResponse1);
    });

    afterEach(() => {
      sandBox.restore();
    });

    it('should take OVC output and generate a signed sweep transaction', async function () {
      const params = testData.ovcResponse;
      const recoveryTxn = await basecoin.createBroadcastableSweepTransaction(params);
      recoveryTxn.transactions[0].serializedTx.should.equal(
        'AvR+L909kzRq6NuaUe9F6Jt97MOiFs7jpW8MuOrwz4EbKF40d31dci/bgLTq4gpk/Hh3s5cA8FtbLkDQr15PqAE7yd8LOXvsLtO2REqMM/OCZ8wItfsqfTfia2xIfibRW3wHgw63jiaojbXeSqaYajJ/Ca7YwBUz5blydI3fYLgPAgECBsLVtfT7mpvNii8wPk0G942N7TAHE/RW2iq/8LPqAYWqBRo0vIrNQ4djl2+Wh2EVBQ9zgoVTVm0RHXrIv/6/WHxPX1mHv+JqpmAT79ltNjYPK0M2yR+ZMln7VgUTBWFNQvLqE/j/nXlY2/JpxuNr/fXLXEPeS04dPvt9qz1dAoYEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAADpiH20cxLj7KnOaoI5ANNoPxYjs472FdjDeMPft3kXdAgQDAgUBBAQAAAAEAgADDAIAAADwopo7AAAAAA=='
      );
      (recoveryTxn.transactions[0].scanIndex ?? 0).should.equal(0);
      (recoveryTxn.lastScanIndex ?? 0).should.equal(0);
    });

    it('should take consolidation OVC output and generate multiple signed sweep transactions', async function () {
      const params = testData.ovcResponse2;
      const recoveryTxn = await basecoin.createBroadcastableSweepTransaction(params);
      recoveryTxn.transactions[0].serializedTx.should.equal(
        'AtQPLzOmLuKwHY6N5XoJIZK/T7W10uYWm/MRte3GFUdl+w3gHLjSa9H66WSfFNubQxIPckxJDyltkP7ksLDf9QgBNJM2UWbBUH5wT0JJHILlhCs33HX8DeE/8Tdsw6tGfZoMhCnSKv6TPWtBxy7Sb6sW8ksCUPnAWuHGGKmgjEMBAgECBmLrqxJrY2kbN/tcrQw3P8P15OljFGabFJAKBrUO1grNBRo0vIrNQ4djl2+Wh2EVBQ9zgoVTVm0RHXrIv/6/WHxPX1mHv+JqpmAT79ltNjYPK0M2yR+ZMln7VgUTBWFNQsLVtfT7mpvNii8wPk0G942N7TAHE/RW2iq/8LPqAYWqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAIZQniiS73D6mwfpnfhVMC4lyYJtRSrmoZpF7yIlUdIDAgQDAgUBBAQAAAAEAgADDAIAAADwPc0dAAAAAA=='
      );
      (recoveryTxn.transactions[0].scanIndex ?? 0).should.equal(1);
      recoveryTxn.transactions[1].serializedTx.should.equal(
        'AuLhOA5zmOBZR85lo+nKdTopVwJAMrMp6NW+8UnGNsSBSpBkqfWZQqSg9s+7aTlXezm5vxol+Pl6t7PpVNTOHwLcp9xJp3TFHdivEbhwJKldR4Ny+pasoFx+Bgk8q6g1iNiq7XSi1Ov3bs7euMkTj7nDRFqP8lv7xLTcvrBm9OQJAgECBp14ImBCdmVROlw0UveYS1MvG/ljCRI3MJTFmsxuXEoWBRo0vIrNQ4djl2+Wh2EVBQ9zgoVTVm0RHXrIv/6/WHw0hyxvpVwtIx9/zeX2O16eTrY+aKIh1mdKg4MMg0eyxMLVtfT7mpvNii8wPk0G942N7TAHE/RW2iq/8LPqAYWqAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGp9UXGSxWjuCKhF9z0peIzwNcMUWyGrNE2AYuqUAAAC7ws1XFslinwgtpISUViVWIVTHyD2Q0qj24YjKmrAmXAgQDAgUBBAQAAAAEAgADDAIAAADwPc0dAAAAAA=='
      );
      (recoveryTxn.transactions[1].scanIndex ?? 0).should.equal(2);
      (recoveryTxn.lastScanIndex ?? 0).should.equal(20);
    });

    it('should recover a txn for non-bitgo recoveries (latest blockhash)', async function () {
      // Latest Blockhash Recovery (BitGo-less)
      const latestBlockHashTxn = await basecoin.recover({
        userKey: testData.keys.userKey,
        backupKey: testData.keys.backupKey,
        bitgoKey: testData.keys.bitgoKey,
        recoveryDestination: testData.keys.destinationPubKey,
        walletPassphrase: testData.keys.walletPassword,
      });
      latestBlockHashTxn.should.not.be.empty();
      latestBlockHashTxn.should.hasOwnProperty('serializedTx');
      latestBlockHashTxn.should.hasOwnProperty('scanIndex');
      should.equal((latestBlockHashTxn as MPCTx).scanIndex, 0);

      const latestBlockhashTxnDeserialize = new Transaction(coin);
      latestBlockhashTxnDeserialize.fromRawTransaction((latestBlockHashTxn as MPCTx).serializedTx);
      const latestBlockhashTxnJson = latestBlockhashTxnDeserialize.toJson();

      should.equal(latestBlockhashTxnJson.nonce, testData.SolInputData.blockhash);
      should.equal(latestBlockhashTxnJson.feePayer, testData.accountInfo.bs58EncodedPublicKey);
      should.equal(latestBlockhashTxnJson.numSignatures, testData.SolInputData.latestBlockhashSignatures);
      const solCoin = basecoin as any;
      sandBox.assert.callCount(solCoin.getDataFromNode, 3);
    });

    it('should recover a txn for non-bitgo recoveries (durable nonce)', async function () {
      // Durable Nonce Recovery (BitGo-less)
      const durableNonceTxn = await basecoin.recover({
        userKey: testData.keys.userKey,
        backupKey: testData.keys.backupKey,
        bitgoKey: testData.keys.bitgoKey,
        recoveryDestination: testData.keys.destinationPubKey,
        walletPassphrase: testData.keys.walletPassword,
        durableNonce: {
          publicKey: testData.keys.durableNoncePubKey,
          secretKey: testData.keys.durableNoncePrivKey,
        },
      });

      durableNonceTxn.should.not.be.empty();
      durableNonceTxn.should.hasOwnProperty('serializedTx');
      durableNonceTxn.should.hasOwnProperty('scanIndex');
      should.equal((durableNonceTxn as MPCTx).scanIndex, 0);

      const durableNonceTxnDeserialize = new Transaction(coin);
      durableNonceTxnDeserialize.fromRawTransaction((durableNonceTxn as MPCTx).serializedTx);
      const durableNonceTxnJson = durableNonceTxnDeserialize.toJson();

      should.equal(durableNonceTxnJson.nonce, testData.SolInputData.durableNonceBlockhash);
      should.equal(durableNonceTxnJson.feePayer, testData.accountInfo.bs58EncodedPublicKey);
      should.equal(durableNonceTxnJson.numSignatures, testData.SolInputData.durableNonceSignatures);
      const solCoin = basecoin as any;
      sandBox.assert.callCount(solCoin.getDataFromNode, 4);
    });

    it('should recover a txn for unsigned sweep recoveries', async function () {
      // Unsigned Sweep Recovery
      const unsignedSweepTxn = (await basecoin.recover({
        bitgoKey: testData.keys.bitgoKey,
        recoveryDestination: testData.keys.destinationPubKey,
        durableNonce: {
          publicKey: testData.keys.durableNoncePubKey,
          secretKey: testData.keys.durableNoncePrivKey,
        },
      })) as MPCSweepTxs;

      unsignedSweepTxn.should.not.be.empty();
      unsignedSweepTxn.txRequests[0].transactions[0].unsignedTx.should.hasOwnProperty('serializedTx');
      unsignedSweepTxn.txRequests[0].transactions[0].unsignedTx.should.hasOwnProperty('scanIndex');
      should.equal(unsignedSweepTxn.txRequests[0].transactions[0].unsignedTx.scanIndex, 0);

      const unsignedSweepTxnDeserialize = new Transaction(coin);
      unsignedSweepTxnDeserialize.fromRawTransaction(
        unsignedSweepTxn.txRequests[0].transactions[0].unsignedTx.serializedTx
      );
      const unsignedSweepTxnJson = unsignedSweepTxnDeserialize.toJson();

      should.equal(unsignedSweepTxnJson.nonce, testData.SolInputData.durableNonceBlockhash);
      should.equal(unsignedSweepTxnJson.feePayer, testData.accountInfo.bs58EncodedPublicKey);
      should.equal(unsignedSweepTxnJson.numSignatures, testData.SolInputData.unsignedSweepSignatures);
      const solCoin = basecoin as any;
      sandBox.assert.callCount(solCoin.getDataFromNode, 4);
    });

    it('should handle error in recover function if a required field is missing/incorrect', async function () {
      // missing userkey
      await basecoin
        .recover({
          backupKey: testData.keys.backupKey,
          bitgoKey: testData.keys.bitgoKey,
          recoveryDestination: testData.keys.destinationPubKey,
          walletPassphrase: testData.keys.walletPassword,
        })
        .should.rejectedWith('missing userKey');

      // missing backupkey
      await basecoin
        .recover({
          userKey: testData.keys.userKey,
          bitgoKey: testData.keys.bitgoKey,
          recoveryDestination: testData.keys.destinationPubKey,
          walletPassphrase: testData.keys.walletPassword,
        })
        .should.rejectedWith('missing backupKey');

      // missing wallet passphrase
      await basecoin
        .recover({
          userKey: testData.keys.userKey,
          backupKey: testData.keys.backupKey,
          bitgoKey: testData.keys.bitgoKey,
          recoveryDestination: testData.keys.destinationPubKey,
        })
        .should.rejectedWith('missing wallet passphrase');

      // incorrect wallet passphrase, user key, backup key combination
      await basecoin
        .recover({
          userKey: testData.keys.userKey,
          backupKey: testData.keys.backupKey,
          bitgoKey: testData.keys.bitgoKey,
          recoveryDestination: testData.keys.destinationPubKey,
          walletPassphrase: testData.keys.walletPassword + 'incorrect',
        })
        .should.rejectedWith("Error decrypting user keychain: password error - ccm: tag doesn't match");

      // no wallet with sufficient funds
      await basecoin
        .recover({
          userKey: testData.keys.userKey,
          backupKey: testData.keys.backupKey,
          bitgoKey: testData.keys.bitgoKeyNoFunds,
          recoveryDestination: testData.keys.destinationPubKey,
          walletPassphrase: testData.keys.walletPassword,
        })
        .should.rejectedWith('Did not find address with funds to recover');
    });

    it('should recover sol tokens to recovery destination with no existing token accounts', async function () {
      const tokenTxn = await basecoin.recover({
        userKey: testData.wrwUser.userKey,
        backupKey: testData.wrwUser.backupKey,
        bitgoKey: testData.wrwUser.bitgoKey,
        recoveryDestination: testData.keys.destinationPubKey,
        tokenContractAddress: usdtMintAddress,
        walletPassphrase: testData.wrwUser.walletPassphrase,
        durableNonce: {
          publicKey: testData.keys.durableNoncePubKey,
          secretKey: testData.keys.durableNoncePrivKey,
        },
      });

      tokenTxn.should.not.be.empty();
      tokenTxn.should.hasOwnProperty('serializedTx');
      tokenTxn.should.hasOwnProperty('scanIndex');
      should.equal((tokenTxn as MPCTx).scanIndex, 0);

      const tokenTxnDeserialize = new Transaction(coin);
      tokenTxnDeserialize.fromRawTransaction((tokenTxn as MPCTx).serializedTx);
      const tokenTxnJson = tokenTxnDeserialize.toJson();

      should.equal(tokenTxnJson.nonce, testData.SolInputData.durableNonceBlockhash);
      should.equal(tokenTxnJson.feePayer, testData.wrwUser.walletAddress0);
      should.equal(tokenTxnJson.numSignatures, testData.SolInputData.durableNonceSignatures);

      const instructionsData = tokenTxnJson.instructionsData as InstructionParams[];
      should.equal(instructionsData.length, 3);
      should.equal(instructionsData[0].type, 'NonceAdvance');

      const destinationUSDTTokenAccount = await getAssociatedTokenAccountAddress(
        usdtMintAddress,
        testData.keys.destinationPubKey
      );
      should.equal(instructionsData[1].type, 'CreateAssociatedTokenAccount');
      should.equal((instructionsData[1] as AtaInit).params.mintAddress, usdtMintAddress);
      should.equal((instructionsData[1] as AtaInit).params.ataAddress, destinationUSDTTokenAccount);
      should.equal((instructionsData[1] as AtaInit).params.ownerAddress, testData.keys.destinationPubKey);
      should.equal((instructionsData[1] as AtaInit).params.tokenName, 'tsol:usdt');
      should.equal((instructionsData[1] as AtaInit).params.payerAddress, testData.wrwUser.walletAddress0);

      const sourceUSDTTokenAccount = await getAssociatedTokenAccountAddress(
        usdtMintAddress,
        testData.wrwUser.walletAddress0
      );
      should.equal(instructionsData[2].type, 'TokenTransfer');
      should.equal((instructionsData[2] as TokenTransfer).params.fromAddress, testData.wrwUser.walletAddress0);
      should.equal((instructionsData[2] as TokenTransfer).params.toAddress, destinationUSDTTokenAccount);
      should.equal((instructionsData[2] as TokenTransfer).params.amount, '2000000000');
      should.equal((instructionsData[2] as TokenTransfer).params.tokenName, 'tsol:usdt');
      should.equal((instructionsData[2] as TokenTransfer).params.sourceAddress, sourceUSDTTokenAccount);

      const solCoin = basecoin as any;
      sandBox.assert.callCount(solCoin.getDataFromNode, 7);
    });

    it('should recover sol tokens to recovery destination with existing token accounts', async function () {
      const tokenTxn = await basecoin.recover({
        userKey: testData.wrwUser.userKey,
        backupKey: testData.wrwUser.backupKey,
        bitgoKey: testData.wrwUser.bitgoKey,
        recoveryDestination: testData.keys.destinationPubKey2,
        tokenContractAddress: usdtMintAddress,
        walletPassphrase: testData.wrwUser.walletPassphrase,
        durableNonce: {
          publicKey: testData.keys.durableNoncePubKey,
          secretKey: testData.keys.durableNoncePrivKey,
        },
      });

      tokenTxn.should.not.be.empty();
      tokenTxn.should.hasOwnProperty('serializedTx');
      tokenTxn.should.hasOwnProperty('scanIndex');
      should.equal((tokenTxn as MPCTx).scanIndex, 0);

      const tokenTxnDeserialize = new Transaction(coin);
      tokenTxnDeserialize.fromRawTransaction((tokenTxn as MPCTx).serializedTx);
      const tokenTxnJson = tokenTxnDeserialize.toJson();

      should.equal(tokenTxnJson.nonce, testData.SolInputData.durableNonceBlockhash);
      should.equal(tokenTxnJson.feePayer, testData.wrwUser.walletAddress0);
      should.equal(tokenTxnJson.numSignatures, testData.SolInputData.durableNonceSignatures);

      const instructionsData = tokenTxnJson.instructionsData as TokenTransfer[];
      should.equal(instructionsData.length, 2);
      should.equal(instructionsData[0].type, 'NonceAdvance');

      const sourceUSDTTokenAccount = await getAssociatedTokenAccountAddress(
        usdtMintAddress,
        testData.wrwUser.walletAddress0
      );
      const destinationUSDTTokenAccount = await getAssociatedTokenAccountAddress(
        usdtMintAddress,
        testData.keys.destinationPubKey2
      );
      should.equal(instructionsData[1].type, 'TokenTransfer');
      should.equal(instructionsData[1].params.fromAddress, testData.wrwUser.walletAddress0);
      should.equal(instructionsData[1].params.toAddress, destinationUSDTTokenAccount);
      should.equal(instructionsData[1].params.amount, '2000000000');
      should.equal(instructionsData[1].params.tokenName, 'tsol:usdt');
      should.equal(instructionsData[1].params.sourceAddress, sourceUSDTTokenAccount);

      const solCoin = basecoin as any;
      sandBox.assert.callCount(solCoin.getDataFromNode, 7);
    });

    it('should recover sol tokens to recovery destination with existing token accounts for unsigned sweep recoveries', async function () {
      const feeResponse = testData.SolResponses.getFeesForMessageResponse;
      feeResponse.body.result.value = 10000;
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getFeeForMessage',
            params: [
              sinon.match.string,
              {
                commitment: 'finalized',
              },
            ],
          },
        })
        .resolves(feeResponse);

      const tokenTxn = (await basecoin.recover({
        bitgoKey: testData.wrwUser.bitgoKey,
        recoveryDestination: testData.keys.destinationPubKey2,
        durableNonce: {
          publicKey: testData.keys.durableNoncePubKey,
          secretKey: testData.keys.durableNoncePrivKey,
        },
        tokenContractAddress: testData.tokenAddress.TestUSDC,
      })) as MPCSweepTxs;

      // 2 signatures and no rent exemption fee since the destination already has token accounts
      const expectedFee = 5000 + 5000;

      const { serializedTx, scanIndex, feeInfo, parsedTx } = tokenTxn.txRequests[0].transactions[0].unsignedTx;
      assert.ok(serializedTx);
      assert.strictEqual(scanIndex, 0);
      assert.ok(feeInfo);
      assert.strictEqual(feeInfo.feeString, expectedFee.toString());
      assert.strictEqual(feeInfo.fee, expectedFee);
      assert.ok(parsedTx);
      assert.ok(parsedTx.inputs instanceof Array && parsedTx.inputs.length === 1);
      assert.ok(parsedTx.outputs instanceof Array && parsedTx.outputs.length === 1);

      const tokenTxnDeserialize = new Transaction(coin);
      tokenTxnDeserialize.fromRawTransaction(tokenTxn.txRequests[0].transactions[0].unsignedTx.serializedTx);
      const tokenTxnJson = tokenTxnDeserialize.toJson();

      assert.strictEqual(tokenTxnJson.nonce, testData.SolInputData.durableNonceBlockhash);
      assert.strictEqual(tokenTxnJson.feePayer, testData.wrwUser.walletAddress0);
      assert.strictEqual(tokenTxnJson.numSignatures, testData.SolInputData.unsignedSweepSignatures);
      const solCoin = basecoin as any;
      sandBox.assert.callCount(solCoin.getDataFromNode, 7);
    });

    it('should recover sol funds from ATA address for non-bitgo recoveries', async function () {
      // close ATA address instruction type txn
      const closeATATxns = await basecoin.recoverCloseATA({
        userKey: testData.closeATAkeys.userKey,
        backupKey: testData.closeATAkeys.backupKey,
        bitgoKey: testData.closeATAkeys.bitgoKey,
        recoveryDestination: testData.closeATAkeys.destinationPubKey,
        walletPassphrase: testData.closeATAkeys.walletPassword,
        closeAtaAddress: testData.closeATAkeys.closeAtaAddress,
        recoveryDestinationAtaAddress: testData.closeATAkeys.recoveryDestinationAtaAddress,
      });
      closeATATxns.should.not.be.empty();
      should.equal(
        closeATATxns[0].txId,
        '2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb'
      );
      should.equal(
        closeATATxns[1].txId,
        '5oUBgXX4enGmFEspG64goy3PRysjfrekZGg3rZNkBHUCQFd482vrVWbfDcRYMBEJt65JXymfEPm8M6d89X4xV79n'
      );
    });
  });

  describe('Build Consolidation Recoveries:', () => {
    const sandBox = sinon.createSandbox();
    const coin = coins.get('tsol');
    const usdtMintAddress = '9cgpBeNZ2HnLda7NWaaU1i3NyTstk2c4zCMUcoAGsi9C';
    const durableNonces = {
      publicKeys: [
        testData.keys.durableNoncePubKey,
        testData.keys.durableNoncePubKey2,
        testData.keys.durableNoncePubKey3,
      ],
      secretKey: testData.keys.durableNoncePrivKey,
    };

    beforeEach(() => {
      const callBack = sandBox.stub(Sol.prototype, 'getDataFromNode' as keyof Sol);

      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getLatestBlockhash',
            params: [
              {
                commitment: 'finalized',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getBlockhashResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getFeeForMessage',
            params: [
              sinon.match.string,
              {
                commitment: 'finalized',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getFeesForMessageResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.wrwUser.walletAddress1],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponseNoFunds);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.wrwUser.walletAddress2],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.wrwUser.walletAddress3],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getBalance',
            params: [testData.wrwUser.walletAddress5],
          },
        })
        .resolves(testData.SolResponses.getAccountBalanceResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getMinimumBalanceForRentExemption',
            params: [165],
          },
        })
        .resolves(testData.SolResponses.getMinimumBalanceForRentExemptionResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getAccountInfo',
            params: [
              testData.keys.durableNoncePubKey,
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getAccountInfoResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getAccountInfo',
            params: [
              testData.keys.durableNoncePubKey2,
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getAccountInfoResponse2);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.wrwUser.walletAddress1,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponseNoAccounts);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.wrwUser.walletAddress2,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponseNoAccounts);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.wrwUser.walletAddress3,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponseNoAccounts);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.wrwUser.walletAddress5,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponse);
      callBack
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'getTokenAccountsByOwner',
            params: [
              testData.wrwUser.walletAddress0,
              {
                programId: 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA',
              },
              {
                encoding: 'jsonParsed',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.getTokenAccountsByOwnerResponse);
    });

    afterEach(() => {
      sandBox.restore();
    });

    it('should build signed consolidation recoveries', async function () {
      const res = (await basecoin.recoverConsolidations({
        userKey: testData.wrwUser.userKey,
        backupKey: testData.wrwUser.backupKey,
        bitgoKey: testData.wrwUser.bitgoKey,
        walletPassphrase: testData.wrwUser.walletPassphrase,
        startingScanIndex: 1,
        endingScanIndex: 4,
        durableNonces: durableNonces,
      })) as MPCTxs;
      res.should.not.be.empty();
      res.transactions.length.should.equal(2);
      (res.lastScanIndex ?? 0).should.equal(3);

      const txn1 = res.transactions[0];
      const latestBlockhashTxnDeserialize1 = new Transaction(coin);
      latestBlockhashTxnDeserialize1.fromRawTransaction((txn1 as MPCTx).serializedTx);
      const latestBlockhashTxnJson1 = latestBlockhashTxnDeserialize1.toJson();

      const nonce1 = testData.SolResponses.getAccountInfoResponse.body.result.value.data.parsed.info.blockhash;
      should.equal(latestBlockhashTxnJson1.nonce, nonce1);
      should.equal(latestBlockhashTxnJson1.feePayer, testData.wrwUser.walletAddress2);
      should.equal(latestBlockhashTxnJson1.numSignatures, testData.SolInputData.durableNonceSignatures);

      const txn2 = res.transactions[1];
      const latestBlockhashTxnDeserialize2 = new Transaction(coin);
      latestBlockhashTxnDeserialize2.fromRawTransaction((txn2 as MPCTx).serializedTx);
      const latestBlockhashTxnJson2 = latestBlockhashTxnDeserialize2.toJson();

      const nonce2 = testData.SolResponses.getAccountInfoResponse2.body.result.value.data.parsed.info.blockhash;
      should.equal(latestBlockhashTxnJson2.nonce, nonce2);
      should.equal(latestBlockhashTxnJson2.feePayer, testData.wrwUser.walletAddress3);
      should.equal(latestBlockhashTxnJson2.numSignatures, testData.SolInputData.durableNonceSignatures);
    });

    it('should build unsigned consolidation recoveries', async function () {
      const res = (await basecoin.recoverConsolidations({
        bitgoKey: testData.wrwUser.bitgoKey,
        startingScanIndex: 1,
        endingScanIndex: 4,
        durableNonces: durableNonces,
      })) as MPCSweepTxs;
      res.should.not.be.empty();
      res.txRequests.length.should.equal(2);

      const txn1 = res.txRequests[0].transactions[0].unsignedTx;
      txn1.should.hasOwnProperty('serializedTx');
      txn1.should.hasOwnProperty('signableHex');
      txn1.should.hasOwnProperty('scanIndex');
      (txn1.scanIndex ?? 0).should.equal(2);
      txn1.should.hasOwnProperty('coin');
      txn1.coin?.should.equal('tsol');
      txn1.should.hasOwnProperty('derivationPath');
      txn1.derivationPath?.should.equal('m/2');

      txn1.should.hasOwnProperty('coinSpecific');
      const coinSpecific1 = txn1.coinSpecific;
      coinSpecific1?.should.hasOwnProperty('commonKeychain');

      const latestBlockhashTxnDeserialize1 = new Transaction(coin);
      latestBlockhashTxnDeserialize1.fromRawTransaction((txn1 as MPCTx).serializedTx);
      const latestBlockhashTxnJson1 = latestBlockhashTxnDeserialize1.toJson();

      const nonce1 = testData.SolResponses.getAccountInfoResponse.body.result.value.data.parsed.info.blockhash;
      should.equal(latestBlockhashTxnJson1.nonce, nonce1);
      should.equal(latestBlockhashTxnJson1.feePayer, testData.wrwUser.walletAddress2);
      should.equal(latestBlockhashTxnJson1.numSignatures, testData.SolInputData.unsignedSweepSignatures);

      const txn2 = res.txRequests[1].transactions[0].unsignedTx;
      txn2.should.hasOwnProperty('serializedTx');
      txn2.should.hasOwnProperty('signableHex');
      txn2.should.hasOwnProperty('scanIndex');
      (txn2.scanIndex ?? 0).should.equal(3);
      txn2.should.hasOwnProperty('coin');
      txn2.coin?.should.equal('tsol');
      txn2.should.hasOwnProperty('derivationPath');
      txn2.derivationPath?.should.equal('m/3');

      txn2.should.hasOwnProperty('coinSpecific');
      const coinSpecific2 = txn2.coinSpecific;
      coinSpecific2?.should.hasOwnProperty('commonKeychain');
      coinSpecific2?.should.hasOwnProperty('lastScanIndex');
      coinSpecific2?.lastScanIndex?.should.equal(3);

      const latestBlockhashTxnDeserialize2 = new Transaction(coin);
      latestBlockhashTxnDeserialize2.fromRawTransaction((txn2 as MPCTx).serializedTx);
      const latestBlockhashTxnJson2 = latestBlockhashTxnDeserialize2.toJson();

      const nonce2 = testData.SolResponses.getAccountInfoResponse2.body.result.value.data.parsed.info.blockhash;
      should.equal(latestBlockhashTxnJson2.nonce, nonce2);
      should.equal(latestBlockhashTxnJson2.feePayer, testData.wrwUser.walletAddress3);
      should.equal(latestBlockhashTxnJson2.numSignatures, testData.SolInputData.unsignedSweepSignatures);
    });

    it('should build unsigned token consolidation recoveries', async function () {
      const res = (await basecoin.recoverConsolidations({
        bitgoKey: testData.wrwUser.bitgoKey,
        startingScanIndex: 3,
        endingScanIndex: 5,
        tokenContractAddress: usdtMintAddress,
        durableNonces: durableNonces,
      })) as MPCSweepTxs;
      res.should.not.be.empty();
      res.txRequests.length.should.equal(1);

      const txn1 = res.txRequests[0].transactions[0].unsignedTx;
      txn1.should.hasOwnProperty('serializedTx');
      txn1.should.hasOwnProperty('signableHex');
      txn1.should.hasOwnProperty('scanIndex');
      (txn1.scanIndex ?? 0).should.equal(4);
      txn1.should.hasOwnProperty('coin');
      txn1.coin?.should.equal('tsol');
      txn1.should.hasOwnProperty('derivationPath');
      txn1.derivationPath?.should.equal('m/4');

      txn1.should.hasOwnProperty('coinSpecific');
      const coinSpecific1 = txn1.coinSpecific;
      coinSpecific1?.should.hasOwnProperty('commonKeychain');

      const latestBlockhashTxnDeserialize1 = new Transaction(coin);
      latestBlockhashTxnDeserialize1.fromRawTransaction((txn1 as MPCTx).serializedTx);
      const latestBlockhashTxnJson1 = latestBlockhashTxnDeserialize1.toJson();

      const nonce1 = testData.SolResponses.getAccountInfoResponse.body.result.value.data.parsed.info.blockhash;
      should.equal(latestBlockhashTxnJson1.nonce, nonce1);
      should.equal(latestBlockhashTxnJson1.feePayer, testData.wrwUser.walletAddress5);
      should.equal(latestBlockhashTxnJson1.numSignatures, testData.SolInputData.unsignedSweepSignatures);
    });

    it('should skip building consolidate transaction if balance is equal to zero', async function () {
      await basecoin
        .recoverConsolidations({
          userKey: testData.wrwUser.userKey,
          backupKey: testData.wrwUser.backupKey,
          bitgoKey: testData.wrwUser.bitgoKey,
          walletPassphrase: testData.wrwUser.walletPassphrase,
          startingScanIndex: 1,
          endingScanIndex: 2,
          durableNonces: durableNonces,
        })
        .should.rejectedWith('Did not find an address with funds to recover');
    });

    it('should throw if startingScanIndex is not ge to 1', async () => {
      await basecoin
        .recoverConsolidations({
          userKey: testData.wrwUser.userKey,
          backupKey: testData.wrwUser.backupKey,
          bitgoKey: testData.wrwUser.bitgoKey,
          startingScanIndex: -1,
          durableNonces: durableNonces,
        })
        .should.be.rejectedWith(
          'Invalid starting or ending index to scan for addresses. startingScanIndex: -1, endingScanIndex: 19.'
        );
    });

    it('should throw if scan factor is too high', async () => {
      await basecoin
        .recoverConsolidations({
          userKey: testData.wrwUser.userKey,
          backupKey: testData.wrwUser.backupKey,
          bitgoKey: testData.wrwUser.bitgoKey,
          startingScanIndex: 1,
          endingScanIndex: 300,
          durableNonces: durableNonces,
        })
        .should.be.rejectedWith(
          'Invalid starting or ending index to scan for addresses. startingScanIndex: 1, endingScanIndex: 300.'
        );
    });
  });

  describe('broadcastTransaction', function () {
    const sandBox = sinon.createSandbox();

    afterEach(() => {
      sandBox.restore();
    });

    it('should broadcast a transaction succesfully', async function () {
      const serializedSignedTransaction = testData.rawTransactions.transfer.signed;
      const broadcastStub = sandBox
        .stub(Sol.prototype, 'getDataFromNode' as keyof Sol)
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'sendTransaction',
            params: [
              serializedSignedTransaction,
              {
                encoding: 'base64',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.broadcastTransactionResponse);

      const broadcastTxn = await basecoin.broadcastTransaction({ serializedSignedTransaction });
      assert.ok(broadcastTxn);
      assert.ok(broadcastTxn.txId);
      assert.strictEqual(
        broadcastTxn.txId,
        '2id3YC2jK9G5Wo2phDx4gJVAew8DcY5NAojnVuao8rkxwPYPe8cSwE5GzhEgJA2y8fVjDEo6iR6ykBvDxrTQrtpb'
      );
      assert.strictEqual(broadcastStub.callCount, 1);
    });

    it('should throw if got an error from the node', async function () {
      const serializedSignedTransaction = testData.rawTransactions.transfer.signed;
      const broadcastStub = sandBox
        .stub(Sol.prototype, 'getDataFromNode' as keyof Sol)
        .withArgs({
          payload: {
            id: '1',
            jsonrpc: '2.0',
            method: 'sendTransaction',
            params: [
              serializedSignedTransaction,
              {
                encoding: 'base64',
              },
            ],
          },
        })
        .resolves(testData.SolResponses.broadcastTransactionResponseError);

      await assert.rejects(
        async () => {
          await basecoin.broadcastTransaction({ serializedSignedTransaction });
        },
        { message: 'Error broadcasting transaction: Transaction simulation failed: Blockhash not found' }
      );
      assert.strictEqual(broadcastStub.callCount, 1);
    });

    it('should throw if is not a valid transaction', async function () {
      const serializedSignedTransaction = 'randomstring';

      await assert.rejects(
        async () => {
          await basecoin.broadcastTransaction({ serializedSignedTransaction });
        },
        { message: 'Invalid raw transaction' }
      );
    });

    it('should throw if is not a signed transaction', async function () {
      const serializedSignedTransaction = testData.rawTransactions.transfer.unsigned;

      await assert.rejects(
        async () => {
          await basecoin.broadcastTransaction({ serializedSignedTransaction });
        },
        { message: 'Invalid raw transaction' }
      );
    });
  });
});

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


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