You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
253 lines
6.8 KiB
253 lines
6.8 KiB
// nip57.ts |
|
import { bech32 } from "@scure/base"; |
|
|
|
// pure.ts |
|
import { schnorr } from "@noble/curves/secp256k1"; |
|
import { bytesToHex as bytesToHex2 } from "@noble/hashes/utils"; |
|
|
|
// core.ts |
|
var verifiedSymbol = Symbol("verified"); |
|
var isRecord = (obj) => obj instanceof Object; |
|
function validateEvent(event) { |
|
if (!isRecord(event)) |
|
return false; |
|
if (typeof event.kind !== "number") |
|
return false; |
|
if (typeof event.content !== "string") |
|
return false; |
|
if (typeof event.created_at !== "number") |
|
return false; |
|
if (typeof event.pubkey !== "string") |
|
return false; |
|
if (!event.pubkey.match(/^[a-f0-9]{64}$/)) |
|
return false; |
|
if (!Array.isArray(event.tags)) |
|
return false; |
|
for (let i2 = 0; i2 < event.tags.length; i2++) { |
|
let tag = event.tags[i2]; |
|
if (!Array.isArray(tag)) |
|
return false; |
|
for (let j = 0; j < tag.length; j++) { |
|
if (typeof tag[j] !== "string") |
|
return false; |
|
} |
|
} |
|
return true; |
|
} |
|
|
|
// pure.ts |
|
import { sha256 } from "@noble/hashes/sha256"; |
|
|
|
// utils.ts |
|
import { bytesToHex, hexToBytes } from "@noble/hashes/utils"; |
|
var utf8Decoder = new TextDecoder("utf-8"); |
|
var utf8Encoder = new TextEncoder(); |
|
|
|
// pure.ts |
|
var JS = class { |
|
generateSecretKey() { |
|
return schnorr.utils.randomPrivateKey(); |
|
} |
|
getPublicKey(secretKey) { |
|
return bytesToHex2(schnorr.getPublicKey(secretKey)); |
|
} |
|
finalizeEvent(t, secretKey) { |
|
const event = t; |
|
event.pubkey = bytesToHex2(schnorr.getPublicKey(secretKey)); |
|
event.id = getEventHash(event); |
|
event.sig = bytesToHex2(schnorr.sign(getEventHash(event), secretKey)); |
|
event[verifiedSymbol] = true; |
|
return event; |
|
} |
|
verifyEvent(event) { |
|
if (typeof event[verifiedSymbol] === "boolean") |
|
return event[verifiedSymbol]; |
|
const hash = getEventHash(event); |
|
if (hash !== event.id) { |
|
event[verifiedSymbol] = false; |
|
return false; |
|
} |
|
try { |
|
const valid = schnorr.verify(event.sig, hash, event.pubkey); |
|
event[verifiedSymbol] = valid; |
|
return valid; |
|
} catch (err) { |
|
event[verifiedSymbol] = false; |
|
return false; |
|
} |
|
} |
|
}; |
|
function serializeEvent(evt) { |
|
if (!validateEvent(evt)) |
|
throw new Error("can't serialize event with wrong or missing properties"); |
|
return JSON.stringify([0, evt.pubkey, evt.created_at, evt.kind, evt.tags, evt.content]); |
|
} |
|
function getEventHash(event) { |
|
let eventHash = sha256(utf8Encoder.encode(serializeEvent(event))); |
|
return bytesToHex2(eventHash); |
|
} |
|
var i = new JS(); |
|
var generateSecretKey = i.generateSecretKey; |
|
var getPublicKey = i.getPublicKey; |
|
var finalizeEvent = i.finalizeEvent; |
|
var verifyEvent = i.verifyEvent; |
|
|
|
// kinds.ts |
|
function isReplaceableKind(kind) { |
|
return kind === 0 || kind === 3 || 1e4 <= kind && kind < 2e4; |
|
} |
|
function isAddressableKind(kind) { |
|
return 3e4 <= kind && kind < 4e4; |
|
} |
|
|
|
// nip57.ts |
|
var _fetch; |
|
try { |
|
_fetch = fetch; |
|
} catch { |
|
} |
|
function useFetchImplementation(fetchImplementation) { |
|
_fetch = fetchImplementation; |
|
} |
|
async function getZapEndpoint(metadata) { |
|
try { |
|
let lnurl = ""; |
|
let { lud06, lud16 } = JSON.parse(metadata.content); |
|
if (lud16) { |
|
let [name, domain] = lud16.split("@"); |
|
lnurl = new URL(`/.well-known/lnurlp/${name}`, `https://${domain}`).toString(); |
|
} else if (lud06) { |
|
let { words } = bech32.decode(lud06, 1e3); |
|
let data = bech32.fromWords(words); |
|
lnurl = utf8Decoder.decode(data); |
|
} else { |
|
return null; |
|
} |
|
let res = await _fetch(lnurl); |
|
let body = await res.json(); |
|
if (body.allowsNostr && body.nostrPubkey) { |
|
return body.callback; |
|
} |
|
} catch (err) { |
|
} |
|
return null; |
|
} |
|
function makeZapRequest(params) { |
|
let zr = { |
|
kind: 9734, |
|
created_at: Math.round(Date.now() / 1e3), |
|
content: params.comment || "", |
|
tags: [ |
|
["p", "pubkey" in params ? params.pubkey : params.event.pubkey], |
|
["amount", params.amount.toString()], |
|
["relays", ...params.relays] |
|
] |
|
}; |
|
if ("event" in params) { |
|
zr.tags.push(["e", params.event.id]); |
|
if (isReplaceableKind(params.event.kind)) { |
|
const a = ["a", `${params.event.kind}:${params.event.pubkey}:`]; |
|
zr.tags.push(a); |
|
} else if (isAddressableKind(params.event.kind)) { |
|
let d = params.event.tags.find(([t, v]) => t === "d" && v); |
|
if (!d) |
|
throw new Error("d tag not found or is empty"); |
|
const a = ["a", `${params.event.kind}:${params.event.pubkey}:${d[1]}`]; |
|
zr.tags.push(a); |
|
} |
|
zr.tags.push(["k", params.event.kind.toString()]); |
|
} |
|
return zr; |
|
} |
|
function validateZapRequest(zapRequestString) { |
|
let zapRequest; |
|
try { |
|
zapRequest = JSON.parse(zapRequestString); |
|
} catch (err) { |
|
return "Invalid zap request JSON."; |
|
} |
|
if (!validateEvent(zapRequest)) |
|
return "Zap request is not a valid Nostr event."; |
|
if (!verifyEvent(zapRequest)) |
|
return "Invalid signature on zap request."; |
|
let p = zapRequest.tags.find(([t, v]) => t === "p" && v); |
|
if (!p) |
|
return "Zap request doesn't have a 'p' tag."; |
|
if (!p[1].match(/^[a-f0-9]{64}$/)) |
|
return "Zap request 'p' tag is not valid hex."; |
|
let e = zapRequest.tags.find(([t, v]) => t === "e" && v); |
|
if (e && !e[1].match(/^[a-f0-9]{64}$/)) |
|
return "Zap request 'e' tag is not valid hex."; |
|
let relays = zapRequest.tags.find(([t, v]) => t === "relays" && v); |
|
if (!relays) |
|
return "Zap request doesn't have a 'relays' tag."; |
|
return null; |
|
} |
|
function makeZapReceipt({ |
|
zapRequest, |
|
preimage, |
|
bolt11, |
|
paidAt |
|
}) { |
|
let zr = JSON.parse(zapRequest); |
|
let tagsFromZapRequest = zr.tags.filter(([t]) => t === "e" || t === "p" || t === "a"); |
|
let zap = { |
|
kind: 9735, |
|
created_at: Math.round(paidAt.getTime() / 1e3), |
|
content: "", |
|
tags: [...tagsFromZapRequest, ["P", zr.pubkey], ["bolt11", bolt11], ["description", zapRequest]] |
|
}; |
|
if (preimage) { |
|
zap.tags.push(["preimage", preimage]); |
|
} |
|
return zap; |
|
} |
|
function getSatoshisAmountFromBolt11(bolt11) { |
|
if (bolt11.length < 50) { |
|
return 0; |
|
} |
|
bolt11 = bolt11.substring(0, 50); |
|
const idx = bolt11.lastIndexOf("1"); |
|
if (idx === -1) { |
|
return 0; |
|
} |
|
const hrp = bolt11.substring(0, idx); |
|
if (!hrp.startsWith("lnbc")) { |
|
return 0; |
|
} |
|
const amount = hrp.substring(4); |
|
if (amount.length < 1) { |
|
return 0; |
|
} |
|
const char = amount[amount.length - 1]; |
|
const digit = char.charCodeAt(0) - "0".charCodeAt(0); |
|
const isDigit = digit >= 0 && digit <= 9; |
|
let cutPoint = amount.length - 1; |
|
if (isDigit) { |
|
cutPoint++; |
|
} |
|
if (cutPoint < 1) { |
|
return 0; |
|
} |
|
const num = parseInt(amount.substring(0, cutPoint)); |
|
switch (char) { |
|
case "m": |
|
return num * 1e5; |
|
case "u": |
|
return num * 100; |
|
case "n": |
|
return num / 10; |
|
case "p": |
|
return num / 1e4; |
|
default: |
|
return num * 1e8; |
|
} |
|
} |
|
export { |
|
getSatoshisAmountFromBolt11, |
|
getZapEndpoint, |
|
makeZapReceipt, |
|
makeZapRequest, |
|
useFetchImplementation, |
|
validateZapRequest |
|
};
|
|
|