Skip to main content

2 posts tagged with "sdk"

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

Β· 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: