Source

lib/contract/assembled_transaction.js

"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.AssembledTransaction = void 0;
var _stellarBase = require("@stellar/stellar-base");
var _rpc = require("../rpc");
var _api = require("../rpc/api");
var _transaction = require("../rpc/transaction");
var _rust_result = require("./rust_result");
var _utils = require("./utils");
var _types = require("./types");
var _sent_transaction = require("./sent_transaction");
/* disable max-classes rule, because extending error shouldn't count! */
/* eslint max-classes-per-file: 0 */

/** @module contract */

/**
 * The main workhorse of {@link Client}. This class is used to wrap a
 * transaction-under-construction and provide high-level interfaces to the most
 * common workflows, while still providing access to low-level stellar-sdk
 * transaction manipulation.
 *
 * Most of the time, you will not construct an `AssembledTransaction` directly,
 * but instead receive one as the return value of a `Client` method. If
 * you're familiar with the libraries generated by soroban-cli's `contract
 * bindings typescript` command, these also wraps `Client` and return
 * `AssembledTransaction` instances.
 *
 * Let's look at examples of how to use `AssembledTransaction` for a variety of
 * use-cases:
 *
 * #### 1. Simple read call
 *
 * Since these only require simulation, you can get the `result` of the call
 * right after constructing your `AssembledTransaction`:
 *
 * ```ts
 * const { result } = await AssembledTransaction.build({
 *   method: 'myReadMethod',
 *   args: spec.funcArgsToScVals('myReadMethod', {
 *     args: 'for',
 *     my: 'method',
 *     ...
 *   }),
 *   contractId: 'C123…',
 *   networkPassphrase: '…',
 *   rpcUrl: 'https://…',
 *   publicKey: undefined, // irrelevant, for simulation-only read calls
 *   parseResultXdr: (result: xdr.ScVal) =>
 *     spec.funcResToNative('myReadMethod', result),
 * })
 * ```
 *
 * While that looks pretty complicated, most of the time you will use this in
 * conjunction with {@link Client}, which simplifies it to:
 *
 * ```ts
 * const { result }  = await client.myReadMethod({
 *   args: 'for',
 *   my: 'method',
 *   ...
 * })
 * ```
 *
 * #### 2. Simple write call
 *
 * For write calls that will be simulated and then sent to the network without
 * further manipulation, only one more step is needed:
 *
 * ```ts
 * const assembledTx = await client.myWriteMethod({
 *   args: 'for',
 *   my: 'method',
 *   ...
 * })
 * const sentTx = await assembledTx.signAndSend()
 * ```
 *
 * Here we're assuming that you're using a {@link Client}, rather than
 * constructing `AssembledTransaction`'s directly.
 *
 * Note that `sentTx`, the return value of `signAndSend`, is a
 * {@link SentTransaction}. `SentTransaction` is similar to
 * `AssembledTransaction`, but is missing many of the methods and fields that
 * are only relevant while assembling a transaction. It also has a few extra
 * methods and fields that are only relevant after the transaction has been
 * sent to the network.
 *
 * Like `AssembledTransaction`, `SentTransaction` also has a `result` getter,
 * which contains the parsed final return value of the contract call. Most of
 * the time, you may only be interested in this, so rather than getting the
 * whole `sentTx` you may just want to:
 *
 * ```ts
 * const tx = await client.myWriteMethod({ args: 'for', my: 'method', ... })
 * const { result } = await tx.signAndSend()
 * ```
 *
 * #### 3. More fine-grained control over transaction construction
 *
 * If you need more control over the transaction before simulating it, you can
 * set various {@link MethodOptions} when constructing your
 * `AssembledTransaction`. With a {@link Client}, this is passed as a
 * second object after the arguments (or the only object, if the method takes
 * no arguments):
 *
 * ```ts
 * const tx = await client.myWriteMethod(
 *   {
 *     args: 'for',
 *     my: 'method',
 *     ...
 *   }, {
 *     fee: '10000', // default: {@link BASE_FEE}
 *     simulate: false,
 *     timeoutInSeconds: 20, // default: {@link DEFAULT_TIMEOUT}
 *   }
 * )
 * ```
 *
 * Since we've skipped simulation, we can now edit the `raw` transaction and
 * then manually call `simulate`:
 *
 * ```ts
 * tx.raw.addMemo(Memo.text('Nice memo, friend!'))
 * await tx.simulate()
 * ```
 *
 * If you need to inspect the simulation later, you can access it with
 * `tx.simulation`.
 *
 * #### 4. Multi-auth workflows
 *
 * Soroban, and Stellar in general, allows multiple parties to sign a
 * transaction.
 *
 * Let's consider an Atomic Swap contract. Alice wants to give 10 of her Token
 * A tokens to Bob for 5 of his Token B tokens.
 *
 * ```ts
 * const ALICE = 'G123...'
 * const BOB = 'G456...'
 * const TOKEN_A = 'C123…'
 * const TOKEN_B = 'C456…'
 * const AMOUNT_A = 10n
 * const AMOUNT_B = 5n
 * ```
 *
 * Let's say Alice is also going to be the one signing the final transaction
 * envelope, meaning she is the invoker. So your app, from Alice's browser,
 * simulates the `swap` call:
 *
 * ```ts
 * const tx = await swapClient.swap({
 *   a: ALICE,
 *   b: BOB,
 *   token_a: TOKEN_A,
 *   token_b: TOKEN_B,
 *   amount_a: AMOUNT_A,
 *   amount_b: AMOUNT_B,
 * })
 * ```
 *
 * But your app can't `signAndSend` this right away, because Bob needs to sign
 * it first. You can check this:
 *
 * ```ts
 * const whoElseNeedsToSign = tx.needsNonInvokerSigningBy()
 * ```
 *
 * You can verify that `whoElseNeedsToSign` is an array of length `1`,
 * containing only Bob's public key.
 *
 * Then, still on Alice's machine, you can serialize the
 * transaction-under-assembly:
 *
 * ```ts
 * const json = tx.toJSON()
 * ```
 *
 * And now you need to send it to Bob's browser. How you do this depends on
 * your app. Maybe you send it to a server first, maybe you use WebSockets, or
 * maybe you have Alice text the JSON blob to Bob and have him paste it into
 * your app in his browser (note: this option might be error-prone 😄).
 *
 * Once you get the JSON blob into your app on Bob's machine, you can
 * deserialize it:
 *
 * ```ts
 * const tx = swapClient.txFromJSON(json)
 * ```
 *
 * Or, if you're using a client generated with `soroban contract bindings
 * typescript`, this deserialization will look like:
 *
 * ```ts
 * const tx = swapClient.fromJSON.swap(json)
 * ```
 *
 * Then you can have Bob sign it. What Bob will actually need to sign is some
 * _auth entries_ within the transaction, not the transaction itself or the
 * transaction envelope. Your app can verify that Bob has the correct wallet
 * selected, then:
 *
 * ```ts
 * await tx.signAuthEntries()
 * ```
 *
 * Under the hood, this uses `signAuthEntry`, which you either need to inject
 * during initial construction of the `Client`/`AssembledTransaction`,
 * or which you can pass directly to `signAuthEntries`.
 *
 * Now Bob can again serialize the transaction and send back to Alice, where
 * she can finally call `signAndSend()`.
 *
 * To see an even more complicated example, where Alice swaps with Bob but the
 * transaction is invoked by yet another party, check out
 * [test-swap.js](../../test/e2e/src/test-swap.js).
 *
 * @memberof module:contract
 */
