Move zapper commands over

This commit is contained in:
Jonathan Staab 2023-09-09 16:40:02 -07:00
parent dd39413208
commit 44f65c4580
14 changed files with 111 additions and 236 deletions

View File

@ -42,7 +42,7 @@
{:else if m.type === "note/create"} {:else if m.type === "note/create"}
<NoteCreate pubkey={m.pubkey} quote={m.quote} writeTo={m.relays} /> <NoteCreate pubkey={m.pubkey} quote={m.quote} writeTo={m.relays} />
{:else if m.type === "zap/create"} {:else if m.type === "zap/create"}
<ZapModal pubkey={m.pubkey} note={m.note} author={m.author} zapper={m.zapper} /> <ZapModal pubkey={m.pubkey} note={m.note} />
{:else if m.type === "notification/info"} {:else if m.type === "notification/info"}
<NotificationInfo zaps={m.zaps} likes={m.likes} replies={m.replies} /> <NotificationInfo zaps={m.zaps} likes={m.likes} replies={m.replies} />
{:else if m.type === "thread/detail"} {:else if m.type === "thread/detail"}

View File

@ -57,7 +57,6 @@ export const Nip05 = engine.Nip05
export const Nip24 = engine.Nip24 export const Nip24 = engine.Nip24
export const Nip28 = engine.Nip28 export const Nip28 = engine.Nip28
export const Nip44 = engine.Nip44 export const Nip44 = engine.Nip44
export const Nip57 = engine.Nip57
export const Nip59 = engine.Nip59 export const Nip59 = engine.Nip59
export const Nip65 = engine.Nip65 export const Nip65 = engine.Nip65
export const Outbox = engine.Outbox export const Outbox = engine.Outbox

View File

@ -15,8 +15,8 @@
import PersonBadge from "src/app/shared/PersonBadge.svelte" import PersonBadge from "src/app/shared/PersonBadge.svelte"
import RelayCard from "src/app/shared/RelayCard.svelte" import RelayCard from "src/app/shared/RelayCard.svelte"
import {toastProgress} from "src/app/state" import {toastProgress} from "src/app/state"
import {getUserRelayUrls} from "src/engine2" import {zappers, getUserRelayUrls, processZaps} from "src/engine2"
import {Env, Nip57, Builder, Nip65, Keys, Outbox, user} from "src/app/engine" import {Env, Builder, Nip65, Keys, Outbox, user} from "src/app/engine"
export let note export let note
export let reply export let reply
@ -24,7 +24,7 @@
export let showEntire export let showEntire
export let setFeedRelay export let setFeedRelay
const zapper = Nip57.zappers.key(note.pubkey) const zapper = zappers.key(note.pubkey)
const nevent = nip19.neventEncode({id: note.id, relays: [note.seen_on]}) const nevent = nip19.neventEncode({id: note.id, relays: [note.seen_on]})
const interpolate = (a, b) => t => a + Math.round((b - a) * t) const interpolate = (a, b) => t => a + Math.round((b - a) * t)
const likesCount = tweened(0, {interpolate}) const likesCount = tweened(0, {interpolate})
@ -82,16 +82,14 @@
$: allLikes = like ? likes.filter(n => n.id !== like?.id).concat(like) : likes $: allLikes = like ? likes.filter(n => n.id !== like?.id).concat(like) : likes
$: $likesCount = allLikes.length $: $likesCount = allLikes.length
$: zaps = Nip57.processZaps(note.zaps, note.pubkey) $: zaps = processZaps(note.zaps, note.pubkey)
$: zap = zap || find(z => z.request.pubkey === Keys.pubkey.get(), zaps) $: zap = zap || find(z => z.request.pubkey === Keys.pubkey.get(), zaps)
$: $zapsTotal = $: $zapsTotal =
sum( sum(
pluck( pluck(
"invoiceAmount", "invoiceAmount",
zap zap ? zaps.filter(n => n.id !== zap?.id).concat(processZaps([zap], note.pubkey)) : zaps
? zaps.filter(n => n.id !== zap?.id).concat(Nip57.processZaps([zap], note.pubkey))
: zaps
) )
) / 1000 ) / 1000

View File

@ -1,37 +1,37 @@
<script> <script>
import {pipe, assoc, assocPath} from "ramda"
import {modal} from "src/partials/state" import {modal} from "src/partials/state"
import Popover from "src/partials/Popover.svelte" import Popover from "src/partials/Popover.svelte"
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Content from "src/partials/Content.svelte" import Content from "src/partials/Content.svelte"
import Card from "src/partials/Card.svelte" import Card from "src/partials/Card.svelte"
import Heading from "src/partials/Heading.svelte" import Heading from "src/partials/Heading.svelte"
import {people} from "src/engine2"
document.title = "About" document.title = "About"
const hash = import.meta.env.VITE_BUILD_HASH const hash = import.meta.env.VITE_BUILD_HASH
const nprofile = const nprofile =
"nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3xamnwvaz7tmjv4kxz7tpvfkx2tn0wfnszymhwden5te0dehhxarj9cmrswpwdaexwqgmwaehxw309a3ksunfwd68q6tvdshxummnw3erztnrdakszynhwden5te0danxvcmgv95kutnsw43qzrthwden5te0dehhxtnvdakqzynhwden5te0wp6hyurvv4cxzeewv4eszxrhwden5te0wfjkccte9eekummjwsh8xmmrd9skckx3ht0" "nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3xamnwvaz7tmjv4kxz7tpvfkx2tn0wfnszymhwden5te0dehhxarj9cmrswpwdaexwqgmwaehxw309a3ksunfwd68q6tvdshxummnw3erztnrdakszynhwden5te0danxvcmgv95kutnsw43qzrthwden5te0dehhxtnvdakqzynhwden5te0wp6hyurvv4cxzeewv4eszxrhwden5te0wfjkccte9eekummjwsh8xmmrd9skckx3ht0"
const pubkey = "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96"
const npub = "npub1jlrs53pkdfjnts29kveljul2sm0actt6n8dxrrzqcersttvcuv3qdjynqn" const npub = "npub1jlrs53pkdfjnts29kveljul2sm0actt6n8dxrrzqcersttvcuv3qdjynqn"
// Provide complete details in case they haven't loaded coracle's profile // Provide complete details in case they haven't loaded coracle's profile
const zap = () => people.key(pubkey).update(
modal.push({ pipe(
type: "zap/create", assocPath(["profile", "name"], "Coracle"),
pubkey: "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96", assoc("zapper", {
author: {
pubkey: "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96",
name: "Coracle",
},
zapper: {
pubkey: "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96",
lnurl: lnurl:
"lnurl1dp68gurn8ghj7em909ek2u3wve6kuep09emk2mrv944kummhdchkcmn4wfk8qtmrdaexzcmvv5tqwy7g", "lnurl1dp68gurn8ghj7em909ek2u3wve6kuep09emk2mrv944kummhdchkcmn4wfk8qtmrdaexzcmvv5tqwy7g",
callback: "https://api.geyser.fund/.well-known/lnurlp/coracle", callback: "https://api.geyser.fund/.well-known/lnurlp/coracle",
nostrPubkey: "b6dcdddf86675287d1a4e8620d92aa905c258d850bf8cc923d39df1edfee5ee7", nostrPubkey: "b6dcdddf86675287d1a4e8620d92aa905c258d850bf8cc923d39df1edfee5ee7",
maxSendable: 5000000000, maxSendable: 5000000000,
minSendable: 1000, minSendable: 1000,
},
}) })
)
)
const zap = () => modal.push({type: "zap/create", pubkey})
</script> </script>
<Content gap="8" class="gap-8"> <Content gap="8" class="gap-8">

View File

@ -7,13 +7,11 @@
import Anchor from "src/partials/Anchor.svelte" import Anchor from "src/partials/Anchor.svelte"
import Input from "src/partials/Input.svelte" import Input from "src/partials/Input.svelte"
import Textarea from "src/partials/Textarea.svelte" import Textarea from "src/partials/Textarea.svelte"
import {getSetting, getUserRelayUrls} from "src/engine2" import {getSetting, requestZap, collectInvoice, loadZapResponse} from "src/engine2"
import {Directory, Nip65, Outbox, Network, Builder, Nip57} from "src/app/engine" import {Directory} from "src/app/engine"
export let pubkey export let pubkey
export let note = null export let note = null
export let zapper = null
export let author = null
let sub let sub
let zap = { let zap = {
@ -25,27 +23,15 @@
confirmed: false, confirmed: false,
} }
const _zapper = zapper || Nip57.zappers.key(pubkey).get()
const _author = author || Directory.getProfile(pubkey)
const loadZapInvoice = async () => { const loadZapInvoice = async () => {
zap.loading = true zap.loading = true
const amount = zap.amount * 1000 const {invoice, relays} = await requestZap({
const relayLimit = getSetting("relay_limit")
const relays = note
? Nip65.getPublishHints(relayLimit, note, getUserRelayUrls("write"))
: Nip65.getPubkeyHints(relayLimit, pubkey, "read")
const rawEvent = Builder.requestZap(
relays,
zap.message,
pubkey, pubkey,
note?.id, event: note,
amount, amount: zap.amount,
_zapper.lnurl content: zap.message,
) })
const signedEvent = await Outbox.prep(rawEvent)
const invoice = await Nip57.fetchInvoice(_zapper, signedEvent, amount)
// If they closed the dialog before fetch resolved, we're done // If they closed the dialog before fetch resolved, we're done
if (!zap) { if (!zap) {
@ -55,21 +41,14 @@
zap.invoice = invoice zap.invoice = invoice
zap.loading = false zap.loading = false
await Nip57.collectInvoice(invoice) await collectInvoice(invoice)
// Listen for the zap confirmation // Listen for the zap confirmation
sub = Network.subscribe({ sub = loadZapResponse({relays, pubkey})
relays,
filter: { sub.on("event", event => {
kinds: [9735],
authors: [_zapper.nostrPubkey],
"#p": [pubkey],
since: zap.startedAt - 10,
},
onEvent: event => {
zap.confirmed = true zap.confirmed = true
setTimeout(() => modal.pop(), 1000) setTimeout(() => modal.pop(), 1000)
},
}) })
} }
@ -81,7 +60,7 @@
<Content size="lg"> <Content size="lg">
<div class="text-center"> <div class="text-center">
<h1 class="staatliches text-2xl">Send a zap</h1> <h1 class="staatliches text-2xl">Send a zap</h1>
<p>to {Directory.displayProfile(_author)}</p> <p>to {Directory.displayPubkey(pubkey)}</p>
</div> </div>
{#if zap.confirmed} {#if zap.confirmed}
<div class="flex items-center justify-center gap-2 text-gray-1"> <div class="flex items-center justify-center gap-2 text-gray-1">

View File

@ -12,7 +12,6 @@ import {Nip05} from "./components/Nip05"
import {Nip24} from "./components/Nip24" import {Nip24} from "./components/Nip24"
import {Nip28} from "./components/Nip28" import {Nip28} from "./components/Nip28"
import {Nip44} from "./components/Nip44" import {Nip44} from "./components/Nip44"
import {Nip57} from "./components/Nip57"
import {Nip59} from "./components/Nip59" import {Nip59} from "./components/Nip59"
import {Nip65} from "./components/Nip65" import {Nip65} from "./components/Nip65"
import {Outbox} from "./components/Outbox" import {Outbox} from "./components/Outbox"
@ -33,7 +32,6 @@ export class Engine {
Nip24 = new Nip24() Nip24 = new Nip24()
Nip28 = new Nip28() Nip28 = new Nip28()
Nip44 = new Nip44() Nip44 = new Nip44()
Nip57 = new Nip57()
Nip59 = new Nip59() Nip59 = new Nip59()
Nip65 = new Nip65() Nip65 = new Nip65()
Outbox = new Outbox() Outbox = new Outbox()

View File

@ -1,147 +0,0 @@
import {Fetch, tryFunc} from "hurdak"
import {now, tryJson, hexToBech32, bech32ToHex} from "src/util/misc"
import {invoiceAmount} from "src/util/lightning"
import {warn} from "src/util/logger"
import {Tags} from "src/util/nostr"
import type {Engine} from "src/engine/Engine"
import type {Zapper, ZapEvent, Event} from "src/engine/types"
import {collection} from "src/engine/util/store"
export class Nip57 {
zappers = collection<Zapper>("pubkey")
processZaps = (zaps: Event[], pubkey: string) => {
const zapper = this.zappers.key(pubkey).get()
if (!zapper) {
return []
}
return zaps
.map((zap: Event) => {
const zapMeta = Tags.from(zap).asMeta() as {
bolt11: string
description: string
}
return tryJson(() => ({
...zap,
invoiceAmount: invoiceAmount(zapMeta.bolt11),
request: JSON.parse(zapMeta.description),
})) as ZapEvent
})
.filter((zap: ZapEvent) => {
if (!zap) {
return false
}
// Don't count zaps that the user sent himself
if (zap.request.pubkey === pubkey) {
return false
}
const {invoiceAmount, request} = zap
const reqMeta = Tags.from(request).asMeta() as {
amount?: string
lnurl?: string
}
// Verify that the zapper actually sent the requested amount (if it was supplied)
if (reqMeta.amount && parseInt(reqMeta.amount) !== invoiceAmount) {
return false
}
// If the sending client provided an lnurl tag, verify that too
if (reqMeta.lnurl && reqMeta.lnurl !== zapper.lnurl) {
return false
}
// Verify that the zap note actually came from the recipient's zapper
if (zapper.nostrPubkey !== zap.pubkey) {
return false
}
return true
})
}
getLnUrl(address: string): string {
// Try to parse it as a lud06 LNURL
if (address.startsWith("lnurl1")) {
return tryFunc(() => bech32ToHex(address)) as string
}
// Try to parse it as a lud16 address
if (address.includes("@")) {
const [name, domain] = address.split("@")
if (domain && name) {
return `https://${domain}/.well-known/lnurlp/${name}`
}
}
}
async fetchInvoice(zapper, event, amount) {
const {callback, lnurl} = zapper
const s = encodeURI(JSON.stringify(event))
const res = await Fetch.fetchJson(`${callback}?amount=${amount}&nostr=${s}&lnurl=${lnurl}`)
if (!res.pr) {
warn(JSON.stringify(res))
}
return res?.pr
}
async collectInvoice(invoice) {
const {webln} = window as {webln?: any}
if (webln) {
await webln.enable()
try {
webln.sendPayment(invoice)
} catch (e) {
warn(e)
}
}
}
initialize(engine: Engine) {
engine.Events.addHandler(0, (e: Event) => {
tryJson(async () => {
const kind0 = JSON.parse(e.content)
const zapper = this.zappers.key(e.pubkey)
const address = (kind0.lud16 || kind0.lud06 || "").toLowerCase()
if (!address || e.created_at < zapper.get()?.created_at) {
return
}
const lnurl = this.getLnUrl(address)
if (!lnurl) {
return
}
const url = engine.Settings.dufflepud("zapper/info")
const result = (await tryFunc(() => Fetch.postJson(url, {lnurl}))) as any
if (!result?.allowsNostr || !result?.nostrPubkey) {
return
}
zapper.set({
pubkey: e.pubkey,
lnurl: hexToBech32("lnurl", lnurl),
callback: result.callback,
minSendable: result.minSendable,
maxSendable: result.maxSendable,
nostrPubkey: result.nostrPubkey,
created_at: e.created_at,
updated_at: now(),
})
})
})
}
}

View File

@ -13,7 +13,6 @@ export {Nip04} from "./components/Nip04"
export {Nip05} from "./components/Nip05" export {Nip05} from "./components/Nip05"
export {Nip28} from "./components/Nip28" export {Nip28} from "./components/Nip28"
export {Nip44} from "./components/Nip44" export {Nip44} from "./components/Nip44"
export {Nip57} from "./components/Nip57"
export {Nip65} from "./components/Nip65" export {Nip65} from "./components/Nip65"
export {User} from "./util/User" export {User} from "./util/User"

View File

@ -4,4 +4,5 @@ export * from "./nip04"
export * from "./nip24" export * from "./nip24"
export * from "./nip28" export * from "./nip28"
export * from "./nip32" export * from "./nip32"
export * from "./nip57"
export * from "./nip95" export * from "./nip95"

View File

@ -0,0 +1,56 @@
import {Fetch} from "hurdak"
import {warn} from "src/util/logger"
import {people} from "src/engine2/state"
import {signer, getSetting, getPubkeyHints, getPublishHints} from "src/engine2/queries"
import {buildEvent} from "./util"
export async function requestZap({content, amount, pubkey, event}) {
const person = people.key(pubkey).get()
if (!person?.zapper) {
throw new Error("Can't zap a person without a zapper")
}
const {callback, lnurl} = person.zapper
const msats = amount * 1000
const relays = event
? getPublishHints(getSetting("relay_limit"), event)
: getPubkeyHints(getSetting("relay_limit"), pubkey, "read")
const tags = [
["relays", ...relays],
["amount", msats.toString()],
["lnurl", lnurl],
["p", pubkey],
]
if (event) {
tags.push(["e", event.id])
}
const zap = signer.get().prepAsUser(buildEvent(9734, {content, tags}))
const zapString = encodeURI(JSON.stringify(zap))
const res = await Fetch.fetchJson(
`${callback}?amount=${amount}&nostr=${zapString}&lnurl=${lnurl}`
)
if (!res.pr) {
warn(JSON.stringify(res))
}
return {relays, invoice: res?.pr}
}
export async function collectInvoice(invoice) {
const {webln} = window as {webln?: any}
if (webln) {
await webln.enable()
try {
webln.sendPayment(invoice)
} catch (e) {
warn(e)
}
}
}

View File

@ -1,7 +1,5 @@
import {Fetch} from "hurdak"
import {tryJson} from "src/util/misc" import {tryJson} from "src/util/misc"
import {invoiceAmount} from "src/util/lightning" import {invoiceAmount} from "src/util/lightning"
import {warn} from "src/util/logger"
import {Tags} from "src/util/nostr" import {Tags} from "src/util/nostr"
import type {Event, ZapEvent} from "src/engine2/model" import type {Event, ZapEvent} from "src/engine2/model"
import {people} from "src/engine2/state" import {people} from "src/engine2/state"
@ -60,29 +58,3 @@ export function processZaps(zaps: Event[], pubkey: string) {
return true return true
}) })
} }
export async function fetchInvoice(zapper, event, amount) {
const {callback, lnurl} = zapper
const s = encodeURI(JSON.stringify(event))
const res = await Fetch.fetchJson(`${callback}?amount=${amount}&nostr=${s}&lnurl=${lnurl}`)
if (!res.pr) {
warn(JSON.stringify(res))
}
return res?.pr
}
export async function collectInvoice(invoice) {
const {webln} = window as {webln?: any}
if (webln) {
await webln.enable()
try {
webln.sendPayment(invoice)
} catch (e) {
warn(e)
}
}
}

