diff --git a/frontend/src/pages/CreatePollPage/index.tsx b/frontend/src/pages/CreatePollPage/index.tsx
index ba05050..50317d0 100644
--- a/frontend/src/pages/CreatePollPage/index.tsx
+++ b/frontend/src/pages/CreatePollPage/index.tsx
@@ -14,7 +14,8 @@ export const CreatePollPage = () => {
previousStep, nextStep,
fields,
hasErrorsOnCurrentPage,
- createPoll
+ createPoll,
+ isCreating,
} = useCreatePollForm()
return (
@@ -28,7 +29,7 @@ export const CreatePollPage = () => {
{ stepIndex > 0 && }
{ stepIndex < numberOfSteps - 1 && }
- { stepIndex === numberOfSteps - 1 && }
+ { stepIndex === numberOfSteps - 1 && }
diff --git a/frontend/src/pages/CreatePollPage/useCreatePollForm.ts b/frontend/src/pages/CreatePollPage/useCreatePollForm.ts
index 2895cd5..12be660 100644
--- a/frontend/src/pages/CreatePollPage/useCreatePollForm.ts
+++ b/frontend/src/pages/CreatePollPage/useCreatePollForm.ts
@@ -11,10 +11,12 @@ import {
import { useCreatePollUtils } from './useCreatePollUtils';
import classes from "./index.module.css"
-import { StringUtils } from '../../utils/string.utils';
import { DateUtils } from '../../utils/date.utils';
import { useTime } from '../../hooks/useTime';
import { MIN_CLOSE_TIME_MINUTES } from '../../constants/config';
+import { AclOptions } from '../../types';
+import { renderAddress } from '../../components/Addresses';
+import { useNavigate } from 'react-router-dom';
// The steps / pages of the wizard
const StepTitles= {
@@ -59,6 +61,10 @@ export const useCreatePollForm = () => {
checkXchainTokenHolder,
getXchainTokenDetails,
getXchainBlock,
+ getAllowAllACLOptions,
+ getTokenHolderAclOptions,
+ getAllowListAclOptions,
+ getXchainAclOptions,
createPoll: doCreatePoll,
} = useCreatePollUtils()
@@ -67,6 +73,7 @@ export const useCreatePollForm = () => {
const [stepIndex, setStepIndex] = useState(0);
const [validationPending, setValidationPending] = useState(false)
+ const navigate = useNavigate()
const { now } = useTime()
const question= useTextField({
@@ -180,12 +187,15 @@ export const useCreatePollForm = () => {
onItemEdited: (index, value, me) => {
if ((value.indexOf(",") !== -1)|| (value.indexOf(" ") !== -1) || (value.indexOf("\n") !== -1)) {
const addresses = splitAddresses(value)
+ const newAddresses = [...me.value]
for (let i = 0; i < addresses.length; i++) {
- me.value[index + i] = addresses[i]
+ newAddresses[index + i] = addresses[i]
}
- me.setValue(me.value)
+ me.setValue(newAddresses)
}
},
+ validateOnChange: true,
+ showValidationSuccess: true,
})
const chainChoices: Choice[] = useMemo(
@@ -267,7 +277,8 @@ export const useCreatePollForm = () => {
controls.updateStatus({message: progress})
})
if (!slot) return "Can't confirm this token at this wallet."
- xchainWalletSlot.setValue(`Confirmed ${slot.balanceDecimal} ${xchainTokenSymbol.value} at slot #${slot.index}.`)
+ xchainWalletBalance.setValue(`Confirmed ${slot.balanceDecimal} ${xchainTokenSymbol.value}`)
+ xchainWalletSlotNumber.setValue(slot.index.toString())
},
async (_value, changed, controls) => {
if (!changed) return
@@ -275,7 +286,7 @@ export const useCreatePollForm = () => {
const block = await getXchainBlock(chain.value)
if (!block?.hash) return "Failed to fetch latest block."
xchainBlockHash.setValue(block.hash)
- xchainBlockHeight.setValue(block.number)
+ xchainBlockHeight.setValue(block.number.toString())
},
],
validateOnChange: true,
@@ -284,14 +295,23 @@ export const useCreatePollForm = () => {
const hasValidXchainWallet = hasValidXchainTokenAddress && xchainWalletAddress.isValidated && !xchainWalletAddress.hasProblems
- const xchainWalletSlot = useLabel({
- name: "xchainWalletSlot",
+ const xchainWalletBalance = useLabel({
+ name: "xchainWalletBalance",
label: "Tokens confirmed:",
visible: hasValidXchainWallet,
initialValue: "",
classnames: classes.explanation
})
+ const xchainWalletSlotNumber = useLabel({
+ name: "xchainWalletSlotNumber",
+ label: "Stored at:",
+ visible: hasValidXchainWallet,
+ initialValue: "",
+ classnames: classes.explanation,
+ formatter: slot => `Slot #${slot}`
+ })
+
const xchainBlockHash = useLabel({
name: "xchainBlockHash",
label: "Reference Block Hash",
@@ -349,17 +369,17 @@ export const useCreatePollForm = () => {
],
} as const)
- const suggestedAmountOfRose = useTextField({
+ const amountOfSubsidy = useTextField({
name: "suggestedAmountOfRose",
visible: gasFree.value,
- label: "Suggested amount of ROSE",
+ label: "Amount of ROSE to set aside",
})
useEffect(
() => {
if (!gasFree.value) return
const cost = aclCostEstimates[accessControlMethod.value] * expectedRanges[numberOfExpectedVoters.value]
- suggestedAmountOfRose.setValue(cost.toString())
+ amountOfSubsidy.setValue(cost.toString())
},
[gasFree.value, accessControlMethod.value, numberOfExpectedVoters.value]
);
@@ -389,7 +409,7 @@ export const useCreatePollForm = () => {
description: "Everyone can see who voted for what.",
},
]
- })
+ } as const)
const hasCloseDate = useBooleanField({
name: "hasCloseDate",
@@ -434,12 +454,18 @@ export const useCreatePollForm = () => {
if (hasValidCloseDate) {
const deadline = pollCloseDate.value.getTime()/1000
const remaining = DateUtils.calculateRemainingTimeFrom(deadline, now)
- pollCloseLabel.setValue(DateUtils.getTextDescriptionOfTime(remaining))
+ pollCloseLabel.setValue(DateUtils.getTextDescriptionOfTime(remaining) ?? "")
}
},
[hasCloseDate.value, hasValidCloseDate, now],
);
+ const creationStatus = useLabel({
+ name: "creationStatus",
+ label: "",
+ initialValue: "",
+ })
+
const stepFields: Record = {
basics: [
question,
@@ -456,18 +482,19 @@ export const useCreatePollForm = () => {
xchainTokenAddress,
[xchainTokenName, xchainTokenSymbol],
xchainWalletAddress,
- xchainWalletSlot,
+ [xchainWalletBalance, xchainWalletSlotNumber],
[xchainBlockHash, xchainBlockHeight],
voteWeighting,
gasFree,
gasFreeExplanation,
- [numberOfExpectedVoters, suggestedAmountOfRose],
+ [numberOfExpectedVoters, amountOfSubsidy],
],
results: [
resultDisplayType,
hasCloseDate,
pollCloseDate,
pollCloseLabel,
+ creationStatus,
],
}
@@ -487,24 +514,60 @@ export const useCreatePollForm = () => {
setStepIndex(stepIndex + 1)
}
+ const getAclOptions = async (
+ updateStatus?: ((status: string | undefined) => void) | undefined,
+ ): Promise<[string, AclOptions]> => {
+ const acl = accessControlMethod.value
+ switch (acl) {
+ case 'acl_allowAll':
+ return getAllowAllACLOptions()
+ case 'acl_tokenHolder':
+ return getTokenHolderAclOptions(sapphireTokenAddress.value)
+ case 'acl_allowList':
+ return getAllowListAclOptions(addressWhitelist.value)
+ case 'acl_xchain':
+ return await getXchainAclOptions({
+ chainName: chain.value,
+ contractAddress: xchainTokenAddress.value,
+ slotNumber: parseInt(xchainWalletSlotNumber.value),
+ blockHash: xchainBlockHash.value,
+ }, updateStatus)
+ default:
+ throw new Error(`Unknown ACL contract ${acl}`);
+ }
+ }
+
const createPoll = async () => {
setValidationPending(true)
const hasErrors = await findErrorsInFields(stepFields[step])
setValidationPending(false)
if (hasErrors) return
- console.log("Should create poll", question.value)
+ const logger = (message?: string | undefined) => creationStatus.setValue(message ?? "")
+
+ // const logger = console.log
setIsCreating(true)
try {
- const newId = await doCreatePoll(
- question.value,
- description.value,
- answers.value,
- )
+ const [aclData, aclOptions] = await getAclOptions(logger)
+ const newId = await doCreatePoll({
+ question: question.value,
+ description: description.value,
+ answers: answers.value,
+ aclData, aclOptions,
+ subsidizeAmount: gasFree.value ? 10n : undefined,
+ publishVotes: resultDisplayType.value === "percentages_and_votes",
+ closeTime: hasCloseDate.value ? pollCloseDate.value : undefined,
+ }, logger)
+
console.log("Created new poll", newId)
+ navigate(`/polls/${newId.substring(2)}`)
} catch (ex) {
- console.log("Failed to create poll", ex)
+ let exString = "" + ex;
+ if (exString.startsWith("Error: user rejected action")) {
+ exString = "Signer refused to sign transaction."
+ }
+ logger("Failed to create poll: " + exString)
} finally {
setIsCreating(false)
}
diff --git a/frontend/src/pages/CreatePollPage/useCreatePollUtils.ts b/frontend/src/pages/CreatePollPage/useCreatePollUtils.ts
index edb97b9..f833c9c 100644
--- a/frontend/src/pages/CreatePollPage/useCreatePollUtils.ts
+++ b/frontend/src/pages/CreatePollPage/useCreatePollUtils.ts
@@ -1,6 +1,6 @@
import { useEthereum } from '../../hooks/useEthereum';
import { useContracts } from '../../hooks/useContracts';
-import { getAddress } from 'ethers';
+import { AbiCoder, getAddress, ParamType } from 'ethers';
import { AclOptions, Poll, PollManager } from "../../types"
import { encryptJSON } from '../../utils/crypto.demo';
import { Pinata } from '../../utils/Pinata';
@@ -9,7 +9,9 @@ import {
tokenDetailsFromProvider,
xchainRPC,
isERCTokenContract,
- guessStorageSlot
+ guessStorageSlot,
+ getBlockHeaderRLP,
+ fetchAccountProof,
} from '@oasisprotocol/side-dao-contracts';
import { useCallback } from 'react';
import {
@@ -42,41 +44,135 @@ export const useCreatePollUtils = () => {
, []
)
- const getACLOptions = async (): Promise<[string, AclOptions]> => {
- const acl = acl_allowAll;
- // const abi = AbiCoder.defaultAbiCoder();
+ const getAllowAllACLOptions = (): [string, AclOptions] => {
return [
'0x', // Empty bytes is passed
{
- address: acl,
+ address: VITE_CONTRACT_ACL_ALLOWALL,
options: { allowAll: true },
},
];
}
+ /**
+ * Encode the %%values%% as the %%types%% into ABI data.
+ *
+ * @returns DataHexstring
+ */
+ const abiEncode = (types: ReadonlyArray, values: ReadonlyArray): string => {
+ const abi = AbiCoder.defaultAbiCoder();
+ return abi.encode(types, values)
+ }
+
+ const getTokenHolderAclOptions = (tokenAddress: string): [string, AclOptions] => {
+ return [
+ abiEncode(['address'], [tokenAddress]),
+ {
+ address: VITE_CONTRACT_ACL_TOKENHOLDER,
+ options: { token: tokenAddress },
+ },
+ ];
+ }
+
+ const getAllowListAclOptions = (addresses: string[]): [string, AclOptions] => {
+ return [
+ abiEncode(['address[]'], [addresses]),
+ {
+ address: VITE_CONTRACT_ACL_VOTERALLOWLIST,
+ options: { allowList: true },
+ },
+ ];
+ }
+
+ const getXchainAclOptions = async (
+ props: {
+ chainName: string,
+ contractAddress: string,
+ slotNumber: number,
+ blockHash: string,
+ },
+ updateStatus?: ((status: string | undefined) => void) | undefined,
+ ): Promise<[string, AclOptions]> => {
+ const showStatus = updateStatus ?? ((message?: string | undefined) => console.log(message))
+ const { chainName, contractAddress, slotNumber, blockHash } = props
+ const chainId = chains[chainName]
+ const rpc = xchainRPC(chainId);
+ showStatus("Getting block header RLP")
+ const headerRlpBytes = await getBlockHeaderRLP(rpc, blockHash);
+ // console.log('headerRlpBytes', headerRlpBytes);
+ showStatus("Fetching account proof")
+ const rlpAccountProof = await fetchAccountProof(rpc, blockHash, contractAddress);
+ // console.log('rlpAccountProof', rlpAccountProof);
+ return [
+ abiEncode(
+ ['tuple(tuple(bytes32,address,uint256),bytes,bytes)'],
+ [
+ [
+ [
+ blockHash,
+ contractAddress,
+ slotNumber,
+ ],
+ headerRlpBytes,
+ rlpAccountProof,
+ ],
+ ],
+ ),
+ {
+ address: VITE_CONTRACT_ACL_STORAGEPROOF,
+ options: {
+ xchain: {
+ chainId,
+ blockHash,
+ address: contractAddress,
+ slot: slotNumber,
+ },
+ },
+ },
+ ];
+ }
+
const createPoll = async (
- question: string,
- description: string,
- answers: string[],
+ props: {
+ question: string,
+ description: string,
+ answers: string[],
+ aclData: string,
+ aclOptions: AclOptions,
+ subsidizeAmount: bigint | undefined,
+ publishVotes: boolean,
+ closeTime: Date | undefined,
+ },
+ updateStatus: (message: string) => void,
) => {
- const [aclData, aclOptions] = await getACLOptions();
+ const {
+ question, description, answers,
+ aclData, aclOptions,
+ subsidizeAmount,
+ publishVotes, closeTime,
+ } = props
+ updateStatus("Compiling data")
const poll: Poll = {
creator: eth.state.address!,
name: question,
description,
choices: answers,
options: {
- publishVotes: false, // publishVotes.value,
- closeTimestamp: 0, //toValue(expirationTime) ? toValue(expirationTime)!.valueOf() / 1000 : 0,
+ publishVotes,
+ closeTimestamp: closeTime ? closeTime.getTime() / 1000 : 0,
},
acl: aclOptions,
};
const { key, cipherbytes } = encryptJSON(poll);
+ updateStatus("Saving poll data to IPFS")
const ipfsHash = await Pinata.pinData(cipherbytes);
- console.log('Poll ipfsHash', ipfsHash);
+
+ if (!ipfsHash) throw new Error("Failed to save to IPFS, try again!")
+ // console.log('Poll ipfsHash', ipfsHash);
+ // updateStatus("Saved to IPFS")
const proposalParams: PollManager.ProposalParamsStruct = {
ipfsHash,
@@ -84,14 +180,18 @@ export const useCreatePollUtils = () => {
numChoices: answers.length,
publishVotes: poll.options.publishVotes,
closeTimestamp: poll.options.closeTimestamp,
- acl: acl_allowAll, // toValue(chosenPollACL),
+ acl: aclOptions.address,
};
+ // console.log("params are", proposalParams)
+
+ updateStatus("Calling signer")
const createProposalTx = await daoSigner!.create(proposalParams, aclData, {
- // Provide additional subsidy
- value: 10, //toValue(subsidyAmount) ?? 0n,
+ value: subsidizeAmount ?? 0n,
});
- console.log('doCreatePoll: creating proposal tx', createProposalTx.hash);
+ // console.log('doCreatePoll: creating proposal tx', createProposalTx.hash);
+
+ updateStatus("Sending transaction")
const receipt = (await createProposalTx.wait())!;
if (receipt.status !== 1) {
@@ -99,7 +199,11 @@ export const useCreatePollUtils = () => {
}
const proposalId = receipt.logs[0].data;
- console.log('doCreatePoll: Proposal ID', proposalId);
+ updateStatus("Created poll")
+
+ // console.log('doCreatePoll: Proposal ID', proposalId);
+
+ return proposalId;
}
const getSapphireTokenDetails = async (address: string) => {
@@ -148,6 +252,10 @@ export const useCreatePollUtils = () => {
getXchainTokenDetails,
checkXchainTokenHolder,
getXchainBlock,
+ getAllowAllACLOptions,
+ getTokenHolderAclOptions,
+ getAllowListAclOptions,
+ getXchainAclOptions,
createPoll,
}