diff --git a/contracts/deploy/09-shutter.ts b/contracts/deploy/09-shutter.ts new file mode 100644 index 000000000..b8d53fb1a --- /dev/null +++ b/contracts/deploy/09-shutter.ts @@ -0,0 +1,26 @@ +import { HardhatRuntimeEnvironment } from "hardhat/types"; +import { DeployFunction } from "hardhat-deploy/types"; +import { HomeChains, isSkipped } from "./utils"; + +const deployArbitration: DeployFunction = async (hre: HardhatRuntimeEnvironment) => { + const { deployments, getNamedAccounts, getChainId } = hre; + const { deploy } = deployments; + + // fallback to hardhat node signers on local network + const deployer = (await getNamedAccounts()).deployer ?? (await hre.ethers.getSigners())[0].address; + const chainId = Number(await getChainId()); + console.log("deploying to %s with deployer %s", HomeChains[chainId], deployer); + + await deploy("DisputeKitShutterPoC", { + from: deployer, + args: [], + log: true, + }); +}; + +deployArbitration.tags = ["Shutter"]; +deployArbitration.skip = async ({ network }) => { + return isSkipped(network, !HomeChains[network.config.chainId ?? 0]); +}; + +export default deployArbitration; diff --git a/contracts/package.json b/contracts/package.json index 0b4d3dd75..a1b1a8b5d 100644 --- a/contracts/package.json +++ b/contracts/package.json @@ -140,7 +140,6 @@ "hardhat-gas-reporter": "^2.2.2", "hardhat-tracer": "^3.1.0", "hardhat-watcher": "^2.5.0", - "node-fetch": "^3.3.2", "pino": "^8.21.0", "pino-pretty": "^10.3.1", "prettier": "^3.3.3", @@ -157,6 +156,8 @@ "@chainlink/contracts": "^1.3.0", "@kleros/vea-contracts": "^0.6.0", "@openzeppelin/contracts": "^5.2.0", + "@shutter-network/shutter-sdk": "^0.0.1", + "isomorphic-fetch": "^3.0.0", "viem": "^2.24.1" } } diff --git a/contracts/scripts/shutter.ts b/contracts/scripts/shutter.ts new file mode 100644 index 000000000..a1c05e1c3 --- /dev/null +++ b/contracts/scripts/shutter.ts @@ -0,0 +1,267 @@ +import { encryptData, decrypt as shutterDecrypt } from "@shutter-network/shutter-sdk"; +import { Hex, stringToHex, hexToString } from "viem"; +import crypto from "crypto"; +import "isomorphic-fetch"; + +// Time in seconds to wait before the message can be decrypted +export const DECRYPTION_DELAY = 5; + +interface ShutterApiMessageData { + eon: number; + identity: string; + identity_prefix: string; + eon_key: string; + tx_hash: string; +} + +interface ShutterApiResponse { + message: ShutterApiMessageData; + error?: string; +} + +interface ShutterDecryptionKeyData { + decryption_key: string; + identity: string; + decryption_timestamp: number; +} + +/** + * Fetches encryption data from the Shutter API + * @param decryptionTimestamp Unix timestamp when decryption should be possible + * @returns Promise with the eon key and identity + */ +async function fetchShutterData(decryptionTimestamp: number): Promise { + try { + console.log(`Sending request to Shutter API with decryption timestamp: ${decryptionTimestamp}`); + + // Generate a random identity prefix + const identityPrefix = generateRandomBytes32(); + console.log(`Generated identity prefix: ${identityPrefix}`); + + const response = await fetch("https://shutter-api.shutter.network/api/register_identity", { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + decryptionTimestamp, + identityPrefix, + }), + }); + + // Log the response status + console.log(`API response status: ${response.status}`); + + // Get the response text + const responseText = await response.text(); + + if (!response.ok) { + throw new Error(`API request failed with status ${response.status}: ${responseText}`); + } + + // Parse the JSON response + let jsonResponse: ShutterApiResponse; + try { + jsonResponse = JSON.parse(responseText); + } catch (error) { + throw new Error(`Failed to parse API response as JSON: ${responseText}`); + } + + // Check if we have the message data + if (!jsonResponse.message) { + throw new Error(`API response missing message data: ${JSON.stringify(jsonResponse)}`); + } + + return jsonResponse.message; + } catch (error) { + console.error("Error fetching data from Shutter API:", error); + throw error; + } +} + +/** + * Fetches the decryption key from the Shutter API + * @param identity The identity used for encryption + * @returns Promise with the decryption key data + */ +async function fetchDecryptionKey(identity: string): Promise { + console.log(`Fetching decryption key for identity: ${identity}`); + + const response = await fetch(`https://shutter-api.shutter.network/api/get_decryption_key?identity=${identity}`, { + method: "GET", + headers: { + accept: "application/json", + }, + }); + + // Get the response text + const responseText = await response.text(); + + // Try to parse the error response even if the request failed + let jsonResponse; + try { + jsonResponse = JSON.parse(responseText); + } catch (error) { + throw new Error(`Failed to parse API response as JSON: ${responseText}`); + } + + // Handle the "too early" error case specifically + if (!response.ok) { + if (jsonResponse?.description?.includes("timestamp not reached yet")) { + throw new Error( + `Cannot decrypt yet: The decryption timestamp has not been reached.\n` + + `Please wait at least ${DECRYPTION_DELAY} seconds after encryption before attempting to decrypt.\n` + + `Error details: ${jsonResponse.description}` + ); + } + throw new Error(`API request failed with status ${response.status}: ${responseText}`); + } + + // Check if we have the message data + if (!jsonResponse.message) { + throw new Error(`API response missing message data: ${JSON.stringify(jsonResponse)}`); + } + + return jsonResponse.message; +} + +/** + * Ensures a string is a valid hex string with 0x prefix + * @param hexString The hex string to validate + * @returns The validated hex string with 0x prefix + */ +function ensureHexString(hexString: string | undefined): `0x${string}` { + if (!hexString) { + throw new Error("Hex string is undefined or null"); + } + + // Add 0x prefix if it doesn't exist + const prefixedHex = hexString.startsWith("0x") ? hexString : `0x${hexString}`; + return prefixedHex as `0x${string}`; +} + +/** + * Generates a random 32 bytes + * @returns Random 32 bytes as a hex string with 0x prefix + */ +function generateRandomBytes32(): `0x${string}` { + return ("0x" + + crypto + .getRandomValues(new Uint8Array(32)) + .reduce((acc, byte) => acc + byte.toString(16).padStart(2, "0"), "")) as Hex; +} + +/** + * Encrypts a message using the Shutter API + * @param message The message to encrypt + * @returns Promise with the encrypted commitment and identity + */ +export async function encrypt(message: string): Promise<{ encryptedCommitment: string; identity: string }> { + // Set decryption timestamp + const decryptionTimestamp = Math.floor(Date.now() / 1000) + DECRYPTION_DELAY; + + // Fetch encryption data from Shutter API + console.log(`Fetching encryption data for decryption at timestamp ${decryptionTimestamp}...`); + const shutterData = await fetchShutterData(decryptionTimestamp); + + // Extract the eon key and identity from the response and ensure they have the correct format + const eonKeyHex = ensureHexString(shutterData.eon_key); + const identityHex = ensureHexString(shutterData.identity); + + // Message to encrypt + const msgHex = stringToHex(message); + + // Generate a random sigma + const sigmaHex = generateRandomBytes32(); + + console.log("Eon Key:", eonKeyHex); + console.log("Identity:", identityHex); + console.log("Sigma:", sigmaHex); + + // Encrypt the message + const encryptedCommitment = await encryptData(msgHex, identityHex, eonKeyHex, sigmaHex); + + return { encryptedCommitment, identity: identityHex }; +} + +/** + * Decrypts a message using the Shutter API + * @param encryptedMessage The encrypted message to decrypt + * @param identity The identity used for encryption + * @returns Promise with the decrypted message + */ +export async function decrypt(encryptedMessage: string, identity: string): Promise { + // Fetch the decryption key + const decryptionKeyData = await fetchDecryptionKey(identity); + console.log("Decryption key:", decryptionKeyData.decryption_key); + + // Ensure the decryption key is properly formatted + const decryptionKey = ensureHexString(decryptionKeyData.decryption_key); + + // Decrypt the message + const decryptedHexMessage = await shutterDecrypt(encryptedMessage, decryptionKey); + + // Convert the decrypted hex message back to a string + return hexToString(decryptedHexMessage as `0x${string}`); +} + +async function main() { + try { + const command = process.argv[2]?.toLowerCase(); + + if (!command) { + console.error(` +Usage: yarn ts-node shutter.ts [arguments] + +Commands: + encrypt Encrypt a message + decrypt Decrypt a message (requires the identity used during encryption) + +Examples: + yarn ts-node shutter.ts encrypt "my secret message" + yarn ts-node shutter.ts decrypt "encrypted-data" "0x1234..."`); + process.exit(1); + } + + switch (command) { + case "encrypt": { + const message = process.argv[3]; + if (!message) { + console.error("Error: Missing message to encrypt"); + console.error("Usage: yarn ts-node shutter.ts encrypt "); + process.exit(1); + } + const { encryptedCommitment, identity } = await encrypt(message); + console.log("\nEncrypted Commitment:", encryptedCommitment); + console.log("Identity:", identity); + break; + } + case "decrypt": { + const [encryptedMessage, identity] = [process.argv[3], process.argv[4]]; + if (!encryptedMessage || !identity) { + console.error("Error: Missing required arguments for decrypt"); + console.error("Usage: yarn ts-node shutter.ts decrypt "); + console.error("Note: The identity is the one returned during encryption"); + process.exit(1); + } + const decryptedMessage = await decrypt(encryptedMessage, identity); + console.log("\nDecrypted Message:", decryptedMessage); + break; + } + default: { + console.error(`Error: Unknown command '${command}'`); + console.error("Valid commands are: encrypt, decrypt"); + process.exit(1); + } + } + } catch (error) { + console.error("\nError:", error); + process.exit(1); + } +} + +// Execute if run directly +if (require.main === module) { + main(); +} diff --git a/contracts/scripts/shutterAutoVote.ts b/contracts/scripts/shutterAutoVote.ts new file mode 100644 index 000000000..084f77dfd --- /dev/null +++ b/contracts/scripts/shutterAutoVote.ts @@ -0,0 +1,238 @@ +import { createPublicClient, createWalletClient, http, Hex, getContract, Address, decodeEventLog } from "viem"; +import { mnemonicToAccount } from "viem/accounts"; +import { hardhat } from "viem/chains"; +import { encrypt, decrypt, DECRYPTION_DELAY } from "./shutter"; +import { abi as DisputeKitShutterPoCAbi } from "../deployments/localhost/DisputeKitShutterPoC.json"; +import crypto from "crypto"; + +// Constants +const SEPARATOR = "␟"; // U+241F + +// Store encrypted votes for later decryption +type EncryptedVote = { + coreDisputeID: bigint; + juror: Address; + identity: Hex; + encryptedVote: string; + timestamp: number; +}; + +const encryptedVotes: EncryptedVote[] = []; + +const disputeKitAddress = "0x5FbDB2315678afecb367f032d93F642f64180aa3" as const; + +const MNEMONIC = "test test test test test test test test test test test junk"; +const account = mnemonicToAccount(MNEMONIC); + +const transport = http(); +const publicClient = createPublicClient({ + chain: hardhat, + transport, +}); + +const walletClient = createWalletClient({ + account, + chain: hardhat, + transport, +}); + +const disputeKit = getContract({ + address: disputeKitAddress, + abi: DisputeKitShutterPoCAbi, + client: { public: publicClient, wallet: walletClient }, +}); + +type CommitCastEventArgs = { + _coreDisputeID: bigint; + _juror: Address; + _voteIDs: bigint[]; + _commit: Hex; + _identity: Hex; + _encryptedVote: string; +}; + +/** + * Generate a random salt + */ +function generateSalt(): Hex { + return ("0x" + crypto.randomBytes(32).toString("hex")) as Hex; +} + +/** + * Encodes vote parameters into a message string with separators + */ +function encode({ choice, salt, justification }: { choice: bigint; salt: Hex; justification: string }): string { + return `${choice}${SEPARATOR}${salt}${SEPARATOR}${justification}`; +} + +/** + * Decodes a message string into its component parts + * @param message The message to decode + * @returns Object containing the decoded components + */ +function decode(message: string) { + const [choice, salt, justification] = message.split(SEPARATOR); + return { + choice: BigInt(choice), + salt, + justification, + }; +} + +/** + * Cast a commit on-chain + */ +async function castCommit({ + coreDisputeID, + voteIDs, + choice, + justification, +}: { + coreDisputeID: bigint; + voteIDs: bigint[]; + choice: bigint; + justification: string; +}) { + try { + // Generate salt first + const salt = generateSalt(); + + // Encode the vote parameters into a message + const message = encode({ + choice, + salt, + justification, + }); + + // Encrypt the message using shutter.ts + const { encryptedCommitment: encryptedVote, identity } = await encrypt(message); + + // Compute hash using all vote IDs + const commitHash = await disputeKit.read.hashVote([choice, salt, justification]); + + // Cast the commit on-chain + const txHash = await disputeKit.write.castCommit([ + coreDisputeID, + voteIDs, + commitHash, + identity as Hex, + encryptedVote, + ]); + + // Wait for transaction to be mined + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + // Watch for CommitCast event + const events = await disputeKit.getEvents.CommitCast(); + console.log("CommitCast event:", (events[0] as any).args); + + // Store encrypted vote for later decryption + encryptedVotes.push({ + coreDisputeID, + juror: account.address, + identity: identity as Hex, + encryptedVote, + timestamp: Math.floor(Date.now() / 1000), + }); + + return { commitHash, identity, salt }; + } catch (error) { + console.error("Error in castCommit:", error); + throw error; + } +} + +/** + * Continuously monitor for votes ready to be decrypted and cast + */ +export async function autoVote() { + while (true) { + try { + const currentTime = Math.floor(Date.now() / 1000); + const sleep = DECRYPTION_DELAY + 10; + + // Find votes ready for decryption + const readyVotes = encryptedVotes.filter((vote) => currentTime - vote.timestamp >= sleep); + + for (const vote of readyVotes) { + try { + // Retrieve the CommitCast event + const filter = await publicClient.createContractEventFilter({ + abi: DisputeKitShutterPoCAbi, + eventName: "CommitCast", + args: [vote.coreDisputeID, vote.juror], + }); + let events = await publicClient.getLogs(filter); + if (events.length !== 1) { + throw new Error("No CommitCast event found"); + } + const { args } = decodeEventLog({ + abi: DisputeKitShutterPoCAbi, + eventName: "CommitCast", + topics: events[0].topics, + data: events[0].data, + }); + const commitCast = args as unknown as CommitCastEventArgs; // Workaround getLogs type inference issue + + // Attempt to decrypt the vote + const decryptedMessage = await decrypt(commitCast._encryptedVote, commitCast._identity); + + // Decode the decrypted message + const { choice, salt, justification } = decode(decryptedMessage); + + // Cast the vote on-chain + const txHash = await disputeKit.write.castVote([ + vote.coreDisputeID, + commitCast._voteIDs, + choice, + salt, + justification, + ]); + + // Wait for transaction to be mined + await publicClient.waitForTransactionReceipt({ hash: txHash }); + + // Watch for VoteCast event + events = await disputeKit.getEvents.VoteCast(); + console.log("VoteCast event:", (events[0] as any).args); + + // Remove the processed vote + const index = encryptedVotes.indexOf(vote); + if (index > -1) encryptedVotes.splice(index, 1); + } catch (error) { + console.error(`Error processing vote for identity ${vote.identity}:`, error); + } + } + + console.log(`Sleeping for ${sleep} seconds`); + await new Promise((resolve) => setTimeout(resolve, sleep * 1000)); + } catch (error) { + console.error("Error in autoVote loop:", error); + // Continue the loop even if there's an error + } + } +} + +// Main function to start the auto voting process +async function main() { + try { + // Cast an encrypted commit + await castCommit({ + coreDisputeID: 0n, + voteIDs: [0n, 1n, 2n], + choice: 2n, + justification: "This is my vote justification", + }); + + // Start the auto voting process + await autoVote(); + } catch (error) { + console.error("Error in main:", error); + process.exit(1); + } +} + +// Execute if run directly +if (require.main === module) { + main(); +} diff --git a/contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol b/contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol new file mode 100644 index 000000000..09aa846ba --- /dev/null +++ b/contracts/src/arbitration/dispute-kits/DisputeKitShutterPoC.sol @@ -0,0 +1,113 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.0; + +import "hardhat/console.sol"; + +contract DisputeKitShutterPoC { + struct Vote { + address account; // The address of the juror. + bytes32 commit; // The hash of the encrypted message + salt + uint256 choice; // The choice of the juror. + bool voted; // True if the vote has been cast. + } + + Vote[] public votes; + uint256 public winningChoice; // The choice with the most votes. Note that in the case of a tie, it is the choice that reached the tied number of votes first. + mapping(uint256 => uint256) public counts; // The sum of votes for each choice in the form `counts[choice]`. + bool public tied; // True if there is a tie, false otherwise. + uint256 public totalCommitted; + uint256 public totalVoted; + + event CommitCast( + uint256 indexed _coreDisputeID, + address indexed _juror, + uint256[] _voteIDs, + bytes32 _commit, + bytes32 _identity, + bytes _encryptedVote + ); + + event VoteCast( + uint256 indexed _coreDisputeID, + address indexed _juror, + uint256[] _voteIDs, + uint256 indexed _choice, + string _justification + ); + + constructor() { + address juror = msg.sender; + votes.push(Vote({account: juror, commit: bytes32(0), choice: 0, voted: false})); + votes.push(Vote({account: juror, commit: bytes32(0), choice: 0, voted: false})); + votes.push(Vote({account: juror, commit: bytes32(0), choice: 0, voted: false})); + } + + /** + * @dev Computes the hash of a vote using ABI encoding + * @param _choice The choice being voted for + * @param _justification The justification for the vote + * @param _salt A random salt for commitment + * @return bytes32 The hash of the encoded vote parameters + */ + function hashVote(uint256 _choice, bytes32 _salt, string memory _justification) public pure returns (bytes32) { + bytes32 justificationHash = keccak256(bytes(_justification)); + return keccak256(abi.encode(_choice, _salt, justificationHash)); + } + + function castCommit( + uint256 _coreDisputeID, + uint256[] calldata _voteIDs, + bytes32 _commit, + bytes32 _identity, + bytes calldata _encryptedVote + ) external { + // Store the commitment hash for each voteID + for (uint256 i = 0; i < _voteIDs.length; i++) { + require(votes[_voteIDs[i]].account == msg.sender, "The caller has to own the vote."); + votes[_voteIDs[i]].commit = _commit; + } + + totalCommitted += _voteIDs.length; + emit CommitCast(_coreDisputeID, msg.sender, _voteIDs, _commit, _identity, _encryptedVote); + } + + function castVote( + uint256 _coreDisputeID, + uint256[] calldata _voteIDs, + uint256 _choice, + bytes32 _salt, + string memory _justification + ) external { + require(_voteIDs.length > 0, "No voteID provided"); + + // TODO: what happens if hiddenVotes are not enabled? + + // Verify the commitment hash for all votes at once + bytes32 computedHash = hashVote(_choice, _salt, _justification); + + for (uint256 i = 0; i < _voteIDs.length; i++) { + require(votes[_voteIDs[i]].commit == computedHash, "The commitment hash does not match."); + require(!votes[_voteIDs[i]].voted, "Vote already cast."); + votes[_voteIDs[i]].choice = _choice; + votes[_voteIDs[i]].voted = true; + } + + totalVoted += _voteIDs.length; + + counts[_choice] += _voteIDs.length; + if (_choice == winningChoice) { + if (tied) tied = false; + } else { + // Voted for another choice. + if (counts[_choice] == counts[winningChoice]) { + // Tie. + if (!tied) tied = true; + } else if (counts[_choice] > counts[winningChoice]) { + // New winner. + winningChoice = _choice; + tied = false; + } + } + emit VoteCast(_coreDisputeID, msg.sender, _voteIDs, _choice, _justification); + } +} diff --git a/yarn.lock b/yarn.lock index 6aa34039e..6ea49d46d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5560,6 +5560,7 @@ __metadata: "@nomiclabs/hardhat-solhint": "npm:^4.0.1" "@openzeppelin/contracts": "npm:^5.2.0" "@openzeppelin/upgrades-core": "npm:^1.42.2" + "@shutter-network/shutter-sdk": "npm:^0.0.1" "@typechain/ethers-v6": "npm:^0.5.1" "@typechain/hardhat": "npm:^9.1.0" "@types/chai": "npm:^4.3.20" @@ -5585,7 +5586,7 @@ __metadata: hardhat-gas-reporter: "npm:^2.2.2" hardhat-tracer: "npm:^3.1.0" hardhat-watcher: "npm:^2.5.0" - node-fetch: "npm:^3.3.2" + isomorphic-fetch: "npm:^3.0.0" pino: "npm:^8.21.0" pino-pretty: "npm:^10.3.1" prettier: "npm:^3.3.3" @@ -6888,6 +6889,15 @@ __metadata: languageName: node linkType: hard +"@noble/curves@npm:1.8.2": + version: 1.8.2 + resolution: "@noble/curves@npm:1.8.2" + dependencies: + "@noble/hashes": "npm:1.7.2" + checksum: 10/540e7b7a8fe92ecd5cef846f84d07180662eb7fd7d8e9172b8960c31827e74f148fe4630da962138a6be093ae9f8992d14ab23d3682a2cc32be839aa57c03a46 + languageName: node + linkType: hard + "@noble/curves@npm:^1.7.0": version: 1.7.0 resolution: "@noble/curves@npm:1.7.0" @@ -6946,6 +6956,13 @@ __metadata: languageName: node linkType: hard +"@noble/hashes@npm:1.7.2": + version: 1.7.2 + resolution: "@noble/hashes@npm:1.7.2" + checksum: 10/b5af9e4b91543dcc46a811b5b2c57bfdeb41728361979a19d6110a743e2cb0459872553f68d3a46326d21959964db2776b8c8b4db85ac1d9f63ebcaddf7d59b6 + languageName: node + linkType: hard + "@noble/hashes@npm:^1.6.1": version: 1.6.1 resolution: "@noble/hashes@npm:1.6.1" @@ -8561,6 +8578,17 @@ __metadata: languageName: node linkType: hard +"@shutter-network/shutter-sdk@npm:^0.0.1": + version: 0.0.1 + resolution: "@shutter-network/shutter-sdk@npm:0.0.1" + dependencies: + browser-or-node: "npm:^3.0.0" + lodash: "npm:^4.17.21" + viem: "npm:^2.23.2" + checksum: 10/5e021c9dc27cb88dd487e1a289501394302d88fe349a873525610405eb2b351563556b1d0ef2ac789cff99b03ba6019c0a9761a093a0aef5a82941986ca58517 + languageName: node + linkType: hard + "@sinclair/typebox@npm:^0.24.1": version: 0.24.51 resolution: "@sinclair/typebox@npm:0.24.51" @@ -12986,6 +13014,13 @@ __metadata: languageName: node linkType: hard +"browser-or-node@npm:^3.0.0": + version: 3.0.0 + resolution: "browser-or-node@npm:3.0.0" + checksum: 10/51d74cc5d0139da3d37e83ff3906fcca20d02c42aa8b81a48d9ea01806f36df1a4b55006670071b1d7423967777275920054ec8b723410534b580b0232c5093d + languageName: node + linkType: hard + "browser-process-hrtime@npm:^1.0.0": version: 1.0.0 resolution: "browser-process-hrtime@npm:1.0.0" @@ -21179,6 +21214,16 @@ __metadata: languageName: node linkType: hard +"isomorphic-fetch@npm:^3.0.0": + version: 3.0.0 + resolution: "isomorphic-fetch@npm:3.0.0" + dependencies: + node-fetch: "npm:^2.6.1" + whatwg-fetch: "npm:^3.4.1" + checksum: 10/568fe0307528c63405c44dd3873b7b6c96c0d19ff795cb15846e728b6823bdbc68cc8c97ac23324509661316f12f551e43dac2929bc7030b8bc4d6aa1158b857 + languageName: node + linkType: hard + "isomorphic-timers-promises@npm:^1.0.1": version: 1.0.1 resolution: "isomorphic-timers-promises@npm:1.0.1" @@ -33420,6 +33465,27 @@ __metadata: languageName: node linkType: hard +"viem@npm:^2.23.2": + version: 2.28.0 + resolution: "viem@npm:2.28.0" + dependencies: + "@noble/curves": "npm:1.8.2" + "@noble/hashes": "npm:1.7.2" + "@scure/bip32": "npm:1.6.2" + "@scure/bip39": "npm:1.5.4" + abitype: "npm:1.0.8" + isows: "npm:1.0.6" + ox: "npm:0.6.9" + ws: "npm:8.18.1" + peerDependencies: + typescript: ">=5.0.4" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10/fbdf2ce3020b0ee37457726375cb2479cf716e5ef021470a79f2713c5def1ae53c881458cc3853d1cab8ba655385280aeebbdc155e81b8cb54c6387758e59435 + languageName: node + linkType: hard + "vite-node@npm:1.6.0": version: 1.6.0 resolution: "vite-node@npm:1.6.0" @@ -34149,6 +34215,13 @@ __metadata: languageName: node linkType: hard +"whatwg-fetch@npm:^3.4.1": + version: 3.6.20 + resolution: "whatwg-fetch@npm:3.6.20" + checksum: 10/2b4ed92acd6a7ad4f626a6cb18b14ec982bbcaf1093e6fe903b131a9c6decd14d7f9c9ca3532663c2759d1bdf01d004c77a0adfb2716a5105465c20755a8c57c + languageName: node + linkType: hard + "whatwg-fetch@npm:^3.6.2": version: 3.6.2 resolution: "whatwg-fetch@npm:3.6.2"