PHP WebShell

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

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

import {
  BaseCoin,
  BitGoBase,
  EDDSAMethods,
  InvalidAddressError,
  KeyPair,
  MPCAlgorithm,
  MultisigType,
  multisigTypes,
  ParsedTransaction,
  ParseTransactionOptions,
  SignedTransaction,
  SignTransactionOptions,
  TransactionExplanation,
  TssVerifyAddressOptions,
  VerifyTransactionOptions,
  EDDSAMethodTypes,
  MPCRecoveryOptions,
  MPCTx,
  MPCUnsignedTx,
  RecoveryTxRequest,
  OvcInput,
  OvcOutput,
  Environments,
  MPCSweepTxs,
  PublicKey,
  MPCTxs,
  MPCSweepRecoveryOptions,
} from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin, coins } from '@bitgo/statics';
import { KeyPair as TonKeyPair } from './lib/keyPair';
import BigNumber from 'bignumber.js';
import * as _ from 'lodash';
import { Transaction, TransactionBuilderFactory, Utils, TransferBuilder } from './lib';
import TonWeb from 'tonweb';
import { getDerivationPath } from '@bitgo/sdk-lib-mpc';
import { getFeeEstimate } from './lib/utils';

export interface TonParseTransactionOptions extends ParseTransactionOptions {
  txHex: string;
  fromAddressBounceable?: boolean;
  toAddressBounceable?: boolean;
}

