damus.io

damus.io website
git clone git://jb55.com/damus.io
Log | Files | Refs | README

commit dfef728d9125548fb543e4da0f5870e6f8380470
parent 232ca7c087a6c8cbf9dc18102ce02c7732ce4666
Author: William Casarin <jb55@jb55.com>
Date:   Thu, 27 Oct 2022 14:11:43 -0700

move webv2 to master

no point having a branch for this.

Diffstat:
Awebv2/.gitignore | 1+
Awebv2/Makefile | 7+++++++
Awebv2/bech32.js | 169+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebv2/damus.css | 181+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebv2/damus.js | 574+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebv2/img/damus-nobg.svg | 186+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebv2/index.html | 52++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebv2/noble-secp256k1.js | 1203+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Awebv2/nostr.js | 360+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
9 files changed, 2733 insertions(+), 0 deletions(-)

diff --git a/webv2/.gitignore b/webv2/.gitignore @@ -0,0 +1 @@ +tags diff --git a/webv2/Makefile b/webv2/Makefile @@ -0,0 +1,7 @@ +tags: fake + ctags damus.js nostr.js > $@ + +dist: + rsync -avzP ./ charon:/www/damus.io/web/ + +.PHONY: fake diff --git a/webv2/bech32.js b/webv2/bech32.js @@ -0,0 +1,169 @@ +var ALPHABET = 'qpzry9x8gf2tvdw0s3jn54khce6mua7l'; +var ALPHABET_MAP = {}; +for (var z = 0; z < ALPHABET.length; z++) { + var x = ALPHABET.charAt(z); + ALPHABET_MAP[x] = z; +} +function polymodStep(pre) { + var b = pre >> 25; + return (((pre & 0x1ffffff) << 5) ^ + (-((b >> 0) & 1) & 0x3b6a57b2) ^ + (-((b >> 1) & 1) & 0x26508e6d) ^ + (-((b >> 2) & 1) & 0x1ea119fa) ^ + (-((b >> 3) & 1) & 0x3d4233dd) ^ + (-((b >> 4) & 1) & 0x2a1462b3)); +} +function prefixChk(prefix) { + var chk = 1; + for (var i = 0; i < prefix.length; ++i) { + var c = prefix.charCodeAt(i); + if (c < 33 || c > 126) + return 'Invalid prefix (' + prefix + ')'; + chk = polymodStep(chk) ^ (c >> 5); + } + chk = polymodStep(chk); + for (var i = 0; i < prefix.length; ++i) { + var v = prefix.charCodeAt(i); + chk = polymodStep(chk) ^ (v & 0x1f); + } + return chk; +} +function convertbits(data, inBits, outBits, pad) { + var value = 0; + var bits = 0; + var maxV = (1 << outBits) - 1; + var result = []; + for (var i = 0; i < data.length; ++i) { + value = (value << inBits) | data[i]; + bits += inBits; + while (bits >= outBits) { + bits -= outBits; + result.push((value >> bits) & maxV); + } + } + if (pad) { + if (bits > 0) { + result.push((value << (outBits - bits)) & maxV); + } + } + else { + if (bits >= inBits) + return 'Excess padding'; + if ((value << (outBits - bits)) & maxV) + return 'Non-zero padding'; + } + return result; +} +function toWords(bytes) { + return convertbits(bytes, 8, 5, true); +} +function fromWordsUnsafe(words) { + var res = convertbits(words, 5, 8, false); + if (Array.isArray(res)) + return res; +} +function fromWords(words) { + var res = convertbits(words, 5, 8, false); + if (Array.isArray(res)) + return res; + throw new Error(res); +} +function getLibraryFromEncoding(encoding) { + var ENCODING_CONST; + if (encoding === 'bech32') { + ENCODING_CONST = 1; + } + else { + ENCODING_CONST = 0x2bc830a3; + } + function encode(prefix, words, LIMIT) { + LIMIT = LIMIT || 90; + if (prefix.length + 7 + words.length > LIMIT) + throw new TypeError('Exceeds length limit'); + prefix = prefix.toLowerCase(); + // determine chk mod + var chk = prefixChk(prefix); + if (typeof chk === 'string') + throw new Error(chk); + var result = prefix + '1'; + for (var i = 0; i < words.length; ++i) { + var x = words[i]; + if (x >> 5 !== 0) + throw new Error('Non 5-bit word'); + chk = polymodStep(chk) ^ x; + result += ALPHABET.charAt(x); + } + for (var i = 0; i < 6; ++i) { + chk = polymodStep(chk); + } + chk ^= ENCODING_CONST; + for (var i = 0; i < 6; ++i) { + var v = (chk >> ((5 - i) * 5)) & 0x1f; + result += ALPHABET.charAt(v); + } + return result; + } + function __decode(str, LIMIT) { + LIMIT = LIMIT || 90; + if (str.length < 8) + return str + ' too short'; + if (str.length > LIMIT) + return 'Exceeds length limit'; + // don't allow mixed case + var lowered = str.toLowerCase(); + var uppered = str.toUpperCase(); + if (str !== lowered && str !== uppered) + return 'Mixed-case string ' + str; + str = lowered; + var split = str.lastIndexOf('1'); + if (split === -1) + return 'No separator character for ' + str; + if (split === 0) + return 'Missing prefix for ' + str; + var prefix = str.slice(0, split); + var wordChars = str.slice(split + 1); + if (wordChars.length < 6) + return 'Data too short'; + var chk = prefixChk(prefix); + if (typeof chk === 'string') + return chk; + var words = []; + for (var i = 0; i < wordChars.length; ++i) { + var c = wordChars.charAt(i); + var v = ALPHABET_MAP[c]; + if (v === undefined) + return 'Unknown character ' + c; + chk = polymodStep(chk) ^ v; + // not in the checksum? + if (i + 6 >= wordChars.length) + continue; + words.push(v); + } + if (chk !== ENCODING_CONST) + return 'Invalid checksum for ' + str; + return { prefix: prefix, words: words }; + } + function decodeUnsafe(str, LIMIT) { + var res = __decode(str, LIMIT); + if (typeof res === 'object') + return res; + } + function decode(str, LIMIT) { + var res = __decode(str, LIMIT); + if (typeof res === 'object') + return res; + throw new Error(res); + } + return { + decodeUnsafe: decodeUnsafe, + decode: decode, + encode: encode, + toWords: toWords, + fromWordsUnsafe: fromWordsUnsafe, + fromWords: fromWords + }; +} + +const bech32 = getLibraryFromEncoding('bech32'); +const bech32m = getLibraryFromEncoding('bech32m'); + diff --git a/webv2/damus.css b/webv2/damus.css @@ -0,0 +1,181 @@ +.header { + display: flex; + margin: 30px 0 30px 0; + flex-direction: column; + align-items: center; +} + +.logo { + margin-bottom: 0; + letter-spacing: -0.05em; +} + +.logo img { + padding-right: 18px; + width: 60px; +} + +body { + min-height: 100vh; + font-family: system-ui, sans; +} + +#reply-top { + display: flex; + align-items: center; +} + +#reply-modal { + position: fixed; /* Stay in place */ + z-index: 1; /* Sit on top */ + left: 0; + top: 0; + width: 100%; /* Full width */ + height: 100%; /* Full height */ + background: rgba(0,0,0,0.4); +} + +#reply-content { + background-color: rgba(255,255,255,0.2); + border: 0; + width: 100%; + color: white; + border-radius: 5px; +} + +button { + border: 0; + padding: 5px; + color: white; + background-color: #C073EB; + border-radius: 5px; +} + +.close { + margin: 0 8px 0 8px; + font-size: 1.3em; + font-weight: bold; + text-decoration: none; + color: white; +} + +.small-text { + font-size: 12px; +} + +#reply-modal-content { + /*display: none; */ + padding: 10px; + margin: 10% auto; + width: 60%; + height: 50%; + overflow: auto; /* Enable scroll if needed */ + background: linear-gradient(0deg, #A74BDB 0%, #C45BFE 100%); + border-radius: 15px; +} + +html { + line-height: 1.5; + font-size: 20px; + font-family: "Georgia", sans-serif; + + color: white; + background: linear-gradient(45deg, rgba(28,85,255,1) 0%, rgba(127,53,171,1) 59%, #C45BFE 100%); +} +.container { + margin: 0 auto; + max-width: 36em; + hyphens: auto; + word-wrap: break-word; + text-rendering: optimizeLegibility; + font-kerning: normal; +} +@media (max-width: 600px) { + .container { + font-size: 0.9em; + padding: 1em; + } +} +@media print { + .container { + background-color: transparent; + color: black; + font-size: 12pt; + } + p, h2, h3 { + orphans: 3; + widows: 3; + } + h2, h3, h4 { + page-break-after: avoid; + } +} + +.pfp { + width: 60px; + height: 60px; + margin: 0 15px 0 15px; + border-radius: 50%; +} + +.comment { + display: flex; + font-family: system-ui, sans; + margin-bottom: 20px; + flex-wrap: wrap; + align-items: center; +} + +.comment p { + background-color: rgba(255.0,255.0,255.0,0.1); + padding: 10px; + border-radius: 8px; + margin: 0; + width: 55%; +} + +.comment .info { + text-align: right; + width: 18%; + line-height: 0.8em; +} + +.quote { + border-left: 2px solid white; + margin-left: 10px; + padding: 10px; + background-color: rgba(255.0,255.0,255.0,0.1); + display: block; +} + +.comment .info span { + font-size: 11px; + color: rgba(255.0,255.0,255.0,0.7); +} + +@media (max-width: 800px){ + /* Reverse the order of elements in the user comments, + so that the avatar and info appear after the text. */ + .comment .info { + order: 2; + width: 50%; + text-align: left; + } + + .pfp { + order: 1; + margin: 0 15px 0 0; + } + + .comment { + padding: 10px; + border-radius: 8px; + background-color: rgba(255.0,255.0,255.0,0.1); + } + + .comment p { + order: 3; + margin-top: 10px; + width: 100%; + } +} diff --git a/webv2/damus.js b/webv2/damus.js @@ -0,0 +1,574 @@ + +let DSTATE + +function uuidv4() { + return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => + (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) + ); +} + +function insert_event_sorted(evs, new_ev) { + for (let i = 0; i < evs.length; i++) { + const ev = evs[i] + + if (new_ev.id === ev.id) { + return false + } + + if (new_ev.created_at > ev.created_at) { + evs.splice(i, 0, new_ev) + return true + } + } + + evs.push(new_ev) + return true +} + +function init_contacts() { + return { + event: null, + friends: new Set(), + friend_of_friends: new Set(), + } +} + +function init_home_model() { + return { + done_init: false, + loading: true, + all_events: {}, + events: [], + profiles: {}, + last_event_of_kind: {}, + contacts: init_contacts() + } +} + +async function damus_web_init(thread) +{ + const {RelayPool} = nostrjs + const pool = RelayPool(["wss://relay.damus.io"]) + const now = (new Date().getTime()) / 1000 + const model = init_home_model() + DSTATE = model + + const ids = { + comments: "comments",//uuidv4(), + profiles: "profiles",//uuidv4(), + account: "account",//uuidv4(), + home: "home",//uuidv4(), + contacts: "contacts",//uuidv4(), + notifications: "notifications",//uuidv4(), + dms: "dms",//uuidv4(), + } + + model.pubkey = get_pubkey() + if (!model.pubkey) + return + model.pool = pool + model.el = document.querySelector("#posts") + + pool.on('open', (relay) => { + //let authors = followers + // TODO: fetch contact list + log_debug("relay connected", relay.url) + + if (!model.done_init) { + model.loading = false + send_initial_filters(ids.account, model.pubkey, relay) + } else { + send_home_filters(ids, model, relay) + } + //relay.subscribe(comments_id, {kinds: [1,42], limit: 100}) + }); + + pool.on('event', (relay, sub_id, ev) => { + handle_home_event(ids, model, relay, sub_id, ev) + }) + + pool.on('eose', async (relay, sub_id) => { + if (sub_id === ids.home) { + handle_comments_loaded(ids.profiles, model, relay) + } else if (sub_id === ids.profiles) { + handle_profiles_loaded(ids.profiles, model, relay) + } + }) + + return pool +} + +let rerender_home_timer +function handle_home_event(ids, model, relay, sub_id, ev) { + model.all_events[ev.id] = ev + + switch (sub_id) { + case ids.home: + if (ev.content !== "") + insert_event_sorted(model.events, ev) + if (model.realtime) { + if (rerender_home_timer) + clearTimeout(rerender_home_timer) + rerender_home_timer = setTimeout(render_home_view.bind(null, model), 200) + } + break; + case ids.account: + switch (ev.kind) { + case 3: + model.loading = false + process_contact_event(model, ev) + model.done_init = true + model.pool.unsubscribe(ids.account, [relay]) + break + case 0: + handle_profile_event(model, ev) + break + } + case ids.profiles: + try { + model.profiles[ev.pubkey] = JSON.parse(ev.content) + } catch { + console.log("failed to parse", ev.content) + } + } +} + +function handle_profile_event(model, ev) { + console.log("PROFILE", ev) +} + +function send_initial_filters(account_id, pubkey, relay) { + const filter = {authors: [pubkey], kinds: [3], limit: 1} + relay.subscribe(account_id, filter) +} + +function send_home_filters(ids, model, relay) { + const friends = contacts_friend_list(model.contacts) + friends.push(model.pubkey) + + const contacts_filter = {kinds: [0], authors: friends} + const dms_filter = {kinds: [4], limit: 500} + const our_dms_filter = {kinds: [4], authors: [ model.pubkey ], limit: 500} + const home_filter = {kinds: [1,42,6,7], authors: friends, limit: 500} + const notifications_filter = {kinds: [1,42,6,7], "#p": [model.pubkey], limit: 100} + + let home_filters = [home_filter] + let notifications_filters = [notifications_filter] + let contacts_filters = [contacts_filter] + let dms_filters = [dms_filter, our_dms_filter] + + let last_of_kind = {} + if (relay) { + last_of_kind = + model.last_event_of_kind[relay] = + model.last_event_of_kind[relay] || {} + } + + update_filters_with_since(last_of_kind, home_filters) + update_filters_with_since(last_of_kind, contacts_filters) + update_filters_with_since(last_of_kind, notifications_filters) + update_filters_with_since(last_of_kind, dms_filters) + + const subto = relay? [relay] : undefined + model.pool.subscribe(ids.home, home_filters, subto) + model.pool.subscribe(ids.contacts, contacts_filters, subto) + model.pool.subscribe(ids.notifications, notifications_filters, subto) + model.pool.subscribe(ids.dms, dms_filters, subto) +} + +function get_since_time(last_event) { + if (!last_event) { + return null + } + + return last_event.created_at - 60 * 10 +} + +function update_filter_with_since(last_of_kind, filter) { + const kinds = filter.kinds || [] + let initial = null + let earliest = kinds.reduce((earliest, kind) => { + const last = last_of_kind[kind] + let since = get_since_time(last) + + if (!earliest) { + if (since === null) + return null + + return since + } + + if (since === null) + return earliest + + return since < earliest ? since : earliest + + }, initial) + + if (earliest) + filter.since = earliest +} + +function update_filters_with_since(last_of_kind, filters) { + for (const filter of filters) { + update_filter_with_since(last_of_kind, filter) + } +} + +function contacts_friend_list(contacts) { + return Array.from(contacts.friends) +} + +function process_contact_event(model, ev) { + load_our_contacts(model.contacts, model.pubkey, ev) + load_our_relays(model.pubkey, model.pool, ev) + add_contact_if_friend(model.contacts, ev) +} + +function add_contact_if_friend(contacts, ev) { + if (!contact_is_friend(contacts, ev.pubkey)) { + return + } + + add_friend_contact(contacts, ev) +} + +function contact_is_friend(contacts, pk) { + return contacts.friends.has(pk) +} + +function add_friend_contact(contacts, contact) { + contacts.friends[contact.pubkey] = true + + for (const tag of contact.tags) { + if (tag.count >= 2 && tag[0] == "p") { + contacts.friend_of_friends.add(tag[1]) + } + } +} + +function load_our_relays(our_pubkey, pool, ev) { + if (ev.pubkey != our_pubkey) + return + + let relays + try { + relays = JSON.parse(ev.content) + } catch (e) { + log_debug("error loading relays", e) + return + } + + for (const relay of Object.keys(relays)) { + log_debug("adding relay", relay) + if (!pool.has(relay)) + pool.add(relay) + } +} + +function log_debug(fmt, ...args) { + console.log("[debug] " + fmt, ...args) +} + +function load_our_contacts(contacts, our_pubkey, ev) { + if (ev.pubkey !== our_pubkey) + return + + contacts.event = ev + + for (const tag of ev.tags) { + if (tag.length > 1 && tag[0] === "p") { + contacts.friends.add(tag[1]) + } + } +} + +function handle_profiles_loaded(profiles_id, model, relay) { + // stop asking for profiles + model.pool.unsubscribe(profiles_id, relay) + model.realtime = true + render_home_view(model) +} + +function debounce(f, interval) { + let timer = null; + let first = true; + + return (...args) => { + clearTimeout(timer); + return new Promise((resolve) => { + timer = setTimeout(() => resolve(f(...args)), first? 0 : interval); + first = false + }); + }; +} + +// load profiles after comment notes are loaded +function handle_comments_loaded(profiles_id, model, relay) +{ + const pubkeys = model.events.reduce((s, ev) => { + s.add(ev.pubkey) + return s + }, new Set()) + const authors = Array.from(pubkeys) + + // load profiles + const filter = {kinds: [0], authors: authors} + console.log("subscribe", profiles_id, filter, relay) + model.pool.subscribe(profiles_id, filter, relay) +} + +function render_home_view(model) { + log_debug("rendering home view") + model.el.innerHTML = render_events(model) +} + +function render_events(model) { + const render = render_event.bind(null, model) + return model.events.map(render).join("\n") +} + +function render_event(model, ev, opts={}) { + const profile = model.profiles[ev.pubkey] || { + name: "anon", + display_name: "Anonymous", + } + const delta = time_delta(new Date().getTime(), ev.created_at*1000) + const pk = ev.pubkey + const bar = opts.nobar? "" : render_action_bar(ev) + return ` + <div class="comment"> + <div class="info"> + ${render_name(ev.pubkey, profile)} + <span>${delta}</span> + </div> + <img class="pfp" onerror="this.onerror=null;this.src='${robohash(pk)}';" src="${get_picture(pk, profile)}"> + <p> + ${format_content(ev.content)} + + ${bar} + </p> + </div> + ` +} + +function close_reply() { + const modal = document.querySelector("#reply-modal") + modal.style.display = "none"; +} + +function gather_reply_tags(pubkey, from) { + let tags = [] + for (const tag of from.tags) { + if (tag.length >= 2) { + if (tag[0] === "e") { + tags.push(tag) + } else if (tag[0] === "p" && tag[1] !== pubkey) { + tags.push(tag) + } + } + } + tags.push(["e", from.id, "", "reply"]) + if (from.pubkey !== pubkey) + tags.push(["p", from.pubkey]) + return tags +} + +async function create_reply(privkey, pubkey, content, from) { + const tags = gather_reply_tags(pubkey, from) + const created_at = Math.floor(new Date().getTime() / 1000) + const kind = from.kind + + let reply = { pubkey, tags, content, created_at, kind } + + reply.id = await nostrjs.calculate_id(reply) + reply.sig = await sign_id(privkey, reply.id) + + return reply +} + +async function send_reply() { + const modal = document.querySelector("#reply-modal") + const replying_to = modal.querySelector("#replying-to") + const evid = replying_to.dataset.evid + const ev = DSTATE.all_events[evid] + + const { pool } = DSTATE + const content = document.querySelector("#reply-content").value + const pubkey = get_pubkey() + const privkey = get_privkey() + + let reply = await create_reply(privkey, pubkey, content, ev) + console.log(nostrjs.event_commitment(reply), reply) + pool.send(["EVENT", reply]) + + close_reply() +} + +function bech32_decode(pubkey) { + const decoded = bech32.decode(pubkey) + const bytes = fromWords(decoded.words) + return nostrjs.hex_encode(bytes) +} + +function get_local_state(key) { + if (DSTATE[key] != null) + return DSTATE[key] + + return localStorage.getItem(key) +} + +function set_local_state(key, val) { + DSTATE[key] = val + localStorage.setItem(key, val) +} + +function get_pubkey() { + let pubkey = get_local_state('pubkey') + + if (pubkey) + return pubkey + + pubkey = prompt("Enter pubkey (hex or npub)") + + if (!pubkey) + throw new Error("Need pubkey to continue") + + if (pubkey[0] === "n") + pubkey = bech32_decode(pubkey) + + set_local_state('pubkey', pubkey) + return pubkey +} + +function get_privkey() { + let privkey = get_local_state('privkey') + + if (privkey) + return privkey + + if (!privkey) + privkey = prompt("Enter private key") + + if (!privkey) + throw new Error("can't get privkey") + + if (privkey[0] === "n") { + privkey = bech32_decode(privkey) + } + + set_local_state('privkey', privkey) + + return privkey +} + +async function sign_id(privkey, id) +{ + //const digest = nostrjs.hex_decode(id) + const sig = await nobleSecp256k1.schnorr.sign(id, privkey) + return nostrjs.hex_encode(sig) +} + +function reply_to(evid) { + const modal = document.querySelector("#reply-modal") + const replying = modal.style.display === "none"; + const replying_to = modal.querySelector("#replying-to") + + replying_to.dataset.evid = evid + const ev = DSTATE.all_events[evid] + replying_to.innerHTML = render_event(DSTATE, ev, {nobar: true}) + + modal.style.display = replying? "block" : "none"; +} + +function render_action_bar(ev) { + return ` + <a href="javascript:reply_to('${ev.id}')">reply</a> + ` +} + +function convert_quote_blocks(content) +{ + const split = content.split("\n") + let blockin = false + return split.reduce((str, line) => { + if (line !== "" && line[0] === '>') { + if (!blockin) { + str += "<span class='quote'>" + blockin = true + } + str += sanitize(line.slice(1)) + } else { + if (blockin) { + blockin = false + str += "</span>" + } + str += sanitize(line) + } + return str + "<br/>" + }, "") +} + +function format_content(content) +{ + return convert_quote_blocks(content) +} + +function sanitize(content) +{ + if (!content) + return "" + return content.replaceAll("<","&lt;").replaceAll(">","&gt;") +} + +function robohash(pk) { + return "https://robohash.org/" + pk +} + +function get_picture(pk, profile) +{ + return sanitize(profile.picture) || robohash(pk) +} + +function render_name(pk, profile={}) +{ + const display_name = profile.display_name || profile.user + const username = profile.name || "anon" + const name = display_name || username + + return `<div class="username">${sanitize(name)}</div>` +} + +function time_delta(current, previous) { + var msPerMinute = 60 * 1000; + var msPerHour = msPerMinute * 60; + var msPerDay = msPerHour * 24; + var msPerMonth = msPerDay * 30; + var msPerYear = msPerDay * 365; + + var elapsed = current - previous; + + if (elapsed < msPerMinute) { + return Math.round(elapsed/1000) + ' seconds ago'; + } + + else if (elapsed < msPerHour) { + return Math.round(elapsed/msPerMinute) + ' minutes ago'; + } + + else if (elapsed < msPerDay ) { + return Math.round(elapsed/msPerHour ) + ' hours ago'; + } + + else if (elapsed < msPerMonth) { + return Math.round(elapsed/msPerDay) + ' days ago'; + } + + else if (elapsed < msPerYear) { + return Math.round(elapsed/msPerMonth) + ' months ago'; + } + + else { + return Math.round(elapsed/msPerYear ) + ' years ago'; + } +} diff --git a/webv2/img/damus-nobg.svg b/webv2/img/damus-nobg.svg @@ -0,0 +1,186 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<!-- Created with Inkscape (http://www.inkscape.org/) --> + +<svg + width="146.15311mm" + height="184.664mm" + viewBox="0 0 146.15311 184.66401" + version="1.1" + id="svg5" + inkscape:version="1.2-alpha (0bd5040e, 2022-02-05)" + sodipodi:docname="damus-nobg.svg" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:xlink="http://www.w3.org/1999/xlink" + xmlns="http://www.w3.org/2000/svg" + xmlns:svg="http://www.w3.org/2000/svg"> + <sodipodi:namedview + id="namedview7" + pagecolor="#ffffff" + bordercolor="#000000" + borderopacity="0.25" + inkscape:pageshadow="2" + inkscape:pageopacity="0.0" + inkscape:pagecheckerboard="0" + inkscape:blackoutopacity="0.0" + inkscape:document-units="mm" + showgrid="false" + inkscape:zoom="0.5946522" + inkscape:cx="73.992831" + inkscape:cy="206.8436" + inkscape:window-width="1435" + inkscape:window-height="844" + inkscape:window-x="0" + inkscape:window-y="25" + inkscape:window-maximized="0" + inkscape:current-layer="layer2" /> + <defs + id="defs2"> + <linearGradient + inkscape:collect="always" + id="linearGradient39361"> + <stop + style="stop-color:#0de8ff;stop-opacity:0.78082192;" + offset="0" + id="stop39357" /> + <stop + style="stop-color:#d600fc;stop-opacity:0.95433789;" + offset="1" + id="stop39359" /> + </linearGradient> + <inkscape:path-effect + effect="bspline" + id="path-effect255" + is_visible="true" + lpeversion="1" + weight="33.333333" + steps="2" + helper_size="0" + apply_no_weight="true" + apply_with_weight="true" + only_selected="false" /> + <linearGradient + inkscape:collect="always" + id="linearGradient2119"> + <stop + style="stop-color:#1c55ff;stop-opacity:1;" + offset="0" + id="stop2115" /> + <stop + style="stop-color:#7f35ab;stop-opacity:1;" + offset="0.5" + id="stop2123" /> + <stop + style="stop-color:#ff0bd6;stop-opacity:1;" + offset="1" + id="stop2117" /> + </linearGradient> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient2119" + id="linearGradient2121" + x1="10.067794" + y1="248.81357" + x2="246.56145" + y2="7.1864405" + gradientUnits="userSpaceOnUse" /> + <linearGradient + inkscape:collect="always" + xlink:href="#linearGradient39361" + id="linearGradient39367" + x1="62.104473" + y1="128.78963" + x2="208.25758" + y2="128.78963" + gradientUnits="userSpaceOnUse" /> + </defs> + <g + inkscape:label="Background" + inkscape:groupmode="layer" + id="layer1" + sodipodi:insensitive="true" + style="display:none" + transform="translate(-62.104473,-36.457485)"> + <rect + style="fill:url(#linearGradient2121);fill-opacity:1;stroke-width:0.264583" + id="rect61" + width="256" + height="256" + x="-5.3875166e-08" + y="-1.0775033e-07" + ry="0" + inkscape:label="Gradient" + sodipodi:insensitive="true" /> + </g> + <g + inkscape:groupmode="layer" + id="layer2" + inkscape:label="Logo" + sodipodi:insensitive="true" + transform="translate(-62.104473,-36.457485)"> + <path + style="fill:url(#linearGradient39367);fill-opacity:1;stroke:#ffffff;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 101.1429,213.87373 C 67.104473,239.1681 67.104473,42.67112 67.104473,42.67112 135.18122,57.58146 203.25844,72.491904 203.25758,105.24181 c -8.6e-4,32.74991 -68.07625,83.33755 -102.11468,108.63192 z" + id="path253" + sodipodi:insensitive="true" /> + </g> + <g + inkscape:groupmode="layer" + id="layer3" + inkscape:label="Poly" + sodipodi:insensitive="true" + transform="translate(-62.104473,-36.457485)"> + <path + style="fill:#ffffff;fill-opacity:0.325424;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 67.32839,76.766948 112.00424,99.41949 100.04873,52.226693 Z" + id="path4648" /> + <path + style="fill:#ffffff;fill-opacity:0.274576;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 111.45696,98.998695 107.00758,142.60261 70.077729,105.67276 Z" + id="path9299" /> + <path + style="fill:#ffffff;fill-opacity:0.379661;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 111.01202,99.221164 29.14343,-37.15232 25.80641,39.377006 z" + id="path9301" /> + <path + style="fill:#ffffff;fill-opacity:0.447458;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 111.45696,99.443631 57.17452,55.172309 -2.89209,-53.17009 z" + id="path9368" /> + <path + style="fill:#ffffff;fill-opacity:0.20678;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 106.78511,142.38015 62.06884,12.68073 -57.17452,-55.617249 z" + id="path9370" /> + <path + style="fill:#ffffff;fill-opacity:0.244068;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="m 106.78511,142.38015 -28.47603,32.9254 62.51378,7.56395 z" + id="path9372" /> + <path + style="fill:#ffffff;fill-opacity:0.216949;stroke:#ffffff;stroke-width:2;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + d="M 165.96186,101.44585 195.7727,125.02756 182.64703,78.754017 Z" + id="path9374" /> + </g> + <g + inkscape:groupmode="layer" + id="layer4" + inkscape:label="Vertices" + transform="translate(-62.104473,-36.457485)"> + <circle + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="path27764" + cx="106.86934" + cy="142.38014" + r="2.0022209" /> + <circle + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="circle28773" + cx="111.54119" + cy="99.221161" + r="2.0022209" /> + <circle + style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-width:4;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" + id="circle29091" + cx="165.90784" + cy="101.36163" + r="2.0022209" /> + </g> +</svg> diff --git a/webv2/index.html b/webv2/index.html @@ -0,0 +1,52 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8"> + <meta name="viewport" content="width=device-width, initial-scale=1"> + + <title>Damus Web</title> + + <link rel="stylesheet" href="damus.css?v=3"> + </head> + <body> + <section class="header"> + <span class="logo"> + <img src="img/damus-nobg.svg"/> + </span> + </section> + <div class="container"> + <div id="posts"> + </div> + + <div style="display: none" id="reply-modal"> + <div id="reply-modal-content"> + <span id="reply-top"> + <a class="close" href="javascript:close_reply()">✕</a> + <span class="small-text"> + Replying to... + </span> + </span> + + <div id="replying-to"> + </div> + + <div> + <textarea id="reply-content"></textarea> + </div> + + <div style="float:right"> + <button onclick="send_reply()" id="reply-button">Reply</button> + </div> + </div> + </div> + </div> + <script src="noble-secp256k1.js?v=1"></script> + <script src="bech32.js?v=1"></script> + <script src="nostr.js?v=3"></script> + <script src="damus.js?v=5"></script> + <script> + const relay = damus_web_init("4e8b44bb43018f79bd3efcdcd71af43814cdf996e0c62adedda1ac33bf5e1371") + </script> + </body> +</html> + diff --git a/webv2/noble-secp256k1.js b/webv2/noble-secp256k1.js @@ -0,0 +1,1203 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.nobleSecp256k1 = {})); +})(this, (function (exports) { 'use strict'; + + const _nodeResolve_empty = {}; + + const nodeCrypto = /*#__PURE__*/Object.freeze({ + __proto__: null, + 'default': _nodeResolve_empty + }); + + /*! noble-secp256k1 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ + const _0n = BigInt(0); + const _1n = BigInt(1); + const _2n = BigInt(2); + const _3n = BigInt(3); + const _8n = BigInt(8); + const CURVE = Object.freeze({ + a: _0n, + b: BigInt(7), + P: BigInt('0xfffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f'), + n: BigInt('0xfffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364141'), + h: _1n, + Gx: BigInt('55066263022277343669578718895168534326250603453777594175500187360389116729240'), + Gy: BigInt('32670510020758816978083085130507043184471273380659243275938904335757337482424'), + beta: BigInt('0x7ae96a2b657c07106e64479eac3434e99cf0497512f58995c1396c28719501ee'), + }); + function weistrass(x) { + const { a, b } = CURVE; + const x2 = mod(x * x); + const x3 = mod(x2 * x); + return mod(x3 + a * x + b); + } + const USE_ENDOMORPHISM = CURVE.a === _0n; + class ShaError extends Error { + constructor(message) { + super(message); + } + } + class JacobianPoint { + constructor(x, y, z) { + this.x = x; + this.y = y; + this.z = z; + } + static fromAffine(p) { + if (!(p instanceof Point)) { + throw new TypeError('JacobianPoint#fromAffine: expected Point'); + } + return new JacobianPoint(p.x, p.y, _1n); + } + static toAffineBatch(points) { + const toInv = invertBatch(points.map((p) => p.z)); + return points.map((p, i) => p.toAffine(toInv[i])); + } + static normalizeZ(points) { + return JacobianPoint.toAffineBatch(points).map(JacobianPoint.fromAffine); + } + equals(other) { + if (!(other instanceof JacobianPoint)) + throw new TypeError('JacobianPoint expected'); + const { x: X1, y: Y1, z: Z1 } = this; + const { x: X2, y: Y2, z: Z2 } = other; + const Z1Z1 = mod(Z1 * Z1); + const Z2Z2 = mod(Z2 * Z2); + const U1 = mod(X1 * Z2Z2); + const U2 = mod(X2 * Z1Z1); + const S1 = mod(mod(Y1 * Z2) * Z2Z2); + const S2 = mod(mod(Y2 * Z1) * Z1Z1); + return U1 === U2 && S1 === S2; + } + negate() { + return new JacobianPoint(this.x, mod(-this.y), this.z); + } + double() { + const { x: X1, y: Y1, z: Z1 } = this; + const A = mod(X1 * X1); + const B = mod(Y1 * Y1); + const C = mod(B * B); + const x1b = X1 + B; + const D = mod(_2n * (mod(x1b * x1b) - A - C)); + const E = mod(_3n * A); + const F = mod(E * E); + const X3 = mod(F - _2n * D); + const Y3 = mod(E * (D - X3) - _8n * C); + const Z3 = mod(_2n * Y1 * Z1); + return new JacobianPoint(X3, Y3, Z3); + } + add(other) { + if (!(other instanceof JacobianPoint)) + throw new TypeError('JacobianPoint expected'); + const { x: X1, y: Y1, z: Z1 } = this; + const { x: X2, y: Y2, z: Z2 } = other; + if (X2 === _0n || Y2 === _0n) + return this; + if (X1 === _0n || Y1 === _0n) + return other; + const Z1Z1 = mod(Z1 * Z1); + const Z2Z2 = mod(Z2 * Z2); + const U1 = mod(X1 * Z2Z2); + const U2 = mod(X2 * Z1Z1); + const S1 = mod(mod(Y1 * Z2) * Z2Z2); + const S2 = mod(mod(Y2 * Z1) * Z1Z1); + const H = mod(U2 - U1); + const r = mod(S2 - S1); + if (H === _0n) { + if (r === _0n) { + return this.double(); + } + else { + return JacobianPoint.ZERO; + } + } + const HH = mod(H * H); + const HHH = mod(H * HH); + const V = mod(U1 * HH); + const X3 = mod(r * r - HHH - _2n * V); + const Y3 = mod(r * (V - X3) - S1 * HHH); + const Z3 = mod(Z1 * Z2 * H); + return new JacobianPoint(X3, Y3, Z3); + } + subtract(other) { + return this.add(other.negate()); + } + multiplyUnsafe(scalar) { + const P0 = JacobianPoint.ZERO; + if (typeof scalar === 'bigint' && scalar === _0n) + return P0; + let n = normalizeScalar(scalar); + if (n === _1n) + return this; + if (!USE_ENDOMORPHISM) { + let p = P0; + let d = this; + while (n > _0n) { + if (n & _1n) + p = p.add(d); + d = d.double(); + n >>= _1n; + } + return p; + } + let { k1neg, k1, k2neg, k2 } = splitScalarEndo(n); + let k1p = P0; + let k2p = P0; + let d = this; + while (k1 > _0n || k2 > _0n) { + if (k1 & _1n) + k1p = k1p.add(d); + if (k2 & _1n) + k2p = k2p.add(d); + d = d.double(); + k1 >>= _1n; + k2 >>= _1n; + } + if (k1neg) + k1p = k1p.negate(); + if (k2neg) + k2p = k2p.negate(); + k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z); + return k1p.add(k2p); + } + precomputeWindow(W) { + const windows = USE_ENDOMORPHISM ? 128 / W + 1 : 256 / W + 1; + const points = []; + let p = this; + let base = p; + for (let window = 0; window < windows; window++) { + base = p; + points.push(base); + for (let i = 1; i < 2 ** (W - 1); i++) { + base = base.add(p); + points.push(base); + } + p = base.double(); + } + return points; + } + wNAF(n, affinePoint) { + if (!affinePoint && this.equals(JacobianPoint.BASE)) + affinePoint = Point.BASE; + const W = (affinePoint && affinePoint._WINDOW_SIZE) || 1; + if (256 % W) { + throw new Error('Point#wNAF: Invalid precomputation window, must be power of 2'); + } + let precomputes = affinePoint && pointPrecomputes.get(affinePoint); + if (!precomputes) { + precomputes = this.precomputeWindow(W); + if (affinePoint && W !== 1) { + precomputes = JacobianPoint.normalizeZ(precomputes); + pointPrecomputes.set(affinePoint, precomputes); + } + } + let p = JacobianPoint.ZERO; + let f = JacobianPoint.ZERO; + const windows = 1 + (USE_ENDOMORPHISM ? 128 / W : 256 / W); + const windowSize = 2 ** (W - 1); + const mask = BigInt(2 ** W - 1); + const maxNumber = 2 ** W; + const shiftBy = BigInt(W); + for (let window = 0; window < windows; window++) { + const offset = window * windowSize; + let wbits = Number(n & mask); + n >>= shiftBy; + if (wbits > windowSize) { + wbits -= maxNumber; + n += _1n; + } + if (wbits === 0) { + let pr = precomputes[offset]; + if (window % 2) + pr = pr.negate(); + f = f.add(pr); + } + else { + let cached = precomputes[offset + Math.abs(wbits) - 1]; + if (wbits < 0) + cached = cached.negate(); + p = p.add(cached); + } + } + return { p, f }; + } + multiply(scalar, affinePoint) { + let n = normalizeScalar(scalar); + let point; + let fake; + if (USE_ENDOMORPHISM) { + const { k1neg, k1, k2neg, k2 } = splitScalarEndo(n); + let { p: k1p, f: f1p } = this.wNAF(k1, affinePoint); + let { p: k2p, f: f2p } = this.wNAF(k2, affinePoint); + if (k1neg) + k1p = k1p.negate(); + if (k2neg) + k2p = k2p.negate(); + k2p = new JacobianPoint(mod(k2p.x * CURVE.beta), k2p.y, k2p.z); + point = k1p.add(k2p); + fake = f1p.add(f2p); + } + else { + const { p, f } = this.wNAF(n, affinePoint); + point = p; + fake = f; + } + return JacobianPoint.normalizeZ([point, fake])[0]; + } + toAffine(invZ = invert(this.z)) { + const { x, y, z } = this; + const iz1 = invZ; + const iz2 = mod(iz1 * iz1); + const iz3 = mod(iz2 * iz1); + const ax = mod(x * iz2); + const ay = mod(y * iz3); + const zz = mod(z * iz1); + if (zz !== _1n) + throw new Error('invZ was invalid'); + return new Point(ax, ay); + } + } + JacobianPoint.BASE = new JacobianPoint(CURVE.Gx, CURVE.Gy, _1n); + JacobianPoint.ZERO = new JacobianPoint(_0n, _1n, _0n); + const pointPrecomputes = new WeakMap(); + class Point { + constructor(x, y) { + this.x = x; + this.y = y; + } + _setWindowSize(windowSize) { + this._WINDOW_SIZE = windowSize; + pointPrecomputes.delete(this); + } + hasEvenY() { + return this.y % _2n === _0n; + } + static fromCompressedHex(bytes) { + const isShort = bytes.length === 32; + const x = bytesToNumber(isShort ? bytes : bytes.subarray(1)); + if (!isValidFieldElement(x)) + throw new Error('Point is not on curve'); + const y2 = weistrass(x); + let y = sqrtMod(y2); + const isYOdd = (y & _1n) === _1n; + if (isShort) { + if (isYOdd) + y = mod(-y); + } + else { + const isFirstByteOdd = (bytes[0] & 1) === 1; + if (isFirstByteOdd !== isYOdd) + y = mod(-y); + } + const point = new Point(x, y); + point.assertValidity(); + return point; + } + static fromUncompressedHex(bytes) { + const x = bytesToNumber(bytes.subarray(1, 33)); + const y = bytesToNumber(bytes.subarray(33, 65)); + const point = new Point(x, y); + point.assertValidity(); + return point; + } + static fromHex(hex) { + const bytes = ensureBytes(hex); + const len = bytes.length; + const header = bytes[0]; + if (len === 32 || (len === 33 && (header === 0x02 || header === 0x03))) { + return this.fromCompressedHex(bytes); + } + if (len === 65 && header === 0x04) + return this.fromUncompressedHex(bytes); + throw new Error(`Point.fromHex: received invalid point. Expected 32-33 compressed bytes or 65 uncompressed bytes, not ${len}`); + } + static fromPrivateKey(privateKey) { + return Point.BASE.multiply(normalizePrivateKey(privateKey)); + } + static fromSignature(msgHash, signature, recovery) { + msgHash = ensureBytes(msgHash); + const h = truncateHash(msgHash); + const { r, s } = normalizeSignature(signature); + if (recovery !== 0 && recovery !== 1) { + throw new Error('Cannot recover signature: invalid recovery bit'); + } + const prefix = recovery & 1 ? '03' : '02'; + const R = Point.fromHex(prefix + numTo32bStr(r)); + const { n } = CURVE; + const rinv = invert(r, n); + const u1 = mod(-h * rinv, n); + const u2 = mod(s * rinv, n); + const Q = Point.BASE.multiplyAndAddUnsafe(R, u1, u2); + if (!Q) + throw new Error('Cannot recover signature: point at infinify'); + Q.assertValidity(); + return Q; + } + toRawBytes(isCompressed = false) { + return hexToBytes(this.toHex(isCompressed)); + } + toHex(isCompressed = false) { + const x = numTo32bStr(this.x); + if (isCompressed) { + const prefix = this.hasEvenY() ? '02' : '03'; + return `${prefix}${x}`; + } + else { + return `04${x}${numTo32bStr(this.y)}`; + } + } + toHexX() { + return this.toHex(true).slice(2); + } + toRawX() { + return this.toRawBytes(true).slice(1); + } + assertValidity() { + const msg = 'Point is not on elliptic curve'; + const { x, y } = this; + if (!isValidFieldElement(x) || !isValidFieldElement(y)) + throw new Error(msg); + const left = mod(y * y); + const right = weistrass(x); + if (mod(left - right) !== _0n) + throw new Error(msg); + } + equals(other) { + return this.x === other.x && this.y === other.y; + } + negate() { + return new Point(this.x, mod(-this.y)); + } + double() { + return JacobianPoint.fromAffine(this).double().toAffine(); + } + add(other) { + return JacobianPoint.fromAffine(this).add(JacobianPoint.fromAffine(other)).toAffine(); + } + subtract(other) { + return this.add(other.negate()); + } + multiply(scalar) { + return JacobianPoint.fromAffine(this).multiply(scalar, this).toAffine(); + } + multiplyAndAddUnsafe(Q, a, b) { + const P = JacobianPoint.fromAffine(this); + const aP = a === _0n || a === _1n || this !== Point.BASE ? P.multiplyUnsafe(a) : P.multiply(a); + const bQ = JacobianPoint.fromAffine(Q).multiplyUnsafe(b); + const sum = aP.add(bQ); + return sum.equals(JacobianPoint.ZERO) ? undefined : sum.toAffine(); + } + } + Point.BASE = new Point(CURVE.Gx, CURVE.Gy); + Point.ZERO = new Point(_0n, _0n); + function sliceDER(s) { + return Number.parseInt(s[0], 16) >= 8 ? '00' + s : s; + } + function parseDERInt(data) { + if (data.length < 2 || data[0] !== 0x02) { + throw new Error(`Invalid signature integer tag: ${bytesToHex(data)}`); + } + const len = data[1]; + const res = data.subarray(2, len + 2); + if (!len || res.length !== len) { + throw new Error(`Invalid signature integer: wrong length`); + } + if (res[0] === 0x00 && res[1] <= 0x7f) { + throw new Error('Invalid signature integer: trailing length'); + } + return { data: bytesToNumber(res), left: data.subarray(len + 2) }; + } + function parseDERSignature(data) { + if (data.length < 2 || data[0] != 0x30) { + throw new Error(`Invalid signature tag: ${bytesToHex(data)}`); + } + if (data[1] !== data.length - 2) { + throw new Error('Invalid signature: incorrect length'); + } + const { data: r, left: sBytes } = parseDERInt(data.subarray(2)); + const { data: s, left: rBytesLeft } = parseDERInt(sBytes); + if (rBytesLeft.length) { + throw new Error(`Invalid signature: left bytes after parsing: ${bytesToHex(rBytesLeft)}`); + } + return { r, s }; + } + class Signature { + constructor(r, s) { + this.r = r; + this.s = s; + this.assertValidity(); + } + static fromCompact(hex) { + const arr = hex instanceof Uint8Array; + const name = 'Signature.fromCompact'; + if (typeof hex !== 'string' && !arr) + throw new TypeError(`${name}: Expected string or Uint8Array`); + const str = arr ? bytesToHex(hex) : hex; + if (str.length !== 128) + throw new Error(`${name}: Expected 64-byte hex`); + return new Signature(hexToNumber(str.slice(0, 64)), hexToNumber(str.slice(64, 128))); + } + static fromDER(hex) { + const arr = hex instanceof Uint8Array; + if (typeof hex !== 'string' && !arr) + throw new TypeError(`Signature.fromDER: Expected string or Uint8Array`); + const { r, s } = parseDERSignature(arr ? hex : hexToBytes(hex)); + return new Signature(r, s); + } + static fromHex(hex) { + return this.fromDER(hex); + } + assertValidity() { + const { r, s } = this; + if (!isWithinCurveOrder(r)) + throw new Error('Invalid Signature: r must be 0 < r < n'); + if (!isWithinCurveOrder(s)) + throw new Error('Invalid Signature: s must be 0 < s < n'); + } + hasHighS() { + const HALF = CURVE.n >> _1n; + return this.s > HALF; + } + normalizeS() { + return this.hasHighS() ? new Signature(this.r, CURVE.n - this.s) : this; + } + toDERRawBytes(isCompressed = false) { + return hexToBytes(this.toDERHex(isCompressed)); + } + toDERHex(isCompressed = false) { + const sHex = sliceDER(numberToHexUnpadded(this.s)); + if (isCompressed) + return sHex; + const rHex = sliceDER(numberToHexUnpadded(this.r)); + const rLen = numberToHexUnpadded(rHex.length / 2); + const sLen = numberToHexUnpadded(sHex.length / 2); + const length = numberToHexUnpadded(rHex.length / 2 + sHex.length / 2 + 4); + return `30${length}02${rLen}${rHex}02${sLen}${sHex}`; + } + toRawBytes() { + return this.toDERRawBytes(); + } + toHex() { + return this.toDERHex(); + } + toCompactRawBytes() { + return hexToBytes(this.toCompactHex()); + } + toCompactHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s); + } + } + function concatBytes(...arrays) { + if (!arrays.every((b) => b instanceof Uint8Array)) + throw new Error('Uint8Array list expected'); + if (arrays.length === 1) + return arrays[0]; + const length = arrays.reduce((a, arr) => a + arr.length, 0); + const result = new Uint8Array(length); + for (let i = 0, pad = 0; i < arrays.length; i++) { + const arr = arrays[i]; + result.set(arr, pad); + pad += arr.length; + } + return result; + } + const hexes = Array.from({ length: 256 }, (v, i) => i.toString(16).padStart(2, '0')); + function bytesToHex(uint8a) { + if (!(uint8a instanceof Uint8Array)) + throw new Error('Expected Uint8Array'); + let hex = ''; + for (let i = 0; i < uint8a.length; i++) { + hex += hexes[uint8a[i]]; + } + return hex; + } + const POW_2_256 = BigInt('0x10000000000000000000000000000000000000000000000000000000000000000'); + function numTo32bStr(num) { + if (typeof num !== 'bigint') + throw new Error('Expected bigint'); + if (!(_0n <= num && num < POW_2_256)) + throw new Error('Expected number < 2^256'); + return num.toString(16).padStart(64, '0'); + } + function numTo32b(num) { + const b = hexToBytes(numTo32bStr(num)); + if (b.length !== 32) + throw new Error('Error: expected 32 bytes'); + return b; + } + function numberToHexUnpadded(num) { + const hex = num.toString(16); + return hex.length & 1 ? `0${hex}` : hex; + } + function hexToNumber(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToNumber: expected string, got ' + typeof hex); + } + return BigInt(`0x${hex}`); + } + function hexToBytes(hex) { + if (typeof hex !== 'string') { + throw new TypeError('hexToBytes: expected string, got ' + typeof hex); + } + if (hex.length % 2) + throw new Error('hexToBytes: received invalid unpadded hex' + hex.length); + const array = new Uint8Array(hex.length / 2); + for (let i = 0; i < array.length; i++) { + const j = i * 2; + const hexByte = hex.slice(j, j + 2); + const byte = Number.parseInt(hexByte, 16); + if (Number.isNaN(byte) || byte < 0) + throw new Error('Invalid byte sequence'); + array[i] = byte; + } + return array; + } + function bytesToNumber(bytes) { + return hexToNumber(bytesToHex(bytes)); + } + function ensureBytes(hex) { + return hex instanceof Uint8Array ? Uint8Array.from(hex) : hexToBytes(hex); + } + function normalizeScalar(num) { + if (typeof num === 'number' && Number.isSafeInteger(num) && num > 0) + return BigInt(num); + if (typeof num === 'bigint' && isWithinCurveOrder(num)) + return num; + throw new TypeError('Expected valid private scalar: 0 < scalar < curve.n'); + } + function mod(a, b = CURVE.P) { + const result = a % b; + return result >= _0n ? result : b + result; + } + function pow2(x, power) { + const { P } = CURVE; + let res = x; + while (power-- > _0n) { + res *= res; + res %= P; + } + return res; + } + function sqrtMod(x) { + const { P } = CURVE; + const _6n = BigInt(6); + const _11n = BigInt(11); + const _22n = BigInt(22); + const _23n = BigInt(23); + const _44n = BigInt(44); + const _88n = BigInt(88); + const b2 = (x * x * x) % P; + const b3 = (b2 * b2 * x) % P; + const b6 = (pow2(b3, _3n) * b3) % P; + const b9 = (pow2(b6, _3n) * b3) % P; + const b11 = (pow2(b9, _2n) * b2) % P; + const b22 = (pow2(b11, _11n) * b11) % P; + const b44 = (pow2(b22, _22n) * b22) % P; + const b88 = (pow2(b44, _44n) * b44) % P; + const b176 = (pow2(b88, _88n) * b88) % P; + const b220 = (pow2(b176, _44n) * b44) % P; + const b223 = (pow2(b220, _3n) * b3) % P; + const t1 = (pow2(b223, _23n) * b22) % P; + const t2 = (pow2(t1, _6n) * b2) % P; + return pow2(t2, _2n); + } + function invert(number, modulo = CURVE.P) { + if (number === _0n || modulo <= _0n) { + throw new Error(`invert: expected positive integers, got n=${number} mod=${modulo}`); + } + let a = mod(number, modulo); + let b = modulo; + let x = _0n, u = _1n; + while (a !== _0n) { + const q = b / a; + const r = b % a; + const m = x - u * q; + b = a, a = r, x = u, u = m; + } + const gcd = b; + if (gcd !== _1n) + throw new Error('invert: does not exist'); + return mod(x, modulo); + } + function invertBatch(nums, p = CURVE.P) { + const scratch = new Array(nums.length); + const lastMultiplied = nums.reduce((acc, num, i) => { + if (num === _0n) + return acc; + scratch[i] = acc; + return mod(acc * num, p); + }, _1n); + const inverted = invert(lastMultiplied, p); + nums.reduceRight((acc, num, i) => { + if (num === _0n) + return acc; + scratch[i] = mod(acc * scratch[i], p); + return mod(acc * num, p); + }, inverted); + return scratch; + } + const divNearest = (a, b) => (a + b / _2n) / b; + const ENDO = { + a1: BigInt('0x3086d221a7d46bcde86c90e49284eb15'), + b1: -_1n * BigInt('0xe4437ed6010e88286f547fa90abfe4c3'), + a2: BigInt('0x114ca50f7a8e2f3f657c1108d9d44cfd8'), + b2: BigInt('0x3086d221a7d46bcde86c90e49284eb15'), + POW_2_128: BigInt('0x100000000000000000000000000000000'), + }; + function splitScalarEndo(k) { + const { n } = CURVE; + const { a1, b1, a2, b2, POW_2_128 } = ENDO; + const c1 = divNearest(b2 * k, n); + const c2 = divNearest(-b1 * k, n); + let k1 = mod(k - c1 * a1 - c2 * a2, n); + let k2 = mod(-c1 * b1 - c2 * b2, n); + const k1neg = k1 > POW_2_128; + const k2neg = k2 > POW_2_128; + if (k1neg) + k1 = n - k1; + if (k2neg) + k2 = n - k2; + if (k1 > POW_2_128 || k2 > POW_2_128) { + throw new Error('splitScalarEndo: Endomorphism failed, k=' + k); + } + return { k1neg, k1, k2neg, k2 }; + } + function truncateHash(hash) { + const { n } = CURVE; + const byteLength = hash.length; + const delta = byteLength * 8 - 256; + let h = bytesToNumber(hash); + if (delta > 0) + h = h >> BigInt(delta); + if (h >= n) + h -= n; + return h; + } + let _sha256Sync; + let _hmacSha256Sync; + class HmacDrbg { + constructor() { + this.v = new Uint8Array(32).fill(1); + this.k = new Uint8Array(32).fill(0); + this.counter = 0; + } + hmac(...values) { + return utils.hmacSha256(this.k, ...values); + } + hmacSync(...values) { + return _hmacSha256Sync(this.k, ...values); + } + checkSync() { + if (typeof _hmacSha256Sync !== 'function') + throw new ShaError('hmacSha256Sync needs to be set'); + } + incr() { + if (this.counter >= 1000) + throw new Error('Tried 1,000 k values for sign(), all were invalid'); + this.counter += 1; + } + async reseed(seed = new Uint8Array()) { + this.k = await this.hmac(this.v, Uint8Array.from([0x00]), seed); + this.v = await this.hmac(this.v); + if (seed.length === 0) + return; + this.k = await this.hmac(this.v, Uint8Array.from([0x01]), seed); + this.v = await this.hmac(this.v); + } + reseedSync(seed = new Uint8Array()) { + this.checkSync(); + this.k = this.hmacSync(this.v, Uint8Array.from([0x00]), seed); + this.v = this.hmacSync(this.v); + if (seed.length === 0) + return; + this.k = this.hmacSync(this.v, Uint8Array.from([0x01]), seed); + this.v = this.hmacSync(this.v); + } + async generate() { + this.incr(); + this.v = await this.hmac(this.v); + return this.v; + } + generateSync() { + this.checkSync(); + this.incr(); + this.v = this.hmacSync(this.v); + return this.v; + } + } + function isWithinCurveOrder(num) { + return _0n < num && num < CURVE.n; + } + function isValidFieldElement(num) { + return _0n < num && num < CURVE.P; + } + function kmdToSig(kBytes, m, d) { + const k = bytesToNumber(kBytes); + if (!isWithinCurveOrder(k)) + return; + const { n } = CURVE; + const q = Point.BASE.multiply(k); + const r = mod(q.x, n); + if (r === _0n) + return; + const s = mod(invert(k, n) * mod(m + d * r, n), n); + if (s === _0n) + return; + const sig = new Signature(r, s); + const recovery = (q.x === sig.r ? 0 : 2) | Number(q.y & _1n); + return { sig, recovery }; + } + function normalizePrivateKey(key) { + let num; + if (typeof key === 'bigint') { + num = key; + } + else if (typeof key === 'number' && Number.isSafeInteger(key) && key > 0) { + num = BigInt(key); + } + else if (typeof key === 'string') { + if (key.length !== 64) + throw new Error('Expected 32 bytes of private key'); + num = hexToNumber(key); + } + else if (key instanceof Uint8Array) { + if (key.length !== 32) + throw new Error('Expected 32 bytes of private key'); + num = bytesToNumber(key); + } + else { + throw new TypeError('Expected valid private key'); + } + if (!isWithinCurveOrder(num)) + throw new Error('Expected private key: 0 < key < n'); + return num; + } + function normalizePublicKey(publicKey) { + if (publicKey instanceof Point) { + publicKey.assertValidity(); + return publicKey; + } + else { + return Point.fromHex(publicKey); + } + } + function normalizeSignature(signature) { + if (signature instanceof Signature) { + signature.assertValidity(); + return signature; + } + try { + return Signature.fromDER(signature); + } + catch (error) { + return Signature.fromCompact(signature); + } + } + function getPublicKey(privateKey, isCompressed = false) { + return Point.fromPrivateKey(privateKey).toRawBytes(isCompressed); + } + function recoverPublicKey(msgHash, signature, recovery, isCompressed = false) { + return Point.fromSignature(msgHash, signature, recovery).toRawBytes(isCompressed); + } + function isProbPub(item) { + const arr = item instanceof Uint8Array; + const str = typeof item === 'string'; + const len = (arr || str) && item.length; + if (arr) + return len === 33 || len === 65; + if (str) + return len === 66 || len === 130; + if (item instanceof Point) + return true; + return false; + } + function getSharedSecret(privateA, publicB, isCompressed = false) { + if (isProbPub(privateA)) + throw new TypeError('getSharedSecret: first arg must be private key'); + if (!isProbPub(publicB)) + throw new TypeError('getSharedSecret: second arg must be public key'); + const b = normalizePublicKey(publicB); + b.assertValidity(); + return b.multiply(normalizePrivateKey(privateA)).toRawBytes(isCompressed); + } + function bits2int(bytes) { + const slice = bytes.length > 32 ? bytes.slice(0, 32) : bytes; + return bytesToNumber(slice); + } + function bits2octets(bytes) { + const z1 = bits2int(bytes); + const z2 = mod(z1, CURVE.n); + return int2octets(z2 < _0n ? z1 : z2); + } + function int2octets(num) { + return numTo32b(num); + } + function initSigArgs(msgHash, privateKey, extraEntropy) { + if (msgHash == null) + throw new Error(`sign: expected valid message hash, not "${msgHash}"`); + const h1 = ensureBytes(msgHash); + const d = normalizePrivateKey(privateKey); + const seedArgs = [int2octets(d), bits2octets(h1)]; + if (extraEntropy != null) { + if (extraEntropy === true) + extraEntropy = utils.randomBytes(32); + const e = ensureBytes(extraEntropy); + if (e.length !== 32) + throw new Error('sign: Expected 32 bytes of extra data'); + seedArgs.push(e); + } + const seed = concatBytes(...seedArgs); + const m = bits2int(h1); + return { seed, m, d }; + } + function finalizeSig(recSig, opts) { + let { sig, recovery } = recSig; + const { canonical, der, recovered } = Object.assign({ canonical: true, der: true }, opts); + if (canonical && sig.hasHighS()) { + sig = sig.normalizeS(); + recovery ^= 1; + } + const hashed = der ? sig.toDERRawBytes() : sig.toCompactRawBytes(); + return recovered ? [hashed, recovery] : hashed; + } + async function sign(msgHash, privKey, opts = {}) { + const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy); + let sig; + const drbg = new HmacDrbg(); + await drbg.reseed(seed); + while (!(sig = kmdToSig(await drbg.generate(), m, d))) + await drbg.reseed(); + return finalizeSig(sig, opts); + } + function signSync(msgHash, privKey, opts = {}) { + const { seed, m, d } = initSigArgs(msgHash, privKey, opts.extraEntropy); + let sig; + const drbg = new HmacDrbg(); + drbg.reseedSync(seed); + while (!(sig = kmdToSig(drbg.generateSync(), m, d))) + drbg.reseedSync(); + return finalizeSig(sig, opts); + } + const vopts = { strict: true }; + function verify(signature, msgHash, publicKey, opts = vopts) { + let sig; + try { + sig = normalizeSignature(signature); + msgHash = ensureBytes(msgHash); + } + catch (error) { + return false; + } + const { r, s } = sig; + if (opts.strict && sig.hasHighS()) + return false; + const h = truncateHash(msgHash); + let P; + try { + P = normalizePublicKey(publicKey); + } + catch (error) { + return false; + } + const { n } = CURVE; + const sinv = invert(s, n); + const u1 = mod(h * sinv, n); + const u2 = mod(r * sinv, n); + const R = Point.BASE.multiplyAndAddUnsafe(P, u1, u2); + if (!R) + return false; + const v = mod(R.x, n); + return v === r; + } + function schnorrChallengeFinalize(ch) { + return mod(bytesToNumber(ch), CURVE.n); + } + class SchnorrSignature { + constructor(r, s) { + this.r = r; + this.s = s; + this.assertValidity(); + } + static fromHex(hex) { + const bytes = ensureBytes(hex); + if (bytes.length !== 64) + throw new TypeError(`SchnorrSignature.fromHex: expected 64 bytes, not ${bytes.length}`); + const r = bytesToNumber(bytes.subarray(0, 32)); + const s = bytesToNumber(bytes.subarray(32, 64)); + return new SchnorrSignature(r, s); + } + assertValidity() { + const { r, s } = this; + if (!isValidFieldElement(r) || !isWithinCurveOrder(s)) + throw new Error('Invalid signature'); + } + toHex() { + return numTo32bStr(this.r) + numTo32bStr(this.s); + } + toRawBytes() { + return hexToBytes(this.toHex()); + } + } + function schnorrGetPublicKey(privateKey) { + return Point.fromPrivateKey(privateKey).toRawX(); + } + class InternalSchnorrSignature { + constructor(message, privateKey, auxRand = utils.randomBytes()) { + if (message == null) + throw new TypeError(`sign: Expected valid message, not "${message}"`); + this.m = ensureBytes(message); + const { x, scalar } = this.getScalar(normalizePrivateKey(privateKey)); + this.px = x; + this.d = scalar; + this.rand = ensureBytes(auxRand); + if (this.rand.length !== 32) + throw new TypeError('sign: Expected 32 bytes of aux randomness'); + } + getScalar(priv) { + const point = Point.fromPrivateKey(priv); + const scalar = point.hasEvenY() ? priv : CURVE.n - priv; + return { point, scalar, x: point.toRawX() }; + } + initNonce(d, t0h) { + return numTo32b(d ^ bytesToNumber(t0h)); + } + finalizeNonce(k0h) { + const k0 = mod(bytesToNumber(k0h), CURVE.n); + if (k0 === _0n) + throw new Error('sign: Creation of signature failed. k is zero'); + const { point: R, x: rx, scalar: k } = this.getScalar(k0); + return { R, rx, k }; + } + finalizeSig(R, k, e, d) { + return new SchnorrSignature(R.x, mod(k + e * d, CURVE.n)).toRawBytes(); + } + error() { + throw new Error('sign: Invalid signature produced'); + } + async calc() { + const { m, d, px, rand } = this; + const tag = utils.taggedHash; + const t = this.initNonce(d, await tag(TAGS.aux, rand)); + const { R, rx, k } = this.finalizeNonce(await tag(TAGS.nonce, t, px, m)); + const e = schnorrChallengeFinalize(await tag(TAGS.challenge, rx, px, m)); + const sig = this.finalizeSig(R, k, e, d); + if (!(await schnorrVerify(sig, m, px))) + this.error(); + return sig; + } + calcSync() { + const { m, d, px, rand } = this; + const tag = utils.taggedHashSync; + const t = this.initNonce(d, tag(TAGS.aux, rand)); + const { R, rx, k } = this.finalizeNonce(tag(TAGS.nonce, t, px, m)); + const e = schnorrChallengeFinalize(tag(TAGS.challenge, rx, px, m)); + const sig = this.finalizeSig(R, k, e, d); + if (!schnorrVerifySync(sig, m, px)) + this.error(); + return sig; + } + } + async function schnorrSign(msg, privKey, auxRand) { + return new InternalSchnorrSignature(msg, privKey, auxRand).calc(); + } + function schnorrSignSync(msg, privKey, auxRand) { + return new InternalSchnorrSignature(msg, privKey, auxRand).calcSync(); + } + function initSchnorrVerify(signature, message, publicKey) { + const raw = signature instanceof SchnorrSignature; + const sig = raw ? signature : SchnorrSignature.fromHex(signature); + if (raw) + sig.assertValidity(); + return { + ...sig, + m: ensureBytes(message), + P: normalizePublicKey(publicKey), + }; + } + function finalizeSchnorrVerify(r, P, s, e) { + const R = Point.BASE.multiplyAndAddUnsafe(P, normalizePrivateKey(s), mod(-e, CURVE.n)); + if (!R || !R.hasEvenY() || R.x !== r) + return false; + return true; + } + async function schnorrVerify(signature, message, publicKey) { + try { + const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey); + const e = schnorrChallengeFinalize(await utils.taggedHash(TAGS.challenge, numTo32b(r), P.toRawX(), m)); + return finalizeSchnorrVerify(r, P, s, e); + } + catch (error) { + return false; + } + } + function schnorrVerifySync(signature, message, publicKey) { + try { + const { r, s, m, P } = initSchnorrVerify(signature, message, publicKey); + const e = schnorrChallengeFinalize(utils.taggedHashSync(TAGS.challenge, numTo32b(r), P.toRawX(), m)); + return finalizeSchnorrVerify(r, P, s, e); + } + catch (error) { + if (error instanceof ShaError) + throw error; + return false; + } + } + const schnorr = { + Signature: SchnorrSignature, + getPublicKey: schnorrGetPublicKey, + sign: schnorrSign, + verify: schnorrVerify, + signSync: schnorrSignSync, + verifySync: schnorrVerifySync, + }; + Point.BASE._setWindowSize(8); + const crypto = { + node: nodeCrypto, + web: typeof self === 'object' && 'crypto' in self ? self.crypto : undefined, + }; + const TAGS = { + challenge: 'BIP0340/challenge', + aux: 'BIP0340/aux', + nonce: 'BIP0340/nonce', + }; + const TAGGED_HASH_PREFIXES = {}; + const utils = { + bytesToHex, + hexToBytes, + concatBytes, + mod, + invert, + isValidPrivateKey(privateKey) { + try { + normalizePrivateKey(privateKey); + return true; + } + catch (error) { + return false; + } + }, + _bigintTo32Bytes: numTo32b, + _normalizePrivateKey: normalizePrivateKey, + hashToPrivateKey: (hash) => { + hash = ensureBytes(hash); + if (hash.length < 40 || hash.length > 1024) + throw new Error('Expected 40-1024 bytes of private key as per FIPS 186'); + const num = mod(bytesToNumber(hash), CURVE.n - _1n) + _1n; + return numTo32b(num); + }, + randomBytes: (bytesLength = 32) => { + if (crypto.web) { + return crypto.web.getRandomValues(new Uint8Array(bytesLength)); + } + else if (crypto.node) { + const { randomBytes } = crypto.node; + return Uint8Array.from(randomBytes(bytesLength)); + } + else { + throw new Error("The environment doesn't have randomBytes function"); + } + }, + randomPrivateKey: () => { + return utils.hashToPrivateKey(utils.randomBytes(40)); + }, + sha256: async (...messages) => { + if (crypto.web) { + const buffer = await crypto.web.subtle.digest('SHA-256', concatBytes(...messages)); + return new Uint8Array(buffer); + } + else if (crypto.node) { + const { createHash } = crypto.node; + const hash = createHash('sha256'); + messages.forEach((m) => hash.update(m)); + return Uint8Array.from(hash.digest()); + } + else { + throw new Error("The environment doesn't have sha256 function"); + } + }, + hmacSha256: async (key, ...messages) => { + if (crypto.web) { + const ckey = await crypto.web.subtle.importKey('raw', key, { name: 'HMAC', hash: { name: 'SHA-256' } }, false, ['sign']); + const message = concatBytes(...messages); + const buffer = await crypto.web.subtle.sign('HMAC', ckey, message); + return new Uint8Array(buffer); + } + else if (crypto.node) { + const { createHmac } = crypto.node; + const hash = createHmac('sha256', key); + messages.forEach((m) => hash.update(m)); + return Uint8Array.from(hash.digest()); + } + else { + throw new Error("The environment doesn't have hmac-sha256 function"); + } + }, + sha256Sync: undefined, + hmacSha256Sync: undefined, + taggedHash: async (tag, ...messages) => { + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === undefined) { + const tagH = await utils.sha256(Uint8Array.from(tag, (c) => c.charCodeAt(0))); + tagP = concatBytes(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return utils.sha256(tagP, ...messages); + }, + taggedHashSync: (tag, ...messages) => { + if (typeof _sha256Sync !== 'function') + throw new ShaError('sha256Sync is undefined, you need to set it'); + let tagP = TAGGED_HASH_PREFIXES[tag]; + if (tagP === undefined) { + const tagH = _sha256Sync(Uint8Array.from(tag, (c) => c.charCodeAt(0))); + tagP = concatBytes(tagH, tagH); + TAGGED_HASH_PREFIXES[tag] = tagP; + } + return _sha256Sync(tagP, ...messages); + }, + precompute(windowSize = 8, point = Point.BASE) { + const cached = point === Point.BASE ? point : new Point(point.x, point.y); + cached._setWindowSize(windowSize); + cached.multiply(_3n); + return cached; + }, + }; + Object.defineProperties(utils, { + sha256Sync: { + configurable: false, + get() { + return _sha256Sync; + }, + set(val) { + if (!_sha256Sync) + _sha256Sync = val; + }, + }, + hmacSha256Sync: { + configurable: false, + get() { + return _hmacSha256Sync; + }, + set(val) { + if (!_hmacSha256Sync) + _hmacSha256Sync = val; + }, + }, + }); + + exports.CURVE = CURVE; + exports.Point = Point; + exports.Signature = Signature; + exports.getPublicKey = getPublicKey; + exports.getSharedSecret = getSharedSecret; + exports.recoverPublicKey = recoverPublicKey; + exports.schnorr = schnorr; + exports.sign = sign; + exports.signSync = signSync; + exports.utils = utils; + exports.verify = verify; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/webv2/nostr.js b/webv2/nostr.js @@ -0,0 +1,360 @@ +const nostrjs = (function nostrlib() { +const WS = typeof WebSocket !== 'undefined' ? WebSocket : require('ws') + +function RelayPool(relays, opts) +{ + if (!(this instanceof RelayPool)) + return new RelayPool(relays) + + this.onfn = {} + this.relays = [] + + for (const relay of relays) { + this.add(relay) + } + + return this +} + +RelayPool.prototype.close = function relayPoolClose() { + for (const relay of this.relays) { + relay.close() + } +} + +RelayPool.prototype.on = function relayPoolOn(method, fn) { + for (const relay of this.relays) { + this.onfn[method] = fn + relay.onfn[method] = fn.bind(null, relay) + } +} + +RelayPool.prototype.has = function relayPoolHas(relayUrl) { + for (const relay of this.relays) { + if (relay.url === relayUrl) + return true + } + + return false +} + +RelayPool.prototype.setupHandlers = function relayPoolSetupHandlers() +{ + // setup its message handlers with the ones we have already + const keys = Object.keys(this.onfn) + for (const handler of keys) { + for (const relay of this.relays) { + relay.onfn[handler] = this.onfn[handler].bind(null, relay) + } + } +} + +RelayPool.prototype.remove = function relayPoolRemove(url) { + let i = 0 + + for (const relay of this.relays) { + if (relay.url === url) { + relay.ws && relay.ws.close() + this.relays = this.replays.splice(i, 1) + return true + } + + i += 1 + } + + return false +} + +RelayPool.prototype.subscribe = function relayPoolSubscribe(sub_id, filters, relay_ids) { + const relays = relay_ids ? this.find_relays(relay_ids) : this.relays + for (const relay of relays) { + relay.subscribe(sub_id, filters) + } +} + +RelayPool.prototype.unsubscribe = function relayPoolUnsubscibe(sub_id, relay_ids) { + const relays = relay_ids ? this.find_relays(relay_ids) : this.relays + for (const relay of relays) { + relay.unsubscribe(sub_id) + } +} + +RelayPool.prototype.send = function relayPoolSend(payload, relay_ids) { + const relays = relay_ids ? this.find_relays(relay_ids) : this.relays + for (const relay of relays) { + relay.send(payload) + } +} + +RelayPool.prototype.add = function relayPoolAdd(relay) { + if (relay instanceof Relay) { + if (this.has(relay.url)) + return false + + this.relays.push(relay) + this.setupHandlers() + return true + } + + if (this.has(relay)) + return false + + const r = Relay(relay, this.opts) + this.relays.push(r) + this.setupHandlers() + return true +} + +RelayPool.prototype.find_relays = function relayPoolFindRelays(relay_ids) { + if (relay_ids instanceof Relay) + return [relay_ids] + + if (relay_ids.length === 0) + return [] + + if (!relay_ids[0]) + throw new Error("what!?") + + if (relay_ids[0] instanceof Relay) + return relay_ids + + return this.relays.reduce((acc, relay) => { + if (relay_ids.some((rid) => relay.url === rid)) + acc.push(relay) + return acc + }, []) +} + +Relay.prototype.wait_connected = async function relay_wait_connected(data) { + let retry = 1000 + while (true) { + if (this.ws.readyState !== 1) { + await sleep(retry) + retry *= 1.5 + } + else { + return + } + } +} + + +function Relay(relay, opts={}) +{ + if (!(this instanceof Relay)) + return new Relay(relay, opts) + + this.url = relay + this.opts = opts + + if (opts.reconnect == null) + opts.reconnect = true + + const me = this + me.onfn = {} + + init_websocket(me) + + return this +} + +function init_websocket(me) { + let ws + try { + ws = me.ws = new WS(me.url); + } catch(e) { + return null + } + return new Promise((resolve, reject) => { + let resolved = false + ws.onmessage = (m) => { handle_nostr_message(me, m) } + ws.onclose = () => { + if (me.onfn.close) + me.onfn.close() + if (me.reconnecting) + return reject(new Error("close during reconnect")) + if (!me.manualClose && me.opts.reconnect) + reconnect(me) + } + ws.onerror = () => { + if (me.onfn.error) + me.onfn.error() + if (me.reconnecting) + return reject(new Error("error during reconnect")) + if (me.opts.reconnect) + reconnect(me) + } + ws.onopen = () => { + if (me.onfn.open) + me.onfn.open() + + if (resolved) return + + resolved = true + resolve(me) + } + }); +} + +function sleep(ms) { + return new Promise(resolve => setTimeout(resolve, ms)); +} + +async function reconnect(me) +{ + const reconnecting = true + let n = 100 + try { + me.reconnecting = true + await init_websocket(me) + me.reconnecting = false + } catch { + //console.error(`error thrown during reconnect... trying again in ${n} ms`) + await sleep(n) + n *= 1.5 + } +} + +Relay.prototype.on = function relayOn(method, fn) { + this.onfn[method] = fn +} + +Relay.prototype.close = function relayClose() { + if (this.ws) { + this.manualClose = true + this.ws.close() + } +} + +Relay.prototype.subscribe = function relay_subscribe(sub_id, filters) { + if (Array.isArray(filters)) + this.send(["REQ", sub_id, ...filters]) + else + this.send(["REQ", sub_id, filters]) +} + +Relay.prototype.unsubscribe = function relay_unsubscribe(sub_id) { + this.send(["CLOSE", sub_id]) +} + +Relay.prototype.send = async function relay_send(data) { + await this.wait_connected() + this.ws.send(JSON.stringify(data)) +} + +function handle_nostr_message(relay, msg) +{ + let data + try { + data = JSON.parse(msg.data) + } catch (e) { + console.error("handle_nostr_message", e) + return + } + if (data.length >= 2) { + switch (data[0]) { + case "EVENT": + if (data.length < 3) + return + return relay.onfn.event && relay.onfn.event(data[1], data[2]) + case "EOSE": + return relay.onfn.eose && relay.onfn.eose(data[1]) + case "NOTICE": + return relay.onfn.notice && relay.onfn.notice(...data.slice(1)) + } + } +} + +async function sha256(message) { + if (crypto.subtle) { + const buffer = await crypto.subtle.digest('SHA-256', message); + return new Uint8Array(buffer); + } else if (require) { + const { createHash } = require('crypto'); + const hash = createHash('sha256'); + [message].forEach((m) => hash.update(m)); + return Uint8Array.from(hash.digest()); + } else { + throw new Error("The environment doesn't have sha256 function"); + } +} + +async function calculate_id(ev) { + const commit = event_commitment(ev) + const buf = new TextEncoder().encode(commit); + return hex_encode(await sha256(buf)) +} + +function event_commitment(ev) { + const {pubkey,created_at,kind,tags,content} = ev + return JSON.stringify([0, pubkey, created_at, kind, tags, content]) +} + +function hex_char(val) { + if (val < 10) + return String.fromCharCode(48 + val) + if (val < 16) + return String.fromCharCode(97 + val - 10) +} + +function hex_encode(buf) { + let str = "" + for (let i = 0; i < buf.length; i++) { + const c = buf[i] + str += hex_char(c >> 4) + str += hex_char(c & 0xF) + } + return str +} + +function char_to_hex(cstr) { + const c = cstr.charCodeAt(0) + // c >= 0 && c <= 9 + if (c >= 48 && c <= 57) { + return c - 48; + } + // c >= a && c <= f + if (c >= 97 && c <= 102) { + return c - 97 + 10; + } + // c >= A && c <= F + if (c >= 65 && c <= 70) { + return c - 65 + 10; + } + return -1; +} + + +function hex_decode(str, buflen) +{ + let bufsize = buflen || 33 + let c1, c2 + let i = 0 + let j = 0 + let buf = new Uint8Array(bufsize) + let slen = str.length + while (slen > 1) { + if (-1==(c1 = char_to_hex(str[j])) || -1==(c2 = char_to_hex(str[j+1]))) + return null; + if (!bufsize) + return null; + j += 2 + slen -= 2 + buf[i++] = (c1 << 4) | c2 + bufsize--; + } + + return buf +} + +return { + RelayPool, + calculate_id, + event_commitment, + hex_encode, + hex_decode, +} +})() + +if (typeof module !== 'undefined' && module.exports) + module.exports = nostrjs