feat: zaps
This commit is contained in:
parent
d1087d0405
commit
eb77e91b57
@ -7,6 +7,7 @@
|
||||
"@fortawesome/free-solid-svg-icons": "^6.2.1",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@jukben/emoji-search": "^2.0.1",
|
||||
"@noble/hashes": "^1.2.0",
|
||||
"@noble/secp256k1": "^1.7.0",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@reduxjs/toolkit": "^1.9.1",
|
||||
|
@ -103,4 +103,4 @@ export default function HyperText({ link, creator }: { link: string, creator: He
|
||||
}, [link]);
|
||||
|
||||
return render();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
import "./LNURLTip.css";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { bech32ToText } from "Util";
|
||||
import { HexKey } from "Nostr";
|
||||
import useEventPublisher from "Feed/EventPublisher";
|
||||
import Modal from "Element/Modal";
|
||||
import QrCode from "Element/QrCode";
|
||||
import Copy from "Element/Copy";
|
||||
import useWebln from "Hooks/useWebln";
|
||||
|
||||
interface LNURLService {
|
||||
allowsNostr?: boolean
|
||||
nostrPubkey?: HexKey
|
||||
minSendable?: number,
|
||||
maxSendable?: number,
|
||||
metadata: string,
|
||||
@ -31,12 +35,15 @@ export interface LNURLTipProps {
|
||||
invoice?: string, // shortcut to invoice qr tab
|
||||
title?: string,
|
||||
notice?: string
|
||||
note?: HexKey
|
||||
author?: HexKey
|
||||
}
|
||||
|
||||
export default function LNURLTip(props: LNURLTipProps) {
|
||||
const onClose = props.onClose || (() => { });
|
||||
const service = props.svc;
|
||||
const show = props.show || false;
|
||||
const { note, author } = props
|
||||
const amounts = [50, 100, 500, 1_000, 5_000, 10_000, 50_000];
|
||||
const [payService, setPayService] = useState<LNURLService>();
|
||||
const [amount, setAmount] = useState<number>();
|
||||
@ -46,6 +53,7 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
const [error, setError] = useState<string>();
|
||||
const [success, setSuccess] = useState<LNURLSuccessAction>();
|
||||
const webln = useWebln(show);
|
||||
const publisher = useEventPublisher();
|
||||
|
||||
useEffect(() => {
|
||||
if (show && !props.invoice) {
|
||||
@ -117,7 +125,16 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
|
||||
async function loadInvoice() {
|
||||
if (!amount || !payService) return null;
|
||||
const url = `${payService.callback}?amount=${Math.floor(amount * 1000)}${comment ? `&comment=${encodeURIComponent(comment)}` : ""}`;
|
||||
let url = ''
|
||||
const amountParam = `amount=${Math.floor(amount * 1000)}`
|
||||
const commentParam = comment ? `&comment=${encodeURIComponent(comment)}` : ""
|
||||
if (payService.allowsNostr && payService.nostrPubkey && author) {
|
||||
const ev = await publisher.zap(author, note, comment)
|
||||
const nostrParam = ev && `&nostr=${encodeURIComponent(JSON.stringify(ev.ToObject()))}`
|
||||
url = `${payService.callback}?${amountParam}${commentParam}${nostrParam}`;
|
||||
} else {
|
||||
url = `${payService.callback}?${amountParam}${commentParam}`;
|
||||
}
|
||||
try {
|
||||
let rsp = await fetch(url);
|
||||
if (rsp.ok) {
|
||||
@ -235,4 +252,4 @@ export default function LNURLTip(props: LNURLTipProps) {
|
||||
</div>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import useEventPublisher from "Feed/EventPublisher";
|
||||
import { getReactions, hexToBech32, normalizeReaction, Reaction } from "Util";
|
||||
import { NoteCreator } from "Element/NoteCreator";
|
||||
import LNURLTip from "Element/LNURLTip";
|
||||
import { parseZap, ZapsSummary } from "Element/Zap";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import { default as NEvent } from "Nostr/Event";
|
||||
import { RootState } from "State/Store";
|
||||
@ -50,6 +51,9 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
const langNames = new Intl.DisplayNames([...window.navigator.languages], { type: "language" });
|
||||
const reactions = useMemo(() => getReactions(related, ev.Id, EventKind.Reaction), [related, ev]);
|
||||
const reposts = useMemo(() => getReactions(related, ev.Id, EventKind.Repost), [related, ev]);
|
||||
const zaps = useMemo(() => getReactions(related, ev.Id, EventKind.ZapReceipt).map(parseZap).filter(z => z.valid), [related]);
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0)
|
||||
const didZap = zaps.some(a => a.zapper === login);
|
||||
const groupReactions = useMemo(() => {
|
||||
return reactions?.reduce((acc, { content }) => {
|
||||
let r = normalizeReaction(content);
|
||||
@ -97,10 +101,11 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
if (service) {
|
||||
return (
|
||||
<>
|
||||
<div className="reaction-pill" onClick={() => setTip(true)}>
|
||||
<div className={`reaction-pill ${didZap ? 'reacted' : ''}`} onClick={() => setTip(true)}>
|
||||
<div className="reaction-pill-icon">
|
||||
<Zap />
|
||||
</div>
|
||||
{zapTotal > 0 && (<div className="reaction-pill-number">{formatShort(zapTotal)}</div>)}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@ -259,7 +264,7 @@ export default function NoteFooter(props: NoteFooterProps) {
|
||||
show={reply}
|
||||
setShow={setReply}
|
||||
/>
|
||||
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={() => setTip(false)} show={tip} />
|
||||
<LNURLTip svc={author?.lud16 || author?.lud06} onClose={() => setTip(false)} show={tip} author={author?.pubkey} note={ev.Id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
@ -7,6 +7,7 @@ import useTimelineFeed, { TimelineSubject } from "Feed/TimelineFeed";
|
||||
import { TaggedRawEvent } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import LoadMore from "Element/LoadMore";
|
||||
import Zap, { parseZap } from "Element/Zap";
|
||||
import Note from "Element/Note";
|
||||
import NoteReaction from "Element/NoteReaction";
|
||||
import useModeration from "Hooks/useModeration";
|
||||
@ -50,6 +51,9 @@ export default function Timeline({ subject, postsOnly = false, method, ignoreMod
|
||||
case EventKind.TextNote: {
|
||||
return <Note key={e.id} data={e} related={related.notes} ignoreModeration={ignoreModeration} />
|
||||
}
|
||||
case EventKind.ZapReceipt: {
|
||||
return <Zap zap={parseZap(e)} />
|
||||
}
|
||||
case EventKind.Reaction:
|
||||
case EventKind.Repost: {
|
||||
let eRef = e.tags.find(a => a[0] === "e")?.at(1);
|
||||
|
@ -144,6 +144,24 @@ export default function useEventPublisher() {
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
zap: async (author: HexKey, note?: HexKey, msg?: string) => {
|
||||
if (pubKey) {
|
||||
let ev = NEvent.ForPubKey(pubKey);
|
||||
ev.Kind = EventKind.ZapRequest;
|
||||
if (note) {
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(["e", note]))
|
||||
}
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(["p", author]))
|
||||
// @ts-ignore
|
||||
const relayTag = ['relays', ...Object.keys(relays)]
|
||||
// @ts-ignore
|
||||
ev.Tags.push(new Tag(relayTag))
|
||||
processContent(ev, msg || '');
|
||||
return await signEvent(ev);
|
||||
}
|
||||
},
|
||||
/**
|
||||
* Reply to a note
|
||||
*/
|
||||
|
@ -41,7 +41,7 @@ export default function useLoginFeed() {
|
||||
|
||||
let sub = new Subscriptions();
|
||||
sub.Id = "login:notifications";
|
||||
sub.Kinds = new Set([EventKind.TextNote]);
|
||||
sub.Kinds = new Set([EventKind.TextNote, EventKind.ZapReceipt]);
|
||||
sub.PTags = new Set([pubKey]);
|
||||
sub.Limit = 1;
|
||||
return sub;
|
||||
|
@ -31,7 +31,7 @@ export default function useThreadFeed(id: u256) {
|
||||
|
||||
// get replies to this event
|
||||
const subRelated = new Subscriptions();
|
||||
subRelated.Kinds = new Set(pref.enableReactions ? [EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost] : [EventKind.TextNote]);
|
||||
subRelated.Kinds = new Set(pref.enableReactions ? [EventKind.Reaction, EventKind.TextNote, EventKind.Deletion, EventKind.Repost, EventKind.ZapReceipt] : [EventKind.TextNote]);
|
||||
subRelated.ETags = thisSub.Ids;
|
||||
thisSub.AddSubscription(subRelated);
|
||||
|
||||
@ -56,4 +56,4 @@ export default function useThreadFeed(id: u256) {
|
||||
}, [main.store]);
|
||||
|
||||
return main.store;
|
||||
}
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ export default function useTimelineFeed(subject: TimelineSubject, options: Timel
|
||||
if (trackingEvents.length > 0 && pref.enableReactions) {
|
||||
sub = new Subscriptions();
|
||||
sub.Id = `timeline-related:${subject.type}`;
|
||||
sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion]);
|
||||
sub.Kinds = new Set([EventKind.Reaction, EventKind.Deletion, EventKind.ZapReceipt]);
|
||||
sub.ETags = new Set(trackingEvents);
|
||||
}
|
||||
return sub ?? null;
|
||||
|
17
src/Feed/ZapsFeed.ts
Normal file
17
src/Feed/ZapsFeed.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { useMemo } from "react";
|
||||
import { HexKey } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
import { Subscriptions } from "Nostr/Subscriptions";
|
||||
import useSubscription from "./Subscription";
|
||||
|
||||
export default function useZapsFeed(pubkey: HexKey) {
|
||||
const sub = useMemo(() => {
|
||||
let x = new Subscriptions();
|
||||
x.Id = `zaps:${pubkey}`;
|
||||
x.Kinds = new Set([EventKind.ZapReceipt]);
|
||||
x.PTags = new Set([pubkey]);
|
||||
return x;
|
||||
}, [pubkey]);
|
||||
|
||||
return useSubscription(sub, { leaveOpen: true, cache: true });
|
||||
}
|
@ -10,6 +10,8 @@ const enum EventKind {
|
||||
Reaction = 7, // NIP-25
|
||||
Auth = 22242, // NIP-42
|
||||
Lists = 30000, // NIP-51
|
||||
ZapRequest = 9734, // NIP tba
|
||||
ZapReceipt = 9735 // NIP tba
|
||||
};
|
||||
|
||||
export default EventKind;
|
||||
|
@ -213,3 +213,8 @@
|
||||
.qr-modal .modal-body {
|
||||
width: unset;
|
||||
}
|
||||
|
||||
.profile .zap-amount {
|
||||
font-weight: normal;
|
||||
margin-left: 4px;
|
||||
}
|
||||
|
@ -4,11 +4,14 @@ import { useEffect, useMemo, useState } from "react";
|
||||
import { useSelector } from "react-redux";
|
||||
import { useNavigate, useParams } from "react-router-dom";
|
||||
|
||||
import { formatShort } from "Number";
|
||||
import Link from "Icons/Link";
|
||||
import Qr from "Icons/Qr";
|
||||
import Zap from "Icons/Zap";
|
||||
import Envelope from "Icons/Envelope";
|
||||
import { useUserProfile } from "Feed/ProfileFeed";
|
||||
import useZapsFeed from "Feed/ZapsFeed";
|
||||
import { parseZap } from "Element/Zap";
|
||||
import FollowButton from "Element/FollowButton";
|
||||
import { extractLnAddress, parseId, hexToBech32 } from "Util";
|
||||
import Avatar from "Element/Avatar";
|
||||
@ -58,6 +61,11 @@ export default function ProfilePage() {
|
||||
const website_url = (user?.website && !user.website.startsWith("http"))
|
||||
? "https://" + user.website
|
||||
: user?.website || "";
|
||||
const zapFeed = useZapsFeed(id)
|
||||
const zaps = useMemo(() => {
|
||||
return zapFeed.store.notes.map(parseZap).filter(z => z.valid && !z.e && z.p === id)
|
||||
}, [zapFeed.store.notes, id])
|
||||
const zapsTotal = zaps.reduce((acc, z) => acc + z.amount, 0)
|
||||
|
||||
useEffect(() => {
|
||||
setTab(ProfileTab.Notes);
|
||||
@ -89,7 +97,7 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<LNURLTip svc={lnurl} show={showLnQr} onClose={() => setShowLnQr(false)} />
|
||||
<LNURLTip svc={lnurl} show={showLnQr} onClose={() => setShowLnQr(false)} author={id} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@ -163,6 +171,9 @@ export default function ProfilePage() {
|
||||
<>
|
||||
<IconButton onClick={() => setShowLnQr(true)}>
|
||||
<Zap width={14} height={16} />
|
||||
<span className="zap-amount">
|
||||
{zapsTotal > 0 && formatShort(zapsTotal)}
|
||||
</span>
|
||||
</IconButton>
|
||||
{!loggedOut && (
|
||||
<>
|
||||
|
@ -1,8 +1,13 @@
|
||||
import * as secp from "@noble/secp256k1";
|
||||
import { sha256 as hash } from '@noble/hashes/sha256';
|
||||
import { bech32 } from "bech32";
|
||||
import { HexKey, TaggedRawEvent, u256 } from "Nostr";
|
||||
import { HexKey, RawEvent, TaggedRawEvent, u256 } from "Nostr";
|
||||
import EventKind from "Nostr/EventKind";
|
||||
|
||||
export const sha256 = (str: string) => {
|
||||
return secp.utils.bytesToHex(hash(str))
|
||||
}
|
||||
|
||||
export async function openFile(): Promise<File | undefined> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let elm = document.createElement("input");
|
||||
@ -156,4 +161,4 @@ export function unixNow() {
|
||||
export function debounce(timeout: number, fn: () => void) {
|
||||
let t = setTimeout(fn, timeout);
|
||||
return () => clearTimeout(t);
|
||||
}
|
||||
}
|
||||
|
86
src/element/Zap.css
Normal file
86
src/element/Zap.css
Normal file
@ -0,0 +1,86 @@
|
||||
.zap {
|
||||
background-color: var(--note-bg);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-direction: column;
|
||||
padding: 10px;
|
||||
border-radius: 16px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.zap .summary {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.zap .body a {
|
||||
color: var(--highlight);
|
||||
}
|
||||
|
||||
.amount {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.amount:before {
|
||||
content: '⚡️ ';
|
||||
}
|
||||
|
||||
.top-zap .amount:before {
|
||||
content: '';
|
||||
}
|
||||
|
||||
.zaps-summary {
|
||||
margin-top: 8px;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.top-zap {
|
||||
font-size: 12px;
|
||||
border: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.top-zap .pfp {
|
||||
margin-right: .3em;
|
||||
}
|
||||
|
||||
.top-zap .summary {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.top-zap .avatar {
|
||||
width: 21px;
|
||||
height: 21px;
|
||||
}
|
||||
|
||||
.top-zap .profile-name {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.top-zap .nip05 {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.top-zap .amount {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.amount-number {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.rest-zaps {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.rest-zaps:before {
|
||||
content: ", ";
|
||||
}
|
153
src/element/Zap.tsx
Normal file
153
src/element/Zap.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import "./Zap.css";
|
||||
import { useMemo } from "react";
|
||||
// @ts-expect-error
|
||||
import { decode as invoiceDecode } from "light-bolt11-decoder";
|
||||
import { bytesToHex } from "@noble/hashes/utils";
|
||||
|
||||
import { sha256 } from "Util";
|
||||
import { formatShort } from "Number";
|
||||
import { HexKey, TaggedRawEvent } from "Nostr";
|
||||
import Event from "Nostr/Event";
|
||||
import Text from "Element/Text";
|
||||
import ProfileImage from "Element/ProfileImage";
|
||||
|
||||
function findTag(e: TaggedRawEvent, tag: string) {
|
||||
const maybeTag = e.tags.find((evTag) => {
|
||||
return evTag[0] === tag
|
||||
})
|
||||
return maybeTag && maybeTag[1]
|
||||
}
|
||||
|
||||
type Section = {
|
||||
name: string
|
||||
value?: any
|
||||
letters?: string
|
||||
}
|
||||
|
||||
function getSection(sections: Section[], name: string) {
|
||||
return sections.find((s) => s.name === name)
|
||||
}
|
||||
|
||||
function getInvoice(zap: TaggedRawEvent) {
|
||||
const bolt11 = findTag(zap, 'bolt11')
|
||||
const decoded = invoiceDecode(bolt11)
|
||||
|
||||
const amount = decoded.sections.find((section: any) => section.name === 'amount')?.value
|
||||
const hash = decoded.sections.find((section: any) => section.name === 'description_hash')?.value;
|
||||
|
||||
return { amount, hash: hash ? bytesToHex(hash) : undefined };
|
||||
}
|
||||
|
||||
function getZapper(zap: TaggedRawEvent, dhash: string) {
|
||||
const zapRequest = findTag(zap, 'description')
|
||||
if (zapRequest) {
|
||||
const rawEvent: TaggedRawEvent = JSON.parse(zapRequest);
|
||||
if (Array.isArray(rawEvent)) {
|
||||
// old format, ignored
|
||||
return;
|
||||
}
|
||||
const metaHash = sha256(zapRequest);
|
||||
const ev = new Event(rawEvent)
|
||||
return { pubkey: ev.PubKey, valid: metaHash == dhash };
|
||||
}
|
||||
}
|
||||
|
||||
interface ParsedZap {
|
||||
id: HexKey
|
||||
e?: HexKey
|
||||
p: HexKey
|
||||
amount: number
|
||||
content: string
|
||||
zapper?: HexKey
|
||||
valid: boolean
|
||||
}
|
||||
|
||||
export function parseZap(zap: TaggedRawEvent): ParsedZap {
|
||||
const { amount, hash } = getInvoice(zap)
|
||||
const zapper = hash ? getZapper(zap, hash) : { valid: false, pubkey: undefined };
|
||||
const e = findTag(zap, 'e')
|
||||
const p = findTag(zap, 'p')!
|
||||
return {
|
||||
id: zap.id,
|
||||
e,
|
||||
p,
|
||||
amount: Number(amount) / 1000,
|
||||
zapper: zapper?.pubkey,
|
||||
content: zap.content,
|
||||
valid: zapper?.valid ?? false,
|
||||
}
|
||||
}
|
||||
|
||||
const Zap = ({ zap }: { zap: ParsedZap }) => {
|
||||
const { amount, content, zapper, valid } = zap
|
||||
|
||||
return valid ? (
|
||||
<div className="zap">
|
||||
<div className="summary">
|
||||
{zapper && <ProfileImage pubkey={zapper} />}
|
||||
<div className="amount">
|
||||
<span className="amount-number">{formatShort(amount)}</span> sats
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">
|
||||
<Text
|
||||
creator={zapper!}
|
||||
content={content}
|
||||
tags={[]}
|
||||
users={new Map()}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
|
||||
interface ZapsSummaryProps { zaps: ParsedZap[] }
|
||||
|
||||
export const ZapsSummary = ({ zaps }: ZapsSummaryProps) => {
|
||||
const zapTotal = zaps.reduce((acc, z) => acc + z.amount, 0)
|
||||
|
||||
const topZap = zaps.length > 0 && zaps.reduce((acc, z) => {
|
||||
return z.amount > acc.amount ? z : acc
|
||||
})
|
||||
const restZaps = zaps.filter(z => topZap && z.id !== topZap.id)
|
||||
const restZapsTotal = restZaps.reduce((acc, z) => acc + z.amount, 0)
|
||||
const sortedZaps = useMemo(() => {
|
||||
const s = [...restZaps]
|
||||
s.sort((a, b) => b.amount - a.amount)
|
||||
return s
|
||||
}, [restZaps])
|
||||
const { zapper, amount, content, valid } = topZap || {}
|
||||
|
||||
return (
|
||||
<div className="zaps-summary">
|
||||
{amount && valid && zapper && (
|
||||
<div className={`top-zap`}>
|
||||
<div className="summary">
|
||||
<ProfileImage pubkey={zapper} />
|
||||
<div className="amount">
|
||||
zapped <span className="amount-number">{formatShort(amount)}</span> sats
|
||||
</div>
|
||||
</div>
|
||||
<div className="body">
|
||||
{content && (
|
||||
<Text
|
||||
creator={zapper}
|
||||
content={content}
|
||||
tags={[]}
|
||||
users={new Map()}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{restZapsTotal > 0 && (
|
||||
<div className="rest-zaps">
|
||||
{restZaps.length} other{restZaps.length > 1 ? 's' : ''} zapped
|
||||
<span className="amount-number">{formatShort(restZapsTotal)}</span> sats
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Zap
|
@ -1569,6 +1569,11 @@
|
||||
dependencies:
|
||||
eslint-scope "5.1.1"
|
||||
|
||||
"@noble/hashes@^1.2.0":
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@noble/hashes/-/hashes-1.2.0.tgz#a3150eeb09cc7ab207ebf6d7b9ad311a9bdbed12"
|
||||
integrity sha512-FZfhjEDbT5GRswV3C6uvLPHMiVD6lQBmpoX5+eSiPaMTXte/IKqI5dykDxzZB/WBeK/CDuQRBWarPdi3FNY2zQ==
|
||||
|
||||
"@noble/secp256k1@^1.7.0":
|
||||
version "1.7.1"
|
||||
resolved "https://registry.yarnpkg.com/@noble/secp256k1/-/secp256k1-1.7.1.tgz#b251c70f824ce3ca7f8dc3df08d58f005cc0507c"
|
||||
|
Loading…
x
Reference in New Issue
Block a user