-
Notifications
You must be signed in to change notification settings - Fork 377
Description
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.
CheckMetadataHashrollout and changes inChargeAssetTxPayment). 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:
- Enables interoperability across libraries, wallets, extensions, and tooling.
- Supports both Extrinsic V4 and Extrinsic V5 (including General transactions and versioned transaction extensions).
- 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
HexStringvalues MUST be hexadecimal symbols0-f, even-length, and0xprefixed.versionMUST be1.metadataMUST be a SCALE RuntimeMetadataPrefixed blob beginning with ASCII “meta” and a version byte. (V14+ allowed for V4; V16+ required whentxExtVersion > 0to select extension sets by version.)- For V4 transactions,
txExtVersionMUST be0. For V5 transactions,txExtVersionMUST be a runtime-supported extension set version (per RFC-0099).
- Extensions array
- Match extensions by
idexactly as declared in metadata. - Unknown
ids (not declared in the correspondingtxExtVersionof 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).
- Match extensions by
- Mortality UX
bestBlockHeightis 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(unknowntxExtVersionfor this runtime)MissingMandatoryExtension(metadata indicates required extension not provided and not inferable)InvalidHex/DecodeError(malformedcallData/extra/additionalSigned)SignerUnavailable(nosignerresolution possible)
Backward compatibility & migration
createTransactiondoes not removesignPayload(for now). Libraries can feature-detect and MUST prefercreateTransaction; fall back tosignPayloadwhere 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. IftxExtVersion > 0they must use extrinsicV5.
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