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 account
balanceof the logged in user from the ledger canister. We also fetch all the
transactions` 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