# Polkadot-API > Next-Gen TS API to interact with Polkadot-based chains ## Top-level client `PolkadotClient` interface shapes the top-level API for `polkadot-api`. Once we get a client using `createClient` function, we'll find the following: ### Create a client In order to create a client, you only need to have a [provider](/providers). Optionally, you can pass `getMetadata` and `setMetadata` functions, useful for metadata caching. You can [find a recipe in the docs](/recipes/metadata-caching) on how to use this! ```ts twoslash // [!include ~/snippets/startSm.ts] import { getSmProvider } from "polkadot-api/sm-provider" const provider = getSmProvider(() => smoldot.addChain({ chainSpec: "" })) //---cut--- import { createClient } from "polkadot-api" const client = createClient(provider) ``` ### `PolkadotClient` Let's dive into each part of the `PolkadotClient` interface. #### `getChainSpecData` Type: `() => Promise<{name: string; genesisHash: string; properties: any}>{:ts}` Retrieve the ChainSpecData directly as it comes from the [JSON-RPC spec](https://paritytech.github.io/json-rpc-interface-spec/api/chainSpec.html). The consumer shouldn't make assumptions on this data, as it might change from session to session and it is not strictly typed. #### `getMetadata$` Type: `(atBlock: HexString) => Observable{:ts}` Retrieves the most modern version of the metadata for a given block. That is, if metadata versions 14, 15 and 16 are available, metadata version 16 will be returned. The observable will emit once, and immediately complete. #### `getMetadata` Type: `(atBlock: HexString, signal?: AbortSignal) => Promise{:ts}` Retrieves the most modern version of the metadata for a given block. That is, if metadata versions 14, 15 and 16 are available, metadata version 16 will be returned. The function accepts an abort signal to make the promise abortable. #### `finalizedBlock$` Type: `Observable{:ts}` This Observable emits [`BlockInfo`](/types#blockinfo) for every new finalized block. It is multicast and stateful. For a new subscription, it will synchronously repeat its latest known state. #### `getFinalizedBlock` Type: `() => Promise{:ts}` This function returns [`BlockInfo`](/types#blockinfo) for the latest known finalized block. #### `bestBlocks$` Type: `Observable{:ts}` This Observable emits an array of [`BlockInfo`](/types#blockinfo), being the first element the latest known best block, and the last element the latest known finalized block. The following guarantees apply: * It is a multicast and stateful observable. For a new subscription, it will synchronously repeat its latest known state. * In every emission, the array will have length of at least `1{:ts}`. * The emitted arrays are immutable data structures; i.e. a new array is emitted at every event but the reference to its children are stable if the children didn't change. #### `getBestBlocks` Type: `() => Promise{:ts}` This function returns the latest known state of [`bestBlocks$`](/client#bestblocks). It holds the same guarantees. #### `blocks$` Type: `Observable{:ts}` This observable emits [`BlockInfo`](/types#blockinfo) for every block the client discovers. This observable follows the following rules: * Right after subscription, the observable will emit synchronously the latest finalized block and all its known descendants. * The emissions are "continuous"; i.e. for every block emitted it is guaranteed that the parent of it has already been emitted. * The Observable will complete if the continuity of the blocks cannot be guaranteed. #### `hodlBlock` Type: `(blockHash: HexString) => () => void{:ts}` This function prevents the block from being unpinned. Returns a function that releases the hold, allowing the block to be unpinned once no other operations remain. ```ts twoslash import type { PolkadotClient } from "polkadot-api" const client: PolkadotClient = null as any // ---cut--- const finalized = await client.getFinalizedBlock() const releaseFn = client.hodlBlock(finalized.hash) // the block will not be released! setTimeout(async () => { const body = await client.getBlockBody(finalized.hash) releaseFn() }, 100_000) ``` #### `getBlockBody$` Type: `(hash: HexString) => Observable{:ts}` Retrieves the body of the block given by its hash. Each entry is a SCALE-encoded extrinsic as `Uint8Array`. The observable will emit once, and immediately complete. #### `getBlockBody` Type: `(hash: HexString, signal?: AbortSignal) => Promise{:ts}` Retrieves the body of the block given by its hash. Each entry is a SCALE-encoded extrinsic as `Uint8Array`. #### `getBlockHeader$` Type: `(hash: HexString) => Observable{:ts}` Retrieves the decoded header of the block given by its hash. Use `getFinalizedBlock()` or `getBestBlocks()` to obtain a block hash. The observable will emit once, and immediately complete. #### `getBlockHeader` Type: `(hash: HexString, signal?: AbortSignal) => Promise{:ts}` Retrieves the decoded header of the block given by its hash. #### `submit` Type: `(transaction: Uint8Array, at?: HexString) => Promise{:ts}` Broadcasts a transaction. The promise will resolve when the transaction is found in a finalized block, and will reject if the transaction is deemed invalid (either before or after broadcasting). This function follows the same logic as the [transaction API `submitAndWatch` function](/typed/tx#signandsubmit), find more information about it there. #### `submitAndWatch` Type: `(transaction: Uint8Array, at?: HexString) => Observable{:ts}` Broadcasts a transaction. This function follows the same logic as the [transaction API `signSubmitAndWatch` function](/typed/tx#signsubmitandwatch), find more information about the emitted events there. #### `getTypedApi` Type: `(descriptors: ChainDefinition) => TypedApi{:ts}` The Typed API is the entry point to the runtime-specific interactions with Polkadot-API. You can do storage queries, create transactions, run view functions, etc! [`TypedApi` has its own documentation](/typed). Check it out! #### `getUnsafeApi` Type: `() => UnsafeApi{:ts}` The Unsafe API is another way to access the specific interactions of the chain you're connected to. Nevertheless, it has its own caveats, read the docs before using it! [`UnsafeApi` has its own documentation](/unsafe). Check it out! #### `rawQuery` Type: ```ts rawQuery: ( storageKey: HexString | string, options?: { at: "best" | "finalized" | HexString; signal?: AbortSignal }, ) => Promise ``` This function allows to access the raw storage value of a given key. It'll return the encoded value, or `null{:ts}` if the value is not found. Parameters: * `storageKey`: it can be both an encoded key (as `HexString`) or a well-known Substrate key (such as `":code"{:ts}`). * `options`: Optionally pass `at` (block hash, `"finalized"{:ts}` (default), or `"best"{:ts}`) and/or `signal`, to make the promise abortable. #### `destroy` Type: `() => void{:ts}` This function will unfollow the provider, error every subscription pending and disconnect from the provider. After calling it, nothing else can be done with the client. #### `_request` Type: `(method: string, params: Array) => Promise{:ts}` This function allows to call any RPC endpoint through the JSON-RPC provider. This method is not typed by itself, but you can add your own types. It is meant as an escape-hatch for chain-specific nodes, you should use all the other APIs for regular interactions. For example, with `system_version`: ```ts twoslash import type { PolkadotClient } from "polkadot-api" const client: PolkadotClient = null as any // ---cut--- const nodeVersion = await client._request("system_version", []) // ^? ``` #### `_subscribe` Type: ```ts _subscribe: = any[]>( method: string, unsubscribeMethod: string, params: Params, ) => Observable ``` This function allows to call subscription endpoints through the JSON-RPC provider. This API is meant as an escape hatch, and its stability is not guaranteed across minor versions. ## Codegen Technically, to connect to a chain, all you need is just the [provider](/providers). But to interact with it, you need to know the list of storage, runtime, and transaction calls and their types. During runtime, the library can request the metadata for the chain it's connected to, and from this, it generates all the codecs to interact with it. But as a developer, you need to get that information beforehand. Polkadot-API has a CLI that downloads the metadata for a chain and then uses that metadata to generate all the type descriptors. ### `papi add` `papi add` registers a new chain. It requires a key, which is the name of the constant the codegen will create, and a source (`-f`, `-w`, `-c`, `-n`, or `--wasm`). The command download the fresh metadata for that chain and stores this information for later use into a `.papi` folder, with a configuration file `polkadot-api.json` and the metadata file `${key}.scale`. You can add as many chains as you want, but each has to have a unique `key` (which must be a valid JS variable name). ```sh > npx papi add --help Usage: polkadot-api add [options] Add a new chain spec to the list Arguments: key Key identifier for the chain spec Options: --config Source for the config file -f, --file Source from metadata encoded file -w, --wsUrl Source from websocket url -c, --chainSpec Source from chain spec file -n, --name Source from a well-known chain (choices: "kusama", "paseo", "polkadot", "polkadot_collectives", "westend", [...]") --wasm Source from runtime wasm file --at Only for -w/--wsUrl. Fetch the metadata for a specific block or hash --no-persist Do not persist the metadata as a file --skip-codegen Skip running codegen after adding -h, --help display help for command ``` `papi add` by default runs codegen automatically. If you want to add multiple chains without having to rerun codegen, you can use the flag `--skip-codegen`, and then run `papi generate` command once you want to run the codegen. ```sh npx papi generate # `generate` is the default command, so you can just run npx papi ``` :::info It's recommended to add `papi` to the `postinstall` script in package.json to have it automatically generate the code after installation: ```js { // ... "scripts": { // ... "postinstall": "papi" } } ``` ::: ### `papi generate` (or `papi`) The code is generated into a [local package](https://docs.npmjs.com/cli/v9/configuring-npm/package-json/#local-paths) located in `.papi/descriptors`, which gets installed as a regular `@polkadot-api/descriptors` node modules package. The folder `.papi` should be added to your source control repository. PAPI already handles the ignored files, you don't need to ignore anything. When the metadata is updated, the codegen will update the generated code and also the package version, to have package managers update the `@polkadot-api/descriptors` package. :::info If you're using yarn v1, you might need to run `yarn --force` after a codegen for it to detect the change. ::: ### Descriptors content The generated code contains all of the types extracted from the metadata of all chains: * For every pallet: * Storage queries * Transactions * Events * Errors * Constants * View Functions * Every runtime call These are consumed by `getTypedApi()` or `getUnsafeApi()`, which allows the IDE to reference any of these calls with autocompletion, etc. At runtime, it also contains a representation of the type for each of these calls, so that it can detect incompatibilities with the chain it's connected to. The types are anonymous (they don't have a name in the metadata), but PolkadotAPI has a directory of well-known types for some of the most widely used Enums. If a chain is using one of these well-known types, it's also generated and exported. :::warning It's important to know that the descriptors exported by the codegen **should be treated as a black box**. When importing the descriptors of a chain, the type might not actually match what's in runtime, and its internals are subject to change. It has to be treated as an opaque token, passing it directly to `.getTypedApi()`. ::: ### `papi update` `papi update` refreshes the stored metadata for one or more previously registered chains. It re-downloads the metadata from the original source (WebSocket, chain-spec, or well-known chain) and overwrites the persisted `${key}.scale` file. For those entries generated from WASM, or directly from a metadata file, it is a no-op. ```sh > npx papi update --help Usage: polkadot-api update [options] [keys] Update the metadata files and generate descriptor files Arguments: keys Keys of the metadata files to update, separated by commas. Leave empty for all Options: --config Source for the config file --skip-codegen Skip running codegen after adding -h, --help display help for command ``` If you provide one or more `keys` (comma-separated), only those chains are updated. If you omit `keys`, `papi update` will attempt to update all chains. After it, [papi generate](/codegen#papi-generate-or-papi) runs by default. Pass `--skip-codegen` to avoid this step. ### Usage Import from `@polkadot-api/descriptors` every chain and type that you need, then use it through `getTypedApi()`. ```ts twoslash // [!include ~/snippets/startSm.ts] import { getSmProvider } from "polkadot-api/sm-provider" import { chainSpec } from "polkadot-api/chains/polkadot" import { createClient } from "polkadot-api" // ---cut--- import { dot, MultiAddress } from "@polkadot-api/descriptors" const dotClient = createClient( getSmProvider(() => smoldot.addChain({ chainSpec })), ) const dotApi = dotClient.getTypedApi(dot) const tx = dotApi.tx.Balances.transfer_keep_alive({ dest: MultiAddress.Id("SS58ADDR"), value: 10n, }) const encodedData = await tx.getEncodedData() ``` :::info `getTypedApi` has nearly no cost at runtime, so it can be safely called many times. ::: ### Whitelist By default, PAPI generates the descriptors for every possible interaction for each chain. These are 50\~150KB files that are lazy-loaded, and it's possible to optimize them by whitelisting which calls you'll be using in your dApp. ```ts twoslash // .papi/whitelist.ts import type { WhitelistEntry } from "@polkadot-api/descriptors" // the export name has to *EXACTLY* match `whitelist` export const whitelist: WhitelistEntry[] = [ // this will get all calls, queries, and consts inside Balances pallet "*.Balances", // this just a specific tx "tx.PolkadotXcm.transfer_assets", // all queries inside system pallet "query.System.*", // all constants "const.*", ] ``` The whitelist file should be placed in `.papi/whitelist.ts` from the root of your npm package. For projects that use more than one chain definition, you can choose to have different whitelisted entries per each key by using `WhitelistEntriesByChain`. This is an object interface which takes each chain for each key, and also has a "common" key `"*"` for a global one. The whitelist is additive: Each chain will have the interactions of their own key plus the common key. The `WhitelistEntriesByChain` accepts every interaction from every chain. The descriptors package also exports a `WhitelistEntry` helper type for each defined chain that can be used to help better narrow down those types. In the following example we have two chains defined: `dot` for Polkadot Asset Hub and `dotRelay` for Polkadot Relay Chain. ```ts twoslash import type { WhitelistEntriesByChain, DotRelayWhitelistEntry, DotWhitelistEntry, } from "@polkadot-api/descriptors" export const whitelist: WhitelistEntriesByChain = { // Applies to every chain "*": [ "query.System.Account", "tx.Balances.transfer_keep_alive", "tx.Balances.transfer_allow_death", ], dotRelay: [ "tx.XcmPallet.transfer_assets", // Optional. Typescript trick to get better narrowing down. ] satisfies DotRelayWhitelistEntry[], dot: ["tx.PolkadotXcm.transfer_assets"] satisfies DotWhitelistEntry[], } ``` Any chain not specified in the object will just inherit the whitelist from the common key `*`. If neither the common key or the chain key is specified, it will result in an empty chain with no interactions. A full working example of a dApp with whitelist could be found at [our repo](https://github.com/polkadot-api/polkadot-api/tree/main/examples/vite/.papi/whitelist.ts). ### Codegen without descriptors package PAPI is designed to create a descriptors package, `@polkadot-api/descriptors`, allowing it to be imported like any other package. To ensure compatibility with most package managers and bundlers, the CLI automatically adds this package as a dependency to your project. For advanced use cases, you can configure the CLI to perform only the code generation without installing it as a dependency. To do this, add the following option to your configuration file, `.papi/polkadot-api.json`: ```json { "descriptorPath": …, "entries": { … }, "options": { "noDescriptorsPackage": true } } ``` :::info You can also modify the `"descriptorPath"` property to specify a different path for generating the descriptors. ::: ## Getting Started ### Community templates If you want to quickly get a project up and running, check out the following community templates: * [PAPI Starter Template](https://github.com/polkadot-api/starter-template): React, shadcn/ui, PolkaHub, React Query * [Create Polkadot dApp](https://github.com/paritytech/create-polkadot-dapp): React, tailwind, ReactiveDot, DOTConnect * [Create Dot App](https://github.com/preschian/create-dot-app): Many different template stacks, including React or Vue. * [POP CLI](https://github.com/r0gue-io/pop-cli): Ink! smart contracts with frontend integration. ### Installation Start by installing `polkadot-api`, and download the latest metadata from the chain you want to connect to and generate the types: :::code-group ```sh [npm] npm i polkadot-api # `papi add` is the command # `dot` is the name we're giving to this chain (can be any JS variable name) # `-n polkadot` specifies to download the metadata from the well-known chain polkadot npx papi add dot -n polkadot # Wait for the latest metadata to download, then generate the types: npx papi ``` ```sh [pnpm] pnpm add polkadot-api # `papi add` is the command # `dot` is the name we're giving to this chain (can be any JS variable name) # `-n polkadot` specifies to download the metadata from the well-known chain polkadot pnpm papi add dot -n polkadot # Wait for the latest metadata to download, then generate the types: pnpm papi ``` ```sh [bun] bun add polkadot-api # `papi add` is the command # `dot` is the name we're giving to this chain (can be any JS variable name) # `-n polkadot` specifies to download the metadata from the well-known chain polkadot bun papi add dot -n polkadot # Wait for the latest metadata to download, then generate the types: bun papi ``` ::: :::info It's a really good idea to add papi to the "postinstall" script in package.json to automate generating the types after installation. ::: Now you can create a `PolkadotClient` instance with a [provider](/providers) of your choice and start interacting with the API: ### 1. Create the provider and Start the client :::code-group ```typescript twoslash [Smoldot] // [!include ~/snippets/gettingStarted.ts:import] // [!include ~/snippets/gettingStarted.ts:smoldot] ``` ```typescript twoslash [WebSocket] // [!include ~/snippets/gettingStarted.ts:import] // [!include ~/snippets/gettingStarted.ts:websocket] ``` ::: ### 2. Start consuming the client! ```typescript twoslash // [!include ~/snippets/gettingStarted.ts:import] // [!include ~/snippets/gettingStarted.ts:smoldot] // ---cut--- // [!include ~/snippets/gettingStarted.ts:usage] ``` ### 3. Discover our documentation! To continue learning about PAPI, we recommend reading about: 1. [CLI & Codegen](/codegen). Fully grasp how to generate descriptors, and why does it matter for PAPI. 2. [Providers](/providers). Discover all options that our providers offer! You'll need a provider to proceed with the client. 3. [Client](/client). Get information for blocks, metadata, etc. Essentially, everything generic that does not depend on the runtime itself. For runtime specifics, keep reading... 4. [Typed API](/typed). The cherry on top of the cake! Interact with the network: transactions, storage entries, runtime apis, and others! 5. [Signers](/signers). You'll find here how PAPI abstracts away signers, and how can be used to sign transactions! ## Ink! Polkadot-API adds typescript definitions for ink! contracts, as well as utilities to encode and decode messages, contract storage and events. The ink client with types support can be found at `polkadot-api/ink`. It's chain-agnostic, meaning it doesn't dictate which runtime APIs, storage or transactions are required. For a more integrated ink! support with easier development, see [Ink! SDK](/sdks/ink-sdk). ### Codegen The first step is to generate the types from the contract metadata. The Polkadot-API CLI has a command specific for ink: ```sh > pnpm papi ink --help Usage: polkadot-api ink [options] [command] Add, update or remove ink contracts Options: -h, --help display help for command Commands: add [options] Add or update an ink contract remove [options] Remove an ink contract help [command] display help for command ``` So to generate the types for a contract, run the `ink add` command: ```sh > pnpm papi ink add "path to .contract or .json metadata file" ``` This will add the contract to the `.papi` subfolder, and generate the type descriptors for the ink! contract. These can be found in `@polkadot-api/descriptors` within an object named "contracts", and are keyed by contract name. The contract name by default is the one specified in the contract's metadata, but if needed it can be overriden with the `--key` parameter in `ink add`. The generated code contains all the types, and also the required info from the metadata to encode and decode values. :::warning The descriptors exported from `@polkadot-api/descriptors` must always be treated as black boxes, passed directly as inputs to the ink client. The type structure or the internals is subject to change without a major version bump. ::: ### Ink! Client Start by creating the ink client from `polkadot-api/ink`. In the following example we will use a psp22 contract deployed on test AlephZero: ```ts // Having added test AlephZero chain with `papi add` import { contracts, testAzero } from "@polkadot-api/descriptors" import { getInkClient } from "polkadot-api/ink" import { createClient } from "polkadot-api" import { getWsProvider } from "polkadot-api/ws" const client = createClient( getWsProvider("wss://aleph-zero-testnet-rpc.dwellir.com"), ) // Create a psp22 ink! client const psp22Client = getInkClient(contracts.psp22) // typedAPI for test AlephZero const typedApi = client.getTypedApi(testAzero) ``` #### Deploy contract Use the `inkClient.constructor(label: string)` to get the functions to encode and decode constructor messages. For example, a dry-run of a psp22 contract deployment: ```ts const code = ...; // read the contract wasm to deploy as a Uint8Array based on your JS runtime. // Takes in the constructor name (TS suggests the ones available) const psp22Constructor = psp22Client.constructor("new") // Encode the data for that constructor, also with full TS support const constructorData = psp22Constructor.encode({ supply: 100_000_000_000_000n, name: "PAPI token", symbol: "PAPI", decimals: 9, }) // Generate a random salt - For demo purposes using a hardcoded ones. const salt = Binary.fromText("Salt 100") // Perform the call to the RuntimeAPI to dry-run a contract deployment. const response = await typedApi.apis.ContractsApi.instantiate( ADDRESS.alice, // Origin 0n, // Value undefined, // GasLimit undefined, // StorageDepositLimit Enum("Upload", code), constructorData, salt, ) if (response.result.success) { const contractAddress = response.result.value.account_id console.log("Resulting address", contractAddress) // Events come in decoded and typed const events = psp22Client.event.filter(contractAddress, response.events) console.log("events", events) // The response message can also be decoded, and it's also fully typed const responseMessage = psp22Constructor.decode(response.result.value.result) console.log("Result response", responseMessage) } else { console.log("dry run failed") } ``` The same methods can be used to perform a deployment transaction, but through `typedApi.tx.Contracts.instantiate_with_code`. #### Send Message Similarly, the payload for contract messages can be encoded and decoded through the `inkClient.message(label: string)`. For example, to dry-run a PSP22::increase\_allowance message: ```ts // Takes in the message name (TS suggests the ones available) const increaseAllowance = psp22Client.message("PSP22::increase_allowance") // Encode the data for that message, also with full TS support const messageData = increaseAllowance.encode({ delta_value: 100_000_000n, spender: ADDRESS.bob, }) const response = await typedApi.apis.ContractsApi.call( ADDRESS.alice, // Origin ADDRESS.psp22, // Contract address 0n, // Value undefined, // GasLimit undefined, // StorageDepositLimit messageData, ) if (response.result.success) { // Events come in decoded and typed const events = psp22Client.event.filter(ADDRESS.psp22, response.events) console.log("events", events) // The response message can also be decoded, and it's also fully typed const responseMessage = increaseAllowance.decode(response.result.value) console.log("Result response", responseMessage) } else { console.log("dry run failed") } ``` #### Events Every message or contract deployment generates `SystemEvent`s that are available through the regular RuntimeApi's `response.events` or through `transactionResult.events`. These can be filtered using the [Events Filter API](/typed/events#filter). Ink! has its own SystemEvent, `Contracts.ContractEmitted` (or `Revive.ContractEmitted`) which can contain events emitted within the contract, but the payload is SCALE encoded, with a type definition declared in the metadata. Polkadot-API's inkClient offers an API to decode a specific ink! event, and also to filter all the `Contracts.ContractEmitted` events from a list and return them already decoded: ```ts type InkEvent = { data: Uint8Array } type SystemEvent = { type: string; value: unknown } interface InkEventInterface { // For v5 events, we need the event's `signatureTopic` to decode it. decode: (value: InkEvent, signatureTopic: string) => E // For v4 events, the index within the metadata is used instead. decode: (value: InkEvent) => E filter: ( address: string, // Contract address events: Array< // Accepts events coming from Runtime-APIs. | { event: SystemEvent; topics: Uint8Array[] } // Also accepts events coming from transactions. | (SystemEvent & { topics: Uint8Array[] }) >, ) => Array } ``` See the previous examples, as they also include event filtering. #### Storage The `inkClient` also offers an API to encode storage keys and decode their result. The storage of a contract is defined through a StorageLayout in the contract's metadata. Depending on the type of each value, the values can be accessed directly from the storage root, or they might need a separate call. For instance, a storage layout that's just nested structs, will have all the contract's storage accessible directly from the root, and the decoder will return a regular JS object. But if somewhere inside there's a Vector, a HashMap, or other unbounded structures, then that value is not accessible from the root, and must be queried separately. To get the codecs for a specific storage query, use `inkClient.storage()`: ```ts interface StorageCodecs { // key arg can be omitted if that storage entry takes no keys encode: (key: K) => Uint8Array decode: (data: Uint8Array) => V } interface StorageInterface { // path can be omitted to target the root storage (equivalent to path = "") storage:

>( path: P, ) => StorageCodecs } ``` For example, to read the root storage of psp22: ```ts const psp22Root = psp22Client.storage() const response = await typedApi.apis.ContractsApi.get_storage( ADDRESS.psp22, // Root doesn't need key, so we just encode without any argument. psp22Root.encode(), ) if (response.success && response.value) { const decoded = psp22Root.decode(response.value) console.log("storage", decoded) // The values are typed console.log("decimals", decoded.decimals) console.log("total supply", decoded.data.total_supply) // Note that `decoded.data.balances` is not defined (nor included in the type), because // even though it's defined in the layout as a property of `data.balances`, it's a HashMap, // so it's not returned through the root storage key. } else { console.log("error", response.value) } ``` And to get the balances for a particular address, we have to do a separate storage query: ```ts // Pass in the path to the storage root to query (TS also autosuggest the possible values) const psp22Balances = psp22Client.storage("data.balances") const response = await typedApi.apis.ContractsApi.get_storage( ADDRESS.psp22, // Balances is a HashMap, needs a key, which is the address to get the balance for psp22Balances.encode(ADDRESS.alice), ) if (response.success && response.value) { const decoded = psp22Balances.decode(response.value) console.log("alice balance", decoded) } else { console.log("error", response.value) } ``` #### Attributes Ink! adds some attributes to some of the messages or constructors to indicate various features: * `default`: Whether a message or constructor should be treated as the default. * `payable`: Whether a message or constructor accepts a `value` parameter which will transfer tokens from the origin signer to the contract's address. * `mutates`: Whether a message performs a change to the storage. The ink client exposes these properties on the message and constructor objects: ```ts const psp22Constructor = psp22Client.constructor("new") console.log(psp22Constructor.attributes) const increaseAllowance = psp22Client.message("PSP22::increase_allowance") console.log(increaseAllowance.attributes) ``` Additionally, if the contract does have a "default" message or constructor specified, then the id of that can also be found in the client itself: ```ts const defaultConstructor = psp22Client.defaultConstructor ? psp22Client.message(psp22Client.defaultConstructor) : null const defaultMessage = psp22Client.defaultMessage ? psp22Client.message(psp22Client.defaultMessage) : null ``` :::note For better typescript support, if you're working with just one contract that has a defaultConstructor, then the types will already be non-nullable. In that case you shouldn't need the nullable ternary. ::: ## Offline API The rationale behind the offline api is to allow consumers to do certain actions with PAPI without the need of a provider. As expected, this client is more limited than the regular [`PolkadotClient`](/client). Let's see an example first of all: ```typescript import { getOfflineApi } from "polkadot-api" import { dotDescriptors } from "@polkadot-api/descriptors" // you'll need them! const offline = await getOfflineApi(dotDescriptors) // it is async! // metadata constants can be easily accessed const prefix = offline.constants.System.SS58Prefix // directly the value; e.g. `0` const { spec_name, spec_version } = offline.constants.System.Version // transactions can be created and signed const tx = offline.tx.Balances.transfer_keep_alive({ dest: MultiAddress.Id(myAddr), value: amount, }) tx.encodedData // we have the encoded callData tx.decodedCall // and the decodedCall const tx2 = offline.tx.Utility.batch({ calls: [ offline.tx.System.remark({ remark: Binary.fromText("HELLO!") }).decodedCall, tx.decodedCall, ], }) // we can sign txs, but we need to add more stuff than usual! const signedTx = await tx2.sign(signer, { nonce: 24, // nonce is always compulsory mortality: { mortal: false }, // and mortality! }) ``` ### Constants Constants can be accessed easily having the metadata. `offline.constants.Pallet.Entry` already gives the decoded value. ### Transactions This is the main usecase of offline api. It allows to create and encode transactions, and even sign them. The transactions are created in the exact same way as in the regular API ([see docs](/typed/tx)). Nevertheless, only a subset of the fields are exposed: * `decodedCall`: it enables to get the *PAPI decoded* transaction. It is helpful to create other txs that require them as a parameter (e.g. `Utility.batch`). * `encodedData`: a `Uint8Array` with the encoded call data. * `sign`: it takes the same arguments as the regular API, but there are two compulsory signed extensions: * `nonce`: nonce cannot be retrieved anymore from the chain, and therefore has to be passed * `mortality`: transactions can be signed either mortal or immortal. In case the tx were to be mortal, the block information has to be passed as well. ```typescript type Mortality = | { mortal: false } | { mortal: true period: number startAtBlock: { height: number; hash: HexString } } ``` ## Requirements PAPI is designed to work flawlessly with almost any Polkadot-like chain. Even though, we have some requirements that the chain and environment have to fulfill. ### Environment PAPI supports all modern browsers (e.g. Chrome, Firefox, Safari...), besides Node JS, Bun, etc. Some environments have particularities: #### NodeJS PAPI is developed using the latest NodeJS LTS (currently `22.x`). The minimum required version is `20.19.0`. We recommend using the latest NodeJS LTS. #### Bun In case you are using `bun` as your Javascript runtime, then Papi will work flawlessly with it! Make sure to use a fairly recent version. ### Typescript In order for PAPI to be imported correctly, and get all the types, the minimum working version of `typescript` is `5.2`. PAPI is always developed using the latest `typescript` stable version. ### Chain #### Runtime The most important thing to take into account is that the runtime needs to have a Runtime Metadata version `>=14`. We don't support any chain below that. Besides that, Polkadot-API requires runtimes to implement some basic runtime calls. They are generally implemented in all chains developed using FRAME: * In order to get the metadata, it needs `Metadata_metadata_versions` and `Metadata_metadata_at_version`. If they are not present, then `Metadata_metadata` needs to be there and answer a `v14` Metadata. * To create and broadcast transactions, Polkadot-API needs `AccountNonceApi_account_nonce` and `TaggedTransactionQueue_validate_transaction`. To estimate the fees, it also requires `TransactionPaymentApi_query_info`. In order to create transactions as well, the following constant is required: * `System.Version`, having `spec_version` and `transaction_version` fields. ## Static APIs The `StaticApis` object provides **synchronous** access to runtime-specific operations that would otherwise be asynchronous. It targets a specific runtime version (identified by the block's code hash), making it ideal for scenarios where you need multiple synchronous operations without repeated async calls. ### Getting StaticApis You obtain a `StaticApis` instance by calling `getStaticApis()` on a `TypedApi`: ```ts const typedApi = client.getTypedApi(dot) // Get StaticApis for the finalized block (default) const staticApis = await typedApi.getStaticApis() // Or target a specific block const staticApis = await typedApi.getStaticApis({ at: "best" }) const staticApis = await typedApi.getStaticApis({ at: "0x1234..." }) // block hash ``` The promise resolves once the runtime metadata for that block is loaded. After that, all operations on `staticApis` are synchronous. ### Interface ```ts type StaticApis = { id: string constants: ConstantsApi tx: TxApi query: QueryApi txFromCallData: (callData: Uint8Array) => Transaction compat: CompatApi } ``` ### `id` A unique identifier for the runtime. This can be useful for caching or comparing runtime versions. ```ts const staticApis = await typedApi.getStaticApis() console.log("Runtime ID:", staticApis.id) ``` ### `constants` Access runtime constants synchronously. Unlike `typedApi.constants.Pallet.Constant()` which returns a `Promise`, static APIs return the value directly: ```ts // Async version (TypedApi) const version = await typedApi.constants.System.Version() // Sync version (StaticApis) const staticApis = await typedApi.getStaticApis() const version = staticApis.constants.System.Version ``` This is particularly useful when you need to read multiple constants without awaiting each one: ```ts const staticApis = await typedApi.getStaticApis() // All synchronous! const version = staticApis.constants.System.Version const ss58Prefix = staticApis.constants.System.SS58Prefix const existentialDeposit = staticApis.constants.Balances.ExistentialDeposit ``` ### `tx` Build transaction call data synchronously. This is useful when you need to encode transactions without signing them. ```ts const staticApis = await typedApi.getStaticApis() // Get the call data for a transaction const callData: Uint8Array = staticApis.tx.Balances.transfer_keep_alive({ dest: MultiAddress.Id("5GrwvaEF..."), value: 10n ** 10n, }).getCallData() ``` The `tx` API mirrors the structure of `typedApi.tx`, but instead of returning a full `Transaction` object, it returns an object with a `getCallData()` method that synchronously produces the encoded call data. ### `query` Access storage key encoding synchronously. This is useful when you need to compute storage keys without making RPC calls. ```ts const staticApis = await typedApi.getStaticApis() // Get the storage key for an account const storageKey = staticApis.query.System.Account.getKey( "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY", ) ``` ### `txFromCallData` Decode raw call data back into a `Transaction` object synchronously: ```ts const staticApis = await typedApi.getStaticApis() // Encode a call const callData = staticApis.tx.System.remark({ remark: Binary.fromText("Hello!"), }).getCallData() // Decode it back const tx = staticApis.txFromCallData(callData) console.log(tx.decodedCall) // { type: "System", value: { type: "remark", value: { remark: Uint8Array } } } ``` This is the synchronous equivalent of `typedApi.txFromCallData()`. It's useful when working with call data from external sources (like multisig proposals or governance referenda). ### `compat` Check compatibility of runtime interactions. After generating the descriptors (see [Codegen](/codegen) section), you have a typed interface to every interaction with the chain. However, breaking runtime upgrades might happen between development and the runtime execution of your app. The `compat` API enables you to check at runtime if there was a breaking upgrade that affected your particular method, giving you the opportunity to adapt and use the newly compatible interactions instead. The `compat` object has the same structure as the TypedApi: * `compat.tx.Pallet.Call` * `compat.query.Pallet.Entry` * `compat.constants.Pallet.Constant` * `compat.apis.ApiName.method` * `compat.event.Pallet.Event` Each entry exposes: * `isCompatible(level?: CompatibilityLevel): boolean` * `getCompatibilityLevel(): CompatibilityLevel` #### `CompatibilityLevel` The enum `CompatibilityLevel` defines 4 levels of compatibility: ```ts 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, } ``` ##### `Incompatible` The operation is not compatible with the current runtime. It might be that the interaction was removed completely, or a smaller change such as having a property removed from a storage query. ##### `Partial` Some values will be compatible depending on the actual values being sent or received. For instance, `getCompatibilityLevel` for the transaction `Utility.batch_all` might return `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 removed transaction as one of its inputs. Another instance of partial compatibility is when an optional property on an input struct was made mandatory. 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. ##### `BackwardsCompatible` The operation had some changes, but they are backwards compatible with the descriptors generated at dev time. In the case of `Utility.batch_all`, this might happen when a new transaction is added as a possible input. There was a change, and PAPI lets you know about it with this level, but you can be sure that any transaction you pass as an input will still work. A backwards-compatible change also happens in structs. For instance, if an input struct removes one of its properties, those operations are still compatible. ##### `Identical` Types are identical between the descriptors and the current runtime. No changes were made. #### `getCompatibilityLevel` Returns the compatibility level for the interaction: ```ts const staticApis = await typedApi.getStaticApis() const level = staticApis.compat.query.System.Account.getCompatibilityLevel() switch (level) { case CompatibilityLevel.Identical: console.log("Types are identical") break case CompatibilityLevel.BackwardsCompatible: console.log("Compatible with changes") break case CompatibilityLevel.Partial: console.log("Partially compatible - check your values") break case CompatibilityLevel.Incompatible: console.log("Not compatible!") break } ``` #### `isCompatible` A utility to directly check for compatibility. The `threshold` parameter sets the minimum level required for the result to be `true` (inclusive). Passing `CompatibilityLevel.BackwardsCompatible` will return `true` for both identical and backwards compatible, but not for partial compatibility. ```ts interface IsCompatible { (threshold?: CompatibilityLevel): boolean } // Equivalent to: function isCompatible(threshold = CompatibilityLevel.Partial) { return getCompatibilityLevel() >= threshold } ``` When called without arguments, `isCompatible()` defaults to `CompatibilityLevel.Partial`, meaning it returns `true` for partial, backwards compatible, and identical. #### Example ```ts import { CompatibilityLevel } from "polkadot-api" const staticApis = await typedApi.getStaticApis() // Check if a transaction is at least partially compatible (default) if (staticApis.compat.tx.Balances.transfer_keep_alive.isCompatible()) { // Safe to use - at least partial compatibility } // Require backwards compatibility or better if ( staticApis.compat.tx.Balances.transfer_keep_alive.isCompatible( CompatibilityLevel.BackwardsCompatible, ) ) { // Safe to use - fully backwards compatible } // Check runtime APIs before using them if ( staticApis.compat.apis.StakingApi.nominations_quota.isCompatible( CompatibilityLevel.Partial, ) ) { const quota = await typedApi.apis.StakingApi.nominations_quota(123n) } // Get the exact level for more nuanced handling const level = staticApis.compat.query.System.Account.getCompatibilityLevel() if (level === CompatibilityLevel.Partial) { console.warn("Query has partial compatibility - verify your usage") } ``` The compatibility check is synchronous because `getStaticApis()` already waits for both the runtime metadata and the descriptors to be loaded. This makes it efficient to check multiple interactions without repeated async calls. See [this recipe](/recipes/upgrade) for an example of handling runtime upgrades ## 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](/codegen) section for a deeper explanation) to generate the types used at devel time. `TypedApi` object looks like: ```ts type TypedApi = { query: StorageApi tx: TxApi event: EvApi apis: RuntimeCallsApi constants: ConstApi txFromCallData: TxFromBinary getStaticApis: (options?: { at?: HexString | "finalized" | "best" signal?: AbortSignal }) => Promise } ``` Every field except for `getStaticApis` and `txFromCallData` is a `Record>`. 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. `txFromCallData` will be explained as well in the [`tx`](/typed/tx) section. `getStaticApis()` returns a promise that resolves once the runtime is loaded, providing synchronous access to constants, compatibility checks, and transaction encoding. See the [Static APIs](/static) for details. ## UnsafeApi The `UnsafeApi` enables interaction with the chain easily to the same extend [TypedApi](/typed) does, but it does not requires any descriptors. It is an advanced method and should only be used if you really know what you are doing. In order to create it, you can still pass a descriptors' type to get the same type inference as in the `typedApi`, but the shape of the entries at runtime level is not guaranteed. Its primary use cases are applications that make no assumptions about the current runtime (e.g., a dev console that reads the latest metadata and exposes interactions dynamically, rather than relying on hard-coded calls), or environments where generating descriptors is not possible. :::warning The `UnsafeApi` does not provide any compatibility checks protection as `TypedApi` does. ::: The UnsafeApi has the following structure: ```ts type UnsafeApi = { query: StorageApi tx: TxApi txFromCallData: TxFromBinary event: EvApi apis: RuntimeCallsApi constants: ConstApi getStaticApis: (options?: { at?: HexString | "finalized" | "best" signal?: AbortSignal }) => Promise } ``` In order to create the unsafe api, it is actually very straightforward: ```ts const unsafeApi = client.getUnsafeApi() // without typings // optionally generate descriptors to get type inference import { dot } from "@polkadot-api/descriptors" const unsafeApi = client.getUnsafeApi() // with typings ``` One can notice the API is actually very similar to the `TypedApi`, check [its docs](/typed) for the API reference since it behaves the same way, except that it doesn't perform any compatibility check. ## Migrate to V2 PAPI v2 brings a few changes that make development easier, with a more consistent interface throughout the API. Here's a migration guide to help you switch from version 1 to version 2 ### JSON-RPC providers #### WebSocket Provider ##### getWsProvider The WsProvider utilities have been moved to `polkadot-api/ws`. As the latest LTS of NodeJS supports the latest WebSocket spec, the `node` and `web` sub-paths have been removed. The `getWsProvider` provider now automatically uses the appropriate methods based on the capabilities of the node. Therefore, the `withPolkadotSdkCompat` function from `polkadot-sdk-compat` has been removed, and can be safely omitted. ##### v1 ```ts import { createClient } from "polkadot-api" import { getWsProvider } from "polkadot-api/ws-provider" // [!code hl] import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat" const client = createClient( withPolkadotSdkCompat(getWsProvider("wss://rpc.ibp.network/polkadot")), // [!code hl] ) ``` ##### v2 ```ts import { createClient } from "polkadot-api" import { getWsProvider } from "polkadot-api/ws" // [!code hl] const client = createClient( getWsProvider("wss://rpc.ibp.network/polkadot"), // [!code hl] ) ``` ##### createWsClient Additionally, there's now a convenience function `createWsClient` that creates a PAPI client directly from a WsURL ##### v1 ```ts import { createClient } from "polkadot-api" import { getWsProvider } from "polkadot-api/ws-provider" const client = createClient(getWsProvider("wss://rpc.ibp.network/polkadot")) ``` ##### v2 ```ts import { createWsClient } from "polkadot-api/ws" const client = createWsClient("wss://rpc.ibp.network/polkadot") ``` ##### Config option changes **innerEnhancer** This configuration option has been removed. If you were using it for logging, the new property `logger` provides all events happening on the underlying websocket: connections, disconnections, errors and also in and out messages. If you were using it for more advanced use cases, it also accepts a `websocketClass` to customize the WebSocket class used by the provider. **Status change events:** The `WsEvent` enum is still available and exported from `polkadot-api/ws`. The only change is that the enum values (`CONNECTING`, `CONNECTED`, `CLOSE`, `ERROR`) now resolve to their string equivalents instead of numbers. If you were using `WsEvent.CONNECTING`, etc. then it should continue to work without changes. #### Smoldot Provider When a smoldot chain is destroyed it can't be reused. In v1 there were some edge cases where a smoldot provider would stop working because it required a new smoldot chain to be instantiated. For this reason, in v2 now `getSmProvider` takes a factory function instead. It's important to note that you should create the smoldot chain inside that function, as re-using the same chain could end up with the same issue. ##### v1 ```ts import { createClient } from "polkadot-api" import { getSmProvider } from "polkadot-api/sm-provider" import { start } from "polkadot-api/smoldot" import { chainSpec } from "polkadot-api/chains/westend" const smoldot = start() const westendChain = smoldot.addChain({ chainSpec }) const client = createClient(getSmProvider(westendChain)) ``` ##### v2 ```ts import { createClient } from "polkadot-api" import { getSmProvider } from "polkadot-api/sm-provider" import { start } from "polkadot-api/smoldot" import { chainSpec } from "polkadot-api/chains/westend" const smoldot = start() const client = createClient( getSmProvider(() => smoldot.addChain({ chainSpec })), ) ``` #### JsonRpcProvider The `JsonRpcProvider` interface has changed: instead of using stringified messages, it has them parsed, for both input and output. All of the providers and enhancers provided by polkadot-api have been migrated, so it shouldn't require any change. If you were using custom enhancers, then you can omit parsing/stringifying. ##### Custom Enhancers and Providers If you have custom providers or enhancers, you need to update them to work with parsed messages: ##### v1 ```ts import type { JsonRpcProvider } from "polkadot-api/ws-provider" const myEnhancer = (parent: JsonRpcProvider): JsonRpcProvider => (onMessage) => { const inner = parent((msg) => { const parsed = JSON.parse(msg) // ❌ Messages were strings // ... process parsed onMessage(JSON.stringify(parsed)) // ❌ Send stringified }) return { send(message) { inner.send(message) // ❌ Message was a string }, disconnect() { inner.disconnect() }, } } ``` ##### v2 ```ts import type { JsonRpcProvider } from "polkadot-api" // ✅ Import from main package const myEnhancer = (parent: JsonRpcProvider): JsonRpcProvider => (onMessage) => { const inner = parent((msg) => { // ✅ msg is already parsed - no JSON.parse needed // ... process msg directly onMessage(msg) // ✅ Send parsed object directly }) return { send(message) { inner.send(message) // ✅ message is parsed object }, disconnect() { inner.disconnect() }, } } ``` **Key differences:** * Import `JsonRpcProvider` from `"polkadot-api"` (not `"polkadot-api/ws"`) * Remove all `JSON.parse()` calls - messages are already parsed * Remove all `JSON.stringify()` calls - send parsed objects * If interfacing with workers or external systems that use strings, parse/stringify at the boundary ### Binary The `Binary` class has been removed. Now PAPI works with `Uint8Array`s, the native primitive for binary data. Instead, `Binary` is a set of utilities to easily convert various formats (hex, text, opaque) with `Uint8Array`. ##### v1 ```ts import { Binary } from "polkadot-api" const myBytes = Binary.fromText("I'm migrating to PAPI v2!") console.log("bytes", myBytes.asBytes()) console.log("hex", myBytes.asHex()) console.log("text", myBytes.asText()) ``` ##### v2 ```ts import { Binary } from "polkadot-api" const myBytes = Binary.fromText("I'm migrating to PAPI v2!") console.log("bytes", myBytes) console.log("hex", Binary.toHex(myBytes)) console.log("text", Binary.toText(myBytes)) ``` **Important notes:** * The pattern changed from `instance.asMethod()` to `Binary.toMethod(instance)` * `Uint8Array` is used directly - no wrapping needed * `Binary.fromBytes()` **does not exist** in v2 - just use the `Uint8Array` directly: ```ts // v1 Binary.fromBytes(myUint8Array).asHex() // v2 Binary.toHex(myUint8Array) // Pass Uint8Array directly ``` ### FixedSizeBinary Fixed-size binaries are usually more convenient to be expressed as Hex, as they often represent hashes or addresses. For this reason, they are not `Uint8Array`s, but just hex strings typed as `SizedHex`. The generic in `SizedHex` is only to show information about the expected length for that parameter. ##### v1 ```ts import { FixedSizeBinary } from "polkadot-api" const myHash = FixedSizeBinary.fromHex("0x1234567890") console.log("bytes", myHash.asBytes()) console.log("hex", myHash.asHex()) ``` ##### v2 ```ts import { Binary, SizedHex } from "polkadot-api" const myHash: SizedHex<5> = "0x1234567890" console.log("bytes", Binary.fromHex(myHash)) console.log("hex", myHash) ``` ### Compatibility API & RuntimeToken The compatibility API has changed significantly. The runtime and compatibility token has been removed, and the function to check the compatibility is now in a separate `typedApi.getStaticApis()` `typedApi.getStaticApis()` returns a promise of a set of APIs that target one specific runtime. Once the runtime is loaded, the promise resolves and you have access to a few APIs that can run synchronously. You can optionally pass in the block you want to use for that, but it defaults to `finalized`. Once that block runtime is loaded, then you have synchronous access to anything that's inside: `txFromCallData(callData)` to create a tx from a call data, `constants` to access the constants, `tx` to get call data, and `compat` to check compatibility across all APIs. #### Compatibility Previously, compatibility APIs were integrated within the TypedAPI. This has been moved in v2 to `getStaticApis`, to make it clearer that they are compared against a specific runtime. The new API allows to have better control on runtime upgrades, but could require a bit of refactoring. If you want a quick 1-to-1 update on inline compatibility checks, it's more simple: ##### v1 ```ts const typedApi = client.getTypedApi(dot) const isCompatible = await typedApi.apis.StakingApi.nominations_quota.isCompatible() if (isCompatible) { typedApi.apis.StakingApi.nominations_quota(123n) } ``` ##### v2 ```ts const typedApi = client.getTypedApi(dot) const staticApis = await typedApi.getStaticApis() const isCompatible = staticApis.compat.apis.StakingApi.nominations_quota.isCompatible() if (isCompatible) { typedApi.apis.StakingApi.nominations_quota(123n) } ``` #### RuntimeToken V1 had an API to be able to make some async operations become synchronous, with what was called the `CompatibilityToken` for the TypedAPI, or the `RuntimeToken` for the unsafe API. You would get this token and then you could pass it along constant calls, compatibility calls, etc. and you'd get the result synchronously. This token has been removed. This can be achieved now with `getStaticApis()`, which conveys that they are against one specific block. ##### v1 ```ts const typedApi = client.getTypedApi(dot) const token = await typedApi.compatibilityToken const version = typedApi.constants.System.Version(compatibilityToken) ``` ##### v2 ```ts const typedApi = client.getTypedApi(dot) const staticApis = await typedApi.getStaticApis() const version = staticApis.constants.System.Version ``` This also allows you to fetch the constant for one specific block (as opposed to v1 where you could only get it for the finalized block) Another place the token was used for a synchronous version was when encoding or decoding a transaction call data. In this case, it's also inside staticApis: ##### v1 ```ts const typedApi = client.getTypedApi(dot) const token = await typedApi.compatibilityToken const callData = typedApi.tx.System.remark({ data: Binary.fromText("Hello!"), }).getEncodedData(token) // Reverse it const tx = typedApi.txFromCallData(callData, token) ``` ##### v2 ```ts const typedApi = client.getTypedApi(dot) const staticApis = await typedApi.getStaticApis() const callData = staticApis.tx.System.remark( Binary.fromText("Hello!"), ).getCallData() // Reverse it const tx = staticApis.txFromCallData(callData) console.log(tx.decodedCall) ``` ### WatchValue The `watchValue` method of storage queries now returns an observable with an object containing both the value and the block information: `{ value: T, block: BlockInfo }`. Also, the API changes to be more consistent with other methods: The second optional argument now takes the `at: HexString | 'finalized' | 'best'` as a property of an object. ##### v1 ```ts const typedApi = client.getTypedApi(dot) const ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" typedApi.query.System.Account.watchValue(ALICE, "best").subscribe( (accountValue) => { console.log("value", accountValue) }, ) ``` ##### v2 ```ts const typedApi = client.getTypedApi(dot) const ALICE = "5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY" typedApi.query.System.Account.watchValue(ALICE, { at: "best" }).subscribe( (update) => { console.log("block", update.block) console.log("value", update.value) }, ) // If you want the exact behaviour as before import { distinctUntilChanged, map } from "rxjs" typedApi.query.System.Account.watchValue(ALICE, { at: "best" }) .pipe( map((v) => v.value), // The reference of `value` will be kept if it didn't change distinctUntilChanged(), ) .subscribe((accountValue) => { console.log("value", accountValue) }) ``` ### Events #### Pulling events The events API has been reworked. Instead of `pull(): Promise`, which returned the list of events in the latest finalized, v2 has `get(blockHash: HexString): Promise`, which is more explicit over which block you want to get the events from. Additionally, some return values were inconsistent: Some methods returned the inner value of the event, others returned an object with `{ meta: { block, phase }, payload: T }`, others included the topics, etc. This has changed so that every method returns the same interface: `{ original: SystemEvent, payload: T }`. The original event keeps the same structure from SystemEvents, which include topics and phase. ##### v1 ```ts const typedApi = client.getTypedApi(dot) const events = await typedApi.event.Balances.Transfer.pull() events.forEach((evt) => { console.log(evt.meta.block, evt.meta.phase, evt.payload) }) ``` ##### v2 ```ts const typedApi = client.getTypedApi(dot) const finalizedBlock = await client.getFinalizedBlock() const events = await typedApi.event.Balances.Transfer.get(finalizedBlock.hash) events.forEach((evt) => { console.log( finalizedBlock.hash, evt.original.phase, evt.original.topics, evt.payload, ) }) ``` #### Watching events `watch(filter): Observable<{ meta, payload }>` has also changed significantly. The filter parameter is removed, as it can easily be achieved by composing observables with the `filter` operator. The observable used to return the events flattened out. This has changed so that it performs one single emission per block, with the hash that it was found `{ block: BlockInfo, events: Array<{ original: SystemEvent, payload: T }>}`. ##### v1 ```ts const typedApi = client.getTypedApi(dot) typedApi.event.Balances.Transfer.watch((evt) => evt.from === ALICE).subscribe( (evt) => { console.log(evt.meta.block, evt.meta.phase, evt.payload) }, ) ``` ##### v2 ```ts const typedApi = client.getTypedApi(dot) // This is more versatile, but this will be a copy-paste of the original behavior (except for the event object shape) typedApi.event.Balances.Transfer.watch() .pipe( mergeMap(({ block, events }) => events .filter((evt) => evt.from === ALICE) .map((evt) => ({ ...evt, block })), ), ) .subscribe((evt) => { console.log(evt.block, evt.original.phase, evt.original.topics, evt.payload) }) ``` ### Utility function and overload changes #### Transaction at The `at: BlockHash` parameter when creating a transaction now only accepts a specific block hash. It's important to understand that this parameter only affects the mortality of the transaction: It will only be valid on that specific block or its children, up to the configured mortality period. For this reason, using `at: 'best'` was often problematic since the tx wouldn't be included after a re-org. If omitted, it will be the finalized block. If you were passing `best` and you are aware of the consequences of re-orgs, then get a blockHash from `client.getBestBlocks()` and pass it to the `at` parameter. #### `watchBlockBody` `client.watchBlockBody` has been renamed to `client.getBlockBody$` #### `jsonPrint` The `jsonPrint` function has been removed. Instead, the `polkadot-api/utils` package now exports a `jsonSerialize` and `jsonDeserialize` replacer functions to be used with the browser's `JSON.stringify` and `JSON.parse` functions. This is a bit more flexible since it can be now composed with other JSON replacer functions, as well as allows for a 1-line JSON string. ##### v1 ```ts import { jsonPrint } from "polkadot-api/utils" // ... console.log(jsonPrint(systemAccount)) ``` ##### v2 ```ts import { jsonSerialize } from "polkadot-api/utils" // ... console.log(JSON.stringify(systemAccount, jsonSerialize)) ``` #### `mergeUint8(...args)` `mergeUint8` utility function now only accepts one argument which must be an array. To migrate, simply wrap the arguments with an array. #### `getBlockBody` and `getBlockHeader` `getBlockBody` and `getBlockHeader` took an optional argument `hash?: HexString`, which defaulted to the finalized block. However, it doesn't make sense to get a random body or header, so now it requires passing in a specific block. If you need that, get the hash from `client.getBestBlocks()` or `client.getFinalizedBlock()` ### Chain renames Chain names have changed to become easier to use: * Westend: All `westend2` prefix have changed to `westend`: `westend`, `westend_asset_hub`, etc. * Kusama: All `ksmcc3` prefix have changed to `kusama`: `kusama`, `kusama_asset_hub`, etc. * Rococo: Rococo has been dropped, as it was sunset. The CLI will auto-migrate the config for existing projects with this change, but it will only accept the new chain names for newly added chains. ### Troubleshooting #### TypeScript Errors **Error: `Property 'isCompatible' does not exist`** You need to use `getStaticApis()`: ```ts // Before await api.apis.SomeApi.someCall.isCompatible() // After const staticApis = await api.getStaticApis() await staticApis.compat.apis.SomeApi.someCall.isCompatible() ``` **Error: `'JsonRpcProvider' is not exported by "polkadot-api/ws"`** For custom providers/enhancers, import from the main package: ```ts // Wrong import type { JsonRpcProvider } from "polkadot-api/ws" // Correct import type { JsonRpcProvider } from "polkadot-api" ``` **Error: `Property 'fromBytes' does not exist on type 'Binary'`** `Binary.fromBytes()` doesn't exist in v2. Pass `Uint8Array` directly: ```ts // Before Binary.fromBytes(myUint8Array).asHex() // After Binary.toHex(myUint8Array) ``` **Error: `Argument of type 'Promise' is not assignable to parameter`** `getSmProvider` now expects a function: ```ts // Before getSmProvider(Promise.all([...])) // After getSmProvider(() => Promise.all([...])) ``` ## Typed Codecs ### Overview In advanced use cases -mainly for library authors or developer tool creators- it’s often necessary to tap into the **codecs** used in different chain interactions. This is exactly what the `getTypedCodecs` API offers: a **strongly-typed interface** that grants access to all relevant codecs as defined in a chain’s metadata. This API is particularly helpful when you need to **manually encode or decode values** for runtime storage, calls, constants, or events, without relying on runtime type guessing. Let’s take a look at how this works in practice. ### Example: Manual Storage Entry Encoding Suppose you're in a development environment and want to manually set a storage entry for `XcmPallet.Queries`. Here’s how `getTypedCodecs` helps: ```ts import { getTypedCodecs } from "polkadot-api" import { dot, XcmPalletQueryStatus, XcmV4Response, XcmVersionedResponse, } from "@polkadot-api/descriptors" const codecs = await getTypedCodecs(dot) const xcmQueriesCodecs = codecs.query.XcmPallet.Queries export const encodedValue = xcmQueriesCodecs.value.enc( XcmPalletQueryStatus.Ready({ at: 100000, response: XcmVersionedResponse.V4(XcmV4Response.Null()), }), ) export const encodedKey = xcmQueriesCodecs.args.enc([100n]) ``` You get **fully typed access** to both key and value encoders for any storage item, safely and with full IDE support. ### Example: Inner Enum Variant Codec Need to access the codec for a specific enum variant, like `XcmPalletQueryStatus.Ready`? You can go even deeper: ```ts import { getTypedCodecs } from "polkadot-api" import { dot, XcmV4Response, XcmVersionedResponse, } from "@polkadot-api/descriptors" const codecs = await getTypedCodecs(dot) codecs.query.XcmPallet.Queries.value.inner.Ready.enc({ at: 100, response: XcmVersionedResponse.V4(XcmV4Response.Null()), }) ``` You can directly encode the structure required by the `Ready` variant of the enum. And again, the entire structure is **strongly typed**. *** 🎥 Want to see this in action? Check out this short video to experience the powerful DX this API offers: