How to create a faucet Dapp for ckBTC testnet token using react and Motoko

How to create a faucet Dapp for ckBTC testnet token using react and Motoko

Introduction

Building a token faucet dapp can be a great way to distribute tokens to users, promote your token, or provide tokens to developers for testing purposes. A token faucet dapp is a decentralized application that allows users to request a certain amount of tokens from a smart contract on the blockchain. In this article, we are going to look at how to create a ckBTC testnet token faucet on the Internet Computer(ICP) Blockchain. By the end of this article, you will have an understanding of how token faucets work, and how to create tokens on the ICP Blockchain

Prerequisites

  • Prior knowledge of front-end development using HTML, CSS, Javascript, and React
  • dfx installed on your machine
  • Prior knowledge of the command line or Terminal
  • Code editors like VsCode or sublime Text
  • Internet connection
  • Internet Browser

What is DFX

It is a command line execution environment for creating, deploying, and managing dapps on the Internet Computer. It also provides a local execution environment to test and deploy dapps before deploying them on the live network

What is internet identity

Internet Identity in the Internet Computer ecosystem is a decentralized, open-source identity system that allows users to create and manage their digital identities. It is built on top of the Internet Computer blockchain, which provides a secure and immutable ledger for storing user identity information.

With Internet Identity, users can create a unique digital identity that is linked to their public key on the blockchain. This identity can be used to authenticate and interact with various decentralized applications and services on the Internet Computer. Internet Identity also allows users to control their data and decide how it is shared with other parties.

Here is what we will build in this project and the code for this project can be found here

How the Dapp Works

  • This is a Dapp that allows the users to claim some testnet ckBTC tokens for their project, a user needs to login with their Internent Identity to be able to claim the tokens.

  • The user can only claim tokens once every 24 hours. For the first time, the user will get 0.0015 tckBTC and 0.0005 tckBTC tokens for all the other subsiquent times.

  • The Dapp allows the users to view the history of all their transations involving the testnet ckBTC.

  • Users can also transfer their tokens to other users.

Setting up the project

We are going to use a boiler template for this project in order to make this article not long. the template already has the Internet Identity(II) setup. If you want to learn how to set up the internet identity for your project from scratch, I recommend checking out this YouTube video that explains the whole process. With that in mind, the first thing to do is to open up your terminal.

  • Clone the repo.
    git clone https://github.com/sam-the-tutor/React-Motoko-II-Template.git
    
  • Navigate to the project folder
    cd React-Motoko-II-Template
    
  • Install the dependencies
    npm install
    
  • Start the replica and deploy the project
    dfx start --background
    dfx deploy
    
  • Start the development server
    npm start
    

You will be provided with a link usually http://localhost:3000/. Click on that link and the project will be opened in a browser. If everything is okay at this stage, you should see a page like the one below.

Click on the button to log in with your internet Identity, if you have already created one. Since it will be your first time using this project, you will be prompted to create a new identity on the local host to use for this project.

Follow the instructions to create one and you will be automatically logged in once the process is complete. On successful login, you will be presented with this page, that shows a logout button and the principal ID of your Identity.

Note: The reason why we set up the Internet Identity is because we want to stimulate real users when interacting with our test tokens. Otherwise, we would only be using the default anonymous identity which is sometimes not supported in some ledgers like the ICP ledger Ledger

All the configurations for the Internet Identity are in the use-auth-client.jsx file. In that file, we use the React useContext to make some of our functionality global to the project and we will use that functionality in our project

Setting up the canisters

ckBTC follows the ICRC1 token standard on the Internet Computer, and so in this section we will setup the ledger, and Index canister testnet token for ckBTC that follows the same standard.

Deploying the testnet ckBTC ledger canister

Open the project using your favorite code editor. Add the following canister configuration in the dfx.json file

"icrc1_ledger_canister": {
      "type": "custom",
      "candid": "https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icrc1/ledger/ledger.did",
      "wasm": "https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ic-icrc1-ledger.wasm.gz",
    }

