PHP WebShell

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

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

import { AbstractUtxoCoin, getWalletKeys, RootWalletKeys } from '@bitgo/abstract-utxo';
import {
  HalfSignedUtxoTransaction,
  IInscriptionBuilder,
  IWallet,
  KeyIndices,
  PrebuildTransactionResult,
  PreparedInscriptionRevealData,
  SubmitTransactionResponse,
  xprvToRawPrv,
  xpubToCompressedPub,
} from '@bitgo/sdk-core';
import * as utxolib from '@bitgo/utxo-lib';
import {
  createPsbtForSingleInscriptionPassingTransaction,
  DefaultInscriptionConstraints,
  InscriptionOutputs,
  inscriptions,
  parseSatPoint,
  isSatPoint,
  ErrorNoLayout,
  findOutputLayoutForWalletUnspents,
  MAX_UNSPENTS_FOR_OUTPUT_LAYOUT,
  SatPoint,
} from '@bitgo/utxo-ord';
import assert from 'assert';

const SUPPLEMENTARY_UNSPENTS_MIN_VALUE_SATS = [0, 20_000, 200_000];

export class InscriptionBuilder implements IInscriptionBuilder {
  private readonly wallet: IWallet;
  private readonly coin: AbstractUtxoCoin;

  constructor(wallet: IWallet, coin: AbstractUtxoCoin) {
    this.wallet = wallet;
    this.coin = coin;
  }

  async prepareReveal(inscriptionData: Buffer, contentType: string): Promise<PreparedInscriptionRevealData> {
    const user = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
    assert(user.pub);

    const derived = this.coin.deriveKeyWithSeed({ key: user.pub, seed: inscriptionData.toString() });
    const compressedPublicKey = xpubToCompressedPub(derived.key);
    const xOnlyPublicKey = utxolib.bitgo.outputScripts.toXOnlyPublicKey(Buffer.from(compressedPublicKey, 'hex'));

    return inscriptions.createInscriptionRevealData(xOnlyPublicKey, contentType, inscriptionData, this.coin.network);
  }

  private async prepareTransferWithExtraInputs(
    satPoint: SatPoint,
    feeRateSatKB: number,
    {
      signer,
      cosigner,
      inscriptionConstraints,
      txFormat,
    }: {
      signer: utxolib.bitgo.KeyName;
      cosigner: utxolib.bitgo.KeyName;
      inscriptionConstraints: {
        minChangeOutput?: bigint;
        minInscriptionOutput?: bigint;
        maxInscriptionOutput?: bigint;
      };
      txFormat?: 'psbt' | 'legacy';
    },
    rootWalletKeys: RootWalletKeys,
    outputs: InscriptionOutputs,
    inscriptionUnspents: utxolib.bitgo.WalletUnspent<bigint>[],
    supplementaryUnspentsMinValue: number
  ): Promise<PrebuildTransactionResult> {
    let supplementaryUnspents: utxolib.bitgo.WalletUnspent<bigint>[] = [];
    if (supplementaryUnspentsMinValue > 0) {
      const response = await this.wallet.unspents({
        minValue: supplementaryUnspentsMinValue,
      });
      // Filter out the inscription unspent from the supplementary unspents
      supplementaryUnspents = response.unspents
        .filter((unspent) => unspent.id !== inscriptionUnspents[0].id)
        .slice(0, MAX_UNSPENTS_FOR_OUTPUT_LAYOUT - 1)
        .map((unspent) => {
          unspent.value = BigInt(unspent.value);
          return unspent;
        });
    }
    const psbt = createPsbtForSingleInscriptionPassingTransaction(
      this.coin.network,
      {
        walletKeys: rootWalletKeys,
        signer,
        cosigner,
      },
      inscriptionUnspents,
      satPoint,
      outputs,
      { feeRateSatKB, ...inscriptionConstraints },
      { supplementaryUnspents }
    );
    if (!psbt) {
      throw new Error('Fee too high for the selected unspent with this fee rate');
    }

    const allUnspents = [...inscriptionUnspents, ...supplementaryUnspents];

    // TODO: Remove the call to this function because it's already called inside the createPsbt function above.
    // Create & use a getFee function inside the created PSBT instead, lack of which necessitates a duplicate call here.
    const outputLayout = findOutputLayoutForWalletUnspents(allUnspents, satPoint, outputs, {
      feeRateSatKB,
      ...inscriptionConstraints,
    });
    if (!outputLayout) {
      throw new Error('Fee too high for the selected unspent with this fee rate');
    }
    return {
      walletId: this.wallet.id(),
      txHex: txFormat === 'psbt' ? psbt.toHex() : psbt.getUnsignedTx().toHex(),
      txInfo: { unspents: allUnspents },
      feeInfo: { fee: Number(outputLayout.layout.feeOutput), feeString: outputLayout.layout.feeOutput.toString() },
    };
  }

