Skip to main content

10 posts tagged with "composer"

View All Tags
Go back

· 26 min read
Oluwafemi Alofe

header

Introduction

This comprehensive tutorial will guide you through setting up a crypto payment subscription platform on Celo. By the end of this tutorial, you will have a working subscription platform and the knowledge to customize and expand it for your unique needs. This tutorial will be broken up into four parts; you must follow them in order as they build upon one another.

Background Knowledge

In the web2 world, its common place to offer subscription services and allow your customer link a debit card so you can charge them periodically for the time of thier subscription. With the advert of stable curreny and merchant accepting them alongside exisiting payment method such as Paypal and card, thier needs to be full compatibilty of auto payment charge.

Prerequisites

To start building, you’ll need a basic understanding of web development, Node (v16), yarn, and Git.

  • Your computer has Node.js installed. If not download from here
  • Familiar with React/Nextjs

Requirements

For our project, we would be needing the following tools and framework

  • Celo Composer React App - for UI
  • Subgraph Packages - to index the data on the blockchain such that it's possible to query people's payments.
  • OpenZeppelin Defender Admin
  • Auto Task and Relayer

Github Code

For your reference, you can use the completed tutorial github code

Getting Started

To get started, we need to create our payment subscription Contract and UI with nextjs and tailwind CSS.

Install the celo composer to set up out development environment and create a new celo composer. On your terminal run these two commands;

npm i @celo/celocomposer -g
npx celo-composer-create

This will prompt you to select the framework and the template you want to use

After choosing the framework and the template, you'll be prompted to choose the smart contract development environment tool, decide whether or not to enable subgraph support, and give the project a name. Your terminal should seem like this at the end.

Open up your folder on your VS Code and run yarn install to install the dependencies, and yarn run react:app dey in your terminal to start our local environment. your web interface should look like this.

Next, we need to create the cards as seen on the Create a new file called PaymentCard.js in your component folder and add the following code inside

import React from "react";

export default function PaymentCard({ planName, price }) {
return (
<div className="flex flex-col max-w-lg p-6 mx-auto text-center text-gray-900 bg-white border border-gray-100 rounded-lg shadow dark:border-gray-600 xl:p-8 dark:bg-gray-800 dark:text-white">
<h3 className="mb-4 text-2xl font-semibold">{planName}</h3>
<p className="font-light text-gray-500 sm:text-lg dark:text-gray-400">
Best option for personal use &amp; for your next project.
</p>
<div className="flex items-baseline justify-center my-8">
<span className="mr-2 text-3xl font-extrabold">{price} cUSD</span>
<span className="text-gray-500 dark:text-gray-400">/month</span>
</div>

<ul role="list" className="mb-8 space-y-4 text-left">
<li className="flex items-center space-x-3">
<svg
className="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
></path>
</svg>
<span>Individual configuration</span>
</li>
<li className="flex items-center space-x-3">
<svg
className="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
></path>
</svg>
<span>No setup, or hidden fees</span>
</li>
<li className="flex items-center space-x-3">
<svg
className="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
></path>
</svg>
<span>
Team size: <span className="font-semibold">1 developer</span>
</span>
</li>
<li className="flex items-center space-x-3">
<svg
className="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fillRule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
></path>
</svg>
<span>
Premium support: <span className="font-semibold">6 months</span>
</span>
</li>
<li className="flex items-center space-x-3">
<svg
className="flex-shrink-0 w-5 h-5 text-green-500 dark:text-green-400"
fill="currentColor"
viewBox="0 0 20 20"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z"
clip-rule="evenodd"
></path>
</svg>
<span>
Free updates: <span className="font-semibold">6 months</span>
</span>
</li>
</ul>
<a
href="#"
className="text-white bg-purple-600 hover:bg-purple-700 focus:ring-4 focus:ring-purple-200 font-medium rounded-lg text-sm px-5 py-2.5 text-center dark:text-white dark:focus:ring-purple-900"
>
Get started
</a>
</div>
);
}

4 In your index.tx file , import your payment card, your final code should look like this.

import React, { useEffect, useState } from "react";
import PaymentCard from "../components/PaymentCard";

export default function Home() {
return (
<div className="space-y-8 lg:grid lg:grid-cols-3 sm:gap-6 xl:gap-10 lg:space-y-0">
<div className="flex">
<PaymentCard planName={"Basic"} price={2} />
</div>
<div className="flex">
<PaymentCard planName={"Premium"} price={5} />
</div>
<div className="flex">
<PaymentCard planName={"Enterprise"} price={12} />
</div>
</div>
);
}

Result below. However you can choose to add more cards to your own project and play around the styling, but for this tutorial we are just going to stick to three different payment plan

After this, click on your connect wallet button to see if it works, once it works its meant to show a disconnect button. However we also need to display the network its been connected to and the address of the wallet. So in your Header.tsx file, duplicate the button tag and add two more buttons to the header. Your final code should look like this

import { Disclosure } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import { useCelo } from "@celo/react-celo";
import Image from "next/image";
import { useEffect, useState } from "react";

export default function Header() {
let [componentInitialized, setComponentInitialized] = useState(false);
let { initialised, address, network, connect, disconnect } = useCelo();

useEffect(() => {
if (initialised) {
setComponentInitialized(true);
}
}, [initialised]);

return (
<Disclosure as="nav" className="bg-prosperity border-b border-black">
{({ open }) => (
<>
<div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
<div className="relative flex h-16 justify-between">
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center rounded-md p-2 text-black focus:outline-none focus:ring-1 focus:ring-inset focus:rounded-none focus:ring-black">
<span className="sr-only">Open main menu</span>
{open ? (
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
<div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex flex-shrink-0 items-center">
<Image
className="block h-8 w-auto lg:block"
src="/logo.svg"
width="24"
height="24"
alt="Celo Logo"
/>
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<a
href="#"
className="inline-flex items-center border-b-2 border-black px-1 pt-1 text-sm font-medium text-gray-900"
>
Home
</a>
</div>
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
{componentInitialized && address ? (
<>
<button
type="button"
className="mr-2 inline-flex content-center place-items-center rounded-full border border-wood bg-black py-2 px-5 text-md font-medium text-snow hover:bg-forest"
>
{network.name}
</button>
<button
type="button"
className="mr-2 inline-flex content-center place-items-center rounded-full border border-wood bg-black py-2 px-5 text-md font-medium text-snow hover:bg-forest"
>
{truncateAddress(address)}
</button>
<button
type="button"
className="inline-flex content-center place-items-center rounded-full border border-wood bg-black py-2 px-5 text-md font-medium text-snow hover:bg-forest"
onClick={disconnect}
>
Disconnect
</button>
</>
) : (
<button
type="button"
className="inline-flex content-center place-items-center rounded-full border border-wood bg-forest py-2 px-5 text-md font-medium text-snow hover:bg-black"
onClick={() =>
connect().catch((e) => console.log((e as Error).message))
}
>
Connect
</button>
)}
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="space-y-1 pt-2 pb-4">
<Disclosure.Button
as="a"
href="#"
className="block border-l-4 border-black py-2 pl-3 pr-4 text-base font-medium text-black"
>
Home
</Disclosure.Button>
{/* Add here your custom menu elements */}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
}

Your output should look like below. You can see the Alfajores network and your wallet address after connecting your wallet.

However, the address button is way to long and doesn't look nice, we can make this better by truncating the address.

Add the following code below your imports and also call the truncate

const truncateAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};

Your final code should look like this

import { Disclosure } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import { useCelo } from "@celo/react-celo";
import Image from "next/image";
import { useEffect, useState } from "react";

const truncateAddress = (address: string) => {
return `${address.slice(0, 6)}...${address.slice(-4)}`;
};
export default function Header() {
let [componentInitialized, setComponentInitialized] = useState(false);
let { initialised, address, network, connect, disconnect } = useCelo();

useEffect(() => {
if (initialised) {
setComponentInitialized(true);
}
}, [initialised]);

return (
<Disclosure as="nav" className="bg-prosperity border-b border-black">
{({ open }) => (
<>
<div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
<div className="relative flex h-16 justify-between">
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center rounded-md p-2 text-black focus:outline-none focus:ring-1 focus:ring-inset focus:rounded-none focus:ring-black">
<span className="sr-only">Open main menu</span>
{open ? (
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
<div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex flex-shrink-0 items-center">
<Image
className="block h-8 w-auto lg:block"
src="/logo.svg"
width="24"
height="24"
alt="Celo Logo"
/>
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<a
href="#"
className="inline-flex items-center border-b-2 border-black px-1 pt-1 text-sm font-medium text-gray-900"
>
Home
</a>
</div>
</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
{componentInitialized && address ? (
<>
<button
type="button"
className="mr-2 inline-flex content-center place-items-center rounded-full border border-wood bg-black py-2 px-5 text-md font-medium text-snow hover:bg-forest"
>
{network.name}
</button>
<button
type="button"
className="mr-2 inline-flex content-center place-items-center rounded-full border border-wood bg-black py-2 px-5 text-md font-medium text-snow hover:bg-forest"
>
{truncateAddress(address)}
</button>
<button
type="button"
className="inline-flex content-center place-items-center rounded-full border border-wood bg-black py-2 px-5 text-md font-medium text-snow hover:bg-forest"
onClick={disconnect}
>
Disconnect
</button>
</>
) : (
<button
type="button"
className="inline-flex content-center place-items-center rounded-full border border-wood bg-forest py-2 px-5 text-md font-medium text-snow hover:bg-black"
onClick={() =>
connect().catch((e) => console.log((e as Error).message))
}
>
Connect
</button>
)}
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="space-y-1 pt-2 pb-4">
<Disclosure.Button
as="a"
href="#"
className="block border-l-4 border-black py-2 pl-3 pr-4 text-base font-medium text-black"
>
Home
</Disclosure.Button>
{/* Add here your custom menu elements */}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
);
}

After this we would be writing our smart contract that interacts with our subscription, so head over to your terminal and run this command yarn run hardhat:accounts to view the account that is set up. You should get an error message stating you do not have any account setup, therefore we need a deployer wallet. To do this rename the file *env.example to .env and add a test private key that has already been given by celo here and copy the private key already given to us.

6 After this, re-run the above command and you should see an address in your terminal. Verify if the account as some celo in it via celoscan and if it doesn't you can request for a test token via celo faucet. Head over to open zepplin contracts and make use of the wizard shown below and copy the code generated into a new file created in the contract folder called MockCUSD.sol file

Let's install some OpenZeppelin contracts so we can get access to the ERC-721 contracts. In your terminal, execute the following command:

cd ..
cd hardhat
yarn add @openzeppelin/contracts
  • In the contracts folder, create a new Solidity file called PaymentSubscription.sol
  • Now we would write some code in the PaymentSubscription.sol. We would be importing Openzeppelin's ERC721 Contract.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
import "@openzeppelin/contracts/security/Pausable.sol";
import "@openzeppelin/contracts/access/Ownable.sol";
import "hardhat/console.sol";

contract PaymentSubscription is Pausable, Ownable {
//Available plans
enum Plan {
Basic,
Premium,
Enterprise
}

struct Subscription {
Plan plan;
uint256 price;
uint256 startDate;
uint256 endDate;
uint256 nextCharge;
bool active;
}

struct PlanDetail {
Plan plan;
uint256 price;
uint256 duration;
}

//All plans
mapping(Plan => PlanDetail) public plans;

//All subscriptions
mapping(address => Subscription) public subscriptions;

//Active subscriptions
mapping(address => bool) public activeSubscriptions;

//Emits when a new plan is created
event PlanCreated(Plan plan, uint256 price, uint256 duration);

event SubscriptionCreated(address indexed subscriber, Plan plan);
event SubscriptionCancelled(address indexed subscriber);
event SubscriptionCharged(
address indexed subscriber,
Plan plan,
uint256 nextCharge
);

//Token used for subscription payments
address public subscriptionToken;

constructor(address _subscriptionToken) {
require(_subscriptionToken != address(0), "Invalid token address");
subscriptionToken = _subscriptionToken;
plans[Plan.Basic] = PlanDetail(Plan.Basic, 2e18, 1 hours);
plans[Plan.Premium] = PlanDetail(Plan.Premium, 5e18, 1 hours);
plans[Plan.Enterprise] = PlanDetail(Plan.Enterprise, 12e18, 1 hours);

emit PlanCreated(Plan.Basic, 2e18, 1 hours);
emit PlanCreated(Plan.Premium, 5e18, 1 hours);
emit PlanCreated(Plan.Enterprise, 12e18, 1 hours);
}

function subscribe(Plan _plan, uint8 duration) public whenNotPaused {
require(uint8(_plan) <= 2, "Invalid plan");
require(duration > 0, "Invalid duration");
require(duration <= 12, "Invalid duration");
require(!activeSubscriptions[msg.sender], "Already subscribed");

uint256 requiredAllowance = plans[_plan].price * duration;

//Check if the user has approved the contract to spend the required amount, if not revert
require(
IERC20(subscriptionToken).allowance(msg.sender, address(this)) >=
requiredAllowance,
"Insufficient allowance"
);

//Check that we can charge for the first month
require(
IERC20(subscriptionToken).balanceOf(msg.sender) >=
plans[_plan].price,
"Insufficient balance"
);

subscriptions[msg.sender] = Subscription({
plan: _plan,
price: plans[_plan].price,
startDate: block.timestamp,
nextCharge: block.timestamp + plans[_plan].duration,
endDate: block.timestamp + plans[_plan].duration * duration,
active: true
});

_charge(msg.sender);

activeSubscriptions[msg.sender] = true;

emit SubscriptionCreated(msg.sender, _plan);
}

function _charge(address susbcriber) internal {
require(
IERC20(subscriptionToken).transferFrom(
susbcriber,
address(this),
subscriptions[susbcriber].price
),
"Transfer failed"
);

//Set the next charge date
subscriptions[susbcriber].nextCharge =
block.timestamp +
plans[subscriptions[susbcriber].plan].duration;

if (
subscriptions[susbcriber].nextCharge >
subscriptions[susbcriber].endDate
) {
_cancel(susbcriber);
}

emit SubscriptionCharged(
susbcriber,
subscriptions[susbcriber].plan,
subscriptions[susbcriber].nextCharge
);
}

function _cancel(address subscriber) internal {
activeSubscriptions[subscriber] = false;
delete subscriptions[subscriber];

emit SubscriptionCancelled(subscriber);
}

function charge(address subscriber) public onlyOwner whenNotPaused {
require(activeSubscriptions[subscriber], "Not subscribed");
require(
subscriptions[subscriber].nextCharge <= block.timestamp,
"Not time to charge yet"
);

require(
IERC20(subscriptionToken).allowance(subscriber, address(this)) >=
subscriptions[subscriber].price,
"Insufficient allowance"
);
_charge(subscriber);
}

function withdrawSubscriptionToken(
address to,
uint256 amount
) public onlyOwner {
require(
IERC20(subscriptionToken).transfer(to, amount),
"Transfer failed"
);
}
}

Compile the contract, open up a terminal and execute these commands

npx hardhat compile

If there are no errors, you are good to go 😚

Prefer Video

If you would rather learn from a video, we have a recording available of this tutorial on our YouTube. Watch the video by clicking on the screenshot below.

Video Tutorial

Finally we have successfully completed our payment subscription UI and the contract and the next step is to write test for our smart contract which would be done in the next tutorial.

Section 2

Congratulations on making it to this section! You've already read about smart contracts and UI using Nextjs and Tailwind in the first part of our tutorial. Now, we'll delve deeper into writing and verifying the smart contract by providing step-by-step instructions in this article.

Prerequisites

To proceed, it's required that you finish the UI and smart contract tutorial, along with the associated unit tests. Further information on unit tests can be found here.

Step 1

Navigate into your test folder and create a new file called subscription-test.js

Head over to Hardhat Network Helper which gives you the ability to mine blocks up to a certain timestamp or block number. To install paste the below command into your terminal

yarn add --dev @nomicfoundation/hardhat-network-helpers

Once the initial task is completed, we must organize our test cases in a way that simplifies identifying what needs to be tested and the expected results. Below are the test cases we will use for this project:

  • Do we have three plans?
  • Is the first plan what we expect?
  • Is the third plan what we expect?
  • Is the second plan what we expect?
  • Can we subscribe to the right plan?
  • Can we subscribe to the wrong plan?
  • Can we subscribe to the same plan twice?
  • Can a user subscribe to a plan without enough allowance?
  • Can a user be charged 11 more times after the first charge?
  • Can a user subscribe to a plan without enough balance for the first charge?

To run the first step add the following code to your subscription-test.js file

const { expect, assert } = require("chai");
const { ethers } = require("hardhat");
const helpers = require("@nomicfoundation/hardhat-network-helpers");

const oneHour = 60 * 60 * 1;

before(async function () {
const [deployer, accountA, accountB, accountC] = await ethers.getSigners();
const MockCUSD = await ethers.getContractFactory("MockCUSD");
const cUSD = await MockCUSD.deploy();
await cUSD.deployed();

const PaymentSubscription = await ethers.getContractFactory(
"PaymentSubscription"
);
const paymentSubscription = await PaymentSubscription.deploy(cUSD.address);

await paymentSubscription.deployed();

this.paymentSubscription = paymentSubscription;
this.cUSD = cUSD;
this.deployer = deployer;
this.accountA = accountA;
this.accountB = accountB;
this.accountC = accountC;
});

describe("PaymentSubscription", function () {
it("Should have Basic plan", async function () {
const basicPlan = await this.paymentSubscription.plans(0);
expect(basicPlan.price).to.equal(ethers.utils.parseEther("2"));
expect(basicPlan.duration).to.equal(oneHour);
});
});

In your terminal, run this command:

 npx hardhat test test/subscription-test.js --network hardhat

You should see the following result in your terminal

Also, we need to try this for our premium plan and enterprise plan. In that same file add the following code

it("Should have Premium plan", async function () {
const premiumPlan = await this.paymentSubscription.plans(1);
expect(premiumPlan.price).to.equal(ethers.utils.parseEther("5"));
expect(premiumPlan.duration).to.equal(oneHour);
});

it("Should have Enterprise plan", async function () {
const enterprisePlan = await this.paymentSubscription.plans(2);
expect(enterprisePlan.price).to.equal(ethers.utils.parseEther("12"));
expect(enterprisePlan.duration).to.equal(oneHour);
});

and run the same command in your terminal. Your terminal should look like this if properly executed.

Step 2

Also, we want a user to be able to subscribe to a 12-month plan. To do this, we need to add the following code after the enterprise plan function

it("Should allow user to subscribe to a 12 months plan", async function () {
const basic = await this.paymentSubscription.plans(0);
await this.cUSD.approve(
this.paymentSubscription.address,
basic.price.mul(ethers.BigNumber.from(12))
);
await this.paymentSubscription.subscribe(basic.plan, 12);
const subscription = await this.paymentSubscription.subscriptions(
this.deployer.address
);
});

Your final code should look like this.

const { expect, assert } = require("chai");
const { ethers } = require("hardhat");
const helpers = require("@nomicfoundation/hardhat-network-helpers");

const oneHour = 60 * 60 * 1;

before(async function () {
const [deployer, accountA, accountB, accountC] = await ethers.getSigners();
const MockCUSD = await ethers.getContractFactory("MockCUSD");
const cUSD = await MockCUSD.deploy();
await cUSD.deployed();

const PaymentSubscription = await ethers.getContractFactory(
"PaymentSubscription"
);
const paymentSubscription = await PaymentSubscription.deploy(cUSD.address);

await paymentSubscription.deployed();

this.paymentSubscription = paymentSubscription;
this.cUSD = cUSD;
this.deployer = deployer;
this.accountA = accountA;
this.accountB = accountB;
this.accountC = accountC;
});

describe("PaymentSubscription", function () {
it("Should have Basic plan", async function () {
const basicPlan = await this.paymentSubscription.plans(0);
expect(basicPlan.price).to.equal(ethers.utils.parseEther("2"));
expect(basicPlan.duration).to.equal(oneHour);
});

it("Should allow user to subscribe to a 12 months plan", async function () {
const basic = await this.paymentSubscription.plans(0);
await this.cUSD.approve(
this.paymentSubscription.address,
basic.price.mul(ethers.BigNumber.from(12))
);
await this.paymentSubscription.subscribe(basic.plan, 12);
const subscription = await this.paymentSubscription.subscriptions(
this.deployer.address
);

Run the same command in your terminal, and your terminal should give you the following output below:

However, we need to confirm that the time a user subscribes is the actual current block time and that a user can subscribe to the wrong plan or the same plan twice. Paste the following code after the last function.

const currentTime = (await ethers.provider.getBlock("latest")).timestamp;

expect(subscription.plan).to.equal(basic.plan);
expect(subscription.startDate).to.equal(ethers.BigNumber.from(currentTime));
expect(subscription.endDate).to.equal(
ethers.BigNumber.from(currentTime + 12 * oneHour)
);
expect(subscription.nextCharge).to.equal(
ethers.BigNumber.from(currentTime + oneHour)
);
});

it("Should not allow user to subscribe to the wrong plan", async function () {
await expect(this.paymentSubscription.subscribe(3, 12)).to.be.rejectedWith(
Error
);
});

it("Should not allow user to subscribe to the same plan twice", async function () {
const basic = await this.paymentSubscription.plans(0);

await this.cUSD.approve(
this.paymentSubscription.address,
basic.price.mul(ethers.BigNumber.from(12))
);

expect(
this.paymentSubscription.subscribe(basic.plan, 12)
).to.be.revertedWith("Already subscribed");
});

Run the same command in your terminal, and you should get the following results if executed correctly:

Step 3

Finally, we would also test to see if a user could subscribe to a plan without enough allowance, be charged 11 more times after the first charge, and subscribe to a plan without enough balance for the first charge. Add the following code after your last test

it("Should not allow user to subscribe to a plan without enough allowance", async function () {
const basic = await this.paymentSubscription.plans(0);

await expect(
this.paymentSubscription.connect(this.accountA).subscribe(basic.plan, 12)
).to.be.revertedWith("Insufficient allowance");
});

it("Should not allow user to subscribe to a plan without enough balance for the first charge", async function () {
const basic = await this.paymentSubscription.plans(0);

await this.cUSD
.connect(this.accountA)
.approve(
this.paymentSubscription.address,
basic.price.mul(ethers.BigNumber.from(12))
);

await expect(
this.paymentSubscription.connect(this.accountA).subscribe(basic.plan, 12)
).to.be.revertedWith("Insufficient balance");
});

it("Should allow user to be charged 11 more times after the first charge", async function () {
await this.cUSD.mint(this.accountC.address, ethers.utils.parseEther("24"));

const basic = await this.paymentSubscription.plans(0);

await this.cUSD
.connect(this.accountC)
.approve(
this.paymentSubscription.address,
basic.price.mul(ethers.BigNumber.from(12))
);

await this.paymentSubscription
.connect(this.accountC)
.subscribe(basic.plan, 12);

for (let monthsCharged = 2; monthsCharged <= 12; monthsCharged++) {
const currentBal = await this.cUSD.balanceOf(this.accountC.address);
const subscription = await this.paymentSubscription.subscriptions(
this.accountC.address
);

await helpers.time.increase(oneHour);

console.table({
monthsCharged,
currentBal: ethers.utils.formatEther(currentBal),
nextCharge: ethers.utils.formatEther(subscription.nextCharge),
});

await this.paymentSubscription
.connect(this.deployer)
.charge(this.accountC.address);
}

expect(await this.cUSD.balanceOf(this.accountC.address)).to.equal(0);

const subscription = await this.paymentSubscription.subscriptions(
this.accountC.address
);
const active = await this.paymentSubscription.activeSubscriptions(
this.accountC.address
);

expect(subscription.nextCharge).to.equal(0);
expect(active).to.equal(false);
});
});

Run the same command in your terminal, and you should get the following results if executed correctly:

Awesome! We now have all our tests working, and the next thing we need to do is verify our contract and run a test coverage. To do this head over to solidity coverage and run the command below in your terminal

yarn add solidity-coverage --dev

Require the plugin in hardhat.config.js by pasting this code

require("solidity-coverage");

In your terminal run,

npx hardhat coverage --testfiles "test/registry/*.ts" --network hardhat

To verify your contract,

  • Head over to Celoscan
  • Login or Signup
  • Click on the API-KEYs menu on the sidebar and generate a key
  • Paste the key in the space available for it in the .env file

In your terminal run

npx hardhat deploy --network alfajores

To deploy, open up deploy.js file in the deploy folder and paste the following code inside

module.exports = async ({ getNamedAccounts, deployments }) => {
const { deploy } = deployments;
const { deployer } = await getNamedAccounts();

const cUSD = await deploy("MockCUSD", {
from: deployer,
log: true,
});

console.log("cUSD deployed to:", cUSD.address);

await deploy("PaymentSubscription", {
from: deployer,
args: [cUSD.address],
log: true,
});
};

module.exports.tags = ["MockCUSD"];

In your terminal, run

yarn run deploy

And to verify our contracts we need to use the format below.

npx hardhat verify <CONTRACT_ADDRESS> <CONSTRUCTOR_ARGS> --network alfajores

To verify our subscription payment smart contract that was deployed above, the command to run will be:

npx hardhat hardhat verify 0x95BD5b1B16C586025bF0750c21bd1de433de8D4c 0xEb3345B25d59Ad1dD153DAf883b377258E8515F9 --network alfajores

If you prefer a video, you can click on the screenshot below and watch the video tutorial. In the next article, we will be setting up Subgraph to query subscription details from the OpenZeppelin Defender Autotask script.

Video Tutorial

Section 3

Congrats on reaching this section! You've learned about smart contracts, UI with Nextjs and Tailwind, testing, and contract verification in the past two tutorials. In this article, we will provide step-by-step instructions for setting up a subgraph that queries subscription details from the OpenZeplin Defender autotask script. This will help you better understand the process.

Prerequisites

  • Create an account on the subgraph
  • Click on sign in with GitHub
  • Install the graph globally on your local machine using npm. In your terminal run

In the project folder run:

yarn subgraphs:get-abi

Step 1

To initialize the graph in our project, cd into the subgraph package folder and run

graph init

Choose Ethereum for protocol, and hosted services, and add your GitHub username as your subgraph name.

Add celo-alfajores as the network, and you should see the output below if executed successfully.

In our subgraph, a new folder called ebook-payment-subscription-platform has been created or in your own case whatever you named your folder name, Inside the folder, open the schema.graphql file and paste the following code inside.

type Plan @entity(immutable: true) {
id: Bytes!
plan: Int!
price: BigInt!
duration: BigInt!
subscriptions: [SubscriptionP!]! @derivedFrom(field: "plan")
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}


type SubscriptionP @entity(immutable: true) {
id: Bytes!
subscriber: Bytes! # address
plan: Plan!
nextCharge: BigInt!
endDate: BigInt!
blockNumber: BigInt!
blockTimestamp: BigInt!
transactionHash: Bytes!
}

Then in your subgraph.yaml file delete the default code and paste the following code

specVersion: 0.0.5
schema:
file: ./schema.graphql
dataSources:
- kind: ethereum
name: PaymentSubscription
network: celo-alfajores
source:
address: "0x95BD5b1B16C586025bF0750c21bd1de433de8D4c"
abi: PaymentSubscription
mapping:
kind: ethereum/events
apiVersion: 0.0.7
language: wasm/assemblyscript
entities:
- Plan
- SubscriptionP
abis:
- name: PaymentSubscription
file: ./abis/PaymentSubscription.json
eventHandlers:
- event: PlanCreated(uint8,uint256,uint256)
handler: handlePlanCreated
- event: SubscriptionCancelled(indexed address)
handler: handleSubscriptionCancelled
- event: SubscriptionCharged(indexed address,uint8,uint256)
handler: handleSubscriptionCharged
- event: SubscriptionCreated(indexed address,uint8)
handler: handleSubscriptionCreated
file: ./src/payment-subscription.ts

In your terminal run

graph codegen

You should see the following output

Lets proceed to doing dome cleanups in our file , navigate to the src folder and open paymentsubscription.ts file, delete the default code and paste the following

import { BigInt, Bytes } from "@graphprotocol/graph-ts";
import {
PlanCreated as PlanCreatedEvent,
SubscriptionCancelled as SubscriptionCancelledEvent,
SubscriptionCharged as SubscriptionChargedEvent,
SubscriptionCreated as SubscriptionCreatedEvent,
PaymentSubscription as PaymentSubscriptionContract,
PaymentSubscription__subscriptionsResult,
} from "../generated/PaymentSubscription/PaymentSubscription";
import { Plan, SubscriptionP as Subscription } from "../generated/schema";

export function handlePlanCreated(event: PlanCreatedEvent): void {
let entity = new Plan(
event.transaction.hash.concatI32(event.logIndex.toI32())
);

entity.plan = event.params.plan;
entity.price = event.params.price;
entity.duration = event.params.duration;

entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;

entity.save();
}

export function handleSubscriptionCancelled(
event: SubscriptionCancelledEvent
): void {
let entity = Subscription.load(event.params.subscriber);

if (entity == null) {
return;
}

entity.nextCharge = BigInt.fromI32(0);
entity.endDate = BigInt.fromI32(0);

entity.save();
}

export function handleSubscriptionCharged(
event: SubscriptionChargedEvent
): void {
let entity = Subscription.load(event.params.subscriber);

if (entity == null) {
entity = new Subscription(event.params.subscriber);

let paymentSubscriptionContract = PaymentSubscriptionContract.bind(
event.address
);
let subscription = paymentSubscriptionContract.subscriptions(
event.params.subscriber
);

entity.plan = Bytes.fromI32(subscription.getPlan());
entity.subscriber = event.params.subscriber;
entity.endDate = subscription.getEndDate();
}

entity.nextCharge = event.params.nextCharge;

entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;

entity.save();
}

export function handleSubscriptionCreated(
event: SubscriptionCreatedEvent
): void {
let entity = new Subscription(event.params.subscriber);

entity.subscriber = event.params.subscriber;

let paymentSubscriptionContract = PaymentSubscriptionContract.bind(
event.address
);
let subscription = paymentSubscriptionContract.subscriptions(
event.params.subscriber
);

entity.plan = Bytes.fromI32(subscription.getPlan());
entity.endDate = subscription.getEndDate();
entity.nextCharge = subscription.getNextCharge();

entity.blockNumber = event.block.number;
entity.blockTimestamp = event.block.timestamp;
entity.transactionHash = event.transaction.hash;

entity.save();
}

Step 2

The next action is to upload our subgraph , Head over to your dashboard and create a subgraph.

in your terminal run

graph auth --product hosted-service<the number generated for you>
graph deploy --product hosted-service<yourname-subgraphname>

You have successfully setup subgraph to query subscription details from openzepplin defender

If you prefer a video, you can click on the screenshot below and watch the video tutorial. In the next article, we will Connect UI to Smart Contract & Setup Defender Admin, Replay & Autotask

Video Tutorial

Final Section

Introduction

Congrats on reaching this section! You've learned about smart contracts, UI with Nextjs and Tailwind, testing, a contract verification in the past three tutorials. In this article, we will provide step-by-step instructions on how to Connect UI to Smart Contract & Setup Defender Admin, Replay & Autotask. This will help you better understand the process.

Prerequisites

  • Create an account on the subgraph
  • Click on sign in with GitHub
  • Install the graph globally on your local machine using npm. In your terminal run

Step 1

To begin, paste the following code in your index.tx file

import React, { useEffect, useState } from "react";
import PaymentCard from "../components/PaymentCard";
import {
abi as psAbi,
address as psAddress,
} from "@ebook-payment-subscription-platform/hardhat/deployments/alfajores/PaymentSubscription.json";
import { useCelo } from "@celo/react-celo";
import { parseEther } from "ethers/lib/utils.js";

const plans = {
0: { name: "Basic", price: 2 },
1: { name: "Premium", price: 5 },
2: { name: "Enterprise", price: 12 },
};

export default function Home() {
const subscriptionToken = "0xEb3345B25d59Ad1dD153DAf883b377258E8515F9";
const [activePlan, setActivePlan] = useState(null);
const { kit, address } = useCelo();
const paymentSubscriptionContract = new kit.connection.web3.eth.Contract(
psAbi,
psAddress
);
const cUsdContract = new kit.connection.web3.eth.Contract(
[
{
inputs: [
{
internalType: "address",
name: "spender",
type: "address",
},
{
internalType: "uint256",
name: "amount",
type: "uint256",
},
],
name: "approve",
outputs: [
{
internalType: "bool",
name: "",
type: "bool",
},
],
stateMutability: "nonpayable",
type: "function",
},
],
subscriptionToken
);

const subscribeToPlan = async (plan) => {
try {
const tx = await cUsdContract.methods
.approve(
psAddress,
parseEther((plans[plan].price * 12).toString()).toHexString()
)
.send({ from: address });

if (tx.status) {
const tx = await paymentSubscriptionContract.methods
.subscribe(plan, 12)
.send({ from: address });

if (tx.status) {
setActivePlan(plan);
}
}
} catch (error) {
console.log(error);
}
};

useEffect(() => {
const getActivePlan = async () => {
const plan = await paymentSubscriptionContract.methods
.subscriptions(address)
.call();

if (plan.endDate !== "0") {
setActivePlan(parseInt(plan.plan));
}
};

getActivePlan();
}, [address]);

return (
<div className="space-y-8 lg:grid lg:grid-cols-3 sm:gap-6 xl:gap-10 lg:space-y-0">
<div className="flex">
<PaymentCard
planName={"Basic"}
active={activePlan == 0}
price={2}
onClick={() => subscribeToPlan(0)}
/>
</div>
<div className="flex">
<PaymentCard
planName={"Premium"}
active={activePlan == 1}
price={5}
onClick={() => subscribeToPlan(1)}
/>
</div>
<div className="flex">
<PaymentCard
planName={"Enterprise"}
active={activePlan == 2}
price={12}
onClick={() => subscribeToPlan(2)}
/>
</div>
</div>
);
}

Import the mock cUSD token we created with its contract address

To test the UI , in your terminal run

yarn run dev

and click on subscribe, your metamask should pop up and you should see the output below

Sign the transaction and once succesfully executed, the subscribe button should change to unsubscribe.

Verify your contract on celoscan and you should see your transaction.

Step 2

Head over to openzepplin, click on defender and sign up. Then click on add a contract

  • name : EbookSPP
  • network : celo alfajores
  • address : "your contract address"

NOTE: it might throw an error saying "unable to fetch abi" , copy and paste the contract abi and click on create

Step 3

Next we create a relay. A relay is like a private key that helps in creating a wallet that can be used like a relayer to interact with the smart contract. You can read more on relayer here.

To create a relayer follow these steps below:

  • click on relayer on the side bar
  • name : Ebook Relayer SPP
  • network : Celo alfajores

then click on create. send some celo to your relayer from your wallet.

Step 4

Transfer the ownership of the contract to the relayer, I will strongly advice that you watch the video tutorial link that would be attached at the end of the article from this step because it is easier to graps and understand.

Make sure you go through the youtube tutorial here for the last part of the project.

Thank you for making it through from the first part to the final part.

Conclusion

By now, you must have gotten the idea of how you can build automated payment subscription platform for charging your customer in crypto (CUSD). You have learn 4 major things; how to write test, how to deploy and query a subgraph, what a relayer is and finally how to automate things in a smart contract.

About Author

Oluwafemi Alofe is a Blockchain developer with 3 years of applicable knowledge and 8 years of total experience as a Software engineer. He has written and deployed vulnerability-free smart contracts as a blockchain developer on a variety of blockchains, including but not limited to Ethereum, BCS, and Celo. Oluwafemi has a strong interest in developing distributed systems, and continues to learn new things every day to stay current with technological advancements.

He is has two Udemy courses on solidity development with over 6,300 student enrolled and also a book on Amazon KDP for PHP and Laravel Developers.

He's currently a platform owner at myco.io, first world, watch to earn platform with over 1 Million users.

Connect with Oluwafemi on Twitter or Linkedin.

Go back

· 5 min read

header

Introduction

In this tutorial, we will be learning how to deploy a dapp built using celo composer on a decentalised cloud service called spheron protocol.

Prerequisites​

To start building, you’ll need a basic understanding of web development, Node (v12), yarn, and Git.

Celo Composer

The easiest way to get started with Celo Composer is using @celo/celo-composer . This CLI tool enables you to quickly start building dApps on Celo for multiple frameworks including React, React Native (w/o Expo), Flutter, and Angular. You can create the dApp using the default Composer templates provided by Celo. In our case, we will work with react, specifically NextJS. The stack included in celo composer:

Spheron Protocol

Spheron Protocol is an all-in-one decentralized platform for automating modern dapps.

They provide a frictionless developer experience to take care of the hard things: deploying instantly, scaling automatically, and serving personalized content on decentralized networks.

Let's start building:


Getting Started

  1. Bootstrap the application using this Celo Composer command.
npx @celo/celo-composer create
  1. Select React framework(NextJS)

image

  1. Select react-celo as the web3 library

image

  1. Select none for the smart contract framework since we’ll be working with the 0x Protocol API.

image

  1. Select No for subgraph

image

  1. Give a name to your project and you are done.

image

Launch the app in your code editor then install the dependencies required in the project. In the root directory, run

yarn

//or

npm install

Upload Project to Github

  • Navigate to your repositories

image

  • Give the repository a name.

image

  • Click on create repository or press enter.

image

  • Follow the guide to upload your project on github

image


Deploying the App on Spheron

image

  • Click "Sign Up” button

image

  • Click "Continue with GitHub”

image

  • Select an organisation of your choice.

image

  • Click "New Project”

image

  • Click "Github”

image

  • Pick the github account.

image

  • Click the "Search repository" field and search the repository that you want to deploy.

image

  • Select "Filecoin" or "IPFS”

image

  • Input the directory with react application which is "packages/react-app”

image

  • Pick the Javascript framework i.e NextJS.

image

  • Click "Deploy”

image

  • Check the logs for the installation process.

image


Testing the App

image

  • Click "Connect”

image

  • Click "MetaMask” or any other wallet of your choice.

image4


Conclusion

With the assistance of Spheron protocol, you have the capability to continue developing your application, and once you push your changes to GitHub, they are automatically deployed through your deployed link. This is made possible by the built-in CI/CD workflow.

Why choose Spheron Protocol

Here are reasons why you should use spheron to deploy your decentralised app:

  • Secure data storage: Spheron Protocol utilizes blockchain technology to provide secure and tamper-proof data storage. The data is distributed across the network and is protected by strong encryption and access controls, ensuring that it is only accessible to authorized parties.

  • Data privacy and ownership: With Spheron Protocol, users have full control and ownership of their data. They can choose who has access to their data and can revoke access at any time. This ensures that user privacy is protected and gives users the confidence to share their data on the platform.

  • Scalability: Spheron Protocol is designed to be highly scalable and can handle large volumes of data and transactions. This makes it suitable for a wide range of applications that require high performance and throughput.

  • Interoperability: Spheron Protocol is designed to be interoperable with other blockchain networks and protocols, making it easier for developers to integrate their apps with the platform.

  • Incentivization: Spheron Protocol provides incentives for users to participate in the network by offering rewards for contributing computing resources and validating transactions. This incentivizes users to help secure the network and ensure its reliability.

  • Lower costs: Spheron Protocol can help reduce the costs of data storage and management by eliminating the need for centralized servers and intermediaries. This can result in significant cost savings for businesses and organizations.

  • Community support: Spheron Protocol has an active and growing community of developers, users, and enthusiasts who are committed to building a decentralized data ecosystem. This community provides support, feedback, and resources to help developers build and deploy their decentralized apps on the platform.

  • Use cases: Spheron Protocol has a wide range of potential use cases in various industries such as healthcare, finance, supply chain management, and more. By deploying your decentralized app on Spheron Protocol, you can tap into this growing market and leverage the benefits of blockchain technology.

About the Author

I am Dennis Kimathi, a UI/UX designer, full-stack developer, and blockchain enthusiast. I have a strong passion for creating user-centered designs that are not only aesthetically pleasing but also efficient and intuitive to use. Over the years, I have honed my skills in developing web applications that are robust, scalable, and secure, thanks to my experience in full-stack development.

As a blockchain enthusiast, I am constantly exploring and learning about the potential of this technology and how it can be applied in various industries. I believe that blockchain has the potential to revolutionize the way we do things, from finance to healthcare and beyond. Its ability to provide decentralized, secure, and transparent systems has the potential to bring about significant changes that will benefit society.

Fun fact about me, i am into archeoastronomy.

Resources

Go back

· 20 min read
Glory Agatevure

header

Introduction​

This tutorial will take you through a step-by-step guide on how to create a frontend and backend (Smart Contract) dApp explaining how to create a decentralized version of Buy Me A Coffee. Buy Me A Coffee is a platform that will enable individuals to support the creative works of creators. The dApp will be built using Celo composer, react, solidity, hardhat, Pinta IPFS, and deployment to Fleek. This will enable payment to be made using CELO native token.

Prerequisites​

To successfully follow along in this tutorial you need basic knowledge of:

  • HTML, CSS, React and Next.js
  • Blockchain, solidity and hardhat

Requirements​

  • Vscode - But you can use any code editor of your choice
  • Hardhat - used to deploy the smart contract
  • Alfajores Testnet Account - required to connect to the dApp and make test transactions.
  • Node - an open-source, cross-platform JavaScript runtime environment.

Let’s Get Started

The goal of this tutorial is to build the web3 version of Buy Me A Coffee. Buy Me A Coffee is a dApp that allows individuals to support the creative works of creators.

In this tutorial, we will be creating a smart contract we will use to interact with the dApp on the frontend using Celo Composer.

With Celo Composer gives us access to React, Next.js, Tailwind CSS and Hardhat options out of the box to interact with our smart contract.

The frontend comprises of the following pages;

  • The landing page that displays the list of creators
  • Creator’s dashboard Page
  • Support Page
  • Withdraw Tip Page
  • Create Account Page

Wallet/Account Setup, Funding and Block Explorer

Before performing any transaction on the Celo Blockchain, you will need to have an account setup. For this tutorial we will be making use of our Celo Testnet account which is the Alfajores network. You can either configure Celo on Metamask or download the Celo Wallet Extension. For further testing of your dApp on mobile you can also install the Alfajores mobile wallet.

Once your account has been created the next thing to do is to fund the account. You can fund your account with test tokens from the Alfajores Token Faucet Site.

*Fig 0-1 Alfajores Token Faucet Alfajores Token Faucet

And you can use the Celo Alfajores Block Explorer to view the details of a transaction.

Fig 0-2 Alfajores Block Explorer Alfajores Block Explorer

Celo Composer

Celo Composer a tool built by the Celo team that allows you to quickly build, deploy, and iterate on decentralized applications using Celo. It provides a number of frameworks, examples, and Celo specific functionality to help you get started with your next dApp. To get started using Celo Composer run the below command.

npx @celo/celo-composer create

This will prompt you to select the framework and the template you want to use. For this tutorial we will be making use of React.

Fig 3-1 Celo Composer Prompt

Celo Composer Prompt

Once you have finished following the prompt you will then give your project a name and your project will be created with some boilerplate code. You will see a package folder inside the package folder you will see hardhat folder and react-app folder.

The hardhat folder is where the contract and the deployment code lives. And the react-app handles the next.js frontend code.

Note

Before any else after the project has been created. Run npm install or yarn add this will install all required dependencies that came with the boilerplate code in your newly created project. Also follow the Celo Composer guide for more comprehensive setup details.

To deploy your contract you can simply navigate within the hardhat folder. To run the frontend code you can navigate to the react-app folder and do npm run dev

File Structure

Fig 3-2 File Structure

File Structure1
File Structure2

Smart Contract Creation

The smart contract will be written using Solidity. Solidity is an object-oriented, high-level language for implementing smart contracts. The smart contract comprises of the following functions;

  • setCreatorDetails: This function handles creation of the creator's account.
  • getCreatorList: This function returns the entire list of creators
  • sendTip: This function sends a CELO tip to the creator
  • creatorWithdrawTip: This function is called by the creator to withdraw tips received.
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.7;
import "hardhat/console.sol";
import "@openzeppelin/contracts/utils/Counters.sol";


contract Coffee {

using Counters for Counters.Counter;
Counters.Counter private _creatorIds;
uint public creatorCounter;

struct CreatorInfo {
uint id;
string username;
string ipfsHash;
address payable walletAddress;
string userbio;
uint donationsReceived;
uint supporters;
}

event CreatorEvent (
uint id,
string username,
address payable walletAddress,
string ipfsHash,
string userbio,
uint donationsReceived,
uint supporters
);

// Event to emit when a SupporterEvent is created.
event SupporterEvent(
address indexed from,
uint256 timestamp,
string message
);

// payable address can receive ether
address payable public owner;

// payable constructor can receive ether. Assigning the contract deployer as the owner
constructor() payable {
owner = payable(msg.sender);
}

mapping(address => bool) isAddressExist;
mapping(string => bool) isUsernameExist;
CreatorInfo[] creatorList;

// function to create new creator account
function setCreatorDetail(
string memory _username,
string memory _ipfsHash,
string memory _userbio) public {

// Validation
require(bytes(_username).length > 0);
require(bytes(_ipfsHash).length > 0);
require(bytes(_userbio).length > 0);

uint _donationsReceived;
uint _supporters;
/**
*@dev require statement to block multiple entry
*/
require(isAddressExist[msg.sender] == false, "Address already exist");
require(isUsernameExist[_username] == false, "Username already exist");

/* Increment the counter */
// _creatorIds.increment();

creatorList.push(CreatorInfo(_creatorIds.current(),_username, _ipfsHash, payable(msg.sender), _userbio, _donationsReceived, _supporters));
isAddressExist[msg.sender] = true;
isUsernameExist[_username] = true;

// emit a Creator event
emit CreatorEvent (
_creatorIds.current(),
_username,
payable(msg.sender),
_ipfsHash,
_userbio,
_donationsReceived,
_supporters
);
_creatorIds.increment();
}

// Return the entire list of creators
function getCreatorList() public view returns (CreatorInfo[] memory) {
return creatorList;
}

/**
* @dev send tip to a creator (sends an CELO tip)
* @param _message a nice message from the supporter
*/
function sendTip(string memory _message, uint _index) public payable {
creatorList[_index].donationsReceived += msg.value;
creatorList[_index].supporters +=1;

// Must accept more than 0 ETH for a coffee.
require(msg.value > 0, "Insufficient balance!");

// Emit a Supporter event with details about the support.
emit SupporterEvent(
msg.sender,
block.timestamp,
_message
);
}

// Creator withdraw function. This function can be called by the creator
function creatorWithdrawTip(uint index, uint amount) public returns (address payable _creatorAddress){
CreatorInfo storage creatorDetail = creatorList[index];
uint creatorBal = creatorDetail.donationsReceived;
address payable creatorAddress = creatorDetail.walletAddress;
creatorList[index].donationsReceived -= amount;
// check to ensure the amount to be withdrawn is not more than the creator balance
require(amount <= creatorBal, "Insufficient bal");

// Check to ensure the caller of the function is the creator
require(msg.sender == creatorAddress, "You are not the creator");

// // send input ether amount to creator
// Note that "recipient" is declared as payable
(bool success, ) = creatorAddress.call{value: amount}("");
require(success, "Failed to send Ether");
return creatorAddress;
}
}

Deploy the Smart Contract

Your deployment code inside packages/hardhat/scripts/deploy.ts should look like this 👇

// We require the Hardhat Runtime Environment explicitly here. This is optional
// but useful for running the script in a standalone fashion through `node <script>`.
//
// When running the script with `npx hardhat run <script>` you'll find the Hardhat
// Runtime Environment's members available in the global scope.
const hre = require("hardhat");

async function main() {
// Hardhat always runs the compile task when running scripts with its command
// line interface.
//
// If this script is run directly using `node` you may want to call compile
// manually to make sure everything is compiled
// await hre.run('compile');

// We get the contract to deploy
const Coffee = await hre.ethers.getContractFactory("Coffee");
const coffee = await Coffee.deploy();

// await coffee.deployed();

// console.log("Coffee deployed to:", coffee.address);

const contractAddress = await (await coffee.deployed()).address;
console.log(`Contract was deployed to ${contractAddress}`)
}

// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.error(error);
process.exit(1);
});

And your hardhat.config.tsx file should look like this 👇

Fig 1-0 Hardhat Config File ardhat Config

Note:

Your Private key should be in your .env file at the root of the hardhat folder.

To deploy the smart contract navigate to your project directory and run this command on your terminal

npx hardhat run scripts/deploy.ts --network alfajores

Once this is done. You will get an output that shows you the contract address and the contract ABI will also be created. The contract address and ABI ( Application Binary Interface) will be used later on the frontend to interact with the contract.

Fig 1-1 Contract Deployment Output

Contract Deployment Output

Fig 1-2 Generated ABI or JSON (JavaScript Object Notation) file Generated ABI

Setting up and Uploading Creators Photos to Pinata

Pinata is a decentralized storage platform for builders and creators of all kinds in web3.

If you are new to Pinata click on the signup button to create an account.

Fig 2-1 Pinata Sign-up Pinata Sign-up

If you already have an account click on the login button to get started uploading to Pinata.

Fig 2-2 Pinata Login Pinata Login

Once logged in click on the developer tab and then click on the new key button to create a new key where your creators photos will be uploaded to

Fig 2-2 Pinata User Dashboard Pinata User Dashboard

Environment Variable You will need to create a .env file at the root of your react-app directory. Also don’t forget to add your .env file to .gitignore. This will prevent exposing your PINATA JWT TOKEN and any other private key you might want to use in your app.

Your .env file should look like this 👇

NEXT_PUBLIC_PINATA_JWT = YOUR PINTA JWT TOKEN

Note: In order for your next.js app to publicly access the values of your .env variables you will need to start the naming with NEXT_PUBLIC

Pinata Image Upload Code

Here is the code snippet required to upload photos to Pinata

import axios from "axios"
const FormData = require("form-data");
const JWT = `Bearer ${process.env.NEXT_PUBLIC_PINATA_JWT}`

export const pinFileToPinata = async (selectedFile : string | File | number | readonly string[] | undefined) => {
const formData = new FormData();

formData.append('file', selectedFile)

const metadata = JSON.stringify({
name: 'Coffee Dapp',
});
formData.append('pinataMetadata', metadata);

const options = JSON.stringify({
cidVersion: 0,
})
formData.append('pinataOptions', options);
try{
const res = await axios.post("https://api.pinata.cloud/pinning/pinFileToIPFS", formData, {
maxBodyLength: Infinity,
headers: {
'Content-Type': `multipart/form-data; boundary=${formData._boundary}`,
Authorization: JWT
}
});
console.log(res.data.IpfsHash);
return res.data.IpfsHash
} catch (error) {
console.log(error);
}
};

Frontend Interaction with React Using Celo Composer

All files in the component folder were auto generated by the Celo Composer. You can take a look at the Header, Footer and Layout file. For this tutorial we will make some modifications to the Header.tsx file.

Here is what the Header.tsx file looks like.

import { Disclosure } from "@headlessui/react";
import { Bars3Icon, XMarkIcon } from "@heroicons/react/24/outline";
import {
useCelo,
} from '@celo/react-celo';
import Image from "next/image";
import { useEffect, useState } from "react";
import Link from 'next/link'
import { getCreators } from "@/interact";

interface ICreator {
donationsReceived: string;
id: number;
ipfsHash: string;
supporters: number;
userbio: string;
username: string;
walletAddress: string
}

export default function Header() {
const [data, setData] = useState<any>({})

let [componentInitialized, setComponentInitialized] = useState(false);
let {
initialised,
address,
kit,
connect,
disconnect
} = useCelo();

useEffect(() => {
if (initialised) {
setComponentInitialized(true);
}
const creatorData = async () => {
const creators = await getCreators(kit)
return setData(!address ? null : creators.find((item: any) => item.walletAddress === address))
}
creatorData()
}, [initialised, kit]);
console.log("data", data)
console.log(address)
return (
<Disclosure as="nav" className="bg-prosperity border-b border-black">
{({ open }) => (
<>
<div className="mx-auto max-w-7xl px-2 sm:px-6 lg:px-8">
<div className="relative flex h-16 justify-between">
<div className="absolute inset-y-0 left-0 flex items-center sm:hidden">
{/* Mobile menu button */}
<Disclosure.Button className="inline-flex items-center justify-center rounded-md p-2 text-black focus:outline-none focus:ring-1 focus:ring-inset focus:rounded-none focus:ring-black">
<span className="sr-only">Open main menu</span>
{open ? (
<XMarkIcon className="block h-6 w-6" aria-hidden="true" />
) : (
<Bars3Icon className="block h-6 w-6" aria-hidden="true" />
)}
</Disclosure.Button>
</div>
<div className="flex flex-1 items-center justify-center sm:items-stretch sm:justify-start">
<div className="flex flex-shrink-0 items-center">
<Image className="block h-8 w-auto lg:block" src="/logo.svg" width="24" height="24" alt="Celo Logo" />
</div>
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<Link href="/"
className="inline-flex items-center border-b-2 border-black px-1 pt-1 text-sm font-medium text-gray-900"
>
Home
</Link>
</div>
{ data === undefined || address === null ? null :
<div className="hidden sm:ml-6 sm:flex sm:space-x-8">
<Link className="inline-flex items-center border-b-2 border-black px-1 pt-1 text-sm font-medium text-gray-900"
href={{
pathname: `/Dashboard/`,
// query: { username: address === null ? null : data.username}// the data
}}
>
Dashboard
</Link>

</div>
}

</div>
<div className="absolute inset-y-0 right-0 flex items-center pr-2 sm:static sm:inset-auto sm:ml-6 sm:pr-0">
{componentInitialized && address ? (
<div>
<button className="border-2 border-black rounded-md mr-2 p-2">{`${address.substring(0,15)}...`}</button>
<button
type="button"
className="inline-flex content-center place-items-center rounded-full border border-wood bg-black py-2 px-5 text-md font-medium text-snow hover:bg-forest"
onClick={disconnect}
>Disconnect</button>
</div>


) : (
<button
type="button"
className="inline-flex content-center place-items-center rounded-full border border-wood bg-forest py-2 px-5 text-md font-medium text-snow hover:bg-black"
onClick={() =>
connect().catch((e) => console.log((e as Error).message))
}
>Connect</button>
)}
</div>
</div>
</div>
<Disclosure.Panel className="sm:hidden">
<div className="space-y-1 pt-2 pb-4">
<Disclosure.Button
as="a"
href="#"
className="block border-l-4 border-black py-2 pl-3 pr-4 text-base font-medium text-black"
>
Home
</Disclosure.Button>
{/* Add here your custom menu elements */}
</div>
</Disclosure.Panel>
</>
)}
</Disclosure>
)
}

Contract Interaction Code

Interact.tsx

import { BigNumber } from "ethers";
import contractABI from "./Coffee.json"
import Router from "next/router";

const contractAddress = "0xc43072beD839F27C42a898167C56Ef271871FF07";

export function donationContract(kit: any) {
return new kit.connection.web3.eth.Contract(contractABI.abi, contractAddress)
}
/*
* Save the new feed to the blockchain
*/
export const createCreator = async (address: string | null | undefined, username: string,
profilePixUrl: string, userBio: string, kit: any) => {
try {
const txHash = await donationContract(kit).methods.setCreatorDetail(
username, profilePixUrl, userBio
).send({
from: address,
})
console.log(txHash)
Router.push("/")
} catch (e) {
console.log(e)
}
}

export const sendTip = async (address: string | null | undefined, message: string, index: string | string[] | undefined, amount: BigNumber, kit: any) => {
try {
const txHash = await donationContract(kit).methods.sendTip(message, index).send({
from: address,
value: amount,
})
console.log(txHash)
Router.push("/")
} catch (e) {
console.log(e)
}
}

export const getCreators = async (kit: any) => {
try {
const creatorCount = await donationContract(kit).methods.getCreatorList().call()
console.log(creatorCount)
return creatorCount;
} catch (e) {
console.log(e)
}
}

export const creatorWithdrawTip = async (address: string | null | undefined, index: string | string[] | undefined, amount: BigNumber, kit: any) => {
try {
const txHash = await donationContract(kit).methods.creatorWithdrawTip(index, amount).send({
from: address,
})
console.log(txHash)
Router.push("/Dashboard")
} catch (e) {
console.log(e)
}
}

Code Snippet for the different Pages of the dApp

_app.tsx file

import "../styles/globals.css";
import React, {useState, useEffect} from "react";
import type { AppProps } from "next/app";
import { CeloProvider, Alfajores, NetworkNames} from '@celo/react-celo';
import '@celo/react-celo/lib/styles.css';

import Layout from "../components/Layout";

function App({ Component, pageProps }: AppProps) {
const [showChild, setShowChild] = useState(false);
useEffect(() => {
setShowChild(true);
}, []);

if (!showChild) {
return null;
}

if (typeof window === 'undefined') {
return <></>;
} else {
return (
<CeloProvider
dapp={{
name: 'celo-composer dapp',
description: 'My awesome celo-composer description',
url: 'https://example.com',
icon: 'https://example.com/favicon.ico',
}}
// defaultNetwork={Alfajores.name}
networks={[Alfajores]}
network={{
name: NetworkNames.Alfajores,
rpcUrl: 'https://alfajores-forno.celo-testnet.org',
graphQl: 'https://alfajores-blockscout.celo-testnet.org/graphiql',
explorer: 'https://alfajores-blockscout.celo-testnet.org',
chainId: 44787,
}}
connectModal={{
providersOptions: { searchable: true },
}}
>
<Layout>
<Component {...pageProps} />
</Layout>
</CeloProvider>
)
}
}

export default App;

CreateAccount.tsx

This page handles creation of account by creators. And a creator can only create one account as specified in the contract.

import React, { useState } from 'react'
import { pinFileToPinata } from '@/pinata/pinProfilePix'
import { useCelo } from '@celo/react-celo';
import { createCreator } from '@/interact';

export default function CreateAccount() {
const [username, setUsername] = useState<string>("")
const [userBio, setUserBio] = useState<string>("")
const [profilePix, setProfilePix] = useState<string | File | number | readonly string[] | undefined>(undefined)
const { address, kit } = useCelo()
const handleUsername = (e: React.FormEvent<HTMLInputElement>) => {
setUsername(e.currentTarget.value)
console.log(e.currentTarget.value)

}

const handleUserBio = (e: React.FormEvent<HTMLTextAreaElement>) => {
setUserBio(e.currentTarget.value)

}

const handleprofilePix = (e: React.ChangeEvent<HTMLInputElement>) => {
if (e.target.files != null) {
setProfilePix(e.target.files[0]);
console.log(e.target.files[0])
}
}
const createAccount = async () => {
if (!address) {
alert("Please connect your wallet")
return
}

if (username === "") {
alert("Username required!")
return
}

if (userBio === "") {
alert("Brief bio required")
return
}
if (username.indexOf(' ') >= 0) {
// setErrorMessage("Space not allowed here")
alert("Space not allowed here")
return
}

if (!profilePix) {
alert("Please upload your profile photo")
return
}

const pinataHash = await pinFileToPinata(profilePix)
await createCreator(address, username, pinataHash, userBio, kit)
}

return (
<div>
<h1 className='mb-4 text-lg font-bold'>Are you a Creator? Create Account here </h1>
<div className='w-full'>
<input className='w-full border-2 rounded-md mb-2 p-2' type="text" placeholder='username' value={username} onChange={handleUsername} />
</div>
<div>
<textarea className='w-full border-2 rounded-md p-2' placeholder='Brief bio' value={userBio} onChange={handleUserBio} />
</div>
<label>Upload your profile pix</label>
<div>
<input className='w-full' type="file" id="formFile" onChange={handleprofilePix} />
</div>
<button className=' w-full bg-yellow-300 mt-4 p-4 rounded-md' onClick={createAccount}>Create Account</button>
</div>
)
}

The UI looks like this 👇

Fig 3-3 Create Account

Create Account

Index.tsx

This page displays the list of creators and a button to access the create account page.

import React, { useState, useEffect } from 'react'
import Image from "next/image"
import Link from "next/link"
import { getCreators } from '@/interact'
import { useCelo } from '@celo/react-celo';
import Router from 'next/router';

export default function Home() {
const [creators, setCreators] = useState<any[]>([])
const { kit, address } = useCelo()

useEffect(() => {
const allCreators = async () => {
try {
const creatorList = await getCreators(kit)
setCreators(creatorList)
} catch (e) {
console.log(e)
}

}
allCreators()
}, [kit])

return (
<div>
<div>
<h1>Are you a creator? click the button below to create account!</h1>
<button className="bg-yellow-300 rounded-md p-4 mt-4" onClick={() => window.open("CreateAccount")}> Create Creator Account</button>
</div>

<div className="flex justify-around">
{!address ? <div className='text-center mt-2'>Please connect your wallet to view listed creators </div>
: creators && creators.map((item, index) => <div key={index} className="w-3/4 mt-2 mx-2 border-2 border-yellow-300 p-4 rounded-md">
<Image src={`https://ipfs.io/ipfs/${item.ipfsHash}`} alt="profile-pix" width={300} height={200} />
<p>{item.username}</p>
<p>{item.userbio}</p>
<label>Donation received:</label>
<span className="font-bold">{`${item.donationsReceived/1e18 } CELO`}</span>
<p className="">{`Supporters: ${item.supporters}`}</p>
<Link
href={{
pathname: `/Support/`,
query: { id: item.id, walletAddress: item.walletAddress }// the data
}}
>
<button className="w-full bg-yellow-300 rounded-md p-2 my-2">{` Support ${item.walletAddress.substring(0,7)}...`}</button>
</Link>
</div>
)}
</div>
</div>
)
}

The UI looks like this 👇

Fig 3-4 Creators List Page Creators List Page

Support.tsx

This page handles the support form that individuals can use to send in their support to creators.

import React, { useState } from 'react'
import { sendTip } from '@/interact'
import { useCelo } from '@celo/react-celo'
import { ethers } from 'ethers'
import { useRouter } from 'next/router'

export default function Support() {
const [amount, setAmount] = useState<string>("")
const [comment, setComment] = useState<string>("")
const { address, kit } = useCelo()
const router = useRouter()

const {
query: { id, walletAddress}
} = router

const props = {
id,
walletAddress
}

const handleAmount = (e: React.FormEvent<HTMLInputElement>) => {
setAmount(e.currentTarget.value)
console.log(e.currentTarget.value)
}

const handleComment = (e: React.FormEvent<HTMLTextAreaElement>) => {
setComment(e.currentTarget.value)
console.log(e.currentTarget.value)
}
const sendSupport = async () => {
if (amount === "") {
alert("amount required!")
return
}
await sendTip(address, comment, props.id, ethers.utils.parseUnits(amount, "ether"), kit)
}

return (
<div>
<h1 className='mb-4 text-lg font-bold'>Support Creator </h1>
<div className='w-full'>
<input className='w-full border-2 rounded-md mb-2 p-2' type="number" placeholder='amount' value={amount} onChange={handleAmount} />
</div>
<div>
<textarea className='w-full border-2 rounded-md p-2' placeholder='Say something nice' value={comment} onChange={handleComment} />
</div>
<div>
<input className='w-full border-2 rounded-md p-2' type="text" placeholder='wallet Address' disabled value={props.walletAddress} />
</div>
<button className=' w-full bg-yellow-300 mt-4 p-4 rounded-md' onClick={sendSupport}>Send Support</button>
</div>
)}

The UI looks like this 👇

Fig 3-5 Support Page Support Page

Dashboard.tsx

This page handles information about the creator. And it's only made visible when a connected user has a creator account.

import React, {useState, useEffect} from 'react'
import Image from 'next/image'
import { getCreators } from '@/interact'
import { useCelo } from '@celo/react-celo'
import Link from 'next/link'

export default function Dashboard() {
const [data, setData] = useState<any>({})
const { address, kit } = useCelo()
useEffect(() => {
const creatorData = async () => {
const creators = await getCreators(kit)
if (!address) {
return null
} else {
return setData(creators.find((item: any) => item.walletAddress === address))
}
}
creatorData()
}, [address, kit])
console.log("creator id", data.id)
return (
<div>
{ !address ? <div>Please connect your wallet</div> :
<div className="w-full mt-2 mx-2 border-2 border-yellow-300 p-4 rounded-md">
<Image src={`https://ipfs.io/ipfs/${data.ipfsHash}`} alt="profile-pix" width={200} height={200} />
<p>{data.username}</p>
<p>{data.userBio}</p>
<label> Donation received: </label>
<span className="font-bold">{data.donationsReceived === undefined ? null : `${data.donationsReceived/1e18} CUSD`}</span>
<p className="mb-4">{`Supporters: ${data.supporters}`}</p>
{/* <a className='className="w-full bg-yellow-300 rounded-md p-2 my-4"' href="Withdraw">Withdraw Tip</a> */}
<Link
href={{
pathname: `/Withdraw`,
query: { id: data.id, walletAddress: data.walletAddress }// the data
}}
>
<button className="w-full bg-yellow-300 rounded-md p-2 my-2">{`Withdraw Tip`}</button>
</Link>
</div>
}
</div>
)
}

The UI looks like this 👇

Fig 3-6 Dashboard Page

Dashboard Page

Withdraw.tsx

This page handles withdrawal by the creator.

import React, { useState } from 'react'
import { creatorWithdrawTip } from '@/interact'
import { useCelo } from '@celo/react-celo'
import { ethers } from 'ethers'
import {useRouter} from 'next/router'

export default function Withdraw() {
const [amount, setAmount] = useState<string>("")
const { address, kit } = useCelo()
const router = useRouter()
const {
query: { id, walletAddress}
} = router

const props = {
id,
walletAddress
}
const handleAmount = (e: React.FormEvent<HTMLInputElement>) => {
setAmount(e.currentTarget.value)
console.log(e.currentTarget.value)

}
console.log("id is", props.id)
const withdrawTip = async () => {
if (amount === "") {
alert("amount required!")
return
}
await creatorWithdrawTip(address, props.id, ethers.utils.parseUnits(amount, "ether"), kit)
}

return (
<div>
<h1 className='mb-4 text-lg font-bold'>Withdraw Tip</h1>
<div className='w-full'>
<input className='w-full border-2 rounded-md mb-2 p-2' type="number" placeholder='amount' value={amount} onChange={handleAmount} />
</div>
<div>
<input className='w-full border-2 rounded-md p-2' type="text" placeholder='wallet Address' disabled value={props.walletAddress} />
</div>
<button className=' w-full bg-yellow-300 mt-4 p-4 rounded-md' onClick={withdrawTip}>Withdraw Tip</button>
</div>
)}

The UI looks like this 👇

Fig 3-7 Withdraw Page

Withdraw Page

Deployment on Fleek 🌠

Fleek is an IPFS decentralized deployment platform. To get started using Fleek you first need to create an account. You can create an account with your github account. This will connect your github account to fleek for easy access to your repositories and hence faster deployment.

Once logged in, click on the add new site button. This will load the projects on your github account and you can select the project you want to add using the search field.

In my own case I have already added this project so it’s not going to show on the list.

Fig 4-1 Fleek Landing Page

Fleek Landing Page

Fig 4-2 Sign in with Git Provider

Sign in with Git Provider

Fig 4-3 Add New Site Add New Site

You might run into some few errors when uploading a next.js app to Fleek. So it will be useful to follow this next.js deployment guide.

Note Don’t forget to add your Pinata JWT TOKEN and any other environment variable used on the react-app to your fleek environment variable deployment setup.

On your fleek dashboard click on the settings tab -> Build and Deploy -> Advanced build Settings. You will see Environment Variables. You can click on the edit settings button to add variables.

Fig 4-4 Add Environment Variables Add Environment Variables

You can also change the generated domain url under Domain Management to your own custom domain. If you don’t have a custom domain you can change the generated site url to something more readable as long as the name is available. To do this click on Site Details then click on the Change Site Button. That’s all you need to do!

Conclusion​

Congratulations 🎉 on finishing this tutorial! Thank you for taking the time to complete it. In this tutorial, you have learned how to create a full stack dApp using Celo Composer and Solidity.

To have access to the full codebase, here is the link to the project repo on github.

About the Author​

Glory Agatevure is a blockchain engineer, technical writer, and co-founder of Africinnovate. You can connect with me on Linkedin, Twitter and Github.

References​

Go back

· 6 min read

header

Introduction

Every technology has its tools and tooling approach and blockchain networks are no different. For one that has decided to develop on the Celo blockchain, there are numerous tools available for potential Celo developers and those already developing on the Celo network to make their work easy.

A bad workman quarrels with his tools they say, getting acquainted with tools that would be highlighted here would ensure one is an authority at what he does. Moreso, you will be using the right tool for the job.

Prerequisites

  • You need basic programming knowledge
  • Head over to Celo docs to familiarize yourself with few concepts

Requirements

  • We'll need Metamask, install it from HERE.
  • Node.js from V12. or higher.

Celo Tools

Celo team has developed numerous SDKs, libraries and tools to enable both potential blockchain developers, and those already working on the Celo network to have as many options as possible in choosing the right tools for development. Appropriate usage of them helps us to avoid overkills in development and interaction processes.

Celo Connect

Celo connect provides the most basic functionality needed to interact with Celo blockchain. For instance, if all you need is sending and signing transactions or working without need to use Celo Contract Wrappers, Celo connect should be your go-to tool.

Sticking to the exact tool for a job ensures you comply with one of the basics principles in programming - You Aren't Going to Need It. (YAGNI)

Installation

npm install @celo/connect

Basic Usage of @celo/connect

Celo connect

ContractKit

Contract Kit is a library designed to make Celo interaction very easy. It contains packages needed to interact on the Celo Network. It sets feeCurrency for a transaction, gives full access to Celo Contract Wrappers and much more unlike @celo/connect

Installation

npm install @celo/contractkit

Basic usage of @celo/contractkit

Contract Kit

React Celo

React Celo library has done the heavy lifting for React developers. All the cool functionalities that are available in the contractkit are exposed as React hooks via useCelo and, also uses React Context Provider to have react-celo states available throughout your application.

If you just want to set up Celo in your React application without bordering on what is happening under the hood, react-celo is the best bet. It is ideal for scaffolding Celo projects for a quick prototype and, also for production projects.

It is worth mentioning that it supports several wallets; Celo Dance, Celo Extension Wallet, Celo Terminal, Celo Wallet, Ledger, MetaMask, Plaintext private key, Omni, Valora, WalletConnect.

Both Developer and User Experience are factored in by default. For instance, to get last connected account or account that has once connected to your dApp is now a breeze in react-celo.

Below, is the react-celo modal system for connecting to your user's wallet of choice.

React Celo

Installation

npm install @celo/react-celo @celo/contractkit

Basic Usage of @celo/react-celo

React Celo usage

Celo Composer

With Celo Composer, one can easily set up Celo on any of the popular front-end frameworks like; React, React Native, Flutter and Angular using CLI.

This tool is perfect for web2 developers trying to get started on the Celo blockchain.

Installation

npm install @celo/celo-composer

Basic Usage of @celo/celo-composer

To get started, run npx @celo/celo-composer create, you would be presented with stages of prompts to assist in scaffolding a front-end application of your choice alongside with Solidity development framework like; Hardhat and Truffle.

The directory structure looks like the image below. Inside the packages folder, is the front-end (react-app) and Solidity (hardhat) frameworks we chose to work with.

Celo Composer

Rainbowkit Celo

RainbowKit Celo is an offshoot from Rainbowkit library but for Celo blockchain. Intuitive, responsive and customizable features make it outstanding for Celo Developers.

It supports main CELO wallets (Valora, Celo Wallet, and Celo Terminal). Like React Celo, it is another option that allows Celo blockchain interaction faster using React.

Installation

npm install @celo/rainbowkit-celo

Basic Usage of @celo/rainbowkit-celo

Raibowkit Celo

You should have an interface that looks like the one below after starting the app.

Raibowkit Celo UI

Getting an error during package installation?

If you are faced with error Could not resolve dependency, try adding –legacy-peer-deps flag to your command e.g npm install rainbow-me/rainbowkit –legacy-peer-deps

Celo CLI

If you are the type of developer that is obsessed with Command Line, Celo community has not left you out, @celo/celocli to the rescue.

Installation

It is very important that you must have Node.js v12.x else it would not work. You can check this tutorial on how to manage your Node version. After you have installed the appropriate Node version run:

npm install -g @celo/celocli

to install it globally on your machine, or

npm install @celo/celocli

to install it on your project directory, afterwards your run

npx celocli

After successful installation, if you run celocli, you will see the list of the available commands.

Celo CLI commands

Basic Usage of @celo/celocli

For a simple demonstration, I’m going to check my Celo testnet (Alfajores) account balance. First configure celocli to work with Alfajores. You can do the same for any of the Celo networks.

celocli config:set --node=https://alfajores-forno.celo-testnet.org

Then run

celocli account:balance <enter your Celo wallet address here>

You will now have your Celo wallet balances listed like below.

Celo CLI account

To know more about the account module run

celocli account

It will list all the available options for account. Same goes for other modules.

Conclusion

One should be free from a trial and error approach as regards making decisions on the correct Celo tool to use on one’s project.

We were able to see Celo tools briefly. Head over to Celo docs to see more of the developer tools, libraries, and SDKs.

About the Author

A software engineer, co-founder, Africinnovate and a Web3 enthusiast. I used to call myself VueDetective. Connect with me on LinkedIn and Github

References

https://docs.celo.org/developer

Go back

· 11 min read
Ernest Nnamdi

header

Hello there!! 🙋🏾‍♂️

This is the third instalment of the composer series where I demonstrate how to build defi applications in no time using the Celo-composer. The first two instalments were Building a crowdfunding refi dapp using celo composer and react and Building a decentralized newsfeed with Celo composer, React, and IPFS.

The Radical Shift

So here I’m thinking that every company, tech or otherwise will eventually come to be functionally web3-savvy to various degrees. Ultimately, if they play in these climes for long enough.

We have more companies globally, waking up to the fact they can do more with blockchain technology. This means, of course, they are probably already sighing at the tedium that comes with migrating to or incorporating web3 into their existing systems.

Blockchain companies are now burdened with the task of juggling both providing vital services and lowering the learning curve. At the critical merge point ( the point where new tech is introduced into the work dynamic), plan-to-execution time drops significantly ( this is precipitated by the need to work in a different way that requires the usage of the particular tech i.e web3 in this instance).

The obvious solution then becomes creating solutions that ease developers into web3. This my friends is where Celo stands clear of the crowd. In what regard you may ask? The success of web3 and the full potential of blockchain technology can only be realised with massive adoption. Without adoption, there will be no reason to build. That’s why Celo has taken the lead in the industry by providing tooling to support developers of all levels, from the expert to the web2 developer still toying with the idea of building defi applications. One of such tools is the Celo composer.


Celo Composer

The Celo Composer is a starter pack built on the react-celo toolkit to get you up and running fast in developing DApps on the Celo blockchain. This starter pack is best suited for web2 developers currently transitioning into web3 as it abstracts all the complexities involved in setting up and developing Defi applications and replaces them with a plug-and-play environment.

The starter pack, which currently supports React, React-Native, and Flutter requires little to no configurations from you as it eases you into the web3 sphere.

Now for today’s project

Here’s a list of what we’ll cover in this article:

  • ✅ Step 1: Setting up your environment.
  • ✅ Step 2: Creating your smart contract.
  • ✅ Step 3: Deploying your smart contract.
  • ✅ Step 4: Getting started with the frontend.
  • ✅ Step 5: Interacting with your smart contract from the frontend.

What are we building?

In this article, we are going to use the Celo Composer starter-kit which comes pre-integrated with NextJS, and also Tailwind for styling. This dapp will allow users whose wallet are connected, stake their tokens and earn returns on them.

full-build

This is the end result of our project today. If your learning style is “code first”, you can find the complete code for this project here on Github.

Do follow the commands in the README-md file to get started with setting up your project.

Prerequisites

  • Solidity
  • React
  • Tailwind

Step 1: Setting up your environment ✅​

There are two options to setting up your environment.

Using the Template:

Navigate to the Celo Composer repository and follow the step by step guide entailed in the README-md.

celo-composer

Using the CLI:

The Celo Composer CLI is the easiest way to setup your environment because unlike the first option, it only installs dependencies and boilerplate code necessary for the framework you intend to use.

npx @celo/celo-composer create

Running this command throws up a prompt for you to select the framework of your choice and you are fully setup to start using the starter-kit.

To install dependencies locally and setup test wallet, please refer to the README-md.

Step 2: Creating your smart contract ✅

  • Open your project on your text editor and ensure you’re on the root folder and the navigate to the packages/hardhat folder and rename the .envexample file to .env.

  • In the .env file, paste in the private key of your wallet. If you are wondering where to find your private key, please refer to the README-md in the Celo composer repository. If you prefer to setup your metamask wallet to work with this and future projects, please refer to this guide.

code

  • Navigate to the contracts folder and here we are going to add two contracts. One for the ERC20 token we are going to be using, and our staking contract. So create two new files, Piron.sol and Staking.sol respectively.

code

  • In the Piron.sol file, paste in this contract and save. This is a basic ERC20 contract compliant with the IERC20 standards. This token whose symbol is PTK will serve as our staking and reward token.

code

  • In the Staking.sol file, paste in this contract. This is a simple staking contract that accepts the address of our token(Piron token) and assigns it to the pirToken variable. This contract has six functions or methods but we will only be interacting with three, which are the staking function, pause and unpause function.

code

Step 3: Deploying your smart contract ✅

After setting up and creating our smart contracts, the only thing left to do on the backend of our project is to deploy it to the blockchain. Deploying contracts using the Celo Composer toolkit is an unbelievably seamless process.

  • Navigate to the deploy folder in the hardhat directory and in your 00-deploy.js file, you will see the deployment function for the greeter contract(A sample contract that comes with the starter kit).
await deploy(“Greeter”, {
from: deployer,
args: [“hello world”],
log: true,
})

Update the function to deploy your PironToken and StakePIR by replacing the function with this

const piron = await deploy(“PironToken”, {
from: deployer,
log: true,
})

await deploy(“Greeter”, {
from: deployer,
args: [piron.address],
log: true,
})

module.exports.tags=["PironToken", "StakePIR"];

code

We have two deploy functions, the first functions (piron) deploys our staking and reward token and the second deploys our staking contract and takes as an argument, the contract address of our token(PironToken).

There you have it! All that is left to do, is to open up your terminal, navigate to the hardhat directory

cd packages/hardhat

and run

yarn deploy

This compiles your contracts and deploys them to the Celo blockchain (Alfajores testnet).

View smart contract

Open the Celo Block Explorer (Alfajores Testnet) and paste the transaction or deployed address to view the transaction or smart contract. You can also check your wallet to confirm that the gas fee has been deducted from your balance.


Step 4: Getting started on the frontend ✅

Now we are done with the hardhat folder, we move on to the react-app folder. Navigate to the react-app folder by running on your terminal

cd ../react-app

or if you are in the root directory

cd packages/react-app

Tailwind

Follow the official tailwind guide to add tailwind to your project.

Note: Ensure you are in the react-app directory before installing tailwind.

After installing tailwind, you will notice two new files has been created for you. In the tailwind.config.js file, replace the boilerplate code with this.

code

After updating the tailwind config file, create a new folder in the react-app directory called styles.

In this new folder, create a file called global.css and then paste in this code. These are just basic styling and mostly gradients(most of which we are not going to use, so feel free to take advantage of them to customize your frontend).

code

The final setup for the tailwind configuration is to import the global.css file in the pages/_app.tsx file.

 import “../styles/global.css”;

Add this below the import statements in your _app.tsx file and voila we are done setting up tailwind.

code

AppLayout.tsx

The first file we are going to change is the components/layout/AppLayout.tsx file. We are going to replace it with the following code:

import * as React from “react”;
import Meta from “../meta/Meta”;
import { Header } from “./Header”;
interface Props {
title: string;
description: string;
children: React.ReactNode;
}
export default function AppLayout({ title, description, children }: Props) {
return (

<div className=”flex-1 h-full bg-gray-800">
<Header />
<Meta title={title} description={description} />
{children}
</div>
);
}

code

Utils/Index.tsx

In the utils folder, you are going to add one more utility function to the index.tsx file. Paste in this function below

export function formatTime(timestamp: number) {
const milliseconds = timestamp \* 1000;
const dateObject = new Date(milliseconds);
const humanDateFormat = dateObject.toLocaleDateString()
return humanDateFormat;
}

Step 5: Interacting with your smart contract from the frontend ✅

Index.tsx

Next file we are going to work on, is the index.tsx file. Here, we are going to delete all the placeholder code and replace it with this

code

Code Walkthrough
const contracts =
deployedContracts[network?.chainId?.toString()]?.[
network?.name?.toLocaleLowerCase()
]?.contracts;

The variable contracts is being assigned, every deployed contract which is imported from the deployments folder in the hardhat directory. The variable is in turn, being passed in to the HomePage.tsx.

HomePage.tsx

Navigate to the pages directory and create a new file called HomePage.tsx and in this newly created file, paste in this code. This will serve as as our home screen.

code

Code walkthrough
const { kit, address } = useCelo();

The useCelo() is gotten from react-celo which is a React hook and the easiest way to access Celo in your React application. Kit is used to query onchain data while address returns the address of the user after the connect function has connected the wallet to the project(Refer to components/layout/Header.tsx to see the connect function in action).

const pironContract = contracts
? (new kit.connection.web3.eth.Contract(
contracts.PironToken.abi,
contracts.PironToken.address
) as any as PironToken)
: null;

This creates a new instance of the contract (In this case our PironToken contract). It leverages kit which was destructured from useCelo() to create a new contract instance by passing in, the abi and address of the contract.

Note: “contracts” was passed in to the HomePage from the Index.tsx and contains all deployed contracts if any.

await pironContract.methods
.approve(contracts.StakePIR.address, “1000000000”)
.send({ from: address });

The above function is contained in the submit function and it is responsible for calling our smart contract methods or functions. This particular function calls the approve function in the pironContract and passes as arguments, the contract address of the staking contract and also a gas amount.

const paused = await stakingContract.methods.paused().call();

The above function, is quite similar to the previous in the sense that they both call methods or functions in the smart contract but do notice that theres is a difference between them. .call() is used in place of .send({…}) this is because this particular function does not create a new transaction on the blockchain.

Input.tsx

Navigate to the components directory and create a file called Input.tsx and paste in this code. This component is our custom input field to accept input from the frontend.

code

ProjectCard.tsx

Still in the components directory, create another file called ProjectCard.tsx and paste this code in it. The ProjectCard component is a way to display all the data we fetch from the blockchain.

This card will contain relevant information about our stake contract like number of stakers, start date, end date, etc and also allow up to make some contract calls such as pause, unPause and Claim Rewards.

code

Conclusion

This brings to an end, this session on building with Celo composer. But your learning don’t have to end here as PRs are welcome for those who want to contribute to this project.

Here is a quick recap of everything we covered.

  • ✅ Step 1: Setting up your environment.
  • ✅ Step 2: Creating your smart contract.
  • ✅ Step 3: Deploying your smart contract.
  • ✅ Step 4: Getting started on the frontend.
  • ✅ Step 5: Interacting with your smart contract from the frontend.

For those who have questions or want to be part of our developer community, join us on discord!.

Till next time,

Adios ✌🏾

Go back

· 13 min read
Ernest Nnamdi

Hello, friends! 🙋🏾‍♂️

header

Stacks

What is the conventional wisdom for getting involved in web3? Something involving solidity.

What does this look like for you as a web2 developer seeking to make this transition? New language? - definitely, but also a headache of new tools and frameworks to learn and getting conversant with. The learning curve between you and your excellent Defi app causes light (or heavy) showers on your parade.

As a developer evangelist, I won't be doing my job if I didn't tell you there is a better way out. The blockchain space is still very much in the "developer phase," and companies, protocols, and blockchains now have the responsibility of lowering the barrier to entry by making it easy to break into blockchain from traditional web/mobile development.

This is an area where Celo stands clear of other blockchains and protocols with its impeccable SDKs, APIs, and toolkits. Speaking of toolkits, we will use the Celo Composer toolkit today.


Celo Composer

The Celo Composer is a starter pack built on the react-celo toolkit to get you up and running fast in developing DApps on the Celo blockchain.

This starter pack is best suited for web2 developers currently transitioning into web3 as it abstracts all the complexities involved in setting up and developing Defi applications and replaces it with a plug-and-play environment.

The starter pack, which currently supports React, React-Native, and Flutter requires little to no configurations from you as it eases you into the web3 sphere.

Now to the crux of the matter

Here's a list of what we'll cover in this article:

  • ✅ Step 1: Setting up your environment.
  • ✅ Step 2: Creating your smart contract.
  • ✅ Step 3: Deploying your smart contract.
  • ✅ Step 4: Getting started with the frontend.
  • ✅ Step 5: Interacting with your smart contract from the frontend.

What are we building?

In this article, we are using the Celo Composer starter-kit, which comes pre-integrated with NextJs, Tailwind CSS, and IPFS HTTP client, to build a decentralized news feed where users can connect their wallets and share news as it happens around them and also read submissions from others.

full-build

This is what the final project looks like. If your learning style is "head first," then you can find the complete code for this project on Github.

Do follow the commands in the README-md file to get started with setting up your project.

Prerequisites

  • Solidity
  • React
  • Tailwind

Setting up your environment

There are two options to setting up your environment.

Using the Template: Navigate to the Celo Composer repository and follow the step by step guide entailed in the README-md .

celo-composer

Using the CLI: The Celo Composer CLI is the easiest way to setup your environment because unlike the first option, it only installs dependencies and boilerplate code necessary for the framework you intend to use.

npx @celo/celo-composer create

Running this command throws up a prompt for you to select the framework of your choice and youre fully setup to start using the starter-kit.

celo-composer

To install dependencies locally and setup test wallet, please refer to the README-md.


Step 1: Create your smart contract ✅

  • On your terminal, make sure you are in the root directory and then navigate to the contracts folder by running the following command:
cd packages/hardhat/contracts
  • Create a new file in the contracts folder called NewsFeed.sol
  • Copy the smart contract code from here and paste it into the file you created, and now your NewsFeed.sol file should look like this:

newsfeed.sol


Step 2: Write your deploy script ✅

After setting up our environment and saving our smart contract in our newly created file, the next step is to update the deploy script to be able to compile and deploy our smart contract to the blockchain.

  • Navigate to the packages/hardhat/deploy/00-deploy.js file, and you'll see the deploy function for the greeter contract( A sample contract that comes with the starter kit).
await deploy("Greeter", {
from: deployer,
args: ["hello world"],
log: true,
})
  • Update the function to deploy our NewsFeed instead by replacing the function with this.
await deploy("NewsFeed", {
from: deployer,
log: true,
})
  • Once that is done, scroll to the bottom of the page and update the exports by adding NewsFeed to them, and voila, you are done! This is all the setup that is required on the backend.
module.exports.tags = ["NewsFeed"];

Step 3: Deploy your smart contract ✅

Now we are done with the smart contract and have updated our deploy script; next is deploying the smart contract to the blockchain. To do this, run a straightforward command. Go back to your terminal, ensure you're on the correct directory, i.e., packages/hardhat, then run

yarn deploy

View smart contract

Open Celo Block Explorer (Alfajores Testnet) and paste the transaction or deployed address to view the transaction or smart contract. You can also check your wallet to confirm that the gas fee has been deducted from your balance.


Step 4: Getting Started on the frontend ✅

Navigate to the React app by running the following command on your terminal.

Note: No worries if you have closed the terminal you used in deploying your smart contract! Ensure you are in the root directory of your application, then run the following

cd packages/react-app

If not, run the following

cd ../react-app

Note: This presumes you followed the env setup guide and have installed the dependencies already

Tailwind CSS

Follow the official tailwind guide to add tailwind to your project.

After that, delete everything in the tailwind.config.css file generated automatically for you and replace it with this code.

Your tailwind.config.js file should look like this now.

tailwind

Create a new folder called styles in your react-app directory and a new file in the styles folder called global.css. Paste this code into the global.css file.

Your global.css file should now look like this:

tailwind

The final setup for our tailwind configuration is to import the global.css file into the pages/_app.tsx file. Add

 import ../styles/global.css

to the list of imports, and voila! You are ready to start using tailwind in your project.

The next stop is the pages/index.tsx file. Delete everything in the file and replace it with this code. Your index.tsxfile should look like this:

index.tsx


AppLayout.tsx

Now, navigate to the layout/AppLayout which is nested in the components folder and update the function with the code below:

import * as React from "react";
import Meta from "../meta/Meta";