Skip to main content

· 3 min read

MVP Development Roadmap: Diabetes Ease Pro

Phase 1: Project Initiation and Planning (Duration: 2 months)

  • Objective: Define the scope and goals for the MVP development of Diabetes Ease Pro, an innovative AI-powered diabetes management system.
  • Tasks:
    • Assemble a multidisciplinary team, including AI/ML experts, developers, designers, and healthcare professionals.
    • Create a comprehensive project plan with clear milestones and timelines.
    • Conduct market research to understand user needs and preferences, leveraging AI for data analysis.
    • Define the MVP feature set with AI-driven functionalities for personalized diabetes management.
    • Identify core features and functionality.
    • Determine target user demographics.
    • Analyze competitors in the diabetes management app market.
    • Identify market trends and user needs.
    • Assemble a development team.
    • Appoint project manager and roles.
    • Choose the technology stack for app development.
    • Set up development environments.

Phase 2: AI-Driven Diabetes Management (Duration: 4 months)

  • Objective: Develop the core AI and ML technologies for accurate glucose monitoring and personalized recommendations.
  • Tasks:
    • Design AI algorithms for real-time glucose level prediction and trend analysis.
    • Implement machine learning models for personalized dietary guidance and medication reminders.
    • Utilize AI for predictive analytics to optimize insulin dosages and improve glycemic control.
    • Focus on data security protocols to protect user health data.

Phase 3: Integration and Data Monetization (Duration: 3 months)

  • Objective: Integrate Diabetes Ease Pro with continuous glucose monitors (iCGMs) and explore data monetization opportunities while ensuring user data privacy.
  • Tasks:
    • Integrate with leading iCGM devices to provide seamless glucose data acquisition.
    • Develop a secure data monetization model, allowing users to share their anonymized data and receive incentives.
    • Implement robust encryption and authentication mechanisms to protect user data.
    • Establish user consent and transparency regarding data sharing and monetization.

Phase 4: Automated Insulin Delivery Integration (Duration: 5 months)

  • Objective: Transform Diabetes Ease Pro into an Automated Insulin Delivery (AID) system by integrating with insulin pumps and other diabetes partners.
  • Tasks:
    • Collaborate with insulin pump manufacturers to ensure seamless communication and automated insulin adjustments.
    • Implement AI-driven algorithms to optimize insulin delivery based on real-time glucose data.
    • Seek regulatory approvals for the AID system, adhering to healthcare standards.
    • Showcase regulatory compliance and approvals prominently.

Phase 5: Continuous Improvement and User Feedback (Duration: Ongoing)

  • Objective: Commit to continuous improvement by gathering user feedback and staying at the forefront of AI and diabetes management technology.
  • Tasks:
    • Establish a dedicated customer support team for user inquiries and assistance.
    • Develop educational resources and training materials for users.
    • Regularly update the system based on user feedback and emerging AI/ML advancements.
    • Plan for future phases of development and expansion as the diabetes management landscape evolves.

· 3 min read

Diabetes Ease Pro Integrated Project Plan and MVP Roadmap

Introduction

Welcome to the Diabetes Ease Pro project, a groundbreaking venture aimed at transforming diabetes management through innovation and technology. This integrated plan outlines our comprehensive strategy, spanning from project initiation to continuous improvement, demonstrating our commitment to improving the lives of individuals with diabetes.

Project Overview

Diabetes Ease Pro is an AI-powered diabetes management system that leverages the capabilities of Artificial Intelligence (AI) and Machine Learning (ML) to provide personalized solutions for diabetes patients. Our project plan aligns seamlessly with our MVP roadmap to create a cohesive approach to development and implementation.

Key Objectives

  1. Develop Innovative AI Solutions: Utilize AI and ML technologies to predict glucose levels, offer dietary guidance, and optimize insulin dosages.

  2. Ensure Data Security: Implement robust encryption and authentication mechanisms to safeguard user health data.

  3. User Data Monetization: Explore data monetization opportunities while respecting user privacy and consent.

  4. Automated Insulin Delivery Integration: Collaborate with insulin pump manufacturers to create an Automated Insulin Delivery (AID) system.

  5. Continuous Improvement: Commit to continuous development, user support, and staying at the forefront of AI and diabetes management technology.

Phases of Development

