Part 2
Introduction
Welcome back to our Poet Chain X series! In Part 1, we built the complete smart contract logic for our poetry auction platform. Now it's time to bring it to life by deploying to a test network and building a user interface to interact with it.
In this part, we'll cover:
- Ink!v6, Revive pallet, Account mapping
- Building and deploying the contract to Paseo testnet
- Interacting with the contract using Polkadot API (PAPI)
- Building a Next.js frontend with the ink! SDK
Prerequisites
Before starting, ensure you have:
- Completed Part 1 (contract code ready)
-
cargo-contract
CLI installed - A Polkadot.js wallet extension with test accounts
- Node.js 18+ and pnpm installed
- Basic familiarity with React/Next.js
Ink!v6, Revive, Account Mapping
What Changed in ink! v6?
ink! v6 introduced a significant architectural change by targeting the pallet-revive
instead of the older pallet-contracts
. This change was made to achieve compatibility with EVM contracts while maintaining ink!'s Rust-based development experience.
Key Differences:
-
Addresses: Contracts now use Ethereum-style 20-byte addresses (like
0x6f38a07b...
) instead of 32-byte SS58 addresses -
File Format: Contract blobs now use
.polkavm
format instead of.wasm
- Account Mapping: Accounts must be explicitly mapped before any contract interaction
What is "Mapping" and Why Does it Matter?
In blockchains using pallet-revive
(which enables ink! and Solidity contracts in Substrate chains), accounts come in two flavors:
-
Substrate (native) accounts — 32-byte identifiers in the standard Polkadot/Substrate format (SS58 addresses like
5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
) -
Ethereum-style addresses — 20-byte (H160) addresses used in the EVM/contract world (like
0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc
)
Mapping is the process of linking a native Substrate account to an Ethereum-style address (H160). It doesn't issue a new keypair or change your identity it simply registers the association on-chain via a map_account
transaction.
Why is This Needed?
The contract layer (pallet-revive
) expects addresses in Ethereum format when performing:
- Contract calls
- Contract instantiations
- Gas accounting
- Event emission
By mapping your account, you tell the runtime: "Yes, this Substrate account is allowed to interact in the contract world under this corresponding H160 address."
Without mapping, any attempt by a native account to deploy or call a contract will be rejected with DispatchError::AccountUnmapped
. This applies to all contract operations, including read-only queries (dry-runs).
Building the Contract
Step 1: Compile the Contract
From your project root directory:
cargo contract build --release
This generates three important files in target/ink/
:
-
poet_chain_x.polkavm
- The deployable contract blob -
poet_chain_x.json
- Contract metadata with ABI -
poet_chain_x.contract
- Combined metadata and code
Step 2: Verify Build Output
Check the build was successful:
ls -lh target/ink/
You should see your .polkavm
file (the compiled contract) and .json
metadata file.
Deploying to Paseo Testnet
Step 3: Get Test Tokens
Before deploying, you need PAS tokens:
- Visit the Paseo Faucet
- Select "Paseo Asset Hub" network
- Enter your wallet address
- Request tokens
Step 4: Map Your Account
Before deploying or interacting with contracts, you must map your account. You can do this using the Polkadot.js Apps interface:
- Connect to Paseo Asset Hub
- Navigate to Developer → Extrinsics
- Select your account
- Choose
revive → mapAccount()
- Submit the transaction
Step 5: Deploy Using cargo-contract
cargo contract instantiate \
--constructor new \
--args "Roses are red, violets are blue" 100 \
--suri //Alice \
--url wss://testnet-passet-hub.polkadot.io \
--execute
Breaking down the command:
-
instantiate
: Deploy and initialize the contract -
--constructor new
: Call thenew
constructor -
--args
: Pass poem text and duration (100 blocks) -
--suri //Alice
: Use Alice's test account -
--url
: Connect to Paseo testnet -
--execute
: Actually perform the deployment (remove for dry-run)
Output:
Event Instantiated
deployer: 5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
contract: 0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc
Code hash 0x7cfc118b19d5e0a224ae0932a25986c439cec72dfc096511f74e549a4bcc03ff
Save your contract address! You'll need it for all future interactions.
Interacting with the Contract via CLI
Let's test our deployed contract using the cargo contract
CLI.
Reading Contract State
Query the stored poem:
cargo contract call \
--contract 0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc \
--message get_poem \
--suri //Alice \
--url wss://testnet-passet-hub.polkadot.io
Check auction info (current block, end block, active status):
cargo contract call \
--contract 0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc \
--message get_auction_info \
--suri //Alice \
--url wss://testnet-passet-hub.polkadot.io
Placing a Bid
cargo contract call \
--contract 0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc \
--message bid \
--suri //Bob \
--value 1000000000000 \
--url wss://testnet-passet-hub.polkadot.io \
--execute \
--skip-confirm
Important: The --value
is in Planck (smallest unit). The above sends 1 PAS token.
Ending the Auction
After the auction duration has passed:
cargo contract call \
--contract 0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc \
--message end_auction \
--suri //Alice \
--url wss://testnet-passet-hub.polkadot.io \
--execute
Using the Bash Test Script
The repository includes test_auction.sh
for automated testing. Update the contract address:
#!/bin/bash
CONTRACT="0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc" # Your deployed address
# The script then runs through the complete auction flow
Run it:
chmod +x test_auction.sh
./test_auction.sh
Building the Frontend
Now let's create a web interface using Next.js and the Polkadot API (PAPI) with the ink! SDK.
Step 1: Setup Next.js Project
npx create-next-app@latest poet-chain-x --typescript
cd poet-chain-x
Step 2: Install Dependencies
pnpm install @polkadot-api/sdk-ink polkadot-api @polkadot/extension-dapp
pnpm install @polkadot/api @polkadot/util @polkadot/util-crypto
pnpm install lucide-react @radix-ui/react-tabs @radix-ui/react-select
Step 3: Generate Type Definitions
This is the magic that gives you full TypeScript support:
# Generate chain types
pnpm papi add -w wss://testnet-passet-hub.polkadot.io passet
# Generate contract types from metadata
pnpm papi ink add ./contracts_p/poet_chain_x.json
This creates .papi/descriptors/dist.ts
with typed contract interfaces.
Understanding the ink! SDK
The ink! SDK provides a high-level API for contract interaction:
import { createInkSdk } from "@polkadot-api/sdk-ink"
import { createClient } from "polkadot-api"
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat"
import { getWsProvider } from "polkadot-api/ws-provider"
import { contracts } from '.papi/descriptors'
// Create client
const client = createClient(
withPolkadotSdkCompat(
getWsProvider("wss://testnet-passet-hub.polkadot.io")
)
)
// Create ink SDK
const inkSdk = createInkSdk(client)
Working with Contract Addresses
Checking if Account is Mapped
Before any interaction:
const isMapped = await inkSdk.addressIsMapped(accountAddress)
if (!isMapped) {
console.log("Account needs to be mapped first!")
// Show UI prompt to map account
}
Getting Contract Instance
const contractAddress = "0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc"
const poetContract = inkSdk.getContract(contracts.poet_chain_x, contractAddress)
// Verify contract compatibility
if (!(await poetContract.isCompatible())) {
throw new Error("Contract code hash has changed")
}
Querying Contract State
Queries are read-only operations that don't cost gas:
// Get the poem
const poemResult = await poetContract.query("get_poem", {
origin: accountAddress,
})
if (poemResult.success) {
console.log("Poem:", poemResult.value.response)
} else {
console.error("Query failed:", poemResult.value)
}
// Get auction info
const infoResult = await poetContract.query("get_auction_info", {
origin: accountAddress,
})
if (infoResult.success) {
const [currentBlock, endBlock, isActive] = infoResult.value.response
console.log(`Blocks remaining: ${endBlock - currentBlock}`)
console.log(`Active: ${isActive}`)
}
// Get winner
const winnerResult = await poetContract.query("get_winner", {
origin: accountAddress,
})
if (winnerResult.success) {
const [bidderOption, amount] = winnerResult.value.response
if (bidderOption) {
const bidderAddress = bidderOption.asHex()
console.log(`Highest bidder: ${bidderAddress}`)
console.log(`Amount: ${amount}`)
}
}
Sending Transactions
Transactions modify state and require signing:
import {
getInjectedExtensions,
connectInjectedExtension,
} from "polkadot-api/pjs-signer"
// Connect to wallet
const extensions = getInjectedExtensions()
const selectedExtension = await connectInjectedExtension(extensions[0])
const accounts = selectedExtension.getAccounts()
const polkadotSigner = accounts[0].polkadotSigner
// Place a bid
const bidAmount = 1.5 // PAS tokens
const bidInPlanck = BigInt(Math.floor(bidAmount * 1_000_000_000_000))
const bidResult = await poetContract
.send("bid", {
origin: accountAddress,
value: bidInPlanck,
})
.signAndSubmit(polkadotSigner)
if (bidResult.ok) {
console.log("Bid placed in block:", bidResult.block)
// Filter contract events
const events = poetContract.filterEvents(bidResult.events)
console.log("Contract events:", events)
} else {
console.error("Transaction failed:", bidResult.dispatchError)
}
Deploying from Frontend
You can deploy contracts directly from your app:
import { Binary } from "polkadot-api"
// Read contract file uploaded by user
const contractFile: File = ... // from file input
const arrayBuffer = await contractFile.arrayBuffer()
const codeBlob = new Uint8Array(arrayBuffer)
const code = Binary.fromBytes(codeBlob)
// Get deployer
const deployer = inkSdk.getDeployer(contracts.poet_chain_x, code)
// Estimate deployment address (optional)
const estimatedAddress = await deployer.estimateAddress("new", {
origin: accountAddress,
data: {
poem: poemText,
duration: durationBlocks,
},
})
console.log("Contract will be deployed at:", estimatedAddress)
// Dry run first
const dryRunResult = await deployer.dryRun("new", {
origin: accountAddress,
data: {
poem: poemText,
duration: durationBlocks,
},
})
if (!dryRunResult.success) {
console.error("Deployment dry-run failed")
return
}
// Actual deployment
const deployResult = await dryRunResult.value
.deploy()
.signAndSubmit(polkadotSigner)
// Get deployed address
const deploymentData = inkSdk.readDeploymentEvents(
contracts.poet_chain_x,
deployResult.events
)
console.log("Deployed at:", deploymentData[0].address)
console.log("Events:", deploymentData[0].contractEvents)
Complete Frontend Component Example
Here's the core auction manager component:
"use client"
import { useState, useEffect } from "react"
import { createInkSdk } from "@polkadot-api/sdk-ink"
import { createClient } from "polkadot-api"
import { withPolkadotSdkCompat } from "polkadot-api/polkadot-sdk-compat"
import { getWsProvider } from "polkadot-api/ws-provider"
import { contracts } from '@/.papi/descriptors'
export function AuctionManager({ account }: { account: string }) {
const [contractAddress, setContractAddress] = useState("")
const [poemText, setPoemText] = useState("")
const [currentBid, setCurrentBid] = useState("0")
const [highestBidder, setHighestBidder] = useState("")
const [isLoading, setIsLoading] = useState(false)
// Format PAS tokens from Planck
const formatPAS = (planck: string) => {
const pas = Number(planck) / 1_000_000_000_000
return pas.toFixed(4)
}
// Load auction data
const loadAuction = async () => {
if (!contractAddress) return
setIsLoading(true)
try {
const client = createClient(
withPolkadotSdkCompat(
getWsProvider("wss://testnet-passet-hub.polkadot.io")
)
)
const inkSdk = createInkSdk(client)
// Check if account is mapped
const isMapped = await inkSdk.addressIsMapped(account)
if (!isMapped) {
alert("Please map your account first using the Revive pallet")
return
}
const contract = inkSdk.getContract(contracts.poet_chain_x, contractAddress)
// Query poem
const poemResult = await contract.query("get_poem", { origin: account })
if (poemResult.success) {
setPoemText(poemResult.value.response)
}
// Query winner
const winnerResult = await contract.query("get_winner", { origin: account })
if (winnerResult.success) {
const [bidderOption, amount] = winnerResult.value.response
if (bidderOption) {
setHighestBidder(bidderOption.asHex())
}
setCurrentBid(amount.toString())
}
client.destroy()
} catch (error) {
console.error("Failed to load auction:", error)
alert("Error loading auction")
} finally {
setIsLoading(false)
}
}
// Place bid
const placeBid = async (bidAmount: string) => {
// Implementation shown above
}
return (
<div className="space-y-4">
<input
type="text"
placeholder="Contract Address"
value={contractAddress}
onChange={(e) => setContractAddress(e.target.value)}
className="w-full px-4 py-2 border rounded"
/>
<button
onClick={loadAuction}
disabled={isLoading}
className="px-4 py-2 bg-blue-500 text-white rounded"
>
{isLoading ? "Loading..." : "Load Auction"}
</button>
{poemText && (
<div className="p-4 bg-gray-100 rounded">
<h3 className="font-bold mb-2">Poem:</h3>
<p className="whitespace-pre-line">{poemText}</p>
</div>
)}
{currentBid !== "0" && (
<div className="p-4 border rounded">
<p>Current Bid: {formatPAS(currentBid)} PAS</p>
{highestBidder && (
<p className="text-sm text-gray-600 font-mono">
Bidder: {highestBidder}
</p>
)}
</div>
)}
</div>
)
}
Important Account Mapping Workflow
Here's a complete flow for handling account mapping in your app:
async function ensureAccountMapped(inkSdk: InkSdk, address: string) {
// Check if already mapped
const isMapped = await inkSdk.addressIsMapped(address)
if (!isMapped) {
console.log("Account not mapped. Requesting mapping transaction...")
// Show UI to user
alert(`Your account needs to be mapped before interacting with contracts.
Please submit a Revive.mapAccount() transaction.`)
// You can also programmatically submit the mapping transaction
// using typedApi.tx.Revive.map_account()
return false
}
return true
}
// Use before any contract interaction
const canInteract = await ensureAccountMapped(inkSdk, userAddress)
if (!canInteract) {
return // Show error or mapping instructions
}
Troubleshooting Common Issues
1. "Account not mapped" Error
Problem: Trying to interact with contract before mapping account.
Solution:
const isMapped = await inkSdk.addressIsMapped(account)
if (!isMapped) {
// Submit Revive.map_account() transaction
}
2. "Bid too low" Error
Problem: Bid amount not exceeding current highest bid.
Solution: Always fetch current bid first and ensure new bid is strictly greater:
const minimumBid = (currentBid / 1_000_000_000_000) + 0.0001 // Add minimum increment
3. Contract Address Format
Problem: Using SS58 address instead of Ethereum-style address.
Solution: ink! v6 contracts use 20-byte hex addresses:
- ❌
5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY
- ✅
0x6f38a07b338aed6b7146df28ea2a4f8d2c420afc
4. Transaction Fails Silently
Problem: Not checking transaction result properly.
Solution:
const result = await contract.send(...).signAndSubmit(signer)
if (result.ok) {
console.log("Success!")
} else {
console.error("Failed:", result.dispatchError)
}
Best Practices
1. Always Dry-Run First
Before submitting transactions, dry-run to estimate gas and check for errors:
const dryRun = await contract.query("bid", {
origin: account,
value: bidAmount,
})
if (dryRun.success) {
// Now send actual transaction
const tx = await contract.send("bid", { ... })
}
3. Cache Contract Metadata
Don't regenerate contract types on every build. Commit .papi/descriptors
to version control.
4. Use Proper Error Handling
try {
const result = await contract.query(...)
if (result.success) {
// Handle success
} else {
// Handle contract error
console.error("Contract error:", result.value)
}
} catch (error) {
// Handle network/SDK error
console.error("SDK error:", error)
}
Summary
In Part 2, we've covered:
✅ Ink! v6 and the Revive pallet
✅ Building and deploying contracts to Paseo testnet
✅ Critical importance of account mapping
✅ Interacting with contracts via cargo-contract CLI
✅ Building a full-featured frontend with PAPI ink! SDK
✅ Best practices for production deployment
Resources
- Polkadot API Documentation
- ink! SDK Documentation
- Paseo Testnet Faucet
- Polkadot.js Apps
- cargo-contract Documentation
What's Next?
If you enjoyed following along and want to see this project fully realized, here’s a little challenge for you: If this article gets 1,000 likes, we’ll actually build the full production ready Poet Chain X platform. That means a live auction site, interactive frontend, and everything you’ve learned here is ready for the real world.
And if you’ve tried building your own version or made tweaks to the contract or UI, drop a comment below and share your progress. I’d love to see what you’ve built!
Consider this your invitation to help turn code into reality!
Congratulations! You've now built a complete blockchain-powered poetry auction platform. You've learned contract development, deployment, and frontend integration on Polkadot. These skills transfer to any ink! smart contract project.
Have questions? The complete source code for both the contract and frontend is available in the GitHub repository.
Top comments (0)