Skip to content

TypedApi

The TypedApi allows to interact with the runtime metadata easily and with a great developer experience. It'll allow to make storage calls, create transactions, etc. It uses the descriptors generated by PAPI CLI (see Codegen section for a deeper explanation) to generate the types used at devel time. TypedApi object looks like:

type TypedApi = {
  query: StorageApi
  tx: TxApi
  event: EvApi
  apis: RuntimeCallsApi
  constants: ConstApi
  compatibilityToken: Promise<CompatibilityToken>
}

Every field except for compatibilityToken is a Record<string, Record<string, ???>>. The first index defines the pallet, and the second one defines which query/tx/event/api/constant within that pallet. Each one of them will be described in the following pages, but let's focus on the compatibility check, which is common for all of them.

getCompatibilityLevel

The getCompatibilityLevel is under each query/tx/event/api/constant. After generating the descriptors (see Codegen section), we have a typed interface to every interaction with the chain. Nevertheless, breaking runtime upgrades might hit the runtime between developing and the runtime execution of your app. getCompatibilityLevel enables you to check on runtime if there was a breaking upgrade that hit your particular method.

The enum CompatibilityLevel defines 4 levels of compatibility:

enum CompatibilityLevel {
  // No possible value from origin will be compatible with dest
  Incompatible,
  // Some values of origin will be compatible with dest
  Partial,
  // Every value from origin will be compatible with dest
  BackwardsCompatible,
  // Types are identical
  Identical,
}

A CompatibilityLevel.Partial means that the operation might be compatible depending on the actual values being sent or received. For instance, getCompatibilityLevel for the transaction utility.batch_all might return a CompatibilityLevel.Partial if one of the transactions it takes as input was removed. In this case, the call will be compatible as long as you don't send the transaction that was removed as one of its inputs.

Another instance of a partial compatibility case could be for instance if an optional property on a struct that's an input was made mandatory. In this case, if your dApp was always populating that field, it will still work properly, but if you had cases where you weren't setting it, then it will be incompatible.

On the other hand, a CompatibilityLevel.BackwardsCompatible, means that the operation had some changes, but they are backwards compatible with the descriptors generated on dev time. In the case of utility.batch_all, this might happen when a new transaction is added as a possible input. In this case, there was a change, and PAPI lets you know about it with this level, but you can be sure that any transaction that you pass in as an input will still work.

A backwards-compatible change also happens in structs. For instance, if an input struct removes one of their properties, those operations are still compatible.

This compatibility check needs to have the runtime from the current connection, but also the descriptors that were generated from the CLI and are lazy loaded. This means that by default, getCompatibilityLevel is asynchronous, because it potentially needs to wait until that is loaded before it can run the check.

This is where compatibilityToken comes into play. This is a promise that will resolve when both the connection to the current runtime and the descriptors have fully loaded. So if you want getCompatibilityLevel to be synchronous, then you can await the compatibilityToken just once (e.g. on dApp initialization), and then pass it as a parameter to getCompatibilityLevel to have it synchronously. This is because the token internally has all the information needed to run the compatibility check.

interface GetCompatibilityLevel {
  (): Promise<CompatibilityLevel>
  (compatibilityToken: CompatibilityToken): CompatibilityLevel
}

There's also a small utility next to .getCompatibilityLevel() to directly check for compatibility called .isCompatible(threshold): boolean. The threshold sets the level you want to set for the result to be true, inclusive. So passing in CompatibilityLevel.BackwardsCompatible, will return true for both identical and backwards compatible, but not for patial compatibility.

interface IsCompatible {
  (threshold: CompatibilityLevel): Promise<boolean>
  (
    threshold: CompatibilityLevel,
    compatibilityToken: CompatibilityToken,
  ): boolean
}
 
// Possible "pseudocode" implementation, to show the equivalence
function isCompatible(threshold, token) {
  return getCompatibilityLevel(token) >= threshold
}

For example, let's use typedApi.query.System.Number. It's a simple query, we'll see in the next pages how to interact with it. In this example we'll focus on getCompatibilityLevel.

const query = typedApi.query.System.Number
 
// in this case `getCompatibilityLevel` returns a Promise<boolean>
if (await query.isCompatible(CompatibilityLevel.BackwardsCompatible)) {
  // do your stuff, the query is compatible
} else {
  // the call is not compatible!
  // keep an eye on what you do
}
 
// Alternatively, we can await just once the compatibilityToken
const compatibilityToken = await typedApi.compatibilityToken
 
// And later on we can use it, so that `getCompatibilityLevel` is sync
if (
  query.isCompatible(CompatibilityLevel.BackwardsCompatible, compatibilityToken)
) {
  // do your stuff, the query is compatible
} else {
  // the call is not compatible!
  // keep an eye on what you do
}

As you can see, isCompatible and getCompatibilityLevel are really powerful since we can prepare for runtime upgrades seamlessly using PAPI. See this recipe for an example!

Let's continue with the rest of the fields!