Skip to content

Commit

Permalink
Merge pull request #10 from polkadot-api/compatibility
Browse files Browse the repository at this point in the history
feat: explain compatibilityToken
  • Loading branch information
voliva authored Jul 19, 2024
2 parents a3c59df + 1e97dc8 commit 7866f93
Show file tree
Hide file tree
Showing 6 changed files with 871 additions and 890 deletions.
7 changes: 5 additions & 2 deletions docs/pages/recipes/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ const nextApi = client.getTypedApi(nextDot)

function performTransfer() {
// check if we're running on the next version to run that first
if (await nextApi.tx.Balances.new_fancy_transfer.getCompatibilityLevel() >= CompatibilityLevel.BackwardsCompatible) {
if (
(await nextApi.tx.Balances.new_fancy_transfer.getCompatibilityLevel()) >=
CompatibilityLevel.BackwardsCompatible
) {
nextApi.tx.Balances.new_fancy_transfer({
dest: MultiAddress.Id("addr"),
value: 5n,
Expand All @@ -58,4 +61,4 @@ Furthermore, the runtime upgrade might happen while the dApp is running, and thi

As a note, `getCompatibilityLevel` is a function available on every interaction on the typedApi (queries, apis, constants, events, transactions). If used without any parameter it will return a `Promise<CompatibilityLevel>`, because it needs to wait for the runtime to be loaded before it can tell whether it's compatible or not.

If you have multiple `getCompatibilityLevel` checks that you don't want to wait for each one of them, you can first wait for the runtime to be loaded with `await dotApi.runtime.latest()`, and then pass this to `getCompatibilityLevel` as a paramter. This will make `getCompatibilityLevel` return synchronously.
If you have multiple `getCompatibilityLevel` checks and don't want to wait for each one individually, you can first wait for the descriptors to be loaded with `await dotApi.compatibilityToken`, and then pass this result to `getCompatibilityLevel` as a parameter, allowing it to return synchronously. See [TypedApi getCompatibilityLevel](/typed#getcompatibilitylevel) for a deeper explanation.
39 changes: 18 additions & 21 deletions docs/pages/typed.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,25 +9,15 @@ type TypedApi = {
event: EvApi
apis: RuntimeCallsApi
constants: ConstApi
runtime: RuntimeApi
compatibilityToken: Promise<CompatibilityToken>
}
```
Let's start with the simplest one, `runtime` field. It's just:
```ts
type RuntimeApi = Observable<Runtime> & {
latest: () => Promise<Runtime>
}
```
It's an observable that holds the current runtime information for that specific client, with a `latest` function to be able to wait for the runtime to load (it'll be helpful for some functions that need a `Runtime`, see [this recipe](/recipes/upgrade)).
All the other fields are a `Record<string, Record<string, ???>>`. The first index defines the pallet that we're looking for, and the second one defines which query/tx/event/api/constant are we looking for inside that pallet. Let's see, one by one, what do we find inside of it!
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
First of all, let's understand `getCompatibilityLevel` field. It's under each query/tx/event/api/constant in any runtime. After generating the descriptors (see [Codegen](/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 `getCompatibilityLevel` is under each query/tx/event/api/constant. After generating the descriptors (see [Codegen](/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:
Expand All @@ -52,32 +42,39 @@ On the other hand, a `CompatibilityLevel.BackwardsCompatible`, means that the op

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

It needs the runtime and the descriptors to be loaded, so it has two overloads, one where it will wait for them to be loaded, returning a promise, or another that returns synchronously if you already have a reference to the `Runtime` object from `typedApi.runtime.latest()`.
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.

```ts
interface GetCompatibilityLevel {
(): Promise<CompatibilityLevel>
(runtime: Runtime): CompatibilityLevel
(compatibilityToken: CompatibilityToken): CompatibilityLevel
}
```

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. We're only interested on `getCompatibilityLevel`.
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`.

```ts
const query = typedApi.query.System.Number
const runtime = await typedApi.runtime.latest() // we already learnt about it!

// Set our threshold for compatibility
const isCompatible = (level: CompatibilityLevel) =>
level >= CompatibilityLevel.BackwardsCompatible

// in this case `getCompatibilityLevel` returns a Promise<boolean>
if ((await query.getCompatibilityLevel()) >= CompatibilityLevel.BackwardsCompatible) {
if (isCompatible(await query.getCompatibilityLevel())) {
// do your stuff, the query is compatible
} else {
// the call is not compatible!
// keep an eye on what you do
}

// another option would be to use the already loaded runtime
// in this case, `getCompatibilityLevel` is sync, and returns a boolean
if (query.getCompatibilityLevel(runtime) >= CompatibilityLevel.BackwardsCompatible) {
// 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 (isCompatible(query.getCompatibilityLevel(compatibilityToken))) {
// do your stuff, the query is compatible
} else {
// the call is not compatible!
Expand Down
10 changes: 6 additions & 4 deletions docs/pages/typed/constants.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,18 @@

Constants are the simplest structure that we find inside the `TypedApi`. Constants are hard-coded key-value pairs that are embedded in the runtime metadata. In PAPI their structure is just a simple function that return its decoded value, with two alternatives. As explained in [the previous section](/typed#getcompatibilitylevel) for `getCompatibility Level`, we have two options to get the value:

- Promise-based call, without passing the runtime
- Synchronous return, passing the runtime previously awaited for
- Promise-based call, without passing the compatibility token
- Synchronous return, passing the compatibility token previously awaited for

This is because, like every other chain interaction, constants are also ran through the compatibility check in case any type has changed.

Let's use `typedApi.constants.System.Version`. See in this example how it's used:

```ts
// in this case the function is asynchronous
const versionAsync = await typedApi.constants.System.Version()

const runtime = await typedApi.runtime.latest() // we already learnt about it!
const compatibilityToken = await typedApi.compatibilityToken // we already learnt about it!
// in this case it's sync
const versionSync = typedApi.constants.System.Version(runtime)
const versionSync = typedApi.constants.System.Version(compatibilityToken)
```
10 changes: 5 additions & 5 deletions docs/pages/typed/tx.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,13 @@ const proxyTx = typedApi.tx.Proxy.proxy({

## `getEncodedData`

`getEncodedData`, instead, packs the call data (without signed extensions, of course!) as a SCALE-encoded blob. It requires a `Runtime` field (like `getCompatibilityLevel`). You can call without it, and it'll be a `Promise`-based call, or pass the runtime and it'll answer synchronously. Let's see an example:
`getEncodedData`, instead, packs the call data (without signed extensions, of course!) as a SCALE-encoded blob. It also runs the compatibility check, so it needs the runtime and descriptors loaded. As we've seen with `getCompatibilityLevel`, if you call it directly it'll be a `Promise`-based call, or you can pass in a `compatibilityToken` you've previously awaited for and it'll answer synchronously. Let's see an example:

```ts
// `getEncodedData` has this interface
interface TxCall {
(): Promise<Binary>
(runtime: Runtime): Binary
(compatibilityToken: CompatibilityToken): Binary
}

import { MultiAddress } from "@polkadot-api/descriptors"
Expand All @@ -84,9 +84,9 @@ const tx: Transaction = typedApi.tx.Balances.transfer_keep_alive({
// without argument it's async!
const encodedTx = await tx.getEncodedData()

// with runtime argument it's sync!
const runtime = await typedApi.runtime.latest()
const encodedTx = tx.getEncodedData(runtime)
// with compatibilityToken argument it's sync!
const compatibilityToken = await typedApi.compatibilityToken
const encodedTx = tx.getEncodedData(compatibilityToken)
```

## `TxOptions`
Expand Down
14 changes: 7 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,16 @@
"format": "prettier --write README.md \"docs/**/*.{js,jsx,ts,tsx,json,md}\""
},
"dependencies": {
"@radix-ui/react-tabs": "^1.0.4",
"@types/react": "latest",
"react": "latest",
"react-dom": "latest",
"typescript": "latest",
"vocs": "1.0.0-alpha.52"
"@radix-ui/react-tabs": "^1.1.0",
"@types/react": "^18.3.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"typescript": "^5.5.3",
"vocs": "1.0.0-alpha.55"
},
"devDependencies": {
"@homer0/prettier-plugin-jsdoc": "^9.0.2",
"prettier": "^3.3.2"
"prettier": "^3.3.3"
},
"prettier": {
"printWidth": 80,
Expand Down
Loading

0 comments on commit 7866f93

Please sign in to comment.