Skip to content

RFC: Deprecate signPayload in favour of new createTransaction #6213

@josepot

Description

@josepot

The signPayload function has become a de-facto standard because wallets, extensions, and other signers expose it as the public interface for creating transactions.

However, this interface is very problematic and has held the ecosystem back. In short:

  • It predates Metadata V14 and doesn’t account for custom extensions defined in the metadata.
  • It only supports a small, opinionated set of signed extensions via a restrictive API.
  • Any time we add or change an extension, DApps break (e.g. CheckMetadataHash rollout and changes in ChargeAssetTxPayment). It can't leverage the work of RFC-99.
  • The API isn’t documented rigorously and leaks PJS implementation details, eg:
    • blockNumber: expects big-endian encoding.
    • nonce: is encoded differently from what's defined in the metadata.
  • It cannot fully support Extrinsic V5 (general transactions) and/or versioned transaction extensions.

Goal

Introduce a new, well-specified, forward-compatible function named createTransaction that:

  1. Enables interoperability across libraries, wallets, extensions, and tooling.
  2. Supports both Extrinsic V4 and Extrinsic V5 (including General transactions and versioned transaction extensions).
  3. Allows chains to define custom extensions without requiring bespoke wallets.

This interface should live side-by-side with signPayload. Callers should prefer createTransaction when present; signPayload remains for backward compatibility.


Proposed TypeScript interface

type HexString = `0x${string}`;

export interface TxPayloadV1 {
  /** Payload version. MUST be 1. */
  version: 1;

  /**
   * Signer selection hint. Allows the implementer to identify which private-key / scheme to use.
   * - Use a wallet-defined handle (e.g., address/SS58, account-name, etc). This identifier
   * was previously made available to the consumer. 
   * - Set `null` to let the implementer pick the signer (or if the signer is implied).
   */
  signer: string | null;

  /**
   * SCALE-encoded Call (module indicator + function indicator + params).
   */
  callData: HexString;

  /**
   * Transaction extensions supplied by the caller (order irrelevant).
   * The consumer SHOULD provide every extension that is relevant to them.
   * The implementer MAY infer missing ones.
   */
  extensions: Array<{
    /** Identifier as defined in metadata (e.g., "CheckSpecVersion", "ChargeAssetTxPayment"). */
    id: string;

    /**
     * Explicit "extra" to sign (goes into the extrinsic body).
     * SCALE-encoded per the extension's "extra" type as defined in the metadata.
     */
    extra: HexString;

    /**
     * "Implicit" data to sign (known by the chain, not included into the extrinsic body).
     * SCALE-encoded per the extension's "additionalSigned" type as defined in the metadata.
     */
    additionalSigned: HexString;
  }>;

  /**
   * Transaction Extension Version.
   * - For Extrinsic V4 MUST be 0.
   * - For Extrinsic V5, set to any version supported by the runtime.
   * The implementer:
   *  - MUST use this field to determine the required extensions for creating the extrinsic.
   *  - MAY use this field to infer missing extensions that the implementer could know how to handle.
   */
  txExtVersion: number;

  /**
   * Context needed for decoding, display, and (optionally) inferring certain extensions.
   */
  context: {
    /**
     * RuntimeMetadataPrefixed blob (SCALE), starting with ASCII "meta" magic (`0x6d657461`),
     * then a metadata version (V14+). For V5+ versioned extensions, MUST provide V16+.
     */
    metadata: HexString;

    /**
     * Native token display info (used by some implementers), also needed to compute
     * the `CheckMetadataHash` value.
     */
    tokenSymbol: string;
    tokenDecimals: number;

    /**
     * Highest known block number to aid mortality UX.
     */
    bestBlockHeight: number;
  };
}

/**
 * Creates a SCALE-encoded extrinsic (ready to broadcast).
 */
export type TxCreator = (input: TxPayloadV1) => Promise<HexString>;

Normative behavior (what implementers MUST/SHOULD do)

  • Return value: A Promise that resolves to an Hexadecimal SCALE-encoded extrinsic ready to broadcast.
  • Input invariants
    • HexString values MUST be hexadecimal symbols 0-f, even-length, and 0x prefixed.
    • version MUST be 1.
    • metadata MUST be a SCALE RuntimeMetadataPrefixed blob beginning with ASCII “meta” and a version byte. (V14+ allowed for V4; V16+ required when txExtVersion > 0 to select extension sets by version.)
    • For V4 transactions, txExtVersion MUST be 0. For V5 transactions, txExtVersion MUST be a runtime-supported extension set version (per RFC-0099).
  • Extensions array
    • Match extensions by id exactly as declared in metadata.
    • Unknown ids (not declared in the corresponding txExtVersion of the metadata) are not allowed.
    • There can't be repeated ids.
    • The order is irrelevant.
    • If required by the runtime but not provided, the implementer SHOULD try to infer and append missing extensions using metadata (and chain state, if available).
  • Mortality UX
    • bestBlockHeight is provided so that "offline" implementers can display to the signer the range of blocks in which the transaction will be valid.
  • Errors
    Implementations SHOULD throw structured errors for:
    • UnsupportedTxExtensionVersion (unknown txExtVersion for this runtime)
    • MissingMandatoryExtension (metadata indicates required extension not provided and not inferable)
    • InvalidHex / DecodeError (malformed callData / extra / additionalSigned)
    • SignerUnavailable (no signer resolution possible)

Backward compatibility & migration

  • createTransaction does not remove signPayload (for now). Libraries can feature-detect and MUST prefer createTransaction; fall back to signPayload where needed.
  • For chains still on extrinsic V4, set txExtVersion = 0.
  • For chains supporting both extrinsic V4 and V5, implementers can decide which version is better suited depending on the extensions provided when txExtVersion = 0. If txExtVersion > 0 they must use extrinsic V5.

FAQ

What if the implementer already has the metadata, or can’t afford receiving such a large payload?

The purpose of this interface is to support both online and offline implementers. It is explicitly designed to be consumed by a JavaScript process, but the concrete implementation details are left flexible.

Different implementers can build compliant libraries while adapting to the constraints of their environment. Some examples:

  • Hardware devices (e.g., Ledger): A Ledger implementer could implement this interface even though it cannot receive the entire metadata blob. In that case, the accompanying JavaScript library would take responsibility for interpreting the metadata and transforming it into the minimal information that needs to be sent to the device.

  • Browser extensions: An extension that only supports a fixed set of chains might already cache the metadata locally. In this case, the extension can safely ignore the provided context.metadata (or reject the promise outright if the chain is not supported).

The guiding principle is that the interface guarantees interoperability at the JavaScript boundary, while implementers remain free to optimize the transport and storage formats used internally.


cc: @0xKheops @valentunn @carlosala @voliva @TarikGul @saltict

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions