Move zap parsing to @snort/system

This commit is contained in:
Kieran 2023-06-21 16:08:48 +01:00
parent e9bc25bd88
commit 75fd4fb7aa
Signed by: Kieran
GPG Key ID: DE71CEB3925BE941
32 changed files with 206 additions and 193 deletions

View File

@ -29,17 +29,3 @@ declare module "translations/*.json" {
const value: Record<string, string>; const value: Record<string, string>;
export default value; export default value;
} }
declare module "light-bolt11-decoder" {
export function decode(pr?: string): ParsedInvoice;
export interface ParsedInvoice {
paymentRequest: string;
sections: Section[];
}
export interface Section {
name: string;
value: string | Uint8Array | number | undefined;
}
}

View File

@ -3,7 +3,7 @@ import { db, EventInteraction } from "Db";
import { LoginStore } from "Login"; import { LoginStore } from "Login";
import { sha256 } from "SnortUtils"; import { sha256 } from "SnortUtils";
class EventInteractionCache extends FeedCache<EventInteraction> { export class EventInteractionCache extends FeedCache<EventInteraction> {
constructor() { constructor() {
super("EventInteraction", db.eventInteraction); super("EventInteraction", db.eventInteraction);
} }
@ -40,5 +40,3 @@ class EventInteractionCache extends FeedCache<EventInteraction> {
return [...this.cache.values()]; return [...this.cache.values()];
} }
} }
export const InteractionCache = new EventInteractionCache();

View File

@ -1,7 +1,7 @@
import { Payment, db } from "Db"; import { Payment, db } from "Db";
import { FeedCache } from "@snort/shared"; import { FeedCache } from "@snort/shared";
class Payments extends FeedCache<Payment> { export class Payments extends FeedCache<Payment> {
constructor() { constructor() {
super("PaymentsCache", db.payments); super("PaymentsCache", db.payments);
} }
@ -14,5 +14,3 @@ class Payments extends FeedCache<Payment> {
return [...this.cache.values()]; return [...this.cache.values()];
} }
} }
export const PaymentsCache = new Payments();

View File

