š Hey builders,
Have you ever tried connecting a frontend to a blockchain contract and felt like you were solving a Rubikās cube in the dark? Yeah⦠me too. š
Thatās why I wrote this guide, to walk you step-by-step through how I managed to connect a React + Vite frontend to an ink! smart contract using PAPI (Polkadot-API).
Spoiler alert: once I figured out the workflow, it felt less like rocket science and more like following a recipe. šāØ
Why this guide?
Instead of drowning in docs and scattered tutorials, I wanted a single walkthrough that shows:
-
How to build & deploy an ink! contract.
-
How to generate type-safe bindings with PAPI.
-
How to make a frontend talk to the contract through a wallet.
If youāve been curious about ink! + PAPI but donāt know where to start, this article is for you. š
Why PAPI?
Polkadot-API (PAPI) is the modern, modular JavaScript/TypeScript SDK for building dApps in the Polkadot ecosystem. It provides strong TypeScript support, a light-client-first approach, and SDKs (including an Ink! SDK) that generate typed bindings from contract metadata ā making contract interactions safer and easier to write.
What I built
A small React + Vite app that:
-
Connects to a browser wallet (Polkadot.js / Talisman).
-
Reads a PSP22 contract balance (read-only query).
-
Sends a transfer (signed transaction).
The contract metadata is included in the repo and PAPI generated TypeScript descriptors were used to produce strongly typed calls.
Steps I followed (practical)
1. Build the ink! contract
Install cargo-contract
and build:
cargo install cargo-contract --force
cargo contract build --release
cargo contract build
produces the contract artifact and the metadata required by PAPI.
2. Generate PAPI descriptors from the .contract
I used the PAPI CLI to add my chain and the contract metadata:
pnpm papi add -w wss:// mychain
pnpm papi ink add ./path/to/.contract
This creates typed descriptors available as @polkadot-api/descriptors
for import in the frontend. The Ink! tooling in PAPI generates types and helpers for dry-runs, deploys, queries, storage access and sending messages.
3. Frontend: create client & Ink SDK
In src/papiClient.ts
:
import { createClient } from "polkadot-api";
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat";
import { getWsProvider } from "polkadot-api/ws-provider/web";
import { createInkSdk } from "@polkadot-api/sdk-ink";
import { contracts, mychain } from "@polkadot-api/descriptors";
const client = createClient(
withPolkadotSdkCompat(getWsProvider("wss://"))
);
const typedApi = client.getTypedApi(mychain);
const inkSdk = createInkSdk(typedApi, contracts.);
export { inkSdk };
This gives typed, chain-aware contract helpers.
4. Wallet connection & signer
I used PAPIās pjs-signer
helpers to connect the Polkadot.js/Talisman extension and obtain a polkadotSigner
to sign transactions:
import { getInjectedExtensions, connectInjectedExtension } from "polkadot-api/pjs-signer";
const exts = getInjectedExtensions();
const selected = await connectInjectedExtension(exts[0]); // choose extension
const accounts = selected.getAccounts();
const signer = accounts[0].polkadotSigner;
The signer implements the PolkadotSigner
interface that PAPI methods accept.
5. Query and send
Using the Ink SDK:
const instance = inkSdk.getContract(CONTRACT_ADDRESS);
// Query (dry-run)
const q = await instance.query("PSP22::balance_of", {
origin: alice,
data: { owner: alice },
});
// Send (signed tx)
const res = await instance.send("PSP22::transfer", {
origin: alice,
data: { to: bob, value: 100n },
}).signAndSubmit(signer);
The SDK exposes .query(...)
and .send(...).signAndSubmit(...)
, very helpful for dry-runs and signed transactions.
Final notes & tips
-
Use the generated descriptors, they save time and reduce errors.
papi.how -
Always use dry-runs
(.query()
or.dryRun())
before sending signed txs to check gas/storage cost.
papi.how -
Do not commit private keys to the repo; use browser wallets for signing.
papi.how
Links & resources