PhaseObjectiveDurationKey Tasks
Phase 1: Project Initiation and PlanningDefine project scope and goals for the MVP development of Diabetes Ease Pro.2 months- Assemble a multidisciplinary team. - Create a comprehensive project plan. - Conduct AI-driven market research. - Define MVP feature set with AI functionalities.
Phase 2: AI-Driven Diabetes ManagementDevelop core AI and ML technologies for accurate glucose monitoring and personalized recommendations.4 months- Design AI algorithms for glucose prediction and analysis. - Implement ML models for dietary guidance. - Utilize AI for predictive analytics and data security.
Phase 3: Integration and Data MonetizationIntegrate with continuous glucose monitors (iCGMs) and explore data monetization while ensuring user data privacy.3 months- Integrate with iCGM devices. - Develop secure data monetization model. - Implement encryption and user consent mechanisms.
Phase 4: Automated Insulin Delivery IntegrationTransform Diabetes Ease Pro into an AID system by integrating with insulin pumps and diabetes partners.5 months- Collaborate with insulin pump manufacturers. - Implement AI algorithms for insulin optimization. - Seek regulatory approvals.
Phase 5: Continuous Improvement and User FeedbackCommit to continuous development, user support, and staying at the forefront of AI and diabetes management.Ongoing- Establish customer support. - Develop user resources. - Regularly update the system based on feedback.

Conclusion

Our integrated project plan and MVP roadmap showcase our dedication to revolutionizing diabetes management through AI and ML innovation. By ensuring data security, user privacy, and data monetization, we aim to create a user-centric solution that offers both advanced functionality and peace of mind. We invite you to join us on this transformative journey towards simplifying and enhancing diabetes management for individuals worldwide.

· 2 min read

Continued from Part 1 In preparation for the Techstars Startup Weekend #Boston

Lean Canvas: Diabetes Ease Pro

Problem

  • Customer Segments: Individuals with diabetes (Type 1 and Type 2) seeking advanced and AI-driven diabetes management solutions.
  • Existing Alternatives: Traditional glucose monitoring devices and diabetes management apps.
  • High-Level Concept: Providing a comprehensive AI-powered diabetes management platform.

Solution

  • Unique Value Proposition: An AI-driven system for real-time glucose monitoring, personalized recommendations, and automated insulin delivery.
  • Unfair Advantage: Our team's expertise in AI/ML and healthcare technology.

Channels

  • Early Adoptions: Partner with healthcare providers and clinics for initial user adoption.
  • Content Marketing: Create educational content on AI in diabetes management.
  • PR: Share success stories of users benefiting from Diabetes Ease Pro.
  • Data Monetization: Implement a secure data monetization model, allowing users to share data while maintaining privacy.

Key Metrics

  • Monthly Active Users: Measure user engagement and growth.
  • Data Monetization Revenue: Track revenue generated from user data sharing.
  • Regulatory Approvals: Highlight compliance with healthcare standards and regulatory approvals.

Unfair Advantage

  • AI Expertise: Our team consists of AI/ML experts, giving us a unique advantage in developing innovative diabetes management solutions.

Integration with iCGMs and AID

  • Objective: Integration with iCGMs and insulin pumps to become an Automated Insulin Delivery (AID) system.
  • Tasks: Collaborate with diabetes partners for seamless integration and regulatory compliance.

Comparative Analysis of Glucose Information Accuracy

  • Diabetes Ease Pro leverages AI for real-time glucose predictions and personalized recommendations(grocery list, meal plan, diabetes community support), ensuring accuracy and reliability in diabetes management.

· 3 min read

In my journey as a developer, designer, AI/ML enthusiast, and music producer, I've always been driven by a desire to make a meaningful impact on the world. And one issue that hits close to home is diabetes management. I've personally experienced the challenges of fluctuating blood sugar levels, and I've witnessed friends and loved ones struggle with this condition. That's why I'm passionate about leveraging my skills and expertise to transform diabetes management into something easy, fun, and cost-effective.

Analyzing the Landscape

To embark on this mission, it's essential to analyze the competitive landscape and market trends in diabetes management and Continuous Glucose Monitoring (CGM) technology. Here's a comprehensive look at the current state of this dynamic field:

Competitive Landscape

The diabetes management and CGM (Continuous Glucose Monitoring) technology market are highly competitive, with numerous players vying for market share [1]. This competition fuels innovation, pushing companies to develop more accurate and user-friendly CGM devices.

Digital diabetes management is another aspect contributing to this competition. Companies are constantly innovating to provide solutions that empower individuals to manage their condition effectively [4].

Market Growth

The CGM technology market is poised for significant growth, with a projected Compound Annual Growth Rate (CAGR) of 9.5% during the forecast period [3]. This growth underscores the increasing demand for real-time glucose monitoring solutions.

In the United States, the Diabetes Devices Market is expected to reach a staggering USD 23.68 billion in 2023, with a CAGR of 6.27% [6]. This growth potential underscores the importance of the diabetes management sector.

Innovation and Technology

Innovation is at the heart of this market. Companies are continuously working to improve the accuracy and usability of CGM devices, making them more accessible and effective [1].

The integration of digital solutions, such as mobile apps and data analytics, is a prominent trend in diabetes management. These tools empower patients to monitor and manage their condition proactively, enhancing their quality of life [4].

