Skip to main content

30 posts tagged with "smartcontract"

View All Tags
Go back

· 8 min read
Israel Okunaya

building-an-nft-marketplace-on-celo-with-python

Introduction

NFTs or Non-Fungible, an application of blockchain is used to represent unique digital assets such as collectibles, arts, etc. Celo is a blockchain platform that is designed to send, receive, and store digital assets securely on a mobile phone. The Celo blockchain is built using the Solidity programming language and is fully compatible with Ethereum. In this tutorial, we will be building an NFT marketplace with Python using the brownie framework and deploying it on Celo Testnet.

Prerequisites

To understand this tutorial, you must be familiar with the following:

  • Building Smart Contracts
  • The Python programming language

Requirements

You should have the following installed on your computer to follow along:

Setting Up Project

To get started, we have to create a new directory for our project and install the following dependencies:

mkdir nft-marketplace
cd nft-nft-marketplace
# Create virtual environment
python3.10 -m venv venv
# activate virtual environment
source venv/bin/activate

Note: Brownie works bet with python3.10

# Install ganache
npm install -g ganache
# Install eth-brownie and python-dotenv
pip3 install eth-brownie python-dotenv

After installing dependencies, we need to initialize our project as a brownie project.

brownie int

This command generates some folder which will look like this:

image

After initializing brownie into our project, in your root directory, create two files called .env and brownie-config.yaml. The .env file is used to store environment variables that should not be exposed to the public such as our private key, mnemonic phrase, etc, while brownie-config.yaml is used to configure brownie in our project.

.env

MNEMONIC="..."
PRIVATE_KEY="0x..."

brownie-config.yaml

reports:
exclude_contracts:
- SafeMath
depedencies:
- OpenZeppelin/openzeppelin-contracts@4.8.0
compiler:
solc:
version: 0.8.15
optimizer:
enabled: true
runs: 200
remappings:
- "@openzeppelin=OpenZeppelin/openzeppelin-contracts@4.8.0"
networks:
default: celo-alfajores
console:
show_colors: true
color_style: monokai
auto_suggest: true
completions: true
editing_mode: emacs
dotenv: .env
wallets:
from_mnemonic: ${MNEMONIC}
from_key: ${PRIVATE_KEY}

The next step is to add Celo Testnet (Alfajores) to our brownie project:

brownie networks add Celo celo-alfajores host=https://alfajores-forno.celo-testnet.org chainid=44787 explorer=https://alfajores-blockscout.celo-testnet.org

You can see the list of networs that have been added to our brownie project:

brownie network list

image

Implementing the Smart Contract

Next ,we have to write the smart contraxr for our NFT marketplace. In your contracts directory, create a new file called NFTMarketplace.sol

agma solidity ^0.8.0;
import "@openzeppelin/contracts/token/ERC721/ERC721.sol";
import "@openzeppelin/contracts/token/ERC721/IERC721Receiver.sol";
import "@openzeppelin/contracts/utils/math/SafeMath.sol";
contract NFTMarketplace {
using SafeMath for uint256;
struct Auction {
address tokenAddress;
uint256 tokenId;
address payable seller;
uint256 price;
uint256 endTime;
bool active;
}
address public owner;
uint256 public feePercentage; // percentage of the sale price taken as fee
mapping(address => mapping(uint256 => Auction)) public auctions; // map of all active auctions
event AuctionCreated(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed seller,
uint256 price,
uint256 endTime
);
event AuctionEnded(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed buyer,
uint256 price
);
constructor() {
owner = msg.sender;
feePercentage = 1; // 1% fee by default
}
function createAuction(address _tokenAddress, uint256 _tokenId, uint256 _price, uint256 _duration) public {
require(_duration > 0, "Duration must be greater than zero");
require(_price > 0, "Price must be greater than zero");
IERC721 tokenContract = IERC721(_tokenAddress);
require(tokenContract.ownerOf(_tokenId) == msg.sender, "You don't own this NFT");
uint256 endTime = block.timestamp.add(_duration);
Auction memory auction = Auction(_tokenAddress, _tokenId, payable(msg.sender), _price, endTime, true);
auctions[_tokenAddress][_tokenId] = auction;
emit AuctionCreated(_tokenAddress, _tokenId, msg.sender, _price, endTime);
}
function endAuction(address _tokenAddress, uint256 _tokenId) public {
Auction storage auction = auctions[_tokenAddress][_tokenId];
require(auction.active, "Auction has already ended");
require(block.timestamp >= auction.endTime, "Auction hasn't ended yet");
address payable seller = auction.seller;
uint256 price = auction.price;
auction.active = false;
IERC721 tokenContract = IERC721(_tokenAddress);
tokenContract.safeTransferFrom(address(this), msg.sender, _tokenId);
uint256 fee = price.mul(feePercentage).div(100);
seller.transfer(price.sub(fee));
emit AuctionEnded(_tokenAddress, _tokenId, msg.sender, price);
}
function setFeePercentage(uint256 _feePercentage) public {
require(msg.sender == owner, "Only the owner can set the fee percentage");
require(_feePercentage >= 0 && _feePercentage <= 100, "Fee percentage must be between 0 and 100");
feePercentage = _feePercentage;
}
function withdraw() public {
require(msg.sender == owner, "Only the owner can withdraw funds");
address payable self = payable(address(this));
self.transfer(self.balance);
}
// fallback function
receive() external payable {}
}

Now let's go through the code step by step

pragma solidity ^0.8.0;
import "./IERC721.sol";
import "./IERC721Receiver.sol";
import "./SafeMath.sol";

The above code specifies our solidity version and imports the IERC721, IERC721Receiver, and SafeMath (for blockchain math operations) interfaces.

using SafeMath for uint256;
struct Auction {
address tokenAddress;
uint256 tokenId;
address payable seller;
uint256 price;
uint256 endTime;
bool active;
}

Enables us to use the SafeMath foe uint256 tyoe operations and the Auction struct defines the structure of the Auction.

mapping(address => mapping(uint256 => Auction)) public auctions;

This maps each token address and token ID to an active auction.

event AuctionCreated(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed seller,
uint256 price,
uint256 endTime
);
event AuctionEnded(
address indexed tokenAddress,
uint256 indexed tokenId,
address indexed buyer,
uint256 price
);

These are the eents that are emitted when an auction is creares and ended.

constructor() {
owner = msg.sender;
feePercentage = 1; // 1% fee by default
}

This sets the owner of the contract and the default fee (in percentage).

function createAuction(address _tokenAddress, uint256 _tokenId, uint256 _price, uint256 _duration) public {
require(_duration > 0, "Duration must be greater than zero");
require(_price > 0, "Price must be greater than zero");
IERC721 tokenContract = IERC721(_tokenAddress);
require(tokenContract.ownerOf(_tokenId) == msg.sender, "You don't own this NFT");
uint256 endTime = block.timestamp.add(_duration);
Auction memory auction = Auction(_tokenAddress, _tokenId, payable(msg.sender), _price, endTime, true);
auctions[_tokenAddress][_tokenId] = auction;
emit AuctionCreated(_tokenAddress, _tokenId, msg.sender, _price, endTime);
}
function endAuction(address _tokenAddress, uint256 _tokenId) public {
Auction storage auction = auctions[_tokenAddress][_tokenId];
require(auction.active, "Auction has already ended");
require(block.timestamp >= auction.endTime, "Auction hasn't ended yet");
address payable seller = auction.seller;
uint256 price = auction.price;
auction.active = false;
IERC721 tokenContract = IERC721(_tokenAddress);
tokenContract.safeTransferFrom(address(this), msg.sender, _tokenId);
uint256 fee = price.mul(feePercentage).div(100);
seller.transfer(price.sub(fee));
emit AuctionEnded(_tokenAddress, _tokenId, msg.sender, price);
}

The createAuction function creates a new auction and the endAuction ends an active auction.

function setFeePercentage(uint256 _feePercentage) public {
require(msg.sender == owner, "Only the owner can set the fee percentage");
require(_feePercentage >= 0 && _feePercentage <= 100, "Fee percentage must be between 0 and 100");
feePercentage = _feePercentage;
}
function withdraw() public {
require(msg.sender == owner, "Only the owner can withdraw funds");
address payable self = payable(address(this));
self.transfer(self.balance);
}

The setFeePercentage functino sets the percentage fee for the contract and the withdraw function enables the creator of the contract to withdraw the fund from it.

// fallback function
receive() external payable {}

A callback functnion that is called when ether is sent to the contract

Deploying the Contract

Next, we need to compile and deploy the contract on the Celo Testnet, Run the following command to compile the contract.

brownie compile

image

To deploy the smart contract on Celo create a new file called deploy.py in the scripts directory of your project.

from brownie import NFTMarketplace, accounts, config, network
def deploy_marketplace():
# Load the account to deploy from
dev = accounts.add(config["wallets"]["from_key"])
print(f"Deploying from {dev.address}")
# Deploy the contract
marketplace = NFTMarketplace.deploy({"from": dev})
print(f"NFTMarketplace contract deployed to {marketplace.address}")
def main():
# Set the network to deploy to
network_name = network.show_active()
print(f"Deploying to {network_name} network")
# Call the deploy function
deploy_marketplace()

The deploy_marketplace functino get the account we would use to deploy the contract.

Deploy the Contract

 Deploy the contract to the Celo Alfajores testnet
brownie run scripts/deploy.py --network celo-alfajores

image

Conclusion

In this tutorial we implemented an NFT smart contract that allows users to create and participate in ERC721 token auctions. The contract charges a fee for each sale, which is determined by the contract's owner. When an auction concludes, the winning bidder receives the NFT, while the seller receives the sale price less the fee. The contract also gives the owner the option to withdraw funds from the contract.

We have learned how to implement an NFT exchange on Celo using brownie in Python. We explored topics such as the basics of setting up a development environment, creating a smart contract and finally compiling and deploying the contract on Celo Testnet or Afrajores.

Next Step

We can explore more functionalities that we can add to this contract, such as the ability for bidders to increase the current bid by a minimum increment, the ability to see auction history, time extension on bid time, etc.

References

  1. Celo Developer Documentation
  2. Solidity Documentation
  3. Brownie Documentation
  4. Github Repository

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he's given to simplifying its complexities with text and video tutorials.

Go back

· 14 min read
Israel Okunaya

header

Introduction

In this tutorial, I will take you through setting up a wallet connection in your Dapp using Rainbowkit-celo. RainbowKit is a React library that makes it easy to add a wallet connection to your dapp. It's intuitive, responsive, and customizable. Rainbowkit-celo is a plugin to help rainbowkit developers support the CELO protocol faster. It includes the chain information and the main CELO wallets (Valora, Celo Wallet, Celo Terminal, etc.).

Prerequisites

For this tutorial, you will need to have some level of knowledge of Javascript, React, tailwindcss, and Solidity.

Requirements

For this article, you will need:

  • A code editor. VScode is preferable.
  • A chrome browser.
  • A crypto wallet: Valora, Celo wallet, MetaMask
  • Nodejs installed on your computer. If not, download from here

The Smart Contract

For this tutorial, we will be building a Will/Inheritance smart contract. To build the smart contract and deploy it to the Celo blockchain, we will be using hardhat blockchain. Hardhat is a development environment for Ethereum software. With hardhat, you can write your smart contract, deploy them, run tests, and debug your code.

Building the Will/Inheritance Smart Contract

First, create a folder where the hardhat project and your Dapp will go. In your terminal, execute these commands.

mkdir inheritance-smartContract
cd inheritance-smartContract

Then in the inheritance-smartContract folder, we will set up a Hardhat project.

mkdir will-contract
cd will-contract
yarn init --yes
yarn add --dev hardhat

If you are using Windows, you must add one more dependency with the code below:

yarn add --dev @nomiclabs/hardhat-waffle hardhat-deploy

In the same directory where you installed the Hardhat project, run:

npx hardhat

Select Create a Javascript Project and follow the steps in the terminal to complete your Hardhat setup.

Once your project is set up, create a new file in the contract folder and name it will.sol.

// SPDX-License-Identifier: GPL-3.0
pragma solidity >=0.8.2 <0.9.0;
contract Will {
address private owner;
address private _heir;
bool private isAlive;
event OwnerChanged(address newOwner);
event ChangeHeir(address newHeir);
event IsAliveChanged(bool isAlive);
event FundsTransferred(address recipient, uint256 amount);
constructor() {
owner = msg.sender;
isAlive = true;
}
function changeOwner(address newOwner) public {
require(msg.sender == owner, "Only owners can change owner");
owner = newOwner;
emit OwnerChanged(newOwner);
}
function setHeir(address heir) public {
require(msg.sender == owner, "Only owners can set heir");
_heir = heir;
}
function changeHeir(address newHeir) public {
require(msg.sender == owner, "Only owners can remove a heir");
_heir = newHeir;
emit ChangeHeir(newHeir);
}
function changeIsAlive(bool _isAlive) public {
require(msg.sender == owner, "Only the owner can change the status");
isAlive = _isAlive;
emit IsAliveChanged(_isAlive);
}
function transferFunds(address payable recipient, uint256 amount) public {
require(msg.sender == owner || (msg.sender == _heir && !isAlive), "Only the owner or heir can transfer funds");
require(amount <= address(this).balance, "Insufficient funds");
recipient.transfer(amount);
emit FundsTransferred(recipient, amount);
}

function getOwner() public view returns (address) {
return owner;
}

function getHeir() public view returns (address) {
return _heir;
}

function getIsAlive() public view returns (bool) {
return isAlive;
}

receive() external payable {}
}

In the smart contract code above, the Will contract has an owner, a designated heir, and a status that determines whether the owner is alive. The contract also has four functions:

  • changeOwner() allows the current owner to transfer ownership to a new address.
  • setHeir() allows the current owner to set a designated heir.
  • changeHeir() allows the current owner to change the designated heir.
  • changeIsAlive() allows the current owner to change their status to alive or deceased.
  • transferFunds() allows the owner or the heir (if the owner is deceased) to transfer the funds to a designated recipient.

The contract also has three functions to allow anyone to view the current owner, heir, and status.

The contract also includes several events that get emitted whenever the owner, heir, status, or funds are changed, to allow for easy monitoring of changes to the contract.

Note: This is just a simple smart contract example, and a real-world will would likely have more complex logic to handle edge cases and to ensure that the contract is secure and reliable.

Configure Deployment

We will deploy the smart contract to the Alfajores network on the Celo blockchain. To do this, we will need to connect to the Alfajores testnet through forno by writing a deployment script. Replace the content of the deploy.js file with the code below to deploy the smart contract.

const { ethers } = require("hardhat");
async function main() {
const willContract = await ethers.getContractFactory("MyWill");
const deployedWillContract = await willContract.deploy();
await deployedWillContract.deployed();
console.log("Will Contract Address:", deployedWillContract.address);
}
// We recommend this pattern to be able to use async/await everywhere
// and properly handle errors.
main()
.then(() => process.exit(0))
.catch((error) => {
console.log(error);
process.exit(1);
});

Now create a .env in the will-contract folder and add your MNEMONIC key in the .env file like this

MNEMONIC=//add your wallet seed phrase here

Now we will install dotenv package to be able to import the env file and use it in our config.

In your terminal pointing to the will-contract folder, enter this command

yarn add dotenv

Next, open the hardhat.config.js file and replace the content of the file provided to us by Hardhat with the configuration code for deployment made available by Celo.

require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });
require("hardhat-deploy");
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "alfajores",
networks: {
localhost: {
url: "http://127.0.0.1:7545",
},
alfajores: {
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
//chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
chainId: 42220,
},
},
solidity: "0.8.4",
};

Add the gas price and gas to the Alfajores object.

alfajores: {
gasPrice: 200000000000,
gas: 41000000000,
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0
}

Check out the Celo doc for more details on what each part of the configuration code does.

The content of the hardhat.config.js file should now look like this.

require("@nomiclabs/hardhat-waffle");
require("dotenv").config({ path: ".env" });
require("hardhat-deploy");
// You need to export an object to set up your config
// Go to https://hardhat.org/config/ to learn more
// Prints the Celo accounts associated with the mnemonic in .env
task("accounts", "Prints the list of accounts", async (taskArgs, hre) => {
const accounts = await hre.ethers.getSigners();
for (const account of accounts) {
console.log(account.address);
}
});
/**
* @type import('hardhat/config').HardhatUserConfig
*/
module.exports = {
defaultNetwork: "alfajores",
networks: {
localhost: {
url: "http://127.0.0.1:7545",
},
alfajores: {
gasPrice: 200000000000,
gas: 41000000000,
url: "https://alfajores-forno.celo-testnet.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
//chainId: 44787
},
celo: {
url: "https://forno.celo.org",
accounts: {
mnemonic: process.env.MNEMONIC,
path: "m/44'/52752'/0'/0",
},
chainId: 42220,
},
},
solidity: "0.8.4",
};

Compiling the Contract

To compile the contract, in your terminal, point to the will-contract folder and run this command.

npx hardhat compile

After the compilation is successful, run the following command in your terminal.

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

Save the Whitelist Contract Address that was printed on your terminal somewhere, because you would need it further down in the tutorial.

The Dapp

To develop the Dapp, we will be using Reactjs. React is a free and open-source front-end JavaScript library for building user interfaces based on components.

To create a new react-app, in your terminal, point to the inheritance-smartContract folder and type.

mkdir dapp
cd dapp
yarn create react-app

Press enter and allow the react-app to initialize.

You should have a folder structure that should look like this:

image

Now to run the app, execute this command in the terminal

yarn start

Go to http://localhost:3000 to view your running app.

Now we will install tailwindcss for the styling of the Dapp. To install tailwindcss, in your terminal, still pointing to the dapp folder, run.

yarn add --dev tailwindcss

After installation has been completed, run

npx tailwindcss init

npx tailwindcss init creates a tailwind.config.js file in your dapp folder.

in the tailwind.config.js file, copy and paste this code.

/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./src/**/*.{js,jsx,ts,tsx}"],
theme: {
extend: {},
},
plugins: [],
};

Then replace the content of the index.css file with the @tailwind directives for each of Tailwind’s layers.

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

Now we will install Rainbowkit-celo. To install Rainbowkit-celo, open up your terminal pointing to the dapp folder and run

yarn add @celo-rainbowkit-celo

The @celo-rainbowkit-celo package has @rainbow-me/rainbowkit as a peer dependency and expects it to be installed too. To install it, run the command in your terminal

yarn add @rainbow-me/rainbowkit wagmi

Next we will install webjs and @celo/contractkit. We will be using the contract kit to interact with the Celo blockchain.

yarn add web3 @celo/contractkit

We will have to make further configurations to our project folder to enable us use web and @celo/contractkit without errors or bugs. These configurations involve installing webpack and other dependencies. To install webpack, type these commands in your terminal. Make sure your terminal still points to the dapp folder you created earlier.

yarn add --dev webpack

After successfully installing webpack, create a webpack.config.js file in the dapp folder and paste the code.

const path = require("path");
module.exports = {
entry: "./src/index.js",
output: {
filename: "main.js",
path: path.resolve(__dirname, "build"),
},
node: {
net: "empty",
},
};

For the other dependencies, paste this in your terminal.

yarn add --dev react-app-rewired crypto-browserify stream-browserify assert stream-http https-browserify os-browserify url buffer process

Create config-overrides.js in the dapp folder with the content:

const webpack = require("webpack");
module.exports = function override(config) {
const fallback = config.resolve.fallback || {};
Object.assign(fallback, {
crypto: require.resolve("crypto-browserify"),
stream: require.resolve("stream-browserify"),
assert: require.resolve("assert"),
http: require.resolve("stream-http"),
https: require.resolve("https-browserify"),
os: require.resolve("os-browserify"),
url: require.resolve("url"),
});
config.resolve.fallback = fallback;
config.plugins = (config.plugins || []).concat([
new webpack.ProvidePlugin({
process: "process/browser",
Buffer: ["buffer", "Buffer"],
}),
]);
return config;
};

In the package.json file, change the scripts field for start, build, and test. Replace react-scripts with react-app-rewired:

"scripts": {
"start": "react-app-rewired start",
"build": "react-app-rewired build",
"test": "react-app-rewired test",
"eject": "react-scripts eject"
},

Now go to your index.js file in the dapp folder and copy and import the follow dependencies.

import {
RainbowKitProvider,
connectorsForWallets,
} from "@rainbow-me/rainbowkit";
import {
metaMaskWallet,
omniWallet,
walletConnectWallet,
} from "@rainbow-me/rainbowkit/wallets";
import { Valora, CeloWallet, CeloDance } from "@celo/rainbowkit-celo/wallets";
import { configureChains, createClient, WagmiConfig } from "wagmi";
import { jsonRpcProvider } from "wagmi/providers/jsonRpc";
import { Alfajores, Celo } from "@celo/rainbowkit-celo/chains";
import "@rainbow-me/rainbowkit/styles.css";

Below the imports, add the following code.

const { chains, provider } = configureChains(
[Alfajores, Celo],
[
jsonRpcProvider({
rpc: (chain) => ({ http: chain.rpcUrls.default.http[0] }),
}),
]
);
const connectors = connectorsForWallets([
{
groupName: "Recommended with CELO",
wallets: [
Valora({ chains }),
CeloWallet({ chains }),
CeloDance({ chains }),
metaMaskWallet({ chains }),
omniWallet({ chains }),
walletConnectWallet({ chains }),
],
},
]);
const wagmiClient = createClient({
autoConnect: true,
connectors,
provider,
});

Now wrap the <App/> component with the RainbowkitProvider and WagmiConfig

<WagmiConfig client={wagmiClient}>
<RainbowKitProvider chains={chains}>
<App />
</RainbowKitProvider>
</WagmiConfig>

Your index.js file should look like this.

import React from "react";
import ReactDOM from "react-dom/client";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import {
RainbowKitProvider,
connectorsForWallets,
} from "@rainbow-me/rainbowkit";
import {
metaMaskWallet,
omniWallet,
walletConnectWallet,
} from "@rainbow-me/rainbowkit/wallets";
import { Valora, CeloWallet, CeloDance } from "@celo/rainbowkit-celo/wallets";
import { configureChains, createClient, WagmiConfig } from "wagmi";
import { jsonRpcProvider } from "wagmi/providers/jsonRpc";
import celoGroups from "@celo/rainbowkit-celo/lists";
import { Alfajores, Celo } from "@celo/rainbowkit-celo/chains";
import "@rainbow-me/rainbowkit/styles.css";
const { chains, provider } = configureChains(
[Alfajores, Celo],
[
jsonRpcProvider({
rpc: (chain) => ({ http: chain.rpcUrls.default.http[0] }),
}),
]
);
const connectors = connectorsForWallets([
{
groupName: "Recommended with CELO",
wallets: [
Valora({ chains }),
CeloWallet({ chains }),
CeloDance({ chains }),
metaMaskWallet({ chains }),
omniWallet({ chains }),
walletConnectWallet({ chains }),
],
},
]);
const wagmiClient = createClient({
autoConnect: true,
connectors,
provider,
});
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(
<React.StrictMode>
<WagmiConfig client={wagmiClient}>
<RainbowKitProvider chains={chains}>
<App />
</RainbowKitProvider>
</WagmiConfig>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

In your App.js file, copy and paste this code

import "./App.css";
import { ConnectButton } from "@rainbow-me/rainbowkit";
import { useSigner } from "wagmi";
import { useState, useEffect } from "react";
import Web3 from "web3";
import { newKitFromWeb3 } from "@celo/contractkit";
import { abi } from "./will.abi";
function App() {
const { data: signer } = useSigner();
const [address, setAddress] = useState("");
const [displayValue, setDisplayValue] = useState();
const willContractAddress = "0x24B1525F0061CA0c91bE9f966c41C652A9a8D383"; //The address of your own smart contract;
useEffect(() => {
const getAddress = async () => {
if (signer) {
const address = await signer.getAddress();
setAddress(address);
}
};
getAddress();
}, [signer]);
if (address) {
console.log(address);
}
console.log(abi);
const getContractKit = () => {
if (signer) {
const web3 = new Web3(window.celo);
const kit = newKitFromWeb3(web3);
return kit;
}
};
const kit = getContractKit();
console.log(kit);
const getOwner = async () => {
const kit = getContractKit();
const contract = new kit.web3.eth.Contract(abi, willContractAddress);
try {
const owner = await contract.methods.getOwner().call();
setDisplayValue(owner);
} catch (error) {
console.log(error);
}
};
const getHeir = async () => {
const kit = getContractKit();
const contract = new kit.web3.eth.Contract(abi, willContractAddress);
try {
const heir = await contract.methods.getHeir().call();
setDisplayValue(heir);
} catch (error) {
console.log(error);
}
};
const getStatus = async () => {
const kit = getContractKit();
const contract = new kit.web3.eth.Contract(abi, willContractAddress);
try {
const status = await contract.methods.getIsAlive().call();
console.log(status);
setDisplayValue(status);
} catch (error) {
console.log(error);
}
};
return (
<div className="flex flex-col items-center justify-center h-screen">
<h1 className="mb-4 font-semibold text-2xl">
Simple Will Smart Contract
</h1>
<ConnectButton />
<div className="mt-8">
<div className="bg-white border border-green-500 border-solid p-4 rounded-lg text-center">
{displayValue ? displayValue : ""}
</div>
<div className="mt-4 grid grid-cols-3 gap-8">
<button className="bg-red-400 py-2 px-4 text-white rounded-lg">
Change Owner
</button>
<button className="bg-red-400 py-2 px-4 text-white rounded-lg">
Set Heir
</button>
<button className="bg-red-400 py-2 px-4 text-white rounded-lg">
Change Heir
</button>
<button className="bg-red-400 py-2 px-4 text-white rounded-lg">
Change Deceased Status
</button>
<button
onClick={getOwner}
className="bg-green-400 py-2 px-4 text-white rounded-lg"
>
Owner
</button>
<button
onClick={getHeir}
className="bg-green-400 py-2 px-4 text-white rounded-lg"
>
Heir
</button>
<button
onClick={getStatus}
className="bg-green-400 py-2 px-4 text-white rounded-lg"
>
Status
</button>
<button className="bg-red-600 py-2 px-4 text-white rounded-lg">
Transfer Inheritance
</button>
</div>
</div>
</div>
);
}
export default App;

In the code above we have a UI that has the ConnectButton provided by @rainbow-me/rainbowkit rendered. This custom button allows us to connect to default crypto wallets provided by Celo and other crypto wallets .that support Celo.

The getOwner(), getHeir() and getStatus() are functions that are called when a user clicks a particular button. These functions connect with the smart contract we created and deployed to call the various methods we created in the smart contract.

Start up the React app by running this command in your terminal to view the Dapp

yarn start

Make sure the terminal points to the dapp folder before running the command.

This is how the Dapp should look when the application opens

image

Conclusion

So far, we have been able to create a smart contract Dapp using React. We created the UI enabling a user to connect their wallet to the Dapp using Rainbowkit-celo. We also connected with the smart contract we deployed from the Dapp and called some of the methods we created in the smart contract.

Next Step

If you wish to read more on rainbowkit and Rainbowkit-celo, check out these docs links:

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he's given to simplifying its complexities with text and video tutorials.

Go back

· 8 min read
Israel Okunaya

header

Introduction

Smart Contract Upgradability is a state whereby changes or upgrades to a smart contract do not affect its state and functionality. This functionality allows smart contracts to be easily managed and improved upon over time.

Just like the normal software development lifecycle, where the software needs to be maintained, scaled, and optimized even after building its underlying infrastructure, smart contracts should undergo such a process too because bugs and vulnerabilities can be discovered even after a contract has been deployed, need for improvements maybe as a result of changes from the underlying blockchain protocol. But keep in mind that the process of upgrading a smart contract should be done with adequate planning so as to avoid bringing vulnerability attacks to your contract.

In this tutorial, we will explore different methods of upgrading a smart contract and how to implement one of them in Solidity.

Prerequisites

To understand this tutorial, you must be familiar with:

  • Building Smart contracts
  • Solidity
  • Remix or Truffle

Requirements

Smart Contract Upgradability Approaches

Smart contract upgradability has several approaches but we will explore 3 common approaches and highlight their pros and cons.

  1. Proxy Contracts: Using the proxy contract approach, a new contract is created that acts as an intermediary between the client and the actual contract. The proxy contract refers to the current implementation contract and delegated all method calls to it. When an upgrade is required, a new implementation contract is deployed and the proxy contract is updated to point to the newly deployed implementation contract. This method allows for smooth upgrades without interfering with the contract's state or functionality.
  2. Upgradeable Libraries: The approach of an upgradeable library entails creating a library contract with reusable functions and deploying it separately from the contract that uses it. The library contract contains only the necessary state variables and delegated all function calls to it. When an upgrade is required, a new library contract is deployed, and the contract that makes use of it is updated to point to the new library contract. This method encourages code reuse and reduces the amount of code that must be redeployed during upgrades.
  3. Eternal Storage: The eternal storage method involves decoupling the state of the contract from its logic. The logic is stored in a contract that delegated all state-related operations to the storage contract. When an upgrade is required, a new logic contract is deployed and the storage contract is updated to point to the newly deployed logic contract. This method ensures that the state of the contract is preserved during upgrades.

Implementing an Upgradable Smart Contract on Celo

The proxy contract approach will be used to demonstrate how to implement an upgradable contract on the Celo blockchain. We'll write a simple contract that keeps track of a counter and allows users to increment it. The contract will then be upgraded by adding a new function that resets the counter to zero.

Step1: Create a Proxy Contract

The first step is to draft the proxy contract, which will serve as an intermediary between the client and the implementation contract. The proxy contract should keep a reference to the current implementation contract and delegate all method calls to it.

Create a new directory and put all the code examples in this tutorial in it.

mkdir upgrading-smart-contracts
cd upgrading-smart-contracts

Here is an example code:

Create a file called CounterProxy.sol and put the following example code in it:

CounterProxy.sol

pragma solidity ^0.8.0;

contract CounterProxy {
address public implementation;

constructor(address _implementation) {
implementation = _implementation;
}

fallback() external payable {
address _impl = implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)

switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}
receive() external payable {

}
}

In the code above, the constructor takes in the address of the implementation contract as argument. All method calls are delegated to the implementation contract via the fallback function. It accomplishes this by copying the method call data from the client's transaction and forwarding it to the implementation contract via a delegatecall function. The result of the call is returned to the client if it is successful. If the call fails, an exception is thrown, and the client's transaction is rolled back.

Step2: Create the Actual Implementation Contract

The next step is to create the implementation contract which contains the actual logic of incrementing the counter by one.

Counter.sol

pragma solidity ^0.8.0;

contract Counter {
uint256 public count;

function increment() public {
count++;
}
}

The count variable is public and stores the current value of the counter and the increment function increases the counter by one.

Step3: Deploy the Proxy and Implementation Contract

Next, deploy the proxy and implementation contract on the Celo blockchain. You can follow this tutorial on how to deploy a smart contract on Celo using Python.

After you have deployed the implementation contract, get its address and pass it to the constructor of the proxy contract.

The updated Proxy contract code to deploy:

CounterProxy.sol

// SPDX-License-Identifier: MIT

pragma solidity ^0.8.0;

import "./Counter.sol";

contract CounterProxy {
address public implementation;

constructor(address _implementation) {
implementation = _implementation;
}

fallback() external payable {
address _impl = implementation;
assembly {
let ptr := mload(0x40)
calldatacopy(ptr, 0, calldatasize())
let result := delegatecall(gas(), _impl, ptr, calldatasize(), 0, 0)
let size := returndatasize()
returndatacopy(ptr, 0, size)

switch result
case 0 { revert(ptr, size) }
default { return(ptr, size) }
}
}

function setImplementation(address _newImplementation) external {
require(msg.sender == address(this), "Only the contract itself can call this function");
implementation = _newImplementation;
}



receive() external payable {

}
}

In the code above, the "CounterFactory" contract includes a function called "createCounter," which generates a new instance of the "Counter" contract as well as a new instance of the "CounterProxy" contract. The "Counter" contract's address is passed to the "CounterProxy" contract's constructor. The function returns the address of the proxy contract. The “setImplementation” function sets the contract to implement the new contract address.

Step4: Upgrade the Implementation Contract

The next step is to upgrade the implementation contract to include a new function that resets the counter to zero.

Counter.sol

pragma solidity ^0.8.0;

contract Counter {
uint256 public count;

function increment() public {
count++;
}

function reset() public {
count = 0;
}
}

Step5: Upgrade the Proxy Contract

The final step is to modify the proxy contract so that it points to the new implementation contract. We have to deploy a new instance of the implementation contract and update the proxy contract's "implementation" variable to point to the new contract address.

contract CounterFactory {
function createCounter() public returns (address) {
Counter counter = new Counter();
CounterProxy proxy = new CounterProxy(address(counter));
return address(proxy);
}

function upgradeCounter(address payable _proxy) public {
Counter counter = new Counter();
CounterProxy proxy = CounterProxy(_proxy);
proxy.setImplementation(address(counter));
}
}

A new "upgradeCounter" function takes the address of an existing proxy contract and upgrades it to use the new implementation contract. The function creates a new instance of the implementation contract and then modifies the proxy contract's "implementation" variable to point to the new contract address.

To upgrade the contract, we can use the address of the existing proxy contract to call the "upgradeCounter" function. This will update the proxy contract's implementation contract, allowing us to add new functionality to the contract without having to deploy a new proxy contract.

Conclusion

In this tutorial, we explored what smart contract upgradability is and how we can implement them in solidity. We also looked at how we can implement contract upgradability patterns such as the Proxy contract, Eternal Storage and Upgradeable Libraries.

Next Steps

We can read up on more upgradability patterns such as the proxy factor, governance contract, etc. You can also learn how they are implemented in Solifity and how to deploy them on the Celo blockchain.

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he's given to simplifying its complexities with text and video tutorials.

References

  1. Celo Developer Documentation
  2. Solidity Documentation
  3. OpenZeppelin Upgrades
Go back

· 7 min read
Israel Okunaya

header

Introduction

Lottery applications are one of the major applications of blockchain technology since the early days of Ethereum. Smart contracts enable the operation of secure and transparent lotteries that cannot be manipulated by centralized parties. Furthermore, they can automate many lottery-related processes, such as ticket sales, winner selection, and prize distribution. In this tutorial, we will learn how to to build a lottery smart contract and deploy it on the Celo blockchain with Python using brownie framework.

Prerequisites

To understand this tutorial, you must be familiar with:

  • Building Smart contracts
  • The Python Programming Language

Requirements

You should have the following installed on your computer to follow along:

Setting Up The Project

To get started, we have to create a new directory for our project and install the following dependencies:

mkdir lottery
cd lottery
# Create virtual environment
python3.10 -m venv venv

# activate virtual environment
source venv/bin/activate

Note: For some reason, brownie works best with python3.10

# Install ganache
npm install -g ganache

# Install eth-brownie and python-dotenv
pip3 install eth-brownie python-dotenv

After installing dependencies, we need to initialize our project as a brownie project.

brownie init

This command generates some folders which look like this:

Screenshot

After initializing brownie into our project, in your root directory, create two files called .env and brownie-config.yaml. The .env file is used to store environment variables that shouldn’t be exposed to the public such as our private key, mnemonic phrase, etc, while brownie-config.yaml is used to configure brownie in our project.

.env

MNEMONIC='...'
PRIVATE_KEY='0x...'

brownie-config.yaml

compiler:
solc:
version: 0.8.15
optimizer:
enabled: true
runs: 200
networks:
default: celo-alfajores
console:
show_colors: true
color_style: monokai
auto_suggest: true
completions: true
editing_mode: emacs
dotenv: .env
wallets:
from_mnemonic: ${MNEMONIC}
from_key: ${PRIVATE_KEY}

The next step is to add Celo Testnet or Alfajores to our brownie project:

brownie networks add Celo celo-alfajores host=https://alfajores-forno.celo-testnet.org chainid=44787 explorer=https://alfajores-blockscout.celo-testnet.org

You can see the list of networks that have been added to our brownie project:

brownie networks list

Screenshot

Implementing the Smart Contract

Next, we have to write the smart contract for our Lottery application. In your contracts directory, create a new file called Lottery.sol

Lottery.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.15;

contract Lottery {
address public manager;
address payable[] public players;
uint public minimumBet;
uint public numTickets;

constructor(uint _minimumBet, uint _numTickets) {
manager = msg.sender;
minimumBet = _minimumBet;
numTickets = _numTickets;
}

function enter() public payable {
require(msg.value >= minimumBet, "Not enough ether to enter the lottery.");
require(players.length < numTickets, "The lottery is full.");

players.push(payable(msg.sender));
}

function pickWinner() public restricted {
require(players.length == numTickets, "The lottery is not full yet.");

uint index = random() % numTickets;
players[index].transfer(address(this).balance);

// reset the lottery
delete players;
}

function getPlayers() public view returns (address payable[] memory) {
return players;
}

function getBalance() public view returns (uint) {
return address(this).balance;
}

function random() private view returns (uint) {
return uint(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, players.length)));
}

modifier restricted() {
require(msg.sender == manager, "Only the manager can call this function.");
_;
}
}

Let’s go through the code step by step:

    address public manager;
address payable[] public players;
uint public minimumBet;
uint public numTickets;

constructor(uint _minimumBet, uint _numTickets) {
manager = msg.sender;
minimumBet = _minimumBet;
numTickets = _numTickets;
}

The manager is the address of the owner of the contract who is the only one allowed to pick the winner of the lottery. The player's array holds the addresses of the lottery players. The minimumBet is the minimum amount of ether required to place a bet in the lottery, and the numTickets is the number of tickets available to purchase. The constructor sets the manager as the lottery owner, the minimum bet, and the total number of tickets available.

 function enter() public payable {
require(msg.value >= minimumBet, "Not enough ether to enter the lottery.");
require(players.length < numTickets, "The lottery is full.");

players.push(payable(msg.sender));
}

The enter function enables players to enter the lottery and makes sure they deposit at least the minimum bet amount.

    function pickWinner() public restricted {
require(players.length == numTickets, "The lottery is not full yet.");

uint index = random() % numTickets;
players[index].transfer(address(this).balance);

// reset the lottery
delete players;
}

The pickWinner function picks at random the winner of the lottery and makes sure it’s only the contract manager that can send the balance of the contract

function getPlayers() public view returns (address payable[] memory) {
return players;
}

function getBalance() public view returns (uint) {
return address(this).balance;
}

function random() private view returns (uint) {
return uint(keccak256(abi.encodePacked(block.timestamp, block.prevrandao, players.length)));
}

modifier restricted() {
require(msg.sender == manager, "Only the manager can call this function.");
_;
}

