PHP WebShell

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

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

import {
  BaseCoin,
  BaseTransaction,
  BitGoBase,
  Environments,
  getBip32Keys,
  getIsUnsignedSweep,
  KeyPair,
  MultisigType,
  multisigTypes,
  SignedTransaction,
  TransactionRecipient,
  TransactionType,
  VerifyAddressOptions,
  VerifyTransactionOptions,
} from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin, CoinFamily, coins } from '@bitgo/statics';
import {
  bufferCVFromString,
  ClarityType,
  ClarityValue,
  createStacksPrivateKey,
  cvToString,
  cvToValue,
  deserializeTransaction,
  noneCV,
  privateKeyToString,
  publicKeyFromBuffer,
  publicKeyToString,
  someCV,
  standardPrincipalCV,
  uintCV,
} from '@stacks/transactions';
import { serializePayload } from '@stacks/transactions/dist/payload';
import BigNumber from 'bignumber.js';

import { ExplainTransactionOptions, StxSignTransactionOptions, StxTransactionExplanation } from './types';
import { StxLib } from '.';
import { TransactionBuilderFactory } from './lib';
import { TransactionBuilder } from './lib/transactionBuilder';
import { findContractTokenNameUsingContract, findTokenNameByContract, getAddressDetails } from './lib/utils';
import {
  AddressDetails,
  NativeStxBalance,
  RecoveryInfo,
  RecoveryOptions,
  RecoveryTransaction,
  SingleFungibleTokenBalance,
  StxNonceResponse,
  StxTxnFeeEstimationResponse,
  TxData,
} from './lib/iface';
import { TransferBuilder } from './lib/transferBuilder';
import { FungibleTokenTransferBuilder } from './lib/fungibleTokenTransferBuilder';

