order of functions

This commit is contained in:
Vitor Pamplona 2024-01-28 12:50:44 -05:00
parent 23a501f0e3
commit 0d8073db51

View File

@ -1,434 +1,436 @@
// from https://github.com/paulmillr/noble-secp256k1/blob/main/index.ts#L803 // from https://github.com/paulmillr/noble-secp256k1/blob/main/index.ts#L803
function hexToBytes(hex) { function hexToBytes(hex) {
if (typeof hex !== 'string') { if (typeof hex !== 'string') {
throw new TypeError('hexToBytes: expected string, got ' + typeof hex) throw new TypeError('hexToBytes: expected string, got ' + typeof hex)
} }
if (hex.length % 2) if (hex.length % 2)
throw new Error('hexToBytes: received invalid unpadded hex' + hex.length) throw new Error('hexToBytes: received invalid unpadded hex' + hex.length)
const array = new Uint8Array(hex.length / 2) const array = new Uint8Array(hex.length / 2)
for (let i = 0; i < array.length; i++) { for (let i = 0; i < array.length; i++) {
const j = i * 2 const j = i * 2
const hexByte = hex.slice(j, j + 2) const hexByte = hex.slice(j, j + 2)
const byte = Number.parseInt(hexByte, 16) const byte = Number.parseInt(hexByte, 16)
if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence') if (Number.isNaN(byte) || byte < 0) throw new Error('Invalid byte sequence')
array[i] = byte array[i] = byte
} }
return array return array
}
// decode nip19 ('npub') to hex
const npub2hexa = (npub) => {
let { prefix, words } = bech32.bech32.decode(npub, 90)
if (prefix === 'npub') {
let data = new Uint8Array(bech32.bech32.fromWords(words))
return buffer.Buffer.from(data).toString('hex')
}
}
// encode hex to nip19 ('npub')
const hexa2npub = (hex) => {
const data = hexToBytes(hex)
const words = bech32.bech32.toWords(data)
const prefix = 'npub'
return bech32.bech32.encode(prefix, words, 90)
}
// parse inserted pubkey
const parsePubkey = (pubkey) =>
pubkey.match('npub1') ? npub2hexa(pubkey) : pubkey
// download js file
const downloadFile = (data, fileName) => {
const prettyJs = 'const data = ' + JSON.stringify(data, null, 2)
const tempLink = document.createElement('a')
const taBlob = new Blob([prettyJs], { type: 'text/javascript' })
tempLink.setAttribute('href', URL.createObjectURL(taBlob))
tempLink.setAttribute('download', fileName)
tempLink.click()
}
const updateRelayStatus = (relay, status, addToCount, until, relayStatusAndCount) => {
if (relayStatusAndCount[relay] == undefined) {
relayStatusAndCount[relay] = {}
} }
// decode nip19 ('npub') to hex if (status)
const npub2hexa = (npub) => { relayStatusAndCount[relay].status = status
let { prefix, words } = bech32.bech32.decode(npub, 90)
if (prefix === 'npub') { relayStatusAndCount[relay].until = until
let data = new Uint8Array(bech32.bech32.fromWords(words))
return buffer.Buffer.from(data).toString('hex') if (relayStatusAndCount[relay].count != undefined)
} relayStatusAndCount[relay].count = relayStatusAndCount[relay].count + addToCount
else
relayStatusAndCount[relay].count = addToCount
displayRelayStatus(relayStatusAndCount)
}
const displayRelayStatus = (relayStatusAndCount) => {
if (Object.keys(relayStatusAndCount).length > 0) {
const newText = Object.keys(relayStatusAndCount).map(
it => {
let untilStr = "";
if (relayStatusAndCount[it].until)
untilStr = " <" + new Date(relayStatusAndCount[it].until * 1000).toLocaleDateString("en-US")
const relayName = it.replace("wss://", "").replace("ws://", "")
const line = "<td>" + relayName + "</td><td>" + relayStatusAndCount[it].status + "</td><td>" + untilStr + "</td><td>" + relayStatusAndCount[it].count + "</td>"
return "<tr>" +line+ "</tr>"
}
).join("<br />")
$('#checking-relays').html(newText)
} else {
$('#checking-relays-header').html("")
$('#checking-relays').html("")
} }
}
// encode hex to nip19 ('npub') // fetch events from relay, returns a promise
const hexa2npub = (hex) => { const fetchFromRelay = async (relay, filters, pubkey, events, relayStatus) =>
const data = hexToBytes(hex) new Promise((resolve, reject) => {
const words = bech32.bech32.toWords(data) try {
const prefix = 'npub' updateRelayStatus(relay, "Starting", 0, undefined, relayStatus)
return bech32.bech32.encode(prefix, words, 90) // open websocket
} const ws = new WebSocket(relay)
// parse inserted pubkey // prevent hanging forever
const parsePubkey = (pubkey) => let myTimeout = setTimeout(() => {
pubkey.match('npub1') ? npub2hexa(pubkey) : pubkey ws.close()
reject(relay)
}, 10_000)
// download js file const subscriptions = Object.fromEntries(filters.map ( (filter, index) => {
const downloadFile = (data, fileName) => { let id = "my-sub-"+index
const prettyJs = 'const data = ' + JSON.stringify(data, null, 2)
const tempLink = document.createElement('a')
const taBlob = new Blob([prettyJs], { type: 'text/javascript' })
tempLink.setAttribute('href', URL.createObjectURL(taBlob))
tempLink.setAttribute('download', fileName)
tempLink.click()
}
const updateRelayStatus = (relay, status, addToCount, until, relayStatusAndCount) => { return [
if (relayStatusAndCount[relay] == undefined) { id, {
relayStatusAndCount[relay] = {} id: id,
} counter: 0,
lastEvent: null,
done: false,
filter: filter,
eventIds: new Set()
}
]
}))
if (status) // subscribe to events filtered by author
relayStatusAndCount[relay].status = status ws.onopen = () => {
clearTimeout(myTimeout)
relayStatusAndCount[relay].until = until myTimeout = setTimeout(() => {
if (relayStatusAndCount[relay].count != undefined)
relayStatusAndCount[relay].count = relayStatusAndCount[relay].count + addToCount
else
relayStatusAndCount[relay].count = addToCount
displayRelayStatus(relayStatusAndCount)
}
const displayRelayStatus = (relayStatusAndCount) => {
if (Object.keys(relayStatusAndCount).length > 0) {
const newText = Object.keys(relayStatusAndCount).map(
it => {
let untilStr = "";
if (relayStatusAndCount[it].until)
untilStr = " <" + new Date(relayStatusAndCount[it].until * 1000).toLocaleDateString("en-US")
const relayName = it.replace("wss://", "").replace("ws://", "")
const line = "<td>" + relayName + "</td><td>" + relayStatusAndCount[it].status + "</td><td>" + untilStr + "</td><td>" + relayStatusAndCount[it].count + "</td>"
return "<tr>" +line+ "</tr>"
}
).join("<br />")
$('#checking-relays').html(newText)
} else {
$('#checking-relays-header').html("")
$('#checking-relays').html("")
}
}
// fetch events from relay, returns a promise
const fetchFromRelay = async (relay, filters, pubkey, events, relayStatus) =>
new Promise((resolve, reject) => {
try {
updateRelayStatus(relay, "Starting", 0, undefined, relayStatus)
// open websocket
const ws = new WebSocket(relay)
// prevent hanging forever
let myTimeout = setTimeout(() => {
ws.close() ws.close()
reject(relay) reject(relay)
}, 10_000) }, 10_000)
updateRelayStatus(relay, "Downloading", 0, undefined, relayStatus)
const subscriptions = Object.fromEntries(filters.map ( (filter, index) => { for (const [key, sub] of Object.entries(subscriptions)) {
let id = "my-sub-"+index ws.send(JSON.stringify(['REQ', sub.id, sub.filter]))
}
}
return [ // Listen for messages
id, { ws.onmessage = (event) => {
id: id, const [msgType, subscriptionId, data] = JSON.parse(event.data)
counter: 0, // event messages
lastEvent: null, if (msgType === 'EVENT') {
done: false,
filter: filter,
eventIds: new Set()
}
]
}))
// subscribe to events filtered by author
ws.onopen = () => {
clearTimeout(myTimeout) clearTimeout(myTimeout)
myTimeout = setTimeout(() => { myTimeout = setTimeout(() => {
ws.close() ws.close()
reject(relay) reject(relay)
}, 10_000) }, 10_000)
updateRelayStatus(relay, "Downloading", 0, undefined, relayStatus)
for (const [key, sub] of Object.entries(subscriptions)) { try {
ws.send(JSON.stringify(['REQ', sub.id, sub.filter])) const { id } = data
if (!subscriptions[subscriptionId].lastEvent || data.created_at < subscriptions[subscriptionId].lastEvent.created_at)
subscriptions[subscriptionId].lastEvent = data
if (data.id in subscriptions[subscriptionId].eventIds) return
subscriptions[subscriptionId].eventIds.add(data.id)
subscriptions[subscriptionId].counter++
// don't save/reboradcast kind 3s that are not from the author.
// their are too big.
if (data.kind == 3 && data.pubkey != pubkey) {
return
}
let until = undefined
if (subscriptions[subscriptionId].lastEvent) {
until = subscriptions[subscriptionId].lastEvent.created_at
}
updateRelayStatus(relay, undefined, 1, until, relayStatus)
// prevent duplicated events
if (events[id]) return
else events[id] = data
// show how many events were found until this moment
$('#events-found').text(`${Object.keys(events).length} events found`)
} catch(err) {
console.log(err, event)
return
} }
} }
// Listen for messages // end of subscription messages
ws.onmessage = (event) => { if (msgType === 'EOSE') {
const [msgType, subscriptionId, data] = JSON.parse(event.data) // Restarting the filter is necessary to go around Max Limits for each relay.
// event messages if (subscriptions[subscriptionId].counter < 2) {
if (msgType === 'EVENT') { subscriptions[subscriptionId].done = true
clearTimeout(myTimeout) console.log(relay, subscriptionId, event.data)
myTimeout = setTimeout(() => {
let alldone = Object.values(subscriptions).every(filter => filter.done === true);
if (alldone) {
updateRelayStatus(relay, "Done", 0, undefined, relayStatus)
ws.close() ws.close()
reject(relay) resolve(relay)
}, 10_000)
try {
const { id } = data
if (!subscriptions[subscriptionId].lastEvent || data.created_at < subscriptions[subscriptionId].lastEvent.created_at)
subscriptions[subscriptionId].lastEvent = data
if (data.id in subscriptions[subscriptionId].eventIds) return
subscriptions[subscriptionId].eventIds.add(data.id)
subscriptions[subscriptionId].counter++
// don't save/reboradcast kind 3s that are not from the author.
// their are too big.
if (data.kind == 3 && data.pubkey != pubkey) {
return
}
let until = undefined
if (subscriptions[subscriptionId].lastEvent) {
until = subscriptions[subscriptionId].lastEvent.created_at
}
updateRelayStatus(relay, undefined, 1, until, relayStatus)
// prevent duplicated events
if (events[id]) return
else events[id] = data
// show how many events were found until this moment
$('#events-found').text(`${Object.keys(events).length} events found`)
} catch(err) {
console.log(err, event)
return
} }
} else {
//console.log("Limit: ", { ...filters[0], until: lastSub1Event.created_at })
subscriptions[subscriptionId].counter = 0
ws.send(JSON.stringify(['REQ', subscriptions[subscriptionId].id, { ...subscriptions[subscriptionId].filter, until: subscriptions[subscriptionId].lastEvent.created_at } ]))
} }
}
// end of subscription messages if (msgType === 'AUTH') {
if (msgType === 'EOSE') { signNostrAuthEvent(relay, subscriptionId).then(
// Restarting the filter is necessary to go around Max Limits for each relay. (event) => {
if (subscriptions[subscriptionId].counter < 2) { if (event)
subscriptions[subscriptionId].done = true ws.send(JSON.stringify(['EVENT', event]))
console.log(relay, subscriptionId, event.data) else {
let alldone = Object.values(subscriptions).every(filter => filter.done === true);
if (alldone) {
updateRelayStatus(relay, "Done", 0, undefined, relayStatus)
ws.close()
resolve(relay)
}
} else {
//console.log("Limit: ", { ...filters[0], until: lastSub1Event.created_at })
subscriptions[subscriptionId].counter = 0
ws.send(JSON.stringify(['REQ', subscriptions[subscriptionId].id, { ...subscriptions[subscriptionId].filter, until: subscriptions[subscriptionId].lastEvent.created_at } ]))
}
}
if (msgType === 'AUTH') {
signNostrAuthEvent(relay, subscriptionId).then(
(event) => {
if (event)
ws.send(JSON.stringify(['EVENT', event]))
else {
updateRelayStatus(relay, "AUTH Req", 0, undefined, relayStatus)
ws.close()
reject(relay)
}
},
(reason) => {
updateRelayStatus(relay, "AUTH Req", 0, undefined, relayStatus) updateRelayStatus(relay, "AUTH Req", 0, undefined, relayStatus)
ws.close() ws.close()
reject(relay) reject(relay)
}, }
) },
} (reason) => {
updateRelayStatus(relay, "AUTH Req", 0, undefined, relayStatus)
ws.close()
reject(relay)
},
)
} }
ws.onerror = (err) => { }
updateRelayStatus(relay, "Done", 0, undefined, relayStatus) ws.onerror = (err) => {
try { updateRelayStatus(relay, "Done", 0, undefined, relayStatus)
ws.close()
reject(relay)
} catch {
reject(relay)
}
}
ws.onclose = (socket, event) => {
updateRelayStatus(relay, "Done", 0, undefined, relayStatus)
resolve(relay)
}
} catch (exception) {
console.log(exception)
updateRelayStatus(relay, "Error", 0, undefined, relayStatus)
try { try {
ws.close() ws.close()
} catch (exception) { reject(relay)
} } catch {
reject(relay)
reject(relay)
}
})
// query relays for events published by this pubkey
const getEvents = async (filters, pubkey) => {
// events hash
const events = {}
// batch processing of 10 relays
await processInPool(relays, (relay, poolStatus) => fetchFromRelay(relay, filters, pubkey, events, poolStatus), 10)
displayRelayStatus({})
// return data as an array of events
return Object.keys(events).map((id) => events[id])
}
const processInPool = async (items, processItem, poolSize) => {
let pool = {};
let poolStatus = {}
let remaining = [...items]
while (remaining.length) {
let processing = remaining.splice(0, 1)
let item = processing[0]
pool[item] = processItem(item, poolStatus);
if (Object.keys(pool).length > poolSize - 1) {
try {
const resolvedId = await Promise.race(Object.values(pool)); // wait for one Promise to finish
delete pool[resolvedId]; // remove that Promise from the pool
} catch (resolvedId) {
delete pool[resolvedId]; // remove that Promise from the pool
} }
} }
ws.onclose = (socket, event) => {
updateRelayStatus(relay, "Done", 0, undefined, relayStatus)
resolve(relay)
}
} catch (exception) {
console.log(exception)
updateRelayStatus(relay, "Error", 0, undefined, relayStatus)
try {
ws.close()
} catch (exception) {
}
$('#fetching-progress').val(items.length - remaining.length) reject(relay)
} }
})
await Promise.allSettled(Object.values(pool)); // query relays for events published by this pubkey
const getEvents = async (filters, pubkey) => {
// events hash
const events = {}
// batch processing of 10 relays
await processInPool(relays, (relay, poolStatus) => fetchFromRelay(relay, filters, pubkey, events, poolStatus), 10)
displayRelayStatus({})
// return data as an array of events
return Object.keys(events).map((id) => events[id])
} }
const sendAllEvents = async (relay, data, relayStatus, ws) => { const processInPool = async (items, processItem, poolSize) => {
console.log("Sending:", data.length, " events") let pool = {};
for (evnt of data) { let poolStatus = {}
ws.send(JSON.stringify(['EVENT', evnt])) let remaining = [...items]
while (remaining.length) {
let processing = remaining.splice(0, 1)
let item = processing[0]
pool[item] = processItem(item, poolStatus);
if (Object.keys(pool).length > poolSize - 1) {
try {
const resolvedId = await Promise.race(Object.values(pool)); // wait for one Promise to finish
delete pool[resolvedId]; // remove that Promise from the pool
} catch (resolvedId) {
delete pool[resolvedId]; // remove that Promise from the pool
}
} }
$('#fetching-progress').val(items.length - remaining.length)
} }
// send events to a relay, returns a promisse await Promise.allSettled(Object.values(pool));
const sendToRelay = async (relay, data, relayStatus) => }
new Promise((resolve, reject) => {
try {
const ws = new WebSocket(relay)
updateRelayStatus(relay, "Starting", 0, undefined, relayStatus) const sendAllEvents = async (relay, data, relayStatus, ws) => {
console.log("Sending:", data.length, " events")
for (evnt of data) {
ws.send(JSON.stringify(['EVENT', evnt]))
}
}
// prevent hanging forever // send events to a relay, returns a promisse
let myTimeout = setTimeout(() => { const sendToRelay = async (relay, data, relayStatus) =>
new Promise((resolve, reject) => {
try {
const ws = new WebSocket(relay)
updateRelayStatus(relay, "Starting", 0, undefined, relayStatus)
// prevent hanging forever
let myTimeout = setTimeout(() => {
ws.close()
reject('timeout')
}, 10_000)
// fetch events from relay
ws.onopen = () => {
updateRelayStatus(relay, "Sending", 0, undefined, relayStatus)
clearTimeout(myTimeout)
myTimeout = setTimeout(() => {
ws.close() ws.close()
reject('timeout') reject('timeout')
}, 10_000) }, 10_000)
// fetch events from relay sendAllEvents(relay, data, relayStatus, ws)
ws.onopen = () => { }
updateRelayStatus(relay, "Sending", 0, undefined, relayStatus) // Listen for messages
ws.onmessage = (event) => {
clearTimeout(myTimeout)
myTimeout = setTimeout(() => {
ws.close()
reject('timeout')
}, 10_000)
clearTimeout(myTimeout) const [msgType, subscriptionId, inserted] = JSON.parse(event.data)
myTimeout = setTimeout(() => { // event messages
ws.close() // end of subscription messages
reject('timeout') if (msgType === 'OK') {
}, 10_000) if (inserted == true) {
updateRelayStatus(relay, undefined, 1, undefined, relayStatus)
sendAllEvents(relay, data, relayStatus, ws)
}
// Listen for messages
ws.onmessage = (event) => {
clearTimeout(myTimeout)
myTimeout = setTimeout(() => {
ws.close()
reject('timeout')
}, 10_000)
const [msgType, subscriptionId, inserted] = JSON.parse(event.data)
// event messages
// end of subscription messages
if (msgType === 'OK') {
if (inserted == true) {
updateRelayStatus(relay, undefined, 1, undefined, relayStatus)
} else {
console.log(relay, event.data)
}
} else { } else {
console.log(relay, event.data) console.log(relay, event.data)
} }
} else {
console.log(relay, event.data)
} }
ws.onerror = (err) => { }
updateRelayStatus(relay, "Error", 0, undefined, relayStatus) ws.onerror = (err) => {
console.log("Error", err)
ws.close()
reject(err)
}
ws.onclose = (socket, event) => {
updateRelayStatus(relay, "Done", 0, undefined, relayStatus)
console.log("OnClose", relayStatus)
resolve()
}
} catch (exception) {
console.log(exception)
updateRelayStatus(relay, "Error", 0, undefined, relayStatus) updateRelayStatus(relay, "Error", 0, undefined, relayStatus)
try { console.log("Error", err)
ws.close() ws.close()
} catch (exception) { reject(err)
}
reject(exception)
} }
}) ws.onclose = (socket, event) => {
updateRelayStatus(relay, "Done", 0, undefined, relayStatus)
console.log("OnClose", relayStatus)
resolve()
}
} catch (exception) {
console.log(exception)
updateRelayStatus(relay, "Error", 0, undefined, relayStatus)
try {
ws.close()
} catch (exception) {
}
reject(exception)
}
})
// broadcast events to list of relays // broadcast events to list of relays
const broadcastEvents = async (data) => { const broadcastEvents = async (data) => {
// batch processing of 10 relays // batch processing of 10 relays
let broadcastFunctions = [...relays] let broadcastFunctions = [...relays]
let relayStatus = {} let relayStatus = {}
while (broadcastFunctions.length) { while (broadcastFunctions.length) {
let relaysForThisRound = broadcastFunctions.splice(0, 10) let relaysForThisRound = broadcastFunctions.splice(0, 10)
$('#broadcasting-progress').val(relays.length - broadcastFunctions.length) $('#broadcasting-progress').val(relays.length - broadcastFunctions.length)
await Promise.allSettled( relaysForThisRound.map((relay) => sendToRelay(relay, data, relayStatus)) ) await Promise.allSettled( relaysForThisRound.map((relay) => sendToRelay(relay, data, relayStatus)) )
}
displayRelayStatus(relayStatus)
}
async function generateNostrEventId(msg) {
const digest = [
0,
msg.pubkey,
msg.created_at,
msg.kind,
msg.tags,
msg.content,
];
const digest_str = JSON.stringify(digest);
const hash = await sha256Hex(digest_str);
return hash;
}
function sha256Hex(string) {
const utf8 = new TextEncoder().encode(string);
return crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => {
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
});
}
async function signNostrAuthEvent(relay, auth_challenge) {
try {
if (!window.nostr) {
throw "Nostr extension not loaded or available"
} }
displayRelayStatus(relayStatus) let msg = {
kind: 22243, // NIP-42++
content: "",
tags: [
["relay", relay]
["challenge", auth_challenge]
],
};
// set msg fields
msg.created_at = Math.floor((new Date()).getTime() / 1000);
msg.pubkey = await window.nostr.getPublicKey();
// Generate event id
msg.id = await generateNostrEventId(msg);
// Sign event
signed_msg = await window.nostr.signEvent(msg);
} catch (e) {
console.log("Failed to sign message with browser extension", e);
return undefined;
} }
async function signNostrAuthEvent(relay, auth_challenge) { return signed_msg;
try { }
if (!window.nostr) {
throw "Nostr extension not loaded or available"
}
let msg = {
kind: 22243, // NIP-42++
content: "",
tags: [
["relay", relay]
["challenge", auth_challenge]
],
};
// set msg fields
msg.created_at = Math.floor((new Date()).getTime() / 1000);
msg.pubkey = await window.nostr.getPublicKey();
// Generate event id
msg.id = await generateNostrEventId(msg);
// Sign event
signed_msg = await window.nostr.signEvent(msg);
} catch (e) {
console.log("Failed to sign message with browser extension", e);
return undefined;
}
return signed_msg;
}
async function generateNostrEventId(msg) {
const digest = [
0,
msg.pubkey,
msg.created_at,
msg.kind,
msg.tags,
msg.content,
];
const digest_str = JSON.stringify(digest);
const hash = await sha256Hex(digest_str);
return hash;
}
function sha256Hex(string) {
const utf8 = new TextEncoder().encode(string);
return crypto.subtle.digest('SHA-256', utf8).then((hashBuffer) => {
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray
.map((bytes) => bytes.toString(16).padStart(2, '0'))
.join('');
return hashHex;
});
}