PHP WebShell

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

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

/**
 * @prettier
 */
import { BigNumber } from 'bignumber.js';
import { bip32 } from '@bitgo/secp256k1';
import Keccak from 'keccak';
import * as secp256k1 from 'secp256k1';
import * as _ from 'lodash';
import {
  AvalancheNetwork,
  BaseCoin as StaticsBaseCoin,
  CoinFamily,
  coins,
  ethGasConfigs,
  EthereumNetwork,
} from '@bitgo/statics';
import {
  BaseCoin,
  BaseTransaction,
  BitGoBase,
  common,
  FeeEstimateOptions,
  FullySignedTransaction,
  getIsUnsignedSweep,
  InvalidAddressError,
  IWallet,
  KeyPair,
  MultisigType,
  multisigTypes,
  ParsedTransaction,
  ParseTransactionOptions,
  Recipient,
  TransactionExplanation,
  Util,
  VerifyAddressOptions,
} from '@bitgo/sdk-core';
import {
  AbstractEthLikeNewCoins,
  GetSendMethodArgsOptions,
  optionalDeps,
  RecoverOptions,
  RecoveryInfo,
  SendMethodArgs,
  TransactionBuilder as EthTransactionBuilder,
  TransactionPrebuild,
} from '@bitgo/sdk-coin-eth';
import { getToken, isValidEthAddress } from './lib/utils';
import { KeyPair as AvaxcKeyPair, TransactionBuilder } from './lib';
import request from 'superagent';
import { BN, pubToAddress } from 'ethereumjs-util';
import { Buffer } from 'buffer';
import {
  AvaxSignTransactionOptions,
  BuildOptions,
  ExplainTransactionOptions,
  FeeEstimate,
  HopParams,
  HopPrebuild,
  HopTransactionBuildOptions,
  OfflineVaultTxInfo,
  PrecreateBitGoOptions,
  PresignTransactionOptions,
  SignedTransaction,
  SignFinalOptions,
  VerifyAvaxcTransactionOptions,
} from './iface';
import { AvaxpLib } from '@bitgo/sdk-coin-avaxp';
import { SignTransactionOptions } from '@bitgo/abstract-eth';

/** COIN-1708 : Avaxc is added for CCR in WRW,
 * hence adding the feature for AbstractEthLikeNewCoins
 * Super class changed from BaseCoin to AbstractEthLikeNewCoins
 * @since Sept 2024
 */
export class AvaxC extends AbstractEthLikeNewCoins {
  static hopTransactionSalt = 'bitgoHopAddressRequestSalt';

  protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;

  protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
    super(bitgo, staticsCoin);

    if (!staticsCoin) {
      throw new Error('missing required constructor parameter staticsCoin');
    }

    this._staticsCoin = staticsCoin;
  }

  static createInstance(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>): BaseCoin {
    return new AvaxC(bitgo, staticsCoin);
  }

  getBaseFactor(): number {
    return Math.pow(10, this._staticsCoin.decimalPlaces);
  }

  getChain(): string {
    return this._staticsCoin.name;
  }

  /**
   * Method to return the coin's network object
   * @returns {BaseNetwork}
   */
  getNetwork(): EthereumNetwork {
    return this._staticsCoin.network as EthereumNetwork;
  }

  /**
   * Get the base chain that the coin exists on.
   */
  getBaseChain(): string {
    return this.getChain();
  }

  getFamily(): CoinFamily {
    return this._staticsCoin.family;
  }

  getFullName(): string {
    return this._staticsCoin.fullName;
  }

  valuelessTransferAllowed(): boolean {
    return true;
  }

  isValidAddress(address: string): boolean {
    // also validate p-chain address for cross-chain txs
    return !!address && (isValidEthAddress(address) || AvaxpLib.Utils.isValidAddress(address));
  }

  isToken(): boolean {
    return false;
  }

  /** {@inheritDoc } **/
  supportsMultisig(): boolean {
    return true;
  }

  /** inherited doc */
  getDefaultMultisigType(): MultisigType {
    return multisigTypes.onchain;
  }

  generateKeyPair(seed?: Buffer): KeyPair {
    const avaxKeyPair = seed ? new AvaxcKeyPair({ seed }) : new AvaxcKeyPair();
    const extendedKeys = avaxKeyPair.getExtendedKeys();
    return {
      pub: extendedKeys.xpub,
      prv: extendedKeys.xprv!,
    };
  }

  async parseTransaction(params: ParseTransactionOptions): Promise<ParsedTransaction> {
    return {};
  }

  async verifyAddress({ address }: VerifyAddressOptions): Promise<boolean> {
    if (!this.isValidAddress(address)) {
      throw new InvalidAddressError(`invalid address: ${address}`);
    }
    return true;
  }

  /**
   * Verify that a transaction prebuild complies with the original intention
   *
   * @param params
   * @param params.txParams params object passed to send
   * @param params.txPrebuild prebuild object returned by server
   * @param params.wallet Wallet object to obtain keys to verify against
   * @returns {boolean}
   */
  async verifyTransaction(params: VerifyAvaxcTransactionOptions): Promise<boolean> {
    const { txParams, txPrebuild, wallet } = params;
    if (!txParams?.recipients || !txPrebuild?.recipients || !wallet) {
      throw new Error(`missing params`);
    }
    if (txParams.hop && txParams.recipients.length > 1) {
      throw new Error(`tx cannot be both a batch and hop transaction`);
    }
    if (txPrebuild.recipients.length > 1) {
      throw new Error(
        `${this.getChain()} doesn't support sending to more than 1 destination address within a single transaction. Try again, using only a single recipient.`
      );
    }
    if (txParams.hop && txPrebuild.hopTransaction) {
      // Check recipient amount for hop transaction
      if (txParams.recipients.length !== 1) {
        throw new Error(`hop transaction only supports 1 recipient but ${txParams.recipients.length} found`);
      }
      // Check tx sends to hop address
      let expectedHopAddress;
      if (txPrebuild.hopTransaction.type === 'Export') {
        const decodedHopTx = await this.explainAtomicTransaction(txPrebuild.hopTransaction.tx);
        expectedHopAddress = optionalDeps.ethUtil.stripHexPrefix(decodedHopTx.inputs[0].address);
      } else {
        const decodedHopTx = optionalDeps.EthTx.TransactionFactory.fromSerializedData(
          optionalDeps.ethUtil.toBuffer(txPrebuild.hopTransaction.tx)
        );
        expectedHopAddress = optionalDeps.ethUtil.stripHexPrefix(decodedHopTx.getSenderAddress().toString());
      }
      const actualHopAddress = optionalDeps.ethUtil.stripHexPrefix(txPrebuild.recipients[0].address);
      if (expectedHopAddress.toLowerCase() !== actualHopAddress.toLowerCase()) {
        throw new Error('recipient address of txPrebuild does not match hop address');
      }

      // Convert TransactionRecipient array to Recipient array
      const recipients: Recipient[] = txParams.recipients.map((r) => {
        return {
          address: r.address,
          amount: typeof r.amount === 'number' ? r.amount.toString() : r.amount,
        };
      });

      // Check destination address and amount
      await this.validateHopPrebuild(wallet, txPrebuild.hopTransaction, { recipients });
    } else if (txParams.recipients.length > 1) {
      // Check total amount for batch transaction
      let expectedTotalAmount = new BigNumber(0);
      for (let i = 0; i < txParams.recipients.length; i++) {
        expectedTotalAmount = expectedTotalAmount.plus(txParams.recipients[i].amount);
      }
      if (!expectedTotalAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
        throw new Error(
          'batch transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
        );
      }
    } else {
      // Check recipient address and amount for normal transaction
      if (txParams.recipients.length !== 1) {
        throw new Error(`normal transaction only supports 1 recipient but ${txParams.recipients.length} found`);
      }
      const expectedAmount = new BigNumber(txParams.recipients[0].amount);
      if (!expectedAmount.isEqualTo(txPrebuild.recipients[0].amount)) {
        throw new Error(
          'normal transaction amount in txPrebuild received from BitGo servers does not match txParams supplied by client'
        );
      }
      if (
        AvaxC.isAVAXCAddress(txParams.recipients[0].address) &&
        txParams.recipients[0].address !== txPrebuild.recipients[0].address
      ) {
        throw new Error('destination address in normal txPrebuild does not match that in txParams supplied by client');
      }
    }
    // Check coin is correct for all transaction types
    if (!this.verifyCoin(txPrebuild)) {
      throw new Error(`coin in txPrebuild did not match that in txParams supplied by client`);
    }
    return true;
  }

  private static isAVAXCAddress(address: string): boolean {
    return !!address.match(/0x[a-fA-F0-9]{40}/);
  }

  verifyCoin(txPrebuild: TransactionPrebuild): boolean {
    return txPrebuild.coin === this.getChain();
  }

  isValidPub(pub: string): boolean {
    let valid = true;
    try {
      new AvaxcKeyPair({ pub });
    } catch (e) {
      valid = false;
    }
    return valid;
  }

  /**
   * Check whether gas limit passed in by user are within our max and min bounds
   * If they are not set, set them to the defaults
   * @param {number} userGasLimit - user defined gas limit
   * @returns {number} the gas limit to use for this transaction
   */
  setGasLimit(userGasLimit?: number): number {
    if (!userGasLimit) {
      return ethGasConfigs.defaultGasLimit;
    }
    const gasLimitMax = ethGasConfigs.maximumGasLimit;
    const gasLimitMin = ethGasConfigs.minimumGasLimit;
    if (userGasLimit < gasLimitMin || userGasLimit > gasLimitMax) {
      throw new Error(`Gas limit must be between ${gasLimitMin} and ${gasLimitMax}`);
    }
    return userGasLimit;
  }

  /**
   * Check whether the gas price passed in by user are within our max and min bounds
   * If they are not set, set them to the defaults
   * @param {number} userGasPrice - user defined gas price
   * @returns the gas price to use for this transaction
   */
  setGasPrice(userGasPrice?: number): number {
    if (!userGasPrice) {
      return ethGasConfigs.defaultGasPrice;
    }

    const gasPriceMax = ethGasConfigs.maximumGasPrice;
    const gasPriceMin = ethGasConfigs.minimumGasPrice;
    if (userGasPrice < gasPriceMin || userGasPrice > gasPriceMax) {
      throw new Error(`Gas price must be between ${gasPriceMin} and ${gasPriceMax}`);
    }
    return userGasPrice;
  }

  /**
   * Make a query to avax.network for information such as balance, token balance, solidity calls
   * @param {Object} query — key-value pairs of parameters to append after /api
   * @returns {Promise<Object>} response from avax.network
   */
  async recoveryBlockchainExplorerQuery(query: Record<string, any>): Promise<any> {
    const response = await request
      .post(common.Environments[this.bitgo.getEnv()].avaxcNetworkBaseUrl + '/ext/bc/C/rpc')
      .send(query);

    if (!response.ok) {
      throw new Error('could not reach avax.network');
    }

    if (response.body.status === '0' && response.body.message === 'NOTOK') {
      throw new Error('avax.network rate limit reached');
    }
    return response.body;
  }

  /**
   * Queries public block explorer to get the next nonce that should be used for
   * the given AVAXC address
   * @param {string} address — address to fetch for
   * @returns {number} address nonce
   */
  async getAddressNonce(address: string): Promise<number> {
    // Get nonce for backup key (should be 0)
    const result = await this.recoveryBlockchainExplorerQuery({
      jsonrpc: '2.0',
      method: 'eth_getTransactionCount',
      params: [address, 'latest'],
      id: 1,
    });
    if (!result || isNaN(result.result)) {
      throw new Error('Unable to find next nonce from avax.network, got: ' + JSON.stringify(result));
    }
    const nonceHex = result.result;
    return new optionalDeps.ethUtil.BN(nonceHex.slice(2), 16).toNumber();
  }

  /**
   * Queries avax.network for the balance of an address
   * @param {string} address - the AVAXC address
   * @returns {Promise<BigNumber>} address balance
   */
  async queryAddressBalance(address: string): Promise<BN> {
    const result = await this.recoveryBlockchainExplorerQuery({
      jsonrpc: '2.0',
      method: 'eth_getBalance',
      params: [address, 'latest'],
      id: 1,
    });
    // throw if the result does not exist or the result is not a valid number
    if (!result || !result.result || isNaN(result.result)) {
      throw new Error(`Could not obtain address balance for ${address} from avax.network, got: ${result.result}`);
    }
    const nativeBalanceHex = result.result;
    return new optionalDeps.ethUtil.BN(nativeBalanceHex.slice(2), 16);
  }

  /**
   * Queries avax.network for the token balance of an address
   * @param {string} walletContractAddress - the AVAXC address
   * @param {string} tokenContractAddress - the Token contract address
   * @returns {Promise<BigNumber>} address balance
   */
  async queryAddressTokenBalance(tokenContractAddress: string, walletContractAddress: string): Promise<BN> {
    // get token balance using contract call
    const tokenBalanceData = optionalDeps.ethAbi
      .simpleEncode('balanceOf(address)', walletContractAddress)
      .toString('hex');
    const tokenBalanceDataHex = optionalDeps.ethUtil.addHexPrefix(tokenBalanceData);
    const result = await this.recoveryBlockchainExplorerQuery({
      jsonrpc: '2.0',
      method: 'eth_call',
      params: [
        {
          to: tokenContractAddress,
          data: tokenBalanceDataHex,
        },
        'latest',
      ],
      id: 1,
    });
    // throw if the result does not exist or the result is not a valid number
    if (!result || !result.result || isNaN(result.result)) {
      throw new Error(
        `Could not obtain address token balance for ${walletContractAddress} from avax.network, got: ${result.result}`
      );
    }
    const tokenBalanceHex = result.result;
    return new optionalDeps.ethUtil.BN(tokenBalanceHex.slice(2), 16);
  }

  /**
   * Queries the contract (via avax.network) for the next sequence ID
   * @param {string} address - address of the contract
   * @returns {Promise<number>} sequence ID
   */
  async querySequenceId(address: string): Promise<number> {
    // Get sequence ID using contract call
    const sequenceIdMethodSignature = optionalDeps.ethAbi.methodID('getNextSequenceId', []);
    const sequenceIdArgs = optionalDeps.ethAbi.rawEncode([], []);
    const sequenceIdData = Buffer.concat([sequenceIdMethodSignature, sequenceIdArgs]).toString('hex');
    const sequenceIdDataHex = optionalDeps.ethUtil.addHexPrefix(sequenceIdData);
    const result = await this.recoveryBlockchainExplorerQuery({
      jsonrpc: '2.0',
      method: 'eth_call',
      params: [{ to: address, data: sequenceIdDataHex }, 'latest'],
      id: 1,
    });
    if (!result || !result.result) {
      throw new Error('Could not obtain sequence ID from avax.network, got: ' + result.result);
    }
    const sequenceIdHex = result.result;
    return new optionalDeps.ethUtil.BN(sequenceIdHex.slice(2), 16).toNumber();
  }

  /**
   * @param {Object} recipient - recipient info
   * @param {number} expireTime - expiry time
   * @param {number} contractSequenceId - sequence id
   * @returns {(string|Array)} operation array
   */
  getOperation(recipient: Recipient, expireTime: number, contractSequenceId: number): (string | Buffer)[][] {
    return [
      ['string', 'address', 'uint', 'bytes', 'uint', 'uint'],
      [
        'ETHER',
        new optionalDeps.ethUtil.BN(optionalDeps.ethUtil.stripHexPrefix(recipient.address), 16),
        recipient.amount,
        Buffer.from(optionalDeps.ethUtil.stripHexPrefix(optionalDeps.ethUtil.padToEven(recipient.data || '')), 'hex'),
        expireTime,
        contractSequenceId,
      ],
    ];
  }

  /**
   * Calculate the operation hash in the same way solidity would
   * @param {Recipient[]} recipients - tx recipients
   * @param {number} expireTime - expiration time
   * @param {number} contractSequenceId - contract sequence id
   * @returns {string} operation hash
   */
  getOperationSha3ForExecuteAndConfirm(
    recipients: Recipient[],
    expireTime: number,
    contractSequenceId: number
  ): string {
    if (!recipients || !Array.isArray(recipients)) {
      throw new Error('expecting array of recipients');
    }

    // Right now we only support 1 recipient
    if (recipients.length !== 1) {
      throw new Error('must send to exactly 1 recipient');
    }

    if (!_.isNumber(expireTime)) {
      throw new Error('expireTime must be number of seconds since epoch');
    }

    if (!_.isNumber(contractSequenceId)) {
      throw new Error('contractSequenceId must be number');
    }

    // Check inputs
    recipients.forEach(function (recipient) {
      if (
        !_.isString(recipient.address) ||
        !optionalDeps.ethUtil.isValidAddress(optionalDeps.ethUtil.addHexPrefix(recipient.address))
      ) {
        throw new Error('Invalid address: ' + recipient.address);
      }

      let amount;
      try {
        amount = new BigNumber(recipient.amount);
      } catch (e) {
        throw new Error('Invalid amount for: ' + recipient.address + ' - should be numeric');
      }

      recipient.amount = amount.toFixed(0);

      if (recipient.data && !_.isString(recipient.data)) {
        throw new Error('Data for recipient ' + recipient.address + ' - should be of type hex string');
      }
    });

    const recipient = recipients[0];
    return optionalDeps.ethUtil.bufferToHex(
      optionalDeps.ethAbi.soliditySHA3(...this.getOperation(recipient, expireTime, contractSequenceId))
    );
  }

  /**
   * Default expire time for a contract call (1 week)
   * @returns {number} Time in seconds
   */
  getDefaultExpireTime(): number {
    return Math.floor(new Date().getTime() / 1000) + 60 * 60 * 24 * 7;
  }

  /**
   * Build arguments to call the send method on the wallet contract
   * @param {Object} txInfo - data for send method args
   * @returns {SendMethodArgs[]}
   */
  getSendMethodArgs(txInfo: GetSendMethodArgsOptions): SendMethodArgs[] {
    // Method signature is
    // sendMultiSig(address toAddress, uint value, bytes data, uint expireTime, uint sequenceId, bytes signature)
    return [
      {
        name: 'toAddress',
        type: 'address',
        value: txInfo.recipient.address,
      },
      {
        name: 'value',
        type: 'uint',
        value: txInfo.recipient.amount,
      },
      {
        name: 'data',
        type: 'bytes',
        value: optionalDeps.ethUtil.toBuffer(optionalDeps.ethUtil.addHexPrefix(txInfo.recipient.data || '')),
      },
      {
        name: 'expireTime',
        type: 'uint',
        value: txInfo.expireTime,
      },
      {
        name: 'sequenceId',
        type: 'uint',
        value: txInfo.contractSequenceId,
      },
      {
        name: 'signature',
        type: 'bytes',
        value: optionalDeps.ethUtil.toBuffer(optionalDeps.ethUtil.addHexPrefix(txInfo.signature)),
      },
    ];
  }

  /**
   * Builds a funds recovery transaction without BitGo
   * Steps:
   * 1) Node query - how much money is in the account
   * 2) Build transaction - build our transaction for the amount
   * 3) Send signed build - send our signed build to a public node
   * @param {Object} params The options with which to recover
   * @param {string} params.userKey - [encrypted] xprv
   * @param {string} params.backupKey - [encrypted] xprv or xpub if the xprv is held by a KRS provider
   * @param {string} params.walletPassphrase - used to decrypt userKey and backupKey
   * @param {string} params.walletContractAddress - the AVAXC address of the wallet contract
   * @param {string} params.recoveryDestination - target address to send recovered funds to
   * @returns {Promise<RecoveryInfo>} - recovery tx info
   */
  async recover(params: RecoverOptions): Promise<RecoveryInfo | OfflineVaultTxInfo> {
    if (params.bitgoFeeAddress) {
      return (await this.recoverEthLikeforEvmBasedRecovery(params)) as RecoveryInfo | OfflineVaultTxInfo;
    }

    if (_.isUndefined(params.userKey)) {
      throw new Error('missing userKey');
    }

    if (_.isUndefined(params.backupKey)) {
      throw new Error('missing backupKey');
    }

    if (_.isUndefined(params.walletPassphrase) && !params.userKey.startsWith('xpub')) {
      throw new Error('missing wallet passphrase');
    }

    if (_.isUndefined(params.walletContractAddress) || !this.isValidAddress(params.walletContractAddress)) {
      throw new Error('invalid walletContractAddress');
    }

    let tokenName;
    if (params.tokenContractAddress) {
      if (!this.isValidAddress(params.tokenContractAddress)) {
        throw new Error('invalid tokenContractAddress');
      }
      const network = this.getNetwork();
      const token = getToken(params.tokenContractAddress, network);
      if (_.isUndefined(token)) {
        throw new Error('token not supported');
      }
      tokenName = token.name;
    }

    if (_.isUndefined(params.recoveryDestination) || !this.isValidAddress(params.recoveryDestination)) {
      throw new Error('invalid recoveryDestination');
    }

    // TODO (BG-56531): add support for krs
    const isUnsignedSweep = getIsUnsignedSweep(params);

    // Clean up whitespace from entered values
    let userKey = params.userKey.replace(/\s/g, '');
    const backupKey = params.backupKey.replace(/\s/g, '');

    // Set new tx fees (using default config values from platform)
    const gasLimit = new optionalDeps.ethUtil.BN(this.setGasLimit(params.gasLimit));
    const gasPrice = params.eip1559
      ? new optionalDeps.ethUtil.BN(params.eip1559.maxFeePerGas)
      : new optionalDeps.ethUtil.BN(this.setGasPrice(params.gasPrice));
    if (!userKey.startsWith('xpub') && !userKey.startsWith('xprv')) {
      try {
        userKey = this.bitgo.decrypt({
          input: userKey,
          password: params.walletPassphrase,
        });
      } catch (e) {
        throw new Error(`Error decrypting user keychain: ${e.message}`);
      }
    }

    let backupKeyAddress;
    let backupSigningKey;
    if (isUnsignedSweep) {
      const backupKeyPair = new AvaxcKeyPair({ pub: backupKey });
      backupKeyAddress = backupKeyPair.getAddress();
    } else {
      // Decrypt backup private key and get address
      let backupPrv;

      try {
        backupPrv = this.bitgo.decrypt({
          input: backupKey,
          password: params.walletPassphrase,
        });
      } catch (e) {
        throw new Error(`Error decrypting backup keychain: ${e.message}`);
      }

      const keyPair = new AvaxcKeyPair({ prv: backupPrv });
      backupSigningKey = keyPair.getKeys().prv;
      if (!backupSigningKey) {
        throw new Error('no private key');
      }
      backupKeyAddress = keyPair.getAddress();
    }
    const backupKeyNonce = await this.getAddressNonce(backupKeyAddress);

    // get balance of backupKey to ensure funds are available to pay fees
    const backupKeyBalance = await this.queryAddressBalance(backupKeyAddress);

    const totalGasNeeded = gasPrice.mul(gasLimit);
    const weiToGwei = 10 ** 9;
    if (backupKeyBalance.lt(totalGasNeeded)) {
      throw new Error(
        `Backup key address ${backupKeyAddress} has balance ${backupKeyBalance
          .div(new BN(weiToGwei))
          .toString()} Gwei.` +
          `This address must have a balance of at least ${(totalGasNeeded / weiToGwei).toString()}` +
          ` Gwei to perform recoveries. Try sending some AVAX to this address then retry.`
      );
    }

    let txAmount;
    if (params.tokenContractAddress) {
      // get token balance of wallet
      txAmount = await this.queryAddressTokenBalance(params.tokenContractAddress, params.walletContractAddress);
    } else {
      // get balance of wallet and deduct fees to get transaction amount
      txAmount = await this.queryAddressBalance(params.walletContractAddress);
    }

    // build recipients object
    const recipients = [
      {
        address: params.recoveryDestination,
        amount: txAmount.toString(10),
      },
    ];

    // Get sequence ID using contract call
    // we need to wait between making two avax.network calls to avoid getting banned
    await new Promise((resolve) => setTimeout(resolve, 1000));
    const sequenceId = await this.querySequenceId(params.walletContractAddress);

    let operationHash, signature;
    // Get operation hash and sign it
    if (!isUnsignedSweep) {
      operationHash = this.getOperationSha3ForExecuteAndConfirm(recipients, this.getDefaultExpireTime(), sequenceId);
      signature = Util.ethSignMsgHash(operationHash, Util.xprvToEthPrivateKey(userKey));

      try {
        Util.ecRecoverEthAddress(operationHash, signature);
      } catch (e) {
        throw new Error('Invalid signature');
      }
    }

    const txInfo = {
      recipient: recipients[0],
      expireTime: this.getDefaultExpireTime(),
      contractSequenceId: sequenceId,
      operationHash,
      signature,
      gasLimit: gasLimit.toString(10),
      tokenContractAddress: params.tokenContractAddress,
    };

    const txBuilder = this.getTransactionBuilder() as TransactionBuilder;
    txBuilder.counter(backupKeyNonce);
    txBuilder.contract(params.walletContractAddress);
    let txFee;
    if (params.eip1559) {
      txFee = {
        eip1559: {
          maxPriorityFeePerGas: params.eip1559.maxPriorityFeePerGas,
          maxFeePerGas: params.eip1559.maxFeePerGas,
        },
      };
    } else {
      txFee = { fee: gasPrice.toString() };
    }
    txBuilder.fee({
      ...txFee,
      gasLimit: gasLimit.toString(),
    });
    if (params.tokenContractAddress) {
      txBuilder
        .transfer()
        .coin(tokenName)
        .amount(recipients[0].amount)
        .contractSequenceId(sequenceId)
        .expirationTime(this.getDefaultExpireTime())
        .to(params.recoveryDestination);
    } else {
      txBuilder
        .transfer()
        .coin(this.getChain())
        .amount(recipients[0].amount)
        .contractSequenceId(sequenceId)
        .expirationTime(this.getDefaultExpireTime())
        .to(params.recoveryDestination);
    }

    if (isUnsignedSweep) {
      const tx = await txBuilder.build();
      const response: OfflineVaultTxInfo = {
        txHex: tx.toBroadcastFormat(),
        userKey,
        backupKey,
        coin: this.getChain(),
        token: tokenName,
        gasPrice: optionalDeps.ethUtil.bufferToInt(gasPrice).toFixed(),
        gasLimit,
        recipients: [txInfo.recipient],
        walletContractAddress: tx.toJson().to,
        amount: txInfo.recipient.amount,
        backupKeyNonce,
        eip1559: params.eip1559,
      };
      _.extend(response, txInfo);
      response.nextContractSequenceId = response.contractSequenceId;
      return response;
    }

    const userKeyPair = new AvaxcKeyPair({ prv: userKey });
    txBuilder.transfer().key(userKeyPair.getKeys().prv!);
    txBuilder.sign({ key: backupSigningKey });
    const signedTx = await txBuilder.build();

    return {
      id: signedTx.toJson().id,
      tx: signedTx.toBroadcastFormat(),
    };
  }

  /**
   * Create a new transaction builder for the current chain
   * @return a new transaction builder
   */
  protected getTransactionBuilder(): EthTransactionBuilder {
    return new TransactionBuilder(coins.get(this.getBaseChain()));
  }

  protected getAtomicBuilder(): AvaxpLib.TransactionBuilderFactory {
    return new AvaxpLib.TransactionBuilderFactory(coins.get(this.getAvaxP()));
  }

  /**
   * Explain a transaction from txHex, overriding BaseCoins
   * transaction can be either atomic or eth txn.
   * @param params The options with which to explain the transaction
   */
  async explainTransaction(params: ExplainTransactionOptions): Promise<TransactionExplanation> {
    const txHex = params.txHex || (params.halfSigned && params.halfSigned.txHex);
    if (!txHex) {
      throw new Error('missing txHex in explain tx parameters');
    }
    if (params.crossChainType) {
      return this.explainAtomicTransaction(txHex);
    }
    if (!params.feeInfo) {
      throw new Error('missing feeInfo in explain tx parameters');
    }
    const txBuilder = this.getTransactionBuilder();
    txBuilder.from(txHex);
    const tx = await txBuilder.build();
    return Object.assign(this.explainEVMTransaction(tx), { fee: params.feeInfo });
  }

  /**
   * Explains an atomic transaction using atomic builder.
   * @param txHex
   * @private
   */
  private async explainAtomicTransaction(txHex: string) {
    const txBuilder = this.getAtomicBuilder().from(txHex);
    const tx = await txBuilder.build();
    return tx.explainTransaction();
  }

  /**
   * Verify signature for an atomic transaction using atomic builder.
   * @param txHex
   * @return true if signature is from the input address
   * @private
   */
  private async verifySignatureForAtomicTransaction(txHex: string): Promise<boolean> {
    const txBuilder = this.getAtomicBuilder().from(txHex);
    const tx = await txBuilder.build();
    const payload = tx.signablePayload;
    const signatures = tx.signature.map((s) => Buffer.from(s, 'hex'));
    const network = _.get(tx, '_network');
    const recoverPubky = signatures.map((s) =>
      AvaxpLib.Utils.recoverySignature(network as unknown as AvalancheNetwork, payload, s)
    );
    const expectedSenders = recoverPubky.map((r) => pubToAddress(r, true));
    const senders = tx.inputs.map((i) => AvaxpLib.Utils.parseAddress(i.address));
    return expectedSenders.every((e) => senders.some((sender) => e.equals(sender)));
  }

  /**
   * Explains an EVM transaction using regular eth txn builder
   * @param tx
   * @private
   */
  private explainEVMTransaction(tx: BaseTransaction) {
    const outputs = tx.outputs.map((output) => {
      return {
        address: output.address,
        amount: output.value,
      };
    });
    const displayOrder = ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee'];
    return {
      displayOrder,
      id: tx.id,
      outputs: outputs,
      outputAmount: outputs
        .reduce((accumulator, output) => accumulator.plus(output.amount), new BigNumber('0'))
        .toFixed(0),
      changeOutputs: [], // account based does not use change outputs
      changeAmount: '0', // account base does not make change
    };
  }

  /**
   * Above is standard BaseCoins functions
   * ================================================================================================================
   * ================================================================================================================
   * Below is transaction functions
   */

  /**
   * Coin-specific things done before signing a transaction, i.e. verification
   * @param params
   */
  async presignTransaction(params: PresignTransactionOptions): Promise<PresignTransactionOptions> {
    if (!_.isUndefined(params.hopTransaction) && !_.isUndefined(params.wallet) && !_.isUndefined(params.buildParams)) {
      await this.validateHopPrebuild(params.wallet, params.hopTransaction);
    }
    return params;
  }

  /**
   * Modify prebuild after receiving it from the server. Add things like nlocktime
   */
  async postProcessPrebuild(params: TransactionPrebuild): Promise<TransactionPrebuild> {
    if (!_.isUndefined(params.hopTransaction) && !_.isUndefined(params.wallet) && !_.isUndefined(params.buildParams)) {
      await this.validateHopPrebuild(params.wallet, params.hopTransaction, params.buildParams);
    }
    return params;
  }

  /**
   * Validates that the hop prebuild from the HSM is valid and correct
   * @param wallet The wallet that the prebuild is for
   * @param hopPrebuild The prebuild to validate
   * @param originalParams The original parameters passed to prebuildTransaction
   * @returns void
   * @throws Error if The prebuild is invalid
   */
  async validateHopPrebuild(
    wallet: IWallet,
    hopPrebuild: HopPrebuild,
    originalParams?: { recipients: Recipient[] }
  ): Promise<void> {
    const { tx, id, signature } = hopPrebuild;

    // first, validate the HSM signature
    const serverXpub = common.Environments[this.bitgo.getEnv()].hsmXpub;
    const serverPubkeyBuffer: Buffer = bip32.fromBase58(serverXpub).publicKey;
    const signatureBuffer: Buffer = Buffer.from(optionalDeps.ethUtil.stripHexPrefix(signature), 'hex');
    const messageBuffer: Buffer =
      hopPrebuild.type === 'Export' ? AvaxC.getTxHash(tx) : Buffer.from(optionalDeps.ethUtil.stripHexPrefix(id), 'hex');

    const sig = new Uint8Array(signatureBuffer.length === 64 ? signatureBuffer : signatureBuffer.slice(1));
    const isValidSignature: boolean = secp256k1.ecdsaVerify(sig, messageBuffer, serverPubkeyBuffer);
    if (!isValidSignature) {
      throw new Error(`Hop txid signature invalid`);
    }

    if (hopPrebuild.type === 'Export') {
      const explainHopExportTx = await this.explainAtomicTransaction(tx);
      // If original params are given, we can check them against the transaction prebuild params
      if (!_.isNil(originalParams)) {
        const { recipients } = originalParams;

        // Then validate that the tx params actually equal the requested params to nano avax plus import tx fee.
        const originalAmount = new BigNumber(recipients[0].amount).div(1e9).plus(1e6).toFixed(0);
        const originalDestination: string | undefined = recipients[0].address;
        const hopAmount = explainHopExportTx.outputAmount;
        const hopDestination = explainHopExportTx.outputs[0].address;
        if (originalAmount !== hopAmount) {
          throw new Error(`Hop amount: ${hopAmount} does not equal original amount: ${originalAmount}`);
        }
        if (originalDestination && hopDestination.toLowerCase() !== originalDestination.toLowerCase()) {
          throw new Error(
            `Hop destination: ${hopDestination} does not equal original recipient: ${originalDestination}`
          );
        }
      }
      if (!(await this.verifySignatureForAtomicTransaction(tx))) {
        throw new Error(`Invalid hop transaction signature, txid: ${id}`);
      }
    } else {
      const builtHopTx = optionalDeps.EthTx.TransactionFactory.fromSerializedData(optionalDeps.ethUtil.toBuffer(tx));
      // If original params are given, we can check them against the transaction prebuild params
      if (!_.isNil(originalParams)) {
        const { recipients } = originalParams;

        // Then validate that the tx params actually equal the requested params
        const originalAmount = new BigNumber(recipients[0].amount);
        const originalDestination: string = recipients[0].address;

        const hopAmount = new BigNumber(optionalDeps.ethUtil.bufferToHex(builtHopTx.value as unknown as Buffer));
        if (!builtHopTx.to) {
          throw new Error(`Transaction does not have a destination address`);
        }
        const hopDestination = builtHopTx.to.toString();
        if (!hopAmount.eq(originalAmount)) {
          throw new Error(`Hop amount: ${hopAmount} does not equal original amount: ${originalAmount}`);
        }
        if (hopDestination.toLowerCase() !== originalDestination.toLowerCase()) {
          throw new Error(`Hop destination: ${hopDestination} does not equal original recipient: ${hopDestination}`);
        }
      }

      if (!builtHopTx.verifySignature()) {
        // We dont want to continue at all in this case, at risk of AVAX being stuck on the hop address
        throw new Error(`Invalid hop transaction signature, txid: ${id}`);
      }
      if (optionalDeps.ethUtil.addHexPrefix(builtHopTx.hash().toString('hex')) !== id) {
        throw new Error(`Signed hop txid does not equal actual txid`);
      }
    }
  }

  /**
   * Helper function for signTransaction for the rare case that SDK is doing the second signature
   * Note: we are expecting this to be called from the offline vault
   * @param params.txPrebuild
   * @param params.prv
   * @returns {{txHex: string}}
   */
  async signFinal(params: SignFinalOptions): Promise<FullySignedTransaction> {
    const keyPair = new AvaxcKeyPair({ prv: params.prv });
    const signingKey = keyPair.getKeys().prv;
    if (_.isUndefined(signingKey)) {
      throw new Error('missing private key');
    }

    const txBuilder = this.getTransactionBuilder() as TransactionBuilder;
    try {
      txBuilder.from(params.txPrebuild!.halfSigned!.txHex);
    } catch (e) {
      throw new Error('invalid half-signed transaction');
    }

    txBuilder.sign({ key: signingKey });
    const tx = await txBuilder.build();
    return {
      txHex: tx.toBroadcastFormat(),
    };
  }

  /**
   * Assemble half-sign prebuilt transaction
   * @param params
   */
  async signTransaction(params: AvaxSignTransactionOptions | SignTransactionOptions): Promise<SignedTransaction> {
    // Normally the SDK provides the first signature for an AVAXC tx,
    // but for unsigned sweep recoveries it can provide the second and final one.
    if (params.isLastSignature) {
      // In this case when we're doing the second (final) signature, the logic is different.
      return await this.signFinal(params as unknown as SignFinalOptions);
    }

    const txBuilder = this.getTransactionBuilder() as TransactionBuilder;
    txBuilder.from(params.txPrebuild.txHex);
    txBuilder.transfer().key(new AvaxcKeyPair({ prv: params.prv }).getKeys().prv!);
    if (params.walletVersion) {
      txBuilder.walletVersion(params.walletVersion);
    }
    const transaction = await txBuilder.build();

    const recipients = transaction.outputs.map((output) => ({ address: output.address, amount: output.value }));

    const txParams = {
      eip1559: params.txPrebuild.eip1559,
      txHex: transaction.toBroadcastFormat(),
      recipients: recipients,
      expireTime: params.txPrebuild.expireTime,
      hopTransaction: params.txPrebuild.hopTransaction,
      custodianTransactionId: params.custodianTransactionId,
    };

    return { halfSigned: txParams };
  }

  /**
   * Modify prebuild before sending it to the server. Add things like hop transaction params
   * @param buildParams The whitelisted parameters for this prebuild
   * @param buildParams.hop True if this should prebuild a hop tx, else false
   * @param buildParams.recipients The recipients array of this transaction
   * @param buildParams.wallet The wallet sending this tx
   * @param buildParams.walletPassphrase the passphrase for this wallet
   */
  async getExtraPrebuildParams(buildParams: BuildOptions): Promise<BuildOptions> {
    if (
      !_.isUndefined(buildParams.hop) &&
      buildParams.hop &&
      !_.isUndefined(buildParams.wallet) &&
      !_.isUndefined(buildParams.recipients)
    ) {
      if (this.isToken()) {
        throw new Error(
          `Hop transactions are not enabled for AVAXC tokens, nor are they necessary. Please remove the 'hop' parameter and try again.`
        );
      }
      return (await this.createHopTransactionParams({
        recipients: buildParams.recipients,
        type: buildParams.type,
      })) as any;
    }
    return {};
  }

  /**
   * Creates the extra parameters needed to build a hop transaction
   * @param {HopTransactionBuildOptions} The original build parameters
   * @returns extra parameters object to merge with the original build parameters object and send to the platform
   */
  async createHopTransactionParams({ recipients, type }: HopTransactionBuildOptions): Promise<HopParams> {
    if (!recipients || !Array.isArray(recipients)) {
      throw new Error('expecting array of recipients');
    }

    // Right now we only support 1 recipient
    if (recipients.length !== 1) {
      throw new Error('must send to exactly 1 recipient');
    }
    const recipientAddress = recipients[0].address;
    const recipientAmount = recipients[0].amount;
    const feeEstimateParams = {
      recipient: recipientAddress,
      amount: recipientAmount,
      hop: true,
      type,
    };
    const feeEstimate: FeeEstimate = await this.feeEstimate(feeEstimateParams);

    const gasLimit = feeEstimate.gasLimitEstimate;
    const gasPrice = Math.round(feeEstimate.feeEstimate / gasLimit);
    const gasPriceMax = gasPrice * 5;
    // Payment id a random number so its different for every tx
    const paymentId = Math.floor(Math.random() * 10000000000).toString();

    // TODO(BG-62671): after completed [Wallet-platform] Remove use of userReqSig for avaxc hop transaction
    const userReqSig = '0x';

    return {
      hopParams: {
        userReqSig,
        gasPriceMax,
        paymentId,
        gasLimit,
      },
    };
  }

  /**
   * Fetch fee estimate information from the server
   * @param {Object} params The params passed into the function
   * @param {Boolean} [params.hop] True if we should estimate fee for a hop transaction
   * @param {String} [params.recipient] The recipient of the transaction to estimate a send to
   * @param {String} [params.data] The ETH tx data to estimate a send for
   * @returns {Object} The fee info returned from the server
   */
  async feeEstimate(params: FeeEstimateOptions): Promise<FeeEstimate> {
    const query: FeeEstimateOptions = {};
    if (params && params.hop) {
      query.hop = params.hop;
    }
    if (params && params.recipient) {
      query.recipient = params.recipient;
    }
    if (params && params.data) {
      query.data = params.data;
    }
    if (params && params.amount) {
      query.amount = params.amount;
    }
    if (params && params.type) {
      query.type = params.type;
    }

    return await this.bitgo.get(this.url('/tx/fee')).query(query).result();
  }

  /**
   * Calculate tx hash like evm from tx hex.
   * @param {string} tx
   * @returns {Buffer} tx hash
   */
  static getTxHash(tx: string): Buffer {
    const hash = Keccak('keccak256');
    hash.update(optionalDeps.ethUtil.stripHexPrefix(tx), 'hex');
    return hash.digest();
  }

  async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
    // TODO: Fix this later
    return true;
  }

  /**
   * Ensure either enterprise or newFeeAddress is passed, to know whether to create new key or use enterprise key
   * @param params
   * @param params.enterprise {String} the enterprise id to associate with this key
   * @param params.newFeeAddress {Boolean} create a new fee address (enterprise not needed in this case)
   */
  preCreateBitGo(params: PrecreateBitGoOptions): void {
    // We always need params object, since either enterprise or newFeeAddress is required
    if (!_.isObject(params)) {
      throw new Error(`preCreateBitGo must be passed a params object. Got ${params} (type ${typeof params})`);
    }

    if (_.isUndefined(params.enterprise) && _.isUndefined(params.newFeeAddress)) {
      throw new Error(
        'expecting enterprise when adding BitGo key. If you want to create a new AVAX bitgo key, set the newFeeAddress parameter to true.'
      );
    }

    // Check whether key should be an enterprise key or a BitGo key for a new fee address
    if (!_.isUndefined(params.enterprise) && !_.isUndefined(params.newFeeAddress)) {
      throw new Error(`Incompatible arguments - cannot pass both enterprise and newFeeAddress parameter.`);
    }

    if (!_.isUndefined(params.enterprise) && !_.isString(params.enterprise)) {
      throw new Error(`enterprise should be a string - got ${params.enterprise} (type ${typeof params.enterprise})`);
    }

    if (!_.isUndefined(params.newFeeAddress) && !_.isBoolean(params.newFeeAddress)) {
      throw new Error(
        `newFeeAddress should be a boolean - got ${params.newFeeAddress} (type ${typeof params.newFeeAddress})`
      );
    }
  }

  getAvaxP(): string {
    return this.getChain().toString() === 'avaxc' ? 'avaxp' : 'tavaxp';
  }

  /**
   * Fetch the gas price from the explorer
   */
  async getGasPriceFromExternalAPI(): Promise<BN> {
    try {
      const res = await this.recoveryBlockchainExplorerQuery({
        jsonrpc: '2.0',
        method: 'eth_gasPrice',
        id: 1,
      });
      const gasPrice = new BN(res.result.slice(2), 16);
      console.log(` Got gas price: ${gasPrice}`);
      return gasPrice;
    } catch (e) {
      throw new Error('Failed to get gas price');
    }
  }

  /**
   * Fetch the gas limit from the explorer
   * @param intendedChain
   * @param from
   * @param to
   * @param data
   */
  async getGasLimitFromExternalAPI(intendedChain: string, from: string, to: string, data: string): Promise<BN> {
    try {
      const res = await this.recoveryBlockchainExplorerQuery({
        jsonrpc: '2.0',
        method: 'eth_estimateGas',
        params: [
          {
            from,
            to,
            data,
          },
          'latest',
        ],
        id: 1,
      });
      const gasLimit = new BN(res.result.slice(2), 16);
      console.log(`Got gas limit: ${gasLimit}`);
      return gasLimit;
    } catch (e) {
      throw new Error(
        `Failed to get gas limit. Please make sure to use the privateKey aka userKey of ${intendedChain} wallet ${to}`
      );
    }
  }
}

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


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