PHP WebShell

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

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

/**
 * @prettier
 */
import * as assert from 'assert';
import * as should from 'should';
import * as nock from 'nock';
import * as utxolib from '@bitgo/utxo-lib';
import { Triple } from '@bitgo/sdk-core';
import {
  getFixture,
  keychainsBase58,
  KeychainBase58,
  mockUnspent,
  shouldEqualJSON,
  utxoCoins,
  transactionHexToObj,
  getDefaultWalletKeys,
  defaultBitGo,
  getUtxoCoin,
} from '../util';
import { getSeed } from '@bitgo/sdk-test';
import { nockBitGo } from '../util/nockBitGo';
import { createFullSignedTransaction } from '../util/transaction';
import { getDefaultWalletUnspentSigner } from '../util/keychains';
import { MockCrossChainRecoveryProvider } from './mock';
import {
  AbstractUtxoCoin,
  CrossChainRecoverySigned,
  CrossChainRecoveryUnsigned,
  getWallet,
  supportedCrossChainRecoveries,
} from '@bitgo/abstract-utxo';
import * as sinon from 'sinon';

type WalletUnspent<TNumber extends number | bigint = number> = utxolib.bitgo.WalletUnspent<TNumber>;

function getKeyId(k: KeychainBase58): string {
  return getSeed(k.pub).toString('hex');
}

function nockWallet(coin: AbstractUtxoCoin, walletId: string, walletKeys: Triple<KeychainBase58>): nock.Scope[] {
  return [
    nockBitGo()
      .get(`/api/v2/${coin.getChain()}/wallet/${walletId}`)
      .reply(200, {
        id: walletId,
        coin: coin.getChain(),
        label: 'crossChainRecovery',
        keys: walletKeys.map((k) => getKeyId(k)),
      })
      .persist(),
    ...walletKeys.map((k) =>
      nockBitGo()
        .get(`/api/v2/${coin.getChain()}/key/${getKeyId(k)}`)
        .reply(200, k)
        .persist()
    ),
  ];
}

type Address = {
  address: string;
  chain: number;
  index: number;
  coinSpecific: unknown;
};

function nockWalletAddress(coin: AbstractUtxoCoin, walletId: string, address: Address): nock.Scope {
  return nockBitGo()
    .get(`/api/v2/${coin.getChain()}/wallet/${walletId}/address/${address.address}`)
    .reply(200, {
      address: address.address,
      chain: address.chain,
      index: address.index,
      coin: coin.getChain(),
      wallet: walletId,
      coinSpecific: address.coinSpecific,
    })
    .persist();
}

/**
 * Setup test for cross-chain recovery.
 *
 * Users can receive deposits on wallet addresses that are on a different chain.
 *
 * For instance, a user can receive litecoin on a bitcoin wallet.
 * This means that the litecoin blockchain has a transaction with outputs that are spendable
 * with keys that were originally created for a BitGo BTC wallet.
 * In this example, LTC is the "source coin" and BTC is the "recovery coin"
 * In cases like these we must use construct a transaction for litecoin network using keys of the
 * bitcoin wallet.
 *
 * @param sourceCoin - the coin to construct the transaction for
 * @param recoveryCoin - the coin the receiving wallet was set up for
 */
