Skip to main content

Updating Testnet from Rinkeby to Goerli

ยท 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