zhereh-frontend

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README

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:
Mpackage-lock.json | 14++++++++++++++
Mpackage.json | 1+
Msrc/components/ProposalForm.svelte | 82+++++++++++--------------------------------------------------------------------
Msrc/components/ProposalView.svelte | 158+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------
Asrc/lib/hash.ts | 41+++++++++++++++++++++++++++++++++++++++++
Msrc/shared/token-vote-abi.ts | 54++++++++++++++++++++++++++----------------------------
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