Polkadot Provider API: a common interface for building decentralized applications

Hi @tomaka,

Thank you for reviewing the draft. I appreciate your feedback.

I intended to update the interfaces I mentioned earlier, but I seem to have lost editing access :sweat_smile:. Nevertheless, we’ve tested the interface and identified some improvements. Below is the refined interface proposal:

type Callback<T> = (value: T) => void
type UnsubscribeFn = () => void

interface PolkadotProvider {
  // Retrieves the current list of available Chains
  // that the dApp can connect to
  getChains: () => Chains

  // Registers a callback invoked when the list
  // of available chains changes
  onChainsChange: (chains: Callback<Chains>) => UnsubscribeFn

  // Allows the dApp to request the Provider to register a Chain
  addChain: (chainspec: string) => Promise<Chain>
}

// The key is the genesis-hash of the chain
type Chains = Record<string, Chain>

interface Chain {
  genesisHash: string
  name: string

  // it pulls the current list of available accounts for this Chain
  getAccounts: () => Array<Account>

  // registers a callback that will be invoked whenever the list
  // of available accounts for this chain has changed
  onAccountsChange: (accounts: Callback<Array<Account>>) => UnsubscribeFn

  // returns a JSON RPC Provider that it's compliant with new
  // JSON-RPC API spec:
  // https://paritytech.github.io/json-rpc-interface-spec/api.html
  connect: (
    // the listener callback that the JsonRpcProvider
    // will be sending messages to
    onMessage: Callback<string>,

    // the listener that will be notified when the connectivity changes
    onStatusChange: Callback<ProviderStatus>,
  ) => Promise<JsonRpcProvider>
}

type ProviderStatus = "connected" | "disconnected"

interface JsonRpcProvider {
  // it sends messages to the JSON RPC Server
  send: (message: string) => void

  // `publicKey` is the SS58Formated public key
  // `callData` is the scale encoded call-data
  // (module index, call index and args)
  createTx: (publicKey: string, callData: Uint8Array) => Promise<Uint8Array>

  // it disconnects from the JSON RPC Server and it de-registers
  // the `onMessage` and `onStatusChange` callbacks that
  // were previously registered
  disconnect: UnsubscribeFn
}

interface Account {
  // SS58Formated public key
  publicKey: string

  // The provider may have captured a display name
  displayName?: string
}

Let me address your points:

  1. On the use of Callback and UnsubscribeFn types:

    • You’re right regarding send. It indeed isn’t a callback function, and this oversight has been rectified in the latest review.
    • For onMessage and onStateChange: They are genuine callback functions. They’re designed for the consumer to supply, allowing the producer to send data back, aligning with the classic callback definition.
    • As for disconnect: The intention behind naming it an “unsubscription function” is to reflect its purpose, which is to de-register the onMessage and onStatusChange callbacks that were previously set up.
  2. Chain Identification with Genesis Hash:

    • I must admit, this was a revelation! When seeking advice from some seasoned experts at Parity on how best to identify a chain (similar to Ethereum’s chain_id and its use by the Ethereum Provider as specified here), I understood that the genesis hash could be employed for such identification. Clearly, I may have misinterpreted this. I appreciate you pointing this out. It’s crucial because it might have inadvertently influenced some API decisions in another library. We’ll certainly need to reevaluate how we can uniquely identify a chain.

    • EDIT: Upon some consultation with domain experts, it appears that the most accurate method to uniquely identify a chain is by utilizing a combination of (hash_of_forked_block, block_number_of_forked_block). Meaning that: for chains that haven’t experienced any forks, the identifier would be (genesis_hash, 0). We could represent this information as a hexadecimal string, where a non-forked chain will have a 32-byte long hexadecimal string representing the genesis hash. However, for a forked-chain, its identifier will be longer than 32 bytes. This extra length will be attributable to the compact encoded block number, appended to the hash of the forked block. This mechanism ensures that every chain gets a distinct identifier. This approach not only provides a unique identifier for each chain but also allows for easy differentiation between original and forked chains based on the length of the hexadecimal string. What do you think?

  3. Merging ChainProvider and JsonRpcProvider:

    • EDIT: they’ve now been merged. Thanks @tomaka!
  4. Account Object Invalidation:

    • You’ve touched upon an important change we’ve recently integrated. The suggestion to shift createTx from Account to ChainProvider and specify the account as an additional parameter resonated with us. This not only makes Account a pure data structure but also alleviates the ambiguity you pointed out. The latest proposal encapsulates this change. Please take a moment to review it and let me know if this clears things up.
  5. Concept of “Suggesting a Chain”:

    • The idea behind “suggesting a chain” primarily serves to shield users from potentially harmful dApps. To illustrate: imagine a versatile dApp built for both Kusama and Polkadot. The dApp can allow users to select their preferred network. When a user opts for Polkadot, the dApp checks the availability of the Polkadot relay chain (which is likely present) and then the collectives parachain. If the latter is absent, it prompts the Provider to add it.
    • It’s crucial to note that proposing a new chain doesn’t automatically mean the Provider will save or distribute that chain to other dApps. User consent is paramount. If the Provider is, for instance, an extension, it would ideally solicit user approval before connecting to a new chain. Thus, users hold the final decision on chain persistence and sharing across dApps.
    • As a hypothetical example, many dApp users (myself included) would be inclined to persist the Polkadot collectives parachain in their provider. Yet, it’s entirely feasible for a Provider to neither retain nor share any user-added chains with other dApps.
  6. Regarding ProviderStatus:

    • Your interpretation is understandable. The ProviderStatus pertains not to the state of the light client but to the communication medium enabling the JsonRpcProvider. Examples could range from a WebSocket connection being terminated or a Worker process being unexpectedly halted.
    • A connected status implies the JsonRpc API interface is primed for messaging. Conversely, a disconnected status signals that:
      a) No mechanism is attending to the messages dispatched via send.
      b) The onMessage callback will remain inactive.
    • Essentially, it notifies the consumer about the unavailability of the medium and perhaps prompts them to initiate a fresh connection. Also, a ‘disconnected’ state is irreversible. I do recognize the importance of clarity in the spec on this, and maybe designations like ready/halted might be more intuitive.
  7. Promise Error Specifications:

    • Absolutely! This oversight will be addressed. Comprehensive documentation detailing potential errors is forthcoming. Thank you for flagging it.
  8. Advocating for JSON-RPC Calls:

    • While I acknowledge the consistency and predictability that JSON-RPC calls offer, especially when handling known errors, could you elucidate on the additional corner cases these calls could potentially rectify?
  9. Minimizing Promises:

    • I concur with your perspective on the complexity introduced by promises, especially when considering race conditions. In response, only two functions now return promises, notably connect and createTx, given their inherent asynchronous operations. The polling of accounts and networks has been restructured to be “synchronous”, eliminating the need for promises there.

Your keen scrutiny has been pivotal in refining this interface. I’m eager to hear your thoughts on these clarifications.