Files
client/www/tools.html
T

1478 lines
56 KiB
HTML

<!DOCTYPE html>
<?xml version="1.0" encoding="UTF-8"?>
<html lang="en" dir="ltr">
<head>
<meta charset="utf-8" />
<title>TOOLS</title>
<link rel="stylesheet" href="./css/client.css" />
<!-- Initialize theme BEFORE any components load -->
<script>
(function () {
const savedTheme = localStorage.getItem('theme');
if (savedTheme === 'dark') {
document.documentElement.classList.add('dark-mode');
if (document.body) {
document.body.classList.add('dark-mode');
}
}
})();
</script>
<link rel="shortcut icon" type="image/x-icon" href="./favicon/favicon-dots2.ico" />
<!-- SVG.js library (required by HamburgerMorphing) -->
<script src="./js/vendor/svg.min.js"></script>
<style media="screen">
#divBody {
display: flex;
flex-direction: row;
flex-wrap: wrap;
justify-content: center;
padding: 20px;
gap: 20px;
}
.divTool {
border: var(--header-border);
border-radius: 20px;
margin: 10px;
padding: 20px;
width: 30%;
min-width: 27em;
background: var(--secondary-color);
}
.divButton {
display: flex;
flex-direction: row;
align-items: center;
border: 2px solid var(--primary-color);
justify-content: center;
margin-top: 2em;
height: 4em;
border-radius: 10px;
background: transparent;
color: var(--primary-color);
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s, color 0.2s;
}
.divSideTitle {
font-weight: bold;
text-align: center;
color: var(--primary-color);
margin-bottom: 10px;
font-size: 1.1em;
}
.divButton:hover {
background-color: var(--accent-color);
border-color: var(--accent-color);
color: var(--secondary-color);
}
.divButton:active {
background-color: var(--muted-color);
border-color: var(--muted-color);
color: var(--primary-color);
}
.inDiv {
background-color: var(--secondary-color);
font-size: 55%;
border: 1px solid var(--primary-color);
min-height: 2em;
padding: 0.5em;
margin-bottom: 5px;
color: var(--text-color);
}
.inDivTall {
background-color: var(--secondary-color);
font-size: 55%;
border: 1px solid var(--primary-color);
min-height: 4em;
padding: 0.5em;
margin-bottom: 5px;
word-wrap: break-word;
white-space: pre-wrap;
overflow-y: auto;
max-height: 6em;
color: var(--text-color);
}
.outDiv {
background-color: var(--muted-color);
font-size: 55%;
border: 1px solid var(--primary-color);
min-height: 2em;
padding: 0.5em;
margin-bottom: 5px;
overflow: auto;
color: var(--primary-color);
cursor: pointer;
}
.outDivTall {
background-color: var(--muted-color);
font-size: 55%;
border: 1px solid var(--primary-color);
min-height: 8em;
padding: 0.5em;
margin-bottom: 5px;
overflow: auto;
color: var(--primary-color);
cursor: pointer;
}
.clsClipboard:hover {
opacity: 0.8;
background-color: var(--accent-color);
color: var(--secondary-color);
}
.clsClipboard:active {
background-color: var(--muted-color);
color: var(--primary-color);
}
.toolLabel {
font-size: 0.9em;
color: var(--text-color);
margin-top: 8px;
margin-bottom: 2px;
}
.divButtonRow {
display: flex;
gap: 10px;
margin-top: 1em;
}
.divButtonHalf {
flex: 1;
margin-top: 0;
}
.mnemonicJsonBox {
min-height: 2em;
max-height: 2em;
white-space: nowrap;
overflow-y: auto;
}
.mnemonicJsonBox.expanded {
min-height: 8em;
max-height: 12em;
white-space: pre-wrap;
}
#selNip19EncodeType {
width: 100%;
display: block;
}
</style>
</head>
<body>
<!-- ================================================================
HAMBURGER BUTTON (Fixed, separate from header)
================================================================ -->
<div id="divSvgHam" class="divHeaderButtons">
<!-- HamburgerMorphing will be injected here -->
</div>
<!-- ================================================================
HEADER
================================================================ -->
<div id="divHeader">
<div id="divHeaderFlexLeft">
<!-- Hamburger is now separate fixed element -->
</div>
<div id="divHeaderFlexCenter">
<div class="divHeaderText">TOOLS</div>
</div>
<div id="divHeaderFlexRight">
<!-- No button in header right - logout is in sidenav footer -->
</div>
</div>
<!-- ================================================================
BODY - TOOLS
================================================================ -->
<div id="divBody">
<!-- KEY GENERATION -->
<div class="divTool">
<div class="divSideTitle">GENERATE NEW IDENTITY</div>
<div class="toolLabel">Secret Key (hex):</div>
<div id="inpSK" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">Secret Key (nsec):</div>
<div id="inpSKBech32" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">Public Key (hex):</div>
<div id="inpPK" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">Public Key (npub):</div>
<div id="inpPKBech32" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">FIPS IPv6:</div>
<div id="inpPKFipsIpv6" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">JSON:</div>
<div id="inpAllKeys" class="outDivTall clsClipboard">&nbsp;</div>
<div id="btnConnectPool" class="divButton btn">Generate</div>
</div>
<!-- NSEC TO NPUB -->
<div class="divTool">
<div class="divSideTitle">CONVERT NSEC TO NPUB</div>
<div class="toolLabel">nsec/hex:</div>
<div id="inpNsecToNpub" class="inDiv" contenteditable="true"></div>
<div class="toolLabel" id="labelNsecOut">nsec:</div>
<div id="outNsecOther" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">npub (encoded):</div>
<div id="outNpubEncoded" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">npub (hex):</div>
<div id="outNpubHex" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">JSON:</div>
<div id="outNsecToNpubJson" class="outDivTall clsClipboard">&nbsp;</div>
</div>
<!-- CREATE EVENT -->
<div class="divTool">
<div class="divSideTitle">CREATE EVENT</div>
<div class="toolLabel">Secret Key:</div>
<div id="inpEventSK" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Kind:</div>
<div id="inpKind" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Tags:</div>
<div id="inpTags" class="inDiv" contenteditable="true">[]</div>
<div class="toolLabel">Content:</div>
<div id="inpContent" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Event:</div>
<div id="taEvent" class="outDiv clsClipboard" contenteditable="false">&nbsp;</div>
<div id="btnCreateEvent" class="divButton btn">Create</div>
</div>
<!-- NPUB TO HEX -->
<div class="divTool">
<div class="divSideTitle">CONVERT NPUB TO HEX</div>
<div class="toolLabel">npub:</div>
<div id="inpNpub" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">hex:</div>
<div id="outHex1" class="outDiv clsClipboard">&nbsp;</div>
</div>
<!-- NPUB TO FIPS IPV6 -->
<div class="divTool">
<div class="divSideTitle">CONVERT NPUB TO FIPS IPV6</div>
<div class="toolLabel">npub:</div>
<div id="inpNpubFipsIpv6" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">fips ipv6:</div>
<div id="outNpubFipsIpv6" class="outDiv clsClipboard">&nbsp;</div>
</div>
<!-- NSEC TO HEX -->
<div class="divTool">
<div class="divSideTitle">CONVERT NSEC TO HEX</div>
<div class="toolLabel">nsec:</div>
<div id="inpNsec" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">hex:</div>
<div id="outHex2" class="outDiv clsClipboard">&nbsp;</div>
</div>
<!-- HEX TO NPUB -->
<div class="divTool">
<div class="divSideTitle">CONVERT HEX TO NPUB</div>
<div class="toolLabel">hex:</div>
<div id="inpHex" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">npub:</div>
<div id="outNpub" class="outDiv clsClipboard">&nbsp;</div>
</div>
<!-- HEX TO NSEC -->
<div class="divTool">
<div class="divSideTitle">CONVERT HEX TO NSEC</div>
<div class="toolLabel">hex:</div>
<div id="inpHex2" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">nsec:</div>
<div id="outNsec" class="outDiv clsClipboard">&nbsp;</div>
</div>
<!-- ENCODE NIP-19 -->
<div class="divTool">
<div class="divSideTitle">ENCODE NIP-19</div>
<div class="toolLabel">Type:</div>
<select id="selNip19EncodeType" class="inDiv">
<option value="npub">npub</option>
<option value="nsec">nsec</option>
<option value="note">note</option>
<option value="nprofile">nprofile</option>
<option value="nevent">nevent</option>
<option value="naddr">naddr</option>
<option value="nrelay">nrelay</option>
<option value="other">other</option>
</select>
<div id="divNip19EncodeTypeCustomWrap" class="clsHidden">
<div class="toolLabel">Custom Type (used when Type is "other"):</div>
<div id="inpNip19EncodeTypeCustom" class="inDiv" contenteditable="true"></div>
</div>
<div class="toolLabel">Special Value (hex or identifier depending on type):</div>
<div id="inpNip19EncodeSpecial" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Relays (optional, comma/newline separated):</div>
<div id="inpNip19EncodeRelays" class="inDivTall" contenteditable="true"></div>
<div class="toolLabel">Author Pubkey Hex (optional, for nevent/naddr):</div>
<div id="inpNip19EncodeAuthor" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Kind (optional for nevent, required for naddr):</div>
<div id="inpNip19EncodeKind" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Encoded:</div>
<div id="outNip19Encoded" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">JSON:</div>
<div id="outNip19EncodeJson" class="outDivTall clsClipboard">&nbsp;</div>
<div id="btnEncodeNip19" class="divButton btn">Encode</div>
</div>
<!-- DECODE NIP-19 -->
<div class="divTool">
<div class="divSideTitle">DECODE NIP-19</div>
<div class="toolLabel">Bech32 Input (npub, nsec, note, nprofile, nevent, naddr, nrelay):</div>
<div id="inpNip19DecodeValue" class="inDivTall" contenteditable="true"></div>
<div class="toolLabel">Type:</div>
<div id="outNip19DecodeType" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">Special Value:</div>
<div id="outNip19DecodeSpecial" class="outDiv clsClipboard">&nbsp;</div>
<div class="toolLabel">JSON:</div>
<div id="outNip19DecodeJson" class="outDivTall clsClipboard">&nbsp;</div>
<div id="btnDecodeNip19" class="divButton btn">Decode</div>
</div>
<!-- LINUX TIME -->
<div class="divTool">
<div class="divSideTitle">LINUX EPOCH TIME</div>
<div class="toolLabel">Seconds since 1970:</div>
<div id="outLinuxTime" class="outDiv clsClipboard">&nbsp;</div>
</div>
<!-- GENERATE MNEMONIC PHRASE -->
<div class="divTool">
<div class="divSideTitle">GENERATE MNEMONIC PHRASE</div>
<div class="toolLabel">Phrase:</div>
<div id="outMnemonicPhrase" class="outDiv clsClipboard">&nbsp;</div>
<div id="btnGenMnemonic" class="divButton btn">Generate</div>
</div>
<!-- DECODE MNEMONIC PHRASE -->
<div class="divTool">
<div class="divSideTitle">DECODE MNEMONIC PHRASE</div>
<div class="toolLabel">Phrase:</div>
<div id="inMnemonicPhrase" class="inDiv" contenteditable="true"></div>
<div id="lblMnemonicJson0" class="toolLabel">Index 00 JSON:</div>
<div id="outMnemonicJson0" class="outDivTall mnemonicJsonBox clsClipboard">&nbsp;</div>
<div id="lblMnemonicJson1" class="toolLabel">Index 01 JSON:</div>
<div id="outMnemonicJson1" class="outDivTall mnemonicJsonBox clsClipboard">&nbsp;</div>
<div id="lblMnemonicJson2" class="toolLabel">Index 02 JSON:</div>
<div id="outMnemonicJson2" class="outDivTall mnemonicJsonBox clsClipboard">&nbsp;</div>
<div id="lblMnemonicJson3" class="toolLabel">Index 03 JSON:</div>
<div id="outMnemonicJson3" class="outDivTall mnemonicJsonBox clsClipboard">&nbsp;</div>
<div id="lblMnemonicJson4" class="toolLabel">Index 04 JSON:</div>
<div id="outMnemonicJson4" class="outDivTall mnemonicJsonBox clsClipboard">&nbsp;</div>
<div class="divButtonRow">
<div id="btnMnemonicPrev" class="divButton divButtonHalf btn">Previous</div>
<div id="btnMnemonicNext" class="divButton divButtonHalf btn">Next</div>
</div>
</div>
<!-- ENCODE NIP-04 -->
<div class="divTool">
<div class="divSideTitle">ENCODE NIP-04</div>
<div class="toolLabel">nsec/hex:</div>
<div id="inpNip04EncPrivkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">npub/hex:</div>
<div id="inpNip04EncPubkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Plain Text:</div>
<div id="inpNip04EncPlaintext" class="inDivTall" contenteditable="true"></div>
<div class="toolLabel">Encrypted Message:</div>
<div id="outNip04Encrypted" class="outDivTall clsClipboard">&nbsp;</div>
<div id="btnEncodeNip04" class="divButton btn">Encrypt</div>
</div>
<!-- DECODE NIP-04 -->
<div class="divTool">
<div class="divSideTitle">DECODE NIP-04</div>
<div class="toolLabel">nsec/hex:</div>
<div id="inpNip04DecPrivkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">npub/hex:</div>
<div id="inpNip04DecPubkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Encrypted Message:</div>
<div id="inpNip04Encrypted" class="inDivTall" contenteditable="true"></div>
<div class="toolLabel">Plain Text:</div>
<div id="outNip04Decrypted" class="outDivTall clsClipboard">&nbsp;</div>
<div id="btnDecodeNip04" class="divButton btn">Decrypt</div>
</div>
<!-- ENCODE NIP-44 -->
<div class="divTool">
<div class="divSideTitle">ENCODE NIP-44</div>
<div class="toolLabel">nsec/hex:</div>
<div id="inpNip44EncPrivkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">npub/hex:</div>
<div id="inpNip44EncPubkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Plain Text:</div>
<div id="inpNip44EncPlaintext" class="inDivTall" contenteditable="true"></div>
<div class="toolLabel">Encrypted Message:</div>
<div id="outNip44Encrypted" class="outDivTall clsClipboard">&nbsp;</div>
<div id="btnEncodeNip44" class="divButton btn">Encrypt</div>
</div>
<!-- DECODE NIP-44 -->
<div class="divTool">
<div class="divSideTitle">DECODE NIP-44</div>
<div class="toolLabel">nsec/hex:</div>
<div id="inpNip44DecPrivkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">npub/hex:</div>
<div id="inpNip44DecPubkey" class="inDiv" contenteditable="true"></div>
<div class="toolLabel">Encrypted Message:</div>
<div id="inpNip44Encrypted" class="inDivTall" contenteditable="true"></div>
<div class="toolLabel">Plain Text:</div>
<div id="outNip44Decrypted" class="outDivTall clsClipboard">&nbsp;</div>
<div id="btnDecodeNip44" class="divButton btn">Decrypt</div>
</div>
</div>
<!-- ================================================================
FOOTER
================================================================ -->
<div id="divFooter">
<div id="divFooterLeft" class="divFooterBox"></div>
<div id="divFooterCenter" class="divFooterBox"></div>
<div id="divFooterRight" class="divFooterBox"></div>
<div id="divFooterBalance" class="divFooterBox">0 sats</div>
</div>
<!-- ================================================================
SIDENAV
================================================================ -->
<div id="divSideNav">
<div id="divSideNavHeader">
<!-- No close button - use main hamburger to close -->
</div>
<div id="divSideNavBody">
<div id="divFiles"></div>
</div>
<div id="divAiSection" class="sidenavSection">
<div id="divAiSectionTitle" class="sidenavSectionTitle">AI</div>
<div id="divAiList" class="sidenavSectionList">
<div id="divAiProvidersList">No saved providers yet.</div>
</div>
</div>
<div id="divRelaySection">
<div id="divRelaySectionTitle">
リレー
</div>
<div id="divRelayList">
Loading relays...
</div>
</div>
<div id="divBlossomSection">
<div id="divBlossomSectionTitle">ブロッサム</div>
<div id="divBlossomList">Loading blossom servers...</div>
</div>
<div id="divVersionBar">
<span id="versionDisplay">v0.0.1</span>
<div id="divVersionBarButtons">
<button id="themeToggleButton" title="Toggle Dark/Light Mode">
<div id="themeToggleHamburgerContainer"></div>
</button>
<button id="logoutButton" title="Logout">
<div id="logoutHamburgerContainer"></div>
</button>
</div>
</div>
</div>
<!-- ================================================================
REQUIRED SCRIPTS
================================================================ -->
<script src="./nostr.bundle.js"></script>
<script src="/nostr-login-lite/nostr-lite.js"></script>
<script type="module">
/* ================================================================
IMPORTS
================================================================ */
import { initNDKPage, getPubkey, injectHeaderAvatar, injectHeaderLoginButton, subscribe, publishEvent, disconnect, getVersion, updateVersionDisplay } from './js/init-ndk.mjs';
import { HamburgerMorphing } from "./hamburger_morphing/hamburger.mjs";
import { initFooterRelayStatus, updateFooterRelayStatus, initSidenavRelaySection, updateSidenavRelaySection, setRelayActivityState } from './js/relay-ui.mjs';
import { initBlossomSection, updateBlossomSection } from './js/blossom-ui.mjs';
import { initAiSectionWithLocalConfig } from './js/ai-ui.mjs';
const versionInfo = await getVersion();
const VERSION = versionInfo.VERSION;
console.log(`[tools.html ${VERSION}] Loading...`);
/* ================================================================
GLOBAL VARIABLES
================================================================ */
let updateIntervalId = null;
let currentPubkey = null;
let NostrTools = null;
// Hamburger menu
let hamburgerInstance = null;
let isNavOpen = false;
// Version bar buttons
let logoutHamburger = null;
let themeToggleHamburger = null;
let isDarkMode = false;
// Mnemonic decode pagination
const MNEMONIC_PAGE_SIZE = 5;
let mnemonicPageIndex = 0;
/* ================================================================
DOM VARIABLES
================================================================ */
const divBody = document.getElementById("divBody");
const divSideNav = document.getElementById("divSideNav");
const divSideNavBody = document.getElementById("divSideNavBody");
const divFooterCenter = document.getElementById("divFooterCenter");
const divFooterRight = document.getElementById("divFooterRight");
// Tool elements
const btnConnectPool = document.getElementById(`btnConnectPool`);
const btnCreateEvent = document.getElementById(`btnCreateEvent`);
const inpNpub = document.getElementById(`inpNpub`);
const inpNpubFipsIpv6 = document.getElementById(`inpNpubFipsIpv6`);
const inpNsec = document.getElementById(`inpNsec`);
const inpHex = document.getElementById(`inpHex`);
const inpHex2 = document.getElementById(`inpHex2`);
const outLinuxTime = document.getElementById(`outLinuxTime`);
const outMnemonicPhrase = document.getElementById(`outMnemonicPhrase`);
const inMnemonicPhrase = document.getElementById(`inMnemonicPhrase`);
/* ================================================================
HELPER: Wait for NostrTools
================================================================ */
async function waitForNostrTools(timeout = 5000) {
const startTime = Date.now();
while (!window.NostrTools && !window.NOSTR_LOGIN_LITE?.NostrTools) {
if (Date.now() - startTime > timeout) {
console.error('Available window properties:', Object.keys(window).filter(k => k.toLowerCase().includes('nostr')));
throw new Error('NostrTools failed to load within timeout');
}
await new Promise(resolve => setTimeout(resolve, 50));
}
return window.NostrTools || window.NOSTR_LOGIN_LITE?.NostrTools;
}
/* ================================================================
HAMBURGER MENU
================================================================ */
function initHamburgerMenu() {
hamburgerInstance = new HamburgerMorphing('#divSvgHam', {
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
hamburgerInstance.animateTo('burger');
}
/* ================================================================
SIDENAV FUNCTIONS
================================================================ */
function openNav() {
divSideNav.style.zIndex = 3;
divSideNav.style.width = "clamp(400px, 50vw, 600px)";
isNavOpen = true;
if (hamburgerInstance) {
hamburgerInstance.animateTo('arrow_left');
}
divSideNavBody.innerHTML = "Settings and options coming soon...";
// Initialize version bar buttons when sidenav opens (lazy load)
if (!logoutHamburger) {
logoutHamburger = new HamburgerMorphing('#logoutHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
logoutHamburger.animateTo('x');
}
if (!themeToggleHamburger) {
themeToggleHamburger = new HamburgerMorphing('#themeToggleHamburgerContainer', {
size: 24,
foreground: 'var(--primary-color)',
background: 'var(--secondary-color)',
hover: 'var(--accent-color)'
});
const savedTheme = localStorage.getItem('theme');
isDarkMode = savedTheme === 'dark' || document.body.classList.contains('dark-mode');
const initialShape = isDarkMode ? 'moon' : 'circle';
themeToggleHamburger.animateTo(initialShape);
}
}
function closeNav() {
divSideNav.style.width = "0vw";
divSideNav.style.zIndex = -1;
isNavOpen = false;
if (hamburgerInstance) {
hamburgerInstance.animateTo('burger');
}
}
function toggleNav() {
if (isNavOpen) {
closeNav();
} else {
openNav();
}
}
/* ================================================================
UPDATE FOOTER
================================================================ */
const UpdateFooter = async () => {
try {
await updateFooterRelayStatus();
await updateSidenavRelaySection();
await updateBlossomSection();
// Update Linux time
if (outLinuxTime) {
outLinuxTime.innerText = Math.floor(Date.now() / 1000);
}
divFooterCenter.innerHTML = '';
divFooterRight.innerHTML = '';
} catch (error) {
console.error('[tools.html] Error updating footer:', error);
}
};
/* ================================================================
LOGOUT
================================================================ */
const Logout = async () => {
console.log("[tools.html] Starting logout process...");
if (updateIntervalId) {
clearInterval(updateIntervalId);
updateIntervalId = null;
}
disconnect();
if (window.NOSTR_LOGIN_LITE && window.NOSTR_LOGIN_LITE.logout) {
await window.NOSTR_LOGIN_LITE.logout();
}
localStorage.clear();
sessionStorage.clear();
if (window.indexedDB) {
const databases = await window.indexedDB.databases();
for (const db of databases) {
if (db.name) {
window.indexedDB.deleteDatabase(db.name);
}
}
}
console.log("[tools.html] Logged out, reloading page");
location.reload(true);
};
/* ================================================================
TOOL FUNCTIONS
================================================================ */
// Helper function to convert hex string to Uint8Array
function hexToUint8Array(hex) {
const bytes = new Uint8Array(hex.length / 2);
for (let i = 0; i < hex.length; i += 2) {
bytes[i / 2] = parseInt(hex.substr(i, 2), 16);
}
return bytes;
}
// Helper function to convert Uint8Array to hex string
function uint8ArrayToHex(bytes) {
return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
}
// Helper function to normalize key input (accepts both hex and bech32)
const normalizeKey = (keyInput, isPrivate = false) => {
const trimmed = keyInput.trim();
if (!trimmed) return null;
try {
if (isPrivate) {
if (trimmed.startsWith('nsec')) {
return NostrTools.nip19.decode(trimmed).data;
} else {
return hexToUint8Array(trimmed);
}
} else {
if (trimmed.startsWith('npub')) {
return NostrTools.nip19.decode(trimmed).data;
} else {
return trimmed;
}
}
} catch (error) {
throw new Error(`Invalid ${isPrivate ? 'private' : 'public'} key format: ${error.message}`);
}
};
// Helper function to produce NIP-06 style derivation path string
const getDerivationPath = (accountIndex = 0) => `m/44'/1237'/${accountIndex}'/0/0`;
const pubkeyHexToFipsIpv6 = async (pubkeyHex) => {
const pubkeyBytes = hexToUint8Array(pubkeyHex);
const hashBuffer = await crypto.subtle.digest('SHA-256', pubkeyBytes);
const hash = new Uint8Array(hashBuffer);
const nodeAddr = hash.slice(0, 16);
nodeAddr[0] = 0xfd;
const ipv6Groups = [];
for (let i = 0; i < 16; i += 2) {
const group = ((nodeAddr[i] << 8) | nodeAddr[i + 1]).toString(16);
ipv6Groups.push(group);
}
return ipv6Groups.join(':');
};
const GenerateKeys = async () => {
let sk = NostrTools.generateSecretKey();
let pk = NostrTools.getPublicKey(sk);
let nsec = NostrTools.nip19.nsecEncode(sk);
let npub = NostrTools.nip19.npubEncode(pk);
let skHex = Array.from(sk).map(b => b.toString(16).padStart(2, '0')).join('');
let fipsIpv6 = await pubkeyHexToFipsIpv6(pk);
document.getElementById(`inpSK`).innerText = skHex;
document.getElementById(`inpPK`).innerText = pk;
document.getElementById(`inpSKBech32`).innerText = nsec;
document.getElementById(`inpPKBech32`).innerText = npub;
document.getElementById(`inpPKFipsIpv6`).innerText = fipsIpv6;
let nAll = {}
nAll.nsec = nsec
nAll.npub = npub
nAll.npubHex = pk
nAll.nsecHex = skHex
nAll.fipsIpv6 = fipsIpv6
nAll.strDerivationPath = getDerivationPath(0)
document.getElementById(`inpAllKeys`).innerText = JSON.stringify(nAll, "", 2);
};
const CreateEvent = async () => {
console.log("Create Event");
let arrInp = JSON.parse(document.getElementById(`inpTags`).innerText || '[]');
let Temp = await objCreateEvent(
document.getElementById(`inpEventSK`).innerText,
document.getElementById(`inpKind`).innerText,
document.getElementById(`inpContent`).innerText,
arrInp
);
document.getElementById(`taEvent`).innerText = JSON.stringify(Temp, undefined, 4);
};
const objCreateEvent = async (PrivateKey, Kind, Content, arrTags = []) => {
let privateKeyBytes;
if (typeof PrivateKey === 'string') {
if (PrivateKey.startsWith('nsec')) {
privateKeyBytes = NostrTools.nip19.decode(PrivateKey).data;
} else {
privateKeyBytes = hexToUint8Array(PrivateKey);
}
} else {
privateKeyBytes = PrivateKey;
}
let event = {
kind: Number(Kind),
created_at: Math.floor(Date.now() / 1000),
tags: arrTags,
content: Content,
pubkey: NostrTools.getPublicKey(privateKeyBytes),
};
event.id = NostrTools.getEventHash(event);
event.sig = NostrTools.finalizeEvent(event, privateKeyBytes).sig;
return event;
};
const NpubToHex = async () => {
try {
let Hex = NostrTools.nip19.decode(document.getElementById(`inpNpub`).innerText).data;
document.getElementById(`outHex1`).innerText = Hex;
} catch (error) {
document.getElementById(`outHex1`).innerText = 'Error: Invalid npub';
}
};
const NpubToFipsIpv6 = async () => {
try {
const input = document.getElementById(`inpNpubFipsIpv6`).innerText.trim();
if (!input) {
document.getElementById(`outNpubFipsIpv6`).innerText = '';
return;
}
const decoded = NostrTools.nip19.decode(input);
if (decoded.type !== 'npub') {
throw new Error('Input must be npub');
}
const pubkeyHex = decoded.data;
const fipsIpv6 = await pubkeyHexToFipsIpv6(pubkeyHex);
document.getElementById(`outNpubFipsIpv6`).innerText = fipsIpv6;
} catch (error) {
document.getElementById(`outNpubFipsIpv6`).innerText = 'Error: Invalid npub';
}
};
const NsecToHex = async () => {
try {
let decoded = NostrTools.nip19.decode(document.getElementById(`inpNsec`).innerText).data;
let Hex = Array.from(decoded).map(b => b.toString(16).padStart(2, '0')).join('');
document.getElementById(`outHex2`).innerText = Hex;
} catch (error) {
document.getElementById(`outHex2`).innerText = 'Error: Invalid nsec';
}
};
const HexToNpub = async () => {
try {
let hex = document.getElementById(`inpHex`).innerText.trim();
let npub = NostrTools.nip19.npubEncode(hex);
document.getElementById(`outNpub`).innerText = npub;
} catch (error) {
document.getElementById(`outNpub`).innerText = 'Error: Invalid hex';
}
};
const HexToNsec = async () => {
try {
let hex = document.getElementById(`inpHex2`).innerText.trim();
let bytes = hexToUint8Array(hex);
let nsec = NostrTools.nip19.nsecEncode(bytes);
document.getElementById(`outNsec`).innerText = nsec;
} catch (error) {
document.getElementById(`outNsec`).innerText = 'Error: Invalid hex';
}
};
const GenerateMnemonic = async () => {
let sw = NostrTools.nip06.generateSeedWords();
document.getElementById(`outMnemonicPhrase`).innerText = sw;
document.getElementById(`inMnemonicPhrase`).innerText = sw;
DecodeMnemonic(true);
};
const updateMnemonicPaginationState = () => {
const btnMnemonicPrev = document.getElementById(`btnMnemonicPrev`);
if (btnMnemonicPrev) {
const disabled = mnemonicPageIndex <= 0;
btnMnemonicPrev.style.opacity = disabled ? '0.5' : '1';
btnMnemonicPrev.style.pointerEvents = disabled ? 'none' : 'auto';
}
};
const setMnemonicBoxesExpanded = (expanded) => {
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
const box = document.getElementById(`outMnemonicJson${i}`);
if (!box) continue;
box.classList.toggle('expanded', expanded);
}
};
const DecodeMnemonic = async (resetToFirstPage = false) => {
const mnemonicInput = document.getElementById(`inMnemonicPhrase`);
const mnemonic = mnemonicInput.innerText.trim();
mnemonicInput.innerText = mnemonic;
if (resetToFirstPage) {
mnemonicPageIndex = 0;
}
try {
if (!mnemonic) {
setMnemonicBoxesExpanded(false);
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
const keyIndex = (mnemonicPageIndex * MNEMONIC_PAGE_SIZE) + i;
const label = document.getElementById(`lblMnemonicJson${i}`);
if (label) label.innerText = `Index ${String(keyIndex).padStart(2, '0')} JSON:`;
document.getElementById(`outMnemonicJson${i}`).innerText = '';
}
updateMnemonicPaginationState();
return;
}
setMnemonicBoxesExpanded(true);
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
const keyIndex = (mnemonicPageIndex * MNEMONIC_PAGE_SIZE) + i;
const label = document.getElementById(`lblMnemonicJson${i}`);
if (label) label.innerText = `Index ${String(keyIndex).padStart(2, '0')} JSON:`;
const privKey = NostrTools.nip06.privateKeyFromSeedWords(mnemonic, "", keyIndex, 0, 0);
const nsec = NostrTools.nip19.nsecEncode(privKey);
const nsecHex = Array.from(privKey).map(b => b.toString(16).padStart(2, '0')).join('');
const npubHex = NostrTools.getPublicKey(privKey);
const npub = NostrTools.nip19.npubEncode(npubHex);
const fipsIpv6 = await pubkeyHexToFipsIpv6(npubHex);
const outJson = {
index: keyIndex,
nsec,
npub,
npubHex,
nsecHex,
fipsIpv6,
strDerivationPath: getDerivationPath(keyIndex)
};
document.getElementById(`outMnemonicJson${i}`).innerText = JSON.stringify(outJson, "", 2);
}
} catch (error) {
setMnemonicBoxesExpanded(true);
for (let i = 0; i < MNEMONIC_PAGE_SIZE; i++) {
const keyIndex = (mnemonicPageIndex * MNEMONIC_PAGE_SIZE) + i;
const label = document.getElementById(`lblMnemonicJson${i}`);
if (label) label.innerText = `Index ${String(keyIndex).padStart(2, '0')} JSON:`;
document.getElementById(`outMnemonicJson${i}`).innerText = 'Error';
}
}
updateMnemonicPaginationState();
};
// NIP-04 Encryption
const EncodeNip04 = async () => {
try {
const privateKeyInput = document.getElementById(`inpNip04EncPrivkey`).innerText.trim();
const publicKeyInput = document.getElementById(`inpNip04EncPubkey`).innerText.trim();
const plaintext = document.getElementById(`inpNip04EncPlaintext`).innerText.trim();
if (!privateKeyInput || !publicKeyInput || !plaintext) {
document.getElementById(`outNip04Encrypted`).innerText = "Error: Please fill all fields";
return;
}
const privateKey = normalizeKey(privateKeyInput, true);
const publicKey = normalizeKey(publicKeyInput, false);
const encrypted = await NostrTools.nip04.encrypt(privateKey, publicKey, plaintext);
document.getElementById(`outNip04Encrypted`).innerText = encrypted;
} catch (error) {
console.error("NIP-04 encryption error:", error);
document.getElementById(`outNip04Encrypted`).innerText = `Error: ${error.message}`;
}
};
// NIP-04 Decryption
const DecodeNip04 = async () => {
try {
const privateKeyInput = document.getElementById(`inpNip04DecPrivkey`).innerText.trim();
const publicKeyInput = document.getElementById(`inpNip04DecPubkey`).innerText.trim();
const encrypted = document.getElementById(`inpNip04Encrypted`).innerText.trim();
if (!privateKeyInput || !publicKeyInput || !encrypted) {
document.getElementById(`outNip04Decrypted`).innerText = "Error: Please fill all fields";
return;
}
const privateKey = normalizeKey(privateKeyInput, true);
const publicKey = normalizeKey(publicKeyInput, false);
const decrypted = await NostrTools.nip04.decrypt(privateKey, publicKey, encrypted);
document.getElementById(`outNip04Decrypted`).innerText = decrypted;
} catch (error) {
console.error("NIP-04 decryption error:", error);
document.getElementById(`outNip04Decrypted`).innerText = `Error: ${error.message}`;
}
};
// NIP-44 Encryption
const EncodeNip44 = async () => {
try {
const privateKeyInput = document.getElementById(`inpNip44EncPrivkey`).innerText.trim();
const publicKeyInput = document.getElementById(`inpNip44EncPubkey`).innerText.trim();
const plaintext = document.getElementById(`inpNip44EncPlaintext`).innerText.trim();
if (!privateKeyInput || !publicKeyInput || !plaintext) {
document.getElementById(`outNip44Encrypted`).innerText = "Error: Please fill all fields";
return;
}
const privateKeyBytes = normalizeKey(privateKeyInput, true);
const publicKey = normalizeKey(publicKeyInput, false);
const conversationKey = NostrTools.nip44.getConversationKey(privateKeyBytes, publicKey);
const encrypted = NostrTools.nip44.encrypt(plaintext, conversationKey);
document.getElementById(`outNip44Encrypted`).innerText = encrypted;
} catch (error) {
console.error("NIP-44 encryption error:", error);
document.getElementById(`outNip44Encrypted`).innerText = `Error: ${error.message}`;
}
};
// NIP-44 Decryption
const DecodeNip44 = async () => {
try {
const privateKeyInput = document.getElementById(`inpNip44DecPrivkey`).innerText.trim();
const publicKeyInput = document.getElementById(`inpNip44DecPubkey`).innerText.trim();
const encrypted = document.getElementById(`inpNip44Encrypted`).innerText.trim()
.replace(/\s+/g, '')
.replace(/\u200B/g, '');
if (!privateKeyInput || !publicKeyInput || !encrypted) {
document.getElementById(`outNip44Decrypted`).innerText = "Error: Please fill all fields";
return;
}
const privateKeyBytes = normalizeKey(privateKeyInput, true);
const publicKey = normalizeKey(publicKeyInput, false);
const conversationKey = NostrTools.nip44.getConversationKey(privateKeyBytes, publicKey);
const decrypted = NostrTools.nip44.decrypt(encrypted, conversationKey);
document.getElementById(`outNip44Decrypted`).innerText = decrypted;
} catch (error) {
console.error("NIP-44 decryption error:", error);
document.getElementById(`outNip44Decrypted`).innerText = `Error: ${error.message}`;
}
};
// NSEC to NPUB Conversion
const NsecToNpub = async () => {
try {
let input = document.getElementById('inpNsecToNpub').innerText.trim();
if (!input) {
document.getElementById('outNpubEncoded').innerText = '';
document.getElementById('outNpubHex').innerText = '';
document.getElementById('labelNsecOut').innerText = 'nsec:';
document.getElementById('outNsecOther').innerText = '';
document.getElementById('outNsecToNpubJson').innerText = '';
return;
}
let privateKeyHex;
let nsecEncoded;
let nsecHex;
if (input.startsWith('nsec')) {
privateKeyHex = NostrTools.nip19.decode(input).data;
nsecEncoded = input;
nsecHex = Array.from(privateKeyHex).map(b => b.toString(16).padStart(2, '0')).join('');
document.getElementById('labelNsecOut').innerText = 'nsec (hex):';
document.getElementById('outNsecOther').innerText = nsecHex;
} else {
privateKeyHex = hexToUint8Array(input);
nsecHex = input;
nsecEncoded = NostrTools.nip19.nsecEncode(privateKeyHex);
document.getElementById('labelNsecOut').innerText = 'nsec (encoded):';
document.getElementById('outNsecOther').innerText = nsecEncoded;
}
let publicKeyHex = NostrTools.getPublicKey(privateKeyHex);
let npubEncoded = NostrTools.nip19.npubEncode(publicKeyHex);
document.getElementById('outNpubEncoded').innerText = npubEncoded;
document.getElementById('outNpubHex').innerText = publicKeyHex;
let jsonOutput = {
nsec: nsecEncoded,
npub: npubEncoded,
npubHex: publicKeyHex,
nsecHex: nsecHex
};
document.getElementById('outNsecToNpubJson').innerText = JSON.stringify(jsonOutput, "", 2);
} catch (error) {
console.error("NSEC to NPUB conversion error:", error);
document.getElementById('outNpubEncoded').innerText = 'Error: Invalid input';
document.getElementById('outNpubHex').innerText = 'Error: Invalid input';
document.getElementById('labelNsecOut').innerText = 'nsec:';
document.getElementById('outNsecOther').innerText = 'Error: Invalid input';
document.getElementById('outNsecToNpubJson').innerText = 'Error: Invalid input';
}
};
const UpdateNip19CustomTypeVisibility = () => {
const selNip19EncodeType = document.getElementById('selNip19EncodeType');
const divNip19EncodeTypeCustomWrap = document.getElementById('divNip19EncodeTypeCustomWrap');
if (!selNip19EncodeType || !divNip19EncodeTypeCustomWrap) return;
if (selNip19EncodeType.value === 'other') {
divNip19EncodeTypeCustomWrap.classList.remove('clsHidden');
} else {
divNip19EncodeTypeCustomWrap.classList.add('clsHidden');
}
};
// NIP-19 Encode
const EncodeNip19 = async () => {
try {
const selectedType = document.getElementById('selNip19EncodeType').value.trim().toLowerCase();
const customTypeRaw = document.getElementById('inpNip19EncodeTypeCustom').innerText.trim().toLowerCase();
const typeRaw = selectedType === 'other' ? customTypeRaw : selectedType;
const specialRaw = document.getElementById('inpNip19EncodeSpecial').innerText.trim();
const relaysRaw = document.getElementById('inpNip19EncodeRelays').innerText.trim();
const authorRaw = document.getElementById('inpNip19EncodeAuthor').innerText.trim();
const kindRaw = document.getElementById('inpNip19EncodeKind').innerText.trim();
if (!typeRaw) {
document.getElementById('outNip19Encoded').innerText = 'Error: Missing type';
document.getElementById('outNip19EncodeJson').innerText = 'Error: Missing type';
return;
}
const relays = relaysRaw
? relaysRaw
.split(/\n|,/)
.map(v => v.trim())
.filter(Boolean)
: [];
let encoded = '';
let jsonOutput = {
type: typeRaw,
selectedType
};
if (typeRaw === 'npub') {
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('npub requires 32-byte hex pubkey');
encoded = NostrTools.nip19.npubEncode(specialRaw);
jsonOutput.pubkey = specialRaw;
} else if (typeRaw === 'nsec') {
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('nsec requires 32-byte hex private key');
encoded = NostrTools.nip19.nsecEncode(hexToUint8Array(specialRaw));
jsonOutput.seckey = specialRaw;
} else if (typeRaw === 'note') {
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('note requires 32-byte hex event id');
encoded = NostrTools.nip19.noteEncode(specialRaw);
jsonOutput.id = specialRaw;
} else if (typeRaw === 'nprofile') {
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('nprofile requires 32-byte hex pubkey as special value');
const payload = { pubkey: specialRaw };
if (relays.length) payload.relays = relays;
encoded = NostrTools.nip19.nprofileEncode(payload);
jsonOutput.payload = payload;
} else if (typeRaw === 'nevent') {
if (!/^[a-fA-F0-9]{64}$/.test(specialRaw)) throw new Error('nevent requires 32-byte hex event id as special value');
const payload = { id: specialRaw };
if (relays.length) payload.relays = relays;
if (authorRaw) {
if (!/^[a-fA-F0-9]{64}$/.test(authorRaw)) throw new Error('author must be 32-byte hex pubkey');
payload.author = authorRaw;
}
if (kindRaw) {
const kind = Number(kindRaw);
if (!Number.isInteger(kind) || kind < 0) throw new Error('kind must be a non-negative integer');
payload.kind = kind;
}
encoded = NostrTools.nip19.neventEncode(payload);
jsonOutput.payload = payload;
} else if (typeRaw === 'naddr') {
if (!authorRaw) throw new Error('naddr requires author pubkey hex');
if (!/^[a-fA-F0-9]{64}$/.test(authorRaw)) throw new Error('author must be 32-byte hex pubkey');
const kind = Number(kindRaw);
if (!Number.isInteger(kind) || kind < 0) throw new Error('naddr requires a non-negative integer kind');
const payload = {
identifier: specialRaw,
pubkey: authorRaw,
kind
};
if (relays.length) payload.relays = relays;
encoded = NostrTools.nip19.naddrEncode(payload);
jsonOutput.payload = payload;
} else if (typeRaw === 'nrelay') {
if (!specialRaw) throw new Error('nrelay requires relay URL as special value');
encoded = NostrTools.nip19.nrelayEncode(specialRaw);
jsonOutput.relay = specialRaw;
} else {
throw new Error('Unsupported type, use npub, nsec, note, nprofile, nevent, naddr, nrelay, or choose other with a supported custom value');
}
jsonOutput.encoded = encoded;
document.getElementById('outNip19Encoded').innerText = encoded;
document.getElementById('outNip19EncodeJson').innerText = JSON.stringify(jsonOutput, '', 2);
} catch (error) {
console.error('NIP-19 encode error:', error);
document.getElementById('outNip19Encoded').innerText = `Error: ${error.message}`;
document.getElementById('outNip19EncodeJson').innerText = `Error: ${error.message}`;
}
};
// NIP-19 Decode
const DecodeNip19 = async () => {
try {
const value = document.getElementById('inpNip19DecodeValue').innerText.trim();
if (!value) {
document.getElementById('outNip19DecodeType').innerText = '';
document.getElementById('outNip19DecodeSpecial').innerText = '';
document.getElementById('outNip19DecodeJson').innerText = '';
return;
}
const decoded = NostrTools.nip19.decode(value);
const type = decoded.type;
const data = decoded.data;
let special = '';
let outputData = data;
if (type === 'nsec' && data instanceof Uint8Array) {
special = uint8ArrayToHex(data);
outputData = {
nsecHex: special
};
} else if (type === 'npub' || type === 'note') {
special = data;
} else if (type === 'nprofile') {
special = data.pubkey || '';
} else if (type === 'nevent') {
special = data.id || '';
} else if (type === 'naddr') {
special = data.identifier || '';
} else if (type === 'nrelay') {
special = data;
}
document.getElementById('outNip19DecodeType').innerText = type;
document.getElementById('outNip19DecodeSpecial').innerText = special;
document.getElementById('outNip19DecodeJson').innerText = JSON.stringify({ type, data: outputData }, '', 2);
} catch (error) {
console.error('NIP-19 decode error:', error);
document.getElementById('outNip19DecodeType').innerText = 'Error';
document.getElementById('outNip19DecodeSpecial').innerText = `Error: ${error.message}`;
document.getElementById('outNip19DecodeJson').innerText = `Error: ${error.message}`;
}
};
/* ================================================================
EVENT LISTENERS
================================================================ */
function setupEventListeners() {
// Hamburger
const divSvgHam = document.getElementById('divSvgHam');
if (divSvgHam) {
divSvgHam.addEventListener('click', toggleNav);
}
// Tool buttons
if (btnConnectPool) btnConnectPool.addEventListener("click", GenerateKeys);
if (btnCreateEvent) btnCreateEvent.addEventListener("click", CreateEvent);
if (inpNpub) inpNpub.addEventListener("input", NpubToHex);
if (inpNpubFipsIpv6) inpNpubFipsIpv6.addEventListener("input", NpubToFipsIpv6);
if (inpNsec) inpNsec.addEventListener("input", NsecToHex);
if (inpHex) inpHex.addEventListener("input", HexToNpub);
if (inpHex2) inpHex2.addEventListener("input", HexToNsec);
const btnGenMnemonic = document.getElementById(`btnGenMnemonic`);
const btnMnemonicPrev = document.getElementById(`btnMnemonicPrev`);
const btnMnemonicNext = document.getElementById(`btnMnemonicNext`);
if (btnGenMnemonic) btnGenMnemonic.addEventListener("click", GenerateMnemonic);
if (btnMnemonicPrev) {
btnMnemonicPrev.addEventListener("click", () => {
if (mnemonicPageIndex > 0) {
mnemonicPageIndex -= 1;
DecodeMnemonic(false);
}
});
}
if (btnMnemonicNext) {
btnMnemonicNext.addEventListener("click", () => {
mnemonicPageIndex += 1;
DecodeMnemonic(false);
});
}
if (inMnemonicPhrase) {
inMnemonicPhrase.addEventListener("keydown", (event) => {
if (event.key === 'Enter') {
event.preventDefault();
DecodeMnemonic(true);
}
});
}
// NIP-04 and NIP-44
const btnEncodeNip04 = document.getElementById(`btnEncodeNip04`);
const btnDecodeNip04 = document.getElementById(`btnDecodeNip04`);
const btnEncodeNip44 = document.getElementById(`btnEncodeNip44`);
const btnDecodeNip44 = document.getElementById(`btnDecodeNip44`);
if (btnEncodeNip04) btnEncodeNip04.addEventListener("click", EncodeNip04);
if (btnDecodeNip04) btnDecodeNip04.addEventListener("click", DecodeNip04);
if (btnEncodeNip44) btnEncodeNip44.addEventListener("click", EncodeNip44);
if (btnDecodeNip44) btnDecodeNip44.addEventListener("click", DecodeNip44);
// NSEC to NPUB
const inpNsecToNpub = document.getElementById(`inpNsecToNpub`);
if (inpNsecToNpub) inpNsecToNpub.addEventListener("input", NsecToNpub);
// NIP-19
const btnEncodeNip19 = document.getElementById(`btnEncodeNip19`);
const btnDecodeNip19 = document.getElementById(`btnDecodeNip19`);
const inpNip19DecodeValue = document.getElementById(`inpNip19DecodeValue`);
const selNip19EncodeType = document.getElementById(`selNip19EncodeType`);
if (btnEncodeNip19) btnEncodeNip19.addEventListener("click", EncodeNip19);
if (btnDecodeNip19) btnDecodeNip19.addEventListener("click", DecodeNip19);
if (inpNip19DecodeValue) inpNip19DecodeValue.addEventListener("input", DecodeNip19);
if (selNip19EncodeType) selNip19EncodeType.addEventListener("change", UpdateNip19CustomTypeVisibility);
UpdateNip19CustomTypeVisibility();
// Clipboard functionality
let clsClipboard = document.getElementsByClassName(`clsClipboard`);
for (let Each of clsClipboard) {
Each.addEventListener("click", function () {
navigator.clipboard.writeText(Each.innerText);
// Visual feedback
const originalBg = Each.style.backgroundColor;
Each.style.backgroundColor = 'green';
setTimeout(() => {
Each.style.backgroundColor = originalBg;
}, 200);
});
}
// Version bar buttons
const themeToggleButton = document.getElementById('themeToggleButton');
const logoutButton = document.getElementById('logoutButton');
if (themeToggleButton) {
themeToggleButton.addEventListener('click', () => {
isDarkMode = !isDarkMode;
if (isDarkMode) {
localStorage.setItem('theme', 'dark');
} else {
localStorage.setItem('theme', 'light');
}
localStorage.setItem('sidenavWasOpen', isNavOpen ? 'true' : 'false');
window.location.reload();
});
}
if (logoutButton) {
logoutButton.addEventListener('click', async () => {
try {
await Logout();
} catch (error) {
console.error('Logout failed:', error);
}
});
}
}
/* ================================================================
INITIALIZATION
================================================================ */
(async function main() {
console.log("[tools.html] Starting initialization...");
try {
// Initialize hamburger menu
initHamburgerMenu();
// Setup event listeners
setupEventListeners();
// Initialize NDK (allow unauthenticated access for public tools page)
await initNDKPage({ allowUnauthenticated: true });
// Try authenticated header state; fall back to login button when unauthenticated
try {
currentPubkey = await getPubkey();
await injectHeaderAvatar(currentPubkey);
console.log("[tools.html] Authenticated as:", currentPubkey);
} catch (authError) {
currentPubkey = null;
await injectHeaderLoginButton();
console.log("[tools.html] Running unauthenticated mode");
}
// Initialize relay UI
initFooterRelayStatus();
initSidenavRelaySection();
await initBlossomSection();
initAiSectionWithLocalConfig();
// Load NostrTools
NostrTools = await waitForNostrTools();
console.log("[tools.html] NostrTools loaded");
// Listen for relay activity
window.addEventListener('ndkRelayActivity', (event) => {
const { relayUrl, activity, stats } = event.detail;
setRelayActivityState(relayUrl, activity);
});
window.addEventListener('message', (event) => {
if (event.data && event.data.type === 'relayActivity') {
const { relayUrl, activity } = event.data;
setRelayActivityState(relayUrl, activity);
}
});
// Restore sidenav state
const sidenavWasOpen = localStorage.getItem('sidenavWasOpen');
if (sidenavWasOpen === 'true') {
localStorage.removeItem('sidenavWasOpen');
openNav();
}
// Start update loop
updateIntervalId = setInterval(UpdateFooter, 1000);
// Update version display
await updateVersionDisplay();
console.log('[tools.html] Initialization complete');
} catch (error) {
console.error('[tools.html] Initialization failed:', error);
divBody.innerHTML = `<div style="text-align: center; padding: 50px;">
<div style="font-size: 24px; margin-bottom: 20px; color: red;">❌ Authentication Error</div>
<div style="font-size: 16px; color: #666;">${error.message}</div>
<div style="margin-top: 20px;">
<button onclick="location.reload()" style="padding: 10px 20px; font-size: 16px;">Retry</button>
</div>
</div>`;
}
})();
</script>
</body>
</html>