@ -1,11 +1,14 @@
import { UserProfileCache, UserRelaysCache, RelayMetricCache } from "@snort/system"; import { UserProfileCache, UserRelaysCache, RelayMetricCache } from "@snort/system";
import { InteractionCache } from "./EventInteractionCache"; import { EventInteractionCache } from "./EventInteractionCache";
import { ChatCache } from "./ChatCache"; import { ChatCache } from "./ChatCache";
import { Payments } from "./PaymentsCache";
export const UserCache = new UserProfileCache(); export const UserCache = new UserProfileCache();
export const UserRelays = new UserRelaysCache(); export const UserRelays = new UserRelaysCache();
export const RelayMetrics = new RelayMetricCache(); export const RelayMetrics = new RelayMetricCache();
export const Chats = new ChatCache(); export const Chats = new ChatCache();
export const PaymentsCache = new Payments();
export const InteractionCache = new EventInteractionCache();
export async function preload(follows?: Array<string>) { export async function preload(follows?: Array<string>) {
const preloads = [ const preloads = [

View File

@ -2,11 +2,11 @@ import "./Invoice.css";
import { useState } from "react"; import { useState } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { useMemo } from "react"; import { useMemo } from "react";
import { decodeInvoice } from "@snort/shared";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { useWallet } from "Wallet"; import { useWallet } from "Wallet";
import { decodeInvoice } from "SnortUtils";
import messages from "./messages"; import messages from "./messages";

View File

@ -2,14 +2,15 @@ import { ProxyImg } from "Element/ProxyImg";
import React, { MouseEvent, useEffect, useState } from "react"; import React, { MouseEvent, useEffect, useState } from "react";
import { FormattedMessage, FormattedNumber } from "react-intl"; import { FormattedMessage, FormattedNumber } from "react-intl";
import { Link } from "react-router-dom"; import { Link } from "react-router-dom";
import { decodeInvoice, InvoiceDetails } from "@snort/shared";
import "./MediaElement.css"; import "./MediaElement.css";
import Modal from "Element/Modal"; import Modal from "Element/Modal";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { decodeInvoice, InvoiceDetails, kvToObject } from "SnortUtils"; import { kvToObject } from "SnortUtils";
import AsyncButton from "Element/AsyncButton"; import AsyncButton from "Element/AsyncButton";
import { useWallet } from "Wallet"; import { useWallet } from "Wallet";
import { PaymentsCache } from "Cache/PaymentsCache"; import { PaymentsCache } from "Cache";
import { Payment } from "Db"; import { Payment } from "Db";
import PageSpinner from "Element/PageSpinner"; import PageSpinner from "Element/PageSpinner";
import { LiveVideoPlayer } from "Element/LiveVideoPlayer"; import { LiveVideoPlayer } from "Element/LiveVideoPlayer";

View File

@ -3,12 +3,11 @@ import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react";
import { useNavigate, Link } from "react-router-dom"; import { useNavigate, Link } from "react-router-dom";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt } from "@snort/system"; import { TaggedRawEvent, HexKey, EventKind, NostrPrefix, Lists, EventExt, parseZap } from "@snort/system";
import { System } from "index"; import { System } from "index";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { parseZap } from "Element/Zap";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import Text from "Element/Text"; import Text from "Element/Text";
import { import {
@ -135,7 +134,7 @@ export default function Note(props: NoteProps) {
); );
const zaps = useMemo(() => { const zaps = useMemo(() => {
const sortedZaps = getReactions(related, ev.id, EventKind.ZapReceipt) const sortedZaps = getReactions(related, ev.id, EventKind.ZapReceipt)
.map(a => parseZap(a, ev)) .map(a => parseZap(a, UserCache, ev))
.filter(z => z.valid); .filter(z => z.valid);
sortedZaps.sort((a, b) => b.amount - a.amount); sortedZaps.sort((a, b) => b.amount - a.amount);
return sortedZaps; return sortedZaps;

View File

@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { Menu, MenuItem } from "@szhsin/react-menu"; import { Menu, MenuItem } from "@szhsin/react-menu";
import { useLongPress } from "use-long-press"; import { useLongPress } from "use-long-press";
import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists } from "@snort/system"; import { TaggedRawEvent, HexKey, u256, encodeTLV, NostrPrefix, Lists, ParsedZap } from "@snort/system";
import { LNURL } from "@snort/shared"; import { LNURL } from "@snort/shared";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
@ -17,7 +17,7 @@ import { NoteCreator } from "Element/NoteCreator";
import { ReBroadcaster } from "Element/ReBroadcaster"; import { ReBroadcaster } from "Element/ReBroadcaster";
import Reactions from "Element/Reactions"; import Reactions from "Element/Reactions";
import SendSats from "Element/SendSats"; import SendSats from "Element/SendSats";
import { ParsedZap, ZapsSummary } from "Element/Zap"; import { ZapsSummary } from "Element/Zap";
import { RootState } from "State/Store"; import { RootState } from "State/Store";
import { setReplyTo, setShow, reset } from "State/NoteCreator"; import { setReplyTo, setShow, reset } from "State/NoteCreator";
import { import {

View File

@ -1,10 +1,9 @@
import { TaggedRawEvent } from "@snort/system"; import { TaggedRawEvent, ParsedZap } from "@snort/system";
import { LNURL } from "@snort/shared"; import { LNURL } from "@snort/shared";
import { useState } from "react"; import { useState } from "react";
import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl";
import { useUserProfile } from "@snort/system-react"; import { useUserProfile } from "@snort/system-react";
import { ParsedZap } from "Element/Zap";
import Text from "Element/Text"; import Text from "Element/Text";
import useEventPublisher from "Feed/EventPublisher"; import useEventPublisher from "Feed/EventPublisher";
import { useWallet } from "Wallet"; import { useWallet } from "Wallet";

View File

@ -2,12 +2,11 @@ import "./Reactions.css";
import { useState, useMemo, useEffect } from "react"; import { useState, useMemo, useEffect } from "react";
import { useIntl, FormattedMessage } from "react-intl"; import { useIntl, FormattedMessage } from "react-intl";
import { TaggedRawEvent } from "@snort/system"; import { TaggedRawEvent, ParsedZap } from "@snort/system";
import { formatShort } from "Number"; import { formatShort } from "Number";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { Tab } from "Element/Tabs"; import { Tab } from "Element/Tabs";
import { ParsedZap } from "Element/Zap";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import Tabs from "Element/Tabs"; import Tabs from "Element/Tabs";
import Modal from "Element/Modal"; import Modal from "Element/Modal";

View File

@ -2,19 +2,20 @@ import "./Timeline.css";
import { FormattedMessage } from "react-intl"; import { FormattedMessage } from "react-intl";
import { useCallback, useMemo } from "react"; import { useCallback, useMemo } from "react";
import { useInView } from "react-intersection-observer"; import { useInView } from "react-intersection-observer";
import { TaggedRawEvent, EventKind, u256 } from "@snort/system"; import { TaggedRawEvent, EventKind, u256, parseZap } from "@snort/system";
import Icon from "Icons/Icon"; import Icon from "Icons/Icon";
import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils"; import { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed"; import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed";
import LoadMore from "Element/LoadMore"; import LoadMore from "Element/LoadMore";
import Zap, { parseZap } from "Element/Zap"; import Zap from "Element/Zap";
import Note from "Element/Note"; import Note from "Element/Note";
import NoteReaction from "Element/NoteReaction"; import NoteReaction from "Element/NoteReaction";
import useModeration from "Hooks/useModeration"; import useModeration from "Hooks/useModeration";
import ProfilePreview from "Element/ProfilePreview"; import ProfilePreview from "Element/ProfilePreview";
import Skeleton from "Element/Skeleton"; import Skeleton from "Element/Skeleton";
import { UserCache } from "Cache";
export interface TimelineProps { export interface TimelineProps {
postsOnly: boolean; postsOnly: boolean;
@ -93,7 +94,7 @@ const Timeline = (props: TimelineProps) => {
); );
} }
case EventKind.ZapReceipt: { case EventKind.ZapReceipt: {
const zap = parseZap(e); const zap = parseZap(e, UserCache);
return zap.event ? null : <Zap zap={zap} key={e.id} />; return zap.event ? null : <Zap zap={zap} key={e.id} />;
} }
case EventKind.Reaction: case EventKind.Reaction:

View File

@ -1,105 +1,16 @@
import "./Zap.css"; import "./Zap.css";
import { useMemo } from "react"; import { useMemo } from "react";
import { ParsedZap } from "@snort/system";
import { FormattedMessage, useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import { HexKey, TaggedRawEvent } from "@snort/system";
import { decodeInvoice, InvoiceDetails, sha256, unwrap } from "SnortUtils"; import { unwrap } from "SnortUtils";
import { formatShort } from "Number"; import { formatShort } from "Number";
import Text from "Element/Text"; import Text from "Element/Text";
import ProfileImage from "Element/ProfileImage"; import ProfileImage from "Element/ProfileImage";
import { findTag } from "SnortUtils";
import { UserCache } from "Cache";
import useLogin from "Hooks/useLogin"; import useLogin from "Hooks/useLogin";
import messages from "./messages"; import messages from "./messages";
function getInvoice(zap: TaggedRawEvent): InvoiceDetails | undefined {
const bolt11 = findTag(zap, "bolt11");
if (!bolt11) {
throw new Error("Invalid zap, missing bolt11 tag");
}
return decodeInvoice(bolt11);
}
export function parseZap(zapReceipt: TaggedRawEvent, refNote?: TaggedRawEvent): ParsedZap {
let innerZapJson = findTag(zapReceipt, "description");
if (innerZapJson) {
try {
const invoice = getInvoice(zapReceipt);
if (innerZapJson.startsWith("%")) {
innerZapJson = decodeURIComponent(innerZapJson);
}
const zapRequest: TaggedRawEvent = JSON.parse(innerZapJson);
if (Array.isArray(zapRequest)) {
// old format, ignored
throw new Error("deprecated zap format");
}
const isForwardedZap = refNote?.tags.some(a => a[0] === "zap") ?? false;
const anonZap = zapRequest.tags.find(a => a[0] === "anon");
const metaHash = sha256(innerZapJson);
const pollOpt = zapRequest.tags.find(a => a[0] === "poll_option")?.[1];
const ret: ParsedZap = {
id: zapReceipt.id,
zapService: zapReceipt.pubkey,
amount: (invoice?.amount ?? 0) / 1000,
event: findTag(zapRequest, "e"),
sender: zapRequest.pubkey,
receiver: findTag(zapRequest, "p"),
valid: true,
anonZap: anonZap !== undefined,
content: zapRequest.content,
errors: [],
pollOption: pollOpt ? Number(pollOpt) : undefined,
};
if (invoice?.descriptionHash !== metaHash) {
ret.valid = false;
ret.errors.push("description_hash does not match zap request");
}
if (findTag(zapRequest, "p") !== findTag(zapReceipt, "p")) {
ret.valid = false;
ret.errors.push("p tags dont match");
}
if (ret.event && ret.event !== findTag(zapReceipt, "e")) {
ret.valid = false;
ret.errors.push("e tags dont match");
}
if (findTag(zapRequest, "amount") === invoice?.amount) {
ret.valid = false;
ret.errors.push("amount tag does not match invoice amount");
}
if (UserCache.getFromCache(ret.receiver)?.zapService !== ret.zapService && !isForwardedZap) {
ret.valid = false;
ret.errors.push("zap service pubkey doesn't match");
}
return ret;
} catch (e) {
// ignored: console.debug("Invalid zap", zapReceipt, e);
}
}
return {
id: zapReceipt.id,
zapService: zapReceipt.pubkey,
amount: 0,
valid: false,
anonZap: false,
errors: ["invalid zap, parsing failed"],
};
}
export interface ParsedZap {
id: HexKey;
event?: HexKey;
receiver?: HexKey;
amount: number;
content?: string;
sender?: HexKey;
valid: boolean;
zapService: HexKey;
anonZap: boolean;
errors: Array<string>;
pollOption?: number;
}
const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean }) => { const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean }) => {
const { amount, content, sender, valid, receiver } = zap; const { amount, content, sender, valid, receiver } = zap;
const pubKey = useLogin().publicKey; const pubKey = useLogin().publicKey;

View File

@ -1,9 +1,9 @@
import { useMemo } from "react"; import { useMemo } from "react";
import { HexKey, EventKind, FlatNoteStore, RequestBuilder } from "@snort/system"; import { HexKey, EventKind, FlatNoteStore, RequestBuilder, parseZap } from "@snort/system";
import { useRequestBuilder } from "@snort/system-react"; import { useRequestBuilder } from "@snort/system-react";
import { parseZap } from "Element/Zap";
import { System } from "index"; import { System } from "index";
import { UserCache } from "Cache";
export default function useZapsFeed(pubkey?: HexKey) { export default function useZapsFeed(pubkey?: HexKey) {
const sub = useMemo(() => { const sub = useMemo(() => {
@ -18,7 +18,7 @@ export default function useZapsFeed(pubkey?: HexKey) {
const zaps = useMemo(() => { const zaps = useMemo(() => {
if (zapsFeed.data) { if (zapsFeed.data) {
const profileZaps = zapsFeed.data const profileZaps = zapsFeed.data
.map(a => parseZap(a)) .map(a => parseZap(a, UserCache))
.filter(z => z.valid && z.receiver === pubkey && z.sender !== pubkey && !z.event); .filter(z => z.valid && z.receiver === pubkey && z.sender !== pubkey && !z.event);
profileZaps.sort((a, b) => b.amount - a.amount); profileZaps.sort((a, b) => b.amount - a.amount);
return profileZaps; return profileZaps;

View File

@ -1,7 +1,7 @@
import { useSyncExternalStore } from "react"; import { useSyncExternalStore } from "react";
import { HexKey, u256 } from "@snort/system"; import { HexKey, u256 } from "@snort/system";
import { InteractionCache } from "Cache/EventInteractionCache"; import { InteractionCache } from "Cache";
import { EventInteraction } from "Db"; import { EventInteraction } from "Db";
import { sha256, unwrap } from "SnortUtils"; import { sha256, unwrap } from "SnortUtils";

View File

@ -3,7 +3,6 @@ import * as utils from "@noble/curves/abstract/utils";
import { sha256 as hash } from "@noble/hashes/sha256"; import { sha256 as hash } from "@noble/hashes/sha256";
import { hmac } from "@noble/hashes/hmac"; import { hmac } from "@noble/hashes/hmac";
import { bytesToHex } from "@noble/hashes/utils"; import { bytesToHex } from "@noble/hashes/utils";
import { decode as invoiceDecode } from "light-bolt11-decoder";
import { bech32, base32hex } from "@scure/base"; import { bech32, base32hex } from "@scure/base";
import { import {
HexKey, HexKey,
@ -316,51 +315,6 @@ export const delay = (t: number) => {
}); });
}; };
export interface InvoiceDetails {
amount?: number;
expire?: number;
timestamp?: number;
description?: string;
descriptionHash?: string;
paymentHash?: string;
expired: boolean;
pr: string;
}
export function decodeInvoice(pr: string): InvoiceDetails | undefined {
try {
const parsed = invoiceDecode(pr);
const amountSection = parsed.sections.find(a => a.name === "amount");
const amount = amountSection ? Number(amountSection.value as number | string) : undefined;
const timestampSection = parsed.sections.find(a => a.name === "timestamp");
const timestamp = timestampSection ? Number(timestampSection.value as number | string) : undefined;
const expirySection = parsed.sections.find(a => a.name === "expiry");
const expire = expirySection ? Number(expirySection.value as number | string) : undefined;
const descriptionSection = parsed.sections.find(a => a.name === "description")?.value;
const descriptionHashSection = parsed.sections.find(a => a.name === "description_hash")?.value;
const paymentHashSection = parsed.sections.find(a => a.name === "payment_hash")?.value;
const ret = {
pr,
amount: amount,
expire: timestamp && expire ? timestamp + expire : undefined,
timestamp: timestamp,
description: descriptionSection as string | undefined,
descriptionHash: descriptionHashSection ? bytesToHex(descriptionHashSection as Uint8Array) : undefined,
paymentHash: paymentHashSection ? bytesToHex(paymentHashSection as Uint8Array) : undefined,
expired: false,
};
if (ret.expire) {
ret.expired = ret.expire < new Date().getTime() / 1000;
}
return ret;
} catch (e) {
console.error(e);
}
}
export interface Magnet { export interface Magnet {
dn?: string | string[]; dn?: string | string[];
tr?: string | string[]; tr?: string | string[];

View File

@ -1,7 +1,7 @@
import { useEffect, useSyncExternalStore } from "react"; import { useEffect, useSyncExternalStore } from "react";
import { ExternalStore } from "@snort/shared"; import { ExternalStore, decodeInvoice } from "@snort/shared";
import { decodeInvoice, unwrap } from "SnortUtils"; import { unwrap } from "SnortUtils";
import LNDHubWallet from "./LNDHub"; import LNDHubWallet from "./LNDHub";
import { NostrConnectWallet } from "./NostrWalletConnect"; import { NostrConnectWallet } from "./NostrWalletConnect";
import { setupWebLNWalletConfig, WebLNWallet } from "./WebLN"; import { setupWebLNWalletConfig, WebLNWallet } from "./WebLN";

View File

@ -1,6 +1,6 @@
{ {
"name": "@snort/shared", "name": "@snort/shared",
"version": "1.0.1", "version": "1.0.2",
"description": "Shared components for Snort", "description": "Shared components for Snort",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -16,6 +16,7 @@
"@noble/hashes": "^1.3.1", "@noble/hashes": "^1.3.1",
"@scure/base": "^1.1.1", "@scure/base": "^1.1.1",
"debug": "^4.3.4", "debug": "^4.3.4",
"dexie": "^3.2.4" "dexie": "^3.2.4",
"light-bolt11-decoder": "^3.0.0"
} }
} }

14
packages/shared/src/d.ts Normal file
View File

@ -0,0 +1,14 @@
declare module "light-bolt11-decoder" {
export function decode(pr?: string): ParsedInvoice;
export interface ParsedInvoice {
paymentRequest: string;
sections: Section[];
}
export interface Section {
name: string;
value: string | Uint8Array | number | undefined;
}
}

View File

@ -2,4 +2,5 @@ export * from "./external-store";
export * from "./lnurl"; export * from "./lnurl";
export * from "./utils"; export * from "./utils";
export * from "./work-queue"; export * from "./work-queue";
export * from "./feed-cache"; export * from "./feed-cache";
export * from "./invoices";

View File

@ -0,0 +1,48 @@
import { bytesToHex } from "@noble/hashes/utils";
import { decode as invoiceDecode } from "light-bolt11-decoder";
export interface InvoiceDetails {
amount?: number;
expire?: number;
timestamp?: number;
description?: string;
descriptionHash?: string;
paymentHash?: string;
expired: boolean;
pr: string;
}
export function decodeInvoice(pr: string): InvoiceDetails | undefined {
try {
const parsed = invoiceDecode(pr);
const amountSection = parsed.sections.find(a => a.name === "amount");
const amount = amountSection ? Number(amountSection.value as number | string) : undefined;
const timestampSection = parsed.sections.find(a => a.name === "timestamp");
const timestamp = timestampSection ? Number(timestampSection.value as number | string) : undefined;
const expirySection = parsed.sections.find(a => a.name === "expiry");
const expire = expirySection ? Number(expirySection.value as number | string) : undefined;
const descriptionSection = parsed.sections.find(a => a.name === "description")?.value;
const descriptionHashSection = parsed.sections.find(a => a.name === "description_hash")?.value;
const paymentHashSection = parsed.sections.find(a => a.name === "payment_hash")?.value;
const ret = {
pr,
amount: amount,
expire: timestamp && expire ? timestamp + expire : undefined,
timestamp: timestamp,
description: descriptionSection as string | undefined,
descriptionHash: descriptionHashSection ? bytesToHex(descriptionHashSection as Uint8Array) : undefined,
paymentHash: paymentHashSection ? bytesToHex(paymentHashSection as Uint8Array) : undefined,
expired: false,
};
if (ret.expire) {
ret.expired = ret.expire < new Date().getTime() / 1000;
}
return ret;
} catch (e) {
console.error(e);
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "@snort/system", "name": "@snort/system",
"version": "1.0.8", "version": "1.0.9",
"description": "Snort nostr system package", "description": "Snort nostr system package",
"main": "dist/index.js", "main": "dist/index.js",
"types": "dist/index.d.ts", "types": "dist/index.d.ts",
@ -25,7 +25,7 @@
"typescript": "^5.0.4" "typescript": "^5.0.4"
}, },
"dependencies": { "dependencies": {
"@snort/shared": "^1.0.1", "@snort/shared": "^1.0.2",
"@noble/curves": "^1.0.0", "@noble/curves": "^1.0.0",
"@scure/base": "^1.1.1", "@scure/base": "^1.1.1",
"@stablelib/xchacha20": "^1.0.1", "@stablelib/xchacha20": "^1.0.1",

View File

@ -3,7 +3,7 @@ import debug from "debug";
import { unixNowMs, FeedCache } from "@snort/shared"; import { unixNowMs, FeedCache } from "@snort/shared";
import { EventKind, HexKey, SystemInterface, TaggedRawEvent, PubkeyReplaceableNoteStore, RequestBuilder } from "."; import { EventKind, HexKey, SystemInterface, TaggedRawEvent, PubkeyReplaceableNoteStore, RequestBuilder } from ".";
import { ProfileCacheExpire } from "./Const"; import { ProfileCacheExpire } from "./Const";
import { mapEventToProfile, MetadataCache } from "./cache"; import { mapEventToProfile, MetadataCache } from "./Cache";
const MetadataRelays = [ const MetadataRelays = [
"wss://purplepag.es" "wss://purplepag.es"

View File

@ -1,6 +1,6 @@
import { FeedCache } from "@snort/shared"; import { FeedCache } from "@snort/shared";
import { Connection } from "Connection"; import { Connection } from "Connection";
import { RelayMetrics } from "cache"; import { RelayMetrics } from "Cache";
export class RelayMetricHandler { export class RelayMetricHandler {
readonly #cache: FeedCache<RelayMetrics>; readonly #cache: FeedCache<RelayMetrics>;

View File

@ -0,0 +1,92 @@
import { FeedCache } from "@snort/shared";
import { sha256, decodeInvoice, InvoiceDetails } from "@snort/shared";
import { HexKey, NostrEvent } from "Nostr";
import { findTag } from "./Utils";
import { MetadataCache } from "./Cache";
function getInvoice(zap: NostrEvent): InvoiceDetails | undefined {
const bolt11 = findTag(zap, "bolt11");
if (!bolt11) {
throw new Error("Invalid zap, missing bolt11 tag");
}
return decodeInvoice(bolt11);
}
export function parseZap(zapReceipt: NostrEvent, userCache: FeedCache<MetadataCache>, refNote?: NostrEvent): ParsedZap {
let innerZapJson = findTag(zapReceipt, "description");
if (innerZapJson) {
try {
const invoice = getInvoice(zapReceipt);
if (innerZapJson.startsWith("%")) {
innerZapJson = decodeURIComponent(innerZapJson);
}
const zapRequest: NostrEvent = JSON.parse(innerZapJson);
if (Array.isArray(zapRequest)) {
// old format, ignored
throw new Error("deprecated zap format");
}
const isForwardedZap = refNote?.tags.some(a => a[0] === "zap") ?? false;
const anonZap = zapRequest.tags.find(a => a[0] === "anon");
const metaHash = sha256(innerZapJson);
const pollOpt = zapRequest.tags.find(a => a[0] === "poll_option")?.[1];
const ret: ParsedZap = {
id: zapReceipt.id,
zapService: zapReceipt.pubkey,
amount: (invoice?.amount ?? 0) / 1000,
event: findTag(zapRequest, "e"),
sender: zapRequest.pubkey,
receiver: findTag(zapRequest, "p"),
valid: true,
anonZap: anonZap !== undefined,
content: zapRequest.content,
errors: [],
pollOption: pollOpt ? Number(pollOpt) : undefined,
};
if (invoice?.descriptionHash !== metaHash) {
ret.valid = false;
ret.errors.push("description_hash does not match zap request");
}
if (findTag(zapRequest, "p") !== findTag(zapReceipt, "p")) {
ret.valid = false;
ret.errors.push("p tags dont match");
}
if (ret.event && ret.event !== findTag(zapReceipt, "e")) {
ret.valid = false;
ret.errors.push("e tags dont match");
}
if (findTag(zapRequest, "amount") === invoice?.amount) {
ret.valid = false;
ret.errors.push("amount tag does not match invoice amount");
}
if (userCache.getFromCache(ret.receiver)?.zapService !== ret.zapService && !isForwardedZap) {
ret.valid = false;
ret.errors.push("zap service pubkey doesn't match");
}
return ret;
} catch (e) {
// ignored: console.debug("Invalid zap", zapReceipt, e);
}
}
return {
id: zapReceipt.id,
zapService: zapReceipt.pubkey,
amount: 0,
valid: false,
anonZap: false,
errors: ["invalid zap, parsing failed"],
};
}
export interface ParsedZap {
id: HexKey;
event?: HexKey;
receiver?: HexKey;
amount: number;
content?: string;
sender?: HexKey;
valid: boolean;
zapService: HexKey;
anonZap: boolean;
errors: Array<string>;
pollOption?: number;
}

View File

@ -18,14 +18,15 @@ export * from "./EventPublisher";
export * from "./EventBuilder"; export * from "./EventBuilder";
export * from "./NostrLink"; export * from "./NostrLink";
export * from "./ProfileCache"; export * from "./ProfileCache";
export * from "./Zaps";
export * from "./impl/nip4"; export * from "./impl/nip4";
export * from "./impl/nip44"; export * from "./impl/nip44";
export * from "./cache"; export * from "./Cache";
export * from "./cache/UserRelayCache"; export * from "./Cache/UserRelayCache";
export * from "./cache/UserCache"; export * from "./Cache/UserCache";
export * from "./cache/RelayMetricCache"; export * from "./Cache/RelayMetricCache";
export interface SystemInterface { export interface SystemInterface {
/** /**

View File

@ -13,6 +13,6 @@
"outDir": "dist", "outDir": "dist",
"skipLibCheck": true "skipLibCheck": true
}, },
"include": ["src/**/*.ts"], "include": ["./src/**/*.ts"],
"files": ["src/index.ts"] "files": ["./src/index.ts"]
} }

View File

@ -2155,7 +2155,7 @@
estree-walker "^1.0.1" estree-walker "^1.0.1"
picomatch "^2.2.2" picomatch "^2.2.2"
"@scure/base@^1.1.1", "@scure/base@~1.1.0": "@scure/base@1.1.1", "@scure/base@^1.1.1", "@scure/base@~1.1.0":
version "1.1.1" version "1.1.1"
resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938"
integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA==
@ -7033,6 +7033,13 @@ light-bolt11-decoder@^2.1.0:
bn.js "^4.11.8" bn.js "^4.11.8"
buffer "^6.0.3" buffer "^6.0.3"
light-bolt11-decoder@^3.0.0:
version "3.0.0"
resolved "https://registry.yarnpkg.com/light-bolt11-decoder/-/light-bolt11-decoder-3.0.0.tgz#f644576120426c9ef65621bde254f11016055044"
integrity sha512-AKvOigD2pmC8ktnn2TIqdJu0K0qk6ukUmTvHwF3JNkm8uWCqt18Ijn33A/a7gaRZ4PghJ59X+8+MXrzLKdBTmQ==
dependencies:
"@scure/base" "1.1.1"
lilconfig@2.1.0, lilconfig@^2.1.0: lilconfig@2.1.0, lilconfig@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz"