This tutorial has three parts:
- Building a Crowdfunding Smart Contract in Celo
- Deploying a Crowdfunding Smart Contract in Celo
- Interacting with the Crowdfunding Smart Contracts
Building a Crowdfunding Smart Contract in Celo
We're going to write a smart contract in Solidity which facilitates crowdfunding (like GoFundMe, Kickstarter, and Indiegogo) on Celo in 172 lines of code.
Our contract will be able to create fundraisers, let people donate to them, and payout the money raised to the project creator. And it will do this all in cUSD (the Celo stablecoin).
The usual way of doing this might involve Plaid (banking), Stripe (payments), a database (for storing data), and AWS (for hosting):
Solidity and Celo make building the backend for this easy! Not to mention, 🌎 from day one.
This three part tutorial series will take us through writing the smart contract, deploying it, and interacting with it using Javascript.
Prerequisites
Click the image below in order to watch the Youtube video for "Building a Crowdfunding Smart Contract in Celo".
Video: Building a Crowdfunding Smart Contract in Celo
This tutorial is meant for intermediate Web 3 developers. It assumes you have some experience programming in Javascript and Solidity, and an understanding of basic Ethereum and object oriented programming concepts.
Before we continue, make sure you have truffle installed. If you don't, run the following line of code in your terminal:
npm install -g truffle@5.3.12
Note: This tutorial uses Node v14.16.1
Project setup
First, open the terminal and make a new project folder. We’ll call it celo-crowdfunding:
mkdir celo-crowdfunding && cd celo-crowdfunding
Next, let’s initialize the project directory with the Node package manager
npm
:npm init -y
After it has been initialized, we’ll need to install some additional packages for interacting with the smart contract in the next tutorial:
- ContractKit is a package created by the Celo team to aid in development
- dotenv is used for reading environment variables in our code
- web3.js is a library which facilitates our interactions with the blockchain
- OpenZeppelin contracts is a library of well-tested Solidity code that we will reuse
Install all of the above using
npm
:npm install -—save @celo/contractkit dotenv web3@1.3.6 @openzeppelin/contracts
After all the
npm
packages have installed, run truffle init
in the terminal to initialize Truffle.Here's what a successful run of
truffle init
will look like:Writing the Contract
First things first, open the newly created project in your favorite code editor and create a file called CeloCrowdfund.sol in your
contracts/
folder.At the top of the file, add the Solidity version and import the SafeMath contract and the ERC-20 interface from OpenZeppelin:
pragma solidity >=0.4.22 <0.9.0; // Importing OpenZeppelin's SafeMath Implementation import "@openzeppelin/contracts/utils/math/SafeMath.sol"; // IERC-20 contract import "@openzeppelin/contracts/token/ERC20/IERC20.sol";
SafeMath is a wrapper for
uint256
in Solidity. We use SafeMath because integers in Solidity are vulnerable to overflow errors which can cause significant problems for our smart contracts.We also import the
ERC-20
contract interface since it implements the basic ERC-20
functions, and cUSD uses the ERC-20 standard.Next, we're going to initialize our contract:
contract CeloCrowdfund { // SafeMath for safe integer operations using SafeMath for uint256; // List of all the projects Project[] private projects; }
This is the start of the CeloCrowdfund contract. It includes a line to use SafeMath and an array of the
Project
type. Next, we'll create a contract named Project
which will handle each project users create.In the same file as the
CeloCrowdfund
contract, create a Project
contract and an enum
called ProjectState
:contract CeloCrowdfund { // SafeMath for safe integer operations using SafeMath for uint256; // List of all the projects Project[] private projects; } contract Project { using SafeMath for uint256; enum ProjectState { Fundraising, Expired, Successful } IERC20 private cUSDToken; }
We use the
enum
named ProjectState
in order to keep track of a project's current state. A project can be in the fundraising, expired, or successful state. We use enum
because it creates a custom type for ProjectState
.We also create a private variable named
cUSDToken
which is of type IERC20
. This is the variable we'll use to interface with the cUSD tokens.Expanding the Project contract
Next, we'll add some public variables which describe a
Project
. Public variables in Solidity can be accessed by any other contract or dApp. We make these variables public in our contract because we will need it when we interact with the contract in order to get details about the Project
. For more info on Solidity variable types, be sure to check out this guide.It should look like this:
contract Project { using SafeMath for uint256; enum ProjectState { Fundraising, Expired, Successful } IERC20 private cUSDToken; // Initialize public variables address payable public creator; uint public goalAmount; uint public completeAt; uint256 public currentBalance; uint public raisingDeadline; string public title; string public description; string public imageLink; // Initialize state at fundraising ProjectState public state = ProjectState.Fundraising; mapping (address => uint) public contributions; }
After initializing the variables, we create a
state
variable to start as the fundraising state when the Project
contract is initialized. Next, we create a mapping from user addresses to to the amount they donate as a uint
to keep track of the contributions made to the Project
. In Solidity, a mapping is like a hash table or a dictionary.// Event when funding is received event ReceivedFunding(address contributor, uint amount, uint currentTotal); // Event for when the project creator has received their funds event CreatorPaid(address recipient); modifier theState(ProjectState _state) { require(state == _state); _; }
We will use the
ReceivedFunding
and CreatorPaid
events later on in our contract to store transaction logs on the blockchain. This is helpful for having a record of timestamps and transactions being made by our contract.We also use a function modifier in the contract to check that the state of the project is always of type
state
. Modifiers are a reusable way to check a condition before executing a function in Solidity. We'll use the modifier in a couple of functions later in our smart contract.Next, we'll add a constructor for the
Project
contract after the modifier:constructor ( IERC20 token, address payable projectCreator, string memory projectTitle, string memory projectDescription, string memory projectImageLink, uint fundRaisingDeadline, uint projectGoalAmount ) { cUSDToken = token; creator = projectCreator; title = projectTitle; description = projectDescription; imageLink = projectImageLink; goalAmount = projectGoalAmount; raisingDeadline = fundRaisingDeadline; currentBalance = 0; }
If you've done some object oriented programming in the past, constructors should be familiar to you. They're essentially the parameters we need to create a
Project
object.The contribute() function
Next, let's create a function in the
Project
contract for contributing money to a project:// Fund a project function contribute(uint256 amount) external theState(ProjectState.Fundraising) payable { cUSDToken.transferFrom(msg.sender, address(this), amount); contributions[msg.sender] = contributions[msg.sender].add(amount); currentBalance = currentBalance.add(amount); emit ReceivedFunding(msg.sender, amount, currentBalance); checkIfFundingExpired(); }
The
contribute()
function is an external function. This means it can only be called from other smart contracts or transactions.The first thing the
contribute()
function does is use the IERC20 transferFrom()
function in order to send the amount passed in the parameter from msg.sender
(the caller) to address(this)
(the contract):cUSDToken.transferFrom(msg.sender, address(this), amount);
Next, it adds the user's address to the mapping of
contributions
with the user's address as the key and the funding amount as the value:contributions[msg.sender] = contributions[msg.sender].add(amount);
Then the function updates the project's current balance and emits a
ReceivedFunding()
event:currentBalance = currentBalance.add(amount); emit ReceivedFunding(msg.sender, amount, currentBalance);
The checkIfFundingCompleteOrExpired() function
At the bottom of
contribute()
, the function calls checkIfFundingCompleteOrExpired();
which doesn't exist yet. Let's create that now!Write the following function in the
Project
contract:// check project state function checkIfFundingExpired() public { if (block.timestamp > raisingDeadline) { state = ProjectState.Expired; } }
This function checks if the deadline is past the
block.timestamp
(the current time of the most recent block). If the project has expired, the state is updated.Next, let's make the
payOut()
function.The payOut() function
Below the
checkIfFundingCompleteOrExpired()
function, add the following for payOut()
:function payOut() external returns (bool result) { require(msg.sender == creator); uint256 totalRaised = currentBalance; currentBalance = 0; if (cUSDToken.transfer(msg.sender, totalRaised)) { emit CreatorPaid(creator); state = ProjectState.Successful; return true; } else { currentBalance = totalRaised; state = ProjectState.Successful; } return false; }
The first thing the
payOut()
function does is it checks that the address calling the function is the same as the project creator by using require()
. We do this to make sure only the project creator can withdraw their funds.Next, the
payOut()
function will send the full amount raised by a project back to the project creator. It does this by calling the tranfer()
function from the IERC20
interface.transfer()
returns a boolean value. If it works, then the CreatorPaid()
event is emitted and the state is updated. If not, we reset the currentBalance
variable and update the state anyway.The getDetails() function
Finally, we're going to add the last function in our
Project
contract, the getDetails()
function.function getDetails() public view returns ( address payable projectCreator, string memory projectTitle, string memory projectDescription, string memory projectImageLink, uint fundRaisingDeadline, ProjectState currentState, uint256 projectGoalAmount, uint256 currentAmount ) { projectCreator = creator; projectTitle = title; projectDescription = description; projectImageLink = imageLink; fundRaisingDeadline = raisingDeadline; currentState = state; projectGoalAmount = goalAmount; currentAmount = currentBalance; }
This function returns information about the project by returning the public variables we set at the start of the
Project
contract.That's it for the
Project
contract!Back to the crowdfund contract
It's time to get back to the
CeloCrowdfund
contract now that we've finished the Project
contract. Scroll back up to the top of the file where your CeloCrowdfund
contract is.We'll start by adding an
event
for when a project is started:contract CeloCrowdfund { // SafeMath for safe integer operations using SafeMath for uint256; // List of all the projects Project[] private projects; // event for when new project starts event ProjectStarted( address contractAddress, address projectCreator, string title, string description, string imageLink, uint256 fundRaisingDeadline, uint256 goalAmount ); }
We've used
events
in a couple of places in the contract so far. We're going to use this event
to log when a project is created to the blockchain. It will take parameters which contain all the Project
data we use to create a new Project
.Next, let's make the
startProject()
function to start a project:function startProject( IERC20 cUSDToken, string calldata title, string calldata description, string calldata imageLink, uint durationInDays, uint amountToRaise ) external { uint raiseUntil = block.timestamp.add(durationInDays.mul(1 days)); Project newProject = new Project(cUSDToken, payable(msg.sender), title, description, imageLink, raiseUntil, amountToRaise); projects.push(newProject); emit ProjectStarted( address(newProject), msg.sender, title, description, imageLink, raiseUntil, amountToRaise ); }
The
startProject()
function takes in some basic info for creating a project like the title
, description
, imageLink
, duration
, and amount
to raise.It then makes the
raiseUntil
variable use days by multiplying the durationInDays
by 1 days
. This turns the durationInDays
variable from a uint
to something block.timestamp
will accept.Next, our
startProject()
function creates a newProject
of type Project
(from our Project
contract) with the parameters the Project
contract constructor takes.Finally, the function emits a
ProjectStarted()
log.One last thing for our
CeloCrowdfund
contract: we'll add a function to return the list of Projects
created:function returnProjects() external view returns(Project[] memory) { return projects; }
And that's it for our two contracts!
Conclusion
Just like that, we've created two smart contracts which will allow for crowdfunding in Celo.
Hopefully creating this smart contract has given you a sense of what's possible. Without too much hassle and infrastructure setup, we're able to use this contract to accept payments and help users coordinate towards raising money for a project they want to support. And all things considered, it wasn't too long or complex for an entire project backend.
In the next section, we will discuss deploying the contracts we've written to the Celo network!
Note: This tutorial and smart contract is based on the contracts for Coperacha, an app built by the tutorial author. If you want to see these contracts being used in a mobile app, you can see an example of that here.
Deploying a Crowdfunding Smart Contract in Celo
If you've done part 1 of this tutorial series, you have a crowdfunding contract written in Solidity. Now that we've got a crowdfunding contract, we'll need to deploy it to the Celo test network in order to interact with it. Let's do that now!
Prerequisites
This tutorial assumes that you have followed and completed part 1 (Building a Crowdfunding Smart Contract in Celo). If you have not, please go back and follow that tutorial first.
Click the image below in order to watch the YouTube video for "Deploying a Crowdfunding Smart Contract in Celo".
Deployment Setup
First, open the terminal and
cd
into the Celo crowdfunding project folder.cd celo-crowdfunding
The migrations folder
If there is already the
1_initial_migration.js
file in the folder, delete it.Next, create a new file named
1_celo_crowdfund.js
and write the following:const CeloCrowdfund = artifacts.require("CeloCrowdfund"); module.exports = function (deployer) { deployer.deploy(CeloCrowdfund); };
Migrations in Truffle are essentially deployment scripts. What we have written is a very simple deployment script which takes our contract (
CeloCrowdfund
) and deploys it.Connecting to a Testnet node
We're going to use DataHub to connect to the Celo test network. If you don't have an account, sign up on the DataHub website and resume this tutorial once you have your Celo API key.
Now that we have an API key for the DataHub node, we'll want to secure it. Anyone who obtains your API key will be able to use your DataHub account's node. To secure it, let's create a
.env
file in the root directory of the celoSmartContract
folder. The .env
file is used for environment secrets, which means its the perfect place to store the DataHub API key.To create this
.env
file, navigate to the root directory of the project and type the following command into the terminal:touch .env
Next, open the
.env
file in a text editor and add the following variable, making sure to enter the API key between the /
's:REST_URL=https://celo-alfajores--rpc.datahub.figment.io/apikey/<YOUR API KEY>/
Note: there needs to be a trailing
/
at the end of the URL for it to work!Note: If the plan is to commit this repo into Github, you won't want to upload the contents of the
.env
file. To get Git to ignore the .env
file, create a .gitignore
file from the terminal like so:touch .gitignore
Inside the
.gitignore
, put the following in a new line:.env
And that's it! The
.env
file will now be ignored by Git.Getting a Celo account
Next, we’re going to need a Celo account to deploy from. We will need three things for deployment:
- A Celo account address
- A Celo account private key
- A Celo account loaded with testnet funds​
First things first, let's get an account and a private key. Create a file named getAccount.js in the project folder. In that file, write the following:
const ContractKit = require('@celo/contractkit'); const Web3 = require('web3'); require('dotenv').config(); const main = async() => { const web3 = new Web3(process.env.REST_URL); const client = ContractKit.newKitFromWeb3(web3); const account = web3.eth.accounts.create(); console.log('address: ', account.address); console.log('privateKey: ', account.privateKey); }; main().catch((err) => { console.error(err); });
Let's break this down.
First, the script imports
ContractKit
, Web3
, and dotenv
. Next, it connects to the Celo network via the REST_URL in the .env
file using the following line:const web3 = new Web3(process.env.REST_URL);
Then it creates an account on that same testnet connection with the following line:
const account = web3.eth.accounts.create();
After that, we just print out the address and private key for future use.
Note: The above code is from #2. Create your first Celo account, so feel free to rewind if you need a review.
Next, run the script in the terminal with the command
node getAccount.js
.The output should print out the address and private key for you new Celo account. It will look like this:
​​​​
Note: It is important to keep your private key secure! Whoever has it can access all your funds on Celo.
Copy the privateKey into the
.env
file by adding a line with PRIVATE_KEY=YOUR-PRIVATE-KEY
- Where YOUR-PRIVATE-KEY is the private key you got from the script output.Now that you have a Celo account, take the address and paste it into the Celo developer faucet. This will give you testnet funds you can use to deploy the smart contract. Fill out the form and wait a couple of seconds, and your account should be loaded up and ready to go.
​​​​
Truffle config
The truffle-config.js is used in order to tell truffle how you want to deploy the contract.
For our purposes, we will need to add the following code to the project truffle-config file:
const ContractKit = require('@celo/contractkit'); const Web3 = require('web3'); require('dotenv').config({path: '.env'}); // Create connection to DataHub Celo Network node const web3 = new Web3(process.env.REST_URL); const client = ContractKit.newKitFromWeb3(web3); // Initialize account from our private key const account = web3.eth.accounts.privateKeyToAccount(process.env.PRIVATE_KEY); // We need to add private key to ContractKit in order to sign transactions client.addAccount(account.privateKey); module.exports = { compilers: { solc: { version: "0.8.0", // Fetch exact version from solc-bin (default: truffle's version) } }, networks: { test: { host: "127.0.0.1", port: 7545, network_id: "*" }, alfajores: { provider: client.connection.web3.currentProvider, // CeloProvider network_id: 44787 // latest Alfajores network id } } };
First, the config file imports
ContractKit
, Web3
, and dotenv
just like the getAccounts.js
file.It connects to our Celo node by getting the
REST_URL
from .env
:const web3 = new Web3(process.env.REST_URL);
Then it gets the account from the private key in the
.env
file in order to deploy from your account:const account = web3.eth.accounts.privateKeyToAccount(process.env.PRIVATE_KEY);
We also add the private key to
ContractKit
in order to sign transactions with:client.addAccount(account.privateKey);
And finally, in the
module.exports
function, we set the Solidity version we want to use under compilers: {
and the network we want to deploy to under networks: {
.The following block of code is what tells truffle to deploy to Alfajores (Celo's testnet):
alfajores: { provider: client.connection.web3.currentProvider, // CeloProvider network_id: 44787 // latest Alfajores network id }
Deployment
We’re almost there! Run
truffle compile
to check that you did everything correctly. If things are working, you should see the following output:Now that we’ve compiled the smart contract, the last step is to deploy it. Run the following to deploy to the Alfajores testnet:
truffle migrate --network alfajores
You should see the following deployment output:
If you see something like the above, the code works! To see the smart contract on the Celo network, open the Alfajores block explorer & paste in the address on the contract address line from the Truffle output:
> contract address: YOUR-CELO-ADDRESS
You should see a successful contract deployment at that address in the block explorer:
Next Steps
Now that we've deployed our Celo crowdfunding smart contract on Celo, we can move on to interacting with it. In the next tutorial, we'll interact with our smart contract by creating new crowdfunding projects and donating to them.
Potential Errors & Solutions
You can view the source code on GitHub. In any case, here are some common errors you may encounter.
If you get the following error:
Error: Invalid JSON RPC response: {"message":"no Route matched with those values"}
​​​​
Then it's a problem with the REST_URL in the .env file.
Make sure the URL has a trailing / at the end! It should look like this:
REST_URL = https://celo-alfajores--rpc.datahub.figment.io/apikey/YOUR-API-KEY/
If the contract didn't deploy to the test network, the output might look like this:
​​​​
Where the contracts were compiled but it didn't give you an address for where it was deployed to.
To fix this, make sure you have your account loaded up with testnet funds from the faucet.
If you want to double-check that your account received the funds, go to the Alfajores block explorer and search for your account's address.
Make sure your account isn't empty like this one!
​​
Interacting with the Crowdfunding Smart Contracts
Welcome to the last tutorial in this three part series on creating a crowdfunding smart contract on Celo. In this last part, we're going to write Javascript code in order to interact with the smart contract we wrote and deployed in the previous two parts.
Now that we have our smart contract on the Celo test network, it's time to use it!
Prerequisites
You will need the smart contracts we wrote in part 1, and the deployments you made in part 2. Therefore, for the code in this tutorial to work you will need to have completed the prior two tutorials.
Click the image below in order to watch the YouTube video for "Interacting with the Crowdfunding Smart Contracts".
Video: Interacting with the Crowdfunding Smart Contracts
Setup
The first step is to create a Javascript file to write our smart contract interactions in. In the root of the
celo-crowdfunding
folder, create a file named interact.js
.We will use the packages we installed via NPM in the first tutorial here. We will also need one additional module, BigNumber, in order to work with the large numbers the Celo blockchain uses.
In the terminal, run
npm install bignumber.js
. That's it for the setup of this step!Importing our modules
The first step for interacting with our smart contracts is importing the modules we'll need for our script.
At the top of the
interact.js
file, write the following:const Web3 = require('web3'); const ContractKit = require('@celo/contractkit'); const web3 = new Web3('https://alfajores-forno.celo-testnet.org'); const kit = ContractKit.newKitFromWeb3(web3); const CeloCrowdfund = require('./build/contracts/CeloCrowdfund.json'); const Project = require('./build/contracts/Project.json'); const BigNumber = require('bignumber.js'); require('dotenv').config({path: '.env'});
We will use all of these modules later on in order to interact with the crowdfunding contracts we previously made.
Getting our contract
The next thing we want to go is get our smart contract as a variable we can use. We'll also need to get our Celo account because we'll be sending transactions to the network.
After the imports, write the following:
// Get Celo account info let account = web3.eth.accounts.privateKeyToAccount(process.env.PRIVATE_KEY); kit.connection.addAccount(account.privateKey)
The above code imports your private key we set in the second tutorial from the
.env
file and puts it into the account variable. It also adds the Celo account to contractKit.Next, let's create an async function where we will write our interactions with the smart contract:
async function interact() { // Check the Celo network ID const networkId = await web3.eth.net.getId(); // Get the contract associated with the current network const deployedNetwork = CeloCrowdfund.networks[networkId]; // Create a new contract instance from the celo crowdfund contract let celoCrowdfundContract = new kit.web3.eth.Contract(CeloCrowdfund.abi, deployedNetwork && deployedNetwork.address); console.log("Account address: ", account.address); console.log(celoCrowdfundContract); } interact();
Let's run this. In the terminal, type:
node interact.js
. The result should be something like the following:If all goes well, the output should show the account address, and the contract that we created. The contract output looks different than regular solidity code because it is a contract ABI.
Great! So we've verified that the contract is getting imported correctly and our account works.
Next, comment out the
console.log
of the celoCrowdfundContract
variable since we won't use it anymore.The next step is to create a new
Project
. Create a new function outside of interact()
called createProject()
:async function createProject(celoCrowdfundContract, stableToken, gasPrice) { var projectGoal = BigNumber(1E18); await celoCrowdfundContract.methods.startProject(stableToken.address, 'Test project', 'We are testing the create project function', 'https://i.imgur.com/Flfo4hJ.png', 5, projectGoal).send({from: account.address, feeCurrency: stableToken.address, gasPrice: gasPrice}); console.log("Created new project"); }
This function takes in the
celoCrowdfunding
, stableToken
, and gasPrice
as parameters. It then creates a projectGoal
using BigNumber
.We create a number that's of size 1,000,000,000,000,000,000 or 1^18 because cUSD has a size of 18 decimals. Solidity doesn't have support for floating point numbers, so the workaround is to make really large numbers. 1^18 cUSD is equal to $1 cUSD.
Next, we call the
startProject()
method in our crowdfunding contract and pass in the parameters it requires. If we go back to the CeloCrowdfund.sol
contract, we'll see the createProject()
method takes in the following:function startProject( IERC20 cUSDToken, string calldata title, string calldata description, string calldata imageLink, uint durationInDays, uint amountToRaise )
These are the parameters we supply when we call the function.
Now that we have that, we will create a stableToken variable which uses the
ContractKit
stabletoken wrapper in order to get a reference to the cUSD coin.We will also get the gas price to pass in to our functions. We'll set the gas higher than the recommended minimums in order to get our transactions confirmed faster. For a primer on gas fees in Celo, feel free to check out this post.
After getting those two variables, we will call the
createProject()
helper function inside interact()
. We'll also return all the projects in our contract using the returnProjects()
function in our CeloCrowdfund
contract, to verify that it worked:// Print wallet address so we can check it on the block explorer console.log("Account address: ", account.address); // Get the cUSD ContractKit wrapper var stableToken = await kit.contracts.getStableToken(); // Get the gas price minimum and set the new gas price to be double const gasPriceMinimumContract = await kit.contracts.getGasPriceMinimum() const gasPriceMinimum = await gasPriceMinimumContract.getGasPriceMinimum(stableToken.address) const gasPrice = Math.ceil(gasPriceMinimum * 2) // This should be much higher than the current average, so the transaction will confirm faster await createProject(celoCrowdfundContract, stableToken, gasPrice); // Return projects inside the celo crowdfund contract var result = await celoCrowdfundContract.methods.returnProjects().call(); console.log("List of addressses for each of the projects created:", result);
After running the code, you should see the new project created.
Sending money to a project
Now that we've created a project, we'll need to send some money to it.
Just like how we created a variable called
celoCrowdfundContract
to access our CeloCrowdfund
contract, we'll need to create a variable to access the Project
contract.To do this we can write the following:
var projectInstanceContract = new web3.eth.Contract( Project.abi, deployedNetwork && result[result.length - 1] // Get the most recently deployed Project );
This will create a variable to access the most recently created Project contract instance from the array returned by the
returnProjects()
function.Now that we can access our project, we'll need to do two things in order to send cUSD to the contract. Since cUSD follows the ERC-20 standard, we'll need to approve sending cUSD to the contract prior to sending money. After it's approved, we can send cUSD to our contract.
We'll approve a large amount of cUSD for the contract as an example. To approve 500 cUSD to be sent, write the following:
// Approve the project to spend up to 500 cUSD from wallet var approveAmount = BigNumber(500E18); await stableToken.approve(projectInstanceContract._address, approveAmount).sendAndWaitForReceipt({from: account.address});
Great! Now we have approved our contract to receive 500 cUSD. The next step is to actually send some money.
Outside of the
interact()
function, create a new function called contribute()
:async function contribute(stableToken, projectInstanceContract, gasPrice) { var sendAmount = BigNumber(2E18); // Call contribute() function with 2 cUSD await projectInstanceContract.methods.contribute(sendAmount).send({from: account.address, feeCurrency: stableToken.address, gasPrice: gasPrice}); console.log("Contributed to the project\n"); }
In the
contribute()
function, we create a variable called sendAmount
which is set to 2E18. This is equivalent to 2 cUSD. Next, we use our projectInstanceContract
variable to call the contribute()
function in our Project()
contract.Back in our
interact()
function, let's call the contribute()
helper function we created:await contribute(stableToken, projectInstanceContract, gasPrice);
Displaying our balances
There are now two cUSD balances we care about: the balance of our Celo wallet, and the balance of our Project which the
contribute()
function sent 2 cUSD to.Outside the
interact()
function, create a helper function called createBalances()
to print this all out:async function printBalances(stableToken, projectInstanceContract) { var balanceOfUser = (await stableToken.balanceOf(account.address)).toString(); console.log("User's address: ", account.address); console.log("User's cUSD balance: ", balanceOfUser/1E18, " cUSD\n"); var balanceOfContract = (await stableToken.balanceOf(projectInstanceContract._address)).toString(); console.log("Contract address: ", projectInstanceContract._address); console.log("Contract cUSD balance: ", balanceOfContract/1E18, " cUSD\n"); }
The
printBalances()
function uses the stableToken
variable's balanceOf()
function in order to get our Celo wallet's cUSD balance. We then use that same balanceOf()
function in order to get the balance of the contract, and then we print it all out.Next, inside the
interact()
function just call the printBalances()
function after contribute()
:await printBalances(stableToken, projectInstanceContract);
Paying out from our Project contract
So far we're able to create a Project, and fund it. The final step is paying out from our contract!
Outside the
interact()
function, create a helper function called payOut()
:async function payOut(stableToken, projectInstanceContract, gasPrice) { var payOut = await projectInstanceContract.methods.payOut().send({from: account.address, feeCurrency: stableToken.address, gasPrice: gasPrice}); console.log("Paying out from project"); }
The
payOut()
function calls the payOut()
function inside our Project
smart contract. This will send all the funds sent back to the project creator.Finally, let's call the
payOut()
helper function inside our interact()
function:await contribute(stableToken, projectInstanceContract, gasPrice); await printBalances(stableToken, projectInstanceContract); await payOut(stableToken, projectInstanceContract, gasPrice); console.log("After pay out: "); await printBalances(stableToken, projectInstanceContract);
We'll want to wait 5 seconds before printing just to make sure the
payOut()
transaction has been confirmed on the blockchain.Now let's run the code! In your terminal, type:
node interact.js
The output should look something like this:
Awesome! As we can see from the terminal output, our script creates a new project, contributes 2 cUSD to it, waits 5 seconds, and then pays out from the project. We can see the flow of the 2 cUSD going from the Celo wallet --> the contract --> back to the wallet.
It works!
Conclusion
This three part series has shown you how to write a smart contract for Celo, deploy it, and then interact with the smart contract we created.
You can use this third tutorial as a jumping off point for interacting with any smart contract you make using Javascript. Whether you're creating a web dApp or a mobile dApp, you can use this code in order to interact with your contracts.
You can also view the source code here
About the Authors
The written tutorials were created by Alex Reyes. Alex is a student (BS, Computer Science) and crypto enthusiast who's learning all about the world of web3 one day at a time and he's contributing to Web3 communities actively. He previously completed internships at Facebook and Microsoft.
The videos were created by Neo Cho. Neo is a student (BS, Computer Science) at the University of Central Florida. He enjoys learning about crypto and is excited about the future of web3.