Skip to main content

4 posts tagged with "blockchain"

View All Tags

Β· 10 min read
Asia K
  1. create new dapp on alchemy api endpoint url on quicknode to get updated api url (video walkthrough)

  2. update React to version 18.0.0

  3. update environmental variables in .env file

update env url

  1. destructured import statement

import { ChainId, ThirdwebProvider } from '@thirdweb-dev/react';

updated index.jsx file to

JSX

import React from "react";
//import ReactDOM from 'react-dom';
import { createRoot } from "react-dom/client";
import "./index.css";
import App from "./App";

// Import thirdweb provider and Rinkeby ChainId
import { ThirdwebProvider } from "@thirdweb-dev/react";
import { ChainId } from "@thirdweb-dev/sdk";

// This is the chainId your dApp will work on.
const activeChainId = ChainId.Goerli;

// Wrap your app with the thirdweb provider

const container = document.getElementById("root");
const root = createRoot(container);

root.render(
<React.StrictMode>
<ThirdwebProvider desiredChainId={activeChainId}>
<App />
</ThirdwebProvider>
</React.StrictMode>
);

ran script to initialize the thirdweb sdk

scripts/1-initialize.js

commented out code to test and make sure that address was being read and logged in the console.

JSX

//import { useState, useEffect, useMemo } from "react";
//import { AddressZero } from "@ethersproject/constants";
//import { useAddress, useMetamask, useEditionDrop, useToken, useVote, useNetwork } from "@thirdweb-dev/react";
import { useAddress, useMetamask, useNetwork } from "@thirdweb-dev/react";

import { ChainId } from "@thirdweb-dev/sdk";

const App = () => {
// Use the hooks thirdweb give us.
const address = useAddress();
const network = useNetwork();
const connectWithMetamask = useMetamask();
console.log("πŸ’― Address:", address);

if (address && network?.[0].data.chain.id !== ChainId.Goerli) {
return (
<div className="unsupported-network">
<h2>Please connect to Goerli</h2>
<p>
This dapp was designed to work on the Goerli network, please switch
networks in your connected wallet.
</p>
</div>
);
}
// If the user hasn't connected their wallet, then let them call connectWithMetamask.
if (!address) {
return (
<div className="landing">
<h1>Welcome to UpCyDAO</h1>
<button onClick={connectWithMetamask} className="btn-hero">
Connect your wallet
</button>
</div>
);
}
};
/* const [proposals, setProposals] = useState([]);
const [isVoting, setIsVoting] = useState(false);
const [hasVoted, setHasVoted] = useState(false);
const [isClaiming, setIsClaiming] = useState(false);
// isClaiming keeps a loading state while the NFT is minting.

const [hasClaimedNFT, setHasClaimedNFT] = useState(false);
const [memberAddresses, setMemberAddresses] = useState([]);
const [memberTokenAmounts, setMemberTokenAmounts] = useState([]); */
// the state of the amount of tokens each member has

//const vote = useVote("0x813244Ca4AC13550F7411A5Cd40C29AF6Cb35BA5");
// Vote ERC-20 from https://rinkeby.etherscan.io/address/0x813244Ca4AC13550F7411A5Cd40C29AF6Cb35BA5
// provides access to coded proposals

//const token = useToken("0xeEe746dcE397378567039d845740D9bf28Fb399D");
// ERC-20 from https://rinkeby.etherscan.io/address/0xeEe746dcE397378567039d845740D9bf28Fb399D

/* const editionDrop = useEditionDrop(
"0xd844F24e6916C3cc569FaAE9FfD2aD9e9bCCe772"
); */
// Initializing the editionDrop contract

// shortening the wallet address with JavaScript substring() method
/* const shortenAddress = (str) => {
return str.substring(0, 6) + "..." + str.substring(str.length - 4);
}; */

// Retrieve all our existing proposals from the contract, return nothing if not a member.
/* useEffect(() => {
if (!hasClaimedNFT) {
return;
} */

// getting all proposals with vote.getAll() method
/* const getAllProposals = async () => {
try {
const proposals = await vote.getAll();
setProposals(proposals);
} catch (error) {
console.log("failed to get proposals", error);
}
};
getAllProposals();
}, [hasClaimedNFT, vote]); */

// checking if the user already voted.
/* useEffect(() => {
if (!hasClaimedNFT) {
return;
} */

// If we haven't finished retrieving the proposals from the useEffect above
// then we can't check if the user voted yet!
/* if (!proposals.length) {
return;
}

const checkIfUserHasVoted = async () => {
try {
const hasVoted = await vote.hasVoted(proposals[0].proposalId);
setHasVoted(hasVoted);
if (hasVoted) {
console.log("βœ– User has already voted");
} else {
console.log("β˜‘ User has not voted yet");
}
} catch (error) {
console.error("Failed to check if wallet has voted", error);
}
};
checkIfUserHasVoted();
}, [hasClaimedNFT, proposals, address, vote]);
*/
// This useEffect grabs all the addresses of our members holding the NFT.
/* useEffect(() => {
if (!hasClaimedNFT) {
return;
}

// getting the address of users who hold the tokenId 0
const getAllAddresses = async () => {
try {
const memberAddresses =
await editionDrop.history.getAllClaimerAddresses(0);
setMemberAddresses(memberAddresses);
console.log("🏠 Member addresses", memberAddresses);
} catch (error) {
console.error("Failed to get member list.", error);
}
};
getAllAddresses();
}, [hasClaimedNFT, editionDrop.history]);
*/
// getting the # of tokens held by each member. If the member has no tokens, returns nothing.
/* useEffect(() => {
if (!hasClaimedNFT) {
return;
} */

/* const getAllBalances = async () => {
try {
const amounts = await token.history.getAllHolderBalances();
setMemberTokenAmounts(amounts);
console.log("πŸ‘œ Amounts", amounts);
} catch (error) {
console.error("failed to get member balances", error);
}
};
getAllBalances();
}, [hasClaimedNFT, token.history]);
*/
// Combining the memberAddresses and memberTokenAmounts into a single array
/* const memberList = useMemo(() => {
return memberAddresses.map((address) => {
// checking if the address in the memberTokenAmounts array is findable.
// If the address is found, returning the amount of tokens the user has.
const member = memberTokenAmounts?.find(
({ holder }) => holder === address
);
// Otherwise, return 0.
return {
address,
tokenAmount: member?.balance.displayValue || "0",
};
});
}, [memberAddresses, memberTokenAmounts]);

useEffect(() => {
// If a wallet address is not connected, exit!
if (!address) {
return;
} */

/* // checking the balance of tokens in the address
const checkBalance = async () => {
try {
const balance = await editionDrop.balanceOf(address, 0);
if (balance.gt(0)) {
setHasClaimedNFT(true);
console.log("🌟 This user has a membership NFT!");
} else {
setHasClaimedNFT(false);
console.log("😭 This user doesn't have a membership NFT.");
}
} catch (error) {
setHasClaimedNFT(false);
console.error("Failed to get balance", error);
}
};
checkBalance();
}, [address, editionDrop]);

const mintNft = async () => {
try {
setIsClaiming(true);
await editionDrop.claim("0", 1);
console.log(
`🌊 Successfully Minted! Check it out on OpenSea: https://testnets.opensea.io/assets/${editionDrop.getAddress()}/0`
);
setHasClaimedNFT(true);
} catch (error) {
setHasClaimedNFT(false);
console.error("Failed to mint NFT", error);
} finally {
setIsClaiming(false);
}
};
*/
// checking if a chain on our preferred network is found, and if it is not found, then prompting the user to switch networks in their wallet.
/* if (address && network?.[0].data.chain.id !== ChainId.Goerli) {
return (
<div className="unsupported-network">
<h2>Please connect to Goerli</h2>
<p>
This dapp was designed to work on the Goerli network, please switch
networks in your connected wallet.
</p>
</div>
);
}
// If the user hasn't connected their wallet, then let them call connectWithMetamask.
if (!address) {
return (
<div className="landing">
<h1>Welcome to UpCyDAO</h1>
<button onClick={connectWithMetamask} className="btn-hero">
Connect your wallet
</button>
</div>
);
}
*/
// If the user has already claimed the membership, display the members only DAO dashboard rendering all the members + token amounts.
/* if (hasClaimedNFT) {
return (
<div className="member-page">
<h1>UpCyDAO Member Page</h1>
<p className="thankyou">Thank You for being a member!</p>
<div>
<div>
<h2>Member List</h2>
<table className="card">
<thead>
<tr>
<th>Address</th>
<th>Token Amount</th>
</tr>
</thead>
<tbody>
{memberList.map((member) => {
return (
<tr key={member.address}>
<td>{shortenAddress(member.address)}</td>
<td>{member.tokenAmount}</td>
</tr>
);
})}
</tbody>
</table>
</div>
<div>
<h2>Active Proposals</h2>
<form
onSubmit={async (e) => {
e.preventDefault();
e.stopPropagation();

// disabling the button to prevent double clicks
setIsVoting(true);

// getting the votes from the form for the values
const votes = proposals.map((proposal) => {
const voteResult = {
proposalId: proposal.proposalId,
//abstain by default
vote: 2,
};
proposal.votes.forEach((vote) => {
const elem = document.getElementById(
proposal.proposalId + "-" + vote.type
);

if (elem.checked) {
voteResult.vote = vote.type;
return;
}
});
return voteResult;
});

try {
// checking if the wallet still needs to delegate their tokens before they can vote
const delegation = await token.getDelegationOf(address);
// if the delegation is the 0x0 address that means they have not delegated their governance tokens yet
if (delegation === AddressZero) {
//if they haven't delegated their tokens yet, we'll have them delegate them before voting
await token.delegateTo(address);
}
// voting on the proposals
try {
await Promise.all(
votes.map(async ({ proposalId, vote: _vote }) => {
// before voting can take place, checking whether the proposal is open for voting
// 1. getting the latest state of the proposal
const proposal = await vote.get(proposalId);
// checking if the proposal is open for voting (state === 1 means it is open)
if (proposal.state === 1) {
// if it is open for voting, user can vote on it
return vote.vote(proposalId, _vote);
}
// if the proposal is not open for voting, returns nothing, letting us continue
return;
})
);
try {
// if any of the propsals are ready to be executed we'll need to execute them
// a proposal is ready to be executed if it is in state 4
await Promise.all(
votes.map(async ({ proposalId }) => {
// getting the latest state of the proposal again, since we may have just voted before
const proposal = await vote.get(proposalId);

//if the state is in state 4 (meaning that it is ready to be executed), then executing the proposal
if (proposal.state === 4) {
return vote.execute(proposalId);
}
})
);
// At this point, the vote has been successfully. Setting the "hasVoted" state to true
setHasVoted(true);
// and log out a success message
console.log("successfully voted");
} catch (err) {
console.error("failed to execute votes", err);
}
} catch (err) {
console.error("failed to vote", err);
}
} catch (err) {
console.error("failed to delegate tokens");
} finally {
// in *either* case we need to set the isVoting state to false to enable the button again
setIsVoting(false);
}
}}>
{proposals.map((proposal) => (
<div key={proposal.proposalId} className="card">
<h5>{proposal.description}</h5>
<div>
{proposal.votes.map(({ type, label }) => (
<div key={type}>
<input
type="radio"
id={proposal.proposalId + "-" + type}
name={proposal.proposalId}
value={type}
//default the "abstain" vote to checked
defaultChecked={type === 2}
/>
<label htmlFor={proposal.proposalId + "-" + type}>
{label}
</label>
</div>
))}
</div>
</div>
))}
<button disabled={isVoting || hasVoted} type="submit">
{isVoting
? "Voting..."
: hasVoted
? "You Have Already Voted"
: "Submit Your Votes"}
</button>
{!hasVoted && (
<small className="note">
This will trigger multiple transactions that you will need to
sign.
</small>
)}
</form>
</div>
</div>
</div>
);
} */

