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>;
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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();
|
|
||||||
|
@ -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();
|
|
||||||
|
@ -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 = [
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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";
|
||||||
|
@ -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;
|
||||||
|
@ -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 {
|
||||||
|
@ -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";
|
||||||
|
@ -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";
|
||||||
|
@ -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:
|
||||||
|
@ -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;
|
||||||
|
@ -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;
|
||||||
|
@ -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";
|
||||||
|
|
||||||
|
@ -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[];
|
||||||
|
@ -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";
|
||||||
|
@ -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
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 "./utils";
|
||||||
export * from "./work-queue";
|
export * from "./work-queue";
|
||||||
export * from "./feed-cache";
|
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",
|
"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",
|
||||||
|
@ -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"
|
||||||
|
@ -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>;
|
||||||
|
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 "./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 {
|
||||||
/**
|
/**
|
||||||
|
@ -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"]
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
|
Loading…
Reference in New Issue
Block a user