254 lines
6.8 KiB
JavaScript
254 lines
6.8 KiB
JavaScript
// 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 [0, 3].includes(kind) || 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 (lud06) {
|
|
let { words } = bech32.decode(lud06, 1e3);
|
|
let data = bech32.fromWords(words);
|
|
lnurl = utf8Decoder.decode(data);
|
|
} else if (lud16) {
|
|
let [name, domain] = lud16.split("@");
|
|
lnurl = new URL(`/.well-known/lnurlp/${name}`, `https://${domain}`).toString();
|
|
} 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
|
|
};
|