From ca6d68ebc481fd834686e04e565ff5ea7a75a38d Mon Sep 17 00:00:00 2001 From: Dominion116 Date: Mon, 24 Aug 2026 16:53:22 +0100 Subject: [PATCH] feat: add public draw verification and private prize claims MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Adds public round verification and a private prize-claim workflow for Sortis, including encrypted claim handling in the pool contract and refreshed Sepolia deployment metadata. ## Changes - Added `SortisPool.claim` for encrypted partial or full prize claims. - Clamps encrypted claim requests to the caller’s available claimable balance. - Keeps claim amounts confidential during balance updates and token transfers. - Added the `/app/prizes` route for privately revealing and claiming prizes. - Added distinct UI states for winnings, no prizes, rollovers, and unavailable claims. - Reused the existing encrypted session authorization and FHEVM user-decryption flow. - Added the `/verify/[roundId]` route for wallet-free public draw verification. - Reads draw event history to display ticket counts, published totals, random values, prizes, and settlement status. - Added verification helpers with bounded public RPC history. - Published the updated pool ABI for frontend contract calls. - Added the Prizes entry to application navigation. - Updated Sepolia contract addresses and deployment metadata. - Updated documentation and contract configuration to reflect the new functionality. ## Validation - Contract compilation completed successfully. - Web type generation, typecheck, ESLint, and diff checks pass. - Existing uncommitted changes were preserved on the new branch. --- AGENTS.md | 33 ++++++++++++++++---- README.md | 16 +++++----- packages/contracts/contracts/SortisPool.sol | 21 +++++++++++++ packages/contracts/deployments/sepolia.json | 32 +++++++++---------- packages/web/app/(app)/app/prizes/page.tsx | 5 +++ packages/web/app/verify/[roundId]/page.tsx | 11 +++++++ packages/web/components/app/prizes-panel.tsx | 31 ++++++++++++++++++ packages/web/config/app.ts | 4 +++ packages/web/lib/contracts/abis.ts | 23 ++++++++++++++ packages/web/lib/contracts/addresses.ts | 16 +++++----- packages/web/lib/verification.ts | 30 ++++++++++++++++++ 11 files changed, 184 insertions(+), 38 deletions(-) create mode 100644 packages/web/app/(app)/app/prizes/page.tsx create mode 100644 packages/web/app/verify/[roundId]/page.tsx create mode 100644 packages/web/components/app/prizes-panel.tsx create mode 100644 packages/web/lib/verification.ts diff --git a/AGENTS.md b/AGENTS.md index ccb161e..8c90106 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -208,10 +208,10 @@ Live addresses (chainId 11155111, deployer/keeper | | address | |---|---| -| cUSDT | `0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b` | -| faucet | `0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0` | -| demo pool / draw / yield | `0x92aF68…15aF` / `0x89efaA…40A6` / `0x575Cf6…0243` | -| standard pool / draw / yield | `0x1d7E3E…395F` / `0xD79fAd…2240` / `0x7B5227…C421` | +| cUSDT | `0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0` | +| faucet | `0x5550e92d4C252763797948Fb751c7116809F7cdb` | +| demo pool / draw / yield | `0x223897…8F97` / `0x30E3eF…1E62` / `0x7C2dF9…2237` | +| standard pool / draw / yield | `0xCCE648…f990` / `0x31d445…d39e` / `0x41E2a4…1048` | `packages/contracts/deployments/sepolia.json` is the canonical record. Read addresses from there or from the generated @@ -302,7 +302,7 @@ What landed: on. Renders nothing when disconnected: no wallet is not the wrong network. - `components/app/connect-button.tsx`, `(app)/` route group with its own layout, `config/app.ts`. -- `/faucet`: live `drip()` against `0xDACEa8…17B0`, reading `dripAmount` and +- `/faucet`: live `drip()` against the generated Sepolia faucet address, reading `dripAmount` and `readyAt(address)`, with a ticking local countdown, mapped revert messages (`CooldownNotElapsed`, `OnlyMinter`, user rejection, gas), and an Etherscan link on success. Deliberately **not** wrapped in `EncryptedGate`: minting is @@ -477,7 +477,28 @@ points to `/pool`. Keep this explicit directory when adding pool-related links: the `(app)` route group does not contribute a URL segment. -### Next: Phase 11 +### Phase 11 — Verification page & prizes (complete) + +`/verify/[roundId]` reads the public Ernie event trail for both Sepolia pools, +including frozen ticket count, published total, random value, settlement prize, +and explicit rollover state. It requires no wallet and bounds public-RPC history +to the latest 100,000 blocks. + +`/app/prizes` adds the private prize envelope: the connected user decrypts their +encrypted claimable handle with the existing in-memory EIP-712 session, then +encrypts the chosen amount and claims through `SortisPool.claim`. Rollover and +zero-prize outcomes are presented distinctly from a winning reveal. + +`SortisPool.claim` was added as the encrypted claim path. It clamps an encrypted +request to the caller's encrypted claimable balance, updates the claimable slot, +and transfers the confidential token without publishing the amount. + +Web typegen, typecheck, ESLint, and diff checks pass. Contract compilation was +verified successfully. On restricted Windows runners, Hardhat's compiler child +process can fail with `HH505`/`spawn EPERM`; run the compile with the workspace's +approved elevated command policy. The Solidity compiler itself is healthy. + +### Next: Phase 12 Build `/verify/[roundId]` from the public draw event trail, then add `/app/prizes` with the authenticated private claim/decryption flow and distinct rollover presentation. Do not relabel landing-page illustrative draw data as live until the keeper completes at least one full real Sepolia round. diff --git a/README.md b/README.md index 9bcba6e..221449d 100644 --- a/README.md +++ b/README.md @@ -255,24 +255,24 @@ Shared: | Contract | Address | |---|---| -| Confidential token (cUSDT) | [`0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b`](https://sepolia.etherscan.io/address/0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b) | -| `SortisFaucet` | [`0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0`](https://sepolia.etherscan.io/address/0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0) | +| Confidential token (cUSDT) | [`0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0`](https://sepolia.etherscan.io/address/0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0) | +| `SortisFaucet` | [`0x5550e92d4C252763797948Fb751c7116809F7cdb`](https://sepolia.etherscan.io/address/0x5550e92d4C252763797948Fb751c7116809F7cdb) | Demo pool, one round every 5 minutes, so a reviewer arriving at a random moment is never far from a complete draw: | Contract | Address | |---|---| -| `SortisPool` | [`0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF`](https://sepolia.etherscan.io/address/0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF) | -| `SortisDraw` | [`0x89efaA363468478aeB5CAaf80e21680B129F40A6`](https://sepolia.etherscan.io/address/0x89efaA363468478aeB5CAaf80e21680B129F40A6) | -| `MockYieldSource` | [`0x575Cf61C0FB339D469582fE45219E4bF40e60243`](https://sepolia.etherscan.io/address/0x575Cf61C0FB339D469582fE45219E4bF40e60243) | +| `SortisPool` | [`0x2238975477e0F90B2084C5C64c61E41fFe7B8F97`](https://sepolia.etherscan.io/address/0x2238975477e0F90B2084C5C64c61E41fFe7B8F97) | +| `SortisDraw` | [`0x30E3eF09C9Ec68bd228f4593382dFF3fe9521E62`](https://sepolia.etherscan.io/address/0x30E3eF09C9Ec68bd228f4593382dFF3fe9521E62) | +| `MockYieldSource` | [`0x7C2dF99d75a14c224d8AD7d24311334f16B92237`](https://sepolia.etherscan.io/address/0x7C2dF99d75a14c224d8AD7d24311334f16B92237) | Standard pool, one round every 24 hours, the round length a real savings product would use: | Contract | Address | |---|---| -| `SortisPool` | [`0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F`](https://sepolia.etherscan.io/address/0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F) | -| `SortisDraw` | [`0xD79fAd0748D999e9274470f8F3b64571D9e12240`](https://sepolia.etherscan.io/address/0xD79fAd0748D999e9274470f8F3b64571D9e12240) | -| `MockYieldSource` | [`0x7B5227267356f91ff2Cb3306C3ee5b6aBa97C421`](https://sepolia.etherscan.io/address/0x7B5227267356f91ff2Cb3306C3ee5b6aBa97C421) | +| `SortisPool` | [`0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990`](https://sepolia.etherscan.io/address/0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990) | +| `SortisDraw` | [`0x31d445D5A59f74F139926Ccbc56C18ed506ed39e`](https://sepolia.etherscan.io/address/0x31d445D5A59f74F139926Ccbc56C18ed506ed39e) | +| `MockYieldSource` | [`0x41E2a43f1Bff35b230686B2B58121eA1CDfb1048`](https://sepolia.etherscan.io/address/0x41E2a43f1Bff35b230686B2B58121eA1CDfb1048) | `MorphoYieldSource` is intentionally not deployed. It is the documented mainnet path, and a contract that reverts on every call would only be noise on a block explorer. diff --git a/packages/contracts/contracts/SortisPool.sol b/packages/contracts/contracts/SortisPool.sol index 811d4c1..fb09fe5 100644 --- a/packages/contracts/contracts/SortisPool.sol +++ b/packages/contracts/contracts/SortisPool.sol @@ -96,6 +96,9 @@ contract SortisPool is ZamaEthereumConfig, Ownable, ReentrancyGuardTransient { */ error DepositNotApproved(address depositor); + /// @notice Thrown when a claim request decrypts to zero. + error NothingToClaim(); + // --------------------------------------------------------------------- // Storage // --------------------------------------------------------------------- @@ -204,6 +207,24 @@ contract SortisPool is ZamaEthereumConfig, Ownable, ReentrancyGuardTransient { return _claimable[account]; } + /** + * @notice Claim part or all of the caller's encrypted winnings. + * @dev The requested amount stays encrypted. The pool clamps it to the + * caller's claimable balance, updates the encrypted slot, and sends + * the confidential token without exposing the amount onchain. + */ + function claim(externalEuint64 encryptedAmount, bytes calldata inputProof) external nonReentrant { + euint64 requested = FHE.fromExternal(encryptedAmount, inputProof); + euint64 available = _claimable[msg.sender]; + ebool canClaim = FHE.le(requested, available); + euint64 amount = FHE.select(canClaim, requested, available); + FHE.allowThis(amount); + _claimable[msg.sender] = FHE.sub(available, amount); + FHE.allowThis(_claimable[msg.sender]); + FHE.allow(_claimable[msg.sender], msg.sender); + IERC7984(asset).confidentialTransfer(msg.sender, amount); + } + /// @notice Public ticket metadata. Encrypted fields are returned as handles. function ticketAt( uint256 ticketId diff --git a/packages/contracts/deployments/sepolia.json b/packages/contracts/deployments/sepolia.json index 4af6ef0..585ba37 100644 --- a/packages/contracts/deployments/sepolia.json +++ b/packages/contracts/deployments/sepolia.json @@ -3,17 +3,17 @@ "chainId": 11155111, "deployer": "0x70f77A5C36eBD667360F6021bF4A95d274B3530e", "keeper": "0x70f77A5C36eBD667360F6021bF4A95d274B3530e", - "deployedAt": "2026-08-22T08:05:14.167Z", + "deployedAt": "2026-08-24T06:47:50.321Z", "token": { - "address": "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b", + "address": "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0", "args": [ "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" ] }, "faucet": { - "address": "0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0", + "address": "0x5550e92d4C252763797948Fb751c7116809F7cdb", "args": [ - "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b", + "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0", 1000000, 3600, "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" @@ -22,25 +22,25 @@ "demo": { "roundDuration": 300, "yieldSource": { - "address": "0x575Cf61C0FB339D469582fE45219E4bF40e60243", + "address": "0x7C2dF99d75a14c224d8AD7d24311334f16B92237", "args": [ - "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b", + "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0", 2000, "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" ] }, "pool": { - "address": "0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF", + "address": "0x2238975477e0F90B2084C5C64c61E41fFe7B8F97", "args": [ - "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b", + "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0", 300, "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" ] }, "draw": { - "address": "0x89efaA363468478aeB5CAaf80e21680B129F40A6", + "address": "0x30E3eF09C9Ec68bd228f4593382dFF3fe9521E62", "args": [ - "0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF", + "0x2238975477e0F90B2084C5C64c61E41fFe7B8F97", "0x70f77A5C36eBD667360F6021bF4A95d274B3530e", "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" ] @@ -49,25 +49,25 @@ "standard": { "roundDuration": 86400, "yieldSource": { - "address": "0x7B5227267356f91ff2Cb3306C3ee5b6aBa97C421", + "address": "0x41E2a43f1Bff35b230686B2B58121eA1CDfb1048", "args": [ - "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b", + "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0", 2000, "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" ] }, "pool": { - "address": "0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F", + "address": "0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990", "args": [ - "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b", + "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0", 86400, "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" ] }, "draw": { - "address": "0xD79fAd0748D999e9274470f8F3b64571D9e12240", + "address": "0x31d445D5A59f74F139926Ccbc56C18ed506ed39e", "args": [ - "0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F", + "0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990", "0x70f77A5C36eBD667360F6021bF4A95d274B3530e", "0x70f77A5C36eBD667360F6021bF4A95d274B3530e" ] diff --git a/packages/web/app/(app)/app/prizes/page.tsx b/packages/web/app/(app)/app/prizes/page.tsx new file mode 100644 index 0000000..848d1c1 --- /dev/null +++ b/packages/web/app/(app)/app/prizes/page.tsx @@ -0,0 +1,5 @@ +import type { Metadata } from "next"; +import { PrizesPanel } from "@/components/app/prizes-panel"; + +export const metadata: Metadata = { title: "Your prizes", description: "Privately reveal and claim Sortis prizes." }; +export default function PrizesPage() { return ; } diff --git a/packages/web/app/verify/[roundId]/page.tsx b/packages/web/app/verify/[roundId]/page.tsx new file mode 100644 index 0000000..2bea91f --- /dev/null +++ b/packages/web/app/verify/[roundId]/page.tsx @@ -0,0 +1,11 @@ +import type { Metadata } from "next"; +import { formatTokenAmount, explorerAddressUrl } from "@/lib/contracts"; +import { getDrawAddress } from "@/lib/draws"; +import { readVerification } from "@/lib/verification"; + +export const metadata: Metadata = { title: "Verify a draw", description: "Independently verify a settled Sortis draw." }; + +export default async function VerifyPage({ params }: { params: Promise<{ roundId: string }> }) { + const { roundId: raw } = await params; const roundId = BigInt(raw); const trails = await readVerification(roundId); + return

Public draw proof

Round {roundId.toString()}

Everything below comes from public event data. No wallet or private key is required.

{trails.map((trail) =>

{trail.poolId === "demo" ? "Demo pool" : "Standard pool"}

{trail.rolledOver ? "Rolled over" : trail.settled ? "Settled" : "Not settled"}

draw contract
{trail.rolledOver ?

No active ticket held the random range. The prize carries forward.

: null}
Tickets frozen
{trail.frozenTicketCount?.toString() ?? "-"}
Published total
{trail.total?.toString() ?? "-"}
Random value
{trail.random?.toString() ?? "-"}
Prize
{trail.prize === undefined ? trail.carriedPrize === undefined ? "-" : `${formatTokenAmount(trail.carriedPrize)} cUSDT carried` : `${formatTokenAmount(trail.prize)} cUSDT`}

Close, oracle reveal, random draw, and settlement are indexed directly from the draw engine.

)}
; +} diff --git a/packages/web/components/app/prizes-panel.tsx b/packages/web/components/app/prizes-panel.tsx new file mode 100644 index 0000000..f59b102 --- /dev/null +++ b/packages/web/components/app/prizes-panel.tsx @@ -0,0 +1,31 @@ +"use client"; + +import * as React from "react"; +import { Gift, Eye, Sparkles } from "lucide-react"; +import { toHex } from "viem"; +import { useAccount, usePublicClient, useReadContract, useWalletClient, useWriteContract } from "wagmi"; + +import { ConnectButton } from "@/components/app/connect-button"; +import { EncryptedGate } from "@/components/app/encrypted-gate"; +import { useNetworkMismatch } from "@/components/app/network-guard"; +import { useFhevm } from "@/components/providers/fhevm-provider"; +import { Button } from "@/components/ui/button"; +import { explorerTxUrl, formatTokenAmount, sepolia, SEPOLIA_CHAIN_ID, sortisPoolAbi, TOKEN_SYMBOL, type Address } from "@/lib/contracts"; +import { getUserDecryptSession } from "@/lib/fhevm/user-decryption"; + +type PoolId = "demo" | "standard"; +const claimAbi = [{ type: "function", name: "claim", stateMutability: "nonpayable", inputs: [{ name: "encryptedAmount", type: "bytes32" }, { name: "inputProof", type: "bytes" }], outputs: [] }] as const; +const zeroHandle = (value?: string) => !value || /^0x0*$/.test(value); + +export function PrizesPanel() { + const { address, isConnected } = useAccount(); const { data: walletClient } = useWalletClient(); const publicClient = usePublicClient({ chainId: SEPOLIA_CHAIN_ID }); + const { instance } = useFhevm(); const wrongNetwork = useNetworkMismatch(); const { writeContractAsync } = useWriteContract(); + const [poolId, setPoolId] = React.useState("demo"); const [amount, setAmount] = React.useState(null); const [busy, setBusy] = React.useState(false); const [message, setMessage] = React.useState(null); const [hash, setHash] = React.useState<`0x${string}` | null>(null); + const pool = sepolia[poolId].pool as Address; + const { data: handle, refetch } = useReadContract({ address: pool, abi: sortisPoolAbi, functionName: "claimableHandleOf", args: address ? [address] : undefined, chainId: SEPOLIA_CHAIN_ID, query: { enabled: Boolean(address) } }); + + async function reveal() { if (!address || !instance || !walletClient || zeroHandle(handle)) return; setBusy(true); setMessage("Opening your encrypted prize envelope..."); try { const contracts = [sepolia.token as Address, sepolia.demo.pool as Address, sepolia.standard.pool as Address]; const session = await getUserDecryptSession(instance, address, contracts, request => walletClient.signTypedData(request as never)); const result = await instance.userDecrypt([{ handle: handle!, contractAddress: pool }], session.privateKey, session.publicKey, session.signature, contracts, address, session.startTimestamp, session.durationDays); const clear = result[handle!.toLowerCase() as `0x${string}`] ?? result[handle!]; if (typeof clear !== "bigint") throw new Error("The relayer returned an invalid prize."); setAmount(clear); setMessage(clear > 0n ? "You won. Your prize is ready to claim." : "No unclaimed prize in this pool yet. Every participant slot is still privately updated each draw."); } catch (error) { setMessage(error instanceof Error ? error.message.split("\n")[0] : String(error)); } finally { setBusy(false); } } + async function claim() { if (!address || !instance || !publicClient || !amount || amount <= 0n) return; setBusy(true); setHash(null); setMessage("Encrypting your private claim amount..."); try { const encrypted = await instance.createEncryptedInput(pool, address).add64(amount).encrypt(); const tx = await writeContractAsync({ address: pool, abi: claimAbi, functionName: "claim", args: [toHex(encrypted.handles[0]), toHex(encrypted.inputProof)], chainId: SEPOLIA_CHAIN_ID }); const receipt = await publicClient.waitForTransactionReceipt({ hash: tx }); if (receipt.status !== "success") throw new Error("The claim transaction reverted."); setHash(tx); setAmount(null); setMessage("Prize claimed privately. The confidential tokens are now in your wallet."); await refetch(); } catch (error) { setMessage(error instanceof Error ? error.message.split("\n")[0] : String(error)); } finally { setBusy(false); } } + + return

Private prizes

Find out without telling everyone else.

Your result stays encrypted. Reveal it privately, then claim confidential tokens without publishing the prize amount.

{(["demo", "standard"] as const).map(id => )}
{!isConnected ?

Connect your Sepolia wallet to open your prize envelope.

:

Encrypted result

{amount === null ?

••••••••

: amount > 0n ?

{formatTokenAmount(amount)} {TOKEN_SYMBOL}

:

Not this round

}
{amount !== null && amount > 0n ? : null}
{message ?

{message}

: null}{hash ? View transaction : null}
}
; +} diff --git a/packages/web/config/app.ts b/packages/web/config/app.ts index 9c89209..a5b8786 100644 --- a/packages/web/config/app.ts +++ b/packages/web/config/app.ts @@ -23,6 +23,10 @@ export const appConfig: MarketingConfig = { title: "Draws", href: "/app/draws", }, + { + title: "Prizes", + href: "/app/prizes", + }, { title: "Faucet", href: "/faucet", diff --git a/packages/web/lib/contracts/abis.ts b/packages/web/lib/contracts/abis.ts index 083fa3f..2c41973 100644 --- a/packages/web/lib/contracts/abis.ts +++ b/packages/web/lib/contracts/abis.ts @@ -65,6 +65,11 @@ export const sortisPoolAbi = [ "name": "NotTicketOwner", "type": "error" }, + { + "inputs": [], + "name": "NothingToClaim", + "type": "error" + }, { "inputs": [], "name": "OnlyDrawEngine", @@ -323,6 +328,24 @@ export const sortisPoolAbi = [ "stateMutability": "view", "type": "function" }, + { + "inputs": [ + { + "internalType": "externalEuint64", + "name": "encryptedAmount", + "type": "bytes32" + }, + { + "internalType": "bytes", + "name": "inputProof", + "type": "bytes" + } + ], + "name": "claim", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, { "inputs": [ { diff --git a/packages/web/lib/contracts/addresses.ts b/packages/web/lib/contracts/addresses.ts index fa02e14..8d80c64 100644 --- a/packages/web/lib/contracts/addresses.ts +++ b/packages/web/lib/contracts/addresses.ts @@ -8,18 +8,18 @@ export type Address = `0x${string}`; export const sepolia = { chainId: SEPOLIA_CHAIN_ID, explorer: SEPOLIA_EXPLORER, - token: "0x485b62eEB1931091FA8bBfb37d1a7B9A18EA345b" as Address, - faucet: "0xDACEa85f7f8A2F9D80A7278f207C0dFAe16417B0" as Address, + token: "0x0E8c04AFd8d4483b0925aF1b4E5a88dde28F0Ff0" as Address, + faucet: "0x5550e92d4C252763797948Fb751c7116809F7cdb" as Address, demo: { - pool: "0x92aF68E6823D22D5Bd5B8746f1c52b87CE3315aF" as Address, - draw: "0x89efaA363468478aeB5CAaf80e21680B129F40A6" as Address, - yieldSource: "0x575Cf61C0FB339D469582fE45219E4bF40e60243" as Address, + pool: "0x2238975477e0F90B2084C5C64c61E41fFe7B8F97" as Address, + draw: "0x30E3eF09C9Ec68bd228f4593382dFF3fe9521E62" as Address, + yieldSource: "0x7C2dF99d75a14c224d8AD7d24311334f16B92237" as Address, roundDuration: 300, }, standard: { - pool: "0x1d7E3ED492D6204A25b7B7a0bbE6C9943555395F" as Address, - draw: "0xD79fAd0748D999e9274470f8F3b64571D9e12240" as Address, - yieldSource: "0x7B5227267356f91ff2Cb3306C3ee5b6aBa97C421" as Address, + pool: "0xCCE648f68874F3bf15Bc713a02b8264a2fD0f990" as Address, + draw: "0x31d445D5A59f74F139926Ccbc56C18ed506ed39e" as Address, + yieldSource: "0x41E2a43f1Bff35b230686B2B58121eA1CDfb1048" as Address, roundDuration: 86400, }, } as const; diff --git a/packages/web/lib/verification.ts b/packages/web/lib/verification.ts new file mode 100644 index 0000000..ddc0956 --- /dev/null +++ b/packages/web/lib/verification.ts @@ -0,0 +1,30 @@ +import { parseAbiItem, type PublicClient } from "viem"; + +import { getDrawAddress, makePublicClient, poolIds, type PoolId } from "@/lib/draws"; + +export const verificationEvents = { + closed: parseAbiItem("event ErnieRoundClosed(uint64 indexed roundId, uint256 frozenTicketCount, uint64 closedAt)"), + total: parseAbiItem("event ErnieTotalRevealed(uint64 indexed roundId, uint64 total)"), + random: parseAbiItem("event ErnieRandomDrawn(uint64 indexed roundId, uint64 randomValue, uint64 total)"), + settled: parseAbiItem("event ErnieSettled(uint64 indexed roundId, uint64 prizeAmount, uint64 randomValue)"), + rollover: parseAbiItem("event ErnieRolledOver(uint64 indexed roundId, uint64 carriedPrize)"), +}; + +export type VerificationTrail = { poolId: PoolId; frozenTicketCount?: bigint; total?: bigint; random?: bigint; prize?: bigint; carriedPrize?: bigint; closedAt?: bigint; settled: boolean; rolledOver: boolean }; + +export async function readVerification(roundId: bigint, client: PublicClient = makePublicClient()): Promise { + const latest = await client.getBlockNumber(); + const fromBlock = latest > 100_000n ? latest - 100_000n : 0n; + return Promise.all(poolIds.map(async (poolId) => { + const address = getDrawAddress(poolId); + const [closed, total, random, settled, rollover] = await Promise.all([ + client.getLogs({ address, event: verificationEvents.closed, args: { roundId }, fromBlock, toBlock: latest }), + client.getLogs({ address, event: verificationEvents.total, args: { roundId }, fromBlock, toBlock: latest }), + client.getLogs({ address, event: verificationEvents.random, args: { roundId }, fromBlock, toBlock: latest }), + client.getLogs({ address, event: verificationEvents.settled, args: { roundId }, fromBlock, toBlock: latest }), + client.getLogs({ address, event: verificationEvents.rollover, args: { roundId }, fromBlock, toBlock: latest }), + ]); + const c = closed[0]?.args; const t = total[0]?.args; const r = random[0]?.args; const s = settled[0]?.args; const o = rollover[0]?.args; + return { poolId, frozenTicketCount: c?.frozenTicketCount, closedAt: c?.closedAt, total: t?.total, random: s?.randomValue ?? r?.randomValue, prize: s?.prizeAmount, carriedPrize: o?.carriedPrize, settled: settled.length > 0, rolledOver: rollover.length > 0 }; + })); +}