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:
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("<","<").replaceAll(">",">")
+}
+
+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