mirror of
https://github.com/coracle-social/coracle.git
synced 2024-09-29 16:31:04 +00:00
Move zapper commands over
This commit is contained in:
parent
dd39413208
commit
44f65c4580
@ -42,7 +42,7 @@
|
||||
{:else if m.type === "note/create"}
|
||||
<NoteCreate pubkey={m.pubkey} quote={m.quote} writeTo={m.relays} />
|
||||
{: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"}
|
||||
<NotificationInfo zaps={m.zaps} likes={m.likes} replies={m.replies} />
|
||||
{:else if m.type === "thread/detail"}
|
||||
|
@ -57,7 +57,6 @@ export const Nip05 = engine.Nip05
|
||||
export const Nip24 = engine.Nip24
|
||||
export const Nip28 = engine.Nip28
|
||||
export const Nip44 = engine.Nip44
|
||||
export const Nip57 = engine.Nip57
|
||||
export const Nip59 = engine.Nip59
|
||||
export const Nip65 = engine.Nip65
|
||||
export const Outbox = engine.Outbox
|
||||
|
@ -15,8 +15,8 @@
|
||||
import PersonBadge from "src/app/shared/PersonBadge.svelte"
|
||||
import RelayCard from "src/app/shared/RelayCard.svelte"
|
||||
import {toastProgress} from "src/app/state"
|
||||
import {getUserRelayUrls} from "src/engine2"
|
||||
import {Env, Nip57, Builder, Nip65, Keys, Outbox, user} from "src/app/engine"
|
||||
import {zappers, getUserRelayUrls, processZaps} from "src/engine2"
|
||||
import {Env, Builder, Nip65, Keys, Outbox, user} from "src/app/engine"
|
||||
|
||||
export let note
|
||||
export let reply
|
||||
@ -24,7 +24,7 @@
|
||||
export let showEntire
|
||||
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 interpolate = (a, b) => t => a + Math.round((b - a) * t)
|
||||
const likesCount = tweened(0, {interpolate})
|
||||
@ -82,16 +82,14 @@
|
||||
$: allLikes = like ? likes.filter(n => n.id !== like?.id).concat(like) : likes
|
||||
$: $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)
|
||||
|
||||
$: $zapsTotal =
|
||||
sum(
|
||||
pluck(
|
||||
"invoiceAmount",
|
||||
zap
|
||||
? zaps.filter(n => n.id !== zap?.id).concat(Nip57.processZaps([zap], note.pubkey))
|
||||
: zaps
|
||||
zap ? zaps.filter(n => n.id !== zap?.id).concat(processZaps([zap], note.pubkey)) : zaps
|
||||
)
|
||||
) / 1000
|
||||
|
||||
|
@ -1,37 +1,37 @@
|
||||
<script>
|
||||
import {pipe, assoc, assocPath} from "ramda"
|
||||
import {modal} from "src/partials/state"
|
||||
import Popover from "src/partials/Popover.svelte"
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Content from "src/partials/Content.svelte"
|
||||
import Card from "src/partials/Card.svelte"
|
||||
import Heading from "src/partials/Heading.svelte"
|
||||
import {people} from "src/engine2"
|
||||
|
||||
document.title = "About"
|
||||
|
||||
const hash = import.meta.env.VITE_BUILD_HASH
|
||||
const nprofile =
|
||||
"nprofile1qqsf03c2gsmx5ef4c9zmxvlew04gdh7u94afnknp33qvv3c94kvwxgspz3mhxue69uhhyetvv9ujuerpd46hxtnfduq3xamnwvaz7tmjv4kxz7tpvfkx2tn0wfnszymhwden5te0dehhxarj9cmrswpwdaexwqgmwaehxw309a3ksunfwd68q6tvdshxummnw3erztnrdakszynhwden5te0danxvcmgv95kutnsw43qzrthwden5te0dehhxtnvdakqzynhwden5te0wp6hyurvv4cxzeewv4eszxrhwden5te0wfjkccte9eekummjwsh8xmmrd9skckx3ht0"
|
||||
const pubkey = "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96"
|
||||
const npub = "npub1jlrs53pkdfjnts29kveljul2sm0actt6n8dxrrzqcersttvcuv3qdjynqn"
|
||||
|
||||
// Provide complete details in case they haven't loaded coracle's profile
|
||||
const zap = () =>
|
||||
modal.push({
|
||||
type: "zap/create",
|
||||
pubkey: "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96",
|
||||
author: {
|
||||
pubkey: "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96",
|
||||
name: "Coracle",
|
||||
},
|
||||
zapper: {
|
||||
pubkey: "8ec86ac9e10979998652068ee6b00223b8e3265aabb3fe28fb6b3b6e294adc96",
|
||||
people.key(pubkey).update(
|
||||
pipe(
|
||||
assocPath(["profile", "name"], "Coracle"),
|
||||
assoc("zapper", {
|
||||
lnurl:
|
||||
"lnurl1dp68gurn8ghj7em909ek2u3wve6kuep09emk2mrv944kummhdchkcmn4wfk8qtmrdaexzcmvv5tqwy7g",
|
||||
callback: "https://api.geyser.fund/.well-known/lnurlp/coracle",
|
||||
nostrPubkey: "b6dcdddf86675287d1a4e8620d92aa905c258d850bf8cc923d39df1edfee5ee7",
|
||||
maxSendable: 5000000000,
|
||||
minSendable: 1000,
|
||||
},
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
const zap = () => modal.push({type: "zap/create", pubkey})
|
||||
</script>
|
||||
|
||||
<Content gap="8" class="gap-8">
|
||||
|
@ -7,13 +7,11 @@
|
||||
import Anchor from "src/partials/Anchor.svelte"
|
||||
import Input from "src/partials/Input.svelte"
|
||||
import Textarea from "src/partials/Textarea.svelte"
|
||||
import {getSetting, getUserRelayUrls} from "src/engine2"
|
||||
import {Directory, Nip65, Outbox, Network, Builder, Nip57} from "src/app/engine"
|
||||
import {getSetting, requestZap, collectInvoice, loadZapResponse} from "src/engine2"
|
||||
import {Directory} from "src/app/engine"
|
||||
|
||||
export let pubkey
|
||||
export let note = null
|
||||
export let zapper = null
|
||||
export let author = null
|
||||
|
||||
let sub
|
||||
let zap = {
|
||||
@ -25,27 +23,15 @@
|
||||
confirmed: false,
|
||||
}
|
||||
|
||||
const _zapper = zapper || Nip57.zappers.key(pubkey).get()
|
||||
const _author = author || Directory.getProfile(pubkey)
|
||||
|
||||
const loadZapInvoice = async () => {
|
||||
zap.loading = true
|
||||
|
||||
const amount = zap.amount * 1000
|
||||
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,
|
||||
const {invoice, relays} = await requestZap({
|
||||
pubkey,
|
||||
note?.id,
|
||||
amount,
|
||||
_zapper.lnurl
|
||||
)
|
||||
const signedEvent = await Outbox.prep(rawEvent)
|
||||
const invoice = await Nip57.fetchInvoice(_zapper, signedEvent, amount)
|
||||
event: note,
|
||||
amount: zap.amount,
|
||||
content: zap.message,
|
||||
})
|
||||
|
||||
// If they closed the dialog before fetch resolved, we're done
|
||||
if (!zap) {
|
||||
@ -55,21 +41,14 @@
|
||||
zap.invoice = invoice
|
||||
zap.loading = false
|
||||
|
||||
await Nip57.collectInvoice(invoice)
|
||||
await collectInvoice(invoice)
|
||||
|
||||
// Listen for the zap confirmation
|
||||
sub = Network.subscribe({
|
||||
relays,
|
||||
filter: {
|
||||
kinds: [9735],
|
||||
authors: [_zapper.nostrPubkey],
|
||||
"#p": [pubkey],
|
||||
since: zap.startedAt - 10,
|
||||
},
|
||||
onEvent: event => {
|
||||
sub = loadZapResponse({relays, pubkey})
|
||||
|
||||
sub.on("event", event => {
|
||||
zap.confirmed = true
|
||||
setTimeout(() => modal.pop(), 1000)
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
@ -81,7 +60,7 @@
|
||||
<Content size="lg">
|
||||
<div class="text-center">
|
||||
<h1 class="staatliches text-2xl">Send a zap</h1>
|
||||
<p>to {Directory.displayProfile(_author)}</p>
|
||||
<p>to {Directory.displayPubkey(pubkey)}</p>
|
||||
</div>
|
||||
{#if zap.confirmed}
|
||||
<div class="flex items-center justify-center gap-2 text-gray-1">
|
||||
|
@ -12,7 +12,6 @@ import {Nip05} from "./components/Nip05"
|
||||
import {Nip24} from "./components/Nip24"
|
||||
import {Nip28} from "./components/Nip28"
|
||||
import {Nip44} from "./components/Nip44"
|
||||
import {Nip57} from "./components/Nip57"
|
||||
import {Nip59} from "./components/Nip59"
|
||||
import {Nip65} from "./components/Nip65"
|
||||
import {Outbox} from "./components/Outbox"
|
||||
@ -33,7 +32,6 @@ export class Engine {
|
||||
Nip24 = new Nip24()
|
||||
Nip28 = new Nip28()
|
||||
Nip44 = new Nip44()
|
||||
Nip57 = new Nip57()
|
||||
Nip59 = new Nip59()
|
||||
Nip65 = new Nip65()
|
||||
Outbox = new Outbox()
|
||||
|
@ -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(),
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
@ -13,7 +13,6 @@ export {Nip04} from "./components/Nip04"
|
||||
export {Nip05} from "./components/Nip05"
|
||||
export {Nip28} from "./components/Nip28"
|
||||
export {Nip44} from "./components/Nip44"
|
||||
export {Nip57} from "./components/Nip57"
|
||||
export {Nip65} from "./components/Nip65"
|
||||
|
||||
export {User} from "./util/User"
|
||||
|
@ -4,4 +4,5 @@ export * from "./nip04"
|
||||
export * from "./nip24"
|
||||
export * from "./nip28"
|
||||
export * from "./nip32"
|
||||
export * from "./nip57"
|
||||
export * from "./nip95"
|
||||
|
56
src/engine2/commands/nip57.ts
Normal file
56
src/engine2/commands/nip57.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
import {Fetch} from "hurdak"
|
||||
import {tryJson} from "src/util/misc"
|
||||
import {invoiceAmount} from "src/util/lightning"
|
||||
import {warn} from "src/util/logger"
|
||||
import {Tags} from "src/util/nostr"
|
||||
import type {Event, ZapEvent} from "src/engine2/model"
|
||||
import {people} from "src/engine2/state"
|
||||
@ -60,29 +58,3 @@ export function processZaps(zaps: Event[], pubkey: string) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
// 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.
|
||||
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 hintGroups = pubkeys.map(pubkey => getPubkeyRelayUrls(pubkey, RelayMode.Read))
|
||||
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[]) =>
|
||||
|
@ -5,4 +5,5 @@ export * from "./pubkeys"
|
||||
export * from "./subscription"
|
||||
export * from "./thread"
|
||||
export * from "./nip04"
|
||||
export * from "./nip57"
|
||||
export * from "./nip59"
|
||||
|
19
src/engine2/requests/nip57.ts
Normal file
19
src/engine2/requests/nip57.ts
Normal 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,
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user