From 0a58c310af3ee204bcd653304cb6f243842a6282 Mon Sep 17 00:00:00 2001 From: SourceSensei Date: Sat, 29 Aug 2026 19:33:59 +0100 Subject: [PATCH 01/11] promote verified mainnet frontend to main --- .env.example | 19 +- .github/workflows/deploy-pages.yml | 82 + README.md | 222 +- context/architecture.md | 16 +- .../decisions/0002-cipherbid-vault-custody.md | 32 + context/deployment.md | 8 +- context/privacy.md | 18 +- context/product.md | 6 +- context/security.md | 7 +- contracts/snfoundry.toml | 5 + contracts/src/commitment.cairo | 20 +- contracts/src/demo_erc721.cairo | 99 + contracts/src/lib.cairo | 650 +++- contracts/tests/test_auction_house.cairo | 540 +++ contracts/tests/test_commitment.cairo | 127 +- contracts/tests/test_contract.cairo | 77 - contracts/tests/test_demo_erc721.cairo | 66 + docs/evidence/README.md | 68 + .../evidence/hackathon-requirements-matrix.md | 140 + docs/evidence/mainnet/demo-script.md | 164 + docs/evidence/mainnet/deployment.json | 62 + docs/evidence/mainnet/deployment.md | 55 + docs/evidence/mainnet/release-candidate.md | 79 + docs/evidence/sepolia-feasibility.md | 49 +- docs/evidence/sepolia/demo-runbook.md | 97 + docs/evidence/sepolia/deployment-manifest.md | 99 + docs/evidence/task-0-demo-matrix.md | 107 + docs/evidence/task-1-1-wallet-api-route.md | 221 ++ docs/evidence/task-1-2-bid-ingress-wire.md | 273 ++ .../task-1-3-lifecycle-wire-matrix.md | 473 +++ .../task-2-1-auction-configuration.md | 295 ++ docs/evidence/task-2-2-bid-credentials.md | 241 ++ .../task-2-3-lifecycle-specification.md | 71 + docs/evidence/task-2-4-security-invariants.md | 73 + docs/evidence/winning-product-scope.md | 94 + .../2026-08-24-premium-protocol-console.md | 54 + ...2026-08-27-wallet-connect-panel-styling.md | 127 + ...026-08-29-github-pages-mainnet-frontend.md | 471 +++ ...26-08-24-cipherbid-vault-custody-design.md | 189 ++ ...6-08-24-premium-protocol-console-design.md | 37 + .../2026-08-27-wallet-connect-panel-design.md | 54 + ...29-github-pages-mainnet-frontend-design.md | 166 + strk20.json | 5 +- web/.prettierignore | 1 + web/next.config.ts | 19 +- web/package.json | 13 +- web/playwright.config.ts | 14 +- web/pnpm-lock.yaml | 2945 +++++++---------- web/scripts/configure-mainnet-env.ts | 66 + web/scripts/create-mainnet-auction.ts | 286 ++ web/scripts/create-sepolia-auction.ts | 427 +++ web/scripts/deploy-mainnet.ts | 344 ++ web/scripts/preflight-mainnet.ts | 117 + web/scripts/verify-pages-workflow.ts | 15 + web/src/app/auction/page.tsx | 10 + web/src/app/auctions/[auctionId]/page.tsx | 24 - web/src/app/create/page.tsx | 16 + web/src/app/demo/setup/page.tsx | 6 + web/src/app/globals.css | 55 +- web/src/app/layout.tsx | 4 +- web/src/app/page.tsx | 107 +- web/src/config/deployment.ts | 93 + web/src/config/mainnetAuctionPlan.ts | 114 + web/src/config/mainnetDeploymentPlan.ts | 127 + web/src/config/mainnetRelease.ts | 71 + web/src/config/pagesWorkflowPolicy.ts | 180 + web/src/config/publicDeployment.ts | 12 + .../features/auction/auctionBrowserLoader.ts | 16 + web/src/features/auction/auctionConfig.ts | 97 + .../features/auction/auctionCreationPlan.ts | 130 + web/src/features/auction/auctionLifecycle.ts | 131 + .../features/auction/auctionLiveViewModel.ts | 51 + web/src/features/auction/auctionMath.ts | 22 + web/src/features/auction/auctionReader.ts | 206 ++ web/src/features/auction/auctionRoute.ts | 38 + web/src/features/auction/commitment.ts | 33 +- .../features/auction/demoBidderReadiness.ts | 121 + web/src/features/auction/lifecycleCalls.ts | 54 + .../auction/ui/AtomicDeliveryReceipt.tsx | 107 + .../features/auction/ui/AuctionActions.tsx | 456 +++ .../features/auction/ui/AuctionBidPreview.tsx | 71 +- .../features/auction/ui/AuctionLivePage.tsx | 296 ++ .../features/auction/ui/AuctionPageClient.tsx | 79 + .../features/auction/ui/ProtocolConsole.tsx | 44 + .../auction/ui/SecondPriceIllustration.tsx | 6 +- .../features/auction/ui/SellerCreateForm.tsx | 203 ++ .../features/auction/ui/SellerCreatePage.tsx | 63 + web/src/features/credentials/credentials.ts | 163 + .../features/credentials/recoveryBundle.ts | 326 ++ web/src/features/demo/demoBidderShield.ts | 167 + .../features/demo/ui/DemoBidderSetupPage.tsx | 193 ++ web/src/features/privacy/canonicalCap.ts | 8 +- web/src/features/privacy/strk20Actions.ts | 29 - .../features/privacy/strk20ClaimActions.ts | 61 + .../transactions/auctionTransactionFlows.ts | 334 ++ .../features/transactions/receiptVerifier.ts | 163 + .../transactions/transactionOrchestrator.ts | 143 + .../features/wallet/WalletConnectPanel.tsx | 213 +- .../wallet/browserWalletDependencies.ts | 7 + web/src/features/wallet/walletConnection.ts | 16 +- web/src/features/wallet/walletStore.ts | 59 +- web/tests/e2e/auction-bid-preview.spec.ts | 60 +- web/tests/e2e/feasibility.spec.ts | 25 +- .../fixtures/auction-configuration-v2.json | 56 + web/tests/fixtures/auction-lifecycle-v1.json | 108 + web/tests/fixtures/bid-credentials-v1.json | 86 + web/tests/fixtures/bid-ingress-v1.json | 117 + web/tests/fixtures/lifecycle-routes-v2.json | 189 ++ web/tests/unit/AtomicDeliveryReceipt.test.tsx | 46 + web/tests/unit/AuctionActions.test.tsx | 108 + web/tests/unit/AuctionBidPreview.test.tsx | 18 +- web/tests/unit/AuctionLivePage.test.tsx | 76 + web/tests/unit/AuctionPageClient.test.tsx | 135 + web/tests/unit/DemoBidderSetupPage.test.tsx | 87 + web/tests/unit/Home.test.tsx | 13 +- web/tests/unit/SellerCreateForm.test.tsx | 50 + web/tests/unit/WalletConnectPanel.test.tsx | 138 +- web/tests/unit/auctionConfig.test.ts | 228 ++ web/tests/unit/auctionCreationPlan.test.ts | 78 + .../unit/auctionLifecycleFixture.test.ts | 108 + web/tests/unit/auctionLiveViewModel.test.ts | 99 + web/tests/unit/auctionMath.test.ts | 28 +- web/tests/unit/auctionReader.test.ts | 91 + web/tests/unit/auctionRoute.test.ts | 28 + .../unit/auctionTransactionFlows.test.ts | 235 ++ web/tests/unit/bidIngressWireFixture.test.ts | 150 + .../unit/browserWalletDependencies.test.ts | 24 + web/tests/unit/commitment.test.ts | 132 +- web/tests/unit/credentials.test.ts | 85 + web/tests/unit/demoBidderReadiness.test.ts | 126 + web/tests/unit/demoBidderShield.test.ts | 88 + web/tests/unit/deployment.test.ts | 72 + web/tests/unit/lifecycleWireFixture.test.ts | 250 ++ web/tests/unit/mainnetAuctionPlan.test.ts | 88 + web/tests/unit/mainnetDeploymentPlan.test.ts | 96 + web/tests/unit/mainnetRelease.test.ts | 47 + web/tests/unit/nextConfig.test.ts | 23 + web/tests/unit/pagesWorkflowPolicy.test.ts | 127 + web/tests/unit/playwrightConfig.test.ts | 16 + web/tests/unit/publicDeployment.test.ts | 42 + web/tests/unit/receiptVerifier.test.ts | 125 + web/tests/unit/recoveryBundle.test.ts | 68 + web/tests/unit/strk20Actions.test.ts | 26 +- .../unit/transactionOrchestrator.test.ts | 116 + web/tests/unit/walletConnection.test.ts | 51 +- web/tests/unit/walletStore.test.ts | 24 +- 146 files changed, 16593 insertions(+), 2320 deletions(-) create mode 100644 .github/workflows/deploy-pages.yml create mode 100644 context/decisions/0002-cipherbid-vault-custody.md create mode 100644 contracts/src/demo_erc721.cairo create mode 100644 contracts/tests/test_auction_house.cairo delete mode 100644 contracts/tests/test_contract.cairo create mode 100644 contracts/tests/test_demo_erc721.cairo create mode 100644 docs/evidence/README.md create mode 100644 docs/evidence/hackathon-requirements-matrix.md create mode 100644 docs/evidence/mainnet/demo-script.md create mode 100644 docs/evidence/mainnet/deployment.json create mode 100644 docs/evidence/mainnet/deployment.md create mode 100644 docs/evidence/mainnet/release-candidate.md create mode 100644 docs/evidence/sepolia/demo-runbook.md create mode 100644 docs/evidence/sepolia/deployment-manifest.md create mode 100644 docs/evidence/task-0-demo-matrix.md create mode 100644 docs/evidence/task-1-1-wallet-api-route.md create mode 100644 docs/evidence/task-1-2-bid-ingress-wire.md create mode 100644 docs/evidence/task-1-3-lifecycle-wire-matrix.md create mode 100644 docs/evidence/task-2-1-auction-configuration.md create mode 100644 docs/evidence/task-2-2-bid-credentials.md create mode 100644 docs/evidence/task-2-3-lifecycle-specification.md create mode 100644 docs/evidence/task-2-4-security-invariants.md create mode 100644 docs/evidence/winning-product-scope.md create mode 100644 docs/superpowers/plans/2026-08-24-premium-protocol-console.md create mode 100644 docs/superpowers/plans/2026-08-27-wallet-connect-panel-styling.md create mode 100644 docs/superpowers/plans/2026-08-29-github-pages-mainnet-frontend.md create mode 100644 docs/superpowers/specs/2026-08-24-cipherbid-vault-custody-design.md create mode 100644 docs/superpowers/specs/2026-08-24-premium-protocol-console-design.md create mode 100644 docs/superpowers/specs/2026-08-27-wallet-connect-panel-design.md create mode 100644 docs/superpowers/specs/2026-08-29-github-pages-mainnet-frontend-design.md create mode 100644 web/scripts/configure-mainnet-env.ts create mode 100644 web/scripts/create-mainnet-auction.ts create mode 100644 web/scripts/create-sepolia-auction.ts create mode 100644 web/scripts/deploy-mainnet.ts create mode 100644 web/scripts/preflight-mainnet.ts create mode 100644 web/scripts/verify-pages-workflow.ts create mode 100644 web/src/app/auction/page.tsx delete mode 100644 web/src/app/auctions/[auctionId]/page.tsx create mode 100644 web/src/app/create/page.tsx create mode 100644 web/src/app/demo/setup/page.tsx create mode 100644 web/src/config/deployment.ts create mode 100644 web/src/config/mainnetAuctionPlan.ts create mode 100644 web/src/config/mainnetDeploymentPlan.ts create mode 100644 web/src/config/mainnetRelease.ts create mode 100644 web/src/config/pagesWorkflowPolicy.ts create mode 100644 web/src/config/publicDeployment.ts create mode 100644 web/src/features/auction/auctionBrowserLoader.ts create mode 100644 web/src/features/auction/auctionConfig.ts create mode 100644 web/src/features/auction/auctionCreationPlan.ts create mode 100644 web/src/features/auction/auctionLifecycle.ts create mode 100644 web/src/features/auction/auctionLiveViewModel.ts create mode 100644 web/src/features/auction/auctionReader.ts create mode 100644 web/src/features/auction/auctionRoute.ts create mode 100644 web/src/features/auction/demoBidderReadiness.ts create mode 100644 web/src/features/auction/lifecycleCalls.ts create mode 100644 web/src/features/auction/ui/AtomicDeliveryReceipt.tsx create mode 100644 web/src/features/auction/ui/AuctionActions.tsx create mode 100644 web/src/features/auction/ui/AuctionLivePage.tsx create mode 100644 web/src/features/auction/ui/AuctionPageClient.tsx create mode 100644 web/src/features/auction/ui/ProtocolConsole.tsx create mode 100644 web/src/features/auction/ui/SellerCreateForm.tsx create mode 100644 web/src/features/auction/ui/SellerCreatePage.tsx create mode 100644 web/src/features/credentials/credentials.ts create mode 100644 web/src/features/credentials/recoveryBundle.ts create mode 100644 web/src/features/demo/demoBidderShield.ts create mode 100644 web/src/features/demo/ui/DemoBidderSetupPage.tsx create mode 100644 web/src/features/privacy/strk20ClaimActions.ts create mode 100644 web/src/features/transactions/auctionTransactionFlows.ts create mode 100644 web/src/features/transactions/receiptVerifier.ts create mode 100644 web/src/features/transactions/transactionOrchestrator.ts create mode 100644 web/tests/fixtures/auction-configuration-v2.json create mode 100644 web/tests/fixtures/auction-lifecycle-v1.json create mode 100644 web/tests/fixtures/bid-credentials-v1.json create mode 100644 web/tests/fixtures/bid-ingress-v1.json create mode 100644 web/tests/fixtures/lifecycle-routes-v2.json create mode 100644 web/tests/unit/AtomicDeliveryReceipt.test.tsx create mode 100644 web/tests/unit/AuctionActions.test.tsx create mode 100644 web/tests/unit/AuctionLivePage.test.tsx create mode 100644 web/tests/unit/AuctionPageClient.test.tsx create mode 100644 web/tests/unit/DemoBidderSetupPage.test.tsx create mode 100644 web/tests/unit/SellerCreateForm.test.tsx create mode 100644 web/tests/unit/auctionConfig.test.ts create mode 100644 web/tests/unit/auctionCreationPlan.test.ts create mode 100644 web/tests/unit/auctionLifecycleFixture.test.ts create mode 100644 web/tests/unit/auctionLiveViewModel.test.ts create mode 100644 web/tests/unit/auctionReader.test.ts create mode 100644 web/tests/unit/auctionRoute.test.ts create mode 100644 web/tests/unit/auctionTransactionFlows.test.ts create mode 100644 web/tests/unit/bidIngressWireFixture.test.ts create mode 100644 web/tests/unit/credentials.test.ts create mode 100644 web/tests/unit/demoBidderReadiness.test.ts create mode 100644 web/tests/unit/demoBidderShield.test.ts create mode 100644 web/tests/unit/deployment.test.ts create mode 100644 web/tests/unit/lifecycleWireFixture.test.ts create mode 100644 web/tests/unit/mainnetAuctionPlan.test.ts create mode 100644 web/tests/unit/mainnetDeploymentPlan.test.ts create mode 100644 web/tests/unit/mainnetRelease.test.ts create mode 100644 web/tests/unit/nextConfig.test.ts create mode 100644 web/tests/unit/pagesWorkflowPolicy.test.ts create mode 100644 web/tests/unit/playwrightConfig.test.ts create mode 100644 web/tests/unit/publicDeployment.test.ts create mode 100644 web/tests/unit/receiptVerifier.test.ts create mode 100644 web/tests/unit/recoveryBundle.test.ts create mode 100644 web/tests/unit/transactionOrchestrator.test.ts diff --git a/.env.example b/.env.example index 03b2682..e92c2c0 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,15 @@ -# Public chain identity -NEXT_PUBLIC_STARKNET_CHAIN_ID=SN_MAIN -NEXT_PUBLIC_STRK20_POOL_ADDRESS=0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a -NEXT_PUBLIC_STARKNET_RPC_URL=https://rpc.starknet.lava.build +# Verified public Sepolia deployment +NEXT_PUBLIC_CIPHERBID_NETWORK=sepolia +NEXT_PUBLIC_STARKNET_RPC_URL=https://api.zan.top/public/starknet-sepolia/rpc/v0_10 +NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS=0x0705b1080174f2b10c02fd8b2e00b918e4dc91f9021ee6a208f53d5909fcc87d +NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH=0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4 +NEXT_PUBLIC_STRK20_POOL_ADDRESS=0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91 +NEXT_PUBLIC_STRK_TOKEN_ADDRESS=0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d -# Optional server-only Alchemy RPC. Never prefix this variable with NEXT_PUBLIC. +# Optional server-only RPC override. Never prefix this variable with NEXT_PUBLIC. STARKNET_RPC_URL= -# Populated only after verified deployments -NEXT_PUBLIC_CIPHERBID_AUCTION_HOUSE= +# Mainnet release builds must be generated from the verified deployment record, +# not by editing addresses manually: +# cd web +# pnpm env:mainnet --write diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml new file mode 100644 index 0000000..4acf005 --- /dev/null +++ b/.github/workflows/deploy-pages.yml @@ -0,0 +1,82 @@ +name: Deploy CipherBid Pages + +on: + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Setup Node + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: 24.13.1 + cache: pnpm + cache-dependency-path: web/pnpm-lock.yaml + + - name: Enable Corepack + run: corepack enable + + - name: Install dependencies + working-directory: web + run: pnpm install --frozen-lockfile + + - name: Verify workflow policy + working-directory: web + run: pnpm pages:verify + + - name: Verify Web + working-directory: web + run: | + pnpm format:check + pnpm lint + pnpm typecheck + pnpm test + + - name: Configure Pages + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 + + - name: Build static site + working-directory: web + env: + CIPHERBID_PAGES_BUILD: "1" + NEXT_PUBLIC_CIPHERBID_NETWORK: mainnet + NEXT_PUBLIC_STARKNET_RPC_URL: https://api.zan.top/public/starknet-mainnet/rpc/v0_10 + NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS: "0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e" + NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH: "0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4" + NEXT_PUBLIC_STRK20_POOL_ADDRESS: "0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a" + NEXT_PUBLIC_STRK_TOKEN_ADDRESS: "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d" + run: pnpm build + + - name: Upload Pages artifact + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 + with: + path: web/out + + deploy: + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + permissions: + pages: write + id-token: write + steps: + - name: Deploy Pages + id: deployment + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 diff --git a/README.md b/README.md index fd75f7a..1484d0b 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,218 @@ # CipherBid -**Private Vickrey NFT auctions on Starknet with STRK20-funded sealed bids and shielded refunds.** +**Private bids, guaranteed onchain delivery.** -CipherBid is an open-source auction house for atomically delivered ERC-721 assets. Every bidder locks the same public collateral cap through STRK20, while the actual bid remains sealed in a domain-separated Poseidon commitment until the reveal window. The highest valid bidder wins and pays the greater of the reserve or second-highest valid bid. +CipherBid is an open-source Vickrey auction house for ERC-721 assets on Starknet. Every bidder escrows the same public STRK collateral cap through STRK20 while committing to a private bid amount. After the bidding window closes, bidders reveal their commitments, the highest valid bidder wins, and the NFT is delivered atomically at the greater of the reserve or second-highest valid bid. + +> **Demo deployment only:** the deployed contracts and public funding transactions below are verified on Starknet mainnet. The two-wallet private bid lifecycle is still pending Ready X deposits, so `strk20.json` intentionally contains no qualifying transaction claims yet. ## Why equal collateral? -A STRK20 `privacy_invoke` withdraws funds from the pool to the helper through a public ERC-20 transfer. Escrowing each variable bid amount would reveal it. CipherBid therefore locks the same cap for every bidder, preventing pre-reveal amount leakage while ensuring every accepted bid is fully funded. +A STRK20 `privacy_invoke` withdraws tokens from the privacy pool to the helper through a public ERC-20 edge. Escrowing each bidder's variable bid would reveal that amount before the reveal phase. CipherBid therefore locks the same cap for every accepted bidder. The public transfer proves every bid is funded without disclosing whether the sealed bid is `2 STRK`, `3 STRK`, or another value at or below the cap. + +This is **STRK20-funded sealed bidding with equalized real collateral**. It is not an unfunded hash-only auction, and it does not claim bids remain private after reveal. + +## Verified mainnet deployment + +| Component | Address / transaction | +| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| AuctionHouse | [`0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e`](https://voyager.online/contract/0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e) | +| DemoERC721 | [`0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f`](https://voyager.online/contract/0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f) | +| Demo NFT | Token ID `99` | +| STRK20 pool | `0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a` | +| STRK token | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` | +| AuctionHouse declaration | [`0x552781e5ecb2ab9826474c8395ca5fd2f534ce6155367ab67ae195f5e2c9dc6`](https://voyager.online/tx/0x552781e5ecb2ab9826474c8395ca5fd2f534ce6155367ab67ae195f5e2c9dc6) | +| DemoERC721 declaration | [`0x01efc7df78014f252d346af3b88a5035b002cf005079604f6e3bf9df0f1fa9b`](https://voyager.online/tx/0x01efc7df78014f252d346af3b88a5035b002cf005079604f6e3bf9df0f1fa9b) | + +Public readback confirms the reviewed class hashes, canonical pool and STRK token, a maximum of 32 bidders, and deployer ownership of NFT `99`. See the [deployment evidence](docs/evidence/mainnet/deployment.md) and [machine-readable manifest](docs/evidence/mainnet/deployment.json). + +## Public frontend + +The production target is GitHub Pages at `https://sourcesenseitherealone.github.io/cipherbid/`. The source-controlled deployment workflow uses immutable action pins, least-privilege token permissions, and only public mainnet configuration. Do not treat that URL as deployed evidence until the workflow has run from `main` and the resulting routes have been independently read back. + +The exportable live-auction route is `/auction?id=`. It validates one auction ID, reads public Starknet state in the browser, verifies the deployed class/configuration and NFT custody, then renders wallet controls. Ready X still owns private-note discovery, proving, signing, and submission. + +## Canonical demo + +The bounded mainnet demo uses one seller, two separate Ready X accounts, and one read-only observer: -This is **STRK20-funded sealed bidding with equalized real collateral**. It is not an unfunded hash-only auction, and it does not claim that a variable bid amount remains encrypted after leaving the pool. +| Term | Value | +| ----------------------- | ---------: | +| Reserve | `1 STRK` | +| Equal collateral cap | `4 STRK` | +| Bidder A sealed bid | `2 STRK` | +| Bidder B sealed bid | `3 STRK` | +| Expected winner | Bidder B | +| Expected clearing price | `2 STRK` | +| Loser refund | `4 STRK` | +| Winner surplus | `2 STRK` | +| Seller proceeds | `2 STRK` | +| Bidding window | 10 minutes | +| Reveal window | 5 minutes | -## Sprint MVP +Both bidders must first shield `24 STRK` and wait at least ten accepted blocks before the timed auction starts. Public readiness verifies registration, deposit amount, and maturity only. Ready X remains authoritative for unspent private-note balance. -- One reusable Cairo auction-house deployment -- One escrowed ERC-721 per auction -- STRK-only Vickrey settlement -- STRK20 wallet-driven anonymous bid ingress -- Shielded refunds, winner surplus, and seller proceeds -- Original responsive Next.js interface -- Honest privacy and compliance evidence +## Architecture + +```text +Seller / public Starknet account + ├─ approves DemoERC721 token 99 + └─ creates auction atomically ─────────────┐ + ▼ +Ready X bidder wallet CipherBid AuctionHouse + ├─ owns viewing key and notes ├─ escrows the NFT + ├─ discovers mature STRK notes ├─ accepts equal 4 STRK collateral + ├─ creates proof ├─ stores Poseidon commitments + └─ submits Wallet API action ───────►├─ verifies reveals + ├─ computes Vickrey clearing price +STRK20 pool ├─ transfers NFT atomically to winner + ├─ screens public deposits └─ authorizes refunds/surplus/proceeds + ├─ verifies private proof + └─ invokes AuctionHouse +``` + +### Commitment binding + +A bid commitment binds the domain tag, Starknet chain ID, AuctionHouse address, auction ID, bid amount, random nonce, claim handle, and NFT recipient. Recovery material is also bound to network, chain ID, deployment, and auction ID before reveal or claim. + +### Contract invariants + +- Only the configured STRK20 pool may call `privacy_invoke`. +- Ingress is accounted from the helper's actual STRK balance delta. +- Every accepted bidder locks the same cap. +- Bidder count and settlement work are bounded. +- NFT custody is established during auction creation and delivery is part of settlement. +- Claims are one-time and commitment-bound. +- All `u256 → u128` conversions are checked. +- External interactions follow checks-effects-interactions and reentrancy protection. ## Privacy boundary -| Public | Private before reveal | -| --- | --- | -| Auction terms, NFT, reserve, cap, deadlines | Bidder's main wallet identity | -| Bid count and timing | Actual bid amount | -| Identical collateral transfer amount | Bid and claim secrets | -| Revealed bids, winner, clearing price | STRK20 note ownership and source linkage | +| Public | Private before reveal | +| -------------------------------------------------------- | --------------------------------------------------- | +| Auction terms, NFT, reserve, cap, and deadlines | Bid amount and random bid nonce | +| STRK20 registration and public deposits | Private-note ownership and note-selection witnesses | +| Identical collateral transfer amount | Wallet viewing key and proof witness | +| Bid count and transaction timing | Claim secret | +| Revealed bids, winner, and clearing price | Bidder's main-wallet linkage inside the pool | +| Withdrawals, open-note edges, and direct lifecycle calls | Recovery plaintext outside its active in-memory use | + +Ready X owns private-note discovery, proof generation, signing, and private transaction submission. CipherBid never requests a viewing key or private-note witness. The browser does hold the active bid credential briefly to construct the interaction and encrypt an exportable recovery bundle; plaintext secrets must never enter browser storage, logs, analytics, URLs, clipboard, Git, or a backend. + +## Threat model and limitations + +CipherBid protects the bid amount until reveal and prevents an unfunded winner, but it does not provide perfect anonymity: + +- deposits, withdrawals, open-note amounts, timing, and public account activity remain visible; +- distinctive amounts or tightly timed setup can shrink the anonymity set; +- opening a channel near a public action may create timing linkage; +- every valid reveal makes the bid amount public by design; +- a malicious web page could substitute dapp-built Wallet API actions before the wallet prompt, so users must verify target and amount in Ready X; +- wallet, prover, relayer, RPC, screening, and browser availability remain operational dependencies; +- private balances cannot be verified by the dapp; +- the present contracts and signer configuration are a bounded hackathon demo, not an audited production deployment. + +STRK20's auditor disclosure mechanism can reveal activity under its protocol policy; a viewing key can read but cannot spend funds. + +## Repository layout + +```text +contracts/ Cairo AuctionHouse and DemoERC721 +web/ Next.js application and Wallet API integration +context/ Product, architecture, security, and stack decisions +docs/evidence/ Secret-free specifications and public readbacks +strk20.json Final verified submission metadata +``` + +## Local development + +### Prerequisites + +- Node.js 24 +- pnpm 10 +- Cairo compiler 2.20.0 +- Scarb 2.20.1 +- Starknet Foundry 0.63.0 +- Ready X for real STRK20 wallet flows + +### Install and configure + +```bash +npx --yes pnpm@10.18.1 --dir web install --frozen-lockfile +cd web +npx --yes pnpm@10.18.1 exec tsx scripts/configure-mainnet-env.ts \ + --deployment-record ../docs/evidence/mainnet/deployment.json \ + --write +``` + +The configuration command writes only public deployment values and refuses to overwrite an existing `.env.local`. + +### Run + +```bash +cd web +npx --yes pnpm@10.18.1 exec next dev --webpack -p 4110 +``` + +Open: + +- `http://127.0.0.1:4110/` — auction browser +- `http://127.0.0.1:4110/auction?id=1` — public auction reader; ID `1` remains unavailable until a real auction exists +- `http://127.0.0.1:4110/create` — seller creation flow +- `http://127.0.0.1:4110/demo/setup` — Ready X bidder shielding + +### Contract checks + +```bash +cd contracts +scarb fmt --check +scarb build +snforge test +``` + +### Web checks + +```bash +npx --yes pnpm@10.18.1 --dir web format:check +npx --yes pnpm@10.18.1 --dir web lint +npx --yes pnpm@10.18.1 --dir web typecheck +npx --yes pnpm@10.18.1 --dir web test +npx --yes pnpm@10.18.1 --dir web test:e2e +npx --yes pnpm@10.18.1 --dir web pages:verify +npx --yes pnpm@10.18.1 --dir web build +CIPHERBID_PAGES_BUILD=1 npx --yes pnpm@10.18.1 --dir web build +``` + +## Operational scripts + +Mainnet write scripts default to plan-only and require explicit `--execute`: + +```bash +cd web +npx --yes pnpm@10.18.1 run deploy:mainnet +npx --yes pnpm@10.18.1 run auction:preflight:mainnet +npx --yes pnpm@10.18.1 exec tsx scripts/create-mainnet-auction.ts --auction-id +``` + +Do not add `--execute` until the printed plan, signer, network, public bidder readiness, recovery destination, and remaining release budget have been verified. Never commit `.env.local`, wallet state, recovery bundles, runtime evidence, browser state, or signing material. -Deposits, withdrawals, timing, open-note amounts, and app-side anonymizer calls can remain public. The app never receives or exports a user's STRK20 viewing key. +## Evidence -## Status +- [Evidence index](docs/evidence/README.md) +- [Mainnet deployment](docs/evidence/mainnet/deployment.md) +- [Mainnet release candidate](docs/evidence/mainnet/release-candidate.md) +- [Canonical demo matrix](docs/evidence/task-0-demo-matrix.md) +- [Lifecycle specification](docs/evidence/task-2-3-lifecycle-specification.md) +- [Security invariants](docs/evidence/task-2-4-security-invariants.md) +- [Hackathon requirements matrix](docs/evidence/hackathon-requirements-matrix.md) +- [Sepolia rehearsal](docs/evidence/sepolia/demo-runbook.md) -Foundation work is in progress on `development`. Contract, client, deployment, and verified mainnet evidence will be added incrementally. `strk20.json` remains empty until real successful mainnet transactions and deployed addresses are read back and verified. +`strk20.json` contains only the two verified contract addresses. Its transaction and URL fields remain intentionally empty until at least three successful, independently verified mainnet transactions touch STRK20, the real lifecycle route is publicly verified, and the video is published. Deployment and funding transactions alone do not qualify. -## Development +## Scope -The pinned stack and commands will live in [`context/stack.md`](context/stack.md). Mainnet writes are separately budgeted and human-approved. +The sprint MVP supports one STRK payment token, ERC-721 assets, one-unit Vickrey auctions, and at most 32 bidders. It intentionally excludes a broad marketplace, first-price or multi-unit auctions, ERC-1155, off-chain delivery, user accounts, a database, a custom prover, and custom privacy cryptography. ## License -MIT +[MIT](LICENSE) diff --git a/context/architecture.md b/context/architecture.md index fb80338..8248af3 100644 --- a/context/architecture.md +++ b/context/architecture.md @@ -1,14 +1,16 @@ # Architecture ```text -Browser - -> privacy-capable Starknet wallet (keys, note discovery, proving) - -> STRK20 pool (private note spend / relayed submission) - -> CipherBidAuctionHouse.privacy_invoke (uniform collateral, reveal, claims) - -> ERC-721 contract (custody and winner delivery) - Browser -> Starknet RPC (read-only auction/event/receipt state) + +CipherBid Vault CLI (user-operated; no browser bridge) + -> Starknet RPC (independent auction verification) + -> Privacy SDK / dedicated account (keys, note discovery, proving) + -> STRK20 pool -> CipherBidAuctionHouse.privacy_invoke (private uniform-cap bid ingress only) + -> CipherBidAuctionHouse.reveal / claim (direct public lifecycle calls) + -> ERC-721 contract (custody and winner delivery) + -> local OS-protected credential store ``` -The Cairo auction house is authoritative for auction lifecycle and accounting. The wallet is authoritative for the user's private STRK20 state. The browser owns only typed transaction preparation and ephemeral encrypted recovery handling. There is no custodial backend or database. +The Cairo auction house is authoritative for auction lifecycle and accounting. CipherBid Vault is the user-operated custodian of its dedicated execution account, STRK20 viewing key, private notes, bid nonce, and encrypted offline claim bundle. The browser owns only public read-only state and may display a public vault receipt; it never prepares or submits auction actions, or handles recovery plaintext or any credential secret. The user's normal privacy wallet remains separate and may fund the vault profile outside the CipherBid website. There is no CipherBid custodial backend or database. diff --git a/context/decisions/0002-cipherbid-vault-custody.md b/context/decisions/0002-cipherbid-vault-custody.md new file mode 100644 index 0000000..ce57868 --- /dev/null +++ b/context/decisions/0002-cipherbid-vault-custody.md @@ -0,0 +1,32 @@ +# Decision 0002: Keep auction credentials in a user-operated local vault + +**Status:** Accepted + +## Decision + +CipherBid adopts `cipherbid-vault`, a user-operated local CLI, as the sole owner of the vault-profile execution-account key, vault viewing key, vault-private notes, auction bid nonce, encrypted credential record, and offline claim private key. + +The web app remains a public-data-only reader. It may display public descriptors and read-back receipts, but it never submits auction actions. It must not receive the sealed bid amount, bid nonce, claim private key, vault master key, execution-account key, viewing key, private notes, or recovery plaintext. + +The vault uses the maintained Privacy SDK for its own dedicated account to submit STRK20 equal-cap ingress, and that account also sends direct public reveal/claim calls. It never imports the user's normal wallet key or viewing key. Users fund the vault profile independently of the CipherBid website. + +## Rationale + +The installed Wallet API 0.10.3 supports STRK20 action preparation and invocation, but no reviewed capability owns a dapp-specific bid/recovery credential end to end. Browser-side generation or encrypted storage would violate CipherBid's hard browser boundary, and browser-built actions can be modified before a wallet prompt. The Privacy SDK route would place a viewing key and a signing key into a browser runtime, which is unsuitable; it is acceptable only in the separately distributed, user-operated vault. + +A user-operated companion makes the new trust assumption explicit, local, and auditable without introducing a CipherBid cloud custodian or backend. + +## Consequences + +- Bid creation remains disabled until the vault and an authenticated onchain claim path exist. +- The auction protocol will replace `claimSecret` with an offline claim key and onchain public-key claim authorization. +- The vault is Windows-first in the MVP and must use maintained OS-protection and encrypted-backup libraries rather than custom cryptography. +- No localhost daemon, native browser bridge, cloud backup, telemetry, or automatic clipboard export is allowed. +- Public reveal/claim submissions from the dedicated execution account may be linkable to each other. Product copy must disclose this limitation. +- The vault profile is local user custody with account-key compromise, offline-claim-bundle recovery, funding, and rotation responsibilities; it is not a lightweight browser helper. + +## Reference + +The detailed protocol, storage boundary, user flows, contract revision, threats, and acceptance criteria are in: + +`docs/superpowers/specs/2026-08-24-cipherbid-vault-custody-design.md` diff --git a/context/deployment.md b/context/deployment.md index ef1132f..61677e9 100644 --- a/context/deployment.md +++ b/context/deployment.md @@ -1,7 +1,13 @@ # Deployment -Development begins with local contract tests and read-only/live-pool shape checks, then Sepolia lifecycle evidence. Mainnet declaration, deployment, funding, and transactions require a fresh human-approved manifest containing exact class hashes, constructor arguments, pool/token addresses, fee estimate, and maximum STRK budget. +Development began with local contract tests and read-only/live-pool shape checks, then a Sepolia rehearsal. The reviewed AuctionHouse and DemoERC721 are now declared and deployed on mainnet under the human-approved `150 STRK` release ceiling. The private two-bidder lifecycle remains gated on Ready X deposits and must not be claimed complete until its pool-touching receipts and state transitions are read back. Mainnet pool: `0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a` +AuctionHouse: `0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e` + +DemoERC721: `0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f` + +Canonical public deployment evidence: `docs/evidence/mainnet/deployment.json` and `docs/evidence/mainnet/deployment.md`. + Never record a transaction or contract in `strk20.json` until it is read back, successful, on the expected network, and tied to the expected state transition. diff --git a/context/privacy.md b/context/privacy.md index ba07d12..7260a1a 100644 --- a/context/privacy.md +++ b/context/privacy.md @@ -1,18 +1,26 @@ # Privacy -## Hidden before reveal +## Sealed bid data until reveal -- Bidder main-wallet identity through STRK20 relaying - Actual committed bid amount -- Bid and claim secrets +- Bid nonce + +## Private pool data beyond reveal + - Note ownership/source linkage inside the pool +## Never exposed to the application + +- Wallet private key, seed phrase, viewing key, session material, or private notes +- Recovery-bundle password or its decrypted payload outside the active import/export operation + ## Public - Seller, NFT, reserve, cap, deadlines - Bid count and timing - Identical cap transferred from pool to auction helper -- Revealed amounts, winner recipient, clearing price +- Bid commitment, claim handles, revealed amounts, winner recipient, clearing price - Deposits, withdrawals, open-note amounts, and app-side anonymizer activity +- Connected-wallet address for seller creation and direct reveal activity, and its timing -The application never handles a viewing key. STRK20's governance-appointed auditor escrow is protocol-level lawful disclosure, not an auction-scoped application feature. +For the sprint demo, the browser receives bidder and seller app-specific credentials only in memory in order to construct commitments/actions and encrypted recovery bundles. It must never persist plaintext to browser storage, cookies, URLs, logs, analytics, crash reports, clipboard, Git, or a server. The wallet owns its viewing key, notes, proof generation, signing, and submission. STRK20's governance-appointed auditor escrow is protocol-level lawful disclosure, not an auction-scoped CipherBid feature. Seller creation and direct reveal can link to the connected account; STRK20 claims still expose timing and output amounts. CipherBid does not claim complete identity privacy. diff --git a/context/product.md b/context/product.md index 0fcac08..3d115e9 100644 --- a/context/product.md +++ b/context/product.md @@ -4,7 +4,11 @@ CipherBid is an open-source Starknet auction house for atomically delivered ERC- ## Sprint MVP -One ERC-721, one payment token (STRK), one auction type (Vickrey), one reusable auction-house deployment, shielded refund/proceeds claims, and a public three-minute demo. +One ERC-721, one payment token (STRK), one auction type (Vickrey), one reusable auction-house deployment, a Starter-Kit-derived Wallet API connection and bid UI, shielded refund/proceeds claims, and a public 90–120 second demo. + +## Canonical demo case + +The submission proof uses one issuer, two separate supported privacy-wallet sessions, and one read-only observer. The sole demo scenario is `0 < R ≤ A < B ≤ C`; Bidder B wins and pays `max(R, A)`. The secret-free ledger and observer assertions live in `docs/evidence/task-0-demo-matrix.md`. ## Non-goals diff --git a/context/security.md b/context/security.md index 1418e26..36f813d 100644 --- a/context/security.md +++ b/context/security.md @@ -1,12 +1,13 @@ # Security -- Bind commitments to chain ID, deployment, auction ID, amount, bid secret, claim handle, and NFT recipient. -- Separate reveal and claim secrets. +- Bind commitments to chain ID, deployment, auction ID, amount, memory-only bid nonce, claim handle, and NFT recipient. +- Require the commitment-bound claim secret for one-time claims; reject wrong, missing, replayed, or cross-auction claim credentials. - Require the configured STRK20 pool as `privacy_invoke` caller. - Account for incoming collateral by verified token balance delta. - Use checks-effects-interactions and reentrancy guards around token/NFT callbacks. - Bound bidder count and settlement work. - Prove value conservation for every lifecycle branch. - Parse decimal strings into integer base units; never serialize floating point. -- Keep all keys and secrets outside Git, logs, analytics, URLs, and server storage. +- Wallet private keys, viewing keys, private notes, and session material stay inside the connected wallet. Bid amount, nonce, and claim secret may exist only in browser memory for the active interaction and inside a mandatory password-encrypted downloaded recovery bundle; never write plaintext to browser storage, Git, logs, analytics, URLs, clipboard, or a server. +- Require explicit target/cap/commitment confirmation, a `strk20PrepareInvoke` preflight, bounded receipt polling, and chain readback before UI success. Treat dapp-built action substitution before a wallet prompt as a disclosed residual risk. - Use synthetic low-value assets and explicit budget approval for mainnet. diff --git a/contracts/snfoundry.toml b/contracts/snfoundry.toml index 686c2ab..a980e37 100644 --- a/contracts/snfoundry.toml +++ b/contracts/snfoundry.toml @@ -9,3 +9,8 @@ # wait-params = { timeout = 300, retry-interval = 10 } # Wait for submitted transaction parameters # block-explorer = "Voyager" # Block explorer service used to display links to transaction details # show-explorer-links = true # Print links pointing to pages with transaction details in the chosen block explorer + +[sncast.sepolia] +url = "https://api.zan.top/public/starknet-sepolia/rpc/v0_10" +account = "cipherbid-sepolia-deployer" +accounts-file = "/home/sourcesensei/.starknet_accounts/starknet_open_zeppelin_accounts.json" diff --git a/contracts/src/commitment.cairo b/contracts/src/commitment.cairo index f1f0f95..bf8bebd 100644 --- a/contracts/src/commitment.cairo +++ b/contracts/src/commitment.cairo @@ -1,4 +1,5 @@ use core::poseidon::poseidon_hash_span; +use starknet::ContractAddress; const CLAIM_DOMAIN: felt252 = 'CIPHERBID_CLAIM_V1'; const BID_DOMAIN: felt252 = 'CIPHERBID_BID_V1'; @@ -10,24 +11,27 @@ pub fn compute_claim_handle(claim_secret: felt252) -> felt252 { pub fn compute_bid_commitment( chain_id: felt252, - auction_house: felt252, + auction_house: ContractAddress, auction_id: u64, amount: u128, - bid_secret: felt252, + bid_nonce: felt252, claim_handle: felt252, - asset_recipient: felt252, + asset_recipient: ContractAddress, ) -> felt252 { + let auction_house_felt: felt252 = auction_house.into(); + let asset_recipient_felt: felt252 = asset_recipient.into(); assert(chain_id != 0, 'ZERO_CHAIN_ID'); - assert(auction_house != 0, 'ZERO_AUCTION_HOUSE'); + assert(auction_house_felt != 0, 'ZERO_AUCTION_HOUSE'); + assert(auction_id != 0, 'ZERO_AUCTION_ID'); assert(amount != 0, 'ZERO_BID_AMOUNT'); - assert(bid_secret != 0, 'ZERO_BID_SECRET'); + assert(bid_nonce != 0, 'ZERO_BID_NONCE'); assert(claim_handle != 0, 'ZERO_CLAIM_HANDLE'); - assert(asset_recipient != 0, 'ZERO_RECIPIENT'); + assert(asset_recipient_felt != 0, 'ZERO_RECIPIENT'); poseidon_hash_span( array![ - BID_DOMAIN, chain_id, auction_house, auction_id.into(), amount.into(), bid_secret, - claim_handle, asset_recipient, + BID_DOMAIN, chain_id, auction_house_felt, auction_id.into(), amount.into(), bid_nonce, + claim_handle, asset_recipient_felt, ] .span(), ) diff --git a/contracts/src/demo_erc721.cairo b/contracts/src/demo_erc721.cairo new file mode 100644 index 0000000..46b9968 --- /dev/null +++ b/contracts/src/demo_erc721.cairo @@ -0,0 +1,99 @@ +use starknet::ContractAddress; + +#[starknet::interface] +pub trait IDemoERC721 { + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; + fn approve(ref self: TContractState, spender: ContractAddress, token_id: u256); + fn get_approved(self: @TContractState, token_id: u256) -> ContractAddress; + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ); +} + +#[starknet::contract] +pub mod DemoERC721 { + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; + use super::IDemoERC721; + + #[storage] + struct Storage { + owners: Map, + balances: Map, + approvals: Map, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + Transfer: Transfer, + Approval: Approval, + } + + #[derive(Drop, starknet::Event)] + struct Transfer { + #[key] + from: ContractAddress, + #[key] + to: ContractAddress, + token_id: u256, + } + + #[derive(Drop, starknet::Event)] + struct Approval { + #[key] + owner: ContractAddress, + #[key] + approved: ContractAddress, + token_id: u256, + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress, token_id: u256) { + let zero: ContractAddress = 0.try_into().unwrap(); + assert(owner != zero, 'ZERO_OWNER'); + self.owners.write(token_id, owner); + self.balances.write(owner, 1); + self.emit(Transfer { from: zero, to: owner, token_id }); + } + + #[abi(embed_v0)] + impl DemoERC721Impl of IDemoERC721 { + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.balances.read(account) + } + + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + self.owners.read(token_id) + } + + fn approve(ref self: ContractState, spender: ContractAddress, token_id: u256) { + let owner = self.owners.read(token_id); + assert(owner == get_caller_address(), 'NOT_OWNER'); + self.approvals.write(token_id, spender); + self.emit(Approval { owner, approved: spender, token_id }); + } + + fn get_approved(self: @ContractState, token_id: u256) -> ContractAddress { + self.approvals.read(token_id) + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ) { + let zero: ContractAddress = 0.try_into().unwrap(); + assert(to != zero, 'ZERO_RECIPIENT'); + let owner = self.owners.read(token_id); + let caller = get_caller_address(); + assert(owner == from, 'BAD_FROM'); + assert(caller == owner || caller == self.approvals.read(token_id), 'NOT_AUTHORIZED'); + + self.owners.write(token_id, to); + self.approvals.write(token_id, zero); + self.balances.write(from, self.balances.read(from) - 1); + self.balances.write(to, self.balances.read(to) + 1); + self.emit(Transfer { from, to, token_id }); + } + } +} diff --git a/contracts/src/lib.cairo b/contracts/src/lib.cairo index d0c4e22..03c74f9 100644 --- a/contracts/src/lib.cairo +++ b/contracts/src/lib.cairo @@ -1,6 +1,21 @@ pub mod commitment; +pub mod demo_erc721; use starknet::ContractAddress; +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct AuctionConfig { + pub auction_id: u64, + pub seller: ContractAddress, + pub seller_claim_handle: felt252, + pub nft_contract: ContractAddress, + pub token_id: u256, + pub reserve_price: u128, + pub cap: u128, + pub bidding_deadline: u64, + pub reveal_deadline: u64, + pub bidder_limit: u32, +} + #[derive(Copy, Drop, Serde)] pub struct OpenNoteDeposit { pub note_id: felt252, @@ -8,85 +23,630 @@ pub struct OpenNoteDeposit { pub amount: u128, } +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct BidRecord { + pub commitment: felt252, + pub claim_handle: felt252, + pub revealed: bool, + pub amount: u128, + pub asset_recipient: ContractAddress, +} + +#[derive(Copy, Drop, Serde, starknet::Store)] +pub struct AuctionState { + pub settled: bool, + pub sold: bool, + pub winner_index: u32, + pub winner_commitment: felt252, + pub winner_recipient: ContractAddress, + pub clearing_price: u128, + pub seller_entitlement: u128, + pub seller_authorized_note: felt252, + pub seller_claim_consumed: bool, +} + +#[starknet::interface] +pub trait IERC721 { + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ); + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; +} + +#[starknet::interface] +pub trait IERC20 { + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; +} + #[starknet::interface] -pub trait IAuctionIngressSpike { +pub trait IAuctionHouse { + fn get_house_config(self: @TContractState) -> (ContractAddress, ContractAddress, u32); + fn create_auction( + ref self: TContractState, + auction_id: u64, + seller_claim_handle: felt252, + nft_contract: ContractAddress, + token_id: u256, + reserve_price: u128, + cap: u128, + bidding_deadline: u64, + reveal_deadline: u64, + bidder_limit: u32, + ); + fn get_auction_config(self: @TContractState, auction_id: u64) -> AuctionConfig; fn privacy_invoke( ref self: TContractState, operation: u8, auction_id: u64, - a: felt252, - b: felt252, - c: felt252, - d: felt252, + primary_value: felt252, + claim_handle: felt252, + reserved_0: felt252, + reserved_1: felt252, pool_address: ContractAddress, - note_id: felt252, + open_note_id: felt252, ) -> Span; - - fn get_spike_state(self: @TContractState) -> (u8, u64, felt252, felt252); - fn get_cap(self: @TContractState) -> u128; + fn get_bid_count(self: @TContractState, auction_id: u64) -> u32; + fn get_bid(self: @TContractState, auction_id: u64, accepted_index: u32) -> BidRecord; + fn reveal_bid( + ref self: TContractState, + auction_id: u64, + accepted_index: u32, + amount: u128, + bid_nonce: felt252, + asset_recipient: ContractAddress, + ); + fn settle_auction(ref self: TContractState, auction_id: u64); + fn get_auction_state(self: @TContractState, auction_id: u64) -> AuctionState; + fn authorize_seller_proceeds( + ref self: TContractState, + auction_id: u64, + seller_claim_handle: felt252, + open_note_id: felt252, + ); + fn get_accounted_payment_balance(self: @TContractState) -> u128; } #[starknet::contract] -mod AuctionIngressSpike { - use starknet::storage::{StoragePointerReadAccess, StoragePointerWriteAccess}; - use starknet::{ContractAddress, get_caller_address}; - use super::{IAuctionIngressSpike, OpenNoteDeposit}; +mod AuctionHouse { + use core::poseidon::poseidon_hash_span; + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ + ContractAddress, get_block_timestamp, get_caller_address, get_contract_address, get_tx_info, + }; + use crate::commitment::{compute_bid_commitment, compute_claim_handle}; + use super::{ + AuctionConfig, AuctionState, BidRecord, IAuctionHouse, IERC20Dispatcher, + IERC20DispatcherTrait, IERC721Dispatcher, IERC721DispatcherTrait, OpenNoteDeposit, + }; + + const MAX_SUPPORTED_BIDDERS: u32 = 32; + const BID_SLOT_DOMAIN: felt252 = 'CIPHERBID_SLOT_V1'; + const COMMITMENT_KEY_DOMAIN: felt252 = 'CIPHERBID_COMMIT_KEY_V1'; + const CLAIM_KEY_DOMAIN: felt252 = 'CIPHERBID_CLAIM_KEY_V1'; + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + AuctionCreated: AuctionCreated, + BidCommitted: BidCommitted, + BidRevealed: BidRevealed, + AuctionSettled: AuctionSettled, + SellerProceedsAuthorized: SellerProceedsAuthorized, + LoserRefundClaimed: LoserRefundClaimed, + WinnerSurplusClaimed: WinnerSurplusClaimed, + SellerProceedsClaimed: SellerProceedsClaimed, + } + + #[derive(Drop, starknet::Event)] + struct AuctionCreated { + #[key] + auction_id: u64, + #[key] + seller: ContractAddress, + seller_claim_handle: felt252, + nft_contract: ContractAddress, + token_id: u256, + reserve_price: u128, + cap: u128, + bidding_deadline: u64, + reveal_deadline: u64, + bidder_limit: u32, + } + + #[derive(Drop, starknet::Event)] + struct BidCommitted { + #[key] + auction_id: u64, + #[key] + accepted_index: u32, + commitment: felt252, + claim_handle: felt252, + cap: u128, + } + + #[derive(Drop, starknet::Event)] + struct BidRevealed { + #[key] + auction_id: u64, + #[key] + accepted_index: u32, + amount: u128, + asset_recipient: ContractAddress, + } + + #[derive(Drop, starknet::Event)] + struct AuctionSettled { + #[key] + auction_id: u64, + sold: bool, + winner_index: u32, + winner_commitment: felt252, + winner_recipient: ContractAddress, + clearing_price: u128, + seller_entitlement: u128, + } + + #[derive(Drop, starknet::Event)] + struct SellerProceedsAuthorized { + #[key] + auction_id: u64, + seller_claim_handle: felt252, + open_note_id: felt252, + } + + #[derive(Drop, starknet::Event)] + struct LoserRefundClaimed { + #[key] + auction_id: u64, + claim_handle: felt252, + open_note_id: felt252, + amount: u128, + } + + #[derive(Drop, starknet::Event)] + struct WinnerSurplusClaimed { + #[key] + auction_id: u64, + claim_handle: felt252, + open_note_id: felt252, + amount: u128, + } + + #[derive(Drop, starknet::Event)] + struct SellerProceedsClaimed { + #[key] + auction_id: u64, + seller_claim_handle: felt252, + open_note_id: felt252, + amount: u128, + } #[storage] struct Storage { pool: ContractAddress, - cap: u128, - last_operation: u8, - last_auction_id: u64, - last_a: felt252, - last_b: felt252, + payment_token: ContractAddress, + max_bidders: u32, + auction_exists: Map, + auctions: Map, + bid_counts: Map, + bids: Map, + commitment_seen: Map, + claim_handle_seen: Map, + claim_handle_index_plus_one: Map, + bidder_claim_consumed: Map, + accounted_payment_balance: u128, + auction_states: Map, } #[constructor] - fn constructor(ref self: ContractState, pool: ContractAddress, cap: u128) { - assert(cap != 0, 'ZERO_CAP'); + fn constructor( + ref self: ContractState, + pool: ContractAddress, + payment_token: ContractAddress, + max_bidders: u32, + ) { + let pool_felt: felt252 = pool.into(); + let payment_token_felt: felt252 = payment_token.into(); + assert(pool_felt != 0, 'ZERO_POOL'); + assert(payment_token_felt != 0, 'ZERO_TOKEN'); + assert(pool != payment_token, 'POOL_IS_TOKEN'); + assert(max_bidders != 0, 'ZERO_MAX_BIDDERS'); + assert(max_bidders <= MAX_SUPPORTED_BIDDERS, 'MAX_BIDDERS_TOO_HIGH'); + self.pool.write(pool); - self.cap.write(cap); + self.payment_token.write(payment_token); + self.max_bidders.write(max_bidders); } #[abi(embed_v0)] - impl AuctionIngressSpikeImpl of IAuctionIngressSpike { + impl AuctionHouseImpl of IAuctionHouse { + fn get_house_config(self: @ContractState) -> (ContractAddress, ContractAddress, u32) { + (self.pool.read(), self.payment_token.read(), self.max_bidders.read()) + } + + fn create_auction( + ref self: ContractState, + auction_id: u64, + seller_claim_handle: felt252, + nft_contract: ContractAddress, + token_id: u256, + reserve_price: u128, + cap: u128, + bidding_deadline: u64, + reveal_deadline: u64, + bidder_limit: u32, + ) { + let seller = get_caller_address(); + let seller_felt: felt252 = seller.into(); + let nft_felt: felt252 = nft_contract.into(); + assert(auction_id != 0, 'ZERO_AUCTION_ID'); + assert(seller_felt != 0, 'ZERO_SELLER'); + assert(seller_claim_handle != 0, 'ZERO_SELLER_CLAIM'); + assert(nft_felt != 0, 'ZERO_NFT'); + assert(reserve_price != 0, 'ZERO_RESERVE'); + assert(cap != 0, 'ZERO_CAP'); + assert(reserve_price <= cap, 'RESERVE_ABOVE_CAP'); + assert(get_block_timestamp() < bidding_deadline, 'BIDDING_NOT_FUTURE'); + assert(bidding_deadline < reveal_deadline, 'BAD_DEADLINES'); + assert(bidder_limit != 0, 'ZERO_BIDDER_LIMIT'); + assert(bidder_limit <= self.max_bidders.read(), 'BIDDER_LIMIT_TOO_HIGH'); + assert(!self.auction_exists.read(auction_id), 'AUCTION_EXISTS'); + + let config = AuctionConfig { + auction_id, + seller, + seller_claim_handle, + nft_contract, + token_id, + reserve_price, + cap, + bidding_deadline, + reveal_deadline, + bidder_limit, + }; + self.auction_exists.write(auction_id, true); + self.auctions.write(auction_id, config); + self + .emit( + AuctionCreated { + auction_id, + seller, + seller_claim_handle, + nft_contract, + token_id, + reserve_price, + cap, + bidding_deadline, + reveal_deadline, + bidder_limit, + }, + ); + + let house = get_contract_address(); + let nft = IERC721Dispatcher { contract_address: nft_contract }; + nft.transfer_from(seller, house, token_id); + assert(nft.owner_of(token_id) == house, 'NFT_NOT_CUSTODIED'); + } + + fn get_auction_config(self: @ContractState, auction_id: u64) -> AuctionConfig { + assert(self.auction_exists.read(auction_id), 'AUCTION_NOT_FOUND'); + self.auctions.read(auction_id) + } + fn privacy_invoke( ref self: ContractState, operation: u8, auction_id: u64, - a: felt252, - b: felt252, - c: felt252, - d: felt252, + primary_value: felt252, + claim_handle: felt252, + reserved_0: felt252, + reserved_1: felt252, pool_address: ContractAddress, - note_id: felt252, + open_note_id: felt252, ) -> Span { - let pool = self.pool.read(); - assert(get_caller_address() == pool, 'CALLER_NOT_POOL'); - assert(pool_address == pool, 'BAD_POOL'); + let configured_pool = self.pool.read(); + assert(get_caller_address() == configured_pool, 'CALLER_NOT_POOL'); + assert(pool_address == configured_pool, 'BAD_POOL'); + assert(reserved_0 == 0 && reserved_1 == 0, 'RESERVED_NOT_ZERO'); + assert(self.auction_exists.read(auction_id), 'AUCTION_NOT_FOUND'); + let config = self.auctions.read(auction_id); + + if operation == 0 { + assert(open_note_id == 0, 'BID_NOTE_NOT_ZERO'); + assert(primary_value != 0, 'ZERO_COMMITMENT'); + assert(claim_handle != 0, 'ZERO_CLAIM_HANDLE'); + assert(claim_handle != config.seller_claim_handle, 'SELLER_HANDLE_REUSED'); + assert(get_block_timestamp() < config.bidding_deadline, 'BIDDING_CLOSED'); + let accepted_index = self.bid_counts.read(auction_id); + assert(accepted_index < config.bidder_limit, 'BIDDER_LIMIT_REACHED'); + + let commitment_key = value_key(COMMITMENT_KEY_DOMAIN, auction_id, primary_value); + let claim_key = value_key(CLAIM_KEY_DOMAIN, auction_id, claim_handle); + assert(!self.commitment_seen.read(commitment_key), 'DUPLICATE_COMMITMENT'); + assert(!self.claim_handle_seen.read(claim_key), 'DUPLICATE_CLAIM_HANDLE'); + + let previous_balance = self.accounted_payment_balance.read(); + let next_balance = previous_balance + config.cap; + let actual_balance = IERC20Dispatcher { + contract_address: self.payment_token.read(), + } + .balance_of(get_contract_address()); + let expected_balance: u256 = next_balance.into(); + assert(actual_balance == expected_balance, 'BAD_COLLATERAL_DELTA'); + + let zero_address: ContractAddress = 0.try_into().unwrap(); + self + .bids + .write( + bid_key(auction_id, accepted_index), + BidRecord { + commitment: primary_value, + claim_handle, + revealed: false, + amount: 0, + asset_recipient: zero_address, + }, + ); + self.commitment_seen.write(commitment_key, true); + self.claim_handle_seen.write(claim_key, true); + self.claim_handle_index_plus_one.write(claim_key, accepted_index + 1); + self.bid_counts.write(auction_id, accepted_index + 1); + self.accounted_payment_balance.write(next_balance); + self + .emit( + BidCommitted { + auction_id, + accepted_index, + commitment: primary_value, + claim_handle, + cap: config.cap, + }, + ); + + let deposits: Array = array![]; + return deposits.span(); + } + + assert(operation == 1 || operation == 2 || operation == 3, 'BAD_OPERATION'); + assert(open_note_id != 0, 'ZERO_OPEN_NOTE'); + assert(primary_value != 0, 'ZERO_CLAIM_SECRET'); + assert(claim_handle != 0, 'ZERO_CLAIM_HANDLE'); + assert(compute_claim_handle(primary_value) == claim_handle, 'BAD_CLAIM_SECRET'); + let mut state = self.auction_states.read(auction_id); + assert(state.settled, 'AUCTION_NOT_SETTLED'); + + let amount = if operation == 3 { + assert(state.sold, 'NO_SELLER_PROCEEDS'); + assert(claim_handle == config.seller_claim_handle, 'BAD_SELLER_HANDLE'); + assert(!state.seller_claim_consumed, 'SELLER_CLAIM_CONSUMED'); + assert(state.seller_authorized_note == open_note_id, 'UNAUTHORIZED_SELLER_NOTE'); + assert(state.seller_entitlement != 0, 'ZERO_SELLER_PROCEEDS'); + state.seller_claim_consumed = true; + self.auction_states.write(auction_id, state); + state.seller_entitlement + } else { + let claim_key = value_key(CLAIM_KEY_DOMAIN, auction_id, claim_handle); + let index_plus_one = self.claim_handle_index_plus_one.read(claim_key); + assert(index_plus_one != 0, 'CLAIM_NOT_FOUND'); + assert(!self.bidder_claim_consumed.read(claim_key), 'BIDDER_CLAIM_CONSUMED'); + let accepted_index = index_plus_one - 1; + let bid = self.bids.read(bid_key(auction_id, accepted_index)); + assert(bid.claim_handle == claim_handle, 'CLAIM_HANDLE_MISMATCH'); + let claim_amount = if operation == 1 { + assert(!state.sold || accepted_index != state.winner_index, 'WINNER_NOT_LOSER'); + config.cap + } else { + assert(state.sold && accepted_index == state.winner_index, 'NOT_WINNER'); + assert(config.cap > state.clearing_price, 'NO_WINNER_SURPLUS'); + config.cap - state.clearing_price + }; + self.bidder_claim_consumed.write(claim_key, true); + claim_amount + }; - self.last_operation.write(operation); - self.last_auction_id.write(auction_id); - self.last_a.write(a); - self.last_b.write(b); + let previous_balance = self.accounted_payment_balance.read(); + assert(previous_balance >= amount, 'ACCOUNTING_UNDERFLOW'); + let payment_token = self.payment_token.read(); + let actual_balance = IERC20Dispatcher { contract_address: payment_token } + .balance_of(get_contract_address()); + let expected_balance: u256 = previous_balance.into(); + assert(actual_balance == expected_balance, 'PAYMENT_BALANCE_DRIFT'); + self.accounted_payment_balance.write(previous_balance - amount); + assert( + IERC20Dispatcher { contract_address: payment_token } + .approve(configured_pool, amount.into()), + 'POOL_APPROVAL_FAILED', + ); + if operation == 1 { + self.emit(LoserRefundClaimed { auction_id, claim_handle, open_note_id, amount }); + } else if operation == 2 { + self.emit(WinnerSurplusClaimed { auction_id, claim_handle, open_note_id, amount }); + } else { + self + .emit( + SellerProceedsClaimed { + auction_id, seller_claim_handle: claim_handle, open_note_id, amount, + }, + ); + } - let _unused = (c, d, note_id); - let deposits: Array = array![]; + let deposits = array![ + OpenNoteDeposit { note_id: open_note_id, token: payment_token, amount }, + ]; deposits.span() } - fn get_spike_state(self: @ContractState) -> (u8, u64, felt252, felt252) { - ( - self.last_operation.read(), - self.last_auction_id.read(), - self.last_a.read(), - self.last_b.read(), - ) + fn get_bid_count(self: @ContractState, auction_id: u64) -> u32 { + assert(self.auction_exists.read(auction_id), 'AUCTION_NOT_FOUND'); + self.bid_counts.read(auction_id) } - fn get_cap(self: @ContractState) -> u128 { - self.cap.read() + fn get_bid(self: @ContractState, auction_id: u64, accepted_index: u32) -> BidRecord { + assert(accepted_index < self.bid_counts.read(auction_id), 'BID_NOT_FOUND'); + self.bids.read(bid_key(auction_id, accepted_index)) } + + fn reveal_bid( + ref self: ContractState, + auction_id: u64, + accepted_index: u32, + amount: u128, + bid_nonce: felt252, + asset_recipient: ContractAddress, + ) { + assert(self.auction_exists.read(auction_id), 'AUCTION_NOT_FOUND'); + let config = self.auctions.read(auction_id); + let now = get_block_timestamp(); + assert(now >= config.bidding_deadline, 'REVEAL_NOT_OPEN'); + assert(now < config.reveal_deadline, 'REVEAL_CLOSED'); + assert(accepted_index < self.bid_counts.read(auction_id), 'BID_NOT_FOUND'); + assert(amount != 0 && amount <= config.cap, 'BAD_REVEAL_AMOUNT'); + assert(bid_nonce != 0, 'ZERO_BID_NONCE'); + let recipient_felt: felt252 = asset_recipient.into(); + assert(recipient_felt != 0, 'ZERO_RECIPIENT'); + + let key = bid_key(auction_id, accepted_index); + let mut bid = self.bids.read(key); + assert(!bid.revealed, 'BID_ALREADY_REVEALED'); + let expected = compute_bid_commitment( + get_tx_info().chain_id, + get_contract_address(), + auction_id, + amount, + bid_nonce, + bid.claim_handle, + asset_recipient, + ); + assert(expected == bid.commitment, 'COMMITMENT_MISMATCH'); + + bid.revealed = true; + bid.amount = amount; + bid.asset_recipient = asset_recipient; + self.bids.write(key, bid); + self.emit(BidRevealed { auction_id, accepted_index, amount, asset_recipient }); + } + + fn settle_auction(ref self: ContractState, auction_id: u64) { + assert(self.auction_exists.read(auction_id), 'AUCTION_NOT_FOUND'); + let config = self.auctions.read(auction_id); + assert(get_block_timestamp() >= config.reveal_deadline, 'SETTLEMENT_NOT_READY'); + let previous_state = self.auction_states.read(auction_id); + assert(!previous_state.settled, 'ALREADY_SETTLED'); + + let count = self.bid_counts.read(auction_id); + let zero_address: ContractAddress = 0.try_into().unwrap(); + let mut highest: u128 = 0; + let mut second_highest: u128 = 0; + let mut winner_index: u32 = 0; + let mut winner_commitment: felt252 = 0; + let mut winner_recipient = zero_address; + let mut index: u32 = 0; + loop { + if index == count { + break; + } + let bid = self.bids.read(bid_key(auction_id, index)); + if bid.revealed { + if bid.amount > highest { + second_highest = highest; + highest = bid.amount; + winner_index = index; + winner_commitment = bid.commitment; + winner_recipient = bid.asset_recipient; + } else if bid.amount > second_highest { + second_highest = bid.amount; + } + } + index += 1; + } + + let sold = highest >= config.reserve_price; + let clearing_price = if sold { + if second_highest > config.reserve_price { + second_highest + } else { + config.reserve_price + } + } else { + 0 + }; + let nft_recipient = if sold { + winner_recipient + } else { + config.seller + }; + self + .auction_states + .write( + auction_id, + AuctionState { + settled: true, + sold, + winner_index, + winner_commitment, + winner_recipient, + clearing_price, + seller_entitlement: clearing_price, + seller_authorized_note: 0, + seller_claim_consumed: !sold, + }, + ); + self + .emit( + AuctionSettled { + auction_id, + sold, + winner_index, + winner_commitment, + winner_recipient, + clearing_price, + seller_entitlement: clearing_price, + }, + ); + + let nft = IERC721Dispatcher { contract_address: config.nft_contract }; + nft.transfer_from(get_contract_address(), nft_recipient, config.token_id); + assert(nft.owner_of(config.token_id) == nft_recipient, 'NFT_DELIVERY_FAILED'); + } + + fn get_auction_state(self: @ContractState, auction_id: u64) -> AuctionState { + assert(self.auction_exists.read(auction_id), 'AUCTION_NOT_FOUND'); + self.auction_states.read(auction_id) + } + + fn authorize_seller_proceeds( + ref self: ContractState, + auction_id: u64, + seller_claim_handle: felt252, + open_note_id: felt252, + ) { + assert(self.auction_exists.read(auction_id), 'AUCTION_NOT_FOUND'); + let config = self.auctions.read(auction_id); + assert(get_caller_address() == config.seller, 'CALLER_NOT_SELLER'); + assert(seller_claim_handle == config.seller_claim_handle, 'BAD_SELLER_HANDLE'); + assert(open_note_id != 0, 'ZERO_OPEN_NOTE'); + let mut state = self.auction_states.read(auction_id); + assert(state.settled && state.sold, 'NO_SELLER_PROCEEDS'); + assert(!state.seller_claim_consumed, 'SELLER_CLAIM_CONSUMED'); + state.seller_authorized_note = open_note_id; + self.auction_states.write(auction_id, state); + self.emit(SellerProceedsAuthorized { auction_id, seller_claim_handle, open_note_id }); + } + + fn get_accounted_payment_balance(self: @ContractState) -> u128 { + self.accounted_payment_balance.read() + } + } + + fn bid_key(auction_id: u64, accepted_index: u32) -> felt252 { + poseidon_hash_span(array![BID_SLOT_DOMAIN, auction_id.into(), accepted_index.into()].span()) + } + + fn value_key(domain: felt252, auction_id: u64, value: felt252) -> felt252 { + poseidon_hash_span(array![domain, auction_id.into(), value].span()) } } diff --git a/contracts/tests/test_auction_house.cairo b/contracts/tests/test_auction_house.cairo new file mode 100644 index 0000000..3f161f0 --- /dev/null +++ b/contracts/tests/test_auction_house.cairo @@ -0,0 +1,540 @@ +use cipherbid::commitment::{compute_bid_commitment, compute_claim_handle}; +use cipherbid::{ + IAuctionHouseDispatcher, IAuctionHouseDispatcherTrait, IAuctionHouseSafeDispatcher, + IAuctionHouseSafeDispatcherTrait, +}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_block_timestamp, + start_cheat_caller_address, stop_cheat_caller_address, +}; +use starknet::ContractAddress; + +#[starknet::interface] +trait IMockERC721 { + fn approve(ref self: TContractState, spender: ContractAddress, token_id: u256); + fn transfer_from( + ref self: TContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ); + fn owner_of(self: @TContractState, token_id: u256) -> ContractAddress; +} + +#[starknet::contract] +mod MockERC721 { + use starknet::storage::{Map, StorageMapReadAccess, StorageMapWriteAccess}; + use starknet::{ContractAddress, get_caller_address}; + use super::IMockERC721; + + #[storage] + struct Storage { + owners: Map, + approvals: Map, + } + + #[constructor] + fn constructor(ref self: ContractState, owner: ContractAddress, token_id: u256) { + self.owners.write(token_id, owner); + } + + #[abi(embed_v0)] + impl MockERC721Impl of IMockERC721 { + fn approve(ref self: ContractState, spender: ContractAddress, token_id: u256) { + assert(self.owners.read(token_id) == get_caller_address(), 'NOT_OWNER'); + self.approvals.write(token_id, spender); + } + + fn transfer_from( + ref self: ContractState, from: ContractAddress, to: ContractAddress, token_id: u256, + ) { + let owner = self.owners.read(token_id); + let caller = get_caller_address(); + assert(owner == from, 'BAD_FROM'); + assert(caller == owner || caller == self.approvals.read(token_id), 'NOT_AUTHORIZED'); + self.owners.write(token_id, to); + self.approvals.write(token_id, 0.try_into().unwrap()); + } + + fn owner_of(self: @ContractState, token_id: u256) -> ContractAddress { + self.owners.read(token_id) + } + } +} + +#[starknet::interface] +trait IMockERC20 { + fn mint(ref self: TContractState, recipient: ContractAddress, amount: u256); + fn approve(ref self: TContractState, spender: ContractAddress, amount: u256) -> bool; + fn allowance(self: @TContractState, owner: ContractAddress, spender: ContractAddress) -> u256; + fn pull( + ref self: TContractState, owner: ContractAddress, recipient: ContractAddress, amount: u256, + ); + fn balance_of(self: @TContractState, account: ContractAddress) -> u256; +} + +#[starknet::contract] +mod MockERC20 { + use starknet::storage::{ + Map, StorageMapReadAccess, StorageMapWriteAccess, StoragePointerReadAccess, + StoragePointerWriteAccess, + }; + use starknet::{ContractAddress, get_caller_address}; + use super::IMockERC20; + + #[storage] + struct Storage { + balances: Map, + approved_owner: ContractAddress, + approved_spender: ContractAddress, + approved_amount: u256, + } + + #[abi(embed_v0)] + impl MockERC20Impl of IMockERC20 { + fn mint(ref self: ContractState, recipient: ContractAddress, amount: u256) { + self.balances.write(recipient, self.balances.read(recipient) + amount); + } + + fn approve(ref self: ContractState, spender: ContractAddress, amount: u256) -> bool { + self.approved_owner.write(get_caller_address()); + self.approved_spender.write(spender); + self.approved_amount.write(amount); + true + } + + fn allowance( + self: @ContractState, owner: ContractAddress, spender: ContractAddress, + ) -> u256 { + if self.approved_owner.read() == owner && self.approved_spender.read() == spender { + self.approved_amount.read() + } else { + 0 + } + } + + fn pull( + ref self: ContractState, + owner: ContractAddress, + recipient: ContractAddress, + amount: u256, + ) { + assert(get_caller_address() == self.approved_spender.read(), 'NOT_SPENDER'); + assert(owner == self.approved_owner.read(), 'BAD_ALLOWANCE_OWNER'); + let allowed = self.approved_amount.read(); + assert(amount <= allowed, 'ALLOWANCE_TOO_LOW'); + self.balances.write(owner, self.balances.read(owner) - amount); + self.balances.write(recipient, self.balances.read(recipient) + amount); + self.approved_amount.write(allowed - amount); + } + + fn balance_of(self: @ContractState, account: ContractAddress) -> u256 { + self.balances.read(account) + } + } +} + +fn address(value: felt252) -> ContractAddress { + value.try_into().unwrap() +} + +fn deploy_house( + pool: ContractAddress, payment_token: ContractAddress, max_bidders: u32, +) -> ContractAddress { + let contract = declare("AuctionHouse").unwrap().contract_class(); + let mut calldata = array![pool.into(), payment_token.into(), max_bidders.into()]; + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + contract_address +} + +fn deploy_nft(owner: ContractAddress, token_id: u256) -> ContractAddress { + let contract = declare("MockERC721").unwrap().contract_class(); + let mut calldata = array![owner.into(), token_id.low.into(), token_id.high.into()]; + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + contract_address +} + +fn deploy_token() -> ContractAddress { + let contract = declare("MockERC20").unwrap().contract_class(); + let mut calldata = array![]; + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + contract_address +} + +#[test] +fn deployment_configuration_is_immutable_and_public() { + let pool = address(0x123); + let token = address(0x456); + let house = deploy_house(pool, token, 32); + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + + let (configured_pool, configured_token, max_bidders) = dispatcher.get_house_config(); + assert(configured_pool == pool, 'BAD_POOL'); + assert(configured_token == token, 'BAD_TOKEN'); + assert(max_bidders == 32, 'BAD_MAX_BIDDERS'); +} + +#[test] +fn rejects_invalid_deployment_configuration() { + let contract = declare("AuctionHouse").unwrap().contract_class(); + let pool = address(0x123); + let token = address(0x456); + + let mut zero_pool = array![address(0).into(), token.into(), 32]; + assert(contract.deploy(@zero_pool).is_err(), 'ZERO_POOL_ACCEPTED'); + + let mut zero_token = array![pool.into(), address(0).into(), 32]; + assert(contract.deploy(@zero_token).is_err(), 'ZERO_TOKEN_ACCEPTED'); + + let mut same_addresses = array![pool.into(), pool.into(), 32]; + assert(contract.deploy(@same_addresses).is_err(), 'SAME_ADDRESSES'); + + let mut zero_bound = array![pool.into(), token.into(), 0]; + assert(contract.deploy(@zero_bound).is_err(), 'ZERO_BOUND_ACCEPTED'); + + let mut excessive_bound = array![pool.into(), token.into(), 33]; + assert(contract.deploy(@excessive_bound).is_err(), 'LARGE_BOUND_ACCEPTED'); +} + +#[test] +fn creation_custodies_nft_and_freezes_configuration() { + let seller = address(0x777); + let pool = address(0x123); + let payment_token = address(0x456); + let house = deploy_house(pool, payment_token, 32); + let token_id: u256 = 99; + let nft = deploy_nft(seller, token_id); + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + start_cheat_caller_address(house, seller); + dispatcher.create_auction(7, 0xabc, nft, token_id, 2, 5, 100, 200, 2); + + assert(nft_dispatcher.owner_of(token_id) == house, 'NFT_NOT_CUSTODIED'); + let config = dispatcher.get_auction_config(7); + assert(config.auction_id == 7, 'BAD_AUCTION_ID'); + assert(config.seller == seller, 'BAD_SELLER'); + assert(config.seller_claim_handle == 0xabc, 'BAD_SELLER_CLAIM'); + assert(config.nft_contract == nft, 'BAD_NFT'); + assert(config.token_id == token_id, 'BAD_TOKEN_ID'); + assert(config.reserve_price == 2, 'BAD_RESERVE'); + assert(config.cap == 5, 'BAD_CAP'); + assert(config.bidding_deadline == 100, 'BAD_BID_DEADLINE'); + assert(config.reveal_deadline == 200, 'BAD_REVEAL_DEADLINE'); + assert(config.bidder_limit == 2, 'BAD_BIDDER_LIMIT'); +} + +#[test] +#[feature("safe_dispatcher")] +fn failed_custody_rolls_back_creation_and_allows_retry() { + let seller = address(0x777); + let house = deploy_house(address(0x123), address(0x456), 32); + let token_id: u256 = 99; + let nft = deploy_nft(seller, token_id); + start_cheat_caller_address(house, seller); + + let safe_dispatcher = IAuctionHouseSafeDispatcher { contract_address: house }; + let failed = safe_dispatcher.create_auction(7, 0xabc, nft, token_id, 2, 5, 100, 200, 2); + assert(failed.is_err(), 'UNAPPROVED_CUSTODY'); + + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + dispatcher.create_auction(7, 0xabc, nft, token_id, 2, 5, 100, 200, 2); + assert(dispatcher.get_auction_config(7).seller == seller, 'RETRY_NOT_CREATED'); + assert(nft_dispatcher.owner_of(token_id) == house, 'RETRY_NOT_CUSTODIED'); +} + +#[test] +fn pool_parks_exact_cap_and_records_bounded_bid() { + let seller = address(0x777); + let pool = address(0x123); + let token = deploy_token(); + let house = deploy_house(pool, token, 32); + let token_id: u256 = 99; + let nft = deploy_nft(seller, token_id); + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + start_cheat_caller_address(house, seller); + dispatcher.create_auction(7, 0xabc, nft, token_id, 2, 5, 100, 200, 2); + + let token_dispatcher = IMockERC20Dispatcher { contract_address: token }; + token_dispatcher.mint(house, 5); + start_cheat_caller_address(house, pool); + let deposits = dispatcher.privacy_invoke(0, 7, 0x111, 0x222, 0, 0, pool, 0); + + assert(deposits.is_empty(), 'BID_RETURNED_NOTE'); + assert(dispatcher.get_bid_count(7) == 1, 'BAD_BID_COUNT'); + let bid = dispatcher.get_bid(7, 0); + assert(bid.commitment == 0x111, 'BAD_COMMITMENT'); + assert(bid.claim_handle == 0x222, 'BAD_CLAIM_HANDLE'); + assert(!bid.revealed, 'BID_ALREADY_REVEALED'); +} + +#[test] +#[feature("safe_dispatcher")] +fn wrong_collateral_amount_reverts_without_consuming_slot() { + let seller = address(0x777); + let pool = address(0x123); + let token = deploy_token(); + let house = deploy_house(pool, token, 32); + let token_id: u256 = 99; + let nft = deploy_nft(seller, token_id); + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + start_cheat_caller_address(house, seller); + dispatcher.create_auction(7, 0xabc, nft, token_id, 2, 5, 100, 200, 2); + + let token_dispatcher = IMockERC20Dispatcher { contract_address: token }; + token_dispatcher.mint(house, 4); + start_cheat_caller_address(house, pool); + let safe = IAuctionHouseSafeDispatcher { contract_address: house }; + assert(safe.privacy_invoke(0, 7, 0x111, 0x222, 0, 0, pool, 0).is_err(), 'SHORT_CAP'); + + token_dispatcher.mint(house, 1); + dispatcher.privacy_invoke(0, 7, 0x111, 0x222, 0, 0, pool, 0); + assert(dispatcher.get_bid_count(7) == 1, 'FAILED_BID_CONSUMED_SLOT'); +} + +#[test] +#[feature("safe_dispatcher")] +fn reveal_recomputes_commitment_and_persists_public_bid_data_once() { + let seller = address(0x777); + let pool = address(0x123); + let token = deploy_token(); + let house = deploy_house(pool, token, 32); + let token_id: u256 = 99; + let nft = deploy_nft(seller, token_id); + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + start_cheat_caller_address(house, seller); + dispatcher.create_auction(7, 0xabc, nft, token_id, 2, 5, 100, 200, 2); + + let claim_handle = 0x222; + let bid_nonce = 0x333; + let recipient = address(0x888); + let commitment = compute_bid_commitment( + 'SN_SEPOLIA', house, 7, 3, bid_nonce, claim_handle, recipient, + ); + IMockERC20Dispatcher { contract_address: token }.mint(house, 5); + start_cheat_caller_address(house, pool); + dispatcher.privacy_invoke(0, 7, commitment, claim_handle, 0, 0, pool, 0); + + start_cheat_block_timestamp(house, 100); + dispatcher.reveal_bid(7, 0, 3, bid_nonce, recipient); + let bid = dispatcher.get_bid(7, 0); + assert(bid.revealed, 'BID_NOT_REVEALED'); + assert(bid.amount == 3, 'BAD_REVEALED_AMOUNT'); + assert(bid.asset_recipient == recipient, 'BAD_RECIPIENT'); + + let safe = IAuctionHouseSafeDispatcher { contract_address: house }; + assert(safe.reveal_bid(7, 0, 3, bid_nonce, recipient).is_err(), 'DOUBLE_REVEAL'); +} + +#[test] +#[feature("safe_dispatcher")] +fn settlement_delivers_nft_and_records_vickrey_price() { + let seller = address(0x777); + let pool = address(0x123); + let token = deploy_token(); + let house = deploy_house(pool, token, 32); + let token_id: u256 = 99; + let nft = deploy_nft(seller, token_id); + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + start_cheat_caller_address(house, seller); + let seller_secret = 0x551; + let seller_handle = compute_claim_handle(seller_secret); + dispatcher.create_auction(7, seller_handle, nft, token_id, 2, 5, 100, 200, 2); + + let recipient_a = address(0x881); + let recipient_b = address(0x882); + let secret_a = 0x541; + let secret_b = 0x542; + let handle_a = compute_claim_handle(secret_a); + let handle_b = compute_claim_handle(secret_b); + let commitment_a = compute_bid_commitment( + 'SN_SEPOLIA', house, 7, 3, 0x331, handle_a, recipient_a, + ); + let commitment_b = compute_bid_commitment( + 'SN_SEPOLIA', house, 7, 4, 0x332, handle_b, recipient_b, + ); + let token_dispatcher = IMockERC20Dispatcher { contract_address: token }; + start_cheat_caller_address(house, pool); + token_dispatcher.mint(house, 5); + dispatcher.privacy_invoke(0, 7, commitment_a, handle_a, 0, 0, pool, 0); + token_dispatcher.mint(house, 5); + dispatcher.privacy_invoke(0, 7, commitment_b, handle_b, 0, 0, pool, 0); + + start_cheat_block_timestamp(house, 100); + dispatcher.reveal_bid(7, 0, 3, 0x331, recipient_a); + dispatcher.reveal_bid(7, 1, 4, 0x332, recipient_b); + start_cheat_block_timestamp(house, 200); + dispatcher.settle_auction(7); + + let state = dispatcher.get_auction_state(7); + assert(state.settled, 'NOT_SETTLED'); + assert(state.sold, 'NOT_SOLD'); + assert(state.winner_index == 1, 'BAD_WINNER'); + assert(state.winner_recipient == recipient_b, 'BAD_WINNER_RECIPIENT'); + assert(state.clearing_price == 3, 'BAD_CLEARING_PRICE'); + assert(state.seller_entitlement == 3, 'BAD_SELLER_ENTITLEMENT'); + assert(nft_dispatcher.owner_of(token_id) == recipient_b, 'NFT_NOT_DELIVERED'); + + start_cheat_caller_address(house, seller); + dispatcher.authorize_seller_proceeds(7, seller_handle, 0x903); + start_cheat_caller_address(house, pool); + let safe = IAuctionHouseSafeDispatcher { contract_address: house }; + assert( + safe.privacy_invoke(3, 7, seller_secret, seller_handle, 0, 0, pool, 0x999).is_err(), + 'SELLER_REDIRECT_ACCEPTED', + ); + + let loser = dispatcher.privacy_invoke(1, 7, secret_a, handle_a, 0, 0, pool, 0x901); + assert(loser.len() == 1, 'BAD_LOSER_OUTPUTS'); + let loser_deposit = *loser.at(0); + assert(loser_deposit.note_id == 0x901, 'BAD_LOSER_NOTE'); + assert(loser_deposit.token == token, 'BAD_LOSER_TOKEN'); + assert(loser_deposit.amount == 5, 'BAD_LOSER_AMOUNT'); + assert(token_dispatcher.allowance(house, pool) == 5, 'BAD_LOSER_APPROVAL'); + start_cheat_caller_address(token, pool); + token_dispatcher.pull(house, pool, 5); + stop_cheat_caller_address(token); + assert( + safe.privacy_invoke(1, 7, secret_a, handle_a, 0, 0, pool, 0x904).is_err(), + 'LOSER_REPLAY_ACCEPTED', + ); + + assert( + safe.privacy_invoke(1, 7, secret_b, handle_b, 0, 0, pool, 0x902).is_err(), + 'WINNER_AS_LOSER', + ); + let surplus = dispatcher.privacy_invoke(2, 7, secret_b, handle_b, 0, 0, pool, 0x902); + assert(surplus.len() == 1, 'BAD_SURPLUS_OUTPUTS'); + let surplus_deposit = *surplus.at(0); + assert(surplus_deposit.note_id == 0x902, 'BAD_SURPLUS_NOTE'); + assert(surplus_deposit.amount == 2, 'BAD_SURPLUS_AMOUNT'); + assert(token_dispatcher.allowance(house, pool) == 2, 'BAD_SURPLUS_APPROVAL'); + start_cheat_caller_address(token, pool); + token_dispatcher.pull(house, pool, 2); + stop_cheat_caller_address(token); + + let proceeds = dispatcher.privacy_invoke(3, 7, seller_secret, seller_handle, 0, 0, pool, 0x903); + assert(proceeds.len() == 1, 'BAD_SELLER_OUTPUTS'); + let proceeds_deposit = *proceeds.at(0); + assert(proceeds_deposit.note_id == 0x903, 'BAD_SELLER_NOTE'); + assert(proceeds_deposit.amount == 3, 'BAD_SELLER_AMOUNT'); + assert(token_dispatcher.allowance(house, pool) == 3, 'BAD_SELLER_APPROVAL'); + start_cheat_caller_address(token, pool); + token_dispatcher.pull(house, pool, 3); + stop_cheat_caller_address(token); + assert( + safe.privacy_invoke(3, 7, seller_secret, seller_handle, 0, 0, pool, 0x903).is_err(), + 'SELLER_REPLAY_ACCEPTED', + ); + + assert(token_dispatcher.balance_of(house) == 0, 'COLLATERAL_STRANDED'); + assert(dispatcher.get_accounted_payment_balance() == 0, 'ACCOUNTING_NOT_ZERO'); +} + +#[test] +fn equal_bids_use_earliest_accepted_index() { + let seller = address(0x777); + let pool = address(0x123); + let token = deploy_token(); + let house = deploy_house(pool, token, 32); + let token_id: u256 = 100; + let nft = deploy_nft(seller, token_id); + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + start_cheat_caller_address(house, seller); + dispatcher.create_auction(8, compute_claim_handle(0x601), nft, token_id, 2, 5, 100, 200, 2); + + let recipient_a = address(0x891); + let recipient_b = address(0x892); + let handle_a = compute_claim_handle(0x611); + let handle_b = compute_claim_handle(0x612); + let commitment_a = compute_bid_commitment( + 'SN_SEPOLIA', house, 8, 4, 0x621, handle_a, recipient_a, + ); + let commitment_b = compute_bid_commitment( + 'SN_SEPOLIA', house, 8, 4, 0x622, handle_b, recipient_b, + ); + let token_dispatcher = IMockERC20Dispatcher { contract_address: token }; + start_cheat_caller_address(house, pool); + token_dispatcher.mint(house, 5); + dispatcher.privacy_invoke(0, 8, commitment_a, handle_a, 0, 0, pool, 0); + token_dispatcher.mint(house, 5); + dispatcher.privacy_invoke(0, 8, commitment_b, handle_b, 0, 0, pool, 0); + start_cheat_block_timestamp(house, 100); + dispatcher.reveal_bid(8, 0, 4, 0x621, recipient_a); + dispatcher.reveal_bid(8, 1, 4, 0x622, recipient_b); + start_cheat_block_timestamp(house, 200); + dispatcher.settle_auction(8); + + let state = dispatcher.get_auction_state(8); + assert(state.winner_index == 0, 'TIE_NOT_EARLIEST'); + assert(state.clearing_price == 4, 'BAD_TIE_PRICE'); + assert(nft_dispatcher.owner_of(token_id) == recipient_a, 'TIE_NFT_MISDELIVERED'); +} + +#[test] +fn no_sale_returns_nft_and_unrevealed_bid_gets_full_cap() { + let seller = address(0x777); + let pool = address(0x123); + let token = deploy_token(); + let house = deploy_house(pool, token, 32); + let token_id: u256 = 101; + let nft = deploy_nft(seller, token_id); + let nft_dispatcher = IMockERC721Dispatcher { contract_address: nft }; + start_cheat_caller_address(nft, seller); + nft_dispatcher.approve(house, token_id); + stop_cheat_caller_address(nft); + let dispatcher = IAuctionHouseDispatcher { contract_address: house }; + start_cheat_caller_address(house, seller); + dispatcher.create_auction(9, compute_claim_handle(0x701), nft, token_id, 3, 5, 100, 200, 1); + + let secret = 0x711; + let handle = compute_claim_handle(secret); + let recipient = address(0x899); + let commitment = compute_bid_commitment('SN_SEPOLIA', house, 9, 2, 0x721, handle, recipient); + let token_dispatcher = IMockERC20Dispatcher { contract_address: token }; + token_dispatcher.mint(house, 5); + start_cheat_caller_address(house, pool); + dispatcher.privacy_invoke(0, 9, commitment, handle, 0, 0, pool, 0); + start_cheat_block_timestamp(house, 200); + dispatcher.settle_auction(9); + + let state = dispatcher.get_auction_state(9); + assert(state.settled && !state.sold, 'EXPECTED_NO_SALE'); + assert(state.seller_entitlement == 0, 'NO_SALE_SELLER_VALUE'); + assert(nft_dispatcher.owner_of(token_id) == seller, 'NFT_NOT_RETURNED'); + + let refund = dispatcher.privacy_invoke(1, 9, secret, handle, 0, 0, pool, 0x909); + assert(refund.len() == 1, 'BAD_NO_SALE_OUTPUTS'); + assert((*refund.at(0)).amount == 5, 'BAD_NO_SALE_REFUND'); + assert(token_dispatcher.allowance(house, pool) == 5, 'BAD_NO_SALE_APPROVAL'); + start_cheat_caller_address(token, pool); + token_dispatcher.pull(house, pool, 5); + stop_cheat_caller_address(token); + assert(token_dispatcher.balance_of(house) == 0, 'NO_SALE_COLLATERAL_STRANDED'); + assert(dispatcher.get_accounted_payment_balance() == 0, 'NO_SALE_ACCOUNTING'); +} diff --git a/contracts/tests/test_commitment.cairo b/contracts/tests/test_commitment.cairo index babada5..59d6c3e 100644 --- a/contracts/tests/test_commitment.cairo +++ b/contracts/tests/test_commitment.cairo @@ -4,7 +4,7 @@ const CHAIN_ID: felt252 = 'SN_SEPOLIA'; const AUCTION_HOUSE: felt252 = 0x222; const AUCTION_ID: u64 = 7; const AMOUNT: u128 = 3000000000000000000; -const BID_SECRET: felt252 = 987654321; +const BID_NONCE: felt252 = 987654321; const CLAIM_HANDLE: felt252 = 0x3078725b5aaffe73f545ebca32c0b5a4af14404599edd691c752e59ffca3724; const ASSET_RECIPIENT: felt252 = 0x333; @@ -13,12 +13,18 @@ fn commitment( auction_house: felt252, auction_id: u64, amount: u128, - bid_secret: felt252, + bid_nonce: felt252, claim_handle: felt252, asset_recipient: felt252, ) -> felt252 { compute_bid_commitment( - chain_id, auction_house, auction_id, amount, bid_secret, claim_handle, asset_recipient, + chain_id, + auction_house.try_into().expect('INVALID_AUCTION_HOUSE'), + auction_id, + amount, + bid_nonce, + claim_handle, + asset_recipient.try_into().expect('INVALID_RECIPIENT'), ) } @@ -32,7 +38,7 @@ fn matches_frozen_typescript_poseidon_vectors() { ); assert( commitment( - CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_SECRET, CLAIM_HANDLE, ASSET_RECIPIENT, + CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT, ) == 0x34fe5ddb49c604d4b8b63f768c4d6e4159bdd4166bdc3e1e7094217c9f6313e, 'BAD_BID_VECTOR', ); @@ -41,7 +47,7 @@ fn matches_frozen_typescript_poseidon_vectors() { #[test] fn every_bid_domain_field_changes_the_commitment() { let expected = commitment( - CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_SECRET, CLAIM_HANDLE, ASSET_RECIPIENT, + CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT, ); assert( commitment( @@ -49,7 +55,7 @@ fn every_bid_domain_field_changes_the_commitment() { AUCTION_HOUSE, AUCTION_ID, AMOUNT, - BID_SECRET, + BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT, ) != expected, @@ -61,7 +67,7 @@ fn every_bid_domain_field_changes_the_commitment() { AUCTION_HOUSE + 1, AUCTION_ID, AMOUNT, - BID_SECRET, + BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT, ) != expected, @@ -73,7 +79,7 @@ fn every_bid_domain_field_changes_the_commitment() { AUCTION_HOUSE, AUCTION_ID + 1, AMOUNT, - BID_SECRET, + BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT, ) != expected, @@ -85,7 +91,7 @@ fn every_bid_domain_field_changes_the_commitment() { AUCTION_HOUSE, AUCTION_ID, AMOUNT + 1, - BID_SECRET, + BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT, ) != expected, @@ -97,11 +103,11 @@ fn every_bid_domain_field_changes_the_commitment() { AUCTION_HOUSE, AUCTION_ID, AMOUNT, - BID_SECRET + 1, + BID_NONCE + 1, CLAIM_HANDLE, ASSET_RECIPIENT, ) != expected, - 'BID_SECRET_NOT_BOUND', + 'BID_NONCE_NOT_BOUND', ); assert( commitment( @@ -109,7 +115,7 @@ fn every_bid_domain_field_changes_the_commitment() { AUCTION_HOUSE, AUCTION_ID, AMOUNT, - BID_SECRET, + BID_NONCE, CLAIM_HANDLE + 1, ASSET_RECIPIENT, ) != expected, @@ -121,7 +127,7 @@ fn every_bid_domain_field_changes_the_commitment() { AUCTION_HOUSE, AUCTION_ID, AMOUNT, - BID_SECRET, + BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT + 1, ) != expected, @@ -136,19 +142,106 @@ fn rejects_zero_claim_secret() { } #[test] -#[should_panic(expected: 'ZERO_BID_SECRET')] -fn rejects_zero_bid_secret() { +#[should_panic(expected: 'ZERO_BID_NONCE')] +fn rejects_zero_bid_nonce() { commitment(CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, 0, CLAIM_HANDLE, ASSET_RECIPIENT); } #[test] #[should_panic(expected: 'ZERO_CLAIM_HANDLE')] fn rejects_zero_claim_handle() { - commitment(CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_SECRET, 0, ASSET_RECIPIENT); + commitment(CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_NONCE, 0, ASSET_RECIPIENT); } #[test] #[should_panic(expected: 'ZERO_BID_AMOUNT')] fn rejects_zero_bid_amount() { - commitment(CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, 0, BID_SECRET, CLAIM_HANDLE, ASSET_RECIPIENT); + commitment(CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, 0, BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT); +} + +#[test] +fn matches_minimum_and_maximum_boundary_vectors() { + let minimum_claim_handle = compute_claim_handle(1); + assert( + minimum_claim_handle == 0x6b7f8ff6dee712dbd900e4e0269931a6dc86de5359e13dc740ca1898d110b48, + 'BAD_MIN_CLAIM_VECTOR', + ); + assert( + commitment( + 1, 1, 1, 1, 1, minimum_claim_handle, 1, + ) == 0x5c8b0026c8ddfd09e47cba64881b66d371c620d84b0e573f811ec2334526848, + 'BAD_MIN_BID_VECTOR', + ); + + let max_felt = 0x800000000000011000000000000000000000000000000000000000000000000; + let max_address = 0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff; + let maximum_claim_handle = compute_claim_handle(max_felt); + assert( + maximum_claim_handle == 0x51f784d5ce10bdf76e3c632882ba6e181464bd8f4493fd9e7bfc44c6deefd34, + 'BAD_MAX_CLAIM_VECTOR', + ); + assert( + commitment( + max_felt, + max_address, + 0xffffffffffffffff, + 0xffffffffffffffffffffffffffffffff, + max_felt, + maximum_claim_handle, + max_address, + ) == 0x1dc855fa1871e1425360884f6b03c77837f2c5d47e551f86b55af0e0f8fa1b5, + 'BAD_MAX_BID_VECTOR', + ); +} + +#[test] +#[should_panic(expected: 'ZERO_AUCTION_ID')] +fn rejects_zero_auction_id() { + commitment(CHAIN_ID, AUCTION_HOUSE, 0, AMOUNT, BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT); +} + +#[test] +#[should_panic(expected: 'ZERO_CHAIN_ID')] +fn rejects_zero_chain_id() { + commitment(0, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT); +} + +#[test] +#[should_panic(expected: 'ZERO_AUCTION_HOUSE')] +fn rejects_zero_auction_house() { + commitment(CHAIN_ID, 0, AUCTION_ID, AMOUNT, BID_NONCE, CLAIM_HANDLE, ASSET_RECIPIENT); +} + +#[test] +#[should_panic(expected: 'ZERO_RECIPIENT')] +fn rejects_zero_recipient() { + commitment(CHAIN_ID, AUCTION_HOUSE, AUCTION_ID, AMOUNT, BID_NONCE, CLAIM_HANDLE, 0); +} + +#[test] +#[should_panic(expected: 'INVALID_AUCTION_HOUSE')] +fn rejects_out_of_range_auction_house() { + commitment( + CHAIN_ID, + 0x800000000000000000000000000000000000000000000000000000000000000, + AUCTION_ID, + AMOUNT, + BID_NONCE, + CLAIM_HANDLE, + ASSET_RECIPIENT, + ); +} + +#[test] +#[should_panic(expected: 'INVALID_RECIPIENT')] +fn rejects_out_of_range_recipient() { + commitment( + CHAIN_ID, + AUCTION_HOUSE, + AUCTION_ID, + AMOUNT, + BID_NONCE, + CLAIM_HANDLE, + 0x800000000000000000000000000000000000000000000000000000000000000, + ); } diff --git a/contracts/tests/test_contract.cairo b/contracts/tests/test_contract.cairo deleted file mode 100644 index 9111656..0000000 --- a/contracts/tests/test_contract.cairo +++ /dev/null @@ -1,77 +0,0 @@ -use cipherbid::{IAuctionIngressSpikeDispatcher, IAuctionIngressSpikeDispatcherTrait}; -use snforge_std::{ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address}; -use starknet::ContractAddress; - -fn address(value: felt252) -> ContractAddress { - value.try_into().unwrap() -} - -fn deploy_contract(pool: ContractAddress, cap: u128) -> ContractAddress { - let contract = declare("AuctionIngressSpike").unwrap().contract_class(); - let mut calldata = array![pool.into(), cap.into()]; - let (contract_address, _) = contract.deploy(@calldata).unwrap(); - contract_address -} - -#[test] -fn pool_can_park_bid_collateral_without_returning_an_open_note() { - let pool = address(0x123); - let contract_address = deploy_contract(pool, 5); - start_cheat_caller_address(contract_address, pool); - let dispatcher = IAuctionIngressSpikeDispatcher { contract_address }; - - let deposits = dispatcher.privacy_invoke(0, 7, 101, 202, 0, 0, pool, 0); - - assert(deposits.len() == 0, 'BID_MUST_PARK'); - let (operation, auction_id, commitment, claim_handle) = dispatcher.get_spike_state(); - assert(operation == 0, 'BAD_OPERATION'); - assert(auction_id == 7, 'BAD_AUCTION'); - assert(commitment == 101, 'BAD_COMMITMENT'); - assert(claim_handle == 202, 'BAD_CLAIM'); -} - -#[test] -fn pool_can_route_a_no_value_reveal() { - let pool = address(0x123); - let contract_address = deploy_contract(pool, 5); - start_cheat_caller_address(contract_address, pool); - let dispatcher = IAuctionIngressSpikeDispatcher { contract_address }; - - let deposits = dispatcher.privacy_invoke(1, 7, 3, 404, 202, 0x333, pool, 0); - - assert(deposits.len() == 0, 'REVEAL_MOVED_VALUE'); - let (operation, auction_id, amount, bid_secret) = dispatcher.get_spike_state(); - assert(operation == 1, 'BAD_OPERATION'); - assert(auction_id == 7, 'BAD_AUCTION'); - assert(amount == 3, 'BAD_AMOUNT'); - assert(bid_secret == 404, 'BAD_SECRET'); -} - -#[test] -#[should_panic(expected: 'CALLER_NOT_POOL')] -fn direct_caller_cannot_drive_the_spike() { - let pool = address(0x123); - let attacker = address(0x456); - let contract_address = deploy_contract(pool, 5); - start_cheat_caller_address(contract_address, attacker); - let dispatcher = IAuctionIngressSpikeDispatcher { contract_address }; - - dispatcher.privacy_invoke(0, 7, 101, 202, 0, 0, pool, 0); -} - -#[test] -fn configured_uniform_cap_is_public() { - let pool = address(0x123); - let contract_address = deploy_contract(pool, 5); - let dispatcher = IAuctionIngressSpikeDispatcher { contract_address }; - - assert(dispatcher.get_cap() == 5, 'BAD_CAP'); -} - -#[test] -fn rejects_zero_uniform_cap() { - let contract = declare("AuctionIngressSpike").unwrap().contract_class(); - let mut calldata = array![address(0x123).into(), 0]; - - assert(contract.deploy(@calldata).is_err(), 'ZERO_CAP_ACCEPTED'); -} diff --git a/contracts/tests/test_demo_erc721.cairo b/contracts/tests/test_demo_erc721.cairo new file mode 100644 index 0000000..a2955c7 --- /dev/null +++ b/contracts/tests/test_demo_erc721.cairo @@ -0,0 +1,66 @@ +use cipherbid::demo_erc721::{ + IDemoERC721Dispatcher, IDemoERC721DispatcherTrait, IDemoERC721SafeDispatcher, + IDemoERC721SafeDispatcherTrait, +}; +use snforge_std::{ + ContractClassTrait, DeclareResultTrait, declare, start_cheat_caller_address, + stop_cheat_caller_address, +}; +use starknet::ContractAddress; + +fn address(value: felt252) -> ContractAddress { + value.try_into().unwrap() +} + +fn deploy_demo(owner: ContractAddress, token_id: u256) -> ContractAddress { + let contract = declare("DemoERC721").unwrap().contract_class(); + let mut calldata = array![owner.into(), token_id.low.into(), token_id.high.into()]; + let (contract_address, _) = contract.deploy(@calldata).unwrap(); + contract_address +} + +#[test] +fn constructor_mints_one_approved_transferable_demo_token() { + let seller = address(0x777); + let auction_house = address(0x123); + let recipient = address(0x888); + let token_id: u256 = 99; + let nft = deploy_demo(seller, token_id); + let dispatcher = IDemoERC721Dispatcher { contract_address: nft }; + + assert(dispatcher.owner_of(token_id) == seller, 'BAD_INITIAL_OWNER'); + assert(dispatcher.balance_of(seller) == 1, 'BAD_INITIAL_BALANCE'); + + start_cheat_caller_address(nft, seller); + dispatcher.approve(auction_house, token_id); + stop_cheat_caller_address(nft); + assert(dispatcher.get_approved(token_id) == auction_house, 'BAD_APPROVAL'); + + start_cheat_caller_address(nft, auction_house); + dispatcher.transfer_from(seller, recipient, token_id); + stop_cheat_caller_address(nft); + + assert(dispatcher.owner_of(token_id) == recipient, 'BAD_TRANSFER_OWNER'); + assert(dispatcher.balance_of(seller) == 0, 'SELLER_BALANCE_NOT_CLEARED'); + assert(dispatcher.balance_of(recipient) == 1, 'RECIPIENT_BALANCE_NOT_SET'); + assert(dispatcher.get_approved(token_id) == address(0), 'APPROVAL_NOT_CLEARED'); +} + +#[test] +#[feature("safe_dispatcher")] +fn unauthorized_transfer_and_zero_owner_deployment_are_rejected() { + let seller = address(0x777); + let attacker = address(0x666); + let token_id: u256 = 99; + let nft = deploy_demo(seller, token_id); + let safe_dispatcher = IDemoERC721SafeDispatcher { contract_address: nft }; + + start_cheat_caller_address(nft, attacker); + let unauthorized = safe_dispatcher.transfer_from(seller, attacker, token_id); + stop_cheat_caller_address(nft); + assert(unauthorized.is_err(), 'UNAUTHORIZED_TRANSFER'); + + let contract = declare("DemoERC721").unwrap().contract_class(); + let mut zero_owner = array![address(0).into(), token_id.low.into(), token_id.high.into()]; + assert(contract.deploy(@zero_owner).is_err(), 'ZERO_OWNER_ACCEPTED'); +} diff --git a/docs/evidence/README.md b/docs/evidence/README.md new file mode 100644 index 0000000..a6f3d22 --- /dev/null +++ b/docs/evidence/README.md @@ -0,0 +1,68 @@ +# CipherBid evidence index + +This directory contains public, secret-free specifications and verified readbacks. Files describe only what their cited source or chain readback proves. Wallet keys, viewing keys, bid nonces, claim secrets, recovery payloads, private notes, proof witnesses, sessions, and raw wallet output never belong here. + +## Verified mainnet evidence + +| Artifact | Status | Scope | +| -------------------------------------------------------------- | ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| [`mainnet/deployment.md`](mainnet/deployment.md) | Verified | Mainnet declarations, deployed addresses, successful receipts, class hashes, immutable AuctionHouse config, DemoERC721 ownership, and bounded fee accounting | +| [`mainnet/deployment.json`](mainnet/deployment.json) | Verified | Machine-readable public deployment manifest consumed by frontend and auction-plan tooling | +| [`mainnet/release-candidate.md`](mainnet/release-candidate.md) | Superseded pre-write freeze plus current status | Approved accounts, protocol addresses, demo economics, pool-fee assumptions, and release stop conditions | + +The mainnet private lifecycle is **not yet verified**. Do not create `mainnet/transactions.md`, `mainnet/auction-lifecycle.md`, or add hashes to `strk20.json` until the corresponding pool-touching transactions succeed and independent receipt/state readback passes. + +## Prepared recording control + +| Artifact | Status | Scope | +| -------------------------------------------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | +| [`mainnet/demo-script.md`](mainnet/demo-script.md) | Prepared; recording blocked | Maximum-three-minute narration, capture rules, and hard evidence gates bound to the deployed mainnet contracts and canonical `2/3 STRK` case | + +The script is not demo-video evidence. Its hard gate forbids recording or publication until the complete real lifecycle, claims, and Atomic Delivery Receipt are independently verified. + +## Durable frontend publication + +The reviewed deployment target is `https://sourcesenseitherealone.github.io/cipherbid/`, with live auction reads at `/auction?id=`. The pinned GitHub Pages workflow and static export are implementation controls, not hosted evidence by themselves. A public deployment record is created under `submission/` only after a successful `main` workflow, Pages API readback, HTTP checks, and clean-browser verification. Until then, `strk20.json.demo_url` remains empty. + +## Canonical lifecycle and security controls + +| Artifact | Scope | +| ---------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | +| [`task-0-demo-matrix.md`](task-0-demo-matrix.md) | Canonical seller, two-bidder, observer, settlement, and public/private evidence matrix | +| [`task-1-1-wallet-api-route.md`](task-1-1-wallet-api-route.md) | Wallet API custody route and capability boundary | +| [`task-1-2-bid-ingress-wire.md`](task-1-2-bid-ingress-wire.md) | Equal-cap private ingress wire contract | +| [`task-1-3-lifecycle-wire-matrix.md`](task-1-3-lifecycle-wire-matrix.md) | Typed lifecycle calls, events, and readbacks | +| [`task-2-1-auction-configuration.md`](task-2-1-auction-configuration.md) | Auction configuration and bounds | +| [`task-2-2-bid-credentials.md`](task-2-2-bid-credentials.md) | Commitment and recovery credential model | +| [`task-2-3-lifecycle-specification.md`](task-2-3-lifecycle-specification.md) | Full state-machine specification | +| [`task-2-4-security-invariants.md`](task-2-4-security-invariants.md) | Contract, custody, conservation, and replay invariants | + +## Rehearsal evidence + +| Artifact | Scope | +| ------------------------------------------------------------------ | -------------------------------------------------------------- | +| [`sepolia/deployment-manifest.md`](sepolia/deployment-manifest.md) | Historical Sepolia deployment identity and configuration | +| [`sepolia/demo-runbook.md`](sepolia/demo-runbook.md) | Historical public no-sale rehearsal and execution instructions | +| [`sepolia-feasibility.md`](sepolia-feasibility.md) | Earlier live feasibility findings and limitations | + +Sepolia evidence is rehearsal evidence only. It does not establish mainnet private-lifecycle completion. + +## Submission controls + +| Artifact | Scope | +| ---------------------------------------------------------------------- | ------------------------------------------------------------------ | +| [`hackathon-requirements-matrix.md`](hackathon-requirements-matrix.md) | Official requirement-to-evidence mapping and truthfulness controls | +| [`winning-product-scope.md`](winning-product-scope.md) | Bounded product scope and non-goals | + +## Publication gate + +A mainnet lifecycle transaction may enter public evidence only when all applicable checks pass: + +1. the exact transaction exists on Starknet mainnet; +2. its receipt is `SUCCEEDED` and accepted on L2 or L1; +3. expected CipherBid events come from the verified AuctionHouse; +4. operations expected to touch STRK20 include a pool event; +5. post-transaction contract/NFT state matches the intended transition; +6. the record contains no secret-bearing wallet, recovery, proof, or session data. + +`strk20.json` additionally requires at least three unique qualifying mainnet hashes plus verified public demo and video URLs. Empty fields are preferable to fabricated or premature claims. diff --git a/docs/evidence/hackathon-requirements-matrix.md b/docs/evidence/hackathon-requirements-matrix.md new file mode 100644 index 0000000..2182234 --- /dev/null +++ b/docs/evidence/hackathon-requirements-matrix.md @@ -0,0 +1,140 @@ +# CipherBid Official Hackathon Requirement Matrix + +**Status:** Official-source and evidence-routing baseline + +**Official-source snapshot verified:** 2026-08-27T10:12:47Z + +**Submission deadline:** **August 31, 2026 at 23:59 UTC**.[4][5] + +This document maps every official Private Sprint rule to a CipherBid implementation control and a concrete evidence destination. It is a control document, not deployment proof: a path marked **planned** must remain unpopulated until the corresponding public fact has been independently read back. + +## Authority and interpretation + +The sprint hub defines the public-repository application flow and the four judging weights.[1] + +The official hackathon repository defines registration, eligibility, `strk20.json`, mainnet, demo, deadline, payout, and judging rules.[2][4] + +The contribution guidance adds the operational checks for transaction existence, success, live-pool contact, project-contract events, public links, accurate privacy claims, secrets, and README coverage.[3][5] + +Where this matrix is stricter than the official minimum, the row is labeled **CipherBid control**. In particular, CipherBid requires each applicable listed transaction to include a decoded auction-house event and an expected state-transition readback. + +## Current gate snapshot + +| Gate | Current evidence | Status | +| --- | --- | --- | +| Registration | [Application PR #178](https://github.com/starkience/strk20-hackathon/pull/178), applied upstream commit [`6895886`](https://github.com/starkience/strk20-hackathon/commit/6895886bd5e31e202306bcff1c2203e2f6369d08), and metadata correction [PR #218](https://github.com/starkience/strk20-hackathon/pull/218), applied as [`18259c8`](https://github.com/starkience/strk20-hackathon/commit/18259c8dfa0408e06226df51c64bc1a9dd75ff54). | Satisfied | +| Public hub entry | [Private Sprint hub](https://strk20.starknet.io/hackathon) renders `SourceSenseiTheRealOne / cipherbid`; the official project index records category `DeFi` and `inspired_by: RFP-08`. | Satisfied | +| Public repository | [SourceSenseiTheRealOne/cipherbid](https://github.com/SourceSenseiTheRealOne/cipherbid) is publicly reachable without authentication. | Satisfied | +| Open-source license | [`LICENSE`](../../LICENSE) is the MIT License and GitHub detects SPDX `MIT`. | Satisfied | +| Mainnet contracts, demo, video, and transactions | Root [`strk20.json`](../../strk20.json) intentionally contains empty values until real public evidence is verified. | Pending | + +## Evidence destination catalog + +| Evidence ID | Canonical destination | What may be recorded | +| --- | --- | --- | +| `E-REG` | Official registry row, PR #178, PR #218, applied upstream commits, and public hub row | Public registration facts only. | +| `E-REPO` | Public repository, [`LICENSE`](../../LICENSE), [`README.md`](../../README.md), [`THIRD_PARTY_NOTICES.md`](../../THIRD_PARTY_NOTICES.md) | Public/open-source identity, setup, architecture, privacy boundary, and attribution. | +| `E-DEPLOY` | Root [`strk20.json`](../../strk20.json) `contracts`, [`mainnet/deployment.md`](mainnet/deployment.md), and [`mainnet/deployment.json`](mainnet/deployment.json) | Exact network, deployed address, class hash, constructor/config readback, source/artifact identity, and explorer URLs. No signer material. | +| `E-TX` | Root [`strk20.json`](../../strk20.json) `transactions` plus planned reviewed summary `docs/evidence/mainnet/transactions.md` | Hash, explorer URL, final receipt status, live pool contact, decoded CipherBid event, expected state delta, and verification time. No raw wallet/session dumps. | +| `E-LIFECYCLE` | [`docs/evidence/task-0-demo-matrix.md`](task-0-demo-matrix.md) plus planned reviewed summary `docs/evidence/mainnet/auction-lifecycle.md` | Seller creation/custody, two private ingresses, observer state, reveals, settlement, claims, NFT ownership, and value-conservation readbacks. | +| `E-DEMO` | Root [`strk20.json`](../../strk20.json) `demo_url`, GitHub Website field, and planned `docs/evidence/submission/link-checks.md` | Public URL, clean-browser result, HTTP/browser reachability, route checked, and verification time. | +| `E-VIDEO` | Root [`strk20.json`](../../strk20.json) `demo_video` and planned `docs/evidence/submission/link-checks.md` | Public three-minute video URL, duration check, clean-browser result, and verification time. | +| `E-DOCS` | [`README.md`](../../README.md), `context/`, contract/web setup instructions, threat model, limitations, and license | Judge-facing documentation that can be followed and built upon. | +| `E-GATES` | Planned reviewed summary `docs/evidence/release-gates.md` | Exact final commit, required test/lint/type/build/security gate results, secret-scan result, and `git diff --check`. No generated runtime payloads. | +| `E-PAYOUT` | Organizer communication record kept outside the public repository | One public payout address for the team. Never a seed phrase, private key, wallet export, or session. | + +Planned evidence summaries are reviewed, secret-free indexes of public facts. Raw browser state, wallet state, recovery payloads, sessions, generated reports, and unredacted runtime captures are not evidence destinations and must not be committed. + +## Official requirement-to-evidence matrix + +| ID | Authority | Official requirement | CipherBid implementation/control | Evidence destination | Acceptance gate | Status | +| --- | --- | --- | --- | --- | --- | --- | +| `REG-01` | Official | Fork the hackathon repository, append one project object to `registry.json`, and open the application PR.[4][5] | CipherBid used the public fork and application PR #178. | `E-REG` | Upstream registry contains exactly one CipherBid row and the application check was successful. | Satisfied | +| `REG-02` | Official | The registration must provide a public GitHub `repo_url` with at least one commit.[4][5] | Registered URL is `https://github.com/SourceSenseiTheRealOne/cipherbid`. | `E-REG`, `E-REPO` | URL resolves publicly and the hub indexes the repository. | Satisfied | +| `REG-03` | Official | `telegram` must contain one bare username per team member, without `@` or `t.me`.[4][5] | Registration uses `sourcesensei`. | `E-REG` | Upstream row contains the exact bare handle. | Satisfied | +| `REG-04` | Official | The application is the only required PR; subsequent progress, stack, contracts, demo, and builders are read from the project repository on the hub refresh cycle.[1][4][5] | No progress or final-submission PR will be opened. PR #218 was a registration-metadata correction, not a progress submission. | `E-REG`, project repository history, hub row | Final evidence is published in CipherBid itself and appears after index refresh. | Control active | +| `REG-05` | Official | Registration can happen before deployment and remains open throughout the sprint.[4][5] | Registration was completed before deployment; verified mainnet contract fields were added only after public readback. | `E-REG`, root `strk20.json` | Published deployment claims match verified chain state. | Satisfied | +| `ELIG-01` | Official | Individuals and teams, and both new and existing projects, are eligible.[4] | CipherBid is entered as an individual open-source project. | `E-REG`, repository contributor history | Registration identifies the builder and public project. | Satisfied | +| `ELIG-02` | Official | Ideas are non-exclusive; using or varying an RFP does not reserve it.[4] | CipherBid declares `inspired_by: RFP-08` without exclusivity claims. | `E-REG`, `README.md` | Hub metadata and product description stay accurate. | Satisfied | +| `REPO-01` | Official | The repository must be public, open-source, and licensed.[4][5] | CipherBid is public and carries the standard MIT License. | `E-REPO` | Unauthenticated repository fetch succeeds and GitHub detects SPDX `MIT`. | Satisfied | +| `REPO-02` | Official | The repository, demo, and every linked artifact must resolve for a visitor who is not logged in.[5] | All submission URLs receive clean-browser checks before closure. | `E-REPO`, `E-DEMO`, `E-VIDEO`, planned link-check summary | Each final URL opens without authentication or private-network access. | Pending final link checks | +| `SEC-01` | Official | Never commit real private keys; committed keys, addresses, and endpoints must use placeholders where they are not public evidence.[5] | `.gitignore`, local exclusions, secret scanning, wallet-memory boundaries, and human review prevent secret-bearing artifacts from entering Git. Public verified contract addresses/hashes are allowed evidence; signer material is never allowed. | `E-GATES`, repository history | Final secret scan reports no committed credential or private session material. | Control active | +| `ACCURACY-01` | Official | Product claims must accurately describe what is and is not private; overclaiming harms integration-depth scoring.[5] | Preserve equal public cap, sealed bid until reveal, public timing/count/helper/reveal/settlement/claim boundaries, and post-close linkability disclosures. | `README.md`, task-0 demo matrix, demo narration, `E-DOCS` | Security review and demo script contain no stronger privacy claim than implemented behavior. | Control active | +| `MAINNET-01` | Official | A winning product must actually run on Starknet mainnet against the live STRK20 pool for a real user.[1][4][5] | Deploy the reviewed auction house, run the browser-wallet two-bidder lifecycle, and read back every state transition against the official mainnet pool. | `E-DEPLOY`, `E-LIFECYCLE`, `E-TX`, `E-DEMO` | Exact final artifact/config identity and complete successful mainnet lifecycle are proven. | Pending | +| `SUB-01` | Official | The repository state at **August 31, 2026 at 23:59 UTC** is the submission; there is no second submission PR.[4][5] | Freeze and verify the exact final commit before the deadline, then let the hub index the repository. | `E-GATES`, repository commit URL, hub row | Final commit and all required public fields exist before the deadline. | Pending | +| `SUB-02` | Official | A live public demo that anyone can open is required to be scored.[4][5] | Publish the production demo without login and verify it in a clean browser. | `E-DEMO`, root `strk20.json` `demo_url` when explicit | Clean-browser route and public URL both succeed. | Pending | +| `SUB-03` | Official | A public three-minute demo video is required to be scored.[1][4][5] | Record the canonical seller/two-bidder/observer lifecycle and keep the published cut at no more than three minutes. | `E-VIDEO`, root `strk20.json` `demo_video` | URL is public, playable, and duration-verified. | Pending | +| `SUB-04` | Official | Root `strk20.json` must list at least three verified Starknet mainnet transaction hashes.[4][5] | Reserve the minimum slots for Bidder A ingress, Bidder B ingress, and at least one STRK20 claim; add hashes only after all transaction gates pass. | `E-TX`, root `strk20.json` | Array contains at least three unique, verified mainnet hashes. | Pending | +| `TX-01` | Official | Every listed hash must exist on Starknet mainnet.[4][5] | Query an independent RPC/explorer and bind the result to the exact hash/network. | `E-TX` | Mainnet receipt lookup returns the exact candidate hash. | Pending | +| `TX-02` | Official | Every listed transaction must have succeeded.[4][5] | Require final accepted/succeeded execution; timeout remains unconfirmed and revert is rejected. | `E-TX` | Final receipt is successful, not merely submitted or pending. | Pending | +| `TX-03` | Official | Every listed transaction must have touched the live STRK20 pool.[4][5] | Decode the trace/receipt and prove interaction with the official mainnet pool address. | `E-TX` | Exact configured live-pool address appears in the transaction execution path. | Pending | +| `TX-04` | Official | If project contracts are listed, each qualifying transaction must carry an event from one of those contracts; touching the pool only through someone else's contract is insufficient.[5] | List the deployed CipherBid auction house and decode its ABI-frozen event from every applicable candidate transaction. | `E-DEPLOY`, `E-TX` | Receipt includes a matching event whose emitter is the listed CipherBid auction-house address. | Pending | +| `TX-05` | CipherBid control | Applicable listed transactions must emit the lifecycle-specific CipherBid auction-house event and produce the expected state delta. | Both private ingresses must emit the accepted-bid event; the qualifying claim must emit its claim event. Exact event names/selectors are frozen with the final ABI rather than invented here. | `E-TX`, `E-LIFECYCLE` | Emitter, selector, decoded fields, auction ID, and post-state all agree with the expected transition. | Pending | +| `CONTRACT-01` | Official | `contracts` is optional, but listed deployed addresses are detected and shown with their network.[4][5] | List the verified AuctionHouse and DemoERC721 only after class/config/source identity readback. | `E-DEPLOY`, root `strk20.json` `contracts` | Both addresses exist on mainnet and match the reviewed artifacts/configuration. | Implemented; pending publication | +| `DEMO-01` | Official | `demo_url` is optional only when the hub discovers the demo automatically; discovery preference is explicit `strk20.json`, GitHub Pages, repository Website, then latest successful deployment.[4][5] | Set the repository Website and also set `demo_url` for deterministic discovery before closure. | `E-DEMO` | Hub row links to the intended public production demo. | Pending | +| `PAYOUT-01` | Official | A winning team must provide one payout address.[4] | Designate one public payout address through organizer communication only after operator review. | `E-PAYOUT` | Exactly one address is supplied; no signing or recovery material is disclosed. | Pending organizer request | +| `DOC-01` | Official | README coverage should explain what the project does, why privacy is needed, how to run it locally, and the mainnet contract addresses.[5] | The root README documents architecture, exact STRK20 integration, browser-wallet flow, setup, deployment, demo, threat model, privacy boundary, and limitations. | `E-DOCS`, `E-DEPLOY` | A clean checkout can follow setup/build instructions and find verified mainnet addresses. | Implemented; pending publication | +| `LINK-01` | Official | Every public URL must be link-checked before submission.[5] | Validate repository, demo, video, explorer, contract, transaction, and documentation links from a clean unauthenticated browser/session. | `E-DEMO`, `E-VIDEO`, planned link-check summary | All required URLs return the intended public resource. | Pending | +| `INDEX-01` | Official | The hub automatically shows missing demo, video, and mainnet requirements.[4][5] | Treat hub requirements as a final independent readback, not as the source of transaction truth. | Public hub row plus `E-DEMO`, `E-VIDEO`, `E-TX` | Hub reports demo, video, and mainnet requirements satisfied after final refresh. | Pending | + +## Scoring matrix + +| Score ID | Weight | Official criterion | CipherBid scoring implementation | Evidence destination | Closure test | +| --- | ---: | --- | --- | --- | --- | +| `SCORE-STRK20` | **30%** | STRK20 integration depth: shielded balances, private transfers, anonymizer contracts, SDK use, and stealth-account techniques are named examples.[1][4] | Real equal-cap collateral enters through the live STRK20 pool and CipherBid `privacy_invoke`; claims return through the reviewed STRK20 route; wallet performs proof/submission while the app keeps wallet private material out of scope. | `E-TX`, `E-LIFECYCLE`, `E-DOCS`, cross-layer action/ABI tests | At least two bid ingresses and one claim prove live-pool use through the listed auction house. | +| `SCORE-MAINNET` | **30%** | Working mainnet product: it must run on mainnet for a real user.[1][4] | Public UI, supported wallets, deployed Cairo auction house, NFT custody, two bids, reveals, settlement, claims, and readback-confirmed outcomes. | `E-DEPLOY`, `E-LIFECYCLE`, `E-DEMO`, `E-TX` | Complete canonical lifecycle succeeds from clean browser sessions and on-chain state agrees. | +| `SCORE-INNOVATION` | **25%** | Innovation rewards something the ecosystem lacks or a better version of an existing idea.[1][4] | Equalized real collateral prevents pre-reveal variable-amount leakage while preserving funded Vickrey settlement and atomic ERC-721 delivery. | `README.md`, contract tests, demo narration, `E-LIFECYCLE` | Demo and implementation prove the differentiator without privacy overclaiming. | +| `SCORE-DOCS` | **15%** | Documentation/open-source quality covers a followable README, buildable code, and a license.[1][4][5] | MIT licensing, reproducible setup, architecture, STRK20 wire contract, threat model, limitations, verified addresses, and exact quality gates. | `E-REPO`, `E-DOCS`, `E-GATES` | Clean checkout builds/tests from documented commands and all links resolve. | + +If another team depends on published CipherBid work, that may count in CipherBid's favour, but it is a scoring bonus rather than a submission prerequisite.[4] + +## Minimum transaction acceptance matrix + +| Slot | Intended transaction | Must touch live STRK20 pool | Required CipherBid event | Required readback | Destination | Status | +| --- | --- | --- | --- | --- | --- | --- | +| `TX-A` | Bidder A private equal-cap ingress | Yes | ABI-frozen accepted-bid event from the listed auction house | Receipt success, pool path, event decode, bid-count/commitment state | `E-TX`, `E-LIFECYCLE`, `strk20.json` | Pending | +| `TX-B` | Bidder B private equal-cap ingress | Yes | ABI-frozen accepted-bid event from the listed auction house | Receipt success, pool path, event decode, bid-count/commitment state | `E-TX`, `E-LIFECYCLE`, `strk20.json` | Pending | +| `TX-C` | At least one verified STRK20 claim | Yes | ABI-frozen claim event from the listed auction house | Receipt success, pool path, event decode, consumed-claim/accounting state | `E-TX`, `E-LIFECYCLE`, `strk20.json` | Pending | +| `TX-D+` | Additional refund, surplus, or seller claims | Yes when listed for STRK20 scoring | Matching ABI-frozen claim event | Receipt success and claim/value-conservation readback | `E-TX`, `E-LIFECYCLE`; optional `strk20.json` entries | Optional | + +Reveal and settlement receipts remain mandatory lifecycle evidence even if they do not touch the pool and therefore are not used among the three qualifying `strk20.json` hashes. + +## Evidence population rules + +1. Never put a transaction hash into `strk20.json` merely because a wallet returned it. +2. For every candidate, verify exact mainnet network, hash existence, final success, live-pool contact, expected CipherBid event when applicable, and the expected state transition. +3. Treat receipt timeout as **unconfirmed**, not success and not failure. +4. Bind decoded events to the exact listed CipherBid contract address and final ABI/class identity. +5. Record public explorer/RPC facts in reviewed summaries; do not commit raw browser state, wallets, sessions, private notes, recovery payloads, credentials, or generated runtime evidence. +6. Keep `strk20.json` empty until evidence is real, public, independently read back, and safe to publish. +7. Re-run clean-browser URL checks and all closure gates against the exact final commit before the deadline. + +## Final submission gate + +CipherBid is submission-ready only when all rows below are true simultaneously: + +- [x] Registered in the official registry and visible on the public hub. +- [x] Public repository and valid MIT open-source license. +- [ ] Public demo opens without login. +- [ ] Public demo video exists and is no more than three minutes. +- [ ] Root `strk20.json` contains at least three unique verified mainnet hashes. +- [ ] Every listed hash exists, succeeded, and touched the live STRK20 pool. +- [ ] Every applicable listed hash carries the expected event from the listed CipherBid auction house. +- [ ] Deployed contract/source/class/config identity is read back on mainnet. +- [ ] Canonical seller/two-bidder/observer lifecycle and value conservation are independently verified. +- [ ] README, setup, threat model, privacy limitations, demo, and mainnet addresses are complete. +- [ ] All public links and final quality/security gates pass on the exact final commit. +- [ ] The hub reports demo, video, and mainnet requirements satisfied after refresh. +- [ ] All required evidence is present before **August 31, 2026 at 23:59 UTC**. + +## Program facts that are not implementation gates + +The sprint runs from August 14 through August 31, 2026; winners are announced September 4. The published prize split is $2,500 for first, $1,500 for second, and $1,000 for third.[4] + +## Sources + +[1] https://strk20.starknet.io/hackathon — Private Sprint — STRK20 +[2] https://github.com/starkience/strk20-hackathon — starkience/strk20-hackathon +[3] https://github.com/starkience/strk20-hackathon/blob/main/CONTRIBUTING.md — Private Sprint contribution guidance +[4] https://raw.githubusercontent.com/starkience/strk20-hackathon/main/README.md — Private Sprint README (raw) +[5] https://raw.githubusercontent.com/starkience/strk20-hackathon/main/CONTRIBUTING.md — Private Sprint contribution guidance (raw) diff --git a/docs/evidence/mainnet/demo-script.md b/docs/evidence/mainnet/demo-script.md new file mode 100644 index 0000000..d9df1c7 --- /dev/null +++ b/docs/evidence/mainnet/demo-script.md @@ -0,0 +1,164 @@ +# CipherBid mainnet demo script + +**Target duration:** 2:45–2:55 +**Recording status:** Not ready. Record only after the complete mainnet lifecycle and every receipt/state readback pass. + +## Hard recording gate + +Do not record or publish this demo unless all of the following exist as independently verified public evidence: + +- one created mainnet auction bound to AuctionHouse `0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e`; +- DemoERC721 `0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f`, token `99`, held by the AuctionHouse before settlement; +- two successful pool-touching private ingresses from separate Ready X accounts; +- public bid count `2` without pre-reveal bid amounts; +- successful `2 STRK` and `3 STRK` reveals from encrypted recovery; +- successful settlement with Bidder B winning at `2 STRK`; +- token `99` owned by Bidder B's chosen recipient after settlement; +- successful applicable refund/surplus/proceeds claims and value-conservation readback; +- explorer links and Atomic Delivery Receipt rendered from those real records. + +If any item is missing, stop. Do not use mock hashes, edited balances, rehearsed wallet popups, or prefilled result cards. + +## Capture rules + +- Record a clean browser profile with Ready X already installed and unlocked. +- Never show a seed phrase, private key, viewing key, password, recovery plaintext, private balance, note list, proof witness, or browser extension settings. +- Crop wallet prompts to the public target/action/amount confirmation only when safe. +- Keep explorer and receipt links readable, but do not dwell on long hashes. +- Use the deployed mainnet product, not localhost or the Sepolia rehearsal. +- Keep cuts chronological. Never imply a later readback happened before chain acceptance. + +## 0:00–0:15 — Hook and product truth + +**Screen:** CipherBid home, then the verified AuctionHouse address. + +**Narration:** + +> CipherBid is a private-bid Vickrey auction on Starknet. Every bidder escrows the same public STRK cap, the actual bid stays sealed until reveal, and the NFT is delivered atomically onchain. + +**Visible evidence:** + +- “Private bids. Guaranteed onchain delivery.” +- Starknet mainnet +- verified AuctionHouse explorer link + +## 0:15–0:35 — Why equal collateral + +**Screen:** Auction terms and privacy boundary. + +**Narration:** + +> A variable amount leaving a privacy pool would reveal the bid. CipherBid instead locks the same four-STRK cap for everyone. Observers can verify every bid is funded without learning whether it is two or three STRK before reveal. + +**Visible evidence:** + +- reserve: `1 STRK` +- collateral cap: `4 STRK` +- bidder limit: `2` +- no private balance or viewing-key UI + +## 0:35–0:55 — NFT custody and auction creation + +**Screen:** Seller creation result, then `owner_of(99)` / live auction page. + +**Narration:** + +> The seller creates one short auction. Approval and creation are atomic, and token ninety-nine moves into CipherBid custody before bidding begins. + +**Visible evidence:** + +- real auction ID and deadlines +- DemoERC721 token `99` +- AuctionHouse owns the NFT +- accepted creation transaction + +## 0:55–1:25 — Two private bids + +**Screen:** Bidder A result, switch account, Bidder B result. Keep credential inputs and recovery plaintext out of frame. + +**Narration:** + +> Bidder A privately commits two STRK. Bidder B, from a separate Ready X account, privately commits three. Ready X owns note discovery, proving, signing, and submission; CipherBid never receives a viewing key. + +**Visible evidence:** + +- Bidder A accepted ingress receipt +- Bidder B accepted ingress receipt +- identical `4 STRK` collateral edge for both +- public bid count changes from zero to two + +## 1:25–1:45 — Observer view before reveal + +**Screen:** Read-only auction page and public receipt/event view. + +**Narration:** + +> Before close, the chain shows two funded commitments and equal collateral. It does not reveal either bid amount or private-note ownership. + +**Visible evidence:** + +- bid count `2` +- commitments present +- actual bid values absent from pre-reveal state + +## 1:45–2:10 — Recovery-bound reveal + +**Screen:** Import-verified encrypted recovery flow, then public reveal events. Do not show password or file contents. + +**Narration:** + +> After bidding closes, each bidder imports the encrypted recovery bound to this chain, contract, and auction. The valid reveals publish two and three STRK exactly once. + +**Visible evidence:** + +- network/deployment-bound import success +- Bidder A reveal: `2 STRK` +- Bidder B reveal: `3 STRK` +- accepted reveal receipts + +## 2:10–2:35 — Vickrey settlement and atomic delivery + +**Screen:** Settlement result and Atomic Delivery Receipt. + +**Narration:** + +> Bidder B wins but pays the second price: two STRK. Settlement transfers token ninety-nine to the winner's chosen recipient in the same onchain transition. + +**Visible evidence:** + +- winner: Bidder B +- clearing price: `2 STRK` +- NFT owner changed from AuctionHouse to winner recipient +- accepted settlement receipt + +## 2:35–2:50 — Claims and conservation + +**Screen:** Claim receipts and conservation summary. + +**Narration:** + +> The loser receives four STRK back, the winner can claim the two-STRK surplus, and the seller receives two STRK where current pool fees make the claim economical. Every applicable claim is one-time and read back from chain state. + +**Visible evidence:** + +- loser refund: `4 STRK` +- winner surplus: `2 STRK` +- seller proceeds: `2 STRK`, or an explicit fee-based deferral if uneconomical +- no unexpected collateral remains + +## 2:50–2:58 — Close + +**Screen:** Product home plus public explorer and repository links. + +**Narration:** + +> Private bids, funded execution, and guaranteed onchain delivery. CipherBid is open source, live on Starknet mainnet, and built on STRK20. + +## Final edit checklist + +- total runtime is at most `3:00`; +- every displayed hash exists in the public lifecycle evidence; +- the video URL is public and playable without login; +- no secret or private-wallet state appears in any frame, tooltip, download shelf, address bar, or browser history suggestion; +- captions use “sealed until reveal,” never “permanently hidden”; +- `strk20.json.demo_video` is populated only after public playback and duration verification. diff --git a/docs/evidence/mainnet/deployment.json b/docs/evidence/mainnet/deployment.json new file mode 100644 index 0000000..d539744 --- /dev/null +++ b/docs/evidence/mainnet/deployment.json @@ -0,0 +1,62 @@ +{ + "schema": "cipherbid.mainnet-deployment.v1", + "capturedAt": "2026-08-29T09:28:12Z", + "updatedAt": "2026-08-29T09:35:05Z", + "network": "mainnet", + "chainId": "0x534e5f4d41494e", + "deployer": "0x01017404a72b0d5312d7f41e81e0a87b89387db78361bb4ce60b0e0a390d72aa", + "auctionHouse": "0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e", + "demoNft": "0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f", + "auctionHouseClassHash": "0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4", + "demoErc721ClassHash": "0x06c7cba5680595203f9327f5784130907bad1b808891122ad358c10b93136a41", + "strk20Pool": "0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a", + "paymentToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "maximumBudget": "150000000000000000000", + "totalActualFee": "40936745660631231056", + "totalAuthorizedCeiling": "79974516584897918577", + "totalTransferredValue": "50000000000000000000", + "remainingBudgetCeiling": "20025483415102081423", + "declarationTransactions": [ + { + "contractName": "AuctionHouse", + "classHash": "0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4", + "transactionHash": "0x552781e5ecb2ab9826474c8395ca5fd2f534ce6155367ab67ae195f5e2c9dc6", + "blockNumber": 14038906 + }, + { + "contractName": "DemoERC721", + "classHash": "0x06c7cba5680595203f9327f5784130907bad1b808891122ad358c10b93136a41", + "transactionHash": "0x01efc7df78014f252d346af3b88a5035b002cf005079604f6e3bf9df0f1fa9b", + "blockNumber": 14039038 + } + ], + "deploymentTransactionHash": "0x03732f01800aa06e569e88a78232c3e7396546e314850a1b54dfae05a68a64b4", + "deploymentBlockNumber": 14039088, + "demoNftDeploymentTransactionHash": "0x029ce413d931197cd911b33cdf21f00d4c599bde29d7822e563ea89c55330a4c", + "demoNftDeploymentBlockNumber": 14039156, + "topUpTransactions": [ + { + "role": "Bidder A", + "recipient": "0x00289637e6debed46ce1a64ea30a9f1fa492458bac580c908f940f225fd11a8e", + "amount": "25000000000000000000", + "transactionHash": "0x18d06d74ec6b8888a9ee55ee0b750859c56fd3842d67e49ffe947b0ea2939ae", + "blockNumber": 14039611 + }, + { + "role": "Bidder B", + "recipient": "0x057791bafe2653e8a62509261aeba6a9d09f1fe09f039c9ff0c09c00c24b1f1a", + "amount": "25000000000000000000", + "transactionHash": "0x54ee14acf35d4bd2922a44e7ec76b98a6420d8cca6ac6b3773366ffb345953b", + "blockNumber": 14039623 + } + ], + "houseConfig": { + "strk20Pool": "0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a", + "paymentToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "maximumBidders": 32 + }, + "demoNftTokenId": "99", + "demoNftOwner": "0x01017404a72b0d5312d7f41e81e0a87b89387db78361bb4ce60b0e0a390d72aa", + "explorerUrl": "https://voyager.online/contract/0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e", + "demoNftExplorerUrl": "https://voyager.online/contract/0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f" +} diff --git a/docs/evidence/mainnet/deployment.md b/docs/evidence/mainnet/deployment.md new file mode 100644 index 0000000..b1f8c30 --- /dev/null +++ b/docs/evidence/mainnet/deployment.md @@ -0,0 +1,55 @@ +# CipherBid mainnet deployment evidence + +Captured at `2026-08-29T09:28:12Z` from Starknet mainnet public RPC readback. This page contains public contract and transaction data only. + +## Deployments + +| Contract | Address | Class hash | Deployment transaction | Accepted block | +| ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------: | +| AuctionHouse | [`0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e`](https://voyager.online/contract/0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e) | `0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4` | [`0x03732f01800aa06e569e88a78232c3e7396546e314850a1b54dfae05a68a64b4`](https://voyager.online/tx/0x03732f01800aa06e569e88a78232c3e7396546e314850a1b54dfae05a68a64b4) | 14,039,088 | +| DemoERC721 | [`0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f`](https://voyager.online/contract/0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f) | `0x06c7cba5680595203f9327f5784130907bad1b808891122ad358c10b93136a41` | [`0x029ce413d931197cd911b33cdf21f00d4c599bde29d7822e563ea89c55330a4c`](https://voyager.online/tx/0x029ce413d931197cd911b33cdf21f00d4c599bde29d7822e563ea89c55330a4c) | 14,039,156 | + +Both deployment receipts read back as `ACCEPTED_ON_L2` and `SUCCEEDED`. + +## Declarations + +| Contract | Declaration transaction | Accepted block | Actual fee | +| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------------: | ---------------------------: | +| AuctionHouse | [`0x552781e5ecb2ab9826474c8395ca5fd2f534ce6155367ab67ae195f5e2c9dc6`](https://voyager.online/tx/0x552781e5ecb2ab9826474c8395ca5fd2f534ce6155367ab67ae195f5e2c9dc6) | 14,038,906 | `33.144823441614033664 STRK` | +| DemoERC721 | [`0x01efc7df78014f252d346af3b88a5035b002cf005079604f6e3bf9df0f1fa9b`](https://voyager.online/tx/0x01efc7df78014f252d346af3b88a5035b002cf005079604f6e3bf9df0f1fa9b) | 14,039,038 | `7.370040394951547648 STRK` | + +Both declaration receipts read back as `ACCEPTED_ON_L2` and `SUCCEEDED`, and both class hashes are retrievable at `latest`. + +## Immutable readback + +AuctionHouse `get_house_config` returned: + +| Field | Value | +| --------------- | -------------------------------------------------------------------- | +| STRK20 pool | `0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a` | +| Payment token | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` | +| Maximum bidders | `32` | + +DemoERC721 readback returned: + +| Field | Value | +| ------------- | -------------------------------------------------------------------- | +| Token ID | `99` | +| Owner | `0x01017404a72b0d5312d7f41e81e0a87b89387db78361bb4ce60b0e0a390d72aa` | +| Owner balance | `1` | + +## Bounded execution accounting + +| Field | Value | +| --------------------------------------- | ----------------------------: | +| Frozen release ceiling | `150 STRK` | +| Total explicit authorized ceilings used | `79.374368698599459765 STRK` | +| Total actual fees paid | `40.704804389474492096 STRK` | +| Remaining authorized ceiling | `70.625631301400540235 STRK` | +| Deployer public balance at readback | `159.428234034473002848 STRK` | + +The first execution process lost its RPC connection after the AuctionHouse declaration was accepted. The declaration transaction was recovered by scanning recent public blocks for the exact sender and class hash, then its successful receipt, class availability, resource bounds, and fee were independently read back before later writes resumed. It was not replayed. + +## Privacy boundary + +No signer, viewing key, bid nonce, claim secret, recovery password, recovery payload, private note, proof witness, or wallet session is included in this evidence. The deployment transactions do not touch the STRK20 pool and therefore are not qualifying `strk20.json` lifecycle entries. diff --git a/docs/evidence/mainnet/release-candidate.md b/docs/evidence/mainnet/release-candidate.md new file mode 100644 index 0000000..5e441dd --- /dev/null +++ b/docs/evidence/mainnet/release-candidate.md @@ -0,0 +1,79 @@ +# CipherBid mainnet release candidate + +Initially re-frozen at `2026-08-29T02:47:23Z` before any CipherBid mainnet contract write. The user later approved a hackathon-only custody exception for the original Ready account. Its local key was independently matched to the account's onchain owner before execution. The verified deployment supersedes the pre-write account state below; see [`deployment.md`](./deployment.md). + +## Public identities + +| Role | Address | Live state | +| ----------------- | -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | +| Deployer / seller | `0x01017404a72b0d5312d7f41e81e0a87b89387db78361bb4ce60b0e0a390d72aa` | Ready v0.4.0 account; deployed; `109.428234034473002848 STRK` after deployment and bidder top-ups | +| Bidder A | `0x00289637e6debed46ce1a64ea30a9f1fa492458bac580c908f940f225fd11a8e` | Ready v0.4.0 account; registered with STRK20; `36.950996656593615344 STRK` after verified top-up | +| Bidder B | `0x057791bafe2653e8a62509261aeba6a9d09f1fe09f039c9ff0c09c00c24b1f1a` | Ready v0.4.0 account; registered with STRK20; `36.950870436658591056 STRK` after verified top-up | + +No signing key, viewing key, recovery payload, wallet session, private note, or proof material belongs in this repository or its evidence. + +## Network and protocol + +| Field | Frozen value | +| ----------------------- | -------------------------------------------------------------------- | +| Network | Starknet mainnet | +| Chain ID | `0x534e5f4d41494e` | +| RPC | `https://api.zan.top/public/starknet-mainnet/rpc/v0_10` | +| STRK token | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` | +| STRK20 pool | `0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a` | +| Live pool fee at freeze | `6 STRK` per private operation | +| Note maturity | At least 10 accepted blocks | + +The pool fee is governance-controlled. Runtime preflight must read it again; `6 STRK` is evidence for this freeze, not a permanent protocol constant. + +## Auction lifecycle + +| Field | Value | +| ------------------------ | ------------------: | +| Reserve | `1 STRK` | +| Equal collateral cap | `4 STRK` | +| Bidder limit | `2` | +| Bidder A bid | `2 STRK` | +| Bidder B bid | `3 STRK` | +| Expected winner | Bidder B | +| Expected clearing price | `2 STRK` | +| Expected loser refund | `4 STRK` | +| Expected winner surplus | `2 STRK` | +| Expected seller proceeds | `2 STRK` | +| Bidding window | `10 minutes` | +| Reveal window | `5 minutes` maximum | + +Both public ingresses transfer the same `4 STRK` collateral cap. The actual bid is sealed only until reveal; reveal data, settlement, deposits, withdrawals, timing, and open-note edges are public as described in the root documentation. + +## Funding and write ceiling + +At the observed `6 STRK` pool fee: + +- minimum bidder shield for deposit, bid, and one claim: `22 STRK`; +- bidder shield target with buffer: `24 STRK` each; +- seller shield target for deposit plus proceeds claim: `12 STRK`; +- total mainnet deployer spend ceiling for declarations, deployments, public top-ups, setup, and lifecycle fees: **`150 STRK`**. + +Every transaction is estimated before submission and must fail closed if its cumulative authorized ceiling would exceed `150 STRK`. Both `25 STRK` bidder top-ups succeeded; each Ready X account still needs to submit its `24 STRK` public deposit before the private lifecycle can start. The hackathon deployer signer remains isolated in a dedicated local `0600` sncast account file and is never printed or copied into the repository. + +## Contract artifacts + +| Contract | Frozen reviewed class hash | Mainnet status at freeze | +| ------------ | -------------------------------------------------------------------- | ------------------------ | +| AuctionHouse | `0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4` | Declared and deployed | +| DemoERC721 | `0x06c7cba5680595203f9327f5784130907bad1b808891122ad358c10b93136a41` | Declared and deployed | + +Mainnet deployment must read back the declared class hashes, constructor configuration, maximum bidder count, payment token, STRK20 pool, NFT custody, and transaction receipts before publishing any address or hash. + +## Approval and stop conditions + +The user explicitly authorized mainnet deployment and use of mainnet funds on `2026-08-28`, after asking that implementation and tests finish first. Mainnet writes remain ordered after the local release-candidate implementation and test suite. + +Stop before any write if: + +- the connected or local deployer differs from the frozen deployer; +- chain ID, pool, token, class hash, or account class differs; +- the live pool fee changes without recomputing shield targets; +- estimated cumulative spend exceeds `150 STRK`; +- either bidder is not registered or lacks mature private funds; +- a credential would enter source, logs, command history, or public evidence. diff --git a/docs/evidence/sepolia-feasibility.md b/docs/evidence/sepolia-feasibility.md index 0a4c444..1336415 100644 --- a/docs/evidence/sepolia-feasibility.md +++ b/docs/evidence/sepolia-feasibility.md @@ -4,7 +4,7 @@ Status captured: 2026-08-24T00:04:27+01:00 ## Result -The wallet connection, action-shape, canonical-cap, and contract feasibility boundaries are locally verified. A real STRK20 wallet preparation and transaction proof is **blocked**, not passed: Wallet API 0.10.3 does not expose an app-specific commitment-secret capability, while project policy forbids the application from receiving bid or claim secrets. The available in-app browser also has no Ready/privacy-capable Starknet wallet, and this checkout has no configured `sncast` Sepolia account or signing material. No transaction hash or receipt is recorded below because no Sepolia write occurred. +The wallet connection, action-shape, canonical-cap, and contract feasibility boundaries are locally verified. A real STRK20 wallet preparation and transaction proof is **blocked**, not passed: the production auction house, recovery flow, transaction orchestrator, supported-wallet browser session, and reviewed Sepolia deployment do not yet exist together. Decision 0002 permits app-specific bidder and seller claim credentials only in active browser memory plus mandatory password-encrypted recovery; wallet keys, viewing keys, notes, proofs, and submission remain inside the wallet. No transaction hash or receipt is recorded below because no Sepolia write occurred. ## Read-only live-network evidence @@ -25,43 +25,42 @@ The browser client: 2. creates `WalletAccountV6` through the selected wallet; 3. requires Wallet API `>= 0.10.3` without probing shielded balances; 4. requires `SN_SEPOLIA`; -5. narrows application state to a public connected/not-connected signal and never exposes the WalletAccount to the page; -6. does not request, generate, store, or accept bid/claim secrets; -7. exposes neither `strk20PrepareInvoke` nor `strk20InvokeTransaction` in the product UI. +5. narrows global application state to public connection data; +6. may hold app-specific bidder/seller credentials only inside a future bounded active-memory session and verified encrypted recovery operation; +7. has not yet mounted the reviewed `strk20PrepareInvoke` / `strk20InvokeTransaction` orchestrator into the product UI. -The canonical-cap reader and exact action builders are isolated, unit-tested primitives for the future approved custody boundary. They are not mounted into the product UI. The app never requests or stores a viewing key. +The canonical-cap reader and exact action builders are isolated, unit-tested primitives for the approved direct Wallet API boundary. They are not yet mounted into the product UI. The app never requests or stores a viewing key, wallet key, private note, proof, or wallet session material. ## Public/private observation matrix -| Observation | Bid ingress | Reveal | -| --- | --- | --- | -| Auction helper address | Public | Public | -| Uniform collateral cap | Public contract configuration returned by `get_cap`; identical for every bidder using that spike | No value movement | -| Actual bid amount | Absent from ingress actions; Poseidon-sealed | Public in reveal calldata | -| Bidder-controlled account address | Absent from helper calldata/storage shape | Absent from helper calldata/storage shape | -| Relayed transaction sender | Not yet observed live | Not yet observed live | -| Viewing key | Never requested or received by the app | Never requested or received by the app | -| Transaction receipt/events | Not available: no signed write | Not available: no signed write | +| Observation | Bid ingress | Reveal | +| --------------------------------- | ------------------------------------------------------------------------------------------------ | ----------------------------------------- | +| Auction helper address | Public | Public | +| Uniform collateral cap | Public contract configuration returned by `get_cap`; identical for every bidder using that spike | No value movement | +| Actual bid amount | Absent from ingress actions; Poseidon-sealed | Public in reveal calldata | +| Bidder-controlled account address | Absent from helper calldata/storage shape | Absent from helper calldata/storage shape | +| Relayed transaction sender | Not yet observed live | Not yet observed live | +| Viewing key | Never requested or received by the app | Never requested or received by the app | +| Transaction receipt/events | Not available: no signed write | Not available: no signed write | ## Local executable evidence - Wallet/action/component tests cover exact Wallet API actions, placeholder preservation, capability detection, public-only state, and controlled errors. -- Cairo tests prove only the configured pool can call `privacy_invoke`, the non-zero canonical cap is publicly readable, bid ingress returns an empty open-note span, and invoke-only reveal returns no value. +- Cairo tests prove only the configured pool can call the local `privacy_invoke` spike, the non-zero canonical cap is publicly readable, and bid ingress returns an empty open-note span. Cross-layer lifecycle fixtures now freeze reveal as a standard wallet call and bidder claims as STRK20 open-note flows; production claim behavior remains unimplemented. - TypeScript and Cairo share frozen Poseidon vectors for claim handles and bid commitments. ## Remaining human/network gate The current `AuctionIngressSpike` is local-only and must not receive funds. To convert this record from blocked to passed safely: -1. obtain a reviewed wallet capability or separately approved isolated custody boundary that creates commitments without exposing bid/claim secrets to the application; -2. replace the local spike with the reviewed auction contract that implements authenticated bidder/seller claims and exact balance-delta accounting; -3. open the local app in a browser with Ready installed and unlocked; -4. connect a funded Sepolia account supporting the required capability; -5. deploy the refundable contract with the pool above and one non-zero uniform cap; -6. ensure the wallet has matured shielded Sepolia STRK covering the public cap and pool fee; -7. prepare and submit bid ingress, wait for a successful receipt, and verify helper state/token balance; -8. prepare and submit invoke-only reveal, wait for a successful receipt, and verify helper state; -9. execute and verify the authenticated refund/claim path so no collateral remains stranded; -10. record exact hashes, finality, events, observed public fields, and absence of bidder/viewing-key data. +1. finish and independently review the commitment, configuration, lifecycle, claim, and recovery contracts/fixtures; +2. replace the local spike with the reviewed auction house implementing ERC-721 custody, exact balance-delta accounting, and bidder/seller claims; +3. implement memory-only bidder/seller credential sessions and mandatory encrypted recovery round trips; +4. implement the Wallet API prepare/submit orchestrator with receipt and state readback; +5. deploy the reviewed artifact and a synthetic ERC-721 on Sepolia after explicit test-budget approval; +6. use two distinct supported privacy-wallet sessions for equal-cap bid ingress; +7. submit direct reveals and permissionless settlement through connected wallets; +8. execute loser, winner-surplus, and seller-proceeds STRK20 claims, then confirm final NFT ownership and zero unexpected collateral; +9. record exact hashes, finality, events, observed public fields, and absence of wallet private material or persistent credential plaintext. Until those steps are executed and read back, CipherBid must not claim live Sepolia STRK20 feasibility. diff --git a/docs/evidence/sepolia/demo-runbook.md b/docs/evidence/sepolia/demo-runbook.md new file mode 100644 index 0000000..8de0e17 --- /dev/null +++ b/docs/evidence/sepolia/demo-runbook.md @@ -0,0 +1,97 @@ +# CipherBid Sepolia demo runbook + +## One-time wallet preparation + +The seller is the local Sncast deployer. The active bidder candidates are Xverse Sepolia accounts being tested through the STRK20 Wallet API: + +| Role | Custody | Public address | +| --- | --- | --- | +| Seller | Sncast `cipherbid-sepolia-deployer` | `0x01ff477da49d13f1b48774d0fc2313358e3f358be741b4944b54fccb34f7f424` | +| Bidder A | Xverse | `0x054499e46751979eea7fcc64475836d1a5f591c2d12a7546e42e8516fdbabc4d` | +| Bidder B | Xverse | `0x014ecc190504847edc0b29f427404b2cad833ff8837277af69f4d3bf99d82b52` | + +Before activation or shielding, verify public deployment, funding, and registration state for both bidders. Never paste a private key into chat, source files, `.env`, screenshots, recordings, or issue text. + +Confirm Xverse shows the exact public addresses above before activation, shielding, signing, or connecting to CipherBid. + +## Create a fresh auction + +From `web/`: + +```bash +pnpm auction:create:sepolia -- --bidding-minutes 10 --reveal-minutes 5 +``` + +The script: + +1. creates a unique auction ID; +2. deploys a fresh one-token DemoERC721; +3. creates and import-verifies encrypted seller recovery; +4. sets reserve `2 STRK`, cap `5 STRK`, bidder limit `2`; +5. fixes bidding to 10 minutes and reveal to at most 5 minutes; +6. atomically approves NFT custody and creates the auction; +7. waits for acceptance and validates all config plus `owner_of`; +8. prints the local auction URL and public transaction hashes. + +Use `--bidding-minutes 20` if wallet setup has not already been completed. The script rejects reveal windows greater than five minutes. + +## Prepare private bidder funds + +For each Xverse bidder account: + +1. switch Xverse to Starknet Sepolia; +2. confirm its address exactly matches the table; +3. shield at least `15 STRK` through Xverse's STRK20 privacy flow; +4. wait at least 10 blocks after note creation before bidding; +5. retain enough private STRK for the pool fee and later claim. + +Do not use `sncast` to fake a private bid. An ordinary account invoke cannot replace STRK20 note discovery and proof generation. + +## Submit both sealed bids immediately + +Open the URL printed by the script. + +### Bidder A + +- connect Xverse Bidder A; +- bid `3 STRK`; +- keep the NFT recipient as bidder A unless intentionally demonstrating delivery to another address; +- choose a recovery password of at least 12 characters; +- download and confirm the encrypted bidder recovery bundle; +- approve the Xverse transaction; +- wait for `1/2 bids` readback. + +### Bidder B + +- disconnect/switch account; +- connect Xverse Bidder B; +- bid `4 STRK`; +- download and confirm a separate encrypted recovery bundle; +- approve the Xverse transaction; +- wait for `2/2 bids` readback. + +At this point the public auction should show two commitments and no bid amounts. + +## Reveal and settle + +1. Wait until the bidding deadline. +2. Import Bidder A's encrypted recovery bundle and reveal `3 STRK`. +3. Switch to Bidder B, import its bundle, and reveal `4 STRK`. +4. Complete both reveals within the five-minute reveal window. +5. After the reveal deadline, any connected account may settle. +6. Verify Bidder B wins, clearing price is `3 STRK`, and `owner_of(99)` is Bidder B's committed recipient. + +## Claims + +- Bidder A claims the full `5 STRK` loser refund through STRK20. +- Bidder B claims the `2 STRK` winner surplus through STRK20. +- Seller imports the seller recovery bundle printed by the CLI script, authorizes the resolved open-note ID, then claims `3 STRK` seller proceeds through STRK20. +- Verify all one-time claim flags and zero unexpected collateral. + +## Current tested auction + +The script's first successful live run created: + +http://localhost:4110/auctions/1787917793344 + +That run verified NFT custody and all immutable fields. It is rehearsal evidence, not a replacement for the fresh demo-day run. diff --git a/docs/evidence/sepolia/deployment-manifest.md b/docs/evidence/sepolia/deployment-manifest.md new file mode 100644 index 0000000..10e1906 --- /dev/null +++ b/docs/evidence/sepolia/deployment-manifest.md @@ -0,0 +1,99 @@ +# Sepolia deployment manifest + +## Status + +The CipherBid release candidate and one synthetic ERC-721 are declared, deployed, and read back on Starknet Sepolia. This document contains public chain evidence only. It contains no signer key, wallet private state, recovery bundle, bid nonce, or claim secret. + +The auction lifecycle itself is not complete yet. No auction, bid, reveal, settlement, or claim success is asserted here. + +## Network and immutable identities + +| Field | Verified value | +| --- | --- | +| Network | Starknet Sepolia | +| Chain ID | `0x534e5f5345504f4c4941` | +| RPC used for readback | `https://api.zan.top/public/starknet-sepolia/rpc/v0_10` | +| RPC spec observed | `0.10.3-rc.0` | +| Deployer | `0x01ff477da49d13f1b48774d0fc2313358e3f358be741b4944b54fccb34f7f424` | +| STRK20 pool | `0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91` | +| STRK token | `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` | +| AuctionHouse | `0x0705b1080174f2b10c02fd8b2e00b918e4dc91f9021ee6a208f53d5909fcc87d` | +| DemoERC721 | `0x011beadd9e02a7a633da6436bf342b407231c4fa4b77f2544e9866ba94f4d129` | +| Synthetic token ID | `99` | +| Synthetic token initial owner | deployer address above | + +## Release artifacts + +| Contract | Class hash | Release artifact SHA-256 | Canonical ABI SHA-256 | +| --- | --- | --- | --- | +| AuctionHouse | `0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4` | `aa713e69c211528f9fd891ba6bb13eb59728caf57425ce04c88191e7fd88942d` | `0170ddfbc3c10168010648c94b3f55a62dc9cd4726cc5ba3cab906e21ee38432` | +| DemoERC721 | `0x06c7cba5680595203f9327f5784130907bad1b808891122ad358c10b93136a41` | `3e593d7a555750156b41982ca3241c0fe5d080e22c9fcf6b90a31bcc45166313` | `3a5a341710e16420ce7862837fcb8e6cedc36ba3c197c150bf503b7b5e15d93f` | + +Artifacts were built from `contracts/` with Scarb `2.20.1` and Cairo compiler `2.20.0`. + +## Constructor manifests + +### AuctionHouse + +Ordered calldata: + +1. pool: `0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91` +2. payment token: `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d` +3. maximum bidders: `32` + +Deterministic salt: + +`0x4349504845524249445f41485f5345504f4c49415f5631` (`CIPHERBID_AH_SEPOLIA_V1`) + +### DemoERC721 + +Ordered calldata: + +1. owner: `0x01ff477da49d13f1b48774d0fc2313358e3f358be741b4944b54fccb34f7f424` +2. token ID low limb: `99` +3. token ID high limb: `0` + +Deterministic salt: + +`0x4349504845524249445f4e46545f5345504f4c49415f5631` (`CIPHERBID_NFT_SEPOLIA_V1`) + +## Accepted setup transactions + +| Action | Block | Actual fee | Transaction | +| --- | ---: | ---: | --- | +| Fund deployer | 14,176,454 | `0.052968564618065984 STRK` | [`0x35713067…a3a232`](https://sepolia.voyager.online/tx/0x035713067b8a560c1ec10c71856bf2d17da0c00f2ff853913d743caf27a3a232) | +| Deploy account | 14,176,491 | `0.078454504696804384 STRK` | [`0x074f2e1a…66c526`](https://sepolia.voyager.online/tx/0x074f2e1aee39d5ce58fc811545726b64d32939b4d8fc1d27357cfee45866c526) | +| Declare AuctionHouse | 14,176,557 | `31.513946606579319232 STRK` | [`0x02996c0e…92374`](https://sepolia.voyager.online/tx/0x02996c0ebba0768a92e2dfd53fd2dc72aebb632ef93f678294aad63f8af92374) | +| Deploy AuctionHouse | 14,177,403 | `0.093947231954548848 STRK` | [`0x03c34fde…bec01`](https://sepolia.voyager.online/tx/0x03c34fde6e99d1d0c69b3681676d78a75c665fd9441ff5df51b114ab2a2bec01) | +| Declare DemoERC721 | 14,177,683 | `7.186112307979671552 STRK` | [`0x014430dd…e5ea0`](https://sepolia.voyager.online/tx/0x014430dd2981171a9197ae4b5ebc44a8b6b948b0d31eed82f7eb55a3657e5ea0) | +| Deploy DemoERC721 | 14,177,725 | `0.083351808057320352 STRK` | [`0x04e7c93f…a218`](https://sepolia.voyager.online/tx/0x04e7c93f49afad7857e1c6313f383530ba8d1fb96a86d640d8ebe4d10c08a218) | + +The deployment setup budget excluded faucet funding. Its approved maximum was `53.10384 STRK`; accepted account/class/contract setup spent `38.955812459267664368 STRK`. + +## Independent readback + +After acceptance: + +- `starknet_getClassHashAt(AuctionHouse)` returned the exact reviewed AuctionHouse class hash. +- `get_house_config()` returned the canonical Sepolia STRK20 pool, canonical STRK token, and maximum bidder bound `32`. +- `starknet_getClassHashAt(DemoERC721)` returned the exact reviewed demo class hash. +- `owner_of(99)` returned the deployer. +- `balance_of(deployer)` returned `1`. +- Every listed transaction returned `ACCEPTED_ON_L2` and `SUCCEEDED`. + +## Excluded attempts + +Several DemoERC721 declaration submissions were returned by public RPCs but later disappeared without consuming nonce or declaring the class. They are not evidence and are intentionally excluded from the accepted transaction table. The successful declaration used explicit buffered resource bounds; estimate-tight `--max-fee` submissions were susceptible to silent mempool eviction while gas prices moved. + +## Abort conditions for lifecycle writes + +Stop before accepting or moving value if any condition holds: + +- chain ID, AuctionHouse class hash, pool, token, or maximum bidder bound differs; +- DemoERC721 class, token ID, or owner differs; +- wallet account/network changes after preparation; +- bid cap, reserve, recipient, deadlines, or commitment differs from displayed terms; +- a private transaction lacks the expected STRK20 pool and CipherBid events; +- a submitted hash is absent or timed out; timeout remains unconfirmed, never success; +- NFT custody, reveal, settlement, claim status, or accounting readback disagrees with the expected transition; +- any credential or wallet-private material would leave browser memory or the encrypted recovery bundle. diff --git a/docs/evidence/task-0-demo-matrix.md b/docs/evidence/task-0-demo-matrix.md new file mode 100644 index 0000000..3ee0e6f --- /dev/null +++ b/docs/evidence/task-0-demo-matrix.md @@ -0,0 +1,107 @@ +# CipherBid Task 0 — Canonical Two-Bidder Demo and Evidence Matrix + +**Status:** Frozen planning baseline. This file contains no deployment values, transaction hashes, wallet addresses, credentials, or product fixture data. + +## Exact demo roles + +| Role | Required participant | Demo responsibility | +| --- | --- | --- | +| Issuer / seller | One seller wallet | Creates the auction and escrows one low-value purpose-minted ERC-721. | +| Bidder A | Supported privacy-wallet session A | Connects through the CipherBid UI and submits one sealed bid `A`. | +| Bidder B | Separate supported privacy-wallet session B | Connects through the CipherBid UI and submits one sealed bid `B`. | +| Observer | Read-only RPC/explorer session | Inspects public state before close and verifies the final on-chain result. | + +The two bidder sessions must use different wallet accounts. A wallet cannot play both bidder roles in the canonical demo. + +## Deterministic auction case + +For one auction, set: + +```text +reserve = R +public collateral cap = C +bidder A bid = A +bidder B bid = B + +0 < R ≤ A < B ≤ C +``` + +Expected result after valid post-close reveals: + +```text +winner = Bidder B +clearing price = max(R, A) +Bidder A refund = C +Bidder B surplus claim = C - max(R, A) +seller proceeds = max(R, A) + forfeited collateral +``` + +The issuer chooses final small mainnet values only in the separately approved mainnet action manifest. This document intentionally does not prefill real amounts. + +## Exact lifecycle to demonstrate + +1. Issuer creates the auction and the auction house becomes owner of the ERC-721. +2. Bidder A connects a supported privacy wallet in the UI, completes recovery export verification, and submits one equal-cap STRK20 private bid. +3. Bidder B repeats the same UI flow from a separate wallet session with a higher sealed bid. +4. Before the bid deadline, the observer records the public auction surface. +5. After bidding closes, each bidder reveals through the UI. +6. Anyone settles after the reveal deadline. +7. The observer verifies the NFT owner, clearing price, and accounting state. +8. Bidder A, Bidder B, and the seller complete their applicable claims through the verified STRK20 open-note route. +9. One bidder optionally creates a recipient-scoped disclosure packet; an authorized verifier validates it. + +## Public observer assertion before close + +The observer may see: + +- auction terms, NFT, reserve, cap, and deadlines; +- bid count and timing; +- identical public collateral cap; +- bid commitment and pool/helper interaction. + +The observer must not be able to recover: + +- Bidder A or Bidder B's actual bid amount; +- a commitment opening or bid nonce; +- a bidder-controlled normal-wallet address from auction state/events. + +## Evidence ledger + +Populate a row only after independent chain readback. `Pending` means no fact is claimed yet. + +| Evidence item | Required readback | Evidence value | Status | +| --- | --- | --- | --- | +| Network and chain ID | RPC chain ID | Pending | Pending | +| STRK20 pool | Official pool address + chain read | Pending | Pending | +| Auction class hash | Class declaration/readback | Pending | Pending | +| Auction contract address | Deployment receipt + class/config readback | Pending | Pending | +| ERC-721 contract and token ID | Escrow receipt + `owner_of` readback | Pending | Pending | +| Auction creation | Receipt, event, and `get_auction` readback | Pending | Pending | +| Bidder A private ingress | Successful receipt, pool event, helper/auction readback, explorer URL | Pending | Pending | +| Bidder B private ingress | Successful receipt, pool event, helper/auction readback, explorer URL | Pending | Pending | +| Pre-close observer check | RPC/event query transcript and result | Pending | Pending | +| Bidder A reveal | Receipt/event and auction state readback | Pending | Pending | +| Bidder B reveal | Receipt/event and auction state readback | Pending | Pending | +| Settlement | Receipt/event, winner, price, and NFT-owner readback | Pending | Pending | +| Bidder A claim | Receipt, open-note output, and claim-state readback | Pending | Pending | +| Bidder B claim | Receipt, open-note output, and claim-state readback | Pending | Pending | +| Seller claim | Receipt, open-note output, and claim-state readback | Pending | Pending | +| Disclosure verification | Packet verifier result without secret content | Pending | Pending | +| Demo URL and video | Clean-browser/link check | Pending | Pending | + +## Product-language lock + +Use only these privacy claims: + +- Actual bid values are sealed until the reveal phase. +- Equal collateral, bid timing/count, helper interaction, reveals, winner, clearing price, and claim outputs can be public. +- CipherBid does not claim that variable bid amounts remain encrypted after leaving the STRK20 pool. +- Connected-wallet reveal/claim activity can be linkable after close. +- STRK20 protocol-auditor disclosure is protocol-level; CipherBid does not claim auction-scoped forced or threshold reveal. + +## Task 0 exit criteria + +- One seller, two separate bidder wallets, and one observer are assigned. +- The `0 < R ≤ A < B ≤ C` scenario is the only canonical mainnet demo case. +- Every required proof has a defined chain readback artifact. +- No deployment value, transaction hash, wallet address, secret, or unverified claim has been added to public documentation. diff --git a/docs/evidence/task-1-1-wallet-api-route.md b/docs/evidence/task-1-1-wallet-api-route.md new file mode 100644 index 0000000..30f7314 --- /dev/null +++ b/docs/evidence/task-1-1-wallet-api-route.md @@ -0,0 +1,221 @@ +# CipherBid Task 1.1 — Official Wallet API Route Verification + +**Status:** Verified route baseline + +**Verified:** 2026-08-27T10:30:59Z + +**Decision:** Use the direct Starknet Wallet API route through `WalletAccountV6`; keep Wallet API `>= 0.10.3` as the runtime minimum. + +## Decision summary + +- **Route:** CipherBid remains a user-facing dapp on top of a privacy-enabled wallet. The wallet owns viewing keys, note discovery, proving, signing, and submission; CipherBid describes actions and reads public chain state.[1][2] +- **Wallets:** Current Starknet.js documentation says Ready and Xverse support the STRK20 Wallet API as of August 2026.[3] Starknet's launch documentation independently confirms Ready and Xverse wallet privacy flows, including shielding and private swaps.[4] +- **Runtime authority:** A wallet name is never treated as capability proof. CipherBid must query `walletV6.supportedWalletApi(wallet)` and require at least one stable Wallet API version `>= 0.10.3` before enabling STRK20 actions.[5][15] +- **Least privilege:** Capability detection must not call `strk20Balances`; that is a private-balance data request, not a feature query.[6] +- **Exact CipherBid package set:** `starknet@10.4.0`, `@starknet-io/get-starknet-discovery@6.0.2`, `@starknet-io/get-starknet-wallet-standard@6.0.2`, and `@starknet-io/types-js@0.10.3`. +- **Pools:** Mainnet `0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a`; Sepolia `0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91`.[5][10] +- **Live fees:** On-chain `get_fee_amount()` readback returned **6 STRK on mainnet** and **2 STRK on Sepolia** during this verification. The fee is governance-controlled and must be read, never hardcoded.[8][16] +- **Maturity:** A newly created note may be discoverable immediately but is spendable only after **10 blocks**. CipherBid must expect shielded funds to mature before a later bid transaction.[9][11][12] + +## 1. Ready and Xverse support + +### Current source reconciliation + +| Source | Statement | Freshness | Decision | +| --- | --- | --- | --- | +| Starknet.js WalletAccount guide | Ready and Xverse support the STRK20 Wallet API as of 2026-08.[3] | Current `next` documentation fetched during this task | Authoritative current dapp-facing status. | +| Starknet launch article | Ready and Xverse expose shielding, private swaps, private transfers, and unshielding; both mobile and desktop-extension support are described.[4] | Published 2026-06-09 | Confirms shipped wallet privacy flows. | +| STRK20 agent integration skill | Ready was tested; Xverse dapp-facing Wallet API was still in progress.[5][6] | Last reverified 2026-07-29; explicitly says re-check | Stale on Xverse compared with the newer Starknet.js guide. | + +### Frozen wallet policy + +1. Ready and Xverse are the supported candidate wallets. +2. Ready remains the primary manually tested baseline because the integration skill's executable checklist targets Ready.[5][6] +3. Xverse is no longer classified as “in progress” in this baseline because the newer Starknet.js guide explicitly marks it supported.[3] +4. Neither wallet is trusted by brand string. The exact connected wallet instance must advertise a stable API version meeting the runtime floor. +5. Other wallets remain disabled unless the same capability query proves support; no hardcoded Ready/Xverse allowlist replaces capability detection. +6. Real extension testing for both current wallet builds remains a later browser gate. This task verifies the official route and runtime contract, not a funded wallet transaction. + +## 2. Exact package versions + +### Approved CipherBid pins + +| Package | Exact CipherBid pin | Verification basis | Decision | +| --- | --- | --- | --- | +| `starknet` | `10.4.0` | STRK20 and `WalletAccountV6` landed in 10.4.0.[1][14] The installed package resolves to 10.4.0. | Keep exact. | +| `@starknet-io/get-starknet-discovery` | `6.0.2` | Starknet.js documents get-starknet v6.0.2 as the minimum; the current official sprint starter kit pins 6.0.2.[3][7] | Keep exact. | +| `@starknet-io/get-starknet-wallet-standard` | `6.0.2` | Same v6.0.2 minimum and starter-kit tuple.[3][7] | Keep exact and aligned with discovery. | +| `@starknet-io/types-js` | `0.10.3` | Matches the stable Wallet API v0.10.3 surface and the starter kit.[7][13] | Keep exact. | +| `pnpm` | `10.18.1` | Repository package-manager pin. | Keep exact for reproducibility. | + +The STRK20 integration skill tested get-starknet `6.0.3` with the same `starknet@10.4.0` and `types-js@0.10.3` tuple.[5][6] + +Version `6.0.3` is therefore a supported upgrade option, but this verification does not introduce an unnecessary dependency change: the current CipherBid `6.0.2` pair is the documented minimum and exactly matches the current starter kit.[3][7] + +If the get-starknet pair is upgraded later, discovery and wallet-standard must move together and the real Ready/Xverse connection suite must be repeated. Do not mix `6.0.2` and `6.0.3` or float either package independently. + +### Registry status observed during verification + +| Package/tag | Live registry value | +| --- | --- | +| `starknet` `latest` | `10.0.2` — lacks the STRK20 API described by the official docs.[1] | +| `starknet` `next` | `10.7.1` | +| get-starknet discovery `next` | `6.0.4` | +| get-starknet wallet-standard `next` | `6.0.5` | +| `@starknet-io/types-js` `latest` | `0.10.3` | +| `@starknet-io/types-js` `beta` | `0.10.4-beta.2` | + +These moving tags are evidence for exact pins, not a reason to upgrade. The current development Wallet API specification advertises `0.10.4-rc.1`, while stable v0.10.3 and `types-js@0.10.3` remain the compatibility baseline.[13][15] + +## 3. `WalletAccountV6` connection behavior + +The current Starknet.js guide establishes the get-starknet v6 connection shape and states that the provider performs chain reads while the wallet signs and sends writes.[3] + +The installed `starknet@10.4.0` runtime resolves `WalletAccountV6.connect(provider, wallet, cairoVersion?, paymaster?, silentMode = false)` as follows: + +1. Run the Wallet Standard connection flow. +2. Read the connected account list. +3. Select the first account address. +4. Construct `WalletAccountV6` with the read provider, wallet provider, selected address, optional Cairo version, and optional paymaster. +5. Default to non-silent connection, so unlock/dapp-approval UI may be shown. + +CipherBid's current adapter maps this behavior at: + +- `web/src/features/wallet/browserWalletDependencies.ts:10-27` — `WalletAccountV6.connect`, `requestAccounts`, permissions, chain ID, and `supportedWalletApi` delegates; +- `web/src/features/wallet/walletConnection.ts:30-57` — explicit account/permission checks, normalized address, chain ID, capability versions, and result; +- `web/src/features/wallet/WalletConnectPanel.tsx:97-128` — network/API gates and session invalidation; +- `web/src/features/wallet/browserWalletDependencies.ts:30-35` — account, chain, or feature change subscription. + +Connection acceptance contract: + +- select a discovered get-starknet v6 wallet object; +- construct `WalletAccountV6` with the intended network provider; +- require a non-empty account and the `accounts` permission; +- read the wallet's current chain ID; +- query Wallet API capability separately; +- normalize the account address; +- rebuild/invalidate the connection when account, chain, or advertised features change. + +`WalletAccountV6.connect` already obtains the active account through Wallet Standard. CipherBid currently performs an additional explicit `wallet_requestAccounts` read after connection. That is acceptable only if Ready and Xverse return the already-approved account without a second approval prompt; the later real-browser gate must verify one coherent connection UX. If either wallet re-prompts, remove the duplicate request and source the selected address from the connected `WalletAccountV6` instance. + +## 4. `supportedWalletApi()` capability detection + +The Wallet API specification defines `wallet_supportedWalletApi` as a no-parameter query that returns the latest supported Wallet API version and any compatible past versions. It must return versions while locked, unapproved, or connected, and it is distinct from `wallet_supportedSpecs`, which reports node JSON-RPC versions.[15] + +The Starknet.js delegate is: + +```text +walletV6.supportedWalletApi(wallet) + -> wallet.features["starknet:walletApi"].request({ + type: "wallet_supportedWalletApi" + }) +``` + +CipherBid's runtime rule is: + +```text +supported = returnedVersions.some(stableSemver >= 0.10.3) +``` + +The stable minimum remains **Wallet API `>= 0.10.3`**. The newer `0.10.4-rc.1` development specification is a release candidate, not a reason to raise the minimum or require prerelease strings.[13][15] + +### Least-authority rule + +Do not call `strk20Balances`, with an empty list or otherwise, to detect capability. `wallet_strk20Balances` requests the user's private pool balances and can produce user-consent UI; it is permitted only for a deliberate balance-display feature after the user asks for it.[2][6] + +Current CipherBid evidence: + +- `web/src/features/wallet/walletCapabilities.ts:1-22` compares stable semantic versions against `0.10.3` and fails closed on empty/malformed lists; +- `web/tests/unit/walletCapabilities.test.ts:4-15` covers equal/newer, older, empty, and malformed versions; +- `web/tests/unit/walletConnection.test.ts:18-53` proves the version query is used and no `strk20Balances` dependency exists; +- `web/tests/unit/browserWalletDependencies.test.ts:25-39` proves delegation to `WalletAccountV6` and `walletV6.supportedWalletApi`. + +## 5. Official pool addresses and live readback + +| Network | Official pool address | Official source | Live class hash | Live `get_fee_amount` | Block observed | +| --- | --- | --- | --- | ---: | ---: | +| Starknet mainnet | `0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a` | Canonical mainnet pool in the STRK20 integration links.[5] | `0x67dddd89d80fedadc06b6f160798f94800a4a70164e5a24301cd0d6076b554d` | `6000000000000000000` FRI = **6 STRK** | `13939853` | +| Starknet Sepolia | `0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91` | Official by-example SDK page and Voyager link.[10] | `0x56ab118a8a6e38efc93ad758cefe909fee421fa931ce3cf72df624d345623b2` | `2000000000000000000` FRI = **2 STRK** | `14123649` | + +Live mainnet reads were performed through Lava and the 6 STRK fee was independently reproduced through OnFinality. Sepolia was read through PublicNode. Both addresses returned a class hash, pool version, fee collector, and fee view; they are live contracts rather than documentation-only constants. + +## 6. Live pool fee behavior + +The pool contract defines the fee as a `u128` amount in FRI charged **once per `apply_actions` call**, not once per action in the action array. `apply_actions()` collects the fee before applying the atomic action batch; `0` disables the fee.[8][16] + +When non-zero, the pool transfers STRK from the `apply_actions` caller to the configured fee collector with `transfer_from`. Governance can change both the amount and collector, and emits configuration events.[8][16] + +CipherBid controls: + +1. Read `get_fee_amount()` from the configured pool for the selected network; never hardcode 4, 6, 2, or another amount. +2. Treat one Wallet API private transaction/action batch as one pool-fee charge. +3. Show the current fee before preparation/submission and include it in budget/MAX calculations. +4. Refresh the fee immediately before a funded rehearsal or mainnet action manifest because governance can change it. +5. Keep pool fee separate from Starknet execution/gas sponsorship in UI language. +6. Fail closed if the fee view, pool identity, or network cannot be verified. + +The June launch article's 4 STRK figure was historically correct but is now stale: current mainnet contract state returned 6 STRK.[4] + +## 7. Note maturity + +Official docs state that a note becomes visible to discovery once its transaction is accepted but is spendable only **10 blocks after creation**.[9] The current discovery implementation carries each note's creation block specifically so clients can enforce that 10-block rule, and the SDK changelog records the same behavior.[11][12] + +Required behavior for CipherBid: + +- A user who shields in one transaction cannot immediately spend that new private note in a later bid transaction. +- Treat the funds as **maturing** until the current accepted block is at least 10 blocks after the note's creation block. +- The wallet owns note discovery and selection on this route; CipherBid must not request viewing keys or inspect notes itself.[1] +- Do not repurpose `strk20Balances` as a maturity or capability probe. +- Prefer shielding ahead of the auction interaction and make the wait explicit. +- Same-transaction deposit-and-spend can avoid the maturity delay, but it correlates the public deposit with the private action and is not the default CipherBid privacy route.[6][10] +- If the wallet reports insufficient private balance immediately after shielding, surface a controlled “funds may still be maturing” state rather than blindly resubmitting. + +## 8. Verification evidence + +| Check | Evidence | Result | +| --- | --- | --- | +| Package availability | Live npm registry queries for exact versions and moving dist-tags | Passed | +| Installed package identity | `pnpm list --depth 0 --json` | Exact approved tuple installed | +| `WalletAccountV6.connect` behavior | Installed `starknet@10.4.0` runtime function inspection plus current Starknet.js guide | Confirmed | +| Capability query | Current Wallet API spec, installed Starknet.js delegate, local adapter tests | Confirmed | +| Minimum API | Stable release v0.10.3, current stable types, development spec comparison | Keep `>= 0.10.3` | +| No private-balance probe | Source inspection and unit test | Confirmed | +| Wallet capability tests | `pnpm test` | **12 files / 52 tests passed** | +| Pool identity | Official source addresses plus class-hash readback | Confirmed | +| Mainnet fee | Two live RPC providers | **6 STRK per `apply_actions`** | +| Sepolia fee | Live Sepolia RPC | **2 STRK per `apply_actions`** | +| Maturity | Official docs, SDK changelog, discovery source | **10 blocks** | + +## Task 1.1 gate + +- [x] Current Ready support verified. +- [x] Current Xverse support verified from newer official documentation; runtime and later real-wallet testing remain mandatory. +- [x] Exact CipherBid package tuple frozen. +- [x] `WalletAccountV6` connection behavior confirmed. +- [x] `supportedWalletApi()` capability semantics confirmed. +- [x] Wallet API minimum remains `>= 0.10.3`. +- [x] Balance probing prohibited for capability detection. +- [x] Mainnet and Sepolia pool addresses verified from official sources and live chain state. +- [x] Fee semantics and current live amounts verified. +- [x] Ten-block note maturity behavior verified. + +**Gate result:** Task 1.1 is complete. Task 1.2 may use this route baseline, but it must still freeze CipherBid's exact bid-ingress action and calldata sequence independently. + +## Sources + +[1] https://strk20-by-example.org/starknet-wallet-api/overview.md +[2] https://strk20-by-example.org/starknet-wallet-api/private-defi.md +[3] https://starknet-js.com/docs/next/guides/account/walletAccount +[4] https://www.starknet.io/blog/privacy-live-on-starknet +[5] https://raw.githubusercontent.com/starkience/strk20-agent-skills/main/skills/strk20-privacy-integration/references/links.md +[6] https://raw.githubusercontent.com/starkience/strk20-agent-skills/main/skills/strk20-privacy-integration/references/wallet-api-route.md +[7] https://raw.githubusercontent.com/Akashneelesh/strk20-starter-kit/main/package.json +[8] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/README.md +[9] https://strk20-by-example.org/sdk/note-discovery.md +[10] https://strk20-by-example.org/sdk/getting-started.md +[11] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/sdk/CHANGELOG.md +[12] https://github.com/starkware-libs/starknet-privacy/blob/main/crates/discovery-core/src/discovery/notes.rs +[13] https://github.com/starkware-libs/starknet-specs/releases/tag/v0.10.3 +[14] https://github.com/starknet-io/starknet.js/releases/tag/v10.4.0 +[15] https://raw.githubusercontent.com/starkware-libs/starknet-specs/master/wallet-api/wallet_rpc.json +[16] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/privacy.cairo diff --git a/docs/evidence/task-1-2-bid-ingress-wire.md b/docs/evidence/task-1-2-bid-ingress-wire.md new file mode 100644 index 0000000..8bcdebf --- /dev/null +++ b/docs/evidence/task-1-2-bid-ingress-wire.md @@ -0,0 +1,273 @@ +# CipherBid Task 1.2 — Bid-Ingress Wire Contract + +**Status:** Frozen v1 wire baseline + +**Verified:** 2026-08-27T10:52:48Z + +**Decision:** A bid enters CipherBid through one wallet-submitted STRK20 transaction with exactly two application actions: **`withdraw` then `invoke`**. Bid ingress creates no open note. + +## 1. Normative route decision + +The Wallet API supports ordered `withdraw`, `transfer`, and `invoke` actions. The pool protocol assigns withdrawal phase 6 and invoke phase 7, so `withdraw → invoke` is the protocol-valid order; reversing it is rejected as out of phase.[3][4] + +The official open-note DeFi pattern uses `transfer(amount: "OPEN") → invoke` when the helper produces output that must be credited to an amount-unknown note.[1][2] CipherBid bid ingress is different: the equal collateral cap leaves the pool for the auction house and remains locked there. The helper returns no value to the pool, so its exact return is an empty `Span`.[2][5] + +Therefore: + +- **Selected:** `withdraw(cap, auctionHouse) → invoke(auctionHouse, calldata)`. +- **Rejected:** open-note `transfer("OPEN") → invoke`, because bid ingress has no output note to fill. +- **Rejected:** `invoke → withdraw`, because it violates the pool action-phase order and would call the auction house before collateral arrives.[4] +- **Rejected:** public ERC-20 transfer initiated by the dapp, because the pool must be the public source of the equal cap. +- **Rejected:** direct auction-house call for ingress, because it would not prove live STRK20-funded collateral. + +The pool's private token balance sheet consumes the bidder's selected mature notes and applies the public withdrawal atomically with the invoke. Any failure in the auction house reverts the whole pool transaction.[4][5] + +## 2. Frozen Wallet API action sequence + +The canonical machine-readable sample is: + +`web/tests/fixtures/bid-ingress-v1.json` + +### Action 0 — withdraw the uniform cap + +| Field | Exact value/source | Rule | +| ----------- | ------------------------------------------- | ------------------------------------------------------- | +| `type` | `"withdraw"` | Literal. | +| `token` | configured auction payment token | Must equal the verified auction payment token. | +| `amount` | public uniform collateral cap in base units | Must be the chain-read cap, not the private bid amount. | +| `recipient` | deployed CipherBid auction-house address | The pool publicly transfers the cap here. | + +The Wallet API defines withdrawal as a public transfer to the named recipient, and the pool emits its token, recipient, and amount.[3][6] CipherBid deliberately uses the same public cap for every accepted bidder; it never serializes the private bid amount. + +### Action 1 — invoke CipherBid + +| Field | Exact value/source | Rule | +| ---------- | ------------------------------------------------ | ------------------------------------------------------------------------- | +| `type` | `"invoke"` | Literal. | +| `contract` | same auction-house address as action 0 recipient | Any mismatch fails the fixture and preflight. | +| `calldata` | eight entries frozen below | No insertion, deletion, reordering, normalization, or alternate encoding. | + +No third application action is permitted. The wallet may add its own relayer/paymaster fee withdrawal as specified by the Wallet API; that wallet-owned fee action is not part of CipherBid's supplied action array.[3] + +## 3. Frozen `privacy_invoke` calldata + +The pool calls the target's `privacy_invoke` selector.[5][8] + +The supplied calldata is deserialized directly into the helper's Cairo parameters, so position is part of the wire contract.[2][4] + +| Index | Cairo parameter | Cairo type | Bid-ingress value | Constraint | +| ----: | --------------- | ----------------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| `0` | `operation` | `u8` | `0` / `0x0` | Literal `PLACE_BID`. | +| `1` | `auction_id` | `u64` | selected auction ID | Canonical unsigned 64-bit value. | +| `2` | `primary_value` | `felt252` | domain-separated bid commitment | Non-zero and unique under the auction protocol; lifecycle claims reuse this slot for their claim secret. | +| `3` | `claim_handle` | `felt252` | `Poseidon(CIPHERBID_CLAIM_V1, claim_secret)` | Non-zero and unique where required. | +| `4` | `reserved_0` | `felt252` | `0` / `0x0` | Must remain zero for `PLACE_BID`. | +| `5` | `reserved_1` | `felt252` | `0` / `0x0` | Must remain zero for `PLACE_BID`. | +| `6` | `pool_address` | `ContractAddress` | literal string `${poolAddress}` in the dapp action | Wallet resolves it to the active privacy-pool address. Contract requires equality with its configured pool. | +| `7` | `open_note_id` | `felt252` | `0` / `0x0` | Must remain zero because ingress creates no open note; claims use the open-note placeholder here. | + +The TypeScript builder serializes integer values as canonical felt hex strings. The literal placeholder is not converted to a felt, normalized as an address, or interpolated by CipherBid. + +### Why keep index 6 when the caller is already the pool? + +The Cairo contract separately requires `get_caller_address() == configured_pool`. Index 6 is a redundant cross-layer binding: the wallet-resolved `${poolAddress}` must also equal that configured pool. Neither check replaces deployment-identity verification. + +The reserved slots retain the already-proven eight-felt envelope while this task freezes bid ingress only. They are not permission to add hidden semantics later: any non-zero bid-ingress value is wire drift and must fail before submission. + +## 4. Placeholder contract + +The Wallet API defines exactly these relevant substitutions:[3] + +| Placeholder | Protocol meaning | Bid-ingress use | +| ------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------ | +| `${poolAddress}` | Active STRK20 privacy-pool contract address | **Used once**, exactly at `actions[1].calldata[6]`. | +| `${openNoteIds[N]}` | ID of the zero-based Nth open note created by a same-transaction `transfer` whose amount is literal `"OPEN"` | **Not used**. Bid ingress creates and fills zero open notes. | + +The literal spellings, braces, dollar sign, capitalization, brackets, and zero-based indexing are protocol data.[3] Application code must not “helpfully” rewrite them. + +The Wallet API rejects a transaction when the number of open notes created and filled does not match.[3] CipherBid's bid-ingress count is exactly zero created and zero returned, so the empty-span route is balanced. + +## 5. Frozen `privacy_invoke` return + +Cairo signature: + +```text +privacy_invoke( + operation: u8, + auction_id: u64, + primary_value: felt252, + claim_handle: felt252, + reserved_0: felt252, + reserved_1: felt252, + pool_address: ContractAddress, + open_note_id: felt252, +) -> Span +``` + +`OpenNoteDeposit` is the protocol struct `{ note_id: felt252, token: ContractAddress, amount: u128 }`.[7] + +For `PLACE_BID`, the semantic return is an empty span with length `0`; its raw Cairo serialization is the single length felt `[0x0]`. No trailing return data is allowed. The pool deserializes the span and rejects malformed or trailing data.[2][5][8] + +Consequences: + +- the auction house does not approve an output token for the pool; +- no `OpenNoteDeposit` is applied; +- no `${openNoteIds[N]}` exists; +- the cap remains in auction-house custody/accounting; +- a failed bid acceptance reverts the withdrawal and invoke atomically. + +## 6. Wallet proof, signing, and submission boundary + +The actual bid uses: + +```text +strk20InvokeTransaction(actions) +``` + +Under the current Wallet API, the wallet supplies private state, generates the zero-knowledge proof, displays approval, signs, adds its fee action, and submits the transaction. CipherBid receives only `{ transaction_hash }` on success.[3] + +CipherBid may run only this non-submittable shape preflight first: + +```text +strk20PrepareInvoke(actions, true) +``` + +With `simulate: true`, the wallet explicitly skips proof generation and returns empty proof fields. CipherBid must never call non-simulated `strk20PrepareInvoke` for bid submission, because that would return proof material to the dapp and make the dapp responsible for broadcast.[3] + +### Application data boundary + +CipherBid receives or constructs only: + +- public action data; +- the in-memory bid commitment inputs already allowed by the custody decision; +- simulated call shape with empty proof fields; +- submitted transaction hash; +- public receipt, events, and state readback. + +CipherBid never receives: + +- wallet private keys or seed material; +- viewing keys; +- private notes, channels, nullifiers, or note-selection state; +- non-empty proof data, proof output, or proof facts; +- wallet session secrets. + +## 7. Frozen receipt and event expectations + +A transaction is not a successful bid merely because the wallet returned a hash. Acceptance requires an on-chain receipt with successful execution/finality plus public readback showing the bid exists. + +### Required pool-side evidence + +| Evidence | Required fields/meaning | +| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| ERC-20 `Transfer` | Payment token, sender = configured pool, recipient = auction house, amount = uniform cap. | +| Pool `Withdrawal` | `to_addr = auction_house`, `token = payment_token`, `amount = cap`; encrypted user address is opaque and must not be decoded by CipherBid.[6] | +| Pool `ExternalContractInvoked` | `contract_address = auction_house`, `selector = selector!("privacy_invoke")`.[5][6][8] | +| Pool `NoteUsed` | Zero or more may appear depending on wallet-selected inputs; CipherBid does not decode ownership.[6] | +| Fee evidence | A wallet-added fee withdrawal/STRK transfer may also appear and must not be confused with the cap withdrawal.[3] | + +### Required CipherBid event + +Production auction-house implementation must emit: + +```text +BidCommitted { + auction_id, + commitment, + claim_handle, + collateral +} +``` + +`collateral` must equal the observed incoming payment-token balance delta and the configured cap. The event must not contain bidder address, bid amount, bid nonce/secret, claim secret, viewing-key material, note IDs, or proof material. + +### Forbidden ingress events + +Because the route creates no open note and returns no deposit: + +- no pool `OpenNoteCreated` event; +- no pool `OpenNoteDeposited` event. + +Other protocol events may exist because of registration, note use, or wallet fee handling.[3][6] Receipt verification must match required event identity and fields rather than require an unrealistically exact total event count. + +### Required readback + +After successful receipt confirmation, read the auction house and require: + +- bid count advanced exactly once; +- the commitment and claim handle are registered for the selected auction; +- locked collateral/accounted balance advanced by exactly the cap; +- the bid remains unrevealed; +- no duplicate submission was accepted. + +Timeout means **submitted/unconfirmed**, not success or failure. + +## 8. Cross-layer drift fixture + +The following files freeze the v1 wire contract: + +| File | Role | +| ---------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `web/tests/fixtures/bid-ingress-v1.json` | Canonical action sample, every calldata index, placeholders, return contract, submission boundary, event set, and private-data boundary. | +| `web/tests/unit/bidIngressWireFixture.test.ts` | Executes the real TypeScript builder, compares both actions and every calldata index, parses the Cairo interface argument names/types/order, and checks placeholder/event/privacy expectations. | +| `web/src/features/privacy/strk20Actions.ts` | Production Wallet API action builder. | +| `contracts/src/lib.cairo` | Current Cairo `privacy_invoke` ABI surface; the shared `primary_value` and `open_note_id` slots serve ingress and Task 1.3 claims without changing any index. | +| `contracts/tests/test_contract.cairo` | Cairo proof that the configured pool can route the sample and receives an empty span. | + +The fixture fails if any of these drift: + +- action count or `withdraw → invoke` order; +- token, amount, recipient, or invoke target; +- calldata length; +- any calldata value or index; +- Cairo argument name, type, count, or order; +- literal placeholder location or spelling; +- accidental `${openNoteIds[N]}` introduction; +- return type/expected length declaration; +- proof/submission ownership; +- required or forbidden event names; +- private material allowed into the app. + +The current `AuctionIngressSpike` remains local-only and must not receive funds. It proves caller binding, calldata dispatch, and empty-return compatibility, but it does **not** yet implement production balance-delta accounting or emit `BidCommitted`; those are Task 3 implementation gates, not evidence of a deployed auction house. + +## 9. Requirement-to-evidence matrix + +| Task requirement | Frozen answer | Evidence | +| --------------------------------------- | ----------------------------------------------------------------------------- | ----------------------------------------------------- | +| Derive official sequence | `withdraw → invoke` in one STRK20 transaction | Official Wallet API, pool action phases, fixture | +| Resolve open-note alternative | No open note for ingress; zero created and returned | Official helper/open-note docs, empty-span Cairo test | +| Verify placeholders | `${poolAddress}` at index 6; no `${openNoteIds[N]}` | Wallet API schema, fixture test | +| Freeze order | Action 0 withdraw; action 1 invoke | Fixture deep equality | +| Freeze calldata | Eight indexed entries in §3 | Fixture plus Cairo ABI parity test | +| Freeze arguments/return | Typed eight-argument `privacy_invoke`; empty `Span` | Cairo interface/test, protocol return parser | +| Freeze receipt/events | Cap withdrawal + pool invoke + production `BidCommitted`; no open-note events | §7 and fixture | +| Wallet proves/submits | `strk20InvokeTransaction(actions)` | Wallet API spec | +| App excludes private wallet/proof state | Simulated empty-proof preflight only; hash result on submit | Wallet API spec and fixture | +| Cross-layer drift gates | JSON fixture + TS builder/Cairo-interface parity test | Fresh test execution | + +## Task 1.2 gate + +- [x] Official action model and phase ordering verified. +- [x] Bid ingress resolved to `withdraw → invoke`. +- [x] Open-note route rejected for ingress with explicit rationale. +- [x] `${poolAddress}` frozen literally at calldata index 6. +- [x] `${openNoteIds[N]}` explicitly absent. +- [x] Every action and calldata position frozen. +- [x] `privacy_invoke` arguments and empty return frozen. +- [x] Receipt, required events, forbidden events, and readback frozen. +- [x] Wallet proof/sign/submission responsibility frozen. +- [x] App private-data/proof boundary frozen. +- [x] Cross-layer fixtures created. + +**Gate result:** The bid-ingress wire contract is frozen. Production acceptance, balance-delta accounting, and `BidCommitted` emission remain intentionally deferred to the Task 3 auction-house implementation. + +## Sources + +[1] https://strk20-by-example.org/starknet-wallet-api/private-defi.md +[2] https://strk20-by-example.org/helpers/privacy-invoke.md +[3] https://raw.githubusercontent.com/starkware-libs/starknet-specs/master/wallet-api/wallet_rpc.json +[4] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/actions.cairo +[5] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/privacy.cairo +[6] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/events.cairo +[7] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/objects.cairo +[8] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/utils.cairo diff --git a/docs/evidence/task-1-3-lifecycle-wire-matrix.md b/docs/evidence/task-1-3-lifecycle-wire-matrix.md new file mode 100644 index 0000000..c68d381 --- /dev/null +++ b/docs/evidence/task-1-3-lifecycle-wire-matrix.md @@ -0,0 +1,473 @@ +# CipherBid Task 1.3 — Reveal and Claim Transaction Wire Matrix + +**Status:** Approved v2 lifecycle wire baseline + +**Verified:** 2026-08-28T08:46:58Z + +**Decision:** Reveal, settlement, and seller-proceeds destination authorization are ordinary connected-wallet calls. Loser refunds, winner surplus, and seller proceeds use STRK20 `transfer("OPEN") → invoke` so every monetary exit enters a wallet-owned open note. + +This v2 matrix supersedes both the former STRK20 invoke-only reveal spike and the v1 public seller payout. It preserves every Task 1.2 bid-ingress index while extending the shared `privacy_invoke` operation set with `SELLER_PROCEEDS = 3`. + +## 1. Route classification + +A `WalletAccountV6` can act as a standard Starknet account for ordinary writes; the wallet retains its private key and signs/sends the call.[8] STRK20 actions are reserved for flows that need the pool's private note state, relayed proof, or open-note output.[1][3] + +Within a STRK20 claim, open-note creation precedes external invocation because the protocol assigns note creation to phase 5 and invoke to phase 7.[4] + +| Lifecycle transaction | Route | Touches STRK20 pool? | Reason | +| ---------------------------- | ------------------------------------------------ | -------------------: | ------------------------------------------------------------------------------------------- | +| Reveal A/B | `walletAccount.execute(revealCall)` | No | Revealed values are intentionally public; no token moves. | +| Settlement | `walletAccount.execute(settlementCall)` | No | Permissionless auction computation and ERC-721 delivery need no private pool state. | +| Loser refund | `walletAccount.strk20InvokeTransaction(actions)` | **Yes** | Returns the full cap to an open note controlled by the claimant's privacy wallet. | +| Winner surplus | `walletAccount.strk20InvokeTransaction(actions)` | **Yes** | Returns `cap - clearing_price` to an open note. | +| Seller destination authorize | `walletAccount.execute(authorizationCall)` | No | The configured seller binds the exact simulated open-note ID before the bearer secret airs. | +| Seller proceeds | `walletAccount.strk20InvokeTransaction(actions)` | **Yes** | Returns the clearing price to the seller-authorized open note. | + +### Why seller destination authorization is required + +A plain Wallet API `invoke` reaches the helper through the pool, so the helper sees the pool as caller rather than the wallet identity.[1][2][5] + +The `OpenNoteDeposit` return names a note ID, token, and amount; it does not prove that the note belongs to the configured public seller.[2][7] + +A seller claim secret alone is therefore a bearer credential. Once visible in pending calldata, a copied secret could otherwise be paired with another open-note ID. V2 prevents redirection without introducing a claim-signing key: the configured seller first authorizes the exact note ID through a standard account transaction, and operation `3` accepts only that stored authorization. Copying the later secret can at most race the same payout into the already-authorized seller note; it cannot choose a different destination. Replay still fails after one-time claim consumption. + +This security fix has a deliberate privacy cost: the authorization publicly links the configured seller to the open-note ID. The proceeds enter STRK20 and subsequent note spending remains private, but CipherBid must not claim that the seller-to-note receipt edge is hidden. + +## 2. Standard connected-wallet calls + +All integer calldata uses canonical felt hex. Each call is sent through the already-connected `WalletAccountV6` with `walletAccount.execute(call)`. The wallet signs and submits; CipherBid receives the transaction hash and then verifies receipt plus state readback.[8] + +### 2.1 Reveal + +TypeScript call: + +```text +{ + contractAddress: auction_house, + entrypoint: "reveal_bid", + calldata: [ + auction_id, + amount, + bid_nonce, + claim_handle, + asset_recipient, + ], +} +``` + +Cairo surface: + +```text +reveal_bid( + auction_id: u64, + amount: u128, + bid_nonce: felt252, + claim_handle: felt252, + asset_recipient: ContractAddress, +) +``` + +| Calldata index | Field | Rule | +| -------------: | ----------------- | --------------------------------------------------------------------------------- | +| `0` | `auction_id` | Existing auction; reveal phase only. | +| `1` | `amount` | `1..=cap`; public after this transaction. | +| `2` | `bid_nonce` | Non-zero commitment nonce; public after reveal. | +| `3` | `claim_handle` | Must equal the handle committed during ingress. The claim secret is not revealed. | +| `4` | `asset_recipient` | Non-zero ERC-721 recipient bound into the commitment. | + +The contract recomputes the domain-separated commitment from chain ID, its own address, auction ID, amount, nonce, claim handle, and recipient. It looks up the stored commitment, requires it to be unrevealed, then emits: + +```text +BidRevealed { + auction_id, + commitment, + amount, + asset_recipient, +} +``` + +No claim secret, wallet viewing key, note ID, or proof data appears. Reveal does not call `privacy_invoke`, create an open note, or pay a STRK20 pool fee. + +### 2.2 Settlement + +TypeScript call: + +```text +{ + contractAddress: auction_house, + entrypoint: "settle_auction", + calldata: [auction_id], +} +``` + +Cairo surface: + +```text +settle_auction(auction_id: u64) +``` + +Settlement is permissionless after the reveal deadline. The caller need not be seller, winner, or bidder. The contract computes the winner and clearing price from bounded on-chain state, marks settlement before external NFT delivery, transfers the ERC-721, and emits: + +```text +AuctionSettled { + auction_id, + sold, + winner_commitment, + winner_recipient, + clearing_price, +} +``` + +Expected public evidence is `AuctionSettled`, the ERC-721 `Transfer`, settled-state readback, and `owner_of(token_id) == winner_recipient` when sold. Settlement does not touch the STRK20 pool. + +### 2.3 Seller proceeds destination authorization + +TypeScript call: + +```text +{ + contractAddress: auction_house, + entrypoint: "authorize_seller_proceeds", + calldata: [auction_id, seller_claim_handle, open_note_id], +} +``` + +Cairo surface: + +```text +authorize_seller_proceeds( + auction_id: u64, + seller_claim_handle: felt252, + open_note_id: felt252, +) +``` + +Rules: + +- caller must equal the configured seller; +- auction must be settled and sold; +- supplied handle must equal the immutable `seller_claim_handle`; +- `open_note_id` must be non-zero; +- authorization is allowed only while seller proceeds remain unclaimed; +- the seller may replace an authorization before claim consumption to recover from an abandoned or changed wallet preparation; +- authorization never accepts a payout amount; +- the app re-runs `strk20PrepareInvoke(actions, true)` after authorization and requires the resolved note ID to remain equal before enabling submission. + +Expected event: + +```text +SellerProceedsAuthorized { + auction_id, + seller_claim_handle, + open_note_id, +} +``` + +This transaction does not move money or touch the STRK20 pool. It is a public seller-to-note binding and therefore appears in the demo evidence, but it does not qualify as one of the required pool transactions. + +## 3. Shared STRK20 claim envelope + +Official private DeFi flows create an open note with `transfer(amount: "OPEN")` and then invoke the helper that fills it.[1][2] + +The pool requires the number of created open notes to equal the number filled by the invoke.[3][5] + +Each claim contains exactly one invoke because the pool permits at most one invoke-phase action per transaction.[4] + +CipherBid uses one shared eight-felt `privacy_invoke` envelope for bid ingress and all three monetary claims: + +```text +privacy_invoke( + operation: u8, + auction_id: u64, + primary_value: felt252, + claim_handle: felt252, + reserved_0: felt252, + reserved_1: felt252, + pool_address: ContractAddress, + open_note_id: felt252, +) -> Span +``` + +| Index | Cairo parameter | `PLACE_BID = 0` | `LOSER_REFUND = 1` | `WINNER_SURPLUS = 2` | `SELLER_PROCEEDS = 3` | +| -----: | ------------------------------- | ---------------- | --------------------------------- | --------------------------------- | --------------------------------- | +| `0` | `operation: u8` | `0x0` | `0x1` | `0x2` | `0x3` | +| `1` | `auction_id: u64` | Auction ID | Auction ID | Auction ID | Auction ID | +| `2` | `primary_value: felt252` | Bid commitment | Bidder claim secret | Bidder claim secret | Seller claim secret | +| `3` | `claim_handle: felt252` | Bid claim handle | Bid claim handle | Bid claim handle | Immutable seller claim handle | +| `4` | `reserved_0: felt252` | `0x0` | `0x0` | `0x0` | `0x0` | +| `5` | `reserved_1: felt252` | `0x0` | `0x0` | `0x0` | `0x0` | +| `6` | `pool_address: ContractAddress` | `${poolAddress}` | `${poolAddress}` | `${poolAddress}` | `${poolAddress}` | +| `7` | `open_note_id: felt252` | `0x0` | `${openNoteIds[N]}`, with `N = 0` | `${openNoteIds[N]}`, with `N = 0` | `${openNoteIds[N]}`, with `N = 0` | +| Return | `Span` | Empty | Exactly one deposit | Exactly one deposit | Exactly one deposit | + +`${poolAddress}` and `${openNoteIds[N]}` are literal Wallet API placeholder forms; CipherBid fixes `N` to decimal zero because each claim creates exactly one open note. The wallet resolves them while assembling the STRK20 transaction; CipherBid must not compile, normalize, or interpolate them.[1][3][8] + +The helper's final calldata felt is the output open-note ID, matching the current Wallet API helper convention.[8] All claim operations require `get_caller_address() == configured_pool` and the resolved index-6 pool value to equal that same deployment. + +## 4. Loser refund + +### Wallet actions + +```text +[ + { + type: "transfer", + token: payment_token, + amount: "OPEN", + recipient: connected_wallet_address, + }, + { + type: "invoke", + contract: auction_house, + calldata: [ + 0x1, + auction_id, + claim_secret, + claim_handle, + 0x0, + 0x0, + "${poolAddress}", + "${openNoteIds[N]}", // N = 0 + ], + }, +] +``` + +### Contract validation and output + +- auction is settled; +- handle belongs to a valid revealed non-winning bid in this auction; +- `Poseidon(CIPHERBID_CLAIM_V1, claim_secret) == claim_handle`; +- claim has not been consumed; +- reserved values are zero; +- open-note ID is non-zero; +- effects are marked consumed before token approval; +- output amount is exactly the full uniform cap. + +Return: + +```text +[ + OpenNoteDeposit { + note_id: open_note_id, + token: payment_token, + amount: cap, + }, +] +``` + +`OpenNoteDeposit` is exactly `{ note_id: felt252, token: ContractAddress, amount: u128 }`.[7] The auction house approves the pool to pull exactly `cap`; the pool performs the transfer and fills the note atomically.[2][5] + +Expected event order, allowing unrelated wallet-fee events around it: + +1. Pool `OpenNoteCreated`. +2. Auction house `LoserRefundClaimed { auction_id, claim_handle, open_note_id, amount: cap }`. +3. Pool `ExternalContractInvoked { contract_address: auction_house, selector: privacy_invoke }`. +4. Pool `OpenNoteDeposited { depositor: auction_house, token: payment_token, note_id, amount: cap }`. + +The one-time claim secret becomes public in invoke calldata when consumed. It must never be logged or persisted before submission, and after successful readback it is terminal/discardable. Publishing it at consumption does not reveal the wallet's viewing key, private notes, or the encrypted owner of the output note. + +The refund amount is public because open-note amounts are plaintext by protocol design.[1][2] + +## 5. Winner surplus + +### Wallet actions + +The sequence is identical to loser refund except operation index 0 is `0x2`: + +```text +transfer(payment_token, "OPEN", connected_wallet_address) +→ invoke(auction_house, [0x2, auction_id, claim_secret, claim_handle, 0, 0, "${poolAddress}", "${openNoteIds[N]}"]) // N = 0 +``` + +### Contract validation and output + +- handle belongs to the settled winning bid; +- claim secret recomputes the stored handle; +- claim is unconsumed; +- `surplus = cap - clearing_price` is computed with checked arithmetic; +- surplus must be positive or the UI marks the claim ineligible and builds no transaction; +- effects precede approval. + +Return: + +```text +[ + OpenNoteDeposit { + note_id: open_note_id, + token: payment_token, + amount: cap - clearing_price, + }, +] +``` + +Expected events mirror loser refund with `WinnerSurplusClaimed` and the exact surplus amount. The pool emits `OpenNoteCreated`, `ExternalContractInvoked`, and `OpenNoteDeposited` around the auction-house event.[5][6] + +## 6. Seller proceeds + +### Wallet preparation and authorization + +1. Build the seller claim actions with operation `0x3` and `${openNoteIds[N]}`, with `N = 0`. +2. Run `strk20PrepareInvoke(actions, true)`; the simulated proof remains empty and is not persisted. +3. Extract the resolved open-note ID from the prepared public call. +4. Ask the configured seller wallet to submit `authorize_seller_proceeds(auction_id, seller_claim_handle, open_note_id)`. +5. Confirm the authorization receipt and read back the exact stored note ID. +6. Re-run simulated preparation and require the resolved note ID to match the authorization. +7. Only then enable `strk20InvokeTransaction(actions)`. + +No other wallet-private transaction may intervene between final preparation and claim submission. A mismatch is a controlled stale-authorization state, never permission to substitute another note. + +The pool derives an open-note ID from the channel key, token, and sequential note index; the preparation's random value is used only to encrypt the recipient address for the auditor.[5] The ID is therefore stable across repeated simulation only while those private inputs and the note index remain unchanged. CipherBid relies on the post-authorization re-simulation comparison rather than assuming stability. + +### Claim actions + +```text +transfer(payment_token, "OPEN", connected_seller_address) +→ invoke(auction_house, [0x3, auction_id, seller_claim_secret, seller_claim_handle, 0, 0, "${poolAddress}", "${openNoteIds[0]}"]) +``` + +### Contract validation and output + +- auction is settled and sold; +- supplied handle equals immutable `seller_claim_handle`; +- `Poseidon(CIPHERBID_CLAIM_V1, seller_claim_secret) == seller_claim_handle`; +- seller proceeds remain unconsumed; +- resolved `open_note_id` equals the seller-authorized note ID; +- reserved values are zero; +- exact amount comes only from stored settlement accounting; +- effects are consumed before token approval; +- a failed approval/pull reverts consumption atomically. + +Return: + +```text +[ + OpenNoteDeposit { + note_id: open_note_id, + token: payment_token, + amount: seller_entitlement, + }, +] +``` + +For the current no-forfeiture demo, `seller_entitlement = clearing_price`. Task 2.3 may add explicitly specified forfeitures only by updating the accounting formula and fixture before production implementation. + +Expected events are `OpenNoteCreated`, `SellerProceedsClaimed`, `ExternalContractInvoked`, and `OpenNoteDeposited`. `SellerProceedsClaimed` records auction ID, seller claim handle, authorized note ID, and exact amount, but not the secret. + +## 7. Submission and readback rules + +### Ordinary lifecycle calls + +Reveal, settlement, and seller destination authorization use `walletAccount.execute(call)`. The browser wallet owns the signing key and sends the standard Starknet transaction.[8] + +### STRK20 claims + +Loser, winner, and seller claims use: + +```text +walletAccount.strk20InvokeTransaction(actions) +``` + +The wallet owns note setup, proof generation, signature, fee action, and submission; CipherBid receives `{ transaction_hash }`.[3][8] A simulated `strk20PrepareInvoke(actions, true)` may preflight shape with empty proof fields, but non-simulated proof material must not enter CipherBid application state.[3] + +For every route, a returned hash means submitted only. Success requires: + +- accepted and successful receipt; +- expected event fields; +- exact auction state transition; +- token/NFT ownership or balance readback where applicable; +- consumed-claim readback for claims; +- timeout classified as unconfirmed, not success or revert. + +The pool emits `ExternalContractInvoked` for the helper and `OpenNoteDeposited` for each returned deposit, which makes all three claim routes independently identifiable in receipts.[5][6] + +## 8. Pool-touching and mainnet evidence plan + +The sprint requires at least three successful mainnet transactions in `strk20.json`; each must exist and touch the live STRK20 pool, and project-contract transactions must run through CipherBid.[9][10] + +| Demo transaction | Pool-touching? | Eligible for required three? | Required CipherBid evidence | +| ---------------------------- | -------------: | ---------------------------: | -------------------------------------------------------------------- | +| Bidder A ingress | **Yes** | **Yes** | `BidCommitted` plus cap-withdrawal/pool invoke evidence | +| Bidder B ingress | **Yes** | **Yes** | `BidCommitted` plus cap-withdrawal/pool invoke evidence | +| Reveal A | No | No | `BidRevealed` and commitment readback | +| Reveal B | No | No | `BidRevealed` and commitment readback | +| Settlement | No | No | `AuctionSettled`, NFT transfer, owner readback | +| Loser refund | **Yes** | **Yes — canonical third** | `LoserRefundClaimed`, open-note events, consumed readback | +| Winner surplus | **Yes** | **Yes — preferred fourth** | `WinnerSurplusClaimed`, open-note events, consumed readback | +| Seller destination authorize | No | No | `SellerProceedsAuthorized` plus authorized-note readback | +| Seller proceeds | **Yes** | **Yes — preferred fifth** | `SellerProceedsClaimed`, open-note events, consumed/balance readback | + +### Minimum accepted mainnet set + +1. Bidder A ingress. +2. Bidder B ingress. +3. Loser refund. + +The loser refund is the canonical third because the two-valid-bid demo deterministically has one loser entitled to the full cap. + +### Preferred complete demonstration + +If reviewed budget and time permit, execute and verify all three STRK20 claims after the public seller destination authorization. This produces five pool-touching CipherBid transactions, closes every value path, and provides stronger conservation evidence than the three-hash minimum. No hash enters `strk20.json` until mainnet existence, success, live-pool contact, CipherBid event, and state readback all pass. + +## 9. Canonical fixtures and implementation surfaces + +| File | Authority | +| ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ | +| `web/tests/fixtures/lifecycle-routes-v2.json` | Canonical sample calls/actions, operation values, output formulas, events, authorization, pool classification, and demo order. | +| `web/tests/unit/lifecycleWireFixture.test.ts` | Executes all builders, checks every action/calldata position, enforces the shared Cairo envelope, outputs, events, and evidence set. | +| `web/src/features/auction/lifecycleCalls.ts` | Typed standard-wallet reveal, settlement, and seller-destination authorization builders. | +| `web/src/features/privacy/strk20ClaimActions.ts` | Typed loser-refund, winner-surplus, and seller-proceeds STRK20 action builders. | +| `web/src/features/privacy/strk20Actions.ts` | Bid-ingress actions only; the obsolete STRK20 reveal builder is removed. | +| `web/tests/fixtures/bid-ingress-v1.json` | Shares the same eight Cairo argument names/types/indices. | +| `contracts/src/lib.cairo` | Current shared `privacy_invoke` ABI surface. | +| This matrix | Human-readable contract for Cairo, TypeScript, receipts, and evidence. | + +The current `AuctionIngressSpike` remains local-only. It proves dispatch compatibility but does not implement production settlement, claims, output approval, event emission, or value conservation. The fixtures are binding inputs for Task 3, not evidence that those production paths already exist. + +## 10. Contract review + +### Normative compatibility + +- No route asks CipherBid for a wallet key, viewing key, private note, or non-empty proof. +- Direct calls expose only data that the auction intentionally makes public. +- Bidder claims use the official open-note pattern and return exactly one deposit for one created note.[1][2][5] +- The shared ABI preserves Task 1.2 indices while giving slots 2 and 7 names valid for both ingress and claims. +- Seller proceeds use the same claim-secret domain and add no claim-signing-key or shadow-account dependency. +- Seller destination authorization converts bearer-secret front-running from redirectable theft into same-note replay/racing. +- The public seller-to-note authorization link is disclosed rather than described as hidden. +- Every transfer amount has one authoritative formula: loser `cap`, winner `cap - clearing_price`, seller `clearing_price`. +- Pool-touching evidence is not claimed for ordinary wallet transactions. + +### Deferred implementation, not contract ambiguity + +Task 3 must still implement and adversarially test authorization, phase checks, commitment recomputation, one-time effects, approvals, failed external-call rollback, events, and balance conservation. Task 4 must implement receipt/readback decoders. Those tasks may add internal structure but must not change this public wire matrix without an explicit reviewed version bump. + +## Task 1.3 gate + +- [x] Standard connected-wallet lifecycle calls selected. +- [x] STRK20 open-note claim calls selected. +- [x] Exact reveal target, entrypoint, and five calldata positions frozen. +- [x] Exact settlement target, entrypoint, and calldata frozen. +- [x] Loser refund action order, operation, calldata, and full-cap output frozen. +- [x] Winner surplus action order, operation, calldata, and output formula frozen. +- [x] Seller claim secret/handle and immutable configuration binding frozen. +- [x] Seller destination authorization, operation `3`, exact calldata, and clearing-price open-note output frozen. +- [x] Pool-touching classification frozen. +- [x] Three-transaction mainnet minimum frozen. +- [x] Preferred all-claims demo frozen. +- [x] One shared Cairo/TypeScript/test/evidence matrix established. + +**Gate result:** One approved v2 Wallet API/contract wire matrix now governs ingress, reveal, settlement, all monetary claims, seller destination authorization, cross-layer fixtures, and mainnet evidence classification. + +## Sources + +[1] https://strk20-by-example.org/starknet-wallet-api/private-defi.md — STRK20 Wallet API private DeFi +[2] https://strk20-by-example.org/helpers/privacy-invoke.md — STRK20 privacy_invoke helper anatomy +[3] https://raw.githubusercontent.com/starkware-libs/starknet-specs/master/wallet-api/wallet_rpc.json — Starknet Wallet API specification +[4] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/actions.cairo — STRK20 action source +[5] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/privacy.cairo — STRK20 pool source +[6] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/events.cairo — STRK20 event source +[7] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/objects.cairo — STRK20 object source +[8] https://starknet-js.com/docs/next/guides/account/walletAccount — starknet.js WalletAccount guide +[9] https://raw.githubusercontent.com/starkience/strk20-hackathon/main/README.md — Private Sprint requirements +[10] https://raw.githubusercontent.com/starkience/strk20-hackathon/main/CONTRIBUTING.md — Private Sprint contribution rules diff --git a/docs/evidence/task-2-1-auction-configuration.md b/docs/evidence/task-2-1-auction-configuration.md new file mode 100644 index 0000000..949b4a0 --- /dev/null +++ b/docs/evidence/task-2-1-auction-configuration.md @@ -0,0 +1,295 @@ +# CipherBid Task 2.1 — Auction Configuration Specification + +**Status:** Approved v2 configuration baseline + +**Verified:** 2026-08-28T08:46:58Z + +**Decision:** CipherBid uses one reusable, non-upgradeable auction-house deployment with immutable pool/token/bidder-bound configuration. Each auction stores a permanently immutable seller claim handle, seller/NFT/price/deadline/capacity record, plus separately mutable lifecycle state. + +## 1. Configuration layers + +### Deployment-wide `AuctionHouseConfig` + +```text +AuctionHouseConfig { + pool: ContractAddress, + payment_token: ContractAddress, + max_bidders: u16, +} +``` + +| Field | Frozen rule | +| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `pool` | Non-zero configured STRK20 pool address, distinct from the STRK token contract. It is the only caller accepted by `privacy_invoke`. | +| `payment_token` | Canonical STRK contract `0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d`. STRK uses the same address on Starknet mainnet and Sepolia.[1] | +| `max_bidders` | Deployment ceiling in `2..=32`. It bounds every auction's bidder capacity and settlement iteration. | + +The constructor surface is: + +```text +constructor(pool: ContractAddress, max_bidders: u16) +``` + +`payment_token` is not caller-configurable: the contract assigns the canonical STRK constant. The public `get_house_config()` view returns all three values, allowing deployment readback to prove the configured pool, token, and bound. + +A single deployment can hold many independent auctions. Pool and payment-token addresses are not repeated in each auction record and cannot differ between auctions in the same house. + +Every field typed `ContractAddress` uses the Cairo 2.20.0 range `[0, 2^251)` and CipherBid rejects zero, so valid configured addresses are exactly `1..2^251-1`; `2^251` is the first invalid high value.[2] + +### Per-auction `AuctionConfig` + +```text +AuctionConfig { + auction_id: u64, + seller: ContractAddress, + seller_claim_handle: felt252, + nft_contract: ContractAddress, + token_id: u256, + reserve_price: u128, + collateral_cap: u128, + bidding_deadline: u64, + reveal_deadline: u64, + bidder_limit: u16, +} +``` + +The creation surface is: + +```text +create_auction( + auction_id: u64, + seller_claim_handle: felt252, + nft_contract: ContractAddress, + token_id: u256, + reserve_price: u128, + collateral_cap: u128, + bidding_deadline: u64, + reveal_deadline: u64, + bidder_limit: u16, +) +``` + +`seller` is deliberately absent from calldata. The contract derives it from `get_caller_address()` and stores it in the immutable record. This prevents a caller from creating an auction that falsely names another address as seller. + +## 2. Field-by-field contract + +### Seller address + +- `seller = get_caller_address()` at creation. +- Seller must be non-zero. +- `seller_claim_handle` must be a non-zero felt derived before creation as `Poseidon(CIPHERBID_CLAIM_V1, seller_claim_secret)`. +- The contract stores only the handle; it never receives the seller claim secret before the final claim. +- Creation later proves seller authorization by successful ERC-721 custody transfer; approval/custody mechanics belong to Task 3.2. +- Seller never changes, even if the wallet later rotates keys or transfers other assets. +- Seller is the only caller allowed to authorize the v2 seller-proceeds destination note frozen in Task 1.3. + +### ERC-721 identity + +- `nft_contract` is a non-zero Starknet contract address. +- `token_id` is the full ERC-721 `u256` token identifier. +- Token ID zero is valid and must not be used as an “unset” sentinel. +- The canonical lot identity is `(nft_contract, token_id)`. +- At most one non-terminal/custodied auction may reference the same lot. The custody index and release rules are implemented in Task 3.2. +- Successful creation requires atomic transfer of that exact NFT into auction-house custody; a failed transfer rolls back the record and does not consume the ID. + +### Payment token and pool + +- Every auction in v1 is denominated in canonical STRK. +- All price amounts use STRK base units and are stored as `u128`; no floating point or display-unit decimal enters Cairo calldata. +- `pool` is deployment-wide and immutable. +- `pool != payment_token`; confusing the ERC-20 contract with the privacy pool is rejected during construction. +- `privacy_invoke` requires both caller equality with the stored pool and the resolved `${poolAddress}` argument to match it. +- A pool upgrade at the same address does not change configuration. A new pool address requires a new auction-house deployment. + +### Reserve and uniform collateral cap + +```text +0 < reserve_price <= collateral_cap <= u128::MAX +``` + +- `reserve_price` is the minimum acceptable clearing price. +- `collateral_cap` is the exact public amount every accepted bid locks. +- A bid amount must later satisfy `reserve_price <= amount <= collateral_cap`. +- Equality `reserve_price == collateral_cap` is valid. +- Neither value can change after creation. +- Settlement and claims use these stored values; callers never supply authoritative reserve/cap values again. + +### Deadlines + +Both deadlines are `u64` Starknet block timestamps in Unix seconds. + +Creation requires: + +```text +creation_timestamp < bidding_deadline < reveal_deadline <= u64::MAX +``` + +Exact boundary semantics: + +| Phase | Timestamp condition | +| --------------- | ------------------------------------------- | +| Bidding open | `now < bidding_deadline` | +| Reveal open | `bidding_deadline <= now < reveal_deadline` | +| Settlement open | `now >= reveal_deadline` | + +Consequences: + +- A bid at exactly `bidding_deadline` is rejected. +- A reveal at exactly `bidding_deadline` is allowed. +- A reveal at exactly `reveal_deadline` is rejected. +- Settlement at exactly `reveal_deadline` is allowed. +- Creation with a bidding deadline equal to the current timestamp is rejected. +- There is no admin deadline extension, early close, or cancellation mutation in v1. + +### Bidder count + +Two bounds apply: + +1. `ABSOLUTE_MAX_BIDDERS = 32`, compiled into the v1 model. +2. Deployment `max_bidders`, selected once in `2..=32`. + +Each auction chooses: + +```text +2 <= bidder_limit <= house.max_bidders <= 32 +``` + +`bidder_limit` is capacity, not a minimum participation requirement. Settlement still supports zero bids, one valid reveal, and any count up to the configured limit under the later lifecycle specification. + +The 32-bidder absolute ceiling keeps storage iteration, winner selection, tie handling, settlement gas, property tests, and readback bounded. Raising it requires an explicit new reviewed version and measured Cairo execution evidence. + +## 3. `auction_id` identity and uniqueness + +`auction_id` is an explicit seller-supplied non-zero `u64`. + +The globally meaningful identity is: + +```text +(chain_id, auction_house_address, auction_id) +``` + +Rules: + +- `0` is reserved as an invalid/sentinel value. +- The same numeric ID may exist in another deployment or chain because commitments bind chain and auction-house address. +- Inside one auction-house deployment, each non-zero ID may be created at most once. +- IDs are never recycled after settlement, no-sale, claims, or any terminal state. +- Uniqueness uses an explicit `auction_exists[auction_id]` marker; it must not infer existence from another field because token ID zero and other legitimate zero-valued lifecycle fields exist. +- Duplicate creation rejects before external custody interaction. +- If initial creation reverts atomically, including failed NFT transfer, the existence marker rolls back and that ID remains unused. + +Explicit caller-supplied IDs preserve deterministic URLs, fixtures, commitments, and evidence manifests while making duplicate handling testable. Auto-increment counters are not part of v1. + +## 4. Permanent immutability + +### House configuration + +After constructor success, `pool`, `payment_token`, and `max_bidders` are permanently immutable. + +- No admin setter exists. +- No pool/token migration entrypoint exists. +- The sprint deployment is non-upgradeable; class replacement is not exposed. +- A configuration change requires a new deployment and therefore a new commitment domain. + +### Auction configuration + +After successful `create_auction`, every `AuctionConfig` field is permanently immutable. + +- No seller, seller claim handle, NFT, token ID, reserve, cap, deadline, or bidder-limit update is allowed. +- No “correct typo” or emergency-admin path exists. +- The only mutable values are lifecycle state and accounting: bid/reveal records, phase-derived status, winner, clearing price, settlement flag, claim-consumed flags, custody state, and conserved balances. +- A lifecycle transition must never rewrite the stored configuration. +- Configuration readback before bidding is the user's final verification point. + +This separation prevents a seller or administrator from changing economic terms after commitments are formed. + +## 5. Validation and write ordering + +The production create path must use this order: + +1. Validate deployment configuration during constructor. +2. On creation, validate caller, ID, seller claim handle, addresses, numeric bounds, price relation, deadline relation, and bidder limit. +3. Reject if `auction_exists[auction_id]` is already true. +4. Reject conflicting active NFT custody. +5. Build/write effects using checks-effects-interactions. +6. Transfer the exact ERC-721 into custody. +7. Verify custody through the transfer result/owner readback permitted by the final Cairo interface. +8. Emit `AuctionCreated` with the complete immutable configuration. + +All steps occur in one Starknet transaction. Any failed external call reverts configuration, existence, and custody-index writes atomically. + +Expected creation event: + +```text +AuctionCreated { + auction_id, + seller, + seller_claim_handle, + nft_contract, + token_id, + payment_token, + pool, + reserve_price, + collateral_cap, + bidding_deadline, + reveal_deadline, + bidder_limit, +} +``` + +The event repeats deployment-wide pool/token values so public evidence can decode one receipt without relying on unstated configuration. Storage remains normalized: pool/token live once in house configuration. + +## 6. Typed preimplementation oracle + +The following files implement and test the frozen model without claiming an on-chain auction exists: + +| File | Role | +| -------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | +| `web/src/features/auction/auctionConfig.ts` | Strict bigint/address/bound validator and frozen TypeScript configuration objects. | +| `web/tests/fixtures/auction-configuration-v2.json` | Canonical machine-readable deployment, auction, seller-claim, type, boundary, identity, and immutability fixture. | +| `web/tests/unit/auctionConfig.test.ts` | Positive, exact `ContractAddress`, seller-authority, integer-boundary, price, deadline, bidder-limit, forged-house, fixture, and immutability tests. | +| This specification | Normative Cairo/storage/event contract for Tasks 3.1 and 3.2. | + +The TypeScript helper is a spec oracle for UI/config readback and cross-layer tests. It does not replace Cairo validation. Task 3 must reproduce every check on-chain and add ABI parity fixtures. + +## 7. Requirement matrix + +| Task requirement | Frozen definition | Verification destination | +| ---------------- | --------------------------------------------------------------- | -------------------------------------------------- | +| Reusable house | One deployment, many auction IDs; immutable pool/STRK/max bound | House config fixture and constructor tests | +| Seller | Non-zero creation caller, stored permanently | Seller mismatch tests and `AuctionCreated` | +| Seller claim | Non-zero felt handle, secret never stored | Felt-boundary fixture and creation/readback tests | +| ERC-721 | Non-zero contract plus full `u256` token ID; zero ID allowed | Boundary fixture and custody tests | +| STRK token | Canonical deployment-wide STRK constant | House validation and deployment readback | +| STRK20 pool | Non-zero deployment-wide address distinct from payment token | Constructor and caller-binding tests | +| Reserve | Non-zero `u128` base units | Boundary tests | +| Cap | Non-zero `u128`, exact bid collateral | Boundary and ingress tests | +| Price relation | `0 < reserve <= cap` | Equal, below, zero, overflow tests | +| Bidding deadline | Future `u64` Unix timestamp | Boundary tests | +| Reveal deadline | `u64` strictly after bidding deadline | Boundary tests | +| Bidder bound | Auction `2..=house.max`, house `2..=32` | Limit and forged-house tests | +| Unique ID | Non-zero explicit `u64`, unique forever per deployment | Duplicate and rollback tests in Task 3.2 | +| Immutability | House and auction config permanently immutable | No-setter ABI review and lifecycle invariant tests | + +## Task 2.1 gate + +- [x] Reusable deployment configuration defined. +- [x] Seller source, address, and immutable claim-handle semantics defined. +- [x] ERC-721 contract and `u256` token ID defined. +- [x] Canonical STRK payment token defined. +- [x] Configured STRK20 pool defined. +- [x] Reserve and uniform cap defined in base units. +- [x] `0 < reserve <= cap` frozen. +- [x] Bidding and reveal deadlines plus exact boundaries frozen. +- [x] Bidding deadline strictly precedes reveal deadline. +- [x] Deployment and per-auction bidder bounds frozen at an absolute maximum of 32. +- [x] Non-zero, caller-supplied, never-reused `u64` auction identity frozen. +- [x] House and auction configuration permanently immutable after successful creation. +- [x] Typed fixture and executable validation tests created. + +**Gate result:** Task 2.1 is frozen at v2. Tasks 3.1 and 3.2 may implement storage, constructor, creation, custody, event, and rollback behavior only if they preserve this v2 configuration contract. + +## Sources + +[1] https://raw.githubusercontent.com/starkware-libs/starknet-privacy/main/packages/privacy/src/utils.cairo — Starknet Privacy utility source +[2] https://raw.githubusercontent.com/starkware-libs/cairo/v2.20.0/corelib/src/starknet/contract_address.cairo — Cairo 2.20.0 ContractAddress corelib diff --git a/docs/evidence/task-2-2-bid-credentials.md b/docs/evidence/task-2-2-bid-credentials.md new file mode 100644 index 0000000..6b7052d --- /dev/null +++ b/docs/evidence/task-2-2-bid-credentials.md @@ -0,0 +1,241 @@ +# CipherBid Task 2.2 — Bid Commitment and Credential Specification + +**Status:** Approved v1 credential baseline + +**Verified:** 2026-08-28 + +**Decision:** CipherBid uses one non-zero bid nonce and one non-zero claim secret per bid. The claim secret derives a one-time public handle. The bid commitment binds the chain, deployment, auction, bid, claim handle, and NFT recipient in one domain-separated Starknet Poseidon hash. No claim signing key, public key, signature, or claim nonce exists in the sprint protocol. + +## 1. Canonical domains + +The domains are one-felt Cairo short strings: + +| Purpose | Literal | Encoded felt | +| -------------- | -------------------- | ---------------------------------------- | +| Claim handle | `CIPHERBID_CLAIM_V1` | `0x4349504845524249445f434c41494d5f5631` | +| Bid commitment | `CIPHERBID_BID_V1` | `0x4349504845524249445f4249445f5631` | + +TypeScript uses `hash.computePoseidonHashOnElements`, whose implementation converts every input to `BigInt` and applies Poseidon hash-many.[3] Cairo uses `poseidon_hash_span` over the same ordered felt sequence. + +## 2. Claim handle + +The formula is exact: + +```text +claim_handle = Poseidon([ + CIPHERBID_CLAIM_V1, + claim_secret, +]) +``` + +Rules: + +- `claim_secret` is a non-zero `felt252`. +- Valid range: `1 <= claim_secret < P`. +- `claim_handle` is stored publicly with the bid and must be non-zero. +- A monetary claim supplies the secret once; the contract recomputes the handle and consumes the corresponding claim state before external interaction. +- The claim secret is not revealed during bid ingress or reveal. +- The claim secret becomes public in the claim transaction calldata when consumed; after confirmed state readback it is terminal and must be discarded from active memory. + +The Stark field range is `0 <= x < P`, where `P = 2^251 + 17 * 2^192 + 1`.[2] + +Exact prime: + +```text +P = 0x800000000000011000000000000000000000000000000000000000000000001 +``` + +## 3. Bid commitment + +The formula and order are exact: + +```text +bid_commitment = Poseidon([ + CIPHERBID_BID_V1, + chain_id, + auction_house, + auction_id, + amount, + bid_nonce, + claim_handle, + asset_recipient, +]) +``` + +| Index | Field | Type | Rule | +| ----: | ----------------- | ----------------- | -------------------------------------------------- | +| 0 | bid domain | `felt252` | Exact `CIPHERBID_BID_V1` short-string felt | +| 1 | `chain_id` | `felt252` | Non-zero; binds Starknet network | +| 2 | `auction_house` | `ContractAddress` | Non-zero; binds deployment | +| 3 | `auction_id` | `u64` | Non-zero; binds one auction in the deployment | +| 4 | `amount` | `u128` | Non-zero; later constrained by auction reserve/cap | +| 5 | `bid_nonce` | `felt252` | Non-zero random bid nonce | +| 6 | `claim_handle` | `felt252` | Non-zero derived claim handle | +| 7 | `asset_recipient` | `ContractAddress` | Non-zero committed ERC-721 recipient | + +No prefix, byte length, array length, trailing zero, public key, signature, or additional field is added. + +## 4. Exact boundaries + +Cairo 2.20.0 core defines `ContractAddress` as `[0, 2^251)` and exposes checked conversion from `felt252`.[1] CipherBid additionally excludes zero for auction house and NFT recipient. + +| Value | Valid range | Maximum valid | First invalid high value | +| ------------------ | -------------------------------- | ----------------------------------------- | ----------------------------------------- | +| Felt inputs | `1..P-1` where non-zero required | `P - 1` | `P` | +| Contract addresses | `1..2^251-1` | `0x7fff...fff` (63 hex `f`s) | `0x8000...000` (`2^251`) | +| `auction_id` | `1..u64::MAX` | `18446744073709551615` | `18446744073709551616` | +| `amount` | `1..u128::MAX` | `340282366920938463463374607431768211455` | `340282366920938463463374607431768211456` | + +TypeScript rejects negative values explicitly. Cairo calldata cannot deserialize out-of-range `u64`, `u128`, or `ContractAddress` values into the typed function. Cairo performs explicit non-zero checks after typed deserialization. + +The address boundary must not be confused with `StorageBaseAddress`, whose narrower limit is unrelated to `ContractAddress`. + +## 5. Canonical vectors + +The machine-readable authority is `web/tests/fixtures/bid-credentials-v1.json`. + +### 5.1 Sepolia reference + +```text +claim_secret = 123456789 +claim_handle = 0x3078725b5aaffe73f545ebca32c0b5a4af14404599edd691c752e59ffca3724 +chain_id = 0x534e5f5345504f4c4941 // SN_SEPOLIA +auction_house = 0x222 +auction_id = 7 +amount = 3000000000000000000 +bid_nonce = 987654321 +asset_recipient = 0x333 +commitment = 0x34fe5ddb49c604d4b8b63f768c4d6e4159bdd4166bdc3e1e7094217c9f6313e +``` + +### 5.2 Minimum valid + +```text +claim_secret = 1 +claim_handle = 0x6b7f8ff6dee712dbd900e4e0269931a6dc86de5359e13dc740ca1898d110b48 +chain_id = 1 +auction_house = 1 +auction_id = 1 +amount = 1 +bid_nonce = 1 +asset_recipient = 1 +commitment = 0x5c8b0026c8ddfd09e47cba64881b66d371c620d84b0e573f811ec2334526848 +``` + +### 5.3 Maximum valid + +```text +claim_secret = P - 1 +claim_handle = 0x51f784d5ce10bdf76e3c632882ba6e181464bd8f4493fd9e7bfc44c6deefd34 +chain_id = P - 1 +auction_house = 2^251 - 1 +auction_id = u64::MAX +amount = u128::MAX +bid_nonce = P - 1 +asset_recipient = 2^251 - 1 +commitment = 0x1dc855fa1871e1425360884f6b03c77837f2c5d47e551f86b55af0e0f8fa1b5 +``` + +Both TypeScript and Cairo assert all three vectors. The valid Sepolia vector remains byte-for-byte unchanged from the earlier spike; the boundary vector was corrected to use the authoritative `ContractAddress` range. + +## 6. Invalid-vector matrix + +The fixture freezes three invalid classes for every credential input where applicable: + +| Field | Low invalid | Zero invalid | High invalid | +| ------------- | -----------------: | -----------: | --------------: | +| Claim secret | `-1` in TypeScript | `0` | `P` | +| Chain ID | `-1` in TypeScript | `0` | `P` | +| Auction house | `-1` in TypeScript | `0` | `2^251` | +| Auction ID | `-1` in TypeScript | `0` | `u64::MAX + 1` | +| Amount | `-1` in TypeScript | `0` | `u128::MAX + 1` | +| Bid nonce | `-1` in TypeScript | `0` | `P` | +| Claim handle | `-1` in TypeScript | `0` | `P` | +| NFT recipient | `-1` in TypeScript | `0` | `2^251` | + +Cairo tests separately prove all reachable typed boundary and non-zero failures. TypeScript tests start each invalid mutation from a fully valid reference input so an unrelated invalid field cannot make the assertion pass accidentally. + +## 7. Credential lifecycle + +### Private before reveal + +- bid amount; +- bid nonce; +- claim secret; +- encrypted recovery password and plaintext during the active recovery operation. + +### Public at ingress + +- bid commitment; +- claim handle; +- auction ID; +- identical collateral cap and transaction timing. + +### Public at reveal + +- amount; +- bid nonce; +- claim handle; +- NFT recipient; +- recomputed commitment relation. + +### Public at claim + +- one-time claim secret in invoke calldata; +- claim handle; +- output note ID and public output amount; +- claim event and consumed state. + +The browser may hold app-specific credentials only in the active memory session and mandatory encrypted recovery operation. Wallet keys, viewing keys, private notes, proofs, signing, and submission remain in the wallet. + +## 8. Superseded design exclusion + +The active protocol contains none of: + +```text +claim_signing_key +claim_private_key +claim_public_key +claim_signature +claim_nonce +CIPHERBID_CLAIM_AUTH_V1 +``` + +The historical local-vault document is explicitly non-normative. Active TypeScript and Cairo source use `bidNonce` / `bid_nonce`, never the ambiguous `bidSecret` / `bid_secret` name. + +## 9. Cross-layer authorities + +| Surface | Authority | +| ------------------------------------------ | -------------------------------------------- | +| Machine-readable vectors and invalid cases | `web/tests/fixtures/bid-credentials-v1.json` | +| TypeScript validation/hash implementation | `web/src/features/auction/commitment.ts` | +| TypeScript executable checks | `web/tests/unit/commitment.test.ts` | +| Cairo validation/hash implementation | `contracts/src/commitment.cairo` | +| Cairo executable checks | `contracts/tests/test_commitment.cairo` | +| Human-readable protocol | This document | + +A deliberate change requires a reviewed fixture version bump and simultaneous TypeScript, Cairo, test, and documentation updates. + +## Task 2.2 gate + +- [x] Chain ID bound into the bid commitment. +- [x] Auction-house address bound into the bid commitment. +- [x] Auction ID bound into the bid commitment. +- [x] Bid amount bound into the bid commitment. +- [x] Non-zero bid nonce bound into the bid commitment. +- [x] Claim handle bound into the bid commitment. +- [x] Non-zero NFT recipient bound into the bid commitment. +- [x] `claim_handle = Poseidon(CIPHERBID_CLAIM_V1, claim_secret)` frozen. +- [x] Non-zero claim secret required. +- [x] Felt, `ContractAddress`, `u64`, and `u128` boundaries frozen. +- [x] TypeScript/Cairo reference, minimum, and maximum Poseidon vectors frozen. +- [x] Invalid and boundary vectors added. +- [x] Superseded claim-signing-key design excluded. + +**Gate result:** one v1 bid-credential contract governs TypeScript, Cairo, fixtures, future recovery, reveal, and claim implementation. + +## Sources + +[1] https://raw.githubusercontent.com/starkware-libs/cairo/v2.20.0/corelib/src/starknet/contract_address.cairo — Cairo 2.20.0 ContractAddress corelib +[2] https://docs.starknet.io/build/corelib/core-felt252 — Starknet felt252 core documentation +[3] https://raw.githubusercontent.com/starknet-io/starknet.js/develop/src/utils/hash/classHash/poseidon.ts — starknet.js Poseidon hash source diff --git a/docs/evidence/task-2-3-lifecycle-specification.md b/docs/evidence/task-2-3-lifecycle-specification.md new file mode 100644 index 0000000..6e9eb37 --- /dev/null +++ b/docs/evidence/task-2-3-lifecycle-specification.md @@ -0,0 +1,71 @@ +# CipherBid Task 2.3 — Lifecycle Specification + +**Status:** Frozen v1 + +## States + +- `BiddingOpen`: `now < bidding_deadline`. +- `RevealOpen`: `bidding_deadline <= now < reveal_deadline`. +- `ReadyToSettle`: `now >= reveal_deadline` and not settled. +- `SettledSold`: a valid revealed bid meets reserve. +- `SettledNoSale`: no valid revealed bid meets reserve. +- `ClaimsComplete`: every positive bidder output and any seller entitlement is consumed; zero winner surplus is auto-consumed. + +Settlement is permissionless and executes once. + +## Winner and price + +1. Consider every successfully revealed bid with `0 < amount <= cap`. +2. Sort by amount descending, then accepted index ascending. +3. The first bid wins only if its amount meets reserve. +4. Ties go to the earliest accepted index. +5. With one valid reveal, clearing price is reserve. +6. Otherwise clearing price is `max(reserve, second_highest_revealed_amount)`. +7. If the highest reveal is below reserve, the auction is no-sale. + +## NFT outcome + +- Sold: transfer the ERC-721 to the winner's commitment-bound recipient. +- No sale: return the ERC-721 to the immutable seller. +- State effects precede the external transfer and the transaction reverts atomically on failure. + +## Collateral outcomes + +CipherBid v1 does not penalize non-reveal. + +- Every non-winning accepted bid receives the full cap, including unrevealed bids and every bid in a no-sale auction. +- Winner receives `cap - clearing_price` when positive. +- A zero winner surplus creates no STRK20 transaction and is auto-consumed at settlement. +- Seller receives exactly `clearing_price` in a sold auction. +- No-sale seller entitlement is zero. + +For `N` accepted bids: + +```text +locked_collateral = N * cap + +sold: + locked_collateral + = seller_entitlement + + winner_surplus + + sum(non_winner_refunds) + +no sale: + locked_collateral = sum(all_bidder_refunds) +``` + +## Claims + +- Each accepted bid has one bidder claim state. +- Each claim handle and commitment is unique per auction. +- Claim secret must recompute the stored handle. +- Claim credentials cannot cross auction, chain, or deployment boundaries. +- Positive claims are consumed exactly once before approval/external interaction. +- Seller claim additionally requires the immutable seller handle and current seller-authorized open-note ID. +- Failed external calls revert claim consumption. + +## Executable authority + +- `web/tests/fixtures/auction-lifecycle-v1.json` +- `web/src/features/auction/auctionLifecycle.ts` +- `web/tests/unit/auctionLifecycleFixture.test.ts` diff --git a/docs/evidence/task-2-4-security-invariants.md b/docs/evidence/task-2-4-security-invariants.md new file mode 100644 index 0000000..5d95f81 --- /dev/null +++ b/docs/evidence/task-2-4-security-invariants.md @@ -0,0 +1,73 @@ +# CipherBid Task 2.4 — Security Invariants + +**Status:** Frozen v1 + +## Trust boundaries + +- Only the immutable configured STRK20 pool may call `privacy_invoke`. +- The resolved `${poolAddress}` argument must equal that configured pool. +- Wallet keys, viewing keys, private notes, witnesses, proof material, signing, and submission remain in the wallet. +- CipherBid stores only public commitments/handles and lifecycle/accounting state. + +## Custody and ingress + +- Auction creation atomically transfers the exact ERC-721 into auction-house custody. +- Bidding cannot begin without custody. +- Bid collateral is accepted only during `BiddingOpen`. +- Incoming collateral is measured by ERC-20 balance delta. +- Every accepted bid increases contract balance by exactly the immutable cap. +- Commitments and claim handles are non-zero and unique within the auction. +- Bidder count never exceeds the immutable bounded limit. +- Malformed calldata or failed token movement advances no state. + +## Reveal and settlement + +- Reveal recomputes the complete domain-separated commitment. +- Chain ID, contract address, auction ID, amount, nonce, claim handle, and NFT recipient are all bound. +- Reveal occurs once and only during `RevealOpen`. +- Settlement occurs once and only at/after reveal deadline. +- Tie-breaking is deterministic by accepted index. +- Settlement iteration is bounded by at most 32 bids. +- Settlement state is written before ERC-721 interaction. +- Reentrancy protection covers every external-call path. + +## Claims and authorization + +- Bidder and seller secrets must recompute stored handles. +- Seller proceeds require the currently seller-authorized open-note ID. +- Seller authorization accepts no amount and can be replaced only before consumption. +- A copied seller secret cannot redirect value to another note. +- Every positive claim is consumed exactly once before ERC-20 approval. +- Pool approval equals the exact returned `OpenNoteDeposit.amount`. +- Zero-value open-note deposits are forbidden. +- Failed approval, pull, or pool processing reverts consumption atomically. + +## Conservation + +After every state transition: + +```text +contract_payment_balance + = locked_collateral + - successfully_claimed_value +``` + +At `ClaimsComplete`: + +```text +contract_payment_balance = 0 +locked_collateral = seller_claims + bidder_claims +``` + +No terminal state may strand unexpected collateral. + +## Implementation rules + +- Checks → effects → interactions. +- Explicit typed errors and complete public events. +- No admin mutation of auction economics or custody. +- No caller-supplied authoritative reserve, cap, clearing price, payout amount, seller, or winner. +- Normalize addresses before comparison in TypeScript. +- Parse monetary values as `bigint`/Cairo integers only. +- Timeout is unconfirmed, never success. +- UI success requires receipt plus state/ownership readback. diff --git a/docs/evidence/winning-product-scope.md b/docs/evidence/winning-product-scope.md new file mode 100644 index 0000000..200e33d --- /dev/null +++ b/docs/evidence/winning-product-scope.md @@ -0,0 +1,94 @@ +# CipherBid Winning Product Scope + +**Status:** Sprint authority + +**Decision date:** 2026-08-27 + +## Product identity + +**CipherBid — private bids, guaranteed onchain delivery.** + +CipherBid is a STRK-only Vickrey auction house for one escrowed ERC-721 per auction. A seller transfers the real NFT into the auction house before bidding. Every bidder locks the same public collateral cap through STRK20 while the bid amount remains sealed in a domain-separated Poseidon commitment until reveal. A sold settlement records the result and transfers the NFT to the winner's precommitted recipient atomically; subsequent claims distribute already-locked collateral through STRK20. + +“Atomic delivery” means settlement-state changes and ERC-721 delivery revert together. It does not mean every monetary claim executes in the settlement transaction. + +## Single approved architecture + +- The browser connects directly to a supported privacy-capable wallet through `WalletAccountV6`. +- The wallet exclusively owns wallet keys, viewing keys, private notes, proof generation, signing, and submission. +- CipherBid may hold app-specific bidder and seller claim credentials only in active browser memory and a mandatory password-encrypted recovery bundle. +- Plaintext credentials never enter browser persistence, clipboard, telemetry, logs, URLs, Git, or any server. +- There is no CipherBid backend, database, local-vault daemon, native bridge, or second transaction path in the sprint product. +- Auction creation, reveal, and permissionless settlement are standard connected-wallet calls. +- Bid ingress and loser, winner-surplus, and seller-proceeds claims route through the configured STRK20 pool. +- A separate `cipherbid-vault` design remains historical post-sprint research only. + +## Required demo lifecycle + +1. Seller generates and recovery-verifies a seller claim credential. +2. Seller approves the exact ERC-721 and creates the auction. +3. Readback proves `owner_of(token_id) == auction_house`. +4. Bidder A and Bidder B use distinct privacy-wallet sessions and submit equal-cap funded commitments. +5. A public observer can inspect terms, cap, timing, count, commitments, and pool/helper interaction without seeing bid amounts before reveal. +6. Both bidders reveal amount, nonce, handle, and NFT recipient. +7. Any connected wallet settles after the reveal deadline. +8. Readback proves the correct winner, Vickrey clearing price, and final NFT owner. +9. The loser claims the full cap through STRK20. +10. The winner claims `cap - clearing_price` through STRK20 when positive. +11. The seller claims clearing-price proceeds plus any explicitly specified forfeiture through STRK20. +12. Final accounting proves zero unexpected collateral. + +## Competitive proof surface + +The public Atomic Delivery Receipt must derive each result from chain data and classify it as **Pass**, **Fail**, or **Unavailable**: + +- reviewed contract/class identity; +- NFT custody before bidding; +- identical cap for both bid ingresses; +- absence of bid amount from decoded ingress fields; +- commitment/reveal consistency; +- winner and second-price calculation; +- final ERC-721 ownership; +- exact loser, winner-surplus, and seller claim outputs; +- live STRK20 pool interaction; +- one-time claim consumption; +- value conservation and zero unexpected collateral. + +The receipt may say that a connected wallet address was not present in the inspected ingress fields. It must not claim universal anonymity or prove a negative beyond the inspected public data. + +## Privacy boundary + +### Hidden before reveal + +- actual bid amount; +- bid nonce; +- bidder claim secret; +- STRK20 note ownership and source linkage. + +### Public + +- seller and NFT identity; +- reserve, cap, deadlines, and bidder limit; +- bid count and timing; +- identical cap transfer; +- commitments and claim handles; +- reveal values and recipient; +- winner and clearing price; +- open-note output amounts; +- transaction timing and helper/pool activity. + +Seller identity is public because auction creation is a standard wallet call. V2 publicly links the seller to the authorized destination note and exposes the clearing-price amount; the STRK20 claim makes subsequent note spending private, not the seller-to-note receipt edge. + +## Explicit exclusions + +The sprint product does not include multi-unit, first-price, Dutch, multiple NFT standards, arbitrary payment tokens, cross-chain execution, AI agents, custom ZK winner proofs, a compliance product, a backend, broad marketplace discovery, or sponsored transactions. + +## Authority order + +When documents conflict, use this order: + +1. this winning product scope; +2. Decision 0002 direct Wallet API route; +3. approved task evidence matrices, with later reviewed versions superseding earlier ones; +4. implementation fixtures and tests; +5. historical design documents, which are non-normative. diff --git a/docs/superpowers/plans/2026-08-24-premium-protocol-console.md b/docs/superpowers/plans/2026-08-24-premium-protocol-console.md new file mode 100644 index 0000000..53000c6 --- /dev/null +++ b/docs/superpowers/plans/2026-08-24-premium-protocol-console.md @@ -0,0 +1,54 @@ +# Premium Protocol Console Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Modernize CipherBid's visual-only auction detail page into a premium dark protocol-console while keeping every product interaction disabled and truthful. + +**Architecture:** Keep `AuctionBidPreview` as the route-level presentational composition. Add a small `ProtocolConsole` presentational component, update the existing chart/page surface tokens, and test semantic contracts rather than CSS implementation details. No chain, wallet, secret, or stateful client module is imported. + +**Tech Stack:** Next.js App Router, React, TypeScript, Tailwind CSS, Vitest, Testing Library, Playwright. + +## Global Constraints + +- The page is visual-only: no RPC, wallet, STRK20 action, transaction, storage, key, or secret flow. +- Unknown onchain state remains an explicit placeholder. +- Preserve the accurate equal-public-cap privacy language. +- Respect reduced motion and retain zero-overflow mobile behavior. +- Follow RED → GREEN → REFACTOR and stage only intended files. + +--- + +### Task 1: Protocol console and dark visual hierarchy + +**Files:** +- Create: `web/src/features/auction/ui/ProtocolConsole.tsx` +- Modify: `web/src/features/auction/ui/AuctionBidPreview.tsx` +- Modify: `web/src/features/auction/ui/SecondPriceIllustration.tsx` +- Modify: `web/src/app/globals.css` +- Modify: `web/src/app/layout.tsx` +- Test: `web/tests/unit/AuctionBidPreview.test.tsx` +- Test: `web/tests/e2e/auction-bid-preview.spec.ts` + +**Interfaces:** +- Produces `ProtocolConsole(): JSX.Element`, a static aria-labelled region with no props and no external dependencies. +- `AuctionBidPreview({ auctionId })` remains the route-level API. + +- [ ] Write a failing unit test requiring `Protocol state`, `Uniform cap collateral`, and `Design preview` in a labelled protocol-console region. +- [ ] Run the focused unit test and confirm it fails because the console does not exist. +- [ ] Add the static presentational component and compose it into the auction layout. +- [ ] Update dark surfaces, hierarchy, focus-visible treatment, and metadata without adding motion or product behavior. +- [ ] Run focused unit tests until green. +- [ ] Add browser assertions for console visibility, desktop layout, and true-mobile ordering. +- [ ] Run focused Playwright tests and confirm green. + +### Task 2: Full verification and delivery + +**Files:** +- Modify: `docs/superpowers/specs/2026-08-24-premium-protocol-console-design.md` +- Modify: `docs/superpowers/plans/2026-08-24-premium-protocol-console.md` + +- [ ] Run formatter, lint, strict TypeScript, unit tests, Playwright, production build, audit, and configured Cairo gates. +- [ ] Verify desktop and true 390px mobile rendering with no overflow, logical keyboard order, and reduced-motion behavior. +- [ ] Stage only the intended UI/docs/test/metadata files and run `git diff --cached --check`. +- [ ] Obtain an independent review of exactly the staged diff; fix all blockers and rerun affected gates. +- [ ] Commit the immutable reviewed slice, push its feature branch, and verify the remote SHA. diff --git a/docs/superpowers/plans/2026-08-27-wallet-connect-panel-styling.md b/docs/superpowers/plans/2026-08-27-wallet-connect-panel-styling.md new file mode 100644 index 0000000..6e3f25b --- /dev/null +++ b/docs/superpowers/plans/2026-08-27-wallet-connect-panel-styling.md @@ -0,0 +1,127 @@ +# Wallet Connect Panel Styling Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Restyle CipherBid’s reusable wallet connector into a compact, modern protocol card without changing Starter-Kit Wallet API behavior or the public-only state boundary. + +**Architecture:** Keep all wallet discovery, connection, state transitions, and event invalidation in `WalletConnectPanel`. Add a local `walletInitial()` display helper and Tailwind-only presentation classes for disconnected, connecting, error, and connected states. The existing bid-preview composition continues to render the same component, so it inherits the style without an adapter or a second connector. + +**Tech Stack:** Next.js 16, React 19, Tailwind CSS 4, Zustand, Vitest, Testing Library, Playwright. + +## Global Constraints + +- Do not request balances or add a private-wallet API call. +- Do not store wallet objects, keys, notes, credentials, or raw wallet errors in Zustand/browser persistence. +- Do not render wallet-provided icon URLs; generate a local initial avatar from the wallet name. +- All interactive controls remain at least 44px tall with visible keyboard focus. +- Existing reduced-motion CSS must continue to disable connector transitions. + +--- + +### Task 1: Add failing visual-state contracts + +**Files:** +- Modify: `web/tests/unit/WalletConnectPanel.test.tsx` +- Modify: `web/tests/unit/AuctionBidPreview.test.tsx` +- Modify: `web/tests/e2e/auction-bid-preview.spec.ts` + +**Interfaces:** +- Consumes: `WalletConnectPanel` existing discovered, connecting, connected, error, and disconnect states. +- Produces: assertions for stable test IDs/classes and bid-page runtime visibility. + +- [ ] **Step 1: Write failing tests** + +Assert a discovered wallet row exposes `data-testid="wallet-option-ready"`, has a local `R` avatar, has the `min-h-11` target class, and remains a semantic button. Assert connected metadata is inside `data-testid="wallet-connected-state"`. Assert the auction route renders `data-testid="wallet-connect-module"` without horizontal overflow. + +- [ ] **Step 2: Run tests to verify RED** + +Run: + +```bash +npx --yes pnpm@10.18.1 --dir web test -- tests/unit/WalletConnectPanel.test.tsx tests/unit/AuctionBidPreview.test.tsx +``` + +Expected: FAIL because the visual-state hooks and local avatar do not exist. + +### Task 2: Implement the compact protocol card + +**Files:** +- Modify: `web/src/features/wallet/WalletConnectPanel.tsx` + +**Interfaces:** +- Consumes: `walletName(wallet): string`, Zustand public state, existing callbacks. +- Produces: `walletInitial(name: string): string` and stable `data-testid` values for disconnected, connected, and wallet-option states. + +- [ ] **Step 1: Add `walletInitial`** + +Return the first uppercase printable character from the wallet name, with `W` as the fallback. Do not read or render `wallet.icon`. + +- [ ] **Step 2: Style each state** + +Apply Tailwind classes to create: + +- dark, fine-border protocol module; +- violet focus/hover wallet rows with local initial avatar and chevron; +- muted empty state; +- connecting status and cancel action; +- warning-toned public error panel; +- compact connected metadata grid with green compatibility chip and wrapped monospace account; +- secondary disconnect action. + +- [ ] **Step 3: Run focused tests to GREEN** + +Run: + +```bash +npx --yes pnpm@10.18.1 --dir web test -- tests/unit/WalletConnectPanel.test.tsx tests/unit/AuctionBidPreview.test.tsx +npx --yes pnpm@10.18.1 --dir web typecheck +``` + +Expected: all focused tests and strict TypeScript pass. + +### Task 3: Prove responsive browser behavior + +**Files:** +- Modify: `web/tests/e2e/auction-bid-preview.spec.ts` + +**Interfaces:** +- Consumes: `/auctions/design-preview` and `data-testid="wallet-connect-module"`. +- Produces: desktop and 390px mobile no-overflow evidence. + +- [ ] **Step 1: Add browser assertions** + +At desktop and 390px viewport widths, assert the connector is visible, the disabled bid CTA remains present, and `document.documentElement.scrollWidth - document.documentElement.clientWidth === 0`. + +- [ ] **Step 2: Run the browser spec** + +Run: + +```bash +npx --yes pnpm@10.18.1 --dir web test:e2e +``` + +Expected: Playwright passes against a runner-owned server. + +### Task 4: Closure + +**Files:** No production changes beyond Tasks 1–2. + +- [ ] **Step 1: Run full web gates** + +```bash +npx --yes pnpm@10.18.1 --dir web format:check +npx --yes pnpm@10.18.1 --dir web lint +npx --yes pnpm@10.18.1 --dir web typecheck +npx --yes pnpm@10.18.1 --dir web test +npx --yes pnpm@10.18.1 --dir web test:e2e +npx --yes pnpm@10.18.1 --dir web build +``` + +- [ ] **Step 2: Verify staged scope** + +```bash +git diff --check +git diff --cached --name-only +``` + +Expected: only wallet-panel styling, its tests, and the approved design/plan documents are staged. diff --git a/docs/superpowers/plans/2026-08-29-github-pages-mainnet-frontend.md b/docs/superpowers/plans/2026-08-29-github-pages-mainnet-frontend.md new file mode 100644 index 0000000..bca002c --- /dev/null +++ b/docs/superpowers/plans/2026-08-29-github-pages-mainnet-frontend.md @@ -0,0 +1,471 @@ +# GitHub Pages Mainnet Frontend Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish CipherBid as a durable no-login GitHub Pages application configured to the verified Starknet mainnet contracts before the final paired Ready X auction lifecycle. + +**Architecture:** Replace the forced-dynamic `/auctions/[auctionId]` server route with an exportable `/auction?id=` client loader. The loader validates the query and build-time public deployment manifest, reads and verifies auction state through public RPC in the browser, then renders the existing wallet/action UI. A pinned, least-privilege GitHub Pages workflow builds and deploys the static export after the source is promoted through `development → staging → main`. + +**Tech Stack:** Next.js 16.3.2 App Router, React 19.2.8, TypeScript 5.9.3, starknet.js 10.4.0, Vitest 3.2.6, Playwright 1.58.2, pnpm 10.18.1, `yaml` 2.9.0, GitHub Pages, GitHub Actions. + +## Global Constraints + +- Execute inline in the canonical checkout; do not dispatch agents or create worktrees. +- Preserve the six unrelated dirty files; never stash, reset, clean, stage, or overwrite them. +- Use strict RED → GREEN for every behavior/configuration slice. +- Use GitHub Pages as primary; use Vercel only after a concrete Pages blocker is verified. +- The public route is `/auction?id=` under Pages base path `/cipherbid`. +- Ready X exclusively owns wallet keys, viewing keys, private notes, note selection, proofs, signing, and submission. +- No shielded-balance probe, secret expression, dotenv loading, runtime evidence, signer data, or recovery payload may enter the bundle or workflow. +- Defer bidder deposits, auction creation, bids, reveals, settlement, claims, transaction registration, and final video until the paired final test with the user. +- Finish and verify all independent source/deployment work, then commit and promote it through `development → staging → main` before that paired test. +- Keep `strk20.json.transactions`, `demo_url`, and `demo_video` empty until their separate public evidence gates pass. +- Pin every external GitHub Action to the following verified immutable SHA: + - `actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1` (`v7.0.1`) + - `actions/setup-node@820762786026740c76f36085b0efc47a31fe5020` (`v7.0.0`) + - `actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d` (`v6.0.0`) + - `actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9` (`v5.0.0`) + - `actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128` (`v5.0.0`) + +--- + +### Task 1: Static Auction Route Contract + +**Files:** + +- Create: `web/src/features/auction/auctionRoute.ts` +- Create: `web/tests/unit/auctionRoute.test.ts` +- Modify: `web/src/app/page.tsx` +- Modify: `web/tests/unit/Home.test.tsx` + +**Interfaces:** + +- Produces: `parseAuctionIdValues(values: readonly string[]): AuctionRouteResult` +- Produces: `buildAuctionHref(value: string): string` +- `AuctionRouteResult` is either `{ ok: true; auctionId: bigint; canonicalId: string }` or `{ ok: false; displayId: string; error: string }`. + +- [ ] **Step 1: Write the failing route tests** + +Require: + +```ts +expect(parseAuctionIdValues(["7"])).toEqual({ + ok: true, + auctionId: 7n, + canonicalId: "7", +}); +expect(parseAuctionIdValues([])).toMatchObject({ ok: false }); +expect(parseAuctionIdValues(["7", "8"])).toMatchObject({ ok: false }); +expect(parseAuctionIdValues(["0"])).toMatchObject({ ok: false }); +expect(parseAuctionIdValues(["18446744073709551616"])).toMatchObject({ + ok: false, +}); +expect(parseAuctionIdValues(["%3Cscript%3E"])).toMatchObject({ ok: false }); +expect(buildAuctionHref("7")).toBe("/auction?id=7"); +``` + +Update `Home.test.tsx` to require `/auction?id=1`. + +- [ ] **Step 2: Run RED** + +Run: + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/auctionRoute.test.ts tests/unit/Home.test.tsx +``` + +Expected: failure because `auctionRoute.ts` does not exist and the home link still uses `/auctions/1`. + +- [ ] **Step 3: Implement the minimal pure route module** + +Use one strict decimal regex, `decodeURIComponent` inside `try/catch`, a maximum display length of 80 characters, an exact one-value requirement, and a `u64` upper bound. `buildAuctionHref` must emit only the canonical query-route shape. + +Update `Home` to use `buildAuctionHref(safeAuctionId)`. + +- [ ] **Step 4: Run GREEN** + +Run the focused Vitest command from Step 2. Expected: both files pass. + +- [ ] **Step 5: Commit the route slice** + +```bash +git add web/src/features/auction/auctionRoute.ts web/tests/unit/auctionRoute.test.ts web/src/app/page.tsx web/tests/unit/Home.test.tsx +git commit -m "feat(web): add static auction route contract" +``` + +### Task 2: Browser Public-Read Loader + +**Files:** + +- Create: `web/src/config/publicDeployment.ts` +- Create: `web/src/features/auction/auctionLiveViewModel.ts` +- Create: `web/src/features/auction/auctionBrowserLoader.ts` +- Create: `web/src/features/auction/ui/AuctionPageClient.tsx` +- Create: `web/src/app/auction/page.tsx` +- Create: `web/tests/unit/publicDeployment.test.ts` +- Create: `web/tests/unit/auctionLiveViewModel.test.ts` +- Create: `web/tests/unit/AuctionPageClient.test.tsx` +- Delete: `web/src/app/auctions/[auctionId]/page.tsx` +- Modify: `web/scripts/create-mainnet-auction.ts` +- Modify: `web/scripts/create-sepolia-auction.ts` + +**Interfaces:** + +- Produces: `loadPublicDeploymentManifest(): DeploymentManifest` using direct `process.env.NEXT_PUBLIC_*` references. +- Produces: `toAuctionLiveViewModel(manifest, snapshot): AuctionLiveViewModel`. +- Produces: `loadAuctionLiveViewModel(auctionId: bigint): Promise`. +- Produces: `AuctionPageClient({ loadModel? })`; the injectable loader exists only for deterministic component tests. + +- [ ] **Step 1: Write public-manifest and view-model RED tests** + +Require direct mainnet manifest mapping and canonical validation. Freeze the existing snapshot-to-view-model fields currently embedded in the dynamic server page. + +Run: + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/publicDeployment.test.ts tests/unit/auctionLiveViewModel.test.ts +``` + +Expected: module-not-found failures. + +- [ ] **Step 2: Implement public manifest and pure view-model conversion** + +`loadPublicDeploymentManifest` passes exactly these direct references into `loadDeploymentManifest`: + +```ts +{ + NEXT_PUBLIC_CIPHERBID_NETWORK: process.env.NEXT_PUBLIC_CIPHERBID_NETWORK, + NEXT_PUBLIC_STARKNET_RPC_URL: process.env.NEXT_PUBLIC_STARKNET_RPC_URL, + NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS: process.env.NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS, + NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH: process.env.NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH, + NEXT_PUBLIC_STRK20_POOL_ADDRESS: process.env.NEXT_PUBLIC_STRK20_POOL_ADDRESS, + NEXT_PUBLIC_STRK_TOKEN_ADDRESS: process.env.NEXT_PUBLIC_STRK_TOKEN_ADDRESS, +} +``` + +Move the current `hex` and `viewModel` conversion logic without semantic changes. + +- [ ] **Step 3: Run the first GREEN** + +Run the focused command from Step 1. Expected: both files pass. + +- [ ] **Step 4: Write AuctionPageClient RED tests** + +Cover: + +- initial loading state; +- one valid `id=7` invoking the injected loader with `7n`; +- verified model rendering; +- invalid and duplicate IDs failing before the loader; +- loader rejection rendering an honest bounded error; +- Retry invoking a new load; +- stale first request unable to overwrite a newer retry result; +- hostile query text rendered inert and capped. + +Run: + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/AuctionPageClient.test.tsx +``` + +Expected: module-not-found failure. + +- [ ] **Step 5: Implement the browser loader and static page** + +`auctionBrowserLoader.ts` constructs `RpcProvider`, adapts it to `ChainReader`, calls `readAuctionSnapshot`, then `toAuctionLiveViewModel`. + +`AuctionPageClient` uses `useSearchParams`, an incrementing request generation, and an effect cleanup guard. Render `AuctionLivePage` only with a verified model. The route page wraps the client in `Suspense` with a visible loading fallback. + +Delete the dynamic server page. Update generated local auction URLs to `http://localhost:4110/auction?id=`. + +- [ ] **Step 6: Run GREEN and affected tests** + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/AuctionPageClient.test.tsx tests/unit/AuctionLivePage.test.tsx tests/unit/auctionReader.test.ts tests/unit/publicDeployment.test.ts tests/unit/auctionLiveViewModel.test.ts +``` + +Expected: all pass. + +- [ ] **Step 7: Commit the browser-loader slice** + +Stage only the files listed in Task 2 and commit: + +```bash +git commit -m "feat(web): load auctions from public RPC in browser" +``` + +### Task 3: Deterministic Static Export + +**Files:** + +- Modify: `web/next.config.ts` +- Create: `web/tests/unit/nextConfig.test.ts` +- Modify: `web/tests/unit/playwrightConfig.test.ts` only if its current assumptions need explicit local-mode coverage +- Modify: `web/tests/e2e/auction-bid-preview.spec.ts` + +**Interfaces:** + +- Produces: `createNextConfig(environment: NodeJS.ProcessEnv): NextConfig`. +- Pages mode is selected only by `CIPHERBID_PAGES_BUILD === '1'`. + +- [ ] **Step 1: Write RED configuration tests** + +Require Pages mode to return: + +```ts +{ + output: 'export', + basePath: '/cipherbid', + trailingSlash: true, +} +``` + +Require local mode to omit export/basePath and preserve the current Playwright-owned Next server. + +Run: + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/nextConfig.test.ts tests/unit/playwrightConfig.test.ts +``` + +Expected: `createNextConfig` is missing. + +- [ ] **Step 2: Implement minimal conditional Next configuration** + +Export `createNextConfig` and default-export `createNextConfig(process.env)`. Reject any non-empty Pages base path other than `/cipherbid`; do not accept a user-controlled runtime path. + +- [ ] **Step 3: Move browser contracts to `/auction?id=…`** + +Update Playwright assertions: + +- home emits `/auction?id=7`; +- hostile input uses `/auction?id=`; +- invalid route returns HTTP 200 with honest unavailable state; +- desktop, 390px mobile, reduced motion, focus order, overflow, and zero console/page errors remain. + +- [ ] **Step 4: Run focused GREEN** + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/nextConfig.test.ts tests/unit/playwrightConfig.test.ts +node node_modules/@playwright/test/cli.js test tests/e2e/auction-bid-preview.spec.ts +``` + +Expected: focused unit and browser suites pass. + +- [ ] **Step 5: Prove the mainnet static export** + +Run with the verified public mainnet values already represented by `.env.local` plus: + +```bash +CIPHERBID_PAGES_BUILD=1 node node_modules/next/dist/bin/next build --webpack +``` + +Expected: exit `0`, `web/out/index.html`, `web/out/create/index.html`, `web/out/demo/setup/index.html`, and `web/out/auction/index.html` exist; no dynamic route is reported. + +- [ ] **Step 6: Commit the static-export slice** + +```bash +git commit -m "feat(web): export CipherBid for GitHub Pages" +``` + +### Task 4: Public Pages Workflow Policy and Deployment Workflow + +**Files:** + +- Modify: `web/package.json` +- Modify: `web/pnpm-lock.yaml` +- Create: `web/src/config/pagesWorkflowPolicy.ts` +- Create: `web/scripts/verify-pages-workflow.ts` +- Create: `web/tests/unit/pagesWorkflowPolicy.test.ts` +- Create: `.github/workflows/deploy-pages.yml` + +**Interfaces:** + +- Produces: `verifyPagesWorkflow(document: unknown): readonly string[]` where an empty result is approval. +- Adds dev dependency `yaml@2.9.0`. +- Adds package script `pages:verify` invoking the policy verifier. + +- [ ] **Step 1: Add `yaml@2.9.0` and write RED policy tests before the workflow** + +The baseline test reads `.github/workflows/deploy-pages.yml` and must fail because it does not exist. Unit fixtures must also reject: + +- `pull_request_target`; +- `secrets.NAME`, `secrets['NAME']`, and `secrets["NAME"]`; +- mutable action tags; +- duplicate action slug with an unapproved SHA; +- `actions/checkout` without `persist-credentials: false`; +- dotenv references; +- artifact path other than `web/out`; +- deployment from a branch other than `main`. + +Run: + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/pagesWorkflowPolicy.test.ts +``` + +Expected: baseline fails because the workflow is missing; adversarial fixture assertions pass as they are added. + +- [ ] **Step 2: Implement the closed workflow verifier** + +Parse YAML with `yaml.parse`, require exact objects/arrays/scalars, enumerate every `uses:` occurrence without dictionary deduplication, compare the full ordered action list to the five approved pins, and return generic policy errors without rendering arbitrary workflow values. + +- [ ] **Step 3: Add the pinned Pages workflow** + +Workflow requirements: + +```yaml +name: Deploy CipherBid Pages +on: + push: + branches: [main] + workflow_dispatch: +permissions: + contents: read +concurrency: + group: pages + cancel-in-progress: false +``` + +Build job: + +- Ubuntu runner; +- checkout pin with `persist-credentials: false`; +- setup-node pin using Node `24.13.1`, pnpm cache, and `web/pnpm-lock.yaml`; +- `corepack enable`; +- `pnpm install --frozen-lockfile` in `web`; +- `pnpm pages:verify`, `pnpm format:check`, `pnpm lint`, `pnpm typecheck`, `pnpm test`; +- build with `CIPHERBID_PAGES_BUILD=1` and the six exact public mainnet values; +- configure-pages pin; +- upload-pages-artifact pin with `path: web/out`. + +Deploy job: + +- depends on build; +- `pages: write` and `id-token: write`, with unspecified permissions `none`; +- GitHub Pages environment and URL output; +- deploy-pages immutable pin. + +- [ ] **Step 4: Run GREEN and adversarial policy suite** + +```bash +node node_modules/vitest/vitest.mjs run tests/unit/pagesWorkflowPolicy.test.ts +node node_modules/tsx/dist/cli.mjs scripts/verify-pages-workflow.ts +``` + +Expected: baseline and every negative fixture pass, verifier exits `0`. + +- [ ] **Step 5: Commit the workflow slice** + +```bash +git commit -m "ci: deploy CipherBid through pinned GitHub Pages workflow" +``` + +### Task 5: Documentation and Complete Pre-Lifecycle Verification + +**Files:** + +- Modify: `README.md` +- Modify: `docs/evidence/README.md` +- Create: `docs/evidence/submission/pages-deployment.md` only after hosted readback succeeds +- Modify: `strk20.json` only after the final real lifecycle route is public; before then leave URL and transaction fields empty + +**Interfaces:** + +- Documents canonical Pages URL, static query route, browser public-read boundary, local commands, and Vercel fallback rule. + +- [ ] **Step 1: Update documentation truthfully** + +Document the canonical route and Pages build command. Explicitly state that no real auction/lifecycle receipt is published before the paired test. + +- [ ] **Step 2: Run complete local Web gates** + +```bash +node node_modules/vitest/vitest.mjs run +node node_modules/@playwright/test/cli.js test +node node_modules/prettier/bin/prettier.cjs --check . +node node_modules/eslint/bin/eslint.js . +node node_modules/typescript/bin/tsc --noEmit --incremental false +CIPHERBID_PAGES_BUILD=1 node node_modules/next/dist/bin/next build --webpack +pnpm audit --audit-level high +``` + +Expected: all exit `0` with `151+` unit/integration tests and all configured Chromium tests passing. + +- [ ] **Step 3: Run repository gates** + +From repository root: + +```bash +scarb fmt --check --manifest-path contracts/Scarb.toml +scarb build --manifest-path contracts/Scarb.toml +snforge test --manifest-path contracts/Scarb.toml +git diff --check +codegraph sync +``` + +Expected: Cairo `25/25`, whitespace clean, index current. + +- [ ] **Step 4: Freeze, scan, and commit the complete implementation** + +Stage only hosting/frontend/evidence files, record the staged binary SHA-256, require zero forbidden paths and zero secret candidates, then commit: + +```bash +git commit -m "feat: publish durable CipherBid mainnet frontend" +``` + +### Task 6: Promote to Main and Verify GitHub Pages + +**Files/State:** + +- Git refs and GitHub PRs only; preserve working-tree dirty files. +- GitHub Pages repository setting. +- Public deployment evidence after readback. + +- [ ] **Step 1: Push and merge feature branch into `development`** + +Fetch first, bind the exact feature SHA, push without force, open a PR targeting `development`, verify file count/checks/mergeability, merge, and read back the exact remote merge SHA/tree. + +- [ ] **Step 2: Promote `development → staging` with a target-based disposable branch if histories are noisy** + +Compare two-dot tree delta first. Do not push directly to `staging`. Preserve durable branches and `delete_branch_on_merge: false`. + +- [ ] **Step 3: Promote `staging → main` through review** + +Use the same target-based promotion rule, merge only the intended tree delta, and verify `main` contains the exact approved source tree. + +- [ ] **Step 4: Enable Pages workflow mode and verify hosted run** + +Create/update Pages through the GitHub API with workflow build mode. Read back: + +- Pages API status and canonical URL; +- successful workflow run and deployment environment; +- exact deployed `main` SHA; +- public HTTP 200 for root, `/create/`, `/demo/setup/`, and `/auction/?id=1`; +- correct headings and an honest unavailable state for auction `1`; +- no-login browser rendering, base-path assets, mobile width, no overflow, and no console/page errors. + +- [ ] **Step 5: Publish secret-free Pages evidence** + +Create `docs/evidence/submission/pages-deployment.md` with exact public URL, deployed SHA, workflow run URL, verification timestamp, and route checks. Do not add lifecycle hashes or call auction `1` real. + +- [ ] **Step 6: Commit and promote the evidence-only follow-up to `main`** + +Use the same branch flow and verify the Pages site redeploys from the evidence commit. Keep `strk20.json.demo_url` empty until the final paired lifecycle proves the real auction route. + +## Final Paired Test Boundary + +After all tasks above are complete and `main` is public, stop before any wallet-controlled write. The final session with the user performs, in order: + +1. both Ready X `24 STRK` deposits and ten-block maturity; +2. short-window auction creation; +3. Bidder A `2 STRK` ingress; +4. Bidder B `3 STRK` ingress; +5. both encrypted-recovery reveals; +6. settlement and NFT owner readback; +7. loser refund, winner surplus, and economically viable seller proceeds; +8. lifecycle/value-conservation evidence; +9. public Atomic Delivery Receipt; +10. video recording/publication; +11. verified qualifying hashes, durable URL, and video URL in `strk20.json`; +12. final gates, hub readback, commit, promotion to `main`, and hackathon submission handoff. diff --git a/docs/superpowers/specs/2026-08-24-cipherbid-vault-custody-design.md b/docs/superpowers/specs/2026-08-24-cipherbid-vault-custody-design.md new file mode 100644 index 0000000..d942efd --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-cipherbid-vault-custody-design.md @@ -0,0 +1,189 @@ +# CipherBid Vault — Bid-Credential Custody Design + +**Status:** Approved architecture; documentation-only slice. +**Scope:** Resolve custody for the Vickrey bid nonce and claim authority. This does not implement a vault binary, modify Cairo, enable bidding, or submit a transaction. + +## Decision + +CipherBid will use a **user-operated local CLI**, `cipherbid-vault`, to own and use all auction-specific secret material outside the browser application. The web dapp stays a public-data-only reader and status surface. + +The vault is an explicitly user-operated **dedicated privacy-account companion**. It owns a separate Starknet execution account, a separate STRK20 viewing key, and private notes sent to that account. It retains the auction bid nonce only in the active profile so it can reveal. Each bid has a separate claim signing key retained only in a mandatory encrypted offline claim bundle. The vault creates, protects, recovers, and uses these values locally to submit bid ingress, reveal, and claim operations itself. It never imports the user's normal wallet key or viewing key. The browser does not submit auction actions through the Wallet API. + +## Why this boundary + +The installed Wallet API surface (`@starknet-io/types-js` 0.10.3) provides `wallet_strk20PrepareInvoke` and `wallet_strk20InvokeTransaction` over dapp-provided action arrays. It has no reviewed method to create, retain, or later use an app-specific bid nonce or claim credential. Any browser-built action can also be altered by a compromised website before the wallet prompt, so a public envelope cannot safely authorize equal-cap collateral ingress by itself. + +Using the low-level Privacy SDK in the browser would require the app to manage a viewing key and a signing key, which contradicts CipherBid's non-custodial browser invariant. The same SDK is acceptable inside the separately distributed, user-operated vault because the vault is the explicit dedicated-account custodian. A browser-generated encrypted recovery file remains rejected: the app would still receive plaintext before encryption. + +## Actors and trust boundaries + +| Actor | May hold | Must never hold | +| --- | --- | --- | +| CipherBid web app | public auction descriptor, public receipt/status, transaction hashes | bid amount, bid nonce, claim private key, vault master key, any viewing key, recovery plaintext | +| User's normal privacy wallet | its own signing key, viewing key, and notes | CipherBid vault key material or vault-private notes | +| CipherBid Vault CLI | vault execution-account key, vault viewing key, vault-private notes, bid amount, bid nonce, asset recipient, public claim key, encrypted records; offline claim private key only in an ephemeral claim process after local bundle decryption | user wallet private key, user wallet viewing key, browser session data | +| Offline claim bundle | claim private key and encrypted profile/credential recovery material | browser session data, CipherBid servers, telemetry systems | +| CipherBid auction contract | commitment, claim public key/handle, public lifecycle state, reveal data, used-claim state | unrevealed bid amount, bid nonce before reveal, claim private key | +| RPC / optional future relay | public signed transaction and public reveal/claim calldata | vault records, wallet viewing key, pre-reveal bid nonce | + +The vault's dedicated account is distinct from the user's normal wallet. It signs STRK20 actions with the Privacy SDK and public reveal/claim calls. Its address is public whenever it performs a direct public lifecycle call and may correlate activity; CipherBid must disclose that limitation. Private notes held by the vault are user-controlled local custody, not browser or CipherBid-server custody. + +## Credential model + +Each vault credential is bound to exactly one chain, auction-house deployment, and auction ID. + +### Private credential record + +```text +record_version +vault_profile_id +auction_descriptor_hash +bid_amount_base_units: u128 +bid_nonce: non-zero Stark field element +asset_recipient: non-zero canonical contract-address felt +claim_public_key +claim_bundle_id +commitment +created_at +state: created | ingress_submitted | revealed | claimed | cancelled +``` + +- `bid_nonce` is generated by the vault using the operating system CSPRNG. It and the bid amount are revealed during the auction reveal phase; the claim private key is not. +- The offline claim private key authorizes settlement/refund claims. It replaces the old reusable `claimSecret` preimage model. +- `claim_public_key` is recorded on chain during bid ingress. A later Cairo revision derives a domain-separated claim handle from it and verifies a domain-separated Stark-curve signature for each claim. +- The claim private key is created once, exported and import-verified in an encrypted offline claim bundle before bid ingress, then deleted from the active VaultProfile. It is deliberately unavailable to normal bid and reveal commands; the user must decrypt the bundle locally for each claim. +- The browser receives neither private field nor bid amount. + +### Public auction descriptor + +The web app may create a transportable descriptor containing only public data: + +```text +schema_version, chain_id, auction_house, expected_class_hash, auction_id, +pool, token, collateral_cap, seller, nft_contract, token_id, +bidding_deadline, reveal_deadline, descriptor_hash +``` + +Before creating or using any credential, the vault independently queries the configured Starknet RPC endpoints and rejects a descriptor unless the chain ID, class hash, auction configuration, cap, asset, and phase match. A high-assurance release will require agreement from two configured read-only RPC endpoints. The vault displays the verified human-readable target before the user confirms. + +### Public vault receipt + +After a successful, independently read-back vault submission, the vault may emit a secret-free receipt for display in the web app: + +```text +schema_version, descriptor_hash, commitment, transaction_hash, +observed_status, receipt_checksum +``` + +This receipt never authorizes an action. The web app can only display it or independently read the public chain status; it cannot trigger bid ingress, reveal, or claim operations. + +## User flows + +### Bid ingress + +1. The web app may display/export a public auction descriptor, or the user supplies an auction address/ID directly to the vault. +2. The user runs the vault locally. The vault independently verifies the auction, deployment class hash, and phase through its configured RPCs, then collects the sealed bid through the terminal—not the web page. +3. The vault collects and displays the NFT asset recipient through the terminal, validates it as a non-zero canonical Starknet contract address, then generates and encrypts the credential. The sealed bid and recipient never enter the web app. +4. The vault constructs the canonical equal-cap ingress action with the Privacy SDK, proves it against finalized state, and submits it from the vault profile. +5. The vault marks the credential `ingress_submitted` only after an independently read-back successful receipt and helper-state check. +6. The vault may emit a public receipt. The browser can display it but cannot use it to submit an action. + +### Reveal and claim + +1. The user invokes `cipherbid-vault reveal` or `cipherbid-vault claim` directly. +2. The vault re-verifies chain, deployment class hash, auction phase, commitment, claim public key, expected claim nonce, and credential state. +3. For reveal, the vault reads the active bid nonce and sealed amount locally, constructs the reveal call, and signs/submits it with its dedicated execution account. +4. For a claim, the vault decrypts the selected offline claim bundle locally, verifies its descriptor/profile/claim-public-key binding, loads the claim private key only for this process, constructs the exact `CIPHERBID_CLAIM_AUTH_V1` message and `[r, s]` signature, then signs/submits the direct claim call with its execution account. It clears the decrypted claim-key buffer immediately after signing and never writes it to the active profile, logs, stdout, clipboard, temp storage, or browser transport. +5. The vault reports only public receipt data to the user. The web app may later read public chain status but is not in the secret-bearing path. + +The user funds the vault profile independently of the CipherBid website. Initial public funding and account deployment can be linkable; after the vault profile is registered, a normal privacy wallet can privately transfer matured STRK20 notes to the vault account through the wallet's trusted user interface. The CipherBid website must not create or direct that funding transfer. + +Before accepting a bid, the vault creates a **funding plan** from live read-only state. It must verify the vault account address and registered viewing key, mature private-note balance, pool fee returned by the pool, public fee-token balance, and auction deadlines. `ingress_fee` is a live private-ingress simulation at a block no more than three blocks behind latest. `reveal_fee` and `claim_fee` come from the audited `VaultFeeBounds` manifest for the exact chain ID, auction-house class hash, compiler version, fee token, and contract ABI; the manifest records bounded successful lifecycle estimates and expires after 30 days or on any listed identity change. The public fee reserve is exactly `ceil(3 * (ingress_fee + reveal_fee + claim_fee) / 2)` in the fee token. The vault rejects the bid if the required manifest is missing or expired. It separately requires mature private STRK sufficient for the canonical cap plus the sum of `get_fee_amount` for every planned STRK20 operation in the audited contract manifest. It rejects bid creation when any prerequisite is missing, the live estimate is unavailable, the reserve is insufficient, or fewer than 15 minutes remain in the current phase. It rechecks fee reserve, private balance, deadline, and manifest identity immediately before each later reveal or claim. This reduces, but cannot eliminate, chain outage and fee-spike risk; the UI must never describe it as a transaction-delivery guarantee. + +A future non-custodial relay may be supported only behind a separately reviewed interface. It must receive no local vault record and must not become a prerequisite backend for settlement. Direct execution-account submission is the MVP reference path. + +## Storage and recovery requirements + +### Local storage and profile lifecycle + +The MVP vault is Windows-first and uses a current-user-bound OS protection mechanism (DPAPI) through a maintained library; it must not implement cryptography itself. + +- A `VaultProfile` contains the execution-account private key, viewing key, account address, registration status, fee-token reserve policy, and creation metadata. It is protected and recovered with the same controls as bid records. It never retains an offline claim private key after bundle verification. +- Records are stored outside the repository under the user's local application-data directory. +- Every protected record uses authenticated encryption and binds the record version plus `auction_descriptor_hash` as associated context. +- Writes are atomic, process-locked, and permission-restricted to the current user. +- The vault never writes plaintext credentials to logs, stdout, shell history, crash reports, clipboard, temp files, URLs, or analytics. +- A public receipt is the only normal stdout artifact. + +A profile cannot rotate while it owns mature notes or unsettled auction credentials. Rotation requires creating a new profile, moving remaining private funds through a reviewed private-transfer flow, settling all credentials, then marking the old profile retired. + +Compromise handling has explicit limits: + +- **Execution-account or active-profile compromise, claim bundle intact:** the attacker cannot authorize auction claims because the claim private key is not in the active profile. Stop using the profile, preserve evidence, and restore the claim bundle in a clean environment for auction claims. Do not promise recovery of private notes held by the compromised vault account: without an account-key rotation/revocation primitive, they are treated as compromised. The attacker may still reveal a bid if they obtained its active bid nonce. +- **Offline claim-bundle or full-profile compromise:** there is no cryptographic containment. Stop using the profile, preserve evidence, and treat all vault-held notes and unsettled credentials as potentially lost or contested. This residual risk is disclosed before funding and bid creation. + +### Offline recovery + +Recovery export/import is CLI-only. Every bid requires an interactive write of the new or updated claim bundle and a separate import verification before the vault deletes that bid's claim private key from active memory and permits ingress. A complete recovery export contains the selected VaultProfile, each offline claim private key, and selected credential records encrypted with a maintained, audited format and an interactive passphrase; it is never routed through the web app. The implementation must use a memory-hard KDF and authenticated encryption supplied by the chosen library, verify profile, descriptor, claim-public-key, and asset-recipient bindings on import, and warn that anyone holding the exported bundle plus passphrase can control the vault profile and claim auction proceeds. + +No cloud sync, email recovery, server escrow, or automatic backup is permitted. + +## Required contract revision + +Before the vault can be implemented, the auction contract design must replace `claimSecret` with offline claim-key authorization. + +### Canonical field, key, and hash rules + +- The Stark field modulus is `P = 0x0800000000000011000000000000000000000000000000000000000000000001`. Every felt is an integer in `[0, P)`; strings, padded byte encodings, negative integers, and values `>= P` are rejected. +- The Stark-curve scalar order is `N = 0x0800000000000010ffffffffffffffffb781126dcae7b2321e66a241adc64d2f`. Every private scalar and signature coordinate is in `[1, N)`. The signer normalizes `s` to `min(s, N - s)` and Cairo rejects non-low-`s` signatures where `s > floor(N / 2)`. +- `claim_public_key` is exactly the canonical Stark-curve public-key x-coordinate derived from the claim private scalar. It is serialized as one felt, must be non-zero, and must validate under the same Stark-curve verifier used by the contract. +- `PoseidonMany(values)` is Starknet Poseidon hash-many, not repeated pair hashing: rate `2`, capacity `1`; append one felt `1`, append zero felts until the length is a multiple of two, absorb consecutive two-felt blocks into a zero initial state, apply the canonical Starknet Poseidon permutation after every block, and return state element `0`. TypeScript uses `starknet@10.4.0` `hash.computePoseidonHashOnElements`; Cairo uses the corresponding Starknet Poseidon span primitive. A disagreement with the frozen vectors is a release blocker. +- Domain tags are the one-felt short-string encodings of `CIPHERBID_CLAIM_HANDLE_V1`, `CIPHERBID_BID_V1`, and `CIPHERBID_CLAIM_AUTH_V1`; no UTF-8 bytes, prefixes, or trailing fields are added. + +1. Bid ingress stores a non-zero `claim_public_key` and a domain-separated `claim_handle = PoseidonMany([CIPHERBID_CLAIM_HANDLE_V1, claim_public_key])`. +2. The bid commitment is exactly `PoseidonMany([CIPHERBID_BID_V1, chain_id, auction_house, auction_id, bid_amount, bid_nonce, claim_handle, asset_recipient])`. `auction_id` is the direct `u64 → felt` integer conversion; `bid_amount` is the direct `u128 → felt` integer conversion; addresses use canonical `ContractAddress.into()` felts. +3. Reveal validates the commitment using the vault-held bid nonce. +4. The contract initializes `claim_nonce: u64 = 0` for every accepted commitment. Claim kinds are fixed field values: `BIDDER_REFUND = 1` and `WINNER_SURPLUS = 2`; a seller claim is authorized independently by the seller configuration and never accepts a bidder claim signature. +5. A claim message hash is exactly `PoseidonMany([CIPHERBID_CLAIM_AUTH_V1, chain_id, auction_house, auction_id, commitment, claim_kind, recipient, claim_nonce])`. Every item is one canonical felt: chain ID is its Starknet felt constant, addresses are their canonical `ContractAddress.into()` felt, `auction_id` and `claim_nonce` are non-negative integer-to-felt conversions, and `claim_kind` is one of the fixed values above. +6. The vault signs that single felt with Stark-curve ECDSA. The wire signature is exactly `[r, s]`: two non-zero canonical Stark scalars, no DER/byte encoding, recovery ID, prefix, or trailing elements. Cairo reconstructs the same Poseidon hash and calls `core::ecdsa::check_ecdsa_signature(message_hash, claim_public_key, r, s)`; `false`, any scalar outside `[1, N)`, or high `s` reverts. +7. Before any external token/NFT interaction, the contract verifies the auction phase and eligible claim kind, requires `supplied_nonce == stored_nonce`, marks that claim kind consumed, increments the stored nonce, then performs the interaction. A signature for one claim kind cannot authorize another, and a stale signature fails the nonce check. + +TypeScript, Cairo, and vault implementation must freeze cross-language vectors for the exact message hash and `[r, s]` signature shape before a deployment test. Any signature verifier discrepancy is a release blocker. + +## Explicit non-goals + +- No localhost HTTP/WebSocket server, browser extension bridge, native-messaging integration, or automatic clipboard integration. +- No browser secret generation, `localStorage`, IndexedDB, cookies, service workers, analytics, or server storage for credentials. +- No import of a user's existing wallet seed, Starknet account key, or viewing key into the vault. +- No custom prover. The vault uses the maintained Privacy SDK only for its dedicated account, note discovery, private balance, and STRK20 note custody. +- No live bid/reveal/claim UI or network write until the vault and the authenticated contract path are implemented and independently reviewed. + +## Threat model and limits + +| Threat | Required mitigation | Residual risk | +| --- | --- | --- | +| Compromised website or malicious browser extension | no vault server/API and no browser-submitted auction action; vault independently verifies descriptor and submits ingress itself | attacker can still trick the user into choosing a different public descriptor; vault confirmation must make target clear | +| Malicious RPC | check chain ID, deployment class hash, full auction config; use two RPCs in high-assurance mode | correlated RPC failure can still mislead the vault | +| Lost device/profile | mandatory encrypted offline claim bundle includes the vault profile and credentials | loss of both device/profile and claim bundle loses control of vault notes and ability to reveal/claim | +| Current-user malware | OS-bound storage, signed binary, least privilege | malware running as the unlocked current user is out of scope | +| Execution-account correlation | use a dedicated profile; disclose direct ingress/reveal/claim account visibility in product UX | calls from the same account may be linkable | +| Insufficient gas or private balance | mandatory funding plan, summed lifecycle-fee reserve, live pool-fee readback, maturity/deadline checks, and pre-submit rechecks | fee spikes, RPC failure, proving failure, and network outage can still prevent timely submission | +| Secret leakage | allowlist diagnostics; redaction tests; no secret-bearing browser transport | user can deliberately expose secrets through unsafe manual handling | + +## Acceptance criteria before enabling real bidding + +1. A written vault protocol and threat model are independently reviewed. +2. The vault has deterministic public-receipt and `CLAIM_AUTH_V1` vectors shared with Cairo, including exact Poseidon field order, claim-kind values, message hash, and two-felt `[r, s]` verifier serialization. +3. Tests demonstrate that public receipt serialization cannot contain bid amount, bid nonce, claim private key, recovery plaintext, viewing key, execution-account private key, or vault master key. +4. Tests demonstrate descriptor mismatch, wrong chain, wrong class hash, stale phase, duplicate credential, profile rotation with open obligations, insufficient gas/private balance, expired safety window, wrong claim kind/recipient/nonce rejection, malformed signature rejection, and replayed claim rejection. +5. Static and runtime tests prove no localhost listener, telemetry, clipboard write, browser secret transport, or browser-submitted auction action exists. +6. Secret-scanning tests cover logs, error values, panic paths, persistence, and export/import. +7. The authenticated claim contract and exact balance-delta accounting are implemented, audited, and exercised on Sepolia before funds are accepted. +8. The product UI remains disabled until all of the above pass. + +## Sources checked + +- STRK20 Wallet API overview and private-DeFi guidance: wallet owns viewing keys, notes, proving, and submission; dapps describe actions. +- Installed `@starknet-io/types-js` 0.10.3 declarations: `wallet_strk20PrepareInvoke` and `wallet_strk20InvokeTransaction` accept action arrays but expose no credential-vault or custom-secret method. +- STRK20 Privacy SDK guidance: direct SDK use requires a viewing-key provider and signing account, which is intentionally excluded from the CipherBid browser. +- `docs/evidence/sepolia-feasibility.md`: live feasibility remains blocked until an approved isolated custody boundary and authenticated claims exist. diff --git a/docs/superpowers/specs/2026-08-24-premium-protocol-console-design.md b/docs/superpowers/specs/2026-08-24-premium-protocol-console-design.md new file mode 100644 index 0000000..ec9e915 --- /dev/null +++ b/docs/superpowers/specs/2026-08-24-premium-protocol-console-design.md @@ -0,0 +1,37 @@ +# CipherBid Premium Protocol Console Design + +**Status:** Approved by user on 2026-08-24 + +## Goal + +Refresh the visual-only auction detail route into a premium dark protocol-console experience while preserving CipherBid's public-data-only boundary and every truthful privacy disclosure. + +## Visual direction + +The route uses a cool near-black, layered-surface system inspired by precise protocol tooling rather than a warm marketplace. Indigo remains reserved for CipherBid and primary mechanism emphasis; green is reserved for active-state indicators. The page has no dependency on a brand asset, remote font, or image. + +## Composition + +- A compact dark header identifies CipherBid, the selected auction route, and read-only preview status. +- The hero combines the sealed-NFT artwork with a compact protocol-console panel that describes only known protocol facts: auction mode, collateral rule, settlement rule, and deployment state. +- The bid panel remains disabled and visibly non-operational. It continues to say that it will not make a wallet request or transaction. +- Existing fact cards, Vickrey explanation, privacy boundary, chain evidence, and illustrative second-price chart remain present but use shared dark surfaces and tighter visual hierarchy. + +## Data truthfulness + +The UI must not invent a collection, token ID, reserve, cap, time, bid, account, contract, or transaction. Unknown values remain em dashes or `Awaiting chain data`; the protocol console uses qualitative facts only. It performs no RPC request, wallet action, transaction submission, secret generation, storage access, or client-side time calculation. + +## Accessibility and responsive rules + +- Semantic headings, landmarks, labels, focus visibility, and keyboard order remain intact. +- The protocol console is an aria-labelled region and status is text as well as color. +- Desktop retains the sticky bid card; mobile stacks the protocol console after the lot and before the bid card, with no horizontal overflow. +- No new animation is introduced. The existing reduced-motion guarantee remains comprehensive. + +## Acceptance criteria + +1. `/auctions/[auctionId]` renders a visible `Protocol state` console with mechanism-only data and a `Design preview` disclosure. +2. Existing disabled controls, truthful placeholders, privacy text, chain evidence, and illustrative chart remain. +3. Desktop, true 390px mobile, reduced-motion, keyboard order, and route-id inertness pass browser verification. +4. No wallet/STRK20/secret-related production code changes occur. +5. Only intended UI, UI tests, docs, and metadata paths are staged. diff --git a/docs/superpowers/specs/2026-08-27-wallet-connect-panel-design.md b/docs/superpowers/specs/2026-08-27-wallet-connect-panel-design.md new file mode 100644 index 0000000..e2d96d7 --- /dev/null +++ b/docs/superpowers/specs/2026-08-27-wallet-connect-panel-design.md @@ -0,0 +1,54 @@ +# Wallet Connect Panel Visual Design + +**Date:** 2026-08-27 +**Status:** User-approved visual direction; awaiting written-spec review before implementation. +**Scope:** Restyle `WalletConnectPanel` wherever it appears, including the auction bid card. Preserve its Starter-Kit Wallet API behavior, public-only state boundary, and existing accessibility semantics. + +## Goal + +Make the wallet connector feel like a compact, premium protocol module that belongs inside CipherBid’s dark auction interface, while keeping connection behavior explicit and trustworthy. + +## Visual system + +- Use the existing near-black layered surfaces, fine white-alpha borders, restrained Starknet-violet focus treatment, and green only for confirmed compatibility. +- Keep the component compact enough to sit above the bid amount field without displacing key auction information. +- Do not use external wallet-supplied icons. A wallet name’s first letter supplies a local visual avatar, avoiding a third-party image request or unsafe icon URL. +- Preserve reduced-motion behavior: any hover/focus transition is already disabled by `.cipherbid-auction-page` under `prefers-reduced-motion`. + +## Disconnected and discovered-wallet state + +- Add a small `Wallet access` eyebrow, explanatory copy, and a bordered wallet-list group. +- Each discovered wallet becomes a full-width, 44px-or-larger button with: + - local initial avatar; + - wallet name; + - `Wallet API check` supporting label; + - non-semantic chevron; + - violet hover/focus state with a visible focus ring. +- Empty state is a compact muted panel that tells the user to install or unlock a privacy-capable wallet without implying that no wallet is supported. +- Connecting state replaces the list’s actionable affordance with a clear status panel and a visible cancel button. + +## Connected state + +- Show a concise success heading and green `STRK20 compatible` chip. +- Render wallet, account, chain, and Wallet API versions in a compact metadata grid. +- Display the account in a monospace, safely wrapping value block. +- Render a restrained secondary `Disconnect wallet` action with the same 44px target and focus rules. + +## Error state + +- Keep the controlled public error string in `role="alert"`. +- Add a warning-toned surface and border; do not render raw wallet exceptions. +- Keep discovered-wallet controls available after a connection error so users can retry or select another wallet. + +## Non-goals + +- No bid input, transaction preparation, balance read, wallet-private state, browser persistence, third-party icon fetch, telemetry, or new dependency. +- No animation, glassmorphism, marketing gradient, or visual data pretending to be live chain state. + +## Acceptance criteria + +1. The bid page and feasibility page both render the same styled connector. +2. Discovered-wallet buttons, cancel/disconnect controls, error alert, and connected metadata retain their existing accessible roles/names and keyboard behavior. +3. Button targets are at least 44px high, and account text does not cause horizontal overflow at 390px viewport width. +4. Unit tests assert the added visual-state hooks and preserve current connection safety behavior. +5. Browser checks prove the connector is visible on `/auctions/[auctionId]`, does not create overflow, and preserves the disabled bid CTA before Task 2. diff --git a/docs/superpowers/specs/2026-08-29-github-pages-mainnet-frontend-design.md b/docs/superpowers/specs/2026-08-29-github-pages-mainnet-frontend-design.md new file mode 100644 index 0000000..5652eac --- /dev/null +++ b/docs/superpowers/specs/2026-08-29-github-pages-mainnet-frontend-design.md @@ -0,0 +1,166 @@ +# CipherBid GitHub Pages mainnet frontend design + +**Status:** Approved direction; implementation awaits written-spec review +**Primary host:** GitHub Pages +**Fallback:** Vercel only after a concrete Pages build, deployment, public-RPC, or wallet-runtime blocker is verified + +## Goal + +Publish a durable, public, no-login CipherBid frontend from the existing repository. The deployed app must use the verified Starknet mainnet AuctionHouse and STRK20 configuration, render live auction state from public RPC reads, and continue handing proving, note discovery, signing, and submission to Ready X. + +The durable public auction URL becomes: + +```text +https://sourcesenseitherealone.github.io/cipherbid/auction?id= +``` + +This replaces the server-only `/auctions/[auctionId]` route for the public application. + +## Why GitHub Pages + +GitHub is already authenticated, the repository is public, and Pages can provide a durable no-login URL without introducing hosting credentials. The current auction route is forced dynamic and performs its RPC read on the Next.js server, so it cannot be exported unchanged. A static auction shell with browser-side public RPC reads removes that server requirement without moving any secret into the browser. + +Vercel remains a fallback, not a parallel deployment. Switch only after recording a reproducible Pages blocker. A Vercel fallback requires user authentication and must be independently deployed and read back before publication. + +## Architecture + +### Static deployment configuration + +`web/next.config.ts` will support a Pages build through explicit environment configuration: + +- `output: 'export'` for the Pages production build; +- `basePath: '/cipherbid'` for the repository-scoped URL; +- `trailingSlash: true` for static directory routing; +- no server actions, API routes, middleware, image optimizer, or runtime secret dependency. + +Local development and Playwright retain an empty base path. The base path is compile-time configuration, not user input. + +All internal links use Next `Link` or one shared route helper so `/cipherbid` is applied exactly once. Historical evidence containing old localhost `/auctions/` URLs remains unchanged and explicitly historical. + +### Static auction route + +Create one exportable route: + +```text +/auction?id= +``` + +The route renders a client loader inside a Suspense boundary. The loader: + +1. reads exactly one `id` query value; +2. percent-decodes and validates it as a positive decimal `u64`; +3. loads a build-time public deployment manifest through direct `NEXT_PUBLIC_*` property references; +4. creates a browser `RpcProvider` for the configured public RPC URL; +5. calls the existing `readAuctionSnapshot` validation path; +6. converts the verified snapshot into the existing `AuctionLiveViewModel`; +7. renders `AuctionLivePage` only after successful class, pool, token, auction, bid, and NFT-owner readback. + +Invalid IDs, duplicate query values, malformed configuration, CORS/RPC failures, class mismatches, missing auctions, and read failures render a bounded honest error state with no fabricated values. The query value is rendered only as React text and is capped before display. + +The loader owns a visible loading state and a manual retry action. It does not poll private state. After a wallet transaction, users can retry the public read or reload the route; transaction success remains receipt-and-readback based inside the existing action flow. + +### Route migration + +The forced-dynamic `web/src/app/auctions/[auctionId]/page.tsx` cannot coexist with a generic static export and will be removed after its pure logic is extracted: + +- auction-ID parsing moves to a small tested route module; +- snapshot-to-view-model conversion moves to a pure tested module; +- the home “Open auction” link changes to `/auction?id=`; +- current route and hostile-input browser tests move to the static query route; +- generated mainnet auction evidence uses the new route shape where it emits a user-facing URL. + +No compatibility redirect is claimed for arbitrary `/auctions/` paths because GitHub Pages cannot return an application-owned HTTP 200 for unknown dynamic paths. Public documentation and new evidence use only the canonical query route. + +## Public deployment manifest + +The Pages workflow builds with these public values: + +- network: Starknet mainnet; +- public RPC URL: the existing public mainnet endpoint; +- verified AuctionHouse address and class hash; +- canonical mainnet STRK20 pool; +- canonical STRK token; +- base path `/cipherbid`. + +These values are public deployment identity, not secrets. The workflow must not receive signer material, Ready X state, recovery material, dotenv files, repository secrets, or generated runtime evidence. + +The client manifest adapter must use direct references such as `process.env.NEXT_PUBLIC_CIPHERBID_NETWORK`. Passing or enumerating the whole browser `process.env` object is forbidden because Next.js only guarantees compile-time replacement for statically referenced public variables. + +## GitHub Actions deployment + +Add one public Pages workflow that: + +- runs on pushes to `main` and explicit manual dispatch; +- sets default `GITHUB_TOKEN` permission to `contents: read`; +- grants `pages: write` and `id-token: write` only to the deploy job; +- uses a Pages concurrency group to prevent overlapping deployments; +- checks out without persisted credentials; +- installs the repository-pinned pnpm version and dependencies with a frozen lockfile; +- runs the repository-approved Web verification required for the changed static boundary; +- builds the static export with explicit public mainnet environment values; +- uploads only `web/out` as the Pages artifact; +- deploys through the GitHub Pages environment. + +Every external action is pinned to a verified lowercase 40-character commit SHA with its release label in a comment. `pull_request_target`, secret expressions, mutable action tags, dotenv loading, arbitrary shell downloads, and credential-bearing build steps are forbidden. + +A repository-owned workflow guard is written first and demonstrated RED against the missing workflow. It validates triggers, permissions, checkout credential persistence, exact approved action pins, public environment allowlist, build directory, artifact directory, and forbidden constructs. Negative fixtures cover mutable tags, `pull_request_target`, secret-expression variants, duplicate unapproved action references, and dotenv use. + +Pages repository settings are changed only after the workflow is merged to the permitted environment branch flow. The effect is verified through the Pages API and a public no-login readback. + +## Wallet and privacy boundary + +The host migration does not change custody: + +| Datum or operation | Owner after migration | +| --------------------------------- | ---------------------------------------------------------------------------------- | +| Wallet signing key and session | Ready X | +| Viewing key and private notes | Ready X | +| Note discovery and selection | Ready X | +| Proof construction and submission | Ready X | +| Bid nonce and claim secret | Browser memory during the active operation plus password-encrypted recovery export | +| Public deployment configuration | Static bundle | +| Auction/NFT/bid/settlement reads | Browser through public RPC | + +No shielded-balance probe is introduced. The dapp detects Wallet API capability by supported-version query and requests only the existing action flows. Deposits, withdrawals, open-note amounts, direct reveal/claim activity, and timing remain publicly observable as already documented. + +## Error handling + +- Invalid or duplicate `id`: fail before RPC construction. +- Missing or invalid build manifest: render “deployment unavailable”; no wallet controls. +- RPC/CORS/timeout: render a public-read failure with Retry; do not infer auction state. +- Class/config/NFT mismatch: preserve the existing fail-closed reader result and suppress transaction controls. +- Wallet absent or locked: preserve the current install/unlock guidance. +- Wallet submission timeout: preserve the transaction hash and bounded receipt polling behavior; never call it failed solely because an RPC has not indexed it yet. +- Pages deployment failure: do not set `demo_url`, repository homepage, or submission evidence. + +## TDD slices + +1. **Static route contract:** RED tests require `/auction?id=7`, strict positive-u64 parsing, duplicate rejection, inert hostile input, and home-link generation. +2. **Client manifest adapter:** RED tests require direct public-key mapping, canonical mainnet validation, and no secret-bearing environment keys. +3. **Browser snapshot loader:** RED component tests cover loading, verified success, retry, invalid ID, deployment failure, RPC failure, and stale-request rejection. +4. **Static export:** RED configuration test requires export mode, compile-time base path, and trailing-slash routing without changing local Playwright defaults. +5. **Pages workflow policy:** RED guard fails while the workflow is missing; adversarial fixtures prove each forbidden CI class is rejected. +6. **Runtime:** build the export, serve `web/out` under `/cipherbid`, and verify root, create, setup, valid auction, invalid auction, mobile width, overflow, keyboard order, reduced motion, and no console/page errors. +7. **Public deployment:** merge through `development → staging → main`, enable Pages workflow mode, wait for hosted success, then verify the exact public routes and live mainnet read in a clean browser. + +## Acceptance criteria + +1. `pnpm build` produces `web/out` with no dynamic-server dependency. +2. The canonical public URL is `https://sourcesenseitherealone.github.io/cipherbid/` and opens without authentication. +3. `/cipherbid/create/`, `/cipherbid/demo/setup/`, and `/cipherbid/auction/?id=` return the intended application through Pages. +4. The auction route validates and renders the real mainnet snapshot in the browser; it never renders invented chain values. +5. A hostile or invalid query remains inert and never triggers an RPC read or wallet control. +6. Ready X remains the only holder of wallet keys, viewing keys, private notes, proof witnesses, and submission authority. +7. The workflow policy guard and its adversarial fixtures pass; all external actions use verified immutable pins. +8. Hosted workflow status, Pages API state, public HTTP response, browser runtime, and repository homepage are independently read back before any deployment claim. +9. `strk20.json.demo_url` is populated only after the public route and real lifecycle page are verified. Transactions and video remain empty until their separate evidence gates pass. + +## Out of scope + +- Backend or serverless APIs; +- proxying the Starknet RPC; +- storing Ready X or recovery state; +- fabricating an auction before p9-4; +- claiming the temporary Cloudflare tunnel as durable hosting; +- recording or publishing the demo video before the complete lifecycle; +- automatic fallback to Vercel without a verified Pages blocker and user-visible authentication step. diff --git a/strk20.json b/strk20.json index 89fcd94..dfc875f 100644 --- a/strk20.json +++ b/strk20.json @@ -1,6 +1,9 @@ { "transactions": [], - "contracts": [], + "contracts": [ + "0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e", + "0x05c7080c583304469e853e472d46a20448ff82bf9ee4c87a8efabc35f8177e1f" + ], "demo_video": "", "demo_url": "" } diff --git a/web/.prettierignore b/web/.prettierignore index 00ca9be..cf3855f 100644 --- a/web/.prettierignore +++ b/web/.prettierignore @@ -4,3 +4,4 @@ coverage/ playwright-report/ test-results/ .agents/ +pnpm-lock.yaml diff --git a/web/next.config.ts b/web/next.config.ts index 7329063..1d2e1f6 100644 --- a/web/next.config.ts +++ b/web/next.config.ts @@ -1,7 +1,22 @@ import type { NextConfig } from 'next' -const nextConfig: NextConfig = { - /* config options here */ +type NextEnvironment = Readonly> + +export function createNextConfig(environment: NextEnvironment): NextConfig { + if (environment.CIPHERBID_PAGES_BUILD !== '1') return {} + + const requestedBasePath = environment.CIPHERBID_PAGES_BASE_PATH?.trim() + if (requestedBasePath && requestedBasePath !== '/cipherbid') { + throw new Error('GitHub Pages base path must be /cipherbid') + } + + return { + output: 'export', + basePath: '/cipherbid', + trailingSlash: true, + } } +const nextConfig = createNextConfig(process.env) + export default nextConfig diff --git a/web/package.json b/web/package.json index 090bfb1..bb68159 100644 --- a/web/package.json +++ b/web/package.json @@ -4,6 +4,12 @@ "private": true, "scripts": { "dev": "next dev --webpack", + "auction:create:sepolia": "tsx scripts/create-sepolia-auction.ts", + "auction:create:mainnet": "tsx scripts/create-mainnet-auction.ts", + "auction:preflight:sepolia": "tsx scripts/create-sepolia-auction.ts --preflight-only", + "auction:preflight:mainnet": "tsx scripts/preflight-mainnet.ts", + "deploy:mainnet": "tsx scripts/deploy-mainnet.ts", + "env:mainnet": "tsx scripts/configure-mainnet-env.ts", "build": "next build --webpack", "start": "next start", "lint": "eslint", @@ -13,7 +19,8 @@ "test": "vitest run", "test:watch": "vitest", "test:coverage": "vitest run --coverage", - "test:e2e": "playwright test" + "test:e2e": "playwright test", + "pages:verify": "tsx scripts/verify-pages-workflow.ts" }, "dependencies": { "@starknet-io/get-starknet-discovery": "6.0.2", @@ -42,8 +49,10 @@ "postcss": "8.5.26", "prettier": "3.6.2", "tailwindcss": "4.3.3", + "tsx": "4.23.12", "typescript": "5.9.3", - "vitest": "3.2.6" + "vitest": "3.2.6", + "yaml": "2.9.0" }, "pnpm": { "overrides": { diff --git a/web/playwright.config.ts b/web/playwright.config.ts index 336791a..c583cda 100644 --- a/web/playwright.config.ts +++ b/web/playwright.config.ts @@ -3,6 +3,8 @@ import { defineConfig, devices } from '@playwright/test' export default defineConfig({ testDir: './tests/e2e', fullyParallel: true, + workers: 1, + timeout: 60_000, forbidOnly: Boolean(process.env.CI), retries: process.env.CI ? 2 : 0, reporter: 'list', @@ -17,9 +19,17 @@ export default defineConfig({ }, ], webServer: { - command: 'npx --yes pnpm@10.18.1 dev --hostname 127.0.0.1 --port 4173', + command: 'node node_modules/next/dist/bin/next dev --webpack --hostname 127.0.0.1 --port 4173', url: 'http://127.0.0.1:4173', - reuseExistingServer: !process.env.CI, + reuseExistingServer: false, timeout: 120_000, + env: { + NEXT_PUBLIC_CIPHERBID_NETWORK: 'sepolia', + NEXT_PUBLIC_STARKNET_RPC_URL: 'https://api.zan.top/public/starknet-sepolia/rpc/v0_10', + NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS: '0x0705b1080174f2b10c02fd8b2e00b918e4dc91f9021ee6a208f53d5909fcc87d', + NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH: '0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4', + NEXT_PUBLIC_STRK20_POOL_ADDRESS: '0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91', + NEXT_PUBLIC_STRK_TOKEN_ADDRESS: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d', + }, }, }) diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index 26eac18..e9764f5 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -11,6 +11,7 @@ overrides: postcss: 8.5.26 importers: + .: dependencies: '@starknet-io/get-starknet-discovery': @@ -67,7 +68,7 @@ importers: version: 19.2.4(@types/react@19.2.18) '@vitest/coverage-v8': specifier: 3.2.6 - version: 3.2.6(vitest@3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)) + version: 3.2.6(vitest@3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0)) eslint: specifier: 9.39.5 version: 9.39.5(jiti@2.7.0) @@ -86,1073 +87,896 @@ importers: tailwindcss: specifier: 4.3.3 version: 4.3.3 + tsx: + specifier: 4.23.12 + version: 4.23.12 typescript: specifier: 5.9.3 version: 5.9.3 vitest: specifier: 3.2.6 - version: 3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0) + version: 3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0) + yaml: + specifier: 2.9.0 + version: 2.9.0 packages: + '@adobe/css-tools@4.5.0': - resolution: - { integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q== } + resolution: {integrity: sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q==} '@adraffy/ens-normalize@1.11.1': - resolution: - { integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ== } + resolution: {integrity: sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ==} '@alloc/quick-lru@5.2.0': - resolution: - { integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw== } - engines: { node: '>=10' } + resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} + engines: {node: '>=10'} '@ampproject/remapping@2.3.0': - resolution: - { integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} '@asamuzakjp/css-color@3.2.0': - resolution: - { integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw== } + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} '@babel/code-frame@7.29.7': - resolution: - { integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} '@babel/compat-data@7.29.7': - resolution: - { integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} '@babel/core@7.29.7': - resolution: - { integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-RgHBCvtjbOK2gXSNBNIkNoEc9qoVEtau3hj8gEqKQuL3HZAibKarWFEI3Lfm6EYKkLalOh8eSrj9b+ch9H/VBA==} + engines: {node: '>=6.9.0'} '@babel/generator@7.29.8': - resolution: - { integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg==} + engines: {node: '>=6.9.0'} '@babel/helper-compilation-targets@7.29.7': - resolution: - { integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} '@babel/helper-globals@7.29.7': - resolution: - { integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} '@babel/helper-module-imports@7.29.7': - resolution: - { integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} '@babel/helper-module-transforms@7.29.7': - resolution: - { integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} peerDependencies: '@babel/core': ^7.0.0 '@babel/helper-string-parser@7.29.7': - resolution: - { integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-identifier@7.29.7': - resolution: - { integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} '@babel/helper-validator-option@7.29.7': - resolution: - { integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} '@babel/helpers@7.29.7': - resolution: - { integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} '@babel/parser@7.29.8': - resolution: - { integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA== } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA==} + engines: {node: '>=6.0.0'} hasBin: true '@babel/runtime@7.29.7': - resolution: - { integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} '@babel/template@7.29.7': - resolution: - { integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} '@babel/traverse@7.29.8': - resolution: - { integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg==} + engines: {node: '>=6.9.0'} '@babel/types@7.29.8': - resolution: - { integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==} + engines: {node: '>=6.9.0'} '@bcoe/v8-coverage@1.0.2': - resolution: - { integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==} + engines: {node: '>=18'} '@csstools/color-helpers@5.1.0': - resolution: - { integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} '@csstools/css-calc@2.1.4': - resolution: - { integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 '@csstools/css-color-parser@3.1.0': - resolution: - { integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} peerDependencies: '@csstools/css-parser-algorithms': ^3.0.5 '@csstools/css-tokenizer': ^3.0.4 '@csstools/css-parser-algorithms@3.0.5': - resolution: - { integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} peerDependencies: '@csstools/css-tokenizer': ^3.0.4 '@csstools/css-tokenizer@3.0.4': - resolution: - { integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} '@emnapi/core@1.10.0': - resolution: - { integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw== } + resolution: {integrity: sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==} '@emnapi/runtime@1.10.0': - resolution: - { integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA== } + resolution: {integrity: sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==} '@emnapi/runtime@1.11.3': - resolution: - { integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA== } + resolution: {integrity: sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==} '@emnapi/wasi-threads@1.2.1': - resolution: - { integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w== } + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} '@esbuild/aix-ppc64@0.28.2': - resolution: - { integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==} + engines: {node: '>=18'} cpu: [ppc64] os: [aix] '@esbuild/android-arm64@0.28.2': - resolution: - { integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A== } - engines: { node: '>=18' } + resolution: {integrity: sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==} + engines: {node: '>=18'} cpu: [arm64] os: [android] '@esbuild/android-arm@0.28.2': - resolution: - { integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==} + engines: {node: '>=18'} cpu: [arm] os: [android] '@esbuild/android-x64@0.28.2': - resolution: - { integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q== } - engines: { node: '>=18' } + resolution: {integrity: sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==} + engines: {node: '>=18'} cpu: [x64] os: [android] '@esbuild/darwin-arm64@0.28.2': - resolution: - { integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==} + engines: {node: '>=18'} cpu: [arm64] os: [darwin] '@esbuild/darwin-x64@0.28.2': - resolution: - { integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==} + engines: {node: '>=18'} cpu: [x64] os: [darwin] '@esbuild/freebsd-arm64@0.28.2': - resolution: - { integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==} + engines: {node: '>=18'} cpu: [arm64] os: [freebsd] '@esbuild/freebsd-x64@0.28.2': - resolution: - { integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==} + engines: {node: '>=18'} cpu: [x64] os: [freebsd] '@esbuild/linux-arm64@0.28.2': - resolution: - { integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug== } - engines: { node: '>=18' } + resolution: {integrity: sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==} + engines: {node: '>=18'} cpu: [arm64] os: [linux] '@esbuild/linux-arm@0.28.2': - resolution: - { integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w== } - engines: { node: '>=18' } + resolution: {integrity: sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==} + engines: {node: '>=18'} cpu: [arm] os: [linux] '@esbuild/linux-ia32@0.28.2': - resolution: - { integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==} + engines: {node: '>=18'} cpu: [ia32] os: [linux] '@esbuild/linux-loong64@0.28.2': - resolution: - { integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==} + engines: {node: '>=18'} cpu: [loong64] os: [linux] '@esbuild/linux-mips64el@0.28.2': - resolution: - { integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==} + engines: {node: '>=18'} cpu: [mips64el] os: [linux] '@esbuild/linux-ppc64@0.28.2': - resolution: - { integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==} + engines: {node: '>=18'} cpu: [ppc64] os: [linux] '@esbuild/linux-riscv64@0.28.2': - resolution: - { integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==} + engines: {node: '>=18'} cpu: [riscv64] os: [linux] '@esbuild/linux-s390x@0.28.2': - resolution: - { integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==} + engines: {node: '>=18'} cpu: [s390x] os: [linux] '@esbuild/linux-x64@0.28.2': - resolution: - { integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==} + engines: {node: '>=18'} cpu: [x64] os: [linux] '@esbuild/netbsd-arm64@0.28.2': - resolution: - { integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==} + engines: {node: '>=18'} cpu: [arm64] os: [netbsd] '@esbuild/netbsd-x64@0.28.2': - resolution: - { integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==} + engines: {node: '>=18'} cpu: [x64] os: [netbsd] '@esbuild/openbsd-arm64@0.28.2': - resolution: - { integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==} + engines: {node: '>=18'} cpu: [arm64] os: [openbsd] '@esbuild/openbsd-x64@0.28.2': - resolution: - { integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==} + engines: {node: '>=18'} cpu: [x64] os: [openbsd] '@esbuild/openharmony-arm64@0.28.2': - resolution: - { integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q== } - engines: { node: '>=18' } + resolution: {integrity: sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==} + engines: {node: '>=18'} cpu: [arm64] os: [openharmony] '@esbuild/sunos-x64@0.28.2': - resolution: - { integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g== } - engines: { node: '>=18' } + resolution: {integrity: sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==} + engines: {node: '>=18'} cpu: [x64] os: [sunos] '@esbuild/win32-arm64@0.28.2': - resolution: - { integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==} + engines: {node: '>=18'} cpu: [arm64] os: [win32] '@esbuild/win32-ia32@0.28.2': - resolution: - { integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==} + engines: {node: '>=18'} cpu: [ia32] os: [win32] '@esbuild/win32-x64@0.28.2': - resolution: - { integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g== } - engines: { node: '>=18' } + resolution: {integrity: sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==} + engines: {node: '>=18'} cpu: [x64] os: [win32] '@eslint-community/eslint-utils@4.10.1': - resolution: - { integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg== } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-cuadcxVFE8sDK6iWJbs8Sn0av2Nrh2QSGQhVlBW9AaAHqHwjWsZHT8LJ4hFGPh7ASBV2deFdM7H/DPjulmh8rg==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 '@eslint-community/eslint-utils@4.9.1': - resolution: - { integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ== } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} peerDependencies: eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 '@eslint-community/regexpp@4.12.2': - resolution: - { integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew== } - engines: { node: ^12.0.0 || ^14.0.0 || >=16.0.0 } + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} '@eslint/config-array@0.21.2': - resolution: - { integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/config-helpers@0.4.2': - resolution: - { integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/core@0.17.0': - resolution: - { integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/eslintrc@3.3.6': - resolution: - { integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-l2Ul9PrHsPCKcEY/ac7VgFj9D80C7S68sOKc618SyHDPK36s1XcFebXY0iTzUVn4Yq+YbwvSnDmCz9yxjX+QrA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/js@9.39.5': - resolution: - { integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-QywQuszQh77pIXCsq998c8hbhSTI/azTty1Z6N53dmAudKHhy573j3yvRLsX2BSp8YpLtoCEG8E9DJe+8zUh4A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/object-schema@2.1.7': - resolution: - { integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@eslint/plugin-kit@0.4.1': - resolution: - { integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@humanfs/core@0.19.2': - resolution: - { integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA== } - engines: { node: '>=18.18.0' } + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} '@humanfs/node@0.16.8': - resolution: - { integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ== } - engines: { node: '>=18.18.0' } + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} '@humanfs/types@0.15.0': - resolution: - { integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q== } - engines: { node: '>=18.18.0' } + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} '@humanwhocodes/module-importer@1.0.1': - resolution: - { integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA== } - engines: { node: '>=12.22' } + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} '@humanwhocodes/retry@0.4.3': - resolution: - { integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ== } - engines: { node: '>=18.18' } + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} '@img/colour@1.1.0': - resolution: - { integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} + engines: {node: '>=18'} '@img/sharp-darwin-arm64@0.35.3': - resolution: - { integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [darwin] '@img/sharp-darwin-x64@0.35.3': - resolution: - { integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [darwin] '@img/sharp-freebsd-wasm32@0.35.3': - resolution: - { integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==} + engines: {node: '>=20.9.0'} os: [freebsd] '@img/sharp-libvips-darwin-arm64@1.3.2': - resolution: - { integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg== } + resolution: {integrity: sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==} cpu: [arm64] os: [darwin] '@img/sharp-libvips-darwin-x64@1.3.2': - resolution: - { integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw== } + resolution: {integrity: sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==} cpu: [x64] os: [darwin] '@img/sharp-libvips-linux-arm64@1.3.2': - resolution: - { integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA== } + resolution: {integrity: sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==} cpu: [arm64] os: [linux] '@img/sharp-libvips-linux-arm@1.3.2': - resolution: - { integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ== } + resolution: {integrity: sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==} cpu: [arm] os: [linux] '@img/sharp-libvips-linux-ppc64@1.3.2': - resolution: - { integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw== } + resolution: {integrity: sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==} cpu: [ppc64] os: [linux] '@img/sharp-libvips-linux-riscv64@1.3.2': - resolution: - { integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w== } + resolution: {integrity: sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==} cpu: [riscv64] os: [linux] '@img/sharp-libvips-linux-s390x@1.3.2': - resolution: - { integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ== } + resolution: {integrity: sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==} cpu: [s390x] os: [linux] '@img/sharp-libvips-linux-x64@1.3.2': - resolution: - { integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w== } + resolution: {integrity: sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==} cpu: [x64] os: [linux] '@img/sharp-libvips-linuxmusl-arm64@1.3.2': - resolution: - { integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw== } + resolution: {integrity: sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==} cpu: [arm64] os: [linux] '@img/sharp-libvips-linuxmusl-x64@1.3.2': - resolution: - { integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ== } + resolution: {integrity: sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==} cpu: [x64] os: [linux] '@img/sharp-linux-arm64@0.35.3': - resolution: - { integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] '@img/sharp-linux-arm@0.35.3': - resolution: - { integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==} + engines: {node: '>=20.9.0'} cpu: [arm] os: [linux] '@img/sharp-linux-ppc64@0.35.3': - resolution: - { integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==} + engines: {node: '>=20.9.0'} cpu: [ppc64] os: [linux] '@img/sharp-linux-riscv64@0.35.3': - resolution: - { integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==} + engines: {node: '>=20.9.0'} cpu: [riscv64] os: [linux] '@img/sharp-linux-s390x@0.35.3': - resolution: - { integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==} + engines: {node: '>=20.9.0'} cpu: [s390x] os: [linux] '@img/sharp-linux-x64@0.35.3': - resolution: - { integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] '@img/sharp-linuxmusl-arm64@0.35.3': - resolution: - { integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [linux] '@img/sharp-linuxmusl-x64@0.35.3': - resolution: - { integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [linux] '@img/sharp-wasm32@0.35.3': - resolution: - { integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==} + engines: {node: '>=20.9.0'} '@img/sharp-webcontainers-wasm32@0.35.3': - resolution: - { integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==} + engines: {node: '>=20.9.0'} cpu: [wasm32] '@img/sharp-win32-arm64@0.35.3': - resolution: - { integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==} + engines: {node: '>=20.9.0'} cpu: [arm64] os: [win32] '@img/sharp-win32-ia32@0.35.3': - resolution: - { integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw== } - engines: { node: ^20.9.0 } + resolution: {integrity: sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==} + engines: {node: ^20.9.0} cpu: [ia32] os: [win32] '@img/sharp-win32-x64@0.35.3': - resolution: - { integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==} + engines: {node: '>=20.9.0'} cpu: [x64] os: [win32] '@isaacs/cliui@8.0.2': - resolution: - { integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== } - engines: { node: '>=12' } + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} '@istanbuljs/schema@0.1.6': - resolution: - { integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw== } - engines: { node: '>=8' } + resolution: {integrity: sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==} + engines: {node: '>=8'} '@jridgewell/gen-mapping@0.3.13': - resolution: - { integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA== } + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} '@jridgewell/remapping@2.3.5': - resolution: - { integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ== } + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} '@jridgewell/resolve-uri@3.1.2': - resolution: - { integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} '@jridgewell/sourcemap-codec@1.5.5': - resolution: - { integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== } + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} '@jridgewell/trace-mapping@0.3.31': - resolution: - { integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw== } + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} '@module-federation/error-codes@0.12.0': - resolution: - { integrity: sha512-DEXQjopcBuGzp/NA9OVtASO0uZ6grVK5TIe0PjrbDRyZDxVaYQXKrISxBLOE+3nSIELE98tYpfxptm8WC9A8zA== } + resolution: {integrity: sha512-DEXQjopcBuGzp/NA9OVtASO0uZ6grVK5TIe0PjrbDRyZDxVaYQXKrISxBLOE+3nSIELE98tYpfxptm8WC9A8zA==} '@module-federation/runtime-core@0.12.0': - resolution: - { integrity: sha512-373zBM54196KHURs/O8lry9trCAM3PPidvsF4YdrtahNc8YaQynml0mE3zdZeBnqP6H0/4OpPqMMjACI80Ht8w== } + resolution: {integrity: sha512-373zBM54196KHURs/O8lry9trCAM3PPidvsF4YdrtahNc8YaQynml0mE3zdZeBnqP6H0/4OpPqMMjACI80Ht8w==} '@module-federation/runtime@0.12.0': - resolution: - { integrity: sha512-Cz9/7+gSvrdencwA8LXUMKnZdu0/flyN+yk6t3pkxfhvPJi3W65ZcalAKyOgyk2x8rEYrRSyEXu+/2DIFgrzmA== } + resolution: {integrity: sha512-Cz9/7+gSvrdencwA8LXUMKnZdu0/flyN+yk6t3pkxfhvPJi3W65ZcalAKyOgyk2x8rEYrRSyEXu+/2DIFgrzmA==} '@module-federation/sdk@0.12.0': - resolution: - { integrity: sha512-vh3GcG90fxjbkMghK7iSWcMayi/y8U5DxI6mhEFuz11St3y1UgQO2TZYephL8nISFBld7DdiqAkimx+6Hb3hjQ== } + resolution: {integrity: sha512-vh3GcG90fxjbkMghK7iSWcMayi/y8U5DxI6mhEFuz11St3y1UgQO2TZYephL8nISFBld7DdiqAkimx+6Hb3hjQ==} '@napi-rs/lzma-linux-x64-gnu@1.5.1': - resolution: - { integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ== } - engines: { node: ^22.20 || ^24.12 || >=25 } + resolution: {integrity: sha512-oTXEIha4SsuXdTA4Iyskj0kpdx2yVXdhd75c2v3xGrHFfVMsbhTPZU/nMPL4sWKo4pBHm3aucLaqGlF696dTyQ==} + engines: {node: ^22.20 || ^24.12 || >=25} cpu: [x64] os: [linux] '@napi-rs/wasm-runtime@1.2.3': - resolution: - { integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q== } - engines: { node: ^20.19.0 || ^22.13.0 || >=23.5.0 } + resolution: {integrity: sha512-UMduMbqO5s5zF2NkNacMT/yK5Y5QiKvWr2+50bzIIxFDwVJ2h49b+oyjaCGPhJxd2/gC2x39EHv/gHVuu36x2Q==} + engines: {node: ^20.19.0 || ^22.13.0 || >=23.5.0} peerDependencies: '@emnapi/core': ^1.7.1 || ^2.0.0-alpha.4 '@emnapi/runtime': ^1.7.1 || ^2.0.0-alpha.4 '@next/env@16.3.2': - resolution: - { integrity: sha512-8k4YoG8cM7LWlkfzGNYCRBbFNlernLiMw4s0btVl+CmmWqn3VpYypA72/5Feb1UWdxe6tHqr5KHP4p4Y4m9luA== } + resolution: {integrity: sha512-8k4YoG8cM7LWlkfzGNYCRBbFNlernLiMw4s0btVl+CmmWqn3VpYypA72/5Feb1UWdxe6tHqr5KHP4p4Y4m9luA==} '@next/eslint-plugin-next@16.3.2': - resolution: - { integrity: sha512-z+HW1cZgt8QhByw8p2EbxF94AImgsKIYUbtSkA7Zld2T9yrKAlys4jNOcAOCtv6csX2CoA/5qCVyesL5pHmJ0A== } + resolution: {integrity: sha512-z+HW1cZgt8QhByw8p2EbxF94AImgsKIYUbtSkA7Zld2T9yrKAlys4jNOcAOCtv6csX2CoA/5qCVyesL5pHmJ0A==} '@next/swc-darwin-arm64@16.3.2': - resolution: - { integrity: sha512-ib5Llm93YCKoKWDh6ZaHq6QWTuOZ2bRkSnUwMmX8dsRIOkBNL1vVlSiUKSfixPL9SSh9pvukzqajk/klkn5vqg== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-ib5Llm93YCKoKWDh6ZaHq6QWTuOZ2bRkSnUwMmX8dsRIOkBNL1vVlSiUKSfixPL9SSh9pvukzqajk/klkn5vqg==} + engines: {node: '>= 10'} cpu: [arm64] os: [darwin] '@next/swc-darwin-x64@16.3.2': - resolution: - { integrity: sha512-qd98fX2+I5nYJDioW2o7nSjoxM5KvWdeDefM80igia4+C/qSIEhH4MhTE+hO/7qKM7W37/Mq+dOWp8UePSyLHw== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-qd98fX2+I5nYJDioW2o7nSjoxM5KvWdeDefM80igia4+C/qSIEhH4MhTE+hO/7qKM7W37/Mq+dOWp8UePSyLHw==} + engines: {node: '>= 10'} cpu: [x64] os: [darwin] '@next/swc-linux-arm64-gnu@16.3.2': - resolution: - { integrity: sha512-vqsgb6FAOzcrCccsLXiKtAy5t8EzO+uOazuFaSkQxeY0tNONG3vpHYy8pyBafcI5SNFPTeyard6yTr6SzNGo2A== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-vqsgb6FAOzcrCccsLXiKtAy5t8EzO+uOazuFaSkQxeY0tNONG3vpHYy8pyBafcI5SNFPTeyard6yTr6SzNGo2A==} + engines: {node: '>= 10'} cpu: [arm64] os: [linux] '@next/swc-linux-arm64-musl@16.3.2': - resolution: - { integrity: sha512-xIe1eujfHUB2XcxHGddxJyu6TJRPjC5NpIkQYB/32ESkt5VkQyIAjmLRS38c+s6QY+qjtY/4KarVDzXRuD7lZQ== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-xIe1eujfHUB2XcxHGddxJyu6TJRPjC5NpIkQYB/32ESkt5VkQyIAjmLRS38c+s6QY+qjtY/4KarVDzXRuD7lZQ==} + engines: {node: '>= 10'} cpu: [arm64] os: [linux] '@next/swc-linux-x64-gnu@16.3.2': - resolution: - { integrity: sha512-Fe0SA2j8X0kmc3aveuHD7UktO3AE2+mH3LguP60vGbz7u0z+MrDXbeb5iZFYAwR7EzzzXJ2Yk966w9mGTFMqfA== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-Fe0SA2j8X0kmc3aveuHD7UktO3AE2+mH3LguP60vGbz7u0z+MrDXbeb5iZFYAwR7EzzzXJ2Yk966w9mGTFMqfA==} + engines: {node: '>= 10'} cpu: [x64] os: [linux] '@next/swc-linux-x64-musl@16.3.2': - resolution: - { integrity: sha512-TFBipb+gyesI/2Ve4zVu7kGltBWN/R466G5/1gtt2lECfc22G1pjkTxu68Q9aFcOaXiRGTQfvDbQQFe7mYgxiQ== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-TFBipb+gyesI/2Ve4zVu7kGltBWN/R466G5/1gtt2lECfc22G1pjkTxu68Q9aFcOaXiRGTQfvDbQQFe7mYgxiQ==} + engines: {node: '>= 10'} cpu: [x64] os: [linux] '@next/swc-win32-arm64-msvc@16.3.2': - resolution: - { integrity: sha512-rVtmnNpBYIosDnKD/96dKxFsJnwnn1WRGG/HioSe8XCm2ksSHNrd2R6+hSjvTBxeMNhJ9pYeu/90cWB1nQLuNA== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-rVtmnNpBYIosDnKD/96dKxFsJnwnn1WRGG/HioSe8XCm2ksSHNrd2R6+hSjvTBxeMNhJ9pYeu/90cWB1nQLuNA==} + engines: {node: '>= 10'} cpu: [arm64] os: [win32] '@next/swc-win32-x64-msvc@16.3.2': - resolution: - { integrity: sha512-H4Y2o2/JcHu8LtwzD5CXfHhwxwz8gfsx2HXDEw46Mtev5xHnEmB7HNtZtmriw5ReUOjRtcDqo7XSbU01FT9NlA== } - engines: { node: '>= 10' } + resolution: {integrity: sha512-H4Y2o2/JcHu8LtwzD5CXfHhwxwz8gfsx2HXDEw46Mtev5xHnEmB7HNtZtmriw5ReUOjRtcDqo7XSbU01FT9NlA==} + engines: {node: '>= 10'} cpu: [x64] os: [win32] '@noble/ciphers@1.3.0': - resolution: - { integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw== } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw==} + engines: {node: ^14.21.3 || >=16} '@noble/curves@1.7.0': - resolution: - { integrity: sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw== } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-UTMhXK9SeDhFJVrHeUJ5uZlI6ajXg10O6Ddocf9S6GjbSBVZsJo88HzKwXznNfGpMTRDyJkqMjNDPYgf0qFWnw==} + engines: {node: ^14.21.3 || >=16} '@noble/curves@1.9.1': - resolution: - { integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA== } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-k11yZxZg+t+gWvBbIswW0yoJlu8cHOC7dhunwOzoWH/mXGBiYyR4YY6hAEK/3EUs4UpB8la1RfdRpeGsFHkWsA==} + engines: {node: ^14.21.3 || >=16} '@noble/curves@1.9.7': - resolution: - { integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw== } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-gbKGcRUYIjA3/zCCNaWDciTMFI0dCkvou3TL8Zmy5Nc7sJ47a0jtOeZoTaMxkuqRo9cRhjOdZJXegxYE5FN/xw==} + engines: {node: ^14.21.3 || >=16} '@noble/hashes@1.6.0': - resolution: - { integrity: sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ== } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-YUULf0Uk4/mAA89w+k3+yUYh6NrEvxZa5T6SY3wlMvE2chHkxFUUIDI8/XW1QSC357iA5pSnqt7XEhvFOqmDyQ==} + engines: {node: ^14.21.3 || >=16} '@noble/hashes@1.6.1': - resolution: - { integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w== } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-pq5D8h10hHBjyqX+cfBm0i8JUXJ0UhczFc4r74zbuT9XgewFo2E3J1cOaGtdZynILNmQ685YWGzGE1Zv6io50w==} + engines: {node: ^14.21.3 || >=16} '@noble/hashes@1.8.0': - resolution: - { integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A== } - engines: { node: ^14.21.3 || >=16 } + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} '@nodelib/fs.scandir@2.1.5': - resolution: - { integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== } - engines: { node: '>= 8' } + resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} + engines: {node: '>= 8'} '@nodelib/fs.stat@2.0.5': - resolution: - { integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== } - engines: { node: '>= 8' } + resolution: {integrity: sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==} + engines: {node: '>= 8'} '@nodelib/fs.walk@1.2.8': - resolution: - { integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== } - engines: { node: '>= 8' } + resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} + engines: {node: '>= 8'} '@nolyfill/is-core-module@1.0.39': - resolution: - { integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA== } - engines: { node: '>=12.4.0' } + resolution: {integrity: sha512-nn5ozdjYQpUCZlWGuxcJY/KpxkWQs4DcbMCmKojjyrYDEAGy4Ce19NN4v5MduafTwJlbKc99UA8YhSVqq9yPZA==} + engines: {node: '>=12.4.0'} '@pkgjs/parseargs@0.11.0': - resolution: - { integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== } - engines: { node: '>=14' } + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} '@playwright/test@1.58.2': - resolution: - { integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==} + engines: {node: '>=18'} hasBin: true '@rollup/rollup-android-arm-eabi@4.62.5': - resolution: - { integrity: sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA== } + resolution: {integrity: sha512-jfkGfTwhQpsiSckPF8r9bU3pn3vyd72NlWaO+TgEO6WPSDnUhXzrNYCHBMOYj0ACaUgjm6eERLF+XV9a6RstoA==} cpu: [arm] os: [android] '@rollup/rollup-android-arm64@4.62.5': - resolution: - { integrity: sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA== } + resolution: {integrity: sha512-oGVqyQlxnrz9/ty89oHpU857VUHEl5/Xu4R2lS+aivCTrNnSsbiENzTnNaBsjxH0CNWGPhzHArOLFwo+oKXveA==} cpu: [arm64] os: [android] '@rollup/rollup-darwin-arm64@4.62.5': - resolution: - { integrity: sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A== } + resolution: {integrity: sha512-bW7B8xMEq8n99Q3ieEcPRGuphurdZAaFzQc9Efyyw3FL6DZO6pMy9xhdN+kBoD7Sy05xNXSr4OyPPnpkYriS/A==} cpu: [arm64] os: [darwin] '@rollup/rollup-darwin-x64@4.62.5': - resolution: - { integrity: sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w== } + resolution: {integrity: sha512-YSwBS86QeHOGlrxJ1PSOIZSkzRL/JmKeunhc+lV6M1a6En8QuVCD/T/qIA0J4Gd2Y86RIOBYrLcOUtqGh9+/1w==} cpu: [x64] os: [darwin] '@rollup/rollup-freebsd-arm64@4.62.5': - resolution: - { integrity: sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ== } + resolution: {integrity: sha512-2fST8lILgl7cKbme/1KDdPCmbXbG+gqoV3bHp19L0ypX/3akYMBVdOunPleRCwonoLnXOZ/0F+Mt/v8POFmfcQ==} cpu: [arm64] os: [freebsd] '@rollup/rollup-freebsd-x64@4.62.5': - resolution: - { integrity: sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ== } + resolution: {integrity: sha512-cpIxQCP9J+EVad0a6LO1kY3ZGODlk80VlI+2I96B8xMcdHZ4pLVhfQ49JFpYqjPF91FFkQWftf57YlDcTiw9yQ==} cpu: [x64] os: [freebsd] '@rollup/rollup-linux-arm-gnueabihf@4.62.5': - resolution: - { integrity: sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA== } + resolution: {integrity: sha512-r9fGh3eFs3e/udWh5ZjXQtxiYK/xoFxQaYR/cELxac/Udkl5Th+IsFm0CX3Kl9hmUH/we7EoMpjJgeQNnE0+IA==} cpu: [arm] os: [linux] '@rollup/rollup-linux-arm-musleabihf@4.62.5': - resolution: - { integrity: sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q== } + resolution: {integrity: sha512-xdvFdp7OM6KLJviJT2g/YuRSUjnZgGHk4RNgwIbN7X6cPugOucV60DdHXWzsBVCUdrGb6qSXnJQrrAKMmQuj3Q==} cpu: [arm] os: [linux] '@rollup/rollup-linux-arm64-gnu@4.62.5': - resolution: - { integrity: sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g== } + resolution: {integrity: sha512-rRqILAndyzHzP7T9NFQrq+4HFWNhqkqkKur7eiBpfLmz01PO0JKx5Vchu3YllE4YXI/Ftgq/szrDWg5GJ0mI8g==} cpu: [arm64] os: [linux] '@rollup/rollup-linux-arm64-musl@4.62.5': - resolution: - { integrity: sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw== } + resolution: {integrity: sha512-Gf4X3qVMucayUvux6aXXPgXovocSFUC0rrffDuPI/S2nHhNMhjcZxsrAFYCOF350PRreW1XwzFj3CT/3bKsWCw==} cpu: [arm64] os: [linux] '@rollup/rollup-linux-loong64-gnu@4.62.5': - resolution: - { integrity: sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w== } + resolution: {integrity: sha512-+s5qA0TNM0qm8PK/a5gt/1Hpx+NV08uSuCncvhziIlQzT6AEV2fnUQo7eBtFTFO0nA9scauvoR2HusfXmQnO4w==} cpu: [loong64] os: [linux] '@rollup/rollup-linux-loong64-musl@4.62.5': - resolution: - { integrity: sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw== } + resolution: {integrity: sha512-ybb6QvWwWJCbBWqERpc8K3pYVGIrXlG8MEQ8IIuJY6Y9KdHQxoFoNyfkAOtKn1VHu3KuLidXvwrvGR1mEjeWCw==} cpu: [loong64] os: [linux] '@rollup/rollup-linux-ppc64-gnu@4.62.5': - resolution: - { integrity: sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ== } + resolution: {integrity: sha512-nZb1DtnOyhCmYvsC8A2CwOkopVg+IS1+fPUa7rMOAXtNw5+lLCLLPqd6XAiNrGtoQKsbvIBOwsHnBH/3wnb4HQ==} cpu: [ppc64] os: [linux] '@rollup/rollup-linux-ppc64-musl@4.62.5': - resolution: - { integrity: sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg== } + resolution: {integrity: sha512-yMbj63Sp89ryrXLWyz+sy+fYD2HpOnMCLGbe4Oa1smclFSUukdtD/BgdiHaAetJNb74URD8U4hM+qG5KVzMEkg==} cpu: [ppc64] os: [linux] '@rollup/rollup-linux-riscv64-gnu@4.62.5': - resolution: - { integrity: sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA== } + resolution: {integrity: sha512-mhoan3OJw2kYV/e1jtIdmvUZgyBFeA6zGWsOswmR0Tg19TQbowZuR+JMLID6spbbBN7Zee2ejrgmy3+FxGrIdA==} cpu: [riscv64] os: [linux] '@rollup/rollup-linux-riscv64-musl@4.62.5': - resolution: - { integrity: sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w== } + resolution: {integrity: sha512-5ZTLmjWbb1VZdjuyhe83K/8QO0/h11midQCBP+X5OYn32ra7eOBoM0ZqtaY4nkgNsYgmdVhMYPoyVPTjUpHf3w==} cpu: [riscv64] os: [linux] '@rollup/rollup-linux-s390x-gnu@4.62.5': - resolution: - { integrity: sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg== } + resolution: {integrity: sha512-m53kG+br6PGxOTmgBEM2DHSDs9RVjsyEbUwjJPJGTFm1grWOG8EKJggDCTb60unD4Tjby8fi7/m9XfkEWasVWg==} cpu: [s390x] os: [linux] '@rollup/rollup-linux-x64-gnu@4.62.5': - resolution: - { integrity: sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA== } + resolution: {integrity: sha512-6RHPJR1g/uvdYU8uXBnfq3nlqyZCP82Fr6NHgfGoaIeSh0YEqnX/x6uA9MmJJbnSH7swqX4F+CkGdUF+6doiQA==} cpu: [x64] os: [linux] '@rollup/rollup-linux-x64-musl@4.62.5': - resolution: - { integrity: sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw== } + resolution: {integrity: sha512-xs+OXQtEXgpXT0DmA5+U3qnRZHdCST/5HRQxS8wSPZTUZN/EMWeHuSIod32LQklTBZBV9DyfncKBQ8n5V3eFdw==} cpu: [x64] os: [linux] '@rollup/rollup-openbsd-x64@4.62.5': - resolution: - { integrity: sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw== } + resolution: {integrity: sha512-e7hD+sl3s+mcLQDZ8pbudBVsdG6r5yN4w3LqG2TJ8sQHDpblWj5lrJs/3m01Cvlxbt4x13zu5thLjgypgtkYzw==} cpu: [x64] os: [openbsd] '@rollup/rollup-openharmony-arm64@4.62.5': - resolution: - { integrity: sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg== } + resolution: {integrity: sha512-GiyJaCf+WpMub/17aPcKk27QMl5W6f+KhdPTjlFOn5akH5Wa/DCM9Stdx5cDfmasyKB08MqpVQ1uJE2RkkpbXg==} cpu: [arm64] os: [openharmony] '@rollup/rollup-win32-arm64-msvc@4.62.5': - resolution: - { integrity: sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA== } + resolution: {integrity: sha512-+OQ8U2DdoEfXl8T4Fb18AjmEwbXMerKDKCL8yCPAYhKCEEKoul7rkbeGCBFCbAlaGaa7pmtRTpkAJM2LE/i5FA==} cpu: [arm64] os: [win32] '@rollup/rollup-win32-ia32-msvc@4.62.5': - resolution: - { integrity: sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA== } + resolution: {integrity: sha512-KanvAZrPKbDBFwrgiU9yEVpQoox9QPV1WZOXX7HudJQY+eSlu82CtWxDU8WtuRRvtN5EGkLczkd6Y6DTcvm9wA==} cpu: [ia32] os: [win32] '@rollup/rollup-win32-x64-gnu@4.62.5': - resolution: - { integrity: sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ== } + resolution: {integrity: sha512-1aC3UEWTtRl3RK3VpDJ/Tqk1XI4SLTmXIthAq6wRWo8XiSXJNd+VprJM4/1P4+i6HIaFEFlVi9sTTziniD2tOQ==} cpu: [x64] os: [win32] '@rollup/rollup-win32-x64-msvc@4.62.5': - resolution: - { integrity: sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg== } + resolution: {integrity: sha512-/gDJaRs4gl0NPIwqCz+6PkpmhhjRAD2j6P4rSNHBzUkO3naEx2mIU0pRle1vUNRQ7mE/+8OOeXLTv/J56FKiQg==} cpu: [x64] os: [win32] '@rtsao/scc@1.1.0': - resolution: - { integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g== } + resolution: {integrity: sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==} '@scure/base@1.2.6': - resolution: - { integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg== } + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} '@scure/bip32@1.7.0': - resolution: - { integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw== } + resolution: {integrity: sha512-E4FFX/N3f4B80AKWp5dP6ow+flD1LQZo/w8UnLGYZO674jS6YnYeepycOOksv+vLPSpgN35wgKgy+ybfTb2SMw==} '@scure/bip39@1.6.0': - resolution: - { integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A== } + resolution: {integrity: sha512-+lF0BbLiJNwVlev4eKelw1WWLaiKXw7sSl8T6FvBlWkdX+94aGJ4o8XjUdlyhTCjd8c+B3KT3JfS8P0bLRNU6A==} '@scure/starknet@1.1.0': - resolution: - { integrity: sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ== } + resolution: {integrity: sha512-83g3M6Ix2qRsPN4wqLDqiRZ2GBNbjVWfboJE/9UjfG+MHr6oDSu/CWgy8hsBSJejr09DkkL+l0Ze4KVrlCIdtQ==} '@starknet-io/get-starknet-discovery@6.0.2': - resolution: - { integrity: sha512-Jh6yerjkGq196lEu5RDZdssatb6rNsPRLHHuqpJi0uEgsF3mntqlmfeioSRZkr2C5X1p2BFVExa61Iy5184NRg== } + resolution: {integrity: sha512-Jh6yerjkGq196lEu5RDZdssatb6rNsPRLHHuqpJi0uEgsF3mntqlmfeioSRZkr2C5X1p2BFVExa61Iy5184NRg==} '@starknet-io/get-starknet-virtual-wallet@6.0.2': - resolution: - { integrity: sha512-6c/H9iFbh8jR2qPjrDMZQjJeo4bYae0fLe2Ch4hCZPDZJ3BbesejZCgPt5bxOp5JnTRVFu/AiIB8hsA0DVgEzw== } + resolution: {integrity: sha512-6c/H9iFbh8jR2qPjrDMZQjJeo4bYae0fLe2Ch4hCZPDZJ3BbesejZCgPt5bxOp5JnTRVFu/AiIB8hsA0DVgEzw==} '@starknet-io/get-starknet-wallet-standard@6.0.2': - resolution: - { integrity: sha512-jCnLFNPw4IGgmkwDlHszKbnyEbQKu3pE7cZHyVMFwyvRG98IA9FZKykrTNH7Nq3BILf6EcOZ5SgYUQE/gtqraA== } + resolution: {integrity: sha512-jCnLFNPw4IGgmkwDlHszKbnyEbQKu3pE7cZHyVMFwyvRG98IA9FZKykrTNH7Nq3BILf6EcOZ5SgYUQE/gtqraA==} '@starknet-io/types-js@0.10.2': - resolution: - { integrity: sha512-AtUFPYdmo9DqVus++aBSoY9W13/2PZmillPr8/mXZjc+V0iYJ/QTmkTsbw+es2mnLeLhYWSymW9ivQzyyyKdog== } + resolution: {integrity: sha512-AtUFPYdmo9DqVus++aBSoY9W13/2PZmillPr8/mXZjc+V0iYJ/QTmkTsbw+es2mnLeLhYWSymW9ivQzyyyKdog==} '@starknet-io/types-js@0.10.3': - resolution: - { integrity: sha512-WtTGjqgyjqYSaSks/CQrpERGiLlwhr1TTD4llsr8IKEZHb78OJEmEhzrb/LxJV1SIz+MEsB1pioG62BOmFKYLA== } + resolution: {integrity: sha512-WtTGjqgyjqYSaSks/CQrpERGiLlwhr1TTD4llsr8IKEZHb78OJEmEhzrb/LxJV1SIz+MEsB1pioG62BOmFKYLA==} '@starknet-io/types-js@0.9.2': - resolution: - { integrity: sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg== } + resolution: {integrity: sha512-vWOc0FVSn+RmabozIEWcEny1I73nDGTvOrLYJsR1x7LGA3AZmqt4i/aW69o/3i2NN5CVP8Ok6G1ayRQJKye3Wg==} '@swc/helpers@0.5.23': - resolution: - { integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw== } + resolution: {integrity: sha512-5lSsMOTXURePglDfvuAQUqkGek9Hg2kksOYay2m0+XR++b2NWYL/4sWyuvVBIs8oKnJaxkdi9whaL/sqN13afw==} '@tailwindcss/node@4.3.3': - resolution: - { integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg== } + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} '@tailwindcss/oxide-android-arm64@4.3.3': - resolution: - { integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} cpu: [arm64] os: [android] '@tailwindcss/oxide-darwin-arm64@4.3.3': - resolution: - { integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} cpu: [arm64] os: [darwin] '@tailwindcss/oxide-darwin-x64@4.3.3': - resolution: - { integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} cpu: [x64] os: [darwin] '@tailwindcss/oxide-freebsd-x64@4.3.3': - resolution: - { integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} cpu: [x64] os: [freebsd] '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': - resolution: - { integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} cpu: [arm] os: [linux] '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': - resolution: - { integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] '@tailwindcss/oxide-linux-arm64-musl@4.3.3': - resolution: - { integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} cpu: [arm64] os: [linux] '@tailwindcss/oxide-linux-x64-gnu@4.3.3': - resolution: - { integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] '@tailwindcss/oxide-linux-x64-musl@4.3.3': - resolution: - { integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} cpu: [x64] os: [linux] '@tailwindcss/oxide-wasm32-wasi@4.3.3': - resolution: - { integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ== } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} cpu: [wasm32] bundledDependencies: - '@napi-rs/wasm-runtime' @@ -1163,42 +987,35 @@ packages: - tslib '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': - resolution: - { integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} cpu: [arm64] os: [win32] '@tailwindcss/oxide-win32-x64-msvc@4.3.3': - resolution: - { integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} cpu: [x64] os: [win32] '@tailwindcss/oxide@4.3.3': - resolution: - { integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA== } - engines: { node: '>= 20' } + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} '@tailwindcss/postcss@4.3.3': - resolution: - { integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg== } + resolution: {integrity: sha512-JTSZZGQi1AyKirbLN3azmjVzef92tcX7h+iSqPdaeStyFpGpDlKvvpxeOE8njhbUanbRwr3z8DyzhICWnMtQeg==} '@testing-library/dom@10.4.1': - resolution: - { integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==} + engines: {node: '>=18'} '@testing-library/jest-dom@6.8.0': - resolution: - { integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ== } - engines: { node: '>=14', npm: '>=6', yarn: '>=1' } + resolution: {integrity: sha512-WgXcWzVM6idy5JaftTVC8Vs83NKRmGJz4Hqs4oyOuO2J4r/y79vvKZsb+CaGyCSEbUPI6OsewfPd0G1A0/TUZQ==} + engines: {node: '>=14', npm: '>=6', yarn: '>=1'} '@testing-library/react@16.3.0': - resolution: - { integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==} + engines: {node: '>=18'} peerDependencies: '@testing-library/dom': ^10.0.0 '@types/react': ^18.0.0 || ^19.0.0 @@ -1212,258 +1029,214 @@ packages: optional: true '@testing-library/user-event@14.6.1': - resolution: - { integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw== } - engines: { node: '>=12', npm: '>=6' } + resolution: {integrity: sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==} + engines: {node: '>=12', npm: '>=6'} peerDependencies: '@testing-library/dom': '>=7.21.4' '@tybys/wasm-util@0.10.3': - resolution: - { integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg== } + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} '@types/aria-query@5.0.4': - resolution: - { integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw== } + resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} '@types/chai@5.2.3': - resolution: - { integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA== } + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} '@types/deep-eql@4.0.2': - resolution: - { integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw== } + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.9': - resolution: - { integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg== } + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} '@types/json-schema@7.0.15': - resolution: - { integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA== } + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} '@types/json5@0.0.29': - resolution: - { integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ== } + resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} '@types/node@24.10.2': - resolution: - { integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA== } + resolution: {integrity: sha512-WOhQTZ4G8xZ1tjJTvKOpyEVSGgOTvJAfDK3FNFgELyaTpzhdgHVHeqW8V+UJvzF5BT+/B54T/1S2K6gd9c7bbA==} '@types/react-dom@19.2.4': - resolution: - { integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw== } + resolution: {integrity: sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==} peerDependencies: '@types/react': ^19.2.0 '@types/react@19.2.18': - resolution: - { integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w== } + resolution: {integrity: sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w==} '@typescript-eslint/eslint-plugin@8.67.0': - resolution: - { integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-Un7Heoyj65NREbKAyIrFxeM143NZpExWmy1Nep4DLeQOeLlTeumPjoNKnBrU5D5moWXbPJgRa5Uwcdu0faVNGQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: '@typescript-eslint/parser': ^8.67.0 eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/parser@8.67.0': - resolution: - { integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-fUBfTuuEulWqX6V8+O3PtScV01tzYYRUDTAirHFKoRAt7nOzoGiPt0M/bB47wWNy0coOOcgEwAMUtBpykMxl6w==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/project-service@8.67.0': - resolution: - { integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-cvE8c7ulYeXN9fYuszhCeCsbzyVEXuhrRCybnBre7TUmqb5nRmBfQAwCj0O3WJFDeyAZt4VYv51vMCC9LHSdYw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/scope-manager@8.67.0': - resolution: - { integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-EgvsleTwS4E+WzzSvem8fAUubLwatMNF1B5hHSLQxcvs7q2dtRhGyujHwLJSYlG41niJ7GP24Aha2+0mb1b2kg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/tsconfig-utils@8.67.0': - resolution: - { integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-vV+LUSv5njUWsknE71fqKTlXUva+R76SaeORd6Zojcunk/6DvKFXONU3BrAs2H49mbygUXt6gbYunzwqNwlhdg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/type-utils@8.67.0': - resolution: - { integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-aVWDXbRmdXO9siTfX4ditQI1T9+zVcNazT48EJCD0v40/9RIFoUgZ05CmGEq9H2gixRpjUn/iplwvlcvutJW/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/types@8.67.0': - resolution: - { integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-sBtgslww8nsMYUjhdPBiSyUqSzT8uR6g93A2QXnQC8+cGdjz0CyaOdqHDRJb1AtORbZCNUJBBeFA/tNR2uQmww==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@typescript-eslint/typescript-estree@8.67.0': - resolution: - { integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-EKQBCE9yNlRJYm7jdTW5AhDacDUmSwQb0FAJAmK2EKYrNXIsa2vxcSZx6PvJ/dEdI6lS+Y9W+EXckLj0iPFGcw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/utils@8.67.0': - resolution: - { integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-U9D1FdwEWBwok3hxxSdhclMb0twvt9QnjIQ0VfQ1AiX2epnpSgv2ubVDsayOFyY8K6FX+AQ7E0FKWVG3iKsj1A==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' '@typescript-eslint/visitor-keys@8.67.0': - resolution: - { integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-fkv8dHRDqfGtTHuJeebdrQ7cX6Ad4WAS00rgHh9UGvMycF1mjBfsxry1XsLIFhWZ6Judlh6UdzK+TYlbpCXgnA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} '@unrs/resolver-binding-android-arm-eabi@1.12.2': - resolution: - { integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w== } + resolution: {integrity: sha512-g5T90pqg1bo/7mytQx6F4iBNC0Wsh9cu+z9veDbFjc7HjpesJFWD7QMS0NGStXM075+7dJPPVvBbpZlnrdpi/w==} cpu: [arm] os: [android] '@unrs/resolver-binding-android-arm64@1.12.2': - resolution: - { integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ== } + resolution: {integrity: sha512-YGCRZv/9GLhwmz6mYDeTsm/92BAyR28l6c2ReweVW5pWgfsitWLY8upvfRlGdoyD8HjeTHSYJWyZGD4KJA/nFQ==} cpu: [arm64] os: [android] '@unrs/resolver-binding-darwin-arm64@1.12.2': - resolution: - { integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w== } + resolution: {integrity: sha512-u9DiNT1auQMO20A9SyTuG3wUgQWB9Z7KjAg0uFuCDR1FsAY8A0CG2S6JpHS1xwm/w1G08bjXZDcyOCjv1WAm2w==} cpu: [arm64] os: [darwin] '@unrs/resolver-binding-darwin-x64@1.12.2': - resolution: - { integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA== } + resolution: {integrity: sha512-f7rPLi/T1HVKZu/u6t87lroib16n8vrSzcyxI7lg4BGO9UF26KhQL44sd9eOUgrTYhvRXtWOIZT5PejdPyJfUA==} cpu: [x64] os: [darwin] '@unrs/resolver-binding-freebsd-x64@1.12.2': - resolution: - { integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg== } + resolution: {integrity: sha512-BpcOjWCJub6nRZUS2zA20pmLvjtqAtGejETaIyRLiZiQf++cbrjltLA5NN/xaXfqeOBOSlMFbemIl5/S5tljmg==} cpu: [x64] os: [freebsd] '@unrs/resolver-binding-linux-arm-gnueabihf@1.12.2': - resolution: - { integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A== } + resolution: {integrity: sha512-vZTDvdSISZjJx66OzJqtsOhzifbqRjbmI1Mnu49fQDwog5GtDI4QidRiEAYbZCRj9C8YZEW+3ZjqsyS9GR4k2A==} cpu: [arm] os: [linux] '@unrs/resolver-binding-linux-arm-musleabihf@1.12.2': - resolution: - { integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g== } + resolution: {integrity: sha512-BiPI+IrIlwcW4nLLMM21+B1dFPzd55yAVgVGrdgDjNef+ch03GdxrcyaIz8X9SsQirh/kCQ7mviyWlMxdh2D7g==} cpu: [arm] os: [linux] '@unrs/resolver-binding-linux-arm64-gnu@1.12.2': - resolution: - { integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg== } + resolution: {integrity: sha512-zJc0H99FEPoFfSrNpa91HYfxzfAJCr502oxNK1cfdC9hlaFI43RT+JFCann9JUgZmLzzntChHyn13Sgn9ljHNg==} cpu: [arm64] os: [linux] '@unrs/resolver-binding-linux-arm64-musl@1.12.2': - resolution: - { integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA== } + resolution: {integrity: sha512-KQ3Lki6l+Pz1k/eBipN41ES+YUK30beLGb9YqcB1O542cyLCNE6GaxrfcY3T6EezmGGk84wb5XyO9loTM9tkcA==} cpu: [arm64] os: [linux] '@unrs/resolver-binding-linux-loong64-gnu@1.12.2': - resolution: - { integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q== } + resolution: {integrity: sha512-3SJGEh1DborhG6pyxvhPzCT4bbSIVihsvgJc13P1bHG7KLdNDaF9T3gsTwFc7Jw/5Y5/iWOjkEx7Zy0NvCGX3Q==} cpu: [loong64] os: [linux] '@unrs/resolver-binding-linux-loong64-musl@1.12.2': - resolution: - { integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew== } + resolution: {integrity: sha512-jiuG/Obbel7uw1PwHNFfrkiKhLAF6mnyZ6aWlOAVN9WqKm8v0OFGnciJIHu8+CMvXLQ8AD51LPzAoUfT21D5Ew==} cpu: [loong64] os: [linux] '@unrs/resolver-binding-linux-ppc64-gnu@1.12.2': - resolution: - { integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg== } + resolution: {integrity: sha512-q7xRvVpmcfeL+LlZg8Pbbo6QaTZwDU5BaGZbwfhkEsXJn3Was8xYfE0RBH266xZt0rM6B7i8xAYIvjthuUIWHg==} cpu: [ppc64] os: [linux] '@unrs/resolver-binding-linux-riscv64-gnu@1.12.2': - resolution: - { integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A== } + resolution: {integrity: sha512-0CVdx6lcnT3Q9inOH8tsMIOJ6ImndllMjqJHg8RLVdB7Vq4SfkEXl9mCSsVNuNA4MCYycRicCUxPCabVHJRr6A==} cpu: [riscv64] os: [linux] '@unrs/resolver-binding-linux-riscv64-musl@1.12.2': - resolution: - { integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w== } + resolution: {integrity: sha512-iOwlRo9vnp6R6ohHQS11n0NnfdXx/omhkocmIfaPRpQhKZ+3BDMkkdRVh53qjkFkpPddf+FETA28NwGN7l5l+w==} cpu: [riscv64] os: [linux] '@unrs/resolver-binding-linux-s390x-gnu@1.12.2': - resolution: - { integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw== } + resolution: {integrity: sha512-HYJtLfXq94q8iZNFT1lknx258wlkkWhZeUXJRqzKBBUJ00CvZ+N33zgbCqimLjsyw5Va6uUxhVa12mI+kaveEw==} cpu: [s390x] os: [linux] '@unrs/resolver-binding-linux-x64-gnu@1.12.2': - resolution: - { integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ== } + resolution: {integrity: sha512-mPsUhunKKDih5O96Y6enDQyHc1SqBPlY1E/SfMWDM3EdJ95Z9CArPeCVwCCqbP45ljvivdEk8Fxn+SIb1rDAJQ==} cpu: [x64] os: [linux] '@unrs/resolver-binding-linux-x64-musl@1.12.2': - resolution: - { integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A== } + resolution: {integrity: sha512-azrt6+5ydLd8Vt210AAFis/lZevSfPw93EJRIJG+xPu4WCJ8K0kppCTpMyLPcKT7H15M4Jnt2tMp5bOvCkRC6A==} cpu: [x64] os: [linux] '@unrs/resolver-binding-openharmony-arm64@1.12.2': - resolution: - { integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ== } + resolution: {integrity: sha512-YZ9hP4O0X9PQb8eO980qmLNGH4zT3I9+SZTdt0Pr0YyuGQhYKoOZkV02VzrzyOZJ5xIJ3UFIenKkUkGg8GjgWQ==} cpu: [arm64] os: [openharmony] '@unrs/resolver-binding-wasm32-wasi@1.12.2': - resolution: - { integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A== } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-tYFDIkMxSflfEc/h92ZWNsZlHSwgimbNHSO3PL2JWQHfCuC2q316jMyYU9TIWZsFK2bQwyK5VAdYgn8ygPj69A==} + engines: {node: '>=14.0.0'} cpu: [wasm32] '@unrs/resolver-binding-win32-arm64-msvc@1.12.2': - resolution: - { integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g== } + resolution: {integrity: sha512-qzNyg3xL0VPQmCaUh+N5jSitce6k+uCBfMDesWRnlULOZaqUkaJ0ybdT+UqlAWJoQjuqfIU/0Ptx9bteN4D82g==} cpu: [arm64] os: [win32] '@unrs/resolver-binding-win32-ia32-msvc@1.12.2': - resolution: - { integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g== } + resolution: {integrity: sha512-WD9sY00OfpHVGfsnHZoA8jVT+esS/Bg8z8jzxp5BnDCjjwsuKsPQrzswwpFy4J1AUJbXPRfkpcX0mXrzeXW79g==} cpu: [ia32] os: [win32] '@unrs/resolver-binding-win32-x64-msvc@1.12.2': - resolution: - { integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA== } + resolution: {integrity: sha512-nAB74NfSNKknqQ1RrYj6uz8FcXEomu/MATJZxh/x+BArzN2U3JbOYC0APYzUIGhVY3m5hRxA8VPNdPBoG8txlA==} cpu: [x64] os: [win32] '@vitest/coverage-v8@3.2.6': - resolution: - { integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw== } + resolution: {integrity: sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==} peerDependencies: '@vitest/browser': 3.2.6 vitest: 3.2.6 @@ -1472,12 +1245,10 @@ packages: optional: true '@vitest/expect@3.2.6': - resolution: - { integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ== } + resolution: {integrity: sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==} '@vitest/mocker@3.2.6': - resolution: - { integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw== } + resolution: {integrity: sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==} peerDependencies: msw: ^2.4.9 vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 @@ -1488,47 +1259,37 @@ packages: optional: true '@vitest/pretty-format@3.2.6': - resolution: - { integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA== } + resolution: {integrity: sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==} '@vitest/pretty-format@3.2.7': - resolution: - { integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA== } + resolution: {integrity: sha512-KUHlwqVu0sRlhCdyPdQ/wBoTfRahjUky1MubOmYw9fWfIZy1gNoHpuaaQBPAaMaVYdQYHJLurzj8ECCj5OwTqA==} '@vitest/runner@3.2.6': - resolution: - { integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q== } + resolution: {integrity: sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==} '@vitest/snapshot@3.2.6': - resolution: - { integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw== } + resolution: {integrity: sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==} '@vitest/spy@3.2.6': - resolution: - { integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg== } + resolution: {integrity: sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==} '@vitest/utils@3.2.6': - resolution: - { integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg== } + resolution: {integrity: sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==} '@wallet-standard/base@1.1.1': - resolution: - { integrity: sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ== } - engines: { node: '>=22' } + resolution: {integrity: sha512-gggIHTtxicF9XFMQ12DkfS6NAG92Ak795JeSA7f2whAQ6Y3AkMWWuCMxSZXG2NIPN42kEaZSNVjqMsJRaJRxMQ==} + engines: {node: '>=22'} '@wallet-standard/features@1.1.1': - resolution: - { integrity: sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA== } - engines: { node: '>=22' } + resolution: {integrity: sha512-aCWYmVeSCGViyEU5k7GMoW8zxE4Gs+C1s1Pp2XLesvSNlnZ4PMES9HUnTB3hl0b3RVj7C61yze3IWyrncqg4MA==} + engines: {node: '>=22'} abi-wan-kanabi@2.2.4: - resolution: - { integrity: sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg== } + resolution: {integrity: sha512-0aA81FScmJCPX+8UvkXLki3X1+yPQuWxEkqXBVKltgPAK79J+NB+Lp5DouMXa7L6f+zcRlIA/6XO7BN/q9fnvg==} hasBin: true abitype@1.2.3: - resolution: - { integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg== } + resolution: {integrity: sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg==} peerDependencies: typescript: '>=5.0.4' zod: ^3.22.0 || ^4.0.0 @@ -1539,8 +1300,7 @@ packages: optional: true abitype@1.3.0: - resolution: - { integrity: sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg== } + resolution: {integrity: sha512-fk6Te+bojIFrMvMZrnOO+SxCB+RUksTGOzq/60ZRvs1L+BVzvi2bqt9L3W/17ZLdZsyM1FuYf65P5nlmoiH1Bg==} peerDependencies: typescript: '>=5.0.4' zod: ^3.22.0 || ^4.0.0 @@ -1551,304 +1311,241 @@ packages: optional: true acorn-jsx@5.3.2: - resolution: - { integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ== } + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 acorn@8.18.0: - resolution: - { integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ== } - engines: { node: '>=0.4.0' } + resolution: {integrity: sha512-lGq+9yr1/GuAWaVYIHRjvvySG5/4VfKIvC8EWxStPdcDh/Ka7FG3twP6v4d5BkravUilhIAsG4Qj83t02LWUPQ==} + engines: {node: '>=0.4.0'} hasBin: true agent-base@7.1.4: - resolution: - { integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ== } - engines: { node: '>= 14' } + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} ajv@6.15.0: - resolution: - { integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw== } + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} ansi-regex@5.0.1: - resolution: - { integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== } - engines: { node: '>=8' } + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} ansi-regex@6.3.0: - resolution: - { integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ== } - engines: { node: '>=12' } + resolution: {integrity: sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==} + engines: {node: '>=12'} ansi-styles@4.3.0: - resolution: - { integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== } - engines: { node: '>=8' } + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} ansi-styles@5.2.0: - resolution: - { integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA== } - engines: { node: '>=10' } + resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} + engines: {node: '>=10'} ansi-styles@6.2.3: - resolution: - { integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg== } - engines: { node: '>=12' } + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} ansicolors@0.3.2: - resolution: - { integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg== } + resolution: {integrity: sha512-QXu7BPrP29VllRxH8GwB7x5iX5qWKAAMLqKQGWTeLWVlNHNOpVMJ91dsxQAIWXpjuW5wqvxu3Jd/nRjrJ+0pqg==} argparse@2.0.1: - resolution: - { integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== } + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} aria-query@5.3.0: - resolution: - { integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A== } + resolution: {integrity: sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==} aria-query@5.3.2: - resolution: - { integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} + engines: {node: '>= 0.4'} array-buffer-byte-length@1.0.2: - resolution: - { integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} + engines: {node: '>= 0.4'} array-includes@3.1.9: - resolution: - { integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==} + engines: {node: '>= 0.4'} array.prototype.findlast@1.2.5: - resolution: - { integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==} + engines: {node: '>= 0.4'} array.prototype.findlastindex@1.2.6: - resolution: - { integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==} + engines: {node: '>= 0.4'} array.prototype.flat@1.3.3: - resolution: - { integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==} + engines: {node: '>= 0.4'} array.prototype.flatmap@1.3.3: - resolution: - { integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==} + engines: {node: '>= 0.4'} array.prototype.tosorted@1.1.4: - resolution: - { integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==} + engines: {node: '>= 0.4'} arraybuffer.prototype.slice@1.0.4: - resolution: - { integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} + engines: {node: '>= 0.4'} assertion-error@2.0.1: - resolution: - { integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA== } - engines: { node: '>=12' } + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} ast-types-flow@0.0.8: - resolution: - { integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ== } + resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} ast-v8-to-istanbul@0.3.12: - resolution: - { integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g== } + resolution: {integrity: sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==} async-function@1.0.0: - resolution: - { integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} + engines: {node: '>= 0.4'} async-mutex@0.5.0: - resolution: - { integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA== } + resolution: {integrity: sha512-1A94B18jkJ3DYq284ohPxoXbfTA5HsQ7/Mf4DEhcyLx3Bz27Rh59iScbB6EPiP+B+joue6YCxcMXSbFC1tZKwA==} available-typed-arrays@1.0.7: - resolution: - { integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==} + engines: {node: '>= 0.4'} axe-core@4.13.0: - resolution: - { integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A== } - engines: { node: '>=4' } + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} axobject-query@4.1.0: - resolution: - { integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==} + engines: {node: '>= 0.4'} balanced-match@1.0.2: - resolution: - { integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== } + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} balanced-match@4.0.4: - resolution: - { integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA== } - engines: { node: 18 || 20 || >=22 } + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} baseline-browser-mapping@2.11.18: - resolution: - { integrity: sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw== } - engines: { node: '>=6.0.0' } + resolution: {integrity: sha512-1iEmLEYSiE1SeBoAfPo/Mnx3PzfzHUkDK61ASkCpuk3YXugYLH5DYK1SzqV55F8FMI6s0F+/tCP7Polz1QRjxw==} + engines: {node: '>=6.0.0'} hasBin: true brace-expansion@1.1.18: - resolution: - { integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw== } + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} brace-expansion@2.1.4: - resolution: - { integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg== } + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} brace-expansion@5.0.9: - resolution: - { integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg== } - engines: { node: 20 || >=22 } + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} braces@3.0.3: - resolution: - { integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== } - engines: { node: '>=8' } + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} browserslist@4.28.8: - resolution: - { integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA== } - engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + resolution: {integrity: sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} hasBin: true cac@6.7.14: - resolution: - { integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ== } - engines: { node: '>=8' } + resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} + engines: {node: '>=8'} call-bind-apply-helpers@1.0.2: - resolution: - { integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} call-bind@1.0.9: - resolution: - { integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ==} + engines: {node: '>= 0.4'} call-bound@1.0.4: - resolution: - { integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} callsites@3.1.0: - resolution: - { integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ== } - engines: { node: '>=6' } + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} caniuse-lite@1.0.30001809: - resolution: - { integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ== } + resolution: {integrity: sha512-xxWVywk6a6Arlk+hymeycyn/VgqEfLDxupvhH/xiY5SJ/18kmi9o6MiO320DCUzypORHLtvh0I4i04tUhCNHNQ==} cardinal@2.1.1: - resolution: - { integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw== } + resolution: {integrity: sha512-JSr5eOgoEymtYHBjNWyjrMqet9Am2miJhlfKNdqLp6zoeAh0KN5dRAcxlecj5mAJrmQomgiOBj35xHLrFjqBpw==} hasBin: true chai@5.3.3: - resolution: - { integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} + engines: {node: '>=18'} chalk@4.1.2: - resolution: - { integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== } - engines: { node: '>=10' } + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} check-error@2.1.3: - resolution: - { integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA== } - engines: { node: '>= 16' } + resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} + engines: {node: '>= 16'} client-only@0.0.1: - resolution: - { integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA== } + resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} cliui@8.0.1: - resolution: - { integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ== } - engines: { node: '>=12' } + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} color-convert@2.0.1: - resolution: - { integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== } - engines: { node: '>=7.0.0' } + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} color-name@1.1.4: - resolution: - { integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== } + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} concat-map@0.0.1: - resolution: - { integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== } + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} convert-source-map@2.0.0: - resolution: - { integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== } + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} cross-spawn@7.0.6: - resolution: - { integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== } - engines: { node: '>= 8' } + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} css.escape@1.5.1: - resolution: - { integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg== } + resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} cssstyle@4.6.0: - resolution: - { integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} csstype@3.2.3: - resolution: - { integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== } + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} damerau-levenshtein@1.0.8: - resolution: - { integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA== } + resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} data-urls@5.0.0: - resolution: - { integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} data-view-buffer@1.0.2: - resolution: - { integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==} + engines: {node: '>= 0.4'} data-view-byte-length@1.0.2: - resolution: - { integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==} + engines: {node: '>= 0.4'} data-view-byte-offset@1.0.1: - resolution: - { integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==} + engines: {node: '>= 0.4'} debug@3.2.7: - resolution: - { integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ== } + resolution: {integrity: sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -1856,9 +1553,8 @@ packages: optional: true debug@4.4.3: - resolution: - { integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA== } - engines: { node: '>=6.0' } + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} peerDependencies: supports-color: '*' peerDependenciesMeta: @@ -1866,150 +1562,119 @@ packages: optional: true decimal.js@10.6.0: - resolution: - { integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg== } + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} deep-eql@5.0.2: - resolution: - { integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q== } - engines: { node: '>=6' } + resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} + engines: {node: '>=6'} deep-is@0.1.4: - resolution: - { integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ== } + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} define-data-property@1.1.4: - resolution: - { integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} define-properties@1.2.1: - resolution: - { integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} dequal@2.0.3: - resolution: - { integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== } - engines: { node: '>=6' } + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} detect-libc@2.1.2: - resolution: - { integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ== } - engines: { node: '>=8' } + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} doctrine@2.1.0: - resolution: - { integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==} + engines: {node: '>=0.10.0'} dom-accessibility-api@0.5.16: - resolution: - { integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg== } + resolution: {integrity: sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==} dom-accessibility-api@0.6.3: - resolution: - { integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w== } + resolution: {integrity: sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==} dunder-proto@1.0.1: - resolution: - { integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} eastasianwidth@0.2.0: - resolution: - { integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== } + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} electron-to-chromium@1.5.412: - resolution: - { integrity: sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA== } + resolution: {integrity: sha512-z4rMe3esBzlzovKHj4gxJnsCGZRK5l4baUvm+gCGJBPE+gsyUMKsuU9tnEUtI1dOebXz1ytAPGjvXhmQ7rIPwA==} emoji-regex@8.0.0: - resolution: - { integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== } + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} emoji-regex@9.2.2: - resolution: - { integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== } + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} enhanced-resolve@5.24.5: - resolution: - { integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A== } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} entities@6.0.1: - resolution: - { integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g== } - engines: { node: '>=0.12' } + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} es-abstract-get@1.0.0: - resolution: - { integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==} + engines: {node: '>= 0.4'} es-abstract@1.24.2: - resolution: - { integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg==} + engines: {node: '>= 0.4'} es-define-property@1.0.1: - resolution: - { integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} es-errors@1.3.0: - resolution: - { integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} es-iterator-helpers@1.4.0: - resolution: - { integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-c/A0P0oxkACDc+cKWw8evLXK83oBKgn0qPOqCYT4x9uolpCIJAcYvJC9QYKNDRPsTeGyCrQ326jrvgZWdCdK5Q==} + engines: {node: '>= 0.4'} es-module-lexer@1.7.0: - resolution: - { integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA== } + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} es-object-atoms@1.1.2: - resolution: - { integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} es-set-tostringtag@2.1.0: - resolution: - { integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} es-shim-unscopables@1.1.0: - resolution: - { integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==} + engines: {node: '>= 0.4'} es-to-primitive@1.3.4: - resolution: - { integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-yPDz7wqpg1/mmHLmS3tcfTfbw5f1eryXvyghYBffGdERwe+mV7ZcWzTR8LR17Kvqt3qfPurjlonmnq3MKXIOXw==} + engines: {node: '>= 0.4'} esbuild@0.28.2: - resolution: - { integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==} + engines: {node: '>=18'} hasBin: true escalade@3.2.0: - resolution: - { integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== } - engines: { node: '>=6' } + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} escape-string-regexp@4.0.0: - resolution: - { integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== } - engines: { node: '>=10' } + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} eslint-config-next@16.3.2: - resolution: - { integrity: sha512-gTABOJmyc6pEgSX1Z1VOjBxkSmo5Hkdj+ePclDf8HLGTsnVWzgtDdrOeQrAnUkf0JNJJhwPuXrsIwmFZRKJLoQ== } + resolution: {integrity: sha512-gTABOJmyc6pEgSX1Z1VOjBxkSmo5Hkdj+ePclDf8HLGTsnVWzgtDdrOeQrAnUkf0JNJJhwPuXrsIwmFZRKJLoQ==} peerDependencies: eslint: '>=9.0.0' typescript: '>=3.3.1' @@ -2018,13 +1683,11 @@ packages: optional: true eslint-import-resolver-node@0.3.10: - resolution: - { integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ== } + resolution: {integrity: sha512-tRrKqFyCaKict5hOd244sL6EQFNycnMQnBe+j8uqGNXYzsImGbGUU4ibtoaBmv5FLwJwcFJNeg1GeVjQfbMrDQ==} eslint-import-resolver-typescript@3.10.1: - resolution: - { integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ== } - engines: { node: ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-A1rHYb06zjMGAxdLSkN2fXPBwuSaQ0iO5M/hdyS0Ajj1VBaRp0sPD3dn1FhME3c/JluGFbwSxyCfqdSbtQLAHQ==} + engines: {node: ^14.18.0 || >=16.0.0} peerDependencies: eslint: '*' eslint-plugin-import: '*' @@ -2036,9 +1699,8 @@ packages: optional: true eslint-module-utils@2.14.0: - resolution: - { integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig== } - engines: { node: '>=4' } + resolution: {integrity: sha512-W2WCRZ9Dqntd+2u8jJcVMV2PKulc6RdLgUUoh/yQr3uB6lo/ZOeGx11sv60/8S4QFFKNslAlWhr9u0Ef7ZW6Ig==} + engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' eslint: '*' @@ -2058,9 +1720,8 @@ packages: optional: true eslint-plugin-import@2.32.0: - resolution: - { integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA== } - engines: { node: '>=4' } + resolution: {integrity: sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==} + engines: {node: '>=4'} peerDependencies: '@typescript-eslint/parser': '*' eslint: ^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9 @@ -2069,50 +1730,42 @@ packages: optional: true eslint-plugin-jsx-a11y@6.10.2: - resolution: - { integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q== } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==} + engines: {node: '>=4.0'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9 eslint-plugin-react-hooks@7.1.1: - resolution: - { integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g== } - engines: { node: '>=18' } + resolution: {integrity: sha512-f2I7Gw6JbvCexzIInuSbZpfdQ44D7iqdWX01FKLvrPgqxoE7oMj8clOfto8U6vYiz4yd5oKu39rRSVOe1zRu0g==} + engines: {node: '>=18'} peerDependencies: eslint: ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0 eslint-plugin-react@7.37.5: - resolution: - { integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA== } - engines: { node: '>=4' } + resolution: {integrity: sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==} + engines: {node: '>=4'} peerDependencies: eslint: ^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7 eslint-scope@8.4.0: - resolution: - { integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@3.4.3: - resolution: - { integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag== } - engines: { node: ^12.22.0 || ^14.17.0 || >=16.0.0 } + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} eslint-visitor-keys@4.2.1: - resolution: - { integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} eslint-visitor-keys@5.0.1: - resolution: - { integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA== } - engines: { node: ^20.19.0 || ^22.13.0 || >=24 } + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} eslint@9.39.5: - resolution: - { integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-DgZS62aPLXKlnxILS/AYCoRvHaZeXceIzlXPkkGGzJWSow1aEk0lbTlxUSlyjC8jcaKxAdOnTDz+o1JFSBsyjw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} deprecated: This version is no longer supported. Please see https://eslint.org/version-support for other options. hasBin: true peerDependencies: @@ -2122,74 +1775,59 @@ packages: optional: true espree@10.4.0: - resolution: - { integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} esprima@4.0.1: - resolution: - { integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A== } - engines: { node: '>=4' } + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} hasBin: true esquery@1.7.0: - resolution: - { integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g== } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} esrecurse@4.3.0: - resolution: - { integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag== } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} estraverse@5.3.0: - resolution: - { integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA== } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} estree-walker@3.0.3: - resolution: - { integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g== } + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} esutils@2.0.3: - resolution: - { integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} eventemitter3@5.0.1: - resolution: - { integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA== } + resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} expect-type@1.4.0: - resolution: - { integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA== } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} fast-deep-equal@3.1.3: - resolution: - { integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== } + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} fast-glob@3.3.1: - resolution: - { integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg== } - engines: { node: '>=8.6.0' } + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} + engines: {node: '>=8.6.0'} fast-json-stable-stringify@2.1.0: - resolution: - { integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== } + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} fast-levenshtein@2.0.6: - resolution: - { integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw== } + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} fastq@1.20.1: - resolution: - { integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw== } + resolution: {integrity: sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==} fdir@6.5.0: - resolution: - { integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} peerDependencies: picomatch: ^3 || ^4 peerDependenciesMeta: @@ -2197,451 +1835,359 @@ packages: optional: true file-entry-cache@8.0.0: - resolution: - { integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ== } - engines: { node: '>=16.0.0' } + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} fill-range@7.1.1: - resolution: - { integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== } - engines: { node: '>=8' } + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} find-up@5.0.0: - resolution: - { integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng== } - engines: { node: '>=10' } + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} flat-cache@4.0.1: - resolution: - { integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw== } - engines: { node: '>=16' } + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} flatted@3.4.4: - resolution: - { integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q== } + resolution: {integrity: sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==} for-each@0.3.5: - resolution: - { integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==} + engines: {node: '>= 0.4'} foreground-child@3.3.1: - resolution: - { integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== } - engines: { node: '>=14' } + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} fs-extra@10.1.0: - resolution: - { integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ== } - engines: { node: '>=12' } + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} fsevents@2.3.2: - resolution: - { integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA== } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] fsevents@2.3.3: - resolution: - { integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== } - engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} os: [darwin] function-bind@1.1.2: - resolution: - { integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== } + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} function.prototype.name@1.2.0: - resolution: - { integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-jObKIik1P2QjPHP5nz5BaOtUlfgS0fWo8IUByNXkM+o+02sJOi94em77GwJKQSJ3gfPHdgzLNrHc1uokV4P/ew==} + engines: {node: '>= 0.4'} functions-have-names@1.2.3: - resolution: - { integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== } + resolution: {integrity: sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==} generator-function@2.0.1: - resolution: - { integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==} + engines: {node: '>= 0.4'} gensync@1.0.0-beta.2: - resolution: - { integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== } - engines: { node: '>=6.9.0' } + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} get-caller-file@2.0.5: - resolution: - { integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg== } - engines: { node: 6.* || 8.* || >= 10.* } + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} get-intrinsic@1.3.0: - resolution: - { integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} get-proto@1.0.1: - resolution: - { integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} get-symbol-description@1.1.0: - resolution: - { integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==} + engines: {node: '>= 0.4'} get-tsconfig@4.14.3: - resolution: - { integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA== } + resolution: {integrity: sha512-++QEw4DIY7WGoukz+/+A/8dGYPT9l9yIadnmSgZ8Rjr3YVSVDipQSO9CdnJo9ePqFqUUqh+wk9uIaoiAwsiPkA==} glob-parent@5.1.2: - resolution: - { integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== } - engines: { node: '>= 6' } + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} glob-parent@6.0.2: - resolution: - { integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A== } - engines: { node: '>=10.13.0' } + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} glob@10.5.0: - resolution: - { integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg== } + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true globals@14.0.0: - resolution: - { integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} globals@16.4.0: - resolution: - { integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==} + engines: {node: '>=18'} globalthis@1.0.4: - resolution: - { integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} gopd@1.2.0: - resolution: - { integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} graceful-fs@4.2.11: - resolution: - { integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== } + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} has-bigints@1.1.0: - resolution: - { integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==} + engines: {node: '>= 0.4'} has-flag@4.0.0: - resolution: - { integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== } - engines: { node: '>=8' } + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} has-property-descriptors@1.0.2: - resolution: - { integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== } + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} has-proto@1.2.0: - resolution: - { integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==} + engines: {node: '>= 0.4'} has-symbols@1.1.0: - resolution: - { integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} has-tostringtag@1.0.2: - resolution: - { integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} hasown@2.0.4: - resolution: - { integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} hermes-estree@0.25.1: - resolution: - { integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw== } + resolution: {integrity: sha512-0wUoCcLp+5Ev5pDW2OriHC2MJCbwLwuRx+gAqMTOkGKJJiBCLjtrvy4PWUGn6MIVefecRpzoOZ/UV6iGdOr+Cw==} hermes-parser@0.25.1: - resolution: - { integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA== } + resolution: {integrity: sha512-6pEjquH3rqaI6cYAXYPcz9MS4rY6R4ngRgrgfDshRptUZIc3lw0MCIJIGDj9++mfySOuPTHB4nrSW99BCvOPIA==} html-encoding-sniffer@4.0.0: - resolution: - { integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} html-escaper@2.0.2: - resolution: - { integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg== } + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} http-proxy-agent@7.0.2: - resolution: - { integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig== } - engines: { node: '>= 14' } + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} https-proxy-agent@7.0.6: - resolution: - { integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw== } - engines: { node: '>= 14' } + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} iconv-lite@0.6.3: - resolution: - { integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} ignore@5.3.2: - resolution: - { integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g== } - engines: { node: '>= 4' } + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} ignore@7.0.6: - resolution: - { integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw== } - engines: { node: '>= 4' } + resolution: {integrity: sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw==} + engines: {node: '>= 4'} import-fresh@3.3.1: - resolution: - { integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ== } - engines: { node: '>=6' } + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} imurmurhash@0.1.4: - resolution: - { integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA== } - engines: { node: '>=0.8.19' } + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} indent-string@4.0.0: - resolution: - { integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== } - engines: { node: '>=8' } + resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} + engines: {node: '>=8'} internal-slot@1.1.0: - resolution: - { integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} + engines: {node: '>= 0.4'} is-array-buffer@3.0.5: - resolution: - { integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} + engines: {node: '>= 0.4'} is-async-function@2.1.1: - resolution: - { integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} + engines: {node: '>= 0.4'} is-bigint@1.1.0: - resolution: - { integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==} + engines: {node: '>= 0.4'} is-boolean-object@1.2.2: - resolution: - { integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A==} + engines: {node: '>= 0.4'} is-bun-module@2.0.0: - resolution: - { integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ== } + resolution: {integrity: sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==} is-callable@1.2.7: - resolution: - { integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==} + engines: {node: '>= 0.4'} is-core-module@2.16.2: - resolution: - { integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} is-data-view@1.0.2: - resolution: - { integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==} + engines: {node: '>= 0.4'} is-date-object@1.1.0: - resolution: - { integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} + engines: {node: '>= 0.4'} is-document.all@1.0.0: - resolution: - { integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==} + engines: {node: '>= 0.4'} is-extglob@2.1.1: - resolution: - { integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} is-finalizationregistry@1.1.1: - resolution: - { integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==} + engines: {node: '>= 0.4'} is-fullwidth-code-point@3.0.0: - resolution: - { integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== } - engines: { node: '>=8' } + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} is-generator-function@1.1.2: - resolution: - { integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==} + engines: {node: '>= 0.4'} is-glob@4.0.3: - resolution: - { integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} is-map@2.0.3: - resolution: - { integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==} + engines: {node: '>= 0.4'} is-negative-zero@2.0.3: - resolution: - { integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==} + engines: {node: '>= 0.4'} is-number-object@1.1.1: - resolution: - { integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==} + engines: {node: '>= 0.4'} is-number@7.0.0: - resolution: - { integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== } - engines: { node: '>=0.12.0' } + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} is-potential-custom-element-name@1.0.1: - resolution: - { integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ== } + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} is-regex@1.2.1: - resolution: - { integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==} + engines: {node: '>= 0.4'} is-set@2.0.3: - resolution: - { integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==} + engines: {node: '>= 0.4'} is-shared-array-buffer@1.0.4: - resolution: - { integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==} + engines: {node: '>= 0.4'} is-string@1.1.1: - resolution: - { integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==} + engines: {node: '>= 0.4'} is-symbol@1.1.1: - resolution: - { integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==} + engines: {node: '>= 0.4'} is-typed-array@1.1.15: - resolution: - { integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} + engines: {node: '>= 0.4'} is-weakmap@2.0.2: - resolution: - { integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} + engines: {node: '>= 0.4'} is-weakref@1.1.1: - resolution: - { integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==} + engines: {node: '>= 0.4'} is-weakset@2.0.4: - resolution: - { integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==} + engines: {node: '>= 0.4'} isarray@2.0.5: - resolution: - { integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== } + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} isexe@2.0.0: - resolution: - { integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== } + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} isows@1.0.7: - resolution: - { integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg== } + resolution: {integrity: sha512-I1fSfDCZL5P0v33sVqeTDSpcstAg/N+wF5HS033mogOVIp4B+oHC7oOCsA3axAbBSGTJ8QubbNmnIRN/h8U7hg==} peerDependencies: ws: '*' istanbul-lib-coverage@3.2.2: - resolution: - { integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg== } - engines: { node: '>=8' } + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} istanbul-lib-report@3.0.1: - resolution: - { integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw== } - engines: { node: '>=10' } + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} istanbul-lib-source-maps@5.0.6: - resolution: - { integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A== } - engines: { node: '>=10' } + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} istanbul-reports@3.2.0: - resolution: - { integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA== } - engines: { node: '>=8' } + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} iterator.prototype@1.1.5: - resolution: - { integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} + engines: {node: '>= 0.4'} jackspeak@3.4.3: - resolution: - { integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw== } + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} jiti@2.7.0: - resolution: - { integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ== } + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true js-tokens@10.0.0: - resolution: - { integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q== } + resolution: {integrity: sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==} js-tokens@4.0.0: - resolution: - { integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== } + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} js-tokens@9.0.1: - resolution: - { integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ== } + resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} js-yaml@4.3.1: - resolution: - { integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ== } + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} hasBin: true jsdom@26.1.0: - resolution: - { integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} peerDependencies: canvas: ^3.0.0 peerDependenciesMeta: @@ -2649,258 +2195,209 @@ packages: optional: true jsesc@3.1.0: - resolution: - { integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== } - engines: { node: '>=6' } + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} hasBin: true json-buffer@3.0.1: - resolution: - { integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ== } + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-schema-traverse@0.4.1: - resolution: - { integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg== } + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} json-stable-stringify-without-jsonify@1.0.1: - resolution: - { integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw== } + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} json5@1.0.2: - resolution: - { integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA== } + resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true json5@2.2.3: - resolution: - { integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== } - engines: { node: '>=6' } + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} hasBin: true jsonfile@6.2.1: - resolution: - { integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q== } + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} jsx-ast-utils@3.3.5: - resolution: - { integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ== } - engines: { node: '>=4.0' } + resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==} + engines: {node: '>=4.0'} keyv@4.5.4: - resolution: - { integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw== } + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} language-subtag-registry@0.3.23: - resolution: - { integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ== } + resolution: {integrity: sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==} language-tags@1.0.9: - resolution: - { integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA== } - engines: { node: '>=0.10' } + resolution: {integrity: sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==} + engines: {node: '>=0.10'} levn@0.4.1: - resolution: - { integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ== } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} lightningcss-android-arm64@1.32.0: - resolution: - { integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [android] lightningcss-darwin-arm64@1.32.0: - resolution: - { integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [darwin] lightningcss-darwin-x64@1.32.0: - resolution: - { integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [darwin] lightningcss-freebsd-x64@1.32.0: - resolution: - { integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [freebsd] lightningcss-linux-arm-gnueabihf@1.32.0: - resolution: - { integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} cpu: [arm] os: [linux] lightningcss-linux-arm64-gnu@1.32.0: - resolution: - { integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] lightningcss-linux-arm64-musl@1.32.0: - resolution: - { integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [linux] lightningcss-linux-x64-gnu@1.32.0: - resolution: - { integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] lightningcss-linux-x64-musl@1.32.0: - resolution: - { integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [linux] lightningcss-win32-arm64-msvc@1.32.0: - resolution: - { integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} cpu: [arm64] os: [win32] lightningcss-win32-x64-msvc@1.32.0: - resolution: - { integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} cpu: [x64] os: [win32] lightningcss@1.32.0: - resolution: - { integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} locate-path@6.0.0: - resolution: - { integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw== } - engines: { node: '>=10' } + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} lodash.merge@4.6.2: - resolution: - { integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ== } + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} loose-envify@1.4.0: - resolution: - { integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== } + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} hasBin: true lossless-json@4.3.1: - resolution: - { integrity: sha512-SqD/Bg3ZfltBJ2Z14hJ/BihnvtV553WO4g9/ePtlp4lrnl9jF3AdIJt53A/Wkg/0Li+LMfxaBqgx1MiFZdQlpQ== } + resolution: {integrity: sha512-SqD/Bg3ZfltBJ2Z14hJ/BihnvtV553WO4g9/ePtlp4lrnl9jF3AdIJt53A/Wkg/0Li+LMfxaBqgx1MiFZdQlpQ==} loupe@3.2.1: - resolution: - { integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ== } + resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} lru-cache@10.4.3: - resolution: - { integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== } + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} lru-cache@5.1.1: - resolution: - { integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== } + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} lz-string@1.5.0: - resolution: - { integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ== } + resolution: {integrity: sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==} hasBin: true magic-string@0.30.21: - resolution: - { integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== } + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} magicast@0.3.5: - resolution: - { integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ== } + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} make-dir@4.0.0: - resolution: - { integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw== } - engines: { node: '>=10' } + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} math-intrinsics@1.1.0: - resolution: - { integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} merge2@1.4.1: - resolution: - { integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== } - engines: { node: '>= 8' } + resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} + engines: {node: '>= 8'} micromatch@4.0.8: - resolution: - { integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} min-indent@1.0.1: - resolution: - { integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg== } - engines: { node: '>=4' } + resolution: {integrity: sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==} + engines: {node: '>=4'} minimatch@10.2.6: - resolution: - { integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A== } - engines: { node: 18 || 20 || >=22 } + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} minimatch@3.1.5: - resolution: - { integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w== } + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} minimatch@9.0.9: - resolution: - { integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg== } - engines: { node: '>=16 || 14 >=14.17' } + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} minimist@1.2.8: - resolution: - { integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== } + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} minipass@7.1.3: - resolution: - { integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A== } - engines: { node: '>=16 || 14 >=14.17' } + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} ms@2.1.3: - resolution: - { integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== } + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} nanoid@3.3.18: - resolution: - { integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w== } - engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + resolution: {integrity: sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true napi-postinstall@0.3.4: - resolution: - { integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ== } - engines: { node: ^12.20.0 || ^14.18.0 || >=16.0.0 } + resolution: {integrity: sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==} + engines: {node: ^12.20.0 || ^14.18.0 || >=16.0.0} hasBin: true natural-compare@1.4.0: - resolution: - { integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw== } + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} next@16.3.2: - resolution: - { integrity: sha512-/ZCaubUy17Lld1SiPWxuPbCk2ihqAxF2QNQaPZeEaEb7t1I58qhsJN187D7AfpapHAqUPXH0f/thtdW9dWgWFg== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-/ZCaubUy17Lld1SiPWxuPbCk2ihqAxF2QNQaPZeEaEb7t1I58qhsJN187D7AfpapHAqUPXH0f/thtdW9dWgWFg==} + engines: {node: '>=20.9.0'} hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 @@ -2920,72 +2417,58 @@ packages: optional: true node-exports-info@1.6.2: - resolution: - { integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-kXs9Go0cah0qHVV2v389IXQLdLCeE1xfFtjOAF+iobu0OIoG1pje8At2vMHyaPMiPMnG/LWP50twML21eMcAag==} + engines: {node: '>= 0.4'} node-releases@2.0.53: - resolution: - { integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==} + engines: {node: '>=18'} nwsapi@2.2.24: - resolution: - { integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A== } + resolution: {integrity: sha512-7YRhZ3jS45LwmSCT4b2sVFHt/WuovaktDU07QrtOBY2PXskss5a9jfmR9jptyumwXST+rFjrmppMY1KT/yn35A==} object-assign@4.1.1: - resolution: - { integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} object-inspect@1.13.4: - resolution: - { integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} object-keys@1.1.1: - resolution: - { integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} object.assign@4.1.7: - resolution: - { integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==} + engines: {node: '>= 0.4'} object.entries@1.1.9: - resolution: - { integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==} + engines: {node: '>= 0.4'} object.fromentries@2.0.8: - resolution: - { integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==} + engines: {node: '>= 0.4'} object.groupby@1.0.3: - resolution: - { integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==} + engines: {node: '>= 0.4'} object.values@1.2.1: - resolution: - { integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} + engines: {node: '>= 0.4'} optionator@0.9.4: - resolution: - { integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g== } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} own-keys@1.0.2: - resolution: - { integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-19YVAg7T+WTrxggPukVq7DjTv6+PJ867TmhCvBsYwmbFCsZd344rq2Ld1p0wo8f8Qrrhgp82c6FJRqdXWtSEhg==} + engines: {node: '>= 0.4'} ox@0.14.34: - resolution: - { integrity: sha512-12seOIk7dv8eAoGQhcWaeKZxNz304IVcDvb9U5Y7JZAEVe21Nm1YMxLjhWah+su5BD4Omx4Zz0z5x3ij9M4GYQ== } + resolution: {integrity: sha512-12seOIk7dv8eAoGQhcWaeKZxNz304IVcDvb9U5Y7JZAEVe21Nm1YMxLjhWah+su5BD4Omx4Zz0z5x3ij9M4GYQ==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -2993,8 +2476,7 @@ packages: optional: true ox@0.4.4: - resolution: - { integrity: sha512-oJPEeCDs9iNiPs6J0rTx+Y0KGeCGyCAA3zo94yZhm8G5WpOxrwUtn2Ie/Y8IyARSqqY/j9JTKA3Fc1xs1DvFnw== } + resolution: {integrity: sha512-oJPEeCDs9iNiPs6J0rTx+Y0KGeCGyCAA3zo94yZhm8G5WpOxrwUtn2Ie/Y8IyARSqqY/j9JTKA3Fc1xs1DvFnw==} peerDependencies: typescript: '>=5.4.0' peerDependenciesMeta: @@ -3002,256 +2484,204 @@ packages: optional: true p-limit@3.1.0: - resolution: - { integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ== } - engines: { node: '>=10' } + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} p-locate@5.0.0: - resolution: - { integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw== } - engines: { node: '>=10' } + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} package-json-from-dist@1.0.1: - resolution: - { integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== } + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} parent-module@1.0.1: - resolution: - { integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g== } - engines: { node: '>=6' } + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} parse5@7.3.0: - resolution: - { integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw== } + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} path-exists@4.0.0: - resolution: - { integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w== } - engines: { node: '>=8' } + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} path-key@3.1.1: - resolution: - { integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== } - engines: { node: '>=8' } + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} path-parse@1.0.7: - resolution: - { integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== } + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} path-scurry@1.11.1: - resolution: - { integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA== } - engines: { node: '>=16 || 14 >=14.18' } + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} pathe@2.0.3: - resolution: - { integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== } + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} pathval@2.0.1: - resolution: - { integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ== } - engines: { node: '>= 14.16' } + resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} + engines: {node: '>= 14.16'} picocolors@1.1.1: - resolution: - { integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== } + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} picomatch@2.3.2: - resolution: - { integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA== } - engines: { node: '>=8.6' } + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} picomatch@4.0.5: - resolution: - { integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A== } - engines: { node: '>=12' } + resolution: {integrity: sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==} + engines: {node: '>=12'} playwright-core@1.58.2: - resolution: - { integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + engines: {node: '>=18'} hasBin: true playwright@1.58.2: - resolution: - { integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A== } - engines: { node: '>=18' } + resolution: {integrity: sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==} + engines: {node: '>=18'} hasBin: true possible-typed-array-names@1.1.0: - resolution: - { integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} + engines: {node: '>= 0.4'} postcss@8.5.26: - resolution: - { integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ== } - engines: { node: ^10 || ^12 || >=14 } + resolution: {integrity: sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==} + engines: {node: ^10 || ^12 || >=14} prelude-ls@1.2.1: - resolution: - { integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} prettier@3.6.2: - resolution: - { integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ== } - engines: { node: '>=14' } + resolution: {integrity: sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==} + engines: {node: '>=14'} hasBin: true pretty-format@27.5.1: - resolution: - { integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ== } - engines: { node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0 } + resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} + engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} prop-types@15.8.1: - resolution: - { integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg== } + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} punycode@2.3.1: - resolution: - { integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== } - engines: { node: '>=6' } + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} queue-microtask@1.2.3: - resolution: - { integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== } + resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} react-dom@19.2.8: - resolution: - { integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ== } + resolution: {integrity: sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==} peerDependencies: react: ^19.2.8 react-is@16.13.1: - resolution: - { integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== } + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} react-is@17.0.2: - resolution: - { integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== } + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} react@19.2.8: - resolution: - { integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==} + engines: {node: '>=0.10.0'} redent@3.0.0: - resolution: - { integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg== } - engines: { node: '>=8' } + resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} + engines: {node: '>=8'} redeyed@2.1.1: - resolution: - { integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ== } + resolution: {integrity: sha512-FNpGGo1DycYAdnrKFxCMmKYgo/mILAqtRYbkdQD8Ep/Hk2PQ5+aEAEx+IU713RTDmuBaH0c8P5ZozurNu5ObRQ==} reflect.getprototypeof@1.0.10: - resolution: - { integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==} + engines: {node: '>= 0.4'} regexp.prototype.flags@1.5.4: - resolution: - { integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} + engines: {node: '>= 0.4'} require-directory@2.1.1: - resolution: - { integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} resolve-from@4.0.0: - resolution: - { integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== } - engines: { node: '>=4' } + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} resolve-pkg-maps@1.0.0: - resolution: - { integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== } + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} resolve@2.0.0-next.7: - resolution: - { integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-tqt+NBWwyaMgw3zDsnygx4CByWjQEJHOPMdslYhppaQSJUtL/D4JO9CcBBlhPoI8lz9oJIDXkwXfhF4aWqP8xQ==} + engines: {node: '>= 0.4'} hasBin: true reusify@1.1.0: - resolution: - { integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== } - engines: { iojs: '>=1.0.0', node: '>=0.10.0' } + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} rollup@4.62.5: - resolution: - { integrity: sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw== } - engines: { node: '>=18.0.0', npm: '>=8.0.0' } + resolution: {integrity: sha512-/tqMfgP7GPA3PHhCmuiS4vIjrSVhHLgY++i+dhbG462euyAj7FpM4D9uq1X3BgjlqRdpcOrYhcQtfiQLNc8tqw==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true rrweb-cssom@0.8.0: - resolution: - { integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw== } + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} run-parallel@1.2.0: - resolution: - { integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== } + resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} safe-array-concat@1.1.4: - resolution: - { integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg== } - engines: { node: '>=0.4' } + resolution: {integrity: sha512-wtZlHyOje6OZTGqAoaDKxFkgRtkF9CnHAVnCHKfuj200wAgL+bSJhdsCD2l0Qx/2ekEXjPWcyKkfGb5CPboslg==} + engines: {node: '>=0.4'} safe-push-apply@1.0.0: - resolution: - { integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} + engines: {node: '>= 0.4'} safe-regex-test@1.1.0: - resolution: - { integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==} + engines: {node: '>= 0.4'} safer-buffer@2.1.2: - resolution: - { integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== } + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} saxes@6.0.0: - resolution: - { integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA== } - engines: { node: '>=v12.22.7' } + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} scheduler@0.27.0: - resolution: - { integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q== } + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} semver@6.3.1: - resolution: - { integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== } + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} hasBin: true semver@7.8.5: - resolution: - { integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA== } - engines: { node: '>=10' } + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} hasBin: true set-function-length@1.2.2: - resolution: - { integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==} + engines: {node: '>= 0.4'} set-function-name@2.0.2: - resolution: - { integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==} + engines: {node: '>= 0.4'} set-proto@1.0.0: - resolution: - { integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} + engines: {node: '>= 0.4'} sharp@0.35.3: - resolution: - { integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q== } - engines: { node: '>=20.9.0' } + resolution: {integrity: sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==} + engines: {node: '>=20.9.0'} peerDependencies: '@types/node': '*' peerDependenciesMeta: @@ -3259,143 +2689,114 @@ packages: optional: true shebang-command@2.0.0: - resolution: - { integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== } - engines: { node: '>=8' } + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} shebang-regex@3.0.0: - resolution: - { integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== } - engines: { node: '>=8' } + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} side-channel-list@1.0.1: - resolution: - { integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} side-channel-map@1.0.1: - resolution: - { integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} side-channel-weakmap@1.0.2: - resolution: - { integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} side-channel@1.1.1: - resolution: - { integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==} + engines: {node: '>= 0.4'} siginfo@2.0.0: - resolution: - { integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g== } + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} signal-exit@4.1.0: - resolution: - { integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== } - engines: { node: '>=14' } + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} source-map-js@1.2.1: - resolution: - { integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} stable-hash@0.0.5: - resolution: - { integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA== } + resolution: {integrity: sha512-+L3ccpzibovGXFK+Ap/f8LOS0ahMrHTf3xu7mMLSpEGU0EO9ucaysSylKo9eRDFNhWve/y275iPmIZ4z39a9iA==} stackback@0.0.2: - resolution: - { integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw== } + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} starknet@10.4.0: - resolution: - { integrity: sha512-HoI53jf9DqqjoIz7APWdDoarhSbiD+Y1RpsWhvSOq4QBC838bbdzSWPH8nmoeF3JhmWmW01J/9c7/eEqAcqlWA== } - engines: { node: '>=22' } + resolution: {integrity: sha512-HoI53jf9DqqjoIz7APWdDoarhSbiD+Y1RpsWhvSOq4QBC838bbdzSWPH8nmoeF3JhmWmW01J/9c7/eEqAcqlWA==} + engines: {node: '>=22'} std-env@3.10.0: - resolution: - { integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg== } + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} stop-iteration-iterator@1.1.0: - resolution: - { integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} + engines: {node: '>= 0.4'} string-width@4.2.3: - resolution: - { integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== } - engines: { node: '>=8' } + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} string-width@5.1.2: - resolution: - { integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== } - engines: { node: '>=12' } + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} string.prototype.includes@2.0.1: - resolution: - { integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==} + engines: {node: '>= 0.4'} string.prototype.matchall@4.0.12: - resolution: - { integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} + engines: {node: '>= 0.4'} string.prototype.repeat@1.0.0: - resolution: - { integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w== } + resolution: {integrity: sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==} string.prototype.trim@1.2.11: - resolution: - { integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-PwvK7BU+CMTJGYQCTZb5RWXIML92lftJLhQz1tBzgKiqGxJaMlBAa48POXaNAC2s4y8jr3EFqrkF9+44neS46w==} + engines: {node: '>= 0.4'} string.prototype.trimend@1.0.10: - resolution: - { integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-2+3aDAOmPTmuFwjDnmJG2ctEkQKVki7vOSqaxkv42Mowj1V6PnvuwFCRrR5lChUux1TBskPjfkeTOhqczDMxTw==} + engines: {node: '>= 0.4'} string.prototype.trimstart@1.0.8: - resolution: - { integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} + engines: {node: '>= 0.4'} strip-ansi@6.0.1: - resolution: - { integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== } - engines: { node: '>=8' } + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} strip-ansi@7.2.0: - resolution: - { integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w== } - engines: { node: '>=12' } + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} strip-bom@3.0.0: - resolution: - { integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA== } - engines: { node: '>=4' } + resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} + engines: {node: '>=4'} strip-indent@3.0.0: - resolution: - { integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ== } - engines: { node: '>=8' } + resolution: {integrity: sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==} + engines: {node: '>=8'} strip-json-comments@3.1.1: - resolution: - { integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== } - engines: { node: '>=8' } + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} strip-literal@3.1.0: - resolution: - { integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg== } + resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} styled-jsx@5.1.6: - resolution: - { integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA== } - engines: { node: '>= 12.0.0' } + resolution: {integrity: sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==} + engines: {node: '>= 12.0.0'} peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' @@ -3407,171 +2808,142 @@ packages: optional: true supports-color@7.2.0: - resolution: - { integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== } - engines: { node: '>=8' } + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} supports-preserve-symlinks-flag@1.0.0: - resolution: - { integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} symbol-tree@3.2.4: - resolution: - { integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw== } + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} tailwindcss@4.3.3: - resolution: - { integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ== } + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} tapable@2.3.3: - resolution: - { integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A== } - engines: { node: '>=6' } + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} test-exclude@7.0.2: - resolution: - { integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} tinybench@2.9.0: - resolution: - { integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg== } + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} tinyexec@0.3.2: - resolution: - { integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA== } + resolution: {integrity: sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==} tinyglobby@0.2.17: - resolution: - { integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g== } - engines: { node: '>=12.0.0' } + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} tinypool@1.1.1: - resolution: - { integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg== } - engines: { node: ^18.0.0 || >=20.0.0 } + resolution: {integrity: sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==} + engines: {node: ^18.0.0 || >=20.0.0} tinyrainbow@2.0.0: - resolution: - { integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw== } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==} + engines: {node: '>=14.0.0'} tinyspy@4.0.4: - resolution: - { integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q== } - engines: { node: '>=14.0.0' } + resolution: {integrity: sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==} + engines: {node: '>=14.0.0'} tldts-core@6.1.86: - resolution: - { integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA== } + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} tldts@6.1.86: - resolution: - { integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ== } + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} hasBin: true to-regex-range@5.0.1: - resolution: - { integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== } - engines: { node: '>=8.0' } + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} tough-cookie@5.1.2: - resolution: - { integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A== } - engines: { node: '>=16' } + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} tr46@5.1.1: - resolution: - { integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} ts-api-utils@2.5.0: - resolution: - { integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA== } - engines: { node: '>=18.12' } + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} peerDependencies: typescript: '>=4.8.4' tsconfig-paths@3.15.0: - resolution: - { integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg== } + resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} tslib@2.8.1: - resolution: - { integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== } + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tsx@4.23.12: + resolution: {integrity: sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==} + engines: {node: '>=18.0.0'} + hasBin: true type-check@0.4.0: - resolution: - { integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew== } - engines: { node: '>= 0.8.0' } + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} typed-array-buffer@1.0.3: - resolution: - { integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} typed-array-byte-length@1.0.3: - resolution: - { integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==} + engines: {node: '>= 0.4'} typed-array-byte-offset@1.0.4: - resolution: - { integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==} + engines: {node: '>= 0.4'} typed-array-length@1.0.8: - resolution: - { integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-phPGCwqr2+Qo0fwniCE8e4pKnGu/yFb5nD5Y8bf0EEeiI5GklnACYA9GFy/DrAeRrKHXvHn+1SUsOWgJp6RO+g==} + engines: {node: '>= 0.4'} typescript-eslint@8.67.0: - resolution: - { integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg== } - engines: { node: ^18.18.0 || ^20.9.0 || >=21.1.0 } + resolution: {integrity: sha512-S2udFs8tCKEKffuJ4TB1idGUZiXdCPGi3IPBGWXarbLQ5UPXORV8QEVzJ4gCRduURMb5EkpNCdjbk0eDIuI8Yg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} peerDependencies: eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 typescript: '>=4.8.4 <6.1.0' typescript@5.9.3: - resolution: - { integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw== } - engines: { node: '>=14.17' } + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} hasBin: true unbox-primitive@1.1.0: - resolution: - { integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} + engines: {node: '>= 0.4'} undici-types@7.16.0: - resolution: - { integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw== } + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} universalify@2.0.1: - resolution: - { integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== } - engines: { node: '>= 10.0.0' } + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} unrs-resolver@1.12.2: - resolution: - { integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ== } + resolution: {integrity: sha512-dmlRxBJJayXjqTwC+JtF1HhJmgf3ftQ3YejFcZrf4+KKtJv0qDsK1pjqaaVjG7wJ5NJ6UVP1OqRMQ71Z4C3rxQ==} update-browserslist-db@1.3.1: - resolution: - { integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ== } + resolution: {integrity: sha512-ZZ61DsRsOnakl74HAmp3oSN4aXUmEWXf+i/yv0h7tIBfICc3VdrFErQKUUKPgu3AMsTUMbcongALEN4l6GSUrQ==} hasBin: true peerDependencies: browserslist: '>= 4.21.0' uri-js@4.4.1: - resolution: - { integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg== } + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} viem@2.55.19: - resolution: - { integrity: sha512-4QPIX0eYPLsOBk53NKswVMkQoxuP7GlOBnB4wM6dkDokREO4QENNc3bmyPKK1PBTViXh0TPJCHLjIuU20Qi3fg== } + resolution: {integrity: sha512-4QPIX0eYPLsOBk53NKswVMkQoxuP7GlOBnB4wM6dkDokREO4QENNc3bmyPKK1PBTViXh0TPJCHLjIuU20Qi3fg==} peerDependencies: typescript: '>=5.0.4' peerDependenciesMeta: @@ -3579,15 +2951,13 @@ packages: optional: true vite-node@3.2.4: - resolution: - { integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg== } - engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } + resolution: {integrity: sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true vite@7.3.6: - resolution: - { integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg== } - engines: { node: ^20.19.0 || >=22.12.0 } + resolution: {integrity: sha512-4XP60spRGjSZFf1qYH+dJIkK2znL3zQfl9KkOV9MkkRR/3Dls0dxaBsQPTloEc5BLXWPL9vsOxopxyKoMmDueg==} + engines: {node: ^20.19.0 || >=22.12.0} hasBin: true peerDependencies: '@types/node': ^20.19.0 || >=22.12.0 @@ -3626,9 +2996,8 @@ packages: optional: true vitest@3.2.6: - resolution: - { integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw== } - engines: { node: ^18.0.0 || ^20.0.0 || >=22.0.0 } + resolution: {integrity: sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==} + engines: {node: ^18.0.0 || ^20.0.0 || >=22.0.0} hasBin: true peerDependencies: '@edge-runtime/vm': '*' @@ -3655,82 +3024,67 @@ packages: optional: true w3c-xmlserializer@5.0.0: - resolution: - { integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA== } - engines: { node: '>=18' } + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} webidl-conversions@7.0.0: - resolution: - { integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g== } - engines: { node: '>=12' } + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} whatwg-encoding@3.1.1: - resolution: - { integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ== } - engines: { node: '>=18' } + resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} + engines: {node: '>=18'} deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: - resolution: - { integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} + engines: {node: '>=18'} whatwg-url@14.2.0: - resolution: - { integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw== } - engines: { node: '>=18' } + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} which-boxed-primitive@1.1.1: - resolution: - { integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==} + engines: {node: '>= 0.4'} which-builtin-type@1.2.1: - resolution: - { integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==} + engines: {node: '>= 0.4'} which-collection@1.0.2: - resolution: - { integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==} + engines: {node: '>= 0.4'} which-typed-array@1.1.22: - resolution: - { integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw== } - engines: { node: '>= 0.4' } + resolution: {integrity: sha512-fvO4ExWMFsqyhG3AiPAObMuY1lxaqgYcxbc49CNdWDDECOJNgQyvsOWVwbZc+qf3rzRtxojBK+CMEv0Ld5CYpw==} + engines: {node: '>= 0.4'} which@2.0.2: - resolution: - { integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== } - engines: { node: '>= 8' } + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} hasBin: true why-is-node-running@2.3.0: - resolution: - { integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w== } - engines: { node: '>=8' } + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} hasBin: true word-wrap@1.2.5: - resolution: - { integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA== } - engines: { node: '>=0.10.0' } + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} wrap-ansi@7.0.0: - resolution: - { integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== } - engines: { node: '>=10' } + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} wrap-ansi@8.1.0: - resolution: - { integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== } - engines: { node: '>=12' } + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} ws@8.21.0: - resolution: - { integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g== } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: '>=5.0.2' @@ -3741,9 +3095,8 @@ packages: optional: true ws@8.21.3: - resolution: - { integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw== } - engines: { node: '>=10.0.0' } + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 utf-8-validate: '>=5.0.2' @@ -3754,53 +3107,48 @@ packages: optional: true xml-name-validator@5.0.0: - resolution: - { integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg== } - engines: { node: '>=18' } + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} xmlchars@2.2.0: - resolution: - { integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== } + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} y18n@5.0.8: - resolution: - { integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA== } - engines: { node: '>=10' } + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} yallist@3.1.1: - resolution: - { integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== } + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true yargs-parser@21.1.1: - resolution: - { integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw== } - engines: { node: '>=12' } + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} yargs@17.7.3: - resolution: - { integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g== } - engines: { node: '>=12' } + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} yocto-queue@0.1.0: - resolution: - { integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== } - engines: { node: '>=10' } + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} zod-validation-error@4.0.2: - resolution: - { integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ== } - engines: { node: '>=18.0.0' } + resolution: {integrity: sha512-Q6/nZLe6jxuU80qb/4uJ4t5v2VEZ44lzQjPDhYJNztRQ4wyWc6VF3D3Kb/fAuPetZQnhS3hnajCf9CsWesghLQ==} + engines: {node: '>=18.0.0'} peerDependencies: zod: ^3.25.0 || ^4.0.0 zod@3.25.76: - resolution: - { integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ== } + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} zustand@5.0.9: - resolution: - { integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg== } - engines: { node: '>=12.20.0' } + resolution: {integrity: sha512-ALBtUj0AfjJt3uNRQoL1tL2tMvj6Gp/6e39dnfT6uzpelGru8v1tPOGBzayOWbPJvujM8JojDk3E1LxeFisBNg==} + engines: {node: '>=12.20.0'} peerDependencies: '@types/react': '>=18.0.0' immer: '>=9.0.6' @@ -3817,6 +3165,7 @@ packages: optional: true snapshots: + '@adobe/css-tools@4.5.0': {} '@adraffy/ens-normalize@1.11.1': {} @@ -4800,7 +4149,7 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.12.2': optional: true - '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0))': + '@vitest/coverage-v8@3.2.6(vitest@3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -4815,7 +4164,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.2 tinyrainbow: 2.0.0 - vitest: 3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0) + vitest: 3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -4827,13 +4176,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.6(vite@7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0))': + '@vitest/mocker@3.2.6(vite@7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.6 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0) + vite: 7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0) '@vitest/pretty-format@3.2.6': dependencies: @@ -6750,6 +6099,12 @@ snapshots: tslib@2.8.1: {} + tsx@4.23.12: + dependencies: + esbuild: 0.28.2 + optionalDependencies: + fsevents: 2.3.3 + type-check@0.4.0: dependencies: prelude-ls: 1.2.1 @@ -6865,13 +6220,13 @@ snapshots: - utf-8-validate - zod - vite-node@3.2.4(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0): + vite-node@3.2.4(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0) + vite: 7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -6886,7 +6241,7 @@ snapshots: - tsx - yaml - vite@7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0): + vite@7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0): dependencies: esbuild: 0.28.2 fdir: 6.5.0(picomatch@4.0.5) @@ -6899,12 +6254,14 @@ snapshots: fsevents: 2.3.3 jiti: 2.7.0 lightningcss: 1.32.0 + tsx: 4.23.12 + yaml: 2.9.0 - vitest@3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0): + vitest@3.2.6(@types/node@24.10.2)(jiti@2.7.0)(jsdom@26.1.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.6 - '@vitest/mocker': 3.2.6(vite@7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)) + '@vitest/mocker': 3.2.6(vite@7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.7 '@vitest/runner': 3.2.6 '@vitest/snapshot': 3.2.6 @@ -6922,8 +6279,8 @@ snapshots: tinyglobby: 0.2.17 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0) - vite-node: 3.2.4(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0) + vite: 7.3.6(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@24.10.2)(jiti@2.7.0)(lightningcss@1.32.0)(tsx@4.23.12)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.10.2 @@ -7035,6 +6392,8 @@ snapshots: yallist@3.1.1: {} + yaml@2.9.0: {} + yargs-parser@21.1.1: {} yargs@17.7.3: diff --git a/web/scripts/configure-mainnet-env.ts b/web/scripts/configure-mainnet-env.ts new file mode 100644 index 0000000..6002fbb --- /dev/null +++ b/web/scripts/configure-mainnet-env.ts @@ -0,0 +1,66 @@ +import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { loadDeploymentManifest, type DeploymentEnvironment } from '@/config/deployment' +import { parseMainnetDeploymentRecord, renderMainnetEnvironment } from '@/config/mainnetAuctionPlan' + +const defaultDeploymentRecord = path.resolve(process.cwd(), '..', '.runtime-evidence', 'mainnet', 'deployment.json') + +function parseArguments(argv: readonly string[]): Readonly<{ deploymentRecord: string; write: boolean }> { + let deploymentRecord = defaultDeploymentRecord + let write = false + for (let index = 0; index < argv.length; index += 1) { + const argument = argv[index] + if (argument === '--') continue + if (argument === '--write') { + write = true + continue + } + if (argument === '--deployment-record') { + const value = argv[index + 1] + if (!value || value.startsWith('--')) throw new Error('--deployment-record requires a path') + deploymentRecord = path.resolve(value) + index += 1 + continue + } + throw new Error(`Unsupported option: ${argument}`) + } + return Object.freeze({ deploymentRecord, write }) +} + +function environmentFromText(text: string): DeploymentEnvironment { + const environment: Record = {} + for (const line of text.split('\n')) { + if (!line) continue + const separator = line.indexOf('=') + if (separator <= 0) throw new Error('Rendered mainnet environment is malformed') + const key = line.slice(0, separator) + if (Object.hasOwn(environment, key)) throw new Error(`Rendered mainnet environment repeats ${key}`) + environment[key] = line.slice(separator + 1) + } + return environment +} + +function main(): void { + const options = parseArguments(process.argv.slice(2)) + if (!existsSync(options.deploymentRecord)) { + throw new Error(`Verified mainnet deployment record not found: ${options.deploymentRecord}`) + } + const deployment = parseMainnetDeploymentRecord(readFileSync(options.deploymentRecord, 'utf8')) + const rendered = renderMainnetEnvironment(deployment) + loadDeploymentManifest(environmentFromText(rendered)) + + if (!options.write) { + process.stdout.write(rendered) + return + } + const outputPath = path.resolve(process.cwd(), '.env.local') + writeFileSync(outputPath, rendered, { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + console.log(`Wrote verified public mainnet environment to ${outputPath}`) +} + +try { + main() +} catch (error: unknown) { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 +} diff --git a/web/scripts/create-mainnet-auction.ts b/web/scripts/create-mainnet-auction.ts new file mode 100644 index 0000000..e0d5405 --- /dev/null +++ b/web/scripts/create-mainnet-auction.ts @@ -0,0 +1,286 @@ +import { randomBytes } from 'node:crypto' +import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import { homedir } from 'node:os' +import path from 'node:path' +import { spawnSync } from 'node:child_process' +import { RpcProvider } from 'starknet' +import { parseMainnetDeploymentRecord, buildMainnetAuctionCreationPlan } from '@/config/mainnetAuctionPlan' +import { + AUCTION_HOUSE_CLASS_HASH, + DEMO_ERC721_CLASS_HASH, + MAINNET_ACCOUNT_NAME, + MAINNET_ACCOUNT_FILE, + parseBufferedGasBounds, + requireFrozenMainnetAccount, + type GasBounds, +} from '@/config/mainnetDeploymentPlan' +import { MAINNET_CHAIN_ID, MAINNET_STRK20_POOL, STRK_TOKEN, type DeploymentManifest } from '@/config/deployment' +import { MAINNET_DEPLOYER } from '@/config/mainnetRelease' +import { generateSellerCredential } from '@/features/credentials/credentials' +import { createVerifiedRecoveryBundle } from '@/features/credentials/recoveryBundle' +import { readAuctionSnapshot, type ChainReader } from '@/features/auction/auctionReader' + +const RPC_URL = 'https://api.zan.top/public/starknet-mainnet/rpc/v0_10' + +const TOKEN_ID = 99n +const contractsDirectory = path.resolve(process.cwd(), '..', 'contracts') +const defaultDeploymentRecord = path.resolve(process.cwd(), '..', '.runtime-evidence', 'mainnet', 'deployment.json') + +type Options = Readonly<{ + execute: boolean + auctionId: bigint + deploymentRecord: string +}> + +function optionValue(argv: readonly string[], name: string): string | undefined { + const index = argv.indexOf(name) + if (index < 0) return undefined + const value = argv[index + 1] + if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`) + return value +} + +function parseOptions(argv: readonly string[]): Options { + const supported = new Set(['--execute', '--auction-id', '--deployment-record']) + for (let index = 0; index < argv.length; index += 1) { + const value = argv[index]! + if (!supported.has(value)) throw new Error(`Unsupported option: ${value}`) + if (value !== '--execute') index += 1 + } + return Object.freeze({ + execute: argv.includes('--execute'), + auctionId: BigInt(optionValue(argv, '--auction-id') ?? Date.now().toString()), + deploymentRecord: path.resolve(optionValue(argv, '--deployment-record') ?? defaultDeploymentRecord), + }) +} + +function windowsToWsl(value: string): string { + const normalized = path.resolve(value) + const match = /^([A-Za-z]):[\\/](.*)$/.exec(normalized) + if (!match) throw new Error(`Cannot convert path to WSL: ${normalized}`) + return `/mnt/${match[1].toLowerCase()}/${match[2].replaceAll('\\', '/')}` +} + +function shellQuote(value: string): string { + return `'${value.replaceAll("'", `'\"'\"'`)}'` +} + +function runWsl(tokens: readonly string[]): string { + const command = `cd ${shellQuote(windowsToWsl(contractsDirectory))} && ${tokens.map(shellQuote).join(' ')}` + const result = spawnSync('wsl.exe', ['-d', 'Ubuntu-24.04', '--', 'bash', '-lc', command], { + encoding: 'utf8', + windowsHide: true, + }) + const output = `${result.stdout ?? ''}${result.stderr ?? ''}` + if (result.status !== 0) throw new Error(`Command failed (${result.status ?? 'unknown'}):\n${output}`) + return output +} + +function sncast(args: readonly string[]): string { + return runWsl(['sncast', '--account', MAINNET_ACCOUNT_NAME, '--accounts-file', MAINNET_ACCOUNT_FILE, ...args]) +} + +function accountList(): string { + return runWsl(['sncast', '--accounts-file', MAINNET_ACCOUNT_FILE, 'account', 'list']) +} + +function parseLine(output: string, label: string): string { + const match = new RegExp(`${label}:\\s*(0x[0-9a-fA-F]+|\\d+)`, 'i').exec(output) + if (!match) throw new Error(`Could not parse ${label} from sncast output`) + return match[1] +} + +function nonce(): bigint { + return BigInt( + parseLine(sncast(['get', 'nonce', '--network', 'mainnet', MAINNET_DEPLOYER, '--block-id', 'latest']), 'Nonce'), + ) +} + +function boundArgs(bounds: GasBounds): string[] { + return [ + '--l1-gas', + bounds.l1Gas.toString(), + '--l1-gas-price', + bounds.l1GasPrice.toString(), + '--l2-gas', + bounds.l2Gas.toString(), + '--l2-gas-price', + bounds.l2GasPrice.toString(), + '--l1-data-gas', + bounds.l1DataGas.toString(), + '--l1-data-gas-price', + bounds.l1DataGasPrice.toString(), + ] +} + +function sameFelt(left: string, right: string): boolean { + try { + return BigInt(left) === BigInt(right) + } catch { + return false + } +} + +function callValues(value: readonly string[] | Readonly<{ result: readonly string[] }>): readonly string[] { + return 'result' in value ? value.result : value +} + +function runMainnetPreflight(): void { + const executable = process.platform === 'win32' ? 'npx.cmd' : 'npx' + const result = spawnSync(executable, ['--yes', 'pnpm@10.18.1', 'exec', 'tsx', 'scripts/preflight-mainnet.ts'], { + cwd: process.cwd(), + encoding: 'utf8', + windowsHide: true, + }) + process.stdout.write(result.stdout ?? '') + process.stderr.write(result.stderr ?? '') + if (result.status !== 0) throw new Error('Mainnet bidder readiness failed before auction creation') +} + +function recoveryDirectory(): string { + const root = process.env.LOCALAPPDATA ?? path.join(homedir(), 'AppData', 'Local') + return path.join(root, 'CipherBid', 'mainnet', 'recovery') +} + +async function main(): Promise { + const options = parseOptions(process.argv.slice(2)) + if (!existsSync(options.deploymentRecord)) { + throw new Error(`Verified mainnet deployment record not found: ${options.deploymentRecord}`) + } + const deploymentRecord = parseMainnetDeploymentRecord(readFileSync(options.deploymentRecord, 'utf8')) + const placeholderPlan = buildMainnetAuctionCreationPlan({ + auctionHouse: deploymentRecord.auctionHouse, + nftContract: deploymentRecord.demoNft, + auctionId: options.auctionId, + sellerClaimHandle: 1n, + nowSeconds: Math.floor(Date.now() / 1000), + }) + if (!options.execute) { + console.log( + JSON.stringify( + { + mode: 'plan-only', + auctionHouse: deploymentRecord.auctionHouse, + nftContract: deploymentRecord.demoNft, + remainingBudgetCeiling: deploymentRecord.remainingBudgetCeiling.toString(), + terms: placeholderPlan.form, + note: 'No credential, recovery file, NFT, or auction transaction was created.', + }, + null, + 2, + ), + ) + return + } + + runMainnetPreflight() + requireFrozenMainnetAccount(accountList()) + let remainingBudget = deploymentRecord.remainingBudgetCeiling + const provider = new RpcProvider({ nodeUrl: RPC_URL }) + + const [nftClassHash, nftOwnerResponse] = await Promise.all([ + provider.getClassHashAt(deploymentRecord.demoNft), + provider.callContract({ + contractAddress: deploymentRecord.demoNft, + entrypoint: 'owner_of', + calldata: [TOKEN_ID.toString(), '0'], + }), + ]) + if (!sameFelt(nftClassHash, DEMO_ERC721_CLASS_HASH)) throw new Error('Mainnet DemoERC721 class hash mismatch') + const [nftOwner] = callValues(nftOwnerResponse) + if (!nftOwner || !sameFelt(nftOwner, MAINNET_DEPLOYER)) { + throw new Error('Mainnet DemoERC721 token owner mismatch') + } + + const password = process.env.CIPHERBID_RECOVERY_PASSWORD ?? randomBytes(24).toString('base64url') + const credential = generateSellerCredential({ + network: 'mainnet', + chainId: BigInt(MAINNET_CHAIN_ID), + auctionHouse: BigInt(deploymentRecord.auctionHouse), + auctionId: options.auctionId, + }) + const bundle = await createVerifiedRecoveryBundle([credential], password) + const localRecoveryDirectory = recoveryDirectory() + mkdirSync(localRecoveryDirectory, { recursive: true }) + const bundlePath = path.join(localRecoveryDirectory, `auction-${options.auctionId}.recovery.json`) + writeFileSync(bundlePath, bundle.serialized, { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + let passwordPath: string | undefined + if (!process.env.CIPHERBID_RECOVERY_PASSWORD) { + passwordPath = path.join(localRecoveryDirectory, `auction-${options.auctionId}.password.txt`) + writeFileSync(passwordPath, password, { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + } + + const creationPlan = buildMainnetAuctionCreationPlan({ + auctionHouse: deploymentRecord.auctionHouse, + nftContract: deploymentRecord.demoNft, + auctionId: options.auctionId, + sellerClaimHandle: credential.claimHandle, + nowSeconds: Math.floor(Date.now() / 1000), + }) + const createBase = ['multicall', 'execute', '--network', 'mainnet', '--nonce', nonce().toString()] + const createDryRun = sncast([...createBase, '--dry-run', '--detailed', ...creationPlan.multicallTokens]) + const createBounds = parseBufferedGasBounds(createDryRun, remainingBudget) + const createOutput = sncast(['--wait', ...createBase, ...boundArgs(createBounds), ...creationPlan.multicallTokens]) + const auctionCreationTransactionHash = parseLine(createOutput, 'Transaction Hash') + remainingBudget -= createBounds.ceiling + + const deployment: DeploymentManifest = { + network: 'mainnet', + chainId: MAINNET_CHAIN_ID, + rpcUrl: RPC_URL, + auctionHouse: deploymentRecord.auctionHouse, + auctionHouseClassHash: AUCTION_HOUSE_CLASS_HASH, + strk20Pool: MAINNET_STRK20_POOL, + paymentToken: STRK_TOKEN, + } + const reader: ChainReader = { + callContract: (call) => provider.callContract({ ...call, calldata: call.calldata ? [...call.calldata] : [] }), + getClassHashAt: (address) => provider.getClassHashAt(address), + } + const snapshot = await readAuctionSnapshot(reader, deployment, options.auctionId) + if ( + !sameFelt(snapshot.config.seller, MAINNET_DEPLOYER) || + snapshot.config.sellerClaimHandle !== credential.claimHandle || + !sameFelt(snapshot.config.nftContract, deploymentRecord.demoNft) || + snapshot.config.tokenId !== TOKEN_ID || + snapshot.config.reservePrice.toString() !== creationPlan.form.reservePrice || + snapshot.config.cap.toString() !== creationPlan.form.cap || + snapshot.config.biddingDeadline.toString() !== creationPlan.form.biddingDeadline || + snapshot.config.revealDeadline.toString() !== creationPlan.form.revealDeadline || + snapshot.config.bidderLimit.toString() !== creationPlan.form.bidderLimit || + !snapshot.custodyValid + ) { + throw new Error('Mainnet auction readback does not match the frozen creation plan') + } + + const publicRecord = { + schema: 'cipherbid.mainnet-auction.v1', + auctionId: creationPlan.form.auctionId, + auctionHouse: deploymentRecord.auctionHouse, + nftContract: deploymentRecord.demoNft, + tokenId: creationPlan.form.tokenId, + seller: snapshot.config.seller, + sellerClaimHandle: creationPlan.form.sellerClaimHandle, + reservePrice: creationPlan.form.reservePrice, + cap: creationPlan.form.cap, + biddingDeadline: creationPlan.form.biddingDeadline, + revealDeadline: creationPlan.form.revealDeadline, + bidderLimit: creationPlan.form.bidderLimit, + custodyValid: snapshot.custodyValid, + recoveryBundleId: bundle.bundleId, + nftDeploymentTransactionHash: deploymentRecord.demoNftDeploymentTransactionHash, + auctionCreationTransactionHash, + remainingBudgetCeiling: remainingBudget.toString(), + auctionUrl: `http://localhost:4110/auction?id=${creationPlan.form.auctionId}`, + explorerUrl: `https://voyager.online/contract/${deploymentRecord.auctionHouse}`, + } + const evidenceDirectory = path.resolve(process.cwd(), '..', '.runtime-evidence', 'mainnet') + const recordPath = path.join(evidenceDirectory, `auction-${creationPlan.form.auctionId}.json`) + writeFileSync(recordPath, JSON.stringify(publicRecord, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + console.log(JSON.stringify({ ...publicRecord, bundlePath, passwordPath, recordPath }, null, 2)) +} + +void main().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 +}) diff --git a/web/scripts/create-sepolia-auction.ts b/web/scripts/create-sepolia-auction.ts new file mode 100644 index 0000000..c38a82e --- /dev/null +++ b/web/scripts/create-sepolia-auction.ts @@ -0,0 +1,427 @@ +import { randomBytes } from 'node:crypto' +import { mkdirSync, writeFileSync } from 'node:fs' +import { homedir } from 'node:os' +import path from 'node:path' +import { spawnSync } from 'node:child_process' +import { hash, RpcProvider } from 'starknet' +import { buildAuctionCreationPlan } from '@/features/auction/auctionCreationPlan' +import { + evaluateDemoBidderReadiness, + type DemoBidderStatus, + type PublicDeposit, +} from '@/features/auction/demoBidderReadiness' +import { readAuctionSnapshot, type ChainReader } from '@/features/auction/auctionReader' +import { generateSellerCredential } from '@/features/credentials/credentials' +import { createVerifiedRecoveryBundle } from '@/features/credentials/recoveryBundle' +import { SEPOLIA_DEMO_BIDDER_CONFIG } from '@/features/demo/demoBidderShield' +import type { DeploymentManifest } from '@/config/deployment' + +const AUCTION_HOUSE = '0x0705b1080174f2b10c02fd8b2e00b918e4dc91f9021ee6a208f53d5909fcc87d' as const +const AUCTION_HOUSE_CLASS = '0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4' as const +const DEMO_NFT_CLASS = '0x06c7cba5680595203f9327f5784130907bad1b808891122ad358c10b93136a41' as const +const DEPLOYER = '0x01ff477da49d13f1b48774d0fc2313358e3f358be741b4944b54fccb34f7f424' as const +const POOL = '0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91' as const +const STRK = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d' as const +const CHAIN_ID = '0x534e5f5345504f4c4941' +const RPC_URL = 'https://api.zan.top/public/starknet-sepolia/rpc/v0_10' +const ACCOUNT = 'cipherbid-sepolia-deployer' +const ACCOUNT_FILE = '/home/sourcesensei/.starknet_accounts/starknet_open_zeppelin_accounts.json' +const BIDDER_ACCOUNTS = Object.freeze([ + { + name: 'xverse-bidder-a', + address: SEPOLIA_DEMO_BIDDER_CONFIG.bidderA, + suggestedBid: '3 STRK', + }, + { + name: 'xverse-bidder-b', + address: SEPOLIA_DEMO_BIDDER_CONFIG.bidderB, + suggestedBid: '4 STRK', + }, +]) +const BIDDER_DEPLOYMENT_BLOCK_FLOOR = 14_179_255 +const DEPOSIT_SELECTOR = hash.getSelectorFromName('Deposit') +const TOKEN_ID = 99n +const TIP = 1_000_000_000n +const MAX_TRANSACTION_FEE = 2n * 10n ** 18n + +const deployment: DeploymentManifest = { + network: 'sepolia', + chainId: CHAIN_ID, + rpcUrl: RPC_URL, + auctionHouse: AUCTION_HOUSE, + auctionHouseClassHash: AUCTION_HOUSE_CLASS, + strk20Pool: POOL, + paymentToken: STRK, +} + +type Options = Readonly<{ + auctionId: bigint + reserve: string + cap: string + biddingMinutes: number + revealMinutes: number + bidderLimit: number + nftAddress?: `0x${string}` + planOnly: boolean + preflightOnly: boolean +}> + +type GasBounds = Readonly<{ + l1Gas: bigint + l1GasPrice: bigint + l2Gas: bigint + l2GasPrice: bigint + l1DataGas: bigint + l1DataGasPrice: bigint +}> + +function optionValue(argv: readonly string[], name: string): string | undefined { + const index = argv.indexOf(name) + if (index < 0) return undefined + const value = argv[index + 1] + if (!value || value.startsWith('--')) throw new Error(`${name} requires a value`) + return value +} + +function integerOption(argv: readonly string[], name: string, fallback: number): number { + const raw = optionValue(argv, name) + if (raw === undefined) return fallback + const value = Number(raw) + if (!Number.isSafeInteger(value)) throw new Error(`${name} must be an integer`) + return value +} + +function parseOptions(argv: readonly string[]): Options { + const now = Date.now() + const auctionId = BigInt(optionValue(argv, '--auction-id') ?? now.toString()) + const nftAddress = optionValue(argv, '--nft-address') + if (nftAddress && !/^0x[0-9a-fA-F]+$/.test(nftAddress)) throw new Error('--nft-address must be hexadecimal') + return { + auctionId, + reserve: optionValue(argv, '--reserve') ?? '2', + cap: optionValue(argv, '--cap') ?? '5', + biddingMinutes: integerOption(argv, '--bidding-minutes', 10), + revealMinutes: integerOption(argv, '--reveal-minutes', 5), + bidderLimit: integerOption(argv, '--bidder-limit', 2), + nftAddress: nftAddress as `0x${string}` | undefined, + planOnly: argv.includes('--plan-only'), + preflightOnly: argv.includes('--preflight-only'), + } +} + +function windowsToWsl(value: string): string { + const normalized = path.resolve(value) + const match = /^([A-Za-z]):[\\/](.*)$/.exec(normalized) + if (!match) throw new Error(`Cannot convert path to WSL: ${normalized}`) + return `/mnt/${match[1].toLowerCase()}/${match[2].replaceAll('\\', '/')}` +} + +function shellQuote(value: string): string { + return `'${value.replaceAll("'", `'\"'\"'`)}'` +} + +const contractsDirectory = windowsToWsl(path.resolve(process.cwd(), '..', 'contracts')) + +function sncast(args: readonly string[]): string { + const common = ['sncast', '--account', ACCOUNT, '--accounts-file', ACCOUNT_FILE] + const command = `cd ${shellQuote(contractsDirectory)} && ${[...common, ...args].map(shellQuote).join(' ')}` + const result = spawnSync('wsl.exe', ['-d', 'Ubuntu-24.04', '--', 'bash', '-lc', command], { + encoding: 'utf8', + windowsHide: true, + }) + const output = `${result.stdout ?? ''}${result.stderr ?? ''}` + if (result.status !== 0) throw new Error(`Sncast failed (${result.status ?? 'unknown'}):\n${output}`) + return output +} + +function parsedValue(output: string, label: string): bigint { + const match = new RegExp(`${label}:\\s*(\\d+)`).exec(output) + if (!match) throw new Error(`Could not parse ${label} from Sncast dry run`) + return BigInt(match[1]) +} + +function bufferedBounds(output: string): GasBounds { + const buffer = (value: bigint, numerator: bigint, denominator: bigint) => + value === 0n ? 0n : (value * numerator + denominator - 1n) / denominator + const bounds = { + l1Gas: buffer(parsedValue(output, 'L1 Gas Consumed'), 13n, 10n), + l1GasPrice: buffer(parsedValue(output, 'L1 Gas Price'), 3n, 2n), + l2Gas: buffer(parsedValue(output, 'L2 Gas Consumed'), 13n, 10n), + l2GasPrice: buffer(parsedValue(output, 'L2 Gas Price'), 3n, 2n), + l1DataGas: buffer(parsedValue(output, 'L1 Data Gas Consumed'), 13n, 10n), + l1DataGasPrice: buffer(parsedValue(output, 'L1 Data Gas Price'), 3n, 2n), + } + const ceiling = + bounds.l1Gas * bounds.l1GasPrice + bounds.l2Gas * bounds.l2GasPrice + bounds.l1DataGas * bounds.l1DataGasPrice + if (ceiling > MAX_TRANSACTION_FEE) { + throw new Error(`Buffered transaction ceiling ${ceiling} Fri exceeds the 2 STRK demo limit`) + } + return bounds +} + +function boundArgs(bounds: GasBounds): string[] { + return [ + '--l1-gas', + bounds.l1Gas.toString(), + '--l1-gas-price', + bounds.l1GasPrice.toString(), + '--l2-gas', + bounds.l2Gas.toString(), + '--l2-gas-price', + bounds.l2GasPrice.toString(), + '--l1-data-gas', + bounds.l1DataGas.toString(), + '--l1-data-gas-price', + bounds.l1DataGasPrice.toString(), + '--tip', + TIP.toString(), + ] +} + +function nonce(): bigint { + const output = sncast(['get', 'nonce', '--network', 'sepolia', DEPLOYER, '--block-id', 'latest']) + const match = /Nonce:\s*(\d+)/.exec(output) + if (!match) throw new Error('Could not read deployer nonce') + return BigInt(match[1]) +} + +function transactionHash(output: string): `0x${string}` { + const match = /Transaction Hash:\s*(0x[0-9a-fA-F]+)/i.exec(output) + if (!match) throw new Error('Sncast returned no transaction hash') + return match[1].toLowerCase() as `0x${string}` +} + +function callValues(value: readonly string[] | Readonly<{ result: readonly string[] }>): readonly string[] { + return 'result' in value ? value.result : value +} + +async function publicDeposits(provider: RpcProvider, bidderAddress: `0x${string}`): Promise { + const deposits: PublicDeposit[] = [] + let continuationToken: string | undefined + do { + const page = await provider.getEvents({ + from_block: { block_number: BIDDER_DEPLOYMENT_BLOCK_FLOOR }, + to_block: 'latest', + address: POOL, + keys: [[DEPOSIT_SELECTOR], [bidderAddress], [STRK]], + chunk_size: 100, + ...(continuationToken ? { continuation_token: continuationToken } : {}), + }) + for (const event of page.events) { + if (event.block_number === undefined || event.data[0] === undefined) { + throw new Error('STRK20 deposit event is missing accepted block or amount data') + } + deposits.push( + Object.freeze({ + amount: BigInt(event.data[0]), + blockNumber: event.block_number, + transactionHash: event.transaction_hash as `0x${string}`, + }), + ) + } + continuationToken = page.continuation_token + } while (continuationToken) + return Object.freeze(deposits) +} + +function printableStatus(status: DemoBidderStatus) { + return { + ...status, + depositAmount: status.depositAmount?.toString(), + privateBalanceVerified: false, + } +} + +async function requirePublicBidderReadiness(provider: RpcProvider): Promise { + const latestBlock = await provider.getBlockNumber() + const bidders = await Promise.all( + BIDDER_ACCOUNTS.map(async (bidder) => { + const response = await provider.callContract({ + contractAddress: POOL, + entrypoint: 'get_public_key', + calldata: [bidder.address], + }) + const [publicKey] = callValues(response) + if (!publicKey) throw new Error(`STRK20 returned no public key for ${bidder.name}`) + return { + ...bidder, + publicKey: publicKey as `0x${string}`, + deposits: await publicDeposits(provider, bidder.address), + } + }), + ) + const readiness = evaluateDemoBidderReadiness({ bidders, latestBlock }) + console.log( + JSON.stringify( + { + schema: 'cipherbid.sepolia-bidder-public-readiness.v1', + latestBlock, + ready: readiness.ready, + statuses: readiness.statuses.map(printableStatus), + note: 'Public registration and deposit maturity only; Ready remains authoritative for unspent private balance.', + }, + null, + 2, + ), + ) + if (!readiness.ready) { + const blockers = readiness.statuses.flatMap((status) => + status.blockers.map((blocker) => `${status.name}: ${blocker}`), + ) + throw new Error(`Demo bidder preflight failed before any auction write:\n${blockers.join('\n')}`) + } +} + +function contractAddress(output: string): `0x${string}` { + const match = /Contract Address:\s*(0x[0-9a-fA-F]+)/i.exec(output) + if (!match) throw new Error('Sncast returned no contract address') + return `0x${BigInt(match[1]).toString(16)}` +} + +function deployDemoNft(auctionId: bigint): Readonly<{ address: `0x${string}`; transactionHash: `0x${string}` }> { + const currentNonce = nonce() + const base = [ + 'deploy', + '--network', + 'sepolia', + '--class-hash', + DEMO_NFT_CLASS, + '--constructor-calldata', + DEPLOYER, + TOKEN_ID.toString(), + '0', + '--salt', + auctionId.toString(), + '--nonce', + currentNonce.toString(), + ] + const dryRun = sncast([...base, '--dry-run', '--detailed']) + const output = sncast(['--wait', ...base, ...boundArgs(bufferedBounds(dryRun))]) + process.stdout.write(output) + return { address: contractAddress(output), transactionHash: transactionHash(output) } +} + +function createAuction(multicallTokens: readonly string[]): `0x${string}` { + const currentNonce = nonce() + const base = ['multicall', 'execute', '--network', 'sepolia', '--nonce', currentNonce.toString()] + const dryRun = sncast([...base, '--dry-run', '--detailed', ...multicallTokens]) + const output = sncast(['--wait', ...base, ...boundArgs(bufferedBounds(dryRun)), ...multicallTokens]) + process.stdout.write(output) + return transactionHash(output) +} + +function localRecoveryDirectory(): string { + const root = process.env.LOCALAPPDATA ?? path.join(homedir(), 'AppData', 'Local') + return path.join(root, 'CipherBid', 'sepolia', 'recovery') +} + +async function main() { + const options = parseOptions(process.argv.slice(2)) + if (options.planOnly && options.preflightOnly) throw new Error('--plan-only and --preflight-only cannot be combined') + const provider = new RpcProvider({ nodeUrl: RPC_URL }) + if (!options.planOnly) { + await requirePublicBidderReadiness(provider) + if (options.preflightOnly) return + } + const nowSeconds = Math.floor(Date.now() / 1000) + const password = process.env.CIPHERBID_RECOVERY_PASSWORD ?? randomBytes(24).toString('base64url') + const credential = generateSellerCredential({ + network: 'sepolia', + chainId: BigInt(CHAIN_ID), + auctionHouse: BigInt(AUCTION_HOUSE), + auctionId: options.auctionId, + }) + const bundle = await createVerifiedRecoveryBundle([credential], password) + const recoveryDirectory = localRecoveryDirectory() + mkdirSync(recoveryDirectory, { recursive: true }) + const bundlePath = path.join(recoveryDirectory, `auction-${options.auctionId}.recovery.json`) + writeFileSync(bundlePath, bundle.serialized, { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + let passwordPath: string | undefined + if (!process.env.CIPHERBID_RECOVERY_PASSWORD) { + passwordPath = path.join(recoveryDirectory, `auction-${options.auctionId}.password.txt`) + writeFileSync(passwordPath, password, { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + } + + const nft = options.nftAddress + ? { address: options.nftAddress, transactionHash: undefined } + : options.planOnly + ? { address: '0x1' as const, transactionHash: undefined } + : deployDemoNft(options.auctionId) + const plan = buildAuctionCreationPlan({ + auctionHouse: AUCTION_HOUSE, + nftContract: nft.address, + tokenId: TOKEN_ID, + auctionId: options.auctionId, + claimHandle: credential.claimHandle, + reserve: options.reserve, + cap: options.cap, + nowSeconds, + biddingMinutes: options.biddingMinutes, + revealMinutes: options.revealMinutes, + bidderLimit: options.bidderLimit, + }) + + if (options.planOnly) { + console.log( + JSON.stringify({ plan: plan.form, recoveryBundleId: bundle.bundleId, bundlePath, passwordPath }, null, 2), + ) + return + } + + const createTransactionHash = createAuction(plan.multicallTokens) + const reader: ChainReader = { + callContract: (call) => provider.callContract({ ...call, calldata: call.calldata ? [...call.calldata] : [] }), + getClassHashAt: (value) => provider.getClassHashAt(value), + } + const snapshot = await readAuctionSnapshot(reader, deployment, options.auctionId) + if ( + snapshot.config.seller !== `0x${BigInt(DEPLOYER).toString(16)}` || + snapshot.config.sellerClaimHandle !== credential.claimHandle || + snapshot.config.nftContract !== `0x${BigInt(nft.address).toString(16)}` || + snapshot.config.tokenId !== TOKEN_ID || + snapshot.config.reservePrice.toString() !== plan.form.reservePrice || + snapshot.config.cap.toString() !== plan.form.cap || + snapshot.config.biddingDeadline.toString() !== plan.form.biddingDeadline || + snapshot.config.revealDeadline.toString() !== plan.form.revealDeadline || + snapshot.config.bidderLimit.toString() !== plan.form.bidderLimit || + !snapshot.custodyValid + ) { + throw new Error('Onchain auction readback does not match the generated plan') + } + + const evidenceDirectory = path.resolve(process.cwd(), '..', '.runtime-evidence', 'sepolia') + mkdirSync(evidenceDirectory, { recursive: true }) + const publicRecord = { + schema: 'cipherbid.sepolia-auction.v1', + auctionId: plan.form.auctionId, + auctionHouse: AUCTION_HOUSE, + nftContract: nft.address, + tokenId: plan.form.tokenId, + seller: snapshot.config.seller, + sellerClaimHandle: plan.form.sellerClaimHandle, + reservePrice: plan.form.reservePrice, + cap: plan.form.cap, + biddingDeadline: plan.form.biddingDeadline, + revealDeadline: plan.form.revealDeadline, + bidderLimit: plan.form.bidderLimit, + custodyValid: snapshot.custodyValid, + recoveryBundleId: bundle.bundleId, + bidderAccounts: BIDDER_ACCOUNTS, + nftDeploymentTransactionHash: nft.transactionHash, + auctionCreationTransactionHash: createTransactionHash, + auctionUrl: `http://localhost:4110/auction?id=${plan.form.auctionId}`, + explorerUrl: `https://sepolia.voyager.online/contract/${AUCTION_HOUSE}`, + } + const recordPath = path.join(evidenceDirectory, `auction-${plan.form.auctionId}.json`) + writeFileSync(recordPath, JSON.stringify(publicRecord, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + console.log(JSON.stringify({ ...publicRecord, bundlePath, passwordPath, recordPath }, null, 2)) + console.log('Import the seller account into Ready by revealing the Sncast key locally; the script never prints it.') + console.log( + 'Bidder A and B are already deployed and funded; import their keys locally into Ready and submit immediately.', + ) +} + +main().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : 'Auction creation failed') + process.exitCode = 1 +}) diff --git a/web/scripts/deploy-mainnet.ts b/web/scripts/deploy-mainnet.ts new file mode 100644 index 0000000..c4313b9 --- /dev/null +++ b/web/scripts/deploy-mainnet.ts @@ -0,0 +1,344 @@ +import { mkdirSync, readFileSync, writeFileSync } from 'node:fs' +import path from 'node:path' +import { spawnSync } from 'node:child_process' +import { hash, RpcProvider } from 'starknet' +import { + MAINNET_ACCOUNT_NAME, + MAINNET_ACCOUNT_FILE, + MAINNET_MAXIMUM_BUDGET, + MAINNET_MINIMUM_FUNDING, + buildMainnetDeploymentPlan, + parseBufferedGasBounds, + parseFrozenMainnetAccount, + requireFrozenMainnetAccount, + type GasBounds, +} from '@/config/mainnetDeploymentPlan' +import { MAINNET_CHAIN_ID, MAINNET_STRK20_POOL, STRK_TOKEN } from '@/config/deployment' +import { MAINNET_DEPLOYER } from '@/config/mainnetRelease' + +const RPC_URL = 'https://api.zan.top/public/starknet-mainnet/rpc/v0_10' + +const READY_ACCOUNT_CLASS_HASH = '0x036078334509b514626504edc9fb252328d1a240e4e948bef8d0c08dff45927f' +const contractsDirectory = path.resolve(process.cwd(), '..', 'contracts') +const artifactDirectory = path.join(contractsDirectory, 'target', 'dev') + +function windowsToWsl(value: string): string { + const normalized = path.resolve(value) + const match = /^([A-Za-z]):[\\/](.*)$/.exec(normalized) + if (!match) throw new Error(`Cannot convert path to WSL: ${normalized}`) + return `/mnt/${match[1].toLowerCase()}/${match[2].replaceAll('\\', '/')}` +} + +function shellQuote(value: string): string { + return `'${value.replaceAll("'", `'\"'\"'`)}'` +} + +function runWsl(tokens: readonly string[], workdir = contractsDirectory): string { + const command = `cd ${shellQuote(windowsToWsl(workdir))} && ${tokens.map(shellQuote).join(' ')}` + const result = spawnSync('wsl.exe', ['-d', 'Ubuntu-24.04', '--', 'bash', '-lc', command], { + encoding: 'utf8', + windowsHide: true, + }) + const output = `${result.stdout ?? ''}${result.stderr ?? ''}` + if (result.status !== 0) throw new Error(`Command failed (${result.status ?? 'unknown'}):\n${output}`) + return output +} + +function sncast(args: readonly string[]): string { + return runWsl(['sncast', '--account', MAINNET_ACCOUNT_NAME, '--accounts-file', MAINNET_ACCOUNT_FILE, ...args]) +} + +function accountList(): string { + return runWsl(['sncast', '--accounts-file', MAINNET_ACCOUNT_FILE, 'account', 'list']) +} + +function parseLine(output: string, label: string): string { + const match = new RegExp(`${label}:\\s*(0x[0-9a-fA-F]+|\\d+)`, 'i').exec(output) + if (!match) throw new Error(`Could not parse ${label} from sncast output`) + return match[1] +} + +function nonce(): bigint { + return BigInt( + parseLine(sncast(['get', 'nonce', '--network', 'mainnet', MAINNET_DEPLOYER, '--block-id', 'latest']), 'Nonce'), + ) +} + +function boundArgs(bounds: GasBounds): string[] { + return [ + '--l1-gas', + bounds.l1Gas.toString(), + '--l1-gas-price', + bounds.l1GasPrice.toString(), + '--l2-gas', + bounds.l2Gas.toString(), + '--l2-gas-price', + bounds.l2GasPrice.toString(), + '--l1-data-gas', + bounds.l1DataGas.toString(), + '--l1-data-gas-price', + bounds.l1DataGasPrice.toString(), + ] +} + +function sameFelt(left: string, right: string): boolean { + try { + return BigInt(left) === BigInt(right) + } catch { + return false + } +} + +function callValues(value: readonly string[] | Readonly<{ result: readonly string[] }>): readonly string[] { + return 'result' in value ? value.result : value +} + +async function classDeclared(classHash: string): Promise { + const response = await fetch(RPC_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'starknet_getClass', + params: { block_id: 'latest', class_hash: classHash }, + }), + }) + if (!response.ok) throw new Error(`Mainnet RPC class lookup returned HTTP ${response.status}`) + const payload = (await response.json()) as Readonly<{ + result?: unknown + error?: Readonly<{ code?: number; message?: string }> + }> + if (payload.result !== undefined) return true + if (payload.error?.code === 28) return false + throw new Error(`Mainnet RPC class lookup failed: ${payload.error?.message ?? 'unknown error'}`) +} + +async function accountClassHash(): Promise { + const response = await fetch(RPC_URL, { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + jsonrpc: '2.0', + id: 1, + method: 'starknet_getClassHashAt', + params: { block_id: 'latest', contract_address: MAINNET_DEPLOYER }, + }), + }) + if (!response.ok) throw new Error(`Mainnet RPC account lookup returned HTTP ${response.status}`) + const payload = (await response.json()) as Readonly<{ + result?: string + error?: Readonly<{ code?: number; message?: string }> + }> + if (payload.result !== undefined) return payload.result + if (payload.error?.code === 20) return null + throw new Error(`Mainnet RPC account lookup failed: ${payload.error?.message ?? 'unknown error'}`) +} + +function verifyArtifact(contractName: 'AuctionHouse' | 'DemoERC721', expectedClassHash: string): void { + const artifactPath = path.join(artifactDirectory, `cipherbid_${contractName}.contract_class.json`) + const artifact = JSON.parse(readFileSync(artifactPath, 'utf8')) as Parameters< + typeof hash.computeSierraContractClassHash + >[0] + const actualClassHash = hash.computeSierraContractClassHash(artifact) + if (!sameFelt(actualClassHash, expectedClassHash)) { + throw new Error(`${contractName} artifact class hash does not match the frozen release candidate`) + } +} + +async function readPublicPrerequisites(provider: RpcProvider): Promise< + Readonly<{ + deployed: boolean + publicBalance: bigint + }> +> { + const chainId = await provider.getChainId() + if (!sameFelt(chainId, MAINNET_CHAIN_ID)) throw new Error('RPC is not Starknet mainnet') + const deployedClassHash = await accountClassHash() + if (deployedClassHash !== null && !sameFelt(deployedClassHash, READY_ACCOUNT_CLASS_HASH)) { + throw new Error('Mainnet deployer does not use the frozen Ready account class') + } + const balanceResponse = await provider.callContract({ + contractAddress: STRK_TOKEN, + entrypoint: 'balance_of', + calldata: [MAINNET_DEPLOYER], + }) + const balanceValues = callValues(balanceResponse) + const low = BigInt(balanceValues[0] ?? '0') + const high = BigInt(balanceValues[1] ?? '0') + return Object.freeze({ deployed: deployedClassHash !== null, publicBalance: low + (high << 128n) }) +} + +async function main(): Promise { + const argv = process.argv.slice(2) + const execute = argv.includes('--execute') + if (argv.some((value) => value !== '--execute')) throw new Error('Only --execute is supported') + + const plan = buildMainnetDeploymentPlan() + runWsl(['scarb', 'build']) + for (const declaration of plan.declarations) verifyArtifact(declaration.contractName, declaration.classHash) + + const provider = new RpcProvider({ nodeUrl: RPC_URL }) + const localAccount = parseFrozenMainnetAccount(accountList()) + const publicAccount = await readPublicPrerequisites(provider) + if (localAccount.deployed !== publicAccount.deployed) { + throw new Error('Local account deployment state does not match mainnet') + } + const declarationState = await Promise.all( + plan.declarations.map(async (declaration) => ({ + ...declaration, + declared: await classDeclared(declaration.classHash), + })), + ) + + if (!execute) { + console.log( + JSON.stringify( + { + mode: 'plan-only', + ...plan, + minimumFunding: plan.minimumFunding.toString(), + maximumBudget: plan.maximumBudget.toString(), + account: { + ...localAccount, + publicBalance: publicAccount.publicBalance.toString(), + }, + declarations: declarationState, + rpcUrl: RPC_URL, + note: 'No mainnet transaction was submitted. First execution deploys the Ready account when funded.', + }, + null, + 2, + ), + ) + return + } + + let remainingBudget = plan.maximumBudget + let accountDeploymentTransactionHash: string | undefined + const declarationTransactions: Array< + Readonly<{ contractName: string; classHash: string; transactionHash?: string }> + > = [] + + if (!publicAccount.deployed) { + if (publicAccount.publicBalance < MAINNET_MINIMUM_FUNDING) { + throw new Error('Counterfactual mainnet deployer must hold at least 151 STRK before first execution') + } + const accountDeploymentBase = [ + 'account', + 'deploy', + '--network', + 'mainnet', + '--name', + MAINNET_ACCOUNT_NAME, + '--silent', + ] + const accountDeploymentDryRun = sncast([...accountDeploymentBase, '--dry-run', '--detailed']) + const accountDeploymentBounds = parseBufferedGasBounds(accountDeploymentDryRun, remainingBudget) + const accountDeploymentOutput = sncast(['--wait', ...accountDeploymentBase, ...boundArgs(accountDeploymentBounds)]) + accountDeploymentTransactionHash = parseLine(accountDeploymentOutput, 'Transaction Hash') + remainingBudget -= accountDeploymentBounds.ceiling + const deployedClassHash = await accountClassHash() + if (deployedClassHash === null || !sameFelt(deployedClassHash, READY_ACCOUNT_CLASS_HASH)) { + throw new Error('Ready account deployment did not produce the frozen account class') + } + requireFrozenMainnetAccount(accountList()) + } else { + if (publicAccount.publicBalance < MAINNET_MAXIMUM_BUDGET) { + throw new Error('Mainnet deployer public balance is below the frozen 150 STRK ceiling') + } + requireFrozenMainnetAccount(accountList()) + } + + for (const declaration of declarationState) { + if (declaration.declared) { + declarationTransactions.push({ contractName: declaration.contractName, classHash: declaration.classHash }) + continue + } + const base = [ + 'declare', + '--network', + 'mainnet', + '--contract-name', + declaration.contractName, + '--nonce', + nonce().toString(), + ] + const dryRun = sncast([...base, '--dry-run', '--detailed']) + const bounds = parseBufferedGasBounds(dryRun, remainingBudget) + const output = sncast(['--wait', ...base, ...boundArgs(bounds)]) + const returnedClassHash = parseLine(output, 'Class Hash') + const transactionHash = parseLine(output, 'Transaction Hash') + if (!sameFelt(returnedClassHash, declaration.classHash)) { + throw new Error(`${declaration.contractName} declaration returned an unexpected class hash`) + } + if (!(await classDeclared(declaration.classHash))) { + throw new Error(`${declaration.contractName} declaration was not readable after acceptance`) + } + remainingBudget -= bounds.ceiling + declarationTransactions.push({ + contractName: declaration.contractName, + classHash: declaration.classHash, + transactionHash, + }) + } + + const deploymentBase = [ + 'deploy', + '--network', + 'mainnet', + '--class-hash', + plan.auctionHouse.classHash, + '--constructor-calldata', + ...plan.auctionHouse.constructorCalldata, + '--salt', + plan.auctionHouse.salt, + '--nonce', + nonce().toString(), + ] + const deploymentDryRun = sncast([...deploymentBase, '--dry-run', '--detailed']) + const deploymentBounds = parseBufferedGasBounds(deploymentDryRun, remainingBudget) + const deploymentOutput = sncast(['--wait', ...deploymentBase, ...boundArgs(deploymentBounds)]) + const auctionHouse = parseLine(deploymentOutput, 'Contract Address') as `0x${string}` + const deploymentTransactionHash = parseLine(deploymentOutput, 'Transaction Hash') + remainingBudget -= deploymentBounds.ceiling + + const [deployedClassHash, houseResponse] = await Promise.all([ + provider.getClassHashAt(auctionHouse), + provider.callContract({ contractAddress: auctionHouse, entrypoint: 'get_house_config' }), + ]) + const houseConfig = callValues(houseResponse) + if (!sameFelt(deployedClassHash, plan.auctionHouse.classHash)) + throw new Error('Deployed AuctionHouse class hash mismatch') + if (!sameFelt(houseConfig[0] ?? '', MAINNET_STRK20_POOL)) throw new Error('Deployed AuctionHouse pool mismatch') + if (!sameFelt(houseConfig[1] ?? '', STRK_TOKEN)) throw new Error('Deployed AuctionHouse payment token mismatch') + if (BigInt(houseConfig[2] ?? '0') !== 32n) throw new Error('Deployed AuctionHouse bidder bound mismatch') + + const publicRecord = { + schema: 'cipherbid.mainnet-deployment.v1', + network: 'mainnet', + chainId: MAINNET_CHAIN_ID, + deployer: MAINNET_DEPLOYER, + auctionHouse, + auctionHouseClassHash: plan.auctionHouse.classHash, + demoErc721ClassHash: plan.demoNft.classHash, + strk20Pool: MAINNET_STRK20_POOL, + paymentToken: STRK_TOKEN, + maximumBudget: plan.maximumBudget.toString(), + remainingBudgetCeiling: remainingBudget.toString(), + accountDeploymentTransactionHash, + declarationTransactions, + deploymentTransactionHash, + explorerUrl: `https://voyager.online/contract/${auctionHouse}`, + } + const evidenceDirectory = path.resolve(process.cwd(), '..', '.runtime-evidence', 'mainnet') + mkdirSync(evidenceDirectory, { recursive: true }) + const recordPath = path.join(evidenceDirectory, 'deployment.json') + writeFileSync(recordPath, JSON.stringify(publicRecord, null, 2), { encoding: 'utf8', mode: 0o600, flag: 'wx' }) + console.log(JSON.stringify({ ...publicRecord, recordPath }, null, 2)) +} + +void main().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 +}) diff --git a/web/scripts/preflight-mainnet.ts b/web/scripts/preflight-mainnet.ts new file mode 100644 index 0000000..1153f23 --- /dev/null +++ b/web/scripts/preflight-mainnet.ts @@ -0,0 +1,117 @@ +import { hash, RpcProvider } from 'starknet' +import { + evaluateDemoBidderReadiness, + type DemoBidderStatus, + type PublicDeposit, +} from '@/features/auction/demoBidderReadiness' +import { MAINNET_STRK20_POOL, STRK_TOKEN } from '@/config/deployment' +import { MAINNET_BIDDER_A, MAINNET_BIDDER_B, buildMainnetReleaseCandidate } from '@/config/mainnetRelease' + +const RPC_URL = 'https://api.zan.top/public/starknet-mainnet/rpc/v0_10' +const RELEASE_BLOCK_FLOOR = 14_017_934 +const DEPOSIT_SELECTOR = hash.getSelectorFromName('Deposit') +const BIDDER_ACCOUNTS = Object.freeze([ + { name: 'mainnet-bidder-a', address: MAINNET_BIDDER_A }, + { name: 'mainnet-bidder-b', address: MAINNET_BIDDER_B }, +]) + +function callValues(value: readonly string[] | Readonly<{ result: readonly string[] }>): readonly string[] { + return 'result' in value ? value.result : value +} + +async function publicDeposits(provider: RpcProvider, bidderAddress: `0x${string}`): Promise { + const deposits: PublicDeposit[] = [] + let continuationToken: string | undefined + do { + const page = await provider.getEvents({ + from_block: { block_number: RELEASE_BLOCK_FLOOR }, + to_block: 'latest', + address: MAINNET_STRK20_POOL, + keys: [[DEPOSIT_SELECTOR], [bidderAddress], [STRK_TOKEN]], + chunk_size: 100, + ...(continuationToken ? { continuation_token: continuationToken } : {}), + }) + for (const event of page.events) { + if (event.block_number === undefined || event.data[0] === undefined) { + throw new Error('STRK20 deposit event is missing accepted block or amount data') + } + deposits.push( + Object.freeze({ + amount: BigInt(event.data[0]), + blockNumber: event.block_number, + transactionHash: event.transaction_hash as `0x${string}`, + }), + ) + } + continuationToken = page.continuation_token + } while (continuationToken) + return Object.freeze(deposits) +} + +function printableStatus(status: DemoBidderStatus) { + return { + ...status, + depositAmount: status.depositAmount?.toString(), + privateBalanceVerified: false, + } +} + +async function main(): Promise { + const provider = new RpcProvider({ nodeUrl: RPC_URL }) + const latestBlock = await provider.getBlockNumber() + const feeResponse = await provider.callContract({ + contractAddress: MAINNET_STRK20_POOL, + entrypoint: 'get_fee_amount', + }) + const [feeValue] = callValues(feeResponse) + if (!feeValue) throw new Error('STRK20 returned no pool fee') + const candidate = buildMainnetReleaseCandidate(BigInt(feeValue)) + const bidders = await Promise.all( + BIDDER_ACCOUNTS.map(async (bidder) => { + const response = await provider.callContract({ + contractAddress: MAINNET_STRK20_POOL, + entrypoint: 'get_public_key', + calldata: [bidder.address], + }) + const [publicKey] = callValues(response) + if (!publicKey) throw new Error(`STRK20 returned no public key for ${bidder.name}`) + return { + ...bidder, + publicKey: publicKey as `0x${string}`, + deposits: await publicDeposits(provider, bidder.address), + } + }), + ) + const readiness = evaluateDemoBidderReadiness({ + bidders, + latestBlock, + minimumPublicDeposit: candidate.bidderShieldTarget, + }) + console.log( + JSON.stringify( + { + schema: 'cipherbid.mainnet-bidder-public-readiness.v1', + latestBlock, + releaseBlockFloor: RELEASE_BLOCK_FLOOR, + livePoolFee: candidate.poolFee.toString(), + requiredPublicDeposit: candidate.bidderShieldTarget.toString(), + ready: readiness.ready, + statuses: readiness.statuses.map(printableStatus), + note: 'Public registration and deposit maturity only; Ready X remains authoritative for unspent private balance.', + }, + null, + 2, + ), + ) + if (!readiness.ready) { + const blockers = readiness.statuses.flatMap((status) => + status.blockers.map((blocker) => `${status.name}: ${blocker}`), + ) + throw new Error(`Mainnet bidder preflight failed before any auction write:\n${blockers.join('\n')}`) + } +} + +void main().catch((error: unknown) => { + console.error(error instanceof Error ? error.message : String(error)) + process.exitCode = 1 +}) diff --git a/web/scripts/verify-pages-workflow.ts b/web/scripts/verify-pages-workflow.ts new file mode 100644 index 0000000..58d5720 --- /dev/null +++ b/web/scripts/verify-pages-workflow.ts @@ -0,0 +1,15 @@ +import { existsSync, readFileSync } from 'node:fs' +import path from 'node:path' +import { verifyPagesWorkflow } from '../src/config/pagesWorkflowPolicy' + +const workflowPath = path.resolve(process.cwd(), '..', '.github', 'workflows', 'deploy-pages.yml') +const errors = existsSync(workflowPath) + ? verifyPagesWorkflow(readFileSync(workflowPath, 'utf8')) + : ['workflow-file-missing'] + +if (errors.length > 0) { + for (const error of errors) console.error(error) + process.exitCode = 1 +} else { + console.log('pages_workflow_policy=passed') +} diff --git a/web/src/app/auction/page.tsx b/web/src/app/auction/page.tsx new file mode 100644 index 0000000..230ebe6 --- /dev/null +++ b/web/src/app/auction/page.tsx @@ -0,0 +1,10 @@ +import { Suspense } from 'react' +import { AuctionPageClient, AuctionPageLoading } from '@/features/auction/ui/AuctionPageClient' + +export default function AuctionPage() { + return ( + }> + + + ) +} diff --git a/web/src/app/auctions/[auctionId]/page.tsx b/web/src/app/auctions/[auctionId]/page.tsx deleted file mode 100644 index 53c6ac7..0000000 --- a/web/src/app/auctions/[auctionId]/page.tsx +++ /dev/null @@ -1,24 +0,0 @@ -import { AuctionBidPreview } from '@/features/auction/ui/AuctionBidPreview' - -function displayAuctionId(value: string): string { - let decoded = value - try { - decoded = decodeURIComponent(value) - } catch { - // Keep malformed percent-encoded segments inert and visible rather than failing the route. - } - const normalized = decoded - .replace(/[\u0000-\u001f\u007f]/g, '') - .trim() - .slice(0, 80) - return normalized.length > 0 ? normalized : 'unknown' -} - -export default async function AuctionPage({ - params, -}: Readonly<{ - params: Promise<{ auctionId: string }> -}>) { - const { auctionId } = await params - return -} diff --git a/web/src/app/create/page.tsx b/web/src/app/create/page.tsx new file mode 100644 index 0000000..c5facf6 --- /dev/null +++ b/web/src/app/create/page.tsx @@ -0,0 +1,16 @@ +import { loadDeploymentManifest, type DeploymentManifest } from '@/config/deployment' +import { SellerCreatePage } from '@/features/auction/ui/SellerCreatePage' + +export default function CreateAuctionRoute() { + let deployment: DeploymentManifest | undefined + try { + deployment = loadDeploymentManifest(process.env) + } catch { + deployment = undefined + } + return deployment ? ( + + ) : ( + + ) +} diff --git a/web/src/app/demo/setup/page.tsx b/web/src/app/demo/setup/page.tsx new file mode 100644 index 0000000..2ef1869 --- /dev/null +++ b/web/src/app/demo/setup/page.tsx @@ -0,0 +1,6 @@ +import { DemoBidderSetupPage } from '@/features/demo/ui/DemoBidderSetupPage' +import { loadDeploymentManifest } from '@/config/deployment' + +export default function DemoSetupRoute() { + return +} diff --git a/web/src/app/globals.css b/web/src/app/globals.css index ce3cdc3..9791112 100644 --- a/web/src/app/globals.css +++ b/web/src/app/globals.css @@ -2,9 +2,58 @@ .cipherbid-auction-art { background: - radial-gradient(circle at 72% 28%, rgb(125 92 255 / 42%), transparent 32%), - radial-gradient(circle at 30% 76%, rgb(77 174 134 / 22%), transparent 28%), - linear-gradient(145deg, #17151f, #2b2441 52%, #7564d8); + radial-gradient(circle at 74% 23%, rgb(113 112 255 / 48%), transparent 28%), + radial-gradient(circle at 25% 82%, rgb(40 166 104 / 18%), transparent 25%), + linear-gradient(145deg, #0a0b0d, #151526 52%, #242342); +} + +.cipherbid-auction-page { + background-image: + linear-gradient(rgb(255 255 255 / 0.018) 1px, transparent 1px), + linear-gradient(90deg, rgb(255 255 255 / 0.018) 1px, transparent 1px), + radial-gradient(circle at 50% -20%, rgb(94 106 210 / 0.16), transparent 38%); + background-position: center; + background-size: + 44px 44px, + 44px 44px, + auto; + font-feature-settings: 'cv01', 'ss03'; +} + +.cipherbid-auction-art::before { + position: absolute; + inset: 0; + content: ''; + background-image: linear-gradient(90deg, rgb(255 255 255 / 0.04) 1px, transparent 1px); + background-size: 24px 24px; + mask-image: linear-gradient(to bottom, black, transparent 80%); +} + +.cipherbid-panel :is(h2, h3) { + color: #f7f8f8; +} + +.cipherbid-panel :is(p, li, dt, dd) { + color: #9ba3af; +} + +.cipherbid-panel .text-\[\#6654d9\] { + color: #a8b1ff; +} + +.cipherbid-panel .divide-black\/10, +.cipherbid-panel .border-black\/10 { + border-color: rgb(255 255 255 / 0.08); +} + +.cipherbid-panel .bg-\[\#e1f1e9\], +.cipherbid-panel .bg-\[\#eeeae2\] { + background: rgb(255 255 255 / 0.035); +} + +.cipherbid-panel .text-\[\#195e47\], +.cipherbid-panel .text-\[\#245e4b\] { + color: #aee5c1; } @media (prefers-reduced-motion: reduce) { diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 50ab52b..e485a87 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -2,8 +2,8 @@ import type { Metadata } from 'next' import './globals.css' export const metadata: Metadata = { - title: 'Create Next App', - description: 'Generated by create next app', + title: 'CipherBid — Private Vickrey auctions on Starknet', + description: 'A visual preview of STRK20-funded sealed NFT auction mechanics.', } export default function RootLayout({ diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 78b4877..eca37fd 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -1,30 +1,99 @@ 'use client' import { useState } from 'react' -import { WalletConnectPanel } from '@/features/wallet/WalletConnectPanel' +import Link from 'next/link' +import { buildAuctionHref } from '@/features/auction/auctionRoute' export default function Home() { - const [walletConnected, setWalletConnected] = useState(false) + const [auctionId, setAuctionId] = useState('1') + const safeAuctionId = /^[1-9][0-9]{0,19}$/.test(auctionId) ? auctionId : '1' return ( -
-
-

Starknet Sepolia

-

CipherBid feasibility gate

-

- Connect a Wallet API 0.10.3 wallet. Keys, viewing keys, shielded balances, and raw wallet errors never enter - application state. -

-
-
- setWalletConnected(true)} /> +
+
+
+ CipherBid + + Create auction + +
+ +
+
+

+ STRK20 · Starknet · Vickrey auctions +

+

+ Private bids. Guaranteed onchain delivery. +

+

+ Every bidder locks the same public STRK cap. Bid amounts stay sealed until reveal, while the NFT remains + in contract custody for atomic settlement. +

+
+ + Create an auction + + + Open live auction + +
+
+ + +
- {walletConnected ? ( -

- Wallet capability verified. Bid preparation remains disabled until secrets can stay outside application state - and the auction contract has authenticated refund and claim paths. -

- ) : null}
) } diff --git a/web/src/config/deployment.ts b/web/src/config/deployment.ts new file mode 100644 index 0000000..332c80a --- /dev/null +++ b/web/src/config/deployment.ts @@ -0,0 +1,93 @@ +export const SEPOLIA_CHAIN_ID = '0x534e5f5345504f4c4941' +export const MAINNET_CHAIN_ID = '0x534e5f4d41494e' +export const SEPOLIA_STRK20_POOL = '0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91' +export const MAINNET_STRK20_POOL = '0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a' +export const STRK_TOKEN = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d' + +const CONTRACT_ADDRESS_BOUND = 1n << 251n +const STARK_FIELD_PRIME = (1n << 251n) + 17n * (1n << 192n) + 1n + +export type DeploymentNetwork = 'sepolia' | 'mainnet' +export type DeploymentEnvironment = Readonly> + +export type DeploymentManifest = Readonly<{ + network: DeploymentNetwork + chainId: string + rpcUrl: string + auctionHouse: `0x${string}` + auctionHouseClassHash: `0x${string}` + strk20Pool: `0x${string}` + paymentToken: `0x${string}` +}> + +function required(environment: DeploymentEnvironment, key: string): string { + const value = environment[key]?.trim() + if (!value) throw new Error(`${key} is required`) + return value +} + +function parseFelt(value: string, key: string, exclusiveBound: bigint): `0x${string}` { + if (!/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${key} must be a hexadecimal felt`) + const parsed = BigInt(value) + if (parsed <= 0n || parsed >= exclusiveBound) throw new Error(`${key} is outside the accepted felt range`) + return `0x${parsed.toString(16)}` +} + +function parseRpcUrl(value: string): string { + let url: URL + try { + url = new URL(value) + } catch { + throw new Error('NEXT_PUBLIC_STARKNET_RPC_URL must be an absolute HTTP(S) URL') + } + if (url.protocol !== 'https:' && url.protocol !== 'http:') { + throw new Error('NEXT_PUBLIC_STARKNET_RPC_URL must be an absolute HTTP(S) URL') + } + return value +} + +export function loadDeploymentManifest(environment: DeploymentEnvironment): DeploymentManifest { + const rawNetwork = required(environment, 'NEXT_PUBLIC_CIPHERBID_NETWORK') + if (rawNetwork !== 'sepolia' && rawNetwork !== 'mainnet') { + throw new Error(`Unsupported CipherBid network: ${rawNetwork}`) + } + + const network: DeploymentNetwork = rawNetwork + const chainId = network === 'sepolia' ? SEPOLIA_CHAIN_ID : MAINNET_CHAIN_ID + const canonicalPool = network === 'sepolia' ? SEPOLIA_STRK20_POOL : MAINNET_STRK20_POOL + const configuredPool = parseFelt( + required(environment, 'NEXT_PUBLIC_STRK20_POOL_ADDRESS'), + 'NEXT_PUBLIC_STRK20_POOL_ADDRESS', + CONTRACT_ADDRESS_BOUND, + ) + const configuredToken = parseFelt( + required(environment, 'NEXT_PUBLIC_STRK_TOKEN_ADDRESS'), + 'NEXT_PUBLIC_STRK_TOKEN_ADDRESS', + CONTRACT_ADDRESS_BOUND, + ) + + if (BigInt(configuredPool) !== BigInt(canonicalPool)) { + throw new Error(`STRK20 pool does not match canonical ${network} deployment`) + } + if (BigInt(configuredToken) !== BigInt(STRK_TOKEN)) { + throw new Error('STRK token does not match canonical deployment') + } + + return Object.freeze({ + network, + chainId, + rpcUrl: parseRpcUrl(required(environment, 'NEXT_PUBLIC_STARKNET_RPC_URL')), + auctionHouse: parseFelt( + required(environment, 'NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS'), + 'NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS', + CONTRACT_ADDRESS_BOUND, + ), + auctionHouseClassHash: parseFelt( + required(environment, 'NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH'), + 'NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH', + STARK_FIELD_PRIME, + ), + strk20Pool: canonicalPool, + paymentToken: STRK_TOKEN, + }) +} diff --git a/web/src/config/mainnetAuctionPlan.ts b/web/src/config/mainnetAuctionPlan.ts new file mode 100644 index 0000000..a94df93 --- /dev/null +++ b/web/src/config/mainnetAuctionPlan.ts @@ -0,0 +1,114 @@ +import { buildAuctionCreationPlan } from '@/features/auction/auctionCreationPlan' +import { + AUCTION_HOUSE_CLASS_HASH, + DEMO_ERC721_CLASS_HASH, + MAINNET_MAXIMUM_BUDGET, +} from '@/config/mainnetDeploymentPlan' +import { MAINNET_CHAIN_ID, MAINNET_STRK20_POOL, STRK_TOKEN } from '@/config/deployment' +import { MAINNET_DEPLOYER } from '@/config/mainnetRelease' + +function sameFelt(left: unknown, right: string): boolean { + if (typeof left !== 'string') return false + try { + return BigInt(left) === BigInt(right) + } catch { + return false + } +} + +function record(value: unknown): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) { + throw new Error('Mainnet deployment record must be an object') + } + return value as Record +} + +export function parseMainnetDeploymentRecord(serialized: string): Readonly<{ + auctionHouse: `0x${string}` + demoNft: `0x${string}` + demoNftDeploymentTransactionHash: `0x${string}` + remainingBudgetCeiling: bigint +}> { + let parsed: Record + try { + parsed = record(JSON.parse(serialized)) + } catch { + throw new Error('Mainnet deployment record is invalid JSON') + } + if (parsed.schema !== 'cipherbid.mainnet-deployment.v1' || parsed.network !== 'mainnet') { + throw new Error('Mainnet deployment record schema or network is invalid') + } + if (!sameFelt(parsed.chainId, MAINNET_CHAIN_ID)) throw new Error('Mainnet deployment chain ID mismatch') + if (!sameFelt(parsed.deployer, MAINNET_DEPLOYER)) throw new Error('Mainnet deployment deployer mismatch') + if (!sameFelt(parsed.auctionHouseClassHash, AUCTION_HOUSE_CLASS_HASH)) { + throw new Error('Mainnet deployment AuctionHouse class mismatch') + } + if (!sameFelt(parsed.demoErc721ClassHash, DEMO_ERC721_CLASS_HASH)) { + throw new Error('Mainnet deployment DemoERC721 class mismatch') + } + if (!sameFelt(parsed.strk20Pool, MAINNET_STRK20_POOL)) throw new Error('Mainnet deployment pool mismatch') + if (!sameFelt(parsed.paymentToken, STRK_TOKEN)) throw new Error('Mainnet deployment payment token mismatch') + if (typeof parsed.auctionHouse !== 'string' || !/^0x[0-9a-fA-F]+$/.test(parsed.auctionHouse)) { + throw new Error('Mainnet deployment AuctionHouse address is invalid') + } + if (BigInt(parsed.auctionHouse) <= 0n) throw new Error('Mainnet deployment AuctionHouse address is zero') + if (typeof parsed.demoNft !== 'string' || !/^0x[0-9a-fA-F]+$/.test(parsed.demoNft) || BigInt(parsed.demoNft) <= 0n) { + throw new Error('Mainnet deployment DemoERC721 address is invalid') + } + if ( + typeof parsed.demoNftDeploymentTransactionHash !== 'string' || + !/^0x[0-9a-fA-F]+$/.test(parsed.demoNftDeploymentTransactionHash) || + BigInt(parsed.demoNftDeploymentTransactionHash) <= 0n + ) { + throw new Error('Mainnet DemoERC721 deployment transaction is invalid') + } + if (typeof parsed.remainingBudgetCeiling !== 'string' || !/^\d+$/.test(parsed.remainingBudgetCeiling)) { + throw new Error('Mainnet deployment remaining budget is invalid') + } + const remainingBudgetCeiling = BigInt(parsed.remainingBudgetCeiling) + if (remainingBudgetCeiling <= 0n || remainingBudgetCeiling > MAINNET_MAXIMUM_BUDGET) { + throw new Error('Mainnet deployment remaining budget is outside the frozen ceiling') + } + return Object.freeze({ + auctionHouse: parsed.auctionHouse as `0x${string}`, + demoNft: parsed.demoNft as `0x${string}`, + demoNftDeploymentTransactionHash: parsed.demoNftDeploymentTransactionHash as `0x${string}`, + remainingBudgetCeiling, + }) +} + +export function renderMainnetEnvironment(deployment: Readonly<{ auctionHouse: `0x${string}` }>): string { + return [ + 'NEXT_PUBLIC_CIPHERBID_NETWORK=mainnet', + 'NEXT_PUBLIC_STARKNET_RPC_URL=https://api.zan.top/public/starknet-mainnet/rpc/v0_10', + `NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS=${deployment.auctionHouse}`, + `NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH=${AUCTION_HOUSE_CLASS_HASH}`, + `NEXT_PUBLIC_STRK20_POOL_ADDRESS=${MAINNET_STRK20_POOL}`, + `NEXT_PUBLIC_STRK_TOKEN_ADDRESS=${STRK_TOKEN}`, + '', + ].join('\n') +} + +export function buildMainnetAuctionCreationPlan( + input: Readonly<{ + auctionHouse: `0x${string}` + nftContract: `0x${string}` + auctionId: bigint + sellerClaimHandle: bigint + nowSeconds: number + }>, +) { + return buildAuctionCreationPlan({ + auctionHouse: input.auctionHouse, + nftContract: input.nftContract, + tokenId: 99n, + auctionId: input.auctionId, + claimHandle: input.sellerClaimHandle, + reserve: '1', + cap: '4', + nowSeconds: input.nowSeconds, + biddingMinutes: 10, + revealMinutes: 5, + bidderLimit: 2, + }) +} diff --git a/web/src/config/mainnetDeploymentPlan.ts b/web/src/config/mainnetDeploymentPlan.ts new file mode 100644 index 0000000..2f746c6 --- /dev/null +++ b/web/src/config/mainnetDeploymentPlan.ts @@ -0,0 +1,127 @@ +import { MAINNET_STRK20_POOL, STRK_TOKEN } from '@/config/deployment' +import { MAINNET_DEPLOYER } from '@/config/mainnetRelease' + +export const AUCTION_HOUSE_CLASS_HASH = '0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4' as const +export const DEMO_ERC721_CLASS_HASH = '0x06c7cba5680595203f9327f5784130907bad1b808891122ad358c10b93136a41' as const +export const MAINNET_ACCOUNT_NAME = 'cipherbid-mainnet-deployer' as const +export const MAINNET_ACCOUNT_FILE = '/home/sourcesensei/.starknet_accounts/cipherbid-hackathon-mainnet.json' as const +export const MAINNET_AUCTION_HOUSE_SALT = '0x4349504845524249445f41485f4d41494e4e45545f5631' as const +export const MAINNET_DEMO_NFT_SALT = '0x4349504845524249445f4e46545f4d41494e4e45545f5631' as const +export const MAINNET_MAXIMUM_BUDGET = 150n * 10n ** 18n +export const MAINNET_MINIMUM_FUNDING = 151n * 10n ** 18n + +export type GasBounds = Readonly<{ + l1Gas: bigint + l1GasPrice: bigint + l2Gas: bigint + l2GasPrice: bigint + l1DataGas: bigint + l1DataGasPrice: bigint + ceiling: bigint +}> + +export function buildMainnetDeploymentPlan() { + return { + network: 'mainnet' as const, + accountName: MAINNET_ACCOUNT_NAME, + accountFile: MAINNET_ACCOUNT_FILE, + deployer: MAINNET_DEPLOYER, + minimumFunding: MAINNET_MINIMUM_FUNDING, + maximumBudget: MAINNET_MAXIMUM_BUDGET, + declarations: [ + { contractName: 'AuctionHouse' as const, classHash: AUCTION_HOUSE_CLASS_HASH }, + { contractName: 'DemoERC721' as const, classHash: DEMO_ERC721_CLASS_HASH }, + ], + auctionHouse: { + classHash: AUCTION_HOUSE_CLASS_HASH, + salt: MAINNET_AUCTION_HOUSE_SALT, + constructorCalldata: [MAINNET_STRK20_POOL, STRK_TOKEN, '32'] as const, + }, + demoNft: { + classHash: DEMO_ERC721_CLASS_HASH, + salt: MAINNET_DEMO_NFT_SALT, + }, + } +} + +function sameFelt(left: string, right: string): boolean { + try { + return BigInt(left) === BigInt(right) + } catch { + return false + } +} + +export function parseFrozenMainnetAccount(output: string): Readonly<{ + name: typeof MAINNET_ACCOUNT_NAME + address: typeof MAINNET_DEPLOYER + network: 'alpha-mainnet' + type: 'Ready' + deployed: boolean +}> { + const escapedName = MAINNET_ACCOUNT_NAME.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') + const match = new RegExp(`(?:^|\\n)- ${escapedName}:\\n([\\s\\S]*?)(?=\\n- |$)`).exec(output) + if (!match) throw new Error(`Named mainnet account ${MAINNET_ACCOUNT_NAME} was not found`) + const block = match[1] ?? '' + const field = (name: string) => new RegExp(`^\\s*${name}:\\s*(.+)$`, 'm').exec(block)?.[1]?.trim() + const network = field('network') + const address = field('address') + const type = field('type') + const deployed = field('deployed') + + if (network !== 'alpha-mainnet') throw new Error('Named account must use alpha-mainnet') + if (!address || !sameFelt(address, MAINNET_DEPLOYER)) + throw new Error('Named account address does not match frozen deployer') + if (type !== 'Ready') throw new Error('Named account must use the Ready account type') + if (deployed !== 'true' && deployed !== 'false') + throw new Error('Named Ready mainnet account has invalid deployment state') + + return Object.freeze({ + name: MAINNET_ACCOUNT_NAME, + address: MAINNET_DEPLOYER, + network: 'alpha-mainnet', + type: 'Ready', + deployed: deployed === 'true', + }) +} + +export function requireFrozenMainnetAccount(output: string): Readonly<{ + name: typeof MAINNET_ACCOUNT_NAME + address: typeof MAINNET_DEPLOYER + network: 'alpha-mainnet' + type: 'Ready' +}> { + const account = parseFrozenMainnetAccount(output) + if (!account.deployed) throw new Error('Named Ready mainnet account must already be deployed') + return Object.freeze({ + name: account.name, + address: account.address, + network: account.network, + type: account.type, + }) +} + +function parsedValue(output: string, label: string): bigint { + const match = new RegExp(`${label}:\\s*(\\d+)`).exec(output) + if (!match) throw new Error(`Could not parse ${label} from sncast dry run`) + return BigInt(match[1]) +} + +export function parseBufferedGasBounds(output: string, transactionBudget: bigint): GasBounds { + if (transactionBudget <= 0n) throw new Error('Transaction budget must be positive') + const buffer = (value: bigint, numerator: bigint, denominator: bigint) => + value === 0n ? 0n : (value * numerator + denominator - 1n) / denominator + const l1Gas = buffer(parsedValue(output, 'L1 Gas Consumed'), 13n, 10n) + const l1GasPrice = buffer(parsedValue(output, 'L1 Gas Price'), 3n, 2n) + const l2Gas = buffer(parsedValue(output, 'L2 Gas Consumed'), 13n, 10n) + const l2GasPrice = buffer(parsedValue(output, 'L2 Gas Price'), 3n, 2n) + const l1DataGas = buffer(parsedValue(output, 'L1 Data Gas Consumed'), 13n, 10n) + const l1DataGasPrice = buffer(parsedValue(output, 'L1 Data Gas Price'), 3n, 2n) + const ceiling = l1Gas * l1GasPrice + l2Gas * l2GasPrice + l1DataGas * l1DataGasPrice + if (ceiling > transactionBudget) { + throw new Error( + `Buffered transaction ceiling ${ceiling} Fri exceeds remaining mainnet budget ${transactionBudget} Fri`, + ) + } + return Object.freeze({ l1Gas, l1GasPrice, l2Gas, l2GasPrice, l1DataGas, l1DataGasPrice, ceiling }) +} diff --git a/web/src/config/mainnetRelease.ts b/web/src/config/mainnetRelease.ts new file mode 100644 index 0000000..3e3fb01 --- /dev/null +++ b/web/src/config/mainnetRelease.ts @@ -0,0 +1,71 @@ +import { MAINNET_CHAIN_ID, MAINNET_STRK20_POOL, STRK_TOKEN } from '@/config/deployment' + +const STRK = 10n ** 18n + +export const MAINNET_DEPLOYER = '0x01017404a72b0d5312d7f41e81e0a87b89387db78361bb4ce60b0e0a390d72aa' as const +export const MAINNET_BIDDER_A = '0x00289637e6debed46ce1a64ea30a9f1fa492458bac580c908f940f225fd11a8e' as const +export const MAINNET_BIDDER_B = '0x057791bafe2653e8a62509261aeba6a9d09f1fe09f039c9ff0c09c00c24b1f1a' as const + +export type MainnetReleaseCandidate = Readonly<{ + network: 'mainnet' + chainId: typeof MAINNET_CHAIN_ID + strk20Pool: typeof MAINNET_STRK20_POOL + paymentToken: typeof STRK_TOKEN + deployer: typeof MAINNET_DEPLOYER + bidderA: typeof MAINNET_BIDDER_A + bidderB: typeof MAINNET_BIDDER_B + reserve: bigint + collateralCap: bigint + bidderLimit: 2 + biddingMinutes: 10 + revealMinutes: 5 + bidderABid: bigint + bidderBBid: bigint + winner: 'Bidder B' + clearingPrice: bigint + loserRefund: bigint + winnerSurplus: bigint + sellerProceeds: bigint + poolFee: bigint + minimumBidderShield: bigint + bidderShieldTarget: bigint + sellerShieldTarget: bigint + maximumMainnetBudget: bigint +}> + +export function buildMainnetReleaseCandidate(poolFee: bigint): MainnetReleaseCandidate { + if (poolFee <= 0n) throw new Error('pool fee must be positive') + + const collateralCap = 4n * STRK + const bidderABid = 2n * STRK + const bidderBBid = 3n * STRK + const clearingPrice = bidderABid + const minimumBidderShield = collateralCap + 3n * poolFee + + return Object.freeze({ + network: 'mainnet', + chainId: MAINNET_CHAIN_ID, + strk20Pool: MAINNET_STRK20_POOL, + paymentToken: STRK_TOKEN, + deployer: MAINNET_DEPLOYER, + bidderA: MAINNET_BIDDER_A, + bidderB: MAINNET_BIDDER_B, + reserve: 1n * STRK, + collateralCap, + bidderLimit: 2, + biddingMinutes: 10, + revealMinutes: 5, + bidderABid, + bidderBBid, + winner: 'Bidder B', + clearingPrice, + loserRefund: collateralCap, + winnerSurplus: collateralCap - clearingPrice, + sellerProceeds: clearingPrice, + poolFee, + minimumBidderShield, + bidderShieldTarget: minimumBidderShield + 2n * STRK, + sellerShieldTarget: 2n * poolFee, + maximumMainnetBudget: 150n * STRK, + }) +} diff --git a/web/src/config/pagesWorkflowPolicy.ts b/web/src/config/pagesWorkflowPolicy.ts new file mode 100644 index 0000000..82266a1 --- /dev/null +++ b/web/src/config/pagesWorkflowPolicy.ts @@ -0,0 +1,180 @@ +import { parse } from 'yaml' + +const APPROVED_ACTIONS = Object.freeze([ + 'actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1', + 'actions/setup-node@820762786026740c76f36085b0efc47a31fe5020', + 'actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d', + 'actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9', + 'actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128', +]) + +const PUBLIC_BUILD_ENVIRONMENT = Object.freeze({ + CIPHERBID_PAGES_BUILD: '1', + NEXT_PUBLIC_CIPHERBID_NETWORK: 'mainnet', + NEXT_PUBLIC_STARKNET_RPC_URL: 'https://api.zan.top/public/starknet-mainnet/rpc/v0_10', + NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS: '0x01b32af8bab712ede82117b8ff1b8866e09798f6c81edc255ffe59dd42e4843e', + NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH: '0x06aa99b7ae9e10619b5a3c1713a4d71054844d3dda8e21bef98db6e653d5efc4', + NEXT_PUBLIC_STRK20_POOL_ADDRESS: '0x040337b1af3c663e86e333bab5a4b28da8d4652a15a69beee2b677776ffe812a', + NEXT_PUBLIC_STRK_TOKEN_ADDRESS: '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d', +}) + +type UnknownRecord = Record + +function record(value: unknown): UnknownRecord | null { + if (typeof value !== 'object' || value === null || Array.isArray(value)) return null + return value as UnknownRecord +} + +function exactKeys(value: UnknownRecord, expected: readonly string[]): boolean { + const actual = Object.keys(value).sort() + return actual.length === expected.length && actual.every((key, index) => key === [...expected].sort()[index]) +} + +function exactStringRecord(value: unknown, expected: Readonly>): boolean { + const candidate = record(value) + if (!candidate || !exactKeys(candidate, Object.keys(expected))) return false + return Object.entries(expected).every(([key, expectedValue]) => candidate[key] === expectedValue) +} + +function stepsOf(job: UnknownRecord | null): UnknownRecord[] { + if (!job || !Array.isArray(job.steps)) return [] + return job.steps.map(record).filter((step): step is UnknownRecord => step !== null) +} + +export function verifyPagesWorkflow(source: string): readonly string[] { + const errors: string[] = [] + const fail = (label: string) => { + if (!errors.includes(label)) errors.push(label) + } + + if (typeof source !== 'string' || source.length === 0 || source.length > 100_000) { + return Object.freeze(['workflow-source-invalid']) + } + if (/\bpull_request_target\b/.test(source)) fail('workflow-event-invalid') + if (/\$\{\{\s*secrets(?:\.|\[)/i.test(source)) fail('workflow-secret-expression-forbidden') + if (/(^|[\s"'])dotenv(?:[\s"']|$)|(^|[\s"'])\.env(?:[.\s"']|$)/im.test(source)) { + fail('workflow-dotenv-forbidden') + } + + let parsed: unknown + try { + parsed = parse(source, { maxAliasCount: 0, uniqueKeys: true }) + } catch { + return Object.freeze([...errors, 'workflow-yaml-invalid']) + } + + const root = record(parsed) + if (!root) return Object.freeze([...errors, 'workflow-document-invalid']) + if (root.name !== 'Deploy CipherBid Pages') fail('workflow-name-invalid') + + const events = record(root.on) + const push = events ? record(events.push) : null + if ( + !events || + !exactKeys(events, ['push', 'workflow_dispatch']) || + !push || + !exactKeys(push, ['branches']) || + !Array.isArray(push.branches) || + push.branches.length !== 1 || + push.branches[0] !== 'main' + ) { + fail('workflow-event-invalid') + } + + if (!exactStringRecord(root.permissions, { contents: 'read' })) fail('workflow-root-permissions-invalid') + const concurrency = record(root.concurrency) + if (!concurrency || concurrency.group !== 'pages' || concurrency['cancel-in-progress'] !== false) { + fail('workflow-concurrency-invalid') + } + + const jobs = record(root.jobs) + if (!jobs || !exactKeys(jobs, ['build', 'deploy'])) { + fail('workflow-jobs-invalid') + return Object.freeze(errors) + } + const build = record(jobs.build) + const deploy = record(jobs.deploy) + if (!build || build['runs-on'] !== 'ubuntu-latest') fail('workflow-build-job-invalid') + if (!deploy || deploy['runs-on'] !== 'ubuntu-latest' || deploy.needs !== 'build') fail('workflow-deploy-job-invalid') + + const buildSteps = stepsOf(build) + const deploySteps = stepsOf(deploy) + const actions = [...buildSteps, ...deploySteps] + .map((step) => step.uses) + .filter((value): value is string => typeof value === 'string') + if ( + actions.length !== APPROVED_ACTIONS.length || + actions.some((action, index) => action !== APPROVED_ACTIONS[index]) + ) { + fail('workflow-actions-invalid') + } + + const checkout = buildSteps.find((step) => step.uses === APPROVED_ACTIONS[0]) + const checkoutWith = record(checkout?.with) + if (!checkoutWith || checkoutWith['persist-credentials'] !== false) fail('workflow-checkout-invalid') + + const setup = buildSteps.find((step) => step.uses === APPROVED_ACTIONS[1]) + const setupWith = record(setup?.with) + if ( + !setupWith || + setupWith['node-version'] !== '24.13.1' || + setupWith.cache !== 'pnpm' || + setupWith['cache-dependency-path'] !== 'web/pnpm-lock.yaml' + ) { + fail('workflow-node-setup-invalid') + } + + const runs = buildSteps.map((step) => step.run).filter((value): value is string => typeof value === 'string') + for (const required of [ + 'corepack enable', + 'pnpm install --frozen-lockfile', + 'pnpm pages:verify', + 'pnpm format:check', + 'pnpm lint', + 'pnpm typecheck', + 'pnpm test', + 'pnpm build', + ]) { + if ( + !runs.some((run) => + run + .split(/\r?\n/) + .map((line) => line.trim()) + .includes(required), + ) + ) { + fail('workflow-command-invalid') + } + } + + const buildStep = buildSteps.find((step) => step.name === 'Build static site') + if ( + !buildStep || + buildStep['working-directory'] !== 'web' || + buildStep.run !== 'pnpm build' || + !exactStringRecord(buildStep.env, PUBLIC_BUILD_ENVIRONMENT) + ) { + fail('workflow-build-environment-invalid') + } + + const upload = buildSteps.find((step) => step.uses === APPROVED_ACTIONS[3]) + const uploadWith = record(upload?.with) + if (!uploadWith || uploadWith.path !== 'web/out') fail('workflow-artifact-invalid') + + if (!exactStringRecord(deploy?.permissions, { pages: 'write', 'id-token': 'write' })) { + fail('workflow-deploy-permissions-invalid') + } + const environment = record(deploy?.environment) + if ( + !environment || + environment.name !== 'github-pages' || + environment.url !== '${{ steps.deployment.outputs.page_url }}' + ) { + fail('workflow-environment-invalid') + } + if (deploySteps.length !== 1 || deploySteps[0].id !== 'deployment' || deploySteps[0].uses !== APPROVED_ACTIONS[4]) { + fail('workflow-deploy-step-invalid') + } + + return Object.freeze(errors) +} diff --git a/web/src/config/publicDeployment.ts b/web/src/config/publicDeployment.ts new file mode 100644 index 0000000..51139bf --- /dev/null +++ b/web/src/config/publicDeployment.ts @@ -0,0 +1,12 @@ +import { loadDeploymentManifest, type DeploymentManifest } from '@/config/deployment' + +export function loadPublicDeploymentManifest(): DeploymentManifest { + return loadDeploymentManifest({ + NEXT_PUBLIC_CIPHERBID_NETWORK: process.env.NEXT_PUBLIC_CIPHERBID_NETWORK, + NEXT_PUBLIC_STARKNET_RPC_URL: process.env.NEXT_PUBLIC_STARKNET_RPC_URL, + NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS: process.env.NEXT_PUBLIC_AUCTION_HOUSE_ADDRESS, + NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH: process.env.NEXT_PUBLIC_AUCTION_HOUSE_CLASS_HASH, + NEXT_PUBLIC_STRK20_POOL_ADDRESS: process.env.NEXT_PUBLIC_STRK20_POOL_ADDRESS, + NEXT_PUBLIC_STRK_TOKEN_ADDRESS: process.env.NEXT_PUBLIC_STRK_TOKEN_ADDRESS, + }) +} diff --git a/web/src/features/auction/auctionBrowserLoader.ts b/web/src/features/auction/auctionBrowserLoader.ts new file mode 100644 index 0000000..b0bad43 --- /dev/null +++ b/web/src/features/auction/auctionBrowserLoader.ts @@ -0,0 +1,16 @@ +import { RpcProvider } from 'starknet' +import { loadPublicDeploymentManifest } from '@/config/publicDeployment' +import { toAuctionLiveViewModel } from '@/features/auction/auctionLiveViewModel' +import { readAuctionSnapshot, type ChainReader } from '@/features/auction/auctionReader' +import type { AuctionLiveViewModel } from '@/features/auction/ui/AuctionLivePage' + +export async function loadAuctionLiveViewModel(auctionId: bigint): Promise { + const manifest = loadPublicDeploymentManifest() + const provider = new RpcProvider({ nodeUrl: manifest.rpcUrl }) + const reader: ChainReader = { + callContract: (call) => provider.callContract({ ...call, calldata: call.calldata ? [...call.calldata] : [] }), + getClassHashAt: (address) => provider.getClassHashAt(address), + } + const snapshot = await readAuctionSnapshot(reader, manifest, auctionId) + return toAuctionLiveViewModel(manifest, snapshot) +} diff --git a/web/src/features/auction/auctionConfig.ts b/web/src/features/auction/auctionConfig.ts new file mode 100644 index 0000000..5e21492 --- /dev/null +++ b/web/src/features/auction/auctionConfig.ts @@ -0,0 +1,97 @@ +import type { HexAddress } from '@/features/privacy/strk20Actions' +import { CONTRACT_ADDRESS_BOUND, STARK_FIELD_PRIME } from '@/features/auction/commitment' + +export const MAX_U64 = (1n << 64n) - 1n +export const MAX_U128 = (1n << 128n) - 1n +export const MAX_U256 = (1n << 256n) - 1n +export const ABSOLUTE_MAX_BIDDERS = 32 +export const STRK_TOKEN_ADDRESS = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d' as const + +export type AuctionHouseConfig = Readonly<{ + pool: HexAddress + paymentToken: HexAddress + maxBidders: number +}> + +export type AuctionConfig = Readonly<{ + auctionId: bigint + seller: HexAddress + sellerClaimHandle: bigint + nftContract: HexAddress + tokenId: bigint + reservePrice: bigint + collateralCap: bigint + biddingDeadline: bigint + revealDeadline: bigint + bidderLimit: number +}> + +export type AuctionCreationContext = Readonly<{ + caller: HexAddress + now: bigint + house: AuctionHouseConfig +}> + +function addressValue(value: HexAddress, error: string): bigint { + if (!/^0x[0-9a-f]+$/i.test(value)) throw new Error(error) + + const parsed = BigInt(value) + if (parsed <= 0n || parsed >= CONTRACT_ADDRESS_BOUND) throw new Error(error) + return parsed +} + +function requireIntegerInRange(value: number, min: number, max: number, error: string): void { + if (!Number.isInteger(value) || value < min || value > max) throw new Error(error) +} + +function requireUnsigned(value: bigint, max: bigint, error: string, allowZero: boolean): void { + if (value < 0n || value > max || (!allowZero && value === 0n)) throw new Error(error) +} + +export function defineAuctionHouseConfig(input: AuctionHouseConfig): AuctionHouseConfig { + const pool = addressValue(input.pool, 'STRK20 pool must be non-zero') + const paymentToken = addressValue(input.paymentToken, 'Payment token must be canonical STRK') + if (paymentToken !== BigInt(STRK_TOKEN_ADDRESS)) throw new Error('Payment token must be canonical STRK') + if (pool === paymentToken) throw new Error('STRK20 pool must differ from payment token') + requireIntegerInRange( + input.maxBidders, + 2, + ABSOLUTE_MAX_BIDDERS, + `House max bidders must be between 2 and ${ABSOLUTE_MAX_BIDDERS}`, + ) + + return Object.freeze({ ...input }) +} + +export function defineAuctionConfig(input: AuctionConfig, context: AuctionCreationContext): AuctionConfig { + const house = defineAuctionHouseConfig(context.house) + requireUnsigned(input.auctionId, MAX_U64, 'Auction ID must be between 1 and u64 max', false) + + const seller = addressValue(input.seller, 'Seller must be non-zero') + const caller = addressValue(context.caller, 'Creation caller must be non-zero') + if (seller !== caller) throw new Error('Seller must equal the creation caller') + requireUnsigned(input.sellerClaimHandle, STARK_FIELD_PRIME - 1n, 'Seller claim handle must be a non-zero felt', false) + addressValue(input.nftContract, 'ERC-721 contract must be non-zero') + + requireUnsigned(input.tokenId, MAX_U256, 'Token ID must fit u256', true) + requireUnsigned(input.reservePrice, MAX_U128, 'Reserve must be between 1 and u128 max', false) + requireUnsigned(input.collateralCap, MAX_U128, 'Collateral cap must be between 1 and u128 max', false) + if (input.reservePrice > input.collateralCap) throw new Error('Reserve must not exceed collateral cap') + + requireUnsigned(context.now, MAX_U64, 'Current timestamp must fit u64', true) + requireUnsigned(input.biddingDeadline, MAX_U64, 'Bidding deadline must fit u64', true) + requireUnsigned(input.revealDeadline, MAX_U64, 'Reveal deadline must fit u64', true) + if (input.biddingDeadline <= context.now) throw new Error('Bidding deadline must be in the future') + if (input.biddingDeadline >= input.revealDeadline) { + throw new Error('Bidding deadline must be before reveal deadline') + } + + requireIntegerInRange( + input.bidderLimit, + 2, + house.maxBidders, + 'Auction bidder limit must be between 2 and house maximum', + ) + + return Object.freeze({ ...input }) +} diff --git a/web/src/features/auction/auctionCreationPlan.ts b/web/src/features/auction/auctionCreationPlan.ts new file mode 100644 index 0000000..472fa45 --- /dev/null +++ b/web/src/features/auction/auctionCreationPlan.ts @@ -0,0 +1,130 @@ +const STRK_DECIMALS = 18 +const MAX_U64 = (1n << 64n) - 1n +const MAX_U128 = (1n << 128n) - 1n +const MAX_U256 = (1n << 256n) - 1n +const CONTRACT_ADDRESS_BOUND = 1n << 251n +const STARK_FIELD_PRIME = (1n << 251n) + 17n * (1n << 192n) + 1n + +type HexAddress = `0x${string}` + +export type AuctionCreationPlanInput = Readonly<{ + auctionHouse: HexAddress + nftContract: HexAddress + tokenId: bigint + auctionId: bigint + claimHandle: bigint + reserve: string + cap: string + nowSeconds: number + biddingMinutes: number + revealMinutes: number + bidderLimit: number +}> + +export type AuctionCreationForm = Readonly<{ + auctionId: string + nftContract: HexAddress + tokenId: string + reservePrice: string + cap: string + biddingDeadline: string + revealDeadline: string + bidderLimit: string + sellerClaimHandle: `0x${string}` +}> + +function address(value: HexAddress, label: string): HexAddress { + if (!/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} must be a hexadecimal Starknet address`) + const parsed = BigInt(value) + if (parsed <= 0n || parsed >= CONTRACT_ADDRESS_BOUND) + throw new Error(`${label} is outside the Starknet address range`) + return `0x${parsed.toString(16)}` +} + +function positiveInteger(value: number, label: string): number { + if (!Number.isSafeInteger(value) || value <= 0) throw new Error(`${label} must be a positive integer`) + return value +} + +export function parseStrkAmount(value: string): bigint { + if (typeof value !== 'string' || !/^(0|[1-9][0-9]*)(\.[0-9]+)?$/.test(value)) { + throw new Error('STRK amount must be a non-negative canonical decimal') + } + const [whole, fraction = ''] = value.split('.') + if (fraction.length > STRK_DECIMALS) throw new Error(`STRK amount supports at most ${STRK_DECIMALS} decimal places`) + const parsed = BigInt(whole) * 10n ** BigInt(STRK_DECIMALS) + BigInt(fraction.padEnd(STRK_DECIMALS, '0') || '0') + if (parsed > MAX_U128) throw new Error('STRK amount exceeds u128') + return parsed +} + +export function buildAuctionCreationPlan(input: AuctionCreationPlanInput): Readonly<{ + form: AuctionCreationForm + multicallTokens: readonly string[] +}> { + const auctionHouse = address(input.auctionHouse, 'AuctionHouse') + const nftContract = address(input.nftContract, 'NFT contract') + if (input.tokenId < 0n || input.tokenId > MAX_U256) throw new Error('Token ID must fit u256') + if (input.auctionId <= 0n || input.auctionId > MAX_U64) throw new Error('Auction ID must be between 1 and u64 max') + if (input.claimHandle <= 0n || input.claimHandle >= STARK_FIELD_PRIME) { + throw new Error('Seller claim handle must be a non-zero Stark field element') + } + const reservePrice = parseStrkAmount(input.reserve) + const cap = parseStrkAmount(input.cap) + if (reservePrice <= 0n) throw new Error('Auction reserve must be positive') + if (cap < reservePrice) throw new Error('Auction reserve cannot exceed collateral cap') + const biddingMinutes = positiveInteger(input.biddingMinutes, 'Bidding duration') + const revealMinutes = positiveInteger(input.revealMinutes, 'Reveal duration') + if (revealMinutes > 5) throw new Error('Reveal duration must be at most 5 minutes for the demo lifecycle') + if (!Number.isSafeInteger(input.nowSeconds) || input.nowSeconds <= 0) throw new Error('Current timestamp is invalid') + if (!Number.isSafeInteger(input.bidderLimit) || input.bidderLimit <= 0 || input.bidderLimit > 32) { + throw new Error('Bidder limit must be between 1 and 32') + } + const biddingDeadline = BigInt(input.nowSeconds + biddingMinutes * 60) + const revealDeadline = BigInt(Number(biddingDeadline) + revealMinutes * 60) + if (revealDeadline > MAX_U64) throw new Error('Auction deadlines exceed u64') + const lowMask = (1n << 128n) - 1n + const tokenLow = input.tokenId & lowMask + const tokenHigh = input.tokenId >> 128n + const form: AuctionCreationForm = Object.freeze({ + auctionId: input.auctionId.toString(), + nftContract, + tokenId: input.tokenId.toString(), + reservePrice: reservePrice.toString(), + cap: cap.toString(), + biddingDeadline: biddingDeadline.toString(), + revealDeadline: revealDeadline.toString(), + bidderLimit: input.bidderLimit.toString(), + sellerClaimHandle: `0x${input.claimHandle.toString(16)}`, + }) + return Object.freeze({ + form, + multicallTokens: Object.freeze([ + 'invoke', + '--contract-address', + nftContract, + '--function', + 'approve', + '--calldata', + auctionHouse, + tokenLow.toString(), + tokenHigh.toString(), + '/', + 'invoke', + '--contract-address', + auctionHouse, + '--function', + 'create_auction', + '--calldata', + form.auctionId, + form.sellerClaimHandle, + nftContract, + tokenLow.toString(), + tokenHigh.toString(), + form.reservePrice, + form.cap, + form.biddingDeadline, + form.revealDeadline, + form.bidderLimit, + ]), + }) +} diff --git a/web/src/features/auction/auctionLifecycle.ts b/web/src/features/auction/auctionLifecycle.ts new file mode 100644 index 0000000..0b387da --- /dev/null +++ b/web/src/features/auction/auctionLifecycle.ts @@ -0,0 +1,131 @@ +export type AuctionPhase = + | 'BiddingOpen' + | 'RevealOpen' + | 'ReadyToSettle' + | 'SettledSold' + | 'SettledNoSale' + | 'ClaimsComplete' + +export type SettlementStatus = 'sold' | 'no_sale' + +export type AuctionPhaseInput = Readonly<{ + now: bigint + biddingDeadline: bigint + revealDeadline: bigint + settlement?: SettlementStatus + claimsComplete?: boolean +}> + +export type AcceptedBid = Readonly<{ + acceptedIndex: number + commitment: bigint + amount: bigint | null +}> + +export type BidderClaim = Readonly<{ + acceptedIndex: number + kind: 'loser_refund' | 'winner_surplus' + amount: bigint +}> + +export type VickreySettlement = Readonly<{ + sold: boolean + winnerIndex: number | null + winnerCommitment: bigint | null + clearingPrice: bigint + bidderClaims: readonly BidderClaim[] + sellerEntitlement: bigint + winnerClaimAutoConsumed: boolean + lockedCollateral: bigint + distributedValue: bigint +}> + +export function deriveAuctionPhase(input: AuctionPhaseInput): AuctionPhase { + if (input.now < 0n || input.biddingDeadline < 0n || input.revealDeadline < 0n) { + throw new Error('Auction timestamps must be unsigned') + } + if (input.biddingDeadline >= input.revealDeadline) { + throw new Error('Bidding deadline must precede reveal deadline') + } + if (input.claimsComplete && !input.settlement) { + throw new Error('Claims cannot complete before settlement') + } + if (input.claimsComplete) return 'ClaimsComplete' + if (input.settlement === 'sold') return 'SettledSold' + if (input.settlement === 'no_sale') return 'SettledNoSale' + if (input.now < input.biddingDeadline) return 'BiddingOpen' + if (input.now < input.revealDeadline) return 'RevealOpen' + return 'ReadyToSettle' +} + +export function settleVickrey( + input: Readonly<{ reserve: bigint; cap: bigint; bids: readonly AcceptedBid[] }>, +): VickreySettlement { + if (input.reserve <= 0n || input.cap <= 0n || input.reserve > input.cap) { + throw new Error('Settlement requires 0 < reserve <= cap') + } + + const indices = new Set() + const commitments = new Set() + for (const bid of input.bids) { + if (!Number.isInteger(bid.acceptedIndex) || bid.acceptedIndex < 0 || indices.has(bid.acceptedIndex)) { + throw new Error('Accepted bid indices must be unique non-negative integers') + } + if (bid.commitment <= 0n || commitments.has(bid.commitment)) { + throw new Error('Commitments must be unique and non-zero') + } + if (bid.amount !== null && (bid.amount <= 0n || bid.amount > input.cap)) { + throw new Error('Revealed amount must be between 1 and cap') + } + indices.add(bid.acceptedIndex) + commitments.add(bid.commitment) + } + + const revealed = input.bids + .filter((bid): bid is AcceptedBid & Readonly<{ amount: bigint }> => bid.amount !== null) + .sort((left, right) => { + if (left.amount === right.amount) return left.acceptedIndex - right.acceptedIndex + return left.amount > right.amount ? -1 : 1 + }) + + const highest = revealed[0] + const sold = highest !== undefined && highest.amount >= input.reserve + const secondHighest = revealed[1]?.amount ?? 0n + const clearingPrice = sold ? (secondHighest > input.reserve ? secondHighest : input.reserve) : 0n + const winnerIndex = sold ? highest.acceptedIndex : null + const winnerCommitment = sold ? highest.commitment : null + + const bidderClaims: BidderClaim[] = [] + let winnerClaimAutoConsumed = false + for (const bid of [...input.bids].sort((left, right) => left.acceptedIndex - right.acceptedIndex)) { + if (sold && bid.acceptedIndex === winnerIndex) { + const surplus = input.cap - clearingPrice + if (surplus > 0n) { + bidderClaims.push({ acceptedIndex: bid.acceptedIndex, kind: 'winner_surplus', amount: surplus }) + } else { + winnerClaimAutoConsumed = true + } + } else { + bidderClaims.push({ acceptedIndex: bid.acceptedIndex, kind: 'loser_refund', amount: input.cap }) + } + } + + const sellerEntitlement = clearingPrice + const lockedCollateral = input.cap * BigInt(input.bids.length) + const distributedValue = bidderClaims.reduce((total, claim) => total + claim.amount, sellerEntitlement) + if (distributedValue !== lockedCollateral) { + throw new Error('Settlement does not conserve collateral') + } + + return Object.freeze({ + sold, + winnerIndex, + winnerCommitment, + clearingPrice, + bidderClaims: Object.freeze(bidderClaims), + sellerEntitlement, + winnerClaimAutoConsumed, + lockedCollateral, + distributedValue, + }) +} diff --git a/web/src/features/auction/auctionLiveViewModel.ts b/web/src/features/auction/auctionLiveViewModel.ts new file mode 100644 index 0000000..7abec44 --- /dev/null +++ b/web/src/features/auction/auctionLiveViewModel.ts @@ -0,0 +1,51 @@ +import type { DeploymentManifest } from '@/config/deployment' +import type { readAuctionSnapshot } from '@/features/auction/auctionReader' +import type { AuctionLiveViewModel } from '@/features/auction/ui/AuctionLivePage' + +type AuctionSnapshot = Awaited> + +function hex(value: bigint): `0x${string}` { + return `0x${value.toString(16)}` +} + +export function toAuctionLiveViewModel(manifest: DeploymentManifest, snapshot: AuctionSnapshot): AuctionLiveViewModel { + return { + network: manifest.network, + chainId: manifest.chainId, + rpcUrl: manifest.rpcUrl, + auctionHouse: manifest.auctionHouse, + auctionHouseClassHash: manifest.auctionHouseClassHash, + strk20Pool: manifest.strk20Pool, + paymentToken: manifest.paymentToken, + auctionId: snapshot.config.auctionId.toString(), + seller: snapshot.config.seller, + sellerClaimHandle: hex(snapshot.config.sellerClaimHandle), + nftContract: snapshot.config.nftContract, + tokenId: snapshot.config.tokenId.toString(), + reservePrice: snapshot.config.reservePrice.toString(), + cap: snapshot.config.cap.toString(), + biddingDeadline: snapshot.config.biddingDeadline.toString(), + revealDeadline: snapshot.config.revealDeadline.toString(), + bidderLimit: snapshot.config.bidderLimit, + nftOwner: snapshot.nftOwner, + custodyValid: snapshot.custodyValid, + state: { + settled: snapshot.state.settled, + sold: snapshot.state.sold, + winnerIndex: snapshot.state.winnerIndex, + winnerCommitment: hex(snapshot.state.winnerCommitment), + winnerRecipient: snapshot.state.winnerRecipient, + clearingPrice: snapshot.state.clearingPrice.toString(), + sellerEntitlement: snapshot.state.sellerEntitlement.toString(), + sellerAuthorizedNote: hex(snapshot.state.sellerAuthorizedNote), + sellerClaimConsumed: snapshot.state.sellerClaimConsumed, + }, + bids: snapshot.bids.map((bid) => ({ + commitment: hex(bid.commitment), + claimHandle: hex(bid.claimHandle), + revealed: bid.revealed, + amount: bid.amount.toString(), + assetRecipient: bid.assetRecipient, + })), + } +} diff --git a/web/src/features/auction/auctionMath.ts b/web/src/features/auction/auctionMath.ts index b0edb03..0311890 100644 --- a/web/src/features/auction/auctionMath.ts +++ b/web/src/features/auction/auctionMath.ts @@ -35,3 +35,25 @@ export function parseTokenAmount(input: string, decimals: number, options: Parse return amount } + +export function formatTokenAmount(amount: bigint, decimals: number): string { + if (!Number.isInteger(decimals) || decimals < 0 || decimals > 255) { + throw new Error('Token decimals must be an integer from 0 to 255') + } + if (amount < 0n) throw new Error('Token amount must be non-negative') + if (decimals === 0) return amount.toString() + const scale = 10n ** BigInt(decimals) + const whole = amount / scale + const fraction = (amount % scale).toString().padStart(decimals, '0').replace(/0+$/, '') + return fraction.length === 0 ? whole.toString() : `${whole}.${fraction}` +} + +export function formatUnixTimestamp(timestamp: bigint): string { + if (timestamp < 0n || timestamp > BigInt(Number.MAX_SAFE_INTEGER)) { + throw new Error('Unix timestamp is outside the safe range') + } + const date = new Date(Number(timestamp) * 1_000) + if (!Number.isFinite(date.getTime())) throw new Error('Unix timestamp cannot be represented') + const pad = (value: number) => value.toString().padStart(2, '0') + return `${date.getUTCFullYear()}-${pad(date.getUTCMonth() + 1)}-${pad(date.getUTCDate())} ${pad(date.getUTCHours())}:${pad(date.getUTCMinutes())}:${pad(date.getUTCSeconds())} UTC` +} diff --git a/web/src/features/auction/auctionReader.ts b/web/src/features/auction/auctionReader.ts new file mode 100644 index 0000000..7287fa3 --- /dev/null +++ b/web/src/features/auction/auctionReader.ts @@ -0,0 +1,206 @@ +import type { DeploymentManifest } from '@/config/deployment' + +export type ChainCall = Readonly<{ + contractAddress: string + entrypoint: string + calldata?: readonly string[] +}> + +export type ChainReader = Readonly<{ + callContract: (call: ChainCall) => Promise> + getClassHashAt: (contractAddress: string) => Promise +}> + +export type AuctionConfigSnapshot = Readonly<{ + auctionId: bigint + seller: `0x${string}` + sellerClaimHandle: bigint + nftContract: `0x${string}` + tokenId: bigint + reservePrice: bigint + cap: bigint + biddingDeadline: bigint + revealDeadline: bigint + bidderLimit: number +}> + +export type AuctionStateSnapshot = Readonly<{ + settled: boolean + sold: boolean + winnerIndex: number + winnerCommitment: bigint + winnerRecipient: `0x${string}` + clearingPrice: bigint + sellerEntitlement: bigint + sellerAuthorizedNote: bigint + sellerClaimConsumed: boolean +}> + +export type BidSnapshot = Readonly<{ + commitment: bigint + claimHandle: bigint + revealed: boolean + amount: bigint + assetRecipient: `0x${string}` +}> + +const MAX_SUPPORTED_BIDDERS = 32 + +function normalizeHex(value: string, label: string): `0x${string}` { + if (!/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} is not hexadecimal`) + return `0x${BigInt(value).toString(16)}` +} + +function felt(value: string, label: string): bigint { + if (!/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} is not a felt`) + return BigInt(value) +} + +function boundedNumber(value: string, label: string, maximum = Number.MAX_SAFE_INTEGER): number { + const parsed = felt(value, label) + if (parsed < 0n || parsed > BigInt(maximum)) throw new Error(`${label} is outside its bounded maximum`) + return Number(parsed) +} + +function bool(value: string, label: string): boolean { + const parsed = felt(value, label) + if (parsed !== 0n && parsed !== 1n) throw new Error(`${label} is not a Cairo bool`) + return parsed === 1n +} + +function expectLength(result: readonly string[], length: number, label: string): void { + if (result.length !== length) throw new Error(`${label} returned ${result.length} felts; expected ${length}`) +} + +function resultOf(value: readonly string[] | Readonly<{ result: readonly string[] }>): readonly string[] { + return 'result' in value ? value.result : value +} + +async function call(reader: ChainReader, request: ChainCall): Promise { + return resultOf(await reader.callContract(request)) +} + +function encode(value: bigint | number): string { + return `0x${BigInt(value).toString(16)}` +} + +function sameFelt(left: string, right: string): boolean { + return BigInt(left) === BigInt(right) +} + +export async function readAndValidateDeployment(reader: ChainReader, manifest: DeploymentManifest) { + const [classHashRaw, houseResult] = await Promise.all([ + reader.getClassHashAt(manifest.auctionHouse), + call(reader, { contractAddress: manifest.auctionHouse, entrypoint: 'get_house_config' }), + ]) + const classHash = normalizeHex(classHashRaw, 'Auction house class hash') + if (!sameFelt(classHash, manifest.auctionHouseClassHash)) + throw new Error('Auction house class hash does not match manifest') + expectLength(houseResult, 3, 'get_house_config') + const pool = normalizeHex(houseResult[0], 'Configured pool') + const paymentToken = normalizeHex(houseResult[1], 'Configured payment token') + const maxBidders = boundedNumber(houseResult[2], 'Configured bidder bound', MAX_SUPPORTED_BIDDERS) + if (maxBidders === 0) throw new Error('Configured bidder bound is zero') + if (!sameFelt(pool, manifest.strk20Pool)) throw new Error('Configured STRK20 pool does not match manifest') + if (!sameFelt(paymentToken, manifest.paymentToken)) + throw new Error('Configured payment token does not match manifest') + + return Object.freeze({ + pool: manifest.strk20Pool, + paymentToken: manifest.paymentToken, + maxBidders, + classHash: manifest.auctionHouseClassHash, + }) +} + +function parseAuctionConfig(result: readonly string[]): AuctionConfigSnapshot { + expectLength(result, 11, 'get_auction_config') + return Object.freeze({ + auctionId: felt(result[0], 'auction_id'), + seller: normalizeHex(result[1], 'seller'), + sellerClaimHandle: felt(result[2], 'seller_claim_handle'), + nftContract: normalizeHex(result[3], 'nft_contract'), + tokenId: felt(result[4], 'token_id.low') + (felt(result[5], 'token_id.high') << 128n), + reservePrice: felt(result[6], 'reserve_price'), + cap: felt(result[7], 'cap'), + biddingDeadline: felt(result[8], 'bidding_deadline'), + revealDeadline: felt(result[9], 'reveal_deadline'), + bidderLimit: boundedNumber(result[10], 'bidder_limit', MAX_SUPPORTED_BIDDERS), + }) +} + +function parseAuctionState(result: readonly string[]): AuctionStateSnapshot { + expectLength(result, 9, 'get_auction_state') + return Object.freeze({ + settled: bool(result[0], 'settled'), + sold: bool(result[1], 'sold'), + winnerIndex: boundedNumber(result[2], 'winner_index', MAX_SUPPORTED_BIDDERS - 1), + winnerCommitment: felt(result[3], 'winner_commitment'), + winnerRecipient: normalizeHex(result[4], 'winner_recipient'), + clearingPrice: felt(result[5], 'clearing_price'), + sellerEntitlement: felt(result[6], 'seller_entitlement'), + sellerAuthorizedNote: felt(result[7], 'seller_authorized_note'), + sellerClaimConsumed: bool(result[8], 'seller_claim_consumed'), + }) +} + +function parseBid(result: readonly string[]): BidSnapshot { + expectLength(result, 5, 'get_bid') + return Object.freeze({ + commitment: felt(result[0], 'commitment'), + claimHandle: felt(result[1], 'claim_handle'), + revealed: bool(result[2], 'revealed'), + amount: felt(result[3], 'amount'), + assetRecipient: normalizeHex(result[4], 'asset_recipient'), + }) +} + +export async function readAuctionSnapshot(reader: ChainReader, manifest: DeploymentManifest, auctionId: bigint) { + if (auctionId <= 0n) throw new Error('Auction ID must be positive') + const deployment = await readAndValidateDeployment(reader, manifest) + const auctionCalldata = [encode(auctionId)] + const [configResult, stateResult, countResult] = await Promise.all([ + call(reader, { + contractAddress: manifest.auctionHouse, + entrypoint: 'get_auction_config', + calldata: auctionCalldata, + }), + call(reader, { + contractAddress: manifest.auctionHouse, + entrypoint: 'get_auction_state', + calldata: auctionCalldata, + }), + call(reader, { + contractAddress: manifest.auctionHouse, + entrypoint: 'get_bid_count', + calldata: auctionCalldata, + }), + ]) + const config = parseAuctionConfig(configResult) + const state = parseAuctionState(stateResult) + expectLength(countResult, 1, 'get_bid_count') + const bidCount = boundedNumber(countResult[0], 'Bid count', deployment.maxBidders) + if (bidCount > config.bidderLimit) throw new Error('Bid count exceeds immutable auction bidder limit') + + const bids = await Promise.all( + Array.from({ length: bidCount }, (_, acceptedIndex) => + call(reader, { + contractAddress: manifest.auctionHouse, + entrypoint: 'get_bid', + calldata: [encode(auctionId), encode(acceptedIndex)], + }).then(parseBid), + ), + ) + const ownerResult = await call(reader, { + contractAddress: config.nftContract, + entrypoint: 'owner_of', + calldata: [encode(config.tokenId & ((1n << 128n) - 1n)), encode(config.tokenId >> 128n)], + }) + expectLength(ownerResult, 1, 'owner_of') + const nftOwner = normalizeHex(ownerResult[0], 'NFT owner') + const expectedOwner = !state.settled ? manifest.auctionHouse : state.sold ? state.winnerRecipient : config.seller + const custodyValid = sameFelt(nftOwner, expectedOwner) + if (!custodyValid) throw new Error('NFT custody does not match auction lifecycle state') + + return Object.freeze({ config, state, bids: Object.freeze(bids), nftOwner, custodyValid }) +} diff --git a/web/src/features/auction/auctionRoute.ts b/web/src/features/auction/auctionRoute.ts new file mode 100644 index 0000000..80c683b --- /dev/null +++ b/web/src/features/auction/auctionRoute.ts @@ -0,0 +1,38 @@ +const MAX_U64 = (1n << 64n) - 1n +const INVALID_ID_ERROR = 'Auction ID must be a positive u64 decimal value.' + +export type AuctionRouteResult = + | Readonly<{ ok: true; auctionId: bigint; canonicalId: string }> + | Readonly<{ ok: false; displayId: string; error: string }> + +function invalid(values: readonly string[], error: string = INVALID_ID_ERROR): AuctionRouteResult { + return { + ok: false, + displayId: (values[0] ?? '').slice(0, 80), + error, + } +} + +export function parseAuctionIdValues(values: readonly string[]): AuctionRouteResult { + if (values.length !== 1) { + return invalid(values, 'Auction URL must contain exactly one auction ID.') + } + + let decoded: string + try { + decoded = decodeURIComponent(values[0]) + } catch { + return invalid(values) + } + + if (!/^[1-9][0-9]{0,19}$/.test(decoded)) return invalid(values) + + const auctionId = BigInt(decoded) + if (auctionId > MAX_U64) return invalid(values) + + return { ok: true, auctionId, canonicalId: auctionId.toString() } +} + +export function buildAuctionHref(value: string): string { + return `/auction?id=${encodeURIComponent(value)}` +} diff --git a/web/src/features/auction/commitment.ts b/web/src/features/auction/commitment.ts index 3e4da95..fe17d19 100644 --- a/web/src/features/auction/commitment.ts +++ b/web/src/features/auction/commitment.ts @@ -1,17 +1,18 @@ import { hash, shortString } from 'starknet' -const STARK_FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001n -const MAX_U64 = (1n << 64n) - 1n -const MAX_U128 = (1n << 128n) - 1n -const CLAIM_DOMAIN = BigInt(shortString.encodeShortString('CIPHERBID_CLAIM_V1')) -const BID_DOMAIN = BigInt(shortString.encodeShortString('CIPHERBID_BID_V1')) +export const STARK_FIELD_PRIME = 0x800000000000011000000000000000000000000000000000000000000000001n +export const CONTRACT_ADDRESS_BOUND = 1n << 251n +export const MAX_U64 = (1n << 64n) - 1n +export const MAX_U128 = (1n << 128n) - 1n +export const CLAIM_DOMAIN = BigInt(shortString.encodeShortString('CIPHERBID_CLAIM_V1')) +export const BID_DOMAIN = BigInt(shortString.encodeShortString('CIPHERBID_BID_V1')) export type BidCommitmentInput = Readonly<{ chainId: bigint auctionHouse: bigint auctionId: bigint amount: bigint - bidSecret: bigint + bidNonce: bigint claimHandle: bigint assetRecipient: bigint }> @@ -22,6 +23,12 @@ function assertFelt(name: string, value: bigint): void { } } +function assertContractAddress(name: string, value: bigint): void { + if (value <= 0n || value >= CONTRACT_ADDRESS_BOUND) { + throw new Error(`${name} must be a non-zero Starknet contract address`) + } +} + export function computeClaimHandle(claimSecret: bigint): bigint { assertFelt('claimSecret', claimSecret) if (claimSecret === 0n) throw new Error('Claim secret must be non-zero') @@ -30,18 +37,16 @@ export function computeClaimHandle(claimSecret: bigint): bigint { export function computeBidCommitment(input: BidCommitmentInput): bigint { assertFelt('chainId', input.chainId) - assertFelt('auctionHouse', input.auctionHouse) - assertFelt('bidSecret', input.bidSecret) + assertContractAddress('auctionHouse', input.auctionHouse) + assertFelt('bidNonce', input.bidNonce) assertFelt('claimHandle', input.claimHandle) - assertFelt('assetRecipient', input.assetRecipient) + assertContractAddress('assetRecipient', input.assetRecipient) if (input.chainId === 0n) throw new Error('Chain ID must be non-zero') - if (input.auctionHouse === 0n) throw new Error('Auction house must be non-zero') - if (input.auctionId < 0n || input.auctionId > MAX_U64) throw new Error('Auction ID must fit u64') + if (input.auctionId <= 0n || input.auctionId > MAX_U64) throw new Error('Auction ID must be between 1 and u64 max') if (input.amount <= 0n || input.amount > MAX_U128) throw new Error('Bid amount must be between 1 and u128 max') - if (input.bidSecret === 0n) throw new Error('Bid secret must be non-zero') + if (input.bidNonce === 0n) throw new Error('Bid nonce must be non-zero') if (input.claimHandle === 0n) throw new Error('Claim handle must be non-zero') - if (input.assetRecipient === 0n) throw new Error('Asset recipient must be non-zero') return BigInt( hash.computePoseidonHashOnElements([ @@ -50,7 +55,7 @@ export function computeBidCommitment(input: BidCommitmentInput): bigint { input.auctionHouse, input.auctionId, input.amount, - input.bidSecret, + input.bidNonce, input.claimHandle, input.assetRecipient, ]), diff --git a/web/src/features/auction/demoBidderReadiness.ts b/web/src/features/auction/demoBidderReadiness.ts new file mode 100644 index 0000000..46315f9 --- /dev/null +++ b/web/src/features/auction/demoBidderReadiness.ts @@ -0,0 +1,121 @@ +const MINIMUM_PUBLIC_DEPOSIT = 15n * 10n ** 18n +const NOTE_MATURITY_BLOCKS = 10 + +type Hex = `0x${string}` + +export type PublicDeposit = Readonly<{ + amount: bigint + blockNumber: number + transactionHash: Hex +}> + +export type DemoBidderObservation = Readonly<{ + name: string + address: Hex + publicKey: Hex + deposits: readonly PublicDeposit[] +}> + +export type DemoBidderStatus = Readonly<{ + name: string + address: Hex + publicKey: Hex + registered: boolean + depositAmount?: bigint + depositBlock?: number + depositTransactionHash?: Hex + confirmations?: number + ready: boolean + blockers: readonly string[] +}> + +function felt(value: Hex, label: string): bigint { + if (!/^0x[0-9a-fA-F]+$/.test(value)) throw new Error(`${label} must be hexadecimal`) + return BigInt(value) +} + +function normalized(value: Hex, label: string): Hex { + const parsed = felt(value, label) + if (parsed <= 0n) throw new Error(`${label} must be non-zero`) + return `0x${parsed.toString(16)}` +} + +export function evaluateDemoBidderReadiness( + input: Readonly<{ + bidders: readonly DemoBidderObservation[] + latestBlock: number + minimumPublicDeposit?: bigint + }>, +): Readonly<{ ready: boolean; statuses: readonly DemoBidderStatus[] }> { + if (!Number.isSafeInteger(input.latestBlock) || input.latestBlock < 0) { + throw new Error('Latest block must be a non-negative safe integer') + } + if (input.bidders.length !== 2) throw new Error('The demo requires exactly two bidder accounts') + const minimumPublicDeposit = input.minimumPublicDeposit ?? MINIMUM_PUBLIC_DEPOSIT + if (minimumPublicDeposit <= 0n) throw new Error('Minimum public deposit must be positive') + const minimumDepositLabel = + minimumPublicDeposit % 10n ** 18n === 0n + ? `${minimumPublicDeposit / 10n ** 18n} STRK` + : `${minimumPublicDeposit} base units of STRK` + + const addresses = input.bidders.map((bidder) => normalized(bidder.address, `${bidder.name} address`)) + if (new Set(addresses).size !== addresses.length) throw new Error('Demo bidder accounts must be distinct') + + const registeredKeys = input.bidders + .map((bidder) => felt(bidder.publicKey, `${bidder.name} public key`)) + .filter((key) => key !== 0n) + .map((key) => key.toString()) + if (new Set(registeredKeys).size !== registeredKeys.length) { + throw new Error('Registered demo bidder viewing public keys must be distinct') + } + + const statuses = input.bidders.map((bidder, index): DemoBidderStatus => { + const publicKey = felt(bidder.publicKey, `${bidder.name} public key`) + const registered = publicKey !== 0n + const qualifyingDeposits = bidder.deposits + .map((deposit) => { + if (deposit.amount < 0n) throw new Error(`${bidder.name} deposit amount cannot be negative`) + if ( + !Number.isSafeInteger(deposit.blockNumber) || + deposit.blockNumber < 0 || + deposit.blockNumber > input.latestBlock + ) { + throw new Error(`${bidder.name} deposit block is invalid`) + } + felt(deposit.transactionHash, `${bidder.name} deposit transaction hash`) + return deposit + }) + .filter((deposit) => deposit.amount >= minimumPublicDeposit) + .sort((left, right) => left.blockNumber - right.blockNumber) + const deposit = qualifyingDeposits[0] + const confirmations = deposit ? input.latestBlock - deposit.blockNumber : undefined + const blockers: string[] = [] + + if (!registered) blockers.push('STRK20 viewing key is not registered') + if (!deposit) { + blockers.push(`No public STRK deposit of at least ${minimumDepositLabel} was found`) + } else if ((confirmations ?? 0) < NOTE_MATURITY_BLOCKS) { + const remaining = NOTE_MATURITY_BLOCKS - (confirmations ?? 0) + blockers.push(`Qualifying deposit needs ${remaining} more block${remaining === 1 ? '' : 's'} before bidding`) + } + + return Object.freeze({ + name: bidder.name, + address: addresses[index]!, + publicKey: `0x${publicKey.toString(16)}`, + registered, + ...(deposit + ? { + depositAmount: deposit.amount, + depositBlock: deposit.blockNumber, + depositTransactionHash: deposit.transactionHash, + confirmations, + } + : {}), + ready: blockers.length === 0, + blockers: Object.freeze(blockers), + }) + }) + + return Object.freeze({ ready: statuses.every((status) => status.ready), statuses: Object.freeze(statuses) }) +} diff --git a/web/src/features/auction/lifecycleCalls.ts b/web/src/features/auction/lifecycleCalls.ts new file mode 100644 index 0000000..023f1a7 --- /dev/null +++ b/web/src/features/auction/lifecycleCalls.ts @@ -0,0 +1,54 @@ +import { num, type Call } from 'starknet' +import type { HexAddress } from '@/features/privacy/strk20Actions' + +export type RevealBidCallInput = Readonly<{ + auctionId: bigint + acceptedIndex: bigint + amount: bigint + bidNonce: bigint + assetRecipient: HexAddress + auctionHouse: HexAddress +}> + +export type AuctionCallInput = Readonly<{ + auctionId: bigint + auctionHouse: HexAddress +}> + +export type SellerProceedsAuthorizationCallInput = AuctionCallInput & + Readonly<{ + claimHandle: bigint + openNoteId: bigint + }> + +const felt = (value: bigint) => num.toHex(value) + +export function buildRevealBidCall(input: RevealBidCallInput): Call { + return { + contractAddress: input.auctionHouse, + entrypoint: 'reveal_bid', + calldata: [ + felt(input.auctionId), + felt(input.acceptedIndex), + felt(input.amount), + felt(input.bidNonce), + input.assetRecipient, + ], + } +} + +export function buildSettleAuctionCall(input: AuctionCallInput): Call { + return { + contractAddress: input.auctionHouse, + entrypoint: 'settle_auction', + calldata: [felt(input.auctionId)], + } +} + +export function buildAuthorizeSellerProceedsCall(input: SellerProceedsAuthorizationCallInput): Call { + return { + contractAddress: input.auctionHouse, + entrypoint: 'authorize_seller_proceeds', + calldata: [felt(input.auctionId), felt(input.claimHandle), felt(input.openNoteId)], + } +} diff --git a/web/src/features/auction/ui/AtomicDeliveryReceipt.tsx b/web/src/features/auction/ui/AtomicDeliveryReceipt.tsx new file mode 100644 index 0000000..9937e5e --- /dev/null +++ b/web/src/features/auction/ui/AtomicDeliveryReceipt.tsx @@ -0,0 +1,107 @@ +import { formatTokenAmount } from '@/features/auction/auctionMath' + +export type VerifiedTransactionReceipt = Readonly<{ + label: string + transactionHash: string + finalityStatus: 'ACCEPTED_ON_L2' | 'ACCEPTED_ON_L1' + blockNumber: number +}> + +export type AtomicSettlementModel = Readonly<{ + network: 'sepolia' | 'mainnet' + settled: boolean + sold: boolean + auctionId: string + nftContract: string + tokenId: string + nftOwner: string + winnerRecipient: string + clearingPrice: string + sellerEntitlement: string + custodyValid: boolean +}> + +export function AtomicDeliveryReceipt({ + settlement, + receipts, +}: Readonly<{ settlement: AtomicSettlementModel; receipts: readonly VerifiedTransactionReceipt[] }>) { + const explorer = settlement.network === 'mainnet' ? 'https://starkscan.co' : 'https://sepolia.starkscan.co' + const deliveryVerified = + settlement.settled && + settlement.sold && + settlement.custodyValid && + BigInt(settlement.nftOwner) === BigInt(settlement.winnerRecipient) + const strk = (value: string) => `${formatTokenAmount(BigInt(value), 18)} STRK` + + return ( +
+

Public execution truth

+

+ Atomic Delivery Receipt +

+

+ {deliveryVerified + ? 'Delivery verified' + : settlement.settled + ? 'No-sale NFT return verified' + : 'Settlement pending'} +

+ +
+
+
Auction
+
#{settlement.auctionId}
+
+
+
NFT
+
+ {settlement.nftContract} / {settlement.tokenId} +
+
+
+
NFT owner
+
{settlement.nftOwner}
+
+
+
Clearing price
+
+ {settlement.settled && settlement.sold ? strk(settlement.clearingPrice) : '—'} +
+
+
+
Seller allocation
+
+ {settlement.settled && settlement.sold ? `Seller receives ${strk(settlement.sellerEntitlement)}` : '—'} +
+
+
+ +

Verified transaction receipts

+ {receipts.length === 0 ? ( +

No verified transaction receipts yet.

+ ) : ( + + )} +
+ ) +} diff --git a/web/src/features/auction/ui/AuctionActions.tsx b/web/src/features/auction/ui/AuctionActions.tsx new file mode 100644 index 0000000..d947703 --- /dev/null +++ b/web/src/features/auction/ui/AuctionActions.tsx @@ -0,0 +1,456 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import { RpcProvider } from 'starknet' +import type { PrivacyWalletConnection } from '@/features/wallet/walletConnection' +import { useWalletStore } from '@/features/wallet/walletStore' +import type { AuctionLiveViewModel } from '@/features/auction/ui/AuctionLivePage' +import { formatTokenAmount, parseTokenAmount } from '@/features/auction/auctionMath' +import { AtomicDeliveryReceipt, type VerifiedTransactionReceipt } from '@/features/auction/ui/AtomicDeliveryReceipt' +import type { DeploymentManifest } from '@/config/deployment' +import { readAuctionSnapshot, type ChainReader } from '@/features/auction/auctionReader' +import { + bindAcceptedIndex, + generateBidderCredential, + type BidderCredential, + type SellerCredential, +} from '@/features/credentials/credentials' +import { decryptRecoveryBundle } from '@/features/credentials/recoveryBundle' +import { verifyTransactionTransition, type CipherBidEventName } from '@/features/transactions/receiptVerifier' +import { TransactionOrchestrator } from '@/features/transactions/transactionOrchestrator' +import { + extractResolvedSellerOpenNoteId, + runPrivateBidFlow, + runPrivateClaimFlow, + runRevealFlow, + runSellerClaimFlow, + runSettlementFlow, + type PrivacyWallet, +} from '@/features/transactions/auctionTransactionFlows' + +export type AuctionActionsProps = Readonly<{ + model: AuctionLiveViewModel + connection: PrivacyWalletConnection | null + onRefresh?: () => void +}> + +function isHex(value: string): value is `0x${string}` { + return /^0x[0-9a-fA-F]+$/.test(value) +} + +function phase(model: AuctionLiveViewModel): 'bidding' | 'reveal' | 'settle' | 'settled' { + if (model.state.settled) return 'settled' + const now = BigInt(Math.floor(Date.now() / 1000)) + if (now < BigInt(model.biddingDeadline)) return 'bidding' + if (now < BigInt(model.revealDeadline)) return 'reveal' + return 'settle' +} + +function publicManifest(model: AuctionLiveViewModel): DeploymentManifest { + if ( + !isHex(model.auctionHouse) || + !isHex(model.auctionHouseClassHash) || + !isHex(model.strk20Pool) || + !isHex(model.paymentToken) + ) { + throw new Error('Live deployment data is malformed') + } + return { + network: model.network, + chainId: model.chainId, + rpcUrl: model.rpcUrl, + auctionHouse: model.auctionHouse, + auctionHouseClassHash: model.auctionHouseClassHash, + strk20Pool: model.strk20Pool, + paymentToken: model.paymentToken, + } +} + +function download(serialized: string, bundleId: string, auctionId: string): void { + const blob = new Blob([serialized], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = url + anchor.download = `cipherbid-auction-${auctionId}-${bundleId.slice(2, 10)}.recovery.json` + anchor.click() + URL.revokeObjectURL(url) +} + +export function AuctionActions({ model, connection, onRefresh }: AuctionActionsProps) { + const refresh = onRefresh ?? (() => window.location.reload()) + const deployment = useMemo(() => publicManifest(model), [model]) + const provider = useMemo(() => new RpcProvider({ nodeUrl: deployment.rpcUrl }), [deployment.rpcUrl]) + const reader = useMemo( + () => ({ + callContract: (call) => provider.callContract({ ...call, calldata: call.calldata ? [...call.calldata] : [] }), + getClassHashAt: (address) => provider.getClassHashAt(address), + }), + [provider], + ) + const orchestrator = useMemo(() => new TransactionOrchestrator(), []) + const [bidAmount, setBidAmount] = useState('') + const [recipientOverrides, setRecipientOverrides] = useState>({}) + const [password, setPassword] = useState('') + const [bidderCredential, setBidderCredential] = useState(null) + const [sellerCredential, setSellerCredential] = useState(null) + const [receipts, setReceipts] = useState([]) + const [status, setStatus] = useState('') + const currentPhase = phase(model) + const enabled = connection !== null && connection.supportsStrk20 + + useEffect(() => orchestrator.subscribe((state) => setStatus(state.status.replaceAll('_', ' '))), [orchestrator]) + + const recipient = connection ? (recipientOverrides[connection.address] ?? connection.address) : '' + + function walletSnapshot() { + const state = useWalletStore.getState() + if (!state.address || !state.chainId) throw new Error('Wallet disconnected') + return Promise.resolve({ address: state.address, chainId: state.chainId }) + } + + async function snapshot() { + return readAuctionSnapshot(reader, deployment, BigInt(model.auctionId)) + } + + async function verify( + transactionHash: string, + expectedEvent: CipherBidEventName, + requirePoolTouch: boolean, + predicate: (value: Awaited>) => boolean, + ) { + return verifyTransactionTransition({ + provider, + manifest: deployment, + transactionHash, + expectedEvent, + requirePoolTouch, + readState: async () => predicate(await snapshot()), + }) + } + + function recordReceipt(label: string, evidence: Awaited>) { + setReceipts((current) => [ + ...current, + { + label, + transactionHash: evidence.transactionHash, + finalityStatus: evidence.finalityStatus, + blockNumber: evidence.blockNumber, + }, + ]) + } + + async function recoveryReady(bundle: Readonly<{ serialized: string; bundleId: `0x${string}` }>) { + download(bundle.serialized, bundle.bundleId, model.auctionId) + if (!window.confirm('Confirm that the encrypted recovery file downloaded and can be stored safely.')) { + throw new Error('Recovery export was not confirmed') + } + } + + async function submitBid() { + if (!connection || !isHex(recipient)) return + try { + const credential = generateBidderCredential({ + network: model.network, + chainId: BigInt(model.chainId), + auctionHouse: BigInt(model.auctionHouse), + auctionId: BigInt(model.auctionId), + amount: (() => { + const amount = parseTokenAmount(bidAmount, 18, { max: BigInt(model.cap) }) + if (amount === 0n) throw new Error('Bid amount must be positive') + return amount + })(), + assetRecipient: BigInt(recipient), + }) + let acceptedIndex = -1 + const result = await runPrivateBidFlow({ + orchestrator, + wallet: connection.account as PrivacyWallet, + expectedWallet: { address: connection.address, chainId: connection.chainId }, + getWalletSnapshot: walletSnapshot, + credential, + recoveryPassword: password, + onRecoveryReady: recoveryReady, + paymentToken: deployment.paymentToken, + cap: BigInt(model.cap), + verify: (hash) => + verify(hash, 'BidCommitted', true, (fresh) => { + acceptedIndex = fresh.bids.findIndex((bid) => bid.commitment === credential.commitment) + return acceptedIndex >= 0 + }), + }) + recordReceipt('Private bid', result.evidence) + setBidderCredential(bindAcceptedIndex(credential, acceptedIndex)) + setStatus('private bid confirmed') + refresh() + } catch { + setStatus('private bid failed or remains unconfirmed') + } + } + + async function importRecovery(file: File | undefined) { + if (!file) return + try { + const credentials = await decryptRecoveryBundle(await file.text(), password) + const matching = credentials.find( + (credential) => + credential.network === model.network && + credential.chainId === BigInt(model.chainId) && + credential.auctionId === BigInt(model.auctionId) && + credential.auctionHouse === BigInt(model.auctionHouse), + ) + if (!matching) throw new Error('No matching credential') + if (matching.role === 'seller') { + setSellerCredential(matching) + } else { + const index = model.bids.findIndex((bid) => BigInt(bid.commitment) === matching.commitment) + setBidderCredential(index >= 0 ? bindAcceptedIndex(matching, index) : matching) + } + setStatus(`${matching.role} recovery imported`) + } catch { + setStatus('recovery import failed') + } + } + + async function reveal() { + if (!connection || !bidderCredential) return + try { + const evidence = await runRevealFlow({ + orchestrator, + wallet: connection.account as PrivacyWallet, + expectedWallet: { address: connection.address, chainId: connection.chainId }, + getWalletSnapshot: walletSnapshot, + credential: bidderCredential, + verify: (hash) => + verify(hash, 'BidRevealed', false, (fresh) => { + const index = bidderCredential.acceptedIndex + return index !== undefined && fresh.bids[index]?.revealed === true + }), + }) + recordReceipt('Bid reveal', evidence) + setStatus('reveal confirmed') + refresh() + } catch { + setStatus('reveal failed or remains unconfirmed') + } + } + + async function settle() { + if (!connection) return + try { + const evidence = await runSettlementFlow({ + orchestrator, + wallet: connection.account as PrivacyWallet, + expectedWallet: { address: connection.address, chainId: connection.chainId }, + getWalletSnapshot: walletSnapshot, + auctionHouse: deployment.auctionHouse, + auctionId: BigInt(model.auctionId), + verify: (hash) => verify(hash, 'AuctionSettled', false, (fresh) => fresh.state.settled && fresh.custodyValid), + }) + recordReceipt('Settlement', evidence) + setStatus('settlement confirmed') + refresh() + } catch { + setStatus('settlement failed or remains unconfirmed') + } + } + + async function bidderClaim() { + if (!connection || !bidderCredential || bidderCredential.acceptedIndex === undefined) return + const winner = model.state.sold && bidderCredential.acceptedIndex === model.state.winnerIndex + try { + const evidence = await runPrivateClaimFlow({ + orchestrator, + wallet: connection.account as PrivacyWallet, + expectedWallet: { address: connection.address, chainId: connection.chainId }, + getWalletSnapshot: walletSnapshot, + kind: winner ? 'winner_surplus' : 'loser_refund', + credential: bidderCredential, + paymentToken: deployment.paymentToken, + recipient: connection.address, + verify: (hash) => + verify( + hash, + winner ? 'WinnerSurplusClaimed' : 'LoserRefundClaimed', + true, + (asyncSnapshot) => asyncSnapshot.state.settled, + ), + }) + recordReceipt(winner ? 'Winner surplus' : 'Loser refund', evidence) + setStatus('private bidder claim confirmed') + refresh() + } catch { + setStatus('private bidder claim failed or remains unconfirmed') + } + } + + async function sellerClaim() { + if (!connection || !sellerCredential) return + try { + const result = await runSellerClaimFlow({ + orchestrator, + wallet: connection.account as Required, + expectedWallet: { address: connection.address, chainId: connection.chainId }, + getWalletSnapshot: walletSnapshot, + credential: sellerCredential, + paymentToken: deployment.paymentToken, + recipient: connection.address, + extractOpenNoteId: (prepared) => + extractResolvedSellerOpenNoteId(prepared, sellerCredential, deployment.strk20Pool), + verifyAuthorization: (hash) => + verify(hash, 'SellerProceedsAuthorized', false, (fresh) => fresh.state.sellerAuthorizedNote > 0n), + verifyClaim: (hash) => verify(hash, 'SellerProceedsClaimed', true, (fresh) => fresh.state.sellerClaimConsumed), + }) + recordReceipt('Seller authorization', result.authorizationEvidence) + recordReceipt('Seller proceeds', result.claimEvidence) + setStatus('private seller proceeds confirmed') + refresh() + } catch { + setStatus('private seller claim failed or remains unconfirmed') + } + } + + return ( +
+

Wallet actions

+

+ Transact privately +

+ {!enabled ?

Connect a compatible wallet to transact.

: null} + +
+ + setBidAmount(event.target.value)} + className="min-h-12 w-full rounded-xl border border-white/10 bg-white/[0.04] px-4 outline-none focus:border-[#a8b1ff] disabled:opacity-50" + /> +

+ Enter any positive bid up to {formatTokenAmount(BigInt(model.cap), 18)} STRK. Bids below the{' '} + {formatTokenAmount(BigInt(model.reservePrice), 18)} STRK reserve cannot win. +

+ + { + if (!connection) return + setRecipientOverrides((current) => ({ ...current, [connection.address]: event.target.value })) + }} + className="min-h-12 w-full rounded-xl border border-white/10 bg-white/[0.04] px-4 font-mono text-xs outline-none focus:border-[#a8b1ff] disabled:opacity-50" + /> + + setPassword(event.target.value)} + className="min-h-12 w-full rounded-xl border border-white/10 bg-white/[0.04] px-4 outline-none focus:border-[#a8b1ff] disabled:opacity-50" + /> + + void importRecovery(event.target.files?.[0])} + className="block min-h-11 w-full text-xs text-[#9ba3af] file:mr-3 file:min-h-11 file:rounded-lg file:border-0 file:bg-white/10 file:px-4 file:text-white" + /> +
+ +
+ + + + + +
+

+ {status} +

+

+ Recovery files are encrypted locally. CipherBid does not store credentials in localStorage or send them to a + backend. +

+
+ +
+
+ ) +} diff --git a/web/src/features/auction/ui/AuctionBidPreview.tsx b/web/src/features/auction/ui/AuctionBidPreview.tsx index 7a132e6..10977c0 100644 --- a/web/src/features/auction/ui/AuctionBidPreview.tsx +++ b/web/src/features/auction/ui/AuctionBidPreview.tsx @@ -1,6 +1,8 @@ import Link from 'next/link' import { SEPOLIA_STRK20_POOL } from '@/lib/starknet/network' +import { ProtocolConsole } from './ProtocolConsole' import { SecondPriceIllustration } from './SecondPriceIllustration' +import { WalletConnectPanel } from '@/features/wallet/WalletConnectPanel' export type AuctionBidPreviewProps = Readonly<{ auctionId: string @@ -27,70 +29,73 @@ const lifecycle = [ ] as const function PlaceholderValue() { - return
+ return
} export function AuctionBidPreview({ auctionId }: AuctionBidPreviewProps) { return ( -
-
+
+
- + CipherBid -
- - Wallet not connected + +
-
+
- + Design preview - +
-

STRK20-funded Vickrey auction

+

+ STRK20-funded Vickrey auction +

A genuinely sealed NFT auction

-

+

Bidders lock the same public STRK collateral while their actual amounts remain sealed until reveal. The winner pays the second price.

@@ -101,7 +106,7 @@ export function AuctionBidPreview({ auctionId }: AuctionBidPreviewProps) {

NFT lot @@ -127,9 +132,11 @@ export function AuctionBidPreview({ auctionId }: AuctionBidPreviewProps) {

+ + @@ -200,9 +211,11 @@ export function AuctionBidPreview({ auctionId }: AuctionBidPreviewProps) {
-
{label}
+
+ {label} +
))} @@ -212,7 +225,7 @@ export function AuctionBidPreview({ auctionId }: AuctionBidPreviewProps) {

Vickrey lifecycle

@@ -238,7 +251,7 @@ export function AuctionBidPreview({ auctionId }: AuctionBidPreviewProps) {

Privacy boundary

What stays private—and what does not

@@ -267,7 +280,7 @@ export function AuctionBidPreview({ auctionId }: AuctionBidPreviewProps) {
diff --git a/web/src/features/auction/ui/AuctionLivePage.tsx b/web/src/features/auction/ui/AuctionLivePage.tsx new file mode 100644 index 0000000..c1cc9c0 --- /dev/null +++ b/web/src/features/auction/ui/AuctionLivePage.tsx @@ -0,0 +1,296 @@ +'use client' + +import { useMemo, useState } from 'react' +import Link from 'next/link' +import { RpcProvider } from 'starknet' +import { WalletConnectPanel } from '@/features/wallet/WalletConnectPanel' +import type { PrivacyWalletConnection } from '@/features/wallet/walletConnection' +import { AuctionActions } from '@/features/auction/ui/AuctionActions' +import { formatTokenAmount, formatUnixTimestamp } from '@/features/auction/auctionMath' + +export type AuctionLiveBid = Readonly<{ + commitment: string + claimHandle: string + revealed: boolean + amount: string + assetRecipient: string +}> + +export type AuctionLiveViewModel = Readonly<{ + network: 'sepolia' | 'mainnet' + chainId: string + rpcUrl: string + auctionHouse: string + auctionHouseClassHash: string + strk20Pool: string + paymentToken: string + auctionId: string + seller: string + sellerClaimHandle: string + nftContract: string + tokenId: string + reservePrice: string + cap: string + biddingDeadline: string + revealDeadline: string + bidderLimit: number + nftOwner: string + custodyValid: boolean + state: Readonly<{ + settled: boolean + sold: boolean + winnerIndex: number + winnerCommitment: string + winnerRecipient: string + clearingPrice: string + sellerEntitlement: string + sellerAuthorizedNote: string + sellerClaimConsumed: boolean + }> + bids: readonly AuctionLiveBid[] +}> + +type AuctionLivePageProps = + | Readonly<{ model: AuctionLiveViewModel; error?: never; auctionId?: never }> + | Readonly<{ model?: never; error: string; auctionId: string; onRetry?: () => void }> + +function short(value: string): string { + return value.length <= 18 ? value : `${value.slice(0, 10)}…${value.slice(-6)}` +} + +function strk(value: string): string { + return `${formatTokenAmount(BigInt(value), 18)} STRK` +} + +function phase(model: AuctionLiveViewModel): string { + if (model.state.settled) return model.state.sold ? 'Sold' : 'No sale' + const now = BigInt(Math.floor(Date.now() / 1000)) + if (now < BigInt(model.biddingDeadline)) return 'Bidding open' + if (now < BigInt(model.revealDeadline)) return 'Reveal open' + return 'Ready to settle' +} + +export function AuctionLivePage(props: AuctionLivePageProps) { + const [connection, setConnection] = useState(null) + const rpcUrl = props.model?.rpcUrl + const provider = useMemo(() => (rpcUrl ? new RpcProvider({ nodeUrl: rpcUrl }) : undefined), [rpcUrl]) + + if (!props.model) { + return ( +
+
+ + CipherBid + +

+ Auction #{props.auctionId} +

+

Live auction unavailable

+

+ {props.error} +

+ {props.onRetry ? ( + + ) : null} +
+
+ ) + } + + const model = props.model + const status = phase(model) + return ( +
+
+
+ + CipherBid + + + {model.network} · live chain data + +
+
+ +
+
+
+ + {status} + + + {model.bids.length}/{model.bidderLimit} bids + +
+

+ Auction #{model.auctionId} +

+

+ Private equal-cap bidding with deterministic second-price settlement and atomic NFT delivery. +

+
+ +
+
+
+
+
+

ERC-721 lot

+

Token #{model.tokenId}

+
+
+
+
+
Contract
+
+ {short(model.nftContract)} +
+
+
+
Current owner
+
+ {model.nftOwner} +
+
+
+
Delivery
+
+ {model.custodyValid ? 'Custody verified' : 'Custody mismatch'} +
+
+
+
+ +
+ {[ + ['Reserve', strk(model.reservePrice)], + ['Uniform collateral cap', strk(model.cap)], + ['Bid deadline', formatUnixTimestamp(BigInt(model.biddingDeadline))], + ['Reveal deadline', formatUnixTimestamp(BigInt(model.revealDeadline))], + ].map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+ +
+
+

+ Accepted bids +

+

Amounts appear only after successful reveal.

+
+
+ + + + + + + + + + {model.bids.map((bid, index) => { + const winner = model.state.settled && model.state.sold && index === model.state.winnerIndex + return ( + + + + + + ) + })} + +
CommitmentAmountResult
+ {short(bid.commitment)} + {bid.revealed ? strk(bid.amount) : 'Sealed'} + {winner ? 'Winner' : model.state.settled ? 'Refund eligible' : 'Pending'} +
+
+
+
+ + +
+
+
+ ) +} diff --git a/web/src/features/auction/ui/AuctionPageClient.tsx b/web/src/features/auction/ui/AuctionPageClient.tsx new file mode 100644 index 0000000..9eb74be --- /dev/null +++ b/web/src/features/auction/ui/AuctionPageClient.tsx @@ -0,0 +1,79 @@ +'use client' + +import { useEffect, useMemo, useState } from 'react' +import Link from 'next/link' +import { useSearchParams } from 'next/navigation' +import { loadAuctionLiveViewModel } from '@/features/auction/auctionBrowserLoader' +import { parseAuctionIdValues } from '@/features/auction/auctionRoute' +import { AuctionLivePage, type AuctionLiveViewModel } from '@/features/auction/ui/AuctionLivePage' + +export type AuctionModelLoader = (auctionId: bigint) => Promise + +type LoadState = + | Readonly<{ status: 'ready'; requestKey: string; model: AuctionLiveViewModel }> + | Readonly<{ status: 'error'; requestKey: string }> + +export function AuctionPageLoading({ auctionId = '' }: Readonly<{ auctionId?: string }>) { + return ( +
+
+ + CipherBid + +

+ {auctionId ? `Loading auction #${auctionId} from public RPC…` : 'Loading auction from public RPC…'} +

+
+
+ ) +} + +export function AuctionPageClient({ + loadModel = loadAuctionLiveViewModel, +}: Readonly<{ loadModel?: AuctionModelLoader }>) { + const searchParams = useSearchParams() + const query = searchParams.toString() + const route = useMemo(() => parseAuctionIdValues(new URLSearchParams(query).getAll('id')), [query]) + const [retry, setRetry] = useState(0) + const [state, setState] = useState(null) + const requestKey = route.ok ? `${route.canonicalId}:${retry}` : '' + + useEffect(() => { + if (!route.ok) return + + let active = true + const activeRequestKey = requestKey + void loadModel(route.auctionId).then( + (model) => { + if (active) setState({ status: 'ready', requestKey: activeRequestKey, model }) + }, + () => { + if (active) setState({ status: 'error', requestKey: activeRequestKey }) + }, + ) + + return () => { + active = false + } + }, [loadModel, requestKey, route]) + + if (!route.ok) { + return + } + + if (!state || state.requestKey !== requestKey) { + return + } + + if (state.status === 'error') { + return ( + setRetry((attempt) => attempt + 1)} + /> + ) + } + + return +} diff --git a/web/src/features/auction/ui/ProtocolConsole.tsx b/web/src/features/auction/ui/ProtocolConsole.tsx new file mode 100644 index 0000000..5037ad7 --- /dev/null +++ b/web/src/features/auction/ui/ProtocolConsole.tsx @@ -0,0 +1,44 @@ +const protocolRows = [ + ['Auction mode', 'Sealed Vickrey'], + ['Collateral model', 'Uniform cap collateral'], + ['Settlement rule', 'Second-price settlement'], + ['Protocol state', 'Contract deployment pending'], +] as const + +export function ProtocolConsole() { + return ( +
+
+
+

+ Protocol console +

+

+ Protocol state +

+
+ + +
+ +
+ {protocolRows.map(([label, value]) => ( +
+
{label}
+
{value}
+
+ ))} +
+ +

+ This panel describes the intended mechanism only. It does not represent deployed contract or auction state. +

+
+ ) +} diff --git a/web/src/features/auction/ui/SecondPriceIllustration.tsx b/web/src/features/auction/ui/SecondPriceIllustration.tsx index 0550e07..e05c3c8 100644 --- a/web/src/features/auction/ui/SecondPriceIllustration.tsx +++ b/web/src/features/auction/ui/SecondPriceIllustration.tsx @@ -2,7 +2,7 @@ export function SecondPriceIllustration() { return (
@@ -11,12 +11,12 @@ export function SecondPriceIllustration() { Second-price clearing, illustrated

- + Illustration — not chain data
-
+
void +}> + +function download(serialized: string, bundleId: string, auctionId: bigint): void { + const blob = new Blob([serialized], { type: 'application/json' }) + const url = URL.createObjectURL(blob) + const anchor = document.createElement('a') + anchor.href = url + anchor.download = `cipherbid-seller-${auctionId}-${bundleId.slice(2, 10)}.recovery.json` + anchor.click() + URL.revokeObjectURL(url) +} + +function unixTimestamp(value: string, label: string): bigint { + const milliseconds = new Date(value).getTime() + if (!Number.isFinite(milliseconds)) throw new Error(`${label} is invalid`) + return BigInt(Math.floor(milliseconds / 1000)) +} + +export function SellerCreateForm({ deployment, connection, onCreated }: SellerCreateFormProps) { + const provider = useMemo(() => new RpcProvider({ nodeUrl: deployment.rpcUrl }), [deployment.rpcUrl]) + const reader = useMemo( + () => ({ + callContract: (call) => provider.callContract({ ...call, calldata: call.calldata ? [...call.calldata] : [] }), + getClassHashAt: (address) => provider.getClassHashAt(address), + }), + [provider], + ) + const orchestrator = useMemo(() => new TransactionOrchestrator(), []) + const [auctionId, setAuctionId] = useState('') + const [nftContract, setNftContract] = useState('') + const [tokenId, setTokenId] = useState('') + const [reservePrice, setReservePrice] = useState('') + const [cap, setCap] = useState('') + const [biddingDeadline, setBiddingDeadline] = useState('') + const [revealDeadline, setRevealDeadline] = useState('') + const [bidderLimit, setBidderLimit] = useState('2') + const [password, setPassword] = useState('') + const [status, setStatus] = useState('') + const [createdAuctionId, setCreatedAuctionId] = useState(null) + const enabled = connection !== null && connection.supportsStrk20 + + useEffect(() => orchestrator.subscribe((state) => setStatus(state.status.replaceAll('_', ' '))), [orchestrator]) + + function walletSnapshot() { + const state = useWalletStore.getState() + if (!state.address || !state.chainId) throw new Error('Wallet disconnected') + return Promise.resolve({ address: state.address, chainId: state.chainId }) + } + + async function submit(event: FormEvent) { + event.preventDefault() + if (!connection || !/^0x[0-9a-fA-F]+$/.test(nftContract)) return + try { + const id = BigInt(auctionId) + const credential = generateSellerCredential({ + network: deployment.network, + chainId: BigInt(deployment.chainId), + auctionHouse: BigInt(deployment.auctionHouse), + auctionId: id, + }) + const bidDeadline = unixTimestamp(biddingDeadline, 'Bidding deadline') + const revealEnd = unixTimestamp(revealDeadline, 'Reveal deadline') + await runSellerCreationFlow({ + orchestrator, + wallet: connection.account as StandardWallet, + expectedWallet: { address: connection.address, chainId: connection.chainId }, + getWalletSnapshot: walletSnapshot, + credential, + recoveryPassword: password, + onRecoveryReady: async (bundle) => { + download(bundle.serialized, bundle.bundleId, id) + if (!window.confirm('Confirm that the encrypted seller recovery file downloaded and can be stored safely.')) { + throw new Error('Seller recovery was not confirmed') + } + }, + nftContract: nftContract as `0x${string}`, + tokenId: BigInt(tokenId), + reservePrice: BigInt(reservePrice), + cap: BigInt(cap), + biddingDeadline: bidDeadline, + revealDeadline: revealEnd, + bidderLimit: BigInt(bidderLimit), + verify: (transactionHash) => + verifyTransactionTransition({ + provider, + manifest: deployment, + transactionHash, + expectedEvent: 'AuctionCreated', + requirePoolTouch: false, + readState: async () => { + const snapshot = await readAuctionSnapshot(reader, deployment, id) + return snapshot.config.seller === connection.address && snapshot.custodyValid + }, + }), + }) + setCreatedAuctionId(id) + setStatus('auction created with NFT in custody') + onCreated?.(id) + } catch { + setStatus('auction creation failed or remains unconfirmed') + } + } + + const fieldClass = + 'min-h-12 w-full rounded-xl border border-white/10 bg-white/[0.04] px-4 outline-none focus:border-[#a8b1ff] disabled:cursor-not-allowed disabled:opacity-50' + + return ( +
void submit(event)} + className="rounded-2xl border border-white/10 bg-[#111217] p-5 sm:p-7" + > +

Seller workflow

+

Create auction

+ {!enabled ? ( +

Connect a compatible wallet to create an auction.

+ ) : null} +
+ {[ + ['Auction ID', auctionId, setAuctionId, 'numeric'], + ['NFT contract address', nftContract, setNftContract, 'text'], + ['NFT token ID', tokenId, setTokenId, 'numeric'], + ['Reserve price', reservePrice, setReservePrice, 'decimal'], + ['Uniform collateral cap', cap, setCap, 'decimal'], + ['Bidding deadline', biddingDeadline, setBiddingDeadline, 'datetime-local'], + ['Reveal deadline', revealDeadline, setRevealDeadline, 'datetime-local'], + ['Bidder limit', bidderLimit, setBidderLimit, 'numeric'], + ].map(([label, value, setter, kind]) => { + const id = `seller-${String(label).toLowerCase().replaceAll(' ', '-')}` + return ( + + ) + })} + +
+

+ Encrypted recovery is downloaded and import-verified before the wallet receives the NFT approval and auction + creation request. +

+ +

+ {status} +

+ {createdAuctionId !== null ? ( + + Open auction #{createdAuctionId.toString()} + + ) : null} + + ) +} diff --git a/web/src/features/auction/ui/SellerCreatePage.tsx b/web/src/features/auction/ui/SellerCreatePage.tsx new file mode 100644 index 0000000..1b9c069 --- /dev/null +++ b/web/src/features/auction/ui/SellerCreatePage.tsx @@ -0,0 +1,63 @@ +'use client' + +import { useMemo, useState } from 'react' +import Link from 'next/link' +import { RpcProvider } from 'starknet' +import type { PrivacyWalletConnection } from '@/features/wallet/walletConnection' +import { WalletConnectPanel } from '@/features/wallet/WalletConnectPanel' +import { SellerCreateForm, type SellerCreateDeployment } from '@/features/auction/ui/SellerCreateForm' + +export function SellerCreatePage({ + deployment, + error, +}: Readonly<{ deployment?: SellerCreateDeployment; error?: string }>) { + const [connection, setConnection] = useState(null) + const provider = useMemo( + () => (deployment ? new RpcProvider({ nodeUrl: deployment.rpcUrl }) : undefined), + [deployment], + ) + + return ( +
+
+
+ + CipherBid + + + Back to auctions + +
+
+

Guaranteed onchain delivery

+

+ Create a private-bid NFT auction +

+

+ The NFT moves into CipherBid custody atomically with immutable reserve, cap, deadlines, and seller claim + handle. +

+
+ {deployment ? ( +
+ setConnection(null)} + /> + +
+ ) : ( +

+ {error ?? 'Auction deployment is not configured.'} +

+ )} +
+
+ ) +} diff --git a/web/src/features/credentials/credentials.ts b/web/src/features/credentials/credentials.ts new file mode 100644 index 0000000..f6126b5 --- /dev/null +++ b/web/src/features/credentials/credentials.ts @@ -0,0 +1,163 @@ +import { + CONTRACT_ADDRESS_BOUND, + MAX_U128, + MAX_U64, + STARK_FIELD_PRIME, + computeBidCommitment, + computeClaimHandle, +} from '@/features/auction/commitment' + +export type CredentialNetwork = 'sepolia' | 'mainnet' + +export type CredentialBinding = Readonly<{ + network: CredentialNetwork + chainId: bigint + auctionHouse: bigint + auctionId: bigint +}> + +export type SellerCredential = Readonly< + CredentialBinding & { + schema: 'cipherbid.credential.v1' + role: 'seller' + claimSecret: bigint + claimHandle: bigint + } +> + +export type BidderCredential = Readonly< + CredentialBinding & { + schema: 'cipherbid.credential.v1' + role: 'bidder' + claimSecret: bigint + claimHandle: bigint + bidNonce: bigint + amount: bigint + assetRecipient: bigint + commitment: bigint + acceptedIndex?: number + } +> + +export type CipherBidCredential = SellerCredential | BidderCredential +export type RandomFill = (target: Uint8Array) => void | Uint8Array + +function assertFelt(name: string, value: bigint, allowZero = false): void { + if (value < 0n || value >= STARK_FIELD_PRIME || (!allowZero && value === 0n)) { + throw new Error(`${name} must be a ${allowZero ? '' : 'non-zero '}Stark field element`) + } +} + +function assertAddress(name: string, value: bigint): void { + if (value <= 0n || value >= CONTRACT_ADDRESS_BOUND) { + throw new Error(`${name} must be a non-zero Starknet contract address`) + } +} + +function validateBinding(binding: CredentialBinding): void { + if (binding.network !== 'sepolia' && binding.network !== 'mainnet') throw new Error('Unsupported credential network') + assertFelt('chainId', binding.chainId) + assertAddress('auctionHouse', binding.auctionHouse) + if (binding.auctionId <= 0n || binding.auctionId > MAX_U64) { + throw new Error('auctionId must be between 1 and u64 max') + } +} + +export function createSellerCredential(input: CredentialBinding & Readonly<{ claimSecret: bigint }>): SellerCredential { + validateBinding(input) + const claimHandle = computeClaimHandle(input.claimSecret) + return Object.freeze({ + schema: 'cipherbid.credential.v1', + role: 'seller', + network: input.network, + chainId: input.chainId, + auctionHouse: input.auctionHouse, + auctionId: input.auctionId, + claimSecret: input.claimSecret, + claimHandle, + }) +} + +export function createBidderCredential( + input: CredentialBinding & + Readonly<{ + claimSecret: bigint + bidNonce: bigint + amount: bigint + assetRecipient: bigint + }>, +): BidderCredential { + validateBinding(input) + assertFelt('Bid nonce', input.bidNonce) + if (input.amount <= 0n || input.amount > MAX_U128) throw new Error('Bid amount must be between 1 and u128 max') + assertAddress('assetRecipient', input.assetRecipient) + const claimHandle = computeClaimHandle(input.claimSecret) + const commitment = computeBidCommitment({ + chainId: input.chainId, + auctionHouse: input.auctionHouse, + auctionId: input.auctionId, + amount: input.amount, + bidNonce: input.bidNonce, + claimHandle, + assetRecipient: input.assetRecipient, + }) + return Object.freeze({ + schema: 'cipherbid.credential.v1', + role: 'bidder', + network: input.network, + chainId: input.chainId, + auctionHouse: input.auctionHouse, + auctionId: input.auctionId, + claimSecret: input.claimSecret, + claimHandle, + bidNonce: input.bidNonce, + amount: input.amount, + assetRecipient: input.assetRecipient, + commitment, + }) +} + +export function bindAcceptedIndex(credential: BidderCredential, acceptedIndex: number): BidderCredential { + if (!Number.isSafeInteger(acceptedIndex) || acceptedIndex < 0 || acceptedIndex >= 32) { + throw new Error('Accepted index must be an integer between 0 and 31') + } + return Object.freeze({ ...credential, acceptedIndex }) +} + +function littleEndianBigInt(bytes: Uint8Array): bigint { + let value = 0n + for (let index = bytes.length - 1; index >= 0; index -= 1) value = (value << 8n) | BigInt(bytes[index]) + return value +} + +export function generateNonZeroFelt(fill?: RandomFill): bigint { + const randomFill: RandomFill = + fill ?? + ((target) => { + if (!globalThis.crypto?.getRandomValues) throw new Error('Secure browser randomness is unavailable') + globalThis.crypto.getRandomValues(target) + }) + const bytes = new Uint8Array(32) + for (;;) { + bytes.fill(0) + randomFill(bytes) + const value = littleEndianBigInt(bytes) + if (value > 0n && value < STARK_FIELD_PRIME) { + bytes.fill(0) + return value + } + } +} + +export function generateSellerCredential(binding: CredentialBinding, fill?: RandomFill): SellerCredential { + return createSellerCredential({ ...binding, claimSecret: generateNonZeroFelt(fill) }) +} + +export function generateBidderCredential( + input: CredentialBinding & Readonly<{ amount: bigint; assetRecipient: bigint }>, + fill?: RandomFill, +): BidderCredential { + const claimSecret = generateNonZeroFelt(fill) + const bidNonce = generateNonZeroFelt(fill) + return createBidderCredential({ ...input, claimSecret, bidNonce }) +} diff --git a/web/src/features/credentials/recoveryBundle.ts b/web/src/features/credentials/recoveryBundle.ts new file mode 100644 index 0000000..664517b --- /dev/null +++ b/web/src/features/credentials/recoveryBundle.ts @@ -0,0 +1,326 @@ +import { + bindAcceptedIndex, + createBidderCredential, + createSellerCredential, + type BidderCredential, + type CipherBidCredential, +} from '@/features/credentials/credentials' + +const BUNDLE_SCHEMA = 'cipherbid.recovery.v1' +const PAYLOAD_SCHEMA = 'cipherbid.recovery.payload.v1' +const PBKDF2_ITERATIONS = 210_000 +const MAX_BUNDLE_BYTES = 65_536 +const MAX_CREDENTIALS = 32 +const encoder = new TextEncoder() +const decoder = new TextDecoder('utf-8', { fatal: true }) + +type RecoveryHeader = Readonly<{ + schema: typeof BUNDLE_SCHEMA + kdf: Readonly<{ + name: 'PBKDF2' + hash: 'SHA-256' + iterations: number + salt: string + }> + cipher: Readonly<{ + name: 'AES-GCM' + iv: string + tagLength: 128 + }> +}> + +type RecoveryEnvelope = RecoveryHeader & { ciphertext: string } + +type PlainCredential = Record + +function cryptoApi(): Crypto { + if (!globalThis.crypto?.subtle || !globalThis.crypto.getRandomValues) { + throw new Error('Web Crypto is unavailable') + } + return globalThis.crypto +} + +function passwordBytes(password: string): Uint8Array { + if (typeof password !== 'string' || password.length < 12) + throw new Error('Recovery password must contain at least 12 characters') + if (password.length > 1_024) throw new Error('Recovery password is too long') + return encoder.encode(password) +} + +function base64Url(bytes: Uint8Array): string { + let binary = '' + for (let index = 0; index < bytes.length; index += 1) binary += String.fromCharCode(bytes[index]) + return btoa(binary).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/g, '') +} + +function decodeBase64Url(value: unknown, label: string): Uint8Array { + if (typeof value !== 'string' || value.length === 0 || !/^[A-Za-z0-9_-]+$/.test(value)) { + throw new Error(`${label} is malformed`) + } + const padded = value.replace(/-/g, '+').replace(/_/g, '/') + '='.repeat((4 - (value.length % 4)) % 4) + const binary = atob(padded) + return Uint8Array.from(binary, (character) => character.charCodeAt(0)) +} + +function isRecord(value: unknown): value is Record { + return typeof value === 'object' && value !== null && !Array.isArray(value) +} + +function exactKeys(value: Record, expected: readonly string[]): void { + const actual = Object.keys(value).sort() + const wanted = [...expected].sort() + if (actual.length !== wanted.length || actual.some((key, index) => key !== wanted[index])) { + throw new Error('Structured input has unexpected fields') + } +} + +function bigintString(value: unknown, label: string, format: 'hex' | 'decimal'): bigint { + if (typeof value !== 'string' || value.length === 0 || value.length > 80) throw new Error(`${label} is malformed`) + const expression = format === 'hex' ? /^0x[0-9a-f]+$/ : /^(0|[1-9][0-9]*)$/ + if (!expression.test(value)) throw new Error(`${label} is malformed`) + return BigInt(value) +} + +function toPlain(credential: CipherBidCredential): PlainCredential { + const base = { + schema: credential.schema, + role: credential.role, + network: credential.network, + chainId: `0x${credential.chainId.toString(16)}`, + auctionHouse: `0x${credential.auctionHouse.toString(16)}`, + auctionId: credential.auctionId.toString(10), + claimSecret: `0x${credential.claimSecret.toString(16)}`, + claimHandle: `0x${credential.claimHandle.toString(16)}`, + } + if (credential.role === 'seller') return base + return { + ...base, + bidNonce: `0x${credential.bidNonce.toString(16)}`, + amount: credential.amount.toString(10), + assetRecipient: `0x${credential.assetRecipient.toString(16)}`, + commitment: `0x${credential.commitment.toString(16)}`, + ...(credential.acceptedIndex === undefined ? {} : { acceptedIndex: credential.acceptedIndex }), + } +} + +function parseCommon(value: Record) { + if (value.schema !== 'cipherbid.credential.v1') throw new Error('Unsupported credential schema') + if (value.network !== 'sepolia' && value.network !== 'mainnet') throw new Error('Unsupported credential network') + return { + network: value.network, + chainId: bigintString(value.chainId, 'chainId', 'hex'), + auctionHouse: bigintString(value.auctionHouse, 'auctionHouse', 'hex'), + auctionId: bigintString(value.auctionId, 'auctionId', 'decimal'), + claimSecret: bigintString(value.claimSecret, 'claimSecret', 'hex'), + } as const +} + +function fromPlain(value: unknown): CipherBidCredential { + if (!isRecord(value)) throw new Error('Credential must be an object') + if (value.role === 'seller') { + exactKeys(value, [ + 'schema', + 'role', + 'network', + 'chainId', + 'auctionHouse', + 'auctionId', + 'claimSecret', + 'claimHandle', + ]) + const credential = createSellerCredential(parseCommon(value)) + if (credential.claimHandle !== bigintString(value.claimHandle, 'claimHandle', 'hex')) { + throw new Error('Seller claim handle does not match secret') + } + return credential + } + if (value.role !== 'bidder') throw new Error('Unsupported credential role') + const hasAcceptedIndex = Object.hasOwn(value, 'acceptedIndex') + exactKeys(value, [ + 'schema', + 'role', + 'network', + 'chainId', + 'auctionHouse', + 'auctionId', + 'claimSecret', + 'claimHandle', + 'bidNonce', + 'amount', + 'assetRecipient', + 'commitment', + ...(hasAcceptedIndex ? ['acceptedIndex'] : []), + ]) + let credential: BidderCredential = createBidderCredential({ + ...parseCommon(value), + bidNonce: bigintString(value.bidNonce, 'bidNonce', 'hex'), + amount: bigintString(value.amount, 'amount', 'decimal'), + assetRecipient: bigintString(value.assetRecipient, 'assetRecipient', 'hex'), + }) + if (credential.claimHandle !== bigintString(value.claimHandle, 'claimHandle', 'hex')) { + throw new Error('Bidder claim handle does not match secret') + } + if (credential.commitment !== bigintString(value.commitment, 'commitment', 'hex')) { + throw new Error('Bid commitment does not match credential fields') + } + if (hasAcceptedIndex) { + if (typeof value.acceptedIndex !== 'number') throw new Error('acceptedIndex is malformed') + credential = bindAcceptedIndex(credential, value.acceptedIndex) + } + return credential +} + +function payloadBytes(credentials: readonly CipherBidCredential[]): Uint8Array { + if (!Array.isArray(credentials) || credentials.length === 0 || credentials.length > MAX_CREDENTIALS) { + throw new Error(`Recovery bundle must contain between 1 and ${MAX_CREDENTIALS} credentials`) + } + return encoder.encode(JSON.stringify({ schema: PAYLOAD_SCHEMA, credentials: credentials.map(toPlain) })) +} + +function parsePayload(bytes: Uint8Array): readonly CipherBidCredential[] { + const parsed = JSON.parse(decoder.decode(bytes)) as unknown + if (!isRecord(parsed)) throw new Error('Recovery payload must be an object') + exactKeys(parsed, ['schema', 'credentials']) + if (parsed.schema !== PAYLOAD_SCHEMA || !Array.isArray(parsed.credentials)) + throw new Error('Unsupported recovery payload') + if (parsed.credentials.length === 0 || parsed.credentials.length > MAX_CREDENTIALS) { + throw new Error('Recovery payload credential count is invalid') + } + return Object.freeze(parsed.credentials.map(fromPlain)) +} + +function parseEnvelope(serialized: string): RecoveryEnvelope { + if ( + typeof serialized !== 'string' || + serialized.length === 0 || + encoder.encode(serialized).length > MAX_BUNDLE_BYTES + ) { + throw new Error('Recovery envelope size is invalid') + } + const parsed = JSON.parse(serialized) as unknown + if (!isRecord(parsed)) throw new Error('Recovery envelope must be an object') + exactKeys(parsed, ['schema', 'kdf', 'cipher', 'ciphertext']) + if (parsed.schema !== BUNDLE_SCHEMA || !isRecord(parsed.kdf) || !isRecord(parsed.cipher)) { + throw new Error('Unsupported recovery envelope') + } + exactKeys(parsed.kdf, ['name', 'hash', 'iterations', 'salt']) + exactKeys(parsed.cipher, ['name', 'iv', 'tagLength']) + if ( + parsed.kdf.name !== 'PBKDF2' || + parsed.kdf.hash !== 'SHA-256' || + parsed.kdf.iterations !== PBKDF2_ITERATIONS || + parsed.cipher.name !== 'AES-GCM' || + parsed.cipher.tagLength !== 128 || + typeof parsed.ciphertext !== 'string' + ) { + throw new Error('Unsupported recovery cryptography') + } + decodeBase64Url(parsed.kdf.salt, 'salt') + decodeBase64Url(parsed.cipher.iv, 'iv') + decodeBase64Url(parsed.ciphertext, 'ciphertext') + return parsed as unknown as RecoveryEnvelope +} + +function headerOf(envelope: RecoveryEnvelope): RecoveryHeader { + return { schema: envelope.schema, kdf: envelope.kdf, cipher: envelope.cipher } +} + +function ownedBuffer(bytes: Uint8Array): ArrayBuffer { + const buffer = new ArrayBuffer(bytes.byteLength) + new Uint8Array(buffer).set(bytes) + return buffer +} + +async function deriveKey(password: Uint8Array, salt: Uint8Array, usage: KeyUsage[]): Promise { + const api = cryptoApi() + const material = await api.subtle.importKey('raw', ownedBuffer(password), 'PBKDF2', false, ['deriveKey']) + return api.subtle.deriveKey( + { name: 'PBKDF2', hash: 'SHA-256', salt: ownedBuffer(salt), iterations: PBKDF2_ITERATIONS }, + material, + { name: 'AES-GCM', length: 256 }, + false, + usage, + ) +} + +export async function encryptRecoveryBundle( + credentials: readonly CipherBidCredential[], + password: string, +): Promise { + const api = cryptoApi() + const secret = passwordBytes(password) + const plaintext = payloadBytes(credentials) + const salt = api.getRandomValues(new Uint8Array(16)) + const iv = api.getRandomValues(new Uint8Array(12)) + const envelope: RecoveryEnvelope = { + schema: BUNDLE_SCHEMA, + kdf: { name: 'PBKDF2', hash: 'SHA-256', iterations: PBKDF2_ITERATIONS, salt: base64Url(salt) }, + cipher: { name: 'AES-GCM', iv: base64Url(iv), tagLength: 128 }, + ciphertext: '', + } + try { + const key = await deriveKey(secret, salt, ['encrypt']) + const aad = encoder.encode(JSON.stringify(headerOf(envelope))) + const encrypted = await api.subtle.encrypt( + { name: 'AES-GCM', iv: ownedBuffer(iv), additionalData: ownedBuffer(aad), tagLength: 128 }, + key, + ownedBuffer(plaintext), + ) + envelope.ciphertext = base64Url(new Uint8Array(encrypted)) + const serialized = JSON.stringify(envelope) + if (encoder.encode(serialized).length > MAX_BUNDLE_BYTES) throw new Error('Encrypted recovery bundle is too large') + return serialized + } finally { + secret.fill(0) + plaintext.fill(0) + } +} + +export async function decryptRecoveryBundle( + serialized: string, + password: string, +): Promise { + let result: readonly CipherBidCredential[] | undefined + let failed = false + let secret: Uint8Array | undefined + let plaintext: Uint8Array | undefined + try { + const api = cryptoApi() + const envelope = parseEnvelope(serialized) + secret = passwordBytes(password) + const salt = decodeBase64Url(envelope.kdf.salt, 'salt') + const iv = decodeBase64Url(envelope.cipher.iv, 'iv') + const ciphertext = decodeBase64Url(envelope.ciphertext, 'ciphertext') + const key = await deriveKey(secret, salt, ['decrypt']) + const aad = encoder.encode(JSON.stringify(headerOf(envelope))) + const decrypted = await api.subtle.decrypt( + { name: 'AES-GCM', iv: ownedBuffer(iv), additionalData: ownedBuffer(aad), tagLength: 128 }, + key, + ownedBuffer(ciphertext), + ) + plaintext = new Uint8Array(decrypted) + result = parsePayload(plaintext) + } catch { + failed = true + } finally { + secret?.fill(0) + plaintext?.fill(0) + } + if (failed || !result) throw new Error('Recovery bundle could not be decrypted or validated') + return result +} + +export async function createVerifiedRecoveryBundle( + credentials: readonly CipherBidCredential[], + password: string, +): Promise> { + const serialized = await encryptRecoveryBundle(credentials, password) + const recovered = await decryptRecoveryBundle(serialized, password) + if (JSON.stringify(recovered.map(toPlain)) !== JSON.stringify(credentials.map(toPlain))) { + throw new Error('Recovery bundle import verification failed') + } + const digest = await cryptoApi().subtle.digest('SHA-256', ownedBuffer(encoder.encode(serialized))) + const bundleId = + `0x${[...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, '0')).join('')}` as const + return Object.freeze({ serialized, bundleId, credentialCount: recovered.length }) +} diff --git a/web/src/features/demo/demoBidderShield.ts b/web/src/features/demo/demoBidderShield.ts new file mode 100644 index 0000000..6ac19e8 --- /dev/null +++ b/web/src/features/demo/demoBidderShield.ts @@ -0,0 +1,167 @@ +import type { WALLET_API } from '@starknet-io/types-js' +import type { PrivacyWalletConnection } from '@/features/wallet/walletConnection' +import { MAINNET_CHAIN_ID, SEPOLIA_CHAIN_ID, STRK_TOKEN, type DeploymentNetwork } from '@/config/deployment' +import { MAINNET_BIDDER_A, MAINNET_BIDDER_B } from '@/config/mainnetRelease' + +const SEPOLIA_BIDDER_A = '0x054499e46751979eea7fcc64475836d1a5f591c2d12a7546e42e8516fdbabc4d' as const +const SEPOLIA_BIDDER_B = '0x014ecc190504847edc0b29f427404b2cad833ff8837277af69f4d3bf99d82b52' as const + +type DemoBidder = 'Bidder A' | 'Bidder B' +export type DemoBidderConfig = Readonly<{ + network: DeploymentNetwork + chainId: string + networkLabel: string + bidderA: `0x${string}` + bidderB: `0x${string}` + paymentToken: `0x${string}` + shieldAmount: `0x${string}` + shieldDisplay: string + allowActivation: boolean + explorerTransactionBase: string +}> + +export const MAINNET_DEMO_BIDDER_CONFIG: DemoBidderConfig = Object.freeze({ + network: 'mainnet', + chainId: MAINNET_CHAIN_ID, + networkLabel: 'Starknet mainnet', + bidderA: MAINNET_BIDDER_A, + bidderB: MAINNET_BIDDER_B, + paymentToken: STRK_TOKEN, + shieldAmount: '0x14d1120d7b1600000', + shieldDisplay: '24', + allowActivation: false, + explorerTransactionBase: 'https://voyager.online/tx', +}) + +export const SEPOLIA_DEMO_BIDDER_CONFIG: DemoBidderConfig = Object.freeze({ + network: 'sepolia', + chainId: SEPOLIA_CHAIN_ID, + networkLabel: 'Starknet Sepolia', + bidderA: SEPOLIA_BIDDER_A, + bidderB: SEPOLIA_BIDDER_B, + paymentToken: STRK_TOKEN, + shieldAmount: '0xd02ab486cedc0000', + shieldDisplay: '15', + allowActivation: true, + explorerTransactionBase: 'https://sepolia.voyager.online/tx', +}) + +export function demoBidderConfig(network: DeploymentNetwork): DemoBidderConfig { + return network === 'mainnet' ? MAINNET_DEMO_BIDDER_CONFIG : SEPOLIA_DEMO_BIDDER_CONFIG +} +type ShieldWallet = Readonly<{ + strk20InvokeTransaction: (actions: readonly WALLET_API.STRK20_ACTION[]) => Promise +}> +type ActivationWallet = Readonly<{ + execute: ( + call: Readonly<{ contractAddress: string; entrypoint: string; calldata: readonly string[] }>, + ) => Promise +}> + +function sameFelt(left: string, right: string): boolean { + try { + return BigInt(left) === BigInt(right) + } catch { + return false + } +} + +export function demoBidderForAddress( + address: string, + config: DemoBidderConfig = MAINNET_DEMO_BIDDER_CONFIG, +): DemoBidder | null { + if (sameFelt(address, config.bidderA)) return 'Bidder A' + if (sameFelt(address, config.bidderB)) return 'Bidder B' + return null +} + +export function publicDemoShieldError(error: unknown): string { + const message = error instanceof Error ? error.message.toLowerCase() : '' + if ( + message.includes('no viewing key') || + message.includes('provisioned via the backend') || + message.includes('not_registered') || + message.includes('not registered') + ) { + return 'Ready X cannot enable private tokens for an imported account. Create a native Ready X Standard Account for this bidder.' + } + if (message.includes('user_refused') || message.includes('rejected') || message.includes('cancelled')) { + return 'The Ready X request was rejected or cancelled. No transaction was submitted.' + } + return 'Ready X could not prepare the shield transaction. No transaction was submitted.' +} + +function shieldWallet(account: unknown): ShieldWallet { + if ( + typeof account !== 'object' || + account === null || + !('strk20InvokeTransaction' in account) || + typeof account.strk20InvokeTransaction !== 'function' + ) { + throw new Error('Connected wallet does not expose the STRK20 Wallet API') + } + return account as ShieldWallet +} + +function activationWallet(account: unknown): ActivationWallet { + if ( + typeof account !== 'object' || + account === null || + !('execute' in account) || + typeof account.execute !== 'function' + ) { + throw new Error('Connected wallet does not expose standard Starknet execution') + } + return account as ActivationWallet +} + +function responseHash(response: unknown): string { + if (typeof response !== 'object' || response === null) throw new Error('Wallet returned no transaction hash') + const record = response as Record + const value = record.transaction_hash ?? record.transactionHash + if (typeof value !== 'string' || !/^0x[0-9a-fA-F]+$/.test(value)) { + throw new Error('Wallet returned no transaction hash') + } + return value +} + +function normalizedChainId(chainId: string): string { + if (chainId === 'SN_SEPOLIA') return SEPOLIA_CHAIN_ID + if (chainId === 'SN_MAIN') return MAINNET_CHAIN_ID + return chainId +} + +function connectedBidder(connection: PrivacyWalletConnection, config: DemoBidderConfig): DemoBidder { + if (!sameFelt(normalizedChainId(connection.chainId), config.chainId)) { + throw new Error(`Switch Ready X to ${config.networkLabel}`) + } + const bidder = demoBidderForAddress(connection.address, config) + if (!bidder) throw new Error('Connect bidder A or bidder B; the seller account cannot shield bidder funds') + return bidder +} + +export async function runDemoBidderActivation( + connection: PrivacyWalletConnection, + config: DemoBidderConfig = MAINNET_DEMO_BIDDER_CONFIG, +): Promise> { + const bidder = connectedBidder(connection, config) + const response = await activationWallet(connection.account).execute({ + contractAddress: config.paymentToken, + entrypoint: 'transfer', + calldata: [connection.address, '0x38d7ea4c68000', '0x0'], + }) + return Object.freeze({ bidder, transactionHash: responseHash(response) }) +} + +export async function runDemoBidderShield( + connection: PrivacyWalletConnection, + config: DemoBidderConfig = MAINNET_DEMO_BIDDER_CONFIG, +): Promise> { + const bidder = connectedBidder(connection, config) + if (!connection.supportsStrk20) throw new Error('Wallet API 0.10.3 or newer is required') + const actions: readonly WALLET_API.STRK20_ACTION[] = Object.freeze([ + Object.freeze({ type: 'deposit', token: config.paymentToken, amount: config.shieldAmount }), + ]) + const response = await shieldWallet(connection.account).strk20InvokeTransaction(actions) + return Object.freeze({ bidder, transactionHash: responseHash(response) }) +} diff --git a/web/src/features/demo/ui/DemoBidderSetupPage.tsx b/web/src/features/demo/ui/DemoBidderSetupPage.tsx new file mode 100644 index 0000000..64b99f2 --- /dev/null +++ b/web/src/features/demo/ui/DemoBidderSetupPage.tsx @@ -0,0 +1,193 @@ +'use client' + +import { useMemo, useState } from 'react' +import Link from 'next/link' +import { RpcProvider } from 'starknet' +import { WalletConnectPanel } from '@/features/wallet/WalletConnectPanel' +import type { PrivacyWalletConnection } from '@/features/wallet/walletConnection' +import type { DeploymentManifest } from '@/config/deployment' +import { + MAINNET_DEMO_BIDDER_CONFIG, + demoBidderConfig, + demoBidderForAddress, + publicDemoShieldError, + runDemoBidderActivation, + runDemoBidderShield, + type DemoBidderConfig, +} from '@/features/demo/demoBidderShield' + +type ShieldResult = Awaited> + +export function DemoBidderSetupPanel({ + connection, + config = MAINNET_DEMO_BIDDER_CONFIG, + activate = runDemoBidderActivation, + shield = runDemoBidderShield, +}: Readonly<{ + connection: PrivacyWalletConnection | null + config?: DemoBidderConfig + activate?: (connection: PrivacyWalletConnection, config: DemoBidderConfig) => Promise + shield?: (connection: PrivacyWalletConnection, config: DemoBidderConfig) => Promise +}>) { + const bidder = connection ? demoBidderForAddress(connection.address, config) : null + const [pending, setPending] = useState(false) + const [result, setResult] = useState(null) + const [status, setStatus] = useState('') + const [error, setError] = useState(null) + const wrongAccount = connection !== null && bidder === null + + async function submitActivation() { + if (!connection || !bidder || pending) return + setPending(true) + setResult(null) + setError(null) + setStatus('Confirm the standard Ready X activation transaction.') + try { + const submitted = await activate(connection, config) + setResult(submitted) + setStatus('Activation transaction submitted. Wait for acceptance, reconnect, then shield.') + } catch { + setStatus('') + setError('Ready X could not submit account activation. No transaction was assumed successful.') + } finally { + setPending(false) + } + } + + async function submitShield() { + if (!connection || !bidder || pending) return + setPending(true) + setResult(null) + setError(null) + setStatus('Confirm the Ready X approval and private deposit prompts.') + try { + const submitted = await shield(connection, config) + setResult(submitted) + setStatus('Shield transaction submitted. Wait for acceptance and ten blocks before bidding.') + } catch (cause) { + setStatus('') + setError(publicDemoShieldError(cause)) + } finally { + setPending(false) + } + } + + const buttonLabel = bidder ? `Shield ${config.shieldDisplay} STRK for ${bidder}` : 'Connect Bidder A or Bidder B' + + return ( +
+

Private funding setup

+

+ Prepare a demo bidder +

+

+ Ready X owns registration, note discovery, proving, and submission. CipherBid requests one public{' '} + {config.shieldDisplay} STRK deposit and never receives a viewing key. +

+ + {bidder ? ( +
+

{bidder} connected

+

{connection?.address}

+
+ ) : null} + {wrongAccount ? ( +

+ Connect bidder A or bidder B; the seller account cannot shield bidder funds. +

+ ) : null} + +
+ {config.allowActivation ? ( + + ) : null} + +
+

+ {status} +

+ {error ? ( +

+ {error} +

+ ) : null} + {result ? ( + + {result.transactionHash} + + ) : null} +

+ The pool currently charges its own private-operation fee. Keep the remaining public STRK for account fees and + later claims. +

+
+ ) +} + +export function DemoBidderSetupPage({ deployment }: Readonly<{ deployment: DeploymentManifest }>) { + const [connection, setConnection] = useState(null) + const provider = useMemo(() => new RpcProvider({ nodeUrl: deployment.rpcUrl }), [deployment.rpcUrl]) + const config = demoBidderConfig(deployment.network) + + return ( +
+
+
+ + CipherBid + + + Back to auctions + +
+
+

+ {config.networkLabel} demo preparation +

+

+ Shield both demo bidders before the timer starts +

+

+ Connect Bidder A, shield once, disconnect, switch Ready X to Bidder B, and repeat. The auction CLI will + refuse to start until both deposits are at least ten blocks old. +

+
+
+ setConnection(null)} + /> + +
+
+
+ ) +} diff --git a/web/src/features/privacy/canonicalCap.ts b/web/src/features/privacy/canonicalCap.ts index 66a99d9..f7b803b 100644 --- a/web/src/features/privacy/canonicalCap.ts +++ b/web/src/features/privacy/canonicalCap.ts @@ -1,5 +1,4 @@ import { MAX_U128 } from '@/features/auction/auctionMath' -import { createSepoliaProvider } from '@/lib/starknet/network' import type { HexAddress } from './strk20Actions' import type { Call } from 'starknet' @@ -7,12 +6,7 @@ type CapProvider = Readonly<{ callContract: (call: Call) => Promise }> -const defaultProvider: CapProvider = createSepoliaProvider() - -export async function readCanonicalCap( - auctionHouse: HexAddress, - provider: CapProvider = defaultProvider, -): Promise { +export async function readCanonicalCap(auctionHouse: HexAddress, provider: CapProvider): Promise { const response = await provider.callContract({ contractAddress: auctionHouse, entrypoint: 'get_cap', diff --git a/web/src/features/privacy/strk20Actions.ts b/web/src/features/privacy/strk20Actions.ts index 6cef193..6b8c072 100644 --- a/web/src/features/privacy/strk20Actions.ts +++ b/web/src/features/privacy/strk20Actions.ts @@ -12,17 +12,7 @@ export type PlaceBidInput = Readonly<{ auctionHouse: HexAddress }> -export type RevealBidInput = Readonly<{ - auctionId: bigint - amount: bigint - bidSecret: bigint - claimHandle: bigint - assetRecipient: HexAddress - auctionHouse: HexAddress -}> - const PLACE_BID = 0n -const REVEAL_BID = 1n const ZERO = num.toHex(0n) const POOL_ADDRESS_PLACEHOLDER = '${poolAddress}' @@ -52,22 +42,3 @@ export function buildPlaceBidActions(input: PlaceBidInput): readonly WALLET_API. }, ] } - -export function buildRevealBidActions(input: RevealBidInput): readonly WALLET_API.STRK20_ACTION[] { - return [ - { - type: 'invoke', - contract: input.auctionHouse, - calldata: [ - felt(REVEAL_BID), - felt(input.auctionId), - felt(input.amount), - felt(input.bidSecret), - felt(input.claimHandle), - input.assetRecipient, - POOL_ADDRESS_PLACEHOLDER, - ZERO, - ], - }, - ] -} diff --git a/web/src/features/privacy/strk20ClaimActions.ts b/web/src/features/privacy/strk20ClaimActions.ts new file mode 100644 index 0000000..7dcd82d --- /dev/null +++ b/web/src/features/privacy/strk20ClaimActions.ts @@ -0,0 +1,61 @@ +import type { WALLET_API } from '@starknet-io/types-js' +import { num } from 'starknet' +import type { HexAddress } from './strk20Actions' + +export type PrivateClaimInput = Readonly<{ + auctionId: bigint + paymentToken: HexAddress + claimSecret: bigint + claimHandle: bigint + auctionHouse: HexAddress + recipient: HexAddress +}> + +const LOSER_REFUND = 1n +const WINNER_SURPLUS = 2n +const SELLER_PROCEEDS = 3n +const ZERO = num.toHex(0n) +const POOL_ADDRESS_PLACEHOLDER = '${poolAddress}' +const OPEN_NOTE_ID_PLACEHOLDER = '${openNoteIds[0]}' + +const felt = (value: bigint) => num.toHex(value) + +function buildClaimActions( + operation: typeof LOSER_REFUND | typeof WINNER_SURPLUS | typeof SELLER_PROCEEDS, + input: PrivateClaimInput, +): readonly WALLET_API.STRK20_ACTION[] { + return [ + { + type: 'transfer', + token: input.paymentToken, + amount: 'OPEN', + recipient: input.recipient, + }, + { + type: 'invoke', + contract: input.auctionHouse, + calldata: [ + felt(operation), + felt(input.auctionId), + felt(input.claimSecret), + felt(input.claimHandle), + ZERO, + ZERO, + POOL_ADDRESS_PLACEHOLDER, + OPEN_NOTE_ID_PLACEHOLDER, + ], + }, + ] +} + +export function buildLoserRefundActions(input: PrivateClaimInput): readonly WALLET_API.STRK20_ACTION[] { + return buildClaimActions(LOSER_REFUND, input) +} + +export function buildWinnerSurplusActions(input: PrivateClaimInput): readonly WALLET_API.STRK20_ACTION[] { + return buildClaimActions(WINNER_SURPLUS, input) +} + +export function buildSellerProceedsActions(input: PrivateClaimInput): readonly WALLET_API.STRK20_ACTION[] { + return buildClaimActions(SELLER_PROCEEDS, input) +} diff --git a/web/src/features/transactions/auctionTransactionFlows.ts b/web/src/features/transactions/auctionTransactionFlows.ts new file mode 100644 index 0000000..6ab9377 --- /dev/null +++ b/web/src/features/transactions/auctionTransactionFlows.ts @@ -0,0 +1,334 @@ +import type { WALLET_API } from '@starknet-io/types-js' +import { num, type Call } from 'starknet' +import { + buildAuthorizeSellerProceedsCall, + buildRevealBidCall, + buildSettleAuctionCall, +} from '@/features/auction/lifecycleCalls' +import { buildPlaceBidActions, type HexAddress } from '@/features/privacy/strk20Actions' +import { + buildLoserRefundActions, + buildSellerProceedsActions, + buildWinnerSurplusActions, +} from '@/features/privacy/strk20ClaimActions' +import type { BidderCredential, CipherBidCredential, SellerCredential } from '@/features/credentials/credentials' +import { createVerifiedRecoveryBundle } from '@/features/credentials/recoveryBundle' +import { TransactionOrchestrator, type WalletSnapshot } from '@/features/transactions/transactionOrchestrator' + +export type VerifiedRecoveryExport = Readonly<{ + serialized: string + bundleId: `0x${string}` + credentialCount: number +}> + +export type StandardWallet = Readonly<{ + execute: (calls: Call | readonly Call[]) => Promise +}> + +export type PrivacyWallet = StandardWallet & + Readonly<{ + strk20InvokeTransaction: (actions: readonly WALLET_API.STRK20_ACTION[]) => Promise + strk20PrepareInvoke?: (actions: readonly WALLET_API.STRK20_ACTION[], simulateOnly: boolean) => Promise + }> + +type BaseFlow = Readonly<{ + orchestrator: TransactionOrchestrator + expectedWallet: WalletSnapshot + getWalletSnapshot: () => Promise +}> + +type Verify = (transactionHash: string) => Promise + +const felt = (value: bigint) => num.toHex(value) +const address = (value: bigint) => num.toHex(value) as HexAddress + +function transactionHash(response: unknown): string { + if (typeof response !== 'object' || response === null) throw new Error('Wallet returned an invalid response') + const record = response as Record + const value = record.transaction_hash ?? record.transactionHash + if (typeof value !== 'string') throw new Error('Wallet returned no transaction hash') + return value +} + +function u256(value: bigint): readonly [string, string] { + if (value < 0n || value >= 1n << 256n) throw new Error('Token ID must fit u256') + return [felt(value & ((1n << 128n) - 1n)), felt(value >> 128n)] +} + +function assertCredentialWallet(credential: CipherBidCredential, wallet: WalletSnapshot): void { + try { + if (credential.chainId !== BigInt(wallet.chainId)) throw new Error('Credential chain does not match wallet') + } catch { + throw new Error('Credential chain does not match wallet') + } +} + +export function extractResolvedSellerOpenNoteId( + prepared: unknown, + credential: SellerCredential, + poolAddress: HexAddress, +): bigint { + if (typeof prepared !== 'object' || prepared === null) throw new Error('Prepared STRK20 call is malformed') + const call = (prepared as Record).call + if (typeof call !== 'object' || call === null) throw new Error('Prepared STRK20 call is malformed') + const calldata = (call as Record).calldata + if (!Array.isArray(calldata) || !calldata.every((item) => typeof item === 'string')) { + throw new Error('Prepared STRK20 calldata is malformed') + } + const expected = [ + 3n, + credential.auctionId, + credential.claimSecret, + credential.claimHandle, + 0n, + 0n, + BigInt(poolAddress), + ] + const matches: bigint[] = [] + for (let start = 0; start + expected.length < calldata.length; start += 1) { + let exact = true + for (let offset = 0; offset < expected.length; offset += 1) { + try { + if (BigInt(calldata[start + offset] as string) !== expected[offset]) exact = false + } catch { + exact = false + } + } + if (exact) { + const openNoteId = BigInt(calldata[start + expected.length] as string) + if (openNoteId > 0n) matches.push(openNoteId) + } + } + if (matches.length !== 1) throw new Error('Prepared STRK20 call does not contain one resolved seller open-note ID') + return matches[0] +} + +export async function runSellerCreationFlow( + input: BaseFlow & + Readonly<{ + wallet: StandardWallet + credential: SellerCredential + recoveryPassword: string + onRecoveryReady: (bundle: VerifiedRecoveryExport) => Promise + nftContract: HexAddress + tokenId: bigint + reservePrice: bigint + cap: bigint + biddingDeadline: bigint + revealDeadline: bigint + bidderLimit: bigint + verify: Verify + }>, +) { + assertCredentialWallet(input.credential, input.expectedWallet) + let recovery: VerifiedRecoveryExport | undefined + const evidence = await input.orchestrator.run({ + operationId: `create:${input.credential.auctionId}`, + expectedWallet: input.expectedWallet, + getWalletSnapshot: input.getWalletSnapshot, + prepare: async () => { + recovery = await createVerifiedRecoveryBundle([input.credential], input.recoveryPassword) + await input.onRecoveryReady(recovery) + const [tokenLow, tokenHigh] = u256(input.tokenId) + return [ + { + contractAddress: input.nftContract, + entrypoint: 'approve', + calldata: [address(input.credential.auctionHouse), tokenLow, tokenHigh], + }, + { + contractAddress: address(input.credential.auctionHouse), + entrypoint: 'create_auction', + calldata: [ + felt(input.credential.auctionId), + felt(input.credential.claimHandle), + input.nftContract, + tokenLow, + tokenHigh, + felt(input.reservePrice), + felt(input.cap), + felt(input.biddingDeadline), + felt(input.revealDeadline), + felt(input.bidderLimit), + ], + }, + ] satisfies readonly Call[] + }, + submit: async (calls) => transactionHash(await input.wallet.execute(calls)), + verify: input.verify, + }) + if (!recovery) throw new Error('Recovery export was not created') + return Object.freeze({ evidence, credential: input.credential, recovery }) +} + +export async function runPrivateBidFlow( + input: BaseFlow & + Readonly<{ + wallet: PrivacyWallet + credential: BidderCredential + recoveryPassword: string + onRecoveryReady: (bundle: VerifiedRecoveryExport) => Promise + paymentToken: HexAddress + cap: bigint + verify: Verify + }>, +) { + assertCredentialWallet(input.credential, input.expectedWallet) + if (input.credential.amount > input.cap) throw new Error('Bid amount exceeds collateral cap') + let recovery: VerifiedRecoveryExport | undefined + const evidence = await input.orchestrator.run({ + operationId: `bid:${input.credential.auctionId}`, + expectedWallet: input.expectedWallet, + getWalletSnapshot: input.getWalletSnapshot, + prepare: async () => { + recovery = await createVerifiedRecoveryBundle([input.credential], input.recoveryPassword) + await input.onRecoveryReady(recovery) + return buildPlaceBidActions({ + auctionId: input.credential.auctionId, + paymentToken: input.paymentToken, + cap: input.cap, + commitment: input.credential.commitment, + claimHandle: input.credential.claimHandle, + auctionHouse: address(input.credential.auctionHouse), + }) + }, + submit: async (actions) => transactionHash(await input.wallet.strk20InvokeTransaction(actions)), + verify: input.verify, + }) + if (!recovery) throw new Error('Recovery export was not created') + return Object.freeze({ evidence, credential: input.credential, recovery }) +} + +export async function runRevealFlow( + input: BaseFlow & Readonly<{ wallet: StandardWallet; credential: BidderCredential; verify: Verify }>, +) { + assertCredentialWallet(input.credential, input.expectedWallet) + if (input.credential.acceptedIndex === undefined) throw new Error('Bid accepted index is required for reveal') + return input.orchestrator.run({ + operationId: `reveal:${input.credential.auctionId}:${input.credential.acceptedIndex}`, + expectedWallet: input.expectedWallet, + getWalletSnapshot: input.getWalletSnapshot, + prepare: async () => + buildRevealBidCall({ + auctionId: input.credential.auctionId, + acceptedIndex: BigInt(input.credential.acceptedIndex!), + amount: input.credential.amount, + bidNonce: input.credential.bidNonce, + assetRecipient: address(input.credential.assetRecipient), + auctionHouse: address(input.credential.auctionHouse), + }), + submit: async (call) => transactionHash(await input.wallet.execute(call)), + verify: input.verify, + }) +} + +export async function runSettlementFlow( + input: BaseFlow & + Readonly<{ + wallet: StandardWallet + auctionHouse: HexAddress + auctionId: bigint + verify: Verify + }>, +) { + return input.orchestrator.run({ + operationId: `settle:${input.auctionId}`, + expectedWallet: input.expectedWallet, + getWalletSnapshot: input.getWalletSnapshot, + prepare: async () => buildSettleAuctionCall(input), + submit: async (call) => transactionHash(await input.wallet.execute(call)), + verify: input.verify, + }) +} + +export type BidderClaimKind = 'loser_refund' | 'winner_surplus' + +export async function runPrivateClaimFlow( + input: BaseFlow & + Readonly<{ + wallet: PrivacyWallet + kind: BidderClaimKind + credential: BidderCredential + paymentToken: HexAddress + recipient: HexAddress + verify: Verify + }>, +) { + assertCredentialWallet(input.credential, input.expectedWallet) + const build = input.kind === 'loser_refund' ? buildLoserRefundActions : buildWinnerSurplusActions + return input.orchestrator.run({ + operationId: `${input.kind}:${input.credential.auctionId}`, + expectedWallet: input.expectedWallet, + getWalletSnapshot: input.getWalletSnapshot, + prepare: async () => + build({ + auctionId: input.credential.auctionId, + paymentToken: input.paymentToken, + claimSecret: input.credential.claimSecret, + claimHandle: input.credential.claimHandle, + auctionHouse: address(input.credential.auctionHouse), + recipient: input.recipient, + }), + submit: async (actions) => transactionHash(await input.wallet.strk20InvokeTransaction(actions)), + verify: input.verify, + }) +} + +export async function runSellerClaimFlow( + input: BaseFlow & + Readonly<{ + wallet: Required + credential: SellerCredential + paymentToken: HexAddress + recipient: HexAddress + extractOpenNoteId: (prepared: unknown) => bigint + verifyAuthorization: Verify + verifyClaim: Verify + }>, +) { + assertCredentialWallet(input.credential, input.expectedWallet) + const actions = buildSellerProceedsActions({ + auctionId: input.credential.auctionId, + paymentToken: input.paymentToken, + claimSecret: input.credential.claimSecret, + claimHandle: input.credential.claimHandle, + auctionHouse: address(input.credential.auctionHouse), + recipient: input.recipient, + }) + let authorizedOpenNoteId: bigint | undefined + const authorizationEvidence = await input.orchestrator.run({ + operationId: `seller-authorize:${input.credential.auctionId}`, + expectedWallet: input.expectedWallet, + getWalletSnapshot: input.getWalletSnapshot, + prepare: async () => { + authorizedOpenNoteId = input.extractOpenNoteId(await input.wallet.strk20PrepareInvoke(actions, true)) + if (authorizedOpenNoteId <= 0n) throw new Error('Wallet preparation returned an invalid open-note ID') + return buildAuthorizeSellerProceedsCall({ + auctionId: input.credential.auctionId, + claimHandle: input.credential.claimHandle, + openNoteId: authorizedOpenNoteId, + auctionHouse: address(input.credential.auctionHouse), + }) + }, + submit: async (call) => transactionHash(await input.wallet.execute(call)), + verify: input.verifyAuthorization, + }) + if (authorizedOpenNoteId === undefined) throw new Error('Seller note authorization was not prepared') + + const claimEvidence = await input.orchestrator.run({ + operationId: `seller-claim:${input.credential.auctionId}`, + expectedWallet: input.expectedWallet, + getWalletSnapshot: input.getWalletSnapshot, + prepare: async () => { + const preparedOpenNoteId = input.extractOpenNoteId(await input.wallet.strk20PrepareInvoke(actions, true)) + if (preparedOpenNoteId !== authorizedOpenNoteId) { + throw new Error('Prepared seller open-note ID changed after authorization') + } + return actions + }, + submit: async (preparedActions) => transactionHash(await input.wallet.strk20InvokeTransaction(preparedActions)), + verify: input.verifyClaim, + }) + + return Object.freeze({ authorizationEvidence, claimEvidence, authorizedOpenNoteId }) +} diff --git a/web/src/features/transactions/receiptVerifier.ts b/web/src/features/transactions/receiptVerifier.ts new file mode 100644 index 0000000..c8402d2 --- /dev/null +++ b/web/src/features/transactions/receiptVerifier.ts @@ -0,0 +1,163 @@ +import type { DeploymentManifest } from '@/config/deployment' +import { hash } from 'starknet' + +const EVENT_NAMES = [ + 'AuctionCreated', + 'BidCommitted', + 'BidRevealed', + 'AuctionSettled', + 'SellerProceedsAuthorized', + 'LoserRefundClaimed', + 'WinnerSurplusClaimed', + 'SellerProceedsClaimed', +] as const + +export type CipherBidEventName = (typeof EVENT_NAMES)[number] + +export const CIPHERBID_EVENT_SELECTORS = Object.freeze( + Object.fromEntries(EVENT_NAMES.map((name) => [name, hash.getSelectorFromName(name)])) as Record< + CipherBidEventName, + string + >, +) + +export type ReceiptProvider = Readonly<{ + waitForTransaction: (transactionHash: string) => Promise +}> + +type ReceiptEvent = Readonly<{ + fromAddress: string + keys: readonly string[] + data: readonly string[] +}> + +type NormalizedReceipt = Readonly<{ + executionStatus: string + finalityStatus: string + blockHash: string + blockNumber: number + events: readonly ReceiptEvent[] +}> + +export class TransactionUnconfirmedError extends Error { + override name = 'TransactionUnconfirmedError' +} + +function record(value: unknown, label: string): Record { + if (typeof value !== 'object' || value === null || Array.isArray(value)) throw new Error(`${label} is malformed`) + return value as Record +} + +function stringField(source: Record, snake: string, camel: string): string { + const value = source[snake] ?? source[camel] + if (typeof value !== 'string' || value.length === 0) throw new Error(`Receipt ${snake} is missing`) + return value +} + +function normalizeReceipt(value: unknown): NormalizedReceipt { + const receipt = record(value, 'Receipt') + const rawEvents = receipt.events + if (!Array.isArray(rawEvents)) throw new Error('Receipt events are missing') + const events = rawEvents.map((rawEvent, index): ReceiptEvent => { + const event = record(rawEvent, `Receipt event ${index}`) + const fromAddress = stringField(event, 'from_address', 'fromAddress') + if (!Array.isArray(event.keys) || !event.keys.every((key) => typeof key === 'string')) { + throw new Error(`Receipt event ${index} keys are malformed`) + } + if (!Array.isArray(event.data) || !event.data.every((item) => typeof item === 'string')) { + throw new Error(`Receipt event ${index} data are malformed`) + } + return { fromAddress, keys: event.keys as string[], data: event.data as string[] } + }) + const rawBlockNumber = receipt.block_number ?? receipt.blockNumber + if (typeof rawBlockNumber !== 'number' || !Number.isSafeInteger(rawBlockNumber) || rawBlockNumber < 0) { + throw new Error('Receipt block_number is malformed') + } + return { + executionStatus: stringField(receipt, 'execution_status', 'executionStatus'), + finalityStatus: stringField(receipt, 'finality_status', 'finalityStatus'), + blockHash: stringField(receipt, 'block_hash', 'blockHash'), + blockNumber: rawBlockNumber, + events, + } +} + +function sameFelt(left: string, right: string): boolean { + try { + return BigInt(left) === BigInt(right) + } catch { + return false + } +} + +async function waitWithTimeout( + provider: ReceiptProvider, + transactionHash: string, + timeoutMs: number, +): Promise { + if (!Number.isSafeInteger(timeoutMs) || timeoutMs <= 0) throw new Error('Receipt timeout must be a positive integer') + let timer: ReturnType | undefined + const timeout = new Promise((_, reject) => { + timer = setTimeout( + () => reject(new TransactionUnconfirmedError(`Transaction ${transactionHash} remains unconfirmed`)), + timeoutMs, + ) + }) + try { + return await Promise.race([provider.waitForTransaction(transactionHash), timeout]) + } finally { + if (timer) clearTimeout(timer) + } +} + +export async function verifyTransactionTransition( + input: Readonly<{ + provider: ReceiptProvider + manifest: DeploymentManifest + transactionHash: string + expectedEvent: CipherBidEventName + requirePoolTouch: boolean + readState: () => Promise + timeoutMs?: number + }>, +) { + if (!/^0x[0-9a-fA-F]+$/.test(input.transactionHash) || BigInt(input.transactionHash) === 0n) { + throw new Error('Transaction hash must be a non-zero hexadecimal felt') + } + const receipt = normalizeReceipt( + await waitWithTimeout(input.provider, input.transactionHash, input.timeoutMs ?? 120_000), + ) + if (receipt.executionStatus !== 'SUCCEEDED') { + throw new Error(`Transaction ${input.transactionHash} did not succeed`) + } + if (receipt.finalityStatus !== 'ACCEPTED_ON_L2' && receipt.finalityStatus !== 'ACCEPTED_ON_L1') { + throw new TransactionUnconfirmedError( + `Transaction ${input.transactionHash} has unconfirmed finality ${receipt.finalityStatus}`, + ) + } + + const expectedSelector = CIPHERBID_EVENT_SELECTORS[input.expectedEvent] + const cipherBidEventFound = receipt.events.some( + (event) => + sameFelt(event.fromAddress, input.manifest.auctionHouse) && + event.keys.length > 0 && + sameFelt(event.keys[0], expectedSelector), + ) + if (!cipherBidEventFound) throw new Error(`Expected CipherBid event ${input.expectedEvent} was not found`) + + const poolTouchFound = receipt.events.some((event) => sameFelt(event.fromAddress, input.manifest.strk20Pool)) + if (input.requirePoolTouch && !poolTouchFound) throw new Error('Expected STRK20 pool event was not found') + + const stateReadbackPassed = await input.readState() + if (!stateReadbackPassed) throw new Error('State readback did not confirm the requested transition') + + return Object.freeze({ + transactionHash: input.transactionHash, + finalityStatus: receipt.finalityStatus, + blockHash: receipt.blockHash, + blockNumber: receipt.blockNumber, + cipherBidEventFound, + poolTouchFound, + stateReadbackPassed, + }) +} diff --git a/web/src/features/transactions/transactionOrchestrator.ts b/web/src/features/transactions/transactionOrchestrator.ts new file mode 100644 index 0000000..9d50441 --- /dev/null +++ b/web/src/features/transactions/transactionOrchestrator.ts @@ -0,0 +1,143 @@ +import { TransactionUnconfirmedError } from '@/features/transactions/receiptVerifier' + +export type WalletSnapshot = Readonly<{ address: string; chainId: string }> + +export type TransactionErrorCode = + | 'ACTIVE_TRANSACTION' + | 'WALLET_CHANGED' + | 'WALLET_REJECTED' + | 'UNCONFIRMED' + | 'INVALID_TRANSACTION_HASH' + | 'FAILED' + +export type TransactionFlowStatus = + | 'preparing' + | 'awaiting_wallet' + | 'confirming' + | 'reading_state' + | 'succeeded' + | 'unconfirmed' + | 'failed' + +export type TransactionFlowState = Readonly<{ + operationId: string + status: TransactionFlowStatus + transactionHash?: string + errorCode?: TransactionErrorCode +}> + +export class TransactionFlowError extends Error { + override name = 'TransactionFlowError' + + constructor( + readonly code: TransactionErrorCode, + message: string, + ) { + super(message) + } +} + +type TransactionRunInput = Readonly<{ + operationId: string + expectedWallet: WalletSnapshot + getWalletSnapshot: () => Promise + prepare: () => Promise + submit: (prepared: Prepared) => Promise + verify: (transactionHash: string) => Promise +}> + +type Listener = (state: TransactionFlowState) => void + +function sameWallet(actual: WalletSnapshot, expected: WalletSnapshot): boolean { + try { + return BigInt(actual.address) === BigInt(expected.address) && BigInt(actual.chainId) === BigInt(expected.chainId) + } catch { + return false + } +} + +function assertWallet(actual: WalletSnapshot, expected: WalletSnapshot): void { + if (!sameWallet(actual, expected)) { + throw new TransactionFlowError('WALLET_CHANGED', 'Connected wallet or network changed during the transaction') + } +} + +function rejectedByWallet(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false + const code = (error as Record).code + return code === 4001 || code === '4001' || code === 'ACTION_REJECTED' || code === 'USER_REJECTED' +} + +function classifyError(error: unknown): TransactionFlowError { + if (error instanceof TransactionFlowError) return error + if (error instanceof TransactionUnconfirmedError) { + return new TransactionFlowError('UNCONFIRMED', 'Transaction remains unconfirmed') + } + if (rejectedByWallet(error)) { + return new TransactionFlowError('WALLET_REJECTED', 'Wallet request was rejected') + } + return new TransactionFlowError('FAILED', 'Transaction flow failed') +} + +export class TransactionOrchestrator { + readonly #listeners = new Set() + #active = false + #state: TransactionFlowState | null = null + + get active(): boolean { + return this.#active + } + + get state(): TransactionFlowState | null { + return this.#state + } + + subscribe(listener: Listener): () => void { + this.#listeners.add(listener) + return () => this.#listeners.delete(listener) + } + + #publish(state: TransactionFlowState): void { + this.#state = Object.freeze({ ...state }) + for (const listener of this.#listeners) listener(this.#state) + } + + async run(input: TransactionRunInput): Promise { + if (this.#active) { + throw new TransactionFlowError('ACTIVE_TRANSACTION', 'Another transaction is already active') + } + if (!input.operationId.trim()) throw new TransactionFlowError('FAILED', 'Operation ID is required') + + this.#active = true + let transactionHash: string | undefined + try { + this.#publish({ operationId: input.operationId, status: 'preparing' }) + assertWallet(await input.getWalletSnapshot(), input.expectedWallet) + const prepared = await input.prepare() + + this.#publish({ operationId: input.operationId, status: 'awaiting_wallet' }) + assertWallet(await input.getWalletSnapshot(), input.expectedWallet) + transactionHash = await input.submit(prepared) + if (!/^0x[0-9a-fA-F]+$/.test(transactionHash) || BigInt(transactionHash) === 0n) { + throw new TransactionFlowError('INVALID_TRANSACTION_HASH', 'Wallet returned an invalid transaction hash') + } + + this.#publish({ operationId: input.operationId, status: 'confirming', transactionHash }) + this.#publish({ operationId: input.operationId, status: 'reading_state', transactionHash }) + const verified = await input.verify(transactionHash) + this.#publish({ operationId: input.operationId, status: 'succeeded', transactionHash }) + return verified + } catch (error) { + const classified = classifyError(error) + this.#publish({ + operationId: input.operationId, + status: classified.code === 'UNCONFIRMED' ? 'unconfirmed' : 'failed', + ...(transactionHash ? { transactionHash } : {}), + errorCode: classified.code, + }) + throw classified + } finally { + this.#active = false + } + } +} diff --git a/web/src/features/wallet/WalletConnectPanel.tsx b/web/src/features/wallet/WalletConnectPanel.tsx index e88048a..66d83d2 100644 --- a/web/src/features/wallet/WalletConnectPanel.tsx +++ b/web/src/features/wallet/WalletConnectPanel.tsx @@ -1,11 +1,12 @@ 'use client' -import { useState, useSyncExternalStore } from 'react' +import { useEffect, useState, useSyncExternalStore } from 'react' import { createStore } from '@starknet-io/get-starknet-discovery' import { browserWalletDependencies } from './browserWalletDependencies' import { connectPrivacyWallet, type PrivacyWalletConnection } from './walletConnection' import { useWalletStore } from './walletStore' -import { createSepoliaProvider, isSepoliaChainId } from '@/lib/starknet/network' +import { createSepoliaProvider, SEPOLIA_CHAIN_ID } from '@/lib/starknet/network' +import { MAINNET_CHAIN_ID } from '@/config/deployment' type WalletDiscovery = Readonly<{ getWallets: () => readonly unknown[] @@ -16,7 +17,11 @@ type WalletConnectPanelProps = Readonly<{ createDiscovery?: () => WalletDiscovery provider?: unknown connect?: (wallet: unknown, provider: unknown) => Promise - onConnected: () => void + subscribeWalletChanges?: (wallet: unknown, onChange: () => void) => () => void + expectedChainId?: string + expectedNetworkLabel?: string + onConnected?: (connection: PrivacyWalletConnection) => void + onDisconnected?: () => void }> const EMPTY_WALLETS: readonly unknown[] = [] @@ -36,10 +41,32 @@ function walletName(wallet: unknown): string { return wallet.name } +function walletInitial(name: string): string { + const initial = [...name].find((character) => /\S/.test(character)) + return (initial ?? 'W').toUpperCase() +} + +function walletOptionTestId(wallet: unknown): string { + return `wallet-option-${walletName(wallet) + .toLowerCase() + .replaceAll(/[^a-z0-9]+/g, '-') + .replaceAll(/(^-|-$)/g, '')}` +} + function isPickable(wallet: unknown): boolean { return !walletName(wallet).toLowerCase().includes('metamask') } +function normalizedChainId(chainId: string): string { + if (chainId === 'SN_SEPOLIA') return SEPOLIA_CHAIN_ID + if (chainId === 'SN_MAIN') return MAINNET_CHAIN_ID + try { + return `0x${BigInt(chainId).toString(16)}` + } catch { + return chainId + } +} + const defaultConnect = (wallet: unknown, provider: unknown) => connectPrivacyWallet(wallet, provider, browserWalletDependencies) const defaultSepoliaProvider = createSepoliaProvider() @@ -48,7 +75,11 @@ export function WalletConnectPanel({ createDiscovery = createBrowserDiscovery, provider = defaultSepoliaProvider, connect = defaultConnect, + subscribeWalletChanges = browserWalletDependencies.subscribeWalletChanges, + expectedChainId = SEPOLIA_CHAIN_ID, + expectedNetworkLabel = 'Starknet Sepolia', onConnected, + onDisconnected, }: WalletConnectPanelProps) { const [discovery] = useState(createDiscovery) const [walletSnapshot] = useState(() => { @@ -69,54 +100,198 @@ export function WalletConnectPanel({ ) const wallets = discoveredWallets.filter(isPickable) const status = useWalletStore((state) => state.status) + const connectedWalletName = useWalletStore((state) => state.walletName) const address = useWalletStore((state) => state.address) + const chainId = useWalletStore((state) => state.chainId) + const walletApiVersions = useWalletStore((state) => state.walletApiVersions) + const supportsStrk20 = useWalletStore((state) => state.supportsStrk20) const error = useWalletStore((state) => state.error) + const [connectedWallet, setConnectedWallet] = useState(null) + const [pendingWalletName, setPendingWalletName] = useState(null) + + useEffect(() => { + if (status !== 'connected' || connectedWallet === null) return + + return subscribeWalletChanges(connectedWallet, () => { + setConnectedWallet(null) + useWalletStore.getState().invalidateConnection('Wallet account or capabilities changed. Reconnect to continue.') + onDisconnected?.() + }) + }, [connectedWallet, onDisconnected, status, subscribeWalletChanges]) async function selectWallet(wallet: unknown) { - useWalletStore.getState().beginConnection() + const attempt = useWalletStore.getState().beginConnection() + setPendingWalletName(walletName(wallet)) try { const connection = await connect(wallet, provider) - if (!isSepoliaChainId(connection.chainId)) { - useWalletStore.getState().failConnection('Switch the wallet to Starknet Sepolia and try again.') + if (normalizedChainId(connection.chainId) !== normalizedChainId(expectedChainId)) { + useWalletStore.getState().failConnection(attempt, `Switch the wallet to ${expectedNetworkLabel} and try again.`) return } if (!connection.supportsStrk20) { - useWalletStore.getState().failConnection('Wallet API 0.10.3 or newer is required for STRK20.') + useWalletStore.getState().failConnection(attempt, 'Wallet API 0.10.3 or newer is required for STRK20.') return } - useWalletStore.getState().completeConnection(connection) - onConnected() + const completed = useWalletStore.getState().completeConnection(attempt, { + ...connection, + walletName: walletName(wallet), + }) + if (!completed) return + setConnectedWallet(wallet) + onConnected?.(connection) } catch { - useWalletStore.getState().failConnection('Wallet connection failed or was rejected.') + useWalletStore.getState().failConnection(attempt, 'Wallet connection failed or was rejected.') + } finally { + setPendingWalletName(null) } } + function disconnectWallet() { + setConnectedWallet(null) + useWalletStore.getState().disconnect() + onDisconnected?.() + } + if (status === 'connected' && address) { return ( -
-

Connected on Starknet Sepolia

- {address} +
+
+
+
+

+ Wallet access +

+

Wallet connected

+
+ + +
+
+
+
Wallet
+
{connectedWalletName}
+
+
+
Chain
+
{chainId}
+
+
+
Account
+
+ {address} +
+
+
+
+ Wallet API +
+
{walletApiVersions.join(', ')}
+
+
+
+
) } return ( -
-

Connect a privacy-capable wallet

- {wallets.length === 0 ?

No Starknet wallet detected. Install or unlock Ready, then refresh.

: null} -
+
+
+ + C + +
+

+ Wallet access +

+

Connect a privacy-capable wallet

+

+ Choose a supported wallet to verify your network and STRK20 capability. +

+
+
+ {wallets.length === 0 ? ( +

+ No Starknet wallet detected. Install or unlock Ready, then refresh. +

+ ) : null} + {status === 'connecting' ? ( +
+

Connecting to {pendingWalletName ?? 'wallet'}…

+ +
+ ) : null} +
{wallets.map((wallet, index) => ( ))}
- {error ?

{error}

: null} + {error ? ( +

+ {error} +

+ ) : null}
) } diff --git a/web/src/features/wallet/browserWalletDependencies.ts b/web/src/features/wallet/browserWalletDependencies.ts index 42f55aa..3aad357 100644 --- a/web/src/features/wallet/browserWalletDependencies.ts +++ b/web/src/features/wallet/browserWalletDependencies.ts @@ -27,4 +27,11 @@ export const browserWalletDependencies: WalletConnectionDependencies = { return result.map(String) }, normalizeAddress: (address) => validateAndParseAddress(address), + subscribeWalletChanges: (wallet, onChange) => { + const events = asWallet(wallet).features?.['standard:events'] + if (!events) return () => undefined + return events.on('change', (changes) => { + if (changes.accounts || changes.chains || changes.features) onChange() + }) + }, } diff --git a/web/src/features/wallet/walletConnection.ts b/web/src/features/wallet/walletConnection.ts index 8d638d2..7c8e96e 100644 --- a/web/src/features/wallet/walletConnection.ts +++ b/web/src/features/wallet/walletConnection.ts @@ -1,4 +1,15 @@ import { supportsWalletApiVersion } from './walletCapabilities' +import { MAINNET_CHAIN_ID, SEPOLIA_CHAIN_ID } from '@/config/deployment' + +function normalizeChainId(chainId: string): string { + if (chainId === 'SN_MAIN') return MAINNET_CHAIN_ID + if (chainId === 'SN_SEPOLIA') return SEPOLIA_CHAIN_ID + try { + return `0x${BigInt(chainId).toString(16)}` + } catch { + return chainId + } +} export type WalletConnectionDependencies = { createAccount: (provider: unknown, wallet: unknown) => Promise @@ -7,6 +18,7 @@ export type WalletConnectionDependencies = { requestChainId: (wallet: unknown) => Promise supportedWalletApi: (wallet: unknown) => Promise normalizeAddress: (address: string) => string + subscribeWalletChanges: (wallet: unknown, onChange: () => void) => () => void } export type PrivacyWalletConnection = Readonly<{ @@ -38,7 +50,7 @@ export async function connectPrivacyWallet( throw new Error('Wallet account permission was not granted') } - const [chainId, walletApiVersions] = await Promise.all([ + const [rawChainId, walletApiVersions] = await Promise.all([ dependencies.requestChainId(wallet), dependencies.supportedWalletApi(wallet), ]) @@ -51,7 +63,7 @@ export async function connectPrivacyWallet( return { account, address: address as `0x${string}`, - chainId, + chainId: normalizeChainId(rawChainId), walletApiVersions: [...walletApiVersions], supportsStrk20: supportsWalletApiVersion(walletApiVersions), } diff --git a/web/src/features/wallet/walletStore.ts b/web/src/features/wallet/walletStore.ts index d66b470..1eae56f 100644 --- a/web/src/features/wallet/walletStore.ts +++ b/web/src/features/wallet/walletStore.ts @@ -5,6 +5,7 @@ import { create } from 'zustand' type WalletStatus = 'disconnected' | 'connecting' | 'connected' | 'error' export type PublicWalletConnection = Readonly<{ + walletName: string address: `0x${string}` chainId: string walletApiVersions: readonly string[] @@ -13,42 +14,66 @@ export type PublicWalletConnection = Readonly<{ type WalletState = { status: WalletStatus + connectionAttempt: number + walletName: string | null address: `0x${string}` | null chainId: string | null walletApiVersions: readonly string[] supportsStrk20: boolean error: string | null - beginConnection: () => void - completeConnection: (connection: PublicWalletConnection) => void - failConnection: (message: string) => void + beginConnection: () => number + completeConnection: (attempt: number, connection: PublicWalletConnection) => boolean + failConnection: (attempt: number, message: string) => boolean + invalidateConnection: (message: string) => void disconnect: () => void } -const disconnectedState = { - status: 'disconnected' as const, - address: null, - chainId: null, - walletApiVersions: [] as readonly string[], - supportsStrk20: false, - error: null, +function disconnectedState(connectionAttempt: number) { + return { + status: 'disconnected' as const, + connectionAttempt, + walletName: null, + address: null, + chainId: null, + walletApiVersions: [] as readonly string[], + supportsStrk20: false, + error: null, + } } /** * Public, non-persisted wallet state adapted from the MIT STRK20 starter kit. * Wallet/key objects and private balances are deliberately excluded. */ -export const useWalletStore = create((set) => ({ - ...disconnectedState, - beginConnection: () => set({ ...disconnectedState, status: 'connecting' }), - completeConnection: (connection) => +export const useWalletStore = create((set, get) => ({ + ...disconnectedState(0), + beginConnection: () => { + const attempt = get().connectionAttempt + 1 + set({ ...disconnectedState(attempt), status: 'connecting' }) + return attempt + }, + completeConnection: (attempt, connection) => { + if (get().connectionAttempt !== attempt) return false set({ status: 'connected', + connectionAttempt: attempt, + walletName: connection.walletName, address: connection.address, chainId: connection.chainId, walletApiVersions: [...connection.walletApiVersions], supportsStrk20: connection.supportsStrk20, error: null, - }), - failConnection: (message) => set({ ...disconnectedState, status: 'error', error: message }), - disconnect: () => set(disconnectedState), + }) + return true + }, + failConnection: (attempt, message) => { + if (get().connectionAttempt !== attempt) return false + set({ ...disconnectedState(attempt), status: 'error', error: message }) + return true + }, + invalidateConnection: (message) => { + const attempt = get().connectionAttempt + 1 + set({ ...disconnectedState(attempt), status: 'error', error: message }) + }, + disconnect: () => set(disconnectedState(get().connectionAttempt + 1)), })) diff --git a/web/tests/e2e/auction-bid-preview.spec.ts b/web/tests/e2e/auction-bid-preview.spec.ts index ed2abda..28556bc 100644 --- a/web/tests/e2e/auction-bid-preview.spec.ts +++ b/web/tests/e2e/auction-bid-preview.spec.ts @@ -1,8 +1,6 @@ import { expect, test } from '@playwright/test' -const route = '/auctions/design-preview' - -test('renders the desktop auction bid preview without runtime errors', async ({ page }) => { +test('renders the live-chain home entry point without runtime errors', async ({ page }) => { const errors: string[] = [] page.on('console', (message) => { if (message.type() === 'error') errors.push(message.text()) @@ -10,39 +8,34 @@ test('renders the desktop auction bid preview without runtime errors', async ({ page.on('pageerror', (error) => errors.push(error.message)) await page.setViewportSize({ width: 1280, height: 900 }) - const response = await page.goto(route) + const response = await page.goto('/') expect(response?.status()).toBe(200) - await expect(page.getByRole('heading', { level: 1, name: 'A genuinely sealed NFT auction' })).toBeVisible() - await expect(page.getByRole('button', { name: 'Bidding unavailable in design preview' })).toBeDisabled() - await expect(page.getByText('0x0254a6b2997ef52e9f830ce1f543f6b29768295e8d17e2267d672c552cfe0d91')).toBeVisible() - - const bidCard = page.getByTestId('bid-preview-card') - await expect(bidCard).toHaveCSS('position', 'sticky') - const initialBidBox = await bidCard.boundingBox() - await page.evaluate(() => window.scrollTo(0, 1200)) - await expect.poll(async () => (await bidCard.boundingBox())?.y ?? Number.POSITIVE_INFINITY).toBeLessThanOrEqual(40) - expect(initialBidBox?.y ?? 0).toBeGreaterThan(40) - await expect(page.locator('.cipherbid-auction-art')).toHaveCSS('background-image', /radial-gradient/) + await expect( + page.getByRole('heading', { level: 1, name: 'Private bids. Guaranteed onchain delivery.' }), + ).toBeVisible() + await expect(page.getByRole('heading', { level: 2, name: 'Open an auction' })).toBeVisible() + await expect(page.getByRole('link', { name: 'Create an auction' })).toHaveAttribute('href', '/create') + await expect(async () => { + await page.getByLabel('Auction ID').fill('7') + expect(await page.getByRole('link', { name: 'Open auction' }).getAttribute('href')).toBe('/auction?id=7') + }).toPass() expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBe(0) expect(errors).toEqual([]) }) -test('stacks the lot, bid card, and facts at a true mobile viewport', async ({ page }) => { +test('stacks the live-chain entry point at a true mobile viewport', async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }) - await page.goto(route) + await page.goto('/') expect(await page.evaluate(() => window.innerWidth)).toBe(390) - const lot = await page.getByTestId('auction-lot').boundingBox() - const bid = await page.getByTestId('bid-preview-card').boundingBox() - const facts = await page.getByRole('region', { name: 'Auction facts' }).boundingBox() + const heading = await page.getByRole('heading', { level: 1 }).boundingBox() + const auctionPanel = await page.locator('#open-auction').boundingBox() + expect(heading).not.toBeNull() + expect(auctionPanel).not.toBeNull() + expect(heading!.y).toBeLessThan(auctionPanel!.y) + await expect(page.getByRole('link', { name: 'Open auction' })).toHaveCSS('min-height', '48px') - expect(lot).not.toBeNull() - expect(bid).not.toBeNull() - expect(facts).not.toBeNull() - await expect(page.getByTestId('bid-preview-card')).toHaveCSS('position', 'static') - expect(lot!.y).toBeLessThan(bid!.y) - expect(bid!.y).toBeLessThan(facts!.y) const overflow = await page.evaluate(() => { const viewportWidth = document.documentElement.clientWidth return [...document.querySelectorAll('body *')] @@ -62,7 +55,7 @@ test('stacks the lot, bid card, and facts at a true mobile viewport', async ({ p test('removes decorative transitions in reduced-motion mode', async ({ page }) => { await page.emulateMedia({ reducedMotion: 'reduce' }) - await page.goto(route) + await page.goto('/') const movingElements = await page.evaluate(() => [...document.querySelectorAll('.cipherbid-auction-page *')] @@ -79,7 +72,7 @@ test('removes decorative transitions in reduced-motion mode', async ({ page }) = expect(movingElements).toEqual([]) }) -test('keeps keyboard order logical and route ids inert', async ({ page }) => { +test('rejects hostile route IDs as inert text', async ({ page }) => { const payload = 'design-preview' const dialogs: string[] = [] page.on('dialog', async (dialog) => { @@ -87,15 +80,14 @@ test('keeps keyboard order logical and route ids inert', async ({ page }) => { await dialog.dismiss() }) - await page.goto(`/auctions/${encodeURIComponent(payload)}`) - await expect(page.locator('main code')).toHaveText(payload) + const response = await page.goto(`/auction?id=${encodeURIComponent(payload)}`) + expect(response?.status()).toBe(200) + await expect(page.getByRole('heading', { level: 1, name: 'Live auction unavailable' })).toBeVisible() + await expect(page.getByRole('alert')).toContainText('Auction ID must be a positive u64 decimal value.') + await expect(page.locator('main')).toContainText(payload) await expect(page.locator('main script')).toHaveCount(0) expect(dialogs).toEqual([]) await page.keyboard.press('Tab') await expect(page.getByRole('link', { name: 'CipherBid' })).toBeFocused() - await page.keyboard.press('Tab') - await expect(page.getByRole('link', { name: 'How privacy works' })).toBeFocused() - await page.keyboard.press('Tab') - await expect(page.getByRole('link', { name: 'Auctions' })).toBeFocused() }) diff --git a/web/tests/e2e/feasibility.spec.ts b/web/tests/e2e/feasibility.spec.ts index 7a7f7b0..a0aa35a 100644 --- a/web/tests/e2e/feasibility.spec.ts +++ b/web/tests/e2e/feasibility.spec.ts @@ -1,10 +1,27 @@ import { expect, test } from '@playwright/test' -test('renders the Sepolia feasibility gate without requesting private wallet state', async ({ page }) => { - await page.goto('/') +test('renders the deployment-bound seller route without requesting private wallet state', async ({ page }) => { + const response = await page.goto('/create') - await expect(page.getByRole('heading', { level: 1, name: 'CipherBid feasibility gate' })).toBeVisible() + expect(response?.status()).toBe(200) + await expect(page.getByRole('heading', { level: 1, name: 'Create a private-bid NFT auction' })).toBeVisible() await expect(page.getByRole('heading', { level: 2, name: 'Connect a privacy-capable wallet' })).toBeVisible() await expect(page.getByText('No Starknet wallet detected. Install or unlock Ready, then refresh.')).toBeVisible() - await expect(page.getByText(/viewing keys, shielded balances, and raw wallet errors never enter/)).toBeVisible() + await expect(page.getByText(/encrypted recovery is downloaded and import-verified/i)).toBeVisible() + await expect(page.getByRole('button', { name: 'Create auction with NFT custody' })).toBeDisabled() + expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBe(0) +}) + +test('renders the verified bidder shield setup without reading private balances', async ({ page }) => { + const response = await page.goto('/demo/setup') + + expect(response?.status()).toBe(200) + await expect( + page.getByRole('heading', { level: 1, name: 'Shield both demo bidders before the timer starts' }), + ).toBeVisible() + await expect(page.getByText('Starknet Sepolia demo preparation')).toBeVisible() + await expect(page.getByText('No Starknet wallet detected. Install or unlock Ready, then refresh.')).toBeVisible() + await expect(page.getByRole('button', { name: 'Connect Bidder A or Bidder B' })).toBeDisabled() + await expect(page.getByText(/never receives a viewing key/i)).toBeVisible() + expect(await page.evaluate(() => document.documentElement.scrollWidth - document.documentElement.clientWidth)).toBe(0) }) diff --git a/web/tests/fixtures/auction-configuration-v2.json b/web/tests/fixtures/auction-configuration-v2.json new file mode 100644 index 0000000..78b1e76 --- /dev/null +++ b/web/tests/fixtures/auction-configuration-v2.json @@ -0,0 +1,56 @@ +{ + "schema": "cipherbid.auction-configuration.v2", + "house": { + "pool": "0x123", + "paymentToken": "0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d", + "maxBidders": 32 + }, + "auction": { + "auctionId": "7", + "seller": "0xabc", + "sellerClaimHandle": "0x54b096c60c80fd98e2e6f2495db67227a0c8c2bdc69733df8fa23a4a5eb0e28", + "nftContract": "0xdef", + "tokenId": "0", + "reservePrice": "1000", + "collateralCap": "5000", + "biddingDeadline": "2000", + "revealDeadline": "3000", + "bidderLimit": 2 + }, + "creation": { + "caller": "0xabc", + "now": "1000" + }, + "types": { + "auctionId": "u64_nonzero", + "seller": "ContractAddress_nonzero", + "sellerClaimHandle": "felt252_nonzero", + "nftContract": "ContractAddress_nonzero", + "tokenId": "u256", + "paymentToken": "ContractAddress_canonical_STRK", + "pool": "ContractAddress_nonzero", + "reservePrice": "u128_nonzero", + "collateralCap": "u128_nonzero", + "biddingDeadline": "u64_unix_seconds", + "revealDeadline": "u64_unix_seconds", + "bidderLimit": "u16_2_to_house_max" + }, + "boundaries": { + "contractAddressBound": "0x800000000000000000000000000000000000000000000000000000000000000" + }, + "semantics": { + "auctionIdScope": ["chain_id", "auction_house_address", "auction_id"], + "auctionIdReuse": "forbidden_forever", + "sellerSource": "creation_caller", + "sellerClaimHandleMutable": false, + "sellerClaimSecretStored": false, + "poolDiffersFromPaymentToken": true, + "tokenIdZeroAllowed": true, + "biddingOpen": "now < bidding_deadline", + "revealOpen": "bidding_deadline <= now < reveal_deadline", + "settlementOpen": "now >= reveal_deadline", + "houseConfigurationMutable": false, + "auctionConfigurationMutable": false, + "lifecycleStateMutable": true + } +} diff --git a/web/tests/fixtures/auction-lifecycle-v1.json b/web/tests/fixtures/auction-lifecycle-v1.json new file mode 100644 index 0000000..63c087f --- /dev/null +++ b/web/tests/fixtures/auction-lifecycle-v1.json @@ -0,0 +1,108 @@ +{ + "schema": "cipherbid.auction-lifecycle.v1", + "states": ["BiddingOpen", "RevealOpen", "ReadyToSettle", "SettledSold", "SettledNoSale", "ClaimsComplete"], + "phaseBoundaries": { + "biddingOpen": "now < bidding_deadline", + "revealOpen": "bidding_deadline <= now < reveal_deadline", + "readyToSettle": "now >= reveal_deadline" + }, + "rules": { + "tieBreak": "lowest_accepted_index", + "oneValidRevealPrice": "reserve", + "noReserveBid": "no_sale", + "unrevealedBid": "full_cap_refund", + "winnerSurplus": "cap - clearing_price", + "loserRefund": "cap", + "sellerEntitlement": "clearing_price", + "zeroWinnerSurplus": "auto_consumed_no_transaction", + "noSaleNftRecipient": "seller", + "soldNftRecipient": "winning_committed_recipient", + "claimConsumption": "exactly_once" + }, + "scenarios": [ + { + "name": "canonical-two-bidder-sold", + "reserve": "2", + "cap": "5", + "bids": [ + { "acceptedIndex": 0, "commitment": "101", "amount": "3" }, + { "acceptedIndex": 1, "commitment": "202", "amount": "4" } + ], + "expected": { + "sold": true, + "winnerIndex": 1, + "clearingPrice": "3", + "bidderClaims": [ + { "acceptedIndex": 0, "kind": "loser_refund", "amount": "5" }, + { "acceptedIndex": 1, "kind": "winner_surplus", "amount": "2" } + ], + "sellerEntitlement": "3", + "lockedCollateral": "10", + "distributedValue": "10" + } + }, + { + "name": "equal-bid-earliest-wins", + "reserve": "2", + "cap": "5", + "bids": [ + { "acceptedIndex": 0, "commitment": "301", "amount": "4" }, + { "acceptedIndex": 1, "commitment": "302", "amount": "4" } + ], + "expected": { + "sold": true, + "winnerIndex": 0, + "clearingPrice": "4", + "bidderClaims": [ + { "acceptedIndex": 0, "kind": "winner_surplus", "amount": "1" }, + { "acceptedIndex": 1, "kind": "loser_refund", "amount": "5" } + ], + "sellerEntitlement": "4", + "lockedCollateral": "10", + "distributedValue": "10" + } + }, + { + "name": "one-valid-reveal", + "reserve": "2", + "cap": "5", + "bids": [ + { "acceptedIndex": 0, "commitment": "401", "amount": "3" }, + { "acceptedIndex": 1, "commitment": "402", "amount": null } + ], + "expected": { + "sold": true, + "winnerIndex": 0, + "clearingPrice": "2", + "bidderClaims": [ + { "acceptedIndex": 0, "kind": "winner_surplus", "amount": "3" }, + { "acceptedIndex": 1, "kind": "loser_refund", "amount": "5" } + ], + "sellerEntitlement": "2", + "lockedCollateral": "10", + "distributedValue": "10" + } + }, + { + "name": "no-bid-meets-reserve", + "reserve": "3", + "cap": "5", + "bids": [ + { "acceptedIndex": 0, "commitment": "501", "amount": "2" }, + { "acceptedIndex": 1, "commitment": "502", "amount": null } + ], + "expected": { + "sold": false, + "winnerIndex": null, + "clearingPrice": "0", + "bidderClaims": [ + { "acceptedIndex": 0, "kind": "loser_refund", "amount": "5" }, + { "acceptedIndex": 1, "kind": "loser_refund", "amount": "5" } + ], + "sellerEntitlement": "0", + "lockedCollateral": "10", + "distributedValue": "10" + } + } + ] +} diff --git a/web/tests/fixtures/bid-credentials-v1.json b/web/tests/fixtures/bid-credentials-v1.json new file mode 100644 index 0000000..c9c6043 --- /dev/null +++ b/web/tests/fixtures/bid-credentials-v1.json @@ -0,0 +1,86 @@ +{ + "schema": "cipherbid.bid-credentials.v1", + "domains": { + "claim": { + "literal": "CIPHERBID_CLAIM_V1", + "felt": "0x4349504845524249445f434c41494d5f5631" + }, + "bid": { + "literal": "CIPHERBID_BID_V1", + "felt": "0x4349504845524249445f4249445f5631" + } + }, + "boundaries": { + "feltPrime": "0x800000000000011000000000000000000000000000000000000000000000001", + "contractAddressBound": "0x800000000000000000000000000000000000000000000000000000000000000", + "u64Max": "18446744073709551615", + "u128Max": "340282366920938463463374607431768211455" + }, + "preimages": { + "claimHandle": ["claim_domain", "claim_secret"], + "bidCommitment": [ + "bid_domain", + "chain_id", + "auction_house", + "auction_id", + "amount", + "bid_nonce", + "claim_handle", + "asset_recipient" + ] + }, + "vectors": [ + { + "name": "sepolia-reference", + "claimSecret": "123456789", + "claimHandle": "0x3078725b5aaffe73f545ebca32c0b5a4af14404599edd691c752e59ffca3724", + "chainId": "0x534e5f5345504f4c4941", + "auctionHouse": "0x222", + "auctionId": "7", + "amount": "3000000000000000000", + "bidNonce": "987654321", + "assetRecipient": "0x333", + "commitment": "0x34fe5ddb49c604d4b8b63f768c4d6e4159bdd4166bdc3e1e7094217c9f6313e" + }, + { + "name": "minimum-valid", + "claimSecret": "1", + "claimHandle": "0x6b7f8ff6dee712dbd900e4e0269931a6dc86de5359e13dc740ca1898d110b48", + "chainId": "1", + "auctionHouse": "1", + "auctionId": "1", + "amount": "1", + "bidNonce": "1", + "assetRecipient": "1", + "commitment": "0x5c8b0026c8ddfd09e47cba64881b66d371c620d84b0e573f811ec2334526848" + }, + { + "name": "maximum-valid", + "claimSecret": "0x800000000000011000000000000000000000000000000000000000000000000", + "claimHandle": "0x51f784d5ce10bdf76e3c632882ba6e181464bd8f4493fd9e7bfc44c6deefd34", + "chainId": "0x800000000000011000000000000000000000000000000000000000000000000", + "auctionHouse": "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "auctionId": "18446744073709551615", + "amount": "340282366920938463463374607431768211455", + "bidNonce": "0x800000000000011000000000000000000000000000000000000000000000000", + "assetRecipient": "0x7ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + "commitment": "0x1dc855fa1871e1425360884f6b03c77837f2c5d47e551f86b55af0e0f8fa1b5" + } + ], + "invalid": { + "claimSecret": ["-1", "0", "0x800000000000011000000000000000000000000000000000000000000000001"], + "chainId": ["-1", "0", "0x800000000000011000000000000000000000000000000000000000000000001"], + "auctionHouse": ["-1", "0", "0x800000000000000000000000000000000000000000000000000000000000000"], + "auctionId": ["-1", "0", "18446744073709551616"], + "amount": ["-1", "0", "340282366920938463463374607431768211456"], + "bidNonce": ["-1", "0", "0x800000000000011000000000000000000000000000000000000000000000001"], + "claimHandle": ["-1", "0", "0x800000000000011000000000000000000000000000000000000000000000001"], + "assetRecipient": ["-1", "0", "0x800000000000000000000000000000000000000000000000000000000000000"] + }, + "credentialModel": { + "private": ["bid_nonce", "claim_secret"], + "derivedPublic": ["claim_handle", "bid_commitment"], + "publicAtReveal": ["amount", "bid_nonce", "claim_handle", "asset_recipient"], + "supersededFieldsForbidden": ["claim_signing_key", "claim_public_key", "claim_signature"] + } +} diff --git a/web/tests/fixtures/bid-ingress-v1.json b/web/tests/fixtures/bid-ingress-v1.json new file mode 100644 index 0000000..cbf3a94 --- /dev/null +++ b/web/tests/fixtures/bid-ingress-v1.json @@ -0,0 +1,117 @@ +{ + "schema": "cipherbid.bid-ingress.v1", + "route": "withdraw_then_invoke", + "sample": { + "auctionId": "7", + "paymentToken": "0x111", + "cap": "5", + "commitment": "101", + "claimHandle": "202", + "auctionHouse": "0x222" + }, + "actions": [ + { + "type": "withdraw", + "token": "0x111", + "amount": "0x5", + "recipient": "0x222" + }, + { + "type": "invoke", + "contract": "0x222", + "calldata": ["0x0", "0x7", "0x65", "0xca", "0x0", "0x0", "${poolAddress}", "0x0"] + } + ], + "placeholders": { + "poolAddress": ["actions[1].calldata[6]"], + "openNoteIds": [] + }, + "privacyInvoke": { + "selector": "privacy_invoke", + "arguments": [ + { + "index": 0, + "name": "operation", + "cairoType": "u8", + "semantic": "PLACE_BID", + "sampleValue": "0x0" + }, + { + "index": 1, + "name": "auction_id", + "cairoType": "u64", + "semantic": "auction identifier", + "sampleValue": "0x7" + }, + { + "index": 2, + "name": "primary_value", + "cairoType": "felt252", + "semantic": "domain-separated sealed bid commitment", + "sampleValue": "0x65" + }, + { + "index": 3, + "name": "claim_handle", + "cairoType": "felt252", + "semantic": "public one-time claim handle", + "sampleValue": "0xca" + }, + { + "index": 4, + "name": "reserved_0", + "cairoType": "felt252", + "semantic": "must be zero for PLACE_BID", + "sampleValue": "0x0" + }, + { + "index": 5, + "name": "reserved_1", + "cairoType": "felt252", + "semantic": "must be zero for PLACE_BID", + "sampleValue": "0x0" + }, + { + "index": 6, + "name": "pool_address", + "cairoType": "ContractAddress", + "semantic": "wallet-resolved configured pool address", + "sampleValue": "${poolAddress}" + }, + { + "index": 7, + "name": "open_note_id", + "cairoType": "felt252", + "semantic": "must be zero because bid ingress creates no open note", + "sampleValue": "0x0" + } + ], + "returnType": "Span", + "expectedLength": 0 + }, + "submission": { + "preflight": "strk20PrepareInvoke(actions, true)", + "preflightProof": "empty", + "submit": "strk20InvokeTransaction(actions)", + "proofGeneration": "wallet", + "signature": "wallet", + "broadcast": "wallet" + }, + "receipt": { + "requiredPoolEvents": ["Withdrawal", "ExternalContractInvoked"], + "requiredAuctionEvents": ["BidCommitted"], + "forbiddenPoolEvents": ["OpenNoteCreated", "OpenNoteDeposited"], + "walletResultFields": ["transaction_hash"] + }, + "privacyBoundary": { + "appReceives": ["transaction_hash"], + "appNeverReceives": [ + "viewing_key", + "private_notes", + "wallet_private_key", + "proof_data", + "proof_output", + "proof_facts" + ] + } +} diff --git a/web/tests/fixtures/lifecycle-routes-v2.json b/web/tests/fixtures/lifecycle-routes-v2.json new file mode 100644 index 0000000..4f51dec --- /dev/null +++ b/web/tests/fixtures/lifecycle-routes-v2.json @@ -0,0 +1,189 @@ +{ + "schema": "cipherbid.lifecycle-routes.v2", + "sample": { + "auctionId": "7", + "acceptedIndex": "0", + "amount": "3", + "bidNonce": "404", + "claimHandle": "202", + "claimSecret": "555", + "sellerClaimSecret": "777", + "sellerClaimHandle": "0x54b096c60c80fd98e2e6f2495db67227a0c8c2bdc69733df8fa23a4a5eb0e28", + "assetRecipient": "0x333", + "paymentToken": "0x111", + "auctionHouse": "0x222", + "walletRecipient": "0x444", + "sellerRecipient": "0x555", + "sellerOpenNoteId": "0x999", + "cap": "5", + "clearingPrice": "3" + }, + "directCalls": { + "reveal": { + "contractAddress": "0x222", + "entrypoint": "reveal_bid", + "calldata": ["0x7", "0x0", "0x3", "0x194", "0x333"] + }, + "settlement": { + "contractAddress": "0x222", + "entrypoint": "settle_auction", + "calldata": ["0x7"] + }, + "sellerProceedsAuthorization": { + "contractAddress": "0x222", + "entrypoint": "authorize_seller_proceeds", + "calldata": ["0x7", "0x54b096c60c80fd98e2e6f2495db67227a0c8c2bdc69733df8fa23a4a5eb0e28", "0x999"] + } + }, + "operationValues": { + "PLACE_BID": 0, + "LOSER_REFUND": 1, + "WINNER_SURPLUS": 2, + "SELLER_PROCEEDS": 3 + }, + "strk20Claims": { + "loserRefund": { + "actions": [ + { + "type": "transfer", + "token": "0x111", + "amount": "OPEN", + "recipient": "0x444" + }, + { + "type": "invoke", + "contract": "0x222", + "calldata": ["0x1", "0x7", "0x22b", "0xca", "0x0", "0x0", "${poolAddress}", "${openNoteIds[0]}"] + } + ], + "outputAmount": "5", + "outputFormula": "cap" + }, + "winnerSurplus": { + "actions": [ + { + "type": "transfer", + "token": "0x111", + "amount": "OPEN", + "recipient": "0x444" + }, + { + "type": "invoke", + "contract": "0x222", + "calldata": ["0x2", "0x7", "0x22b", "0xca", "0x0", "0x0", "${poolAddress}", "${openNoteIds[0]}"] + } + ], + "outputAmount": "2", + "outputFormula": "cap - clearing_price" + }, + "sellerProceeds": { + "actions": [ + { + "type": "transfer", + "token": "0x111", + "amount": "OPEN", + "recipient": "0x555" + }, + { + "type": "invoke", + "contract": "0x222", + "calldata": [ + "0x3", + "0x7", + "0x309", + "0x54b096c60c80fd98e2e6f2495db67227a0c8c2bdc69733df8fa23a4a5eb0e28", + "0x0", + "0x0", + "${poolAddress}", + "${openNoteIds[0]}" + ] + } + ], + "outputAmount": "3", + "outputFormula": "clearing_price" + } + }, + "sellerAuthorization": { + "requiredCaller": "configured_seller", + "preparation": "strk20PrepareInvoke(actions,true)", + "binds": ["auction_id", "seller_claim_handle", "open_note_id"], + "replaceableBeforeClaim": true, + "reprepareAfterAuthorization": true, + "frontRunBehavior": "copied_secret_can_only_target_authorized_note", + "publicDisclosure": "seller_to_open_note_id_link" + }, + "sharedPrivacyInvoke": { + "arguments": [ + { "index": 0, "name": "operation", "cairoType": "u8" }, + { "index": 1, "name": "auction_id", "cairoType": "u64" }, + { "index": 2, "name": "primary_value", "cairoType": "felt252" }, + { "index": 3, "name": "claim_handle", "cairoType": "felt252" }, + { "index": 4, "name": "reserved_0", "cairoType": "felt252" }, + { "index": 5, "name": "reserved_1", "cairoType": "felt252" }, + { "index": 6, "name": "pool_address", "cairoType": "ContractAddress" }, + { "index": 7, "name": "open_note_id", "cairoType": "felt252" } + ], + "returnType": "Span" + }, + "contractOutputs": { + "loserRefund": { + "length": 1, + "noteId": "${openNoteIds[0]}", + "token": "payment_token", + "amount": "cap" + }, + "winnerSurplus": { + "length": 1, + "noteId": "${openNoteIds[0]}", + "token": "payment_token", + "amount": "cap - clearing_price", + "zeroAmountBehavior": "ineligible_no_transaction" + }, + "sellerProceeds": { + "route": "strk20_open_note", + "length": 1, + "noteId": "${openNoteIds[0]}", + "recipientAuthorization": "configured_seller_pre_authorized_note_id", + "token": "payment_token", + "amount": "clearing_price", + "openNoteDeposits": 1 + } + }, + "events": { + "reveal": ["BidRevealed"], + "settlement": ["AuctionSettled", "ERC721.Transfer"], + "loserRefund": ["OpenNoteCreated", "LoserRefundClaimed", "ExternalContractInvoked", "OpenNoteDeposited"], + "winnerSurplus": ["OpenNoteCreated", "WinnerSurplusClaimed", "ExternalContractInvoked", "OpenNoteDeposited"], + "sellerProceedsAuthorization": ["SellerProceedsAuthorized"], + "sellerProceeds": ["OpenNoteCreated", "SellerProceedsClaimed", "ExternalContractInvoked", "OpenNoteDeposited"] + }, + "submission": { + "standardCalls": "walletAccount.execute(call)", + "strk20Claims": "walletAccount.strk20InvokeTransaction(actions)", + "proofGeneration": "wallet_for_all_strk20_claims", + "result": ["transaction_hash"] + }, + "poolTouching": { + "bidderAIngress": true, + "bidderBIngress": true, + "revealA": false, + "revealB": false, + "settlement": false, + "loserRefund": true, + "winnerSurplus": true, + "sellerProceedsAuthorization": false, + "sellerProceeds": true + }, + "mainnetMinimum": ["bidderAIngress", "bidderBIngress", "loserRefund"], + "preferredDemo": [ + "bidderAIngress", + "bidderBIngress", + "revealA", + "revealB", + "settlement", + "loserRefund", + "winnerSurplus", + "sellerProceedsAuthorization", + "sellerProceeds" + ] +} diff --git a/web/tests/unit/AtomicDeliveryReceipt.test.tsx b/web/tests/unit/AtomicDeliveryReceipt.test.tsx new file mode 100644 index 0000000..301d8da --- /dev/null +++ b/web/tests/unit/AtomicDeliveryReceipt.test.tsx @@ -0,0 +1,46 @@ +import { render, screen } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { AtomicDeliveryReceipt } from '@/features/auction/ui/AtomicDeliveryReceipt' + +const settlement = { + network: 'sepolia' as const, + settled: true, + sold: true, + auctionId: '7', + nftContract: '0x999', + tokenId: '99', + nftOwner: '0x888', + winnerRecipient: '0x888', + clearingPrice: '3000000000000000000', + sellerEntitlement: '3000000000000000000', + custodyValid: true, +} + +describe('AtomicDeliveryReceipt', () => { + it('renders verified delivery facts and explorer-linked public receipts', () => { + render( + , + ) + expect(screen.getByRole('heading', { name: 'Atomic Delivery Receipt' })).toBeInTheDocument() + expect(screen.getByText('Delivery verified')).toBeInTheDocument() + expect(screen.getByText('3 STRK')).toBeInTheDocument() + expect(screen.getByRole('link', { name: /Settlement 0xabc/i })).toHaveAttribute( + 'href', + 'https://sepolia.starkscan.co/tx/0xabc', + ) + expect(screen.getByRole('link', { name: /Seller proceeds 0xdef/i })).toBeInTheDocument() + }) + + it('never invents transaction hashes while settlement is pending', () => { + render() + expect(screen.getByText('Settlement pending')).toBeInTheDocument() + expect(screen.getByText('No verified transaction receipts yet.')).toBeInTheDocument() + expect(screen.queryByRole('link')).not.toBeInTheDocument() + }) +}) diff --git a/web/tests/unit/AuctionActions.test.tsx b/web/tests/unit/AuctionActions.test.tsx new file mode 100644 index 0000000..e64fa63 --- /dev/null +++ b/web/tests/unit/AuctionActions.test.tsx @@ -0,0 +1,108 @@ +import { render, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { describe, expect, it, vi } from 'vitest' +import { AuctionActions } from '@/features/auction/ui/AuctionActions' +import type { AuctionLiveViewModel } from '@/features/auction/ui/AuctionLivePage' +import { createSellerCredential } from '@/features/credentials/credentials' +import { createVerifiedRecoveryBundle } from '@/features/credentials/recoveryBundle' +import { MAINNET_CHAIN_ID } from '@/config/deployment' + +const model: AuctionLiveViewModel = { + network: 'sepolia', + chainId: '0x534e5f5345504f4c4941', + rpcUrl: 'https://rpc.example/sepolia', + auctionHouse: '0x123', + auctionHouseClassHash: '0x321', + strk20Pool: '0x456', + paymentToken: '0x654', + auctionId: '7', + seller: '0x777', + sellerClaimHandle: '0xabc', + nftContract: '0x999', + tokenId: '99', + reservePrice: '2000000000000000000', + cap: '5000000000000000000', + biddingDeadline: `${Math.floor(Date.now() / 1000) + 3600}`, + revealDeadline: `${Math.floor(Date.now() / 1000) + 7200}`, + bidderLimit: 2, + nftOwner: '0x123', + custodyValid: true, + state: { + settled: false, + sold: false, + winnerIndex: 0, + winnerCommitment: '0x0', + winnerRecipient: '0x0', + clearingPrice: '0', + sellerEntitlement: '0', + sellerAuthorizedNote: '0x0', + sellerClaimConsumed: false, + }, + bids: [], +} + +const connection = { + account: { + execute: vi.fn(), + strk20InvokeTransaction: vi.fn(), + strk20PrepareInvoke: vi.fn(), + }, + address: '0x777' as const, + chainId: model.chainId, + walletApiVersions: ['0.10.3'], + supportsStrk20: true, +} + +describe('AuctionActions', () => { + it('keeps transaction controls disabled without a connected account', () => { + render() + expect(screen.getByLabelText('Private bid amount')).toBeDisabled() + expect(screen.getByRole('button', { name: 'Submit private bid' })).toBeDisabled() + expect(screen.getByText('Connect a compatible wallet to transact.')).toBeInTheDocument() + }) + + it('enables bounded bidding and encrypted recovery controls for a compatible account', () => { + render() + expect(screen.getByLabelText('Private bid amount')).toBeEnabled() + expect(screen.getByLabelText('Recovery password')).toBeEnabled() + expect(screen.getByLabelText('Import encrypted recovery bundle')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Submit private bid' })).toBeEnabled() + expect( + screen.getByText('Enter any positive bid up to 5 STRK. Bids below the 2 STRK reserve cannot win.'), + ).toBeInTheDocument() + expect(document.body.textContent).not.toContain('claimSecret') + }) + + it('rejects a recovery bundle from another network even when contract and auction IDs match', async () => { + const user = userEvent.setup() + const password = 'correct horse battery staple' + const bundle = await createVerifiedRecoveryBundle( + [ + createSellerCredential({ + network: 'sepolia', + chainId: BigInt(model.chainId), + auctionHouse: BigInt(model.auctionHouse), + auctionId: BigInt(model.auctionId), + claimSecret: 0x123456789abcdefn, + }), + ], + password, + ) + const mainnetModel: AuctionLiveViewModel = { + ...model, + network: 'mainnet', + chainId: MAINNET_CHAIN_ID, + rpcUrl: 'https://rpc.example/mainnet', + } + const mainnetConnection = { ...connection, chainId: MAINNET_CHAIN_ID } + const recoveryFile = new File([bundle.serialized], 'wrong-network.recovery.json', { type: 'application/json' }) + Object.defineProperty(recoveryFile, 'text', { value: async () => bundle.serialized }) + render() + + await user.type(screen.getByLabelText('Recovery password'), password) + await user.upload(screen.getByLabelText('Import encrypted recovery bundle'), recoveryFile) + + expect(await screen.findByText('recovery import failed')).toBeInTheDocument() + expect(screen.queryByText('seller recovery imported')).not.toBeInTheDocument() + }, 10_000) +}) diff --git a/web/tests/unit/AuctionBidPreview.test.tsx b/web/tests/unit/AuctionBidPreview.test.tsx index b88fbc6..03520bb 100644 --- a/web/tests/unit/AuctionBidPreview.test.tsx +++ b/web/tests/unit/AuctionBidPreview.test.tsx @@ -5,18 +5,20 @@ import { AuctionBidPreview } from '@/features/auction/ui/AuctionBidPreview' const maliciousRouteId = 'design-preview' describe('AuctionBidPreview', () => { - it('renders the route id as inert text and exposes no operational bid control', () => { + it('renders the route id as inert text, a real wallet connector, and no operational bid control', () => { render() expect(screen.getByRole('heading', { level: 1, name: 'A genuinely sealed NFT auction' })).toBeInTheDocument() - expect(screen.getByText('Design preview')).toBeInTheDocument() + expect(screen.getAllByText('Design preview')).not.toHaveLength(0) expect(screen.getByText(maliciousRouteId)).toBeInTheDocument() expect(document.querySelector('script')).toBeNull() const breadcrumb = screen.getByRole('navigation', { name: 'Breadcrumb' }) expect(within(breadcrumb).getByText('Auction')).toBeInTheDocument() + expect(screen.getByRole('heading', { level: 2, name: 'Connect a privacy-capable wallet' })).toBeInTheDocument() + expect(screen.getByTestId('wallet-connect-module')).toBeInTheDocument() + expect(screen.getByText('No Starknet wallet detected. Install or unlock Ready, then refresh.')).toBeInTheDocument() expect(screen.getByLabelText('Bid amount')).toBeDisabled() expect(screen.getByRole('button', { name: 'Bidding unavailable in design preview' })).toBeDisabled() - expect(screen.queryByText(/connect wallet/i)).not.toBeInTheDocument() }) it('uses truthful placeholders instead of invented onchain auction data', () => { @@ -59,4 +61,14 @@ describe('AuctionBidPreview', () => { expect(screen.getByText('Winning bid')).toBeInTheDocument() expect(screen.getByText('Second price')).toBeInTheDocument() }) + + it('renders a qualitative protocol console without inventing chain data', () => { + render() + + const protocolConsole = screen.getByRole('region', { name: 'Protocol state' }) + expect(within(protocolConsole).getByText('Uniform cap collateral')).toBeInTheDocument() + expect(within(protocolConsole).getByText('Second-price settlement')).toBeInTheDocument() + expect(within(protocolConsole).getByText('Design preview')).toBeInTheDocument() + expect(within(protocolConsole).queryByText(/\d+\.?\d* STRK/)).not.toBeInTheDocument() + }) }) diff --git a/web/tests/unit/AuctionLivePage.test.tsx b/web/tests/unit/AuctionLivePage.test.tsx new file mode 100644 index 0000000..960698a --- /dev/null +++ b/web/tests/unit/AuctionLivePage.test.tsx @@ -0,0 +1,76 @@ +import { render, screen, within } from '@testing-library/react' +import { describe, expect, it } from 'vitest' +import { AuctionLivePage, type AuctionLiveViewModel } from '@/features/auction/ui/AuctionLivePage' + +const model: AuctionLiveViewModel = { + network: 'sepolia', + chainId: '0x534e5f5345504f4c4941', + rpcUrl: 'https://rpc.example/sepolia', + auctionHouse: '0x123', + auctionHouseClassHash: '0x321', + strk20Pool: '0x456', + paymentToken: '0x654', + auctionId: '7', + seller: '0x777', + sellerClaimHandle: '0xabc', + nftContract: '0x999', + tokenId: '99', + reservePrice: '2000000000000000000', + cap: '5000000000000000000', + biddingDeadline: '100', + revealDeadline: '200', + bidderLimit: 2, + nftOwner: '0x888', + custodyValid: true, + state: { + settled: true, + sold: true, + winnerIndex: 1, + winnerCommitment: '0x222', + winnerRecipient: '0x888', + clearingPrice: '3000000000000000000', + sellerEntitlement: '3000000000000000000', + sellerAuthorizedNote: '0x903', + sellerClaimConsumed: false, + }, + bids: [ + { + commitment: '0x111', + claimHandle: '0xa11', + revealed: true, + amount: '3000000000000000000', + assetRecipient: '0x887', + }, + { + commitment: '0x222', + claimHandle: '0xa22', + revealed: true, + amount: '4000000000000000000', + assetRecipient: '0x888', + }, + ], +} + +describe('AuctionLivePage', () => { + it('renders exact returned auction, settlement, bid, and custody data', () => { + render() + expect(screen.getByRole('heading', { level: 1, name: 'Auction #7' })).toBeInTheDocument() + expect(screen.getAllByText('Sold')).not.toHaveLength(0) + const facts = screen.getByRole('region', { name: 'Live auction facts' }) + expect(within(facts).getByText('2 STRK')).toBeInTheDocument() + expect(within(facts).getByText('5 STRK')).toBeInTheDocument() + expect(within(facts).getByText('1970-01-01 00:01:40 UTC')).toBeInTheDocument() + expect(within(facts).getByText('1970-01-01 00:03:20 UTC')).toBeInTheDocument() + expect(screen.getByText('Custody verified')).toBeInTheDocument() + expect(screen.getByText('0x888', { selector: 'code' })).toBeInTheDocument() + expect(screen.getByRole('row', { name: /0x222.*4 STRK.*Winner/i })).toBeInTheDocument() + expect(screen.getByRole('heading', { name: 'Connect a privacy-capable wallet' })).toBeInTheDocument() + }) + + it('renders an honest unavailable state without fake auction values', () => { + render() + expect(screen.getByRole('alert')).toHaveTextContent('Auction deployment is not configured') + expect(screen.queryByText('2 STRK')).not.toBeInTheDocument() + expect(screen.queryByRole('button', { name: /submit/i })).not.toBeInTheDocument() + }) +}) diff --git a/web/tests/unit/AuctionPageClient.test.tsx b/web/tests/unit/AuctionPageClient.test.tsx new file mode 100644 index 0000000..a45f45b --- /dev/null +++ b/web/tests/unit/AuctionPageClient.test.tsx @@ -0,0 +1,135 @@ +import { act, fireEvent, render, screen, waitFor } from '@testing-library/react' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { AuctionPageClient, type AuctionModelLoader } from '@/features/auction/ui/AuctionPageClient' +import type { AuctionLiveViewModel } from '@/features/auction/ui/AuctionLivePage' + +const navigation = vi.hoisted(() => ({ query: 'id=7' })) + +vi.mock('next/navigation', () => ({ + useSearchParams: () => new URLSearchParams(navigation.query), +})) + +function model(auctionId: string): AuctionLiveViewModel { + return { + network: 'mainnet', + chainId: '0x534e5f4d41494e', + rpcUrl: 'https://rpc.example/mainnet', + auctionHouse: '0x123', + auctionHouseClassHash: '0x456', + strk20Pool: '0x789', + paymentToken: '0xabc', + auctionId, + seller: '0x111', + sellerClaimHandle: '0x12', + nftContract: '0x222', + tokenId: '99', + reservePrice: '1000000000000000000', + cap: '4000000000000000000', + biddingDeadline: '4102444800', + revealDeadline: '4102445100', + bidderLimit: 2, + nftOwner: '0x123', + custodyValid: true, + state: { + settled: false, + sold: false, + winnerIndex: 0, + winnerCommitment: '0x0', + winnerRecipient: '0x0', + clearingPrice: '0', + sellerEntitlement: '0', + sellerAuthorizedNote: '0x0', + sellerClaimConsumed: false, + }, + bids: [], + } +} + +function deferred() { + let resolve!: (value: T) => void + let reject!: (reason?: unknown) => void + const promise = new Promise((resolvePromise, rejectPromise) => { + resolve = resolvePromise + reject = rejectPromise + }) + return { promise, resolve, reject } +} + +beforeEach(() => { + navigation.query = 'id=7' +}) + +describe('AuctionPageClient', () => { + it('loads one validated auction and renders only the verified model', async () => { + const loadModel = vi.fn().mockResolvedValue(model('7')) + + render() + + expect(screen.getByRole('status')).toHaveTextContent('Loading auction #7') + expect(await screen.findByRole('heading', { level: 1, name: 'Auction #7' })).toBeInTheDocument() + expect(loadModel).toHaveBeenCalledOnce() + expect(loadModel).toHaveBeenCalledWith(7n) + }) + + it.each(['', 'id=0', 'id=7&id=8', 'id=18446744073709551616'])( + 'rejects invalid query %s before public RPC loading', + async (query) => { + navigation.query = query + const loadModel = vi.fn() + + render() + + expect(screen.getByRole('heading', { level: 1, name: 'Live auction unavailable' })).toBeInTheDocument() + expect(screen.getByRole('alert')).toHaveTextContent(/auction/i) + expect(loadModel).not.toHaveBeenCalled() + }, + ) + + it('renders hostile query text inertly and caps its display', () => { + navigation.query = `id=${encodeURIComponent(``)}` + const loadModel = vi.fn() + const { container } = render() + + expect(screen.getByRole('alert')).toHaveTextContent('Auction ID must be a positive u64 decimal value.') + expect(screen.getByText(/