Skip to content

Commit

Permalink
feat: change isCompatible to getCompatibilityLevel (#9)
Browse files Browse the repository at this point in the history
Co-authored-by: Carlo Sala <[email protected]>
  • Loading branch information
voliva and carlosala authored Jul 17, 2024
1 parent 72da0fa commit a3c59df
Show file tree
Hide file tree
Showing 8 changed files with 72 additions and 49 deletions.
6 changes: 3 additions & 3 deletions docs/pages/recipes/upgrade.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ 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.isCompatible()) {
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 @@ -56,6 +56,6 @@ function performTransfer() {

Furthermore, the runtime upgrade might happen while the dApp is running, and this will still work without needing to redo the connection. As soon as the upgrade is received, the compatible check will work as expected and the dApp will start using the next runtime.

As a note, `isCompatible` 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<boolean>`, because it needs to wait for the runtime to be loaded before it can tell whether it's compatible or not.
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 `isCompatible` 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 `isCompatible` as a paramter. This will make `isCompatible` return synchronously.
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.
47 changes: 35 additions & 12 deletions docs/pages/typed.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,43 +25,66 @@ It's an observable that holds the current runtime information for that specific
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!
## isCompatible
## getCompatibilityLevel
First of all, let's understand `isCompatible` 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. `isCompatible` enables you to check on runtime if there was a breaking upgrade that hit your particular method.
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.
Let's see its interface, and an example.
The enum `CompatibilityLevel` defines 4 levels of compatibility:
```ts
interface IsCompatible {
(): Promise<boolean>
(runtime: Runtime): boolean
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,
}
```

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 `isCompatible`.
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.

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()`.

```ts
interface GetCompatibilityLevel {
(): Promise<CompatibilityLevel>
(runtime: Runtime): 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`.

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

// in this case `isCompatible` returns a Promise<boolean>
if (await query.isCompatible()) {
// in this case `getCompatibilityLevel` returns a Promise<boolean>
if ((await query.getCompatibilityLevel()) >= CompatibilityLevel.BackwardsCompatible) {
// 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, `isCompatible` is sync, and returns a boolean
if (query.isCompatible(runtime)) {
// in this case, `getCompatibilityLevel` is sync, and returns a boolean
if (query.getCompatibilityLevel(runtime) >= CompatibilityLevel.BackwardsCompatible) {
// 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` is really powerful since we can prepare for runtime upgrades seamlessly using PAPI. See [this recipe](/recipes/upgrade) for an example!
As you can see, `getCompatibilityLevel` is really powerful since we can prepare for runtime upgrades seamlessly using PAPI. See [this recipe](/recipes/upgrade) for an example!

Let's continue with the rest of the fields!
2 changes: 1 addition & 1 deletion docs/pages/typed/apis.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ type CallOptions = Partial<{
}>
interface RuntimeCall<Args, Payload> {
(...args: [...Args, options?: CallOptions]): Promise<Payload>
isCompatible: IsCompatible
getCompatibilityLevel: GetCompatibilityLevel
}
```

Expand Down
2 changes: 1 addition & 1 deletion docs/pages/typed/constants.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Constants

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) for `isCompatible`, we have two options to get the value:
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
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/typed/events.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,11 +32,11 @@ type EvClient<T> = {
pull: EvPull<T>
watch: EvWatch<T>
filter: EvFilter<T>
isCompatible: IsCompatible
getCompatibilityLevel: GetCompatibilityLevel
}
```
We already learnt about `isCompatible`, let's see step by step the other methods:
We already learnt about `getCompatibilityLevel`, let's see step by step the other methods:
## Pull
Expand Down
4 changes: 2 additions & 2 deletions docs/pages/typed/queries.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type CallOptions = Partial<{
}>

type StorageEntryWithoutKeys<Payload> = {
isCompatible: IsCompatible
getCompatibilityLevel: GetCompatibilityLevel
getValue: (options?: CallOptions) => Promise<Payload>
watchValue: (bestOrFinalized?: "best" | "finalized") => Observable<Payload>
}
Expand All @@ -29,7 +29,7 @@ Similarly, we'll use the example of `System.Account` query (it returns the infor
```ts
type StorageEntryWithKeys<Args, Payload> = {
isCompatible: IsCompatible
getCompatibilityLevel: GetCompatibilityLevel
getValue: (...args: [...Args, options?: CallOptions]) => Promise<Payload>
watchValue: (
...args: [...Args, bestOrFinalized?: "best" | "finalized"]
Expand Down
6 changes: 3 additions & 3 deletions docs/pages/typed/tx.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Preparing, signing, and broadcasting extrinsics is one of the main purposes of p
```ts
interface TxEntry<Arg> {
(data: Arg): Transaction
isCompatible: IsCompatible
getCompatibilityLevel: GetCompatibilityLevel
}

type Transaction = {
Expand All @@ -21,7 +21,7 @@ type Transaction = {
}
```
[We already know how `isCompatible` works](/typed#iscompatible). In order to get a `Transaction` object, we need to pass all arguments required by the extrinsic. Let's see two examples, `Balances.transfer_keep_alive` and `NominationPools.claim_payout`.
[We already know how `getCompatibilityLevel` works](/typed#getcompatibilitylevel). In order to get a `Transaction` object, we need to pass all arguments required by the extrinsic. Let's see two examples, `Balances.transfer_keep_alive` and `NominationPools.claim_payout`.
The case of `claim_payout` is the simplest one, since it doesn't take any arguments. Simply as
Expand Down Expand Up @@ -65,7 +65,7 @@ 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 `isCompatible`). 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 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:

```ts
// `getEncodedData` has this interface
Expand Down
50 changes: 25 additions & 25 deletions vocs.config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,31 @@ export default defineConfig({
text: "Getting Started",
link: "/getting-started",
},
{
text: "Providers",
link: "/providers",
},
{
text: "Codegen",
link: "/codegen",
},
{
text: "Types",
link: "/types",
},
{
text: "Signers",
link: "/signers",
},
{
text: "Recipes",
items: [
{
text: "Prepare for runtime upgrade",
link: "/recipes/upgrade",
},
],
},
{
text: "Top-level client",
items: [
Expand Down Expand Up @@ -53,31 +78,6 @@ export default defineConfig({
},
],
},
{
text: "Providers",
link: "/providers",
},
{
text: "Codegen",
link: "/codegen",
},
{
text: "Types",
link: "/types",
},
{
text: "Signers",
link: "/signers",
},
{
text: "Recipes",
items: [
{
text: "Prepare for runtime upgrade",
link: "/recipes/upgrade",
},
],
},
{
text: "Examples",
items: [
Expand Down

0 comments on commit a3c59df

Please sign in to comment.