Monorepo for Aesthetic.Computer aesthetic.computer
4
fork

Configure Feed

Select the types of activity you want to include in your feed.

add prompt.ac/ejjk1z redirect → duckweedtri.prompt.ac for Ida's surface fork

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

+242 -890
+1 -1
SCORE.md
··· 10 10 11 11 <!-- stats:start --> 12 12 351 built-in pieces (333 JS + 18 KidLisp), ~78 API endpoints.<br> 13 - 2799 registered handles, 265 user-published pieces, 4392 paintings, 16174 KidLisp programs, 18016 chat messages, 20 prints ordered.<br> 13 + 2798 registered handles, 265 user-published pieces, 4392 paintings, 16174 KidLisp programs, 18016 chat messages, 20 prints ordered.<br> 14 14 *Last refreshed: Mar 2, 2026* 15 15 <!-- stats:end --> 16 16
+232 -227
system/netlify.toml
··· 903 903 status = 200 904 904 force = true 905 905 [[redirects]] 906 + from = "https://prompt.ac/ejjk1z" 907 + to = "https://duckweedtri.prompt.ac" 908 + status = 301 909 + force = true 910 + [[redirects]] 906 911 from = "https://prompt.ac/*" 907 912 to = "https://aesthetic.computer/:splat" 908 913 status = 301 ··· 912 917 to = "https://l5.aesthetic.computer" 913 918 status = 301 914 919 force = true 915 - [[redirects]] 916 - from = "https://l5.prompt.ac/*" 917 - to = "https://l5.aesthetic.computer/:splat" 918 - status = 301 919 - force = true 920 - [[redirects]] 921 - from = "https://processing.prompt.ac" 922 - to = "https://processing.aesthetic.computer" 923 - status = 301 924 - force = true 925 - [[redirects]] 926 - from = "https://processing.prompt.ac/*" 927 - to = "https://processing.aesthetic.computer/:splat" 928 - status = 301 929 - force = true 930 - [[redirects]] 931 - from = "https://p5.prompt.ac" 932 - to = "https://p5.aesthetic.computer" 933 - status = 301 934 - force = true 935 - [[redirects]] 936 - from = "https://p5.prompt.ac/*" 937 - to = "https://p5.aesthetic.computer/:splat" 938 - status = 301 939 - force = true 940 - [[redirects]] 941 - from = "/processing.prompt.ac" 942 - to = "/processing.aesthetic.computer" 943 - status = 301 944 - force = true 945 - [[redirects]] 946 - from = "/processing.prompt.ac/*" 947 - to = "/processing.aesthetic.computer/:splat" 948 - status = 301 949 - force = true 950 - [[redirects]] 951 - from = "/p5.prompt.ac" 952 - to = "/p5.aesthetic.computer" 953 - status = 301 954 - force = true 955 - [[redirects]] 956 - from = "/p5.prompt.ac/*" 957 - to = "/p5.aesthetic.computer/:splat" 958 - status = 301 959 - force = true 960 - # sitemap.prompt.ac → sitemap.aesthetic.computer 961 - [[redirects]] 962 - from = "https://sitemap.prompt.ac" 963 - to = "https://sitemap.aesthetic.computer" 920 + [[redirects]] 921 + from = "https://l5.prompt.ac/*" 922 + to = "https://l5.aesthetic.computer/:splat" 923 + status = 301 924 + force = true 925 + [[redirects]] 926 + from = "https://processing.prompt.ac" 927 + to = "https://processing.aesthetic.computer" 928 + status = 301 929 + force = true 930 + [[redirects]] 931 + from = "https://processing.prompt.ac/*" 932 + to = "https://processing.aesthetic.computer/:splat" 933 + status = 301 934 + force = true 935 + [[redirects]] 936 + from = "https://p5.prompt.ac" 937 + to = "https://p5.aesthetic.computer" 938 + status = 301 939 + force = true 940 + [[redirects]] 941 + from = "https://p5.prompt.ac/*" 942 + to = "https://p5.aesthetic.computer/:splat" 943 + status = 301 944 + force = true 945 + [[redirects]] 946 + from = "/processing.prompt.ac" 947 + to = "/processing.aesthetic.computer" 948 + status = 301 949 + force = true 950 + [[redirects]] 951 + from = "/processing.prompt.ac/*" 952 + to = "/processing.aesthetic.computer/:splat" 953 + status = 301 954 + force = true 955 + [[redirects]] 956 + from = "/p5.prompt.ac" 957 + to = "/p5.aesthetic.computer" 958 + status = 301 959 + force = true 960 + [[redirects]] 961 + from = "/p5.prompt.ac/*" 962 + to = "/p5.aesthetic.computer/:splat" 963 + status = 301 964 + force = true 965 + # sitemap.prompt.ac → sitemap.aesthetic.computer 966 + [[redirects]] 967 + from = "https://sitemap.prompt.ac" 968 + to = "https://sitemap.aesthetic.computer" 964 969 status = 301 965 970 force = true 966 971 [[redirects]] ··· 994 999 to = "/docs/:splat" 995 1000 status = 200 996 1001 force = true 997 - [[redirects]] 998 - from = "https://l5.aesthetic.computer/l5.aesthetic.computer/*" 999 - to = "/l5.aesthetic.computer/:splat" 1000 - status = 200 1001 - force = true 1002 - [[redirects]] 1003 - from = "https://l5.aesthetic.computer/aesthetic.computer/*" 1004 - to = "/aesthetic.computer/:splat" 1005 - status = 200 1006 - force = true 1007 - [[redirects]] 1008 - from = "https://l5.aesthetic.computer/*" 1009 - to = "/l5.aesthetic.computer/index.html" 1010 - status = 200 1011 - force = true 1012 - [[redirects]] 1013 - from = "https://processing.aesthetic.computer" 1014 - to = "/processing.aesthetic.computer/index.html" 1015 - status = 200 1016 - force = true 1017 - [[redirects]] 1018 - from = "https://processing.aesthetic.computer/docs.json" 1019 - to = "/docs.json" 1020 - status = 200 1021 - force = true 1022 - [[redirects]] 1023 - from = "https://processing.aesthetic.computer/docs/*" 1024 - to = "/docs/:splat" 1025 - status = 200 1026 - force = true 1027 - [[redirects]] 1028 - from = "https://processing.aesthetic.computer/processing.aesthetic.computer/*" 1029 - to = "/processing.aesthetic.computer/:splat" 1030 - status = 200 1031 - force = true 1032 - [[redirects]] 1033 - from = "https://processing.aesthetic.computer/aesthetic.computer/*" 1034 - to = "/aesthetic.computer/:splat" 1035 - status = 200 1036 - force = true 1037 - [[redirects]] 1038 - from = "https://processing.aesthetic.computer/*" 1039 - to = "/processing.aesthetic.computer/index.html" 1040 - status = 200 1041 - force = true 1042 - [[redirects]] 1043 - from = "https://p5.aesthetic.computer" 1044 - to = "/processing.aesthetic.computer/index.html" 1045 - status = 200 1046 - force = true 1047 - [[redirects]] 1048 - from = "https://p5.aesthetic.computer/docs.json" 1049 - to = "/docs.json" 1050 - status = 200 1051 - force = true 1052 - [[redirects]] 1053 - from = "https://p5.aesthetic.computer/docs/*" 1054 - to = "/docs/:splat" 1055 - status = 200 1056 - force = true 1057 - [[redirects]] 1058 - from = "https://p5.aesthetic.computer/processing.aesthetic.computer/*" 1059 - to = "/processing.aesthetic.computer/:splat" 1060 - status = 200 1061 - force = true 1062 - [[redirects]] 1063 - from = "https://p5.aesthetic.computer/aesthetic.computer/*" 1064 - to = "/aesthetic.computer/:splat" 1065 - status = 200 1066 - force = true 1067 - [[redirects]] 1068 - from = "https://p5.aesthetic.computer/*" 1069 - to = "/processing.aesthetic.computer/index.html" 1070 - status = 200 1071 - force = true 1072 - [[redirects]] 1073 - from = "/processing.aesthetic.computer" 1074 - to = "/processing.aesthetic.computer/index.html" 1075 - status = 200 1076 - force = true 1077 - [[redirects]] 1078 - from = "/processing.aesthetic.computer/docs.json" 1079 - to = "/docs.json" 1080 - status = 200 1081 - force = true 1082 - [[redirects]] 1083 - from = "/processing.aesthetic.computer/docs/*" 1084 - to = "/docs/:splat" 1085 - status = 200 1086 - force = true 1087 - [[redirects]] 1088 - from = "/processing.aesthetic.computer/processing.aesthetic.computer/*" 1089 - to = "/processing.aesthetic.computer/:splat" 1090 - status = 200 1091 - force = true 1092 - [[redirects]] 1093 - from = "/processing.aesthetic.computer/*" 1094 - to = "/processing.aesthetic.computer/index.html" 1095 - status = 200 1096 - force = false 1097 - [[redirects]] 1098 - from = "/p5.aesthetic.computer" 1099 - to = "/processing.aesthetic.computer/index.html" 1100 - status = 200 1101 - force = true 1102 - [[redirects]] 1103 - from = "/p5.aesthetic.computer/docs.json" 1104 - to = "/docs.json" 1105 - status = 200 1106 - force = true 1107 - [[redirects]] 1108 - from = "/p5.aesthetic.computer/docs/*" 1109 - to = "/docs/:splat" 1110 - status = 200 1111 - force = true 1112 - [[redirects]] 1113 - from = "/p5.aesthetic.computer/processing.aesthetic.computer/*" 1114 - to = "/processing.aesthetic.computer/:splat" 1115 - status = 200 1116 - force = true 1117 - [[redirects]] 1118 - from = "/p5.aesthetic.computer/*" 1119 - to = "/processing.aesthetic.computer/index.html" 1120 - status = 200 1121 - force = false 1122 - # Screenshot generation handled by oven.aesthetic.computer 1123 - [[redirects]] 1124 - from = "/preview/*" 1125 - to = "https://oven.aesthetic.computer/preview/:splat" 1002 + [[redirects]] 1003 + from = "https://l5.aesthetic.computer/l5.aesthetic.computer/*" 1004 + to = "/l5.aesthetic.computer/:splat" 1005 + status = 200 1006 + force = true 1007 + [[redirects]] 1008 + from = "https://l5.aesthetic.computer/aesthetic.computer/*" 1009 + to = "/aesthetic.computer/:splat" 1010 + status = 200 1011 + force = true 1012 + [[redirects]] 1013 + from = "https://l5.aesthetic.computer/*" 1014 + to = "/l5.aesthetic.computer/index.html" 1015 + status = 200 1016 + force = true 1017 + [[redirects]] 1018 + from = "https://processing.aesthetic.computer" 1019 + to = "/processing.aesthetic.computer/index.html" 1020 + status = 200 1021 + force = true 1022 + [[redirects]] 1023 + from = "https://processing.aesthetic.computer/docs.json" 1024 + to = "/docs.json" 1025 + status = 200 1026 + force = true 1027 + [[redirects]] 1028 + from = "https://processing.aesthetic.computer/docs/*" 1029 + to = "/docs/:splat" 1030 + status = 200 1031 + force = true 1032 + [[redirects]] 1033 + from = "https://processing.aesthetic.computer/processing.aesthetic.computer/*" 1034 + to = "/processing.aesthetic.computer/:splat" 1035 + status = 200 1036 + force = true 1037 + [[redirects]] 1038 + from = "https://processing.aesthetic.computer/aesthetic.computer/*" 1039 + to = "/aesthetic.computer/:splat" 1040 + status = 200 1041 + force = true 1042 + [[redirects]] 1043 + from = "https://processing.aesthetic.computer/*" 1044 + to = "/processing.aesthetic.computer/index.html" 1045 + status = 200 1046 + force = true 1047 + [[redirects]] 1048 + from = "https://p5.aesthetic.computer" 1049 + to = "/processing.aesthetic.computer/index.html" 1050 + status = 200 1051 + force = true 1052 + [[redirects]] 1053 + from = "https://p5.aesthetic.computer/docs.json" 1054 + to = "/docs.json" 1055 + status = 200 1056 + force = true 1057 + [[redirects]] 1058 + from = "https://p5.aesthetic.computer/docs/*" 1059 + to = "/docs/:splat" 1060 + status = 200 1061 + force = true 1062 + [[redirects]] 1063 + from = "https://p5.aesthetic.computer/processing.aesthetic.computer/*" 1064 + to = "/processing.aesthetic.computer/:splat" 1065 + status = 200 1066 + force = true 1067 + [[redirects]] 1068 + from = "https://p5.aesthetic.computer/aesthetic.computer/*" 1069 + to = "/aesthetic.computer/:splat" 1070 + status = 200 1071 + force = true 1072 + [[redirects]] 1073 + from = "https://p5.aesthetic.computer/*" 1074 + to = "/processing.aesthetic.computer/index.html" 1075 + status = 200 1076 + force = true 1077 + [[redirects]] 1078 + from = "/processing.aesthetic.computer" 1079 + to = "/processing.aesthetic.computer/index.html" 1080 + status = 200 1081 + force = true 1082 + [[redirects]] 1083 + from = "/processing.aesthetic.computer/docs.json" 1084 + to = "/docs.json" 1085 + status = 200 1086 + force = true 1087 + [[redirects]] 1088 + from = "/processing.aesthetic.computer/docs/*" 1089 + to = "/docs/:splat" 1090 + status = 200 1091 + force = true 1092 + [[redirects]] 1093 + from = "/processing.aesthetic.computer/processing.aesthetic.computer/*" 1094 + to = "/processing.aesthetic.computer/:splat" 1095 + status = 200 1096 + force = true 1097 + [[redirects]] 1098 + from = "/processing.aesthetic.computer/*" 1099 + to = "/processing.aesthetic.computer/index.html" 1100 + status = 200 1101 + force = false 1102 + [[redirects]] 1103 + from = "/p5.aesthetic.computer" 1104 + to = "/processing.aesthetic.computer/index.html" 1105 + status = 200 1106 + force = true 1107 + [[redirects]] 1108 + from = "/p5.aesthetic.computer/docs.json" 1109 + to = "/docs.json" 1110 + status = 200 1111 + force = true 1112 + [[redirects]] 1113 + from = "/p5.aesthetic.computer/docs/*" 1114 + to = "/docs/:splat" 1115 + status = 200 1116 + force = true 1117 + [[redirects]] 1118 + from = "/p5.aesthetic.computer/processing.aesthetic.computer/*" 1119 + to = "/processing.aesthetic.computer/:splat" 1120 + status = 200 1121 + force = true 1122 + [[redirects]] 1123 + from = "/p5.aesthetic.computer/*" 1124 + to = "/processing.aesthetic.computer/index.html" 1125 + status = 200 1126 + force = false 1127 + # Screenshot generation handled by oven.aesthetic.computer 1128 + [[redirects]] 1129 + from = "/preview/*" 1130 + to = "https://oven.aesthetic.computer/preview/:splat" 1126 1131 status = 200 1127 1132 # sitemap.aesthetic.computer subdomain 1128 1133 [[redirects]] ··· 1155 1160 to = "/l5.aesthetic.computer/index.html" 1156 1161 status = 200 1157 1162 force = true 1158 - [[redirects]] 1159 - from = "/L5/*" 1160 - to = "/l5.aesthetic.computer/index.html" 1161 - status = 200 1162 - force = true 1163 - [[redirects]] 1164 - from = "/processing" 1165 - to = "/processing.aesthetic.computer/index.html" 1166 - status = 200 1167 - force = true 1168 - [[redirects]] 1169 - from = "/processing/*" 1170 - to = "/processing.aesthetic.computer/index.html" 1171 - status = 200 1172 - force = true 1173 - [[redirects]] 1174 - from = "/Processing" 1175 - to = "/processing.aesthetic.computer/index.html" 1176 - status = 200 1177 - force = true 1178 - [[redirects]] 1179 - from = "/Processing/*" 1180 - to = "/processing.aesthetic.computer/index.html" 1181 - status = 200 1182 - force = true 1183 - [[redirects]] 1184 - from = "/p5" 1185 - to = "/processing.aesthetic.computer/index.html" 1186 - status = 200 1187 - force = true 1188 - [[redirects]] 1189 - from = "/p5/*" 1190 - to = "/processing.aesthetic.computer/index.html" 1191 - status = 200 1192 - force = true 1193 - [[redirects]] 1194 - from = "/P5" 1195 - to = "/processing.aesthetic.computer/index.html" 1196 - status = 200 1197 - force = true 1198 - [[redirects]] 1199 - from = "/P5/*" 1200 - to = "/processing.aesthetic.computer/index.html" 1201 - status = 200 1202 - force = true 1203 - [[redirects]] 1204 - from = "/support" 1205 - to = "/support.html" 1206 - status = 200 1163 + [[redirects]] 1164 + from = "/L5/*" 1165 + to = "/l5.aesthetic.computer/index.html" 1166 + status = 200 1167 + force = true 1168 + [[redirects]] 1169 + from = "/processing" 1170 + to = "/processing.aesthetic.computer/index.html" 1171 + status = 200 1172 + force = true 1173 + [[redirects]] 1174 + from = "/processing/*" 1175 + to = "/processing.aesthetic.computer/index.html" 1176 + status = 200 1177 + force = true 1178 + [[redirects]] 1179 + from = "/Processing" 1180 + to = "/processing.aesthetic.computer/index.html" 1181 + status = 200 1182 + force = true 1183 + [[redirects]] 1184 + from = "/Processing/*" 1185 + to = "/processing.aesthetic.computer/index.html" 1186 + status = 200 1187 + force = true 1188 + [[redirects]] 1189 + from = "/p5" 1190 + to = "/processing.aesthetic.computer/index.html" 1191 + status = 200 1192 + force = true 1193 + [[redirects]] 1194 + from = "/p5/*" 1195 + to = "/processing.aesthetic.computer/index.html" 1196 + status = 200 1197 + force = true 1198 + [[redirects]] 1199 + from = "/P5" 1200 + to = "/processing.aesthetic.computer/index.html" 1201 + status = 200 1202 + force = true 1203 + [[redirects]] 1204 + from = "/P5/*" 1205 + to = "/processing.aesthetic.computer/index.html" 1206 + status = 200 1207 + force = true 1208 + [[redirects]] 1209 + from = "/support" 1210 + to = "/support.html" 1211 + status = 200 1207 1212 [[redirects]] 1208 1213 from = "/privacy-policy" 1209 1214 to = "/privacy-policy.html"
+3 -2
system/netlify/functions/keep-mint.mjs
··· 931 931 const attributes = [ 932 932 ...analysis.traits, 933 933 ...(packDate ? [{ name: "Packed on", value: packDate }] : []), 934 - ...(authorHandle && authorHandle !== "@anon" ? [{ name: "Handle", value: `@${authorHandle.replace(/^@/, "")}` }] : []), 935 - ...(userCode ? [{ name: "User", value: userCode }] : []), 934 + ...(authorHandle && authorHandle !== "@anon" ? [{ name: "Author Handle", value: `@${authorHandle.replace(/^@/, "")}` }] : []), 935 + ...(userCode ? [{ name: "Author Code", value: userCode }] : []), 936 936 ...(depCount > 0 ? [{ name: "Dependencies", value: String(depCount) }] : []), 937 + { name: "Analyzer Version", value: ANALYZER_VERSION }, 937 938 ]; 938 939 939 940 // Creator identity for metadata
+2 -1
system/netlify/functions/keep-update.mjs
··· 8 8 9 9 import { authorize, handleFor, hasAdmin } from "../../backend/authorization.mjs"; 10 10 import { connect } from "../../backend/database.mjs"; 11 - import { analyzeKidLisp } from "../../backend/kidlisp-analyzer.mjs"; 11 + import { analyzeKidLisp, ANALYZER_VERSION } from "../../backend/kidlisp-analyzer.mjs"; 12 12 import { stream } from "@netlify/functions"; 13 13 import { TezosToolkit, MichelsonMap } from "@taquito/taquito"; 14 14 import { InMemorySigner } from "@taquito/signer"; ··· 280 280 ...analysis.traits, 281 281 { name: "Updated", value: new Date().toISOString().split('T')[0] }, 282 282 ...(authorHandle && authorHandle !== "@anon" ? [{ name: "Handle", value: authorHandle }] : []), 283 + { name: "Analyzer Version", value: ANALYZER_VERSION }, 283 284 ]; 284 285 285 286 await send("progress", { stage: "metadata", message: "✓ Metadata ready" });
-224
tezos/deploy-v6.mjs
··· 1 - #!/usr/bin/env node 2 - /** 3 - * deploy-v6.mjs - Deploy production KidLisp Keeps v6 contract 4 - * 5 - * Deploys to mainnet using the kidlisp wallet (keeps.tez). 6 - * 7 - * Contract metadata: 8 - * name: "KidLisp" 9 - * version: "6.0.0" 10 - * description: "https://keeps.kidlisp.com" 11 - * homepage: "https://kidlisp.com" 12 - * 13 - * Usage: 14 - * # Set env vars from tezos/kidlisp/.env first: 15 - * export TEZOS_KIDLISP_KEY=edsk... 16 - * export TEZOS_KIDLISP_ADDRESS=tz1Lc2DzTjDPyWFj1iuAVGGZWNjK67Wun2dC 17 - * 18 - * node deploy-v6.mjs # Deploy with kidlisp wallet 19 - * node deploy-v6.mjs --wallet=staging # Deploy with staging wallet (for testing) 20 - * node deploy-v6.mjs --dry-run # Show what would happen without deploying 21 - */ 22 - 23 - import { TezosToolkit } from '@taquito/taquito'; 24 - import { InMemorySigner } from '@taquito/signer'; 25 - import { Parser } from '@taquito/michel-codec'; 26 - import fs from 'fs'; 27 - import path from 'path'; 28 - import { fileURLToPath } from 'url'; 29 - 30 - const __filename = fileURLToPath(import.meta.url); 31 - const __dirname = path.dirname(__filename); 32 - 33 - // Parse CLI args 34 - const args = process.argv.slice(2); 35 - const walletArg = args.find(a => a.startsWith('--wallet=')); 36 - const walletName = walletArg ? walletArg.split('=')[1] : 'kidlisp'; 37 - const dryRun = args.includes('--dry-run'); 38 - 39 - // Wallet configurations 40 - const WALLETS = { 41 - kidlisp: { 42 - envFile: path.join(__dirname, 'kidlisp/.env'), 43 - addressKey: 'KIDLISP_ADDRESS', 44 - keyKey: 'KIDLISP_KEY', 45 - fallbackAddress: 'tz1Lc2DzTjDPyWFj1iuAVGGZWNjK67Wun2dC', 46 - }, 47 - staging: { 48 - envFile: path.join(__dirname, 'staging/.env'), 49 - addressKey: 'STAGING_ADDRESS', 50 - keyKey: 'STAGING_KEY', 51 - fallbackAddress: 'tz1dfoQDuxjwSgxdqJnisyKUxDHweade4Gzt', 52 - }, 53 - }; 54 - 55 - const walletConfig = WALLETS[walletName]; 56 - if (!walletConfig) { 57 - console.error(`Unknown wallet: ${walletName}. Use: kidlisp or staging`); 58 - process.exit(1); 59 - } 60 - 61 - // Load credentials from .env file 62 - function loadEnv(envPath, addressKey, keyKey, fallbackAddress) { 63 - if (!fs.existsSync(envPath)) { 64 - // Fall back to environment variables 65 - const address = process.env[`TEZOS_${addressKey}`] || process.env[addressKey] || fallbackAddress; 66 - const secretKey = process.env[`TEZOS_${keyKey}`] || process.env[keyKey]; 67 - if (!secretKey) { 68 - throw new Error(`No credentials found. Set ${keyKey} env var or create ${envPath}`); 69 - } 70 - return { address, secretKey }; 71 - } 72 - 73 - const content = fs.readFileSync(envPath, 'utf8'); 74 - const vars = {}; 75 - for (const line of content.split('\n')) { 76 - const trimmed = line.trim(); 77 - if (!trimmed || trimmed.startsWith('#')) continue; 78 - const [key, ...rest] = trimmed.split('='); 79 - if (key && rest.length > 0) { 80 - vars[key.trim()] = rest.join('=').trim().replace(/^["']|["']$/g, ''); 81 - } 82 - } 83 - 84 - const address = vars[addressKey] || vars.ADDRESS || fallbackAddress; 85 - const secretKey = vars[keyKey] || vars.KEY || vars.SECRET_KEY; 86 - if (!secretKey) throw new Error(`No secret key found in ${envPath}`); 87 - return { address, secretKey }; 88 - } 89 - 90 - function stringToBytes(str) { 91 - return Buffer.from(str, 'utf8').toString('hex'); 92 - } 93 - 94 - async function deploy() { 95 - console.log('\n╔══════════════════════════════════════════════════════════════╗'); 96 - console.log('║ 🚀 Deploying KidLisp Keeps v6 — Production Contract ║'); 97 - console.log('╚══════════════════════════════════════════════════════════════╝\n'); 98 - 99 - const { address, secretKey } = loadEnv( 100 - walletConfig.envFile, 101 - walletConfig.addressKey, 102 - walletConfig.keyKey, 103 - walletConfig.fallbackAddress 104 - ); 105 - 106 - console.log(`📡 Network: Mainnet`); 107 - console.log(`👤 Wallet: ${walletName} (${address})`); 108 - if (dryRun) console.log(`🔒 DRY RUN — no transaction will be sent\n`); 109 - 110 - const tezos = new TezosToolkit('https://mainnet.api.tez.ie'); 111 - tezos.setSignerProvider(new InMemorySigner(secretKey)); 112 - 113 - // Check balance 114 - const balance = await tezos.tz.getBalance(address); 115 - const balanceXTZ = balance.toNumber() / 1_000_000; 116 - console.log(`💰 Balance: ${balanceXTZ.toFixed(6)} XTZ\n`); 117 - 118 - if (balanceXTZ < 3) { 119 - throw new Error(`Need at least 3 XTZ for deployment. Have ${balanceXTZ.toFixed(6)} XTZ.`); 120 - } 121 - 122 - // Contract-level metadata (TZIP-016) 123 - const contractMetadata = { 124 - name: "KidLisp", 125 - version: "6.0.0", 126 - description: "https://keeps.kidlisp.com", 127 - homepage: "https://kidlisp.com", 128 - authors: ["aesthetic.computer"], 129 - interfaces: ["TZIP-012", "TZIP-016", "TZIP-021"], 130 - imageUri: "https://oven.aesthetic.computer/keeps/latest", 131 - }; 132 - 133 - console.log('📋 Contract metadata:'); 134 - console.log(` name: "${contractMetadata.name}"`); 135 - console.log(` version: "${contractMetadata.version}"`); 136 - console.log(` description: "${contractMetadata.description}"`); 137 - console.log(` homepage: "${contractMetadata.homepage}"`); 138 - console.log(` authors: ${JSON.stringify(contractMetadata.authors)}`); 139 - console.log(` imageUri: "${contractMetadata.imageUri}"`); 140 - console.log(''); 141 - 142 - // Load compiled contract (use v5 compiled output — same bytecode) 143 - // v6 Python source is identical logic, just different docstrings 144 - const contractPath = path.join(__dirname, 'KeepsFA2v5/step_002_cont_0_contract.tz'); 145 - if (!fs.existsSync(contractPath)) { 146 - throw new Error(`Compiled contract not found: ${contractPath}\nRun SmartPy compilation first, or use KeepsFA2v5 compiled output.`); 147 - } 148 - 149 - const contractSource = fs.readFileSync(contractPath, 'utf8'); 150 - console.log('📄 Contract loaded: KeepsFA2v5 (same bytecode as v6)'); 151 - 152 - const parser = new Parser(); 153 - const parsedContract = parser.parseScript(contractSource); 154 - 155 - // Build storage with contract metadata embedded 156 - const contractMetadataBytes = stringToBytes(JSON.stringify(contractMetadata)); 157 - const tezosStoragePointer = stringToBytes('tezos-storage:content'); 158 - 159 - // Initial storage: admin, content_hashes, contract_metadata_locked, default_royalty_bps, 160 - // keep_fee, ledger, metadata, metadata_locked, next_token_id, 161 - // operators, paused, token_creators, token_metadata 162 - // keep_fee = 2500000 mutez = 2.5 XTZ 163 - const initialStorageMichelson = `(Pair "${address}" (Pair {} (Pair False (Pair 1000 (Pair 2500000 (Pair {} (Pair {Elt "" 0x${tezosStoragePointer}; Elt "content" 0x${contractMetadataBytes}} (Pair {} (Pair 0 (Pair {} (Pair False (Pair {} {}))))))))))))`; 164 - 165 - console.log(`\n💰 Keep fee: 2.5 XTZ (2500000 mutez)`); 166 - console.log(`🎨 Royalties: 10% (1000 bps)`); 167 - console.log(`⏸️ Paused: false`); 168 - console.log(`🔓 Metadata locked: false\n`); 169 - 170 - if (dryRun) { 171 - console.log('╔══════════════════════════════════════════════════════════════╗'); 172 - console.log('║ 🔒 DRY RUN COMPLETE — No transaction sent ║'); 173 - console.log('╚══════════════════════════════════════════════════════════════╝\n'); 174 - console.log('Storage Michelson (first 200 chars):'); 175 - console.log(' ', initialStorageMichelson.substring(0, 200), '...\n'); 176 - return; 177 - } 178 - 179 - const parsedStorage = parser.parseMichelineExpression(initialStorageMichelson); 180 - 181 - console.log('📤 Deploying contract...'); 182 - console.log(' (This may take 1-2 minutes...)\n'); 183 - 184 - const originationOp = await tezos.contract.originate({ 185 - code: parsedContract, 186 - init: parsedStorage, 187 - }); 188 - 189 - console.log(`⏳ Operation: ${originationOp.hash}`); 190 - console.log('⏳ Waiting for confirmation...\n'); 191 - 192 - await originationOp.confirmation(1); 193 - 194 - const contractAddress = originationOp.contractAddress; 195 - 196 - console.log('╔══════════════════════════════════════════════════════════════╗'); 197 - console.log('║ ✅ KidLisp v6 Production Contract Deployed! ║'); 198 - console.log('╚══════════════════════════════════════════════════════════════╝\n'); 199 - console.log(`📍 Contract: ${contractAddress}`); 200 - console.log(`🔗 Explorer: https://tzkt.io/${contractAddress}`); 201 - console.log(`🎨 objkt: https://objkt.com/collection/${contractAddress}`); 202 - console.log(`\n📝 Next steps:`); 203 - console.log(` 1. Update constants.mjs: contract = "${contractAddress}"`); 204 - console.log(` 2. Set KEEPS_STAGING = false`); 205 - console.log(` 3. Update env var fallbacks in keep-mint.mjs, keep-update.mjs, etc.`); 206 - console.log(` 4. Deploy to Netlify`); 207 - console.log(` 5. Smoke test: first production mint\n`); 208 - 209 - // Save address 210 - const outputFile = path.join(__dirname, 'v6-contract-address.txt'); 211 - fs.writeFileSync(outputFile, contractAddress); 212 - console.log(`💾 Saved to: ${outputFile}`); 213 - 214 - // Also check new balance 215 - const newBalance = await tezos.tz.getBalance(address); 216 - const newBalanceXTZ = newBalance.toNumber() / 1_000_000; 217 - console.log(`💰 New balance: ${newBalanceXTZ.toFixed(6)} XTZ\n`); 218 - } 219 - 220 - deploy().catch(err => { 221 - console.error('\n❌ Deployment failed!'); 222 - console.error(` Error: ${err.message}\n`); 223 - process.exit(1); 224 - });
+4 -4
tezos/keeps.mjs
··· 956 956 { name: 'Language', value: 'KidLisp' }, 957 957 { name: 'Code', value: `$${pieceName}` }, 958 958 ...(authorDisplayName ? [{ name: 'Author', value: authorDisplayName }] : []), 959 - ...(userCode ? [{ name: 'User', value: userCode }] : []), 959 + ...(userCode ? [{ name: 'User Code', value: userCode }] : []), 960 960 ...(sourceCode ? [{ name: 'Lines of Code', value: String(sourceCode.split('\n').length) }] : []), 961 961 ...(depCount > 0 ? [{ name: 'Dependencies', value: String(depCount) }] : []), 962 - ...(packDate ? [{ name: 'Packed on', value: packDate }] : []), 962 + ...(packDate ? [{ name: 'Packed', value: packDate }] : []), 963 963 { name: 'Interactive', value: 'Yes' }, 964 964 { name: 'Platform', value: 'Aesthetic Computer' }, 965 965 ] ··· 1126 1126 { name: 'Language', value: 'KidLisp' }, 1127 1127 { name: 'Code', value: `$${pieceName}` }, 1128 1128 ...(authorDisplayName ? [{ name: 'Author', value: authorDisplayName }] : []), 1129 - ...(userCode ? [{ name: 'User', value: userCode }] : []), 1129 + ...(userCode ? [{ name: 'User Code', value: userCode }] : []), 1130 1130 ...(sourceCode ? [{ name: 'Lines of Code', value: String(sourceCode.split('\n').length) }] : []), 1131 1131 ...(depCount > 0 ? [{ name: 'Dependencies', value: String(depCount) }] : []), 1132 - ...(packDate ? [{ name: 'Packed on', value: packDate }] : []), 1132 + ...(packDate ? [{ name: 'Packed', value: packDate }] : []), 1133 1133 { name: 'Interactive', value: 'Yes' }, 1134 1134 { name: 'Platform', value: 'Aesthetic Computer' }, 1135 1135 ];
-431
tezos/keeps_fa2_v6.py
··· 1 - """ 2 - Keeps FA2 v6 - KidLisp Production Contract 3 - 4 - Production deployment on keeps.tez. Contract-level metadata: 5 - name: "KidLisp" 6 - description: "https://keeps.kidlisp.com" 7 - homepage: "https://kidlisp.com" 8 - version: "6.0.0" 9 - 10 - Identical contract logic to v5. v6 is a fresh deploy with: 11 - - Admin: keeps.tez (tz1Lc2DzTjDPyWFj1iuAVGGZWNjK67Wun2dC) 12 - - Clean token IDs starting from 0 13 - - Production contract metadata 14 - - 2.5 XTZ mint fee (revenue enabled) 15 - 16 - v5 features (preserved): 17 - - Default fee: 2.5 XTZ (revenue enabled by default) 18 - - Immutable content_hash: edit_metadata preserves content_hash 19 - - Improved error messages with context 20 - 21 - v4 features (preserved): 22 - - 10% Royalty Support: Automatic royalties on secondary sales (objkt.com compatible) 23 - - Emergency Pause: Admin can halt minting in emergencies 24 - - Admin Transfer: Customer service tool for edge cases 25 - 26 - v3 features (preserved): 27 - - edit_metadata: Token owner/creator can edit (preserves attribution) 28 - - token_creators: Tracks original creator for each token 29 - - Pre-encoded metadata bytes (TzKT/objkt compatible) 30 - - Fee system with withdraw capability 31 - - Burn and re-mint functionality 32 - 33 - Key feature: Metadata bytes stored directly WITHOUT sp.pack(), 34 - ensuring compatibility with TzKT, objkt, and other Tezos indexers. 35 - """ 36 - 37 - import smartpy as sp 38 - from smartpy.templates import fa2_lib as fa2 39 - 40 - main = fa2.main 41 - 42 - 43 - @sp.module 44 - def keeps_module(): 45 - import main 46 - 47 - # Order of inheritance: [Admin], [<policy>], <base class>, [<other mixins>]. 48 - class KeepsFA2( 49 - main.Admin, 50 - main.Nft, 51 - main.MintNft, 52 - main.BurnNft, 53 - main.OnchainviewBalanceOf, 54 - ): 55 - """ 56 - FA2 NFT contract for KidLisp Keeps (v6 - Production on keeps.tez). 57 - 58 - Contract-level metadata: 59 - name: "KidLisp" 60 - description: "https://keeps.kidlisp.com" 61 - homepage: "https://kidlisp.com" 62 - 63 - v5 features: 2.5 XTZ fee, immutable content_hash, improved errors 64 - v4 features: royalties, emergency pause, admin transfer 65 - v3 features: owner/creator editable metadata, attribution tracking 66 - """ 67 - 68 - def __init__(self, admin_address, contract_metadata, ledger, token_metadata): 69 - # Initialize on-chain balance view 70 - main.OnchainviewBalanceOf.__init__(self) 71 - 72 - # Initialize the NFT-specific entrypoints 73 - main.BurnNft.__init__(self) 74 - main.MintNft.__init__(self) 75 - 76 - # Initialize the NFT base class 77 - main.Nft.__init__(self, contract_metadata, ledger, token_metadata) 78 - 79 - # Initialize administrative permissions 80 - main.Admin.__init__(self, admin_address) 81 - 82 - # Additional storage for metadata locking 83 - self.data.metadata_locked = sp.cast( 84 - sp.big_map(), 85 - sp.big_map[sp.nat, sp.bool] 86 - ) 87 - 88 - # Track content hashes to prevent duplicate mints 89 - # Maps content_hash (bytes) -> token_id (nat) 90 - self.data.content_hashes = sp.cast( 91 - sp.big_map(), 92 - sp.big_map[sp.bytes, sp.nat] 93 - ) 94 - 95 - # Track original creator for each token (v3) 96 - # Maps token_id -> creator address (the first minter) 97 - self.data.token_creators = sp.cast( 98 - sp.big_map(), 99 - sp.big_map[sp.nat, sp.address] 100 - ) 101 - 102 - # Contract-level metadata lock flag 103 - self.data.contract_metadata_locked = False 104 - 105 - # Mint fee configuration (admin-adjustable) 106 - # 2.5 XTZ default fee 107 - self.data.keep_fee = sp.mutez(2500000) 108 - 109 - # Emergency pause flag 110 - # When true, minting and metadata edits are disabled 111 - self.data.paused = False 112 - 113 - # Default royalty configuration 114 - # Basis points: 1000 = 10%, 2500 = 25% (max) 115 - self.data.default_royalty_bps = 1000 # 10% default 116 - 117 - @sp.entrypoint 118 - def keep(self, params): 119 - """ 120 - Mint a new Keep token with minimal on-chain metadata. 121 - 122 - Full metadata lives in IPFS (via metadata_uri). On-chain stores only 123 - the essential fields needed for display and deduplication. 124 - 125 - Two modes: 126 - 1. Admin calling: mints to specified owner (for server-side minting) 127 - 2. User calling: mints to sender, requires fee payment (default 2.5 XTZ) 128 - 129 - All bytes parameters should be raw hex-encoded UTF-8 strings. 130 - """ 131 - sp.cast(params, sp.record( 132 - name=sp.bytes, 133 - symbol=sp.bytes, 134 - description=sp.bytes, 135 - artifactUri=sp.bytes, 136 - displayUri=sp.bytes, 137 - thumbnailUri=sp.bytes, 138 - decimals=sp.bytes, 139 - creators=sp.bytes, 140 - royalties=sp.bytes, 141 - content_hash=sp.bytes, 142 - metadata_uri=sp.bytes, 143 - owner=sp.address 144 - )) 145 - 146 - # Check if contract is paused 147 - assert not self.data.paused, "MINTING_PAUSED" 148 - 149 - # Determine minting mode and owner 150 - is_admin = self.is_administrator_() 151 - 152 - # Non-admin callers must pay the fee and can only mint to themselves 153 - if not is_admin: 154 - assert sp.amount >= self.data.keep_fee, "INSUFFICIENT_FEE" 155 - # User must mint to themselves (ensures they are firstMinter) 156 - assert params.owner == sp.sender, "MUST_MINT_TO_SELF" 157 - 158 - # Check for duplicate content hash 159 - assert not self.data.content_hashes.contains(params.content_hash), "DUPLICATE_CONTENT_HASH" 160 - 161 - # Get next token ID from the library's counter 162 - token_id = self.data.next_token_id 163 - 164 - # Minimal on-chain token_info — full metadata in IPFS via "" 165 - token_info = sp.cast({ 166 - "name": params.name, 167 - "symbol": params.symbol, 168 - "description": params.description, 169 - "artifactUri": params.artifactUri, 170 - "displayUri": params.displayUri, 171 - "thumbnailUri": params.thumbnailUri, 172 - "decimals": params.decimals, 173 - "creators": params.creators, 174 - "royalties": params.royalties, 175 - "content_hash": params.content_hash, 176 - "": params.metadata_uri 177 - }, sp.map[sp.string, sp.bytes]) 178 - 179 - # Store token metadata 180 - self.data.token_metadata[token_id] = sp.record( 181 - token_id=token_id, 182 - token_info=token_info 183 - ) 184 - 185 - # Assign token to owner 186 - self.data.ledger[token_id] = params.owner 187 - 188 - # Initialize as not locked 189 - self.data.metadata_locked[token_id] = False 190 - 191 - # Store content hash to prevent duplicates 192 - self.data.content_hashes[params.content_hash] = token_id 193 - 194 - # Track the original creator 195 - self.data.token_creators[token_id] = params.owner 196 - 197 - # Increment token counter 198 - self.data.next_token_id = token_id + 1 199 - 200 - @sp.entrypoint 201 - def edit_metadata(self, params): 202 - """ 203 - Update metadata for an existing token. 204 - 205 - Authorization: 206 - - Admin (contract administrator) 207 - - Token owner (current holder) 208 - - Original creator (preserves objkt.com attribution) 209 - 210 - Respects pause flag (cannot edit when paused). 211 - content_hash is immutable — always preserved from original mint. 212 - """ 213 - sp.cast(params, sp.record( 214 - token_id=sp.nat, 215 - token_info=sp.map[sp.string, sp.bytes] 216 - )) 217 - 218 - # Check if contract is paused 219 - assert not self.data.paused, "EDITING_PAUSED" 220 - 221 - assert self.data.token_metadata.contains(params.token_id), "FA2_TOKEN_UNDEFINED" 222 - 223 - # Check authorization: admin, owner, or original creator 224 - is_admin = self.is_administrator_() 225 - is_owner = self.data.ledger.get(params.token_id, default=sp.address("tz1burnburnburnburnburnburnburjAYjjX")) == sp.sender 226 - is_creator = self.data.token_creators.get(params.token_id, default=sp.address("tz1burnburnburnburnburnburnburjAYjjX")) == sp.sender 227 - 228 - assert is_admin or is_owner or is_creator, "NOT_AUTHORIZED" 229 - 230 - # Check if locked 231 - is_locked = self.data.metadata_locked.get(params.token_id, default=False) 232 - assert not is_locked, "METADATA_LOCKED" 233 - 234 - # Preserve immutable content_hash from original metadata 235 - existing_info = self.data.token_metadata[params.token_id].token_info 236 - original_hash = existing_info.get("content_hash", default=sp.bytes("0x")) 237 - 238 - # Update metadata 239 - self.data.token_metadata[params.token_id] = sp.record( 240 - token_id=params.token_id, 241 - token_info=params.token_info 242 - ) 243 - 244 - # Re-inject content_hash (immutable — cannot be changed or removed via edit) 245 - self.data.token_metadata[params.token_id].token_info["content_hash"] = original_hash 246 - 247 - @sp.entrypoint 248 - def lock_metadata(self, token_id): 249 - """Permanently lock metadata for a token (admin or owner only).""" 250 - sp.cast(token_id, sp.nat) 251 - 252 - assert self.data.token_metadata.contains(token_id), "FA2_TOKEN_UNDEFINED" 253 - 254 - # Check authorization: admin or owner 255 - is_admin = self.is_administrator_() 256 - is_owner = self.data.ledger.get(token_id, default=sp.address("tz1burnburnburnburnburnburnburjAYjjX")) == sp.sender 257 - 258 - assert is_admin or is_owner, "NOT_AUTHORIZED" 259 - 260 - self.data.metadata_locked[token_id] = True 261 - 262 - @sp.entrypoint 263 - def set_contract_metadata(self, params): 264 - """Update contract-level metadata (admin only, if not locked).""" 265 - sp.cast(params, sp.list[sp.record(key=sp.string, value=sp.bytes)]) 266 - 267 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 268 - assert not self.data.contract_metadata_locked, "CONTRACT_METADATA_LOCKED" 269 - 270 - for item in params: 271 - self.data.metadata[item.key] = item.value 272 - 273 - @sp.entrypoint 274 - def lock_contract_metadata(self): 275 - """Permanently lock contract-level metadata (admin only).""" 276 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 277 - self.data.contract_metadata_locked = True 278 - 279 - @sp.entrypoint 280 - def set_keep_fee(self, new_fee): 281 - """ 282 - Set the keep fee required for minting. 283 - Admin only. Fee is in mutez (1 tez = 1,000,000 mutez). 284 - """ 285 - sp.cast(new_fee, sp.mutez) 286 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 287 - self.data.keep_fee = new_fee 288 - 289 - @sp.entrypoint 290 - def withdraw_fees(self, destination): 291 - """ 292 - Withdraw accumulated fees from the contract. 293 - Admin only. Sends entire contract balance to destination. 294 - """ 295 - sp.cast(destination, sp.address) 296 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 297 - sp.send(destination, sp.balance) 298 - 299 - @sp.entrypoint 300 - def burn_keep(self, token_id): 301 - """ 302 - Burn a token and remove its content_hash. 303 - This allows re-minting the same piece name. 304 - Admin only. 305 - """ 306 - sp.cast(token_id, sp.nat) 307 - 308 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 309 - assert self.data.token_metadata.contains(token_id), "FA2_TOKEN_UNDEFINED" 310 - 311 - # Get content_hash before burning 312 - token_info = self.data.token_metadata[token_id].token_info 313 - content_hash = token_info.get("content_hash", default=sp.bytes("0x")) 314 - 315 - # Remove from registries 316 - if self.data.content_hashes.contains(content_hash): 317 - del self.data.content_hashes[content_hash] 318 - 319 - if self.data.ledger.contains(token_id): 320 - del self.data.ledger[token_id] 321 - 322 - del self.data.token_metadata[token_id] 323 - 324 - if self.data.metadata_locked.contains(token_id): 325 - del self.data.metadata_locked[token_id] 326 - 327 - if self.data.token_creators.contains(token_id): 328 - del self.data.token_creators[token_id] 329 - 330 - # ===================================================================== 331 - # v4 ENTRYPOINTS (preserved) 332 - # ===================================================================== 333 - 334 - @sp.entrypoint 335 - def pause(self): 336 - """ 337 - Emergency pause - stops minting and metadata edits. 338 - Admin only. 339 - 340 - Use cases: 341 - - Security vulnerability discovered 342 - - IPFS infrastructure issues 343 - - Spam attack detected 344 - - Contract bug found 345 - 346 - Note: Does NOT affect transfers (preserves FA2 composability) 347 - """ 348 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 349 - self.data.paused = True 350 - 351 - @sp.entrypoint 352 - def unpause(self): 353 - """ 354 - Resume normal operations after emergency pause. 355 - Admin only. 356 - """ 357 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 358 - self.data.paused = False 359 - 360 - @sp.entrypoint 361 - def set_default_royalty(self, bps): 362 - """ 363 - Set default royalty percentage for new mints. 364 - Admin only. 365 - 366 - Args: 367 - bps: Basis points (100 = 1%, 1000 = 10%, 2500 = 25% max) 368 - 369 - Example: 370 - set_default_royalty(1000) # 10% royalty 371 - """ 372 - sp.cast(bps, sp.nat) 373 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 374 - assert bps <= 2500, "MAX_ROYALTY_25_PERCENT" 375 - self.data.default_royalty_bps = bps 376 - 377 - @sp.entrypoint 378 - def admin_transfer(self, params): 379 - """Admin emergency transfer""" 380 - sp.cast(params, sp.record( 381 - token_id=sp.nat, 382 - from_=sp.address, 383 - to_=sp.address 384 - )) 385 - 386 - assert self.is_administrator_(), "FA2_NOT_ADMIN" 387 - assert self.data.token_metadata.contains(params.token_id), "FA2_TOKEN_UNDEFINED" 388 - 389 - current_owner = self.data.ledger.get(params.token_id, default=sp.address("tz1burnburnburnburnburnburnburjAYjjX")) 390 - assert current_owner == params.from_, "INVALID_CURRENT_OWNER" 391 - 392 - self.data.ledger[params.token_id] = params.to_ 393 - 394 - 395 - def _get_balance(fa2_contract, args): 396 - """Utility function to call the contract's get_balance view.""" 397 - return sp.View(fa2_contract, "get_balance")(args) 398 - 399 - 400 - def _total_supply(fa2_contract, args): 401 - """Utility function to call the contract's total_supply view.""" 402 - return sp.View(fa2_contract, "total_supply")(args) 403 - 404 - 405 - @sp.add_test() 406 - def test(): 407 - """Minimal test to compile v6 contract.""" 408 - scenario = sp.test_scenario("KeepsFA2v6") 409 - scenario.h1("Keeps FA2 v6 - KidLisp Production Contract") 410 - 411 - # Define test account 412 - admin = sp.test_account("Admin") 413 - 414 - # Create empty initial state 415 - ledger = {} 416 - token_metadata = [] 417 - 418 - # Deploy contract 419 - contract = keeps_module.KeepsFA2( 420 - admin.address, 421 - sp.big_map(), 422 - ledger, 423 - token_metadata 424 - ) 425 - 426 - scenario += contract 427 - 428 - scenario.p("v6: Production deploy on keeps.tez") 429 - scenario.p("Contract name: KidLisp") 430 - scenario.p("Fee: 2.5 XTZ, 10% royalties") 431 - scenario.p("All v3-v5 features preserved")