Skip to content

feat(juicer): /v1/campaigns/juicer/progress + /spend endpoints#256

Open
joshuakrueger-dfx wants to merge 1 commit into
JuiceSwapxyz:developfrom
joshuakrueger-dfx:feat/juicer-campaign
Open

feat(juicer): /v1/campaigns/juicer/progress + /spend endpoints#256
joshuakrueger-dfx wants to merge 1 commit into
JuiceSwapxyz:developfrom
joshuakrueger-dfx:feat/juicer-campaign

Conversation

@joshuakrueger-dfx

Copy link
Copy Markdown

Summary

Backend for the Juicer NFT post-launch loyalty drop. Wallets trade `JUICER_JP_COST = 5000` Juice Points for a backend signature, then mint with that signature on Citrea Mainnet.

Endpoints

```http
GET /v1/campaigns/juicer/progress?walletAddress={addr}&chainId={4114|5115}
POST /v1/campaigns/juicer/spend { walletAddress, chainId }
```

`/progress` returns:

```json
{
"walletAddress": "0x...",
"chainId": 4114,
"availableJp": 5230,
"totalEarnedJp": 5230,
"spentJp": 0,
"cost": 5000,
"isEligibleForNFT": true,
"nftMinted": false
}
```

`/spend` is idempotent: a retried POST returns the stored signature instead of issuing a new one or deducting JP twice. Signatures are deterministic from `(contract, chainId, wallet)` so re-issuance is safe.

Source of truth

Fail-closed properties

  • `/spend` returns 503 if `JUICER_NFT_CONTRACT` env var is empty (contract not deployed yet) — the frontend renders a "not yet live" state instead of minting against a placeholder.
  • `/spend` returns 503 if ponder `/points` is unreachable — never silently lets a wallet spend against a stale balance.
  • The unique `(walletAddress, chainId)` constraint plus the contract's `hasClaimed` mapping form a defence-in-depth pair against double-claim and race conditions.

DB migration

`prisma/schema.prisma` adds the `JuicerSpend` model. Apply with `yarn prisma migrate dev --name juicer_spend` before deploying.

Env vars

```bash
JUICER_NFT_CONTRACT=0x... # populate after smart-contracts#56 deploys
CAMPAIGN_SIGNER_PRIVATE_KEY=0x... # already used by First Squeezer; reuse
CITREA_4114_RPC_URL=https://... # already set
```

Companion PRs (hard dependencies)

Test plan

  • DB migration applied
  • Set `JUICER_NFT_CONTRACT` env var to deployed address
  • `GET /v1/campaigns/juicer/progress` for an unconnected address returns `availableJp: 0, isEligibleForNFT: false`
  • `GET /v1/campaigns/juicer/progress` for a wallet with >=5000 JP returns `isEligibleForNFT: true`
  • `POST /v1/campaigns/juicer/spend` for a wallet with <5000 JP returns 403
  • First successful `POST /spend` returns 201 with signature; second returns 200 with the same signature (idempotency)
  • On-chain `JuicerNFT.claim(signature)` succeeds and contract emits `NFTClaimed`

JP-trade flow for the Juicer NFT loyalty drop. The wallet trades
JUICER_JP_COST (=5000) Juice Points for a backend signature, then
mints with that signature.

What ships
- src/endpoints/juicerCampaign.ts
  * GET  /v1/campaigns/juicer/progress  - returns { availableJp,
      totalEarnedJp, spentJp, cost, isEligibleForNFT, nftMinted, ... }.
      Reads totalEarnedJp from ponder /points/{address}, spent from the
      new juicer_spend table, and best-effort hasClaimed from the NFT
      contract.
  * POST /v1/campaigns/juicer/spend     - idempotent: if a spend row
      already exists for (wallet, chain) the stored signature is
      returned, otherwise validates JP balance, generates the
      claim signature, INSERTs the row, returns 201.
- prisma/schema.prisma: new JuicerSpend model (table juicer_spend)
  with unique (wallet_address, chain_id) constraint as the source of
  truth for spent JP.
- src/lib/constants/campaigns.ts: JUICER_NFT_CONTRACT (env-driven so
  it stays empty until smart-contracts#56 is deployed) +
  JUICER_JP_COST = 5000.
- src/server.ts: mount the two new routes under generalLimiter.

Fail-closed properties
- /spend returns 503 if JUICER_NFT_CONTRACT env var is empty (contract
  not deployed yet) so the frontend renders a "not yet live" state
  rather than minting against a placeholder address.
- /spend returns 503 if ponder is unreachable, never silently lets a
  wallet spend against a stale or missing balance.
- The unique (walletAddress, chainId) constraint plus the contract's
  hasClaimed mapping form a defence-in-depth pair against double-claim.

Hard dependencies before this can serve real data
- JuiceSwapxyz/ponder#138 (fixes /points 500s)
- JuiceSwapxyz/smart-contracts#56 (deploys JuicerNFT, populates
  JUICER_NFT_CONTRACT env var)
- JuiceSwapxyz/bapp#741 (consumer)

Migration
- Apply prisma migrate to provision juicer_spend before deploy.
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