// Render mint nft screen.
/* return (
<div className="mint-nft">
<h1>Mint your UpCyDAO Membership NFT</h1>
<button disabled={isClaiming} onClick={mintNft}>
{isClaiming ? "Minting..." : "Mint your Free NFT"}
</button>
</div>
);
}; */

export default App;

console logged address

  1. comment out or remove // import { ethers } from "ethers"; from 1-initialize-sdk.js

replace all instances of ALCHEMY_API_URL with QUICKNODE_API_URL

replace

const provider = new ethers.providers.JsonRpcProvider(process.env.ALCHEMY_API_URL);
const wallet = new ethers.Wallet(process.env.PRIVATE_KEY, provider);
const sdk = new ThirdwebSDK(wallet);

with ...

const sdk = ThirdwebSDK.fromPrivateKey(
// Your wallet private key. ALWAYS KEEP THIS PRIVATE, DO NOT SHARE IT WITH ANYONE, add it to your .env file and do not commit that file to github!
process.env.PRIVATE_KEY,
// RPC URL, we'll use our QuickNode API URL from our .env file.
process.env.QUICKNODE_API_URL
);
  1. Update the code to use the ChainId.Goerli constant to check against the network id.

if (address && network?.[0].data.chain.id !== ChainId.Goerli) {

  1. update the code that checks if the user's connected wallet is connected to the Goerli network. If it is not connected to the Goerli network, updade the user interface to display a message saying "Please connect to Goerli" and "This dapp was designed to work on the Goerli network, please switch networks in your connected wallet." in the browser.
      <div className="unsupported-network">
<h2>Please connect to Goerli</h2>

<h2>
This dapp was designed to work on the Goerli network, please switch
networks in your connected wallet.
</h2>
</div>
);

connect to goerli

Retracing the steps from building a dao part one, the next step is to deploy the ERC-1155 membership token.

But...

getEditionDrop deprecated

So...

...will be replacing all instances of getEditionDrop with getContract.

2-deploy-drop.js

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
βœ… Successfully deployed editionDrop contract, address: 0x834b5488C4e1fe6Ec2A72996997E52647caD4Dce
βœ… editionDrop metadata: {
name: 'UpCyDAO Membership',
description: 'A DAO for upcycling enthusiasts.',
image: 'https://gateway.ipfscdn.io/ipfs/QmbhN1evA8XNrbM3J67zheJVpsVrGq9EGWH8XjawPNHAPT/0',
seller_fee_basis_points: 0,
fee_recipient: '0x0000000000000000000000000000000000000000',
merkle: {},
symbol: ''
}

view the contract here: https://mumbai.polygonscan.com/address/0x834b5488C4e1fe6Ec2A72996997E52647caD4Dce#code

polygon testnet transaction

Β· One min read
Asia K

In Part One, ...

In Part Two ...

In this part, I've customized the frontend of the dApp.

I started with public/index.html

Here, I've

  • added a favicon
  • changed the page title
  • changed the metadata for the
    • website
    • twitter
    • facebook

Generated a color pallete from the DAO image using coolers.co's image picker

pallette

color pallette picker demo

favicon

CSS Background Pattern generator

Did some research on Design Trends for Web3

"A lot of apps built on blockchain (known as dApps) take with it the characteristics that blockchain embodies such as openness, security, fair distribution, community-driven and self-governing." (source: uxdesign.cc)

"As blockchain becomes more mainstream, education will be needed less. But for now, companies are rightfully concentrating efforts in this space." (source: uxdesign.cc)

Design Inspiration afterparty design afterparty.ai/

llama design llama.xyz/

I wanted to add google fonts to figma

  • Selected a font, then downloaded it
  • opened the desktop app, it appeared in the font selector

"A favicon allows your site to be more recognizable in web browsers." https://medium.com/amsterdam-standard/designing-favicons-importance-design-process-and-trends-2020-4d901270ba02

Β· 14 min read
Asia K

welcome to UpCyDAO

In Building a DAO (Part One) the following user stories were fulfilled:

  • Users can connect their web3 wallet [x]
  • Users can mint a membership NFT [x]

To fulfill these requirements, an ERC-1155 collection was created on the blockchain. To view the Membership NFT on Opensea, click here.

As holders of the Membership NFT, users have access to the DAO.

In this session, the last two user stories will be fulfilled...

  • Users will receive a token airdrop
  • Users will be able to vote on proposals

Deploying the ERC-20 token smart contract​

In order to do a token airdrop, in scripts/5-deploy-token.js, I've created an ERC-20 token smart contract with the code shown below.

import { AddressZero } from '@ethersproject/constants';
import sdk from './1-initialize-sdk.js';

(async () => {
try {
// deploying a standard ERC-20 contract
const tokenAddress = await sdk.deployer.deployToken({
// Giving the token a name
name: "UpCyDAO Governance Token",
// Giving the token a symbol
symbol: "UPCY",
// because the token is not for sale, it's set to be received by AddressZero
primary_sale_recipient: AddressZero,
});
console.log(
"🍹 Successfully deployed token module, address:",
tokenAddress,
);
} catch (error) {
console.error("😝 failed to deploy token module", error);
}
})();

node scripts/5-deploy-token.js

returns and confirms!

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
🍹 Successfully deployed token module, address: 0xeEe746dcE397378567039d845740D9bf28Fb399D

Here's the ERC-20 token smart contract on https://rinkeby.etherscan.io/

ERC-20 token smart contract

Here's a screencap of how I imported the $UPCY into my MetaMask wallet using the contract address.

importing the token in MetaMask

😜 Creating the token’s supply​

With the code shown below, in scripts/6-print-money.js, I told the contract how many tokens are available for circulation.

import sdk from "./1-initialize-sdk.js";

const token = sdk.getToken("0xeEe746dcE397378567039d845740D9bf28Fb399D");

(async () => {
try {
// max token supply
const amount = 1_000_000;
// interact with the deployed ERC-20 contract and mint the tokens
await token.mintToSelf(amount);
const totalSupply = await token.totalSupply();

// printing to the console how many tokens now exist
console.log("πŸ’Έ There are now ", totalSupply.displayValue, "$UPCY in circulation");
} catch (error){
console.error("😿 Failed to print money", error);
}
})();