  /**
   * Build a transaction to send an inscription
   * @param satPoint Satpoint you want to send
   * @param recipient Address you want to send to
   * @param feeRateSatKB Fee rate for transaction
   * @param signer first signer of the transaction
   * @param cosigner second signer of the transaction
   * @param inscriptionConstraints.minChangeOutput (optional) the minimum size of the change output
   * @param inscriptionConstraints.minInscriptionOutput (optional) the minimum number of sats of the output containing the inscription
   * @param inscriptionConstraints.maxInscriptionOutput (optional) the maximum number of sats of the output containing the inscription
   * @param changeAddressType Address type of the change address
   */
  async prepareTransfer(
    satPoint: string,
    recipient: string,
    feeRateSatKB: number,
    {
      signer = 'user',
      cosigner = 'bitgo',
      inscriptionConstraints = DefaultInscriptionConstraints,
      changeAddressType = 'p2wsh',
      txFormat = 'psbt',
    }: {
      signer?: utxolib.bitgo.KeyName;
      cosigner?: utxolib.bitgo.KeyName;
      inscriptionConstraints?: {
        minChangeOutput?: bigint;
        minInscriptionOutput?: bigint;
        maxInscriptionOutput?: bigint;
      };
      changeAddressType?: utxolib.bitgo.outputScripts.ScriptType2Of3;
      txFormat?: 'psbt' | 'legacy';
    }
  ): Promise<PrebuildTransactionResult> {
    assert(isSatPoint(satPoint));

    const rootWalletKeys = await getWalletKeys(this.coin, this.wallet);
    const parsedSatPoint = parseSatPoint(satPoint);
    const transaction = await this.wallet.getTransaction({ txHash: parsedSatPoint.txid });
    const unspents: utxolib.bitgo.WalletUnspent<bigint>[] = [transaction.outputs[parsedSatPoint.vout]];
    unspents[0].value = BigInt(unspents[0].value);

    const changeAddress = await this.wallet.createAddress({
      chain: utxolib.bitgo.getInternalChainCode(changeAddressType),
    });
    const outputs: InscriptionOutputs = {
      inscriptionRecipient: recipient,
      changeOutputs: [
        { chain: changeAddress.chain, index: changeAddress.index },
        { chain: changeAddress.chain, index: changeAddress.index },
      ],
    };

    for (const supplementaryUnspentsMinValue of SUPPLEMENTARY_UNSPENTS_MIN_VALUE_SATS) {
      try {
        return await this.prepareTransferWithExtraInputs(
          satPoint,
          feeRateSatKB,
          { signer, cosigner, inscriptionConstraints, txFormat },
          rootWalletKeys,
          outputs,
          unspents,
          supplementaryUnspentsMinValue
        );
      } catch (error) {
        if (!(error instanceof ErrorNoLayout)) {
          throw error; // Propagate error if it's not an ErrorNoLayout
        } // Otherwise continue trying with higher minValue for supplementary unspents
      }
    }

    throw new Error('Fee too high for the selected unspent with this fee rate'); // Exhausted all tries to supplement
  }

  /**
   *
   * @param walletPassphrase
   * @param tapLeafScript
   * @param commitAddress
   * @param unsignedCommitTx
   * @param commitTransactionUnspents
   * @param recipientAddress
   * @param inscriptionData
   */
  async signAndSendReveal(
    walletPassphrase: string,
    tapLeafScript: utxolib.bitgo.TapLeafScript,
    commitAddress: string,
    unsignedCommitTx: Buffer,
    commitTransactionUnspents: utxolib.bitgo.WalletUnspent[],
    recipientAddress: string,
    inscriptionData: Buffer
  ): Promise<SubmitTransactionResponse> {
    const userKeychain = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
    const xprv = await this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });

    const halfSignedCommitTransaction = (await this.wallet.signTransaction({
      prv: xprv,
      txPrebuild: {
        txHex: unsignedCommitTx.toString('hex'),
        txInfo: { unspents: commitTransactionUnspents },
      },
    })) as HalfSignedUtxoTransaction;

    const derived = this.coin.deriveKeyWithSeed({ key: xprv, seed: inscriptionData.toString() });
    const prv = xprvToRawPrv(derived.key);

    const fullySignedRevealTransaction = await inscriptions.signRevealTransaction(
      Buffer.from(prv, 'hex'),
      tapLeafScript,
      commitAddress,
      recipientAddress,
      Buffer.from(halfSignedCommitTransaction.txHex, 'hex'),
      this.coin.network
    );

    return this.wallet.submitTransaction({
      halfSigned: {
        txHex: halfSignedCommitTransaction.txHex,
        signedChildPsbt: fullySignedRevealTransaction.toHex(),
      },
    });
  }

  /**
   * Sign and send a transaction that transfers an inscription
   * @param walletPassphrase passphrase to unlock your keys
   * @param txPrebuild this is the output of `inscription.prepareTransfer`
   */
  async signAndSendTransfer(
    walletPassphrase: string,
    txPrebuild: PrebuildTransactionResult
  ): Promise<SubmitTransactionResponse> {
    const userKeychain = await this.wallet.baseCoin.keychains().get({ id: this.wallet.keyIds()[KeyIndices.USER] });
    const prv = this.wallet.getUserPrv({ keychain: userKeychain, walletPassphrase });

    const halfSigned = (await this.wallet.signTransaction({ prv, txPrebuild })) as HalfSignedUtxoTransaction;
    return this.wallet.submitTransaction({ halfSigned });
  }
}

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


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