class AssembledTransaction {
  /**
   * The TransactionBuilder as constructed in `{@link
   * AssembledTransaction}.build`. Feel free set `simulate: false` to modify
   * this object before calling `tx.simulate()` manually. Example:
   *
   * ```ts
   * const tx = await myContract.myMethod(
   *   { args: 'for', my: 'method', ... },
   *   { simulate: false }
   * );
   * tx.raw.addMemo(Memo.text('Nice memo, friend!'))
   * await tx.simulate();
   * ```
   */

  /**
   * The Transaction as it was built with `raw.build()` right before
   * simulation. Once this is set, modifying `raw` will have no effect unless
   * you call `tx.simulate()` again.
   */

  /**
   * The result of the transaction simulation. This is set after the first call
   * to `simulate`. It is difficult to serialize and deserialize, so it is not
   * included in the `toJSON` and `fromJSON` methods. See `simulationData`
   * cached, serializable access to the data needed by AssembledTransaction
   * logic.
   */

  /**
   * Cached simulation result. This is set after the first call to
   * {@link AssembledTransaction#simulationData}, and is used to facilitate
   * serialization and deserialization of the AssembledTransaction.
   *
   * Most of the time, if you need this data, you can call
   * `tx.simulation.result`.
   *
   * If you need access to this data after a transaction has been serialized
   * and then deserialized, you can call `simulationData.result`.
   */

