PHP WebShell

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

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

/**
 * @prettier
 */
import * as assert from 'assert';

import { AbstractUtxoCoin, getReplayProtectionAddresses } from '@bitgo/abstract-utxo';
import * as utxolib from '@bitgo/utxo-lib';
import * as nock from 'nock';

import { encryptKeychain, getDefaultWalletKeys, getUtxoWallet, keychainsBase58, utxoCoins } from './util';
import { common, HalfSignedUtxoTransaction, Wallet } from '@bitgo/sdk-core';
import { getSeed, TestBitGo } from '@bitgo/sdk-test';
import { BitGo } from '../../../../../src';

const txFormats = ['legacy', 'psbt'] as const;
export type TxFormat = (typeof txFormats)[number];

type KeyDoc = {
  id: string;
  pub: string;
  source: string;
  encryptedPrv: string;
  coinSpecific: any;
};

const walletPassphrase = 'gabagool';
const webauthnWalletPassPhrase = 'just the gabagool';

const scriptTypes = [...utxolib.bitgo.outputScripts.scriptTypes2Of3, 'taprootKeyPathSpend', 'p2shP2pk'] as const;
export type ScriptType = (typeof scriptTypes)[number];

type Input = {
  scriptType: ScriptType;
  value: bigint;
};

function assertSignable(psbtHex: string, inputScripts: ScriptType[], network: utxolib.Network): void {
  const psbt = utxolib.bitgo.createPsbtFromHex(psbtHex, network);
  // Make sure that you can sign with bitgo key and extract the transaction
  // No signatures should be present if it's a p2shP2pk input
  if (!inputScripts.includes('p2shP2pk')) {
    const key = inputScripts.includes('p2trMusig2') ? rootWalletKeys.backup : rootWalletKeys.bitgo;
    psbt.signAllInputsHD(key, { deterministic: true });
    psbt.validateSignaturesOfAllInputs();
    psbt.finalizeAllInputs();
    const tx = psbt.extractTransaction();
    assert.ok(tx);
  }
}

// Build the key objects
const rootWalletKeys = getDefaultWalletKeys();
const keyDocumentObjects = rootWalletKeys.triple.map((bip32, keyIdx) => {
  return {
    id: getSeed(keychainsBase58[keyIdx].pub).toString('hex'),
    pub: bip32.neutered().toBase58(),
    source: ['user', 'backup', 'bitgo'][keyIdx],
    encryptedPrv: encryptKeychain(walletPassphrase, keychainsBase58[keyIdx]),
    webauthnDevices: [
      {
        otpDeviceId: '123',
        authenticatorInfo: {
          credID: 'credID',
          fmt: 'packed',
          publicKey: 'some value',
        },
        prfSalt: '456',
        encryptedPrv: encryptKeychain(webauthnWalletPassPhrase, keychainsBase58[keyIdx]),
      },
    ],
    coinSpecific: {},
  };
});

