Skip to content

Add Starknet ticketing contracts and deploy to Sepolia - #1

Merged
wheval merged 5 commits into
mainfrom
wheval-miniature-memory
Jul 29, 2026
Merged

Add Starknet ticketing contracts and deploy to Sepolia#1
wheval merged 5 commits into
mainfrom
wheval-miniature-memory

Conversation

@wheval

@wheval wheval commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Adds the on-chain ticketing layer to Zicket: a pair of Cairo contracts, the API and UI wiring around them, and a live deployment on Starknet Sepolia.

Contracts

Contract Role
ZicketEvents Events, ticketing, escrow, refunds, check-in
MockERC20 Payment token

Two kinds of ticket:

  • Public — bound to the buyer's address, one per wallet.
  • Anonymous — the buyer computes commitment = poseidon(secret, nullifier) in the browser and only the commitment reaches the chain. The ticket has no owner. Check-in reveals the preimage and burns nullifier_hash = poseidon(nullifier), so a ticket cannot be used twice.

The secret never leaves the browser, and is written to localStorage before the transaction is signed — it is the only proof of ownership and exists nowhere else.

Live on Sepolia

Contract Address
ZicketEvents 0x7b318fc4…52b8cbb
ZUSD (18dp) 0x785ab439…3333f15

The mock ERC-20 is deliberate here — its mint is permissionless, so it doubles as a faucet and the testnet demo is self-contained. Deploying it off devnet is gated behind ZICKET_DEPLOY_MOCK_TOKEN=1 so a real payment token can't be swapped out by accident.

Trust boundary

POST /api/chain/purchases takes only a transaction hash; every other field is read back from the receipt, so a client cannot fabricate a ticket. Anonymous purchases are stored without a buyer address or email — recording either would defeat the commitment scheme.

Cross-deployment bug found during Sepolia bring-up

Event ids restart at 1 for every ZicketEvents deployment, so tickets.onchain_event_id alone was ambiguous. After repointing at Sepolia, listings still carrying devnet ids resolved to zero-filled events, publish short-circuited as "already published", and purchases reverted inside the account. Tickets now record the contract address that issued the id, and a mismatch reads as unpublished.

Also

  • The e2e harnesses hardcoded devnet's predeployed accounts, so neither could run anywhere else. scripts/starknet/buyers.ts provisions buyers per network — predeployed on devnet, otherwise generated, funded from the deployer and counterfactually deployed.
  • Connecting a wallet only matters at checkout, so the navbar button is gone; the purchase button renders as Connect Wallet until a wallet is attached.
  • README replaces the create-next-app boilerplate with the deployment record, environment reference and script table.

Verification

Check Result
snforge 36/36
pnpm chain:e2e — against live Sepolia 20/20
pnpm chain:flow — against live Sepolia 32/32
tsc --noEmit, pnpm build clean

Includes Poseidon JS↔Cairo parity, anonymous-ticket unlinkability, nullifier double-spend protection, replay idempotency, escrow accounting, and rejection of an unrelated transaction submitted as a purchase.

Migration 0001_stormy_galactus.sql adds tickets.onchain_contract_address and needs applying on deploy.

Note: buying through the browser on Sepolia needs Argent X or Braavos — the burner is devnet-only — so that path was verified via the headless harness rather than a real extension.

wheval and others added 3 commits July 29, 2026 03:25
Zicket had Starknet and zk-verification trust markers on the homepage but no
on-chain layer, and the "Get Ticket" button did nothing. This adds the missing
layer end to end.

Contracts (contracts/, Cairo 2.14 + snforge):

  ZicketEvents supports two kinds of ticket. A public ticket binds to the
  buyer's address, one per wallet. An anonymous ticket is a commitment:
  the buyer computes poseidon(secret, nullifier) in the browser and only that
  reaches the chain, so the ticket has no owner at all. Check-in reveals the
  preimage and burns poseidon(nullifier), which is what stops a ticket being
  used twice without ever identifying who held it.

  Also covers ERC-20 payment, per-event escrow with organizer withdrawal, a
  platform fee in bps, and a cancel/refund path. MockERC20 provides a payment
  token for local runs. 36 snforge tests.

Backend:

  tickets gains its on-chain columns and a new ticket_purchases table keyed on
  the transaction hash. POST /api/chain/purchases accepts only a tx hash and
  reads every other field back from the receipt, so a client cannot fabricate a
  ticket. Anonymous purchases are stored without a buyer address or email --
  recording either would defeat the commitment scheme.

  Listings are published on-chain by a server relayer, so an organizer does not
  need a funded wallet to list.

Frontend:

  Wallet connection is built on starknet.js's own WalletAccount plus SNIP-1193
  discovery. get-starknet-core was tried and rejected: its current release still
  targets the starknet v5 API and does not work against v8.

  The purchase flow writes an anonymous ticket's secret to localStorage before
  the transaction is signed, because the secret is the only proof of ownership
  and exists nowhere else -- if the tab dies mid-flight the attendee can still
  redeem. The UI says plainly that clearing site data loses the ticket.

Notes:

  Seeded events are rebased onto the current date. Every fixture date was in the
  past, which made all of them unpurchasable once the sale window was enforced.

  Recording a purchase now reports whether the row was new, because retrying the
  same transaction hash was inflating the attendee count. Covered by a test.

  src/index.ts can point the Neon driver at a local HTTP proxy, so the stack runs
  against a plain Postgres container; unset in production.

Verified against starknet-devnet in Docker: 36 contract tests, 20 assertions
directly against the deployed contracts, and 32 through the running app.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Both contracts are live on Sepolia and the app runs against them. Deploying
the mock ERC-20 outside devnet is now opt-in via ZICKET_DEPLOY_MOCK_TOKEN so a
real payment token can't be swapped out by accident; on Sepolia the mock is
intentional, since its permissionless mint doubles as a faucet.

The e2e harnesses hardcoded devnet's predeployed accounts, so neither could run
anywhere else. scripts/starknet/buyers.ts provisions buyers per network:
predeployed on devnet, otherwise generated, funded from the deployer and
counterfactually deployed, with keys cached in .env.local.

Scope on-chain linkage to a deployment. Event ids restart at 1 for every
ZicketEvents deployment, so tickets.onchain_event_id alone was ambiguous:
after repointing at Sepolia, listings still carrying devnet ids resolved to
zero-filled events, publish short-circuited as "already published", and buys
reverted. Tickets now record the contract address that issued the id, and a
mismatch reads as unpublished.

20/20 contract assertions and 32/32 full-stack assertions pass against live
Sepolia.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Record the live contract addresses, class hashes, deployer and RPC in the
README with explorer links, and note the JSON-RPC 0.9.0 requirement that
constrains which public endpoints work.

Replace the create-next-app boilerplate header and footer, which described
neither the project nor how it is deployed, with a summary of what Zicket is
and a table of the scripts.

Drop the Connect Wallet button from the navbar. Connecting is only meaningful
at checkout, so the purchase button now renders as Connect Wallet until a
wallet is attached — previously the card just told the user to find the button
in the header.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@vercel

vercel Bot commented Jul 29, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
zicket Error Error Jul 29, 2026 8:39am

Three jobs on every PR: the Cairo contracts (fmt, build, 36 tests), the app
(lint, typecheck, build), and a devnet deploy running the 20 contract
assertions. None of this was checked automatically before.

The build job needs a database, since / and /explore are prerendered from it,
so it brings up Postgres behind the same Neon HTTP proxy the README documents
for local development. Migrations run through psql: drizzle-kit picks the Neon
WebSocket driver, which that proxy does not serve.

scarb fmt --check required reformatting. The section banner comments were long
enough to exceed the line limit, so the formatter wrapped them onto a second
line and split the rule from its heading; shortening them keeps the banners
intact and the formatting canonical.

Verified by reproducing each job locally, including a build against an empty
database and a deploy plus e2e against a fresh devnet with no .env.local.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
A bare POST to the Neon proxy's /sql is rejected with 400 without its
connection headers, so curl -f read a healthy proxy as a failure and the job
timed out waiting on a service that was already up. Wait on the port instead.

Also point setup-scarb at contracts/Scarb.lock so it can cache dependencies;
the workspace is not at the repository root.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@wheval
wheval merged commit 1799e49 into main Jul 29, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant