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}
+
+
+
+
+
+
+
+
+ onVetoPressed()}
+ >
+ Veto
+
+
+
+
+
+
+
+
+
+ executeButtonPressed()}
+ >
+ Execute
+
+
+
+
+
+
+
+
+ {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]))}
+ />
+ )}
+
+
+
+
+
+
+
+
+ submitProposal()}
+ >
+ Submit proposal
+
+
+
+
+
+
+ Add the first action to continue
+
+
+
+ Actions
+
+
+ {actions?.map?.((action, i) => (
+
+ ))}
+
+
+
+
submitProposal()}
+ >
+ Submit proposal
+
+
+
+
+
+
+ );
+}
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
+
+
+
+
+
+ Submit Proposal
+
+
+
+
+
+
+
+ {visibleProposalIds.map((id) => (
+
+ ))}
+
+ setCurrentPage((page) => Math.max(page - 1, 0))}
+ iconLeft={IconType.CHEVRON_LEFT}
+ >
+ Previous
+
+ setCurrentPage((page) => page + 1)}
+ iconRight={IconType.CHEVRON_RIGHT}
+ >
+ Next
+
+
+
+
+
+
+
+
+
+
+ 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}
-
-
+
+
-
-
- onVetoPressed()}
- >
- Veto
-
-
-
-
-
-
+
-
-
- executeButtonPressed()}
- >
- Execute
-
-
-
-
+
+ onVetoPressed()}
+ >
+ Submit veto
+
+
+
+ onExecutePressed()}
+ >
+ Execute
+
+
+
@@ -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
+
+ onClaimLockPressed()}
+ >
+ Claim Locked Tokens
+
+
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
-
+
({
+ address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS,
+ abi: LockToVetoPluginAbi,
+ chainId: PUB_CHAIN.id,
+ functionName: "hasClaimedLock",
+ args: [proposalId, account.address],
+ });
+ const {
+ writeContract: claimLockWrite,
+ data: executeTxHash,
+ error: executingError,
+ status: claimingStatus,
+ } = useWriteContract();
+ const { isLoading: isConfirming, isSuccess: isConfirmed } =
+ useWaitForTransactionReceipt({ hash: executeTxHash });
+
+ const claimLockProposal = () => {
+ if (hasClaimed) return;
+
+ console.log(proposalId, account.address);
+ claimLockWrite({
+ chainId: PUB_CHAIN.id,
+ abi: LockToVetoPluginAbi,
+ address: PUB_LOCK_TO_VOTE_PLUGIN_ADDRESS,
+ functionName: "claimLock",
+ args: [proposalId, account.address],
+ });
+ };
+
+ useEffect(() => {
+ if (claimingStatus === "idle" || claimingStatus === "pending") return;
+ else if (claimingStatus === "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 claim locked tokens", {
+ type: "error",
+ description:
+ "The proposal may contain actions with invalid operations. Please get in contact with us.",
+ });
+ }
+ return;
+ }
+
+ // success
+ if (!executeTxHash) return;
+ else if (isConfirming) {
+ addAlert("Claim submitted", {
+ description: "Waiting for the transaction to be validated",
+ type: "info",
+ txHash: executeTxHash,
+ });
+ return;
+ } else if (!isConfirmed) return;
+
+ addAlert("Claim executed", {
+ description: "The transaction has been validated",
+ type: "success",
+ txHash: executeTxHash,
+ });
+
+ setTimeout(() => reload(), 1000 * 2);
+ }, [claimingStatus, executeTxHash, isConfirming, isConfirmed]);
+
+ return {
+ claimLockProposal,
+ hasClaimed,
+ isConfirming,
+ isConfirmed,
+ };
+}
diff --git a/plugins/lockToVote/pages/proposal.tsx b/plugins/lockToVote/pages/proposal.tsx
index 85813b68..9c429782 100644
--- a/plugins/lockToVote/pages/proposal.tsx
+++ b/plugins/lockToVote/pages/proposal.tsx
@@ -11,6 +11,7 @@ import { useSkipFirstRender } from "@/hooks/useSkipFirstRender";
import { useState } from "react";
import { useProposalVeto } from "@/plugins/lockToVote/hooks/useProposalVeto";
import { useProposalExecute } from "@/plugins/dualGovernance/hooks/useProposalExecute";
+import { useProposalClaimLock } from "@/plugins/lockToVote/hooks/useProposalClaimLock";
import { useAccount } from "wagmi";
type BottomSection = "description" | "vetoes";
@@ -41,6 +42,12 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) {
isConfirming: isConfirmingExecution,
} = useProposalExecute(proposalId);
+ const {
+ claimLockProposal,
+ isConfirming: isConfirmingClaimLock,
+ hasClaimed,
+ } = useProposalClaimLock(proposalId);
+
if (skipRender || !proposal || showProposalLoading) {
return (
@@ -58,12 +65,13 @@ export default function ProposalDetail({ id: proposalId }: { id: string }) {
transactionConfirming={isConfirmingVeto || isConfirmingExecution}
canVeto={canVeto}
canExecute={canExecute}
- addressLockedTokens={vetoes.some((veto) => {
- veto.voter === account.address;
- })}
+ hasClaimed={hasClaimed}
+ addressLockedTokens={vetoes.some(
+ (veto) => veto.voter === account.address
+ )}
onVetoPressed={() => vetoProposal()}
onExecutePressed={() => executeProposal()}
- // onClaimLockPressed={() => claimLockProposal()}
+ onClaimLockPressed={() => claimLockProposal()}
/>
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))}