WikiGetting hands onBuilding a dApp

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:

  1. Copy target/incrementor.json to src/ (this is your ABI)
  2. 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;
  1. 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