function run<TNumber extends number | bigint = number>(sourceCoin: AbstractUtxoCoin, recoveryCoin: AbstractUtxoCoin) {
  describe(`Cross-Chain Recovery [sourceCoin=${sourceCoin.getChain()} recoveryCoin=${recoveryCoin.getChain()}]`, function () {
    const walletKeys = getDefaultWalletKeys();
    const recoveryWalletId = '5abacebe28d72fbd07e0b8cbba0ff39e';
    // the address the accidental deposit went to, in both sourceCoin and addressCoin formats
    const [depositAddressSourceCoin, depositAddressRecoveryCoin] = [sourceCoin, recoveryCoin].map((coin) =>
      coin.generateAddress({ keychains: keychainsBase58, index: 0 })
    );
    // the address where we want to recover our funds to
    const recoveryAddress = sourceCoin.generateAddress({ keychains: keychainsBase58, index: 1 }).address;
    const nocks: nock.Scope[] = [];

    let depositTx: utxolib.bitgo.UtxoTransaction<TNumber>;

    function getDepositUnspents(): utxolib.bitgo.Unspent<TNumber>[] {
      return [
        mockUnspent<TNumber>(
          sourceCoin.network,
          walletKeys,
          'p2sh',
          0,
          (sourceCoin.amountType === 'bigint' ? BigInt('10999999800000001') : 1e8) as TNumber
        ),
      ];
    }

    function getDepositTransaction(): utxolib.bitgo.UtxoTransaction<TNumber> {
      return createFullSignedTransaction<TNumber>(
        sourceCoin.network,
        getDepositUnspents(),
        depositAddressSourceCoin.address,
        getDefaultWalletUnspentSigner()
      );
    }

    before('prepare deposit tx', function () {
      depositTx = getDepositTransaction();
    });

    function getRecoveryUnspents(): WalletUnspent<TNumber>[] {
      return [
        {
          id: depositTx.getId(),
          address: depositAddressSourceCoin.address,
          chain: depositAddressSourceCoin.chain as utxolib.bitgo.ChainCode,
          index: depositAddressSourceCoin.index,
          value: depositTx.outs[0].value,
        },
      ];
    }

    before('setup nocks', function () {
      nocks.push(...nockWallet(recoveryCoin, recoveryWalletId, keychainsBase58));
      nocks.push(nockWalletAddress(recoveryCoin, recoveryWalletId, depositAddressRecoveryCoin));
    });

    after(function () {
      nocks.forEach((n) => n.done());
    });

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

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

    function testMatchFixture(
      name: string,
      getRecoveryResult: () => CrossChainRecoverySigned<TNumber> | CrossChainRecoveryUnsigned<TNumber>
    ) {
      it(`should match fixture (${name})`, async function () {
        const recovery = getRecoveryResult();
        let recoveryObj = {
          ...recovery,
          tx: transactionHexToObj(recovery.txHex as string, sourceCoin.network, sourceCoin.amountType),
        };
        if (sourceCoin.amountType === 'bigint') {
          recoveryObj = JSON.parse(
            JSON.stringify(recoveryObj, (k, v) => {
              if (typeof v === 'bigint') {
                return v.toString();
              } else {
                return v;
              }
            })
          );
        }
        shouldEqualJSON(
          recoveryObj,
          await getFixture(sourceCoin, `recovery/crossChainRecovery-${recoveryCoin.getChain()}-${name}`, recoveryObj)
        );
      });
    }

    function checkRecoveryTransactionSignature(tx: string | utxolib.bitgo.UtxoTransaction<TNumber>) {
      if (typeof tx === 'string') {
        tx = utxolib.bitgo.createTransactionFromBuffer<TNumber>(Buffer.from(tx, 'hex'), sourceCoin.network, {
          amountType: sourceCoin.amountType,
        });
      }
      const unspents = getRecoveryUnspents();
      should.equal(tx.ins.length, unspents.length);
      tx.ins.forEach((input, i) => {
        assert(typeof tx !== 'string');
        utxolib.bitgo
          .verifySignatureWithUnspent<TNumber>(tx, i, getRecoveryUnspents(), walletKeys)
          .should.eql([true, false, false]);
      });
    }

    it('should test signed cross chain recovery', async () => {
      const getRecoveryProviderStub = sinon
        .stub(AbstractUtxoCoin.prototype, 'getRecoveryProvider')
        .returns(new MockCrossChainRecoveryProvider<TNumber>(sourceCoin, getDepositUnspents(), depositTx));
      const params = {
        recoveryCoin,
        txid: depositTx.getId(),
        recoveryAddress,
        wallet: recoveryWalletId,
      };
      const signedRecovery = (await sourceCoin.recoverFromWrongChain<TNumber>({
        ...params,
        xprv: keychainsBase58[0].prv,
      })) as CrossChainRecoverySigned<TNumber>;
      should.equal(getRecoveryProviderStub.callCount, 1);

      testMatchFixture('signed', () => signedRecovery);

      it('should have valid signatures for signed recovery', function () {
        checkRecoveryTransactionSignature(signedRecovery.txHex as string);
      });
    });

    it('should test unsigned cross chain recovery', async () => {
      const getRecoveryProviderStub = sinon
        .stub(AbstractUtxoCoin.prototype, 'getRecoveryProvider')
        .returns(new MockCrossChainRecoveryProvider<TNumber>(sourceCoin, getDepositUnspents(), depositTx));
      const params = {
        recoveryCoin,
        txid: depositTx.getId(),
        recoveryAddress,
        wallet: recoveryWalletId,
      };
      const unsignedRecovery = (await sourceCoin.recoverFromWrongChain<TNumber>({
        ...params,
        signed: false,
      })) as CrossChainRecoveryUnsigned<TNumber>;
      should.equal(getRecoveryProviderStub.callCount, 1);

      testMatchFixture('unsigned', () => unsignedRecovery);

      it('should be signable for unsigned recovery', async function () {
        const signedTx = await sourceCoin.signTransaction<TNumber>({
          txPrebuild: unsignedRecovery,
          prv: keychainsBase58[0].prv,
          pubs: keychainsBase58.map((k) => k.pub) as Triple<string>,
        });
        checkRecoveryTransactionSignature((signedTx as { txHex: string }).txHex);
      });
    });
  });
}