Running node scripts/6-print-money.js returns:

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
πŸ’Έ There are now 1000000.0 $UPCY in circulation

Below is the view from the thirdWeb dashboard

view from the thirdweb dashboard

The view from https://rinkeby.etherscan.io allows me to track the governance tokens in circulation.

token tracker on etherscan

πŸ’Έ Airdropping the governance token​

To fulfill the third user story where users will receive a token airdrop. I had to code a script that allowed randomly selected owners of the membership token to be able to receive the airdrop.

import sdk from './1-initialize-sdk.js';

// initializing the address of the ERC-1155 membership NFT contract and
// initializing the address of the ERC-20 token contract

const editionDrop = sdk.getEditionDrop("0xd844F24e6916C3cc569FaAE9FfD2aD9e9bCCe772");

const token = sdk.getToken("0xeEe746dcE397378567039d845740D9bf28Fb399D");

(async () => {
try {
// getting the addresses of DAO members holding the tokenId(0)
const walletAddresses = await editionDrop.history.getAllClaimerAddresses(0);

// if there are no wallet addresses in the history of all claim addresses then write it to the console
if (walletAddresses.length === 0){
console.log(
"No NFTs have been claimed yet, get some folks to claim some for free!",
);
process.exit(0);
}

// Looping through the array of addresses with a map() function
const airdropTargets = walletAddresses.map((address) => {
// picking a random # between 1000 and 10000
const randomAmount = Math.floor(Math.random() * (10000 - 1000 +1) + 1000);
console.log("✈ Going to airdrop", randomAmount, "tokens to", address);

// setting up the target for the drop, storing in the airdropTarget dictionary
const airdropTarget = {
toAddress: address,
amount: randomAmount,
};
return airdropTarget;
});

// calling transferBatch on each mapped address in the airdroptargets var
console.log("Starting airdrop...");
await token.transferBatch(airdropTargets);
console.log("πŸ–Ό Successfully airdropped tokens to all membership NFT holders!");
} catch (err){
console.log("😿 Failed to airdrop tokens.", err);
}
})();

Running node scripts/7-airdrop-token.js in the terminal returns:

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
✈ Going to airdrop 1313 tokens to 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
✈ Going to airdrop 9015 tokens to 0xC6cC274B9d1d1c43f56944Cb43A8C8465526cCFf
Starting airdrop...
πŸ–Ό Successfully airdropped tokens to all membership NFT holders!

After the airdrop, the thirdweb dashboard displays the remaining amount of governance tokens that I currently own.

govenance tokens after airdrop

On the testnet explorer, how many governance tokens are held by how many and by which wallet addresses.

etherscan after airdrop

Users with governance tokens, have voting privelidges.

Retrieve token holders and token balances​

In App.jsx ...

Updated the import statements as follows,

import { useAddress, useMetamask, useEditionDrop, useToken } from '@thirdweb-dev/react';
import { useState, useEffect, useMemo } from 'react';

Then..

... added the token address under editionDrop

const token = useToken("0xeEe746dcE397378567039d845740D9bf28Fb399D");

The ERC-1155 contract contains all the member wallet addresses, while the ERC-20 asset contains the information regarding the number of governance tokens owned by each member.

  // returns all members holding the NFT !hasClaimedNFT??? 
useEffect(() => {
if (!hasClaimedNFT){
return;
}
const getAllAddresses = async () => {
try {
// references all the members who hold the NFT with the tokenId of 0
const memberAddresses = await editionDrop.history.getAllClaimerAddresses(0);
setMemberAddresses(memberAddresses);
console.log("βš“ Members addresses", memberAddresses);
} catch (error){
console.error("πŸ™€ Failed to get member list", error);
}
};
getAllAddresses();
}, [hasClaimedNFT, editionDrop.history]);

Calling getAllClaimerAddresses(0), gets the addresses of all members, or holders of the NFT on the ERC-1155 collection contract.

 // grabbing the number of tokens each member holds with the useEffect hook
useEffect(() => {
if (!hasClaimedNFT){
return;
}

const getAllBalances = async () => {
try {
const amounts = await token.history.getAllHolderBalances();
setMemberTokenAmounts(amounts);
console.log("πŸ’° Amounts", amounts);
} catch (error) {
console.error("😿 Failed to get member balances", error);
}
};
getAllBalances();
}, [hasClaimedNFT, token.history]);

Calling getAllHolderBalances(), gets the history of the token holders of the ERC-20 contract's governance token.

// combining the memberAddresses and memberTokenAmounts into a single array
const memberList = useMemo(() => {
return memberAddresses.map((address) => {
// checking if the address is in the memberTokenAmounts array
// if so, returning the amount of tokens the user has
// otherwise, returning 0
const member = memberTokenAmounts?.find(({ holder }) => holder === address);

return {
address,
tokenAmount: member?.balance.displayValue || "0"
}
});
}, [memberAddresses, memberTokenAmounts]);

The memberList const references the combination of data into one array containing a "cross-referenced" list pairing the data from both contracts - memberAddresses(ERC-1155), and memberTokenAmounts(ERC-20).

Here is a view from the console output showing holder and their associated balance:

console output amounts

Review the code on Github at this commit

View Token holders on the member dashboard​

// if the user has minted the token - this will render 
if (hasClaimedNFT){
return (
<div className='member-page'>
<h1>UpCyDAO Member Page</h1>
<p>🎊 Congrats on being a member 🎊</p>
<div>
<div>
<h2>Member List</h2>
<table className='card'>
<thead>
<tr>
<th>Address</th>
<th>Token Amount</th>
</tr>
</thead>
<tbody>{memberList.map((member) => {
return (
<tr key ={member.address}>
<td>{shortenAddress(member.address)}</td>
<td>{member.tokenAmount}</td>
</tr>
);
})}
</tbody>
</table>
</div>
</div>
</div>
);
};

The code above shows that if the user has claimed a Membership NFT, then use the map() method to cycle through memberList's members and render the list organized by member address (key={member.address}) in one table cell juxtaposed and paired with the value of the member token amount in the next table cell.

