PHP WebShell

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

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

/**
 * @prettier
 */
import { CoinFamily, BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
import {
  BaseCoin,
  BitGoBase,
  KeyPair,
  ParsedTransaction,
  ParseTransactionOptions,
  SignedTransaction,
  SignTransactionOptions,
  VerifyAddressOptions as BaseVerifyAddressOptions,
  VerifyTransactionOptions,
  TransactionFee,
  TransactionRecipient as Recipient,
  TransactionPrebuild as BaseTransactionPrebuild,
  TransactionExplanation,
  Memo,
  TokenEnablementConfig,
  BaseBroadcastTransactionOptions,
  BaseBroadcastTransactionResult,
  NotSupported,
  MultisigType,
  multisigTypes,
} from '@bitgo/sdk-core';
import { BigNumber } from 'bignumber.js';
import * as stellar from 'stellar-sdk';
import { SeedValidator } from './seedValidator';
import { KeyPair as HbarKeyPair, TransactionBuilderFactory, Transaction } from './lib';
import * as Utils from './lib/utils';
import * as _ from 'lodash';
import {
  Client,
  Transaction as HbarTransaction,
  AccountBalanceQuery,
  AccountBalanceJson,
  Hbar as HbarUnit,
} from '@hashgraph/sdk';
import { PUBLIC_KEY_PREFIX } from './lib/keyPair';
export interface HbarSignTransactionOptions extends SignTransactionOptions {
  txPrebuild: TransactionPrebuild;
  prv: string;
}

export interface TxInfo {
  recipients: Recipient[];
  from: string;
  txid: string;
}

export interface TransactionPrebuild extends BaseTransactionPrebuild {
  txHex: string;
  txInfo: TxInfo;
  feeInfo: TransactionFee;
  source: string;
}

export interface ExplainTransactionOptions {
  txHex?: string;
  halfSigned?: {
    txHex: string;
  };
  feeInfo?: TransactionFee;
  // TODO(BG-24809): get the memo from the toJson
  memo?: {
    type: string;
    value: string;
  };
}

export interface HbarVerifyTransactionOptions extends VerifyTransactionOptions {
  txPrebuild: TransactionPrebuild;
  memo?: Memo;
}

interface VerifyAddressOptions extends BaseVerifyAddressOptions {
  baseAddress: string;
}

export interface RecoveryOptions {
  backupKey: string;
  userKey: string;
  rootAddress: string;
  recoveryDestination: string;
  bitgoKey?: string;
  walletPassphrase?: string;
  maxFee?: string;
  nodeId?: string;
  startTime?: string;
  tokenId?: string;
}

export interface RecoveryInfo {
  id: string;
  tx: string;
  coin: string;
  startTime: string;
  nodeId: string;
}

export interface OfflineVaultTxInfo {
  txHex: string;
  userKey: string;
  backupKey: string;
  bitgoKey?: string;
  address: string;
  coin: string;
  maxFee: string;
  recipients: Recipient[];
  amount: string;
  startTime: string;
  validDuration: string;
  nodeId: string;
  memo: string;
  json?: any;
}

export interface BroadcastTransactionOptions extends BaseBroadcastTransactionOptions {
  startTime?: string;
}

export interface BroadcastTransactionResult extends BaseBroadcastTransactionResult {
  status?: string;
}

export class Hbar extends BaseCoin {
  protected readonly _staticsCoin: Readonly<StaticsBaseCoin>;

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

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

    this._staticsCoin = staticsCoin;
  }

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

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

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

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

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

  /**
   * Flag for sending value of 0
   * @returns {boolean} True if okay to send 0 value, false otherwise
   */
  valuelessTransferAllowed(): boolean {
    return false;
  }

  /**
   * Checks if this is a valid base58 or hex address
   * @param address
   */
  isValidAddress(address: string): boolean {
    try {
      return Utils.isValidAddressWithPaymentId(address);
    } catch (e) {
      return false;
    }
  }

  /** inheritdoc */
  deriveKeyWithSeed(): { derivationPath: string; key: string } {
    throw new NotSupported('method deriveKeyWithSeed not supported for eddsa curve');
  }

  /** inheritdoc */
  generateKeyPair(seed?: Buffer): KeyPair {
    const keyPair = seed ? new HbarKeyPair({ seed }) : new HbarKeyPair();
    const keys = keyPair.getKeys();

    if (!keys.prv) {
      throw new Error('Keypair generation failed to generate a prv');
    }

    return {
      pub: keys.pub,
      prv: keys.prv,
    };
  }

  /** inheritdoc */
  generateRootKeyPair(seed?: Buffer): KeyPair {
    const keyPair = seed ? new HbarKeyPair({ seed }) : new HbarKeyPair();
    const keys = keyPair.getKeys(true);
    if (!keys.prv) {
      throw new Error('Missing prv in key generation.');
    }
    return { prv: keys.prv + keys.pub, pub: keys.pub };
  }

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

  /**
   * Check if address is valid, then make sure it matches the base address.
   *
   * @param {VerifyAddressOptions} params
   * @param {String} params.address - the address to verify
   * @param {String} params.baseAddress - the base address from the wallet
   */
  async isWalletAddress(params: VerifyAddressOptions): Promise<boolean> {
    const { address, baseAddress } = params;
    return Utils.isSameBaseAddress(address, baseAddress);
  }

  async verifyTransaction(params: HbarVerifyTransactionOptions): Promise<boolean> {
    // asset name to transfer amount map
    const coinConfig = coins.get(this.getChain());
    const { txParams: txParams, txPrebuild: txPrebuild, memo: memo } = params;
    const transaction = new Transaction(coinConfig);
    if (!txPrebuild.txHex) {
      throw new Error('missing required tx prebuild property txHex');
    }

    transaction.fromRawTransaction(txPrebuild.txHex);
    const explainTxParams: ExplainTransactionOptions = {
      txHex: txPrebuild.txHex,
      feeInfo: txPrebuild.feeInfo,
      memo: memo,
    };
    const explainedTx = await this.explainTransaction(explainTxParams);

    if (!txParams.recipients) {
      throw new Error('missing required tx params property recipients');
    }

    // for enabletoken, recipient output amount is 0
    const recipients = txParams.recipients.map((recipient) => ({
      ...recipient,
      amount: txParams.type === 'enabletoken' ? '0' : recipient.amount,
    }));
    if (coinConfig.isToken) {
      recipients.forEach((recipient) => {
        if (recipient.tokenName !== undefined && recipient.tokenName !== coinConfig.name) {
          throw new Error('Incorrect token name specified in recipients');
        }
        recipient.tokenName = coinConfig.name;
      });
    }

    // verify recipients from params and explainedTx
    const filteredRecipients = recipients?.map((recipient) => _.pick(recipient, ['address', 'amount', 'tokenName']));
    const filteredOutputs = explainedTx.outputs.map((output) => _.pick(output, ['address', 'amount', 'tokenName']));

    if (!_.isEqual(filteredOutputs, filteredRecipients)) {
      throw new Error('Tx outputs does not match with expected txParams recipients');
    }

    return true;
  }

  /**
   * Assemble keychain and half-sign prebuilt transaction
   *
   * @param params
   * @param params.txPrebuild {Object} prebuild object returned by platform
   * @param params.prv {String} user prv
   * @returns Promise<SignedTransaction>
   */
  async signTransaction(params: HbarSignTransactionOptions): Promise<SignedTransaction> {
    const factory = this.getBuilderFactory();
    const txBuilder = factory.from(params.txPrebuild.txHex);
    txBuilder.sign({ key: params.prv });

    const transaction = await txBuilder.build();

    if (!transaction) {
      throw new Error('Invalid messaged passed to signMessage');
    }

    const response = {
      txHex: transaction.toBroadcastFormat(),
    };
    return transaction.signature.length >= 2 ? response : { halfSigned: response };
  }

  /**
   * Sign message with private key
   *
   * @param key
   * @param message
   * @return {Buffer} A signature over the given message using the given key
   */
  async signMessage(key: KeyPair, message: string | Buffer): Promise<Buffer> {
    const msg = Buffer.isBuffer(message) ? message.toString('utf8') : message;
    // reconstitute keys and sign
    return Buffer.from(new HbarKeyPair({ prv: key.prv }).signMessage(msg));
  }

  /**
   * Builds a funds recovery transaction without BitGo.
   * We need to do three queries during this:
   * 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 params
   */
  public async recover(params: RecoveryOptions): Promise<RecoveryInfo | OfflineVaultTxInfo> {
    const isUnsignedSweep =
      (params.backupKey.startsWith(PUBLIC_KEY_PREFIX) && params.userKey.startsWith(PUBLIC_KEY_PREFIX)) ||
      (Utils.isValidPublicKey(params.userKey) && Utils.isValidPublicKey(params.backupKey));

    // Validate the root address
    if (!this.isValidAddress(params.rootAddress)) {
      throw new Error('invalid rootAddress, got: ' + params.rootAddress);
    }

    // Validate the destination address
    if (!this.isValidAddress(params.recoveryDestination)) {
      throw new Error('invalid recoveryDestination, got: ' + params.recoveryDestination);
    }

    // Validate nodeId
    if (params.nodeId && !Utils.isValidAddress(params.nodeId)) {
      throw new Error('invalid nodeId, got: ' + params.nodeId);
    }

    // validate fee
    if (params.maxFee && !Utils.isValidAmount(params.maxFee)) {
      throw new Error('invalid maxFee, got: ' + params.maxFee);
    }

    // validate startTime

    if (params.startTime) {
      Utils.validateStartTime(params.startTime);
    }

    if (isUnsignedSweep && !params.startTime) {
      throw new Error('start time is required for unsigned sweep');
    }

    if (!isUnsignedSweep && !params.walletPassphrase) {
      throw new Error('walletPassphrase is required for non-bitgo recovery');
    }

    let userPrv: string | undefined;
    let backUp: string | undefined;
    if (!isUnsignedSweep) {
      try {
        userPrv = this.bitgo.decrypt({ input: params.userKey, password: params.walletPassphrase });
        backUp = this.bitgo.decrypt({ input: params.backupKey, password: params.walletPassphrase });
      } catch (e) {
        throw new Error(
          'unable to decrypt userKey or backupKey with the walletPassphrase provided, got error: ' + e.message
        );
      }
    }

    // validate userKey for unsigned sweep
    if (isUnsignedSweep && !Utils.isValidPublicKey(params.userKey)) {
      throw new Error('invalid userKey, got: ' + params.userKey);
    }

    // validate backupKey for unsigned sweep
    if (isUnsignedSweep && !Utils.isValidPublicKey(params.backupKey)) {
      throw new Error('invalid backupKey, got: ' + params.backupKey);
    }

    const { address: destinationAddress, memoId } = Utils.getAddressDetails(params.recoveryDestination);
    const nodeId = params.nodeId ? params.nodeId : '0.0.3';
    const client = this.getHbarClient();
    const balance = await this.getAccountBalance(params.rootAddress, client);
    const fee = params.maxFee ? params.maxFee : '10000000'; // default fee to 1 hbar
    const nativeBalance = HbarUnit.fromString(balance.hbars).toTinybars().toString();
    const spendableAmount = new BigNumber(nativeBalance).minus(fee);

    let txBuilder;
    if (!params.tokenId) {
      if (spendableAmount.isZero() || spendableAmount.isNegative()) {
        throw new Error(`Insufficient balance to recover, got balance: ${nativeBalance} fee: ${fee}`);
      }
      txBuilder = this.getBuilderFactory().getTransferBuilder();
      txBuilder.send({ address: destinationAddress, amount: spendableAmount.toString() });
    } else {
      if (spendableAmount.isNegative()) {
        throw new Error(
          `Insufficient native balance to recover tokens, got native balance: ${nativeBalance} fee: ${fee}`
        );
      }
      const tokenBalance = balance.tokens.find((token) => token.tokenId === params.tokenId);
      const token = Utils.getHederaTokenNameFromId(params.tokenId);
      if (!token) {
        throw new Error(`Unsupported token: ${params.tokenId}`);
      }
      if (!tokenBalance || new BigNumber(tokenBalance.balance).isZero()) {
        throw new Error(`Insufficient balance to recover token: ${params.tokenId} for account: ${params.rootAddress}`);
      }
      txBuilder = this.getBuilderFactory().getTokenTransferBuilder();
      txBuilder.send({ address: destinationAddress, amount: tokenBalance.balance, tokenName: token.name });
    }

    txBuilder.node({ nodeId });
    txBuilder.fee({ fee });
    txBuilder.source({ address: params.rootAddress });
    txBuilder.validDuration(180);
    if (memoId) {
      txBuilder.memo(memoId);
    }

    if (params.startTime) {
      txBuilder.startTime(Utils.normalizeStarttime(params.startTime));
    }
    if (isUnsignedSweep) {
      const tx = await txBuilder.build();
      const txJson = tx.toJson();
      return {
        txHex: tx.toBroadcastFormat(),
        coin: this.getChain(),
        id: txJson.id,
        startTime: txJson.startTime,
        validDuration: txJson.validDuration,
        nodeId: txJson.node,
        memo: txJson.memo,
        userKey: params.userKey,
        backupKey: params.backupKey,
        bitgoKey: params.bitgoKey,
        maxFee: fee,
        address: params.rootAddress,
        recipients: txJson.instructionsData.params.recipients,
        amount: txJson.amount,
        json: txJson,
      };
    }

    txBuilder.sign({ key: userPrv });
    txBuilder.sign({ key: backUp });

    const tx = await txBuilder.build();

    return {
      tx: tx.toBroadcastFormat(),
      id: tx.toJson().id,
      coin: this.getChain(),
      startTime: tx.toJson().startTime,
      nodeId: tx.toJson().node,
    };
  }

  /**
   * Explain a Hedera transaction from txHex
   * @param params
   */
  async explainTransaction(params: ExplainTransactionOptions): Promise<TransactionExplanation> {
    const txHex = params.txHex || (params.halfSigned && params.halfSigned.txHex);
    if (!txHex) {
      throw new Error('missing explain tx parameters');
    }

    const factory = this.getBuilderFactory();
    const txBuilder = factory.from(txHex);
    const tx = await txBuilder.build();
    const txJson = tx.toJson();

    let outputAmount = new BigNumber(0);
    const outputs: { address: string; amount: string; memo: string; tokenName?: string }[] = [];
    // TODO(BG-24809): get the memo from the toJson
    let memo = '';
    if (params.memo) {
      memo = params.memo.value;
    }

    switch (txJson.instructionsData.type) {
      case 'cryptoTransfer':
        const recipients = txJson.instructionsData.params.recipients || [];
        recipients.forEach((recipient) => {
          if (!recipient.tokenName) {
            // token transfer doesn't change outputAmount
            outputAmount = outputAmount.plus(recipient.amount);
          }
          outputs.push({
            address: recipient.address,
            amount: recipient.amount.toString(),
            memo,
            ...(recipient.tokenName && {
              tokenName: recipient.tokenName,
            }),
          });
        });
        break;

      case 'tokenAssociate':
        const tokens = txJson.instructionsData.params.tokenNames || [];
        const accountId = txJson.instructionsData.params.accountId;
        tokens.forEach((token) => {
          outputs.push({
            address: accountId,
            amount: '0',
            memo,
            tokenName: token,
          });
        });
        break;

      default:
        throw new Error('Transaction format outside of cryptoTransfer not supported for explanation.');
    }

    const displayOrder = [
      'id',
      'outputAmount',
      'changeAmount',
      'outputs',
      'changeOutputs',
      'fee',
      'timestamp',
      'expiration',
      'memo',
    ];

    return {
      displayOrder,
      id: txJson.id,
      outputs,
      outputAmount: outputAmount.toString(),
      changeOutputs: [], // account based does not use change outputs
      changeAmount: '0', // account base does not make change
      fee: params.feeInfo?.fee || txJson.fee, // in the instance no feeInfo is passed in as a param, show the fee given by the txJSON
      timestamp: txJson.startTime,
      expiration: txJson.validDuration,
    } as any;
  }

  isStellarSeed(seed: string): boolean {
    return SeedValidator.isValidEd25519SeedForCoin(seed, CoinFamily.XLM);
  }

  convertFromStellarSeed(seed: string): string | null {
    // assume this is a trust custodial seed if its a valid ed25519 prv
    if (!this.isStellarSeed(seed) || SeedValidator.hasCompetingSeedFormats(seed)) {
      return null;
    }

    if (SeedValidator.isValidEd25519SeedForCoin(seed, CoinFamily.XLM)) {
      const keyFromSeed = new HbarKeyPair({ seed: stellar.StrKey.decodeEd25519SecretSeed(seed) });
      const keys = keyFromSeed.getKeys();
      if (keys !== undefined && keys.prv) {
        return keys.prv;
      }
    }

    return null;
  }

  isValidPub(pub: string): boolean {
    return Utils.isValidPublicKey(pub);
  }

  supportsDeriveKeyWithSeed(): boolean {
    return false;
  }

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

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

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

  private getBuilderFactory(): TransactionBuilderFactory {
    return new TransactionBuilderFactory(coins.get(this.getChain()));
  }

  private getHbarClient(): Client {
    const client = this.bitgo.getEnv() === 'prod' ? Client.forMainnet() : Client.forTestnet();
    return client;
  }

  async getAccountBalance(accountId: string, client: Client): Promise<AccountBalanceJson> {
    try {
      const balance = await new AccountBalanceQuery().setAccountId(accountId).execute(client);

      return balance.toJSON();
    } catch (e) {
      throw new Error('Failed to get account balance, error: ' + e.message);
    }
  }

  async broadcastTransaction({
    serializedSignedTransaction,
    startTime,
  }: BroadcastTransactionOptions): Promise<BroadcastTransactionResult> {
    try {
      const hbarTx = HbarTransaction.fromBytes(Utils.toUint8Array(serializedSignedTransaction));

      if (startTime) {
        Utils.isValidTimeString(startTime);
        while (!Utils.shouldBroadcastNow(startTime)) {
          await Utils.sleep(1000);
        }
      }

      return this.clientBroadcastTransaction(hbarTx);
    } catch (e) {
      throw new Error('Failed to broadcast transaction, error: ' + e.message);
    }
  }

  async clientBroadcastTransaction(hbarTx: HbarTransaction) {
    const client = this.getHbarClient();
    const transactionResponse = await hbarTx.execute(client);
    const transactionReceipt = await transactionResponse.getReceipt(client);

    return { txId: transactionResponse.transactionId.toString(), status: transactionReceipt.status.toString() };
  }
}

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


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