From ee5f381bb91f63e16a4a0554bf2d58c6f7c984fb Mon Sep 17 00:00:00 2001 From: nftchance Date: Sun, 5 Mar 2023 22:06:56 -0600 Subject: [PATCH] fix: chain localization * sleep listeners until processes spin up * makes use of providers DRY * refactor primary chain to chain id * resolves #116 & #134 * integrates with #189 * implements frontend contract architecture for #181 --- api/api/settings/base.py | 55 ++-- api/utils/web3.py | 2 +- docker-compose.yml | 17 +- example.env | 14 +- .../src/components/Button/ConnectButton.js | 7 +- .../src/contexts/AuthenticationContext.js | 11 +- .../src/hooks/contracts/contractVersions.js | 33 +-- frontend/src/hooks/contracts/index.js | 14 +- frontend/src/hooks/contracts/useContracts.js | 252 ------------------ frontend/src/hooks/index.js | 19 +- 10 files changed, 90 insertions(+), 334 deletions(-) diff --git a/api/api/settings/base.py b/api/api/settings/base.py index 4669bc7..989451a 100644 --- a/api/api/settings/base.py +++ b/api/api/settings/base.py @@ -1,4 +1,5 @@ import os +import json from dotenv import load_dotenv from pathlib import Path @@ -147,35 +148,22 @@ REST_FRAMEWORK = { CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_ALL_ORIGINS = True -# Web3 settings +# Provider settings NODE_IP = os.getenv("NODE_IP", "0.0.0.0") - ALCHEMY_API_KEY = os.getenv("REACT_APP_ALCHEMY_API_KEY") - -DEFAULT_NETWORK = os.getenv("REACT_APP_DEFAULT_NETWORK", "LOCAL") +CHAIN_ID = int(os.getenv("REACT_APP_CHAIN_ID", 1337)) PROVIDERS = { - 'ETHEREUM': os.getenv("PROVIDER", f"https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}"), - 'POLYGON': os.getenv("POLYGON_PROVIDER", f"https://polygon-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}"), - 'LOCAL': os.getenv("LOCAL_PROVIDER", f"http://{NODE_IP}:8545/"), + 1: os.getenv("PROVIDER", f"https://eth-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}"), + 137: os.getenv("POLYGON_PROVIDER", f"https://polygon-mainnet.g.alchemy.com/v2/{ALCHEMY_API_KEY}"), + 1337: os.getenv("LOCAL_PROVIDER", f"http://{NODE_IP}:8545/"), } -PROVIDERS['DEFAULT'] = os.getenv("PROVIDER", PROVIDERS[DEFAULT_NETWORK]) -PROVIDER = PROVIDERS['ETHEREUM'] - -FACTORY_ADDRESSES = { - "ETHEREUM": "0x0", - "POLYGON": "0x72b03C649953CA95B920f60A5687e4d2DACf45c0", - "LOCAL": "0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512" -} -FACTORY_ADDRESS = os.getenv("FACTORY_ADDRESS", FACTORY_ADDRESSES[DEFAULT_NETWORK]) +CHAIN_PROVIDER = PROVIDERS[CHAIN_ID] +PROVIDER = PROVIDERS[1] +# Blockchain based authentication AUTHENTICATION_BACKENDS = ["siwe_auth.backend.SiweBackend"] -PINATA_API_KEY = os.getenv("API_PINATA_API_KEY") -PINATA_API_SECRET_KEY = os.getenv("API_PINATA_API_SECRET_KEY") -PINATA_INDEXER_URL = os.getenv( - "API_PINATA_INDEXER_URL", "https://badger.mypinata.cloud/ipfs/" -) - +# Onchain schema settings FACTORY_ABI = FACTORY_ABI FACTORY_ABI_FULL = FACTORY_ABI_FULL FACTORY_EVENTS = FACTORY_EVENTS @@ -186,7 +174,28 @@ ORGANIZATION_ABI_FULL = ORGANIZATION_ABI_FULL ORGANIZATION_EVENTS = ORGANIZATION_EVENTS ORGANIZATION_TOPIC_SIGNATURES = ORGANIZATION_TOPIC_SIGNATURES +# Listener settings LISTENER_INIT_BLOCK = os.getenv("LISTENER_INIT_BLOCK", 0) LISTENER_INIT_BLOCK_BUFFER = os.getenv("LISTENER_INIT_BLOCK_BUFFER", 20) LISTENER_CHAIN_ID = os.getenv("LISTENER_CHAIN_ID", 1337) -LISTENER_POLL_INTERVAL = os.getenv("POLL_INTERVAL", 5) \ No newline at end of file +LISTENER_POLL_INTERVAL = os.getenv("LISTENER_POLL_INTERVAL", 5) + +# Onchain reference data +BADGER_ADDRESSES = os.getenv("REACT_APP_BADGER_ADDRESSES", None) +if BADGER_ADDRESSES is None: + raise Exception("BADGER_ADDRESSES not set") + +BADGER_ADDRESSES = {int(k): v for k, v in json.loads(BADGER_ADDRESSES).items()} + +if CHAIN_ID not in BADGER_ADDRESSES: + raise Exception(f"Chain ID {CHAIN_ID} not found in BADGER_ADDRESSES") + +FACTORY_ADDRESS = BADGER_ADDRESSES[CHAIN_ID] + +# IPFS settings +PINATA_API_KEY = os.getenv("API_PINATA_API_KEY") +PINATA_API_SECRET_KEY = os.getenv("API_PINATA_API_SECRET_KEY") +PINATA_INDEXER_URL = os.getenv( + "API_PINATA_INDEXER_URL", "https://badger.mypinata.cloud/ipfs/" +) + diff --git a/api/utils/web3.py b/api/utils/web3.py index 82aa0f6..85da0da 100644 --- a/api/utils/web3.py +++ b/api/utils/web3.py @@ -2,7 +2,7 @@ from django.conf import settings from web3 import Web3 from ens import ENS -w3 = Web3(Web3.HTTPProvider(settings.PROVIDERS['DEFAULT'])) +w3 = Web3(Web3.HTTPProvider(settings.CHAIN_PROVIDER)) ns = ENS.fromWeb3(w3) def get_ens_name(address): diff --git a/docker-compose.yml b/docker-compose.yml index 3f57153..f43717b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -65,9 +65,10 @@ services: - badger_redis command: > sh -c " + sleep 10 && python manage.py migrate && - python manage.py runserver - " + python manage.py runserver 0.0.0.0:8000 + " # Run the onchain listener for Badger Factories in a separate worker # that is loading data into the same database as the API. badger_factory_listener: @@ -89,7 +90,11 @@ services: links: - badger_node:badger_node - badger_db:badger_db - command: python manage.py listen_for_factories + command: > + sh -c " + sleep 25 && + python manage.py listen_for_factories + " # Run the onchain listener for Badger Organizations in a separate worker # that is loading data into the same database as the API. badger_organization_listener: @@ -111,7 +116,11 @@ services: links: - badger_node:badger_node - badger_db:badger_db - command: python manage.py listen_for_organizations + command: > + sh -c " + sleep 25 && + python manage.py listen_for_organizations + " # Run the React frontend for Badger. This will run the frontend # on port 3000 and will be accessible at http://localhost:3000 # while consuming the state of all services apriori. diff --git a/example.env b/example.env index e3b2482..2e030f3 100644 --- a/example.env +++ b/example.env @@ -15,12 +15,12 @@ API_PINATA_API_SECRET_KEY="" # Must provide this to declare which chain the dapp will run on when running. # By using this variable to control the default network, the backend will automatically # configure the correct network for the frontend. -# - ETHEREUM -# - POLYGON -# - LOCAL +# - Ethereum: 1 +# - Polygon: 137 +# - Local: 1337 # ✅ WE PROVIDE A DEFAULT, BUT LOCAL SETTING. ✅ -REACT_APP_DEFAULT_NETWORK = "LOCAL" - +REACT_APP_CHAIN_ID=1337 + # ======================================================================================= # Must provide this in order to run the needed blockchain calls. # This is not just needed locally, the Badger system subsidizes the use of an RPC in preference @@ -52,9 +52,7 @@ PRIVATE_KEY="0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" # the hardhat deployer on localhost which means you will not need to change anything. If you are deploying # to a different network, you will need to change this. # ✅ WE PROVIDE A DEFAULT, BUT A RATE LIMITED ONE. ✅ -REACT_APP_BADGER_SINGLETON="0x5FbDB2315678afecb367f032d93F642f64180aa3" -REACT_APP_PRODUCTION_CHAIN="Localhost" -REACT_APP_BADGER_ADDRESSES={"Localhost":"0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"} +REACT_APP_BADGER_ADDRESSES={"137":"0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512","1337":"0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512"} # ======================================================================================= # Must provide an API key to CoinMarketCap to see the cost of the transactions when running Hardhat tests. diff --git a/frontend/src/components/Button/ConnectButton.js b/frontend/src/components/Button/ConnectButton.js index 685d1ce..f0e9453 100644 --- a/frontend/src/components/Button/ConnectButton.js +++ b/frontend/src/components/Button/ConnectButton.js @@ -4,8 +4,6 @@ import { useConnectModal } from "@rainbow-me/rainbowkit"; import { useAuthentication, useAuthenticationModal } from "@hooks"; -const PRIMARY_PRODUCTION_CHAIN = process.env.REACT_APP_PRODUCTION_CHAIN; - const ConnectButton = () => { const { switchNetwork } = useSwitchNetwork(); @@ -13,8 +11,7 @@ const ConnectButton = () => { const { openConnectModal } = useConnectModal(); - const { primaryChain, isAuthenticating, isWrongNetwork } = - useAuthentication(); + const { primaryChain, isAuthenticating, isWrongNetwork } = useAuthentication(); const { openAuthenticationModal } = useAuthenticationModal(); @@ -31,7 +28,7 @@ const ConnectButton = () => { disabled={!switchNetwork} onClick={switchNetwork.bind(null, primaryChain.id)} > - Switch to {PRIMARY_PRODUCTION_CHAIN} + Switch to {primaryChain.name} ); diff --git a/frontend/src/contexts/AuthenticationContext.js b/frontend/src/contexts/AuthenticationContext.js index c706fc0..e360d26 100644 --- a/frontend/src/contexts/AuthenticationContext.js +++ b/frontend/src/contexts/AuthenticationContext.js @@ -1,7 +1,7 @@ import { createContext, useEffect, useState } from "react"; import { useAccount, useNetwork, useSwitchNetwork } from "wagmi"; -const PRIMARY_PRODUCTION_CHAIN = process.env.REACT_APP_PRODUCTION_CHAIN +const CHAIN_ID = process.env.REACT_APP_CHAIN_ID const AuthenticationContext = createContext(); @@ -18,16 +18,17 @@ const AuthenticationContextProvider = ({ children }) => { const [authenticatedAddress, setAuthenticatedAddress] = useState(getAuthenticatedAddress()); const [isAuthenticating, setIsAuthenticating] = useState(false); - const primaryChain = chains.find(c => c.name === PRIMARY_PRODUCTION_CHAIN) + const primaryChain = chains.find(c => c.id === CHAIN_ID); const isWrongNetwork = isConnected && chain && primaryChain && chains && chain.id !== primaryChain.id; const isAuthenticated = isConnected && !isWrongNetwork && address === authenticatedAddress; + const isReadyToSwitch = !isError && switchNetwork && isWrongNetwork; + useEffect(() => { - /// Using isError here allows us to not prompt another switchNetwork if the user has already rejected the switch. - if (isWrongNetwork && switchNetwork && !isError) switchNetwork(primaryChain.id) - }, [isWrongNetwork, switchNetwork, isError, primaryChain]); + if (isReadyToSwitch) switchNetwork(primaryChain.id) + }, [isReadyToSwitch, primaryChain]); return ( { + acc[key] = BADGER_ADDRESSES_DICT[key][0]; + return acc; +}, {}); -// Gets the Badger implementation to clone based on the version. -// TODO: Add versioning -function getPrimaryImplementation() { - return PRIMARY_IMPLEMENTATION; -} - -// Putting the parse into a try catch block to account for missing env var breaking the app. -function getBadgerAddress(chainName) { +function getBadgerAddress(chainID) { try { - const BADGER_ADDRESSES = JSON.parse(process.env.REACT_APP_BADGER_ADDRESSES); - const address = BADGER_ADDRESSES[chainName] ? BADGER_ADDRESSES[chainName] : BADGER_ADDRESSES[PRIMARY_PRODUCTION_CHAIN]; - return address; - } - catch { + return BADGER_ADDRESSES[chainID]; + } catch { console.error(`Badger contract address not found in .env.`) return null; } } -// Gets the ABI for sash contracts. -// TODO: Add versioning function getBadgerOrganizationAbi() { try { const abi = require('@abis/BadgerOrganization.json'); return { abi: new ethers.utils.Interface(abi) } - } - catch (err) { + } catch (err) { console.error('Error importing BadgerOrganization:', err); return { error: err } } } -// Gets the abi and chain specific address for the Badger contract. function getBadgerAbi(chainName) { try { const abi = require('@abis/Badger.json'); @@ -44,15 +33,13 @@ function getBadgerAbi(chainName) { abi: new ethers.utils.Interface(abi), address: address } - } - catch (err) { + } catch (err) { console.error('Error importing Badger:', err); return { error: err } } } export { - getPrimaryImplementation, getBadgerAddress, getBadgerOrganizationAbi, getBadgerAbi diff --git a/frontend/src/hooks/contracts/index.js b/frontend/src/hooks/contracts/index.js index c20d234..6fad212 100644 --- a/frontend/src/hooks/contracts/index.js +++ b/frontend/src/hooks/contracts/index.js @@ -1,4 +1,14 @@ -export { getPrimaryImplementation, getBadgerAddress, getBadgerOrganizationAbi, getBadgerAbi } from './contractVersions'; export { - useCreateOrg, useEditOrg, useOrgForm, useSetBadge, useManageBadgeOwnership, useSetDelegates, useTransferOwnership, useRenounceOwnership + getBadgerAddress, + getBadgerOrganizationAbi, + getBadgerAbi +} from './contractVersions'; + +export { + useOrgForm, + useSetBadge, + useManageBadgeOwnership, + useSetDelegates, + useTransferOwnership, + useRenounceOwnership } from './useContracts'; \ No newline at end of file diff --git a/frontend/src/hooks/contracts/useContracts.js b/frontend/src/hooks/contracts/useContracts.js index 3d87362..99f4395 100644 --- a/frontend/src/hooks/contracts/useContracts.js +++ b/frontend/src/hooks/contracts/useContracts.js @@ -1,11 +1,9 @@ import { useMemo, useState } from "react"; -import { useNavigate } from "react-router-dom"; import { ethers } from "ethers"; import { usePrepareContractWrite, useContractWrite } from "wagmi" import { - getPrimaryImplementation, getBadgerOrganizationAbi, getBadgerAbi, useFees, @@ -15,24 +13,8 @@ import { useUser } from "@hooks"; -import { postOrgRequest } from "@utils"; - import { IPFS_GATEWAY_URL } from "@static"; -// TODO: Refactor the image field to be stored as a hash on the obj -// TODO: Do the same for the contract hash - -// IPFS has been the major pain point of this project so let's fix that. -// on success of transaction, should pin the image through the calling of a hook - -// sometimes though, we will know the hash when calling the transaction without using an image so having to pass the image in the hooks is wrong and was has been causing a lot of issues -// Basically the only thing that changes between all the code in this file is the args, the function name and whether or not it is using ipfs or not -// I see no reason we couldn't easily have a useOrg and useBadge hook that takes in the args and function name and then does the rest of the work - -// The real architecture I want is useCreateOrg, useEditOrg and then inside each of those have logic for openOrgTx -// The initial hooks were written in a way that required a lot of logic to be above the component any time it was used -// This is a step in the right direction, but I want to get to a point where the hooks are the only thing that needs to be imported - const getOrgFormTxArgs = ({ functionName, authenticatedAddress, name, symbol, imageHash, contractHash }) => { if (functionName === "setOrganizationURI") { return [IPFS_GATEWAY_URL + contractHash] @@ -49,68 +31,6 @@ const getOrgFormTxArgs = ({ functionName, authenticatedAddress, name, symbol, im } } -const useOrg = ({ obj, functionName }) => { - const fees = useFees(); - - const { authenticatedAddress, chain } = useUser(); - - const Badger = useMemo(() => { - if (obj.ethereum_address) return getBadgerOrganizationAbi(); - - return getBadgerAbi(chain.name); - }, [functionName, chain.name]); - - const isReady = Badger && fees && authenticatedAddress; - - const overrides = { gasPrice: fees?.gasPrice }; - - // TODO: Args - const args = [] - - const { config, isSuccess: isPrepared } = usePrepareContractWrite({ - enabled: isReady, - addressOrName: obj.ethereum_address || Badger.address, - contractInterface: Badger.abi, - functionName, - args, - overrides, - onError: (e) => { - const err = e?.error?.message || e?.data?.message || e - - throw new Error(err); - } - }) - - const { writeAsync } = useContractWrite(config); - - // We are separating the openOrg logic because every function called through this needs to have a post request following - const openOrgTx = async ({ - onError = (e) => { console.error(e) }, - onLoading = () => { }, - onSuccess = ({ tx, org, response }) => { } - }) => { - try { - onLoading() - - const tx = await writeAsync() - - const txReceipt = await tx.wait() - - if (txReceipt.status === 0) throw new Error("Error submitting transaction."); - - const response = await postOrgRequest(obj) - - if (!response.ok) throw new Error("Error submitting Organization request.") - - onSuccess({ tx, obj, response }) - } catch (e) { - onError(e); - } - } - - return { openOrgTx } -} - const useOrgForm = ({ obj, image }) => { const fees = useFees(); @@ -208,176 +128,6 @@ const useOrgForm = ({ obj, image }) => { return { openOrgFormTx, isPrepared, isLoading, isSuccess }; } -/** - * @dev Hook to trigger and handle a transaction that calls `createOrganization` on the Badger contract. - */ -const useCreateOrg = ({ enabled, name, symbol, imageHash, contractHash }) => { - const navigate = useNavigate(); - - const fees = useFees(); - - const { authenticatedAddress, chain } = useUser(); - - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - - const Badger = useMemo(() => getBadgerAbi(chain.name), [chain.name]); - - const orgCreatedTopic = Badger.abi.getEventTopic("OrganizationCreated"); - - const isReady = Boolean(enabled && fees && Badger); - - const args = getOrgFormTxArgs({ functionName: "createOrganization", authenticatedAddress, name, symbol, imageHash, contractHash }); - - const { config, isSuccess: isPrepared } = usePrepareContractWrite({ - addressOrName: Badger.address, - contractInterface: Badger.abi, - functionName: "createOrganization", - args: args, - enabled: isReady, - overrides: { gasPrice: fees?.gasPrice }, - onError(e) { - const err = e?.error?.message || e?.data?.message || e - - throw new Error(err); - } - }) - - const { writeAsync } = useContractWrite(config); - - const openCreateOrgTx = async ({ - onError = (e) => { console.error(e) }, - onLoading = () => { }, - onSuccess = ({ tx, org, response }) => { } - }) => { - try { - setIsLoading(true) && onLoading(); - - const tx = await writeAsync(); - - const [txReceipt, imageHash, metadataHash] = await Promise.all([ - tx.wait(), - null, - null - // pinImage(objImage), - // pinMetadata(objMetadata), - ]); - - if (txReceipt.status === 0) throw new Error("Error submitting Organization transaction."); - - const orgCreatedEvent = txReceipt.logs.find((log) => log.topics[0] === orgCreatedTopic); - const orgEvent = Badger.abi.decodeEventLog("OrganizationCreated", orgCreatedEvent.data, orgCreatedEvent.topics); - const contractAddress = orgEvent.organization; - - if (!contractAddress) throw new Error("Could not find event emission from Organization creation."); - - const org = { - ...org, - ethereum_address: contractAddress, - contract_uri_hash: metadataHash, - image_hash: imageHash, - chain: chain.name, - owner: authenticatedAddress, // don't do it like this - is_active: true - } - - const response = await postOrgRequest(org); - - if (!response.ok) throw new Error("Error creating Organization in database."); - - navigate(`/dashboard/organization/${response.id}/`); - - setIsSuccess(true) && onSuccess({ tx, org, response }); - } catch (e) { - console.error(e); - - setIsSuccess(false) && onError(e); - } - } - - return { openCreateOrgTx, isPrepared, isLoading, isSuccess }; -} - -// Edit the contract URI of an organization and update the image, description, and name. -const useEditOrg = ({ enabled, contractAddress, contractUriHash }) => { - const navigate = useNavigate(); - - const fees = useFees(); - - const [isLoading, setIsLoading] = useState(false); - const [isSuccess, setIsSuccess] = useState(false); - - const BadgerOrganization = useMemo(() => getBadgerOrganizationAbi(), []); - - const isReady = Boolean(enabled && fees && BadgerOrganization); - - const args = [ - IPFS_GATEWAY_URL + contractUriHash - ] - - const { config, isSuccess: isPrepared } = usePrepareContractWrite({ - addressOrName: contractAddress, - contractInterface: BadgerOrganization.abi, - functionName: "setOrganizationURI", - args: args, - enabled: isReady, - overrides: { gasPrice: fees?.gasPrice }, - onError(e) { - const err = e?.error?.message || e?.data?.message || e - - throw new Error(err); - } - }) - - const { writeAsync } = useContractWrite(config); - - const openCreateOrgTx = async ({ - onError = (e) => { console.error(e) }, - onLoading = () => { }, - onSuccess = ({ tx, org, response }) => { } - }) => { - try { - setIsLoading(true) && onLoading(); - - const tx = await writeAsync(); - - const [txReceipt, imageHash, metadataHash] = await Promise.all([ - tx.wait(), - null, - null - // pinImage(objImage), - // pinMetadata({ - // name: obj.name, - // description: obj.description, - // imageHash: objImage, - // }) - ]) - - if (txReceipt.status === 0) throw new Error("Error submitting Organization transaction."); - - const org = { - ...org, - contract_uri_hash: metadataHash, - image_hash: imageHash, - } - - const response = await postOrgRequest(org); - - if (!response.ok) throw new Error("Error creating Organization in database."); - - navigate(`/dashboard/organization/${response.id}/`); - - setIsSuccess(true) && onSuccess({ tx, org, response }); - } catch (e) { - console.error(e); - - setIsSuccess(false) && onError(e); - } - } - - return { openCreateOrgTx, isPrepared, isLoading, isSuccess }; -} - // Creates a badge from a cloned sash contract. const useSetBadge = (isTxReady, contractAddress, tokenUri, badge) => { const BadgerOrganization = useMemo(() => getBadgerOrganizationAbi(), []); @@ -594,8 +344,6 @@ const useRenounceOwnership = (isTxReady, orgAddress) => { } export { - useCreateOrg, - useEditOrg, useOrgForm, useSetBadge, useManageBadgeOwnership, diff --git a/frontend/src/hooks/index.js b/frontend/src/hooks/index.js index a5d51c8..7fa2581 100644 --- a/frontend/src/hooks/index.js +++ b/frontend/src/hooks/index.js @@ -1,15 +1,12 @@ export { - getPrimaryImplementation, - getBadgerAddress, - getBadgerOrganizationAbi, - getBadgerAbi, - useCreateOrg, - useEditOrg, - useSetBadge, - useOrgForm, - useManageBadgeOwnership, - useSetDelegates, - useTransferOwnership, + getBadgerAddress, + getBadgerOrganizationAbi, + getBadgerAbi, + useSetBadge, + useOrgForm, + useManageBadgeOwnership, + useSetDelegates, + useTransferOwnership, useRenounceOwnership } from './contracts';