From 75fd4fb7aac1e05a27d1b46311c8a5fe0c259ea9 Mon Sep 17 00:00:00 2001 From: Kieran Date: Wed, 21 Jun 2023 16:08:48 +0100 Subject: [PATCH] Move zap parsing to @snort/system --- packages/app/custom.d.ts | 14 --- .../app/src/Cache/EventInteractionCache.ts | 4 +- packages/app/src/Cache/PaymentsCache.ts | 4 +- packages/app/src/Cache/index.ts | 5 +- packages/app/src/Element/Invoice.tsx | 2 +- packages/app/src/Element/MediaElement.tsx | 5 +- packages/app/src/Element/Note.tsx | 5 +- packages/app/src/Element/NoteFooter.tsx | 4 +- packages/app/src/Element/Poll.tsx | 3 +- packages/app/src/Element/Reactions.tsx | 3 +- packages/app/src/Element/Timeline.tsx | 7 +- packages/app/src/Element/Zap.tsx | 93 +------------------ packages/app/src/Feed/ZapsFeed.ts | 6 +- .../app/src/Hooks/useInteractionCache.tsx | 2 +- packages/app/src/SnortUtils/index.ts | 46 --------- packages/app/src/Wallet/index.ts | 4 +- packages/shared/package.json | 5 +- packages/shared/src/d.ts | 14 +++ packages/shared/src/index.ts | 3 +- packages/shared/src/invoices.ts | 48 ++++++++++ packages/system/package.json | 4 +- .../src/{cache => Cache}/RelayMetricCache.ts | 0 .../system/src/{cache => Cache}/UserCache.ts | 0 .../src/{cache => Cache}/UserRelayCache.ts | 0 packages/system/src/{cache => Cache}/db.ts | 0 packages/system/src/{cache => Cache}/index.ts | 0 packages/system/src/ProfileCache.ts | 2 +- packages/system/src/RelayMetricHandler.ts | 2 +- packages/system/src/Zaps.ts | 92 ++++++++++++++++++ packages/system/src/index.ts | 9 +- packages/system/tsconfig.json | 4 +- yarn.lock | 9 +- 32 files changed, 206 insertions(+), 193 deletions(-) create mode 100644 packages/shared/src/d.ts create mode 100644 packages/shared/src/invoices.ts rename packages/system/src/{cache => Cache}/RelayMetricCache.ts (100%) rename packages/system/src/{cache => Cache}/UserCache.ts (100%) rename packages/system/src/{cache => Cache}/UserRelayCache.ts (100%) rename packages/system/src/{cache => Cache}/db.ts (100%) rename packages/system/src/{cache => Cache}/index.ts (100%) create mode 100644 packages/system/src/Zaps.ts diff --git a/packages/app/custom.d.ts b/packages/app/custom.d.ts index 042f1653..11262519 100644 --- a/packages/app/custom.d.ts +++ b/packages/app/custom.d.ts @@ -29,17 +29,3 @@ declare module "translations/*.json" { const value: Record; 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; - } -} diff --git a/packages/app/src/Cache/EventInteractionCache.ts b/packages/app/src/Cache/EventInteractionCache.ts index f343d4af..9d21c4a2 100644 --- a/packages/app/src/Cache/EventInteractionCache.ts +++ b/packages/app/src/Cache/EventInteractionCache.ts @@ -3,7 +3,7 @@ import { db, EventInteraction } from "Db"; import { LoginStore } from "Login"; import { sha256 } from "SnortUtils"; -class EventInteractionCache extends FeedCache { +export class EventInteractionCache extends FeedCache { constructor() { super("EventInteraction", db.eventInteraction); } @@ -40,5 +40,3 @@ class EventInteractionCache extends FeedCache { return [...this.cache.values()]; } } - -export const InteractionCache = new EventInteractionCache(); diff --git a/packages/app/src/Cache/PaymentsCache.ts b/packages/app/src/Cache/PaymentsCache.ts index 96d487d3..d0f6804d 100644 --- a/packages/app/src/Cache/PaymentsCache.ts +++ b/packages/app/src/Cache/PaymentsCache.ts @@ -1,7 +1,7 @@ import { Payment, db } from "Db"; import { FeedCache } from "@snort/shared"; -class Payments extends FeedCache { +export class Payments extends FeedCache { constructor() { super("PaymentsCache", db.payments); } @@ -14,5 +14,3 @@ class Payments extends FeedCache { return [...this.cache.values()]; } } - -export const PaymentsCache = new Payments(); diff --git a/packages/app/src/Cache/index.ts b/packages/app/src/Cache/index.ts index f872ea4d..33917aa5 100644 --- a/packages/app/src/Cache/index.ts +++ b/packages/app/src/Cache/index.ts @@ -1,11 +1,14 @@ import { UserProfileCache, UserRelaysCache, RelayMetricCache } from "@snort/system"; -import { InteractionCache } from "./EventInteractionCache"; +import { EventInteractionCache } from "./EventInteractionCache"; import { ChatCache } from "./ChatCache"; +import { Payments } from "./PaymentsCache"; export const UserCache = new UserProfileCache(); export const UserRelays = new UserRelaysCache(); export const RelayMetrics = new RelayMetricCache(); export const Chats = new ChatCache(); +export const PaymentsCache = new Payments(); +export const InteractionCache = new EventInteractionCache(); export async function preload(follows?: Array) { const preloads = [ diff --git a/packages/app/src/Element/Invoice.tsx b/packages/app/src/Element/Invoice.tsx index f8704c17..31700cd8 100644 --- a/packages/app/src/Element/Invoice.tsx +++ b/packages/app/src/Element/Invoice.tsx @@ -2,11 +2,11 @@ import "./Invoice.css"; import { useState } from "react"; import { useIntl, FormattedMessage } from "react-intl"; import { useMemo } from "react"; +import { decodeInvoice } from "@snort/shared"; import SendSats from "Element/SendSats"; import Icon from "Icons/Icon"; import { useWallet } from "Wallet"; -import { decodeInvoice } from "SnortUtils"; import messages from "./messages"; diff --git a/packages/app/src/Element/MediaElement.tsx b/packages/app/src/Element/MediaElement.tsx index 73f2300b..6881c06f 100644 --- a/packages/app/src/Element/MediaElement.tsx +++ b/packages/app/src/Element/MediaElement.tsx @@ -2,14 +2,15 @@ import { ProxyImg } from "Element/ProxyImg"; import React, { MouseEvent, useEffect, useState } from "react"; import { FormattedMessage, FormattedNumber } from "react-intl"; import { Link } from "react-router-dom"; +import { decodeInvoice, InvoiceDetails } from "@snort/shared"; import "./MediaElement.css"; import Modal from "Element/Modal"; import Icon from "Icons/Icon"; -import { decodeInvoice, InvoiceDetails, kvToObject } from "SnortUtils"; +import { kvToObject } from "SnortUtils"; import AsyncButton from "Element/AsyncButton"; import { useWallet } from "Wallet"; -import { PaymentsCache } from "Cache/PaymentsCache"; +import { PaymentsCache } from "Cache"; import { Payment } from "Db"; import PageSpinner from "Element/PageSpinner"; import { LiveVideoPlayer } from "Element/LiveVideoPlayer"; diff --git a/packages/app/src/Element/Note.tsx b/packages/app/src/Element/Note.tsx index c47911fc..24af94cf 100644 --- a/packages/app/src/Element/Note.tsx +++ b/packages/app/src/Element/Note.tsx @@ -3,12 +3,11 @@ import React, { useMemo, useState, useLayoutEffect, ReactNode } from "react"; import { useNavigate, Link } from "react-router-dom"; import { useInView } from "react-intersection-observer"; 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 useEventPublisher from "Feed/EventPublisher"; import Icon from "Icons/Icon"; -import { parseZap } from "Element/Zap"; import ProfileImage from "Element/ProfileImage"; import Text from "Element/Text"; import { @@ -135,7 +134,7 @@ export default function Note(props: NoteProps) { ); const zaps = useMemo(() => { const sortedZaps = getReactions(related, ev.id, EventKind.ZapReceipt) - .map(a => parseZap(a, ev)) + .map(a => parseZap(a, UserCache, ev)) .filter(z => z.valid); sortedZaps.sort((a, b) => b.amount - a.amount); return sortedZaps; diff --git a/packages/app/src/Element/NoteFooter.tsx b/packages/app/src/Element/NoteFooter.tsx index 3dc38f8b..d5b7a389 100644 --- a/packages/app/src/Element/NoteFooter.tsx +++ b/packages/app/src/Element/NoteFooter.tsx @@ -3,7 +3,7 @@ import { useSelector, useDispatch } from "react-redux"; import { useIntl, FormattedMessage } from "react-intl"; import { Menu, MenuItem } from "@szhsin/react-menu"; 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 { useUserProfile } from "@snort/system-react"; @@ -17,7 +17,7 @@ import { NoteCreator } from "Element/NoteCreator"; import { ReBroadcaster } from "Element/ReBroadcaster"; import Reactions from "Element/Reactions"; import SendSats from "Element/SendSats"; -import { ParsedZap, ZapsSummary } from "Element/Zap"; +import { ZapsSummary } from "Element/Zap"; import { RootState } from "State/Store"; import { setReplyTo, setShow, reset } from "State/NoteCreator"; import { diff --git a/packages/app/src/Element/Poll.tsx b/packages/app/src/Element/Poll.tsx index ac027950..17f5e06f 100644 --- a/packages/app/src/Element/Poll.tsx +++ b/packages/app/src/Element/Poll.tsx @@ -1,10 +1,9 @@ -import { TaggedRawEvent } from "@snort/system"; +import { TaggedRawEvent, ParsedZap } from "@snort/system"; import { LNURL } from "@snort/shared"; import { useState } from "react"; import { FormattedMessage, FormattedNumber, useIntl } from "react-intl"; import { useUserProfile } from "@snort/system-react"; -import { ParsedZap } from "Element/Zap"; import Text from "Element/Text"; import useEventPublisher from "Feed/EventPublisher"; import { useWallet } from "Wallet"; diff --git a/packages/app/src/Element/Reactions.tsx b/packages/app/src/Element/Reactions.tsx index 8e723277..2bb60ea0 100644 --- a/packages/app/src/Element/Reactions.tsx +++ b/packages/app/src/Element/Reactions.tsx @@ -2,12 +2,11 @@ import "./Reactions.css"; import { useState, useMemo, useEffect } from "react"; import { useIntl, FormattedMessage } from "react-intl"; -import { TaggedRawEvent } from "@snort/system"; +import { TaggedRawEvent, ParsedZap } from "@snort/system"; import { formatShort } from "Number"; import Icon from "Icons/Icon"; import { Tab } from "Element/Tabs"; -import { ParsedZap } from "Element/Zap"; import ProfileImage from "Element/ProfileImage"; import Tabs from "Element/Tabs"; import Modal from "Element/Modal"; diff --git a/packages/app/src/Element/Timeline.tsx b/packages/app/src/Element/Timeline.tsx index 3899dc95..3ecc01ce 100644 --- a/packages/app/src/Element/Timeline.tsx +++ b/packages/app/src/Element/Timeline.tsx @@ -2,19 +2,20 @@ import "./Timeline.css"; import { FormattedMessage } from "react-intl"; import { useCallback, useMemo } from "react"; 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 { dedupeByPubkey, findTag, tagFilterOfTextRepost } from "SnortUtils"; import ProfileImage from "Element/ProfileImage"; import useTimelineFeed, { TimelineFeed, TimelineSubject } from "Feed/TimelineFeed"; import LoadMore from "Element/LoadMore"; -import Zap, { parseZap } from "Element/Zap"; +import Zap from "Element/Zap"; import Note from "Element/Note"; import NoteReaction from "Element/NoteReaction"; import useModeration from "Hooks/useModeration"; import ProfilePreview from "Element/ProfilePreview"; import Skeleton from "Element/Skeleton"; +import { UserCache } from "Cache"; export interface TimelineProps { postsOnly: boolean; @@ -93,7 +94,7 @@ const Timeline = (props: TimelineProps) => { ); } case EventKind.ZapReceipt: { - const zap = parseZap(e); + const zap = parseZap(e, UserCache); return zap.event ? null : ; } case EventKind.Reaction: diff --git a/packages/app/src/Element/Zap.tsx b/packages/app/src/Element/Zap.tsx index 0bcb7821..84e82895 100644 --- a/packages/app/src/Element/Zap.tsx +++ b/packages/app/src/Element/Zap.tsx @@ -1,105 +1,16 @@ import "./Zap.css"; import { useMemo } from "react"; +import { ParsedZap } from "@snort/system"; 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 Text from "Element/Text"; import ProfileImage from "Element/ProfileImage"; -import { findTag } from "SnortUtils"; -import { UserCache } from "Cache"; import useLogin from "Hooks/useLogin"; 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; - pollOption?: number; -} - const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean }) => { const { amount, content, sender, valid, receiver } = zap; const pubKey = useLogin().publicKey; diff --git a/packages/app/src/Feed/ZapsFeed.ts b/packages/app/src/Feed/ZapsFeed.ts index abe76ef8..81ecec04 100644 --- a/packages/app/src/Feed/ZapsFeed.ts +++ b/packages/app/src/Feed/ZapsFeed.ts @@ -1,9 +1,9 @@ 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 { parseZap } from "Element/Zap"; import { System } from "index"; +import { UserCache } from "Cache"; export default function useZapsFeed(pubkey?: HexKey) { const sub = useMemo(() => { @@ -18,7 +18,7 @@ export default function useZapsFeed(pubkey?: HexKey) { const zaps = useMemo(() => { if (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); profileZaps.sort((a, b) => b.amount - a.amount); return profileZaps; diff --git a/packages/app/src/Hooks/useInteractionCache.tsx b/packages/app/src/Hooks/useInteractionCache.tsx index 46a7521d..ab73a99c 100644 --- a/packages/app/src/Hooks/useInteractionCache.tsx +++ b/packages/app/src/Hooks/useInteractionCache.tsx @@ -1,7 +1,7 @@ import { useSyncExternalStore } from "react"; import { HexKey, u256 } from "@snort/system"; -import { InteractionCache } from "Cache/EventInteractionCache"; +import { InteractionCache } from "Cache"; import { EventInteraction } from "Db"; import { sha256, unwrap } from "SnortUtils"; diff --git a/packages/app/src/SnortUtils/index.ts b/packages/app/src/SnortUtils/index.ts index 2d5cfcbc..b06510ca 100644 --- a/packages/app/src/SnortUtils/index.ts +++ b/packages/app/src/SnortUtils/index.ts @@ -3,7 +3,6 @@ import * as utils from "@noble/curves/abstract/utils"; import { sha256 as hash } from "@noble/hashes/sha256"; import { hmac } from "@noble/hashes/hmac"; import { bytesToHex } from "@noble/hashes/utils"; -import { decode as invoiceDecode } from "light-bolt11-decoder"; import { bech32, base32hex } from "@scure/base"; import { 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 { dn?: string | string[]; tr?: string | string[]; diff --git a/packages/app/src/Wallet/index.ts b/packages/app/src/Wallet/index.ts index 8ae307fc..ad8a01dd 100644 --- a/packages/app/src/Wallet/index.ts +++ b/packages/app/src/Wallet/index.ts @@ -1,7 +1,7 @@ import { useEffect, useSyncExternalStore } from "react"; -import { ExternalStore } from "@snort/shared"; -import { decodeInvoice, unwrap } from "SnortUtils"; +import { ExternalStore, decodeInvoice } from "@snort/shared"; +import { unwrap } from "SnortUtils"; import LNDHubWallet from "./LNDHub"; import { NostrConnectWallet } from "./NostrWalletConnect"; import { setupWebLNWalletConfig, WebLNWallet } from "./WebLN"; diff --git a/packages/shared/package.json b/packages/shared/package.json index 2781cea7..8589a7b7 100644 --- a/packages/shared/package.json +++ b/packages/shared/package.json @@ -1,6 +1,6 @@ { "name": "@snort/shared", - "version": "1.0.1", + "version": "1.0.2", "description": "Shared components for Snort", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -16,6 +16,7 @@ "@noble/hashes": "^1.3.1", "@scure/base": "^1.1.1", "debug": "^4.3.4", - "dexie": "^3.2.4" + "dexie": "^3.2.4", + "light-bolt11-decoder": "^3.0.0" } } diff --git a/packages/shared/src/d.ts b/packages/shared/src/d.ts new file mode 100644 index 00000000..7d1eb7b3 --- /dev/null +++ b/packages/shared/src/d.ts @@ -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; + } +} diff --git a/packages/shared/src/index.ts b/packages/shared/src/index.ts index 40e8f328..9d7fa865 100644 --- a/packages/shared/src/index.ts +++ b/packages/shared/src/index.ts @@ -2,4 +2,5 @@ export * from "./external-store"; export * from "./lnurl"; export * from "./utils"; export * from "./work-queue"; -export * from "./feed-cache"; \ No newline at end of file +export * from "./feed-cache"; +export * from "./invoices"; \ No newline at end of file diff --git a/packages/shared/src/invoices.ts b/packages/shared/src/invoices.ts new file mode 100644 index 00000000..a9b5ed4f --- /dev/null +++ b/packages/shared/src/invoices.ts @@ -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); + } +} diff --git a/packages/system/package.json b/packages/system/package.json index 23c11a0f..dca9bad5 100644 --- a/packages/system/package.json +++ b/packages/system/package.json @@ -1,6 +1,6 @@ { "name": "@snort/system", - "version": "1.0.8", + "version": "1.0.9", "description": "Snort nostr system package", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -25,7 +25,7 @@ "typescript": "^5.0.4" }, "dependencies": { - "@snort/shared": "^1.0.1", + "@snort/shared": "^1.0.2", "@noble/curves": "^1.0.0", "@scure/base": "^1.1.1", "@stablelib/xchacha20": "^1.0.1", diff --git a/packages/system/src/cache/RelayMetricCache.ts b/packages/system/src/Cache/RelayMetricCache.ts similarity index 100% rename from packages/system/src/cache/RelayMetricCache.ts rename to packages/system/src/Cache/RelayMetricCache.ts diff --git a/packages/system/src/cache/UserCache.ts b/packages/system/src/Cache/UserCache.ts similarity index 100% rename from packages/system/src/cache/UserCache.ts rename to packages/system/src/Cache/UserCache.ts diff --git a/packages/system/src/cache/UserRelayCache.ts b/packages/system/src/Cache/UserRelayCache.ts similarity index 100% rename from packages/system/src/cache/UserRelayCache.ts rename to packages/system/src/Cache/UserRelayCache.ts diff --git a/packages/system/src/cache/db.ts b/packages/system/src/Cache/db.ts similarity index 100% rename from packages/system/src/cache/db.ts rename to packages/system/src/Cache/db.ts diff --git a/packages/system/src/cache/index.ts b/packages/system/src/Cache/index.ts similarity index 100% rename from packages/system/src/cache/index.ts rename to packages/system/src/Cache/index.ts diff --git a/packages/system/src/ProfileCache.ts b/packages/system/src/ProfileCache.ts index 107b9da0..85c8ad29 100644 --- a/packages/system/src/ProfileCache.ts +++ b/packages/system/src/ProfileCache.ts @@ -3,7 +3,7 @@ import debug from "debug"; import { unixNowMs, FeedCache } from "@snort/shared"; import { EventKind, HexKey, SystemInterface, TaggedRawEvent, PubkeyReplaceableNoteStore, RequestBuilder } from "."; import { ProfileCacheExpire } from "./Const"; -import { mapEventToProfile, MetadataCache } from "./cache"; +import { mapEventToProfile, MetadataCache } from "./Cache"; const MetadataRelays = [ "wss://purplepag.es" diff --git a/packages/system/src/RelayMetricHandler.ts b/packages/system/src/RelayMetricHandler.ts index 2f760114..e51b9d72 100644 --- a/packages/system/src/RelayMetricHandler.ts +++ b/packages/system/src/RelayMetricHandler.ts @@ -1,6 +1,6 @@ import { FeedCache } from "@snort/shared"; import { Connection } from "Connection"; -import { RelayMetrics } from "cache"; +import { RelayMetrics } from "Cache"; export class RelayMetricHandler { readonly #cache: FeedCache; diff --git a/packages/system/src/Zaps.ts b/packages/system/src/Zaps.ts new file mode 100644 index 00000000..a9b2478e --- /dev/null +++ b/packages/system/src/Zaps.ts @@ -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, 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; + pollOption?: number; +} diff --git a/packages/system/src/index.ts b/packages/system/src/index.ts index 6168d5a1..2cee5b91 100644 --- a/packages/system/src/index.ts +++ b/packages/system/src/index.ts @@ -18,14 +18,15 @@ export * from "./EventPublisher"; export * from "./EventBuilder"; export * from "./NostrLink"; export * from "./ProfileCache"; +export * from "./Zaps"; export * from "./impl/nip4"; export * from "./impl/nip44"; -export * from "./cache"; -export * from "./cache/UserRelayCache"; -export * from "./cache/UserCache"; -export * from "./cache/RelayMetricCache"; +export * from "./Cache"; +export * from "./Cache/UserRelayCache"; +export * from "./Cache/UserCache"; +export * from "./Cache/RelayMetricCache"; export interface SystemInterface { /** diff --git a/packages/system/tsconfig.json b/packages/system/tsconfig.json index b57c5cd3..791ddf23 100644 --- a/packages/system/tsconfig.json +++ b/packages/system/tsconfig.json @@ -13,6 +13,6 @@ "outDir": "dist", "skipLibCheck": true }, - "include": ["src/**/*.ts"], - "files": ["src/index.ts"] + "include": ["./src/**/*.ts"], + "files": ["./src/index.ts"] } diff --git a/yarn.lock b/yarn.lock index 67092da7..ce6a2120 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2155,7 +2155,7 @@ estree-walker "^1.0.1" 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" resolved "https://registry.yarnpkg.com/@scure/base/-/base-1.1.1.tgz#ebb651ee52ff84f420097055f4bf46cfba403938" integrity sha512-ZxOhsSyxYwLJj3pLZCefNitxsj093tb2vq90mp2txoYeBqbcjDjqFhyM8eUjq/uFm6zJ+mUuqxlS2FkuSY1MTA== @@ -7033,6 +7033,13 @@ light-bolt11-decoder@^2.1.0: bn.js "^4.11.8" 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: version "2.1.0" resolved "https://registry.npmjs.org/lilconfig/-/lilconfig-2.1.0.tgz"