PHP WebShell

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

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

import assert from 'assert';
import nock from 'nock';

import { BitGoAPI } from '@bitgo/sdk-api';
import { Wallet } from '@bitgo/sdk-core';
import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { coins } from '@bitgo/statics';
import { cvToString } from '@stacks/transactions';

import * as testData from '../fixtures';
import { Stx, StxLib, Tstx } from '../../src';
import { RecoveryInfo, RecoveryOptions, RecoveryTransaction } from '../../src/lib/iface';

const { KeyPair } = StxLib;

describe('STX:', function () {
  const coinName = 'stx';
  const coinNameTest = 'tstx';
  let bitgo: TestBitGoAPI;
  let basecoin;

  const badValidAddresses = [
    '',
    null,
    'abc',
    'SP244HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
    'ST1T758K6T2YRKG9Q0TJ16B6FP5QQREWZSESRS0PY',
  ];

  const goodAddresses = [
    'STB44HYPYAT2BB2QE513NSP81HTMYWBJP02HPGK6',
    'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y',
    'SP2T758K6T2YRKG9Q0TJ16B6FP5QQREWZSESRS0PY',
    'SM3W5QFWGPG1JC8R25EVZDEP3BESJZ831JPNNQFTZ',
    'SM3W5QFWGPG1JC8R25EVZDEP3BESJZ831JPNNQFTZ?memoId=1',
    'ST1WVJMS5VS41F0YMH7D2M0VHXRG4CY43ZJZBS60A?memoId=4',
  ];

  before(function () {
    bitgo = TestBitGo.decorate(BitGoAPI, {
      env: 'mock',
    });
    bitgo.initializeTestVars();
    bitgo.safeRegister('stx', Stx.createInstance);
    bitgo.safeRegister('tstx', Tstx.createInstance);
    basecoin = bitgo.coin(coinNameTest);
  });

  /**
   * Build an unsigned account-lib signle-signature send transaction
   * @param destination The destination address of the transaction
   * @param amount The amount to send to the recipient
   */
  const buildUnsignedTransaction = async function ({ destination, amount = '100000', publicKey, memo = '' }) {
    const factory = new StxLib.TransactionBuilderFactory(coins.get(coinName));
    const txBuilder = factory.getTransferBuilder();
    txBuilder.fee({
      fee: '180',
    });
    txBuilder.to(destination);
    txBuilder.amount(amount);
    txBuilder.nonce(1);
    txBuilder.fromPubKey(publicKey);
    txBuilder.memo(memo);
    txBuilder.numberSignatures(1);
    return await txBuilder.build();
  };

  /**
   * Build an unsigned account-lib multi-signature send transaction
   * @param destination The destination address of the transaction
   * @param amount The amount to send to the recipient
   */
  const buildmultiSigUnsignedTransaction = async function ({ destination, amount = '100000', publicKeys, memo = '' }) {
    const factory = new StxLib.TransactionBuilderFactory(coins.get(coinName));
    const txBuilder = factory.getTransferBuilder();
    txBuilder.fee({
      fee: '180',
    });
    txBuilder.to(destination);
    txBuilder.amount(amount);
    txBuilder.nonce(1);
    txBuilder.fromPubKey(publicKeys);
    txBuilder.numberSignatures(2);
    txBuilder.memo(memo);
    return await txBuilder.build();
  };

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

    localBasecoin = bitgo.coin('stx');
    localBasecoin.should.be.an.instanceof(Stx);
  });

  it('should check valid addresses', function () {
    badValidAddresses.map((addr) => {
      basecoin.isValidAddress(addr).should.equal(false);
    });
    goodAddresses.map((addr) => {
      basecoin.isValidAddress(addr).should.equal(true);
    });
  });

  it('should verify isWalletAddress', async function () {
    const userKey = {
      pub: 'xpub661MyMwAqRbcGS2HMdvANN7o8ESWqwvr5U4ry5fZdD9VHhymWyfoDQF4vzfKotXgGtJTrwrFRz7XbGFov4FqdKKo6mRYNWvMp7P23DjuJnS',
    };
    const backupKey = {
      pub: 'xpub661MyMwAqRbcFEzr5CcpFzPG45rmPf75DTvDobN5gJimCatbHtzR53SbHzDZ1J56byKSsdc8vSujGuQpyPjb7Lsua2NfADJewPxNzL3N6Tj',
    };
    const bitgoKey = {
      pub: 'xpub661MyMwAqRbcGP1adk34VzRQJEMX25rCxjEyU9YFFWNhWNzwPoqgjLoKfnqotLwrz7kBevWbRZnqTSQrQDuJuYUQaDQ5DDPEzEXMwPS9PEf',
    };
    const keychains = [userKey, backupKey, bitgoKey];
    const validAddress1 = 'SNAYQFZ6EF54D5XWJP3GAE1Y8DPYXKFC7TTMYXFV';
    const validAddress2 = 'SNAYQFZ6EF54D5XWJP3GAE1Y8DPYXKFC7TTMYXFV?memoId=2';
    const unrelatedValidAddress = 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y?memoId=1';
    const invalidAddress = 'ST1T758K6T2YRKG9Q0TJ16B6FP5QQREWZSESRS0PY';
    (await basecoin.isWalletAddress({ address: validAddress1, keychains })).should.true();
    (await basecoin.isWalletAddress({ address: validAddress2, keychains })).should.true();
    (await basecoin.isWalletAddress({ address: unrelatedValidAddress, keychains })).should.false();

    assert.rejects(
      async () => basecoin.isWalletAddress({ address: invalidAddress, keychains }),
      `invalid address ${invalidAddress}`
    );
  });

  it('should explain a transfer transaction', async function () {
    const explain = await basecoin.explainTransaction({
      txHex: testData.txForExplainTransfer,
      feeInfo: { fee: '' },
    });
    explain.id.should.equal(testData.txExplainedTransfer.id);
    explain.outputAmount.should.equal(testData.txExplainedTransfer.outputAmount);
    explain.outputs[0].amount.should.equal(testData.txExplainedTransfer.outputAmount);
    explain.outputs[0].address.should.equal(testData.txExplainedTransfer.recipient);
    explain.outputs[0].memo.should.equal(testData.txExplainedTransfer.memo);
    explain.fee.should.equal(testData.txExplainedTransfer.fee);
    explain.changeAmount.should.equal('0');
  });

  it('should explain an unsigned transaction', async function () {
    const key = new KeyPair();
    const destination = 'ST11NJTTKGVT6D1HY4NJRVQWMQM7TVAR091EJ8P2Y';
    const amount = '100000';
    const memo = 'i cannot be broadcast';

    const unsignedTransaction = await buildUnsignedTransaction({
      destination,
      amount,
      publicKey: key.getKeys().pub,
      memo: memo,
    });
    const unsignedHex = unsignedTransaction.toBroadcastFormat();

    const explain = await basecoin.explainTransaction({
      txHex: unsignedHex,
      publicKeys: [key.getKeys().pub],
      feeInfo: { fee: '' },
    });

    explain.memo.should.equal(memo);
    explain.outputs[0].amount.should.equal(amount);
    explain.outputs[0].address.should.equal(destination);
  });

  it('should explain unsigned transfer transaction hex', async function () {
    const explain = await basecoin.explainTransaction({
      txHex: testData.unsignedTxForExplainTransfer,
      publicKeys: ['03797dd653040d344fd048c1ad05d4cbcb2178b30c6a0c4276994795f3e833da41'],
      feeInfo: { fee: '' },
    });
    explain.outputAmount.should.equal(testData.unsignedTxExplainedTransfer.outputAmount);
    explain.outputs[0].amount.should.equal(testData.unsignedTxExplainedTransfer.outputAmount);
    explain.outputs[0].address.should.equal(testData.unsignedTxExplainedTransfer.recipient);
    explain.outputs[0].memo.should.equal(testData.unsignedTxExplainedTransfer.memo);
    explain.fee.should.equal(testData.unsignedTxExplainedTransfer.fee);
    explain.changeAmount.should.equal('0');
  });

  it('should explain a contract call transaction', async function () {
    const explain = await basecoin.explainTransaction({
      txHex: testData.txForExplainContract,
      feeInfo: { fee: '' },
    });
    explain.id.should.equal(testData.txExplainedContract.id);
    explain.fee.should.equal(testData.txExplainedContract.fee);
    explain.contractAddress.should.equal(testData.txExplainedContract.contractAddress);
    explain.contractName.should.equal(testData.txExplainedContract.contractName);
    explain.contractFunction.should.equal(testData.txExplainedContract.functionName);
    explain.contractFunctionArgs[0].type.should.equal(testData.txExplainedContract.functionArgs[0].type);
    explain.contractFunctionArgs[0].value.toString().should.equal(testData.txExplainedContract.functionArgs[0].value);
  });

  it('should explain a fungible token transfer transaction with memo', async function () {
    const explain = await basecoin.explainTransaction({
      txHex: testData.txForExplainFungibleTokenTransfer,
      feeInfo: { fee: '' },
    });
    explain.id.should.equal(testData.fungibleTokenTransferTx.id);
    explain.fee.should.equal(testData.fungibleTokenTransferTx.fee);
    explain.memo.should.equal('1');
    explain.outputAmount.should.equal(testData.fungibleTokenTransferTx.functionArgs[2].value);
    explain.outputs[0].amount.should.equal(testData.fungibleTokenTransferTx.functionArgs[2].value);
    explain.outputs[0].address.should.equal(cvToString(testData.fungibleTokenTransferTx.functionArgs[1]));
    explain.outputs[0].memo.should.equal('1');
    explain.outputs[0].tokenName.should.equal(testData.fungibleTokenTransferTx.tokenName);
  });

  it('should explain a fungible token transfer transaction without memo', async function () {
    const explain = await basecoin.explainTransaction({
      txHex: testData.txForExplainFungibleTokenTransferWithoutMemo,
      feeInfo: { fee: '' },
    });
    explain.id.should.equal(testData.hexWithoutMemoTransferId);
    explain.fee.should.equal(testData.fungibleTokenTransferTx.fee);
    assert.deepEqual(explain.memo, undefined, 'memo should be undefined');
    explain.outputAmount.should.equal(testData.fungibleTokenTransferTx.functionArgs[2].value);
    explain.outputs[0].amount.should.equal(testData.fungibleTokenTransferTx.functionArgs[2].value);
    explain.outputs[0].address.should.equal(cvToString(testData.fungibleTokenTransferTx.functionArgs[1]));
    assert.deepEqual(explain.outputs[0].memo, undefined, 'memo should be undefined');
    explain.outputs[0].tokenName.should.equal(testData.fungibleTokenTransferTx.tokenName);
  });

  describe('Keypairs:', () => {
    it('should generate a keypair from random seed', function () {
      const keyPair = basecoin.generateKeyPair();
      keyPair.should.have.property('pub');
      keyPair.should.have.property('prv');
    });

    it('should generate a keypair from a seed', function () {
      const seedText =
        '80350b4208d381fbfe2276a326603049fe500731c46d3c9936b5ce036b51377f24bab7dd0c2af7f107416ef858ff79b0670c72406dad064e72bb17fc0a9038bb';
      const seed = Buffer.from(seedText, 'hex');
      const keyPair = basecoin.generateKeyPair(seed);
      keyPair.pub.should.equal(
        'xpub661MyMwAqRbcFAwqvSGbk35kJf7CQqdN1w4CMUBBTqH5e3ivjU6D8ugv9hRSgRbRenC4w3ahXdLVahwjgjXhSuQKMdNdn55Y9TNSagBktws'
      );
      keyPair.prv.should.equal(
        'xprv9s21ZrQH143K2gsNpQjbNu91kdGi1NuWei8bZ5mZuVk6mFPnBvmxb7NSJQdbZW3FGpK3Ycn7jorAXcEzMvviGtbyBz5tBrjfnWyQp3g75FK'
      );
    });
  });

  describe('Sign transaction:', () => {
    it('should sign transaction', async function () {
      const key = new KeyPair({
        prv: '21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb752601',
      });
      const destination = 'STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0';
      const amount = '100000';

      const unsignedTransaction = await buildUnsignedTransaction({
        destination,
        amount,
        publicKey: key.getKeys().pub,
      });
      const tx = await basecoin.signTransaction({
        prv: key.getKeys().prv!.toString(),
        pubKeys: [key.getKeys().pub],
        txPrebuild: {
          txHex: unsignedTransaction.toBroadcastFormat(),
        },
      });
      const factory = new StxLib.TransactionBuilderFactory(coins.get(coinName));
      const txBuilder = factory.from(tx.halfSigned.txHex);
      const signedTx = await txBuilder.build();
      const txJson = signedTx.toJson();
      txJson.payload.to.should.equal(destination);
      txJson.payload.amount.should.equal(amount);
      signedTx.signature.length.should.equal(1);
    });

    it('should sign multisig transaction', async function () {
      const key1 = new KeyPair({
        prv: '21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb752601',
      });
      const key2 = new KeyPair({
        prv: 'c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01',
      });
      const key3 = new KeyPair({
        prv: 'e75dcb66f84287eaf347955e94fa04337298dbd95aa0dbb985771104ef1913db01',
      });
      const destination = 'STDE7Y8HV3RX8VBM2TZVWJTS7ZA1XB0SSC3NEVH0';
      const amount = '100000';
      const publicKeys = [key1.getKeys(true).pub, key2.getKeys(true).pub, key3.getKeys(true).pub];
      const unsignedTransaction = await buildmultiSigUnsignedTransaction({
        destination,
        amount,
        publicKeys,
      });
      const tx = await basecoin.signTransaction({
        prv: [
          '21d43d2ae0da1d9d04cfcaac7d397a33733881081f0b2cd038062cf0ccbb752601',
          'c71700b07d520a8c9731e4d0f095aa6efb91e16e25fb27ce2b72e7b698f8127a01',
        ],
        pubKeys: [key1.getKeys().pub, key2.getKeys().pub, key3.getKeys().pub],
        numberSignature: 2,
        txPrebuild: {
          txHex: unsignedTransaction.toBroadcastFormat(),
        },
      });
      const factory = new StxLib.TransactionBuilderFactory(coins.get(coinName));
      const txBuilder = factory.from(tx.txHex);
      const signedTx = await txBuilder.build();
      const txJson = signedTx.toJson();
      txJson.payload.to.should.equal(destination);
      txJson.payload.amount.should.equal(amount);
    });
  });

  describe('getSigningPayload', function () {
    it('should return the tx as a buffer', async function () {
      const nonTSSCoin = bitgo.coin('tstx');
      const bufferTx = await nonTSSCoin.getSignablePayload(testData.unsignedTxForExplainTransfer);
      bufferTx.should.be.deepEqual(Buffer.from(testData.unsignedTxForExplainTransfer));
    });
  });

  describe('Verify Transaction', function () {
    const address1 = '0x174cfd823af8ce27ed0afee3fcf3c3ba259116be';
    const address2 = '0x7e85bdc27c050e3905ebf4b8e634d9ad6edd0de6';
    it('should reject a txPrebuild with more than one recipient', async function () {
      const wallet = new Wallet(bitgo, basecoin, {});

      const txParams = {
        recipients: [
          { amount: '1000000000000', address: address1 },
          { amount: '2500000000000', address: address2 },
        ],
        wallet: wallet,
        walletPassphrase: 'fakeWalletPassphrase',
      };

      await basecoin
        .verifyTransaction({ txParams })
        .should.be.rejectedWith(
          `tstx doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
        );
    });
  });

  describe('Recover Transaction STX', function () {
    before(function () {
      nock.enableNetConnect();
    });
    beforeEach(function () {
      nock.cleanAll();
    });
    after(function () {
      nock.disableNetConnect();
    });

    it('should build a signed recover transaction when private key data is passed', async function () {
      const rootAddress = testData.HOT_WALLET_ROOT_ADDRESS;
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v2/addresses/${rootAddress}/balances/stx`)
        .reply(200, testData.ACCOUNT_BALANCE_RESPONSE);
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v1/address/${rootAddress}/nonces`)
        .reply(200, testData.ACCOUNT_NONCE_RESPONSE);
      nock(`https://api.testnet.hiro.so`, { allowUnmocked: true })
        .post(`/v2/fees/transaction`, testData.FEE_ESTIMATION_REQUEST)
        .reply(200, testData.FEE_ESTIMATION_RESPONSE);

      const recoveryOptions: RecoveryOptions = {
        backupKey: testData.HOT_WALLET_KEY_CARD_INFO.BACKUP_KEY,
        userKey: testData.HOT_WALLET_KEY_CARD_INFO.USER_KEY,
        rootAddress: rootAddress,
        recoveryDestination: testData.DESTINATION_ADDRESS_WRW,
        bitgoKey: testData.HOT_WALLET_KEY_CARD_INFO.BITGO_PUB_KEY,
        walletPassphrase: testData.HOT_WALLET_KEY_CARD_INFO.WALLET_PASSPHRASE,
      };
      const response: RecoveryTransaction = await basecoin.recover(recoveryOptions);
      response.should.have.property('txHex');
      assert.deepEqual(response.txHex, testData.HOT_WALLET_RECOVERY_TX_HEX, 'tx hex not matching!');
    });

    it('should build an unsigned transaction when public keys are passed', async function () {
      const rootAddress = testData.COLD_WALLET_ROOT_ADDRESS;
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v2/addresses/${rootAddress}/balances/stx`)
        .reply(200, testData.ACCOUNT_BALANCE_RESPONSE);
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v1/address/${rootAddress}/nonces`)
        .reply(200, testData.ACCOUNT_NONCE_RESPONSE);
      nock(`https://api.testnet.hiro.so`, { allowUnmocked: true })
        .post(`/v2/fees/transaction`, testData.FEE_ESTIMATION_REQUEST)
        .reply(200, testData.FEE_ESTIMATION_RESPONSE);

      const recoveryOptions: RecoveryOptions = {
        backupKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.BACKUP_KEY,
        userKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.USER_KEY,
        rootAddress: rootAddress,
        recoveryDestination: testData.DESTINATION_ADDRESS_WRW,
        bitgoKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.BITGO_PUB_KEY,
      };
      const response: RecoveryInfo = await basecoin.recover(recoveryOptions);
      response.should.have.property('txHex');
      response.should.have.property('coin');
      response.should.have.property('feeInfo');
      assert.deepEqual(response.txHex, testData.COLD_WALLET_UNSIGNED_SWEEP_TX_HEX, 'tx hex not matching!');
      assert.deepEqual(response.coin, 'tstx', 'coin not matching!');
    });

    it('should throw invalid root address when root address is missing or invalid', async function () {
      const recoveryOptions: RecoveryOptions = {
        backupKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.BACKUP_KEY,
        userKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.USER_KEY,
        rootAddress: '',
        recoveryDestination: testData.DESTINATION_ADDRESS_WRW,
        bitgoKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.BITGO_PUB_KEY,
      };
      await basecoin.recover(recoveryOptions).should.rejectedWith('invalid root address!');
    });

    it('should throw invalid destination address when destination address is missing or invalid', async function () {
      const recoveryOptions: RecoveryOptions = {
        backupKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.BACKUP_KEY,
        userKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.USER_KEY,
        rootAddress: testData.COLD_WALLET_ROOT_ADDRESS,
        recoveryDestination: '',
        bitgoKey: testData.COLD_WALLET_PUBLIC_KEY_INFO.BITGO_PUB_KEY,
      };
      await basecoin.recover(recoveryOptions).should.rejectedWith('invalid destination address!');
    });

    it("should fail with no balance when root address doesn't have balance", async function () {
      const rootAddress = testData.HOT_WALLET_ROOT_ADDRESS;
      const stxBalance = JSON.parse(JSON.stringify(testData.ACCOUNT_BALANCE_RESPONSE));
      stxBalance.balance = '0';
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v2/addresses/${rootAddress}/balances/stx`)
        .reply(200, stxBalance);
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v1/address/${rootAddress}/nonces`)
        .reply(200, testData.ACCOUNT_NONCE_RESPONSE);
      const recoveryOptions: RecoveryOptions = {
        backupKey: testData.HOT_WALLET_KEY_CARD_INFO.BACKUP_KEY,
        userKey: testData.HOT_WALLET_KEY_CARD_INFO.USER_KEY,
        rootAddress: rootAddress,
        recoveryDestination: testData.DESTINATION_ADDRESS_WRW,
        bitgoKey: testData.HOT_WALLET_KEY_CARD_INFO.BITGO_PUB_KEY,
        walletPassphrase: testData.HOT_WALLET_KEY_CARD_INFO.WALLET_PASSPHRASE,
      };
      await basecoin
        .recover(recoveryOptions)
        .should.rejectedWith(`could not find any balance to recover for ${rootAddress}`);
    });

    it('should fail with insufficient balance when stx balance is lower than fee', async function () {
      const rootAddress = testData.HOT_WALLET_ROOT_ADDRESS;
      // deep clone to stop mutation
      const accountBalance = JSON.parse(JSON.stringify(testData.ACCOUNT_BALANCE_RESPONSE));
      accountBalance.balance = '100'; // set balance lower than fee
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v2/addresses/${rootAddress}/balances/stx`)
        .reply(200, accountBalance);
      nock(`https://api.testnet.hiro.so`)
        .get(`/extended/v1/address/${rootAddress}/nonces`)
        .reply(200, testData.ACCOUNT_NONCE_RESPONSE);
      const feeRequestBody = testData.FEE_ESTIMATION_REQUEST;
      feeRequestBody.transaction_payload =
        '00051a1500a1c42f0c11bfe3893f479af18904677685be000000000000006400000000000000000000000000000000000000000000000000000000000000000000';
      nock(`https://api.testnet.hiro.so`, { allowUnmocked: true })
        .post(`/v2/fees/transaction`, feeRequestBody)
        .reply(200, testData.FEE_ESTIMATION_RESPONSE);
      const recoveryOptions: RecoveryOptions = {
        backupKey: testData.HOT_WALLET_KEY_CARD_INFO.BACKUP_KEY,
        userKey: testData.HOT_WALLET_KEY_CARD_INFO.USER_KEY,
        rootAddress: rootAddress,
        recoveryDestination: testData.DESTINATION_ADDRESS_WRW,
        bitgoKey: testData.HOT_WALLET_KEY_CARD_INFO.BITGO_PUB_KEY,
        walletPassphrase: testData.HOT_WALLET_KEY_CARD_INFO.WALLET_PASSPHRASE,
      };
      await basecoin.recover(recoveryOptions).should.rejectedWith('insufficient balance to build the transaction');
    });
  });
});

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


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