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
This commit is contained in:
nftchance 2023-03-05 22:06:56 -06:00
parent 06d805ef66
commit ee5f381bb9
10 changed files with 90 additions and 334 deletions

View File

@ -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)
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/"
)

View File

@ -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):

View File

@ -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.

View File

@ -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.

View File

@ -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}
</button>
);

View File

@ -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 (
<AuthenticationContext.Provider value={{

View File

@ -1,41 +1,30 @@
import { ethers } from "ethers";
const PRIMARY_IMPLEMENTATION = process.env.REACT_APP_BADGER_SINGLETON;
const PRIMARY_PRODUCTION_CHAIN = process.env.REACT_APP_PRODUCTION_CHAIN;
const BADGER_ADDRESSES_DICT = JSON.parse(process.env.REACT_APP_BADGER_ADDRESSES);
const BADGER_ADDRESSES = Object.keys(BADGER_ADDRESSES_DICT).reduce((acc, key) => {
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

View File

@ -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';

View File

@ -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,

View File

@ -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';