PHP WebShell

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

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

/**
 * @prettier
 */

import * as _ from 'lodash';
import * as should from 'should';
import { bip32 } from '@bitgo/utxo-lib';
import { TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../../../src/bitgo';
import { getBuilder, Eth } from '@bitgo/account-lib';
import * as ethAbi from 'ethereumjs-abi';
import * as ethUtil from 'ethereumjs-util';
import { coins, ContractAddressDefinedToken } from '@bitgo/statics';
import { BaseTransaction, TransactionType } from '@bitgo/sdk-core';

describe('ETH-like coins', () => {
  _.forEach(['tetc', 'tcelo', 'trbtc'], (coinName) => {
    describe(`${coinName}`, () => {
      let bitgo;
      let basecoin;
      let coin;

      const sendMultisigTypes = ['address', 'uint256', 'bytes', 'uint256', 'uint256', 'bytes'];
      const sendMultisigTokenTypes = ['address', 'uint256', 'address', 'uint256', 'uint256', 'bytes'];
      const signatureSaltMap = {
        native: {
          tetc: 'ETC',
          tcelo: 'CELO',
          trbtc: 'RSK',
        },
        token: {
          tetc: 'ETC-ERC20',
          tcelo: 'CELO-ERC20',
          trbtc: 'RSK-ERC20',
        },
      };

      /**
       * Get the operation hash that the user key signed
       * @param tx The transaction to calculate operatino hash from
       * @return The operation hash
       */
      const getOperationHash = (tx: BaseTransaction): string => {
        const { data } = tx.toJson();
        const { tokenContractAddress, expireTime, sequenceId, amount, to } = Eth.Utils.decodeTransferData(data);

        if (coin instanceof ContractAddressDefinedToken) {
          return ethAbi.soliditySHA3(
            ...[
              ['string', 'address', 'uint', 'address', 'uint', 'uint'],
              [
                signatureSaltMap.token[coinName],
                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                // @ts-ignore BG-34579: known compatibility issue with @types/ethereumjs-util
                new ethUtil.BN(ethUtil.stripHexPrefix(to), 16),
                amount,
                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                // @ts-ignore BG-34579: known compatibility issue with @types/ethereumjs-util
                new ethUtil.BN(ethUtil.stripHexPrefix(tokenContractAddress), 16),
                expireTime,
                sequenceId,
              ],
            ]
          );
        } else {
          return ethAbi.soliditySHA3(
            ...[
              ['string', 'address', 'uint', 'uint', 'uint'],
              [
                signatureSaltMap.native[coinName],
                // eslint-disable-next-line @typescript-eslint/ban-ts-comment
                // @ts-ignore BG-34579: known compatibility issue with @types/ethereumjs-util
                new ethUtil.BN(ethUtil.stripHexPrefix(to), 16),
                amount,
                expireTime,
                sequenceId,
              ],
            ]
          );
        }
      };

      /**
       * Recover the signing address of a signature
       * @param tx The transaction to recover a signer from
       * @return The eth address of the signer
       */
      const recoverSigner = function (tx: BaseTransaction) {
        const { signature } = Eth.Utils.decodeTransferData(tx.toJson().data);
        const { v, r, s } = ethUtil.fromRpcSig(signature);
        const operationHash = getOperationHash(tx);
        // eslint-disable-next-line @typescript-eslint/ban-ts-comment
        // @ts-ignore known compatibility issue with @types/ethereumjs-util
        const pubKeyBuffer = ethUtil.ecrecover(operationHash, v, r, s);
        return ethUtil.bufferToHex(ethUtil.pubToAddress(ethUtil.importPublic(pubKeyBuffer)));
      };

      /**
       * Build an unsigned account-lib multi-signature send transactino
       * @param destination The destination address of the transaction
       * @param contractAddress The address of the smart contract processing the transaction
       * @param contractSequenceId The sequence id of the contract
       * @param nonce The nonce of the sending address
       * @param expireTime The expire time of the transaction
       * @param amount The amount to send to the recipient
       * @param gasPrice The gas price of the transaction
       * @param gasLimit The gas limit of the transaction
       */
      const buildUnsignedTransaction = async function ({
        destination,
        contractAddress,
        contractSequenceId = 1,
        nonce = 0,
        expireTime = Math.floor(new Date().getTime() / 1000),
        amount = '100000',
        gasPrice = '10000',
        gasLimit = '20000',
      }) {
        const txBuilder: Eth.TransactionBuilder = getBuilder(coinName) as Eth.TransactionBuilder;
        txBuilder.type(TransactionType.Send);
        txBuilder.fee({
          fee: gasPrice,
          gasLimit: gasLimit,
        });
        txBuilder.counter(nonce);
        txBuilder.contract(contractAddress);
        const transferBuilder = txBuilder.transfer() as Eth.TransferBuilder;

        transferBuilder
          .coin(coinName)
          .expirationTime(expireTime)
          .amount(amount)
          .to(destination)
          .contractSequenceId(contractSequenceId);

        return await txBuilder.build();
      };

      before(function () {
        bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
        bitgo.initializeTestVars();
        basecoin = bitgo.coin(coinName);
        coin = coins.get(coinName);
      });

      describe('Is valid address', () => {
        it('Should find valid addresses to be valid', () => {
          basecoin.isValidAddress('0x2af9152fc4afd89a8124731bdfb8710c8751f3ed').should.equal(true);
          basecoin.isValidAddress('0x2af9152FC4afd89A8124731BdFb8710c8751f3eD').should.equal(true);
        });

        it('Should find invalid addresses to be invalid', () => {
          basecoin.isValidAddress('0x2af9152fc4afd89a8124731bdfb8710c8751f3edd').should.equal(false);
          basecoin.isValidAddress('0x2af9152fc4afd89a8124731bdfb8710c8751f3e').should.equal(false);
          basecoin.isValidAddress('2af9152fc4afd89a8124731bdfb8710c8751f3ed').should.equal(false);
          basecoin.isValidAddress('notanaddress').should.equal(false);
          basecoin.isValidAddress('not an address').should.equal(false);
          basecoin.isValidAddress('3KgL6DTUb6gEoqSwMMJzyf96ekH8oZtWtZ').should.equal(false);
        });

        xit('Should not throw when verifying valid addresses', function () {
          // FIXME(BG-43225): not implemented
        });

        xit('Should throw when verifying invalid addresses', function () {
          // FIXME(BG-43225): not implemented
        });
      });

      describe('Is valid pub', () => {
        it('Should find valid pubs to be valid', () => {
          basecoin
            .isValidPub(
              'xpub661MyMwAqRbcF9Nc7TbBo1rZAagiWEVPWKbDKThNG8zqjk76HAKLkaSbTn6dK2dQPfuD7xjicxCZVWvj67fP5nQ9W7QURmoMVAX8m6jZsGp'
            )
            .should.equal(true);
          basecoin
            .isValidPub(
              '04614C070C6D1C18A6A2D6EE2BBBE1FF291A0ABA8ED6B55023C03BE42583AC23A743BCB5EF9DB59E14FD7025A9A5D93C6BA89EEFEB40215BF24933D4F2935D14CB'
            )
            .should.equal(true);
          basecoin.isValidPub('034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa').should.equal(true);
        });

        it('Should find invalid pubs to be invalid', () => {
          basecoin.isValidPub('0x2af9152fc4afd89a8124731bdfb8710c8751f3e').should.equal(false);
          basecoin.isValidPub('0x2af9152fc4afd89a8124731bdfb8710c8751f3ed').should.equal(false);
          basecoin.isValidPub('2af9152fc4afd89a8124731bdfb8710c8751f3ed').should.equal(false);
          basecoin.isValidPub('notapub').should.equal(false);
          basecoin.isValidPub('not a pub').should.equal(false);
          basecoin.isValidPub('3KgL6DTUb6gEoqSwMMJzyf96ekH8oZtWtZ').should.equal(false);
        });
      });

      describe('Generate keypair', () => {
        it('Should generate valid keypair without seed', () => {
          const { pub, prv } = basecoin.generateKeyPair();
          basecoin.isValidPub(pub).should.equal(true);
          const bitgoKey = bip32.fromBase58(prv);
          basecoin.isValidPub(bitgoKey.neutered().toBase58()).should.equal(true);
        });

        it('Should generate valid keypair with seed', () => {
          const seed = Buffer.from('c3b09c24731be2851b641d9d5b3f60fa129695c24071768d15654bea207b7bb6', 'hex');
          const { pub, prv } = basecoin.generateKeyPair(seed);
          basecoin.isValidPub(pub).should.equal(true);
          const bitgoKey = bip32.fromBase58(prv);
          basecoin.isValidPub(bitgoKey.neutered().toBase58()).should.equal(true);
        });
      });

      describe('Sign transaction:', () => {
        const xprv =
          'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2';

        it('should sign transaction internally', async function () {
          const key = new Eth.KeyPair({ prv: xprv });
          const destination = '0xfaa8f14f46a99eb439c50e0c3b835cc21dad51b4';
          const contractAddress = '0x9e2c5712ab4caf402a98c4bf58c79a0dfe718ad1';
          const amount = '100000';
          const inputExpireTime = Math.floor(new Date().getTime() / 1000);
          const inputSequenceId = 1;

          const unsignedTransaction = await buildUnsignedTransaction({
            destination,
            contractAddress,
            amount,
            expireTime: inputExpireTime,
            contractSequenceId: inputSequenceId,
          });

          const tx = await basecoin.signTransaction({
            prv: key.getKeys().prv,
            txPrebuild: {
              txHex: unsignedTransaction.toBroadcastFormat(),
            },
          });

          const txBuilder = basecoin.getTransactionBuilder();
          txBuilder.from(tx.halfSigned.txHex);
          const transaction = await txBuilder.build();
          const txJson = transaction.toJson();
          txJson.to.should.equal(contractAddress);

          let decodedData;
          let recipient;
          let value;
          let data;
          let expireTime;
          let sequenceId;
          if (coin instanceof ContractAddressDefinedToken) {
            decodedData = ethAbi.rawDecode(sendMultisigTokenTypes, Buffer.from(txJson.data.slice(10), 'hex'));
            [recipient, value /* tokenContractAddress */, , expireTime, sequenceId] = decodedData;
            data = Buffer.from('');
          } else {
            decodedData = ethAbi.rawDecode(sendMultisigTypes, Buffer.from(txJson.data.slice(10), 'hex'));
            [recipient, value, data, expireTime, sequenceId] = decodedData;
          }
          ethUtil.addHexPrefix(recipient).should.equal(destination);
          value.toString(10).should.equal(amount);
          inputExpireTime.should.equal(parseInt(expireTime.toString('hex'), 16));
          inputSequenceId.should.equal(parseInt(sequenceId.toString('hex'), 16));
          data.length.should.equal(0);

          const recoveredAddress = recoverSigner(transaction);
          recoveredAddress.should.equal(key.getAddress());
        });

        it('should sign transaction internally with an xprv', async function () {
          const key = new Eth.KeyPair({ prv: xprv });
          const destination = '0xfaa8f14f46a99eb439c50e0c3b835cc21dad51b4';
          const contractAddress = '0x9e2c5712ab4caf402a98c4bf58c79a0dfe718ad1';
          const amount = '100000';
          const inputExpireTime = Math.floor(new Date().getTime() / 1000);
          const inputSequenceId = 1;

          const unsignedTransaction = await buildUnsignedTransaction({
            destination,
            contractAddress,
            amount,
            expireTime: inputExpireTime,
            contractSequenceId: inputSequenceId,
          });

          const tx = await basecoin.signTransaction({
            prv: xprv,
            txPrebuild: {
              txHex: unsignedTransaction.toBroadcastFormat(),
            },
          });

          const txBuilder = basecoin.getTransactionBuilder();
          txBuilder.from(tx.halfSigned.txHex);
          const transaction = await txBuilder.build();
          const txJson = transaction.toJson();
          txJson.to.should.equal(contractAddress);

          let decodedData;
          let recipient;
          let value;
          let data;
          let expireTime;
          let sequenceId;
          if (coin instanceof ContractAddressDefinedToken) {
            decodedData = ethAbi.rawDecode(sendMultisigTokenTypes, Buffer.from(txJson.data.slice(10), 'hex'));
            [recipient, value /* tokenContractAddress */, , expireTime, sequenceId] = decodedData;
            data = Buffer.from('');
          } else {
            decodedData = ethAbi.rawDecode(sendMultisigTypes, Buffer.from(txJson.data.slice(10), 'hex'));
            [recipient, value, data, expireTime, sequenceId] = decodedData;
          }

          ethUtil.addHexPrefix(recipient).should.equal(destination);
          value.toString(10).should.equal(amount);
          inputExpireTime.should.equal(parseInt(expireTime.toString('hex'), 16));
          inputSequenceId.should.equal(parseInt(sequenceId.toString('hex'), 16));
          data.length.should.equal(0);

          const recoveredAddress = recoverSigner(transaction);
          recoveredAddress.should.equal(key.getAddress());
        });

        it('should sign a half signed transaction', async function () {
          const key = new Eth.KeyPair({ prv: xprv });
          const destination = '0xfaa8f14f46a99eb439c50e0c3b835cc21dad51b4';
          const contractAddress = '0x9e2c5712ab4caf402a98c4bf58c79a0dfe718ad1';
          const amount = '100000';
          const inputExpireTime = Math.floor(new Date().getTime() / 1000);
          const inputSequenceId = 1;

          const unsignedTransaction = await buildUnsignedTransaction({
            destination,
            contractAddress,
            amount,
            expireTime: inputExpireTime,
            contractSequenceId: inputSequenceId,
          });

          const tx = await basecoin.signTransaction({
            prv: key.getKeys().prv,
            txPrebuild: {
              txHex: unsignedTransaction.toBroadcastFormat(),
            },
          });

          const fullySignedTx = await basecoin.signTransaction({
            prv: key.getKeys().prv,
            txPrebuild: {
              txHex: tx.halfSigned.txHex,
            },
          });

          fullySignedTx.halfSigned.recipients.length.should.equal(1);
          fullySignedTx.halfSigned.recipients[0].address.should.equal(destination);
          fullySignedTx.halfSigned.recipients[0].amount.should.equal(amount);

          const txBuilder = basecoin.getTransactionBuilder();
          txBuilder.from(fullySignedTx.halfSigned.txHex);
          const transaction = await txBuilder.build();
          const txJson = transaction.toJson();
          txJson.to.should.equal(contractAddress);

          let decodedData;
          let recipient;
          let value;
          let data;
          let expireTime;
          let sequenceId;
          if (coin instanceof ContractAddressDefinedToken) {
            decodedData = ethAbi.rawDecode(sendMultisigTokenTypes, Buffer.from(txJson.data.slice(10), 'hex'));
            [recipient, value /* tokenContractAddress */, , expireTime, sequenceId] = decodedData;
            data = Buffer.from('');
          } else {
            decodedData = ethAbi.rawDecode(sendMultisigTypes, Buffer.from(txJson.data.slice(10), 'hex'));
            [recipient, value, data, expireTime, sequenceId] = decodedData;
          }

          ethUtil.addHexPrefix(recipient).should.equal(destination);
          value.toString(10).should.equal(amount);
          inputExpireTime.should.equal(parseInt(expireTime.toString('hex'), 16));
          inputSequenceId.should.equal(parseInt(sequenceId.toString('hex'), 16));
          data.length.should.equal(0);

          const recoveredAddress = recoverSigner(transaction);
          recoveredAddress.should.equal(key.getAddress());
        });

        it('should fail to sign transaction with invalid tx hex', async function () {
          const key = new Eth.KeyPair({ prv: xprv });
          await basecoin
            .signTransaction({
              prv: key.getKeys().prv,
              txPrebuild: {
                txHex: '0xinvalid',
              },
            })
            .should.be.rejected();
        });
      });

      describe('Explain transaction:', () => {
        const xprv =
          'xprv9s21ZrQH143K3D8TXfvAJgHVfTEeQNW5Ys9wZtnUZkqPzFzSjbEJrWC1vZ4GnXCvR7rQL2UFX3RSuYeU9MrERm1XBvACow7c36vnz5iYyj2';

        it('should fail if the params object is missing parameters', async function () {
          const explainParams = {
            feeInfo: { fee: 1 },
            txHex: null,
          };
          await basecoin.explainTransaction(explainParams).should.be.rejectedWith('missing explain tx parameters');
        });

        it('explain an unsigned transfer transaction', async function () {
          const destination = '0xfaa8f14f46a99eb439c50e0c3b835cc21dad51b4';
          const contractAddress = '0x9e2c5712ab4caf402a98c4bf58c79a0dfe718ad1';

          const unsignedTransaction = await buildUnsignedTransaction({
            destination,
            contractAddress,
          });

          const explainParams = {
            halfSigned: {
              txHex: unsignedTransaction.toBroadcastFormat(),
            },
            feeInfo: { fee: 1 },
          };
          const explanation = await basecoin.explainTransaction(explainParams);
          should.exist(explanation.id);
          // TODO check other fields once account-lib properly explains transaction
        });

        it('explain a signed transfer transaction', async function () {
          const key = new Eth.KeyPair({ prv: xprv });
          const destination = '0xfaa8f14f46a99eb439c50e0c3b835cc21dad51b4';
          const contractAddress = '0x9e2c5712ab4caf402a98c4bf58c79a0dfe718ad1';

          const unsignedTransaction = await buildUnsignedTransaction({
            destination,
            contractAddress,
          });

          const signedTx = await basecoin.signTransaction({
            prv: key.getKeys().prv,
            txPrebuild: {
              txHex: unsignedTransaction.toBroadcastFormat(),
            },
          });

          const explainParams = {
            txHex: signedTx.halfSigned.txHex,
            feeInfo: { fee: 1 },
          };
          const explanation = await basecoin.explainTransaction(explainParams);
          should.exist(explanation.id);
          // TODO check other fields once account-lib properly explains transaction
        });
      });
    });
  });
});

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


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