PHP WebShell

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

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

/**
 * @prettier
 */
import { BigNumber } from 'bignumber.js';
import * as _ from 'lodash';
import * as querystring from 'querystring';
import * as url from 'url';

import {
  BaseCoin,
  BitGoBase,
  checkKrsProvider,
  getBip32Keys,
  InvalidAddressError,
  KeyPair,
  MultisigType,
  multisigTypes,
  ParsedTransaction,
  ParseTransactionOptions,
  promiseProps,
  TokenEnablementConfig,
  UnexpectedAddressError,
  VerifyTransactionOptions,
} from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin, coins, XrpCoin } from '@bitgo/statics';
import * as rippleBinaryCodec from 'ripple-binary-codec';
import * as rippleKeypairs from 'ripple-keypairs';
import * as xrpl from 'xrpl';

import {
  ExplainTransactionOptions,
  FeeInfo,
  HalfSignedTransaction,
  RecoveryInfo,
  RecoveryOptions,
  RecoveryTransaction,
  SignTransactionOptions,
  SupplementGenerateWalletOptions,
  TransactionExplanation,
  VerifyAddressOptions,
} from './lib/iface';
import { KeyPair as XrpKeyPair } from './lib/keyPair';
import utils from './lib/utils';
import ripple from './ripple';
import { TokenTransferBuilder, TransactionBuilderFactory, TransferBuilder } from './lib';

export class Xrp extends BaseCoin {
  protected _staticsCoin: Readonly<StaticsBaseCoin>;
  protected constructor(bitgo: BitGoBase, staticsCoin?: Readonly<StaticsBaseCoin>) {
    super(bitgo);
    if (!staticsCoin) {
      throw new Error('missing required constructor parameter staticsCoin');
    }
    this._staticsCoin = staticsCoin;
  }

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

  /**
   * Factor between the coin's base unit and its smallest subdivison
   */
  public getBaseFactor(): number {
    return Math.pow(10, this._staticsCoin.decimalPlaces);
  }

  /**
   * Identifier for the blockchain which supports this coin
   */
  public getChain(): string {
    return this._staticsCoin.name;
  }

  /**
   * Identifier for the coin family
   */
  public getFamily(): string {
    return this._staticsCoin.family;
  }

  /**
   * Complete human-readable name of this coin
   */
  public getFullName(): string {
    return this._staticsCoin.fullName;
  }

  /**
   * Evaluates whether an address string is valid for this coin
   * @param address
   */
  public isValidAddress(address: string): boolean {
    return utils.isValidAddress(address);
  }

  /**
   * Return boolean indicating whether input is valid public key for the coin.
   *
   * @param {String} pub the pub to be checked
   * @returns {Boolean} is it valid?
   */
  public isValidPub(pub: string): boolean {
    return utils.isValidPublicKey(pub);
  }

  /**
   * Get fee info from server
   */
  public async getFeeInfo(): Promise<FeeInfo> {
    return this.bitgo.get(this.url('/public/feeinfo')).result();
  }

  /** @inheritdoc */
  valuelessTransferAllowed(): boolean {
    return true;
  }

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

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

  public getTokenEnablementConfig(): TokenEnablementConfig {
    return {
      requiresTokenEnablement: true,
      supportsMultipleTokenEnablements: false,
    };
  }

