commit 573364608293674d8752f5cd5a39bd1de8675e4c
parent 6e8d29186fd5c55d5025aab629d43fb96de3ff10
Author: William Muli <willi.wambu@gmail.com>
Date: Thu, 20 Jul 2023 22:22:17 +0300
Hash on frontend and check for match
Diffstat:
6 files changed, 235 insertions(+), 115 deletions(-)
diff --git a/package-lock.json b/package-lock.json
@@ -10,6 +10,7 @@
"dependencies": {
"@wagmi/chains": "^1.1.0",
"ethers": "^6.5.1",
+ "jssha": "^3.3.0",
"svelte-french-toast": "^1.0.4",
"viem": "^1.0.7"
},
@@ -2431,6 +2432,14 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
+ "node_modules/jssha": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz",
+ "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w==",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
@@ -5766,6 +5775,11 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
+ "jssha": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.3.0.tgz",
+ "integrity": "sha512-w9OtT4ALL+fbbwG3gw7erAO0jvS5nfvrukGPMWIAoea359B26ALXGpzy4YJSp9yGnpUvuvOw1nSjSoHDfWSr1w=="
+ },
"kleur": {
"version": "4.1.5",
"resolved": "https://registry.npmjs.org/kleur/-/kleur-4.1.5.tgz",
diff --git a/package.json b/package.json
@@ -39,6 +39,7 @@
"dependencies": {
"@wagmi/chains": "^1.1.0",
"ethers": "^6.5.1",
+ "jssha": "^3.3.0",
"svelte-french-toast": "^1.0.4",
"viem": "^1.0.7"
}
diff --git a/src/components/ProposalForm.svelte b/src/components/ProposalForm.svelte
@@ -6,35 +6,29 @@
import { voteContractAbi } from '../shared/token-vote-abi'
import { Routes } from '../shared/types'
import { Wala } from '$lib/wala'
+ import { computeHash } from '$lib/hash'
type ProposalFormData = {
description: string
targetVote: number
blockWait: number
- options: string[]
}
const formData: ProposalFormData = {
description: '',
targetVote: 0,
blockWait: 0,
- options: []
}
let submitting: boolean = false
const onSubmit = async () => {
- const { description, targetVote, blockWait, options } = formData
+ const { description, targetVote, blockWait } = formData
if (description === '' || targetVote === 0 || blockWait === 0) return
try {
- const optionsValid = options.some(option => option.trim() !== '') && options.length > 0
submitting = true
- if (optionsValid) {
- await createWithOptions(description, blockWait, targetVote, options)
- } else {
- await createWithoutOptions(description, blockWait, targetVote)
- }
+ await createWithoutOptions(description, blockWait, targetVote)
submitting = false
goto(Routes.Home)
} catch (error) {
@@ -42,8 +36,15 @@
}
}
- const createWithoutOptions = async(description: string, blockWait: number, targetVote: number) => {
+ const createWithoutOptions = async (description: string, blockWait: number, targetVote: number) => {
+ const localHash = await computeHash(description, 'SHA-256')
const descriptionHash = await new Wala().put(description, 'text/plain')
+ // compare hashes
+ if (localHash !== descriptionHash) {
+ console.error(`Computed description hash does not match wala hash. Local Hash: ${localHash}, Wala Hash: ${descriptionHash}`)
+ } else {
+ console.log('Description hashes match.')
+ }
const { request } = await publicClient($configuredChain).simulateContract({
address: PUBLIC_VOTE_CONTRACT_ADDRESS as `0x${string}`,
@@ -56,38 +57,8 @@
const receipt = await publicClient($configuredChain).waitForTransactionReceipt({
hash
})
-
return receipt
}
-
- const createWithOptions = async(description: string, blockWait: number, targetVote: number, options: string[]) => {
- const wala = new Wala()
- const descriptionHash = await wala.put(description, 'text/plain')
- const promises = options.map(option => wala.put(option, 'text/plain'))
- const optionHashes = (await Promise.all(promises)).map(optionHash => `0x${optionHash}`)
- const { request } = await publicClient($configuredChain).simulateContract({
- address: PUBLIC_VOTE_CONTRACT_ADDRESS as `0x${string}`,
- abi: voteContractAbi,
- functionName: 'proposeMulti',
- args: [`0x${descriptionHash}`, optionHashes as `0x${string}`[], BigInt(blockWait), targetVote],
- account: $connectionDetails.userAddress
- })
- const hash = await walletClient($configuredChain).writeContract(request)
- const receipt = await publicClient($configuredChain).waitForTransactionReceipt({
- hash
- })
-
- return receipt
- }
-
- const removeOption = (index: number) => {
- const options = formData.options.filter((_option, idx) => idx !== index)
- formData.options = [ ...options ]
- }
-
- const addOption = () => {
- formData.options = [ ...formData.options, '']
- }
</script>
<div class="flex justify-center items-center w-full py-10">
@@ -138,37 +109,6 @@
/>
</div>
</div>
- <h2 class="text-gray-900 text-center w-full font-medium text-md mt-6">Options for voters to choose</h2>
- <div class="form-control w-full">
- {#each formData.options as option, i }
- <div class="flex justify-between items-center gap-5">
- <div class="w-full">
- <label for={`option-${i}`} class="label">
- <span class="label-text text-gray-800">Option {i + 1}</span>
- </label>
- <input
- name={`option-${i}`}
- type="text"
- placeholder="Enter option text"
- class="input input-bordered w-full"
- autocomplete="off"
- bind:value={formData.options[i]}
- />
- </div>
- <button on:click={() => removeOption(i)} type="button" class="btn btn-error btn-sm btn-circle mt-[35px]">
- <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
- <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
- </svg>
- </button>
- </div>
- {/each}
- </div>
- <div class="flex justify-end mt-4">
- <button on:click={addOption} type="button" class="btn btn-primary font-light text-white btn-sm normal-case">
- Add option
- </button>
- </div>
-
<p class="text-gray-600 text-xs mt-8">* Required</p>
<div class="flex justify-between">
diff --git a/src/components/ProposalView.svelte b/src/components/ProposalView.svelte
@@ -1,29 +1,91 @@
<script lang="ts">
- import { formatUnits, hexToString } from "viem"
+ import { createEventDispatcher, onMount } from "svelte"
+ import { formatUnits } from "viem"
import type { Proposal } from "../shared/types"
- import { configuredChain, voteToken } from "../store"
+ import { configuredChain, connectionDetails, voteToken } from "../store"
import { copyToClipboard } from "../utils/copy-clipboard"
- import { onMount } from "svelte"
- import { publicClient } from "../client"
+ import { publicClient, walletClient } from "../client"
import { getProposalStateDescription } from "../utils/proposal"
import { Wala } from "../lib/wala"
-
+ import { computeHash } from "../lib/hash"
+ import { PUBLIC_VOTE_CONTRACT_ADDRESS } from "$env/static/public"
+ import { voteContractAbi } from "../shared/token-vote-abi"
+ // Variables
export let proposal: Proposal
let description: string | undefined
- let options: (string | undefined) [] = []
+ let currentProposalOptions: (string | undefined) [] = []
export let title: string | undefined = undefined
let blockNumber: bigint
+ let formOptions: string[] = [
+ 'Yes',
+ 'No'
+ ]
+ let optionsModalOpen: boolean = false
+ let submitting: boolean = false
+ const dispatch = createEventDispatcher<{ refreshProposal: { proposalIdx: bigint }}>()
$: ({ decimals, symbol } = $voteToken)
+ $: isValidOptions = formOptions.every(option => option?.trim() !== '')
- onMount(async () => {
- blockNumber = await publicClient($configuredChain).getBlockNumber()
- })
+ // Methods
+ const removeOption = (index: number) => {
+ const newOptions = formOptions.filter((_option, idx) => idx !== index)
+ formOptions = [ ...newOptions ]
+ }
+
+ const addOption = () => {
+ formOptions = [ ...formOptions, '']
+ }
const copyAddress = async (text: string) => await copyToClipboard(text)
+ const onSubmit = async () => {
+ try {
+ const wala = new Wala()
+ const localHashes = await Promise.all(formOptions.map(option => computeHash(option, 'SHA-256')))
+ const walaHashes = await Promise.all(formOptions.map(option => wala.put(option, 'text/plain')))
+ const hashesMatch = localHashes.every((hash, idx) => hash === walaHashes[idx])
+ if (hashesMatch) {
+ console.log('Local hashes match hashes returned by wala server')
+ } else {
+ console.error(`Local hashes do not match those returned from wala.`)
+ console.log('Local Hashes: ', localHashes)
+ console.log('Wala Hashes: ', walaHashes)
+ }
+
+ const simulationPromises = walaHashes.map(walaHash => {
+ return publicClient($configuredChain).simulateContract({
+ address: PUBLIC_VOTE_CONTRACT_ADDRESS as `0x${string}`,
+ abi: voteContractAbi,
+ functionName: 'addOption',
+ args: [BigInt(0), `0x${walaHash}`],
+ account: $connectionDetails.userAddress
+ })
+ })
+
+ submitting = true
+ const simulationResult = await Promise.all(simulationPromises)
+ // transaction hashes
+ const txHashes = await Promise.all(simulationResult.map(req => {
+ return walletClient($configuredChain).writeContract(req.request)
+ }))
+ // tx receipts
+ const receipts = await Promise.all(txHashes.map(txHash => publicClient($configuredChain).waitForTransactionReceipt({ hash: txHash })))
+ submitting = false
+ // close options modal
+ optionsModalOpen = false
+ dispatch('refreshProposal', {
+ proposalIdx: BigInt(0)
+ })
+ } catch (error) {
+ console.error('Error creating options', error)
+ }
+ }
+
+ // Lifecyle callbacks
onMount(async () => {
+ blockNumber = await publicClient($configuredChain).getBlockNumber()
const wala = new Wala()
if(proposal.description) {
description = await wala.get(proposal.description) as string
@@ -31,7 +93,7 @@
if(proposal.options) {
const promises = proposal.options.map(option => wala.get(option))
- options = (await Promise.all(promises)) as string[]
+ currentProposalOptions = (await Promise.all(promises)) as string[]
}
})
</script>
@@ -47,16 +109,21 @@
</p>
<p class="text-gray-500 text-sm mt-4">Options</p>
- {#if options.length > 0}
+ {#if currentProposalOptions.length > 0}
<ul class="list-disc list-inside">
- {#each options as option}
+ {#each currentProposalOptions as option}
<li class="text-gray-900 text-sm mt-1">
{option}
</li>
{/each}
</ul>
{:else}
- <p class="text-gray-900 text-sm mt-1 italic">No voting options</p>
+ <button
+ class="btn btn-sm btn-primary text-white w-[110px] normal-case mt-4"
+ on:click={() => optionsModalOpen = true}
+ >
+ Add Options
+ </button>
{/if}
<p class="text-gray-500 text-sm mt-4">Supply</p>
@@ -102,8 +169,67 @@
<p class="text-gray-500 text-sm mt-4">Scan cursor</p>
<p class="text-gray-900 text-sm mt-1">
{proposal.scanCursor}
- </p>
+ </p>
</div>
</div>
</div>
-{/if}
-\ No newline at end of file
+{/if}
+
+<dialog id="networkDetails" class="modal" open={optionsModalOpen}>
+ <div class="modal-box">
+ <button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2" on:click={() => optionsModalOpen = false}>✕</button>
+ <h3 class="font-semibold text-lg mt-[-10px]">Add Options</h3>
+ <div class="form-control w-full">
+ {#each formOptions as _, i }
+ <div class="flex justify-between items-center gap-10">
+ <div class="w-full">
+ <label for={`option-${i}`} class="label">
+ <span class="label-text text-gray-800">Option {i + 1}</span>
+ </label>
+ <input
+ name={`option-${i}`}
+ type="text"
+ placeholder="Enter option text"
+ class="input input-bordered w-full input-sm"
+ autocomplete="off"
+ bind:value={formOptions[i]}
+ required
+ />
+ </div>
+ <button on:click={() => removeOption(i)} type="button" class="btn btn-error btn-xs btn-circle mt-[35px]">
+ <svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-5 h-5">
+ <path stroke-linecap="round" stroke-linejoin="round" d="M19.5 12h-15" />
+ </svg>
+ </button>
+ </div>
+ {/each}
+ </div>
+ <div class="flex justify-start mt-4">
+ <button on:click={addOption} type="button" class="btn btn-primary font-light text-white btn-xs normal-case">
+ Add option
+ </button>
+ </div>
+ {#if formOptions.length > 0}
+ <div class="flex justify-between">
+ <button
+ type="submit"
+ class="btn btn-sm btn-primary self-center mt-6 normal-case text-white"
+ on:click={() => onSubmit()}
+ disabled={!isValidOptions}
+ >
+ Save Options
+ {#if submitting}
+ <span class="loading loading-spinner" />
+ {/if}
+ </button>
+ <button
+ class="btn btn-secondary btn-sm mt-6 normal-case text-white"
+ on:click={() => optionsModalOpen = false}
+ >
+ Cancel
+ </button>
+
+ </div>
+ {/if}
+ </div>
+</dialog>
+\ No newline at end of file
diff --git a/src/lib/hash.ts b/src/lib/hash.ts
@@ -0,0 +1,41 @@
+// Import the jssha library (make sure you have it installed)
+import jsSHA from 'jssha';
+
+// Define a custom type for the hashing algorithm
+type HashAlgorithm = 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512';
+
+/**
+ * Hashes a string or file using the specified algorithm.
+ * @param {string | Blob} data - The string or file to be hashed.
+ * @param {HashAlgorithm} algorithm - The hashing algorithm to use (e.g., "SHA-256", "SHA-512").
+ * @returns {Promise<string>} A promise that resolves with the resulting hash.
+ */
+export function computeHash(data: string | Blob, algorithm: HashAlgorithm): Promise<string> {
+ const shaObj = new jsSHA(algorithm, 'TEXT');
+
+ if (typeof data === 'string') {
+ shaObj.update(data);
+ return Promise.resolve(shaObj.getHash('HEX'));
+ } else if (data instanceof Blob) {
+ const reader = new FileReader();
+
+ return new Promise<string>((resolve, reject) => {
+ reader.onload = function (event) {
+ if (event.target?.result) {
+ shaObj.update(event.target.result as string);
+ resolve(shaObj.getHash('HEX'));
+ } else {
+ reject(new Error('File data is empty.'));
+ }
+ };
+
+ reader.onerror = function (event) {
+ reject(new Error('Error reading file: ' + event.target?.error));
+ };
+
+ reader.readAsBinaryString(data);
+ });
+ } else {
+ return Promise.reject(new Error('Unsupported data type. Only strings and files (Blob objects) are supported.'));
+ }
+}
diff --git a/src/shared/token-vote-abi.ts b/src/shared/token-vote-abi.ts
@@ -84,6 +84,24 @@ export const voteContractAbi = [
{
"inputs": [
{
+ "internalType": "uint256",
+ "name": "_proposalIdx",
+ "type": "uint256"
+ },
+ {
+ "internalType": "bytes32",
+ "name": "_optionDescription",
+ "type": "bytes32"
+ }
+ ],
+ "name": "addOption",
+ "outputs": [],
+ "stateMutability": "nonpayable",
+ "type": "function"
+ },
+ {
+ "inputs": [
+ {
"internalType": "address",
"name": "",
"type": "address"
@@ -284,14 +302,8 @@ export const voteContractAbi = [
"type": "function"
},
{
- "inputs": [
- {
- "internalType": "uint256",
- "name": "_proposalIdx",
- "type": "uint256"
- }
- ],
- "name": "optionCount",
+ "inputs": [],
+ "name": "lastProposalIdx",
"outputs": [
{
"internalType": "uint256",
@@ -305,22 +317,12 @@ export const voteContractAbi = [
{
"inputs": [
{
- "internalType": "bytes32",
- "name": "_description",
- "type": "bytes32"
- },
- {
"internalType": "uint256",
- "name": "_blockWait",
+ "name": "_proposalIdx",
"type": "uint256"
- },
- {
- "internalType": "uint24",
- "name": "_targetVotePpm",
- "type": "uint24"
}
],
- "name": "propose",
+ "name": "optionCount",
"outputs": [
{
"internalType": "uint256",
@@ -328,7 +330,7 @@ export const voteContractAbi = [
"type": "uint256"
}
],
- "stateMutability": "nonpayable",
+ "stateMutability": "view",
"type": "function"
},
{
@@ -339,11 +341,6 @@ export const voteContractAbi = [
"type": "bytes32"
},
{
- "internalType": "bytes32[]",
- "name": "_options",
- "type": "bytes32[]"
- },
- {
"internalType": "uint256",
"name": "_blockWait",
"type": "uint256"
@@ -354,7 +351,7 @@ export const voteContractAbi = [
"type": "uint24"
}
],
- "name": "proposeMulti",
+ "name": "propose",
"outputs": [
{
"internalType": "uint256",
@@ -520,4 +517,4 @@ export const voteContractAbi = [
"stateMutability": "nonpayable",
"type": "function"
}
-] as const
+] as const
+\ No newline at end of file