Something went wrong. Try again.
Monorepo for Aesthetic.Computer aesthetic.computer
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859#!/usr/bin/env node/** * keep-cli.mjs - Interactive CLI for keeping KidLisp pieces on Tezos * * Features: * - Shows AC user info and wallet status * - Previews the piece and artifact * - Uploads to IPFS with progress * - Connects to Temple via Beacon P2P * - Resumes from any stage if interrupted * * Usage: * node keep-cli.mjs <piece-code> * node keep-cli.mjs puf * node keep-cli.mjs puf --resume ipfs # Resume from IPFS upload * node keep-cli.mjs puf --preview # Preview artifact only */
import { pairWallet, sendContractCall } from "./beacon-node.mjs";import { connect } from "../system/backend/database.mjs";import { analyzeKidLisp, ANALYZER_VERSION } from "../system/backend/kidlisp-analyzer.mjs";import { TezosToolkit } from "@taquito/taquito";import * as fs from "fs";import * as path from "path";import * as readline from "readline";
// ═══════════════════════════════════════════════════════════════════// Configuration// ═══════════════════════════════════════════════════════════════════
const CONTRACT_ADDRESS = "KT1StXrQNvRd9dNPpHdCGEstcGiBV6neq79K"; // Ghostnet v3const NETWORK = "ghostnet";const RPC_URL = "https://ghostnet.ecadinfra.com";const OVEN_URL = process.env.OVEN_URL || "https://oven.aesthetic.computer";const MINT_FEE = "5000000"; // 5 XTZ in mutez
// State file for resume functionalityconst STATE_DIR = "/tmp/keep-cli";const getStateFile = (code) => `${STATE_DIR}/${code}.json`;
// ═══════════════════════════════════════════════════════════════════// Colors & Formatting// ═══════════════════════════════════════════════════════════════════
const RESET = "\x1b[0m";const BOLD = "\x1b[1m";const DIM = "\x1b[2m";const ITALIC = "\x1b[3m";const UNDERLINE = "\x1b[4m";
const BLACK = "\x1b[30m";const RED = "\x1b[31m";const GREEN = "\x1b[32m";const YELLOW = "\x1b[33m";const BLUE = "\x1b[34m";const MAGENTA = "\x1b[35m";const CYAN = "\x1b[36m";const WHITE = "\x1b[37m";
const BG_BLACK = "\x1b[40m";const BG_RED = "\x1b[41m";const BG_GREEN = "\x1b[42m";const BG_YELLOW = "\x1b[43m";const BG_BLUE = "\x1b[44m";const BG_MAGENTA = "\x1b[45m";const BG_CYAN = "\x1b[46m";const BG_WHITE = "\x1b[47m";
function box(title, content, color = CYAN) { const width = 66; const top = `${color}╔${"═".repeat(width)}╗${RESET}`; const bottom = `${color}╚${"═".repeat(width)}╝${RESET}`; const titleLine = `${color}║${RESET} ${BOLD}${title}${RESET}${" ".repeat(width - title.length - 2)}${color}║${RESET}`; console.log(top); console.log(titleLine); if (content) { for (const line of content.split("\n")) { const paddedLine = line.padEnd(width - 2); console.log(`${color}║${RESET} ${paddedLine}${color}║${RESET}`); } } console.log(bottom);}
function section(title) { console.log(`\n${BOLD}${CYAN}▸ ${title}${RESET}\n`);}
function status(icon, message, detail = "") { const detailStr = detail ? `${DIM} ${detail}${RESET}` : ""; console.log(` ${icon} ${message}${detailStr}`);}
function progress(current, total, label) { const width = 40; const filled = Math.round((current / total) * width); const bar = "█".repeat(filled) + "░".repeat(width - filled); const pct = Math.round((current / total) * 100); process.stdout.write(`\r ${CYAN}${bar}${RESET} ${pct}% ${DIM}${label}${RESET}`); if (current === total) console.log();}
function clearLine() { process.stdout.write("\r" + " ".repeat(80) + "\r");}
// ═══════════════════════════════════════════════════════════════════// State Management (for resume)// ═══════════════════════════════════════════════════════════════════
function loadState(code) { try { const stateFile = getStateFile(code); if (fs.existsSync(stateFile)) { return JSON.parse(fs.readFileSync(stateFile, "utf8")); } } catch (e) { // Ignore errors } return null;}
function saveState(code, state) { try { if (!fs.existsSync(STATE_DIR)) { fs.mkdirSync(STATE_DIR, { recursive: true }); } fs.writeFileSync(getStateFile(code), JSON.stringify(state, null, 2)); } catch (e) { console.error(`${RED}Warning: Could not save state: ${e.message}${RESET}`); }}
function clearState(code) { try { const stateFile = getStateFile(code); if (fs.existsSync(stateFile)) { fs.unlinkSync(stateFile); } } catch (e) { // Ignore }}
// ═══════════════════════════════════════════════════════════════════// Database & User Info// ═══════════════════════════════════════════════════════════════════
let db = null;
async function initDB() { if (!db) { const conn = await connect(); db = conn.db; } return db;}
async function getUserByHandle(handle) { const db = await initDB(); return db.collection("@handles").findOne({ _id: handle.replace(/^@/, "") });}
async function getUserById(userId) { const db = await initDB(); return db.collection("users").findOne({ _id: userId });}
async function getPiece(code) { const db = await initDB(); // KidLisp pieces are in the 'kidlisp' collection, stored by 'code' const piece = await db.collection("kidlisp").findOne({ code: code }); return piece;}
async function getPinataCredentials() { const db = await initDB(); const secrets = await db.collection("secrets").findOne({ _id: "pinata" }); if (!secrets) throw new Error("Pinata credentials not found"); return { jwt: secrets.jwt };}
// ═══════════════════════════════════════════════════════════════════// Tezos Helpers// ═══════════════════════════════════════════════════════════════════
function stringToBytes(str) { return Buffer.from(str, "utf8").toString("hex");}
async function checkMintStatus(pieceName) { const keyBytes = stringToBytes(pieceName); const url = `https://api.${NETWORK}.tzkt.io/v1/contracts/${CONTRACT_ADDRESS}/bigmaps/content_hashes/keys/${keyBytes}`; try { const response = await fetch(url); if (response.status === 200) { const data = await response.json(); if (data.active) { return { minted: true, tokenId: data.value, objktUrl: `https://ghostnet.objkt.com/asset/${CONTRACT_ADDRESS}/${data.value}`, }; } } return { minted: false }; } catch (e) { return { minted: false }; }}
async function getWalletBalance(address) { try { const tezos = new TezosToolkit(RPC_URL); const balance = await tezos.tz.getBalance(address); return balance.toNumber() / 1000000; } catch (e) { return null; }}
// ═══════════════════════════════════════════════════════════════════// IPFS Upload// ═══════════════════════════════════════════════════════════════════
async function uploadToIPFS(content, name, pinata) { const formData = new FormData(); // Create blob from content const blob = new Blob([content], { type: "text/html" }); formData.append("file", blob, name); // Add metadata formData.append("pinataMetadata", JSON.stringify({ name: `keep-${name}`, keyvalues: { type: "keep-artifact" } })); const response = await fetch("https://api.pinata.cloud/pinning/pinFileToIPFS", { method: "POST", headers: { "Authorization": `Bearer ${pinata.jwt}`, }, body: formData, }); if (!response.ok) { throw new Error(`Pinata upload failed: ${response.status}`); } const result = await response.json(); return { cid: result.IpfsHash, url: `ipfs://${result.IpfsHash}`, gateway: `https://gateway.pinata.cloud/ipfs/${result.IpfsHash}`, };}
async function uploadJSONToIPFS(data, name, pinata) { const response = await fetch("https://api.pinata.cloud/pinning/pinJSONToIPFS", { method: "POST", headers: { "Authorization": `Bearer ${pinata.jwt}`, "Content-Type": "application/json", }, body: JSON.stringify({ pinataContent: data, pinataMetadata: { name: `keep-${name}-metadata` }, }), }); if (!response.ok) { throw new Error(`Pinata JSON upload failed: ${response.status}`); } const result = await response.json(); return { cid: result.IpfsHash, url: `ipfs://${result.IpfsHash}`, gateway: `https://gateway.pinata.cloud/ipfs/${result.IpfsHash}`, };}
// ═══════════════════════════════════════════════════════════════════// Artifact Generation// ═══════════════════════════════════════════════════════════════════
const dev = process.env.NODE_ENV !== "production";const BUNDLE_BASE = dev ? "https://localhost:8888/api" : "https://aesthetic.computer/api";
async function generateArtifact(code, piece) { // Use the local dev server or production bundle endpoint const bundleUrl = `${BUNDLE_BASE}/bundle-html?code=${encodeURIComponent(code)}&format=json`; status("📦", "Generating artifact bundle...", bundleUrl); // Allow self-signed certs in dev if (dev) process.env.NODE_TLS_REJECT_UNAUTHORIZED = "0"; const response = await fetch(bundleUrl); if (!response.ok) { throw new Error(`Bundle generation failed: ${response.status}`); } const data = await response.json(); if (data.error) { throw new Error(`Bundle error: ${data.error}`); } const html = Buffer.from(data.content, "base64").toString("utf8"); return html;}
async function generateThumbnail(code) { const thumbUrl = `${OVEN_URL}/thumbnail?code=${encodeURIComponent(code)}&width=512&height=512`; status("🖼️ ", "Generating thumbnail...", thumbUrl); const response = await fetch(thumbUrl, { timeout: 60000 }); if (!response.ok) { console.log(`${YELLOW} ⚠ Thumbnail generation failed, using placeholder${RESET}`); return null; } return await response.arrayBuffer();}
// ═══════════════════════════════════════════════════════════════════// Build Michelson Parameters// ═══════════════════════════════════════════════════════════════════
function buildKeepParams(metadata) { // Build the deeply nested Michelson params for the keep entrypoint return { prim: "Pair", args: [ { bytes: metadata.artifactUri }, { prim: "Pair", args: [ { bytes: metadata.attributes }, { prim: "Pair", args: [ { bytes: metadata.content_hash }, { prim: "Pair", args: [ { bytes: metadata.content_type }, { prim: "Pair", args: [ { bytes: metadata.creators }, { prim: "Pair", args: [ { bytes: metadata.decimals }, { prim: "Pair", args: [ { bytes: metadata.description }, { prim: "Pair", args: [ { bytes: metadata.displayUri }, { prim: "Pair", args: [ { bytes: metadata.formats }, { prim: "Pair", args: [ { bytes: metadata.isBooleanAmount }, { prim: "Pair", args: [ { bytes: metadata.metadata_uri }, { prim: "Pair", args: [ { bytes: metadata.name }, { prim: "Pair", args: [ { string: metadata.owner }, { prim: "Pair", args: [ { bytes: metadata.rights }, { prim: "Pair", args: [ { bytes: metadata.shouldPreferSymbol }, { prim: "Pair", args: [ { bytes: metadata.symbol }, { prim: "Pair", args: [ { bytes: metadata.tags }, { bytes: metadata.thumbnailUri } ]} ]} ]} ]} ]} ]} ]} ]} ]} ]} ]} ]} ]} ]} ]} ]} ] };}
// ═══════════════════════════════════════════════════════════════════// Interactive Prompts// ═══════════════════════════════════════════════════════════════════
function prompt(question) { return new Promise((resolve) => { const rl = readline.createInterface({ input: process.stdin, output: process.stdout }); rl.question(question, (answer) => { rl.close(); resolve(answer.trim()); }); });}
async function confirm(question) { const answer = await prompt(`${question} ${DIM}(y/n)${RESET} `); return answer.toLowerCase() === "y" || answer.toLowerCase() === "yes";}
// ═══════════════════════════════════════════════════════════════════// Main CLI Flow// ═══════════════════════════════════════════════════════════════════
async function main() { const args = process.argv.slice(2); let pieceCode = args.find(a => !a.startsWith("--")); const resumeStage = args.includes("--resume") ? args[args.indexOf("--resume") + 1] : null; const previewOnly = args.includes("--preview"); const skipConfirm = args.includes("--yes") || args.includes("-y"); console.clear(); // ───────────────────────────────────────────────────────────────── // Header // ───────────────────────────────────────────────────────────────── console.log(`${MAGENTA}${BOLD} ██╗ ██╗███████╗███████╗██████╗ ██║ ██╔╝██╔════╝██╔════╝██╔══██╗ █████╔╝ █████╗ █████╗ ██████╔╝ ██╔═██╗ ██╔══╝ ██╔══╝ ██╔═══╝ ██║ ██╗███████╗███████╗██║ ╚═╝ ╚═╝╚══════╝╚══════╝╚═╝ ${RESET} ${DIM}KidLisp NFT Minting CLI for Aesthetic Computer${RESET} ${DIM}Network: ${CYAN}${NETWORK}${RESET} ${DIM}| Contract: ${CYAN}${CONTRACT_ADDRESS.slice(0, 12)}...${RESET}`);
// ───────────────────────────────────────────────────────────────── // Get piece code // ───────────────────────────────────────────────────────────────── if (!pieceCode) { pieceCode = await prompt(`${CYAN}?${RESET} Enter piece code (e.g., ${GREEN}puf${RESET}): `); if (!pieceCode) { console.log(`${RED}✗ No piece code provided${RESET}\n`); process.exit(1); } } const cleanCode = pieceCode.replace(/^\$/, ""); // ───────────────────────────────────────────────────────────────── // Load saved state (for resume) // ───────────────────────────────────────────────────────────────── let state = loadState(cleanCode) || { stage: "init", code: cleanCode, startedAt: new Date().toISOString(), }; if (resumeStage) { status("🔄", `Resuming from stage: ${resumeStage}`); state.stage = resumeStage; } // ───────────────────────────────────────────────────────────────── // Step 1: Load piece from database // ───────────────────────────────────────────────────────────────── section("Piece Information"); status("🔍", "Loading piece from database..."); let piece; try { piece = await getPiece(cleanCode); } catch (e) { console.log(`${RED} ✗ Database error: ${e.message}${RESET}`); process.exit(1); } if (!piece) { console.log(`${RED} ✗ Piece not found: $${cleanCode}${RESET}`); console.log(`${DIM} Make sure the piece exists in the database${RESET}\n`); process.exit(1); } // Get owner info (KidLisp pieces use 'user' field for owner ID) let owner = null; let ownerHandle = null; if (piece.user) { owner = await getUserById(piece.user); if (owner?.atproto?.handle) { // Handle is stored in atproto.handle (e.g. "jeffrey.at.aesthetic.computer" -> "@jeffrey") const fullHandle = owner.atproto.handle; ownerHandle = fullHandle.replace('.at.aesthetic.computer', ''); } else if (owner?.handle) { ownerHandle = owner.handle; } } console.log(); box(`📜 $${cleanCode}`, [ `${BOLD}Name:${RESET} ${piece.name || cleanCode}`, `${BOLD}Owner:${RESET} ${ownerHandle ? `@${ownerHandle}` : piece.user || "unknown"}`, `${BOLD}Created:${RESET} ${piece.when ? new Date(piece.when).toLocaleDateString() : "unknown"}`, `${BOLD}Hits:${RESET} ${piece.hits || 0}`, `${BOLD}Lines:${RESET} ${piece.source?.split("\\n").length || "?"} lines of KidLisp`, ].join("\\n"), GREEN); // Show code preview (KidLisp uses 'source' field) if (piece.source) { console.log(`\n${DIM} Code preview:${RESET}`); const lines = piece.source.split("\n").slice(0, 5); for (const line of lines) { console.log(`${DIM} ${line.slice(0, 60)}${line.length > 60 ? "..." : ""}${RESET}`); } if (piece.source.split("\n").length > 5) { console.log(`${DIM} ... (${piece.source.split("\n").length - 5} more lines)${RESET}`); } } // ───────────────────────────────────────────────────────────────── // Step 2: Check mint status // ───────────────────────────────────────────────────────────────── section("Mint Status"); status("🔎", "Checking if already minted..."); const mintStatus = await checkMintStatus(cleanCode); if (mintStatus.minted) { console.log(`\n${YELLOW} ⚠ This piece is already minted!${RESET}`); console.log(`${DIM} Token ID: ${mintStatus.tokenId}${RESET}`); console.log(`${DIM} ${mintStatus.objktUrl}${RESET}\n`); if (!await confirm(`${YELLOW}Continue anyway?${RESET}`)) { process.exit(0); } } else { status("✓", "Not yet minted", GREEN); } // ───────────────────────────────────────────────────────────────── // Step 3: Connect wallet // ───────────────────────────────────────────────────────────────── section("Wallet Connection"); let walletAddress = state.walletAddress; let client = null; if (!walletAddress || state.stage === "init") { status("📱", "Connecting to Temple wallet via Beacon P2P..."); console.log(); try { const pairResult = await pairWallet("Aesthetic Computer Keep"); if (!pairResult?.permissionResponse?.address) { throw new Error("Failed to get wallet address"); } client = pairResult.client; walletAddress = pairResult.permissionResponse.address; state.walletAddress = walletAddress; state.stage = "wallet"; saveState(cleanCode, state); } catch (e) { console.log(`${RED} ✗ Wallet connection failed: ${e.message}${RESET}\n`); process.exit(1); } } else { status("✓", `Using saved wallet: ${walletAddress.slice(0, 12)}...`, GREEN); // Need to reconnect for the transaction status("📱", "Reconnecting wallet for transaction..."); console.log(); try { const pairResult = await pairWallet("Aesthetic Computer Keep"); client = pairResult.client; walletAddress = pairResult.permissionResponse.address; } catch (e) { console.log(`${RED} ✗ Wallet reconnection failed: ${e.message}${RESET}\n`); process.exit(1); } } // Get wallet balance const balance = await getWalletBalance(walletAddress); console.log(); box("💳 Wallet", [ `${BOLD}Address:${RESET} ${walletAddress}`, `${BOLD}Balance:${RESET} ${balance !== null ? `${balance.toFixed(2)} ꜩ` : "unknown"}`, `${BOLD}Network:${RESET} ${NETWORK}`, ].join("\n"), BLUE); if (balance !== null && balance < 6) { console.log(`\n${YELLOW} ⚠ Low balance! You need at least 6 ꜩ (5 ꜩ mint fee + gas)${RESET}`); console.log(`${DIM} Get testnet XTZ: https://faucet.ghostnet.teztnets.com${RESET}\n`); } // ───────────────────────────────────────────────────────────────── // Step 4: Generate artifact // ───────────────────────────────────────────────────────────────── section("Artifact Generation"); let artifactHtml = state.artifactHtml; let thumbnailData = state.thumbnailCid ? { cid: state.thumbnailCid } : null; if (!artifactHtml || state.stage === "wallet") { try { artifactHtml = await generateArtifact(cleanCode, piece); status("✓", `Bundle generated (${(artifactHtml.length / 1024).toFixed(1)} KB)`, GREEN); state.artifactHtml = artifactHtml; state.stage = "artifact"; saveState(cleanCode, state); } catch (e) { console.log(`${RED} ✗ Artifact generation failed: ${e.message}${RESET}\n`); process.exit(1); } } else { status("✓", `Using cached artifact (${(artifactHtml.length / 1024).toFixed(1)} KB)`, GREEN); } // Preview option if (previewOnly) { const previewPath = `/tmp/keep-preview-${cleanCode}.html`; fs.writeFileSync(previewPath, artifactHtml); console.log(`\n${CYAN} Preview saved to: ${previewPath}${RESET}`); console.log(`${DIM} Open in browser to test the artifact${RESET}\n`); const openPreview = await confirm(`${CYAN}Open in browser?${RESET}`); if (openPreview) { const { exec } = await import("child_process"); exec(`$BROWSER "file://${previewPath}"`); } if (!await confirm(`${CYAN}Continue with minting?${RESET}`)) { process.exit(0); } } // ───────────────────────────────────────────────────────────────── // Step 5: Upload to IPFS // ───────────────────────────────────────────────────────────────── section("IPFS Upload"); let pinata; try { pinata = await getPinataCredentials(); status("✓", "Pinata credentials loaded", GREEN); } catch (e) { console.log(`${RED} ✗ Could not load Pinata credentials: ${e.message}${RESET}\n`); process.exit(1); } let artifactCid = state.artifactCid; let metadataCid = state.metadataCid; // Upload artifact HTML if (!artifactCid || state.stage === "artifact") { status("📤", "Uploading artifact to IPFS..."); try { const result = await uploadToIPFS(artifactHtml, `${cleanCode}.html`, pinata); artifactCid = result.cid; status("✓", `Artifact uploaded: ${CYAN}${artifactCid.slice(0, 20)}...${RESET}`, GREEN); console.log(`${DIM} Gateway: ${result.gateway}${RESET}`); state.artifactCid = artifactCid; state.stage = "ipfs-artifact"; saveState(cleanCode, state); } catch (e) { console.log(`${RED} ✗ Artifact upload failed: ${e.message}${RESET}\n`); console.log(`${DIM} Run with --resume ipfs-artifact to retry${RESET}\n`); process.exit(1); } } else { status("✓", `Using cached artifact CID: ${artifactCid.slice(0, 20)}...`, GREEN); } // Build metadata const now = new Date().toISOString(); const metadata = { name: `$${cleanCode}`, symbol: `$${cleanCode}`, description: piece.description || `KidLisp piece: $${cleanCode}`, artifactUri: `ipfs://${artifactCid}`, displayUri: `https://aesthetic.computer/$${cleanCode}`, thumbnailUri: thumbnailData?.cid ? `ipfs://${thumbnailData.cid}` : `https://aesthetic.computer/$${cleanCode}/thumbnail`, creators: [walletAddress], decimals: 0, isBooleanAmount: true, shouldPreferSymbol: false, date: now, tags: ["KidLisp"], attributes: [], formats: [ { uri: `ipfs://${artifactCid}`, mimeType: "text/html", fileName: `${cleanCode}.html`, } ], rights: "", }; // Upload metadata JSON if (!metadataCid || state.stage === "ipfs-artifact") { status("📤", "Uploading metadata to IPFS..."); try { const result = await uploadJSONToIPFS(metadata, cleanCode, pinata); metadataCid = result.cid; status("✓", `Metadata uploaded: ${CYAN}${metadataCid.slice(0, 20)}...${RESET}`, GREEN); console.log(`${DIM} Gateway: ${result.gateway}${RESET}`); state.metadataCid = metadataCid; state.stage = "ipfs-metadata"; saveState(cleanCode, state); } catch (e) { console.log(`${RED} ✗ Metadata upload failed: ${e.message}${RESET}\n`); console.log(`${DIM} Run with --resume ipfs-metadata to retry${RESET}\n`); process.exit(1); } } else { status("✓", `Using cached metadata CID: ${metadataCid.slice(0, 20)}...`, GREEN); } // ───────────────────────────────────────────────────────────────── // Step 6: Mint confirmation // ───────────────────────────────────────────────────────────────── section("Ready to Mint"); console.log(); box("🏺 Keep Summary", [ `${BOLD}Piece:${RESET} $${cleanCode}`, `${BOLD}Owner:${RESET} ${walletAddress.slice(0, 20)}...`, `${BOLD}Artifact:${RESET} ipfs://${artifactCid.slice(0, 20)}...`, `${BOLD}Metadata:${RESET} ipfs://${metadataCid.slice(0, 20)}...`, `${BOLD}Mint Fee:${RESET} 5 ꜩ`, `${BOLD}Network:${RESET} ${NETWORK}`, `${BOLD}Contract:${RESET} ${CONTRACT_ADDRESS}`, ].join("\n"), MAGENTA); console.log(); if (!skipConfirm) { const proceed = await confirm(`${BOLD}${GREEN}Proceed with minting?${RESET}`); if (!proceed) { console.log(`\n${YELLOW}Minting cancelled. State saved for resume.${RESET}\n`); process.exit(0); } } // ───────────────────────────────────────────────────────────────── // Step 7: Execute mint transaction // ───────────────────────────────────────────────────────────────── section("Minting"); // Build Michelson parameters const keepMetadata = { artifactUri: stringToBytes(`ipfs://${artifactCid}`), attributes: stringToBytes("[]"), content_hash: stringToBytes(cleanCode), content_type: stringToBytes("text/html"), creators: stringToBytes(JSON.stringify([walletAddress])), decimals: stringToBytes("0"), description: stringToBytes(metadata.description), displayUri: stringToBytes(metadata.displayUri), formats: stringToBytes(JSON.stringify(metadata.formats)), isBooleanAmount: stringToBytes("true"), metadata_uri: stringToBytes(`ipfs://${metadataCid}`), name: stringToBytes(metadata.name), owner: walletAddress, rights: stringToBytes(""), shouldPreferSymbol: stringToBytes("false"), symbol: stringToBytes(metadata.symbol), tags: stringToBytes(JSON.stringify(metadata.tags)), thumbnailUri: stringToBytes(metadata.thumbnailUri), }; const keepParams = buildKeepParams(keepMetadata); status("📤", "Sending transaction to wallet..."); console.log(`\n${YELLOW} 📱 Check Temple wallet to approve the transaction${RESET}\n`); try { const response = await sendContractCall( client, CONTRACT_ADDRESS, "keep", keepParams, MINT_FEE ); if (response.transactionHash) { state.txHash = response.transactionHash; state.stage = "complete"; saveState(cleanCode, state); console.log(); box("🎉 Keep Minted Successfully!", [ `${BOLD}Transaction:${RESET} ${response.transactionHash}`, ``, `${BOLD}View on TzKT:${RESET}`, ` https://ghostnet.tzkt.io/${response.transactionHash}`, ``, `${BOLD}View on Objkt:${RESET}`, ` https://ghostnet.objkt.com/asset/${CONTRACT_ADDRESS}`, ].join("\n"), GREEN); // Clear state on success clearState(cleanCode); } else { throw new Error("No transaction hash returned"); } } catch (e) { console.log(`\n${RED} ✗ Minting failed: ${e.message}${RESET}\n`); console.log(`${DIM} State saved. Run again to retry from mint step.${RESET}\n`); process.exit(1); } console.log();}
// Runmain().catch(err => { console.error(`${RED}Error: ${err.message}${RESET}`); if (err.stack && process.env.DEBUG) { console.error(err.stack); } process.exit(1);});