Monorepo for Aesthetic.Computer
aesthetic.computer
1<!DOCTYPE html>
2<html lang="en">
3<head>
4 <meta charset="UTF-8">
5 <meta name="viewport" content="width=device-width, initial-scale=1.0">
6 <title>🔮 Keeps Multitool</title>
7 <script src="https://unpkg.com/@taquito/taquito@19.2.0/dist/taquito.min.js"></script>
8 <script src="https://unpkg.com/@airgap/beacon-sdk@4.2.2/dist/walletbeacon.min.js"></script>
9 <style>
10 :root {
11 --bg: #0a0a0f;
12 --surface: #12121a;
13 --surface2: #1a1a25;
14 --border: #2a2a3a;
15 --text: #e0e0e8;
16 --text-dim: #8888a0;
17 --accent: #7c3aed;
18 --accent-dim: #5b21b6;
19 --success: #10b981;
20 --warning: #f59e0b;
21 --error: #ef4444;
22 }
23
24 * { box-sizing: border-box; margin: 0; padding: 0; }
25
26 body {
27 font-family: 'SF Mono', 'Fira Code', 'Consolas', monospace;
28 background: var(--bg);
29 color: var(--text);
30 min-height: 100vh;
31 padding: 20px;
32 line-height: 1.6;
33 }
34
35 .container {
36 max-width: 1200px;
37 margin: 0 auto;
38 }
39
40 header {
41 display: flex;
42 justify-content: space-between;
43 align-items: center;
44 margin-bottom: 30px;
45 padding-bottom: 20px;
46 border-bottom: 1px solid var(--border);
47 }
48
49 h1 {
50 font-size: 1.5rem;
51 font-weight: 600;
52 }
53
54 h1 span { opacity: 0.6; }
55
56 .wallet-section {
57 display: flex;
58 align-items: center;
59 gap: 15px;
60 }
61
62 .wallet-info {
63 text-align: right;
64 font-size: 0.85rem;
65 }
66
67 .wallet-address {
68 color: var(--accent);
69 font-family: monospace;
70 }
71
72 .wallet-balance {
73 color: var(--text-dim);
74 }
75
76 button {
77 background: var(--accent);
78 color: white;
79 border: none;
80 padding: 10px 20px;
81 border-radius: 6px;
82 cursor: pointer;
83 font-family: inherit;
84 font-size: 0.9rem;
85 transition: all 0.2s;
86 }
87
88 button:hover { background: var(--accent-dim); }
89 button:disabled { opacity: 0.5; cursor: not-allowed; }
90 button.secondary {
91 background: var(--surface2);
92 border: 1px solid var(--border);
93 }
94 button.secondary:hover { background: var(--border); }
95 button.danger { background: var(--error); }
96 button.success { background: var(--success); }
97
98 .grid {
99 display: grid;
100 grid-template-columns: repeat(auto-fit, minmax(350px, 1fr));
101 gap: 20px;
102 }
103
104 .card {
105 background: var(--surface);
106 border: 1px solid var(--border);
107 border-radius: 12px;
108 padding: 20px;
109 }
110
111 .card h2 {
112 font-size: 1rem;
113 margin-bottom: 15px;
114 display: flex;
115 align-items: center;
116 gap: 8px;
117 }
118
119 .card h2 .icon { font-size: 1.2rem; }
120
121 .form-group {
122 margin-bottom: 15px;
123 }
124
125 label {
126 display: block;
127 font-size: 0.8rem;
128 color: var(--text-dim);
129 margin-bottom: 5px;
130 }
131
132 input, select, textarea {
133 width: 100%;
134 background: var(--surface2);
135 border: 1px solid var(--border);
136 color: var(--text);
137 padding: 10px 12px;
138 border-radius: 6px;
139 font-family: inherit;
140 font-size: 0.9rem;
141 }
142
143 input:focus, select:focus, textarea:focus {
144 outline: none;
145 border-color: var(--accent);
146 }
147
148 textarea { min-height: 80px; resize: vertical; }
149
150 .status-badge {
151 display: inline-flex;
152 align-items: center;
153 gap: 6px;
154 padding: 4px 10px;
155 border-radius: 20px;
156 font-size: 0.75rem;
157 font-weight: 500;
158 }
159
160 .status-badge.connected {
161 background: rgba(16, 185, 129, 0.2);
162 color: var(--success);
163 }
164
165 .status-badge.disconnected {
166 background: rgba(239, 68, 68, 0.2);
167 color: var(--error);
168 }
169
170 .status-badge .dot {
171 width: 8px;
172 height: 8px;
173 border-radius: 50%;
174 background: currentColor;
175 }
176
177 .info-grid {
178 display: grid;
179 grid-template-columns: 1fr 1fr;
180 gap: 10px;
181 }
182
183 .info-item {
184 background: var(--surface2);
185 padding: 12px;
186 border-radius: 8px;
187 }
188
189 .info-item label {
190 margin-bottom: 4px;
191 }
192
193 .info-item .value {
194 font-size: 1.1rem;
195 font-weight: 600;
196 word-break: break-all;
197 }
198
199 .info-item .value.mono {
200 font-size: 0.85rem;
201 font-weight: 400;
202 }
203
204 .log-area {
205 background: var(--bg);
206 border: 1px solid var(--border);
207 border-radius: 8px;
208 padding: 15px;
209 max-height: 300px;
210 overflow-y: auto;
211 font-size: 0.8rem;
212 }
213
214 .log-entry {
215 padding: 6px 0;
216 border-bottom: 1px solid var(--border);
217 }
218
219 .log-entry:last-child { border-bottom: none; }
220
221 .log-entry.error { color: var(--error); }
222 .log-entry.success { color: var(--success); }
223 .log-entry.info { color: var(--text-dim); }
224
225 .log-time {
226 color: var(--text-dim);
227 margin-right: 10px;
228 }
229
230 .network-select {
231 display: flex;
232 gap: 10px;
233 margin-bottom: 15px;
234 }
235
236 .network-btn {
237 flex: 1;
238 padding: 8px;
239 background: var(--surface2);
240 border: 2px solid var(--border);
241 color: var(--text-dim);
242 border-radius: 6px;
243 cursor: pointer;
244 transition: all 0.2s;
245 }
246
247 .network-btn.active {
248 border-color: var(--accent);
249 color: var(--accent);
250 background: rgba(124, 58, 237, 0.1);
251 }
252
253 .token-list {
254 max-height: 200px;
255 overflow-y: auto;
256 }
257
258 .token-item {
259 display: flex;
260 justify-content: space-between;
261 align-items: center;
262 padding: 10px;
263 background: var(--surface2);
264 border-radius: 6px;
265 margin-bottom: 8px;
266 }
267
268 .token-item .id { font-weight: 600; color: var(--accent); }
269 .token-item .name { color: var(--text); }
270 .token-item .owner { font-size: 0.75rem; color: var(--text-dim); }
271
272 .actions {
273 display: flex;
274 gap: 10px;
275 margin-top: 15px;
276 }
277
278 .actions button { flex: 1; }
279
280 .full-width { grid-column: 1 / -1; }
281
282 .tabs {
283 display: flex;
284 gap: 5px;
285 margin-bottom: 15px;
286 border-bottom: 1px solid var(--border);
287 padding-bottom: 10px;
288 }
289
290 .tab {
291 padding: 8px 16px;
292 background: transparent;
293 border: none;
294 color: var(--text-dim);
295 cursor: pointer;
296 border-radius: 6px 6px 0 0;
297 transition: all 0.2s;
298 }
299
300 .tab:hover { color: var(--text); }
301 .tab.active {
302 background: var(--surface2);
303 color: var(--accent);
304 }
305
306 .tab-content { display: none; }
307 .tab-content.active { display: block; }
308
309 @media (max-width: 768px) {
310 .grid { grid-template-columns: 1fr; }
311 header { flex-direction: column; gap: 15px; text-align: center; }
312 .wallet-info { text-align: center; }
313 }
314
315 .warning-badge {
316 background: rgba(245, 158, 11, 0.2);
317 color: var(--warning);
318 padding: 8px 12px;
319 border-radius: 6px;
320 font-size: 0.8rem;
321 margin-top: 10px;
322 }
323
324 .entrypoint-list {
325 display: flex;
326 flex-wrap: wrap;
327 gap: 6px;
328 margin-top: 10px;
329 }
330
331 .entrypoint-tag {
332 background: var(--surface2);
333 border: 1px solid var(--border);
334 padding: 4px 8px;
335 border-radius: 4px;
336 font-size: 0.7rem;
337 color: var(--text-dim);
338 }
339
340 .entrypoint-tag.available {
341 border-color: var(--success);
342 color: var(--success);
343 }
344
345 .entrypoint-tag.missing {
346 border-color: var(--error);
347 color: var(--error);
348 opacity: 0.6;
349 }
350 </style>
351</head>
352<body>
353 <div class="container">
354 <header>
355 <h1>🔮 Keeps Multitool <span>v1.0</span></h1>
356 <div class="wallet-section">
357 <div class="wallet-info" id="walletInfo">
358 <div class="status-badge disconnected">
359 <span class="dot"></span>
360 <span>Not Connected</span>
361 </div>
362 </div>
363 <button id="connectBtn" onclick="toggleWallet()">Connect Wallet</button>
364 </div>
365 </header>
366
367 <div class="card" style="margin-bottom: 20px;">
368 <h2><span class="icon">⚙️</span> Configuration</h2>
369 <div class="network-select">
370 <button class="network-btn active" data-network="ghostnet" onclick="setNetwork('ghostnet')">
371 👻 Ghostnet
372 </button>
373 <button class="network-btn" data-network="mainnet" onclick="setNetwork('mainnet')">
374 🌐 Mainnet
375 </button>
376 </div>
377 <div class="form-group">
378 <label>Contract Address</label>
379 <input type="text" id="contractAddress" value="KT1NeytR5BHDfGBjG9ZuLkPd7nmufmH1icVc" placeholder="KT1...">
380 </div>
381 <button onclick="loadContractInfo()">🔄 Load Contract Info</button>
382 <div id="entrypointsList" class="entrypoint-list" style="display: none;"></div>
383 </div>
384
385 <div class="grid">
386 <!-- Contract Info -->
387 <div class="card">
388 <h2><span class="icon">📊</span> Contract Status</h2>
389 <div class="info-grid">
390 <div class="info-item">
391 <label>Next Token ID</label>
392 <div class="value" id="nextTokenId">-</div>
393 </div>
394 <div class="info-item">
395 <label>Keep Fee</label>
396 <div class="value" id="keepFee">-</div>
397 </div>
398 <div class="info-item">
399 <label>Contract Balance</label>
400 <div class="value" id="contractBalance">-</div>
401 </div>
402 <div class="info-item">
403 <label>Metadata Locked</label>
404 <div class="value" id="metadataLocked">-</div>
405 </div>
406 </div>
407 <div class="info-item" style="margin-top: 10px;">
408 <label>Administrator</label>
409 <div class="value mono" id="adminAddress">-</div>
410 </div>
411 </div>
412
413 <!-- Fee Management -->
414 <div class="card">
415 <h2><span class="icon">💰</span> Fee Management</h2>
416 <div class="form-group">
417 <label>Current Fee</label>
418 <div style="font-size: 1.5rem; font-weight: 600; color: var(--accent);" id="currentFeeDisplay">0 XTZ</div>
419 </div>
420 <div class="form-group">
421 <label>New Fee (XTZ)</label>
422 <input type="number" id="newFee" placeholder="0.5" step="0.1" min="0">
423 </div>
424 <div class="actions">
425 <button onclick="setKeepFee()">Set Fee</button>
426 <button class="success" onclick="withdrawFees()">Withdraw</button>
427 </div>
428 </div>
429
430 <!-- Mint Token -->
431 <div class="card">
432 <h2><span class="icon">✨</span> Keep (Mint)</h2>
433 <div class="form-group">
434 <label>Piece Name (e.g., "cow")</label>
435 <input type="text" id="pieceName" placeholder="cow">
436 </div>
437 <div class="form-group">
438 <label>Owner Address</label>
439 <input type="text" id="ownerAddress" placeholder="tz1... (leave empty for connected wallet)">
440 </div>
441 <div class="form-group">
442 <label>Artifact URI</label>
443 <input type="text" id="artifactUri" placeholder="ipfs://Qm...">
444 </div>
445 <div class="form-group">
446 <label>Description</label>
447 <textarea id="tokenDescription" placeholder="(wipe "blue") (ink "yellow")..."></textarea>
448 </div>
449 <button onclick="mintToken()" style="width: 100%;">🔮 Keep This Piece</button>
450 </div>
451
452 <!-- Token Management -->
453 <div class="card">
454 <h2><span class="icon">🎨</span> Token Management</h2>
455 <div class="tabs">
456 <button class="tab active" onclick="showTab('tokens', this)">Tokens</button>
457 <button class="tab" onclick="showTab('edit', this)">Edit</button>
458 <button class="tab" onclick="showTab('burn', this)">Burn</button>
459 </div>
460
461 <div id="tokens-tab" class="tab-content active">
462 <div class="token-list" id="tokenList">
463 <div style="color: var(--text-dim); text-align: center; padding: 20px;">
464 Click "Load Contract Info" to fetch tokens
465 </div>
466 </div>
467 </div>
468
469 <div id="edit-tab" class="tab-content">
470 <div class="form-group">
471 <label>Token ID</label>
472 <input type="number" id="editTokenId" placeholder="0" min="0">
473 </div>
474 <div class="form-group">
475 <label>New Name</label>
476 <input type="text" id="editName" placeholder="$newname">
477 </div>
478 <button onclick="editMetadata()">Update Metadata</button>
479 </div>
480
481 <div id="burn-tab" class="tab-content">
482 <div class="form-group">
483 <label>Token ID to Burn</label>
484 <input type="number" id="burnTokenId" placeholder="0" min="0">
485 </div>
486 <p style="font-size: 0.8rem; color: var(--warning); margin-bottom: 15px;">
487 ⚠️ This will permanently destroy the token and allow the piece name to be re-minted.
488 </p>
489 <button class="danger" onclick="burnToken()">🔥 Burn Token</button>
490 </div>
491 </div>
492
493 <!-- Lock Controls -->
494 <div class="card">
495 <h2><span class="icon">🔒</span> Lock Controls</h2>
496 <div class="form-group">
497 <label>Lock Token Metadata</label>
498 <div style="display: flex; gap: 10px;">
499 <input type="number" id="lockTokenId" placeholder="Token ID" min="0" style="flex: 1;">
500 <button onclick="lockTokenMetadata()">Lock</button>
501 </div>
502 </div>
503 <hr style="border: none; border-top: 1px solid var(--border); margin: 20px 0;">
504 <div class="form-group">
505 <label>Contract Metadata</label>
506 <p style="font-size: 0.8rem; color: var(--text-dim); margin-bottom: 10px;">
507 Permanently freeze the collection name, description, and image.
508 </p>
509 <button class="danger" onclick="lockContractMetadata()">🔒 Lock Collection Metadata</button>
510 </div>
511 </div>
512
513 <!-- Query Tools -->
514 <div class="card">
515 <h2><span class="icon">🔍</span> Query Tools</h2>
516 <div class="form-group">
517 <label>Check if Piece Exists</label>
518 <div style="display: flex; gap: 10px;">
519 <input type="text" id="checkPieceName" placeholder="cow" style="flex: 1;">
520 <button onclick="checkPieceExists()">Check</button>
521 </div>
522 </div>
523 <div class="form-group">
524 <label>Get Token Owner</label>
525 <div style="display: flex; gap: 10px;">
526 <input type="number" id="ownerTokenId" placeholder="Token ID" min="0" style="flex: 1;">
527 <button onclick="getTokenOwner()">Query</button>
528 </div>
529 </div>
530 <div id="queryResult" style="margin-top: 15px; padding: 10px; background: var(--surface2); border-radius: 6px; display: none;">
531 <label>Result</label>
532 <div class="value mono" id="queryResultValue">-</div>
533 </div>
534 </div>
535
536 <!-- Activity Log -->
537 <div class="card full-width">
538 <h2><span class="icon">📜</span> Activity Log</h2>
539 <div class="log-area" id="logArea">
540 <div class="log-entry info">
541 <span class="log-time">[--:--:--]</span>
542 Keeps Multitool initialized. Connect wallet to begin.
543 </div>
544 </div>
545 <button class="secondary" style="margin-top: 10px;" onclick="clearLog()">Clear Log</button>
546 </div>
547 </div>
548 </div>
549
550 <script>
551 // ========================================================================
552 // State
553 // ========================================================================
554 let state = {
555 network: 'ghostnet',
556 wallet: null,
557 tezos: null,
558 contract: null,
559 address: null,
560 connected: false,
561 entrypoints: [] // Available contract entrypoints
562 };
563
564 const NETWORKS = {
565 ghostnet: {
566 rpc: 'https://ghostnet.ecadinfra.com',
567 tzkt: 'https://api.ghostnet.tzkt.io',
568 explorer: 'https://ghostnet.tzkt.io',
569 name: 'Ghostnet'
570 },
571 mainnet: {
572 rpc: 'https://mainnet.ecadinfra.com',
573 tzkt: 'https://api.tzkt.io',
574 explorer: 'https://tzkt.io',
575 name: 'Mainnet'
576 }
577 };
578
579 // ========================================================================
580 // Logging
581 // ========================================================================
582 function log(message, type = 'info') {
583 const logArea = document.getElementById('logArea');
584 const time = new Date().toLocaleTimeString();
585 const entry = document.createElement('div');
586 entry.className = `log-entry ${type}`;
587 entry.innerHTML = `<span class="log-time">[${time}]</span> ${message}`;
588 logArea.insertBefore(entry, logArea.firstChild);
589 }
590
591 function clearLog() {
592 document.getElementById('logArea').innerHTML = '';
593 log('Log cleared', 'info');
594 }
595
596 // ========================================================================
597 // Network
598 // ========================================================================
599 function setNetwork(network) {
600 state.network = network;
601 document.querySelectorAll('.network-btn').forEach(btn => {
602 btn.classList.toggle('active', btn.dataset.network === network);
603 });
604 log(`Switched to ${NETWORKS[network].name}`, 'info');
605
606 // Reinitialize Tezos toolkit
607 if (window.taquito) {
608 state.tezos = new taquito.TezosToolkit(NETWORKS[network].rpc);
609 if (state.wallet) {
610 state.tezos.setWalletProvider(state.wallet);
611 }
612 }
613 }
614
615 // ========================================================================
616 // Wallet Connection
617 // ========================================================================
618 async function toggleWallet() {
619 if (state.connected) {
620 await disconnectWallet();
621 } else {
622 await connectWallet();
623 }
624 }
625
626 async function connectWallet() {
627 try {
628 log('Connecting wallet...', 'info');
629
630 const network = state.network === 'mainnet'
631 ? { type: 'mainnet' }
632 : { type: 'ghostnet' };
633
634 state.wallet = new beacon.DAppClient({
635 name: 'Keeps Multitool',
636 preferredNetwork: network.type
637 });
638
639 const permissions = await state.wallet.requestPermissions({ network });
640 state.address = permissions.address;
641 state.connected = true;
642
643 // Set up Taquito with wallet
644 state.tezos = new taquito.TezosToolkit(NETWORKS[state.network].rpc);
645 state.tezos.setWalletProvider(state.wallet);
646
647 updateWalletUI();
648 log(`Connected: ${state.address.slice(0, 8)}...${state.address.slice(-6)}`, 'success');
649
650 // Auto-fill owner address
651 document.getElementById('ownerAddress').placeholder = state.address;
652
653 } catch (err) {
654 log(`Connection failed: ${err.message}`, 'error');
655 }
656 }
657
658 async function disconnectWallet() {
659 try {
660 if (state.wallet) {
661 await state.wallet.clearActiveAccount();
662 }
663 state.connected = false;
664 state.address = null;
665 state.wallet = null;
666 updateWalletUI();
667 log('Wallet disconnected', 'info');
668 } catch (err) {
669 log(`Disconnect error: ${err.message}`, 'error');
670 }
671 }
672
673 function updateWalletUI() {
674 const walletInfo = document.getElementById('walletInfo');
675 const connectBtn = document.getElementById('connectBtn');
676
677 if (state.connected) {
678 walletInfo.innerHTML = `
679 <div class="wallet-address">${state.address.slice(0, 8)}...${state.address.slice(-6)}</div>
680 <div class="status-badge connected">
681 <span class="dot"></span>
682 <span>Connected</span>
683 </div>
684 `;
685 connectBtn.textContent = 'Disconnect';
686 connectBtn.className = 'secondary';
687 } else {
688 walletInfo.innerHTML = `
689 <div class="status-badge disconnected">
690 <span class="dot"></span>
691 <span>Not Connected</span>
692 </div>
693 `;
694 connectBtn.textContent = 'Connect Wallet';
695 connectBtn.className = '';
696 }
697 }
698
699 // ========================================================================
700 // Contract Interaction
701 // ========================================================================
702 async function loadContractInfo() {
703 const contractAddress = document.getElementById('contractAddress').value;
704 if (!contractAddress) {
705 log('Please enter a contract address', 'error');
706 return;
707 }
708
709 try {
710 log(`Loading contract ${contractAddress}...`, 'info');
711
712 const tzkt = NETWORKS[state.network].tzkt;
713
714 // Fetch entrypoints first
715 const entrypointsRes = await fetch(`${tzkt}/v1/contracts/${contractAddress}/entrypoints`);
716 if (entrypointsRes.ok) {
717 const entrypoints = await entrypointsRes.json();
718 state.entrypoints = entrypoints.map(e => e.name);
719 log(`Entrypoints: ${state.entrypoints.join(', ')}`, 'info');
720 updateEntrypointUI();
721 }
722
723 // Fetch storage
724 const storageRes = await fetch(`${tzkt}/v1/contracts/${contractAddress}/storage`);
725 if (!storageRes.ok) throw new Error('Contract not found');
726 const storage = await storageRes.json();
727
728 // Fetch balance
729 const balanceRes = await fetch(`${tzkt}/v1/contracts/${contractAddress}`);
730 const contractInfo = await balanceRes.json();
731
732 // Update UI
733 document.getElementById('nextTokenId').textContent = storage.next_token_id || '0';
734 document.getElementById('adminAddress').textContent = storage.administrator || '-';
735 document.getElementById('metadataLocked').textContent = storage.contract_metadata_locked ? 'Yes 🔒' : 'No';
736
737 // Format fee (stored in mutez) - may not exist in older contracts
738 const feeXTZ = (storage.keep_fee || 0) / 1_000_000;
739 document.getElementById('keepFee').textContent = storage.keep_fee !== undefined ? `${feeXTZ} XTZ` : 'N/A (v2.1 required)';
740 document.getElementById('currentFeeDisplay').textContent = storage.keep_fee !== undefined ? `${feeXTZ} XTZ` : 'N/A';
741
742 // Contract balance
743 const balanceXTZ = (contractInfo.balance || 0) / 1_000_000;
744 document.getElementById('contractBalance').textContent = `${balanceXTZ.toFixed(6)} XTZ`;
745
746 log(`Contract loaded: ${storage.next_token_id || 0} tokens`, 'success');
747
748 // Fetch tokens
749 await loadTokens(contractAddress);
750
751 } catch (err) {
752 log(`Failed to load contract: ${err.message}`, 'error');
753 }
754 }
755
756 function updateEntrypointUI() {
757 // Expected entrypoints for full functionality
758 const expectedEntrypoints = [
759 'keep', 'burn_keep', 'edit_metadata', 'lock_metadata',
760 'set_contract_metadata', 'lock_contract_metadata',
761 'set_keep_fee', 'withdraw_fees',
762 'transfer', 'balance_of', 'update_operators'
763 ];
764
765 const listEl = document.getElementById('entrypointsList');
766 listEl.style.display = 'flex';
767 listEl.innerHTML = expectedEntrypoints.map(ep => {
768 const available = state.entrypoints.includes(ep);
769 return `<span class="entrypoint-tag ${available ? 'available' : 'missing'}">${available ? '✓' : '✗'} ${ep}</span>`;
770 }).join('');
771
772 // Show warning if fee entrypoints missing
773 const hasFeeEntrypoints = state.entrypoints.includes('set_keep_fee');
774 if (!hasFeeEntrypoints) {
775 log('⚠️ Fee entrypoints not found - contract needs redeployment for v2.1 features', 'warning');
776 }
777 }
778
779 async function loadTokens(contractAddress) {
780 try {
781 const tzkt = NETWORKS[state.network].tzkt;
782 const res = await fetch(`${tzkt}/v1/tokens?contract=${contractAddress}&limit=50`);
783 const tokens = await res.json();
784
785 const tokenList = document.getElementById('tokenList');
786
787 if (tokens.length === 0) {
788 tokenList.innerHTML = '<div style="color: var(--text-dim); text-align: center; padding: 20px;">No tokens minted yet</div>';
789 return;
790 }
791
792 tokenList.innerHTML = tokens.map(t => `
793 <div class="token-item">
794 <div>
795 <div class="id">#${t.tokenId}</div>
796 <div class="name">${t.metadata?.name || 'Unnamed'}</div>
797 </div>
798 <div style="text-align: right;">
799 <div class="owner">${t.holders?.[0]?.address?.slice(0, 8) || 'Unknown'}...</div>
800 </div>
801 </div>
802 `).join('');
803
804 log(`Loaded ${tokens.length} tokens`, 'success');
805 } catch (err) {
806 log(`Failed to load tokens: ${err.message}`, 'error');
807 }
808 }
809
810 // ========================================================================
811 // Contract Operations
812 // ========================================================================
813 async function setKeepFee() {
814 if (!state.connected) {
815 log('Please connect wallet first', 'error');
816 return;
817 }
818
819 if (!state.entrypoints.includes('set_keep_fee')) {
820 log('⚠️ This contract does not have set_keep_fee entrypoint. Redeploy with v2.1+', 'error');
821 return;
822 }
823
824 const feeXTZ = parseFloat(document.getElementById('newFee').value);
825 if (isNaN(feeXTZ) || feeXTZ < 0) {
826 log('Invalid fee amount', 'error');
827 return;
828 }
829
830 try {
831 const contractAddress = document.getElementById('contractAddress').value;
832 const contract = await state.tezos.wallet.at(contractAddress);
833
834 log(`Setting keep fee to ${feeXTZ} XTZ...`, 'info');
835
836 const op = await contract.methods.set_keep_fee(feeXTZ * 1_000_000).send();
837 log(`Operation submitted: ${op.opHash}`, 'info');
838
839 await op.confirmation();
840 log(`Fee updated to ${feeXTZ} XTZ ✓`, 'success');
841
842 loadContractInfo();
843 } catch (err) {
844 log(`Failed to set fee: ${err.message}`, 'error');
845 }
846 }
847
848 async function withdrawFees() {
849 if (!state.connected) {
850 log('Please connect wallet first', 'error');
851 return;
852 }
853
854 if (!state.entrypoints.includes('withdraw_fees')) {
855 log('⚠️ This contract does not have withdraw_fees entrypoint. Redeploy with v2.1+', 'error');
856 return;
857 }
858
859 try {
860 const contractAddress = document.getElementById('contractAddress').value;
861 const contract = await state.tezos.wallet.at(contractAddress);
862
863 const destination = state.address; // Withdraw to connected wallet
864
865 log(`Withdrawing fees to ${destination.slice(0, 8)}...`, 'info');
866
867 const op = await contract.methods.withdraw_fees(destination).send();
868 log(`Operation submitted: ${op.opHash}`, 'info');
869
870 await op.confirmation();
871 log('Fees withdrawn ✓', 'success');
872
873 loadContractInfo();
874 } catch (err) {
875 log(`Withdraw failed: ${err.message}`, 'error');
876 }
877 }
878
879 async function mintToken() {
880 if (!state.connected) {
881 log('Please connect wallet first', 'error');
882 return;
883 }
884
885 const pieceName = document.getElementById('pieceName').value.trim();
886 if (!pieceName) {
887 log('Please enter a piece name', 'error');
888 return;
889 }
890
891 try {
892 const contractAddress = document.getElementById('contractAddress').value;
893 const contract = await state.tezos.wallet.at(contractAddress);
894
895 const owner = document.getElementById('ownerAddress').value || state.address;
896 const artifactUri = document.getElementById('artifactUri').value || `ipfs://placeholder`;
897 const description = document.getElementById('tokenDescription').value || `$${pieceName}`;
898
899 // Helper to convert string to bytes (hex)
900 const strToBytes = (str) => {
901 const encoder = new TextEncoder();
902 const bytes = encoder.encode(str);
903 return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
904 };
905
906 log(`Minting $${pieceName} to ${owner.slice(0, 8)}...`, 'info');
907
908 // Get current fee
909 const tzkt = NETWORKS[state.network].tzkt;
910 const storageRes = await fetch(`${tzkt}/v1/contracts/${contractAddress}/storage`);
911 const storage = await storageRes.json();
912 const fee = storage.keep_fee || 0;
913
914 const params = {
915 name: strToBytes(`$${pieceName}`),
916 description: strToBytes(description),
917 artifactUri: strToBytes(artifactUri),
918 displayUri: strToBytes(artifactUri),
919 thumbnailUri: strToBytes(''),
920 decimals: strToBytes('0'),
921 symbol: strToBytes('KEEP'),
922 isBooleanAmount: strToBytes('true'),
923 shouldPreferSymbol: strToBytes('false'),
924 formats: strToBytes('[]'),
925 tags: strToBytes(`["$${pieceName}","KidLisp"]`),
926 attributes: strToBytes('[]'),
927 creators: strToBytes(`["${owner}"]`),
928 rights: strToBytes(''),
929 content_type: strToBytes('text/plain'),
930 content_hash: strToBytes(pieceName),
931 metadata_uri: strToBytes(''),
932 owner: owner
933 };
934
935 const op = await contract.methods.keep(
936 params.artifactUri,
937 params.attributes,
938 params.content_hash,
939 params.content_type,
940 params.creators,
941 params.decimals,
942 params.description,
943 params.displayUri,
944 params.formats,
945 params.isBooleanAmount,
946 params.metadata_uri,
947 params.name,
948 params.owner,
949 params.rights,
950 params.shouldPreferSymbol,
951 params.symbol,
952 params.tags,
953 params.thumbnailUri
954 ).send({ amount: fee, mutez: true });
955
956 log(`Operation submitted: ${op.opHash}`, 'info');
957 await op.confirmation();
958 log(`Token minted ✓`, 'success');
959
960 loadContractInfo();
961 } catch (err) {
962 log(`Mint failed: ${err.message}`, 'error');
963 console.error(err);
964 }
965 }
966
967 async function burnToken() {
968 if (!state.connected) {
969 log('Please connect wallet first', 'error');
970 return;
971 }
972
973 const tokenId = parseInt(document.getElementById('burnTokenId').value);
974 if (isNaN(tokenId) || tokenId < 0) {
975 log('Invalid token ID', 'error');
976 return;
977 }
978
979 if (!confirm(`Are you sure you want to burn token #${tokenId}? This cannot be undone.`)) {
980 return;
981 }
982
983 try {
984 const contractAddress = document.getElementById('contractAddress').value;
985 const contract = await state.tezos.wallet.at(contractAddress);
986
987 log(`Burning token #${tokenId}...`, 'info');
988
989 const op = await contract.methods.burn_keep(tokenId).send();
990 log(`Operation submitted: ${op.opHash}`, 'info');
991
992 await op.confirmation();
993 log(`Token #${tokenId} burned ✓`, 'success');
994
995 loadContractInfo();
996 } catch (err) {
997 log(`Burn failed: ${err.message}`, 'error');
998 }
999 }
1000
1001 async function lockTokenMetadata() {
1002 if (!state.connected) {
1003 log('Please connect wallet first', 'error');
1004 return;
1005 }
1006
1007 const tokenId = parseInt(document.getElementById('lockTokenId').value);
1008 if (isNaN(tokenId) || tokenId < 0) {
1009 log('Invalid token ID', 'error');
1010 return;
1011 }
1012
1013 if (!confirm(`Lock metadata for token #${tokenId}? This is permanent!`)) {
1014 return;
1015 }
1016
1017 try {
1018 const contractAddress = document.getElementById('contractAddress').value;
1019 const contract = await state.tezos.wallet.at(contractAddress);
1020
1021 log(`Locking token #${tokenId} metadata...`, 'info');
1022
1023 const op = await contract.methods.lock_metadata(tokenId).send();
1024 log(`Operation submitted: ${op.opHash}`, 'info');
1025
1026 await op.confirmation();
1027 log(`Token #${tokenId} metadata locked ✓`, 'success');
1028 } catch (err) {
1029 log(`Lock failed: ${err.message}`, 'error');
1030 }
1031 }
1032
1033 async function lockContractMetadata() {
1034 if (!state.connected) {
1035 log('Please connect wallet first', 'error');
1036 return;
1037 }
1038
1039 if (!confirm('Lock collection metadata permanently? This cannot be undone!')) {
1040 return;
1041 }
1042
1043 try {
1044 const contractAddress = document.getElementById('contractAddress').value;
1045 const contract = await state.tezos.wallet.at(contractAddress);
1046
1047 log('Locking contract metadata...', 'info');
1048
1049 const op = await contract.methods.lock_contract_metadata().send();
1050 log(`Operation submitted: ${op.opHash}`, 'info');
1051
1052 await op.confirmation();
1053 log('Contract metadata locked ✓', 'success');
1054
1055 loadContractInfo();
1056 } catch (err) {
1057 log(`Lock failed: ${err.message}`, 'error');
1058 }
1059 }
1060
1061 async function editMetadata() {
1062 if (!state.connected) {
1063 log('Please connect wallet first', 'error');
1064 return;
1065 }
1066
1067 const tokenId = parseInt(document.getElementById('editTokenId').value);
1068 const newName = document.getElementById('editName').value.trim();
1069
1070 if (isNaN(tokenId) || tokenId < 0) {
1071 log('Invalid token ID', 'error');
1072 return;
1073 }
1074
1075 log('Edit metadata not fully implemented yet - use CLI', 'info');
1076 }
1077
1078 // ========================================================================
1079 // Query Tools
1080 // ========================================================================
1081 async function checkPieceExists() {
1082 const pieceName = document.getElementById('checkPieceName').value.trim();
1083 if (!pieceName) {
1084 log('Enter a piece name', 'error');
1085 return;
1086 }
1087
1088 try {
1089 const contractAddress = document.getElementById('contractAddress').value;
1090 const tzkt = NETWORKS[state.network].tzkt;
1091
1092 log(`Checking if "$${pieceName}" exists...`, 'info');
1093
1094 // Query content_hashes big_map
1095 const res = await fetch(`${tzkt}/v1/contracts/${contractAddress}/bigmaps/content_hashes/keys`);
1096 const keys = await res.json();
1097
1098 // Convert piece name to hex for comparison
1099 const encoder = new TextEncoder();
1100 const bytes = encoder.encode(pieceName);
1101 const hex = Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
1102
1103 const found = keys.find(k => k.key === hex || k.key === pieceName);
1104
1105 const resultDiv = document.getElementById('queryResult');
1106 const resultValue = document.getElementById('queryResultValue');
1107 resultDiv.style.display = 'block';
1108
1109 if (found) {
1110 resultValue.innerHTML = `<span style="color: var(--warning);">✓ Exists as token #${found.value}</span>`;
1111 log(`"$${pieceName}" exists as token #${found.value}`, 'info');
1112 } else {
1113 resultValue.innerHTML = `<span style="color: var(--success);">✗ Available for minting</span>`;
1114 log(`"$${pieceName}" is available`, 'success');
1115 }
1116 } catch (err) {
1117 log(`Query failed: ${err.message}`, 'error');
1118 }
1119 }
1120
1121 async function getTokenOwner() {
1122 const tokenId = parseInt(document.getElementById('ownerTokenId').value);
1123 if (isNaN(tokenId) || tokenId < 0) {
1124 log('Invalid token ID', 'error');
1125 return;
1126 }
1127
1128 try {
1129 const contractAddress = document.getElementById('contractAddress').value;
1130 const tzkt = NETWORKS[state.network].tzkt;
1131
1132 log(`Querying owner of token #${tokenId}...`, 'info');
1133
1134 const res = await fetch(`${tzkt}/v1/tokens?contract=${contractAddress}&tokenId=${tokenId}`);
1135 const tokens = await res.json();
1136
1137 const resultDiv = document.getElementById('queryResult');
1138 const resultValue = document.getElementById('queryResultValue');
1139 resultDiv.style.display = 'block';
1140
1141 if (tokens.length > 0 && tokens[0].holders?.length > 0) {
1142 const owner = tokens[0].holders[0].address;
1143 resultValue.innerHTML = `<a href="${NETWORKS[state.network].explorer}/${owner}" target="_blank" style="color: var(--accent);">${owner}</a>`;
1144 log(`Token #${tokenId} owned by ${owner.slice(0, 12)}...`, 'success');
1145 } else {
1146 resultValue.innerHTML = '<span style="color: var(--error);">Token not found or no owner</span>';
1147 log(`Token #${tokenId} not found`, 'error');
1148 }
1149 } catch (err) {
1150 log(`Query failed: ${err.message}`, 'error');
1151 }
1152 }
1153
1154 // ========================================================================
1155 // UI Helpers
1156 // ========================================================================
1157 function showTab(tabName, btn) {
1158 document.querySelectorAll('.tab-content').forEach(t => t.classList.remove('active'));
1159 document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
1160 document.getElementById(`${tabName}-tab`).classList.add('active');
1161 btn.classList.add('active');
1162 }
1163
1164 // ========================================================================
1165 // Initialize
1166 // ========================================================================
1167 window.addEventListener('load', () => {
1168 // Initialize Tezos toolkit (read-only until wallet connects)
1169 if (window.taquito) {
1170 state.tezos = new taquito.TezosToolkit(NETWORKS[state.network].rpc);
1171 }
1172 log('Keeps Multitool ready', 'success');
1173 });
1174 </script>
1175</body>
1176</html>