  /**
   * Assemble keychain and half-sign prebuilt transaction
   * @param params
   * - txPrebuild
   * - prv
   * @returns Bluebird<HalfSignedTransaction>
   */
  public async signTransaction({
    txPrebuild,
    prv,
    isLastSignature,
  }: SignTransactionOptions): Promise<HalfSignedTransaction | RecoveryTransaction> {
    if (_.isUndefined(txPrebuild) || !_.isObject(txPrebuild)) {
      if (!_.isUndefined(txPrebuild) && !_.isObject(txPrebuild)) {
        throw new Error(`txPrebuild must be an object, got type ${typeof txPrebuild}`);
      }
      throw new Error('missing txPrebuild parameter');
    }

    if (_.isUndefined(prv) || !_.isString(prv)) {
      if (!_.isUndefined(prv) && !_.isString(prv)) {
        throw new Error(`prv must be a string, got type ${typeof prv}`);
      }
      throw new Error('missing prv parameter to sign transaction');
    }

    if (!txPrebuild.txHex) {
      throw new Error(`missing txHex in txPrebuild`);
    }
    const keyPair = new XrpKeyPair({ prv });
    const address = keyPair.getAddress();
    const privateKey = (keyPair.getPrivateKey() as Buffer).toString('hex');

    const tx = ripple.signWithPrivateKey(txPrebuild.txHex, privateKey, {
      signAs: address,
    });

    // Normally the SDK provides the first signature for an XRP tx, but occasionally it provides the final one as well
    // (recoveries)
    if (isLastSignature) {
      return { txHex: tx.signedTransaction };
    }
    return { halfSigned: { txHex: tx.signedTransaction } };
  }

  /**
   * Ripple requires additional parameters for wallet generation to be sent to the server. The additional parameters are
   * the root public key, which is the basis of the root address, two signed, and one half-signed initialization txs
   * @param walletParams
   * - rootPrivateKey: optional hex-encoded Ripple private key
   */
  async supplementGenerateWallet(
    walletParams: SupplementGenerateWalletOptions
  ): Promise<SupplementGenerateWalletOptions> {
    if (walletParams.rootPrivateKey) {
      if (walletParams.rootPrivateKey.length !== 64) {
        throw new Error('rootPrivateKey needs to be a hexadecimal private key string');
      }
    } else {
      const keyPair = new XrpKeyPair().getKeys();
      if (!keyPair.prv) {
        throw new Error('no privateKey');
      }
      walletParams.rootPrivateKey = keyPair.prv;
    }
    return walletParams;
  }

  /**
   * Explain/parse transaction
   * @param params
   */
  async explainTransaction(params: ExplainTransactionOptions = {}): Promise<TransactionExplanation> {
    let transaction;
    let txHex: string = params.txHex || ((params.halfSigned && params.halfSigned.txHex) as string);
    if (!txHex) {
      throw new Error('missing required param txHex');
    }
    try {
      transaction = rippleBinaryCodec.decode(txHex);
    } catch (e) {
      try {
        transaction = JSON.parse(txHex);
        txHex = rippleBinaryCodec.encode(transaction);
      } catch (e) {
        throw new Error('txHex needs to be either hex or JSON string for XRP');
      }
    }
    let id: string;
    // hashes ids are different for signed and unsigned tx
    // first we try to get the hash id as if it is signed, will throw if its not
    try {
      id = xrpl.hashes.hashSignedTx(txHex);
    } catch (e) {
      id = xrpl.hashes.hashTx(txHex);
    }

    if (transaction.TransactionType === 'AccountSet') {
      return {
        displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'accountSet'],
        id: id,
        changeOutputs: [],
        outputAmount: 0,
        changeAmount: 0,
        outputs: [],
        fee: {
          fee: transaction.Fee,
          feeRate: undefined,
          size: txHex.length / 2,
        },
        accountSet: {
          messageKey: transaction.MessageKey,
          setFlag: transaction.SetFlag,
        },
      };
    } else if (transaction.TransactionType === 'TrustSet') {
      return {
        displayOrder: [
          'id',
          'outputAmount',
          'changeAmount',
          'outputs',
          'changeOutputs',
          'fee',
          'account',
          'limitAmount',
        ],
        id: id,
        changeOutputs: [],
        outputAmount: 0,
        changeAmount: 0,
        outputs: [],
        fee: {
          fee: transaction.Fee,
          feeRate: undefined,
          size: txHex.length / 2,
        },
        account: transaction.Account,
        limitAmount: {
          currency: transaction.LimitAmount.currency,
          issuer: transaction.LimitAmount.issuer,
          value: transaction.LimitAmount.value,
        },
      };
    }

    const address =
      transaction.Destination + (transaction.DestinationTag >= 0 ? '?dt=' + transaction.DestinationTag : '');
    return {
      displayOrder: ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee'],
      id: id,
      changeOutputs: [],
      outputAmount: transaction.Amount,
      changeAmount: 0,
      outputs: [
        {
          address,
          amount: transaction.Amount,
        },
      ],
      fee: {
        fee: transaction.Fee,
        feeRate: undefined,
        size: txHex.length / 2,
      },
    };
  }

  /**
   * Verify that a transaction prebuild complies with the original intention
   * @param txParams params object passed to send
   * @param txPrebuild prebuild object returned by server
   * @param wallet
   * @returns {boolean}
   */
  public async verifyTransaction({ txParams, txPrebuild }: VerifyTransactionOptions): Promise<boolean> {
    const coinConfig = coins.get(this.getChain()) as XrpCoin;
    const explanation = await this.explainTransaction({
      txHex: txPrebuild.txHex,
    });

    const output = [...explanation.outputs, ...explanation.changeOutputs][0];
    const expectedOutput = txParams.recipients && txParams.recipients[0];

    const comparator = (recipient1, recipient2) => {
      if (recipient1.address !== recipient2.address) {
        return false;
      }
      const amount1 = new BigNumber(recipient1.amount);
      const amount2 = new BigNumber(recipient2.amount);
      return amount1.toFixed() === amount2.toFixed();
    };

    if (
      (txParams.type === undefined || txParams.type === 'payment') &&
      typeof output.amount !== 'object' &&
      !comparator(output, expectedOutput)
    ) {
      throw new Error('transaction prebuild does not match expected output');
    }

    if (txParams.type === 'enabletoken') {
      if (txParams.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.`
        );
      }
      const recipient = txParams.recipients[0];
      if (!recipient.tokenName) {
        throw new Error('Recipient must include a token name.');
      }
      const recipientCurrency = utils.getXrpCurrencyFromTokenName(recipient.tokenName).currency;
      if (coinConfig.isToken) {
        if (recipientCurrency !== coinConfig.currencyCode) {
          throw new Error('Incorrect token name specified in recipients');
        }
      }
      if (!('account' in explanation) || !('limitAmount' in explanation) || !explanation.limitAmount.currency) {
        throw new Error('Explanation is missing required keys (account or limitAmount with currency)');
      }
      const baseAddress = explanation.account;
      const currency = explanation.limitAmount.currency;

      if (recipient.address !== baseAddress || recipientCurrency !== currency) {
        throw new Error('Tx outputs does not match with expected txParams recipients');
      }
    }
    return true;
  }

  /**
   * Check if address is a valid XRP address, and then make sure the root addresses match.
   * This prevents attacks where an attack may switch out the new address for one of their own
   * @param address {String} the address to verify
   * @param rootAddress {String} the wallet's root address
   * @return true iff address is a wallet address (based on rootAddress)
   */
  public async isWalletAddress({ address, rootAddress }: VerifyAddressOptions): Promise<boolean> {
    if (!this.isValidAddress(address)) {
      throw new InvalidAddressError(`address verification failure: address "${address}" is not valid`);
    }

    const accountInfoParams = {
      method: 'account_info',
      params: [
        {
          account: address,
          ledger_index: 'current',
          queue: true,
          strict: true,
          signer_lists: true,
        },
      ],
    };

    const accountInfo = (await this.bitgo.post(this.getRippledUrl()).send(accountInfoParams)).body;

    if (accountInfo?.result?.account_data?.Flags == null) {
      throw new Error('Invalid account information: Flags field is missing.');
    }

    const flags = xrpl.parseAccountRootFlags(accountInfo.result.account_data.Flags);

    const addressDetails = utils.getAddressDetails(address);
    const rootAddressDetails = utils.getAddressDetails(rootAddress);

    if (flags.lsfRequireDestTag && addressDetails.destinationTag == null) {
      throw new InvalidAddressError(`Invalid Address: Destination Tag is required for address "${address}".`);
    }

    if (addressDetails.address !== rootAddressDetails.address) {
      throw new UnexpectedAddressError(
        `address validation failure: ${addressDetails.address} vs. ${rootAddressDetails.address}`
      );
    }

    return true;
  }

  /**
   * URL of a well-known, public facing (non-bitgo) rippled instance which can be used for recovery
   */
  public getRippledUrl(): string {
    return 'https://s1.ripple.com:51234';
  }

  /**
   * Builds a funds recovery transaction without BitGo
   * @param params
   * - rootAddress: root XRP wallet address to recover funds from
   * - userKey: [encrypted] xprv
   * - backupKey: [encrypted] xprv, or xpub if the xprv is held by a KRS provider
   * - walletPassphrase: necessary if one of the xprvs is encrypted
   * - bitgoKey: xpub
   * - krsProvider: necessary if backup key is held by KRS
   * - recoveryDestination: target address to send recovered funds to
   */
  public async recover(params: RecoveryOptions): Promise<RecoveryInfo | RecoveryTransaction> {
    const rippledUrl = this.getRippledUrl();
    const isKrsRecovery = params.backupKey.startsWith('xpub') && !params.userKey.startsWith('xpub');
    const isUnsignedSweep = params.backupKey.startsWith('xpub') && params.userKey.startsWith('xpub');

    const accountInfoParams = {
      method: 'account_info',
      params: [
        {
          account: params.rootAddress,
          ledger_index: 'current',
          queue: true,
          strict: true,
          signer_lists: true,
        },
      ],
    };

    const accountLinesParams = {
      method: 'account_lines',
      params: [
        {
          account: params.rootAddress,
          ledger_index: 'validated',
        },
      ],
    };

    if (isKrsRecovery) {
      checkKrsProvider(this, params.krsProvider);
    }

    // Validate the destination address
    if (!this.isValidAddress(params.recoveryDestination)) {
      throw new Error('Invalid destination address!');
    }

    const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: false });

    const { addressDetails, feeDetails, serverDetails, accountLines } = await promiseProps({
      addressDetails: this.bitgo.post(rippledUrl).send(accountInfoParams),
      feeDetails: this.bitgo.post(rippledUrl).send({ method: 'fee' }),
      serverDetails: this.bitgo.post(rippledUrl).send({ method: 'server_info' }),
      accountLines: this.bitgo.post(rippledUrl).send(accountLinesParams),
    });

    const openLedgerFee = new BigNumber(feeDetails.body.result.drops.open_ledger_fee);
    const baseReserve = new BigNumber(serverDetails.body.result.info.validated_ledger.reserve_base_xrp).times(
      this.getBaseFactor()
    );
    const reserveDelta = new BigNumber(serverDetails.body.result.info.validated_ledger.reserve_inc_xrp).times(
      this.getBaseFactor()
    );
    const currentLedger = serverDetails.body.result.info.validated_ledger.seq;
    const sequenceId = addressDetails.body.result.account_data.Sequence;
    const balance = new BigNumber(addressDetails.body.result.account_data.Balance);
    const signerLists = addressDetails.body.result.account_data.signer_lists;
    const accountFlags = addressDetails.body.result.account_data.Flags;
    const ownerCount = new BigNumber(addressDetails.body.result.account_data.OwnerCount);

    // make sure there is only one signer list set
    if (signerLists.length !== 1) {
      throw new Error('unexpected set of signer lists');
    }

    // make sure the signers are user, backup, bitgo
    const userAddress = rippleKeypairs.deriveAddress(keys[0].publicKey.toString('hex'));
    const backupAddress = rippleKeypairs.deriveAddress(keys[1].publicKey.toString('hex'));

    const signerList = signerLists[0];
    if (signerList.SignerQuorum !== 2) {
      throw new Error('invalid minimum signature count');
    }
    const foundAddresses = {};

    const signerEntries = signerList.SignerEntries;
    if (signerEntries.length !== 3) {
      throw new Error('invalid signer list length');
    }
    for (const { SignerEntry } of signerEntries) {
      const weight = SignerEntry.SignerWeight;
      const address = SignerEntry.Account;
      if (weight !== 1) {
        throw new Error('invalid signer weight');
      }

      // if it's a dupe of an address we already know, block
      if (foundAddresses[address] >= 1) {
        throw new Error('duplicate signer address');
      }
      foundAddresses[address] = (foundAddresses[address] || 0) + 1;
    }

    if (foundAddresses[userAddress] !== 1) {
      throw new Error('unexpected incidence frequency of user signer address');
    }
    if (foundAddresses[backupAddress] !== 1) {
      throw new Error('unexpected incidence frequency of user signer address');
    }

    // make sure the flags disable the master key and enforce destination tags
    const USER_KEY_SETTING_FLAG = 65536;
    const MASTER_KEY_DEACTIVATION_FLAG = 1048576;
    const REQUIRE_DESTINATION_TAG_FLAG = 131072;
    if ((accountFlags & USER_KEY_SETTING_FLAG) !== 0) {
      throw new Error('a custom user key has been set');
    }
    if ((accountFlags & MASTER_KEY_DEACTIVATION_FLAG) !== MASTER_KEY_DEACTIVATION_FLAG) {
      throw new Error('the master key has not been deactivated');
    }
    if ((accountFlags & REQUIRE_DESTINATION_TAG_FLAG) !== REQUIRE_DESTINATION_TAG_FLAG) {
      throw new Error('the destination flag requirement has not been activated');
    }

    // recover the funds
    const totalReserveDelta = reserveDelta.times(ownerCount);
    const reserve = baseReserve.plus(totalReserveDelta);
    const recoverableBalance = balance.minus(reserve);

    const rawDestination = params.recoveryDestination;
    const destinationDetails = url.parse(rawDestination);

    if (destinationDetails.query) {
      const queryDetails = querystring.parse(destinationDetails.query);
      if (Array.isArray(queryDetails.dt)) {
        // if queryDetails.dt is an array, that means dt was given multiple times, which is not valid
        throw new InvalidAddressError(
          `destination tag can appear at most once, but ${queryDetails.dt.length} destination tags were found`
        );
      }
    }

    if (recoverableBalance.toNumber() <= 0) {
      throw new Error(
        `Quantity of XRP to recover must be greater than 0. Current balance: ${balance.toNumber()}, blockchain reserve: ${reserve.toNumber()}, spendable balance: ${recoverableBalance.toNumber()}`
      );
    }

    const issuer = params?.issuerAddress;
    const currency = params?.currencyCode;
    if (!!issuer && !!currency) {
      const tokenParams = {
        recoveryDestination: params.recoveryDestination,
        recoverableBalance,
        currentLedger,
        openLedgerFee,
        sequenceId,
        accountLines,
        keys,
        isKrsRecovery,
        isUnsignedSweep,
        userAddress,
        backupAddress,
        issuer,
        currency,
      };

      return this.recoverXrpToken(params, tokenParams);
    }

    const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
    const txBuilder = factory.getTransferBuilder() as TransferBuilder;
    txBuilder
      .to(params.recoveryDestination as string)
      .amount(recoverableBalance.toFixed(0))
      .sender(params.rootAddress)
      .flags(2147483648)
      .lastLedgerSequence(currentLedger + 1000000) // give it 1 million ledgers' time (~1 month, suitable for KRS)
      .fee(openLedgerFee.times(3).toFixed(0)) // the factor three is for the multisigning
      .sequence(sequenceId);

    const tx = await txBuilder.build();
    const serializedTx = tx.toBroadcastFormat();

    if (isUnsignedSweep) {
      return {
        txHex: serializedTx,
        coin: this.getChain(),
      };
    }

    if (!keys[0].privateKey) {
      throw new Error(`userKey is not a private key`);
    }
    const userKey = keys[0].privateKey.toString('hex');
    const userSignature = ripple.signWithPrivateKey(serializedTx, userKey, { signAs: userAddress });

    let signedTransaction: string;

    if (isKrsRecovery) {
      signedTransaction = userSignature.signedTransaction;
    } else {
      if (!keys[1].privateKey) {
        throw new Error(`backupKey is not a private key`);
      }
      const backupKey = keys[1].privateKey.toString('hex');
      const backupSignature = ripple.signWithPrivateKey(serializedTx, backupKey, { signAs: backupAddress });
      signedTransaction = ripple.multisign([userSignature.signedTransaction, backupSignature.signedTransaction]);
    }

    const transactionExplanation: RecoveryInfo = (await this.explainTransaction({
      txHex: signedTransaction,
    })) as RecoveryInfo;

    transactionExplanation.txHex = signedTransaction;

    if (isKrsRecovery) {
      transactionExplanation.backupKey = params.backupKey;
      transactionExplanation.coin = this.getChain();
    }
    return transactionExplanation;
  }

  public async recoverXrpToken(params, tokenParams) {
    const { currency, issuer } = tokenParams;
    const tokenName = (utils.getXrpToken(issuer, currency) as XrpCoin).name;
    const lines = tokenParams.accountLines.body.result.lines;

    let amount;
    for (const line of lines) {
      if (line.currency === currency && line.account === issuer) {
        amount = line.balance;
        break;
      }
    }

    if (amount === undefined) {
      throw new Error(`Does not have Trustline with ${issuer}`);
    }
    if (amount === '0') {
      throw new Error(`Does not have funds to recover`);
    }

    const decimalPlaces = coins.get(tokenName).decimalPlaces;
    amount = new BigNumber(amount).shiftedBy(decimalPlaces).toFixed();

    const FLAG_VALUE = 2147483648;

    const factory = new TransactionBuilderFactory(coins.get(tokenName));
    const txBuilder = factory.getTokenTransferBuilder() as TokenTransferBuilder;
    txBuilder
      .to(tokenParams.recoveryDestination)
      .amount(amount)
      .sender(params.rootAddress)
      .flags(FLAG_VALUE)
      .lastLedgerSequence(tokenParams.currentLedger + 1000000) // give it 1 million ledgers' time (~1 month, suitable for KRS)
      .fee(tokenParams.openLedgerFee.times(3).toFixed(0)) // the factor three is for the multisigning
      .sequence(tokenParams.sequenceId);

    const tx = await txBuilder.build();
    const serializedTx = tx.toBroadcastFormat();

    const { keys, isKrsRecovery, isUnsignedSweep, userAddress, backupAddress } = tokenParams;

    if (isUnsignedSweep) {
      return {
        txHex: serializedTx,
        coin: this.getChain(),
      };
    }

    if (!keys[0].privateKey) {
      throw new Error(`userKey is not a private key`);
    }

    const userKey = keys[0].privateKey.toString('hex');
    const userSignature = ripple.signWithPrivateKey(serializedTx, userKey, { signAs: userAddress });

    let signedTransaction: string;

    if (isKrsRecovery) {
      signedTransaction = userSignature.signedTransaction;
    } else {
      if (!keys[1].privateKey) {
        throw new Error(`backupKey is not a private key`);
      }
      const backupKey = keys[1].privateKey.toString('hex');
      const backupSignature = ripple.signWithPrivateKey(serializedTx, backupKey, { signAs: backupAddress });
      signedTransaction = ripple.multisign([userSignature.signedTransaction, backupSignature.signedTransaction]);
    }

    const transactionExplanation: RecoveryInfo = (await this.explainTransaction({
      txHex: signedTransaction,
    })) as RecoveryInfo;

    transactionExplanation.txHex = signedTransaction;

    if (isKrsRecovery) {
      transactionExplanation.backupKey = params.backupKey;
      transactionExplanation.coin = this.getChain();
    }
    return transactionExplanation;
  }

  /**
   * Generate a new keypair for this coin.
   * @param seed Seed from which the new keypair should be generated, otherwise a random seed is used
   */
  public generateKeyPair(seed?: Buffer): KeyPair {
    const keyPair = seed ? new XrpKeyPair({ seed }) : new XrpKeyPair();
    const keys = keyPair.getExtendedKeys();
    if (!keys.xprv) {
      throw new Error('Missing prv in key generation.');
    }
    return {
      pub: keys.xpub,
      prv: keys.xprv,
    };
  }

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

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


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