From 796af899956d8e93ca3deaedce457d2f1970d84b Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Tue, 12 Mar 2024 12:13:28 +0100 Subject: [PATCH 01/16] feat: first iteration of the Lock To Vote plugin --- constants.ts | 2 + plugins/index.ts | 8 + .../artifacts/LockToVetoPlugin.sol.ts | 635 ++++++++++++ .../OptimisticTokenVotingPlugin.sol.tsx | 942 ++++++++++++++++++ .../components/proposal/description.tsx | 159 +++ .../components/proposal/details.tsx | 60 ++ .../lockToVote/components/proposal/header.tsx | 156 +++ .../lockToVote/components/proposal/index.tsx | 99 ++ plugins/lockToVote/components/vote/tally.tsx | 42 + .../components/vote/vetoes-section.tsx | 43 + .../lockToVote/hooks/useCanCreateProposal.tsx | 45 + plugins/lockToVote/hooks/useProposal.tsx | 154 +++ .../hooks/useProposalVariantStatus.tsx | 21 + .../lockToVote/hooks/useProposalVetoes.tsx | 46 + plugins/lockToVote/hooks/useUserCanVeto.tsx | 23 + plugins/lockToVote/hooks/useVotingToken.tsx | 24 + plugins/lockToVote/index.tsx | 21 + plugins/lockToVote/pages/new.tsx | 321 ++++++ plugins/lockToVote/pages/proposal-list.tsx | 149 +++ plugins/lockToVote/pages/proposal.tsx | 118 +++ plugins/lockToVote/utils/types.tsx | 42 + 21 files changed, 3110 insertions(+) create mode 100644 plugins/lockToVote/artifacts/LockToVetoPlugin.sol.ts create mode 100644 plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol.tsx create mode 100644 plugins/lockToVote/components/proposal/description.tsx create mode 100644 plugins/lockToVote/components/proposal/details.tsx create mode 100644 plugins/lockToVote/components/proposal/header.tsx create mode 100644 plugins/lockToVote/components/proposal/index.tsx create mode 100644 plugins/lockToVote/components/vote/tally.tsx create mode 100644 plugins/lockToVote/components/vote/vetoes-section.tsx create mode 100644 plugins/lockToVote/hooks/useCanCreateProposal.tsx create mode 100644 plugins/lockToVote/hooks/useProposal.tsx create mode 100644 plugins/lockToVote/hooks/useProposalVariantStatus.tsx create mode 100644 plugins/lockToVote/hooks/useProposalVetoes.tsx create mode 100644 plugins/lockToVote/hooks/useUserCanVeto.tsx create mode 100644 plugins/lockToVote/hooks/useVotingToken.tsx create mode 100644 plugins/lockToVote/index.tsx create mode 100644 plugins/lockToVote/pages/new.tsx create mode 100644 plugins/lockToVote/pages/proposal-list.tsx create mode 100644 plugins/lockToVote/pages/proposal.tsx create mode 100644 plugins/lockToVote/utils/types.tsx diff --git a/constants.ts b/constants.ts index d870fc7d..05c6c49c 100644 --- a/constants.ts +++ b/constants.ts @@ -7,6 +7,8 @@ export const PUB_DAO_ADDRESS = (process.env.NEXT_PUBLIC_DAO_ADDRESS ?? export const PUB_TOKEN_ADDRESS = (process.env.NEXT_PUBLIC_TOKEN_ADDRESS ?? "") as Address; +export const PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS = (process.env + .NEXT_PUBLIC_LOCK_TO_VOTE_PLUGIN_ADDRESS ?? "") as Address; export const PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS = (process.env .NEXT_PUBLIC_DUAL_GOVERNANCE_PLUGIN_ADDRESS ?? "") as Address; export const PUB_TOKEN_VOTING_PLUGIN_ADDRESS = (process.env diff --git a/plugins/index.ts b/plugins/index.ts index a1ad746b..896f0e1e 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -2,6 +2,7 @@ import { PUB_DELEGATION_CONTRACT_ADDRESS, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, PUB_TOKEN_VOTING_PLUGIN_ADDRESS, + PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, } from "@/constants"; import { IconType } from "@aragon/ods"; @@ -38,4 +39,11 @@ export const plugins: PluginItem[] = [ icon: IconType.FEEDBACK, pluginAddress: PUB_DELEGATION_CONTRACT_ADDRESS, }, + { + id: "lock-to-vote", + folderName: "lockToVote", + title: "Lock To Vote", + icon: IconType.DEPOSIT, + pluginAddress: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + }, ]; diff --git a/plugins/lockToVote/artifacts/LockToVetoPlugin.sol.ts b/plugins/lockToVote/artifacts/LockToVetoPlugin.sol.ts new file mode 100644 index 00000000..659fef29 --- /dev/null +++ b/plugins/lockToVote/artifacts/LockToVetoPlugin.sol.ts @@ -0,0 +1,635 @@ +import { Abi } from "viem"; +export const LockToVetoPluginAbi: Abi = [ + { + inputs: [ + { internalType: "uint256", name: "proposalId", type: "uint256" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "ClaimLockForbidden", + type: "error", + }, + { + inputs: [ + { internalType: "address", name: "dao", type: "address" }, + { internalType: "address", name: "where", type: "address" }, + { internalType: "address", name: "who", type: "address" }, + { internalType: "bytes32", name: "permissionId", type: "bytes32" }, + ], + name: "DaoUnauthorized", + type: "error", + }, + { + inputs: [ + { internalType: "uint64", name: "limit", type: "uint64" }, + { internalType: "uint64", name: "actual", type: "uint64" }, + ], + name: "DateOutOfBounds", + type: "error", + }, + { + inputs: [ + { internalType: "uint64", name: "limit", type: "uint64" }, + { internalType: "uint64", name: "actual", type: "uint64" }, + ], + name: "MinDurationOutOfBounds", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "limit", type: "uint256" }, + { internalType: "uint256", name: "actual", type: "uint256" }, + ], + name: "MinProposerVotingPowerOutOfBounds", + type: "error", + }, + { inputs: [], name: "NoVotingPower", type: "error" }, + { + inputs: [{ internalType: "address", name: "sender", type: "address" }], + name: "ProposalCreationForbidden", + type: "error", + }, + { + inputs: [{ internalType: "uint256", name: "proposalId", type: "uint256" }], + name: "ProposalExecutionForbidden", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "proposalId", type: "uint256" }, + { internalType: "address", name: "account", type: "address" }, + ], + name: "ProposalVetoingForbidden", + type: "error", + }, + { + inputs: [ + { internalType: "uint256", name: "limit", type: "uint256" }, + { internalType: "uint256", name: "actual", type: "uint256" }, + ], + name: "RatioOutOfBounds", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "previousAdmin", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "AdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beacon", + type: "address", + }, + ], + name: "BeaconUpgraded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: false, internalType: "uint8", name: "version", type: "uint8" }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "voter", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "LockClaimed", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address[]", + name: "members", + type: "address[]", + }, + ], + name: "MembersAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address[]", + name: "members", + type: "address[]", + }, + ], + name: "MembersRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "definingContract", + type: "address", + }, + ], + name: "MembershipContractAnnounced", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint32", + name: "minVetoRatio", + type: "uint32", + }, + { + indexed: false, + internalType: "uint64", + name: "minDuration", + type: "uint64", + }, + { + indexed: false, + internalType: "uint256", + name: "minProposerVotingPower", + type: "uint256", + }, + ], + name: "OptimisticGovernanceSettingsUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "creator", + type: "address", + }, + { + indexed: false, + internalType: "uint64", + name: "startDate", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "endDate", + type: "uint64", + }, + { + indexed: false, + internalType: "bytes", + name: "metadata", + type: "bytes", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + indexed: false, + internalType: "struct IDAO.Action[]", + name: "actions", + type: "tuple[]", + }, + { + indexed: false, + internalType: "uint256", + name: "allowFailureMap", + type: "uint256", + }, + ], + name: "ProposalCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + name: "ProposalExecuted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "implementation", + type: "address", + }, + ], + name: "Upgraded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "voter", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "votingPower", + type: "uint256", + }, + ], + name: "VetoCast", + type: "event", + }, + { + inputs: [], + name: "OPTIMISTIC_GOVERNANCE_INTERFACE_ID", + outputs: [{ internalType: "bytes4", name: "", type: "bytes4" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PROPOSER_PERMISSION_ID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPGRADE_PLUGIN_PERMISSION_ID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "_proposalId", type: "uint256" }], + name: "canExecute", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_proposalId", type: "uint256" }, + { internalType: "address", name: "_voter", type: "address" }, + ], + name: "canVeto", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_proposalId", type: "uint256" }, + { internalType: "address", name: "_member", type: "address" }, + ], + name: "claimLock", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes", name: "_metadata", type: "bytes" }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + internalType: "struct IDAO.Action[]", + name: "_actions", + type: "tuple[]", + }, + { internalType: "uint256", name: "_allowFailureMap", type: "uint256" }, + { internalType: "uint64", name: "_startDate", type: "uint64" }, + { internalType: "uint64", name: "_endDate", type: "uint64" }, + ], + name: "createProposal", + outputs: [{ internalType: "uint256", name: "proposalId", type: "uint256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "dao", + outputs: [{ internalType: "contract IDAO", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "_proposalId", type: "uint256" }], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "_proposalId", type: "uint256" }], + name: "getProposal", + outputs: [ + { internalType: "bool", name: "open", type: "bool" }, + { internalType: "bool", name: "executed", type: "bool" }, + { + components: [ + { internalType: "uint64", name: "startDate", type: "uint64" }, + { internalType: "uint64", name: "endDate", type: "uint64" }, + { internalType: "uint64", name: "snapshotBlock", type: "uint64" }, + { + internalType: "uint256", + name: "minVetoVotingPower", + type: "uint256", + }, + ], + internalType: "struct LockToVetoPlugin.ProposalParameters", + name: "parameters", + type: "tuple", + }, + { internalType: "uint256", name: "vetoTally", type: "uint256" }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "value", type: "uint256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + internalType: "struct IDAO.Action[]", + name: "actions", + type: "tuple[]", + }, + { internalType: "uint256", name: "allowFailureMap", type: "uint256" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getVotingToken", + outputs: [ + { internalType: "contract IERC20Upgradeable", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_proposalId", type: "uint256" }, + { internalType: "address", name: "_voter", type: "address" }, + ], + name: "hasClaimedLock", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_proposalId", type: "uint256" }, + { internalType: "address", name: "_voter", type: "address" }, + ], + name: "hasVetoed", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "implementation", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "contract IDAO", name: "_dao", type: "address" }, + { + components: [ + { internalType: "uint32", name: "minVetoRatio", type: "uint32" }, + { internalType: "uint64", name: "minDuration", type: "uint64" }, + { + internalType: "uint256", + name: "minProposerVotingPower", + type: "uint256", + }, + ], + internalType: "struct LockToVetoPlugin.OptimisticGovernanceSettings", + name: "_governanceSettings", + type: "tuple", + }, + { + internalType: "contract IERC20Upgradeable", + name: "_token", + type: "address", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "_account", type: "address" }], + name: "isMember", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "uint256", name: "_proposalId", type: "uint256" }], + name: "isMinVetoRatioReached", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minDuration", + outputs: [{ internalType: "uint64", name: "", type: "uint64" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minProposerVotingPower", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minVetoRatio", + outputs: [{ internalType: "uint32", name: "", type: "uint32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pluginType", + outputs: [ + { internalType: "enum IPlugin.PluginType", name: "", type: "uint8" }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "proposalCount", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proxiableUUID", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "_interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "totalVotingPower", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "uint32", name: "minVetoRatio", type: "uint32" }, + { internalType: "uint64", name: "minDuration", type: "uint64" }, + { + internalType: "uint256", + name: "minProposerVotingPower", + type: "uint256", + }, + ], + internalType: "struct LockToVetoPlugin.OptimisticGovernanceSettings", + name: "_governanceSettings", + type: "tuple", + }, + ], + name: "updateOptimisticGovernanceSettings", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newImplementation", type: "address" }, + ], + name: "upgradeTo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "newImplementation", type: "address" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_proposalId", type: "uint256" }, + { internalType: "uint256", name: "_amountToLock", type: "uint256" }, + ], + name: "veto", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "_proposalId", type: "uint256" }, + { internalType: "uint256", name: "_amountToLock", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + { internalType: "uint8", name: "v", type: "uint8" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + name: "vetoPermit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol.tsx b/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol.tsx new file mode 100644 index 00000000..e313c41e --- /dev/null +++ b/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol.tsx @@ -0,0 +1,942 @@ +import { Abi } from "viem"; +export const OptimisticTokenVotingPluginAbi: Abi = [ + { + inputs: [ + { + internalType: "address", + name: "dao", + type: "address", + }, + { + internalType: "address", + name: "where", + type: "address", + }, + { + internalType: "address", + name: "who", + type: "address", + }, + { + internalType: "bytes32", + name: "permissionId", + type: "bytes32", + }, + ], + name: "DaoUnauthorized", + type: "error", + }, + { + inputs: [ + { + internalType: "uint64", + name: "limit", + type: "uint64", + }, + { + internalType: "uint64", + name: "actual", + type: "uint64", + }, + ], + name: "DateOutOfBounds", + type: "error", + }, + { + inputs: [ + { + internalType: "uint64", + name: "limit", + type: "uint64", + }, + { + internalType: "uint64", + name: "actual", + type: "uint64", + }, + ], + name: "MinDurationOutOfBounds", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "limit", + type: "uint256", + }, + { + internalType: "uint256", + name: "actual", + type: "uint256", + }, + ], + name: "MinProposerVotingPowerOutOfBounds", + type: "error", + }, + { + inputs: [], + name: "NoVotingPower", + type: "error", + }, + { + inputs: [ + { + internalType: "address", + name: "sender", + type: "address", + }, + ], + name: "ProposalCreationForbidden", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + name: "ProposalExecutionForbidden", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + internalType: "address", + name: "account", + type: "address", + }, + ], + name: "ProposalVetoingForbidden", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "limit", + type: "uint256", + }, + { + internalType: "uint256", + name: "actual", + type: "uint256", + }, + ], + name: "RatioOutOfBounds", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "previousAdmin", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "newAdmin", + type: "address", + }, + ], + name: "AdminChanged", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "beacon", + type: "address", + }, + ], + name: "BeaconUpgraded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint8", + name: "version", + type: "uint8", + }, + ], + name: "Initialized", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address[]", + name: "members", + type: "address[]", + }, + ], + name: "MembersAdded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address[]", + name: "members", + type: "address[]", + }, + ], + name: "MembersRemoved", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "definingContract", + type: "address", + }, + ], + name: "MembershipContractAnnounced", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "uint32", + name: "minVetoRatio", + type: "uint32", + }, + { + indexed: false, + internalType: "uint64", + name: "minDuration", + type: "uint64", + }, + { + indexed: false, + internalType: "uint256", + name: "minProposerVotingPower", + type: "uint256", + }, + ], + name: "OptimisticGovernanceSettingsUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "creator", + type: "address", + }, + { + indexed: false, + internalType: "uint64", + name: "startDate", + type: "uint64", + }, + { + indexed: false, + internalType: "uint64", + name: "endDate", + type: "uint64", + }, + { + indexed: false, + internalType: "bytes", + name: "metadata", + type: "bytes", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + indexed: false, + internalType: "struct IDAO.Action[]", + name: "actions", + type: "tuple[]", + }, + { + indexed: false, + internalType: "uint256", + name: "allowFailureMap", + type: "uint256", + }, + ], + name: "ProposalCreated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + name: "ProposalExecuted", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "implementation", + type: "address", + }, + ], + name: "Upgraded", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + { + indexed: true, + internalType: "address", + name: "voter", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "votingPower", + type: "uint256", + }, + ], + name: "VetoCast", + type: "event", + }, + { + inputs: [], + name: "OPTIMISTIC_GOVERNANCE_INTERFACE_ID", + outputs: [ + { + internalType: "bytes4", + name: "", + type: "bytes4", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "PROPOSER_PERMISSION_ID", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPDATE_OPTIMISTIC_GOVERNANCE_SETTINGS_PERMISSION_ID", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "UPGRADE_PLUGIN_PERMISSION_ID", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_proposalId", + type: "uint256", + }, + ], + name: "canExecute", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_proposalId", + type: "uint256", + }, + { + internalType: "address", + name: "_voter", + type: "address", + }, + ], + name: "canVeto", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes", + name: "_metadata", + type: "bytes", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct IDAO.Action[]", + name: "_actions", + type: "tuple[]", + }, + { + internalType: "uint256", + name: "_allowFailureMap", + type: "uint256", + }, + { + internalType: "uint64", + name: "_startDate", + type: "uint64", + }, + { + internalType: "uint64", + name: "_endDate", + type: "uint64", + }, + ], + name: "createProposal", + outputs: [ + { + internalType: "uint256", + name: "proposalId", + type: "uint256", + }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "dao", + outputs: [ + { + internalType: "contract IDAO", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_proposalId", + type: "uint256", + }, + ], + name: "execute", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_proposalId", + type: "uint256", + }, + ], + name: "getProposal", + outputs: [ + { + internalType: "bool", + name: "open", + type: "bool", + }, + { + internalType: "bool", + name: "executed", + type: "bool", + }, + { + components: [ + { + internalType: "uint64", + name: "startDate", + type: "uint64", + }, + { + internalType: "uint64", + name: "endDate", + type: "uint64", + }, + { + internalType: "uint64", + name: "snapshotBlock", + type: "uint64", + }, + { + internalType: "uint256", + name: "minVetoVotingPower", + type: "uint256", + }, + ], + internalType: "struct OptimisticTokenVotingPlugin.ProposalParameters", + name: "parameters", + type: "tuple", + }, + { + internalType: "uint256", + name: "vetoTally", + type: "uint256", + }, + { + components: [ + { + internalType: "address", + name: "to", + type: "address", + }, + { + internalType: "uint256", + name: "value", + type: "uint256", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + internalType: "struct IDAO.Action[]", + name: "actions", + type: "tuple[]", + }, + { + internalType: "uint256", + name: "allowFailureMap", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "getVotingToken", + outputs: [ + { + internalType: "contract IVotesUpgradeable", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_proposalId", + type: "uint256", + }, + { + internalType: "address", + name: "_voter", + type: "address", + }, + ], + name: "hasVetoed", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "implementation", + outputs: [ + { + internalType: "address", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "contract IDAO", + name: "_dao", + type: "address", + }, + { + components: [ + { + internalType: "uint32", + name: "minVetoRatio", + type: "uint32", + }, + { + internalType: "uint64", + name: "minDuration", + type: "uint64", + }, + { + internalType: "uint256", + name: "minProposerVotingPower", + type: "uint256", + }, + ], + internalType: + "struct OptimisticTokenVotingPlugin.OptimisticGovernanceSettings", + name: "_governanceSettings", + type: "tuple", + }, + { + internalType: "contract IVotesUpgradeable", + name: "_token", + type: "address", + }, + ], + name: "initialize", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "_account", + type: "address", + }, + ], + name: "isMember", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_proposalId", + type: "uint256", + }, + ], + name: "isMinVetoRatioReached", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minDuration", + outputs: [ + { + internalType: "uint64", + name: "", + type: "uint64", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minProposerVotingPower", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "minVetoRatio", + outputs: [ + { + internalType: "uint32", + name: "", + type: "uint32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "pluginType", + outputs: [ + { + internalType: "enum IPlugin.PluginType", + name: "", + type: "uint8", + }, + ], + stateMutability: "pure", + type: "function", + }, + { + inputs: [], + name: "proposalCount", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "proxiableUUID", + outputs: [ + { + internalType: "bytes32", + name: "", + type: "bytes32", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "bytes4", + name: "_interfaceId", + type: "bytes4", + }, + ], + name: "supportsInterface", + outputs: [ + { + internalType: "bool", + name: "", + type: "bool", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_blockNumber", + type: "uint256", + }, + ], + name: "totalVotingPower", + outputs: [ + { + internalType: "uint256", + name: "", + type: "uint256", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { + internalType: "uint32", + name: "minVetoRatio", + type: "uint32", + }, + { + internalType: "uint64", + name: "minDuration", + type: "uint64", + }, + { + internalType: "uint256", + name: "minProposerVotingPower", + type: "uint256", + }, + ], + internalType: + "struct OptimisticTokenVotingPlugin.OptimisticGovernanceSettings", + name: "_governanceSettings", + type: "tuple", + }, + ], + name: "updateOptimisticGovernanceSettings", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + ], + name: "upgradeTo", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + internalType: "address", + name: "newImplementation", + type: "address", + }, + { + internalType: "bytes", + name: "data", + type: "bytes", + }, + ], + name: "upgradeToAndCall", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { + internalType: "uint256", + name: "_proposalId", + type: "uint256", + }, + ], + name: "veto", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/plugins/lockToVote/components/proposal/description.tsx b/plugins/lockToVote/components/proposal/description.tsx new file mode 100644 index 00000000..f8c28978 --- /dev/null +++ b/plugins/lockToVote/components/proposal/description.tsx @@ -0,0 +1,159 @@ +import { Action } from "@/utils/types"; +import { Proposal } from "@/plugins/lockToVote/utils/types"; +import { whatsabi } from "@shazow/whatsabi"; +import { ReactNode, useCallback, useEffect, useState } from "react"; +import { usePublicClient } from "wagmi"; +import { Address, decodeFunctionData } from "viem"; +import { Else, If, IfCase, IfNot, Then } from "@/components/if"; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { AddressText } from "@/components/text/address"; +import { isAddress } from "@/utils/evm"; +import * as DOMPurify from "dompurify"; +import { PUB_CHAIN, PUB_ETHERSCAN_API_KEY } from "@/constants"; + +const DEFAULT_PROPOSAL_SUMMARY = "(No description available)"; + +type FunctionData = { + args: readonly unknown[] | undefined; + functionName: string; + to: Address; +}; + +export default function ProposalDescription(proposal: Proposal) { + const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); + const [decodedActions, setDecodedActions] = useState([]); + const proposalActions = proposal?.actions || []; + + const getFunctionData = async (action: Action) => { + if (!publicClient) return; + + const abiLoader = new whatsabi.loaders.EtherscanABILoader({ + apiKey: PUB_ETHERSCAN_API_KEY, + }); + + const { abi } = await whatsabi.autoload(action.to, { + provider: publicClient, + abiLoader, + followProxies: true, + }); + + return decodeFunctionData({ + abi, + data: action.data as Address, + }); + }; + + const fetchActionData = useCallback(async () => { + const decodedActions = await Promise.all( + proposalActions.map(async (action) => { + let functionData: any; + if (action.data != "0x") { + functionData = await getFunctionData(action); + } else { + functionData = { functionName: "transfer", args: [action.value] }; + } + return { ...functionData, to: action.to } as FunctionData; + }) + ); + setDecodedActions(decodedActions); + }, [proposal]); + + useEffect(() => { + fetchActionData(); + }, [proposal.actions]); + + return ( +
+
+

+ Actions +

+
+ +

The proposal has no actions

+
+ + + + {decodedActions?.map?.((action, i) => ( + + ))} +
+
+ ); +} + +// This should be encapsulated as soon as ODS exports this widget +const Card = function ({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +}; + +const ActionCard = function ({ + action, + idx, +}: { + action: FunctionData; + idx: number; +}) { + return ( + +
+
+

Target contract

+

+ {action.to} +

+
+
+ {idx + 1} +
+
+ +
+

Function name

+

+ {action.functionName} +

+
+ +
+

Parameters

+
    + {action?.args?.length && + action?.args?.map((arg: any, j: number) => ( +
  • + + + {arg.toString()} + + + {arg.toString()} + + +
  • + ))} +
+
+
+ ); +}; diff --git a/plugins/lockToVote/components/proposal/details.tsx b/plugins/lockToVote/components/proposal/details.tsx new file mode 100644 index 00000000..022b98c7 --- /dev/null +++ b/plugins/lockToVote/components/proposal/details.tsx @@ -0,0 +1,60 @@ +import dayjs from "dayjs"; +import { ReactNode } from "react"; + +interface ProposalDetailsProps { + minVetoVotingPower?: bigint; + endDate?: bigint; + snapshotBlock?: bigint; +} + +const ProposalDetails: React.FC = ({ + /** Timestamp */ + endDate, + snapshotBlock, +}) => { + + return ( + <> + +

+ Ending +

+
+ + {dayjs(Number(endDate) * 1000).format("DD/MM/YYYY")} + +

+ {dayjs(Number(endDate) * 1000).format("HH:mm")}h +

+
+
+ +

+ Snapshot +

+
+

Taken at block

+ + {snapshotBlock?.toLocaleString()} + +
+
+ + ); +}; + +// This should be encapsulated as soon as ODS exports this widget +const Card = function ({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +}; + +export default ProposalDetails; diff --git a/plugins/lockToVote/components/proposal/header.tsx b/plugins/lockToVote/components/proposal/header.tsx new file mode 100644 index 00000000..39bab433 --- /dev/null +++ b/plugins/lockToVote/components/proposal/header.tsx @@ -0,0 +1,156 @@ +import { useEffect } from "react"; +import { Button, Tag } from "@aragon/ods"; +import { Proposal } from "@/plugins/lockToVote/utils/types"; +import { AlertVariant } from "@aragon/ods"; +import { Else, If, IfCase, Then } from "@/components/if"; +import { AddressText } from "@/components/text/address"; +import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { OptimisticTokenVotingPluginAbi } from "../../artifacts/OptimisticTokenVotingPlugin.sol"; +import { AlertContextProps, useAlertContext } from "@/context/AlertContext"; +import { useProposalVariantStatus } from "../../hooks/useProposalVariantStatus"; +import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { useRouter } from "next/router"; + +const DEFAULT_PROPOSAL_TITLE = "(No proposal title)"; + +interface ProposalHeaderProps { + proposalNumber: number; + proposal: Proposal; + userCanVeto: boolean; + transactionLoading: boolean; + onVetoPressed: () => void; +} + +const ProposalHeader: React.FC = ({ + proposalNumber, + proposal, + userCanVeto, + transactionLoading, + onVetoPressed, +}) => { + const { reload } = useRouter(); + const { addAlert } = useAlertContext() as AlertContextProps; + const proposalVariant = useProposalVariantStatus(proposal); + + const { + writeContract: executeWrite, + data: executeTxHash, + error, + status, + } = useWriteContract(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ hash: executeTxHash }); + + const executeButtonPressed = () => { + executeWrite({ + chainId: PUB_CHAIN.id, + abi: OptimisticTokenVotingPluginAbi, + address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + functionName: "execute", + args: [proposalNumber], + }); + }; + + useEffect(() => { + if (status === "idle" || status === "pending") return; + else if (status === "error") { + if (error?.message?.startsWith("User rejected the request")) { + addAlert("Transaction rejected by the user", { + timeout: 4 * 1000, + }); + } else { + console.error(error); + addAlert("Could not execute the proposal", { type: "error" }); + } + return; + } + + // success + if (!executeTxHash) return; + else if (isConfirming) { + addAlert("Proposal submitted", { + description: "Waiting for the transaction to be validated", + type: "info", + txHash: executeTxHash, + }); + return; + } else if (!isConfirmed) return; + + addAlert("Proposal executed", { + description: "The transaction has been validated", + type: "success", + txHash: executeTxHash, + }); + + setTimeout(() => reload(), 1000 * 2); + }, [status, executeTxHash, isConfirming, isConfirmed]); + + return ( +
+
+
+
+ {/** bg-info-200 bg-success-200 bg-critical-200 + * text-info-800 text-success-800 text-critical-800 + */} +
+ +
+ + Proposal {proposalNumber + 1} + +
+
+
+ + + + + + + +
+ +
+
+
+
+ + + + + +
+
+
+ +

