Polkadot-js: Changes in the Signer Interface

Changes in the Signer Interface

Summary

As the Polkadot and Substrate ecosystems have evolved, the development of new tools to address emerging challenges has occurred naturally. This progress has led us to reassess some of the earlier design decisions in Polkadot-js, which were appropriate at the time but now require adjustments to enable more generalized and streamlined integration.

The primary concern lies with certain aspects of the signer interface that Polkadot-js introduced, which have since become the standard. The following will outline some of the changes introduced in the most recent releases to address this.

Before we proceed, I would like to express my gratitude to the Polkadot-api team for their invaluable support and collaboration throughout this process. Their teamwork has been instrumental in driving these improvements.

Changes

To reduce opinionated types and coupling in polkadot-js, we decided to generalize the types and allow bypassing calls like signAndSend.

The following changes are reflected in the @polkadot/api library.

SignerResult

Below is the SignerResult interface where the signedTransaction field was added in 12.0.1. This was followed by the addition of withSignedTransaction, which I will address in the SignerPayloadJSON section. These changes were crucial for enabling the new PolkadotGeneric app with Ledger, allowing Signers to include the metadataHash field in extrinsics.

An important note: Polkadot-js does not allow modification of the call data when the signedTransaction is returned from the signer. This data is validated against the original payload that was created.

export interface SignerResult {
  /**
   * @description The id for this request
   */
  id: number;

  /**
   * @description The resulting signature in hex
   */
  signature: HexString;

  /**
   * @description The payload constructed by the signer. This allows the
   * inputted signed transaction to bypass `signAndSend` from adding the signature to the payload,
   * and instead broadcasting the transaction directly. There is a small validation layer. Please refer
   * to the implementation for more information. If the inputted signed transaction is not actually signed, it will fail with an error.
   *
   * This will also work for `signAsync`. The new payload will be added to the Extrinsic, and will be sent once the consumer calls `.send()`.
   *
   * NOTE: This is only implemented for `signPayload`, and will only work when the `withSignedTransaction` option is enabled as an option.
   */
  signedTransaction?: HexString | Uint8Array;
}

SignerPayloadJSON

Below is the SignerPayloadJSON, which includes one major addition and one significant change. In 12.0.2 the withSignedTransaction option was introduced to enable the signedTransaction field in the SignerResult. This change ensures that the signedTransaction field remains disabled by default, preventing disruptions to existing implementations in wallets and dApps. Additionally, in 13.0.1 we updated the assetId type to a more generalized form by setting it to the SCALE-encoded hex value. This is a more recent change with significant impact, but it should be carefully coordinated with wallets and dApps before propagating to downstream polkadot-js libraries.

export interface SignerPayloadJSON {
  /**
   * @description The ss-58 encoded address
   */
  address: string;

  /**
   * @description The id of the asset used to pay fees, in hex
   */
  assetId?: HexString;

  /**
   * @description The checkpoint hash of the block, in hex
   */
  blockHash: HexString;

  /**
   * @description The checkpoint block number, in hex
   */
  blockNumber: HexString;

  /**
   * @description The era for this transaction, in hex
   */
  era: HexString;

  /**
   * @description The genesis hash of the chain, in hex
   */
  genesisHash: HexString;

  /**
   * @description The metadataHash for the CheckMetadataHash SignedExtension, as hex
   */
  metadataHash?: HexString;

  /**
   * @description The encoded method (with arguments) in hex
   */
  method: string;

  /**
   * @description The mode for the CheckMetadataHash SignedExtension, in hex
   */
  mode?: number;

  /**
   * @description The nonce for this transaction, in hex
   */
  nonce: HexString;

  /**
   * @description The current spec version for the runtime
   */
  specVersion: HexString;

  /**
   * @description The tip for this transaction, in hex
   */
  tip: HexString;

  /**
   * @description The current transaction version for the runtime
   */
  transactionVersion: HexString;

  /**
   * @description The applicable signed extensions for this runtime
   */
  signedExtensions: string[];

  /**
   * @description The version of the extrinsic we are dealing with
   */
  version: number;

  /**
   * @description Optional flag that enables the use of the `signedTransaction` field in
   * `singAndSend`, `signAsync`, and `dryRun`.
   */
  withSignedTransaction?: boolean;
}

Impacts on Signer and ISignerPayload

This impacts two major interfaces used with signers, or other api implementations.

export interface ISignerPayload {
  toPayload (): SignerPayloadJSON;
  ...
}
export interface Signer {
  /**
   * @description signs an extrinsic payload from a serialized form
   */
  signPayload?: (payload: SignerPayloadJSON) => Promise<SignerResult>;
  ...
}

Overview

In our ongoing effort to generalize the polkadot-js interfaces for better integration with other wallets and dApps, we hope these changes will facilitate future integrations. This is a step in the right direction and should be a positive addition overall.

Although these changes might seem minor, they can have significant impacts across the ecosystem and potentially cause disruptions. To foster greater transparency with the community, I aim to provide updates like this to solicit feedback and suggestions for additional interface improvements.

8 Likes