export class Stx 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;
  }

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

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

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

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

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

  getTransaction(coinConfig: Readonly<StaticsBaseCoin>): TransactionBuilder {
    return new TransactionBuilderFactory(coinConfig).getTransferBuilder();
  }

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

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

  async verifyTransaction(params: VerifyTransactionOptions): Promise<boolean> {
    const { txParams } = params;
    if (Array.isArray(txParams.recipients) && 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.`
      );
    }
    return true;
  }

  /**
   * 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, keychains } = params;
    if (!keychains || keychains.length !== 3) {
      throw new Error('Invalid keychains');
    }
    const pubs = keychains.map((keychain) => StxLib.Utils.xpubToSTXPubkey(keychain.pub));
    const addressVersion = StxLib.Utils.getAddressVersion(address);
    const baseAddress = StxLib.Utils.getSTXAddressFromPubKeys(pubs, addressVersion).address;
    return StxLib.Utils.isSameBaseAddress(address, baseAddress);
  }

  /**
   * Generate Stacks key pair
   *
   * @param {Buffer} seed - Seed from which the new keypair should be generated, otherwise a random seed is used
   * @returns {Object} object with generated pub and prv
   */
  generateKeyPair(seed?: Buffer): KeyPair {
    const keyPair = seed ? new StxLib.KeyPair({ seed }) : new StxLib.KeyPair();
    const keys = keyPair.getExtendedKeys();

    if (!keys.xprv) {
      throw new Error('Missing xprv in key generation.');
    }

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

  /**
   * Return boolean indicating whether input is valid public key for the coin
   *
   * @param {string} pub the prv to be checked
   * @returns is it valid?
   */
  isValidPub(pub: string): boolean {
    try {
      return StxLib.Utils.isValidPublicKey(pub);
    } catch (e) {
      return false;
    }
  }

  /**
   * Return boolean indicating whether input is valid private key for the coin
   *
   * @param {string} prv the prv to be checked
   * @returns is it valid?
   */
  isValidPrv(prv: string): boolean {
    try {
      return StxLib.Utils.isValidPrivateKey(prv);
    } catch (e) {
      return false;
    }
  }

  isValidAddress(address: string): boolean {
    try {
      return StxLib.Utils.isValidAddressWithPaymentId(address);
    } catch (e) {
      return false;
    }
  }

  /**
   * Signs stacks transaction
   * @param params
   */
  async signTransaction(params: StxSignTransactionOptions): Promise<SignedTransaction> {
    const factory = new StxLib.TransactionBuilderFactory(coins.get(this.getChain()));
    const txBuilder = factory.from(params.txPrebuild.txHex);
    const prvKeys = params.prv instanceof Array ? params.prv : [params.prv];
    prvKeys.forEach((prv) => txBuilder.sign({ key: prv }));
    if (params.pubKeys) txBuilder.fromPubKey(params.pubKeys);
    // if (params.numberSignature) txBuilder.numberSignatures(params.numberSignature);
    const transaction = await txBuilder.build();

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

    const txHex = {
      txHex: transaction.toBroadcastFormat(),
    };

    return transaction.signature.length >= 2 ? txHex : { halfSigned: txHex };
  }

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

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

    const factory = new StxLib.TransactionBuilderFactory(coins.get(this.getChain()));
    const txBuilder = factory.from(txHex);

    if (params.publicKeys !== undefined) {
      txBuilder.fromPubKey(params.publicKeys);
      if (params.publicKeys.length === 1) {
        // definitely a single sig tx
        txBuilder.numberSignatures(1);
      }
    }

    const tx = await txBuilder.build();
    const txJson = tx.toJson();

    if (tx.type === TransactionType.Send) {
      // check if it is a token transaction or native coin transaction
      let transactionRecipient: TransactionRecipient;
      let outputAmount: string;
      let memo: string | undefined;
      if (txJson.payload.contractAddress && txJson.payload.functionArgs.length >= 3) {
        outputAmount = cvToValue(txJson.payload.functionArgs[0]).toString();
        transactionRecipient = {
          address: cvToString(txJson.payload.functionArgs[2]),
          amount: outputAmount,
          tokenName: findTokenNameByContract(txJson.payload.contractAddress, txJson.payload.contractName),
        };
        if (
          txJson.payload.functionArgs.length === 4 &&
          txJson.payload.functionArgs[3].type === ClarityType.OptionalSome
        ) {
          memo = Buffer.from(txJson.payload.functionArgs[3].value.buffer).toString();
          transactionRecipient['memo'] = memo;
        }
      } else {
        outputAmount = txJson.payload.amount;
        memo = txJson.payload.memo;
        transactionRecipient = {
          address: txJson.payload.to,
          amount: outputAmount,
          memo: memo,
        };
      }
      const outputs: TransactionRecipient[] = [transactionRecipient];

      const displayOrder = ['id', 'outputAmount', 'changeAmount', 'outputs', 'changeOutputs', 'fee', 'memo', 'type'];
      return {
        displayOrder,
        id: txJson.id,
        outputAmount: outputAmount.toString(),
        changeAmount: '0',
        outputs,
        changeOutputs: [],
        fee: txJson.fee,
        memo: memo,
        type: tx.type,
      };
    }

    if (tx.type === TransactionType.ContractCall) {
      const displayOrder = [
        'id',
        'fee',
        'type',
        'contractAddress',
        'contractName',
        'contractFunction',
        'contractFunctionArgs',
      ];
      return {
        displayOrder,
        id: txJson.id,
        changeAmount: '0',
        outputAmount: '',
        outputs: [],
        changeOutputs: [],
        fee: txJson.fee,
        type: tx.type,
        contractAddress: txJson.payload.contractAddress,
        contractName: txJson.payload.contractName,
        contractFunction: txJson.payload.functionName,
        contractFunctionArgs: txJson.payload.functionArgs,
      };
    }
  }

  /**
   * Get URLs of some active public nodes
   * @returns {String} node url
   */
  getPublicNodeUrl(): string {
    return Environments[this.bitgo.getEnv()].stxNodeUrl;
  }

  /**
   * Get native stacks balance for an account
   * @param {String} address - stacks address
   * @returns {Promise<NativeStxBalance>}
   */
  protected async getNativeStxBalanceFromNode({ address }: { address: string }): Promise<NativeStxBalance> {
    const endpoint = `${this.getPublicNodeUrl()}/extended/v2/addresses/${address}/balances/stx`;
    try {
      const response = await this.bitgo.get(endpoint);
      if (response.statusCode !== 200) {
        throw new Error(`request failed with status ${response.statusCode}`);
      }
      const body: NativeStxBalance = response.body;
      return body;
    } catch (e) {
      throw new Error(`unable to get native stx balance from node: ${e.message}`);
    }
  }

  /**
   * Get single fungible token balance for an account
   * @param {String} address - stacks address
   * @param {String} assetId - fungible token asset id
   * @returns {Promise<SingleFungibleTokenBalance>}
   */
  protected async getSingleFungibleTokenBalanceFromNode({
    address,
    assetId,
  }: {
    address: string;
    assetId: string;
  }): Promise<SingleFungibleTokenBalance> {
    const endpoint = `${this.getPublicNodeUrl()}/extended/v2/addresses/${address}/balances/ft/${assetId}`;
    try {
      const response = await this.bitgo.get(endpoint);
      if (response.statusCode !== 200) {
        throw new Error(`request failed with status ${response.statusCode}`);
      }
      const body: SingleFungibleTokenBalance = response.body;
      return body;
    } catch (e) {
      throw new Error(`unable to get native stx balance from node: ${e.message}`);
    }
  }

  /**
   * Get nonce data specific to an account from a public node
   * @param {String} address - stacks address
   * @returns {Promise<StxNonceResponse>}
   */
  protected async getAccountNonceFromNode({ address }: { address: string }): Promise<StxNonceResponse> {
    const endpoint = `${this.getPublicNodeUrl()}/extended/v1/address/${address}/nonces`;
    try {
      const response = await this.bitgo.get(endpoint);
      if (response.statusCode !== 200) {
        throw new Error(`request failed with status ${response.statusCode}`);
      }
      const body: StxNonceResponse = response.body;
      return body;
    } catch (e) {
      throw new Error(`unable to get account nonce from node: ${e.message}`);
    }
  }

  /**
   * Get stacks transaction estimated fee
   * @param {String} txHex - hex of stacks transaction payload
   * @param {Number} txHexLength - length of built serialized transaction
   * @returns {Promise<Number>} - fee estimate (taking the lowest)
   */
  protected async getTransactionFeeEstimation({
    txHex,
    txHexLength,
  }: {
    txHex: string;
    txHexLength: number;
  }): Promise<number> {
    const endpoint = `${this.getPublicNodeUrl()}/v2/fees/transaction`;
    const requestBody = {
      transaction_payload: txHex,
      estimated_len: txHexLength,
    };
    try {
      const response = await this.bitgo.post(endpoint).send(requestBody);
      if (response.statusCode !== 200) {
        throw new Error(`request failed with status ${response.statusCode}`);
      }
      const body: StxTxnFeeEstimationResponse = response.body;
      if (body.estimations.length !== 3) {
        throw new Error('Invalid response estimation length');
      }
      return body.estimations[0].fee;
    } catch (e) {
      throw new Error(`unable to get transaction fee estimation: ${e.message}`);
    }
  }

  /**
   * Format for offline vault signing
   * @param {BaseTransaction} tx - base transaction
   * @returns {Promise<RecoveryInfo>}
   */
  protected async formatForOfflineVault(tx: BaseTransaction): Promise<RecoveryInfo> {
    const txJson: TxData = tx.toJson();
    const transactionExplanation: RecoveryInfo = (await this.explainTransaction({
      txHex: tx.toBroadcastFormat(),
      feeInfo: { fee: txJson.fee },
    })) as RecoveryInfo;
    transactionExplanation.coin = this.getChain();
    transactionExplanation.feeInfo = { fee: txJson.fee };
    transactionExplanation.txHex = tx.toBroadcastFormat();
    return transactionExplanation;
  }

  /**
   * Get the recoverable amount & fee after subtracting the txn fee
   * @param {String} serializedHex - serialized txn hex
   * @param {Number} txHexLength - deserialized txn length
   * @param {String} balance - total account balance
   * @param {String} tokenBalance - total token balance
   * @returns {Promise<Record<string, string>>}
   */
  protected async getRecoverableAmountAndFee(
    serializedHex: string,
    txHexLength: number,
    balance: string,
    tokenBalance?: string
  ): Promise<Record<string, string>> {
    const estimatedFee = await this.getTransactionFeeEstimation({
      txHex: serializedHex,
      txHexLength: txHexLength,
    });
    const balanceBN = new BigNumber(balance);
    const feeBN = new BigNumber(estimatedFee);
    if (balanceBN.isLessThan(feeBN)) {
      throw new Error('insufficient balance to build the transaction');
    }
    return {
      recoverableAmount: tokenBalance ?? balanceBN.minus(feeBN).toString(),
      fee: feeBN.toString(),
    };
  }

  /**
   * Method to find the right builder for token or native coin transfer
   * @param {String} contractAddress - token contract address
   * @param {String} contractName - token contract name
   * @returns {TransferBuilder|FungibleTokenTransferBuilder}
   */
  protected getTokenOrNativeTransferBuilder(
    contractAddress?: string,
    contractName?: string
  ): TransferBuilder | FungibleTokenTransferBuilder {
    const isToken = !!contractAddress && !!contractName;
    let factory: TransactionBuilderFactory;
    if (isToken) {
      const tokenName = findTokenNameByContract(contractAddress, contractName);
      if (!tokenName) {
        throw new Error('invalid contract address or contract name, not supported');
      }
      factory = new TransactionBuilderFactory(coins.get(tokenName));
    } else {
      factory = new TransactionBuilderFactory(coins.get(this.getChain()));
    }
    let builder: TransferBuilder | FungibleTokenTransferBuilder;
    if (isToken) {
      builder = factory.getFungibleTokenTransferBuilder();
    } else {
      builder = factory.getTransferBuilder();
    }
    return builder;
  }

  /**
   * Method to build fungible token transfer transaction
   * @param {FungibleTokenTransferBuilder} builder - fungible token transfer builder
   * @param {String} contractAddress - token contract address
   * @param {String} contractName - token contract name
   * @param {String[]} pubs - account public keys
   * @param {Number} nonce - account nonce
   * @param {AddressDetails} rootAddressDetails - root address details
   * @param {AddressDetails} destinationAddressDetails - receive address details
   * @param {String} stxBalance - native stx balance
   * @returns {Promise<BaseTransaction>} - built transaction
   */
  protected async buildTokenTransferTransaction({
    builder,
    contractAddress,
    contractName,
    pubs,
    nonce,
    rootAddressDetails,
    destinationAddressDetails,
    stxBalance,
  }: {
    builder: FungibleTokenTransferBuilder;
    contractAddress: string;
    contractName: string;
    pubs: string[];
    nonce: number;
    rootAddressDetails: AddressDetails;
    destinationAddressDetails: AddressDetails;
    stxBalance: string;
  }): Promise<BaseTransaction> {
    const txBuilder = builder as FungibleTokenTransferBuilder;
    const contractTokenName = findContractTokenNameUsingContract(contractAddress, contractName);
    if (!contractTokenName) {
      throw new Error('invalid contract address or contract name, not supported');
    }
    const assetId = `${contractAddress}.${contractName}::${contractTokenName}`;
    // fetch the token balance
    const tokenBalanceData = await this.getSingleFungibleTokenBalanceFromNode({
      address: rootAddressDetails.address,
      assetId,
    });
    const tokenBalance = tokenBalanceData?.balance;
    if (!Number(tokenBalance) || isNaN(Number(tokenBalance))) {
      throw new Error(
        `no token balance found to recover for address: ${rootAddressDetails.address}, token: ${assetId}`
      );
    }
    txBuilder.fee({ fee: '200' });
    txBuilder.numberSignatures(2);
    txBuilder.fromPubKey(pubs);
    txBuilder.nonce(nonce);
    txBuilder.contractAddress(contractAddress);
    txBuilder.contractName(contractName);
    if (contractTokenName) {
      txBuilder.tokenName(contractTokenName);
    }
    txBuilder.functionName('transfer');
    const functionArgs: ClarityValue[] = [
      uintCV(tokenBalance),
      standardPrincipalCV(rootAddressDetails.address),
      standardPrincipalCV(destinationAddressDetails.address),
    ];
    if (destinationAddressDetails.memoId) {
      functionArgs.push(someCV(bufferCVFromString(destinationAddressDetails.memoId)));
    } else {
      functionArgs.push(noneCV());
    }
    txBuilder.functionArgs(functionArgs);
    const baseTxn = await txBuilder.build();
    const txBroadcastFormat = baseTxn.toBroadcastFormat();
    const txDeserialized = deserializeTransaction(txBroadcastFormat);
    const serializedHex = serializePayload(txDeserialized.payload).toString('hex');
    const { recoverableAmount, fee } = await this.getRecoverableAmountAndFee(
      serializedHex,
      txBroadcastFormat.length,
      stxBalance,
      tokenBalance
    );
    functionArgs[0] = uintCV(recoverableAmount);
    txBuilder.functionArgs(functionArgs);
    txBuilder.fee({ fee: fee });
    return await txBuilder.build();
  }

  /**
   * Method to build native transfer transaction
   * @param {TransferBuilder} builder - transfer builder
   * @param {String[]} pubs - account public keys
   * @param {Number} nonce - account nonce
   * @param {AddressDetails} destinationAddressDetails - receive address details
   * @param {String} stxBalance - native stx balance
   * @returns {Promise<BaseTransaction>} - built transaction
   */
  protected async buildNativeTransferTransaction({
    builder,
    pubs,
    nonce,
    destinationAddressDetails,
    stxBalance,
  }: {
    builder: TransferBuilder;
    pubs: string[];
    nonce: number;
    destinationAddressDetails: AddressDetails;
    stxBalance: string;
  }): Promise<BaseTransaction> {
    const txBuilder = builder as TransferBuilder;
    txBuilder.fee({ fee: '200' });
    txBuilder.numberSignatures(2);
    txBuilder.fromPubKey(pubs);
    txBuilder.nonce(nonce);
    txBuilder.to(destinationAddressDetails.address);
    txBuilder.amount(stxBalance);
    if (destinationAddressDetails.memoId) {
      txBuilder.memo(destinationAddressDetails.memoId);
    }
    const baseTxn = await txBuilder.build();
    const txBroadcastFormat = baseTxn.toBroadcastFormat();
    const txDeserialized = deserializeTransaction(txBroadcastFormat);
    const serializedHex = serializePayload(txDeserialized.payload).toString('hex');
    const { recoverableAmount, fee } = await this.getRecoverableAmountAndFee(
      serializedHex,
      txBroadcastFormat.length,
      stxBalance
    );
    txBuilder.amount(recoverableAmount);
    txBuilder.fee({ fee: fee });
    return await txBuilder.build();
  }

  /**
   * Method that uses appropriate builder and builds transaction depending on token or native coin
   * @param {String[]} pubs - public keys
   * @param {AddressDetails} rootAddressDetails - sender address detail
   * @param {AddressDetails} destinationAddressDetails - receiver address detail
   * @param {Number} nonce - wallet nonce
   * @param {String} balance - wallet balance
   * @param {String | undefined} contractAddress - token contract address
   * @param {String | undefined} contractName - token contract name
   * @returns {Promise<BaseTransaction>} built transaction
   */
  protected async getNativeOrTokenTransaction({
    pubs,
    rootAddressDetails,
    destinationAddressDetails,
    nonce,
    stxBalance,
    contractAddressInput,
    contractName,
  }: {
    pubs: string[];
    rootAddressDetails: AddressDetails;
    destinationAddressDetails: AddressDetails;
    nonce: number;
    stxBalance: string;
    contractAddressInput?: string;
    contractName?: string;
  }): Promise<{ tx: BaseTransaction; builder: TransferBuilder | FungibleTokenTransferBuilder }> {
    const builder = this.getTokenOrNativeTransferBuilder(contractAddressInput, contractName);
    const contractAddress = contractAddressInput?.toUpperCase();
    const isToken = !!contractAddress && !!contractName;
    let finalTx: BaseTransaction;
    if (isToken) {
      finalTx = await this.buildTokenTransferTransaction({
        builder: builder as FungibleTokenTransferBuilder,
        contractAddress,
        contractName,
        pubs,
        nonce,
        rootAddressDetails,
        destinationAddressDetails,
        stxBalance,
      });
    } else {
      finalTx = await this.buildNativeTransferTransaction({
        builder: builder as TransferBuilder,
        pubs,
        nonce,
        destinationAddressDetails,
        stxBalance,
      });
    }
    return {
      tx: finalTx,
      builder: builder,
    };
  }

  /**
   * Method to recover native stx or sip10 tokens from bitgo hot & cold wallets
   * @param {String} params.backupKey - encrypted wallet backup key (public or private)
   * @param {String} params.userKey - encrypted wallet user key (public or private)
   * @param {String} params.rootAddress - wallet root address
   * @param {String} params.recoveryDestination - receive address
   * @param {String} params.bitgoKey - encrypted bitgo public key
   * @param {String} params.walletPassphrase - wallet password
   * @param {String} params.contractId - contract id of the token (mandatory for token recovery)
   * @returns {Promise<RecoveryInfo|RecoveryTransaction>} RecoveryTransaction.txHex - hex of serialized transaction (signed or unsigned)
   */
  async recover(params: RecoveryOptions): Promise<RecoveryInfo | RecoveryTransaction> {
    if (!this.isValidAddress(params.rootAddress)) {
      throw new Error('invalid root address!');
    }
    if (!this.isValidAddress(params.recoveryDestination)) {
      throw new Error('invalid destination address!');
    }
    let contractAddress: string | undefined;
    let contractName: string | undefined;
    if (params.contractId) {
      [contractAddress, contractName] = params.contractId.split('.');
      if ((contractAddress && !contractName) || (contractName && !contractAddress)) {
        throw new Error('invalid contract id, please provide it in the form (contractAddress.contractName)');
      }
    }
    const isUnsignedSweep = getIsUnsignedSweep(params);
    const keys = getBip32Keys(this.bitgo, params, { requireBitGoXpub: true });
    const rootAddressDetails = getAddressDetails(params.rootAddress);
    const [accountBalanceData, accountNonceData] = await Promise.all([
      this.getNativeStxBalanceFromNode({ address: rootAddressDetails.address }),
      this.getAccountNonceFromNode({ address: rootAddressDetails.address }),
    ]);
    const balance = Number(accountBalanceData.balance);
    if (!balance || isNaN(balance)) {
      throw new Error('could not find any balance to recover for ' + params.rootAddress);
    }
    const userPub = publicKeyFromBuffer(keys[0].publicKey);
    const backupPub = publicKeyFromBuffer(keys[1].publicKey);
    const bitgoPubKey = publicKeyFromBuffer(keys[2].publicKey);
    const pubs = [publicKeyToString(userPub), publicKeyToString(backupPub), publicKeyToString(bitgoPubKey)];

    const destinationAddressDetails = getAddressDetails(params.recoveryDestination);
    const nonce =
      typeof accountNonceData?.last_executed_tx_nonce === 'number' ? accountNonceData.last_executed_tx_nonce + 1 : 0;

    const { tx, builder } = await this.getNativeOrTokenTransaction({
      pubs,
      rootAddressDetails,
      destinationAddressDetails,
      nonce,
      stxBalance: accountBalanceData.balance,
      contractAddressInput: contractAddress,
      contractName: contractName,
    });

    if (isUnsignedSweep) {
      return await this.formatForOfflineVault(tx);
    }
    // check the private key & sign
    if (!keys[0].privateKey) {
      throw new Error(`userKey is not a private key`);
    }
    const userKey = createStacksPrivateKey(keys[0].privateKey);
    builder.sign({ key: privateKeyToString(userKey) });

    const halfSignedTx = await builder.build();
    const txHexHalfSigned = halfSignedTx.toBroadcastFormat();
    const builder2 = this.getTokenOrNativeTransferBuilder(contractAddress, contractName);
    builder2.from(txHexHalfSigned);

    if (!keys[1].privateKey) {
      throw new Error(`backupKey is not a private key`);
    }
    const backupKey = createStacksPrivateKey(keys[1].privateKey);
    builder2.sign({ key: privateKeyToString(backupKey) });

    const fullySignedTx = await builder2.build();
    const fullySignedTxHex = fullySignedTx.toBroadcastFormat();

    return {
      txHex: fullySignedTxHex,
    };
  }
}

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


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