From c1a8ac72934e6390b0f05b77d1543f5fc7dafd21 Mon Sep 17 00:00:00 2001 From: audsssy Date: Fri, 16 Dec 2022 11:52:20 -0500 Subject: [PATCH 01/20] add manager --- constants/addresses.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/constants/addresses.ts b/constants/addresses.ts index 9574e929..ca50e691 100644 --- a/constants/addresses.ts +++ b/constants/addresses.ts @@ -102,7 +102,7 @@ export const addresses: { [key: number]: any } = crowdsale2: '0xB682e773768e68C02B8b3892CF32eA090600b4b4', // crowdsale2: '0x2350C968C7B323Ad255E3942fcb9d578638792EC', redemption: '0x2b8f116e4D9E73A3A9E7CAF1655B9FC01588Db8d', - projectManagement: '0x9f0ad778385a2c688533958c6ada56f201ffc246', + manager: '0x01100BcA3ca6265F367Bf028C224DA5200eFE0d7', }, blockExplorer: 'https://goerli.etherscan.io/', }, From 255e40ad13e992b1f0566551f49a187be957efb5 Mon Sep 17 00:00:00 2001 From: audsssy Date: Fri, 16 Dec 2022 16:30:01 -0500 Subject: [PATCH 02/20] add projectmanager contract --- abi/KaliProjectManager.json | 307 ++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 abi/KaliProjectManager.json diff --git a/abi/KaliProjectManager.json b/abi/KaliProjectManager.json new file mode 100644 index 00000000..0fb82366 --- /dev/null +++ b/abi/KaliProjectManager.json @@ -0,0 +1,307 @@ +[ + { + "inputs": [ + { + "internalType": "bytes[]", + "name": "extensionData", + "type": "bytes[]" + } + ], + "name": "callExtension", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "ExpiredProject", + "type": "error" + }, + { + "inputs": [], + "name": "InactiveProject", + "type": "error" + }, + { + "inputs": [], + "name": "InsufficientBudget", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidEthReward", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidInput", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidProject", + "type": "error" + }, + { + "inputs": [], + "name": "NotAuthorized", + "type": "error" + }, + { + "inputs": [], + "name": "Reentrancy", + "type": "error" + }, + { + "inputs": [], + "name": "SetupFailed", + "type": "error" + }, + { + "inputs": [], + "name": "UpdateFailed", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "projectId", + "type": "uint256" + }, + { + "indexed": true, + "internalType": "address", + "name": "contributor", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "name": "ExtensionCalled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "projectId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "enum Status", + "name": "status", + "type": "uint8" + }, + { + "internalType": "address", + "name": "manager", + "type": "address" + }, + { + "internalType": "enum Reward", + "name": "reward", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "budget", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "distributed", + "type": "uint256" + }, + { + "internalType": "uint40", + "name": "deadline", + "type": "uint40" + }, + { + "internalType": "string", + "name": "docs", + "type": "string" + } + ], + "indexed": false, + "internalType": "struct Project", + "name": "project", + "type": "tuple" + } + ], + "name": "ExtensionSet", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint256", + "name": "projectId", + "type": "uint256" + }, + { + "components": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "enum Status", + "name": "status", + "type": "uint8" + }, + { + "internalType": "address", + "name": "manager", + "type": "address" + }, + { + "internalType": "enum Reward", + "name": "reward", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "budget", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "distributed", + "type": "uint256" + }, + { + "internalType": "uint40", + "name": "deadline", + "type": "uint40" + }, + { + "internalType": "string", + "name": "docs", + "type": "string" + } + ], + "indexed": false, + "internalType": "struct Project", + "name": "project", + "type": "tuple" + } + ], + "name": "ProjectUpdated", + "type": "event" + }, + { + "inputs": [ + { + "internalType": "bytes", + "name": "extensionData", + "type": "bytes" + } + ], + "name": "setExtension", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [], + "name": "projectId", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "name": "projects", + "outputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "enum Status", + "name": "status", + "type": "uint8" + }, + { + "internalType": "address", + "name": "manager", + "type": "address" + }, + { + "internalType": "enum Reward", + "name": "reward", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "budget", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "distributed", + "type": "uint256" + }, + { + "internalType": "uint40", + "name": "deadline", + "type": "uint40" + }, + { + "internalType": "string", + "name": "docs", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + } +] From 81a104cf2497e973dabbd02749f80fa8de56f6a0 Mon Sep 17 00:00:00 2001 From: audsssy Date: Fri, 16 Dec 2022 16:30:24 -0500 Subject: [PATCH 03/20] add set project page --- .../newproposal/apps/SetProject.tsx | 277 ++++++++++++++++++ .../dao-dashboard/newproposal/apps/index.tsx | 17 +- .../dao-dashboard/newproposal/index.tsx | 10 + 3 files changed, 301 insertions(+), 3 deletions(-) create mode 100644 components/dao-dashboard/newproposal/apps/SetProject.tsx diff --git a/components/dao-dashboard/newproposal/apps/SetProject.tsx b/components/dao-dashboard/newproposal/apps/SetProject.tsx new file mode 100644 index 00000000..f7fcaa06 --- /dev/null +++ b/components/dao-dashboard/newproposal/apps/SetProject.tsx @@ -0,0 +1,277 @@ +import React, { useState, useEffect } from 'react' +import { useRouter } from 'next/router' +import { ethers } from 'ethers' +import { useContract, useSigner, useContractRead, erc20ABI } from 'wagmi' +import { + Stack, + Input, + Box, + Text, + Button, + FieldSet, + FileInput, + Textarea, + IconClose, + Checkbox, + IconUserSolid, + Tag, +} from '@kalidao/reality' +import FileUploader from '@components/tools/FileUpload' +import KALIDAO_ABI from '@abi/KaliDAO.json' +import MANAGER_ABI from '@abi/KaliProjectManager.json' +import { addresses } from '@constants/addresses' +import { Warning } from '@design/elements' +import Back from '@design/proposal/Back' +import { createProposal } from '../utils' +import { ProposalProps } from '../utils/types' +// import { createDataRoomDetails } from './createDataRoomDetails' +import { fetchEnsAddress } from '@utils/fetchEnsAddress' +import { Select } from '@design/Select' +import { DateInput } from '@design/DateInput' +import { getProvider } from '@utils/getProvider' +import { AddressZero } from '@ethersproject/constants' + +export default function SetProject({ setProposal, title, content }: ProposalProps) { + const router = useRouter() + const daoAddress = router.query.dao as string + const chainId = Number(router.query.chainId) + const provider = getProvider(chainId) + + const { data: signer } = useSigner() + const dataRoomAddress = addresses[chainId]['extensions']['dataRoom'] + + const { data: kalidaoToken } = useContractRead({ + addressOrName: daoAddress, + contractInterface: KALIDAO_ABI, + functionName: 'symbol', + chainId: Number(chainId), + }) + + const kalidao = useContract({ + addressOrName: daoAddress, + contractInterface: KALIDAO_ABI, + signerOrProvider: signer, + }) + + // form + const [record, setRecords] = useState() + const [warning, setWarning] = useState() + const [isEnabled, setIsEnabled] = useState(false) + const [reward, setReward] = useState('select') + const [customToken, setCustomToken] = useState('') + const [customTokenSymbol, setCustomTokenSymbol] = useState('') + const [customTokenDecimals, setCustomTokenDecimals] = useState(0) + const [customTokenDaoBalance, setCustomTokenDaoBalance] = useState(0.0) + const [budget, setBudget] = useState(0) + const [maxBudget, setMaxBudget] = useState(0) + const [deadline, setDeadline] = useState() + const [status, setStatus] = useState() + const [name, setName] = useState('') + const [tags, setTags] = useState([]) + const [users, setUsers] = useState([]) + + const handleTags = (e: React.ChangeEvent) => { + let raw = e.target.value + let _tags: Array = [] + _tags = raw.split(', ') + setTags(_tags) + } + + const validateData = async (data: string[]) => { + if (!data) return + + for (let i = 0; i < data.length; i++) { + if (!ethers.utils.isAddress(data[i])) { + try { + const res = await fetchEnsAddress(data[i]) + if (res && ethers.utils.isAddress(res)) { + data[i] = res as string + } else { + return false + } + } catch (e) { + return false + } + } + } + + return data + } + + const handleDeadline = (e: React.ChangeEvent) => { + e.preventDefault() + let _deadline = e.target.value + + if (Date.parse(_deadline) < Date.now()) { + setWarning('Invalid deadline. Please pick another date and time.') + } else { + _deadline = (Date.parse(_deadline) / 1000).toString() + setDeadline(_deadline) + } + } + + const handleCustomToken = async (e: React.ChangeEvent) => { + const contract = new ethers.Contract(e.target.value, erc20ABI, provider) + const decimals = await contract.decimals() + const symbol = await contract.symbol() + const daoBalanceRaw = await contract.balanceOf(daoAddress) + let daoBalance + + if (customTokenDecimals < 18) { + daoBalance = ethers.utils.formatUnits(daoBalanceRaw, customTokenDecimals) + } else { + daoBalance = ethers.utils.formatEther(daoBalanceRaw) + } + + console.log(daoBalance) + setCustomToken(e.target.value) + setCustomTokenSymbol(symbol) + setCustomTokenDecimals(decimals) + setCustomTokenDaoBalance(parseFloat(daoBalance)) + } + + const handleBudget = async (e: React.ChangeEvent) => { + const _budget = Number(e.target.value) + + let daoBalanceRaw + let daoBalance + + // Check if DAO has enough Ether to cover budget + // Custom token balance is checked in handleCustomToken() + if (reward == 'eth') { + daoBalanceRaw = await provider.getBalance(daoAddress) + daoBalance = ethers.utils.formatEther(daoBalanceRaw) + setMaxBudget(Number(daoBalance)) + } + + console.log(daoBalance) + if (_budget > Number(daoBalance) || _budget > customTokenDaoBalance) { + setWarning('Budget exceeds existing DAO balance.') + } else { + setWarning('') + } + } + + const submit = async () => { + setStatus('Creating proposal...') + if (!signer) { + setWarning('Please connect your wallet.') + return + } + + setStatus('Uploading document to IPFS...') + let recordHash + // recordHash = await createDataRoomDetails(daoAddress, chainId, name, tags, record) + console.log(name, tags, record) + + if (recordHash == '') { + setWarning('Error uploading record.') + setStatus('') + return + } + + setStatus('Creating proposal metadata...') + let docs + try { + docs = await createProposal(daoAddress, chainId, 9, title, content) + } catch (e) { + console.error(e) + return + } + + let iface = new ethers.utils.Interface(MANAGER_ABI) + let payload = iface.encodeFunctionData('setRecord', [daoAddress, [recordHash]]) + console.log('Proposal Params - ', 2, docs, [dataRoomAddress], [0], [payload]) + + setStatus('Creating proposal...') + try { + setWarning('') + const tx = await kalidao.propose( + 2, // CALL prop + docs, + [dataRoomAddress], + [0], + [payload], + ) + console.log('tx', tx) + } catch (e) { + console.log('error', e) + } + setStatus('Proposed.') + } + + useEffect(() => { + const toggleButton = async () => { + if (record && tags.length > 0) { + setIsEnabled(true) + } else { + setIsEnabled(false) + } + } + + toggleButton() + }, [record, tags]) + + return ( +
+ setName(e.target.value)} /> + + + )} + + Current DAO Balance: {customTokenDaoBalance ? customTokenDaoBalance : maxBudget} {customTokenSymbol} + + } + description="Specify a budget for this project." + name="personalLimit" + type="number" + onChange={handleBudget} + /> + + + + + {warning && } + + setProposal?.('appsMenu')} /> + + +
+ ) +} diff --git a/components/dao-dashboard/newproposal/apps/index.tsx b/components/dao-dashboard/newproposal/apps/index.tsx index 7265c5cd..8dd4427e 100644 --- a/components/dao-dashboard/newproposal/apps/index.tsx +++ b/components/dao-dashboard/newproposal/apps/index.tsx @@ -5,7 +5,7 @@ import Back from '@design/proposal/Back' import { useRouter } from 'next/router' import { addresses } from '@constants/addresses' import { fetchExtensionStatus } from '@utils/fetchExtensionStatus' -import { IconSparkles, IconTrash, Stack, Text } from '@kalidao/reality' +import { IconSparkles, IconTrash, IconUserSolid, Stack, Text } from '@kalidao/reality' import { Item } from '../Item' type Props = { @@ -36,9 +36,18 @@ function AppsMenu({ setProposal }: Props) { return ( (1) Swap : - KaliDAOs may swap their KaliDAO tokens for ETH or ERC20 tokens publicly or privately. + Swap allows KaliDAOs to swap KaliDAO tokens for ETH or ERC20 tokens publicly or privately. (2) Redemption : - KaliDAO members may redeem a portion of KaliDAO treasury by burning their KaliDAO tokens. + + Redemption allows KaliDAO members to redeem a portion of DAO treasury by burning their KaliDAO tokens. + + (3) Data Room : + Data Room is on-chain storage for recording off-chain activities or ratifying documents. + (4) Manager : + + KaliDAOs can add a project with a budget and assign a manager to distribute ETH and ERC20 tokens without going + through the proposal process. + {isCrowdsale ? ( <> @@ -49,6 +58,8 @@ function AppsMenu({ setProposal }: Props) { setProposal('swap_add')} label="Add Swap" icon={} /> )} setProposal('redemption')} label="Redemption" icon={} /> + setProposal('project_add')} label="Add a Project" icon={} /> + setProposal('project_update')} label="Update a Project" icon={} /> setProposal('menu')} /> diff --git a/components/dao-dashboard/newproposal/index.tsx b/components/dao-dashboard/newproposal/index.tsx index 5ecd66ed..740a0feb 100644 --- a/components/dao-dashboard/newproposal/index.tsx +++ b/components/dao-dashboard/newproposal/index.tsx @@ -6,6 +6,8 @@ import { SendMenu, SendErc20, SendErc721, SendEth } from './send' import { CallContract, ToggleTransfer, UpdateQuorum, UpdateVotingPeriod, UpdateDocs, InternalMenu } from './internal' import { AppsMenu, SetRedemption } from './apps' import SetSwap from './apps/SetSwap' +import SetProject from './apps/SetProject' +import UpdateProject from './apps/UpdateProject' type Props = { proposalProp: string @@ -95,6 +97,14 @@ export function NewProposalModal({ proposalProp, content, title }: Props) { title: 'Add Swap', component: , }, + project_add: { + title: 'Add a Project', + component: , + }, + project_update: { + title: 'Add a Project', + component: , + }, // TODO: add tribute back // tribute: { // title: 'Tribute', From 5efc266553f7b64ce4ef5749ff9968e8c14bba8e Mon Sep 17 00:00:00 2001 From: audsssy Date: Tue, 20 Dec 2022 17:07:57 -0500 Subject: [PATCH 04/20] update contract --- abi/KaliProjectManager.json | 10 ++++++++++ constants/addresses.ts | 2 +- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/abi/KaliProjectManager.json b/abi/KaliProjectManager.json index 0fb82366..2a7fba58 100644 --- a/abi/KaliProjectManager.json +++ b/abi/KaliProjectManager.json @@ -47,6 +47,11 @@ "name": "NotAuthorized", "type": "error" }, + { + "inputs": [], + "name": "OnlyAccount", + "type": "error" + }, { "inputs": [], "name": "Reentrancy", @@ -57,6 +62,11 @@ "name": "SetupFailed", "type": "error" }, + { + "inputs": [], + "name": "TransferFailed", + "type": "error" + }, { "inputs": [], "name": "UpdateFailed", diff --git a/constants/addresses.ts b/constants/addresses.ts index ca50e691..647b308f 100644 --- a/constants/addresses.ts +++ b/constants/addresses.ts @@ -102,7 +102,7 @@ export const addresses: { [key: number]: any } = crowdsale2: '0xB682e773768e68C02B8b3892CF32eA090600b4b4', // crowdsale2: '0x2350C968C7B323Ad255E3942fcb9d578638792EC', redemption: '0x2b8f116e4D9E73A3A9E7CAF1655B9FC01588Db8d', - manager: '0x01100BcA3ca6265F367Bf028C224DA5200eFE0d7', + project: '0x9BDE417544bc2A281150972A58C9a56F59cbaFa5', }, blockExplorer: 'https://goerli.etherscan.io/', }, From d96925c382b602a2d463468aba539ca044d5c9b8 Mon Sep 17 00:00:00 2001 From: audsssy Date: Tue, 20 Dec 2022 17:08:10 -0500 Subject: [PATCH 05/20] update logic --- .../newproposal/apps/SetProject.tsx | 208 +++++++++++++----- .../newproposal/apps/createProjectDetails.ts | 37 ++++ 2 files changed, 190 insertions(+), 55 deletions(-) create mode 100644 components/dao-dashboard/newproposal/apps/createProjectDetails.ts diff --git a/components/dao-dashboard/newproposal/apps/SetProject.tsx b/components/dao-dashboard/newproposal/apps/SetProject.tsx index f7fcaa06..4495ac34 100644 --- a/components/dao-dashboard/newproposal/apps/SetProject.tsx +++ b/components/dao-dashboard/newproposal/apps/SetProject.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect } from 'react' import { useRouter } from 'next/router' -import { ethers } from 'ethers' +import { BigNumber, ethers } from 'ethers' import { useContract, useSigner, useContractRead, erc20ABI } from 'wagmi' import { Stack, @@ -30,6 +30,7 @@ import { Select } from '@design/Select' import { DateInput } from '@design/DateInput' import { getProvider } from '@utils/getProvider' import { AddressZero } from '@ethersproject/constants' +import { createProjectDetails } from './createProjectDetails' export default function SetProject({ setProposal, title, content }: ProposalProps) { const router = useRouter() @@ -38,7 +39,7 @@ export default function SetProject({ setProposal, title, content }: ProposalProp const provider = getProvider(chainId) const { data: signer } = useSigner() - const dataRoomAddress = addresses[chainId]['extensions']['dataRoom'] + const projectManagementAddress = addresses[chainId]['extensions']['project'] const { data: kalidaoToken } = useContractRead({ addressOrName: daoAddress, @@ -54,28 +55,19 @@ export default function SetProject({ setProposal, title, content }: ProposalProp }) // form - const [record, setRecords] = useState() + const [file, setFile] = useState() const [warning, setWarning] = useState() const [isEnabled, setIsEnabled] = useState(false) const [reward, setReward] = useState('select') const [customToken, setCustomToken] = useState('') const [customTokenSymbol, setCustomTokenSymbol] = useState('') const [customTokenDecimals, setCustomTokenDecimals] = useState(0) - const [customTokenDaoBalance, setCustomTokenDaoBalance] = useState(0.0) - const [budget, setBudget] = useState(0) - const [maxBudget, setMaxBudget] = useState(0) + const [daoTokenBalance, setDaoTokenBalance] = useState() + const [budget, setBudget] = useState() const [deadline, setDeadline] = useState() const [status, setStatus] = useState() - const [name, setName] = useState('') - const [tags, setTags] = useState([]) - const [users, setUsers] = useState([]) - - const handleTags = (e: React.ChangeEvent) => { - let raw = e.target.value - let _tags: Array = [] - _tags = raw.split(', ') - setTags(_tags) - } + const [name, setName] = useState() + const [manager, setManager] = useState() const validateData = async (data: string[]) => { if (!data) return @@ -110,6 +102,37 @@ export default function SetProject({ setProposal, title, content }: ProposalProp } } + const handleReward = async (e: React.ChangeEvent) => { + const selection = e.target.value + let daoBalanceRaw + let daoBalance + + if (selection == 'eth') { + daoBalanceRaw = await provider.getBalance(daoAddress) + daoBalance = ethers.utils.formatEther(daoBalanceRaw) + + setWarning('') + setDaoTokenBalance(daoBalance) + setCustomTokenSymbol('Ξ') + } + + if (selection == 'dao') { + const daoToken = kalidaoToken ? kalidaoToken?.toString() : '' + + setWarning('') + setDaoTokenBalance('Uncapped') + setCustomTokenSymbol(' ') + } + + if (selection == 'custom') { + setWarning('') + setDaoTokenBalance('') + setCustomTokenSymbol('') + } + + setReward(selection) + } + const handleCustomToken = async (e: React.ChangeEvent) => { const contract = new ethers.Contract(e.target.value, erc20ABI, provider) const decimals = await contract.decimals() @@ -117,8 +140,8 @@ export default function SetProject({ setProposal, title, content }: ProposalProp const daoBalanceRaw = await contract.balanceOf(daoAddress) let daoBalance - if (customTokenDecimals < 18) { - daoBalance = ethers.utils.formatUnits(daoBalanceRaw, customTokenDecimals) + if (decimals < 18) { + daoBalance = ethers.utils.formatUnits(daoBalanceRaw, decimals) } else { daoBalance = ethers.utils.formatEther(daoBalanceRaw) } @@ -127,49 +150,112 @@ export default function SetProject({ setProposal, title, content }: ProposalProp setCustomToken(e.target.value) setCustomTokenSymbol(symbol) setCustomTokenDecimals(decimals) - setCustomTokenDaoBalance(parseFloat(daoBalance)) + setDaoTokenBalance(daoBalance) } const handleBudget = async (e: React.ChangeEvent) => { - const _budget = Number(e.target.value) - + let _budget = e.target.value + console.log(_budget, reward) let daoBalanceRaw let daoBalance + if (Number(_budget) == 0) { + setWarning('Budget is required.') + } + // Check if DAO has enough Ether to cover budget // Custom token balance is checked in handleCustomToken() if (reward == 'eth') { daoBalanceRaw = await provider.getBalance(daoAddress) daoBalance = ethers.utils.formatEther(daoBalanceRaw) - setMaxBudget(Number(daoBalance)) + // setDaoTokenBalance(daoBalance) + + console.log(_budget) + if (Number(_budget) > Number(daoBalance)) { + setWarning('Budget exceeds existing DAO balance.') + } else { + setWarning('') + const __budget = ethers.utils.parseEther(_budget) + setBudget(__budget) + } } - console.log(daoBalance) - if (_budget > Number(daoBalance) || _budget > customTokenDaoBalance) { - setWarning('Budget exceeds existing DAO balance.') - } else { - setWarning('') + if (reward == 'dao') { + const __budget = ethers.utils.parseEther(_budget) + setBudget(__budget) + } + + if (reward == 'custom') { + if (Number(_budget) > Number(daoBalance) || Number(_budget) > Number(daoTokenBalance)) { + setWarning('Budget exceeds existing DAO balance.') + } else { + setWarning('') + const __budget = ethers.utils.parseUnits(_budget, customTokenDecimals) + console.log(__budget, _budget, customTokenDecimals) + setBudget(__budget) + } } } const submit = async () => { + // Validate form inputs setStatus('Creating proposal...') if (!signer) { setWarning('Please connect your wallet.') return } - setStatus('Uploading document to IPFS...') - let recordHash - // recordHash = await createDataRoomDetails(daoAddress, chainId, name, tags, record) - console.log(name, tags, record) + if (!name || !manager || reward === 'select' || !budget || !deadline || !file) { + setWarning('All fields are required.') + return + } + + if (reward === 'custom' && customToken == '') { + setWarning('Custom token address is required.') + return + } + + let _reward + let _token + let _budget - if (recordHash == '') { - setWarning('Error uploading record.') + if (reward === 'eth') { + _reward = 0 + _token = AddressZero + _budget = ethers.utils.formatEther(budget) + } else if (reward === 'dao') { + _reward = 1 + _token = daoAddress + _budget = ethers.utils.formatEther(budget) + } else if (reward === 'custom') { + _reward = 2 + _token = customToken + _budget = ethers.utils.formatUnits(budget, customTokenDecimals) + } else { + setWarning('Invalid reward.') + } + + // Upload docs to IFPS + setStatus('Uploading documents to IPFS...') + let detailsHash + detailsHash = await createProjectDetails( + daoAddress, + chainId, + name, + manager, + reward, + Number(_budget), + deadline, + file, + ) + + if (detailsHash == '') { + setWarning('Error uploading documents.') setStatus('') return } + // Upload proposal metadata setStatus('Creating proposal metadata...') let docs try { @@ -179,18 +265,31 @@ export default function SetProject({ setProposal, title, content }: ProposalProp return } - let iface = new ethers.utils.Interface(MANAGER_ABI) - let payload = iface.encodeFunctionData('setRecord', [daoAddress, [recordHash]]) - console.log('Proposal Params - ', 2, docs, [dataRoomAddress], [0], [payload]) + setStatus('Encoding project management details...') + let payload + try { + const abiCoder = ethers.utils.defaultAbiCoder + payload = abiCoder.encode( + ['uint256', 'uint8', 'address', 'uint8', 'address', 'uint256', 'uint40', 'string'], + [0, 1, manager, _reward, _token, budget, deadline, detailsHash], + ) + console.log(0, 1, manager, _reward, _token, budget, deadline, detailsHash) + } catch (e) { + setWarning('Error setting the project management proposal.') + console.log(e) + return + } + + console.log('Proposal Params - ', 2, docs, [projectManagementAddress], [0], [payload]) setStatus('Creating proposal...') try { setWarning('') const tx = await kalidao.propose( - 2, // CALL prop + 9, // EXTENSION prop docs, - [dataRoomAddress], - [0], + [projectManagementAddress], + [1], [payload], ) console.log('tx', tx) @@ -201,36 +300,35 @@ export default function SetProject({ setProposal, title, content }: ProposalProp } useEffect(() => { - const toggleButton = async () => { - if (record && tags.length > 0) { - setIsEnabled(true) - } else { - setIsEnabled(false) - } + const checkDaoEthBalance = async () => { + const balance = await provider.getBalance(daoAddress) + let daoBalance = ethers.utils.formatEther(balance) + console.log(daoBalance, daoTokenBalance) + setDaoTokenBalance(daoBalance) } - toggleButton() - }, [record, tags]) + checkDaoEthBalance() + }, []) return (
- setName(e.target.value)} /> + setName(e.target.value)} /> setManager(e.target.value)} /> + {/* + + */} + Current Manager: {oldManager ? oldManager : 'N/A'} } + description="" + name="manager" + type="text" + placeholder={AddressZero} + onChange={(e) => setNewManager(e.target.value)} + /> + + + Current Project Budget: {oldBudget ? oldBudget : ' '} ${customTokenSymbol ? customTokenSymbol : 'Ξ'} + + } + description="Specify a budget for this project." + name="personalLimit" + type="number" + placeholder="0.0" + min={0} + onChange={handleBudget} + /> + + Current Deadline: {prettyDate(new Date(oldDeadline * 1000))}} + /> + + + Current Document:{' '} + + {oldDocs ? 'Link' : 'N/A'} + + + ) : ( + Current Document: N/A + ) + } + /> + {warning && } + + setProposal?.('appsMenu')} /> + + +
+ ) +} From a12513889cfc60bb91297cd5c7f56dafb6aaff4f Mon Sep 17 00:00:00 2001 From: audsssy Date: Mon, 9 Jan 2023 14:20:32 -0500 Subject: [PATCH 11/20] Update SetProject.tsx --- .../newproposal/apps/SetProject.tsx | 65 ++++++------------- 1 file changed, 20 insertions(+), 45 deletions(-) diff --git a/components/dao-dashboard/newproposal/apps/SetProject.tsx b/components/dao-dashboard/newproposal/apps/SetProject.tsx index 4495ac34..793d785b 100644 --- a/components/dao-dashboard/newproposal/apps/SetProject.tsx +++ b/components/dao-dashboard/newproposal/apps/SetProject.tsx @@ -57,7 +57,6 @@ export default function SetProject({ setProposal, title, content }: ProposalProp // form const [file, setFile] = useState() const [warning, setWarning] = useState() - const [isEnabled, setIsEnabled] = useState(false) const [reward, setReward] = useState('select') const [customToken, setCustomToken] = useState('') const [customTokenSymbol, setCustomTokenSymbol] = useState('') @@ -69,27 +68,6 @@ export default function SetProject({ setProposal, title, content }: ProposalProp const [name, setName] = useState() const [manager, setManager] = useState() - const validateData = async (data: string[]) => { - if (!data) return - - for (let i = 0; i < data.length; i++) { - if (!ethers.utils.isAddress(data[i])) { - try { - const res = await fetchEnsAddress(data[i]) - if (res && ethers.utils.isAddress(res)) { - data[i] = res as string - } else { - return false - } - } catch (e) { - return false - } - } - } - - return data - } - const handleDeadline = (e: React.ChangeEvent) => { e.preventDefault() let _deadline = e.target.value @@ -165,23 +143,22 @@ export default function SetProject({ setProposal, title, content }: ProposalProp // Check if DAO has enough Ether to cover budget // Custom token balance is checked in handleCustomToken() - if (reward == 'eth') { - daoBalanceRaw = await provider.getBalance(daoAddress) - daoBalance = ethers.utils.formatEther(daoBalanceRaw) - // setDaoTokenBalance(daoBalance) - - console.log(_budget) - if (Number(_budget) > Number(daoBalance)) { - setWarning('Budget exceeds existing DAO balance.') - } else { - setWarning('') - const __budget = ethers.utils.parseEther(_budget) - setBudget(__budget) - } - } + // if (reward == 'eth') { + // daoBalanceRaw = await provider.getBalance(daoAddress) + // daoBalance = ethers.utils.formatEther(daoBalanceRaw) + + // console.log(_budget) + // if (Number(_budget) > Number(daoBalance)) { + // setWarning('Budget exceeds existing DAO balance.') + // } else { + // setWarning('') + // const __budget = _budget ? ethers.utils.parseEther(_budget) : ethers.utils.parseEther('0.0') + // setBudget(__budget) + // } + // } if (reward == 'dao') { - const __budget = ethers.utils.parseEther(_budget) + const __budget = _budget ? ethers.utils.parseEther(_budget) : ethers.utils.parseEther('0.0') setBudget(__budget) } @@ -219,16 +196,12 @@ export default function SetProject({ setProposal, title, content }: ProposalProp let _token let _budget - if (reward === 'eth') { + if (reward === 'dao') { _reward = 0 - _token = AddressZero - _budget = ethers.utils.formatEther(budget) - } else if (reward === 'dao') { - _reward = 1 _token = daoAddress _budget = ethers.utils.formatEther(budget) } else if (reward === 'custom') { - _reward = 2 + _reward = 1 _token = customToken _budget = ethers.utils.formatUnits(budget, customTokenDecimals) } else { @@ -239,6 +212,7 @@ export default function SetProject({ setProposal, title, content }: ProposalProp setStatus('Uploading documents to IPFS...') let detailsHash detailsHash = await createProjectDetails( + 0, daoAddress, chainId, name, @@ -280,7 +254,7 @@ export default function SetProject({ setProposal, title, content }: ProposalProp return } - console.log('Proposal Params - ', 2, docs, [projectManagementAddress], [0], [payload]) + console.log('Proposal Params - ', 9, docs, [projectManagementAddress], [0], [payload]) setStatus('Creating proposal...') try { @@ -331,7 +305,7 @@ export default function SetProject({ setProposal, title, content }: ProposalProp onChange={handleReward} options={[ { value: 'select', label: 'Select' }, - { value: 'eth', label: 'Ether' }, + // { value: 'eth', label: 'Ether' }, { value: 'dao', label: `DAO token ($${kalidaoToken})` }, { value: 'custom', label: 'ERC20' }, ]} @@ -360,6 +334,7 @@ export default function SetProject({ setProposal, title, content }: ProposalProp From ae4d06a2d9a47103326ff5458eb7fcb5cb9b67c3 Mon Sep 17 00:00:00 2001 From: audsssy Date: Mon, 9 Jan 2023 14:25:19 -0500 Subject: [PATCH 12/20] nit vercel bug --- components/dao-dashboard/newproposal/apps/SetSwap.tsx | 1 + components/dao-dashboard/newproposal/internal/UpdateDocs.tsx | 2 +- components/tools/FileUpload.tsx | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/components/dao-dashboard/newproposal/apps/SetSwap.tsx b/components/dao-dashboard/newproposal/apps/SetSwap.tsx index 896d8a29..c343996d 100644 --- a/components/dao-dashboard/newproposal/apps/SetSwap.tsx +++ b/components/dao-dashboard/newproposal/apps/SetSwap.tsx @@ -355,6 +355,7 @@ export default function SetCrowdsale({ setProposal, title, content }: ProposalPr diff --git a/components/dao-dashboard/newproposal/internal/UpdateDocs.tsx b/components/dao-dashboard/newproposal/internal/UpdateDocs.tsx index def48e30..b7614619 100644 --- a/components/dao-dashboard/newproposal/internal/UpdateDocs.tsx +++ b/components/dao-dashboard/newproposal/internal/UpdateDocs.tsx @@ -102,7 +102,7 @@ export default function UpdateDocs() { OR - + {warning && } Submit}> + + + + {projects?.map((project, index) => { + return ( + + + {project.name} + + Project ID: + #{project.id} + + + + Status: + {project.status} + + + + Deadline: + {prettyDate(project.deadline)} + + + + Manager: + + + {project.manager.slice(-4) == '.eth' ? project.manager : truncateAddress(project.manager)} + + + + + + Budget: + + {project.budget} {project.reward} + + + + + Distributed: + + {project.distributed} {project.reward} + + + + + Reward Token: + + + {truncateAddress(project.token)} + + + + + + Detail: + + + + + + + + + ) + })} + + + + + ) +} + +export default Projects diff --git a/utils/fetchEnsFromAddress.ts b/utils/fetchEnsFromAddress.ts new file mode 100644 index 00000000..f2d86abd --- /dev/null +++ b/utils/fetchEnsFromAddress.ts @@ -0,0 +1,18 @@ +import { ethers } from 'ethers' +import { getProvider } from './getProvider' + +// fetch from mainnet +export async function fetchEnsFromAddress(address: string, chainId: number) { + if (!address) return + try { + const provider = getProvider(chainId) + console.log('minting address', address) + const ens = await provider.lookupAddress(address) + console.log('minting address', address) + if (ens) return ens + else return address + } catch (e) { + console.log(`fetchEnsAddress ${address}`, e) + return 'Error' + } +} From 1634e8aada72f9336f5d684c4c1a9b04d7ae8ba2 Mon Sep 17 00:00:00 2001 From: audsssy Date: Thu, 2 Feb 2023 14:21:47 -0500 Subject: [PATCH 20/20] add logic to detect if user is a manager --- components/dao-dashboard/layout/Nav.tsx | 5 +- .../apps/utils/fetchDaoProjects.ts | 8 ++- pages/daos/[chainId]/[dao]/projects.tsx | 65 +++++++++++-------- 3 files changed, 47 insertions(+), 31 deletions(-) diff --git a/components/dao-dashboard/layout/Nav.tsx b/components/dao-dashboard/layout/Nav.tsx index 2c4ad0f5..86b5b38e 100644 --- a/components/dao-dashboard/layout/Nav.tsx +++ b/components/dao-dashboard/layout/Nav.tsx @@ -84,9 +84,8 @@ const Nav = ({ address, chainId }: DashboardElementProps) => { useEffect(() => { const getProjects = async () => { - const projects = await fetchDaoProject(address, chainId) - console.log(projects) - if (projects.length > 0) { + const p = await fetchDaoProject(address, chainId) + if (p.projects.length > 0) { setHaveProject(true) } } diff --git a/components/dao-dashboard/newproposal/apps/utils/fetchDaoProjects.ts b/components/dao-dashboard/newproposal/apps/utils/fetchDaoProjects.ts index 8a2c49cd..a8bab3ee 100644 --- a/components/dao-dashboard/newproposal/apps/utils/fetchDaoProjects.ts +++ b/components/dao-dashboard/newproposal/apps/utils/fetchDaoProjects.ts @@ -12,6 +12,7 @@ interface Project { deadline: Date distributed: string manager: string + managerAddress: string reward: string status: string token: string @@ -25,6 +26,7 @@ export const fetchDaoProject = async (dao: string, chainId: number) => { const projectManagementAddress = addresses[chainId]['extensions']['project'] let projects: Array = [] + let managers: Array = [] const kaliPm = new ethers.Contract(projectManagementAddress, PM_ABI, provider) const _projectId: BigNumber = await kaliPm.projectId() @@ -44,7 +46,6 @@ export const fetchDaoProject = async (dao: string, chainId: number) => { const response = await fetch(p.docs) const responseJson = await response.json() - console.log(decimals) projects.push({ id: i, account: p.account, @@ -53,6 +54,7 @@ export const fetchDaoProject = async (dao: string, chainId: number) => { distributed: decimals < 18 ? ethers.utils.formatUnits(p.distributed, decimals) : ethers.utils.formatEther(p.distributed), manager: managerEns, + managerAddress: p.manager, reward: p.reward == 0 ? 'DAO Tokens' : symbol, status: p.status == 0 ? 'Inactive' : 'Active', token: p.token, @@ -60,6 +62,8 @@ export const fetchDaoProject = async (dao: string, chainId: number) => { name: responseJson.name, file: responseJson.file, }) + + managers.push(p.manager) } catch (e) { console.log(e, p.docs) } @@ -67,5 +71,5 @@ export const fetchDaoProject = async (dao: string, chainId: number) => { } console.log(projects) - return projects + return { projects, managers } } diff --git a/pages/daos/[chainId]/[dao]/projects.tsx b/pages/daos/[chainId]/[dao]/projects.tsx index 4193124a..26f9389d 100644 --- a/pages/daos/[chainId]/[dao]/projects.tsx +++ b/pages/daos/[chainId]/[dao]/projects.tsx @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react' import { NextPage } from 'next' import Layout from '@components/dao-dashboard/layout' import { Stack, Text, Box, Button, Input, IconLink } from '@kalidao/reality' -import { useContract, useSigner } from 'wagmi' +import { useAccount, useContract, useSigner } from 'wagmi' import { useRouter } from 'next/router' import { AddressZero } from '@ethersproject/constants' import { addresses } from '@constants/addresses' @@ -21,6 +21,7 @@ interface Project { deadline: Date distributed: string manager: string + managerAddress: string reward: string status: string token: string @@ -36,6 +37,7 @@ const Projects: NextPage = () => { const pmAddress = chainId ? addresses[chainId]['extensions']['project'] : AddressZero const [projects, setProjects] = useState([]) + const [isManagers, setIsManagers] = useState() const [id, setId] = useState(0) const [tokenDecimals, setTokenDecimals] = useState(0) const [contributor, setContributor] = useState() @@ -44,6 +46,7 @@ const Projects: NextPage = () => { const [status, setStatus] = useState() const { data: signer } = useSigner() + const { address } = useAccount() const kaliPm = useContract({ address: pmAddress, @@ -66,9 +69,15 @@ const Projects: NextPage = () => { const submit = async () => { setStatus('Processing...') - console.log(id, contributor, amount) if (!contributor || !amount) { setWarning('All fields are required.') + setStatus('Submit') + return + } + + if (ethers.utils.getAddress(projects[id - 1].managerAddress) != ethers.utils.getAddress(address as string)) { + setWarning('Not a manager for this project.') + setStatus('Submit') return } @@ -81,7 +90,6 @@ const Projects: NextPage = () => { try { const abiCoder = ethers.utils.defaultAbiCoder payload = abiCoder.encode(['uint256', 'address', 'uint256'], [id, contributor, _amount]) - console.log(id, contributor, amount, payload) } catch (e) { setWarning('Error gathering project data.') console.log(e) @@ -95,14 +103,18 @@ const Projects: NextPage = () => { setStatus('Contributor Rewarded.') } catch (e) { console.log('error', e) + setStatus('Submit') } } useEffect(() => { const getProjects = async () => { - const _projects = await fetchDaoProject(daoAddress, chainId) - - setProjects(_projects) + const p = await fetchDaoProject(daoAddress, chainId) + setProjects(p.projects) + const managers = Array.from(new Set(p.managers)) + if (managers.includes(ethers.utils.getAddress(address as string))) { + setIsManagers(true) + } } getProjects() @@ -110,7 +122,7 @@ const Projects: NextPage = () => { return ( - + { gap="3" padding="3" > - - - Drop - - setContributor(e.target.value)} - /> - setAmount(e.target.value)} /> - {warning && } - - - + {isManagers && ( + + + Drop + + setContributor(e.target.value)} + /> + setAmount(e.target.value)} /> + {warning && } + + + + )} {projects?.map((project, index) => { return ( {project.name}