  /**
   * Cached simulation transaction data. This is set after the first call to
   * {@link AssembledTransaction#simulationData}, and is used to facilitate
   * serialization and deserialization of the AssembledTransaction.
   *
   * Most of the time, if you need this data, you can call
   * `simulation.transactionData`.
   *
   * If you need access to this data after a transaction has been serialized
   * and then deserialized, you can call `simulationData.transactionData`.
   */

  /**
   * The Soroban server to use for all RPC calls. This is constructed from the
   * `rpcUrl` in the options.
   */

  /**
   * The signed transaction.
   */

  /**
   * A list of the most important errors that various AssembledTransaction
   * methods can throw. Feel free to catch specific errors in your application
   * logic.
   */
  static Errors = {
    ExpiredState: class ExpiredStateError extends Error {},
    RestorationFailure: class RestoreFailureError extends Error {},
    NeedsMoreSignatures: class NeedsMoreSignaturesError extends Error {},
    NoSignatureNeeded: class NoSignatureNeededError extends Error {},
    NoUnsignedNonInvokerAuthEntries: class NoUnsignedNonInvokerAuthEntriesError extends Error {},
    NoSigner: class NoSignerError extends Error {},
    NotYetSimulated: class NotYetSimulatedError extends Error {},
    FakeAccount: class FakeAccountError extends Error {},
    SimulationFailed: class SimulationFailedError extends Error {},
    InternalWalletError: class InternalWalletError extends Error {},
    ExternalServiceError: class ExternalServiceError extends Error {},
    InvalidClientRequest: class InvalidClientRequestError extends Error {},
    UserRejected: class UserRejectedError extends Error {}
  };

  /**
   * Serialize the AssembledTransaction to a JSON string. This is useful for
   * saving the transaction to a database or sending it over the wire for
   * multi-auth workflows. `fromJSON` can be used to deserialize the
   * transaction. This only works with transactions that have been simulated.
   */
  toJSON() {
    return JSON.stringify({
      method: this.options.method,
      tx: this.built?.toXDR(),
      simulationResult: {
        auth: this.simulationData.result.auth.map(a => a.toXDR("base64")),
        retval: this.simulationData.result.retval.toXDR("base64")
      },
      simulationTransactionData: this.simulationData.transactionData.toXDR("base64")
    });
  }
  static fromJSON(options, {
    tx,
    simulationResult,
    simulationTransactionData
  }) {
    const txn = new AssembledTransaction(options);
    txn.built = _stellarBase.TransactionBuilder.fromXDR(tx, options.networkPassphrase);
    txn.simulationResult = {
      auth: simulationResult.auth.map(a => _stellarBase.xdr.SorobanAuthorizationEntry.fromXDR(a, "base64")),
      retval: _stellarBase.xdr.ScVal.fromXDR(simulationResult.retval, "base64")
    };
    txn.simulationTransactionData = _stellarBase.xdr.SorobanTransactionData.fromXDR(simulationTransactionData, "base64");
    return txn;
  }

  /**
   * Serialize the AssembledTransaction to a base64-encoded XDR string.
   */
  toXDR() {
    if (!this.built) throw new Error("Transaction has not yet been simulated; " + "call `AssembledTransaction.simulate` first.");
    return this.built?.toEnvelope().toXDR('base64');
  }

  /**
   * Deserialize the AssembledTransaction from a base64-encoded XDR string.
   */
  static fromXDR(options, encodedXDR, spec) {
    const envelope = _stellarBase.xdr.TransactionEnvelope.fromXDR(encodedXDR, "base64");
    const built = _stellarBase.TransactionBuilder.fromXDR(envelope, options.networkPassphrase);
    const operation = built.operations[0];
    if (!operation?.func?.value || typeof operation.func.value !== 'function') {
      throw new Error("Could not extract the method from the transaction envelope.");
    }
    const invokeContractArgs = operation.func.value();
    if (!invokeContractArgs?.functionName) {
      throw new Error("Could not extract the method name from the transaction envelope.");
    }
    const method = invokeContractArgs.functionName().toString('utf-8');
    const txn = new AssembledTransaction({
      ...options,
      method,
      parseResultXdr: result => spec.funcResToNative(method, result)
    });
    txn.built = built;
    return txn;
  }
  handleWalletError(error) {
    if (!error) return;
    const {
      message,
      code
    } = error;
    const fullMessage = `${message}${error.ext ? ` (${error.ext.join(', ')})` : ''}`;
    switch (code) {
      case -1:
        throw new AssembledTransaction.Errors.InternalWalletError(fullMessage);
      case -2:
        throw new AssembledTransaction.Errors.ExternalServiceError(fullMessage);
      case -3:
        throw new AssembledTransaction.Errors.InvalidClientRequest(fullMessage);
      case -4:
        throw new AssembledTransaction.Errors.UserRejected(fullMessage);
      default:
        throw new Error(`Unhandled error: ${fullMessage}`);
    }
  }
  constructor(options) {
    this.options = options;
    this.options.simulate = this.options.simulate ?? true;
    this.server = new _rpc.Server(this.options.rpcUrl, {
      allowHttp: this.options.allowHttp ?? false
    });
  }

  /**
   * Construct a new AssembledTransaction. This is the main way to create a new
   * AssembledTransaction; the constructor is private.
   *
   * This is an asynchronous constructor for two reasons:
   *
   * 1. It needs to fetch the account from the network to get the current
   *   sequence number.
   * 2. It needs to simulate the transaction to get the expected fee.
   *
   * If you don't want to simulate the transaction, you can set `simulate` to
   * `false` in the options.
   *
   * If you need to create an operation other than `invokeHostFunction`, you
   * can use {@link AssembledTransaction.buildWithOp} instead.
   *
   * @example
   * const tx = await AssembledTransaction.build({
   *   ...,
   *   simulate: false,
   * })
   */
  static build(options) {
    const contract = new _stellarBase.Contract(options.contractId);
    return AssembledTransaction.buildWithOp(contract.call(options.method, ...(options.args ?? [])), options);
  }

  /**
   * Construct a new AssembledTransaction, specifying an Operation other than
   * `invokeHostFunction` (the default used by {@link AssembledTransaction.build}).
   *
   * Note: `AssembledTransaction` currently assumes these operations can be
   * simulated. This is not true for classic operations; only for those used by
   * Soroban Smart Contracts like `invokeHostFunction` and `createCustomContract`.
   *
   * @example
   * const tx = await AssembledTransaction.buildWithOp(
   *   Operation.createCustomContract({ ... });
   *   {
   *     ...,
   *     simulate: false,
   *   }
   * )
   */
  static async buildWithOp(operation, options) {
    const tx = new AssembledTransaction(options);
    const account = await (0, _utils.getAccount)(options, tx.server);
    tx.raw = new _stellarBase.TransactionBuilder(account, {
      fee: options.fee ?? _stellarBase.BASE_FEE,
      networkPassphrase: options.networkPassphrase
    }).setTimeout(options.timeoutInSeconds ?? _types.DEFAULT_TIMEOUT).addOperation(operation);
    if (options.simulate) await tx.simulate();
    return tx;
  }
  static async buildFootprintRestoreTransaction(options, sorobanData, account, fee) {
    const tx = new AssembledTransaction(options);
    tx.raw = new _stellarBase.TransactionBuilder(account, {
      fee,
      networkPassphrase: options.networkPassphrase
    }).setSorobanData(sorobanData instanceof _stellarBase.SorobanDataBuilder ? sorobanData.build() : sorobanData).addOperation(_stellarBase.Operation.restoreFootprint({})).setTimeout(options.timeoutInSeconds ?? _types.DEFAULT_TIMEOUT);
    await tx.simulate({
      restore: false
    });
    return tx;
  }
  simulate = async ({
    restore
  } = {}) => {
    if (!this.built) {
      if (!this.raw) {
        throw new Error("Transaction has not yet been assembled; " + "call `AssembledTransaction.build` first.");
      }
      this.built = this.raw.build();
    }
    restore = restore ?? this.options.restore;

    // need to force re-calculation of simulationData for new simulation
    delete this.simulationResult;
    delete this.simulationTransactionData;
    this.simulation = await this.server.simulateTransaction(this.built);
    if (restore && _api.Api.isSimulationRestore(this.simulation)) {
      const account = await (0, _utils.getAccount)(this.options, this.server);
      const result = await this.restoreFootprint(this.simulation.restorePreamble, account);
      if (result.status === _api.Api.GetTransactionStatus.SUCCESS) {
        // need to rebuild the transaction with bumped account sequence number
        const contract = new _stellarBase.Contract(this.options.contractId);
        this.raw = new _stellarBase.TransactionBuilder(account, {
          fee: this.options.fee ?? _stellarBase.BASE_FEE,
          networkPassphrase: this.options.networkPassphrase
        }).addOperation(contract.call(this.options.method, ...(this.options.args ?? []))).setTimeout(this.options.timeoutInSeconds ?? _types.DEFAULT_TIMEOUT);
        await this.simulate();
        return this;
      }
      throw new AssembledTransaction.Errors.RestorationFailure(`Automatic restore failed! You set 'restore: true' but the attempted restore did not work. Result:\n${JSON.stringify(result)}`);
    }
    if (_api.Api.isSimulationSuccess(this.simulation)) {
      this.built = (0, _transaction.assembleTransaction)(this.built, this.simulation).build();
    }
    return this;
  };
  get simulationData() {
    if (this.simulationResult && this.simulationTransactionData) {
      return {
        result: this.simulationResult,
        transactionData: this.simulationTransactionData
      };
    }
    const simulation = this.simulation;
    if (!simulation) {
      throw new AssembledTransaction.Errors.NotYetSimulated("Transaction has not yet been simulated");
    }
    if (_api.Api.isSimulationError(simulation)) {
      throw new AssembledTransaction.Errors.SimulationFailed(`Transaction simulation failed: "${simulation.error}"`);
    }
    if (_api.Api.isSimulationRestore(simulation)) {
      throw new AssembledTransaction.Errors.ExpiredState(`You need to restore some contract state before you can invoke this method.\n` + 'You can set `restore` to true in the method options in order to ' + 'automatically restore the contract state when needed.');
    }

    // add to object for serialization & deserialization
    this.simulationResult = simulation.result ?? {
      auth: [],
      retval: _stellarBase.xdr.ScVal.scvVoid()
    };
    this.simulationTransactionData = simulation.transactionData.build();
    return {
      result: this.simulationResult,
      transactionData: this.simulationTransactionData
    };
  }
  get result() {
    try {
      if (!this.simulationData.result) {
        throw new Error("No simulation result!");
      }
      return this.options.parseResultXdr(this.simulationData.result.retval);
    } catch (e) {
      if (!(0, _utils.implementsToString)(e)) throw e;
      const err = this.parseError(e.toString());
      if (err) return err;
      throw e; // eslint-disable-line
    }
  }
  parseError(errorMessage) {
    if (!this.options.errorTypes) return undefined;
    const match = errorMessage.match(_utils.contractErrorPattern);
    if (!match) return undefined;
    const i = parseInt(match[1], 10);
    const err = this.options.errorTypes[i];
    if (!err) return undefined;
    return new _rust_result.Err(err);
  }

  /**
   * Sign the transaction with the signTransaction function included previously.
   * If you did not previously include one, you need to include one now.
   */
  sign = async ({
    force = false,
    signTransaction = this.options.signTransaction
  } = {}) => {
    if (!this.built) {
      throw new Error("Transaction has not yet been simulated");
    }
    if (!force && this.isReadCall) {
      throw new AssembledTransaction.Errors.NoSignatureNeeded("This is a read call. It requires no signature or sending. " + "Use `force: true` to sign and send anyway.");
    }
    if (!signTransaction) {
      throw new AssembledTransaction.Errors.NoSigner("You must provide a signTransaction function, either when calling " + "`signAndSend` or when initializing your Client");
    }

    // filter out contracts, as these are dealt with via cross contract calls
    const sigsNeeded = this.needsNonInvokerSigningBy().filter(id => !id.startsWith('C'));
    if (sigsNeeded.length) {
      throw new AssembledTransaction.Errors.NeedsMoreSignatures(`Transaction requires signatures from ${sigsNeeded}. ` + "See `needsNonInvokerSigningBy` for details.");
    }
    const timeoutInSeconds = this.options.timeoutInSeconds ?? _types.DEFAULT_TIMEOUT;
    this.built = _stellarBase.TransactionBuilder.cloneFrom(this.built, {
      fee: this.built.fee,
      timebounds: undefined,
      sorobanData: this.simulationData.transactionData
    }).setTimeout(timeoutInSeconds).build();
    const signOpts = {
      networkPassphrase: this.options.networkPassphrase
    };
    if (this.options.address) signOpts.address = this.options.address;
    if (this.options.submit !== undefined) signOpts.submit = this.options.submit;
    if (this.options.submitUrl) signOpts.submitUrl = this.options.submitUrl;
    const {
      signedTxXdr: signature,
      error
    } = await signTransaction(this.built.toXDR(), signOpts);
    this.handleWalletError(error);
    this.signed = _stellarBase.TransactionBuilder.fromXDR(signature, this.options.networkPassphrase);
  };

  /**
   * Sends the transaction to the network to return a `SentTransaction` that
   * keeps track of all the attempts to fetch the transaction.
   */
  async send() {
    if (!this.signed) {
      throw new Error("The transaction has not yet been signed. Run `sign` first, or use `signAndSend` instead.");
    }
    const sent = await _sent_transaction.SentTransaction.init(this);
    return sent;
  }

  /**
   * Sign the transaction with the `signTransaction` function included previously.
   * If you did not previously include one, you need to include one now.
   * After signing, this method will send the transaction to the network and
   * return a `SentTransaction` that keeps track * of all the attempts to fetch the transaction.
   */
  signAndSend = async ({
    force = false,
    signTransaction = this.options.signTransaction
  } = {}) => {
    if (!this.signed) {
      // Store the original submit option
      const originalSubmit = this.options.submit;

      // Temporarily disable submission in signTransaction to prevent double submission
      if (this.options.submit) {
        this.options.submit = false;
      }
      try {
        await this.sign({
          force,
          signTransaction
        });
      } finally {
        // Restore the original submit option
        this.options.submit = originalSubmit;
      }
    }
    return this.send();
  };