function run(coin: AbstractUtxoCoin, inputScripts: ScriptType[], txFormat: TxFormat): void {
  function createPrebuildPsbt(inputs: Input[], outputs: { scriptType: 'p2sh'; value: bigint }[]) {
    const psbt = utxolib.testutil.constructPsbt(
      inputs as utxolib.testutil.Input[],
      outputs,
      coin.network,
      rootWalletKeys,
      'unsigned'
    );
    utxolib.bitgo.addXpubsToPsbt(psbt, rootWalletKeys);
    return psbt;
  }

  function createNocks(params: {
    bgUrl: string;
    wallet: Wallet;
    keyDocuments: KeyDoc[];
    prebuild: utxolib.bitgo.UtxoPsbt;
    recipient: { address: string; amount: string };
    addressInfo: Record<string, any>;
    rbfTxIds?: string[];
    feeMultiplier?: number;
    selfSend?: boolean;
    nockOutputAddresses?: boolean;
    txFormat?: string;
  }): nock.Scope[] {
    const nocks: nock.Scope[] = [];

    // Nock the prebuild route (/tx/build, blockheight)
    const expected_params = {
      recipients: [params.recipient],
      rbfTxIds: params.rbfTxIds,
      feeMultiplier: params.feeMultiplier,
      changeAddressType: ['p2trMusig2', 'p2wsh', 'p2shP2wsh', 'p2sh', 'p2tr'],
    };
    if (params.txFormat) {
      expected_params['txFormat'] = params.txFormat;
    }
    nocks.push(
      nock(params.bgUrl)
        .post(`/api/v2/${coin.getChain()}/wallet/${params.wallet.id()}/tx/build`, expected_params)
        .reply(200, { txHex: params.prebuild.toHex(), txInfo: {} })
    );
    nocks.push(nock(params.bgUrl).get(`/api/v2/${coin.getChain()}/public/block/latest`).reply(200, { height: 1000 }));

    // nock the keychain fetch - 3 times (prebuildAndSign, verifyTransaction, and signTransaction)
    params.keyDocuments.forEach((keyDocument) => {
      nocks.push(
        nock(params.bgUrl).get(`/api/v2/${coin.getChain()}/key/${keyDocument.id}`).times(3).reply(200, keyDocument)
      );
    });

    // nock the address info fetch
    if (params.nockOutputAddresses) {
      nocks.push(
        nock(params.bgUrl)
          .get(`/api/v2/${coin.getChain()}/wallet/${params.wallet.id()}/address/${params.addressInfo.address}`)
          .reply(200, params.addressInfo)
      );
    }

    if (params.rbfTxIds) {
      nocks.push(
        nock(params.bgUrl)
          .get(`/api/v2/${coin.getChain()}/wallet/${params.wallet.id()}/tx/${params.rbfTxIds[0]}?includeRbf=true`)
          .reply(200, {
            outputs: [
              {
                address: params.recipient.address,
                value: Number(params.recipient.amount),
                valueString: params.recipient.amount,
                wallet: params.selfSend ? params.wallet.id() : 'some-other-wallet-id', // external output if not a self send
              },
              // Dummy change output to test transfer entries filtering
              {
                address: params.recipient.address,
                value: Number(params.recipient.amount),
                valueString: params.recipient.amount,
                wallet: params.wallet.id(), // internal output
              },
            ],
          })
      );
    }

    // nock the deterministic nonce response
    if (inputScripts.includes('taprootKeyPathSpend')) {
      const psbt = params.prebuild.clone();
      psbt.setAllInputsMusig2NonceHD(rootWalletKeys.user);
      psbt.setAllInputsMusig2NonceHD(rootWalletKeys.bitgo);
      nocks.push(
        nock(params.bgUrl)
          .post(`/api/v2/${coin.getChain()}/wallet/${params.wallet.id()}/tx/signpsbt`, (body) => body.psbt)
          .reply(200, { psbt: psbt.toHex() })
      );
    }

    return nocks;
  }

  describe(`${coin.getFullName()}-prebuildAndSign-txFormat=${txFormat}-inputScripts=${inputScripts.join(
    ','
  )}`, function () {
    const wallet = getUtxoWallet(coin, {
      coinSpecific: { addressVersion: 'base58' },
      keys: keyDocumentObjects.map((k) => k.id),
      id: 'walletId',
    });

    const bitgo = TestBitGo.decorate(BitGo, { env: 'mock' });
    const bgUrl = common.Environments[bitgo.getEnv()].uri;
    let prebuild: utxolib.bitgo.UtxoPsbt;
    let recipient: { address: string; amount: string };
    let addressInfo: Record<string, any>;
    const fee = BigInt(10000);

    before(async function () {
      // Make output address information
      const outputAmount = BigInt(inputScripts.length) * BigInt(1e8) - fee;
      const outputScriptType: utxolib.bitgo.outputScripts.ScriptType = 'p2sh';
      const outputChain = utxolib.bitgo.getExternalChainCode(outputScriptType);
      const outputAddress = utxolib.bitgo.getWalletAddress(rootWalletKeys, outputChain, 0, coin.network);

      recipient = {
        address: outputAddress,
        amount: outputAmount.toString(),
      };
      addressInfo = {
        address: outputAddress,
        chain: outputChain,
        index: 0,
        coin: coin.getChain(),
        wallet: wallet.id(),
        coinSpecific: {},
      };

      prebuild = createPrebuildPsbt(
        inputScripts.map((s) => ({ scriptType: s, value: BigInt(1e8) })),
        [{ scriptType: outputScriptType, value: outputAmount }]
      );
    });

    afterEach(nock.cleanAll);

    [true, false].forEach((useWebauthn) => {
      it(`should succeed with ${useWebauthn ? 'webauthn encryptedPrv' : 'encryptedPrv'}`, async function () {
        const txCoins = ['tzec', 'zec', 'ltc', 'bcha', 'doge', 'dash', 'btg', 'bch'];
        const nocks = createNocks({
          bgUrl,
          wallet,
          keyDocuments: keyDocumentObjects,
          prebuild,
          recipient,
          addressInfo,
          nockOutputAddresses: txFormat !== 'psbt',
          txFormat: !txCoins.includes(coin.getChain()) ? 'psbt' : undefined,
        });

        // call prebuild and sign, nocks should be consumed
        const res = (await wallet.prebuildAndSignTransaction({
          recipients: [recipient],
          walletPassphrase: useWebauthn ? webauthnWalletPassPhrase : walletPassphrase,
        })) as HalfSignedUtxoTransaction;

        nocks.forEach((nock) => assert.ok(nock.isDone()));

        assertSignable(res.txHex, inputScripts, coin.network);
      });

      it('should fail if the wallet passphrase is incorrect', async function () {
        createNocks({
          bgUrl,
          wallet,
          keyDocuments: keyDocumentObjects,
          prebuild,
          recipient,
          addressInfo,
          nockOutputAddresses: txFormat !== 'psbt',
        });

        await wallet
          .prebuildAndSignTransaction({
            recipients: [recipient],
            walletPassphrase: Math.random().toString(),
          })
          .should.be.rejectedWith('unable to decrypt keychain with the given wallet passphrase');
      });
    });

    [true, false].forEach((selfSend) => {
      it(`should be able to build, sign, & verify a replacement transaction with selfSend: ${selfSend}`, async function () {
        const rbfTxIds = ['tx-to-be-replaced'],
          feeMultiplier = 1.5;
        const txCoins = ['tzec', 'zec', 'ltc', 'bcha', 'doge', 'dash', 'btg', 'bch'];
        const nocks = createNocks({
          bgUrl,
          wallet,
          keyDocuments: keyDocumentObjects,
          prebuild,
          recipient,
          addressInfo,
          rbfTxIds,
          feeMultiplier,
          selfSend,
          nockOutputAddresses: txFormat !== 'psbt',
          txFormat: !txCoins.includes(coin.getChain()) ? 'psbt' : undefined,
        });

        // call prebuild and sign, nocks should be consumed
        const res = (await wallet.prebuildAndSignTransaction({
          recipients: [recipient],
          walletPassphrase,
          rbfTxIds,
          feeMultiplier,
        })) as HalfSignedUtxoTransaction;

        nocks.forEach((nock) => assert.ok(nock.isDone()));

        assertSignable(res.txHex, inputScripts, coin.network);
      });
    });
  });
}

utxoCoins
  .filter((coin) => utxolib.getMainnet(coin.network) !== utxolib.networks.bitcoinsv)
  .forEach((coin) => {
    scriptTypes
      // Don't iterate over p2shP2pk - in no scenario would a wallet spend two p2shP2pk inputs as these
      // are single signature inputs that are used for replay protection and are added to the transaction
      // by our system from a separate wallet. We do run tests below where one of the inputs is a p2shP2pk and
      // the other is an input spent by the user.
      .filter((scriptType) => scriptType !== 'p2shP2pk')
      .forEach((inputScript) => {
        const inputScriptCleaned = (
          inputScript === 'taprootKeyPathSpend' ? 'p2trMusig2' : inputScript
        ) as utxolib.bitgo.outputScripts.ScriptType2Of3;

        if (!coin.supportsAddressType(inputScriptCleaned)) {
          return;
        }

        run(coin, [inputScript, inputScript], 'psbt');
        if (getReplayProtectionAddresses(coin.network).length) {
          run(coin, ['p2shP2pk', inputScript], 'psbt');
        }
      });
  });

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


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