From 2efb84752e9d8082418640d9a5fc22142242cf86 Mon Sep 17 00:00:00 2001 From: hans Date: Thu, 3 Mar 2022 17:32:21 -0800 Subject: [PATCH 1/5] updated: usage for new api --- .../react-app/src/routes/create/Create.jsx | 39 +++++++++++++++++-- 1 file changed, 36 insertions(+), 3 deletions(-) diff --git a/packages/react-app/src/routes/create/Create.jsx b/packages/react-app/src/routes/create/Create.jsx index e8549b8..c157bdc 100644 --- a/packages/react-app/src/routes/create/Create.jsx +++ b/packages/react-app/src/routes/create/Create.jsx @@ -19,6 +19,7 @@ import { useHistory } from "react-router-dom"; import { default as MultiAddressInput } from "./components/MultiAddressInput"; import { useColorModeValue } from "@chakra-ui/color-mode"; import Blockie from "../../components/Blockie"; +import { ethers } from "ethers"; const Create = ({ address, @@ -39,7 +40,10 @@ const Create = ({ const [isConfirmDisabled, setIsConfirmDisabled] = useState(true); const [isInvalidName, setIsInvalidName] = useState(false); const [partyObj, setPartyObj] = useState({ + version: "1.0", name: "", + timestamp: "", + nonce: "", description: "", receipts: [], config: { @@ -49,6 +53,9 @@ const Create = ({ participants: [], candidates: [], ballots: [], + notes: [], + ipfs: "", + signature: "", }); const onSubmit = async event => { @@ -57,7 +64,26 @@ const Create = ({ setLoadingText("Submitting..."); setIsLoading(true); - const sig = await userSigner.signMessage(`Create party:\n${partyObj.name}`); + const partySignatureData = { + version: "1.0", + name: partyObj.name, + timestamp: "", + nonce: "", + description: partyObj.description, + participants: partyObj.participants, + candidates: partyObj.candidates, + }; + + const signedParty = { + data: partySignatureData, + signature: "", + }; + + const sig = await userSigner.signMessage(ethers.utils.keccak256(ethers.utils.id(partySignatureData))); + signedParty.signature = sig; + + partyObj.signed = signedParty; + const res = await fetch(`${process.env.REACT_APP_API_URL}/party`, { method: "post", headers: { "Content-Type": "application/json" }, @@ -66,7 +92,8 @@ const Create = ({ const json = await res.json(); routeHistory.push(`/party/${json.id}`); setIsLoading(false); - } catch { + } catch (err) { + console.log(err); setIsLoading(false); } }; @@ -108,13 +135,19 @@ const Create = ({ } setIsInvalidName(false); setPartyObj({ + version: "1.0", name: name, + timestamp: "", + nonce: "", description: description, receipts: [], config: config, participants: voterAddresses, candidates: candidateAddresses, ballots: [], + notes: [], + ipfs: "", + signature: "", }); } catch (err) { setIsConfirmDisabled(true); @@ -150,7 +183,7 @@ const Create = ({ {isInvalidName ? ( - Name has already been used. Please try another. + Name already taken. Please try another. ) : null} From 01f44e99617990cb0ee8c7000b79128de1dd6f66 Mon Sep 17 00:00:00 2001 From: hans Date: Fri, 4 Mar 2022 17:52:20 -0800 Subject: [PATCH 2/5] added: signature ballot verification --- packages/react-app/src/routes/party/Party.jsx | 40 +++++++++++++++---- .../src/routes/party/components/VoteTable.jsx | 14 +++---- 2 files changed, 39 insertions(+), 15 deletions(-) diff --git a/packages/react-app/src/routes/party/Party.jsx b/packages/react-app/src/routes/party/Party.jsx index b54ddb9..a51e979 100644 --- a/packages/react-app/src/routes/party/Party.jsx +++ b/packages/react-app/src/routes/party/Party.jsx @@ -36,18 +36,45 @@ export default function Party({ (async () => { const res = await fetch(`${process.env.REACT_APP_API_URL}/party/${id}`); const party = await res.json(); - const submitted = party.ballots.filter(b => b.data.ballot.address.toLowerCase() === address); + // DEBUG + //const submitted = party.ballots.filter(b => b.data.ballot.address.toLowerCase() === address); + + // EIP-712 Typed Data + // See: https://eips.ethereum.org/EIPS/eip-712 + const domain = { + name: "pay-party", + version: "1", + chainId: targetNetwork.chainId, + verifyingContract: "0x14313eB3823D0268C49d2D977b6Fb1eE2233Ef20", //readContracts?.Distributor?.address, + }; + const types = { + Party: [{ name: "ballot", type: "Ballot" }], + Ballot: [ + { name: "votes", type: "string" }, + { name: "timestamp", type: "string" }, + { name: "partySignature", type: "string" }, + ], + }; + + const submitted = party.ballots.filter(b => { + // Reconstruct the signer from the data + return utils.verifyTypedData(domain, types, b.data, b.signature).toLowerCase() === address.toLowerCase(); + }); + + console.log(submitted); const participating = party.participants.map(adr => adr.toLowerCase()).includes(address); setAccountVoteData(submitted); setCanVote(submitted.length === 0 && participating); setIsPaid(party.receipts.length > 0); const len = party.receipts.length; - if(len > 0) { - setAmountToDistribute(utils.formatEther(party.receipts[len-1].amount)); + if (len > 0) { + setAmountToDistribute(utils.formatEther(party.receipts[len - 1].amount)); } setIsParticipant(participating); setPartyData(party); setLoading(false); + + console.log(distribution); })(); }, []); @@ -178,7 +205,7 @@ export default function Party({ {showDebug &&

{JSON.stringify(partyData)}

} {loading ? (
- +
) : ( - {isPaid && } + {isPaid && } diff --git a/packages/react-app/src/routes/party/components/VoteTable.jsx b/packages/react-app/src/routes/party/components/VoteTable.jsx index c333c59..c12f42b 100644 --- a/packages/react-app/src/routes/party/components/VoteTable.jsx +++ b/packages/react-app/src/routes/party/components/VoteTable.jsx @@ -58,24 +58,24 @@ export const VoteTable = ({ partyData, address, userSigner, targetNetwork, readC verifyingContract: readContracts?.Distributor?.address, }; const types = { - Party: [ - { name: "party", type: "string" }, - { name: "ballot", type: "Ballot" }, - ], + Party: [{ name: "ballot", type: "Ballot" }], Ballot: [ - { name: "address", type: "address" }, { name: "votes", type: "string" }, + { name: "timestamp", type: "string" }, + { name: "partySignature", type: "string" }, ], }; const ballot = { - party: partyData.name, ballot: { - address: address, votes: JSON.stringify(votesData, null, 2), + timestamp: "", + partySignature: partyData.signed.signature, }, }; + //console.log(domain, types); + // NOTE: sign typed data for eip712 is underscored because it's in public beta if (partyData.participants.map(adr => adr.toLowerCase()).includes(address) && !invalidVotesLeft) { const ballots = partyData.ballots; From b50a9c66d2c77eaf982da174cbffea83c496bedb Mon Sep 17 00:00:00 2001 From: hans Date: Mon, 7 Mar 2022 11:00:33 -0800 Subject: [PATCH 3/5] check sig for votes --- .../create/components/MultiAddressInput.jsx | 56 +++++++++---------- packages/react-app/src/routes/party/Party.jsx | 8 +-- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/packages/react-app/src/routes/create/components/MultiAddressInput.jsx b/packages/react-app/src/routes/create/components/MultiAddressInput.jsx index 6416345..6fc374e 100644 --- a/packages/react-app/src/routes/create/components/MultiAddressInput.jsx +++ b/packages/react-app/src/routes/create/components/MultiAddressInput.jsx @@ -52,35 +52,35 @@ export default function MultiAddressInput(props) { const handleChange = e => { const lastInput = e.target.value[e.target.value.length - 1]; - if (lastInput === "," || lastInput === "\n") { - const splitInput = e.currentTarget.value - .split(/[ ,\n]+/) - .filter(c => c !== "") - .map(async uin => { - // Data model - let val = { input: uin, isValid: null, address: null, ens: null }; - try { - if (uin.endsWith(".eth") || uin.endsWith(".xyz")) { - val.address = await ensProvider.resolveName(uin); - val.ens = uin; - } else { - val.ens = await ensProvider.lookupAddress(uin); - val.address = uin; - } - val.isValid = true; - } catch { - val.isValid = false; - console.log("Bad Address: " + uin); + if (lastInput === "," || lastInput === "\n" || lastInput === " ") { + const splitInput = e.currentTarget.value + .split(/[ ,\n]+/) + .filter(c => c !== "") + .map(async uin => { + // Data model + let val = { input: uin, isValid: null, address: null, ens: null }; + try { + if (uin.endsWith(".eth") || uin.endsWith(".xyz")) { + val.address = await ensProvider.resolveName(uin); + val.ens = uin; + } else { + val.ens = await ensProvider.lookupAddress(uin); + val.address = uin; } - return val; - }); - setIsLoading(true); - Promise.all(splitInput) - .then(d => { - onChange([...value, ...d]); - }) - .finally(_ => setIsLoading(false)); - e.target.value = ""; + val.isValid = true; + } catch { + val.isValid = false; + console.log("Bad Address: " + uin); + } + return val; + }); + setIsLoading(true); + Promise.all(splitInput) + .then(d => { + onChange([...value, ...d]); + }) + .finally(_ => setIsLoading(false)); + e.target.value = ""; } }; diff --git a/packages/react-app/src/routes/party/Party.jsx b/packages/react-app/src/routes/party/Party.jsx index a51e979..a248138 100644 --- a/packages/react-app/src/routes/party/Party.jsx +++ b/packages/react-app/src/routes/party/Party.jsx @@ -56,12 +56,10 @@ export default function Party({ ], }; - const submitted = party.ballots.filter(b => { - // Reconstruct the signer from the data - return utils.verifyTypedData(domain, types, b.data, b.signature).toLowerCase() === address.toLowerCase(); - }); + const submitted = party.ballots.filter( + b => utils.verifyTypedData(domain, types, b.data, b.signature).toLowerCase() === address.toLowerCase(), + ); - console.log(submitted); const participating = party.participants.map(adr => adr.toLowerCase()).includes(address); setAccountVoteData(submitted); setCanVote(submitted.length === 0 && participating); From 2556ca790046989bd50cb31aff3e68c6446219b0 Mon Sep 17 00:00:00 2001 From: hans Date: Mon, 7 Mar 2022 12:01:31 -0800 Subject: [PATCH 4/5] added: chain id checks --- .../react-app/src/routes/create/Create.jsx | 1 + packages/react-app/src/routes/party/Party.jsx | 71 +++++++++---------- .../src/routes/party/components/VoteTable.jsx | 4 +- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/packages/react-app/src/routes/create/Create.jsx b/packages/react-app/src/routes/create/Create.jsx index c157bdc..7f2573c 100644 --- a/packages/react-app/src/routes/create/Create.jsx +++ b/packages/react-app/src/routes/create/Create.jsx @@ -129,6 +129,7 @@ const Create = ({ const config = { strategy: "", nvotes: candidateAddresses.length * 5, + chainId: targetNetwork.chainId, }; if (name !== "" && candidateAddresses.length > 0 && voterAddresses.length > 0) { setIsConfirmDisabled(false); diff --git a/packages/react-app/src/routes/party/Party.jsx b/packages/react-app/src/routes/party/Party.jsx index a248138..edbd2b1 100644 --- a/packages/react-app/src/routes/party/Party.jsx +++ b/packages/react-app/src/routes/party/Party.jsx @@ -34,47 +34,46 @@ export default function Party({ useEffect(() => { setLoading(true); (async () => { - const res = await fetch(`${process.env.REACT_APP_API_URL}/party/${id}`); - const party = await res.json(); - // DEBUG - //const submitted = party.ballots.filter(b => b.data.ballot.address.toLowerCase() === address); + if (readContracts && readContracts.Distributor.address) { + const res = await fetch(`${process.env.REACT_APP_API_URL}/party/${id}`); + const party = await res.json(); - // EIP-712 Typed Data - // See: https://eips.ethereum.org/EIPS/eip-712 - const domain = { - name: "pay-party", - version: "1", - chainId: targetNetwork.chainId, - verifyingContract: "0x14313eB3823D0268C49d2D977b6Fb1eE2233Ef20", //readContracts?.Distributor?.address, - }; - const types = { - Party: [{ name: "ballot", type: "Ballot" }], - Ballot: [ - { name: "votes", type: "string" }, - { name: "timestamp", type: "string" }, - { name: "partySignature", type: "string" }, - ], - }; + // TODO: Put this data model in a seperate file for organization + // EIP-712 Typed Data + // See: https://eips.ethereum.org/EIPS/eip-712 + const domain = { + name: "pay-party", + version: "1", + chainId: targetNetwork.chainId, + verifyingContract: readContracts.Distributor.address, + }; + const types = { + Party: [{ name: "ballot", type: "Ballot" }], + Ballot: [ + { name: "votes", type: "string" }, + { name: "timestamp", type: "string" }, + { name: "partySignature", type: "string" }, + ], + }; - const submitted = party.ballots.filter( - b => utils.verifyTypedData(domain, types, b.data, b.signature).toLowerCase() === address.toLowerCase(), - ); + const submitted = party.ballots.filter( + b => utils.verifyTypedData(domain, types, b.data, b.signature).toLowerCase() === address.toLowerCase(), + ); - const participating = party.participants.map(adr => adr.toLowerCase()).includes(address); - setAccountVoteData(submitted); - setCanVote(submitted.length === 0 && participating); - setIsPaid(party.receipts.length > 0); - const len = party.receipts.length; - if (len > 0) { - setAmountToDistribute(utils.formatEther(party.receipts[len - 1].amount)); + const participating = party.participants.map(adr => adr.toLowerCase()).includes(address); + setAccountVoteData(submitted); + setCanVote(submitted.length === 0 && participating); + setIsPaid(party.receipts.length > 0); + const len = party.receipts.length; + if (len > 0) { + setAmountToDistribute(utils.formatEther(party.receipts[len - 1].amount)); + } + setIsParticipant(participating); + setPartyData(party); + setLoading(false); } - setIsParticipant(participating); - setPartyData(party); - setLoading(false); - - console.log(distribution); })(); - }, []); + }, [readContracts]); // Calculate percent distribution from submitted ballots and memo table const calculateDistribution = () => { diff --git a/packages/react-app/src/routes/party/components/VoteTable.jsx b/packages/react-app/src/routes/party/components/VoteTable.jsx index c12f42b..a9916f1 100644 --- a/packages/react-app/src/routes/party/components/VoteTable.jsx +++ b/packages/react-app/src/routes/party/components/VoteTable.jsx @@ -49,12 +49,13 @@ export const VoteTable = ({ partyData, address, userSigner, targetNetwork, readC const vote = async _ => { try { + // TODO: Put this data model in a seperate file for organization // EIP-712 Typed Data // See: https://eips.ethereum.org/EIPS/eip-712 const domain = { name: "pay-party", version: "1", - chainId: targetNetwork.chainId, + chainId: partyData.config.chainId, verifyingContract: readContracts?.Distributor?.address, }; const types = { @@ -65,7 +66,6 @@ export const VoteTable = ({ partyData, address, userSigner, targetNetwork, readC { name: "partySignature", type: "string" }, ], }; - const ballot = { ballot: { votes: JSON.stringify(votesData, null, 2), From 22e0c5522e4fcb40a75fe5f99a310162189fe50a Mon Sep 17 00:00:00 2001 From: hans Date: Mon, 7 Mar 2022 15:14:45 -0800 Subject: [PATCH 5/5] updated: interface with signature metadata and better strategy selection --- packages/react-app/src/routes/party/Party.jsx | 44 ++++++++------ .../routes/party/components/Distribute.jsx | 60 ++++++++++++------- .../src/routes/party/components/Metadata.jsx | 9 +++ .../src/routes/party/components/VoteTable.jsx | 6 +- 4 files changed, 77 insertions(+), 42 deletions(-) diff --git a/packages/react-app/src/routes/party/Party.jsx b/packages/react-app/src/routes/party/Party.jsx index edbd2b1..804ac8e 100644 --- a/packages/react-app/src/routes/party/Party.jsx +++ b/packages/react-app/src/routes/party/Party.jsx @@ -112,25 +112,28 @@ export default function Party({ }; // Cache the calculated distribution and table component - const cachedViewTable = useMemo(() => { - try { - const dist = calculateDistribution(); - setDistribution(dist); - return ( - - ); - } catch (error) { - console.log(error); - return null; - } - }, [partyData, strategy, amountToDistribute]); + const cachedViewTable = useMemo( + _ => { + try { + const dist = calculateDistribution(); + setDistribution(dist); + return ( + + ); + } catch (error) { + console.log(error); + return null; + } + }, + [partyData, strategy, amountToDistribute], + ); const cachedVoteTable = useMemo(() => { try { @@ -236,10 +239,13 @@ export default function Party({ readContracts={readContracts} tx={tx} distribution={distribution} + setDistribution={setDistribution} strategy={strategy} + setStrategy={setStrategy} isSmartContract={isSmartContract} localProvider={localProvider} setAmountToDistribute={setAmountToDistribute} + targetNetwork={targetNetwork} /> {isPaid && } diff --git a/packages/react-app/src/routes/party/components/Distribute.jsx b/packages/react-app/src/routes/party/components/Distribute.jsx index 44f7449..93d61b4 100644 --- a/packages/react-app/src/routes/party/components/Distribute.jsx +++ b/packages/react-app/src/routes/party/components/Distribute.jsx @@ -1,6 +1,6 @@ import { useColorModeValue } from "@chakra-ui/color-mode"; import { QuestionOutlineIcon } from "@chakra-ui/icons"; -import { Box, Button, Center, HStack, Text, Tooltip, Spacer } from "@chakra-ui/react"; +import { Box, Button, Center, HStack, Text, Tooltip, Spacer, Radio, RadioGroup, Stack } from "@chakra-ui/react"; import { InputNumber } from "antd"; import { BigNumber, ethers, utils } from "ethers"; import React, { useState } from "react"; @@ -15,10 +15,13 @@ export const Distribute = ({ writeContracts, tx, distribution, + setDistribution, strategy, + setStrategy, isSmartContract, localProvider, setAmountToDistribute, + targetNetwork, }) => { const [tokenInstance, setTokenInstance] = useState(null); const [amounts, setAmounts] = useState(null); @@ -89,10 +92,12 @@ export const Distribute = ({ setAmounts(amts); setAddresses(adrs); const len = partyData.receipts.length; - if(len > 0) { - amt > 0 ? setAmountToDistribute(amt) : setAmountToDistribute(utils.formatEther(partyData.receipts[len-1].amount)); + if (len > 0) { + amt > 0 + ? setAmountToDistribute(amt) + : setAmountToDistribute(utils.formatEther(partyData.receipts[len - 1].amount)); } else { - amt > 0 ? setAmountToDistribute(amt) : setAmountToDistribute(0); + amt > 0 ? setAmountToDistribute(amt) : setAmountToDistribute(0); } setHasApprovedAllowance(false); } @@ -101,7 +106,7 @@ export const Distribute = ({ } }; - const handleReceipt = res => { + const handleReceipt = async res => { if (res && res.hash && (res.status === "confirmed" || res.status === 1)) { console.log(" 🍾 Transaction " + res.hash + " finished!"); const receipt = { @@ -109,8 +114,10 @@ export const Distribute = ({ amount: total.toHexString(), token: tokenInstance?.address, txn: res.hash, + chainId: targetNetwork.chainId, + strategy: strategy, }; - fetch(`${process.env.REACT_APP_API_URL}/party/${partyData.id}/distribute`, { + await fetch(`${process.env.REACT_APP_API_URL}/party/${partyData.id}/distribute`, { method: "put", headers: { "Content-Type": "application/json" }, body: JSON.stringify(receipt), @@ -119,18 +126,18 @@ export const Distribute = ({ setIsDistributionLoading(false); }; - const handleSafeReceipt = res => { - if (res && res.hash && (res.status === "confirmed" || res.status === 1)) { - console.log(" 🍾 Transaction " + res.hash + " finished!"); - const receipt = { - account: address, - amount: total.toHexString(), - token: tokenInstance?.address, - txn: res.hash, - }; - } - setIsDistributionLoading(false); - }; + // const handleSafeReceipt = res => { + // if (res && res.hash && (res.status === "confirmed" || res.status === 1)) { + // console.log(" 🍾 Transaction " + res.hash + " finished!"); + // const receipt = { + // account: address, + // amount: total.toHexString(), + // token: tokenInstance?.address, + // txn: res.hash, + // }; + // } + // setIsDistributionLoading(false); + // }; // Distribute either Eth, or loaded erc20 const distribute = () => { @@ -180,7 +187,7 @@ export const Distribute = ({ }; // const { sdk, connected, safe } = useSafeAppsSDK(); - // useEffect(async () => { + // useEffect( // console.log(sdk, connected, safe) // const txs = [ @@ -193,7 +200,9 @@ export const Distribute = ({ // ]; // // Returns a hash to identify the Safe transaction // const safeTxHash = await sdk.txs.send({ txs }); - // }, []) + // }, [])\ + + const [strat, setStrat] = useState(); return ( @@ -206,7 +215,14 @@ export const Distribute = ({ - + + Select Strategy + + + Quadratic + Linear + + Amount - + Select a Token (optional) {`${partyData.name}`} +
+ {`Signature: ${partyData.signed.signature.substr( + 0, + 6, + )}...${partyData.signed.signature.substr( + partyData.signed.signature.length - 4, + partyData.signed.signature.length, + )}`} +