Patient-Centric Care

A significant shift is occurring towards patient-centric care. The focus is on improving the quality of life for individuals with diabetes by providing them with better tools and support for managing their condition. This approach aligns with my personal mission to make diabetes management easy and enjoyable [4].

Regulatory and Reimbursement Factors

Regulatory approvals and reimbursement policies play a pivotal role in shaping the industry. They influence the competitive landscape and the adoption of diabetes management and CGM technologies [3].

In conclusion, the diabetes management and CGM technology market are marked by fierce competition, substantial growth prospects, ongoing innovation, and a patient-centered approach to care. As someone who has experienced the impact of diabetes firsthand, I'm more motivated than ever to contribute to this transformative field. With my expertise in technology and a commitment to making a difference, I'm excited to be part of this journey towards revolutionizing diabetes management and improving the lives of those affected by this condition. Together, we can turn the tide in the battle against diabetes.

References

  • 1: "Continuous Glucose Monitoring Devices Market Size, ..." on LinkedIn, Mar 9, 2023.
  • 3: "Continuous Glucose Monitoring (CGM) Market Size & Share ..." on BCC Research.
  • 4: "Digital Diabetes Management Market Size Report, 2022 ..." on Grand View Research.
  • 6: "United States Diabetes Devices Market Size & Share ..." on Mordor Intelligence.

Please note that while the article references these sources, it also incorporates personal experiences and insights from the author, making it a blend of researched information and a personal perspective on the topic of diabetes management and CGM technology.

· 3 min read
Asia K

In this tutorial, we'll guide you through the process of writing tests for the "Produce PLU Code Search" React app using React Testing Library and Jest. This app allows users to search for and display the PLU code for grocery produce items. It features autosuggest and accessibility features.

Prerequisites

Before you begin, make sure you have the following prerequisites:

  1. Node.js: You need to have Node.js installed on your computer. You can download it from nodejs.org.

  2. The "Produce PLU Code Search" React App: Ensure you have the "Produce PLU Code Search" React app set up and ready for testing.

Step 1: Install Dependencies

In your "Produce PLU Code Search" React project, install the necessary testing libraries if you haven't already. Open your terminal and run the following commands:

npm install --save @testing-library/react @testing-library/jest-dom

Step 2: Create the Test File

Create a new test file for the "Produce PLU Code Search" component. In your project directory, create a file named App.test.js. This file will contain your component's tests.

Step 3: Writing the Tests

Inside the App.test.js file, you'll write the tests for your "Produce PLU Code Search" app. Here are the tests we'll create:

Test 1: Rendering the Main Title

import React from 'react';
import { render, screen } from '@testing-library/react';
import App from './App'; // Import the "Produce PLU Code Search" component

test('renders the main title', () => {
render(<App />);
const titleElement = screen.getByText('Produce PLU Code Search');
expect(titleElement).toBeInTheDocument();
});

Test 2: Searching for a Produce Item

test('allows searching for a produce item and displays PLU code', () => {
render(<App />);

// Type a search term
const searchInput = screen.getByPlaceholderText('Enter produce item');
fireEvent.change(searchInput, { target: { value: 'Apple' } });

// Click the search button
const searchButton = screen.getByLabelText('Search');
fireEvent.click(searchButton);

// Verify that the PLU code is displayed
const pluCodeElement = screen.getByText('PLU Code: 123');
expect(pluCodeElement).toBeInTheDocument();
});

Test 3: Displaying Suggestions

test('displays suggestions when typing in the search input', () => {
render(<App />);

// Type a search term
const searchInput = screen.getByPlaceholderText('Enter produce item');
fireEvent.change(searchInput, { target: { value: 'Ap' } });

// Verify that suggestions are displayed
const suggestionElement = screen.getByText('Apple');
expect(suggestionElement).toBeInTheDocument();
});

Test 4: Handling Suggestion Click

test('handles suggestion click and updates the search input and PLU code', () => {
render(<App />);

// Type a search term
const searchInput = screen.getByPlaceholderText('Enter produce item');
fireEvent.change(searchInput, { target: { value: 'Banana' } });

// Click a suggestion
const suggestionElement = screen.getByText('Banana');
fireEvent.click(suggestionElement);

// Verify that the search input and PLU code are updated
expect(searchInput).toHaveValue('Banana');
const pluCodeElement = screen.getByText('PLU Code: 4011');
expect(pluCodeElement).toBeInTheDocument();
});

Step 4: Running the Tests

To run your tests, use the following command in your project directory:

npm test

This command will execute the test suite defined in App.test.js. If all tests pass, you'll see the test results in your terminal.

Congratulations! You've successfully written tests for the "Produce PLU Code Search" React app using React Testing Library and Jest. These tests help ensure that your app behaves as expected, making it easier to catch and fix issues during development.

· 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