| name | product-sdk-contracts |
| description | Use when interacting with smart contracts (PolkaVM/Solidity) on Asset Hub, using ContractManager with cdm.json manifests, createContract for ad-hoc contracts, ContractRuntime creation, or contract type codegen. Covers @parity/product-sdk-contracts.
|
Product SDK Contracts
@parity/product-sdk-contracts provides ergonomic, fully-typed smart contract interactions on Asset Hub. It supports both Solidity contracts (via pallet-revive) and PolkaVM contracts.
Quick Start: With cdm.json Manifest
import { createChainClient } from "@parity/product-sdk-chain-client";
import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub";
import { ContractManager } from "@parity/product-sdk-contracts";
import cdmJson from "./cdm.json";
const client = await createChainClient({
chains: { assetHub: paseo_asset_hub },
});
const manager = ContractManager.fromClient(cdmJson, client.raw.assetHub, paseo_asset_hub, {
signerManager,
});
const counter = manager.getContract("@example/counter");
const { value } = await counter.getCount.query();
console.log("Count:", value);
await counter.increment.tx();
client.destroy();
Quick Start: Ad-Hoc Contract
import { createChainClient } from "@parity/product-sdk-chain-client";
import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub";
import { createContractFromClient } from "@parity/product-sdk-contracts";
const abi = [
{ type: "function", name: "getCount", inputs: [], outputs: [{ name: "", type: "uint32" }], stateMutability: "view" },
{ type: "function", name: "increment", inputs: [], outputs: [], stateMutability: "nonpayable" },
];
const client = await createChainClient({
chains: { assetHub: paseo_asset_hub },
});
const counter = createContractFromClient(
client.raw.assetHub,
paseo_asset_hub,
"0xYourContractAddress...",
abi,
{ signerManager }
);
const { value } = await counter.getCount.query();
await counter.increment.tx();
client.destroy();
ContractManager vs createContract
| ContractManager | createContract / createContractFromClient |
|---|
| When | Multiple contracts with cdm.json manifest | Single contract, known address + ABI |
| Type safety | Full (with codegen) | Generic |
| Address management | Automatic from manifest | You provide it |
| Use case | Production dApps | Quick prototyping, ad-hoc contracts |
Contract Methods
Each method on a contract handle has two variants:
query() — Read-Only Calls
.query() runs a ReviveApi.call dry-run on the chain — no transaction, no gas cost. Targets best-block by default so reads observe the same state a freshly-submitted .tx() sees (matching .tx()'s default resolution). Override per call with { at: "finalized" } or a block hash, or change the runtime default via createContractRuntimeFromClient(client, descriptor, { at }).
const result = await counter.getCount.query();
With options:
const result = await counter.getCount.query({
origin: "0x...",
at: "finalized",
});
Reverts. query() does NOT throw on a contract revert — it returns
{ success: false, value, gasRequired }. Two failure shapes are possible:
- Dispatch-level failures from the chain (e.g.
{ type: "ContractReverted" },
{ type: "AccountNotMapped" }, { type: "Module", ... }) — passed through
on value as-is.
- Contract-level reverts (REVERT flag set on a dispatched-OK call) — surfaced
as a tagged payload:
{ type: "ContractRevertedWithPayload", data, reason?, decoded? }.
reason is the decoded Error(string) message or a mapped Panic description;
decoded carries the viem-decoded { errorName, args } for ABI-defined custom
errors. Discriminate on value.type to tell the two failure paths apart.
tx() — State-Changing Transactions
.tx() returns a Promise<Result<TxResult, ContractError | TxError>> where
Result<T, E> is { ok: true; value: T } | { ok: false; error: E }. It does
not throw — pre-submit failures (ContractSignerMissingError,
ContractDryRunFailedError, ContractRevertedError) and submit failures
(TxError) all come back on the result.error channel. Always check
result.ok before reading result.value.
const result = await counter.increment.tx();
if (!result.ok) {
handle(result.error);
} else {
console.log("Included in block:", result.value.block.hash);
}
With options:
const result = await counter.increment.tx({
signer: customSigner,
waitFor: "finalized",
at: "finalized",
onStatus: (status) => console.log(status),
});
Pre-flight revert detection. Before submitting, tx() runs a dry-run. If
the chain reports the REVERT flag is set, tx() returns err(ContractRevertedError)
(on the result.error channel, NOT thrown) and the extrinsic is NOT submitted
(no gas paid). The error carries methodName, the raw data, and the same
reason / decoded fields as the query payload. Passing both gasLimit AND
storageDepositLimit in options skips the dry-run entirely — including this
revert pre-check.
SignerManager Integration
Pass a SignerManager and a product-account signer to sign contract tx() calls:
import { SignerManager } from "@parity/product-sdk-signer";
const signerManager = new SignerManager({ ss58Prefix: 0, dappName: "your-app" });
await signerManager.connect();
const productRes = await signerManager.getProductAccount("your-app.dot", 0);
if (!productRes.ok) throw productRes.error;
const productAccount = productRes.value;
const manager = ContractManager.fromClient(cdmJson, client.raw.assetHub, paseo_asset_hub, {
signerManager,
});
await counter.increment.tx({ signer: productAccount.getSigner() });
See examples/tx-demo/src/main.ts and
examples/contracts-demo/src/main.ts
for full end-to-end references.
You can also set a default signer or origin:
const manager = ContractManager.fromClient(cdmJson, client.raw.assetHub, paseo_asset_hub, {
defaultSigner: mySigner,
defaultOrigin: "0x...",
});
manager.setDefaults({
signerManager: newSignerManager,
});
Type Codegen
Generate TypeScript types for your contracts:
import { generateContractTypes } from "@parity/product-sdk-contracts";
import { writeFileSync } from "fs";
const types = generateContractTypes([
{ library: "@example/counter", abi: counterAbi },
{ library: "@example/token", abi: tokenAbi },
]);
writeFileSync(".cdm/contracts.d.ts", types);
This generates a module augmentation that makes getContract() return fully-typed handles:
const counter = manager.getContract("@example/counter");
Loading cargo-pvm-contract Artifacts (without CDM)
For contracts built with cargo pvm-contract build, the toolchain emits two
files per contract:
target/<name>.release.abi.json # Solidity-flavoured ABI
target/<name>.release.polkavm # PolkaVM bytecode
Use the ./pvm subpath to feed those artefacts into the contracts package
without going through CDM:
import {
parsePvmContractAbi,
loadPvmContractAbi,
loadPvmContractArtifacts,
} from "@parity/product-sdk-contracts/pvm";
import { createContractFromClient } from "@parity/product-sdk-contracts";
import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub";
import abiJson from "./counter.release.abi.json" with { type: "json" };
const abi = parsePvmContractAbi(abiJson);
const abi2 = await loadPvmContractAbi("./target/counter.release.abi.json");
const { abi: abi3, bytecode } = await loadPvmContractArtifacts("./target/counter.release");
const counter = createContractFromClient(client.raw.assetHub, paseo_asset_hub, "0xC472...", abi);
const { value } = await counter.get.query();
await counter.increment.tx(1n, { signer });
The filesystem helpers lazy-import node:fs/promises so the ./pvm module
remains importable in browser builds — only the call site needs to be in Node.
ContractRuntime Access
For advanced use cases, create a ContractRuntime directly:
import { createContractRuntimeFromClient, createContract } from "@parity/product-sdk-contracts";
import { paseo_asset_hub } from "@parity/product-sdk-descriptors/paseo-asset-hub";
const runtime = createContractRuntimeFromClient(client.raw.assetHub, paseo_asset_hub, {
at: "best",
});
const counter = createContract(runtime, "0x...", abi, { signerManager });
The typed-API factory createContractRuntime(typedApi, { at }) is also exported — useful for tests where you already hold a typed API. Prefer createContractRuntimeFromClient on every production path.
Common Mistakes
-
Using api.contracts — There is no .contracts property on chain clients. Create ContractRuntime yourself or use ContractManager.fromClient().
-
Missing signerManager for tx() — If no signer is available, tx() returns err(ContractSignerMissingError) (on result.error, not thrown). Check !result.ok before reading result.value.
-
Wrong signer type — Contract transactions need a PolkadotSigner. Don't confuse with StatementSignerWithKey (for statement-store).
-
Adding a spurious await — ContractManager.fromClient() and createContractFromClient() are synchronous; don't await them. Only the live-resolution factories ContractManager.fromLive() / fromLiveClient() (and withLiveContractAddresses()) return Promises.
-
Assuming tx() only fails for signer/dispatch reasons — tx() also returns err(ContractRevertedError) when the dry-run shows the contract would revert. Branch on !result.ok and inspect result.error (instanceof ContractRevertedError, or its base ContractError) if you're surfacing revert reasons to users. tx() no longer throws these — they arrive on the result.error channel.
-
Assuming query() throws on revert — It doesn't. Reverts come back as { success: false, value: { type: "ContractRevertedWithPayload", ... } }. Always check success before reading value as the return type.
-
Using manager.getSigner() (legacy account) on chains with unknown signed extensions — signerManager.connect() exposes legacy accounts, whose signer routes through PJS. On chains like Paseo Next v2 that ship AsPgas, PJS throws PJS does not support this signed-extension: AsPgas at signing time. Use signerManager.getProductAccount(<appOrigin>, 0) and productAccount.getSigner() instead — that path goes through host_create_transaction and preserves arbitrary extensions.
-
Assuming .query() reads finalized state — Dry-runs default to best-block, matching .tx()'s submission resolution. A .query() right after a .tx() will read what the just-landed transaction wrote, even before finalization. Pass { at: "finalized" } (per-call) or set the runtime default to "finalized" if your product needs canonical lagged reads.
Reference Files