Monorepo for Aesthetic.Computer aesthetic.computer
at main 403 lines 13 kB view raw view rendered
1# KidLisp Keeps - NFT Minting System 2 3## Overview 4 5"Keeps" are KidLisp pieces preserved as NFTs on Tezos. Each `$code` can only exist once at a time, ensuring uniqueness and provenance. 6 7--- 8 9## Current Contract (v3) 10 11| Network | Contract | Admin | Status | 12|---------|----------|-------|--------| 13| **Mainnet** | `KT1JEVyKjsMLts63e4CNaMUywWTPgeQ41Smi` | staging | Active (Staging) | 14| Ghostnet | `KT1StXrQNvRd9dNPpHdCGEstcGiBV6neq79K` | kidlisp | Testing | 15 16**Legacy (v2):** `KT1EcsqR69BHekYF5mDQquxrvNg5HhPFx6NM` (archived, do not use for new mints) 17 18### Storage 19| Field | Type | Description | 20|-------|------|-------------| 21| `administrator` | address | Admin wallet (can mint, burn, update, lock) | 22| `content_hashes` | big_map[bytes, nat] | Maps piece name → token_id (prevents duplicates) | 23| `contract_metadata_locked` | bool | If true, collection metadata is frozen | 24| `keep_fee` | mutez | Required fee to mint (0 = free) | 25| `ledger` | big_map[nat, address] | Token ownership (token_id → owner) | 26| `metadata` | big_map[string, bytes] | Contract-level TZIP-16 metadata | 27| `metadata_locked` | big_map[nat, bool] | Per-token metadata lock status | 28| `next_token_id` | nat | Auto-incrementing token counter | 29| `operators` | big_map | FA2 operator approvals | 30| `token_creators` | big_map[nat, address] | **v3:** Original creator for each token | 31| `token_metadata` | big_map[nat, record] | Per-token TZIP-21 metadata | 32 33### Entrypoints 34 35| Entrypoint | Access | Description | 36|------------|--------|-------------| 37| `keep` | Admin or User (with fee) | Mint new token with full TZIP-21 metadata | 38| `edit_metadata` | **Admin, Owner, or Creator** | Update token metadata (if not locked) | 39| `lock_metadata` | Admin or Owner | Permanently freeze token metadata | 40| `burn_keep` | Admin | Destroy token and free piece name for re-mint | 41| `set_contract_metadata` | Admin | Update collection metadata (if not locked) | 42| `lock_contract_metadata` | Admin | Permanently freeze collection metadata | 43| `set_keep_fee` | Admin | Set mint fee (in tez) | 44| `withdraw_fees` | Admin | Withdraw accumulated fees | 45| `transfer` | Owner/Operator | FA2 standard transfer | 46| `balance_of` | Public | FA2 standard balance query | 47| `update_operators` | Owner | FA2 operator management | 48 49### v3 Permission Model (edit_metadata) 50 51``` 52edit_metadata authorization (in order of check): 531. Admin — can edit any token 542. Owner — current holder of the token 553. Creator — address stored in token_creators[token_id] 56 57⚠️ For objkt.com artist attribution: 58 The CREATOR must call edit_metadata, not admin! 59 Admin calls show admin as "updater" on objkt. 60``` 61 62### Uniqueness Enforcement 63- Each piece name (e.g., "cow", "roz") can only be minted once 64- `content_hashes` big_map tracks: piece_name → token_id 65- Burning removes the entry, allowing re-mint 66- Checked client-side via TzKT API before IPFS upload 67 68--- 69 70## CLI Commands (`node keeps.mjs`) 71 72| Command | Description | 73|---------|-------------| 74| `deploy` | Deploy new contract | 75| `status` | Show contract info | 76| `balance` | Check wallet balance | 77| `mint <piece> [--thumbnail]` | Mint piece with optional animated thumbnail | 78| `update <token_id> <piece>` | Update token metadata | 79| `lock <token_id>` | Permanently lock token metadata | 80| `burn <token_id>` | Destroy token (allows re-mint) | 81| `redact <token_id> [--reason="..."]` | Censor token content | 82| `set-collection-media --image=<uri>` | Set collection thumbnail | 83| `lock-collection` | Permanently lock collection metadata | 84| `fee` | Show current keep fee | 85| `set-fee <tez>` | Set keep fee (admin only) | 86| `withdraw [dest]` | Withdraw fees to wallet (admin only) | 87 88--- 89 90## Fee System 91 92The contract supports configurable mint fees. See [KEEPS-FEE-SYSTEM.md](./KEEPS-FEE-SYSTEM.md) for full documentation. 93 94### Quick Reference 95 96```bash 97# Check current fee 98node keeps.mjs fee 99 100# Set fee to 5 XTZ 101node keeps.mjs set-fee 5 102 103# Withdraw accumulated fees 104node keeps.mjs withdraw 105``` 106 107### Storage & Entrypoints 108- `keep_fee` (mutez) - Required payment to keep 109- `set_keep_fee(new_fee)` - Admin updates fee 110- `withdraw_fees(destination)` - Admin withdraws balance 111 112⚠️ **Note**: Fee system requires contract v2.1+ (with fee entrypoints). Existing contracts need redeployment. 113 114--- 115 116## Metadata Structure 117 118### Token Metadata (TZIP-21) 119```json 120{ 121 "name": "$cow", 122 "description": "(wipe \"blue\")\n(ink \"yellow\")\n...\n\nby @jeffrey\nac25namuc", 123 "artifactUri": "ipfs://Qm...", 124 "displayUri": "ipfs://Qm...", 125 "thumbnailUri": "ipfs://Qm... (animated WebP)", 126 "symbol": "KEEP", 127 "tags": ["$cow", "KidLisp", "Aesthetic.Computer", "interactive"], 128 "attributes": [ 129 { "name": "Language", "value": "KidLisp" }, 130 { "name": "Code", "value": "$cow" }, 131 { "name": "Author", "value": "@jeffrey" }, 132 { "name": "User Code", "value": "ac25namuc" }, 133 { "name": "Lines of Code", "value": "3" }, 134 { "name": "Dependencies", "value": "2" }, 135 { "name": "Packed", "value": "2025.12.9" }, 136 { "name": "Interactive", "value": "Yes" }, 137 { "name": "Platform", "value": "Aesthetic Computer" } 138 ] 139} 140``` 141 142### Collection Metadata 143```json 144{ 145 "name": "KidLisp Keeps", 146 "version": "2.0.0", 147 "interfaces": ["TZIP-012", "TZIP-016", "TZIP-021"], 148 "imageUri": "https://oven.aesthetic.computer/keeps/latest", 149 "homepage": "https://aesthetic.computer" 150} 151``` 152 153--- 154 155## Infrastructure 156 157### Services 158| Service | URL | Purpose | 159|---------|-----|---------| 160| Oven | `https://oven.aesthetic.computer` | Thumbnail generation (Puppeteer + FFmpeg) | 161| Grab | `https://grab.aesthetic.computer` | Static screenshot fallback | 162| Pinata | IPFS pinning | Artifact and metadata storage | 163| TzKT | `api.ghostnet.tzkt.io` | On-chain data queries | 164 165### Thumbnail Generation 166- **Format**: Animated WebP 167- **Size**: 96x96 @ 2x density (192x192 actual) 168- **Duration**: 8 seconds capture 169- **FPS**: 10 capture → 20 playback 170- **Quality**: 70 171 172--- 173 174## Phase 2: Creator Authorization (PLANNED) 175 176### Problem 177Currently only admin can mint. We want: 1781. Only **handled** users (with `@handle`) can mint 1792. Users can only mint **their own** pieces (pieces they authored) 1803. Minting should be self-service via web UI 181 182### Existing Infrastructure 183 184#### Authentication 185- **Auth0** provides JWT tokens via `/userinfo` endpoint 186- `authorize()` in `backend/authorization.mjs` validates tokens 187- Returns `{ sub, email, email_verified, ... }` 188 189#### Piece Ownership (Already Tracked!) 190The `kidlisp-codes` MongoDB collection already stores: 191```javascript 192{ 193 code: "cow", // Piece name 194 source: "(wipe...)", // Source code 195 hash: "...", // SHA-256 of source 196 user: "auth0|123...", // Creator's Auth0 sub ID ✅ 197 when: Date, // Created timestamp 198} 199``` 200 201#### Handle Resolution 202- `handleFor(userId)` in `backend/authorization.mjs` gets `@handle` from `sub` 203- `fetchAuthorInfo(userId)` in `bundle-html.js` resolves handle + userCode 204 205### Architecture 206 207``` 208┌─────────────────┐ ┌──────────────────────────────┐ ┌─────────────────┐ 209│ AC Frontend │────▶│ /api/kidlisp-keep │────▶│ Tezos Contract │ 210│ (user clicks │ │ (Netlify function) │ │ (SmartPy FA2) │ 211│ "Keep" btn) │ │ │ │ │ 212└─────────────────┘ │ 1. Validate JWT (Auth0) │ └─────────────────┘ 213 │ │ 2. Check user has @handle │ 214 │ JWT Bearer │ 3. Verify piece ownership │ 215 │ token │ 4. Check not already minted │ 216 ▼ │ 5. Generate bundle & thumb │ 217 │ 6. Upload to IPFS │ 218 │ 7. Sign & submit Tezos tx │ 219 └──────────────────────────────┘ 220221222 ┌──────────────────────────────┐ 223 │ MongoDB `kidlisp-codes` │ 224 │ - code → user mapping │ 225 │ - piece ownership proof │ 226 └──────────────────────────────┘ 227``` 228 229### Authorization Flow 230 2311. **User Authentication** (Auth0) 232 ```javascript 233 const user = await authorize({ authorization: req.headers.authorization }); 234 if (!user) return 401 Unauthorized; 235 ``` 236 2372. **Handle Requirement** 238 ```javascript 239 const handle = await handleFor(user.sub); 240 if (!handle) return 403 "You need an @handle to mint"; 241 ``` 242 2433. **Piece Ownership Verification** 244 ```javascript 245 const piece = await db.collection('kidlisp-codes').findOne({ code: pieceName }); 246 if (!piece) return 404 "Piece not found"; 247 if (piece.user !== user.sub) return 403 "You don't own this piece"; 248 ``` 249 2504. **Duplicate Check** 251 ```javascript 252 const duplicate = await checkDuplicatePiece(pieceName); 253 if (duplicate.exists) return 409 "Already minted as token #X"; 254 ``` 255 2565. **Minting** 257 - Generate bundle via existing `bundle-html.js` logic 258 - Generate thumbnail via Oven 259 - Upload to IPFS 260 - Sign transaction with server-side admin key 261 - Submit to Tezos 262 263### API Endpoints 264 265#### `POST /api/kidlisp-keep` 266Mint a new keep (requires auth) 267 268**Headers:** 269- `Authorization: Bearer <JWT>` 270 271**Body:** 272```json 273{ 274 "piece": "cow", 275 "generateThumbnail": true 276} 277``` 278 279**Response:** 280```json 281{ 282 "success": true, 283 "tokenId": 5, 284 "txHash": "op...", 285 "artifactUri": "ipfs://...", 286 "objktUrl": "https://objkt.com/asset/KT1.../5" 287} 288``` 289 290**Errors:** 291- `401` - Not authenticated 292- `403` - No @handle, or not piece owner 293- `404` - Piece not found 294- `409` - Already minted 295 296#### `GET /api/kidlisp-keep?piece=cow` 297Check piece mint status (public) 298 299**Response:** 300```json 301{ 302 "piece": "cow", 303 "canMint": true, 304 "owner": "@jeffrey", 305 "minted": false 306} 307// or if minted: 308{ 309 "piece": "cow", 310 "canMint": false, 311 "minted": true, 312 "tokenId": 5, 313 "objktUrl": "https://..." 314} 315``` 316 317### Security 318 3191. **Admin Key Protection** 320 - Tezos private key in Netlify env: `TEZOS_KIDLISP_KEY` 321 - Never exposed to client 322 - Server signs all transactions 323 3242. **Ownership Enforcement** 325 - Only `piece.user === user.sub` can mint 326 - First saver owns the piece (existing behavior) 327 - Admin can mint any piece (bypass) 328 3293. **On-Chain Protection** 330 - SmartPy contract: `assert self.is_administrator_()` on all mutations 331 - `content_hashes` big_map prevents duplicate minting 332 - Metadata locking is permanent once applied 333 3344. **Rate Limiting** (Future) 335 - Per-user limits (e.g., 5 mints/day) 336 - Prevent spam 337 338### Scalability (Designed for Millions of Pieces) 339 3401. **Token ID Retrieval** - O(1) 341 - Uses `next_token_id - 1` from storage, not pagination 342 - Works at any scale 343 3442. **Duplicate Check** - O(1) 345 - `content_hashes` big_map lookup by key 346 - Big maps are hash tables, constant-time access 347 3483. **Status Command** - O(1) with pagination 349 - Uses TzKT API with `limit` and `sort.desc` 350 - Shows only recent tokens, total count from storage 351 3524. **Gas Costs** - Constant 353 - Big map operations don't increase with collection size 354 - ~0.05 tez per mint regardless of token count 355 3565. **IPFS Storage** 357 - ~50KB per piece average (bundle + metadata + thumb) 358 - 1M pieces ≈ 50GB = ~$7.50/month on Pinata 359 360### Implementation Steps 361 3621. [x] Document existing infrastructure 3632. [ ] Implement `/api/kidlisp-keep` GET (check status) 3643. [ ] Implement `/api/kidlisp-keep` POST (mint) 3654. [ ] Add "Keep" button to UI when viewing own piece 3665. [ ] Test locally with dev server 3676. [ ] Deploy to production 3687. [ ] Add rate limiting 369 370### Environment Variables Needed 371 372```env 373# Netlify env vars (already have most of these) 374TEZOS_KIDLISP_KEY=edsk... # Admin signing key 375TEZOS_CONTRACT_ADDRESS=KT1... # Keeps contract 376TEZOS_NETWORK=ghostnet # or mainnet 377PINATA_API_KEY=... # For IPFS uploads 378PINATA_API_SECRET=... 379OVEN_URL=https://oven.aesthetic.computer 380``` 381 382--- 383 384## Files 385 386| File | Purpose | 387|------|---------| 388| `tezos/keeps_fa2_v2.py` | SmartPy contract source | 389| `tezos/keeps.mjs` | CLI tool for minting/management | 390| `tezos/contract-address.txt` | Current deployed contract | 391| `oven/server.mjs` | Thumbnail generation server | 392| `oven/grabber.mjs` | Puppeteer frame capture | 393 394--- 395 396## Deployment History 397 398| Date | Contract | Network | Notes | 399|------|----------|---------|-------| 400| 2025-12-09 | KT1Ah5m2kzU3GfN42hh57mVJ63kNi95XKBdM | Ghostnet | Current - with burn, redact | 401| 2025-12-09 | KT1FvJyG4e6tRHdJLTjMhvi7mMrrAGkBCdBv | Ghostnet | Added piece-name uniqueness | 402| 2025-12-09 | KT1CfExN8EcSMS5Pm2vzxpQKyzkijNHvGCdm | Ghostnet | Added content_hashes | 403| 2025-12-09 | KT1N9jz6NJaBYW4LVhccZs6ttQMvFEAmkkSM | Ghostnet | First with metadata lock |