PHP WebShell

Текущая директория: /opt/BitGoJS/modules/abstract-utxo/src/transaction/fixedScript

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

import assert from 'assert';

import _ from 'lodash';
import { Triple, VerificationOptions, Wallet } from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';

import {
  AbstractUtxoCoin,
  FixedScriptWalletOutput,
  Output,
  TransactionExplanation,
  ParsedTransaction,
  ParseTransactionOptions,
} from '../../abstractUtxoCoin';
import { fetchKeychains, getKeySignatures, toKeychainTriple, UtxoKeychain, UtxoNamedKeychains } from '../../keychains';
import { ComparableOutput, outputDifference } from '../outputDifference';
import { fromExtendedAddressFormatToScript, toExtendedAddressFormat } from '../recipient';

import { CustomChangeOptions, parseOutput } from './parseOutput';

export type ComparableOutputWithExternal<TValue> = ComparableOutput<TValue> & {
  external: boolean | undefined;
};

export async function parseTransaction<TNumber extends bigint | number>(
  coin: AbstractUtxoCoin,
  params: ParseTransactionOptions<TNumber>
): Promise<ParsedTransaction<TNumber>> {
  const { txParams, txPrebuild, wallet, verification = {}, reqId } = params;

  if (!_.isUndefined(verification.disableNetworking) && !_.isBoolean(verification.disableNetworking)) {
    throw new Error('verification.disableNetworking must be a boolean');
  }
  const disableNetworking = verification.disableNetworking;

  // obtain the keychains and key signatures
  let keychains: UtxoNamedKeychains | VerificationOptions['keychains'] | undefined = verification.keychains;
  if (!keychains) {
    if (disableNetworking) {
      throw new Error('cannot fetch keychains without networking');
    }
    keychains = await fetchKeychains(coin, wallet, reqId);
  }

  if (!UtxoNamedKeychains.is(keychains)) {
    throw new Error('invalid keychains');
  }

  const keychainArray: Triple<UtxoKeychain> = toKeychainTriple(keychains);

  if (_.isUndefined(txPrebuild.txHex)) {
    throw new Error('missing required txPrebuild property txHex');
  }

  // obtain all outputs
  const explanation: TransactionExplanation = await coin.explainTransaction<TNumber>({
    txHex: txPrebuild.txHex,
    txInfo: txPrebuild.txInfo,
    pubs: keychainArray.map((k) => k.pub) as Triple<string>,
  });

  const allOutputs = [...explanation.outputs, ...explanation.changeOutputs];

  let expectedOutputs;
  if (txParams.rbfTxIds) {
    assert(txParams.rbfTxIds.length === 1);

    const txToBeReplaced = await wallet.getTransaction({ txHash: txParams.rbfTxIds[0], includeRbf: true });
    expectedOutputs = txToBeReplaced.outputs.flatMap(
      (output: { valueString: string; address?: string; wallet?: string }) => {
        // For self-sends, the walletId will be the same as the wallet's id
        if (output.wallet === wallet.id()) {
          return [];
        }
        return [coin.toCanonicalTransactionRecipient(output)];
      }
    );
  } else {
    // verify that each recipient from txParams has their own output
    expectedOutputs = (txParams.recipients ?? []).flatMap((output) => {
      if (output.address === undefined) {
        if (output.amount.toString() !== '0') {
          throw new Error(`Only zero amounts allowed for non-encodeable scriptPubkeys: ${output}`);
        }
        return [output];
      }
      return [{ ...output, address: coin.canonicalAddress(output.address) }];
    });
    if (txParams.allowExternalChangeAddress && txParams.changeAddress) {
      // when an external change address is explicitly specified, count all outputs going towards that
      // address in the expected outputs (regardless of the output amount)
      expectedOutputs.push(
        ...allOutputs.flatMap((output) => {
          if (
            output.address === undefined ||
            output.address !== coin.canonicalAddress(txParams.changeAddress as string)
          ) {
            return [];
          }
          return [{ ...output, address: coin.canonicalAddress(output.address) }];
        })
      );
    }
  }

  // get the keychains from the custom change wallet if needed
  let customChange: CustomChangeOptions | undefined;
  const { customChangeWalletId = undefined } = wallet.coinSpecific() || {};
  if (customChangeWalletId) {
    // fetch keychains from custom change wallet for deriving addresses.
    // These keychains should be signed and this should be verified in verifyTransaction
    const customChangeKeySignatures = wallet._wallet.customChangeKeySignatures;
    const customChangeWallet: Wallet = await coin.wallets().get({ id: customChangeWalletId });
    const customChangeKeys = await fetchKeychains(coin, customChangeWallet, reqId);

    if (!customChangeKeys) {
      throw new Error('failed to fetch keychains for custom change wallet');
    }

    if (customChangeKeys.user && customChangeKeys.backup && customChangeKeys.bitgo && customChangeWallet) {
      const customChangeKeychains: Triple<UtxoKeychain> = [
        customChangeKeys.user,
        customChangeKeys.backup,
        customChangeKeys.bitgo,
      ];

      customChange = {
        keys: customChangeKeychains,
        signatures: [customChangeKeySignatures.user, customChangeKeySignatures.backup, customChangeKeySignatures.bitgo],
      };
    }
  }

  /**
   * Loop through all the outputs and classify each of them as either internal spends
   * or external spends by setting the "external" property to true or false on the output object.
   */
  const allOutputDetails: Output[] = await Promise.all(
    allOutputs.map((currentOutput) => {
      return parseOutput({
        currentOutput,
        coin,
        txPrebuild,
        verification,
        keychainArray: toKeychainTriple(keychains),
        wallet,
        txParams: {
          recipients: expectedOutputs,
          changeAddress: txParams.changeAddress,
        },
        customChange,
        reqId,
      });
    })
  );

  const needsCustomChangeKeySignatureVerification = allOutputDetails.some(
    (output) => (output as FixedScriptWalletOutput)?.needsCustomChangeKeySignatureVerification
  );

  const changeOutputs = _.filter(allOutputDetails, { external: false });

  function toComparableOutputsWithExternal(outputs: Output[]): ComparableOutputWithExternal<bigint | 'max'>[] {
    return outputs.map((output) => ({
      script: fromExtendedAddressFormatToScript(output.address, coin.network),
      value: output.amount === 'max' ? 'max' : (BigInt(output.amount) as bigint | 'max'),
      external: output.external,
    }));
  }

  const missingOutputs = outputDifference(
    toComparableOutputsWithExternal(expectedOutputs),
    toComparableOutputsWithExternal(allOutputs)
  );

  const implicitOutputs = outputDifference(
    toComparableOutputsWithExternal(allOutputDetails),
    toComparableOutputsWithExternal(expectedOutputs)
  );
  const explicitOutputs = outputDifference(toComparableOutputsWithExternal(allOutputDetails), implicitOutputs);

  // these are all the non-wallet outputs that had been originally explicitly specified in recipients
  const explicitExternalOutputs = explicitOutputs.filter((output) => output.external);
  // this is the sum of all the originally explicitly specified non-wallet output values
  const explicitExternalSpendAmount = utxolib.bitgo.toTNumber<TNumber>(
    explicitExternalOutputs.reduce((sum: bigint, o) => sum + BigInt(o.value), BigInt(0)) as bigint,
    coin.amountType
  );

  /**
   * The calculation of the implicit external spend amount pertains to verifying the pay-as-you-go-fee BitGo
   * automatically applies to transactions sending money out of the wallet. The logic is fairly straightforward
   * in that we compare the external spend amount that was specified explicitly by the user to the portion
   * that was specified implicitly. To protect customers from people tampering with the transaction outputs, we
   * define a threshold for the maximum percentage of the implicit external spend in relation to the explicit
   * external spend.
   */

  // make sure that all the extra addresses are change addresses
  // get all the additional external outputs the server added and calculate their values
  const implicitExternalOutputs = implicitOutputs.filter((output) => output.external);
  const implicitExternalSpendAmount = utxolib.bitgo.toTNumber<TNumber>(
    implicitExternalOutputs.reduce((sum: bigint, o) => sum + BigInt(o.value), BigInt(0)) as bigint,
    coin.amountType
  );

  function toOutputs(outputs: ComparableOutputWithExternal<bigint | 'max'>[]): Output[] {
    return outputs.map((output) => ({
      address: toExtendedAddressFormat(output.script, coin.network),
      amount: output.value.toString(),
      external: output.external,
    }));
  }

  return {
    keychains,
    keySignatures: getKeySignatures(wallet) ?? {},
    outputs: allOutputDetails,
    missingOutputs: toOutputs(missingOutputs),
    explicitExternalOutputs: toOutputs(explicitExternalOutputs),
    implicitExternalOutputs: toOutputs(implicitExternalOutputs),
    changeOutputs,
    explicitExternalSpendAmount,
    implicitExternalSpendAmount,
    needsCustomChangeKeySignatureVerification,
    customChange,
  };
}

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


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