The above code defines some settings for our token ledger canister, which include the canister name icrc1_ledger_canister as well as the source for both the wasm and candid files for the ledger canister

Open up the terminal and run the following code

dfx identity use minter
export MINTER=$(dfx identity get-principal)

export TOKEN_NAME="Testnet ckBTC"
export TOKEN_SYMBOL="tckBTC"

dfx canister create backend
export DEFAULT=$(dfx canister id backend)

In the terminal, we declare and save some variables to store the name and symbol of our token tckBTC, as well as the default and minter account for the ledger that we will use to deploy the canister.

export PRE_MINTED_TOKENS=10_000_000_000
export TRANSFER_FEE=10_000

dfx identity new archive_controller
dfx identity use archive_controller
export ARCHIVE_CONTROLLER=$(dfx identity get-principal)
export TRIGGER_THRESHOLD=2000
export NUM_OF_BLOCK_TO_ARCHIVE=1000
export CYCLE_FOR_ARCHIVE_CREATION=10000000000000

export FEATURE_FLAGS=true

Next, we declare some variables to store the values for the fee, and all the other information necessary for deployment

No, switch back to the identity that you will use to deploy the canister

dfx identity use default
dfx deploy icrc1_ledger_canister --specified-id mxzaz-hqaaa-aaaar-qaada-cai --argument "(variant {Init =
record {
     token_symbol = \"${TOKEN_SYMBOL}\";
     token_name = \"${TOKEN_NAME}\";
     minting_account = record { owner = principal \"${MINTER}\" };
     transfer_fee = ${TRANSFER_FEE};
     metadata = vec {};
     feature_flags = opt record{icrc2 = ${FEATURE_FLAGS}};
     initial_balances = vec { record { record { owner = principal \"${DEFAULT}\"; }; ${PRE_MINTED_TOKENS}; }; };
     archive_options = record {
         num_blocks_to_archive = ${NUM_OF_BLOCK_TO_ARCHIVE};
         trigger_threshold = ${TRIGGER_THRESHOLD};
         controller_id = principal \"${ARCHIVE_CONTROLLER}\";
         cycles_for_archive_creation = opt ${CYCLE_FOR_ARCHIVE_CREATION};
     };
 }
})"

The above code deploys the token ledger canister will all the settings from our previous step.

We will mint the initial tokens to the backend canister because it is the one that will be distributing those tokens to every user that claims them.

The minting account is the identity in our terminal, in this way we can mint more tokens to the backend canister in case the balance goes low. Learn more about token ledger canisters

Deploying the testnet ckBTC Index canister

In this section, we will deploy the token Index canister. We will use this canister to fetch transactions for individual accounts from our token ledger canister

In the dfx.json, add this code

"icrc1_index_canister": {
      "type": "custom",
      "candid": "https://raw.githubusercontent.com/dfinity/ic/d87954601e4b22972899e9957e800406a0a6b929/rs/rosetta-api/icrc1/index-ng/index-ng.did",
      "wasm": "https://download.dfinity.systems/ic/d87954601e4b22972899e9957e800406a0a6b929/canisters/ic-icrc1-index-ng.wasm.gz",
    }

We define the settings for our index canister

dfx deploy icrc1_index_canister --argument '(opt variant{Init = record {ledger_id = principal "mxzaz-hqaaa-aaaar-qaada-cai"}})'

Next, we run the code to deploy the index canister. In the code, we configure the index canister to connect to our ledger canister thats we deployed earlier on.

"backend": {
      "main": "backend/main.mo",
      "type": "motoko",
      "dependencies": ["icrc1_ledger_canister"]
    }

Finally, we edit the configurations for the backend canister in order to notify it that we want to import and use the icrc_ledger_canister in our Motoko code.

Writing the backend code in Motoko

In this section, we will write our code in Motoko for our project. In the main.mo inside the backend folder, paste the following code

import myToken "canister:icrc1_ledger_canister";
import Principal "mo:base/Principal";
import Nat "mo:base/Nat";
import Error "mo:base/Error";
import Result "mo:base/Result";
import HashMap "mo:base/HashMap";
import Time "mo:base/Time";
import Iter "mo:base/Iter";

We start by importing the token ledger canister and all the necessary packages to use in our code

actor backend{
  //all our code will go here
}

Nexr, we define an actor. This acts as an entry point for all our code. All the code will be written inside the curly brackets for the actor.

private var hasClaimed = HashMap.HashMap<Principal, Bool>(
    1,
    Principal.equal,
    Principal.hash,
  );

  private var lastClaim = HashMap.HashMap<Principal, Int>(
    1,
    Principal.equal,
    Principal.hash,
  );

   let waitingTime : Nat = 24 * 60 * 60 * 1000000000;

  var tokenAmount : Nat = 0;

  private type Result<T, E> = Result.Result<T, E>;

Next, we define some data stores using the HashMaps.

  • The hasClaimed stores the state whether a certain Principal Id is claiming the tokens for the first time or not.

  • The lastClaim stores the last known time when that specific Principal Id claimed the tokens.

 public func claimTokens(account : Principal) : async Result<Text, Text> {

    if (not Principal.isAnonymous(account)) {

      let claim = switch (hasClaimed.get(account)) {
        case null false;
        case (?v) v
      };

      if (claim) {

        tokenAmount := 50000;

        let lastTokenClaim : Int = switch (lastClaim.get(account)) {
          case null 0;
          case (?value) value
        };

        if (not (lastTokenClaim == 0)) {

          if ((lastTokenClaim + waitingTime) < Time.now()) {

            let transferResult = await transferTokens(tokenAmount, account);
            if (transferResult == #ok("success")) {
              lastClaim.put(account, (Time.now()));
              #ok("You have successfully claimed more 5000 tokens");

            } else {

              #err("Error in claiming tokens Try again later");

            }

          } else {

            #err("You need to wait for 24 hours before you can request more tokens.");

          }

        } else {
          #err("Error in determing eligibility")
        }

      } else {

        tokenAmount := 150000;

        let transferResult = await transferTokens(tokenAmount, account);
        if (transferResult == #ok("success")) {

          hasClaimed.put(account, true);
          lastClaim.put(account, (Time.now()));
          #ok("You have successfully claimed 15000 tokens for the first time.");

        } else {

          #err("Error in claiming tokens 2. Try again later");

        }

      }
    } else {
      #err("Use a valid Principal ID to claim tokens")
    }

  };

We define the claimTokens function. It takes in the Principal Id of the user performs three major checks to find out whether;

  • that Principal Id is the anonymous Id
  • that Principal Id has claimed tokens in the past, and the last time the Id might have claimed the tokens.

  • If its the user's first time to claim the tokens, they will receive 0.0015 tckBTC and if not they will receive just 0.0005 tckBTC tokens.

The user will not receive any tokens if the 24-hour period has not passed since they claimed the tokens

private func transferTokens(tokens : Nat, account : Principal) : async Result<Text, Text> {

  let result = await myToken.icrc1_transfer({

    amount = tokens;
    from_subaccount = null;
    created_at_time = null;
    fee = null;
    memo = null;
    to = {
      owner = account;
      subaccount = null
    };

  });

  switch (result) {

    case (#Err(transferError)) {
      #err("failure")
    };
    case (_) {
      #ok("success")
    };

  };

}

Next, we declare a private function claimTokens from where we call the ledger canister to transfer some tokens from the backend canister to the users Principal Id. The function simply returns a text to the user depending on the outcome of the transaction.

Persisting user data across upgrades

private stable var hasClaimedArray : [(Principal, Bool)] = [];
private stable var lastClaimArray : [(Principal, Int)] = [];

system func preupgrade() {
    hasClaimedArray := Iter.toArray(hasClaimed.entries());
    lastClaimArray := Iter.toArray(lastClaim.entries())
  };

  system func postupgrade() {
    hasClaimed := HashMap.fromIter<Principal, Bool>(
      hasClaimedArray.vals(),
      1,
      Principal.equal,
      Principal.hash,
    );
    lastClaim := HashMap.fromIter<Principal, Int>(
      lastClaimArray.vals(),
      1,
      Principal.equal,
      Principal.hash,
    )
  };

We declare two system function preupgrade and postupgrade that hepls us to persist data in our canister during any upgrades. In this way, we will keep track of all the users' activities at all times. Learn more about data persistence

Deploying the backend canister

in the terminal, run the command

dfx deploy backend

This will deploy the backend canister and on successful deployment, click on the link fo the backend to interact with the backend canister in the browser

All the code for the backend canister can be found here

Setting up the frontend

In this section, we will set up all the necessary files and write all the code for our frontend

Downloading the npm package

We need to install some package so as we interact with our ledger and token canisters with ease.

In your terminal, run the command

npm i @dfinity/agent @dfinity/candid @dfinity/principal @dfinity/utils @dfinity/nns-proto @dfinity/ledger-icrc

This will install the package as well as its peer dependencies including the ledger-icrc package that we are going to use.

FIle setup

In the components folder, create a new folder and name it ICRC. We will create three files in that foolder.

  • useIcrcLedger.jsx this file contains our custom react hook that allows us to fetch transactions and the balance of the logged in user
  • dashboard.jsx this file contains the code that displays the page when the user has logged in to the Dapp
  • functions.jsx stores the helper functions that we use in the our files

Inside the compoents folder in the Login.jsx file,paste the following code

import React from 'react'
import { useAuth } from '../use-auth-client'

const Login = () => {
  const { login } = useAuth()
  return (
    <div className="flex h-full w-full items-center justify-center bg-slate-500 shadow-lg border">
      <div>
        <div className="flex flex-col gap-4 items-center">
          <h1 className="flex text-6xl text-red-400">
            Test ckBTC Token Faucet
          </h1>
          <img height="50px" width="50px" src="../assets/ckbtc.png" />
          <p className="text-white">
            Claim your ckBTC test tokens to start developing on the Internet
            Computer
          </p>
          <button
            onClick={login}
            className="border p-2 rounded-lg text-white text-md shadow shadow-black mt-6 flex gap-2 justify-center items-center"
          >
            <img
              height="30px"
              width="30px"
              src="https://cdn.discordapp.com/attachments/950584476658962473/1174985332710723635/ckbtc.png"
            />
            LOGIN
          </button>
        </div>
      </div>
    </div>
  )
}

export default Login

We import useAuth method from the use-auth-client file. This file contains all the logic for the internet Identity. The useAuth method exposes a way on how to get the information of the identity of the user in our Dapp

We declare a react component Login that displays a button to allow the user login to the Dapp using their Internet Identity. Once the user has logged in, they are redirected to the dashboard

Paste the followind code in the useIcrcLedger.jsx file

import React, { useEffect, useState } from 'react'
import { IcrcLedgerCanister, IcrcIndexCanister } from '@dfinity/ledger-icrc'
import { useAuth } from '../../use-auth-client'
import { canisterId as ICLedgerID } from '../../declarations/icrc1_ledger_canister'
import { canisterId as ICIndexID } from '../../declarations/icrc1_index_canister'
import { createAgent } from '@dfinity/utils'
import { Principal } from '@dfinity/principal'
import { transformIcpData } from './functions'

const useIcrcLedger = () => {
  const { identity, principal, changes, agent } = useAuth()
  const [ICRCLegder, setICRCLedger] = useState(null)
  const [ICRCIndex, setICRCIndex] = useState(null)
  const [userBalance, setUserBalance] = useState(null)
  const [userTransactions, setUserTransactions] = useState(null)

  useEffect(() => {
    setUpCanisters()
  }, [identity, changes])

  async function setUpCanisters() {
    try {
      const ledgerActor = IcrcLedgerCanister.create({
        agent,
        canisterId: ICLedgerID,
      })

      const indexActor = IcrcIndexCanister.create({
        agent,
        canisterId: ICIndexID,
      })

      const balance = await ICRCLegder?.balance({
        certified: false,
        owner: principal,
      })

      setUserBalance(Number(balance) / 1e8)
      const trans = await indexActor?.getTransactions({
        max_results: 20,

        account: {
          owner: principal,
          subaccount: null,
        },
      })

      const formatedTrans = transformIcpData(trans.transactions)
      setICRCLedger(ledgerActor)
      setICRCIndex(indexActor)
      setUserTransactions(formatedTrans)
    } catch (error) {
      console.log('error in fetching user balance :', error)
    }
  }

  return { ICRCLegder, userBalance, userTransactions }
}

export default useIcrcLedger

In the above code, we import the necessary packages to use in our component. We also import the canister Ids of the Ledger and Index canisters

Inside the component, we initialize some states to hold our data.

In the setUpCanisters function, using the ledger-icrc package, we initialize actors for the ckBTC Ledger and Index`` canisters, Next,we fetch the accountbalanceof the logged in user from the ledger canister. We also fetch all thetransactions` of the logged in user from the Index canister.

We finalize by updating the states with the relevant information and exporting those states

Paste the code in the dashboard.js file

import React, { useEffect, useState } from 'react'
import useIcrcLedger from './useIcrcLedger'
import { useAuth } from '../../use-auth-client'
import { Principal } from '@dfinity/principal'
import { TransactionTable } from './functions'

const Dashboard = () => {
  const { ICRCLegder, userBalance, userTransactions } = useIcrcLedger()
  const { logout, principal, backendActor, setChanges } = useAuth()
  const [formData, setData] = useState({
    principal_id: '',
    transfer_amount: '',
  })

  //allow the user to claim some testnet ckBTC tokens
  async function claimTokens() {
    try {
      //call the backend actor
      const results = await backendActor.claimTokens(principal)
      if (results.ok) {
        alert('claim successful')
      } else {
        alert('claim has failed')
      }
    } catch (error) {
      console.log('error in claiming tokens :', error)
    }
    setChanges(Math.random())
  }

  //transfer the tokens to another user
  async function transferTokens() {
    try {
      if (formData.principal_id && formData.transfer_amount) {
        const transferResults = await ICRCLegder?.transfer({
          to: {
            owner: Principal.fromText(formData.principal_id),
            subaccount: [],
          },
          fee: 10000,
          memo: [],

          from_subaccount: undefined,
          created_at_time: undefined,
          amount: Number(formData.transfer_amount) * 1e8,
        })
      }
    } catch (error) {
      console.log('error :', error)
    }

    setChanges(Math.random())
  }

  const handleChange = (e) => {
    const { name, value } = e.target
    setData({
      ...formData,
      [name]: value,
    })
  }

  return (
    <div className="flex flex-col h-full w-full justify-center bg-slate-500 shadow-lg border">
      <div className="text-white">
        <h2 className="text-4xl">Principal ID</h2>
        <span>{principal.toString()}</span>
      </div>
      <div className="flex gap-4 justify-center mt-20">
        {/* left side for the claim */}
        <div className="flex flex-col gap-16 border border-white shadow-md p-4 shadow-black">
          <button
            onClick={() => claimTokens()}
            className="border flex gap-1 items-center justify-center text-white p-2 rounded-lg text-md shadow shadow-black mt-6 "
          >
            Claim
            <img
              height="20px"
              width="20px"
              src="https://cdn.discordapp.com/attachments/950584476658962473/1174985332710723635/ckbtc.png"
            />
          </button>

          <div className="flex flex-col gap-4">
            <input
              type="text"
              placeholder="enter principal id"
              className="rounded-md"
              name="principal_id"
              id="principal_id"
              required
              onChange={handleChange}
            />
            <input
              type="number"
              name="transfer_amount"
              id="transfer_amount"
              onChange={handleChange}
              className="rounded-md"
              required
              placeholder="enter amount to trasfer"
            />
            <button
              onClick={() => transferTokens()}
              className="border flex gap-1 items-center justify-center text-white p-2 rounded-lg text-sm shadow shadow-black"
            >
              Transfer
              <img
                height="20px"
                width="20px"
                src="https://cdn.discordapp.com/attachments/950584476658962473/1174985332710723635/ckbtc.png"
              />
            </button>
          </div>
        </div>
        {/* right side for the transactions */}
        <div className="flex flex-col gap-2 border border-white shadow-md p-4 shadow-black w-2/4">
          <div className="flex justify-between">
            <div className="text-white">
              Balance :{' '}
              {!userBalance ? (
                <span>0 tckBTC</span>
              ) : (
                <span>{userBalance} tckBTC</span>
              )}
            </div>
            <button
              onClick={logout}
              className="border p-2 rounded-lg bg-red-300 text-sm shadow shadow-black"
            >
              Logout
            </button>
          </div>
          {/* div to include the table for the transactions */}
          <div>
            <TransactionTable transactions={userTransactions} />
          </div>
        </div>
      </div>
    </div>
  )
}

export default Dashboard

The claimTokens function allows the user to claim the testnet ckBTC tokens from the backend canister. It alerts the user in case of success or failure. The transferTokens function allows the user to send their tokens to another user from the dashboard

Finally, we display an interface that displays the transaction history of the user as well as a button to claim the tokens

Paste the following code in the functions.jsx

import React from 'react'
export function shortenString(str) {
  if (str.length <= 11) {
    return str
  } else {
    let firstPart = str.substring(0, 5)
    let lastPart = str.substring(str.length - 3)
    return `${firstPart}...${lastPart}`
  }
}

export function unixToDate(unixTimestamp) {
  // Convert nanoseconds to seconds
  var unixTimestampSeconds = unixTimestamp / 1000000000

  // Convert Unix timestamp to JavaScript time
  var date = new Date(unixTimestampSeconds * 1000)

  // Extract date and time
  var dateString = date.toLocaleDateString('en-US')
  var timeString = date.toLocaleTimeString('en-US')
  return dateString + ' ' + timeString
}

export function transformIcpData(dataArray) {
  var userTransactions = []
  for (const data of dataArray) {
    const { transaction } = data
    console.log(transaction)

    var transKind
    var transAmount
    var transFrom
    var transTo
    var timeStamp
    if (transaction?.kind === 'approve') {
      transKind = 'Approve'
      transAmount = transaction.approve[0].amount
      transFrom = transaction.approve[0].from.owner.toString()
      transTo = transaction.approve[0].spender.owner.toString()
      timeStamp = transaction.timestamp
    } else if (transaction?.kind === 'mint') {
      transKind = 'Mint'
      transAmount = transaction.mint[0].amount
      transFrom = 'minting account'
      transTo = transaction?.mint[0].to.owner.toString()
      timeStamp = transaction.timestamp
    } else if (transaction?.kind === 'burn') {
      transKind = 'Burn'
      transAmount = transaction.burn[0].amount
      transTo = 'minting account'
      transFrom = transaction.burn[0].from.owner.toString()
      timeStamp = transaction.timestamp
    } else if (transaction?.kind === 'transfer') {
      transKind = 'Transfer'
      transAmount = transaction.transfer[0].amount
      transTo = transaction.transfer[0].to.owner.toString()
      transFrom = transaction.transfer[0].from.owner.toString()
      timeStamp = transaction.timestamp
    }
    userTransactions.push({
      kind: transKind,
      amount: Number(transAmount) / 1e8,
      from: transFrom,
      to: transTo,
      timestamp: timeStamp,
    })
  }

  return userTransactions
}

export const TransactionTable = ({ transactions }) => {
  return (
    <>
      {transactions?.length > 0 ? (
        <div className=" overflow-x-auto h-64">
          <table className="block w-full text-md text-center  dark:text-gray-400 table-fixed">
            <thead>
              <tr className="text-white bg-slate-400">
                <th className="px-6 py-3">Amount</th>
                <th className="px-6 py-3">From</th>
                <th className="px-6 py-3">Kind</th>
                <th className="px-6 py-3">Timestamp</th>
                <th className="px-6 py-3">To</th>
              </tr>
            </thead>
            <tbody className="overflow-y-scroll h-32">
              {transactions.map((transaction, index) => (
                <tr
                  key={index}
                  className="bg-white dark:bg-gray-800 even:bg-red-300 odd:bg-white"
                >
                  <td className="px-6 py-4 w-3">
                    <div className="flex gap-1">
                      <img
                        height="20px"
                        width="20px"
                        src="https://cdn.discordapp.com/attachments/950584476658962473/1174985332710723635/ckbtc.png"
                      />
                      {transaction.amount}
                    </div>
                  </td>
                  <td className="px-6 py-4 w-4">
                    {shortenString(transaction.from)}
                  </td>
                  <td className="px-6 py-4 ">{transaction.kind}</td>
                  <td className="px-6 py-4 ">
                    {unixToDate(Number(transaction.timestamp))}
                  </td>
                  <td className="px-6 py-4">{shortenString(transaction.to)}</td>
                </tr>
              ))}
            </tbody>
          </table>
        </div>
      ) : (
        <div className="text-white bg-red-300">
          No transaction history for this user
        </div>
      )}
    </>
  )
}
  • The shortenString is a helper function that is used to shorten the length of the provided string and we wil use to shorten the length of the Principal ID text.

  • The unixToDate function formats the date from the the unix epoch format to local time format. transformIcpData transforms the transaction data from the Index canister.

  • The TransactionTable component is used to display the transactions history for the logged in user in a table format

Paste the following code in the App.jsx file

import React, { useEffect, useState } from 'react'
import './App.css'

import { AuthProvider, useAuth } from './use-auth-client'
import { BrowserRouter, Navigate, Route, Routes } from 'react-router-dom'
import Dashboard from './components/ICRC/dashboard'
import Login from './components/Login'
function App() {
  const { isAuthenticated } = useAuth()

  return <>{isAuthenticated ? <Dashboard /> : <Login />}</>
}

export default () => (
  <BrowserRouter>
    <AuthProvider>
      <App />
    </AuthProvider>
  </BrowserRouter>
)

Finally, we modify the App.jsx file to redirect the user to the appropriate page depending on whether the user is logged in or not

Installing tailwind css

Installing tailwind is a simple and straight forward process. Follow this guide from the official tailwind website to install it in our project

After installing tailwind css, we are going to do a few changes to our project.

In the App.css, delete everything and paste the following code

@tailwind base;
@tailwind components;
@tailwind utilities;

#root {
  margin: 0 auto;
  padding: 0;
  height: 100vh;
  text-align: center;
  font-family: Cambria, Cochin, Georgia, Times, 'Times New Roman', serif;
}

Delete the index.scss file as we will not use it in this project

Deploying the project

At this stage, we are ready to deploy the whole project. In the terminal, run the command to deploy the project on the local IC replica

dfx generate
dfx deploy

On successful deployment, this is how your project should look like in the browser.

The login page

If it is the first time the user is accessing the Dapp, they will see this page

After the user has done a couple of transactions

On the page, you can claim some testnet ckBTC tokens and one they are in your wallet, you can transfer them to other users.

Next steps

Right now, the project is deployed on the local network, you can deploy it on the main network so that even other users can make ue of it.

Conclusion

In this article, you have learnt how to create a faucet Dapp for ckBTC testnet token. You have also learnt how to use the ledger-icrc npm package in order to configure the Index and Legder canister. I will be glad to connect with you on Twitter