Small fixes, rework zaps
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import {fetchJson, last} from "@welshman/lib"
|
||||
import type {Maybe} from "@welshman/lib"
|
||||
|
||||
/**
|
||||
* NIP-05: mapping nostr public keys to DNS-based internet identifiers (e.g.
|
||||
* `name@example.com`), resolved via each domain's `/.well-known/nostr.json`.
|
||||
*/
|
||||
export type Handle = {
|
||||
nip05: string
|
||||
pubkey?: string
|
||||
nip46?: string[]
|
||||
relays?: string[]
|
||||
}
|
||||
|
||||
export async function queryProfile(nip05: string): Promise<Maybe<Handle>> {
|
||||
const parts = nip05.split("@")
|
||||
const name = parts.length > 1 ? parts[0] : "_"
|
||||
const domain = last(parts)
|
||||
|
||||
try {
|
||||
const {
|
||||
names,
|
||||
relays = {},
|
||||
nip46 = {},
|
||||
} = await fetchJson(`https://${domain}/.well-known/nostr.json?name=${name}`)
|
||||
|
||||
const pubkey = names[name]
|
||||
|
||||
if (!pubkey) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
return {
|
||||
nip05,
|
||||
pubkey,
|
||||
nip46: nip46[pubkey],
|
||||
relays: relays[pubkey],
|
||||
}
|
||||
} catch (_e) {
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
|
||||
export const displayNip05 = (nip05: string) =>
|
||||
nip05?.startsWith("_@") ? last(nip05.split("@")) : nip05
|
||||
|
||||
export const displayHandle = (handle: Handle) => displayNip05(handle.nip05)
|
||||
@@ -123,7 +123,7 @@ export const ROOM_JOIN = 9021
|
||||
export const ROOM_LEAVE = 9022
|
||||
export const ZAP_GOAL = 9041
|
||||
export const ZAP_REQUEST = 9734
|
||||
export const ZAP_RESPONSE = 9735
|
||||
export const ZAP_RECEIPT = 9735
|
||||
export const HIGHLIGHT = 9802
|
||||
export const MUTES = 10000
|
||||
export const PINS = 10001
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {now, tryCatch, fetchJson, hexToBech32, fromPairs} from "@welshman/lib"
|
||||
import {ZAP_RESPONSE, ZAP_REQUEST} from "./Kinds.js"
|
||||
import {now, tryCatch, fetchJson, hexToBech32, fromPairs, sum, allPass, nthEq, nth} from "@welshman/lib"
|
||||
import {ZAP_RECEIPT, ZAP_REQUEST} from "./Kinds.js"
|
||||
import {getTagValue} from "./Tags.js"
|
||||
import type {Filter} from "./Filters.js"
|
||||
import type {TrustedEvent, SignedEvent} from "./Events.js"
|
||||
@@ -138,6 +138,79 @@ export const zapFromEvent = (response: TrustedEvent, zapper: Zapper | undefined)
|
||||
return zap
|
||||
}
|
||||
|
||||
/**
|
||||
* A single recipient of an event's zaps, parsed from a NIP-57 Appendix G `zap`
|
||||
* tag of the form `["zap", <pubkey>, <relay hint>, <weight>]`.
|
||||
*/
|
||||
export type ZapSplit = {
|
||||
pubkey: string
|
||||
relay?: string
|
||||
weight: number
|
||||
}
|
||||
|
||||
export type ZapSplitAmount = ZapSplit & {amount: number}
|
||||
|
||||
const parseWeight = (weight: string | undefined) => {
|
||||
const n = weight === undefined ? NaN : parseFloat(weight)
|
||||
|
||||
return Number.isFinite(n) && n > 0 ? n : 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an event's zap-split recipients per NIP-57 Appendix G:
|
||||
*
|
||||
* - With no `zap` tags the whole zap goes to the event's author.
|
||||
* - If no recipient carries a weight, the zap is split equally (weight 1 each).
|
||||
* - If weights are only partially present, unweighted recipients drop to weight
|
||||
* 0 (i.e. they should not be zapped).
|
||||
*
|
||||
* Weight-0 recipients are still returned so callers have the full recipient set;
|
||||
* they simply receive 0 from `splitZapAmount`.
|
||||
*/
|
||||
export const getZapSplits = (event: TrustedEvent): ZapSplit[] => {
|
||||
const zapTags = event.tags.filter(allPass(nthEq(0, "zap"), nth(1)))
|
||||
|
||||
if (zapTags.length === 0) {
|
||||
return [{pubkey: event.pubkey, weight: 1}]
|
||||
}
|
||||
|
||||
const anyWeighted = zapTags.some(nth(3))
|
||||
|
||||
return zapTags.map(([, pubkey, relay, weight]) => ({
|
||||
pubkey,
|
||||
relay: relay || undefined,
|
||||
weight: anyWeighted ? parseWeight(weight) : 1,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Divide `total` (in any integer unit, e.g. millisats) across an event's
|
||||
* zap-split recipients proportionally to their weights. Any rounding remainder
|
||||
* is handed to the highest-weighted recipient so the parts sum back to exactly
|
||||
* `total`. If every weight is 0, nobody is zapped.
|
||||
*/
|
||||
export const splitZapAmount = (event: TrustedEvent, total: number): ZapSplitAmount[] => {
|
||||
const splits = getZapSplits(event)
|
||||
const totalWeight = sum(splits.map(split => split.weight))
|
||||
|
||||
if (totalWeight === 0) {
|
||||
return splits.map(split => ({...split, amount: 0}))
|
||||
}
|
||||
|
||||
const amounts = splits.map(split => Math.floor((total * split.weight) / totalWeight))
|
||||
|
||||
let maxIndex = 0
|
||||
splits.forEach((split, i) => {
|
||||
if (split.weight > splits[maxIndex].weight) {
|
||||
maxIndex = i
|
||||
}
|
||||
})
|
||||
|
||||
amounts[maxIndex] += total - sum(amounts)
|
||||
|
||||
return splits.map((split, i) => ({...split, amount: amounts[i]}))
|
||||
}
|
||||
|
||||
export type ZapRequestParams = {
|
||||
msats: number
|
||||
zapper: Zapper
|
||||
@@ -201,7 +274,7 @@ export const getZapResponseFilter = ({zapper, pubkey, eventId}: ZapResponseFilte
|
||||
}
|
||||
|
||||
const filter: Filter = {
|
||||
kinds: [ZAP_RESPONSE],
|
||||
kinds: [ZAP_RECEIPT],
|
||||
authors: [zapper.nostrPubkey],
|
||||
since: now() - 30,
|
||||
"#p": [pubkey],
|
||||
|
||||
@@ -4,6 +4,7 @@ export * from "./Encryptable.js"
|
||||
export * from "./Events.js"
|
||||
export * from "./Filters.js"
|
||||
export * from "./Handler.js"
|
||||
export * from "./Handles.js"
|
||||
export * from "./Keys.js"
|
||||
export * from "./Kinds.js"
|
||||
export * from "./Links.js"
|
||||
|
||||
Reference in New Issue
Block a user