Here is a peek at the rendered Member list on the DAO dashboard, viewable by members only!

rendered member page

Review the code on Github at this commit

βš– Building a treasury + governance​

Created a governance contract to offer users the chance to let their voices be heard.

import sdk from './1-initialize-sdk.js';

(async () => {
try {
const voteContractAddress = await sdk.deployer.deployVote({
// Giving the governance contract a name
name: "UpCy DAO Governance Contract",

// assigning the location property of the governance token (hint it's @ the ERC-20 contract address)
voting_token_address: "0xeEe746dcE397378567039d845740D9bf28Fb399D",
voting_delay_in_blocks: 0,
// 1 day = 6570 blocks
voting_period_in_blocks: 6570,
voting_quorum_fraction: 0,
proposal_token_threshold: 0,
});

console.log("Successfully deployed vote contract, address:",
voteContractAddress,
);
} catch (err){
console.error("Failed to deploy vote contract.", err);
}
})();
info

Click here for the above code commit on GitHub.

Notes on the code above

  • deployer.deployVote in the await, sets up the contract
  • voting_token_address: detects which ERC-20 token to accept based on the previously deployed ERC-20 token smart contract address. This way the current contract knows to accept $UPCY
  • voting_delay_in_blocks: creates a pause for users to go over the proposal they are voting on
  • voting_period_in_blocks: creates a time limit on which users have before running out of time to vote
  • voting_period_in_blocks: creates a minimum amount of votes needed for a proposal to pass
  • proposal_token_threshold: allows anyone regardless of the amount of tokens they hold, to create a proposal

Running node scripts/8-deploy-vote.js in the terminal, returns:

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Successfully deployed vote contract, address: 0x813244Ca4AC13550F7411A5Cd40C29AF6Cb35BA5

Here is the link to the deployed governance contract on the blockchain.

I appreciate the way that thirdWeb keeps the contracts organized in the dashboard as shown in the screenshot below.

contract dashboard

🏦 Setting up the treasury​

As in life, so is in DAO. He who Whoever owns the gold, makes the rules. The DAO needs funds for the community to vote on proposals that send funds to wallets.

For this DAO, I allocated 70% to the community treasury.

In the code below:

import sdk from "./1-initialize-sdk.js";

// the governance contract
const vote = sdk.getVote("0x813244Ca4AC13550F7411A5Cd40C29AF6Cb35BA5");

// the ERC-20 contract
const token = sdk.getToken("0xeEe746dcE397378567039d845740D9bf28Fb399D");

(async () => {
try {
// giving the treasury power to mint more if or as needed
await token.roles.grant("minter", vote.getAddress());

console.log(
"Successfully gave voting contract permission to act on the token contract"
);
} catch (error) {
console.error(
"Failed to grant voting contract permissions on the token contract",
error
);
process.exit(1);
}
try {
// grab the wallet's token balance because it's all owned by the minter currently
const ownedTokenBalance = await token.balanceOf(
process.env.WALLET_ADDRESS
);

// Reference 70% of the supply currently held
const ownedAmount = ownedTokenBalance.displayValue;
const percent70 = Number(ownedAmount) / 100 * 70;

// transfer 70% of the supply to the voting contract
await token.transfer(
vote.getAddress(),
percent70
);

console.log("Successfully transferred" + percent70 + " tokens to the voting contract");
} catch (err) {
console.error("Failed to transfer tokens to the voting contract", err);
}
})();
  • token.balanceOf references the total number of tokens in my token creator wallet
  • token.transfer transfers 70% to the voting contract

Running node scripts/9-setup-vote.js returns:

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Successfully gave voting contract permission to act on the token contract
Successfully transferred693689.5 tokens to the voting contract

transferred tokens to voting contract

πŸ“œ Creating the DAO’s first two proposals​

node scripts/10-create-vote-proposals.js

  • We’re creating a proposal that allows the treasury to mint 420,000 new token

  • We’re creating a proposal that transfer 6,900 token to our wallet from the treasury

Failed to create a second proposal. TransactionError: Contract transaction failed

After reviewing the code, I found a syntax error. Redeploying the script caused another error where the first proposal already existed. Commenting out the first proposal and redeploying did the trick...for now. Hopefully, that solves the problem and will not create more problems down the line.

✍️ Let users vote on proposals from the DAO dashboard​

Outcome:

Input:

  • reading all existing proposals from the contract to display for user to read
  • making sure the user delegates their token to vote
  • Display for proposals Conditions:
  • Giving members access to the vote for interacting with the contract
  • creating states for
    • proposals, `useEffect
    • the status of proposals being voted on,
    • and number of users who have voted
  • Members get to vote on proposals using three options: "For", "Against", and "Abstain" Edge Cases & Examples:
  • what if the user has already voted?
  • What if the proposals haven't finished loaded? How might that affect the voting process?

In the first instance, useEffect was used to

  • Call all proposals that exist on the governance contract with the vote.getAll()
  • To render the proposals later, I called setProposals with the proposal function passed through as argument.

In the second instance, useEffect was used to

  • Check whether the address referenced by the .hasVoted() function argument has voted on the first proposal. This check is supplied by the code, vote.hasVoted(proposals[0].proposalId, address).
    • To prevent the member from voting again, the setHasVoted(hasVoted) function and argument is called and if the member has voted previously, or has not previously voted is writted to the console, depending on the case.

user has not voted

Proposals on the contract in the console with log showing that the member has not voted yet.

proposals on the contract in the console

in App.jsx

  • updated import statement to include the useVote hook

import { useAddress, useMetamask, useEditionDrop, useToken, useVote } from '@thirdweb-dev/react';

member page proposals

voting page

click here to view commit on github

🀠 Removing admin powers and handling basic errors.​

Removing admin powers ensures better transparency in the DAO governance structure. This way there is no possible way of minting more tokens to grant them to myself.

import sdk from './1-initialize-sdk.js';

const token = sdk.getToken("0xeEe746dcE397378567039d845740D9bf28Fb399D");

(async () => {
try {
// initializing and logging the current roles
const allRoles = await token.roles.getAll();

console.log("Current Roles: ", allRoles);

// revoking the admin rights of creator on ERC-20 contract
await token.roles.setAll({ admin: [], minter: [] });
console.log(
"πŸ”’ Roles after revoking DAO creator",
await token.roles.getAll()
);
console.log("βœ” Successfully revoked admin rights from the ERC-20 contract" );
} catch (error){
console.error("Failed to revoke admin rights from the DAO treasury.", error);
};
})();

The code above removed all admin rights from each role, leaving the voting contract as the only entity allowed to mint.

running node scripts/11-revoke-roles.js ... returns ...

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Current Roles: {
admin: [ '0x1eD6025c5c6859337bFbe15Cd64b30FF88962605' ],
minter: [
'0x1eD6025c5c6859337bFbe15Cd64b30FF88962605',
'0x813244Ca4AC13550F7411A5Cd40C29AF6Cb35BA5'
],
transfer: [
'0x1eD6025c5c6859337bFbe15Cd64b30FF88962605',
'0x0000000000000000000000000000000000000000'
]
}
πŸ”’ Roles after revoking DAO creator {
admin: [],
minter: [],
transfer: [
'0x1eD6025c5c6859337bFbe15Cd64b30FF88962605',
'0x0000000000000000000000000000000000000000'
]
}
βœ” Successfully revoked admin rights from the ERC-20 contract

πŸ‘ Handling basic unsupported network error​

In App.jsx, added thirdweb's useNetwork hook and ChainId.

import { useAddress, useMetamask, useEditionDrop, useToken, useVote, useNetwork } from "@thirdweb-dev/react";
import { ChainId } from '@thirdweb-dev/sdk';

initialized the useNetwork hook under the useAddress hook.

const network = useNetwork();`

Under the mintNFT function, added the code shown below ...

    if (address && (network?.[0].data.chain.id !== ChainId.Rinkeby)){
return (
<div className="unsupported-network">
<h2>Please connect to Rinkeby</h2>
<p>
This dapp was designed to work on the Rinkeby network, please switch networks in your connected wallet.
</p>
</div>
);
}

Seeing the DAO's token on Uniswap​

https://app.uniswap.org/#/swap?chain=mainnet

Connected MetaMask wallet to get Rinkeby network to show up on the select menu.

Once connected to the wallet, the URL will update to the rinkeby chain.

https://app.uniswap.org/#/swap?chain=rinkeby

click on

  • select a token,
  • then select manage token lists
  • then select the tokens option
  • paste in the ERC-20 token address
  • click the import button

The $UPCY token is now available to trade on Uniswap add token to uniswap

note

what is stopPropagation()? what is delegating a governance token?

REFERENCES:

Β· 14 min read
Asia K

welcome to UpCyDAO

What is $%@# is a DAO?​

DAO stands for Decentralized Autonomous Organization. Let's break that down.

Decentralized means peer-to-peer across a network as opposed to centralized which means belonging to a single entity i.e. Facebook, Amazon, Netflix, and Google among others.

An autonomous organization in this context means that the governance of the organization is automated through smart contracts on the blockchain.

A DAO is a structure that offers businesses and communities the choice to govern themselves and manage their finances in a democratic manner.

The first DAO was named, "The DAO" and "was built using smart contracts, used an open-source framework, and focused on venture capitalism."

Unfortunately it lost millions of ETH, 3.6 to be exact, because of a bug that was exploited in the smart contract.

Other DAO's that followed were able to learn from this mistake and went on to become successful projects.

According to Alchemy.com, there are 8 main types of DAO's.

  1. Protocol DAOs : govern decentralised protocols including borrow/lending applications, decentralized exchanges, and other types of dapps (decentralized applications)
  2. Grant DAOs : facilitate nonprofit donations
  3. Philanthropy DAOs : organize around a shared purpose
  4. Social DAOs : bring together like minded individuals
  5. Collector DAOs : facilitate the purchase of NFT art and other collectables in which each member owns shares
  6. Venture DAOs : pool finances to invest in early stage web3 startups and off chain investments
  7. Media DAOs : produce content in which the members have access to the profits
  8. SubDAOs : manage specific functions including operations, partnerships, marketing, treasury and grants.

References: The 8 Most Important Types of DAOs You Need to Know

This is a very simplistic definition of a DAO but for the purpose of documenting this project, this is all we need to know for now.

The DAO that I will be documenting the creation of is:

UpCyDAO - a DAO for creators that upcycle sustainable and previously used materials for upcycling into new products and works of art. Members receive $UpCy when they interact with the community, providing advice, resources, and examples of how to upcycle non-biodegradeable materials that might otherwise end up in landfills.

User Stories

  • Users can connect their wallet
  • Users can mint a membership NFT
  • Users can receive a token airdrop
  • Users can vote on proposals

IDE : VSCode

We're building this project as a part of a Buildspace.so Hackathon. They've provided a basic react project to fork, which I've forked here

For familiarites sake, I've decided to build locally. The starter code was forked and cloned from the github repo here to my GitHub account here

Running npm start returned:

This happened because I skipped the step of entering npm install for adding node modules.

We've already installed our Ethereum wallet of choice, MetaMask, in the browser. This is neccessary in order to call functions on the smart contract on the blockchain because we will need a private key to authenticate the requests.

Since I'm working with test Eth, I've selected the Rinkeby Test Network vs. Ethereum Mainnet. As mentioned in the previous posts, I'll be requesting test Eth from Chainlink's faucet here.

Here's a screenshot of my MetaMask Wallet with some test Eth.

Connecting the Wallet to the DAO Dashboard

Unlike the previous project, we'll be using thirdweb’s front-end SDK to build our connect to wallet button. The steps required are:

  • in index.jsx
    • import thirdweb provider ... import { ChainId, ThirdwebProvider } from '@thirdweb-dev/react';
    • create var for the chainID that the dApp is working on const activeChainId = ChainId.Rinkeby;
    • wrap the app with the thridweb provider
    ReactDOM.render(
<React.StrictMode>
<ThirdwebProvider desiredChainId={activeChainId}>
<App />
</ThirdwebProvider>
</React.StrictMode>,
document.getElementById('root'),
);

Then, in App.jsx I added:

  • import { useAddress, useMetamask } from '@thirdweb-dev/react';

  • in the const App function, I added variables and thirdweb provided hooks:


    • const address = useAddress();
      const connectWithMetamask = useMetamask();
      console.log("πŸ’― Address:", address);

  • created if statement for the case where a user hasn't connected a wallet to the app, then return a "Connect Your Wallet" button.

  if (!address){
return (
<div className="landing">
<h1>Welcome to UpCyDAO</h1>
<button onClick={connectWithMetamask}className="btn-hero">Connect Your Wallet</button>
</div>
  • added return statement for the case where the user's wallet is connected.

Here is a screenshot of the wallet connecting to MetaMask.

[x] User Story 1. Users can connect their wallet

With User Story 1 complete, it's time to move on to the next - Users can mint a membership NFT that allows them to join the DAO. In this step, if users do not have a membership NFT already, then they will have the opportunity to mint the membership NFT and join the DAO. To do this, I need to write and deploy an NFT contract.

While Solidity is amazing, I'll be using thirdWeb to allow the creation and deployment of the contract with JavaScript.

First, I've created a .env file to store the private keys using the environmental variables below:

PRIVATE_KEY=YOUR_PRIVATE_KEY_HERE
WALLET_ADDRESS=YOUR_WALLET_ADDRESS
ALCHEMY_API_URL=YOUR_ALCHEMY_API_URL

The PRIVATE_KEY was exported from MetaMask. WALLET_ADDRESS is my MetaMask wallet address. Getting the ALCHEMY_API_URL came from creating a new app on Alchemy.com - making sure to select Ethereum and Rinksby Testnet.

Next, Initialize SDK

In scripts/1-initialize-sdk.js

  • added the following import statements
import { ThirdwebSDK } from "@thirdweb-dev/sdk";
import ethers from "ethers";
import { dotenv } from "dotenv";
  • called the .config() method on the dotenv object with dotenv.config
  • added checks to mae sure the .env file is configured correctly
  • initiated vars for provider, wallet and sdk
  • added async function, created var to await for address to be called through .getSigner() and getAddress() on the sdk object
  • exported the initialized thirdweb SDK to use on other scripts

On entering node scripts/1-initialize-sdk.js in the terminal to run the script, it returned:

SyntaxError: Named export 'ThirdWebSDK' not found. The requested module '@thirdweb-dev/sdk' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from '@thirdweb-dev/sdk';
const { ThirdWebSDK } = pkg;

This turned out to be a typo, because the correct line is import { ThirdwebSDK } from "@thirdweb-dev/sdk"; Lettercase matters.

On entering node scripts/1-initialize-sdk.js again, it returned:

SyntaxError: Named export 'dotenv' not found. The requested module 'dotenv' is a CommonJS module, which may not support all module.exports as named exports.
CommonJS modules can always be imported via the default export, for example using:

import pkg from 'dotenv';
const { dotenv } = pkg;

Then I remembered that I never installed the dotenv npm package.

That wasn't the issue because the same message outputted in the terminal. Turns out, it was because I typed a scope around the dotenv in the import statement.

With the errors corrected, typing node scripts/1-initialize-sdk.js into the terminal returned:

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605

This shows that the initialization worked.

🧨 Create an ERC-1155 collection​

An ERC-1155 is an NFT type where each NFT is the same as opposed to an ERC-721 where each token is unique.

In scripts/2-deploy-drop.js:

  • gave the collection a name, description, primary_sale_recipient, and image
  • created an image and saved the image UpCyDAO in the scripts/assets folder

Ran the script node scripts/2-deploy-drop.js in the terminal and got:

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Successfully deployed editionDrop contract, address: 0xd844F24e6916C3cc569FaAE9FfD2aD9e9bCCe772
Successfully deployed editionDrop metadata: {
name: 'UpCyDAO Membership',
description: 'A DAO for upcycling enthusiasts.',
image: 'https://gateway.ipfscdn.io/ipfs/QmbhN1evA8XNrbM3J67zheJVpsVrGq9EGWH8XjawPNHAPT/0',
seller_fee_basis_points: 0,
fee_recipient: '0x0000000000000000000000000000000000000000',
merkle: {},
symbol: ''
}

Here's a screenshot of the deployed contract:

The ERC-1155 contract was deployed to Rinkeby and can be viewed andinteracted with by clicking here - and for the contract creator, on the thirdweb dashboard.

note

thirdWeb automatically pins the collection image to ipfs (interplanetary file storage), a decentralized storage solution. The ipfs address cannot be viewed in Chrome's browser, but can be viewed in the Brave browser.

In the previous steps, I've created the ERC-1155 contract and added some basic metadata.

The next step is to deploy NFT metadata, by setting up our membership NFTs.


LaztMint the NFT to the contract​

The NFT below will give you access to UpCyDAO:

Up Cy

node scripts/3-config-nft.js

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Successfully created a new NFT in the drop!

import sdk from "./1-initialize-sdk.js";

Accessed the ERC-1155 editionDrop contract.

const editionDrop = sdk.getContract("INSERT_EDITION_DROP_ADDRESS");

`import sdk from "./1-initialize-sdk.js"; import { readFileSync } from "fs";

const editionDrop = sdk.getEditionDrop("0xd844F24e6916C3cc569FaAE9FfD2aD9e9bCCe772");

(async () => {
try {
await editionDrop.createBatch([
{
name: "UpCy Bot",
description: "this NFT will give you access to UpCyDAO",
image: readFileSync("scripts/assets/upcy.png"),
},
]);
console.log("Successfully created a new NFT in the drop!");
} catch (error) {
console.error("Failed to create the new NFT.", error);
}
})();

createBatch sets up the nft on the erc-1155 contract address

All members will receive:

A membership NFT

😼 Setup claim condition

In scripts/4-set-claim-condition.js:

startTime: new Date(), condition where the users is allowed to start minting an NFT : set to the current date and time.

maxQuantity: 50_000, the max # of membership NFTs that can be minted.

quantityLimitPerTransaction: 1, the number of transactions that a user can mint at a time has been set to 1 to allow the minting of one NFT membership at a time.

price: 0, means that the price is set at 0 (Free.99)

note

waitInSeconds: MaxUint256, " amount of time between transactions ... the maximum number that the blockchain allows." ???

info

await editionDrop.claimConditions.set("0", claimConditions); adjusts the condiditons of the deployed contract: @0xd844f24e6916c3cc569faae9ffd2ad9e9bcce772 on-chain.

0 is passed as the tokenId as it's the first NFT in our ERC-1155 contract. For a different set of NFTs in the collection i.e. for a tier of user, this can be set 1, 2 etc for each new set ...

Ran, node scripts/4-set-claim-condition.js And it returned:

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Successfully set claim condiiton.

results, available on the thirdWeb dashboard

  • deployed smart contract with specifc rules to follow including

    • date of mint/new membership tied to NFT
    • a maximum quantity of NFTs to be minted/printed into existence
    • the amounth of transactions allowed to be conducted at a time
    • the price to be paid for the NFT
    • the amount of time between transactions

    Originally Printed at asialakay-docs.

SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Successfully created a new NFT in the drop!
(base) @mbp upcy-dao %node scripts/4-set-claim-condition.js
SDK initialized by address: 0x1eD6025c5c6859337bFbe15Cd64b30FF88962605
Successfully set claim condiiton.

copy-pasted the bundle drop address printed out / minted on etherscan

Let users mint your NFTs. Users can mint a membership NFT.

in App.jsx

"if we detect that our user has a membership NFT, show them our "DAO Dashboard" screen where they can vote on proposals and see DAO related info.

if we detect that the user doesn't have our NFT, we'll give them a button to mint one."

added thirdWeb's useEditionDrop hook...

import { useAddress, useMetamask, useEditionDrop } from '@thirdweb-dev/react';

and { useState, useEffect } from 'react';

then initialized the editionDrop contract in a variable

const editionDrop = useEditionDrop("0xd844F24e6916C3cc569FaAE9FfD2aD9e9bCCe772");

Here is where I checked if the user has the membership NFT with the line,

const balance = await editionDrop.balanceOf(address, 0);

"0" is used in the callback because it is the tokenId of the membership NFT.

Here is the full code of App.jsx so far:

import { useAddress, useMetamask, useEditionDrop } from '@thirdweb-dev/react';
import { useState, useEffect } from 'react';

const App = () => {

const address = useAddress();
const connectWithMetamask = useMetamask();
console.log("πŸ’― Address:", address);

// Initialize our editionDrop contract
const editionDrop = useEditionDrop("0xd844F24e6916C3cc569FaAE9FfD2aD9e9bCCe772")
// State variable for us to know if user has our NFT.
const [hasClaimedNFT, setHasClaimedNFT] = useState(false);

useEffect(() => {
if (!address){
return;
}

const checkBalance = async() => {
try {
const balance = await editionDrop.balanceOf(address, 0);
if (balance.gt(0)){
setHasClaimedNFT(true);
console.log("🌚 This user has a membership NFT.");
} else {
setHasClaimedNFT(false);
console.log("This user doesn't have a membership NFT...");
}
} catch (error){
setHasClaimedNFT(false);
console.log("Failed to get balance", error);
}
};
checkBalance();
}, [address, editionDrop]);

if (!address){
return (
<div className="landing">
<h1>Welcome to UpCyDAO</h1>
<button onClick={connectWithMetamask} className="btn-hero">Connect Your Wallet</button>
</div>
);
}

return (
<div className="landing">
<h1>πŸ”— Your Wallet is Connected...</h1>
</div>
);
};

export default App;

Here's what it looks like in the console

no membership nft in the console

✨ Building the "Mint NFT" button

Here is the code added to render the "Mint NFT Button"

  return (
<div className="mint-nft">
<h1>Mint your free πŸͺDAO Membership NFT</h1>
<button
disabled={isClaiming}
onClick={mintNft}
>
{isClaiming ? "Minting..." : "Mint your nft (FREE)"}
</button>
</div>
);

Here is the NFT minting process:

Here is the UX/UI so far

user has NFT in the console

Here are the transaction details on the blockchain explorer

transaction on the blockchain

The DAO and NFTs have now been minted on the opensea testnet @ testnet.opensea.io

opensea testnet demo

πŸ›‘ Showing the DAO Dashboard only if user owns the NFT​

  // if the user has minted the NFT - this will render
if (hasClaimedNFT){
return (
<div className='member-page'>
<h1>UpCyDAO Member Page</h1>
<p>🎊 Congrats on being a member 🎊</p>
</div>
)
}

The code added to App.jsx states that if the dApp detects the user has a membership NFT, it will render a "DAO Dashboard" screen visible only to holders of the ERC-1155 NFT.

congrats member page

Otherwise, if the the dApp detects that the user does not have the membership NFT, I want to render a button for them to mint one.

To test the dApp for the case where the user does not own a membership NFT, I've created a new MetaMask Account following the instructions provided here.

0xC6cC274B9d1d1c43f56944Cb43A8C8465526cCFf

In MetaMask, I disconnected the main account in order to allow a fresh connect wallet button to render on screen. However, there were insufficient funds to mint 😿.

No problem, headed over to the chainlink faucet to add test eth to the new account. Making sure to select the new wallet to add it to.

grabbing testnet eth

All the parts are connected and tested!

dao ux test

new minted transaction

updated membership on opensea

Click here to view the NFT on Opensea!

At this point, I'm halfway there in completing the User stories.

User Stories

  • Users can connect their wallet [x]
  • Users can mint a membership NFT [x]

For a live demo, and to mint a membership token for free, go to https://upcy-dao.vercel.app/

The code for what has been built up to this point can be found by clicking here.

Next, will be completing the super exciting parts dealing with creating tokens and governance! In the next section, the following User stories will be completed.

  • Users can receive a token airdrop
  • Users can vote on proposals

REFERENCES & SOURCES