The getPlayers function returns an array of all the players that entered the lottery. The getBalance function returns the balance of the lottery contract. The random function picks at random the winner of the lottery and the restricted modifier is what allows a function to be restricted to just the lottery manager.

Deploying the Contract

Next, we need to compile and deploy the contract on the Celo Testnet, Run the following command to compile the contract.

brownie compile

Screenshot

To deploy the smart contract on Celo, create a new file called deploy.py in the scripts directory of your project.

deploy.py

from brownie import Lottery, accounts, config, network

def deploy_lottery():
# Load the account to deploy from
dev = accounts.add(config["wallets"]["from_key"])
print(f"Deploying from {dev.address}")

# Deploy the contract
lottery = Lottery.deploy({"from": dev})

print(f"NFTMarketplace contract deployed to {lottery.address}")

def main():
# Set the network to deploy to
network_name = network.show_active()
print(f"Deploying to {network_name} network")

# Call the deploy function
deploy_lottery()

The deploy_lottery function gets the account we would use to deploy the contract

Screenshot

Conclusion

Lottery applications are one of the major applications of blockchain technology since the early days of Ethereum. Smart contracts enable the operation of secure and transparent lotteries that cannot be manipulated by centralized parties. Furthermore, they can automate many lottery-related processes, such as ticket sales, winner selection, and prize distribution. In this tutorial, we will learn how to to build a lottery smart contract and deploy it on the Celo blockchain with Python using brownie framework.

Next Steps

Here are some additional resources to help you learn more about smart contract development and lottery applications:

  • Chainlink VRF: A decentralized and secure method for generating random numbers on the blockchain, Chainlink VRF. It can be used to ensure that lottery applications and other games of chance are fair.
  • CryptoZombies: CryptoZombies is a gamified interactive code school that teaches you how to build Ethereum dApps. A lesson on creating a lottery application is included in the tutorial series.

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he's given to simplifying its complexities with text and video tutorials.

References

  1. Celo Developer Documentation
  2. Solidity Documentation
  3. Brownie Documentation
  4. Github Repo
Go back

· 6 min read
Israel Okunaya

header

Introduction

Crowdfunding is a fundraising strategy that allows individuals or sets of individuals to acquire the necessary funding they need for their projects or ideas. Celo is a fully open-source blockchain that allows developers to build smart contracts and DAPPS (decentralized applications). In this tutorial, we will learn how to build a Crowdfunding application with Python on Celo. We will create a smart contract and deploy it on Celo Alfajores with the Python Web3.py library.

Prerequisites

To understand this tutorail, you must be familiar with:

  • Building Smart contracts
  • The Python programming language

Requirements

You sould have the following installed on you computer to execute allt that is entailed in this tutorial:

Setting up the Project

On your terminal use the following command to create a new folder for your project:

mkdir celo-crowdfunding-python
cd celo-crowdfunding-python

In your new directory, create a python virtual environment and activate it with following commands:

python3 -m venv env
source env/bin/activate

To install the ganache-cli, web3.py, and python-dotenv:

npm install ganche --global
pip3 install python-dotenv web3

Creating the Smart Contract

Next, we have to build a smart contract for our crowdfunding application. Create a file called Crowdfunding.sol in a directory of your project.

Crowdfunding.sol

// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Crowdfunding {
address public owner;
uint256 public totalContributions;
uint256 public goal;
bool public completed;
mapping(address => uint256) public contributions;
event Contribute(address indexed contributor, uint256 amount);
constructor(uint256 _goal) public {
owner = msg.sender;
goal = _goal;
}
function contribute() public payable {
require(!completed, "Crowdfunding has been completed.");
require(msg.value > 0, "Contribution amount must be greater than 0.");
contributions[msg.sender] += msg.value;
totalContributions += msg.value;
emit Contribute(msg.sender, msg.value);
if (totalContributions >= goal) {
completed = true;
}
}
function withdraw() public {
require(completed, "Crowdfunding has not been completed.");
require(msg.sender == owner, "Only the owner can withdraw the funds.");
payable(msg.sender).transfer(totalContributions);
}
}

The smart contract above enables contributors to contribute to a crowdfunding campaign. The contract is completed once the total contributions reach the specified value by the owner of the campaign.

  • A _goal argument is specified in the constructor function which is used to set the amount needed to be reached by the campaign.
  • he contribute function is marked as payable (allows the function to accept payments from contributors) which adds the contributed amount to the totalContributions and ends the campaign once the goal has been reached.
  • The withdraw function makes sure that the owner of the campaign is the only one allowed to access the total contributions.

Deploying the Smart Contract

After we've created the smart contract, the next step is to compile and deploy the smart contract.

Run the following commaand to intall py-solx which is a python wrapper around the solc solidity compiler:

pip install py-solc

Create a new file called deploy.py and paste the following code:

deploy.py


import json
from solcx import compile_standard, install_solc
from web3 import Web3
import os
from dotenv import load_dotenv
load_dotenv()
with open("./Crowdfunding.sol", "r") as file:
crowdfunding_file = file.read()
# print(crowdfunding_file)
# Install Solidity compiler.
_solc_version = "0.6.0"
install_solc(_solc_version)
# Compile Crowdfunding smart contract with solcx.
compiled_sol = compile_standard(
{
"language": "Solidity",
"sources": {"Crowdfunding.sol": {"content": crowdfunding_file}},
"settings": {
"outputSelection": {
"*": {"*": ["abi", "metadata", "evm.bytecode", "evm.sourceMap"]}
}
},
},
solc_version=_solc_version,
)
# print(compiled_sol)
# Write compiled smart contract as JSON.
with open("compiled_sol.json", "w") as file:
json.dump(compiled_sol, file)
# Get byte code
bytecode = compiled_sol["contracts"]["Crowdfunding.sol"]["Crowdfunding"]["evm"][
"bytecode"
]["object"]
# Get ABI from JSON contract.
abi = compiled_sol["contracts"]["Crowdfunding.sol"]["Crowdfunding"]["abi"]

Run the following command to generate a JSON file called compiled_sol.json in your root directory:

python deploy.py

This compiled JSON file would contain the ABI (Application Binary Interface) and bytes code that communicates with the EVM machine.

Connect to a Local Blockchahin with Ganache Cli

Next, we need to connect to a local blockchain to interact with our smart contract:

Run the following on you terminal to connect to ganache:

ganache-cli --deterministic

You should see a ilst of private keys and public addresses to use to connect to your local blockchain and an RPC URL:

image

Add the following code to deploy.py:

# for connecting to ganache
w3 = Web3(Web3.HTTPProvider("http://127.0.0.1:8545"))
chain_id = 1337
my_address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1"
private_key = os.getenv("PRIVATE_KEY")
print(private_key)

The address and private key are gotten from any of the ones that ganache provides for us and the URL is the RPC URL that ganache is running on your PC.

For security purposes, it is best to store your private key as an environment variable.

Create a .env file and add your private key to it.

.env

PRIVATE_KEY='0x......'

Run the following command to deploy your contract on ganache:

python deploy.py

Deploying on Celo Testnet

Let us deploy our application on celo Test also know as Alfajores.

Add the following code to deploy.py

deploy.py

# Deploy to test net (Celo)

w3 = Web3(Web3.HTTPProvider("https://alfajores-forno.celo-testnet.org"))
chain_id = 44787
my_address = "0x892B6Ca9F2213f011D850F397944e77bd0b1Bca3" # celo wallet
private_key = os.getenv("PRIVATE_TESTNET_KEY") # celo wallet
# create the contract in python
Crowdfunding = w3.eth.contract(abi=abi, bytecode=bytecode)
# print(Crowdfunding)
# Get latest transaction
nonce = w3.eth.get_transaction_count(my_address)
# print(nounce)
# Build a transaction
transaction = Crowdfunding.constructor().build_transaction({ "chainId": chain_id, "from": my_address, "nonce": nonce })
# print(transaction)
# Sign a transaction
signed_tx = w3.eth.account.sign_transaction(transaction, private_key=private_key)
# print(signed_tx)
# Send a transaction
tx_hash = w3.eth.send_raw_transaction(signed_tx.rawTransaction)
tx_receipt = w3.eth.wait_for_transaction_receipt(tx_hash)
contract_address = tx_receipt['contractAddress']
print(f'Contract deployed at address: {contract_address}')

In the code above, RPC URL and chain ID are gotten from the Celo Alfajores network. The contract method on the w3.eth class takes in the ABI and bytecode to create the contract. The build_transaction builds a transaction and is passed as an argument to the sign_transaction which signs the transaction. The send_raw_transaction method finally sends the transaction and produces a receipt that contains the contract address of the deployed contract.

Conclusion

In this article, you learned how to create a crowdfunding application with Celo Python SDK. You implemented functionalities such as, creating the crowdfunding smart contract logic, and deploying it to the Celo blockchain

Next Steps

To learn more about building on Celo using Python, you can explore the following resources:

References

About the Author

Israel Okunaya is an ace writer with a flair for simplifying complexities and a knack for storytelling. He leverages over four years of experience to meet the most demanding writing needs in different niches, especially food and travel, blockchain, and marketing. He sees blockchain as a fascinating yet tricky affair. So, he is given to simplifying its complexities with text and video tutorials.

Go back

· 11 min read
Oluwafemi Alofe

Introduction

In this tutorial, we will show you how to create a subscription platform using the Celo composer react-app and the hardhat package. The platform will offer three subscription plans that users can choose from, and payment will be charged monthly in cUSD. We will also use the OpenZeppelin Defender autotask to handle the monthly subscription charges and an email service to notify users of the charge status. By the end of this tutorial, you will have a working subscription platform and the knowledge to customize and build upon it for your own use case.

header

Background Knowledge

Merkle trees are a fundamental data structure to build blockchain systems. In a merkle tree, there are leaf nodes and non-leaf nodes. Each leaf nodes represent a data element while each non-leaf nodes represent the hash of its child nodes. There is also the Merkle root which is the hash of the entire tree. It also serves as a summary of all the data in the tree.

Requirements

Before we begin, make sure to have a package manager installed on your machine. yarn and npm are perfect managers.

Github Code

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

Create a starter app with Celo Composer

In your terminal, run the following command

npx @celo/celo-composer create

You will be prompted to select the framework you will like to work with which in our case is React.

007

You will also be prompted to pick a web3 library for the react app. For this tutorial, we will pick RainbowKit

006

Next up, you will be prompted to choose the smart contract framework you want to work with, Choose Hardhat.

005

For next steps, we will be prompted to create a Subgraph. We would not be creating a subgraph, so go ahead to select No

004

Then, proceed to give your project a name

You did it! You just created a starter project dApp in few minutes using Celo-Composer

Write out your smart contract

What is next now is to cd into your project directory and open in your IDE

cd merkle-drop
code .

Go to the packages folder of your project and navigate to hardhat.

003

Go to contracts folder and create a new file called MerkleAirdrop.sol . We will create a constant to hold the merkle root and token contract that will be airdropped to the recipients. We will also keep track of people who claim eventually by creating a mapping.

// The Merkle root
bytes32 public merkleRoot;

// The token contract
IERC20 public tokenContract;

// Mapping to keep track of who claimed their tokens
mapping(address => bool) public claimed;

We will also create a function that allow users claim the tokens.

// Function to claim tokens by providing a merkle proof
function claimTokens(bytes32[] calldata _proof, uint256 _amount) external {
require(!claimed[msg.sender], "Tokens already claimed");

// Mark the address as claimed
claimed[msg.sender] = true;

// Transfer tokens to the address
tokenContract.transfer(msg.sender, _amount);
}

In this function, there is a simple check to see that the user has already claimed the token. There is also the need to verify the merkle proof so we are going to write a function verifyProof to do that

// Function to verify the merkle proof
function verifyProof(bytes32[] calldata _proof, address _address) public view returns (bool) {
// Compute the leaf hash
bytes32 leaf = keccak256(abi.encodePacked(_address, msg.sender));

// Compute the root hash
bytes32 computedHash = leaf;
for (uint256 i = 0; i < _proof.length; i++) {
bytes32 proofElement = _proof[i];

if (computedHash < proofElement) {
computedHash = keccak256(abi.encodePacked(computedHash, proofElement));
} else {
computedHash = keccak256(abi.encodePacked(proofElement, computedHash));
}
}

// Check if the computed hash matches the Merkle root
return computedHash == merkleRoot;
}

We will then go back to our claimTokens function to call our verifyProof function after checking that tokens had already been claimed. So update the claimTokens function with this line of code

require(verifyProof(_proof, msg.sender), "Invalid proof");

All these would require that you import the openzepelin's ERC20 file

import "@openzeppelin/contracts/token/ERC20/IERC20.sol";

If you don't have a token in mind to give out, you can create one. Just create another solidity file, name it Token.sol for example and copy this piece of code

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "@openzeppelin/contracts/token/ERC20/ERC20.sol";

contract Token is ERC20 {
constructor(uint256 initialSupply) ERC20("My Token", "MTK") {
_mint(msg.sender, initialSupply);
}
}

Create an .env file to store your environment variables and copy your private key from your Celo/Metamask wallet as applicable

CELO_NETWORK=https://alfajores-forno.celo-testnet.org
PRIVATE_KEY=YOUR_PRIVATE_KEY

Compile your contracts using this command

npx hardhat compile

Testing your contract

Testing a smart contract is quite essential because it helps for the smart contract to be secure and function well. We will test our contract to ensure that the functions as intended. In packages >> hardhat >> test , create a test javascript file and write all the possible tests that you think are applicable.

// Import the required dependencies and the smart contract
const { expect } = require("chai");
const { ethers } = require("hardhat");
const { MerkleTree } = require("merkletreejs");

const { StandardMerkleTree } = require("@openzeppelin/merkle-tree");

describe("MerkleAirdrop", function () {
// Define some variables to use in the tests
let merkleRoot;
let airdrop;
let token;
let owner;
let amount = ethers.utils.parseEther("100");

// Create the Merkle Airdrop contract and token contract before each test
beforeEach(async function () {
[owner, recipient1, recipient2] = await ethers.getSigners();
const Token = await ethers.getContractFactory("Token");
token = await Token.deploy(amount);
const MerkleAirdrop = await ethers.getContractFactory("MerkleAirdrop");
merkleRoot = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("test"));
airdrop = await MerkleAirdrop.deploy(merkleRoot, token.address);
await token.transfer(airdrop.address, ethers.utils.parseEther("10"));
});

// Test that the contract deploys successfully
describe("Deployment", function () {
it("Should deploy MerkleAirdrop and Token contracts successfully", async function () {
expect(airdrop.address).to.not.equal(ethers.constants.AddressZero);
expect(token.address).to.not.equal(ethers.constants.AddressZero);
});
});

// Test that only eligible recipients can claim tokens
describe("Claiming tokens", function () {
it("Should allow eligible recipients to claim their tokens and check for invalid proof", async function () {
// Generate a proof

const whitelist = [
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"0xe304cC7Cfed9120ADa3Cd04cC13e210F7c5F37ED",
]; // Replace with real whitelist
const leaves = whitelist.map((address) =>
ethers.utils.keccak256(address)
);
const values = [
["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "5"],
["0xe304cC7Cfed9120ADa3Cd04cC13e210F7c5F37ED", "2"],
];
const tree = new MerkleTree(leaves);
const leaf = ethers.utils.keccak256(
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
);
const proof = tree.getProof(leaf);

// Call the claimTokens function with a valid proof and an amount
const buffer = proof[0].data;
const bytes32Array = [];
for (let i = 0; i < buffer.length; i += 32) {
const slice = buffer.slice(i, i + 32);
const bytes32 = `0x${slice.toString("hex").padEnd(64, "0")}`;
bytes32Array.push(bytes32);
}
const result = await airdrop.claimTokens(bytes32Array, 3, {
gasLimit: 500000,
});
expect(result)
.to.emit(airdrop, "Tokens already claimed")
.withArgs(bytes32Array);

const invalidProof = tree.getProof(
ethers.utils.keccak256("0x70997970C51812dc3A010C7d01b50e0e17dc79C9")
);
expect(invalidProof)
.to.emit(airdrop, "Invalid Proof")
.withArgs(invalidProof);
});
it("Should be able to verify proof", async function () {
const whitelist = [
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
"0xe304cC7Cfed9120ADa3Cd04cC13e210F7c5F37ED",
]; // Replace with real whitelist
const leaves = whitelist.map((address) =>
ethers.utils.keccak256(address)
);
const values = [
["0x70997970C51812dc3A010C7d01b50e0d17dc79C8", "5"],
["0xe304cC7Cfed9120ADa3Cd04cC13e210F7c5F37ED", "2"],
];
const tree = new MerkleTree(leaves);
const leaf = ethers.utils.keccak256(
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8"
);
const proof = tree.getProof(leaf);
// Call the claimTokens function with a valid proof and an amount
const buffer = proof[0].data;
const bytes32Array = [];
for (let i = 0; i < buffer.length; i += 32) {
const slice = buffer.slice(i, i + 32);
const bytes32 = `0x${slice.toString("hex").padEnd(64, "0")}`;
bytes32Array.push(bytes32);
}
const res = await airdrop.verifyProof(
bytes32Array,
"0x70997970C51812dc3A010C7d01b50e0d17dc79C8",
{ gasLimit: 500000 }
);
expect(res)
.to.emit(airdrop, true)
.withArgs("0x70997970C51812dc3A010C7d01b50e0d17dc79C8");
});
});
});

You can proceed to test this by running the following command

npx hardhat test test/your_javascript_file.js

If successful, you should see an output similar to this

002

Deploy your Smart Contract

Create a deployment script file in scripts folder. You can run this command

cd packages/hardhat/scripts && touch deploy.js

Then write your script

const hre = require("hardhat");

async function main() {
const merkleRoot = ethers.utils.keccak256(ethers.utils.toUtf8Bytes("test"));
const tokenContractAddress = "0xB3C20f3011ac4f713b3E6252E9B6A2060EB912a1"; // Replace with your token contract address
const MerkleAirdrop = await hre.ethers.getContractFactory("MerkleAirdrop");
const merkleAirdrop = await MerkleAirdrop.deploy(
merkleRoot,
tokenContractAddress
);
await merkleAirdrop.deployed();
}

main();

Run this command after

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

After a succesful deployment, you would see the message

MerkleAirdrop address deployed to: 0x4004aD23277E51E1086beba0C0E8644Cb0DAe1d5

Starting out the Frontend

In the root of your project folder, create a file called AirdropWrapper.js. This will serve as a gateway between the contract deployed and our component class. We will call our contract in this file.

import { abi } from "./AirdropContract.json";
import { providers, Contract, ethers } from "ethers";
const { MerkleTree } = require("merkletreejs");

require("dotenv").config();

export async function getContract() {
const contractAddress = "0x4004aD23277E51E1086beba0C0E8644Cb0DAe1d5";
const contractABI = abi;
let supportTokenContract;
try {
const { ethereum } = window;
if (ethereum.chainId === "0xaef3") {
const provider = new providers.Web3Provider(ethereum);
const signer = provider.getSigner();
supportTokenContract = new Contract(contractAddress, contractABI, signer);
} else {
throw new Error("Please connect to the Alfajores network");
}
} catch (error) {
console.log("ERROR:", error);
}
return supportTokenContract;
}

export async function claimTokens(proof, amount) {
const contract = await getContract();
const tx = await contract.claimTokens(proof, amount, {
gasLimit: 300000,
});
await tx.wait();
}

