Monorepo for Aesthetic.Computer aesthetic.computer
at main 1176 lines 39 kB view raw
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 &quot;blue&quot;)&#10;(ink &quot;yellow&quot;)..."></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>