+ {proposal.title || DEFAULT_PROPOSAL_TITLE} +

+

+ Proposed by {proposal?.creator} +

+
+ ); +}; + +export default ProposalHeader; diff --git a/plugins/lockToVote/components/proposal/index.tsx b/plugins/lockToVote/components/proposal/index.tsx new file mode 100644 index 00000000..7af96a8c --- /dev/null +++ b/plugins/lockToVote/components/proposal/index.tsx @@ -0,0 +1,99 @@ +import Link from "next/link"; +import { usePublicClient } from "wagmi"; +import { useProposal } from "@/plugins/lockToVote/hooks/useProposal"; +import { Card, Tag, TagVariant } from "@aragon/ods"; +import * as DOMPurify from 'dompurify'; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { useProposalVariantStatus } from "../../hooks/useProposalVariantStatus"; +import { PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; + +const DEFAULT_PROPOSAL_METADATA_TITLE = "(No proposal title)"; +const DEFAULT_PROPOSAL_METADATA_SUMMARY = + "(The metadata of the proposal is not available)"; + +type ProposalInputs = { + proposalId: bigint; +}; + +export default function ProposalCard(props: ProposalInputs) { + const publicClient = usePublicClient(); + const { proposal, status } = useProposal( + publicClient!, + PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + props.proposalId.toString() + ); + const proposalVariant = useProposalVariantStatus(proposal!); + + const showLoading = getShowProposalLoading(proposal, status); + + if (!proposal || showLoading) { + return ( +
+ + + + + +
+ ); + } else if (status.metadataReady && !proposal?.title) { + return ( + + +
+

+ {Number(props.proposalId) + 1} -{" "} + {DEFAULT_PROPOSAL_METADATA_TITLE} +

+

+ {DEFAULT_PROPOSAL_METADATA_SUMMARY} +

+
+
+ + ); + } + + return ( + + +
+ +
+
+

+ {Number(props.proposalId) + 1} - {proposal.title} +

+
+
+ + + ); +} + +function getShowProposalLoading( + proposal: ReturnType["proposal"], + status: ReturnType["status"] +) { + if (!proposal || status.proposalLoading) return true; + else if (status.metadataLoading && !status.metadataError) return true; + else if (!proposal?.title && !status.metadataError) return true; + + return false; +} diff --git a/plugins/lockToVote/components/vote/tally.tsx b/plugins/lockToVote/components/vote/tally.tsx new file mode 100644 index 00000000..cbd873c1 --- /dev/null +++ b/plugins/lockToVote/components/vote/tally.tsx @@ -0,0 +1,42 @@ +import { compactNumber } from "@/utils/numbers"; +import { FC, ReactNode } from "react"; +import { formatUnits } from "viem"; + +interface VoteTallyProps { + voteCount: bigint; + votePercentage: number; +} + +const VetoTally: FC = ({ voteCount, votePercentage }) => ( + +
+

+ Vetoed +

+

+ {compactNumber(formatUnits(voteCount + BigInt(1) || BigInt(0), 18))} +

+
+
+
+
+ +); + +// This should be encapsulated as soon as ODS exports this widget +const Card = function ({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +}; + +export default VetoTally; diff --git a/plugins/lockToVote/components/vote/vetoes-section.tsx b/plugins/lockToVote/components/vote/vetoes-section.tsx new file mode 100644 index 00000000..90f509eb --- /dev/null +++ b/plugins/lockToVote/components/vote/vetoes-section.tsx @@ -0,0 +1,43 @@ +import Blockies from "react-blockies"; +import { VetoCastEvent } from "@/plugins/lockToVote/utils/types"; +import { formatUnits } from "viem"; +import { AddressText } from "@/components/text/address"; +import { Card, Tag } from "@aragon/ods"; +import { compactNumber } from "@/utils/numbers"; + +export default function VetoesSection({ + vetoes, +}: { + vetoes: Array; +}) { + return ( +
+
+
+ {vetoes.map((veto, i) => ( + + ))} +
+
+
+ ); +} + +const VetoCard = function ({ veto }: { veto: VetoCastEvent }) { + return ( + +
+
+ +
+ {veto?.voter} +

+ {compactNumber(formatUnits(veto.votingPower, 18))} votes +

+
+
+ +
+
+ ); +}; diff --git a/plugins/lockToVote/hooks/useCanCreateProposal.tsx b/plugins/lockToVote/hooks/useCanCreateProposal.tsx new file mode 100644 index 00000000..b110e32f --- /dev/null +++ b/plugins/lockToVote/hooks/useCanCreateProposal.tsx @@ -0,0 +1,45 @@ +import { Address } from 'viem' +import { useState, useEffect } from 'react' +import { useBalance, useAccount, useReadContracts } from 'wagmi'; +import { OptimisticTokenVotingPluginAbi } from '@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol'; +import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from '@/constants'; + +export function useCanCreateProposal() { + const [isCreator, setIsCreator] = useState(false); + const [minProposerVotingPower, setMinProposerVotingPower] = useState(); + const [votingToken, setVotingToken] = useState
(); + const { address, isConnecting, isDisconnected } = useAccount() + const {data: balance} = useBalance({ address, token: votingToken, chainId: PUB_CHAIN.id }) + + const { data: contractReads } = useReadContracts({ + contracts: [ + { + chainId: PUB_CHAIN.id, + address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + abi: OptimisticTokenVotingPluginAbi, + functionName: 'minProposerVotingPower', + }, + { + chainId: PUB_CHAIN.id, + address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + abi: OptimisticTokenVotingPluginAbi, + functionName: 'getVotingToken', + } + // TODO: This needs to be checking as well if address has the DAO permission to create props + ] + }) + + useEffect(() => { + if (contractReads?.length) { + setMinProposerVotingPower(contractReads[0]?.result as bigint) + + setVotingToken(contractReads[1]?.result as Address) + } + }, [contractReads]) + + useEffect(() => { + if ( balance !== undefined && minProposerVotingPower !== undefined && balance?.value >= minProposerVotingPower) setIsCreator(true) + }, [balance]) + + return isCreator +} diff --git a/plugins/lockToVote/hooks/useProposal.tsx b/plugins/lockToVote/hooks/useProposal.tsx new file mode 100644 index 00000000..d2b06aa9 --- /dev/null +++ b/plugins/lockToVote/hooks/useProposal.tsx @@ -0,0 +1,154 @@ +import { useState, useEffect } from "react"; +import { Address } from "viem"; +import { useBlockNumber, useReadContract } from "wagmi"; +import { fetchJsonFromIpfs } from "@/utils/ipfs"; +import { PublicClient, getAbiItem } from "viem"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; +import { Action } from "@/utils/types"; +import { + Proposal, + ProposalMetadata, + ProposalParameters, +} from "@/plugins/dualGovernance/utils/types"; +import { useQuery } from "@tanstack/react-query"; +import { PUB_CHAIN } from "@/constants"; + +type ProposalCreatedLogResponse = { + args: { + actions: Action[]; + allowFailureMap: bigint; + creator: string; + endDate: bigint; + startDate: bigint; + metadata: string; + proposalId: bigint; + }; +}; + +const ProposalCreatedEvent = getAbiItem({ + abi: OptimisticTokenVotingPluginAbi, + name: "ProposalCreated", +}); + +export function useProposal( + publicClient: PublicClient, + address: Address, + proposalId: string, + autoRefresh = false +) { + const [proposalCreationEvent, setProposalCreationEvent] = + useState(); + const [metadataUri, setMetadata] = useState(); + const { data: blockNumber} = useBlockNumber(); + + // Proposal on-chain data + const { + data: proposalResult, + error: proposalError, + fetchStatus: proposalFetchStatus, + refetch: proposalRefetch + } = useReadContract({ + address, + abi: OptimisticTokenVotingPluginAbi, + functionName: "getProposal", + args: [proposalId], + chainId: PUB_CHAIN.id, + }); + const proposalData = decodeProposalResultData(proposalResult as any); + + useEffect(() => { + if (autoRefresh) proposalRefetch() + }, [blockNumber]) + + // Creation event + useEffect(() => { + if (!proposalData) return; + publicClient + .getLogs({ + address, + event: ProposalCreatedEvent as any, + args: { + proposalId, + } as any, + fromBlock: proposalData.parameters.snapshotBlock, + toBlock: proposalData.parameters.startDate, + }) + .then((logs) => { + if (!logs || !logs.length) throw new Error("No creation logs"); + + const log: ProposalCreatedLogResponse = logs[0] as any; + setProposalCreationEvent(log.args); + setMetadata(log.args.metadata); + }) + .catch((err) => { + console.error("Could not fetch the proposal defailt", err); + return null; + }); + }, [proposalData?.vetoTally]); + + // JSON metadata + const { + data: metadataContent, + isLoading: metadataLoading, + isSuccess: metadataReady, + error: metadataError, + } = useQuery({ + queryKey: [`dualGovernanceProposal-${address}-${proposalId}`, metadataUri!], + queryFn: () => metadataUri ? fetchJsonFromIpfs(metadataUri) : Promise.resolve(null), + enabled: !!metadataUri +}); + + const proposal = arrangeProposalData( + proposalData, + proposalCreationEvent, + metadataContent + ); + + return { + proposal, + status: { + proposalReady: proposalFetchStatus === "idle", + proposalLoading: proposalFetchStatus === "fetching", + proposalError, + metadataReady, + metadataLoading, + metadataError: metadataError !== undefined, + }, + }; +} + +// Helpers + +function decodeProposalResultData(data?: Array) { + if (!data?.length || data.length < 6) return null; + + return { + active: data[0] as boolean, + executed: data[1] as boolean, + parameters: data[2] as ProposalParameters, + vetoTally: data[3] as bigint, + actions: data[4] as Array, + allowFailureMap: data[5] as bigint, + }; +} + +function arrangeProposalData( + proposalData?: ReturnType, + creationEvent?: ProposalCreatedLogResponse["args"], + metadata?: ProposalMetadata +): Proposal | null { + if (!proposalData) return null; + + return { + actions: proposalData.actions, + active: proposalData.active, + executed: proposalData.executed, + parameters: proposalData.parameters, + vetoTally: proposalData.vetoTally, + allowFailureMap: proposalData.allowFailureMap, + creator: creationEvent?.creator || "", + title: metadata?.title || "", + summary: metadata?.summary || "", + resources: metadata?.resources || [], + }; +} diff --git a/plugins/lockToVote/hooks/useProposalVariantStatus.tsx b/plugins/lockToVote/hooks/useProposalVariantStatus.tsx new file mode 100644 index 00000000..174d1259 --- /dev/null +++ b/plugins/lockToVote/hooks/useProposalVariantStatus.tsx @@ -0,0 +1,21 @@ +import { useState, useEffect } from 'react'; +import { Proposal } from '@/plugins/lockToVote/utils/types'; + +export const useProposalVariantStatus = (proposal: Proposal) => { + const [status, setStatus] = useState({ variant: '', label: '' }); + + useEffect(() => { + if (!proposal || !proposal?.parameters) return; + setStatus( + proposal?.vetoTally >= proposal?.parameters?.minVetoVotingPower + ? { variant: 'critical', label: 'Defeated' } + : proposal?.active + ? { variant: 'primary', label: 'Active' } + : proposal?.executed + ? { variant: 'success', label: 'Executed' } + : { variant: 'success', label: 'Executable' } + ); + }, [proposal?.vetoTally, proposal?.active, proposal?.executed, proposal?.parameters?.minVetoVotingPower]); + + return status; +} diff --git a/plugins/lockToVote/hooks/useProposalVetoes.tsx b/plugins/lockToVote/hooks/useProposalVetoes.tsx new file mode 100644 index 00000000..cd0e780e --- /dev/null +++ b/plugins/lockToVote/hooks/useProposalVetoes.tsx @@ -0,0 +1,46 @@ +import { useState, useEffect } from "react"; +import { Address, getAbiItem } from "viem"; +import { PublicClient } from "viem"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; +import { + Proposal, + VetoCastEvent, + VoteCastResponse, +} from "@/plugins/lockToVote/utils/types"; + +const event = getAbiItem({ + abi: OptimisticTokenVotingPluginAbi, + name: "VetoCast", +}); + +export function useProposalVetoes( + publicClient: PublicClient, + address: Address, + proposalId: string, + proposal: Proposal | null +) { + const [proposalLogs, setLogs] = useState([]); + + async function getLogs() { + if (!proposal?.parameters?.snapshotBlock) return; + + const logs: VoteCastResponse[] = (await publicClient.getLogs({ + address, + event: event as any, + args: { + proposalId, + } as any, + fromBlock: proposal.parameters.snapshotBlock, + toBlock: "latest", // TODO: Make this variable between 'latest' and proposal last block + })) as any; + + const newLogs = logs.flatMap((log) => log.args); + if (newLogs.length > proposalLogs.length) setLogs(newLogs); + } + + useEffect(() => { + getLogs(); + }, [proposal?.parameters?.snapshotBlock]); + + return proposalLogs; +} diff --git a/plugins/lockToVote/hooks/useUserCanVeto.tsx b/plugins/lockToVote/hooks/useUserCanVeto.tsx new file mode 100644 index 00000000..eeaf1d0a --- /dev/null +++ b/plugins/lockToVote/hooks/useUserCanVeto.tsx @@ -0,0 +1,23 @@ +import { useAccount, useBlockNumber, useReadContract } from "wagmi"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; +import { useEffect } from "react"; +import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; + +export function useUserCanVeto(proposalId: bigint) { + const { address } = useAccount(); + const { data: blockNumber } = useBlockNumber({ watch: true }); + + const { data: canVeto, refetch: canVetoRefetch } = useReadContract({ + chainId: PUB_CHAIN.id, + address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + abi: OptimisticTokenVotingPluginAbi, + functionName: "canVeto", + args: [proposalId, address], + }); + + useEffect(() => { + canVetoRefetch(); + }, [blockNumber]); + + return canVeto; +} diff --git a/plugins/lockToVote/hooks/useVotingToken.tsx b/plugins/lockToVote/hooks/useVotingToken.tsx new file mode 100644 index 00000000..2b4a6309 --- /dev/null +++ b/plugins/lockToVote/hooks/useVotingToken.tsx @@ -0,0 +1,24 @@ +import { erc20Abi } from "viem"; +import { useReadContract } from "wagmi"; +import { PUB_TOKEN_ADDRESS } from "@/constants"; + +export function useVotingToken() { + const { + data: tokenSupply, + isError, + isLoading, + } = useReadContract({ + address: PUB_TOKEN_ADDRESS, + abi: erc20Abi, + functionName: "totalSupply", + }); + + return { + address: PUB_TOKEN_ADDRESS, + tokenSupply, + status: { + isLoading, + isError, + }, + }; +} diff --git a/plugins/lockToVote/index.tsx b/plugins/lockToVote/index.tsx new file mode 100644 index 00000000..8566156d --- /dev/null +++ b/plugins/lockToVote/index.tsx @@ -0,0 +1,21 @@ +import { NotFound } from "@/components/not-found"; +import ProposalCreate from "./pages/new"; +import ProposalList from "./pages/proposal-list"; +import ProposalDetail from "./pages/proposal"; +import { useUrl } from "@/hooks/useUrl"; + +export default function PluginPage() { + // Select the inner pages to display depending on the URL hash + const { hash } = useUrl(); + + if (!hash || hash === "#/") return ; + else if (hash === "#/new") return ; + else if (hash.startsWith("#/proposals/")) { + const id = hash.replace("#/proposals/", ""); + + return ; + } + + // Default not found page + return ; +} diff --git a/plugins/lockToVote/pages/new.tsx b/plugins/lockToVote/pages/new.tsx new file mode 100644 index 00000000..5b503669 --- /dev/null +++ b/plugins/lockToVote/pages/new.tsx @@ -0,0 +1,321 @@ +import { create } from "ipfs-http-client"; +import { + Button, + IconType, + Icon, + InputText, + TextAreaRichText, +} from "@aragon/ods"; +import React, { useEffect, useState } from "react"; +import { uploadToIPFS } from "@/utils/ipfs"; +import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; +import { toHex } from "viem"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; +import { useAlertContext } from "@/context/AlertContext"; +import WithdrawalInput from "@/components/input/withdrawal"; +import { FunctionCallForm } from "@/components/input/function-call-form"; +import { Action } from "@/utils/types"; +import { getPlainText } from "@/utils/html"; +import { useRouter } from "next/router"; +import { Else, ElseIf, If, Then } from "@/components/if"; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { + PUB_IPFS_ENDPOINT, + PUB_IPFS_API_KEY, + PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + PUB_CHAIN, +} from "@/constants"; +import { ActionCard } from "@/components/actions/action"; + +enum ActionType { + Signaling, + Withdrawal, + Custom, +} + +const ipfsClient = create({ + url: PUB_IPFS_ENDPOINT, + headers: { "X-API-KEY": PUB_IPFS_API_KEY, Accept: "application/json" }, +}); + +export default function Create() { + const { push } = useRouter(); + const [title, setTitle] = useState(""); + const [summary, setSummary] = useState(""); + const [actions, setActions] = useState([]); + const { addAlert } = useAlertContext(); + const { + writeContract: createProposalWrite, + data: createTxHash, + error, + status, + } = useWriteContract(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ hash: createTxHash }); + const [actionType, setActionType] = useState( + ActionType.Signaling + ); + + const changeActionType = (actionType: ActionType) => { + setActions([]); + setActionType(actionType); + }; + + useEffect(() => { + if (status === "idle" || status === "pending") return; + else if (status === "error") { + if (error?.message?.startsWith("User rejected the request")) { + addAlert("Transaction rejected by the user", { + timeout: 4 * 1000, + }); + } else { + addAlert("Could not create the proposal", { type: "error" }); + } + return; + } + + // success + if (!createTxHash) return; + else if (isConfirming) { + addAlert("Proposal submitted", { + description: "Waiting for the transaction to be validated", + txHash: createTxHash, + }); + return; + } else if (!isConfirmed) return; + + addAlert("Proposal created", { + description: "The transaction has been validated", + type: "success", + txHash: createTxHash, + }); + setTimeout(() => { + push("#/"); + }, 1000 * 2); + }, [status, createTxHash, isConfirming, isConfirmed]); + + const submitProposal = async () => { + // Check metadata + if (!title.trim()) + return addAlert("Invalid proposal details", { + description: "Please, enter a title", + type: "error", + }); + + const plainSummary = getPlainText(summary).trim(); + if (!plainSummary.trim()) + return addAlert("Invalid proposal details", { + description: "Please, enter a summary of what the proposal is about", + type: "error", + }); + + // Check the action + switch (actionType) { + case ActionType.Signaling: + break; + case ActionType.Withdrawal: + if (!actions.length) { + return addAlert("Invalid proposal details", { + description: + "Please ensure that the withdrawal address and the amount to transfer are valid", + type: "error", + }); + } + break; + default: + if (!actions.length || !actions[0].data || actions[0].data === "0x") { + return addAlert("Invalid proposal details", { + description: + "Please ensure that the values of the action to execute are complete and correct", + type: "error", + }); + } + } + + const proposalMetadataJsonObject = { title, summary }; + const blob = new Blob([JSON.stringify(proposalMetadataJsonObject)], { + type: "application/json", + }); + + const ipfsPin = await uploadToIPFS(ipfsClient, blob); + createProposalWrite({ + chainId: PUB_CHAIN.id, + abi: OptimisticTokenVotingPluginAbi, + address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + functionName: "createProposal", + args: [toHex(ipfsPin), actions, 0, 0, 0], + }); + }; + + const handleTitleInput = (event: React.ChangeEvent) => { + setTitle(event?.target?.value); + }; + + const showLoading = status === "pending" || isConfirming; + + return ( +
+
+

+ Create Proposal +

+
+ +
+
+ +
+
+ + Select the type of proposal + +
+
{ + changeActionType(ActionType.Signaling); + }} + className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${ + actionType === ActionType.Signaling + ? "border-primary-300" + : "border-neutral-100" + }`} + > + + + Signaling + +
+
changeActionType(ActionType.Withdrawal)} + className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${ + actionType === ActionType.Withdrawal + ? "border-primary-300" + : "border-neutral-100" + }`} + > + + + DAO Payment + +
+
changeActionType(ActionType.Custom)} + className={`rounded-xl border border-solid border-2 bg-neutral-0 hover:bg-neutral-50 flex flex-col items-center cursor-pointer ${ + actionType === ActionType.Custom + ? "border-primary-300" + : "border-neutral-100" + }`} + > + + + Custom action + +
+
+
+ {actionType === ActionType.Withdrawal && ( + + )} + {actionType === ActionType.Custom && ( + setActions(actions.concat([action]))} + /> + )} +
+
+ + + +
+ +
+
+ + + + +
+ + +

Add the first action to continue

+
+ +

+ Actions +

+
+ {actions?.map?.((action, i) => ( +
+ +
+ ))} +
+
+
+ +
+
+
+
+
+ ); +} diff --git a/plugins/lockToVote/pages/proposal-list.tsx b/plugins/lockToVote/pages/proposal-list.tsx new file mode 100644 index 00000000..11ed2897 --- /dev/null +++ b/plugins/lockToVote/pages/proposal-list.tsx @@ -0,0 +1,149 @@ +import { useAccount, useBlockNumber, useReadContract } from "wagmi"; +import { ReactNode, useEffect, useState } from "react"; +import ProposalCard from "@/plugins/dualGovernance/components/proposal"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; +import { Button, CardEmptyState, IconType } from "@aragon/ods"; +import { useCanCreateProposal } from "@/plugins/dualGovernance/hooks/useCanCreateProposal"; +import Link from "next/link"; +import { Else, ElseIf, If, Then } from "@/components/if"; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { useSkipFirstRender } from "@/hooks/useSkipFirstRender"; +import { PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, PUB_CHAIN } from "@/constants"; +import { digestPagination } from "@/utils/pagination"; +import { useWeb3Modal } from "@web3modal/wagmi/react"; +import { useRouter } from "next/router"; + +export default function Proposals() { + const { isConnected } = useAccount(); + const { open } = useWeb3Modal(); + const { push } = useRouter(); + + const { data: blockNumber } = useBlockNumber({ watch: true }); + const canCreate = useCanCreateProposal(); + const [currentPage, setCurrentPage] = useState(0); + + const { + data: proposalCountResponse, + isLoading, + refetch, + } = useReadContract({ + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + abi: OptimisticTokenVotingPluginAbi, + functionName: "proposalCount", + chainId: PUB_CHAIN.id, + }); + + useEffect(() => { + refetch(); + }, [blockNumber]); + + const skipRender = useSkipFirstRender(); + if (skipRender) return <>; + + const proposalCount = Number(proposalCountResponse); + const { visibleProposalIds, showNext, showPrev } = digestPagination( + proposalCount, + currentPage + ); + + return ( + + +

+ Proposals +

+
+ + + + + +
+
+ + + {visibleProposalIds.map((id) => ( + + ))} +
+ + +
+
+ + + + + + + + push("#/new"), + }} + /> + + + + + open(), + }} + /> + + +
+
+ ); +} + +function MainSection({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} + +function SectionView({ children }: { children: ReactNode }) { + return ( +
+ {children} +
+ ); +} diff --git a/plugins/lockToVote/pages/proposal.tsx b/plugins/lockToVote/pages/proposal.tsx new file mode 100644 index 00000000..cc3432d8 --- /dev/null +++ b/plugins/lockToVote/pages/proposal.tsx @@ -0,0 +1,118 @@ +import { useProposal } from "@/plugins/lockToVote/hooks/useProposal"; +import { ToggleGroup, Toggle } from "@aragon/ods"; +import ProposalDescription from "@/plugins/dualGovernance/components/proposal/description"; +import VetoesSection from "@/plugins/dualGovernance/components/vote/vetoes-section"; +import ProposalHeader from "@/plugins/dualGovernance/components/proposal/header"; +import VetoTally from "@/plugins/dualGovernance/components/vote/tally"; +import ProposalDetails from "@/plugins/lockToVote/components/proposal/details"; +import { Else, If, Then } from "@/components/if"; +import { PleaseWaitSpinner } from "@/components/please-wait"; +import { useSkipFirstRender } from "@/hooks/useSkipFirstRender"; +import { useState } from "react"; +import { useProposalVeto } from "@/plugins/dualGovernance/hooks/useProposalVeto"; +import { useProposalExecute } from "@/plugins/dualGovernance/hooks/useProposalExecute"; + +type BottomSection = "description" | "vetoes"; + +export default function ProposalDetail({ id: proposalId }: { id: string }) { + const skipRender = useSkipFirstRender(); + const [bottomSection, setBottomSection] = + useState("description"); + + const { + proposal, + proposalFetchStatus, + vetoes, + canVeto, + isConfirming: isConfirmingVeto, + vetoProposal, + } = useProposalVeto(proposalId); + + const showProposalLoading = getShowProposalLoading( + proposal, + proposalFetchStatus + ); + + const { + executeProposal, + canExecute, + isConfirming: isConfirmingExecution, + } = useProposalExecute(proposalId); + + if (skipRender || !proposal || showProposalLoading) { + return ( +
+ +
+ ); + } + + return ( +
+
+ vetoProposal()} + onExecutePressed={() => executeProposal()} + /> +
+ +
+ + +
+
+
+

+ {bottomSection === "description" ? "Description" : "Vetoes"} +

+ + val ? setBottomSection(val as BottomSection) : "" + } + > + + + +
+ + + + + + + + + +
+
+ ); +} + +function getShowProposalLoading( + proposal: ReturnType["proposal"], + status: ReturnType["status"] +) { + if (!proposal && status.proposalLoading) return true; + else if (status.metadataLoading && !status.metadataError) return true; + else if (!proposal?.title && !status.metadataError) return true; + + return false; +} diff --git a/plugins/lockToVote/utils/types.tsx b/plugins/lockToVote/utils/types.tsx new file mode 100644 index 00000000..54375c62 --- /dev/null +++ b/plugins/lockToVote/utils/types.tsx @@ -0,0 +1,42 @@ +import { Address } from "viem"; +import { Action } from "@/utils/types"; + +export type ProposalInputs = { + proposalId: bigint; +}; + +export type ProposalParameters = { + startDate: bigint; + endDate: bigint; + snapshotBlock: bigint; + minVetoVotingPower: bigint; +}; + +export type Proposal = { + active: boolean; + executed: boolean; + parameters: ProposalParameters; + vetoTally: bigint; + actions: Action[]; + allowFailureMap: bigint; + creator: string; + title: string; + summary: string; + resources: string[]; +}; + +export type ProposalMetadata = { + title: string; + summary: string; + resources: string[]; +}; + +export type VoteCastResponse = { + args: VetoCastEvent[]; +}; + +export type VetoCastEvent = { + voter: Address; + proposalId: bigint; + votingPower: bigint; +}; From 7a2eb238c34721468aee96d88316d15dd0ce88e7 Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Tue, 12 Mar 2024 12:25:29 +0100 Subject: [PATCH 02/16] fix: dependencies cross plugin --- .../lockToVote/components/proposal/index.tsx | 26 ++++++++----------- plugins/lockToVote/hooks/useUserCanVeto.tsx | 2 +- plugins/lockToVote/pages/new.tsx | 4 +-- plugins/lockToVote/pages/proposal-list.tsx | 6 ++--- plugins/lockToVote/pages/proposal.tsx | 6 ++--- 5 files changed, 20 insertions(+), 24 deletions(-) diff --git a/plugins/lockToVote/components/proposal/index.tsx b/plugins/lockToVote/components/proposal/index.tsx index 7af96a8c..2ef657bb 100644 --- a/plugins/lockToVote/components/proposal/index.tsx +++ b/plugins/lockToVote/components/proposal/index.tsx @@ -2,10 +2,10 @@ import Link from "next/link"; import { usePublicClient } from "wagmi"; import { useProposal } from "@/plugins/lockToVote/hooks/useProposal"; import { Card, Tag, TagVariant } from "@aragon/ods"; -import * as DOMPurify from 'dompurify'; +import * as DOMPurify from "dompurify"; import { PleaseWaitSpinner } from "@/components/please-wait"; import { useProposalVariantStatus } from "../../hooks/useProposalVariantStatus"; -import { PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; +import { PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; const DEFAULT_PROPOSAL_METADATA_TITLE = "(No proposal title)"; const DEFAULT_PROPOSAL_METADATA_SUMMARY = @@ -19,7 +19,7 @@ export default function ProposalCard(props: ProposalInputs) { const publicClient = usePublicClient(); const { proposal, status } = useProposal( publicClient!, - PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, props.proposalId.toString() ); const proposalVariant = useProposalVariantStatus(proposal!); @@ -38,15 +38,11 @@ export default function ProposalCard(props: ProposalInputs) { ); } else if (status.metadataReady && !proposal?.title) { return ( - +

- {Number(props.proposalId) + 1} -{" "} - {DEFAULT_PROPOSAL_METADATA_TITLE} + {Number(props.proposalId) + 1} - {DEFAULT_PROPOSAL_METADATA_TITLE}

{DEFAULT_PROPOSAL_METADATA_SUMMARY} @@ -63,12 +59,12 @@ export default function ProposalCard(props: ProposalInputs) { className="w-full cursor-pointer mb-4" > -

- -
+
+ +

{Number(props.proposalId) + 1} - {proposal.title} diff --git a/plugins/lockToVote/hooks/useUserCanVeto.tsx b/plugins/lockToVote/hooks/useUserCanVeto.tsx index eeaf1d0a..5a7358c1 100644 --- a/plugins/lockToVote/hooks/useUserCanVeto.tsx +++ b/plugins/lockToVote/hooks/useUserCanVeto.tsx @@ -1,7 +1,7 @@ import { useAccount, useBlockNumber, useReadContract } from "wagmi"; import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; import { useEffect } from "react"; -import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; +import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; export function useUserCanVeto(proposalId: bigint) { const { address } = useAccount(); diff --git a/plugins/lockToVote/pages/new.tsx b/plugins/lockToVote/pages/new.tsx index 5b503669..501f75d6 100644 --- a/plugins/lockToVote/pages/new.tsx +++ b/plugins/lockToVote/pages/new.tsx @@ -22,8 +22,8 @@ import { PleaseWaitSpinner } from "@/components/please-wait"; import { PUB_IPFS_ENDPOINT, PUB_IPFS_API_KEY, - PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, PUB_CHAIN, + PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, } from "@/constants"; import { ActionCard } from "@/components/actions/action"; @@ -141,7 +141,7 @@ export default function Create() { createProposalWrite({ chainId: PUB_CHAIN.id, abi: OptimisticTokenVotingPluginAbi, - address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, functionName: "createProposal", args: [toHex(ipfsPin), actions, 0, 0, 0], }); diff --git a/plugins/lockToVote/pages/proposal-list.tsx b/plugins/lockToVote/pages/proposal-list.tsx index 11ed2897..a770e385 100644 --- a/plugins/lockToVote/pages/proposal-list.tsx +++ b/plugins/lockToVote/pages/proposal-list.tsx @@ -1,9 +1,9 @@ import { useAccount, useBlockNumber, useReadContract } from "wagmi"; import { ReactNode, useEffect, useState } from "react"; -import ProposalCard from "@/plugins/dualGovernance/components/proposal"; -import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; +import ProposalCard from "@/plugins/lockToVote/components/proposal"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; import { Button, CardEmptyState, IconType } from "@aragon/ods"; -import { useCanCreateProposal } from "@/plugins/dualGovernance/hooks/useCanCreateProposal"; +import { useCanCreateProposal } from "@/plugins/lockToVote/hooks/useCanCreateProposal"; import Link from "next/link"; import { Else, ElseIf, If, Then } from "@/components/if"; import { PleaseWaitSpinner } from "@/components/please-wait"; diff --git a/plugins/lockToVote/pages/proposal.tsx b/plugins/lockToVote/pages/proposal.tsx index cc3432d8..b1261c80 100644 --- a/plugins/lockToVote/pages/proposal.tsx +++ b/plugins/lockToVote/pages/proposal.tsx @@ -1,9 +1,9 @@ import { useProposal } from "@/plugins/lockToVote/hooks/useProposal"; import { ToggleGroup, Toggle } from "@aragon/ods"; -import ProposalDescription from "@/plugins/dualGovernance/components/proposal/description"; +import ProposalDescription from "@/plugins/lockToVote/components/proposal/description"; import VetoesSection from "@/plugins/dualGovernance/components/vote/vetoes-section"; -import ProposalHeader from "@/plugins/dualGovernance/components/proposal/header"; -import VetoTally from "@/plugins/dualGovernance/components/vote/tally"; +import ProposalHeader from "@/plugins/lockToVote/components/proposal/header"; +import VetoTally from "@/plugins/lockToVote/components/vote/tally"; import ProposalDetails from "@/plugins/lockToVote/components/proposal/details"; import { Else, If, Then } from "@/components/if"; import { PleaseWaitSpinner } from "@/components/please-wait"; From 11cd5b89611e38648334e37348c49a6ddb7b4a0c Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Tue, 12 Mar 2024 14:46:25 +0100 Subject: [PATCH 03/16] fix: proposal page now working --- .../components/proposal/description.tsx | 145 ++-------------- .../lockToVote/components/proposal/header.tsx | 159 +++++++----------- 2 files changed, 67 insertions(+), 237 deletions(-) diff --git a/plugins/lockToVote/components/proposal/description.tsx b/plugins/lockToVote/components/proposal/description.tsx index f8c28978..c6cdb8da 100644 --- a/plugins/lockToVote/components/proposal/description.tsx +++ b/plugins/lockToVote/components/proposal/description.tsx @@ -1,67 +1,11 @@ -import { Action } from "@/utils/types"; import { Proposal } from "@/plugins/lockToVote/utils/types"; -import { whatsabi } from "@shazow/whatsabi"; -import { ReactNode, useCallback, useEffect, useState } from "react"; -import { usePublicClient } from "wagmi"; -import { Address, decodeFunctionData } from "viem"; -import { Else, If, IfCase, IfNot, Then } from "@/components/if"; -import { PleaseWaitSpinner } from "@/components/please-wait"; -import { AddressText } from "@/components/text/address"; -import { isAddress } from "@/utils/evm"; import * as DOMPurify from "dompurify"; -import { PUB_CHAIN, PUB_ETHERSCAN_API_KEY } from "@/constants"; +import { ActionCard } from "@/components/actions/action"; +import { If } from "@/components/if"; -const DEFAULT_PROPOSAL_SUMMARY = "(No description available)"; - -type FunctionData = { - args: readonly unknown[] | undefined; - functionName: string; - to: Address; -}; +const DEFAULT_PROPOSAL_METADATA_SUMMARY = "(No description available)"; export default function ProposalDescription(proposal: Proposal) { - const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); - const [decodedActions, setDecodedActions] = useState([]); - const proposalActions = proposal?.actions || []; - - const getFunctionData = async (action: Action) => { - if (!publicClient) return; - - const abiLoader = new whatsabi.loaders.EtherscanABILoader({ - apiKey: PUB_ETHERSCAN_API_KEY, - }); - - const { abi } = await whatsabi.autoload(action.to, { - provider: publicClient, - abiLoader, - followProxies: true, - }); - - return decodeFunctionData({ - abi, - data: action.data as Address, - }); - }; - - const fetchActionData = useCallback(async () => { - const decodedActions = await Promise.all( - proposalActions.map(async (action) => { - let functionData: any; - if (action.data != "0x") { - functionData = await getFunctionData(action); - } else { - functionData = { functionName: "transfer", args: [action.value] }; - } - return { ...functionData, to: action.to } as FunctionData; - }) - ); - setDecodedActions(decodedActions); - }, [proposal]); - - useEffect(() => { - fetchActionData(); - }, [proposal.actions]); - return (

Actions

-
- +
+

The proposal has no actions

- - - - {decodedActions?.map?.((action, i) => ( - + {proposal.actions?.map?.((action, i) => ( +
+ +
))}
); } - -// This should be encapsulated as soon as ODS exports this widget -const Card = function ({ children }: { children: ReactNode }) { - return ( -
- {children} -
- ); -}; - -const ActionCard = function ({ - action, - idx, -}: { - action: FunctionData; - idx: number; -}) { - return ( - -
-
-

Target contract

-

- {action.to} -

-
-
- {idx + 1} -
-
- -
-

Function name

-

- {action.functionName} -

-
- -
-

Parameters

-
    - {action?.args?.length && - action?.args?.map((arg: any, j: number) => ( -
  • - - - {arg.toString()} - - - {arg.toString()} - - -
  • - ))} -
-
-
- ); -}; diff --git a/plugins/lockToVote/components/proposal/header.tsx b/plugins/lockToVote/components/proposal/header.tsx index 39bab433..19802e13 100644 --- a/plugins/lockToVote/components/proposal/header.tsx +++ b/plugins/lockToVote/components/proposal/header.tsx @@ -1,90 +1,35 @@ -import { useEffect } from "react"; import { Button, Tag } from "@aragon/ods"; -import { Proposal } from "@/plugins/lockToVote/utils/types"; +import { Proposal } from "@/plugins/dualGovernance/utils/types"; import { AlertVariant } from "@aragon/ods"; -import { Else, If, IfCase, Then } from "@/components/if"; +import { ElseIf, If, Then, Else } from "@/components/if"; import { AddressText } from "@/components/text/address"; -import { useWaitForTransactionReceipt, useWriteContract } from "wagmi"; -import { OptimisticTokenVotingPluginAbi } from "../../artifacts/OptimisticTokenVotingPlugin.sol"; -import { AlertContextProps, useAlertContext } from "@/context/AlertContext"; -import { useProposalVariantStatus } from "../../hooks/useProposalVariantStatus"; -import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from "@/constants"; +import { useProposalVariantStatus } from "@/plugins/lockToVote/hooks/useProposalVariantStatus"; import { PleaseWaitSpinner } from "@/components/please-wait"; -import { useRouter } from "next/router"; +import dayjs from "dayjs"; const DEFAULT_PROPOSAL_TITLE = "(No proposal title)"; interface ProposalHeaderProps { proposalNumber: number; proposal: Proposal; - userCanVeto: boolean; - transactionLoading: boolean; + canVeto: boolean; + canExecute: boolean; + transactionConfirming: boolean; onVetoPressed: () => void; + onExecutePressed: () => void; } const ProposalHeader: React.FC = ({ proposalNumber, proposal, - userCanVeto, - transactionLoading, + canVeto, + canExecute, + transactionConfirming, onVetoPressed, + onExecutePressed, }) => { - const { reload } = useRouter(); - const { addAlert } = useAlertContext() as AlertContextProps; const proposalVariant = useProposalVariantStatus(proposal); - - const { - writeContract: executeWrite, - data: executeTxHash, - error, - status, - } = useWriteContract(); - const { isLoading: isConfirming, isSuccess: isConfirmed } = - useWaitForTransactionReceipt({ hash: executeTxHash }); - - const executeButtonPressed = () => { - executeWrite({ - chainId: PUB_CHAIN.id, - abi: OptimisticTokenVotingPluginAbi, - address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, - functionName: "execute", - args: [proposalNumber], - }); - }; - - useEffect(() => { - if (status === "idle" || status === "pending") return; - else if (status === "error") { - if (error?.message?.startsWith("User rejected the request")) { - addAlert("Transaction rejected by the user", { - timeout: 4 * 1000, - }); - } else { - console.error(error); - addAlert("Could not execute the proposal", { type: "error" }); - } - return; - } - - // success - if (!executeTxHash) return; - else if (isConfirming) { - addAlert("Proposal submitted", { - description: "Waiting for the transaction to be validated", - type: "info", - txHash: executeTxHash, - }); - return; - } else if (!isConfirmed) return; - - addAlert("Proposal executed", { - description: "The transaction has been validated", - type: "success", - txHash: executeTxHash, - }); - - setTimeout(() => reload(), 1000 * 2); - }, [status, executeTxHash, isConfirming, isConfirmed]); + const ended = proposal.parameters.endDate <= Date.now() / 1000; return (
@@ -102,44 +47,38 @@ const ProposalHeader: React.FC = ({ />
- Proposal {proposalNumber + 1} + Proposal {proposalNumber}
-
- +
+ - - - - - -
- -
-
-
+
+ +
- - - - - - + + + + + + +
@@ -147,7 +86,23 @@ const ProposalHeader: React.FC = ({ {proposal.title || DEFAULT_PROPOSAL_TITLE}

- Proposed by {proposal?.creator} + Proposed by {proposal?.creator},{" "} + + + ended on{" "} + {dayjs(Number(proposal.parameters.endDate) * 1000).format( + "D MMM YYYY HH:mm" + )} + h + + + ending on{" "} + {dayjs(Number(proposal.parameters.endDate) * 1000).format( + "D MMM YYYY HH:mm" + )} + h + +

); From ec07c4dd4a4acfbc7b3fdaf415ede3e43861647b Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Tue, 12 Mar 2024 15:00:45 +0100 Subject: [PATCH 04/16] fix: end date in the proposal page --- plugins/lockToVote/components/proposal/details.tsx | 1 - plugins/lockToVote/pages/proposal.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/lockToVote/components/proposal/details.tsx b/plugins/lockToVote/components/proposal/details.tsx index 022b98c7..7bbdd917 100644 --- a/plugins/lockToVote/components/proposal/details.tsx +++ b/plugins/lockToVote/components/proposal/details.tsx @@ -12,7 +12,6 @@ const ProposalDetails: React.FC = ({ endDate, snapshotBlock, }) => { - return ( <> diff --git a/plugins/lockToVote/pages/proposal.tsx b/plugins/lockToVote/pages/proposal.tsx index b1261c80..48d91bce 100644 --- a/plugins/lockToVote/pages/proposal.tsx +++ b/plugins/lockToVote/pages/proposal.tsx @@ -71,7 +71,7 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { } />
From 5035f871846a37fbf30e1f81e9cf94f400515eb0 Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Tue, 12 Mar 2024 16:48:06 +0100 Subject: [PATCH 05/16] fix: updating the components to new dual governance --- .../lockToVote/components/proposal/header.tsx | 14 +++ .../lockToVote/components/proposal/index.tsx | 22 +++-- plugins/lockToVote/hooks/useProposal.tsx | 57 +++++------- plugins/lockToVote/hooks/useProposalVeto.tsx | 92 +++++++++++++++++++ plugins/lockToVote/pages/proposal-list.tsx | 6 +- plugins/lockToVote/pages/proposal.tsx | 8 +- 6 files changed, 153 insertions(+), 46 deletions(-) create mode 100644 plugins/lockToVote/hooks/useProposalVeto.tsx diff --git a/plugins/lockToVote/components/proposal/header.tsx b/plugins/lockToVote/components/proposal/header.tsx index 19802e13..9c316329 100644 --- a/plugins/lockToVote/components/proposal/header.tsx +++ b/plugins/lockToVote/components/proposal/header.tsx @@ -14,9 +14,11 @@ interface ProposalHeaderProps { proposal: Proposal; canVeto: boolean; canExecute: boolean; + addressLockedTokens: boolean; transactionConfirming: boolean; onVetoPressed: () => void; onExecutePressed: () => void; + onClaimLockPressed: () => void; } const ProposalHeader: React.FC = ({ @@ -24,9 +26,11 @@ const ProposalHeader: React.FC = ({ proposal, canVeto, canExecute, + addressLockedTokens, transactionConfirming, onVetoPressed, onExecutePressed, + onClaimLockPressed, }) => { const proposalVariant = useProposalVariantStatus(proposal); const ended = proposal.parameters.endDate <= Date.now() / 1000; @@ -78,6 +82,16 @@ const ProposalHeader: React.FC = ({ Execute + + +
diff --git a/plugins/lockToVote/components/proposal/index.tsx b/plugins/lockToVote/components/proposal/index.tsx index 2ef657bb..6d8b2968 100644 --- a/plugins/lockToVote/components/proposal/index.tsx +++ b/plugins/lockToVote/components/proposal/index.tsx @@ -1,11 +1,9 @@ import Link from "next/link"; -import { usePublicClient } from "wagmi"; import { useProposal } from "@/plugins/lockToVote/hooks/useProposal"; import { Card, Tag, TagVariant } from "@aragon/ods"; import * as DOMPurify from "dompurify"; import { PleaseWaitSpinner } from "@/components/please-wait"; import { useProposalVariantStatus } from "../../hooks/useProposalVariantStatus"; -import { PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; const DEFAULT_PROPOSAL_METADATA_TITLE = "(No proposal title)"; const DEFAULT_PROPOSAL_METADATA_SUMMARY = @@ -16,12 +14,7 @@ type ProposalInputs = { }; export default function ProposalCard(props: ProposalInputs) { - const publicClient = usePublicClient(); - const { proposal, status } = useProposal( - publicClient!, - PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, - props.proposalId.toString() - ); + const { proposal, status } = useProposal(props.proposalId.toString()); const proposalVariant = useProposalVariantStatus(proposal!); const showLoading = getShowProposalLoading(proposal, status); @@ -36,9 +29,20 @@ export default function ProposalCard(props: ProposalInputs) {
); + } else if (!proposal?.title && !proposal?.summary) { + // We have the proposal but no metadata yet + return ( + + + + + + + + ); } else if (status.metadataReady && !proposal?.title) { return ( - +

diff --git a/plugins/lockToVote/hooks/useProposal.tsx b/plugins/lockToVote/hooks/useProposal.tsx index d2b06aa9..1e5d8d8d 100644 --- a/plugins/lockToVote/hooks/useProposal.tsx +++ b/plugins/lockToVote/hooks/useProposal.tsx @@ -1,17 +1,15 @@ import { useState, useEffect } from "react"; -import { Address } from "viem"; -import { useBlockNumber, useReadContract } from "wagmi"; -import { fetchJsonFromIpfs } from "@/utils/ipfs"; -import { PublicClient, getAbiItem } from "viem"; -import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; +import { useBlockNumber, usePublicClient, useReadContract } from "wagmi"; +import { getAbiItem } from "viem"; import { Action } from "@/utils/types"; import { Proposal, ProposalMetadata, ProposalParameters, -} from "@/plugins/dualGovernance/utils/types"; -import { useQuery } from "@tanstack/react-query"; -import { PUB_CHAIN } from "@/constants"; +} from "@/plugins/lockToVote/utils/types"; +import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; +import { useMetadata } from "@/hooks/useMetadata"; +import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; type ProposalCreatedLogResponse = { args: { @@ -26,30 +24,26 @@ type ProposalCreatedLogResponse = { }; const ProposalCreatedEvent = getAbiItem({ - abi: OptimisticTokenVotingPluginAbi, + abi: LockToVetoPluginAbi, name: "ProposalCreated", }); -export function useProposal( - publicClient: PublicClient, - address: Address, - proposalId: string, - autoRefresh = false -) { +export function useProposal(proposalId: string, autoRefresh = false) { + const publicClient = usePublicClient(); const [proposalCreationEvent, setProposalCreationEvent] = useState(); const [metadataUri, setMetadata] = useState(); - const { data: blockNumber} = useBlockNumber(); + const { data: blockNumber } = useBlockNumber({ watch: true }); // Proposal on-chain data const { data: proposalResult, error: proposalError, fetchStatus: proposalFetchStatus, - refetch: proposalRefetch - } = useReadContract({ - address, - abi: OptimisticTokenVotingPluginAbi, + refetch: proposalRefetch, + } = useReadContract({ + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + abi: LockToVetoPluginAbi, functionName: "getProposal", args: [proposalId], chainId: PUB_CHAIN.id, @@ -57,15 +51,16 @@ export function useProposal( const proposalData = decodeProposalResultData(proposalResult as any); useEffect(() => { - if (autoRefresh) proposalRefetch() - }, [blockNumber]) + if (autoRefresh) proposalRefetch(); + }, [blockNumber]); // Creation event useEffect(() => { - if (!proposalData) return; + if (!proposalData || !publicClient) return; + publicClient .getLogs({ - address, + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, event: ProposalCreatedEvent as any, args: { proposalId, @@ -81,22 +76,17 @@ export function useProposal( setMetadata(log.args.metadata); }) .catch((err) => { - console.error("Could not fetch the proposal defailt", err); + console.error("Could not fetch the proposal details", err); return null; }); - }, [proposalData?.vetoTally]); + }, [proposalData?.vetoTally, !!publicClient]); // JSON metadata const { data: metadataContent, isLoading: metadataLoading, - isSuccess: metadataReady, error: metadataError, - } = useQuery({ - queryKey: [`dualGovernanceProposal-${address}-${proposalId}`, metadataUri!], - queryFn: () => metadataUri ? fetchJsonFromIpfs(metadataUri) : Promise.resolve(null), - enabled: !!metadataUri -}); + } = useMetadata(metadataUri); const proposal = arrangeProposalData( proposalData, @@ -106,11 +96,12 @@ export function useProposal( return { proposal, + refetch: proposalRefetch, status: { proposalReady: proposalFetchStatus === "idle", proposalLoading: proposalFetchStatus === "fetching", proposalError, - metadataReady, + metadataReady: !metadataError && !metadataLoading && !!metadataContent, metadataLoading, metadataError: metadataError !== undefined, }, diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx new file mode 100644 index 00000000..35bd317d --- /dev/null +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -0,0 +1,92 @@ +import { useEffect } from "react"; +import { + usePublicClient, + useWaitForTransactionReceipt, + useWriteContract, +} from "wagmi"; +import { useProposal } from "./useProposal"; +import { useProposalVetoes } from "@/plugins/dualGovernance/hooks/useProposalVetoes"; +import { useUserCanVeto } from "@/plugins/dualGovernance/hooks/useUserCanVeto"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; +import { useAlertContext, AlertContextProps } from "@/context/AlertContext"; +import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; + +export function useProposalVeto(proposalId: string) { + const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); + + const { + proposal, + status: proposalFetchStatus, + refetch: refetchProposal, + } = useProposal(proposalId, true); + const vetoes = useProposalVetoes( + publicClient!, + PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + proposalId, + proposal + ); + + const { addAlert } = useAlertContext() as AlertContextProps; + const { + writeContract: vetoWrite, + data: vetoTxHash, + error: vetoingError, + status: vetoingStatus, + } = useWriteContract(); + const { isLoading: isConfirming, isSuccess: isConfirmed } = + useWaitForTransactionReceipt({ hash: vetoTxHash }); + const { canVeto, refetch: refetchCanVeto } = useUserCanVeto( + BigInt(proposalId) + ); + + useEffect(() => { + if (vetoingStatus === "idle" || vetoingStatus === "pending") return; + else if (vetoingStatus === "error") { + if (vetoingError?.message?.startsWith("User rejected the request")) { + addAlert("Transaction rejected by the user", { + timeout: 4 * 1000, + }); + } else { + addAlert("Could not create the proposal", { type: "error" }); + } + return; + } + + // success + if (!vetoTxHash) return; + else if (isConfirming) { + addAlert("Veto submitted", { + description: "Waiting for the transaction to be validated", + txHash: vetoTxHash, + }); + return; + } else if (!isConfirmed) return; + + addAlert("Veto registered", { + description: "The transaction has been validated", + type: "success", + txHash: vetoTxHash, + }); + refetchCanVeto(); + refetchProposal(); + }, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]); + + const vetoProposal = () => { + vetoWrite({ + abi: OptimisticTokenVotingPluginAbi, + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + functionName: "veto", + args: [proposalId], + }); + }; + + return { + proposal, + proposalFetchStatus, + vetoes, + canVeto: !!canVeto, + isConfirming: vetoingStatus === "pending" || isConfirming, + isConfirmed, + vetoProposal, + }; +} diff --git a/plugins/lockToVote/pages/proposal-list.tsx b/plugins/lockToVote/pages/proposal-list.tsx index a770e385..00144b9e 100644 --- a/plugins/lockToVote/pages/proposal-list.tsx +++ b/plugins/lockToVote/pages/proposal-list.tsx @@ -1,14 +1,14 @@ import { useAccount, useBlockNumber, useReadContract } from "wagmi"; import { ReactNode, useEffect, useState } from "react"; import ProposalCard from "@/plugins/lockToVote/components/proposal"; -import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; import { Button, CardEmptyState, IconType } from "@aragon/ods"; -import { useCanCreateProposal } from "@/plugins/lockToVote/hooks/useCanCreateProposal"; +import { useCanCreateProposal } from "@/plugins/dualGovernance/hooks/useCanCreateProposal"; import Link from "next/link"; import { Else, ElseIf, If, Then } from "@/components/if"; import { PleaseWaitSpinner } from "@/components/please-wait"; import { useSkipFirstRender } from "@/hooks/useSkipFirstRender"; -import { PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, PUB_CHAIN } from "@/constants"; +import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; import { digestPagination } from "@/utils/pagination"; import { useWeb3Modal } from "@web3modal/wagmi/react"; import { useRouter } from "next/router"; diff --git a/plugins/lockToVote/pages/proposal.tsx b/plugins/lockToVote/pages/proposal.tsx index 48d91bce..85813b68 100644 --- a/plugins/lockToVote/pages/proposal.tsx +++ b/plugins/lockToVote/pages/proposal.tsx @@ -9,13 +9,15 @@ import { Else, If, Then } from "@/components/if"; import { PleaseWaitSpinner } from "@/components/please-wait"; import { useSkipFirstRender } from "@/hooks/useSkipFirstRender"; import { useState } from "react"; -import { useProposalVeto } from "@/plugins/dualGovernance/hooks/useProposalVeto"; +import { useProposalVeto } from "@/plugins/lockToVote/hooks/useProposalVeto"; import { useProposalExecute } from "@/plugins/dualGovernance/hooks/useProposalExecute"; +import { useAccount } from "wagmi"; type BottomSection = "description" | "vetoes"; export default function ProposalDetail({ id: proposalId }: { id: string }) { const skipRender = useSkipFirstRender(); + const account = useAccount(); const [bottomSection, setBottomSection] = useState("description"); @@ -56,8 +58,12 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { transactionConfirming={isConfirmingVeto || isConfirmingExecution} canVeto={canVeto} canExecute={canExecute} + addressLockedTokens={vetoes.some((veto) => { + veto.voter === account.address; + })} onVetoPressed={() => vetoProposal()} onExecutePressed={() => executeProposal()} + // onClaimLockPressed={() => claimLockProposal()} />

From 600aa0912dc74a7b5f1235f8522b332308f43b73 Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Tue, 12 Mar 2024 18:24:19 +0100 Subject: [PATCH 06/16] feat: adding the option to claim back the locked funds --- .../lockToVote/components/proposal/header.tsx | 4 +- .../lockToVote/hooks/useProposalClaimLock.tsx | 95 +++++++++++++++++++ plugins/lockToVote/pages/proposal.tsx | 16 +++- 3 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 plugins/lockToVote/hooks/useProposalClaimLock.tsx diff --git a/plugins/lockToVote/components/proposal/header.tsx b/plugins/lockToVote/components/proposal/header.tsx index 9c316329..52a1acb2 100644 --- a/plugins/lockToVote/components/proposal/header.tsx +++ b/plugins/lockToVote/components/proposal/header.tsx @@ -14,6 +14,7 @@ interface ProposalHeaderProps { proposal: Proposal; canVeto: boolean; canExecute: boolean; + hasClaimed: boolean; addressLockedTokens: boolean; transactionConfirming: boolean; onVetoPressed: () => void; @@ -26,6 +27,7 @@ const ProposalHeader: React.FC = ({ proposal, canVeto, canExecute, + hasClaimed, addressLockedTokens, transactionConfirming, onVetoPressed, @@ -82,7 +84,7 @@ const ProposalHeader: React.FC = ({ Execute - +
From 05c17e4531b4d98913dd95fe4c84a8ae27caa9ba Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Wed, 13 Mar 2024 09:45:13 +0100 Subject: [PATCH 07/16] fix: veto count not exact --- plugins/lockToVote/components/vote/tally.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lockToVote/components/vote/tally.tsx b/plugins/lockToVote/components/vote/tally.tsx index cbd873c1..cef9edce 100644 --- a/plugins/lockToVote/components/vote/tally.tsx +++ b/plugins/lockToVote/components/vote/tally.tsx @@ -14,7 +14,7 @@ const VetoTally: FC = ({ voteCount, votePercentage }) => ( Vetoed

- {compactNumber(formatUnits(voteCount + BigInt(1) || BigInt(0), 18))} + {compactNumber(formatUnits(voteCount || BigInt(0), 18))}

From 181b3435204c3f36fbb4c493cee64704e488eac4 Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Wed, 13 Mar 2024 14:43:39 +0100 Subject: [PATCH 08/16] fix: vetoing --- bun.lockb | Bin 592866 -> 595162 bytes plugins/index.ts | 14 +-- .../lockToVote/components/proposal/header.tsx | 2 +- .../lockToVote/hooks/useCanCreateProposal.tsx | 84 ++++++++++-------- plugins/lockToVote/hooks/useProposalVeto.tsx | 4 + plugins/lockToVote/hooks/useUserCanVeto.tsx | 10 ++- 6 files changed, 65 insertions(+), 49 deletions(-) diff --git a/bun.lockb b/bun.lockb index 6bf81523fcab15f620300d1a0d13bd9ee0c33c07..4163ba07b9ef05839518e81ade73741b7e7878b5 100755 GIT binary patch delta 120229 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
-Oe4^L|brC-Nt+Z!&X;`54N7U{19p`r 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 diff --git a/plugins/index.ts b/plugins/index.ts index 896f0e1e..9c0b3392 100644 --- a/plugins/index.ts +++ b/plugins/index.ts @@ -32,6 +32,13 @@ export const plugins: PluginItem[] = [ icon: IconType.BLOCKCHAIN_BLOCKCHAIN, pluginAddress: PUB_TOKEN_VOTING_PLUGIN_ADDRESS, }, + { + id: "lock-to-vote", + folderName: "lockToVote", + title: "Morpho Vault Gov", + icon: IconType.BLOCKCHAIN_BLOCK, + pluginAddress: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + }, { id: "delegate-wall", folderName: "delegateAnnouncer", @@ -39,11 +46,4 @@ export const plugins: PluginItem[] = [ icon: IconType.FEEDBACK, pluginAddress: PUB_DELEGATION_CONTRACT_ADDRESS, }, - { - id: "lock-to-vote", - folderName: "lockToVote", - title: "Lock To Vote", - icon: IconType.DEPOSIT, - pluginAddress: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, - }, ]; diff --git a/plugins/lockToVote/components/proposal/header.tsx b/plugins/lockToVote/components/proposal/header.tsx index 52a1acb2..75945071 100644 --- a/plugins/lockToVote/components/proposal/header.tsx +++ b/plugins/lockToVote/components/proposal/header.tsx @@ -1,5 +1,5 @@ import { Button, Tag } from "@aragon/ods"; -import { Proposal } from "@/plugins/dualGovernance/utils/types"; +import { Proposal } from "@/plugins/lockToVote/utils/types"; import { AlertVariant } from "@aragon/ods"; import { ElseIf, If, Then, Else } from "@/components/if"; import { AddressText } from "@/components/text/address"; diff --git a/plugins/lockToVote/hooks/useCanCreateProposal.tsx b/plugins/lockToVote/hooks/useCanCreateProposal.tsx index b110e32f..d878bc13 100644 --- a/plugins/lockToVote/hooks/useCanCreateProposal.tsx +++ b/plugins/lockToVote/hooks/useCanCreateProposal.tsx @@ -1,45 +1,55 @@ -import { Address } from 'viem' -import { useState, useEffect } from 'react' -import { useBalance, useAccount, useReadContracts } from 'wagmi'; -import { OptimisticTokenVotingPluginAbi } from '@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol'; -import { PUB_CHAIN, PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS } from '@/constants'; +import { Address } from "viem"; +import { useState, useEffect } from "react"; +import { useBalance, useAccount, useReadContracts } from "wagmi"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; +import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; export function useCanCreateProposal() { - const [isCreator, setIsCreator] = useState(false); - const [minProposerVotingPower, setMinProposerVotingPower] = useState(); - const [votingToken, setVotingToken] = useState
(); - const { address, isConnecting, isDisconnected } = useAccount() - const {data: balance} = useBalance({ address, token: votingToken, chainId: PUB_CHAIN.id }) + const [isCreator, setIsCreator] = useState(false); + const [minProposerVotingPower, setMinProposerVotingPower] = + useState(); + const [votingToken, setVotingToken] = useState
(); + const { address, isConnecting, isDisconnected } = useAccount(); + const { data: balance } = useBalance({ + address, + token: votingToken, + chainId: PUB_CHAIN.id, + }); - const { data: contractReads } = useReadContracts({ - contracts: [ - { - chainId: PUB_CHAIN.id, - address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, - abi: OptimisticTokenVotingPluginAbi, - functionName: 'minProposerVotingPower', - }, - { - chainId: PUB_CHAIN.id, - address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, - abi: OptimisticTokenVotingPluginAbi, - functionName: 'getVotingToken', - } - // TODO: This needs to be checking as well if address has the DAO permission to create props - ] - }) + const { data: contractReads } = useReadContracts({ + contracts: [ + { + chainId: PUB_CHAIN.id, + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + abi: OptimisticTokenVotingPluginAbi, + functionName: "minProposerVotingPower", + }, + { + chainId: PUB_CHAIN.id, + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + abi: OptimisticTokenVotingPluginAbi, + functionName: "getVotingToken", + }, + // TODO: This needs to be checking as well if address has the DAO permission to create props + ], + }); - useEffect(() => { - if (contractReads?.length) { - setMinProposerVotingPower(contractReads[0]?.result as bigint) + useEffect(() => { + if (contractReads?.length) { + setMinProposerVotingPower(contractReads[0]?.result as bigint); - setVotingToken(contractReads[1]?.result as Address) - } - }, [contractReads]) + setVotingToken(contractReads[1]?.result as Address); + } + }, [contractReads]); - useEffect(() => { - if ( balance !== undefined && minProposerVotingPower !== undefined && balance?.value >= minProposerVotingPower) setIsCreator(true) - }, [balance]) + useEffect(() => { + if ( + balance !== undefined && + minProposerVotingPower !== undefined && + balance?.value >= minProposerVotingPower + ) + setIsCreator(true); + }, [balance]); - return isCreator + return isCreator; } diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx index 35bd317d..4156a695 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.tsx +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -39,6 +39,10 @@ export function useProposalVeto(proposalId: string) { BigInt(proposalId) ); + useEffect(() => { + console.log(canVeto); + }, [canVeto]); + useEffect(() => { if (vetoingStatus === "idle" || vetoingStatus === "pending") return; else if (vetoingStatus === "error") { diff --git a/plugins/lockToVote/hooks/useUserCanVeto.tsx b/plugins/lockToVote/hooks/useUserCanVeto.tsx index 5a7358c1..a07f11dd 100644 --- a/plugins/lockToVote/hooks/useUserCanVeto.tsx +++ b/plugins/lockToVote/hooks/useUserCanVeto.tsx @@ -7,17 +7,19 @@ export function useUserCanVeto(proposalId: bigint) { const { address } = useAccount(); const { data: blockNumber } = useBlockNumber({ watch: true }); - const { data: canVeto, refetch: canVetoRefetch } = useReadContract({ + const { data: canVeto, refetch } = useReadContract({ chainId: PUB_CHAIN.id, - address: PUB_DUAL_GOVERNANCE_PLUGIN_ADDRESS, + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, abi: OptimisticTokenVotingPluginAbi, functionName: "canVeto", args: [proposalId, address], }); useEffect(() => { - canVetoRefetch(); + if (Number(blockNumber) % 2 === 0) { + refetch(); + } }, [blockNumber]); - return canVeto; + return { canVeto, refetch }; } From e70ca5f2e6139af5ae92a24d7ca579e05a64b9c8 Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Wed, 13 Mar 2024 15:53:35 +0100 Subject: [PATCH 09/16] feat: fixing vetoing; for now it's harcoded the amount till we have the permit --- plugins/lockToVote/hooks/useProposalVeto.tsx | 17 +++++++---------- plugins/lockToVote/hooks/useUserCanVeto.tsx | 4 ++-- 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx index 4156a695..27df6d4e 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.tsx +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -5,11 +5,12 @@ import { useWriteContract, } from "wagmi"; import { useProposal } from "./useProposal"; -import { useProposalVetoes } from "@/plugins/dualGovernance/hooks/useProposalVetoes"; -import { useUserCanVeto } from "@/plugins/dualGovernance/hooks/useUserCanVeto"; -import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; +import { useProposalVetoes } from "@/plugins/lockToVote/hooks/useProposalVetoes"; +import { useUserCanVeto } from "@/plugins/lockToVote/hooks/useUserCanVeto"; +import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; import { useAlertContext, AlertContextProps } from "@/context/AlertContext"; import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; +import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; export function useProposalVeto(proposalId: string) { const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); @@ -39,10 +40,6 @@ export function useProposalVeto(proposalId: string) { BigInt(proposalId) ); - useEffect(() => { - console.log(canVeto); - }, [canVeto]); - useEffect(() => { if (vetoingStatus === "idle" || vetoingStatus === "pending") return; else if (vetoingStatus === "error") { @@ -51,7 +48,7 @@ export function useProposalVeto(proposalId: string) { timeout: 4 * 1000, }); } else { - addAlert("Could not create the proposal", { type: "error" }); + addAlert("Could not create the veto", { type: "error" }); } return; } @@ -77,10 +74,10 @@ export function useProposalVeto(proposalId: string) { const vetoProposal = () => { vetoWrite({ - abi: OptimisticTokenVotingPluginAbi, + abi: LockToVetoPluginAbi, address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, functionName: "veto", - args: [proposalId], + args: [proposalId, 50000000000000000000], }); }; diff --git a/plugins/lockToVote/hooks/useUserCanVeto.tsx b/plugins/lockToVote/hooks/useUserCanVeto.tsx index a07f11dd..415706eb 100644 --- a/plugins/lockToVote/hooks/useUserCanVeto.tsx +++ b/plugins/lockToVote/hooks/useUserCanVeto.tsx @@ -1,7 +1,7 @@ import { useAccount, useBlockNumber, useReadContract } from "wagmi"; -import { OptimisticTokenVotingPluginAbi } from "@/plugins/lockToVote/artifacts/OptimisticTokenVotingPlugin.sol"; import { useEffect } from "react"; import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; +import { LockToVetoPluginAbi } from "../artifacts/LockToVetoPlugin.sol"; export function useUserCanVeto(proposalId: bigint) { const { address } = useAccount(); @@ -10,7 +10,7 @@ export function useUserCanVeto(proposalId: bigint) { const { data: canVeto, refetch } = useReadContract({ chainId: PUB_CHAIN.id, address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, - abi: OptimisticTokenVotingPluginAbi, + abi: LockToVetoPluginAbi, functionName: "canVeto", args: [proposalId, address], }); From 26515ae0fb9d579aae71196c73d1e9f8354b269f Mon Sep 17 00:00:00 2001 From: xavikh Date: Thu, 14 Mar 2024 11:14:09 +0100 Subject: [PATCH 10/16] Add permit to veto --- .../artifacts/ERC20withPermit.sol.ts | 526 ++++++++++++++++++ plugins/lockToVote/hooks/useProposalVeto.tsx | 96 +++- 2 files changed, 613 insertions(+), 9 deletions(-) create mode 100644 plugins/lockToVote/artifacts/ERC20withPermit.sol.ts diff --git a/plugins/lockToVote/artifacts/ERC20withPermit.sol.ts b/plugins/lockToVote/artifacts/ERC20withPermit.sol.ts new file mode 100644 index 00000000..7c10533c --- /dev/null +++ b/plugins/lockToVote/artifacts/ERC20withPermit.sol.ts @@ -0,0 +1,526 @@ +import { Abi } from "viem"; +export const ERC20Abi: Abi = [ + { + "inputs": [], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [], + "name": "ECDSAInvalidSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "length", + "type": "uint256" + } + ], + "name": "ECDSAInvalidSignatureLength", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "ECDSAInvalidSignatureS", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "allowance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientAllowance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "balance", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "needed", + "type": "uint256" + } + ], + "name": "ERC20InsufficientBalance", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "approver", + "type": "address" + } + ], + "name": "ERC20InvalidApprover", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "receiver", + "type": "address" + } + ], + "name": "ERC20InvalidReceiver", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "sender", + "type": "address" + } + ], + "name": "ERC20InvalidSender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "ERC20InvalidSpender", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + } + ], + "name": "ERC2612ExpiredSignature", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "signer", + "type": "address" + }, + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "ERC2612InvalidSigner", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + }, + { + "internalType": "uint256", + "name": "currentNonce", + "type": "uint256" + } + ], + "name": "InvalidAccountNonce", + "type": "error" + }, + { + "inputs": [], + "name": "InvalidShortString", + "type": "error" + }, + { + "inputs": [ + { + "internalType": "string", + "name": "str", + "type": "string" + } + ], + "name": "StringTooLong", + "type": "error" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Approval", + "type": "event" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "Transfer", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + } + ], + "name": "allowance", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "approve", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "account", + "type": "address" + } + ], + "name": "balanceOf", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "decimals", + "outputs": [ + { + "internalType": "uint8", + "name": "", + "type": "uint8" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "name", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + } + ], + "name": "nonces", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "owner", + "type": "address" + }, + { + "internalType": "address", + "name": "spender", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "deadline", + "type": "uint256" + }, + { + "internalType": "uint8", + "name": "v", + "type": "uint8" + }, + { + "internalType": "bytes32", + "name": "r", + "type": "bytes32" + }, + { + "internalType": "bytes32", + "name": "s", + "type": "bytes32" + } + ], + "name": "permit", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "symbol", + "outputs": [ + { + "internalType": "string", + "name": "", + "type": "string" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "totalSupply", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transfer", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "from", + "type": "address" + }, + { + "internalType": "address", + "name": "to", + "type": "address" + }, + { + "internalType": "uint256", + "name": "value", + "type": "uint256" + } + ], + "name": "transferFrom", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "nonpayable", + "type": "function" + } +] \ No newline at end of file diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx index 35bd317d..398dd55a 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.tsx +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -3,13 +3,18 @@ import { usePublicClient, useWaitForTransactionReceipt, useWriteContract, + useReadContracts, + useSignTypedData, + useAccount, } from "wagmi"; +import { hexToSignature } from "viem"; import { useProposal } from "./useProposal"; import { useProposalVetoes } from "@/plugins/dualGovernance/hooks/useProposalVetoes"; import { useUserCanVeto } from "@/plugins/dualGovernance/hooks/useUserCanVeto"; -import { OptimisticTokenVotingPluginAbi } from "@/plugins/dualGovernance/artifacts/OptimisticTokenVotingPlugin.sol"; +import { LockToVetoPluginAbi } from "@/plugins/lockToVote/artifacts/LockToVetoPlugin.sol"; +import { ERC20Abi } from "@/plugins/lockToVote/artifacts/ERC20withPermit.sol"; import { useAlertContext, AlertContextProps } from "@/context/AlertContext"; -import { PUB_CHAIN, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; +import { PUB_CHAIN, PUB_TOKEN_ADDRESS, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; export function useProposalVeto(proposalId: string) { const publicClient = usePublicClient({ chainId: PUB_CHAIN.id }); @@ -27,6 +32,38 @@ export function useProposalVeto(proposalId: string) { ); const { addAlert } = useAlertContext() as AlertContextProps; + const account_address = useAccount().address!; + + const erc20Contract = { + address: PUB_TOKEN_ADDRESS, + abi: ERC20Abi, + }; + const { data: erc20data } = useReadContracts({ + contracts: [{ + ...erc20Contract, + functionName: "balanceOf", + args: [account_address], + },{ + ...erc20Contract, + functionName: "nonces", + args: [account_address], + },{ + ...erc20Contract, + functionName: "name", + },{ + ...erc20Contract, + functionName: "version", + }] + }); + + const [balanceResult, nonceResult, nameResult, versionResult] = erc20data || []; + + const balance = BigInt(Number(balanceResult?.result)); + const nonce = BigInt(Number(nonceResult?.result)); + const erc20_name = nameResult?.result; + const versionFromContract = versionResult?.result; + + const { signTypedData: vetoPermitSign } = useSignTypedData(); const { writeContract: vetoWrite, data: vetoTxHash, @@ -47,7 +84,8 @@ export function useProposalVeto(proposalId: string) { timeout: 4 * 1000, }); } else { - addAlert("Could not create the proposal", { type: "error" }); + console.error(vetoingError); + addAlert("Could not veto", { type: "error" }); } return; } @@ -72,12 +110,52 @@ export function useProposalVeto(proposalId: string) { }, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]); const vetoProposal = () => { - vetoWrite({ - abi: OptimisticTokenVotingPluginAbi, - address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, - functionName: "veto", - args: [proposalId], - }); + let deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60); + let value = balance; + let destination: `0x${string}` = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS; + + const domain = { + chainId: PUB_CHAIN.id, + name: String(erc20_name), + /* We assume 1 if permit version is not specified */ + version: String(versionFromContract ?? '1'), + verifyingContract: PUB_TOKEN_ADDRESS, + }; + + const types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }; + + const message = { + owner: account_address, + spender: destination, + value, + nonce, + deadline, + }; + + vetoPermitSign({ + account: account_address, + types, + domain, + primaryType: 'Permit', + message, + }, { onSuccess: (hexSignature) => { + let signature = hexToSignature(hexSignature); + + vetoWrite({ + abi: LockToVetoPluginAbi, + address: destination, + functionName: "vetoPermit", + args: [proposalId, value, deadline, signature.v, signature.r, signature.s], + }); + }}); }; return { From 0e763b59259f0ae65b4902e38d7d76cbac3fab58 Mon Sep 17 00:00:00 2001 From: xavikh Date: Thu, 14 Mar 2024 11:44:39 +0100 Subject: [PATCH 11/16] Fix null values --- plugins/lockToVote/hooks/useProposalVeto.tsx | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx index 2ae301ed..c06df574 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.tsx +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -58,11 +58,6 @@ export function useProposalVeto(proposalId: string) { const [balanceResult, nonceResult, nameResult, versionResult] = erc20data || []; - const balance = BigInt(Number(balanceResult?.result)); - const nonce = BigInt(Number(nonceResult?.result)); - const erc20_name = nameResult?.result; - const versionFromContract = versionResult?.result; - const { signTypedData: vetoPermitSign } = useSignTypedData(); const { writeContract: vetoWrite, @@ -109,6 +104,12 @@ export function useProposalVeto(proposalId: string) { }, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]); const vetoProposal = () => { + if (!proposal || !balanceResult || !nonceResult || !nameResult || !versionResult) return; + const balance = BigInt(Number(balanceResult?.result)); + const nonce = BigInt(Number(nonceResult?.result)); + const erc20_name = nameResult?.result; + const versionFromContract = versionResult?.result; + let deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60); let value = balance; let destination: `0x${string}` = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS; From f1081bafa433c7fd58e542ac60e10027a95d6984 Mon Sep 17 00:00:00 2001 From: Carlos Juarez Date: Thu, 14 Mar 2024 14:37:50 +0100 Subject: [PATCH 12/16] feat: fixing the statuses and changes in the namings --- .../components/proposal/details.tsx | 30 +++--- plugins/lockToVote/components/vote/tally.tsx | 4 +- .../lockToVote/hooks/useProposalExecute.tsx | 93 +++++++++++++++++++ .../hooks/useProposalVariantStatus.tsx | 8 +- plugins/lockToVote/pages/proposal.tsx | 4 +- 5 files changed, 117 insertions(+), 22 deletions(-) create mode 100644 plugins/lockToVote/hooks/useProposalExecute.tsx diff --git a/plugins/lockToVote/components/proposal/details.tsx b/plugins/lockToVote/components/proposal/details.tsx index 7bbdd917..61f20a0a 100644 --- a/plugins/lockToVote/components/proposal/details.tsx +++ b/plugins/lockToVote/components/proposal/details.tsx @@ -1,19 +1,31 @@ import dayjs from "dayjs"; +import { compactNumber } from "@/utils/numbers"; import { ReactNode } from "react"; +import { formatUnits } from "viem"; interface ProposalDetailsProps { minVetoVotingPower?: bigint; endDate?: bigint; - snapshotBlock?: bigint; } const ProposalDetails: React.FC = ({ /** Timestamp */ endDate, - snapshotBlock, + minVetoVotingPower, }) => { return ( <> + +

+ Threshold +

+
+

Min. Quorum

+ + {compactNumber(formatUnits(minVetoVotingPower || BigInt(0), 18))} + +
+

Ending @@ -27,23 +39,13 @@ const ProposalDetails: React.FC = ({

- -

- Snapshot -

-
-

Taken at block

- - {snapshotBlock?.toLocaleString()} - -
-
+ ); }; // This should be encapsulated as soon as ODS exports this widget -const Card = function ({ children }: { children: ReactNode }) { +const Card = function({ children }: { children: ReactNode }) { return (

- Vetoed + For

{compactNumber(formatUnits(voteCount || BigInt(0), 18))} @@ -27,7 +27,7 @@ const VetoTally: FC = ({ voteCount, votePercentage }) => ( ); // This should be encapsulated as soon as ODS exports this widget -const Card = function ({ children }: { children: ReactNode }) { +const Card = function({ children }: { children: ReactNode }) { return (

{ + if (!canExecute) return; + + executeWrite({ + chainId: PUB_CHAIN.id, + abi: OptimisticTokenVotingPluginAbi, + address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS, + functionName: "execute", + args: [proposalId], + }); + }; + + useEffect(() => { + if (executingStatus === "idle" || executingStatus === "pending") return; + else if (executingStatus === "error") { + if (executingError?.message?.startsWith("User rejected the request")) { + addAlert("Transaction rejected by the user", { + timeout: 4 * 1000, + }); + } else { + console.error(executingError); + addAlert("Could not execute the proposal", { + type: "error", + description: + "The proposal may contain actions with invalid operations", + }); + } + return; + } + + // success + if (!executeTxHash) return; + else if (isConfirming) { + addAlert("Proposal submitted", { + description: "Waiting for the transaction to be validated", + type: "info", + txHash: executeTxHash, + }); + return; + } else if (!isConfirmed) return; + + addAlert("Proposal executed", { + description: "The transaction has been validated", + type: "success", + txHash: executeTxHash, + }); + + setTimeout(() => reload(), 1000 * 2); + }, [executingStatus, executeTxHash, isConfirming, isConfirmed]); + + return { + executeProposal, + canExecute: + !isCanVoteError && !isCanVoteLoading && !isConfirmed && !!canExecute, + isConfirming, + isConfirmed, + }; +} diff --git a/plugins/lockToVote/hooks/useProposalVariantStatus.tsx b/plugins/lockToVote/hooks/useProposalVariantStatus.tsx index 174d1259..d7008907 100644 --- a/plugins/lockToVote/hooks/useProposalVariantStatus.tsx +++ b/plugins/lockToVote/hooks/useProposalVariantStatus.tsx @@ -7,13 +7,13 @@ export const useProposalVariantStatus = (proposal: Proposal) => { useEffect(() => { if (!proposal || !proposal?.parameters) return; setStatus( - proposal?.vetoTally >= proposal?.parameters?.minVetoVotingPower - ? { variant: 'critical', label: 'Defeated' } + proposal?.vetoTally >= proposal?.parameters?.minVetoVotingPower + ? { variant: 'success', label: 'Executable' } : proposal?.active ? { variant: 'primary', label: 'Active' } - : proposal?.executed + : proposal?.executed ? { variant: 'success', label: 'Executed' } - : { variant: 'success', label: 'Executable' } + : { variant: 'critical', label: 'Defeated' } ); }, [proposal?.vetoTally, proposal?.active, proposal?.executed, proposal?.parameters?.minVetoVotingPower]); diff --git a/plugins/lockToVote/pages/proposal.tsx b/plugins/lockToVote/pages/proposal.tsx index 9c429782..5c1bfb8d 100644 --- a/plugins/lockToVote/pages/proposal.tsx +++ b/plugins/lockToVote/pages/proposal.tsx @@ -10,7 +10,7 @@ import { PleaseWaitSpinner } from "@/components/please-wait"; import { useSkipFirstRender } from "@/hooks/useSkipFirstRender"; import { useState } from "react"; import { useProposalVeto } from "@/plugins/lockToVote/hooks/useProposalVeto"; -import { useProposalExecute } from "@/plugins/dualGovernance/hooks/useProposalExecute"; +import { useProposalExecute } from "@/plugins/lockToVote/hooks/useProposalExecute"; import { useProposalClaimLock } from "@/plugins/lockToVote/hooks/useProposalClaimLock"; import { useAccount } from "wagmi"; @@ -86,7 +86,7 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) { />
From 3ef3c7edc0e77fb2a712615f1faba2b979a17b97 Mon Sep 17 00:00:00 2001 From: xavikh Date: Thu, 14 Mar 2024 19:01:02 +0100 Subject: [PATCH 13/16] Move permit our of veto hook --- .../ERC20Permit.sol.ts | 2 +- hooks/usePermit.ts | 99 +++++++++++++++++++ plugins/lockToVote/hooks/useProposalVeto.tsx | 90 ++++------------- 3 files changed, 118 insertions(+), 73 deletions(-) rename plugins/lockToVote/artifacts/ERC20withPermit.sol.ts => artifacts/ERC20Permit.sol.ts (99%) create mode 100644 hooks/usePermit.ts diff --git a/plugins/lockToVote/artifacts/ERC20withPermit.sol.ts b/artifacts/ERC20Permit.sol.ts similarity index 99% rename from plugins/lockToVote/artifacts/ERC20withPermit.sol.ts rename to artifacts/ERC20Permit.sol.ts index 7c10533c..c6b8ddca 100644 --- a/plugins/lockToVote/artifacts/ERC20withPermit.sol.ts +++ b/artifacts/ERC20Permit.sol.ts @@ -1,5 +1,5 @@ import { Abi } from "viem"; -export const ERC20Abi: Abi = [ +export const ERC20PermitAbi: Abi = [ { "inputs": [], "stateMutability": "nonpayable", diff --git a/hooks/usePermit.ts b/hooks/usePermit.ts new file mode 100644 index 00000000..f4b8124f --- /dev/null +++ b/hooks/usePermit.ts @@ -0,0 +1,99 @@ +import { useEffect } from "react"; +import { + useReadContracts, + useSignTypedData, + useAccount, +} from "wagmi"; +import { hexToSignature } from "viem"; +import { ERC20PermitAbi } from "@/artifacts/ERC20Permit.sol"; +import { useAlertContext, AlertContextProps } from "@/context/AlertContext"; +import { PUB_CHAIN, PUB_TOKEN_ADDRESS } from "@/constants"; + +export function usePermit() { + const { addAlert } = useAlertContext() as AlertContextProps; + + const account_address = useAccount().address!; + const erc20Contract = { + address: PUB_TOKEN_ADDRESS, + abi: ERC20PermitAbi, + }; + const { data: erc20data, refetch: erc20refetch } = useReadContracts({ + contracts: [{ + ...erc20Contract, + functionName: "nonces", + args: [account_address], + },{ + ...erc20Contract, + functionName: "name", + },{ + ...erc20Contract, + functionName: "version", + }] + }); + const [nonceResult, nameResult, versionResult] = erc20data || []; + + const { signTypedDataAsync: permitSign, status: permitSignStatus } = useSignTypedData(); + + useEffect(() => { + switch (permitSignStatus) { + case "idle": + case "pending": + return; + case "error": + addAlert("Could not sign the permit", { type: "error", timeout: 1500 }); + return; + case "success": + addAlert("Permit signed", { type: "success", timeout: 1500 }); + return; + } + }, [permitSignStatus]); + + const signPermit = async (dest: `0x${string}`, value: BigInt, deadline: BigInt = BigInt(Math.floor(Date.now() / 1000) + 60 * 60)) => { + if (!nonceResult || !nameResult || !versionResult) return; + + const nonce = BigInt(Number(nonceResult?.result)); + const erc20_name = String(nameResult?.result); + /* We assume 1 if permit version is not specified */ + const versionFromContract = String(versionResult?.result ?? '1'); + + const domain = { + chainId: PUB_CHAIN.id, + name: erc20_name, + version: versionFromContract, + verifyingContract: PUB_TOKEN_ADDRESS, + }; + + const types = { + Permit: [ + { name: 'owner', type: 'address' }, + { name: 'spender', type: 'address' }, + { name: 'value', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'deadline', type: 'uint256' }, + ], + }; + + const message = { + owner: account_address, + spender: dest, + value, + nonce, + deadline, + }; + + let sig = await permitSign({ + account: account_address, + types, + domain, + primaryType: 'Permit', + message, + }); + + return hexToSignature(sig); + }; + + return { + refetchPermitData: erc20refetch, + signPermit, + }; +} diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx index c06df574..bd05aee5 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.tsx +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -3,16 +3,15 @@ import { usePublicClient, useWaitForTransactionReceipt, useWriteContract, - useReadContracts, - useSignTypedData, + useReadContract, useAccount, } from "wagmi"; -import { hexToSignature } from "viem"; +import { ERC20PermitAbi } from "@/artifacts/ERC20Permit.sol"; import { useProposal } from "./useProposal"; import { useProposalVetoes } from "@/plugins/lockToVote/hooks/useProposalVetoes"; import { useUserCanVeto } from "@/plugins/lockToVote/hooks/useUserCanVeto"; import { LockToVetoPluginAbi } from "@/plugins/lockToVote/artifacts/LockToVetoPlugin.sol"; -import { ERC20Abi } from "@/plugins/lockToVote/artifacts/ERC20withPermit.sol"; +import { usePermit } from "@/hooks/usePermit"; import { useAlertContext, AlertContextProps } from "@/context/AlertContext"; import { PUB_CHAIN, PUB_TOKEN_ADDRESS, PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS } from "@/constants"; @@ -30,35 +29,18 @@ export function useProposalVeto(proposalId: string) { proposalId, proposal ); + const { signPermit, refetchPermitData } = usePermit(); const { addAlert } = useAlertContext() as AlertContextProps; const account_address = useAccount().address!; - const erc20Contract = { + const { data: balanceData } = useReadContract({ address: PUB_TOKEN_ADDRESS, - abi: ERC20Abi, - }; - const { data: erc20data } = useReadContracts({ - contracts: [{ - ...erc20Contract, - functionName: "balanceOf", - args: [account_address], - },{ - ...erc20Contract, - functionName: "nonces", - args: [account_address], - },{ - ...erc20Contract, - functionName: "name", - },{ - ...erc20Contract, - functionName: "version", - }] + abi: ERC20PermitAbi, + functionName: "balanceOf", + args: [account_address], }); - const [balanceResult, nonceResult, nameResult, versionResult] = erc20data || []; - - const { signTypedData: vetoPermitSign } = useSignTypedData(); const { writeContract: vetoWrite, data: vetoTxHash, @@ -79,6 +61,7 @@ export function useProposalVeto(proposalId: string) { timeout: 4 * 1000, }); } else { + console.error(vetoingError); addAlert("Could not create the veto", { type: "error" }); } return; @@ -101,61 +84,24 @@ export function useProposalVeto(proposalId: string) { }); refetchCanVeto(); refetchProposal(); + refetchPermitData(); }, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]); const vetoProposal = () => { - if (!proposal || !balanceResult || !nonceResult || !nameResult || !versionResult) return; - const balance = BigInt(Number(balanceResult?.result)); - const nonce = BigInt(Number(nonceResult?.result)); - const erc20_name = nameResult?.result; - const versionFromContract = versionResult?.result; - - let deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60); - let value = balance; - let destination: `0x${string}` = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS; - - const domain = { - chainId: PUB_CHAIN.id, - name: String(erc20_name), - /* We assume 1 if permit version is not specified */ - version: String(versionFromContract ?? '1'), - verifyingContract: PUB_TOKEN_ADDRESS, - }; + let dest: `0x${string}` = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS; + let value = BigInt(Number(balanceData)); + let deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60); // 1 hour from now - const types = { - Permit: [ - { name: 'owner', type: 'address' }, - { name: 'spender', type: 'address' }, - { name: 'value', type: 'uint256' }, - { name: 'nonce', type: 'uint256' }, - { name: 'deadline', type: 'uint256' }, - ], - }; - - const message = { - owner: account_address, - spender: destination, - value, - nonce, - deadline, - }; - - vetoPermitSign({ - account: account_address, - types, - domain, - primaryType: 'Permit', - message, - }, { onSuccess: (hexSignature) => { - let signature = hexToSignature(hexSignature); + signPermit(dest, value, deadline).then((sig) => { + if (!sig) return; vetoWrite({ abi: LockToVetoPluginAbi, - address: destination, + address: dest, functionName: "vetoPermit", - args: [proposalId, value, deadline, signature.v, signature.r, signature.s], + args: [proposalId, value, deadline, sig.v, sig.r, sig.s], }); - }}); + }); }; return { From d5e877fae4c229ae26bbcc13ef6a8adac49a7794 Mon Sep 17 00:00:00 2001 From: xavikh Date: Fri, 15 Mar 2024 11:57:09 +0100 Subject: [PATCH 14/16] Add user rejected alert --- hooks/usePermit.ts | 30 +++++++++++++------- plugins/lockToVote/hooks/useProposalVeto.tsx | 1 - 2 files changed, 20 insertions(+), 11 deletions(-) diff --git a/hooks/usePermit.ts b/hooks/usePermit.ts index f4b8124f..cd058884 100644 --- a/hooks/usePermit.ts +++ b/hooks/usePermit.ts @@ -32,7 +32,7 @@ export function usePermit() { }); const [nonceResult, nameResult, versionResult] = erc20data || []; - const { signTypedDataAsync: permitSign, status: permitSignStatus } = useSignTypedData(); + const { signTypedDataAsync: permitSign, status: permitSignStatus, error: permitSignError } = useSignTypedData(); useEffect(() => { switch (permitSignStatus) { @@ -40,7 +40,13 @@ export function usePermit() { case "pending": return; case "error": - addAlert("Could not sign the permit", { type: "error", timeout: 1500 }); + if (permitSignError?.message?.startsWith("User rejected the request")) { + addAlert("Transaction rejected by the user", { + timeout: 4 * 1000, + }); + } else { + addAlert("Could not sign the permit", { type: "error", timeout: 1500 }); + } return; case "success": addAlert("Permit signed", { type: "success", timeout: 1500 }); @@ -81,15 +87,19 @@ export function usePermit() { deadline, }; - let sig = await permitSign({ - account: account_address, - types, - domain, - primaryType: 'Permit', - message, - }); + try { + let sig = await permitSign({ + account: account_address, + types, + domain, + primaryType: 'Permit', + message, + }); - return hexToSignature(sig); + return hexToSignature(sig); + } catch (e) { + return; + } }; return { diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx index bd05aee5..8406e0dd 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.tsx +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -61,7 +61,6 @@ export function useProposalVeto(proposalId: string) { timeout: 4 * 1000, }); } else { - console.error(vetoingError); addAlert("Could not create the veto", { type: "error" }); } return; From 599f45811616493eb27fb38a232aa325d0c4269c Mon Sep 17 00:00:00 2001 From: xavikh Date: Fri, 15 Mar 2024 17:09:48 +0100 Subject: [PATCH 15/16] Use proper type for addresses --- hooks/usePermit.ts | 4 ++-- plugins/lockToVote/hooks/useProposalVeto.tsx | 3 ++- utils/ipfs.ts | 4 ++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/hooks/usePermit.ts b/hooks/usePermit.ts index cd058884..e33aa02e 100644 --- a/hooks/usePermit.ts +++ b/hooks/usePermit.ts @@ -4,7 +4,7 @@ import { useSignTypedData, useAccount, } from "wagmi"; -import { hexToSignature } from "viem"; +import { hexToSignature, Address } from "viem"; import { ERC20PermitAbi } from "@/artifacts/ERC20Permit.sol"; import { useAlertContext, AlertContextProps } from "@/context/AlertContext"; import { PUB_CHAIN, PUB_TOKEN_ADDRESS } from "@/constants"; @@ -54,7 +54,7 @@ export function usePermit() { } }, [permitSignStatus]); - const signPermit = async (dest: `0x${string}`, value: BigInt, deadline: BigInt = BigInt(Math.floor(Date.now() / 1000) + 60 * 60)) => { + const signPermit = async (dest: Address, value: BigInt, deadline: BigInt = BigInt(Math.floor(Date.now() / 1000) + 60 * 60)) => { if (!nonceResult || !nameResult || !versionResult) return; const nonce = BigInt(Number(nonceResult?.result)); diff --git a/plugins/lockToVote/hooks/useProposalVeto.tsx b/plugins/lockToVote/hooks/useProposalVeto.tsx index 8406e0dd..1968d9ec 100644 --- a/plugins/lockToVote/hooks/useProposalVeto.tsx +++ b/plugins/lockToVote/hooks/useProposalVeto.tsx @@ -6,6 +6,7 @@ import { useReadContract, useAccount, } from "wagmi"; +import { Address } from "viem"; import { ERC20PermitAbi } from "@/artifacts/ERC20Permit.sol"; import { useProposal } from "./useProposal"; import { useProposalVetoes } from "@/plugins/lockToVote/hooks/useProposalVetoes"; @@ -87,7 +88,7 @@ export function useProposalVeto(proposalId: string) { }, [vetoingStatus, vetoTxHash, isConfirming, isConfirmed]); const vetoProposal = () => { - let dest: `0x${string}` = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS; + let dest: Address = PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS; let value = BigInt(Number(balanceData)); let deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 60); // 1 hour from now diff --git a/utils/ipfs.ts b/utils/ipfs.ts index 17b4611c..0c3d65a1 100644 --- a/utils/ipfs.ts +++ b/utils/ipfs.ts @@ -1,6 +1,6 @@ import { PUB_IPFS_ENDPOINT, PUB_IPFS_API_KEY } from "@/constants"; import { CID, IPFSHTTPClient } from "ipfs-http-client"; -import { fromHex } from "viem"; +import { fromHex, Address } from "viem"; export function fetchJsonFromIpfs(hexIpfsUri: string) { return fetchFromIPFS(hexIpfsUri).then((res) => res.json()); @@ -34,7 +34,7 @@ async function fetchFromIPFS(hexIpfsUri: string): Promise { } function getPath(hexIpfsUri: string) { - const decodedUri = fromHex(hexIpfsUri as `0x${string}`, "string"); + const decodedUri = fromHex(hexIpfsUri as Address, "string"); const path = decodedUri.includes("ipfs://") ? decodedUri.substring(7) : decodedUri; From 710aa0ccf96fe6ba310e0f4e1e391ecd45613a8a Mon Sep 17 00:00:00 2001 From: xavikh Date: Fri, 15 Mar 2024 17:21:03 +0100 Subject: [PATCH 16/16] Fix build error --- plugins/lockToVote/hooks/useProposalClaimLock.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/lockToVote/hooks/useProposalClaimLock.tsx b/plugins/lockToVote/hooks/useProposalClaimLock.tsx index 5a3815db..c6b60e18 100644 --- a/plugins/lockToVote/hooks/useProposalClaimLock.tsx +++ b/plugins/lockToVote/hooks/useProposalClaimLock.tsx @@ -88,7 +88,7 @@ export function useProposalClaimLock(proposalId: string) { return { claimLockProposal, - hasClaimed, + hasClaimed: !!hasClaimed, isConfirming, isConfirmed, };