forked from Kieran/snort
Move zap parsing to @snort/system
This commit is contained in:
parent
e9bc25bd88
commit
75fd4fb7aa
14
packages/app/custom.d.ts
vendored
14
packages/app/custom.d.ts
vendored
@ -29,17 +29,3 @@ declare module "translations/*.json" {
|
||||
const value: Record<string, string>;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
@ -3,7 +3,7 @@ import { db, EventInteraction } from "Db";
|
||||
import { LoginStore } from "Login";
|
||||
import { sha256 } from "SnortUtils";
|
||||
|
||||
class EventInteractionCache extends FeedCache<EventInteraction> {
|
||||
export class EventInteractionCache extends FeedCache<EventInteraction> {
|
||||
constructor() {
|
||||
super("EventInteraction", db.eventInteraction);
|
||||
}
|
||||
@ -40,5 +40,3 @@ class EventInteractionCache extends FeedCache<EventInteraction> {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
}
|
||||
|
||||
export const InteractionCache = new EventInteractionCache();
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { Payment, db } from "Db";
|
||||
import { FeedCache } from "@snort/shared";
|
||||
|
||||
class Payments extends FeedCache<Payment> {
|
||||
export class Payments extends FeedCache<Payment> {
|
||||
constructor() {
|
||||
super("PaymentsCache", db.payments);
|
||||
}
|
||||
@ -14,5 +14,3 @@ class Payments extends FeedCache<Payment> {
|
||||
return [...this.cache.values()];
|
||||
}
|
||||
}
|
||||
|
||||
export const PaymentsCache = new Payments();
|
||||
|
@ -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<string>) {
|
||||
const preloads = [
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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";
|
||||
|
@ -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;
|
||||
|
@ -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 {
|
||||
|
@ -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";
|
||||
|
@ -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";
|
||||
|
@ -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 : <Zap zap={zap} key={e.id} />;
|
||||
}
|
||||
case EventKind.Reaction:
|
||||
|
@ -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<string>;
|
||||
pollOption?: number;
|
||||
}
|
||||
|
||||
const Zap = ({ zap, showZapped = true }: { zap: ParsedZap; showZapped?: boolean }) => {
|
||||
const { amount, content, sender, valid, receiver } = zap;
|
||||
const pubKey = useLogin().publicKey;
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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[];
|
||||
|
@ -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";
|
||||
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
14
packages/shared/src/d.ts
Normal file
14
packages/shared/src/d.ts
Normal 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;
|
||||
}
|
||||
}
|
@ -3,3 +3,4 @@ export * from "./lnurl";
|
||||
export * from "./utils";
|
||||
export * from "./work-queue";
|
||||
export * from "./feed-cache";
|
||||
export * from "./invoices";
|
48
packages/shared/src/invoices.ts
Normal file
48
packages/shared/src/invoices.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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<RelayMetrics>;
|
||||
|
92
packages/system/src/Zaps.ts
Normal file
92
packages/system/src/Zaps.ts
Normal 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;
|
||||
}
|
@ -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 {
|
||||
/**
|
||||
|
@ -13,6 +13,6 @@
|
||||
"outDir": "dist",
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"],
|
||||
"files": ["src/index.ts"]
|
||||
"include": ["./src/**/*.ts"],
|
||||
"files": ["./src/index.ts"]
|
||||
}
|
||||
|
@ -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"
|
||||
|
Loading…
Reference in New Issue
Block a user