  /**
   * Get a list of accounts, other than the invoker of the simulation, that
   * need to sign auth entries in this transaction.
   *
   * Soroban allows multiple people to sign a transaction. Someone needs to
   * sign the final transaction envelope; this person/account is called the
   * _invoker_, or _source_. Other accounts might need to sign individual auth
   * entries in the transaction, if they're not also the invoker.
   *
   * This function returns a list of accounts that need to sign auth entries,
   * assuming that the same invoker/source account will sign the final
   * transaction envelope as signed the initial simulation.
   *
   * One at a time, for each public key in this array, you will need to
   * serialize this transaction with `toJSON`, send to the owner of that key,
   * deserialize the transaction with `txFromJson`, and call
   * {@link AssembledTransaction#signAuthEntries}. Then re-serialize and send to
   * the next account in this list.
   */
  needsNonInvokerSigningBy = ({
    includeAlreadySigned = false
  } = {}) => {
    if (!this.built) {
      throw new Error("Transaction has not yet been simulated");
    }

    // We expect that any transaction constructed by these libraries has a
    // single operation, which is an InvokeHostFunction operation. The host
    // function being invoked is the contract method call.
    if (!("operations" in this.built)) {
      throw new Error(`Unexpected Transaction type; no operations: ${JSON.stringify(this.built)}`);
    }
    const rawInvokeHostFunctionOp = this.built.operations[0];
    return [...new Set((rawInvokeHostFunctionOp.auth ?? []).filter(entry => entry.credentials().switch() === _stellarBase.xdr.SorobanCredentialsType.sorobanCredentialsAddress() && (includeAlreadySigned || entry.credentials().address().signature().switch().name === "scvVoid")).map(entry => _stellarBase.Address.fromScAddress(entry.credentials().address().address()).toString()))];
  };

  /**
   * If {@link AssembledTransaction#needsNonInvokerSigningBy} returns a
   * non-empty list, you can serialize the transaction with `toJSON`, send it to
   * the owner of one of the public keys in the map, deserialize with
   * `txFromJSON`, and call this method on their machine. Internally, this will
   * use `signAuthEntry` function from connected `wallet` for each.
   *
   * Then, re-serialize the transaction and either send to the next
   * `needsNonInvokerSigningBy` owner, or send it back to the original account
   * who simulated the transaction so they can {@link AssembledTransaction#sign}
   * the transaction envelope and {@link AssembledTransaction#send} it to the
   * network.
   *
   * Sending to all `needsNonInvokerSigningBy` owners in parallel is not
   * currently supported!
   */
  signAuthEntries = async ({
    expiration = (async () => (await this.server.getLatestLedger()).sequence + 100)(),
    signAuthEntry = this.options.signAuthEntry,
    address = this.options.publicKey,
    authorizeEntry = _stellarBase.authorizeEntry
  } = {}) => {
    if (!this.built) throw new Error("Transaction has not yet been assembled or simulated");

    // Likely if we're using a custom authorizeEntry then we know better than the `needsNonInvokerSigningBy` logic.
    if (authorizeEntry === _stellarBase.authorizeEntry) {
      const needsNonInvokerSigningBy = this.needsNonInvokerSigningBy();
      if (needsNonInvokerSigningBy.length === 0) {
        throw new AssembledTransaction.Errors.NoUnsignedNonInvokerAuthEntries("No unsigned non-invoker auth entries; maybe you already signed?");
      }
      if (needsNonInvokerSigningBy.indexOf(address ?? "") === -1) {
        throw new AssembledTransaction.Errors.NoSignatureNeeded(`No auth entries for public key "${address}"`);
      }
      if (!signAuthEntry) {
        throw new AssembledTransaction.Errors.NoSigner("You must provide `signAuthEntry` or a custom `authorizeEntry`");
      }
    }
    const rawInvokeHostFunctionOp = this.built.operations[0];
    const authEntries = rawInvokeHostFunctionOp.auth ?? [];

    // eslint-disable-next-line no-restricted-syntax
    for (const [i, entry] of authEntries.entries()) {
      // workaround for https://github.com/stellar/js-stellar-sdk/issues/1070
      const credentials = _stellarBase.xdr.SorobanCredentials.fromXDR(entry.credentials().toXDR());
      if (credentials.switch() !== _stellarBase.xdr.SorobanCredentialsType.sorobanCredentialsAddress()) {
        // if the invoker/source account, then the entry doesn't need explicit
        // signature, since the tx envelope is already signed by the source
        // account, so only check for sorobanCredentialsAddress
        continue; // eslint-disable-line no-continue
      }
      const authEntryAddress = _stellarBase.Address.fromScAddress(credentials.address().address()).toString();

      // this auth entry needs to be signed by a different account
      // (or maybe already was!)
      if (authEntryAddress !== address) continue; // eslint-disable-line no-continue

      const sign = signAuthEntry ?? Promise.resolve;

      // eslint-disable-next-line no-await-in-loop
      authEntries[i] = await authorizeEntry(entry, async preimage => {
        const {
          signedAuthEntry,
          error
        } = await sign(preimage.toXDR("base64"), {
          address
        });
        this.handleWalletError(error);
        return Buffer.from(signedAuthEntry, "base64");
      }, await expiration,
      // eslint-disable-line no-await-in-loop
      this.options.networkPassphrase);
    }
  };

