<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: popoola rahmat</title>
    <description>The latest articles on Forem by popoola rahmat (@rampop).</description>
    <link>https://forem.com/rampop</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3470858%2F4960a0bf-3111-42cf-8c5f-067dcbe74a62.png</url>
      <title>Forem: popoola rahmat</title>
      <link>https://forem.com/rampop</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/rampop"/>
    <language>en</language>
    <item>
      <title>My first ink! full-stack project: From smart contract to frontend connection with PAPI.</title>
      <dc:creator>popoola rahmat</dc:creator>
      <pubDate>Mon, 20 Oct 2025 11:41:33 +0000</pubDate>
      <link>https://forem.com/rampop/my-first-ink-full-stack-project-from-smart-contract-to-frontend-connection-with-papi-18k8</link>
      <guid>https://forem.com/rampop/my-first-ink-full-stack-project-from-smart-contract-to-frontend-connection-with-papi-18k8</guid>
      <description>&lt;h2&gt;
  
  
  INTRODUCTION
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;There’s always a beginning for every milestone, and this was mine.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;When I first heard about ink!, the Rust-based smart contract language for the Polkadot ecosystem, it felt intimidating. The syntax looked different, the tools were unfamiliar, and the entire Substrate environment seemed complex.&lt;br&gt;
At one point, I even paused my learning; it all felt overwhelming. But with proper mentorship, everything changed.&lt;br&gt;
In less than two weeks, I learned more than I thought possible.&lt;br&gt;
That single step became the beginning of one of the most rewarding learning experiences I’ve ever had.&lt;/p&gt;

&lt;p&gt;In this article, I’ll take you through my first ink! full-stack project, a PSP token contract, much like your first ERC-20 contract for people with a Solidity background.&lt;br&gt;
We’ll deploy it using the substrate contracts UI and then connect it to a Next.js frontend using Polkadot API (PAPI) and the ink! SDK.&lt;/p&gt;

&lt;p&gt;And trust me, this is a complete beginner-friendly guide, built the same way I achieved mine.&lt;/p&gt;
&lt;h2&gt;
  
  
  1. Setting Up ink!
&lt;/h2&gt;

&lt;p&gt;I followed the official ink! v6 setup guide: &lt;a href="https://use.ink/docs/v6/getting-started/setup/" rel="noopener noreferrer"&gt;use.ink Docs, Getting Started (v6)&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You’ll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Rust (latest stable)&lt;/li&gt;
&lt;li&gt;cargo-contract&lt;/li&gt;
&lt;li&gt;&lt;a href="https://addons.mozilla.org/en-US/firefox/addon/polkadot-js-extension/" rel="noopener noreferrer"&gt;Polkadot.js extension&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;Passeo passethub test tokens(&lt;a href="https://faucet.polkadot.io/?parachain=1111" rel="noopener noreferrer"&gt;here&lt;/a&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once installed, create your first ink! project with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cargo contract new psp_token
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command scaffolds a new ink! smart contract named psp_token, generating a preconfigured folder structure for you.&lt;/p&gt;

&lt;p&gt;After running the command, you’ll get a directory like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;psp_token/
│
├── Cargo.toml
├── lib.rs
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  2. Writing the Smart Contract
&lt;/h2&gt;

&lt;p&gt;Here’s my ink! contract, a simple PSP token, it's the same as the normal OpenZeppelin ERC standard, and the functions are what are implemented. It supports transfer, transfer_from, approve, increase_allowance, decrease_allowance, token_name, token_symbol, token_decimals, mint, and burn functions all in the lib.rs &lt;/p&gt;

&lt;p&gt;&lt;code&gt;lib.rs&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;#![cfg_attr(not(feature = "std"), no_std, no_main)]

use ink::prelude::string::String;

#[derive(Debug, PartialEq, Eq)]
#[ink::scale_derive(Encode, Decode, TypeInfo)]
#[allow(clippy::cast_possible_truncation)]
pub enum PSP22Error {
    Custom(String),
    InsufficientBalance,
    InsufficientAllowance,
    ZeroRecipientAddress,
    ZeroSenderAddress,
    SafeTransferCheckFailed(String),
}

#[ink::contract]
mod inkerc20 {
    use super::PSP22Error;
    use ink::prelude::string::{ String, ToString };
    use ink::storage::Mapping;
    use ink::primitives::H160;

    /// Defines the storage of your contract.
    /// Add new fields to the below struct in order
    /// to add new static storage fields to your contract.
    #[ink(storage)]
    pub struct PspCoin {
        /// Total token supply.
        total_supply: u128,
        /// Mapping from owner to number of owned token.
        balances: Mapping&amp;lt;H160, u128&amp;gt;,
        /// Mapping of the token amount which an account is allowed to withdraw
        /// from another account.
        allowances: Mapping&amp;lt;(H160, H160), u128&amp;gt;,
        /// Token name
        name: Option&amp;lt;String&amp;gt;,
        /// Token symbol
        symbol: Option&amp;lt;String&amp;gt;,
        /// Token decimals
        decimals: u8,
    }
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://github.com/Rampop01/cross-contract-unit-testing/blob/master/lib.rs" rel="noopener noreferrer"&gt;complete code here&lt;/a&gt;, which also has unit tests and comment line to explain what each functions does, copy and paste in your &lt;code&gt;lib.rs&lt;/code&gt; file Or, if you’d rather skip manual deployment, you can deploy your own PSP token directly using my live frontend: &lt;a href="https://psp-token-frontend.vercel.app/" rel="noopener noreferrer"&gt;https://psp-token-frontend.vercel.app/&lt;/a&gt;, Choose your constructor parameters, for example: total_supply: 100000, name, symbol, and decimals as desired.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg0r7z4ejowm30s4k9aw.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fxg0r7z4ejowm30s4k9aw.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Each #[ink(message)] function can be called from the frontend once deployed.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  3. Building and Generating Metadata
&lt;/h2&gt;

&lt;p&gt;Before connecting our contract to the frontend, we need to build the contract and generate its metadata.&lt;/p&gt;

&lt;p&gt;The build process compiles your Rust code to RISC-V, the format used by the Substrate blockchain, while the metadata file describes the contract’s structure (methods, types, and interfaces).&lt;br&gt;
This metadata is essential when deploying your contract or interacting with it from a frontend application.&lt;/p&gt;

&lt;p&gt;To build and generate metadata, run:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;cargo contract build
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command does three things:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Compiles your smart contract.&lt;/li&gt;
&lt;li&gt;It generates a &lt;code&gt;.contract&lt;/code&gt;, &lt;code&gt;.json&lt;/code&gt;, and &lt;code&gt;.polkavm&lt;/code&gt; file, a bundle that contains both the PolkaVM-compatible binary and the metadata required for deployment and frontend interaction.&lt;/li&gt;
&lt;li&gt;Places the output inside the target/ink/ folder.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  4. Deploying the Contract
&lt;/h2&gt;

&lt;p&gt;You can deploy your contract using the &lt;a href="https://ui.use.ink/add-contract" rel="noopener noreferrer"&gt;Substrate Contracts UI&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Steps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pick a network. For this project, I used PassetHub on the Paseo Testnet. You can connect to it through the network selector at the top of the page, then click “Upload Contract.”&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdem9yvuzjw9ljhatny5f.jpeg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fdem9yvuzjw9ljhatny5f.jpeg" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Select your .contract file, which contains the compiled PolkaVM code and metadata, and click &lt;code&gt;Next&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ai3910xotmwinfqzt11.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F4ai3910xotmwinfqzt11.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Choose your constructor parameters, for example: total_supply: 100000, name, symbol, and decimals as desired.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ju99ar8cmpalmm02020.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F8ju99ar8cmpalmm02020.png" alt=" " width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Deploy on testnet by clicking on &lt;code&gt;upload and instantiate&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;Copy your contract address; you’ll need it later when connecting your frontend.
Congratulations, your contract is on-chain&lt;/li&gt;
&lt;/ul&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;Your smart contract is only half the story.&lt;br&gt;
The real deal happens when your frontend starts talking to the blockchain, fetching balances, sending transactions, and deploying contracts right from the browser&lt;/em&gt;.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  5. Setting Up the Frontend
&lt;/h2&gt;

&lt;p&gt;Now that our smart contract is built and deployed, the next step is to connect it to a frontend, the part users actually interact with.&lt;/p&gt;

&lt;p&gt;For this project, I didn’t use any pre-built templates; I started from scratch using Next.js and Tailwind CSS.&lt;br&gt;
That choice helped me understand every setup piece, and honestly, it was worth it. (Looking forward to using a boilerplate next time)&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Project setup&lt;/code&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Create a new Next.js app
npx create-next-app psp-frontend

# Move into the project directory
cd psp-frontend
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;Installing dependencies&lt;/code&gt;&lt;br&gt;
We’ll install a few packages to help our frontend communicate with the blockchain, connect wallets, and interact with our ink! contract.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;# Core Polkadot API SDK
npm i polkadot-api

# Wallet connection (for Talisman, Subwallet, Polkadot.js)
npm install @talismn/connect-wallets

# ink! SDK for frontend contract interaction
npm i @polkadot-api/sdk-ink

# Add our testnet endpoint (Passet Hub)
npx papi add -w wss://testnet-passet-hub.polkadot.io passet

# Signer and crypto utilities
npm install @polkadot-api/pjs-signer @polkadot/util-crypto @polkadot/util
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;After completing these installations, the frontend is now ready to connect directly to the blockchain and interact with the deployed PSP token contract.&lt;/p&gt;

&lt;h2&gt;
  
  
  6. Preparing the Frontend for Smart Contract Interaction
&lt;/h2&gt;

&lt;p&gt;Now that our dependencies are installed, it’s time to prepare the frontend for smart contract interaction.&lt;/p&gt;

&lt;p&gt;This step is about setting up our folder structure, adding the compiled contract bundle, and generating metadata so that our frontend knows how to connect to the deployed contract on-chain.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Folder Structure Setup
Inside your project’s root directory (psp-frontend/), create the following folders:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;mkdir deployment_data
mkdir -p src/{app,components,contexts,utils}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;em&gt;The -p flag automatically creates all folders in one command.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Your folder structure should now look like this:&lt;br&gt;
psp-frontend/&lt;br&gt;
│&lt;br&gt;
├── .papi/&lt;br&gt;
├── deployment_data/&lt;br&gt;
├── public/&lt;br&gt;
├── src/&lt;br&gt;
│   ├── app/&lt;br&gt;
│   ├── components/&lt;br&gt;
│   ├── contexts/&lt;br&gt;
│   └── utils/&lt;br&gt;
└── package.json&lt;/p&gt;

&lt;p&gt;Let’s briefly explain what each folder does:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Folder&lt;/th&gt;
&lt;th&gt;Purpose&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;.papi/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stores automatically generated metadata by the Polkadot API (PAPI).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;deployment_data/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Stores your compiled &lt;code&gt;.contract&lt;/code&gt; file (the smart contract bundle).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;public/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Holds static assets like images or icons.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/app/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Contains your main pages (e.g., &lt;code&gt;page.tsx&lt;/code&gt;, &lt;code&gt;layout.tsx&lt;/code&gt;).&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/components/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Reusable UI components like Navbar or Buttons.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/contexts/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Handles Web3 logic such as wallet connection and contract client setup.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;src/utils/&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Helper functions or configurations.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Copy Your Contract Bundle&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;After building your ink! contract with: &lt;code&gt;cargo contract build&lt;/code&gt; You’ll find the compiled contract file inside your ink! project folder at: target/ink/psp_token.contract, Copy that &lt;code&gt;.contract&lt;/code&gt; file into your deployment_data folder in the frontend project, so you will have &lt;code&gt;/deployment_data/psp.contract&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;This .contract file contains:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The compiled code&lt;/li&gt;
&lt;li&gt;The contract metadata (ABI, constructors, functions, etc.)&lt;/li&gt;
&lt;li&gt;Everything needed for frontend interaction&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Generate TypeScript bindings with PAPI&lt;/p&gt;

&lt;p&gt;Now, navigate to the frontend directory and run the following:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;npx papi ink add "deployment_data/psp.contract"

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This command is used when you already have a .metadata.json file generated separately from your build process (for example, if you extracted only the metadata file).&lt;br&gt;
It tells PAPI to add this metadata into its local registry (in .papi/) so it can generate TypeScript bindings for it.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;em&gt;By now, your frontend knows what contract it’s connecting to and how to communicate with it.&lt;br&gt;
We’ve laid the foundation for the next crucial step, actually connecting the wallet and initialising the Ink SDK&lt;/em&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;h2&gt;
  
  
  6. Connecting Wallets and Initializing the Ink Client
&lt;/h2&gt;

&lt;p&gt;Before interacting with our deployed contract, the frontend must first connect to a user’s wallet and initialize a client that can communicate with the blockchain.&lt;/p&gt;

&lt;p&gt;In my project, I placed this logic inside the src/contexts folder to keep all blockchain setup separated from UI components. This helps maintain a clean architecture and allows different parts of the app to access shared data like wallet info and contract instances easily.&lt;/p&gt;

&lt;p&gt;Our contexts folder looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;src/
 └── contexts/
      ├── wallet-provider.tsx
      ├── ink-client.tsx
      └── types.ts
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Before any wallet can connect or any contract can be called, we need to define the TypeScript “blueprint” for how data flows across our dApp. In this project, our types.ts file lives inside the src/contexts/ folder and defines two main contexts:&lt;/p&gt;

&lt;p&gt;WalletContext → handles everything about wallet connections and account management.&lt;br&gt;
InkClientContext → handles blockchain communication and smart contract interactions. &lt;a href="https://github.com/Rampop01/psp_token-frontend/blob/master/src/contexts/types.ts" rel="noopener noreferrer"&gt;Here’s the full code&lt;/a&gt;. It makes a complete file  with just 3 steps&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I import ready-made types from the Polkadot and Talisman SDKs:
&lt;/li&gt;
&lt;/ol&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import type { Wallet, WalletAccount } from "@talismn/connect-wallets";
import type { Binary, PolkadotClient } from "polkadot-api";
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;These ensure that when we talk about wallets, accounts, or clients, TypeScript already knows what structure to expect.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;WalletContextType
This defines all the wallet-related actions the dApp supports:&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Property&lt;/th&gt;
&lt;th&gt;Description&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;connect(wallet)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Connects to a specific wallet like Talisman or Polkadot.js&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;disconnect()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Disconnects the current wallet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;wallets&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List of all detected wallets on the user’s browser&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;isConnecting&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Tracks connection state (string when connecting, false when idle)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;activeWallet&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Currently connected wallet instance&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;accounts&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;All accounts available from the connected wallet&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;selectedAccount&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;The currently active user account&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;switchAccount(account)&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Allows switching between multiple accounts&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;And finally create a React Context for it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;export const WalletContext = createContext&amp;lt;WalletContextType | null&amp;gt;(null);
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This will allow any component in the dapp to access wallet data through useContext(WalletContext).&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;InkClientContextType
it defines everything related to smart contract interaction.
Let’s break it into logical groups:
Connection
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;client: PolkadotClient | null;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This represents your live connection to the Polkadot network.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Queries (Read-only)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;fetchTokenInfo()
fetchTokenSupply()
fetchTokenBalance()
fetchAllowance()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These functions use &lt;code&gt;.call()&lt;/code&gt; under the hood, meaning they read data from the contract but don’t change the blockchain state.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Transactions (Write)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;deploy()
transferToken()
transferFromToken()
approveToken()
mintToken()
burnToken()
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These use &lt;code&gt;.send()&lt;/code&gt; and require a signed wallet transaction. Each one modifies blockchain state (for example, transferring tokens, minting new ones, or approving another account).&lt;br&gt;
And like before, we create the React context:&lt;/p&gt;

&lt;p&gt;Not calling any blockchain functions here, it's just to tell TypeScript what to expect later.&lt;br&gt;
When you import these types into wallet-provider.tsx and ink-client.tsx, they become the “rules” your app follows, preventing mistakes while interacting with wallets and contracts.&lt;/p&gt;

&lt;p&gt;After defining our types in &lt;code&gt;types.ts&lt;/code&gt;, the next step is to bring wallet interaction to life.&lt;br&gt;
This file is responsible for connecting wallets, managing accounts, and providing wallet data globally through React Context.&lt;br&gt;
&lt;a href="https://github.com/Rampop01/psp_token-frontend/blob/master/src/contexts/wallet-provider.tsx" rel="noopener noreferrer"&gt;Here’s the full code&lt;/a&gt; &lt;/p&gt;

&lt;p&gt;&lt;code&gt;ink-client.tsx&lt;/code&gt; is the frontend bridge between React and Ink! smart contract deployed on a Substrate-based chain (in this case, testnet-passet-hub.polkadot.io). you can get the &lt;a href="https://github.com/Rampop01/psp_token-frontend/blob/master/src/contexts/ink-client.tsx#L6" rel="noopener noreferrer"&gt;full code here&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Every function here talks to the blockchain, either reading or writing data.&lt;/p&gt;

&lt;p&gt;The key pattern is this:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;strong&gt;Action Type&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Method Used&lt;/strong&gt;&lt;/th&gt;
&lt;th&gt;&lt;strong&gt;Description&lt;/strong&gt;&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Read from chain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;.query()&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Fetches data from the smart contract. It’s free, no gas fee and doesn’t modify blockchain state.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Write to chain&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;.send()&lt;/code&gt; → &lt;code&gt;.signAndSubmit()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;Executes a transaction. It costs gas and permanently updates the blockchain state.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;ul&gt;
&lt;li&gt;Example 1: &lt;code&gt;.query&lt;/code&gt; (Read-Only Calls)
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const fetchTokenSupply = useCallback(async (account: WalletAccount) =&amp;gt; {
  const cl = client ?? (await initializeClient());
  const inkClient = createInkSdk(cl);
  const p2pContract = inkClient.getContract(contracts.ink_cross, contract_address);

  const originAddress = account.address;
  const result = await p2pContract.query("total_supply", { origin: originAddress });

  if (result.success) {
    return result.value.response as bigint;
  }
}, [client, initializeClient]);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;You’re using &lt;code&gt;p2pContract.query()&lt;/code&gt; to ask the contract for data.&lt;/li&gt;
&lt;li&gt;These are “read-only” calls.&lt;/li&gt;
&lt;li&gt;They don’t need signing, don’t cost gas, and don’t change state.&lt;/li&gt;
&lt;li&gt;You only pass: &lt;code&gt;origin&lt;/code&gt;: who’s asking (for context) and &lt;code&gt;data&lt;/code&gt;: optional arguments (like owner, spender), it depends on the argument the deployed function takes&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: If you only need to see something, like balance, allowance, name, symbol, then you should use &lt;code&gt;.query&lt;/code&gt;.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Example 2: &lt;code&gt;.send&lt;/code&gt; (Transactions)
Now check this one, your transferToken function:
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const transferToken = useCallback(
  async (to: Binary, amount: bigint, account: WalletAccount) =&amp;gt; {
    const cl = client ?? (await initializeClient());
    const inkClient = createInkSdk(cl);
    const pspContract = inkClient.getContract(contracts.ink_cross, contract_address);

    const signer = await getPolkadotSigner(account);
    if (signer) {
      const result = await pspContract
        .send("transfer", {
          origin: account.address,
          data: { to, value: amount },
        })
        .signAndSubmit(signer);

      if (result.ok) {
        console.log("Transfer successful:", result);
      } else {
        console.error("Transfer failed:", result);
      }
    }
  },
  [client, initializeClient]
);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;ul&gt;
&lt;li&gt;used .send("transfer", …) instead of .query.&lt;/li&gt;
&lt;li&gt;Then called .signAndSubmit(signer),this means:&lt;/li&gt;
&lt;li&gt;You sign the transaction (proving ownership of the account)&lt;/li&gt;
&lt;li&gt;You submit it to the blockchain for inclusion in a block.&lt;/li&gt;
&lt;li&gt;This actually moves tokens from one account to another.&lt;/li&gt;
&lt;li&gt;It costs gas.&lt;/li&gt;
&lt;li&gt;It changes the blockchain state (balances update).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Note: If the function changes something (transfer, approve, mint, burn, etc.), use .send and .signAndSubmit.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;So When You’re Writing a New Function
You can now follow this logic every time; it's the same pattern
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"use client";

import { useWallet } from "./wallet-provider";
import { createInkSdk, initializeClient } from "@polkadot-api/ink";
import { getPolkadotSigner } from "@polkadot-api/polkadot-signer";
import contracts from "@/contracts.json"; // JSON mapping of contract names to metadata
import { contract_address } from "@/config";

export const useInkClient = () =&amp;gt; {
  const { client, account } = useWallet();

  // Step 1: Connect to the chain client
  const connectToClient = async () =&amp;gt; {
    // If the client from context already exists, reuse it
    const cl = client || (await initializeClient());

    // Create an ink! SDK instance, which provides access to contracts
    const inkClient = createInkSdk(cl);

    // Access our deployed contract by name and address
    // - `contracts.ink_cross` comes from your contracts.json file
    // - `contract_address` is the deployed address of your ink! contract
    const myContract = inkClient.getContract(contracts.ink_cross, contract_address);

    return { myContract };
  };

  // Step 2a: Perform a read (query) operation
  const readAllowance = async (owner: string, spender: string) =&amp;gt; {
    const { myContract } = await connectToClient();

    // A query does not change the blockchain state — it just reads data
    // origin → the account making the query (must be connected)
    // data → arguments your contract function expects
    const result = await myContract.query("allowance", {
      origin: account.address,
      data: { owner, spender },
    });

    // The return value is fetched directly from the contract without gas or fees
    return result;
  };

  // Step 2b: Perform a write (send) operation
  const approveSpender = async (spender: string, value: bigint) =&amp;gt; {
    const { myContract } = await connectToClient();

    // A write (send) changes the contract state — it requires a signer
    const signer = await getPolkadotSigner(account);

    // send() submits a transaction to the blockchain
    // origin → the user's wallet address sending the transaction
    // data → arguments to the contract function
    // .signAndSubmit() actually signs and broadcasts the transaction
    await myContract
      .send("approve", {
        origin: account.address,
        data: { spender, value },
      })
      .signAndSubmit(signer);
  };

  return { readAllowance, approveSpender };
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;h2&gt;
  
  
  The Helpers(This file is inside the utils folder
&lt;/h2&gt;

&lt;p&gt;)&lt;/p&gt;

&lt;p&gt;It ties together address encoding, signers, and data conversions that our ink! client depends on.&lt;/p&gt;

&lt;p&gt;Here’s the real code, fully annotated&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import { decodeAddress, keccak256AsU8a } from "@polkadot/util-crypto";
import { u8aToHex } from "@polkadot/util";
import { connectInjectedExtension } from "@polkadot-api/pjs-signer";
import type { WalletAccount } from "@talismn/connect-wallets";

/**
 *  truncateText
 * Utility for shortening long strings like wallet addresses for display.
 * Example: truncateText("5GrwvaE...", 6, 4) → "5Grwva...kutQY"
 */
export function truncateText(text: string, start: number, end: number): string {
  if (!text || text.length &amp;lt; 1) return "";
  if (start &amp;lt; 0 || start &amp;gt;= text.length || end &amp;gt;= text.length) {
    throw new Error("Invalid start or end values");
  }
  return `${text.slice(0, start)}...${text.slice(text.length - end)}`;
}

/**
 *  decodeU128
 * Converts a U128 value (which can come as an array of four 64-bit parts)
 * back into a single bigint. This is helpful for decoding contract values
 * returned as arrays by ink! (like balances or totalSupply).
 */
export function decodeU128(words: bigint[] | bigint): bigint {
  if (typeof words === "bigint") return words; // already normalized
  if (!Array.isArray(words)) throw new Error("Unexpected type");

  return words[0] + (words[1] &amp;lt;&amp;lt; 64n) + (words[2] &amp;lt;&amp;lt; 128n) + (words[3] &amp;lt;&amp;lt; 192n);
}

/**
 * convertSS58toHex
 * Converts a standard Substrate/Polkadot SS58 address
 * into a 20-byte Ethereum-compatible hex string.
 *
 * Used when interacting with ink! PSP22 tokens on Polkadot chains
 * because ink! expects Binary (0x...) formatted addresses.
 */
export function convertSS58toHex(address: string): string {
  //  Decode the Polkadot (SS58) address into raw public key bytes
  const pubKey = decodeAddress(address);

  //  Hash it using keccak256 to mimic Ethereum-style address derivation
  const pubKeyDigest = keccak256AsU8a(pubKey);

  //  Take the last 20 bytes (standard Ethereum address size)
  const hexAddress = pubKeyDigest.slice(-20);

  //  Convert to readable hex string
  console.log({ hexAddress, textHexAddress: u8aToHex(hexAddress) });
  return u8aToHex(hexAddress);
}

/**
 *  getPolkadotSigner
 * Connects to the injected wallet extension (e.g., Polkadot.js or Talisman)
 * and retrieves the signer for the currently selected account.
 *
 * The signer is what actually signs transactions on-chain.
 */
export async function getPolkadotSigner(account: WalletAccount) {
  // Connect to the injected extension (defaults to polkadot-js)
  const signers = await connectInjectedExtension(
    account.wallet?.extensionName || "polkadot-js"
  );

  console.log({ signers });

  // Find the signer matching our connected account
  const signer = signers
    .getAccounts()
    .find(({ address }) =&amp;gt; address == account.address);

  // Return the signer object (used in .signAndSubmit)
  return signer?.polkadotSigner;
}

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Let’s say you’re calling:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;await inkCtx.transferToken(
  Binary.fromHex(convertSS58toHex(transferRecipient)), // uses helper
  BigInt(transferAmount),
  selectedAccount
);

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;convertSS58toHex()&lt;/code&gt; makes sure your recipient address is in the correct format for ink! contracts.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;selectedAccount&lt;/code&gt; comes with a signer fetched via &lt;code&gt;getPolkadotSigner()&lt;/code&gt;, which authenticates the transaction through the connected wallet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Bringing It All Together
&lt;/h2&gt;

&lt;p&gt;Now that the contexts (WalletContext, InkClientContext) and contract interaction functions are ready, it’s time to build the main page that connects everything together.&lt;/p&gt;

&lt;p&gt;This &lt;code&gt;page.tsx&lt;/code&gt; file acts as the interface between the user and the ink! smart contract, handling token transfers, approvals, minting, burning, and deployment, all powered by our SDK and contexts.&lt;/p&gt;

&lt;p&gt;Let’s break down how it works &lt;/p&gt;

&lt;p&gt;Here’s the complete UI flow in action:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;User connects a wallet (handled globally in WalletContext).&lt;/li&gt;
&lt;li&gt;Token data loads automatically via useEffect.&lt;/li&gt;
&lt;li&gt;Each section has its own handler (transfer, approve, mint, burn, deploy).&lt;/li&gt;
&lt;li&gt;Toasts show live transaction feedback.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Balance and supply refresh dynamically after each successful transaction.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;page.tsx&lt;/code&gt; file uses:&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;WalletContext → manages connected wallet and selected account.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;InkClientContext → gives access to contract methods (transferToken, approveToken, mintToken, etc.).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;react-hot-toast → shows status updates for blockchain transactions.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;useEffect → refetches token info whenever the wallet changes.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Example: Token Transfer Handler&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;// Function to handle token transfer between two accounts
const handleTransferToken = async () =&amp;gt; {
  // Ensure a wallet is connected and the transfer function exists
  if (!selectedAccount || !inkCtx?.transferToken) return;

  // Set the loading state for the transfer button and show a loading toast
  setLoadingStates(prev =&amp;gt; ({ ...prev, transfer: true }));
  toast.loading("Preparing transfer...", { id: "transfer" });

  try {
    // Execute the transfer using the ink! client context

    await inkCtx.transferToken(
      //  Recipient Address
      // The recipient can come in two forms:
      //  - If it's already a hex (starts with "0x"), use it directly.
      //  - Otherwise, it's an SS58 address (like from Polkadot.js), so convert it.
      transferRecipient.startsWith("0x")
        ? Binary.fromHex(transferRecipient)
        : Binary.fromHex(convertSS58toHex(transferRecipient)),

      //  Amount
      // The amount entered in the input is converted to a BigInt
      // because blockchain values (like balances and token amounts)
      // must be represented as big integers to prevent rounding errors.
      BigInt(transferAmount || "0"),

      //  Account
      // The selected wallet account — this acts as the "origin"
      // (the sender of the transaction) and determines who signs it.
      selectedAccount
    );

    // If no error, the transfer succeeded
    toast.success("Transfer successful!", { id: "transfer" });

    //  Refresh user data to reflect the updated balance
    await refreshUserData();

  } catch (err) {
    // Catch any failure, invalid address, insufficient funds, RPC error, etc.
    toast.error(
      err instanceof Error ? err.message : "Transfer failed",
      { id: "transfer" }
    );
  } finally {
    // Reset loading state no matter what
    setLoadingStates(prev =&amp;gt; ({ ...prev, transfer: false }));
  }
};

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is just a snippet of a function from the &lt;code&gt;page.tsx&lt;/code&gt; file with an inline explanation. Complete code can be found &lt;a href="https://github.com/Rampop01/psp_token-frontend/blob/master/src/app/page.tsx" rel="noopener noreferrer"&gt;here&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Conclusion
&lt;/h2&gt;

&lt;p&gt;Everything shared here came directly from what I learned during my journey building and understanding how ink! Smart contracts interact with the frontend.&lt;/p&gt;

&lt;p&gt;There are definitely many approaches out there, more abstracted SDKs, different wallet connectors, and even UI frameworks that simplify everything.&lt;br&gt;
But I decided to go through this process step by step to see how things work under the hood, from wallet connections to contract queries and signed transactions.&lt;/p&gt;

&lt;p&gt;My goal wasn’t to show the only way, but to share one working approach that I believe helps new developers understand what’s really happening when your frontend talks to the blockchain.&lt;/p&gt;

&lt;p&gt;If you’re new to Polkadot or ink!, I hope this helped you gain clarity on how data flows from the UI to the chain and back,&lt;br&gt;
and how &lt;code&gt;.query()&lt;/code&gt; and &lt;code&gt;.send()&lt;/code&gt; play their unique roles in that communication.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;For me personally, moving forward, I’ll be using ready-made templates and tools that speed up the process,&lt;br&gt;
but I’ll always appreciate the foundation this gave me.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;If you’d like to explore this full project, you can clone the complete repository and play around with it here:&lt;br&gt;
The frontend: &lt;a href="https://github.com/Rampop01/psp_token-frontend/tree/master" rel="noopener noreferrer"&gt;https://github.com/Rampop01/psp_token-frontend/tree/master&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The smartcontract: &lt;a href="https://github.com/Rampop01/cross-contract-unit-testing" rel="noopener noreferrer"&gt;https://github.com/Rampop01/cross-contract-unit-testing&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Try changing things, running queries, or sending transactions. It’s one of the best ways to really learn by doing.&lt;/p&gt;

&lt;p&gt;If this article helped you understand how to structure your frontend and interact with ink! contracts more confidently, especially as a beginner, then it’s done its job.&lt;br&gt;
Thanks for reading through my first ink! project, and see you on-chain.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>web3</category>
      <category>beginners</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