export class Ton extends BaseCoin {
  protected readonly _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 Ton(bitgo, staticsCoin);
  }

  /**
   * Factor between the coin's base unit and its smallest subdivison
   */
  public getBaseFactor(): number {
    return 1e9;
  }

  public getChain(): string {
    return 'ton';
  }

  public getFamily(): string {
    return 'ton';
  }

  public getFullName(): string {
    return 'Ton';
  }

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

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

  getMPCAlgorithm(): MPCAlgorithm {
    return 'eddsa';
  }

  allowsAccountConsolidations(): boolean {
    return true;
  }

  async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
    const coinConfig = coins.get(this.getChain());
    const { txPrebuild: txPrebuild, txParams: txParams } = params;
    const transaction = new Transaction(coinConfig);
    const rawTx = txPrebuild.txHex;
    if (!rawTx) {
      throw new Error('missing required tx prebuild property txHex');
    }

    transaction.fromRawTransaction(Buffer.from(rawTx, 'hex').toString('base64'));
    const explainedTx = transaction.explainTransaction();
    if (txParams.recipients !== undefined) {
      const filteredRecipients = txParams.recipients?.map((recipient) => {
        return {
          address: new TonWeb.Address(recipient.address).toString(true, true, true),
          amount: BigInt(recipient.amount),
        };
      });
      const filteredOutputs = explainedTx.outputs.map((output) => {
        return {
          address: new TonWeb.Address(output.address).toString(true, true, true),
          amount: BigInt(output.amount),
        };
      });
      if (!_.isEqual(filteredOutputs, filteredRecipients)) {
        throw new Error('Tx outputs does not match with expected txParams recipients');
      }
      let totalAmount = new BigNumber(0);
      for (const recipients of txParams.recipients) {
        totalAmount = totalAmount.plus(recipients.amount);
      }
      if (!totalAmount.isEqualTo(explainedTx.outputAmount)) {
        throw new Error('Tx total amount does not match with expected total amount field');
      }
    }
    return true;
  }

  async isWalletAddress(params: TssVerifyAddressOptions): Promise<boolean> {
    const { keychains, address: newAddress, index } = params;

    if (!this.isValidAddress(newAddress)) {
      throw new InvalidAddressError(`invalid address: ${newAddress}`);
    }

    if (!keychains) {
      throw new Error('missing required param keychains');
    }

    for (const keychain of keychains) {
      const [address, memoId] = newAddress.split('?memoId=');
      const MPC = await EDDSAMethods.getInitializedMpcInstance();
      const commonKeychain = keychain.commonKeychain as string;

      const derivationPath = 'm/' + index;
      const derivedPublicKey = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
      const expectedAddress = await Utils.default.getAddressFromPublicKey(derivedPublicKey);

      if (memoId) {
        return memoId === `${index}`;
      }

      if (address !== expectedAddress) {
        return false;
      }
    }

    return true;
  }

  async parseTransaction(params: TonParseTransactionOptions): Promise<ParsedTransaction> {
    const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
    const transactionBuilder = factory.from(Buffer.from(params.txHex, 'hex').toString('base64'));

    if (typeof params.toAddressBounceable === 'boolean') {
      transactionBuilder.toAddressBounceable(params.toAddressBounceable);
    }

    if (typeof params.fromAddressBounceable === 'boolean') {
      transactionBuilder.fromAddressBounceable(params.fromAddressBounceable);
    }

    const rebuiltTransaction = await transactionBuilder.build();
    const parsedTransaction = rebuiltTransaction.toJson();
    return {
      inputs: [
        {
          address: parsedTransaction.sender,
          amount: parsedTransaction.amount,
        },
      ],
      outputs: [
        {
          address: parsedTransaction.destination,
          amount: parsedTransaction.amount,
        },
      ],
    };
  }

  generateKeyPair(seed?: Buffer): KeyPair {
    const keyPair = seed ? new TonKeyPair({ seed }) : new TonKeyPair();
    const keys = keyPair.getKeys();
    if (!keys.prv) {
      throw new Error('Missing prv in key generation.');
    }
    return {
      pub: keys.pub,
      prv: keys.prv,
    };
  }

  isValidPub(pub: string): boolean {
    throw new Error('Method not implemented.');
  }

  isValidAddress(address: string): boolean {
    try {
      const addressBase64 = address.replace(/\+/g, '-').replace(/\//g, '_');
      const buf = Buffer.from(addressBase64.split('?memoId=')[0], 'base64');
      return buf.length === 36;
    } catch {
      return false;
    }
  }

  signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
    throw new Error('Method not implemented.');
  }

  /** @inheritDoc */
  async getSignablePayload(serializedTx: string): Promise<Buffer> {
    const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
    const rebuiltTransaction = await factory.from(serializedTx).build();
    return rebuiltTransaction.signablePayload;
  }

  /** @inheritDoc */
  async explainTransaction(params: Record<string, any>): Promise<TransactionExplanation> {
    try {
      const factory = new TransactionBuilderFactory(coins.get(this.getChain()));
      const transactionBuilder = factory.from(Buffer.from(params.txHex, 'hex').toString('base64'));

      const { toAddressBounceable, fromAddressBounceable } = params;

      if (typeof toAddressBounceable === 'boolean') {
        transactionBuilder.toAddressBounceable(toAddressBounceable);
      }

      if (typeof fromAddressBounceable === 'boolean') {
        transactionBuilder.fromAddressBounceable(fromAddressBounceable);
      }

      const rebuiltTransaction = await transactionBuilder.build();
      return rebuiltTransaction.explainTransaction();
    } catch {
      throw new Error('Invalid transaction');
    }
  }

  protected getPublicNodeUrl(): string {
    return Environments[this.bitgo.getEnv()].tonNodeUrl;
  }

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

  async recover(params: MPCRecoveryOptions): Promise<MPCTx | MPCSweepTxs> {
    if (!params.bitgoKey) {
      throw new Error('missing bitgoKey');
    }
    if (!params.recoveryDestination || !this.isValidAddress(params.recoveryDestination)) {
      throw new Error('invalid recoveryDestination');
    }
    if (!params.apiKey) {
      throw new Error('missing apiKey');
    }
    const bitgoKey = params.bitgoKey.replace(/\s/g, '');
    const isUnsignedSweep = !params.userKey && !params.backupKey && !params.walletPassphrase;

    // Build the transaction
    const tonweb = new TonWeb(new TonWeb.HttpProvider(this.getPublicNodeUrl(), { apiKey: params.apiKey }));
    const MPC = await EDDSAMethods.getInitializedMpcInstance();

    const index = params.index || 0;
    const currPath = params.seed ? getDerivationPath(params.seed) + `/${index}` : `m/${index}`;
    const accountId = MPC.deriveUnhardened(bitgoKey, currPath).slice(0, 64);
    const senderAddr = await Utils.default.getAddressFromPublicKey(accountId);
    const balance = await tonweb.getBalance(senderAddr);
    if (new BigNumber(balance).isEqualTo(0)) {
      throw Error('Did not find address with funds to recover');
    }

    const WalletClass = tonweb.wallet.all['v4R2'];
    const wallet = new WalletClass(tonweb.provider, {
      publicKey: tonweb.utils.hexToBytes(accountId),
      wc: 0,
    });
    let seqno = await wallet.methods.seqno().call();
    if (seqno === null) {
      seqno = 0;
    }

    const feeEstimate = await getFeeEstimate(wallet, params.recoveryDestination, balance, seqno as number);

    const totalFeeEstimate = Math.round(
      (feeEstimate.source_fees.in_fwd_fee +
        feeEstimate.source_fees.storage_fee +
        feeEstimate.source_fees.gas_fee +
        feeEstimate.source_fees.fwd_fee) *
        1.5
    );

    if (new BigNumber(totalFeeEstimate).gt(balance)) {
      throw Error('Did not find address with funds to recover');
    }

    const factory = this.getBuilder();
    const expireAt = Math.floor(Date.now() / 1e3) + 60 * 60 * 24 * 7; // 7 days

    const txBuilder = factory
      .getTransferBuilder()
      .sender(senderAddr)
      .sequenceNumber(seqno as number)
      .publicKey(accountId)
      .expireTime(expireAt);

    (txBuilder as TransferBuilder).send({
      address: params.recoveryDestination,
      amount: new BigNumber(balance).minus(new BigNumber(totalFeeEstimate)).toString(),
    });

    const unsignedTransaction = await txBuilder.build();

    if (!isUnsignedSweep) {
      if (!params.userKey) {
        throw new Error('missing userKey');
      }
      if (!params.backupKey) {
        throw new Error('missing backupKey');
      }
      if (!params.walletPassphrase) {
        throw new Error('missing wallet passphrase');
      }

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

      let userPrv;

      try {
        userPrv = this.bitgo.decrypt({
          input: userKey,
          password: params.walletPassphrase,
        });
      } catch (e) {
        throw new Error(`Error decrypting user keychain: ${e.message}`);
      }
      const userSigningMaterial = JSON.parse(userPrv) as EDDSAMethodTypes.UserSigningMaterial;

      let backupPrv;
      try {
        backupPrv = this.bitgo.decrypt({
          input: backupKey,
          password: params.walletPassphrase,
        });
      } catch (e) {
        throw new Error(`Error decrypting backup keychain: ${e.message}`);
      }
      const backupSigningMaterial = JSON.parse(backupPrv) as EDDSAMethodTypes.BackupSigningMaterial;

      const signatureHex = await EDDSAMethods.getTSSSignature(
        userSigningMaterial,
        backupSigningMaterial,
        currPath,
        unsignedTransaction
      );

      const publicKeyObj = { pub: senderAddr };
      txBuilder.addSignature(publicKeyObj as PublicKey, signatureHex);
    }

    const completedTransaction = await txBuilder.build();
    const serializedTx = completedTransaction.toBroadcastFormat();
    const walletCoin = this.getChain();

    const inputs: OvcInput[] = [];
    for (const input of completedTransaction.inputs) {
      inputs.push({
        address: input.address,
        valueString: input.value,
        value: new BigNumber(input.value).toNumber(),
      });
    }
    const outputs: OvcOutput[] = [];
    for (const output of completedTransaction.outputs) {
      outputs.push({
        address: output.address,
        valueString: output.value,
        coinName: output.coin,
      });
    }
    const spendAmount = completedTransaction.inputs.length === 1 ? completedTransaction.inputs[0].value : 0;
    const parsedTx = { inputs: inputs, outputs: outputs, spendAmount: spendAmount, type: '' };
    const feeInfo = { fee: totalFeeEstimate, feeString: totalFeeEstimate.toString() };
    const coinSpecific = { commonKeychain: bitgoKey };
    if (isUnsignedSweep) {
      const transaction: MPCTx = {
        serializedTx: serializedTx,
        scanIndex: index,
        coin: walletCoin,
        signableHex: completedTransaction.signablePayload.toString('hex'),
        derivationPath: currPath,
        parsedTx: parsedTx,
        feeInfo: feeInfo,
        coinSpecific: coinSpecific,
      };
      const unsignedTx: MPCUnsignedTx = { unsignedTx: transaction, signatureShares: [] };
      const transactions: MPCUnsignedTx[] = [unsignedTx];
      const txRequest: RecoveryTxRequest = {
        transactions: transactions,
        walletCoin: walletCoin,
      };
      const txRequests: MPCSweepTxs = { txRequests: [txRequest] };
      return txRequests;
    }

    const transaction: MPCTx = {
      serializedTx: serializedTx,
      scanIndex: index,
    };
    return transaction;
  }

  /**
   * Creates funds sweep recovery transaction(s) without BitGo
   *
   * @param {MPCSweepRecoveryOptions} params parameters needed to combine the signatures
   * and transactions to create broadcastable transactions
   *
   * @returns {MPCTxs} array of the serialized transaction hex strings and indices
   * of the addresses being swept
   */
  async createBroadcastableSweepTransaction(params: MPCSweepRecoveryOptions): Promise<MPCTxs> {
    const req = params.signatureShares;
    const broadcastableTransactions: MPCTx[] = [];
    let lastScanIndex = 0;

    for (let i = 0; i < req.length; i++) {
      const MPC = await EDDSAMethods.getInitializedMpcInstance();
      const transaction = req[i].txRequest.transactions[0].unsignedTx;
      if (!req[i].ovc || !req[i].ovc[0].eddsaSignature) {
        throw new Error('Missing signature(s)');
      }
      const signature = req[i].ovc[0].eddsaSignature;
      if (!transaction.signableHex) {
        throw new Error('Missing signable hex');
      }
      const messageBuffer = Buffer.from(transaction.signableHex!, 'hex');
      const result = MPC.verify(messageBuffer, signature);
      if (!result) {
        throw new Error('Invalid signature');
      }
      const signatureHex = Buffer.concat([Buffer.from(signature.R, 'hex'), Buffer.from(signature.sigma, 'hex')]);
      const txBuilder = this.getBuilder().from(transaction.serializedTx as string);
      if (!transaction.coinSpecific?.commonKeychain) {
        throw new Error('Missing common keychain');
      }
      const commonKeychain = transaction.coinSpecific!.commonKeychain! as string;
      if (!transaction.derivationPath) {
        throw new Error('Missing derivation path');
      }
      const derivationPath = transaction.derivationPath as string;
      const accountId = MPC.deriveUnhardened(commonKeychain, derivationPath).slice(0, 64);
      const tonKeyPair = new TonKeyPair({ pub: accountId });

      // add combined signature from ovc
      txBuilder.addSignature({ pub: tonKeyPair.getKeys().pub }, signatureHex);
      const signedTransaction = await txBuilder.build();
      const serializedTx = signedTransaction.toBroadcastFormat();

      broadcastableTransactions.push({
        serializedTx: serializedTx,
        scanIndex: transaction.scanIndex,
      });

      if (i === req.length - 1 && transaction.coinSpecific!.lastScanIndex) {
        lastScanIndex = transaction.coinSpecific!.lastScanIndex as number;
      }
    }

    return { transactions: broadcastableTransactions, lastScanIndex };
  }
}

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


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