  /**
   * Whether this transaction is a read call. This is determined by the
   * simulation result and the transaction data. If the transaction is a read
   * call, it will not need to be signed and sent to the network. If this
   * returns `false`, then you need to call `signAndSend` on this transaction.
   */
  get isReadCall() {
    const authsCount = this.simulationData.result.auth.length;
    const writeLength = this.simulationData.transactionData.resources().footprint().readWrite().length;
    return authsCount === 0 && writeLength === 0;
  }

  /**
   * Restores the footprint (resource ledger entries that can be read or written)
   * of an expired transaction.
   *
   * The method will:
   * 1. Build a new transaction aimed at restoring the necessary resources.
   * 2. Sign this new transaction if a `signTransaction` handler is provided.
   * 3. Send the signed transaction to the network.
   * 4. Await and return the response from the network.
   *
   * Preconditions:
   * - A `signTransaction` function must be provided during the Client initialization.
   * - The provided `restorePreamble` should include a minimum resource fee and valid
   *   transaction data.
   *
   * @throws {Error} - Throws an error if no `signTransaction` function is provided during
   * Client initialization.
   * @throws {AssembledTransaction.Errors.RestoreFailure} - Throws a custom error if the
   * restore transaction fails, providing the details of the failure.
   */
  async restoreFootprint(
  /**
   * The preamble object containing data required to
   * build the restore transaction.
   */
  restorePreamble, /** The account that is executing the footprint restore operation. If omitted, will use the account from the AssembledTransaction. */
  account) {
    if (!this.options.signTransaction) {
      throw new Error("For automatic restore to work you must provide a signTransaction function when initializing your Client");
    }
    account = account ?? (await (0, _utils.getAccount)(this.options, this.server));
    // first try restoring the contract
    const restoreTx = await AssembledTransaction.buildFootprintRestoreTransaction({
      ...this.options
    }, restorePreamble.transactionData, account, restorePreamble.minResourceFee);
    const sentTransaction = await restoreTx.signAndSend();
    if (!sentTransaction.getTransactionResponse) {
      throw new AssembledTransaction.Errors.RestorationFailure(`The attempt at automatic restore failed. \n${JSON.stringify(sentTransaction)}`);
    }
    return sentTransaction.getTransactionResponse;
  }
}
exports.AssembledTransaction = AssembledTransaction;