Move zap parsing to @snort/system

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

View File

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

View File

@ -3,7 +3,7 @@ import { db, EventInteraction } from "Db";
import { LoginStore } from "Login";
import { 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();

View File

@ -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();

View File

@ -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 = [

View File

@ -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";

View File

@ -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";

View File

@ -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;

View File

@ -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 {

View File

@ -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";

View File

@ -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";

View File

@ -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:

View File

@ -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;

View File

@ -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;

View File

@ -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";

View File

@ -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[];

View File

@ -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";

View File

@ -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
View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"name": "@snort/system",
"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",

View File

@ -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"

View File

@ -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>;

View File

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

View File

@ -18,14 +18,15 @@ export * from "./EventPublisher";
export * from "./EventBuilder";
export * from "./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 {
/**

View File

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

View File

@ -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"