|
| 1 | +/** |
| 2 | + * TODO: |
| 3 | + * The goal is to automatically decrypt each encrypted voteID previously cast as encrypted commitments, and cast them as votes. |
| 4 | + * - modify shutter.ts encrypt() to return {encryptedCommitment, identity} |
| 5 | + * - implement shutterAutoVote.ts that: |
| 6 | + * - provides a castCommit() function which |
| 7 | + * - calls shutter.ts encrypt() with the message "disputeId␟voteId␟choice␟justification" with `U+241F` as separator |
| 8 | + * - calls the DisputeKitShutterPoC.castCommit() function with the encryptedCommitment, voteId, choice and justification |
| 9 | + * - retrieve the DisputeKitShutterPoC.CommitCast event and log its parameters |
| 10 | + * - provides an autoVote() function which |
| 11 | + * - runs continuously as a loop, waking up every 30 seconds |
| 12 | + * - upon wake up, retrieve the details of the previously encrypted messages (and corresponding identities) which have not yet been decrypted and have been encrypted for more than shutter.ts `DECRYPTION_DELAY`. |
| 13 | + * - for each of these messages, call shutter.ts decrypt() with the identity and the encryptedCommitment |
| 14 | + * - if the decryption is successful, call the DisputeKitShutterPoC.castVote() function with the voteId, choice and justification |
| 15 | + * - if the decryption is not successful, log an error |
| 16 | + * - if the castVote() function was called, retrieve the DisputeKitShutterPoC.VoteCast event and log its parameters |
| 17 | + * - shutterAutoVote.ts needs to know: |
| 18 | + * - the _voteIDs: they start from 0 and go up to DisputeKitShutterPoC.maxVoteIDs() |
| 19 | + * - the _coreDisputeID: just use 0 for now. |
| 20 | + **/ |
| 21 | + |
| 22 | +import { createPublicClient, createWalletClient, http, Hex, decodeEventLog, Log, getContract } from "viem"; |
| 23 | +import { privateKeyToAccount } from "viem/accounts"; |
| 24 | +import { hardhat } from "viem/chains"; |
| 25 | +import { encrypt, decrypt, DECRYPTION_DELAY } from "./shutter"; |
| 26 | +import dotenv from "dotenv"; |
| 27 | +import { abi as DisputeKitShutterPoCAbi } from "../deployments/localhost/DisputeKitShutterPoC.json"; |
| 28 | + |
| 29 | +// Load environment variables |
| 30 | +dotenv.config(); |
| 31 | + |
| 32 | +// Constants |
| 33 | +const SEPARATOR = "␟"; // U+241F |
| 34 | + |
| 35 | +// Validate environment variables |
| 36 | +if (!process.env.PRIVATE_KEY) throw new Error("PRIVATE_KEY environment variable is required"); |
| 37 | + |
| 38 | +/** |
| 39 | + * Split a hex string into bytes32 chunks |
| 40 | + */ |
| 41 | +function splitToBytes32(hex: string): Hex[] { |
| 42 | + // Remove 0x prefix if present |
| 43 | + const cleanHex = hex.startsWith("0x") ? hex.slice(2) : hex; |
| 44 | + |
| 45 | + // Split into chunks of 64 characters (32 bytes) |
| 46 | + const chunks: Hex[] = []; |
| 47 | + for (let i = 0; i < cleanHex.length; i += 64) { |
| 48 | + const chunk = `0x${cleanHex.slice(i, i + 64)}` as Hex; |
| 49 | + chunks.push(chunk); |
| 50 | + } |
| 51 | + |
| 52 | + return chunks; |
| 53 | +} |
| 54 | + |
| 55 | +// Store encrypted votes for later decryption |
| 56 | +interface EncryptedVote { |
| 57 | + encryptedCommitment: string; |
| 58 | + identity: Hex; |
| 59 | + timestamp: number; |
| 60 | + voteId: bigint; |
| 61 | +} |
| 62 | + |
| 63 | +const encryptedVotes: EncryptedVote[] = []; |
| 64 | + |
| 65 | +// Initialize Viem clients |
| 66 | +const publicClient = createPublicClient({ |
| 67 | + chain: hardhat, |
| 68 | + transport: http(), |
| 69 | +}); |
| 70 | + |
| 71 | +const PRIVATE_KEY = "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80" as Hex; |
| 72 | +const CONTRACT_ADDRESS = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as const; |
| 73 | + |
| 74 | +const account = privateKeyToAccount(PRIVATE_KEY); |
| 75 | +const walletClient = createWalletClient({ |
| 76 | + account, |
| 77 | + chain: hardhat, |
| 78 | + transport: http(), |
| 79 | +}); |
| 80 | + |
| 81 | +const disputeKit = getContract({ |
| 82 | + address: CONTRACT_ADDRESS, |
| 83 | + abi: DisputeKitShutterPoCAbi, |
| 84 | + client: { public: publicClient, wallet: walletClient }, |
| 85 | +}); |
| 86 | + |
| 87 | +/** |
| 88 | + * Cast an encrypted commit for a vote |
| 89 | + */ |
| 90 | +export async function castCommit({ |
| 91 | + disputeId, |
| 92 | + voteId, |
| 93 | + choice, |
| 94 | + justification, |
| 95 | +}: { |
| 96 | + disputeId: bigint; |
| 97 | + voteId: bigint; |
| 98 | + choice: bigint; |
| 99 | + justification: string; |
| 100 | +}) { |
| 101 | + try { |
| 102 | + // Create message with U+241F separator |
| 103 | + const message = `${disputeId}${SEPARATOR}${voteId}${SEPARATOR}${choice}${SEPARATOR}${justification}`; |
| 104 | + |
| 105 | + // Encrypt the message |
| 106 | + const { encryptedCommitment, identity } = await encrypt(message); |
| 107 | + |
| 108 | + // Split encrypted commitment into bytes32 chunks |
| 109 | + const commitmentChunks = splitToBytes32(encryptedCommitment); |
| 110 | + console.log("Commitment chunks:", commitmentChunks); |
| 111 | + |
| 112 | + // Cast the commit on-chain |
| 113 | + const hash = await disputeKit.write.castCommit([disputeId, [voteId], encryptedCommitment as Hex, identity as Hex]); |
| 114 | + |
| 115 | + // Store encrypted vote for later decryption |
| 116 | + encryptedVotes.push({ |
| 117 | + encryptedCommitment, |
| 118 | + identity: identity as Hex, |
| 119 | + timestamp: Math.floor(Date.now() / 1000), |
| 120 | + voteId, |
| 121 | + }); |
| 122 | + |
| 123 | + // Watch for CommitCast event |
| 124 | + const events = await disputeKit.getEvents.CommitCast(); |
| 125 | + console.log("CommitCast event:", (events[0] as any).args); |
| 126 | + |
| 127 | + return { encryptedCommitment, identity }; |
| 128 | + } catch (error) { |
| 129 | + console.error("Error in castCommit:", error); |
| 130 | + throw error; |
| 131 | + } |
| 132 | +} |
| 133 | + |
| 134 | +/** |
| 135 | + * Continuously monitor for votes ready to be decrypted and cast |
| 136 | + */ |
| 137 | +export async function autoVote() { |
| 138 | + while (true) { |
| 139 | + try { |
| 140 | + const currentTime = Math.floor(Date.now() / 1000); |
| 141 | + |
| 142 | + // Find votes ready for decryption |
| 143 | + const readyVotes = encryptedVotes.filter((vote) => currentTime - vote.timestamp >= DECRYPTION_DELAY); |
| 144 | + console.log("Ready votes:", readyVotes); |
| 145 | + |
| 146 | + for (const vote of readyVotes) { |
| 147 | + try { |
| 148 | + console.log("Decrypting vote:", vote); |
| 149 | + |
| 150 | + // Attempt to decrypt the vote |
| 151 | + const decryptedMessage = await decrypt(vote.encryptedCommitment, vote.identity); |
| 152 | + console.log("Decrypted message:", decryptedMessage); |
| 153 | + |
| 154 | + // Parse the decrypted message |
| 155 | + const [, , choice, justification] = decryptedMessage.split(SEPARATOR); |
| 156 | + |
| 157 | + // Cast the vote on-chain |
| 158 | + const hash = await disputeKit.write.castVote([ |
| 159 | + BigInt(0), |
| 160 | + [vote.voteId], |
| 161 | + BigInt(choice), |
| 162 | + justification, |
| 163 | + vote.identity, |
| 164 | + ]); |
| 165 | + |
| 166 | + // Watch for VoteCast event |
| 167 | + const events = await disputeKit.getEvents.VoteCast(); |
| 168 | + console.log("VoteCast event:", (events[0] as any).args); |
| 169 | + |
| 170 | + // Remove the processed vote |
| 171 | + const index = encryptedVotes.indexOf(vote); |
| 172 | + if (index > -1) encryptedVotes.splice(index, 1); |
| 173 | + } catch (error) { |
| 174 | + console.error(`Error processing vote ${vote.voteId}:`, error); |
| 175 | + } |
| 176 | + } |
| 177 | + |
| 178 | + // Sleep for 30 seconds |
| 179 | + console.log("Sleeping for 30 seconds"); |
| 180 | + await new Promise((resolve) => setTimeout(resolve, 30000)); |
| 181 | + } catch (error) { |
| 182 | + console.error("Error in autoVote loop:", error); |
| 183 | + // Continue the loop even if there's an error |
| 184 | + } |
| 185 | + } |
| 186 | +} |
| 187 | + |
| 188 | +// Main function to start the auto voting process |
| 189 | +async function main() { |
| 190 | + try { |
| 191 | + // Cast an encrypted commit |
| 192 | + await castCommit({ |
| 193 | + disputeId: BigInt(0), |
| 194 | + voteId: BigInt(0), |
| 195 | + choice: BigInt(2), |
| 196 | + justification: "This is my vote justification", |
| 197 | + }); |
| 198 | + |
| 199 | + // Start the auto voting process |
| 200 | + await autoVote(); |
| 201 | + } catch (error) { |
| 202 | + console.error("Error in main:", error); |
| 203 | + process.exit(1); |
| 204 | + } |
| 205 | +} |
| 206 | + |
| 207 | +// Execute if run directly |
| 208 | +if (require.main === module) { |
| 209 | + main(); |
| 210 | +} |
0 commit comments