export async function checkEligibility(whitelist) {
const leaves = whitelist.map((address) => ethers.utils.keccak256(address));
const tree = new MerkleTree(leaves, ethers.utils.keccak256);

const leaf = ethers.utils.keccak256(whitelist[0]);
const proof = tree.getProof(leaf);
const root = tree.getRoot().toString("hex");
return tree.verify(proof, leaf, root);
}

export async function getTheProof(whitelist) {
const leaves = whitelist.map((address) => ethers.utils.keccak256(address));
const tree = new MerkleTree(leaves, ethers.utils.keccak256);

const leaf = ethers.utils.keccak256(whitelist[0]);
const proof = tree.getProof(leaf);
const root = tree.getRoot().toString("hex");
const bytes32Array = [];
const buffer = proof[0].data;
for (let i = 0; i < buffer.length; i += 32) {
const slice = buffer.slice(i, i + 32);
const bytes32 = `0x${slice.toString("hex").padEnd(64, "0")}`;
bytes32Array.push(bytes32);
}

return bytes32Array;
}

In this file, we will also write functions to get our merkle proof, check if an address is eligible for airdrop and claim tokens.

Navigate to react-app folder and go to your components folder, create a new file there named Airdrop.tsx. Here we will import the functions from the wrapper class and call it.

import React, { useState } from "react";
import {
checkEligibility,
claimTokens,
getTheProof,
} from "../../../AirdropWrapper";
import { useAccount } from "wagmi";
const [isAddress, setAddress] = useState("");
const [isEligible, setIsEligible] = useState(false);
const [isClaimed, setIsClaimed] = useState(false);
const { address, isConnecting, isDisconnected } = useAccount();
let whitelist: any = [];
const lowercaseAddress = address.toLowerCase();
whitelist.push(lowercaseAddress);
whitelist.push("0xe304cC7Cfed9120ADa3Cd04cC13e210F7c5F37ED");

const proof = getTheProof(whitelist);
const checkEligibile = async () => {
const isEligible = await checkEligibility(whitelist);
setIsEligible(isEligible);
};
const claimAirdrop = async () => {
const claim = await claimTokens(await proof, "1");
setIsClaimed(true);
};

Complete your frontend to show that the wallet connected is eligible for the airdrop and that the person can claim

<div className="bg-gray-100 p-4">
<h1 className="text-2xl font-bold mb-4">Airdrop</h1>
<input
type="text"
value={address}
onChange={(e) => setAddress(e.target.value)}
placeholder={address}
className="bg-white rounded-md py-2 px-4 mb-4 w-full"
/>
<button
onClick={checkEligibile}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
>
Check eligibility
</button>
{isEligible && !isClaimed && (
<div className="bg-green-100 p-4 rounded mt-4">
<p className="text-green-700 font-bold mb-2">
You are eligible for the airdrop!
</p>
<button
onClick={claimAirdrop}
className="bg-blue-500 hover:bg-blue-600 text-white font-bold py-2 px-4 rounded"
>
Claim airdrop
</button>
</div>
)}
{isEligible && isClaimed && (
<p className="text-gray-700 font-bold mt-4">
You have already claimed the airdrop.
</p>
)}
{!isEligible && (
<p className="text-red-700 font-bold mt-4">
You are not eligible for the airdrop.
</p>
)}
</div>

Proceed to your terminal to run this command

npm run dev

It should compile and deploy to your localhost so you should see an interface similar to this

001

The wallet connected is eligible for the airdrop hence we see that here. Proceed to claim the airdrop by clicking on the button.

There you have it. You have successfully implemented a dApp that gives your users access to claim airdrops using merkle trees.

Conclusion

In this tutorial , we learnt and saw the versatility of Merkle trees in building decentralized applications and showcased how they can be used to provide access to airdrops. This demonstrates the real-world applicability of Merkle trees and their role in building secure and efficient blockchain systems.

Reference

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

· 28 min read
Marc-Aurèle Besner

header

Introduction

In Part 1, we wrote a multi-signature smart contract in Solidity using Hardhat. Multi-signatures are a secure way to protect your cryptocurrency assets and the ownership of your smart contract. The multi-signature contract acts as a wallet capable of executing transactions to transfer Ethereum or call other smart contracts.

In this Part 2, we will write tests for our multi-signature contract using Hardhat. We will also deploy our contract on the Celo Alfajores testnet.

Prerequisites

To follow this tutorial, you will need:

  • A GitHub account GitHub

  • Some knowledge of Solidity

  • Understanding of the multi-signatures concept (you can read more about it in What Is a Multisig Wallet?)

  • Familiarity with the following multi-signature contract terms:

    • Owners - Addresses that can sign and execute transactions on this wallet contract.
    • Threshold - The number of owner signatures required for a transaction to be executed.
    • Nonce - A unique number that identifies each request to prevent signatures from being used on more than one transaction.

Requirements

To complete this tutorial, you will need:

Setup Our Test Helpers

To help us write our tests more efficiently, minimize code duplication, improve the maintainability of our repository, and make our tests more readable, we will create a few helper functions that we can reuse in our tests.

To do so, we will create a new folder called helpers inside the test folder. Inside this folder, we will create a few files:

  • errors.js - This file will contain all the error messages that we will use in our tests (error messages from the require statements in our contract).
  • utils.js - This file will contain all the general-purpose functions that we will use in our tests (retrieve our wallets, deploy our contract, etc.).
  • signature.js - This file will contain the functions that we will use to sign the transaction requests before sending them to the contract.
  • test.js - This file will contain the helper functions that we will use to call the contract functions and verify the results.
  • index.js - This file will export all the functions from the other files.

To create the required directories and files, you can run the following commands:

mkdir test/helper
touch test/helper/errors.js test/helper/utils.js test/helper/signature.js test/helper/test.js test/helper/index.js

This should give you the following directory structure:

Test Helper

Errors helper

In the errors.js file, we will add the following code:

module.exports = {
NOT_SELF: "CeloMultiSig: only this contract can call this function",
MAX_OWNERS_COUNT_EXCEEDED: "CeloMultiSig: cannot add owner above 2^16 - 1",
INVALID_SIGNATURE: "CeloMultiSig: invalid signatures",
INVALID_OWNER: "CeloMultiSig: invalid owner",
OWNER_ALREADY_SIGNED: "CeloMultiSig: owner already signed",
NOT_ENOUGH_GAS: "CeloMultiSig: not enough gas",
OWNER_COUNT_BELOW_THRESHOLD:
"CeloMultiSig: cannot remove owner below threshold",
THRESHOLD_IS_ZERO: "CeloMultiSig: threshold must be greater than 0",
THRESHOLD_GREATER_THAN_OWNERS_COUNT:
"CeloMultiSig: threshold must be less than or equal to owner count",
OLD_OWNER_NOT_OWNER: "CeloMultiSig: old owner must be an owner",
NEW_OWNER_ALREADY_OWNER: "CeloMultiSig: new owner must not be an owner",
NEW_OWNER_IS_ZERO_ADDRESS:
"CeloMultiSig: new owner must not be the zero address",
};

These error messages are the same as the ones we used in our contract. We simply copied and pasted them here and assigned them to a variable. This has the advantage of making our tests more readable and easier to maintain. This way, if we change the error message in our contract, we will only have to change it in one place.

This should give you the following result:

Errors helper

Utils helper

In the utils.js file, we will add the following code:

const { ethers, network, addressBook } = require("hardhat");

module.exports = {
setupProviderAndWallets: async function () {
const provider = ethers.provider;
let owner01;
let owner02;
let owner03;

let user01;
let user02;
let user03;

if (network.config.accounts && network.config.accounts.mnemonic) {
// If the network is configured with a mnemonic, use it to generate the wallets
owner01 = new ethers.Wallet(
ethers.Wallet.fromMnemonic(
network.config.accounts.mnemonic,
`m/44'/60'/0'/0/0`
).privateKey,
provider
);
owner02 = new ethers.Wallet(
ethers.Wallet.fromMnemonic(
network.config.accounts.mnemonic,
`m/44'/60'/0'/0/1`
).privateKey,
provider
);
owner03 = new ethers.Wallet(
ethers.Wallet.fromMnemonic(
network.config.accounts.mnemonic,
`m/44'/60'/0'/0/2`
).privateKey,
provider
);

user01 = new ethers.Wallet(
ethers.Wallet.fromMnemonic(
network.config.accounts.mnemonic,
`m/44'/60'/0'/0/3`
).privateKey,
provider
);
user02 = new ethers.Wallet(
ethers.Wallet.fromMnemonic(
network.config.accounts.mnemonic,
`m/44'/60'/0'/0/4`
).privateKey,
provider
);
user03 = new ethers.Wallet(
ethers.Wallet.fromMnemonic(
network.config.accounts.mnemonic,
`m/44'/60'/0'/0/5`
).privateKey,
provider
);
} else {
// If the network is not configured with a mnemonic, use the 3 first accounts as owners and the 3 next as users
owner01 = new ethers.Wallet(network.config.accounts[0], provider);
owner02 = new ethers.Wallet(network.config.accounts[1], provider);
owner03 = new ethers.Wallet(network.config.accounts[2], provider);

user01 = new ethers.Wallet(network.config.accounts[3], provider);
user02 = new ethers.Wallet(network.config.accounts[4], provider);
user03 = new ethers.Wallet(network.config.accounts[5], provider);
}
return [provider, owner01, owner02, owner03, user01, user02, user03];
},
deployContract: async function (owners, threshold) {
// Retrieve the contract factory
const CeloMultiSig = await ethers.getContractFactory("CeloMultiSig");
// Deploy the contract with the specified parameters for the constructor
const contract = await CeloMultiSig.deploy(owners, threshold, {
gasLimit: 10000000,
});
// Wait for the contract to be deployed
await contract.deployed();
// Save the contract address in the address book
await addressBook.saveContract(
"CeloMultiSig",
contract.address,
network.name,
contract.deployTransaction.from,
network.config.chainId,
contract.deployTransaction.blockHash,
contract.deployTransaction.blockNumber,
undefined,
{
owners,
threshold,
}
);
// Return the contract
return contract;
},
};