View File

@ -125,12 +125,12 @@ export const getRootHints = hintSelector(function* (event) {
// anyone else who is tagged in the original event or the reply. Get everyone's read // anyone else who is tagged in the original event or the reply. Get everyone's read
// relays. Limit how many per pubkey we publish to though. We also want to advertise // relays. Limit how many per pubkey we publish to though. We also want to advertise
// our content to our followers, so publish to our write relays as well. // our content to our followers, so publish to our write relays as well.
export const getPublishHints = (limit: number, event: Event, extraRelays: string[] = []) => { export const getPublishHints = (limit: number, event: Event) => {
const pubkeys = Tags.from(event).type("p").values().all() const pubkeys = Tags.from(event).type("p").values().all()
const hintGroups = pubkeys.map(pubkey => getPubkeyRelayUrls(pubkey, RelayMode.Read)) const hintGroups = pubkeys.map(pubkey => getPubkeyRelayUrls(pubkey, RelayMode.Read))
const authorRelays = getPubkeyRelayUrls(event.pubkey, RelayMode.Write) const authorRelays = getPubkeyRelayUrls(event.pubkey, RelayMode.Write)
return mergeHints(limit, hintGroups.concat([extraRelays, authorRelays])) return mergeHints(limit, [...hintGroups, authorRelays, getUserRelayUrls(RelayMode.Write)])
} }
export const getInboxHints = (limit: number, pubkeys: string[]) => export const getInboxHints = (limit: number, pubkeys: string[]) =>

View File

@ -5,4 +5,5 @@ export * from "./pubkeys"
export * from "./subscription" export * from "./subscription"
export * from "./thread" export * from "./thread"
export * from "./nip04" export * from "./nip04"
export * from "./nip57"
export * from "./nip59" export * from "./nip59"

View File

@ -0,0 +1,19 @@
import {now} from "src/util/misc"
import {people} from "src/engine2/state"
import {Subscription} from "./subscription"
export function loadZapResponse({pubkey, relays}) {
const {zapper} = people.key(pubkey).get()
return new Subscription({
relays,
filters: [
{
kinds: [9735],
authors: [zapper.nostrPubkey],
"#p": [pubkey],
since: now() - 10,
},
],
})
}