FuelLabs/fuels-ts · error · FuelError

TRANSACTION_ERROR

TRANSACTION_ERROR

Error message

The target function ${this.func.name} cannot accept forwarded funds as it's not marked as 'payable'.

What it means

Thrown by FunctionInvocationScope.callParams() when a `forward` amount is supplied but the target Sway function's ABI attributes do not include 'payable'. Forwarding native coins to a contract call requires the Sway function to be annotated #[payable]; without it the VM would reject the funds, so the SDK blocks it up front.

Source

Thrown at packages/program/src/functions/invocation-scope.ts:84

    return this;
  }

  /**
   * Sets the call parameters for the function invocation.
   *
   * @param callParams - The call parameters.
   * @returns The instance of FunctionInvocationScope.
   * @throws If the function is not payable and forward is set.
   */
  callParams(callParams: CallParams) {
    if (!this.hasCallParamsGasLimit && callParams?.gasLimit !== undefined) {
      this.hasCallParamsGasLimit = true;
    }
    this.callParameters = callParams;

    if (callParams?.forward) {
      if (!this.func.attributes.find((attr) => attr.name === 'payable')) {
        throw new FuelError(
          ErrorCode.TRANSACTION_ERROR,
          `The target function ${this.func.name} cannot accept forwarded funds as it's not marked as 'payable'.`
        );
      }

      this.forward = coinQuantityfy(callParams.forward);
    }

    // Update transaction script with new forward params
    this.setArguments(...this.args);

    // Update required coins
    this.updateRequiredCoins();

    return this;
  }
}

View on GitHub (pinned to b3f37c91ac)

Solutions

  1. Mark the Sway function #[payable] (storage(read, write) may also be needed) and redeploy + regenerate types.
  2. If you did not intend to send coins, remove the `forward` entry from callParams.
  3. Re-run typegen against the current contract ABI to ensure the payable attribute is present in the JSON ABI used by the SDK.

Example fix

// Sway
// before
entry fn deposit(amount: u64) { ... }
// after
#[payable]
entry fn deposit(amount: u64) { ... }
Defensive patterns

Strategy: validation

Validate before calling

function isPayable(func: { attributes: Array<{ name: string }> }): boolean {
  return !!func.attributes?.some((a) => a.name === 'payable');
}

if (callParams?.forward && !isPayable(scope.func)) {
  throw new Error(`${scope.func.name} is not payable; remove forward or mark it #[payable] in Sway.`);
}

Type guard

function isPayableFunction(func: { attributes: Array<{ name: string }> }): boolean {
  return !!func.attributes?.some((a) => a.name === 'payable');
}

Try / catch

try {
  await contract.functions.deposit().callParams({ forward: [amt, assetId] }).call();
} catch (e) {
  if (e instanceof FuelError && e.code === ErrorCode.TRANSACTION_ERROR && /payable/.test(e.message)) {
    // mark the Sway function #[payable], redeploy, regenerate types
  } else throw e;
}

Prevention

When it happens

Trigger: Calling .callParams({ forward: [amount, assetId] }) on a function whose ABI has no payable attribute; forwarding to a read/View or non-payable function; ABI regenerated from a contract where #[payable] was removed.

Common situations: Depositing or paying into a function that the developer forgot to mark #[payable] in Sway; using the wrong ABI (stale typegen) where payable was stripped; sending base asset to a pure function.

Related errors


AI-assisted analysis of FuelLabs/fuels-ts@b3f37c91ac (2026-08-12). Data as JSON: /api/errors/e76a82a52fa6340c. Report an issue: GitHub.