Building a dApp with Ink!
There are a few tutorials on building a web frontend for on Ink!, but probably the most relevant is use-Ink Examples. Updated resources are on use.ink.
1. Supercharge the incrementor
We will enhance our first smart contract by adding custom increments, reset functionality, unit tests and a basic frontend.
Follow the instructions of the previous chapter to create a new contract, but this time call it “incrementor”. When compilation is completed Replace lib.rs
with this upgraded code:
#![cfg_attr(not(feature = "std"), no_std, no_main)]
#[ink::contract]
mod incrementor {
#[ink(storage)]
pub struct Incrementor {
count: u32,
}
#[ink(event)]
pub struct Incremented {
new_count: u32,
}
#[ink(event)]
pub struct Reset {
reset_to: u32,
}
impl Incrementor {
#[ink(constructor)]
pub fn new(init_value: u32) -> Self {
Self { count: init_value }
}
#[ink(message)]
pub fn increment(&mut self, by: u32) {
self.count = self.count.checked_add(by).expect("Overflow in increment operation");
}
#[ink(message)]
pub fn get_count(&self) -> u32 {
self.count
}
#[ink(message)]
pub fn reset(&mut self) {
self.count = 0;
self.env().emit_event(Reset { reset_to: 0 });
}
}
#[cfg(test)]
mod tests {
use super::*;
#[ink::test]
fn new_works() {
let contract = Incrementor::new(0);
assert_eq!(contract.get_count(), 0);
}
#[ink::test]
fn increment_works() {
let mut contract = Incrementor::new(1);
contract.increment(1);
assert_eq!(contract.get_count(), 2);
contract.increment(2);
assert_eq!(contract.get_count(), 4);
}
#[ink::test]
fn reset_works() {
let mut contract = Incrementor::new(5);
contract.reset();
assert_eq!(contract.get_count(), 0);
}
}
}
As you can see, we can increment it by a specific amount and reset and test it.
2. Time to run the tests
Polkadot developers test in Rust, not HSpec – but the principles are familiar.
#same cargo workflow you already know
cargo test
Now deploy and instantiate it as learned, and test it with the playground. If successful, it’s time for frontend. Let’s build the web scaffold.
3. Frontend: From Rust to React
npx create-react-app incrementor
cd incrementor
npm install @polkadot/api @polkadot/api-contract @polkadot/extension-dapp
# npm install web-vitals if you have dependencies
Connect to Your Contract:
- Copy
target/incrementor.json
tosrc/
(this is your ABI) - Open your
src/App.js
and paste these contents:
import React, { useState, useEffect } from 'react';
import { ApiPromise, WsProvider } from '@polkadot/api';
import { ContractPromise } from '@polkadot/api-contract';
import { web3Accounts, web3Enable, web3FromAddress } from '@polkadot/extension-dapp';
import metadata from './incrementor.json';
function App() {
const [api, setApi] = useState(null);
const [contract, setContract] = useState(null);
const [account, setAccount] = useState(null);
const [signer, setSigner] = useState(null);
const [maxGasLimit, setMaxGasLimit] = useState(null);
const [count, setCount] = useState(0);
const [incrementBy, setIncrementBy] = useState(1);
const [_error, setError] = useState('');
// Initialize API and contract with error handling
useEffect(() => {
const connect = async () => {
try {
const wsProvider = new WsProvider('wss://rpc.shibuya.astar.network');
const api = await ApiPromise.create({
provider: wsProvider,
types: {
Address: 'AccountId',
LookupSource: 'AccountId',
}
});
const contract = new ContractPromise(
api,
metadata,
'YOUR_CONTRACT_ADDRESS' // Replace with actual address
);
setApi(api);
setMaxGasLimit(api.registry.createType('WeightV2', api.consts.system.blockWeights.maxBlock));
setContract(contract);
setError('');
} catch (err) {
setError(`Connection failed: ${err.message}`);
}
};
connect();
}, []);
// Updated connect wallet with error handling
const connectWallet = async () => {
try {
const extensions = await web3Enable('Incrementor DApp');
if (extensions.length === 0) {
throw new Error('No extension installed');
}
const accounts = await web3Accounts();
if (accounts.length === 0) {
throw new Error('No accounts found');
}
// finds an injector for an address
const injector = await web3FromAddress(accounts[0].address);
setSigner(injector.signer);
setAccount(accounts[0]);
setError('');
} catch (err) {
setError(`Wallet connection failed: ${err.message}`);
}
};
// Get current count
const getCount = async () => {
if (!contract) return;
const { result, output } = await contract.query.getCount(account.address, {
gasLimit: maxGasLimit,
storageDepositLimit: null
});
// check if the call was successful
if (result.isOk) {
console.log("Success", output.toHuman().Ok);
setCount(output.toHuman().Ok);
} else {
console.error("Error", result.asErr);
}
};
// Increment count
const increment = async () => {
if (!contract || !account) return;
const { gasRequired } = await contract.query.increment(
account.address,
{
gasLimit: maxGasLimit,
storageDepositLimit: null,
},
incrementBy
);
await contract.tx
.increment({ value: 0, gasLimit: api.registry.createType('WeightV2', gasRequired) }, incrementBy)
.signAndSend(account.address, { signer }, (result) => {
if (result.status.isInBlock) {
console.log('Transaction included in block');
getCount(); // Refresh count
}
});
};
// Reset count
const reset = async () => {
if (!contract || !account) return;
const { gasRequired } = await contract.query.reset(
account.address,
{
gasLimit: maxGasLimit,
storageDepositLimit: null,
}
);
await contract.tx
.reset({ value: 0, gasLimit: api.registry.createType('WeightV2', gasRequired) })
.signAndSend(account.address, { signer }, (result) => {
if (result.status.isInBlock) {
console.log('Reset transaction included in block');
getCount(); // Refresh count
}
});
};
return (
<div className="App">
<h1>Incrementor DApp</h1>
{!account ? (
<button onClick={connectWallet}>Connect Wallet</button>
) : (
<div>
<p>Connected: {account.meta.name}</p>
<div>
<h2>Current Count: {count}</h2>
<button onClick={getCount}>Refresh Count</button>
</div>
<div>
<input
type="number"
value={incrementBy}
onChange={(e) => setIncrementBy(Number(e.target.value))}
/>
<button onClick={increment}>Increment</button>
</div>
<div>
<button onClick={reset}>Reset</button>
</div>
</div>
)}
</div>
);
}
export default App;
- Substitute
YOUR_CONTRACT_ADDRESS
with the address of the deployed contract.
Everything should be ready now starting the web server
4. Launch Your dApp and play
npm start
Visit localhost:3000