PHP WebShell

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

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

import * as utxolib from '@bitgo/utxo-lib';
import * as should from 'should';
import * as sinon from 'sinon';
import { Wallet, UnexpectedAddressError, VerificationOptions } from '@bitgo/sdk-core';
import { TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../../../src/bitgo';
import { AbstractUtxoCoin, UtxoWallet, Output, TransactionExplanation, TransactionParams } from '@bitgo/abstract-utxo';

describe('Abstract UTXO Coin:', () => {
  describe('Parse Transaction:', () => {
    const bitgo: BitGo = TestBitGo.decorate(BitGo, { env: 'mock' });
    const coin = bitgo.coin('tbtc') as AbstractUtxoCoin;

    /*
     * mock objects which get passed into parse transaction.
     * These objects are structured to force parse transaction into a
     * particular execution path for these tests.
     */
    const verification: VerificationOptions = {
      disableNetworking: true,
      keychains: {
        user: { id: '0', pub: 'aaa', type: 'independent' },
        backup: { id: '1', pub: 'bbb', type: 'independent' },
        bitgo: { id: '2', pub: 'ccc', type: 'independent' },
      },
    };

    const wallet = sinon.createStubInstance(Wallet, {
      migratedFrom: '2MzJxAENaesCFu3orrCdj22c69tLEsKXQoR',
    });

    const outputAmount = (0.01 * 1e8).toString();

    async function runClassifyOutputsTest(
      outputAddress,
      verification,
      expectExternal,
      txParams: TransactionParams = {}
    ) {
      sinon.stub(coin, 'explainTransaction').resolves({
        outputs: [] as Output[],
        changeOutputs: [
          {
            address: outputAddress,
            amount: outputAmount,
          },
        ],
      } as TransactionExplanation);

      if (!txParams.changeAddress) {
        sinon.stub(coin, 'verifyAddress').throws(new UnexpectedAddressError('test error'));
      }

      const parsedTransaction = await coin.parseTransaction({
        txParams,
        txPrebuild: { txHex: '' },
        wallet: wallet as unknown as UtxoWallet,
        verification,
      });

      should.exist(parsedTransaction.outputs[0]);
      parsedTransaction.outputs[0].should.deepEqual({
        address: outputAddress,
        amount: outputAmount,
        external: expectExternal,
      });

      const isExplicit =
        txParams.recipients !== undefined &&
        txParams.recipients.some((recipient) => recipient.address === outputAddress);
      should.equal(parsedTransaction.explicitExternalSpendAmount, isExplicit && expectExternal ? outputAmount : '0');
      should.equal(parsedTransaction.implicitExternalSpendAmount, !isExplicit && expectExternal ? outputAmount : '0');

      (coin.explainTransaction as any).restore();

      if (!txParams.changeAddress) {
        (coin.verifyAddress as any).restore();
      }
    }

    it('should classify outputs which spend change back to a v1 wallet base address as internal', async function () {
      return runClassifyOutputsTest(wallet.migratedFrom(), verification, false);
    });

    it(
      'should classify outputs which spend change back to a v1 wallet base address as external ' +
        'if considerMigratedFromAddressInternal is set and false',
      async function () {
        return runClassifyOutputsTest(
          wallet.migratedFrom(),
          { ...verification, considerMigratedFromAddressInternal: false },
          true
        );
      }
    );

    it('should classify outputs which spend to addresses not on the wallet as external', async function () {
      return runClassifyOutputsTest('2Mxjx4E2EEe4yJuLvdEuAdMUd4id1emPCZs', verification, true);
    });

    it('should accept a custom change address', async function () {
      const changeAddress = '2NAuziD75WnPPHJVwnd4ckgY4SuJaDVVbMD';
      return runClassifyOutputsTest(changeAddress, verification, false, { changeAddress, recipients: [] });
    });

    it('should classify outputs with external address in recipients as explicit', async function () {
      const externalAddress = '2NAuziD75WnPPHJVwnd4ckgY4SuJaDVVbMD';
      return runClassifyOutputsTest(externalAddress, verification, true, {
        recipients: [{ address: externalAddress, amount: outputAmount }],
      });
    });
  });

  describe('Custom Change Wallets', () => {
    const bitgo: BitGo = TestBitGo.decorate(BitGo, { env: 'mock' });
    const coin = bitgo.coin('tbtc') as AbstractUtxoCoin;

    const keys = {
      send: {
        user: { id: '0', key: coin.keychains().create() },
        backup: { id: '1', key: coin.keychains().create() },
        bitgo: { id: '2', key: coin.keychains().create() },
      },
      change: {
        user: { id: '3', key: coin.keychains().create() },
        backup: { id: '4', key: coin.keychains().create() },
        bitgo: { id: '5', key: coin.keychains().create() },
      },
    };

    const customChangeKeySignatures = {
      user: '',
      backup: '',
      bitgo: '',
    };

    const addressData = {
      chain: 11,
      index: 1,
      addressType: 'p2shP2wsh' as const,
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      keychains: [
        { pub: keys.change.user.key.pub! },
        { pub: keys.change.backup.key.pub! },
        { pub: keys.change.bitgo.key.pub! },
      ],
      threshold: 2,
    };

    const { address: changeAddress, coinSpecific } = coin.generateAddress(addressData);

    const changeWalletId = 'changeWalletId';
    const stubData = {
      signedSendingWallet: {
        keyIds: sinon.stub().returns([keys.send.user.id, keys.send.backup.id, keys.send.bitgo.id]),
        coinSpecific: sinon.stub().returns({ customChangeWalletId: changeWalletId }),
      },
      changeWallet: {
        keyIds: sinon.stub().returns([keys.change.user.id, keys.change.backup.id, keys.change.bitgo.id]),
        createAddress: sinon.stub().resolves(changeAddress),
      },
    };

    before(async () => {
      // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
      const sign = async ({ key }) =>
        (await coin.signMessage({ prv: keys.send.user.key.prv }, key.pub!)).toString('hex');
      customChangeKeySignatures.user = await sign(keys.change.user);
      customChangeKeySignatures.backup = await sign(keys.change.backup);
      customChangeKeySignatures.bitgo = await sign(keys.change.bitgo);
    });

    it('should consider addresses derived from the custom change keys as internal spends', async () => {
      const signedSendingWallet = sinon.createStubInstance(Wallet, stubData.signedSendingWallet as any);
      const changeWallet = sinon.createStubInstance(Wallet, stubData.changeWallet as any);

      sinon.stub(coin, 'keychains').returns({
        get: sinon.stub().callsFake(({ id }) => {
          switch (id) {
            case keys.send.user.id:
              return Promise.resolve({ id, ...keys.send.user.key });
            case keys.send.backup.id:
              return Promise.resolve({ id, ...keys.send.backup.key });
            case keys.send.bitgo.id:
              return Promise.resolve({ id, ...keys.send.bitgo.key });
            case keys.change.user.id:
              return Promise.resolve({ id, ...keys.change.user.key });
            case keys.change.backup.id:
              return Promise.resolve({ id, ...keys.change.backup.key });
            case keys.change.bitgo.id:
              return Promise.resolve({ id, ...keys.change.bitgo.key });
          }
        }),
      } as any);

      sinon.stub(coin, 'wallets').returns({
        get: sinon.stub().callsFake(() => Promise.resolve(changeWallet)),
      } as any);

      const outputAmount = 10000;
      const recipients = [];

      sinon.stub(coin, 'explainTransaction').resolves({
        outputs: [],
        changeOutputs: [
          {
            address: changeAddress,
            amount: outputAmount,
          },
        ],
      } as any);

      signedSendingWallet._wallet = signedSendingWallet._wallet || { customChangeKeySignatures };

      const parsedTransaction = await coin.parseTransaction({
        txParams: { changeAddress, recipients },
        txPrebuild: { txHex: '' },
        wallet: signedSendingWallet as any,
        verification: {
          addresses: {
            [changeAddress]: {
              coinSpecific,
              chain: addressData.chain,
              index: addressData.index,
            },
          },
        },
      });

      should.exist(parsedTransaction.outputs[0]);
      parsedTransaction.outputs[0].should.deepEqual({
        address: changeAddress,
        amount: outputAmount,
        external: false,
        needsCustomChangeKeySignatureVerification: true,
      });

      (coin.explainTransaction as any).restore();
      (coin.wallets as any).restore();
      (coin.keychains as any).restore();
    });
  });

  describe('Verify Transaction', () => {
    const bitgo: BitGo = TestBitGo.decorate(BitGo, { env: 'mock' });
    const coin = bitgo.coin('tbtc') as AbstractUtxoCoin;

    const userKeychain = coin.keychains().create();
    const otherKeychain = coin.keychains().create();

    const changeKeys = {
      user: coin.keychains().create(),
      backup: coin.keychains().create(),
      bitgo: coin.keychains().create(),
    };

    // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
    const sign = async (key, keychain) => (await coin.signMessage(keychain, key.pub!)).toString('hex');
    const signUser = (key) => sign(key, userKeychain);
    const signOther = (key) => sign(key, otherKeychain);
    const passphrase = 'test_passphrase';

    const stubData = {
      unsignedSendingWallet: {
        keyIds: sinon.stub().returns(['0', '1', '2']),
      },
      parseTransactionData: {
        badKey: {
          keychains: {
            // user public key swapped out
            user: {
              pub: otherKeychain.pub,
              encryptedPrv: bitgo.encrypt({ input: userKeychain.prv, password: passphrase }),
            },
          },
          needsCustomChangeKeySignatureVerification: true,
        },
        noCustomChange: {
          keychains: { user: userKeychain },
          needsCustomChangeKeySignatureVerification: true,
        },
        emptyCustomChange: {
          keychains: { user: userKeychain },
          needsCustomChangeKeySignatureVerification: true,
          customChange: {},
        },
        // needs to be async function to create signatures
        badSigs: async () => ({
          keychains: { user: userKeychain },
          needsCustomChangeKeySignatureVerification: true,
          customChange: {
            keys: [changeKeys.user, changeKeys.backup, changeKeys.bitgo],
            signatures: [
              await signOther(changeKeys.user),
              await signOther(changeKeys.backup),
              await signOther(changeKeys.bitgo),
            ],
          },
        }),
        goodSigs: async () => ({
          keychains: { user: userKeychain },
          needsCustomChangeKeySignatureVerification: true,
          customChange: {
            keys: [changeKeys.user, changeKeys.backup, changeKeys.bitgo],
            signatures: [
              await signUser(changeKeys.user),
              await signUser(changeKeys.backup),
              await signUser(changeKeys.bitgo),
            ],
          },
          missingOutputs: 1,
        }),
      },
    };

    const unsignedSendingWallet = sinon.createStubInstance(Wallet, stubData.unsignedSendingWallet as any);

    it('should fail if the user private key cannot be verified to match the user public key', async () => {
      sinon.stub(coin, 'parseTransaction').resolves(stubData.parseTransactionData.badKey as any);
      const verifyWallet = sinon.createStubInstance(Wallet, {});

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {},
          wallet: verifyWallet as any,
          verification: {},
        })
        .should.be.rejectedWith(
          /transaction requires verification of user public key, but it was unable to be verified/
        );

      (coin.parseTransaction as any).restore();
    });

    it('should fail if the custom change verification data is required but missing', async () => {
      sinon.stub(coin, 'parseTransaction').resolves(stubData.parseTransactionData.noCustomChange as any);

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {},
          wallet: unsignedSendingWallet as any,
          verification: {},
        })
        .should.be.rejectedWith(/parsed transaction is missing required custom change verification data/);

      (coin.parseTransaction as any).restore();
    });

    it('should fail if the custom change keys or key signatures are missing', async () => {
      sinon.stub(coin, 'parseTransaction').resolves(stubData.parseTransactionData.emptyCustomChange as any);

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {},
          wallet: unsignedSendingWallet as any,
          verification: {},
        })
        .should.be.rejectedWith(/customChange property is missing keys or signatures/);

      (coin.parseTransaction as any).restore();
    });

    it('should fail if the custom change key signatures cannot be verified', async () => {
      sinon.stub(coin, 'parseTransaction').resolves((await stubData.parseTransactionData.badSigs()) as any);

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {},
          wallet: unsignedSendingWallet as any,
          verification: {},
        })
        .should.be.rejectedWith(
          /transaction requires verification of custom change key signatures, but they were unable to be verified/
        );

      (coin.parseTransaction as any).restore();
    });

    it('should successfully verify a custom change transaction when change keys and signatures are valid', async () => {
      sinon.stub(coin, 'parseTransaction').resolves((await stubData.parseTransactionData.goodSigs()) as any);

      // if verify transaction gets rejected with the outputs missing error message,
      // then we know that the verification of the custom change key signatures was successful
      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {},
          wallet: unsignedSendingWallet as any,
          verification: {},
        })
        .should.be.rejectedWith(/expected outputs missing in transaction prebuild/);

      (coin.parseTransaction as any).restore();
    });

    it('should not allow more than 150 basis points of implicit external outputs (for paygo outputs)', async () => {
      const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
        keychains: {} as any,
        keySignatures: {},
        outputs: [],
        missingOutputs: [],
        explicitExternalOutputs: [],
        implicitExternalOutputs: [],
        changeOutputs: [],
        explicitExternalSpendAmount: 10000,
        implicitExternalSpendAmount: 151,
        needsCustomChangeKeySignatureVerification: false,
      });

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {},
          wallet: unsignedSendingWallet as any,
        })
        .should.be.rejectedWith('prebuild attempts to spend to unintended external recipients');

      coinMock.restore();
    });

    it('should allow 150 basis points of implicit external outputs (for paygo outputs)', async () => {
      const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
        keychains: {} as any,
        keySignatures: {},
        outputs: [],
        missingOutputs: [],
        explicitExternalOutputs: [],
        implicitExternalOutputs: [],
        changeOutputs: [],
        explicitExternalSpendAmount: 1000,
        implicitExternalSpendAmount: 15,
        needsCustomChangeKeySignatureVerification: false,
      });

      const bitcoinMock = sinon
        .stub(coin, 'createTransactionFromHex')
        .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction);

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {
            txHex: '00',
          },
          wallet: unsignedSendingWallet as any,
        })
        .should.eventually.be.true();

      coinMock.restore();
      bitcoinMock.restore();
    });

    it('should not allow any implicit external outputs if paygo outputs are disallowed', async () => {
      const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
        keychains: {} as any,
        keySignatures: {},
        outputs: [],
        missingOutputs: [],
        explicitExternalOutputs: [],
        implicitExternalOutputs: [],
        changeOutputs: [],
        explicitExternalSpendAmount: 0,
        implicitExternalSpendAmount: 10,
        needsCustomChangeKeySignatureVerification: false,
      });

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {
            txHex: '00',
          },
          wallet: unsignedSendingWallet as any,
          verification: {
            allowPaygoOutput: false,
          },
        })
        .should.be.rejectedWith('prebuild attempts to spend to unintended external recipients');

      coinMock.restore();
    });

    it('should allow paygo outputs if empty verification object is passed', async () => {
      const coinMock = sinon.stub(coin, 'parseTransaction').resolves({
        keychains: {} as any,
        keySignatures: {},
        outputs: [],
        missingOutputs: [],
        explicitExternalOutputs: [],
        implicitExternalOutputs: [],
        changeOutputs: [],
        explicitExternalSpendAmount: 1000,
        implicitExternalSpendAmount: 15,
        needsCustomChangeKeySignatureVerification: false,
      });

      const bitcoinMock = sinon
        .stub(coin, 'createTransactionFromHex')
        .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction);

      await coin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {
            txHex: '00',
          },
          wallet: unsignedSendingWallet as any,
          verification: {},
        })
        .should.eventually.be.true();

      coinMock.restore();
      bitcoinMock.restore();
    });

    it('should work with bigint amounts', async () => {
      // need a coin that uses bigint
      const bigintCoin = bitgo.coin('tdoge') as AbstractUtxoCoin;

      const coinMock = sinon.stub(bigintCoin, 'parseTransaction').resolves({
        keychains: {} as any,
        keySignatures: {},
        outputs: [],
        missingOutputs: [],
        explicitExternalOutputs: [
          {
            address: 'external_address',
            amount: '10000',
          },
        ],
        implicitExternalOutputs: [
          {
            address: 'external_address_2',
            amount: '15',
          },
        ],
        changeOutputs: [],
        explicitExternalSpendAmount: BigInt(10000),
        implicitExternalSpendAmount: BigInt(15),
        needsCustomChangeKeySignatureVerification: false,
      });

      const bitcoinMock = sinon
        .stub(bigintCoin, 'createTransactionFromHex')
        .returns({ ins: [] } as unknown as utxolib.bitgo.UtxoTransaction);

      await bigintCoin
        .verifyTransaction({
          txParams: {
            walletPassphrase: passphrase,
          },
          txPrebuild: {
            txHex: '00',
          },
          wallet: unsignedSendingWallet as any,
          verification: {},
        })
        .should.eventually.be.true();

      coinMock.restore();
      bitcoinMock.restore();
    });
  });
});

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


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