This file should now look like this:

Utils helper

In this file, we added two functions:

  • setupProviderAndWallets: This function will set up the provider and the wallets we will use in our tests. It will also check if the network is configured with a mnemonic or not. If it is, it will use it to generate the wallets. If it is not, it will use the first 6 accounts in the network configuration.
  • deployContract: This function will deploy the contract with the specified parameters for the constructor. It will also save the contract address in the address book.

Signature helper

In the signature.js file, we will add the following code:

const { network } = require("hardhat");

module.exports = {
signTransaction: async function (
contractAddress,
wallet,
to,
value,
data,
gas,
nonce
) {
const signature = await wallet._signTypedData(
{
name: "CeloMultiSig",
version: "1.0",
chainId: network.config.chainId,
verifyingContract: contractAddress,
},
{
Transaction: [
{
name: "to",
type: "address",
},
{
name: "value",
type: "uint256",
},
{
name: "data",
type: "bytes",
},
{
name: "gas",
type: "uint256",
},
{
name: "nonce",
type: "uint96",
},
],
},
{
to,
value,
data,
gas,
nonce,
}
);
return signature;
},
};

Note This function uses _signTypedData to sign the transaction as per ethersJS documentation here. However, in future versions of ethersJS, this function will be deprecated and replaced by signTypedData.

Like this:

Signature helper

Test helper

In the test.js file, we will add the following code:

const { ethers, network } = require("hardhat");
const { expect } = require("chai");

const signature = require("./signatures");

const ZERO = ethers.BigNumber.from(0);

const sendRawTxn = async (input, sender, ethers, provider) => {
// Get the nonce
const txCount = await provider.getTransactionCount(sender.address);
// Prepare the transaction
const rawTx = {
chainId: network.config.chainId,
nonce: ethers.utils.hexlify(txCount),
to: input.to,
value: input.value || 0x00,
gasLimit: ethers.utils.hexlify(3000000),
gasPrice: ethers.utils.hexlify(25000000000),
data: input.data,
};
// Sign the transaction
const rawTransactionHex = await sender.signTransaction(rawTx);
// Send the transaction
const { hash } = await provider.sendTransaction(rawTransactionHex);
// Wait for the transaction to be mined
return await provider.waitForTransaction(hash);
};

const checkRawTxnResult = async (input, sender, error) => {
let result;
// Check if the transaction should fail or not
if (error)
if (network.name === "hardhat" || network.name === "localhost")
await expect(
sendRawTxn(input, sender, ethers, ethers.provider)
).to.be.revertedWith(error);
else expect.fail("AssertionError: " + error);
else {
result = await sendRawTxn(input, sender, ethers, ethers.provider);
expect(result.status).to.equal(1);
}
return result;
};

const getEventFromReceipt = async (contract, receipt, eventName) => {
// Parse the logs
const log = receipt.logs.map((log) => {
try {
return contract.interface.parseLog(log);
} catch (e) {
return;
}
});
return log;
};

These functions are used to send raw transactions and check the result of the transaction. They also allow us to get the event from the receipt.

It should now look like this:

Test helper

Now after the last line you added, add the following code:

const prepareSignatures = async (
contract,
owners,
to,
value,
data,
gas = 30000
) => {
// Query the next nonce
const nonce = await contract.nonce();
let signatures = "0x";
for (var i = 0; i < owners.length; i++) {
// For each owners, sign the transaction
const sig = await signature.signTransaction(
contract.address,
owners[i],
to,
value,
data,
gas,
nonce
);
// Concatenate the signatures
signatures += sig.substring(2);
}
// Return signatures of all owners
return signatures;
};

This function will prepare the signatures of all the owners of the contract. We will pass an array of owners using their wallets to the function and sign the transaction with each of them. The signatures will be concatenated and returned.

It should now look like this:

Prepare signatures

We still need to add the main helper function for testing the execution of the transaction. Please add the following code:

const execTransaction = async (
contract,
submitter,
owners,
to,
value,
data,
gas = 30000,
errorMsg,
extraEvents,
signatures
) => {
// Prepare signatures if not provided
if (!signatures)
signatures = await prepareSignatures(
contract,
owners,
to,
value,
data,
gas
);
// Prepare transaction
const input = await contract
.connect(submitter)
.populateTransaction.execTransaction(to, value, data, gas, signatures);

// Send the transaction and check the result
const receipt = await checkRawTxnResult(input, submitter, errorMsg);
if (!errorMsg) {
// Check the event emitted (if transaction should succeed)
const event = await getEventFromReceipt(
contract,
receipt,
"TransactionExecuted"
);
let found = false;
for (var i = 0; i < event.length; i++) {
if (event[i] && event[i].name === "TransactionExecuted") {
// If the event is found, check the parameters
expect(event[i].args.sender).to.be.equal(submitter.address);
expect(event[i].args.to).to.be.equal(to);
expect(event[i].args.value).to.be.equal(value);
expect(event[i].args.data).to.be.equal(data);
expect(event[i].args.txnGas).to.be.equal(gas);
found = true;
return receipt;
} else {
// If the event is not found, check if the transaction failed
if (
extraEvents &&
extraEvents.find(
(extraEvent) => extraEvent === "TransactionFailed"
) &&
event[i] &&
event[i].name === "TransactionFailed"
) {
// If the transaction failed, check the parameters and if we expect a failure
expect(event[i].args.sender).to.be.equal(submitter.address);
expect(event[i].args.to).to.be.equal(to);
expect(event[i].args.value).to.be.equal(value);
expect(event[i].args.data).to.be.equal(data);
expect(event[i].args.txnGas).to.be.equal(gas);
found = true;
return receipt;
} else {
// If the transaction failed but we don't expect it, throw an error
if (found) expect.fail("TransactionExecuted event not found");
}
}
}
// If the event is not found, throw an error
if (event.length == 0) expect.fail("TransactionExecuted event not found");
// If we expect an extra event, check if it is emitted
if (extraEvents && extraEvents.length > 0) {
for (let i = 1; i < extraEvents.length; i++) {
const eventsFound = await getEventFromReceipt(contract, receipt, event);
for (var ii = 0; i < eventsFound.length; ii++) {
if (eventsFound[ii]) {
expect(submitter.address).to.be.equal(eventsFound[ii].sender);
return receipt;
}
}
}
}
}
};

This function will prepare the transaction, send it, and check the result. It will also check the event emitted by the transaction. This helper function is flexible and can be used to test the execution of a transaction, the failure of a transaction, or the execution of a transaction with extra event.

Please make sure the function looks like this:

Exec transaction

Lastly, we need to add the function that will be used to test the addOwner, removeOwner, changeThreshold, and replaceOwner functions. Please add the following code:

const addOwner = async (
contract,
submitter,
owners,
ownerToAdd,
gas = 30000,
errorMsg,
extraEvents
) => {
const data = contract.interface.encodeFunctionData("addOwner(address)", [
ownerToAdd,
]);

await execTransaction(
contract,
submitter,
owners,
contract.address,
ZERO,
data,
gas,
errorMsg,
extraEvents
);

if (!errorMsg) expect(await contract.isOwner(ownerToAdd)).to.be.true;
};

const removeOwner = async (
contract,
submitter,
owners,
ownerToRemove,
gas = 30000,
errorMsg,
extraEvents
) => {
const data = contract.interface.encodeFunctionData("removeOwner(address)", [
ownerToRemove,
]);

await execTransaction(
contract,
submitter,
owners,
contract.address,
ZERO,
data,
gas,
undefined,
extraEvents
);

if (!errorMsg) expect(await contract.isOwner(ownerToRemove)).to.be.false;
else expect(await contract.isOwner(ownerToRemove)).to.be.true;
};

const changeThreshold = async (
contract,
submitter,
owners,
newThreshold,
gas = 30000,
errorMsg,
extraEvents
) => {
const data = contract.interface.encodeFunctionData(
"changeThreshold(uint16)",
[newThreshold]
);

await execTransaction(
contract,
submitter,
owners,
contract.address,
ZERO,
data,
gas,
errorMsg,
extraEvents
);

if (!errorMsg) expect(await contract.threshold()).to.be.equal(newThreshold);
};

const replaceOwner = async (
contract,
submitter,
owners,
ownerToAdd,
ownerToRemove,
gas = 30000,
errorMsg,
extraEvents
) => {
const data = contract.interface.encodeFunctionData(
"replaceOwner(address,address)",
[ownerToRemove, ownerToAdd]
);

await execTransaction(
contract,
submitter,
owners,
contract.address,
ZERO,
data,
gas,
errorMsg,
extraEvents
);

if (!errorMsg) {
expect(await contract.isOwner(ownerToAdd)).to.be.true;
expect(await contract.isOwner(ownerToRemove)).to.be.false;
}
};

These four functions make use of the execTransaction function to test the execution of the addOwner, removeOwner, changeThreshold, and replaceOwner functions. They will also check if the owner was added, removed, or replaced correctly or if the threshold was changed correctly.

As our main execTransaction function takes data as an argument, we need to encode our function call before sending it. We can do that using the encodeFunctionData function from the contract interface. This function will encode the function name and the arguments into a data string that can be used to call the function.

Please ensure the text looks like this:

Add housekeeping helper functions

Finally, we need to export all the helper functions. Please add the following code at the end of the file:

module.exports = {
checkRawTxnResult,
prepareSignatures,
execTransaction,
addOwner,
removeOwner,
changeThreshold,
replaceOwner,
};

This way, we can import all the helper functions in our test file.

Please make sure the text looks like this:

Export all the test helpers

Exporting all the helpers

In the index.js file, we will add the following code:

const errors = require("./errors");
const test = require("./test");
const signature = require("./signatures");
const utils = require("./utils");

module.exports = {
errors,
test,
signature,
...utils,
};