function isSupportedCrossChainRecovery(sourceCoin: AbstractUtxoCoin, recoveryCoin: AbstractUtxoCoin): boolean {
  return supportedCrossChainRecoveries[sourceCoin.getFamily()]?.includes(recoveryCoin.getFamily());
}

utxoCoins.forEach((coin) => {
  utxoCoins
    .filter(
      (otherCoin) =>
        coin !== otherCoin &&
        isSupportedCrossChainRecovery(coin, otherCoin) &&
        ((utxolib.isMainnet(coin.network) && utxolib.isMainnet(otherCoin.network)) ||
          (utxolib.isTestnet(coin.network) && utxolib.isTestnet(otherCoin.network)))
    )
    .forEach((otherCoin) => {
      if (coin.amountType === 'bigint') {
        run<bigint>(coin, otherCoin);
      } else {
        run(coin, otherCoin);
      }
    });
});

describe(`Cross-Chain Recovery getWallet`, async function () {
  const bitgo = defaultBitGo;
  const recoveryCoin = getUtxoCoin('btc');
  const recoveryWalletId = '5abacebe28d72fbd07e0b8cbba0ff39e';

  it('should search v1 wallets if the v2 endpoint responds with a 4xx error', async function () {
    const errorResponses = [400, 404];

    for (const error of errorResponses) {
      const nockV2Wallet = nockBitGo(bitgo)
        .get(`/api/v2/${recoveryCoin.getChain()}/wallet/${recoveryWalletId}`)
        .reply(error);
      const nockV1Wallet = nockBitGo(bitgo).get(`/api/v1/wallet/${recoveryWalletId}`).reply(error);
      await assert.rejects(
        () => getWallet(bitgo, recoveryCoin, recoveryWalletId),
        Error(`could not get wallet ${recoveryWalletId} from v1 or v2: ApiResponseError: ${error}`)
      );
      nockV2Wallet.done();
      nockV1Wallet.done();
    }
  });

  it('should throw an error if the v2 endpoint responds with a 5xx error', async function () {
    const errorResponses = [500];
    for (const error of errorResponses) {
      const nockV2Wallet = nockBitGo(bitgo)
        .get(`/api/v2/${recoveryCoin.getChain()}/wallet/${recoveryWalletId}`)
        .reply(error);
      await assert.rejects(() => getWallet(bitgo, recoveryCoin, recoveryWalletId), {
        name: 'ApiResponseError',
        status: 500,
        result: {},
        invalidToken: false,
        needsOTP: false,
      });
      nockV2Wallet.done();
    }
  });
});

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


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