mirror of
https://github.com/purrgrammer/grimoire.git
synced 2026-04-08 22:47:02 +02:00
Add anonymous zap option with throwaway signer (#154)
* feat: add anonymous zap option Add "Zap anonymously" checkbox that allows users to send zaps without revealing their identity. When enabled, creates a throwaway keypair to sign the zap request instead of using the active account's signer. This also enables users without a signer account to send zaps by checking the anonymous option. * feat: prioritize recipient's inbox relays for zap receipts Add selectZapRelays utility that properly selects relays for zap receipt publication with the following priority: 1. Recipient's inbox relays (so they see the zap) 2. Sender's inbox relays (so sender can verify) 3. Fallback aggregator relays This ensures zap receipts are published where recipients will actually see them, rather than just the sender's relays. Includes comprehensive tests for relay selection logic. --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -21,10 +21,14 @@ import {
|
||||
Loader2,
|
||||
CheckCircle2,
|
||||
LogIn,
|
||||
EyeOff,
|
||||
} from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Checkbox } from "@/components/ui/checkbox";
|
||||
import { PrivateKeySigner } from "applesauce-signers";
|
||||
import { generateSecretKey } from "nostr-tools";
|
||||
import QRCode from "qrcode";
|
||||
import { useProfile } from "@/hooks/useProfile";
|
||||
import { use$ } from "applesauce-react/hooks";
|
||||
@@ -138,6 +142,7 @@ export function ZapWindow({
|
||||
const [showQrDialog, setShowQrDialog] = useState(false);
|
||||
const [showLogin, setShowLogin] = useState(false);
|
||||
const [paymentTimedOut, setPaymentTimedOut] = useState(false);
|
||||
const [zapAnonymously, setZapAnonymously] = useState(false);
|
||||
|
||||
// Editor ref and search functions
|
||||
const editorRef = useRef<MentionEditorHandle>(null);
|
||||
@@ -356,6 +361,13 @@ export function ZapWindow({
|
||||
}
|
||||
|
||||
// Step 3: Create and sign zap request event (kind 9734)
|
||||
// If zapping anonymously, create a throwaway signer
|
||||
let anonymousSigner;
|
||||
if (zapAnonymously) {
|
||||
const throwawayKey = generateSecretKey();
|
||||
anonymousSigner = new PrivateKeySigner(throwawayKey);
|
||||
}
|
||||
|
||||
const zapRequest = await createZapRequest({
|
||||
recipientPubkey,
|
||||
amountMillisats,
|
||||
@@ -366,6 +378,7 @@ export function ZapWindow({
|
||||
lnurl: lud16 || undefined,
|
||||
emojiTags,
|
||||
customTags,
|
||||
signer: anonymousSigner,
|
||||
});
|
||||
|
||||
const serializedZapRequest = serializeZapRequest(zapRequest);
|
||||
@@ -657,6 +670,26 @@ export function ZapWindow({
|
||||
className="rounded-md border border-input bg-background px-3 py-1 text-base md:text-sm min-h-9"
|
||||
/>
|
||||
)}
|
||||
|
||||
{/* Anonymous zap checkbox */}
|
||||
{hasLightningAddress && (
|
||||
<div className="flex items-center gap-2">
|
||||
<Checkbox
|
||||
id="zap-anonymously"
|
||||
checked={zapAnonymously}
|
||||
onCheckedChange={(checked) =>
|
||||
setZapAnonymously(checked === true)
|
||||
}
|
||||
/>
|
||||
<label
|
||||
htmlFor="zap-anonymously"
|
||||
className="text-sm text-muted-foreground cursor-pointer flex items-center gap-1.5"
|
||||
>
|
||||
<EyeOff className="size-3.5" />
|
||||
Zap anonymously
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* No Lightning Address Warning */}
|
||||
@@ -667,7 +700,7 @@ export function ZapWindow({
|
||||
)}
|
||||
|
||||
{/* Payment Button */}
|
||||
{!canSign ? (
|
||||
{!canSign && !zapAnonymously ? (
|
||||
<Button
|
||||
onClick={handleLogin}
|
||||
className="w-full"
|
||||
@@ -713,6 +746,12 @@ export function ZapWindow({
|
||||
Pay with Wallet (
|
||||
{selectedAmount || parseInt(customAmount) || 0} sats)
|
||||
</>
|
||||
) : zapAnonymously ? (
|
||||
<>
|
||||
<EyeOff className="size-4 mr-2" />
|
||||
Zap Anonymously (
|
||||
{selectedAmount || parseInt(customAmount) || 0} sats)
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Zap className="size-4 mr-2" />
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
*/
|
||||
|
||||
import { EventFactory } from "applesauce-core/event-factory";
|
||||
import type { ISigner } from "applesauce-signers";
|
||||
import type { NostrEvent } from "@/types/nostr";
|
||||
import type { EventPointer, AddressPointer } from "./open-parser";
|
||||
import accountManager from "@/services/accounts";
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
import { selectZapRelays } from "./zap-relay-selection";
|
||||
|
||||
export interface EmojiTag {
|
||||
shortcode: string;
|
||||
@@ -36,75 +36,50 @@ export interface ZapRequestParams {
|
||||
* Used for additional protocol-specific tagging
|
||||
*/
|
||||
customTags?: string[][];
|
||||
/** Optional signer for anonymous zaps (overrides account signer) */
|
||||
signer?: ISigner;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and sign a zap request event (kind 9734)
|
||||
* This event is NOT published to relays - it's sent to the LNURL callback
|
||||
*
|
||||
* @param params.signer - Optional signer for anonymous zaps. When provided,
|
||||
* uses this signer instead of the active account's signer.
|
||||
*/
|
||||
export async function createZapRequest(
|
||||
params: ZapRequestParams,
|
||||
): Promise<NostrEvent> {
|
||||
const account = accountManager.active;
|
||||
// Use provided signer (for anonymous zaps) or fall back to account signer
|
||||
let signer = params.signer;
|
||||
let senderPubkey: string | undefined;
|
||||
|
||||
if (!account) {
|
||||
throw new Error("No active account. Please log in to send zaps.");
|
||||
}
|
||||
if (signer) {
|
||||
// Anonymous zap - use provided signer
|
||||
senderPubkey = await signer.getPublicKey();
|
||||
} else {
|
||||
// Normal zap - use account signer
|
||||
const account = accountManager.active;
|
||||
|
||||
const signer = account.signer;
|
||||
if (!signer) {
|
||||
throw new Error("No signer available for active account");
|
||||
if (!account) {
|
||||
throw new Error("No active account. Please log in to send zaps.");
|
||||
}
|
||||
|
||||
signer = account.signer;
|
||||
if (!signer) {
|
||||
throw new Error("No signer available for active account");
|
||||
}
|
||||
senderPubkey = account.pubkey;
|
||||
}
|
||||
|
||||
// Get relays for zap receipt publication
|
||||
// Priority: explicit params.relays > semantic author relays > sender read relays > aggregators
|
||||
let relays: string[] | undefined = params.relays
|
||||
? [...new Set(params.relays)] // Deduplicate explicit relays
|
||||
: undefined;
|
||||
|
||||
if (!relays || relays.length === 0) {
|
||||
const collectedRelays: string[] = [];
|
||||
|
||||
// Collect outbox relays from semantic authors (event author and/or addressable event pubkey)
|
||||
const authorsToQuery: string[] = [];
|
||||
if (params.eventPointer?.author) {
|
||||
authorsToQuery.push(params.eventPointer.author);
|
||||
}
|
||||
if (params.addressPointer?.pubkey) {
|
||||
authorsToQuery.push(params.addressPointer.pubkey);
|
||||
}
|
||||
|
||||
// Deduplicate authors
|
||||
const uniqueAuthors = [...new Set(authorsToQuery)];
|
||||
|
||||
// Fetch outbox relays for each author
|
||||
for (const authorPubkey of uniqueAuthors) {
|
||||
const authorOutboxes =
|
||||
(await relayListCache.getOutboxRelays(authorPubkey)) || [];
|
||||
collectedRelays.push(...authorOutboxes);
|
||||
}
|
||||
|
||||
// Include relay hints from pointers
|
||||
if (params.eventPointer?.relays) {
|
||||
collectedRelays.push(...params.eventPointer.relays);
|
||||
}
|
||||
if (params.addressPointer?.relays) {
|
||||
collectedRelays.push(...params.addressPointer.relays);
|
||||
}
|
||||
|
||||
// Deduplicate collected relays
|
||||
const uniqueRelays = [...new Set(collectedRelays)];
|
||||
|
||||
if (uniqueRelays.length > 0) {
|
||||
relays = uniqueRelays;
|
||||
} else {
|
||||
// Fallback to sender's read relays (where they want to receive zap receipts)
|
||||
const senderReadRelays =
|
||||
(await relayListCache.getInboxRelays(account.pubkey)) || [];
|
||||
relays =
|
||||
senderReadRelays.length > 0 ? senderReadRelays : AGGREGATOR_RELAYS;
|
||||
}
|
||||
}
|
||||
// Priority: explicit relays > recipient's inbox > sender's inbox > fallback aggregators
|
||||
const zapRelayResult = await selectZapRelays({
|
||||
recipientPubkey: params.recipientPubkey,
|
||||
senderPubkey,
|
||||
explicitRelays: params.relays,
|
||||
});
|
||||
const relays = zapRelayResult.relays;
|
||||
|
||||
// Build tags
|
||||
const tags: string[][] = [
|
||||
|
||||
266
src/lib/zap-relay-selection.test.ts
Normal file
266
src/lib/zap-relay-selection.test.ts
Normal file
@@ -0,0 +1,266 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { selectZapRelays, getZapRelays } from "./zap-relay-selection";
|
||||
|
||||
// Mock the relay list cache
|
||||
vi.mock("@/services/relay-list-cache", () => ({
|
||||
relayListCache: {
|
||||
getInboxRelays: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock the loaders for AGGREGATOR_RELAYS
|
||||
vi.mock("@/services/loaders", () => ({
|
||||
AGGREGATOR_RELAYS: [
|
||||
"wss://relay.damus.io",
|
||||
"wss://nos.lol",
|
||||
"wss://relay.nostr.band",
|
||||
],
|
||||
}));
|
||||
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
|
||||
describe("selectZapRelays", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("explicit relays", () => {
|
||||
it("should use explicit relays when provided", async () => {
|
||||
const explicitRelays = ["wss://explicit1.com", "wss://explicit2.com"];
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
senderPubkey: "sender456",
|
||||
explicitRelays,
|
||||
});
|
||||
|
||||
expect(result.relays).toEqual(explicitRelays);
|
||||
expect(result.sources.recipientInbox).toEqual([]);
|
||||
expect(result.sources.senderInbox).toEqual([]);
|
||||
expect(result.sources.fallback).toEqual([]);
|
||||
// Should not call cache when explicit relays provided
|
||||
expect(relayListCache.getInboxRelays).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should limit explicit relays to 10", async () => {
|
||||
const explicitRelays = Array.from(
|
||||
{ length: 15 },
|
||||
(_, i) => `wss://relay${i}.com`,
|
||||
);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
explicitRelays,
|
||||
});
|
||||
|
||||
expect(result.relays.length).toBe(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe("recipient inbox priority", () => {
|
||||
it("should prioritize recipient's inbox relays", async () => {
|
||||
const recipientRelays = [
|
||||
"wss://recipient1.com",
|
||||
"wss://recipient2.com",
|
||||
"wss://recipient3.com",
|
||||
];
|
||||
const senderRelays = [
|
||||
"wss://sender1.com",
|
||||
"wss://sender2.com",
|
||||
"wss://sender3.com",
|
||||
];
|
||||
|
||||
vi.mocked(relayListCache.getInboxRelays).mockImplementation(
|
||||
async (pubkey) => {
|
||||
if (pubkey === "recipient123") return recipientRelays;
|
||||
if (pubkey === "sender456") return senderRelays;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
senderPubkey: "sender456",
|
||||
});
|
||||
|
||||
// Recipient relays should come first
|
||||
expect(result.relays.slice(0, 3)).toEqual(recipientRelays);
|
||||
expect(result.sources.recipientInbox).toEqual(recipientRelays);
|
||||
expect(result.sources.senderInbox).toEqual(senderRelays);
|
||||
});
|
||||
|
||||
it("should use only recipient relays when sender is anonymous", async () => {
|
||||
const recipientRelays = ["wss://recipient1.com", "wss://recipient2.com"];
|
||||
|
||||
vi.mocked(relayListCache.getInboxRelays).mockResolvedValue(
|
||||
recipientRelays,
|
||||
);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
// No senderPubkey - anonymous zap
|
||||
});
|
||||
|
||||
expect(result.relays).toEqual(recipientRelays);
|
||||
expect(result.sources.recipientInbox).toEqual(recipientRelays);
|
||||
expect(result.sources.senderInbox).toEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relay deduplication", () => {
|
||||
it("should deduplicate relays shared by recipient and sender", async () => {
|
||||
const sharedRelay = "wss://shared.com";
|
||||
const recipientRelays = [sharedRelay, "wss://recipient-only.com"];
|
||||
const senderRelays = [sharedRelay, "wss://sender-only.com"];
|
||||
|
||||
vi.mocked(relayListCache.getInboxRelays).mockImplementation(
|
||||
async (pubkey) => {
|
||||
if (pubkey === "recipient123") return recipientRelays;
|
||||
if (pubkey === "sender456") return senderRelays;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
senderPubkey: "sender456",
|
||||
});
|
||||
|
||||
// Count occurrences of shared relay
|
||||
const sharedCount = result.relays.filter((r) => r === sharedRelay).length;
|
||||
expect(sharedCount).toBe(1);
|
||||
|
||||
// Should have all unique relays
|
||||
expect(result.relays).toContain(sharedRelay);
|
||||
expect(result.relays).toContain("wss://recipient-only.com");
|
||||
expect(result.relays).toContain("wss://sender-only.com");
|
||||
});
|
||||
});
|
||||
|
||||
describe("fallback relays", () => {
|
||||
it("should use fallback relays when neither party has preferences", async () => {
|
||||
vi.mocked(relayListCache.getInboxRelays).mockResolvedValue(null);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
senderPubkey: "sender456",
|
||||
});
|
||||
|
||||
expect(result.relays.length).toBeGreaterThan(0);
|
||||
expect(result.sources.fallback.length).toBeGreaterThan(0);
|
||||
expect(result.relays).toContain("wss://relay.damus.io");
|
||||
});
|
||||
|
||||
it("should use fallback when recipient has empty relay list", async () => {
|
||||
vi.mocked(relayListCache.getInboxRelays).mockResolvedValue([]);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
});
|
||||
|
||||
expect(result.relays.length).toBeGreaterThan(0);
|
||||
expect(result.sources.fallback.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("relay limits", () => {
|
||||
it("should limit total relays to 10", async () => {
|
||||
const recipientRelays = Array.from(
|
||||
{ length: 8 },
|
||||
(_, i) => `wss://recipient${i}.com`,
|
||||
);
|
||||
const senderRelays = Array.from(
|
||||
{ length: 8 },
|
||||
(_, i) => `wss://sender${i}.com`,
|
||||
);
|
||||
|
||||
vi.mocked(relayListCache.getInboxRelays).mockImplementation(
|
||||
async (pubkey) => {
|
||||
if (pubkey === "recipient123") return recipientRelays;
|
||||
if (pubkey === "sender456") return senderRelays;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
senderPubkey: "sender456",
|
||||
});
|
||||
|
||||
expect(result.relays.length).toBeLessThanOrEqual(10);
|
||||
});
|
||||
|
||||
it("should ensure minimum relays per party when possible", async () => {
|
||||
const recipientRelays = [
|
||||
"wss://r1.com",
|
||||
"wss://r2.com",
|
||||
"wss://r3.com",
|
||||
"wss://r4.com",
|
||||
"wss://r5.com",
|
||||
];
|
||||
const senderRelays = [
|
||||
"wss://s1.com",
|
||||
"wss://s2.com",
|
||||
"wss://s3.com",
|
||||
"wss://s4.com",
|
||||
"wss://s5.com",
|
||||
];
|
||||
|
||||
vi.mocked(relayListCache.getInboxRelays).mockImplementation(
|
||||
async (pubkey) => {
|
||||
if (pubkey === "recipient123") return recipientRelays;
|
||||
if (pubkey === "sender456") return senderRelays;
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
const result = await selectZapRelays({
|
||||
recipientPubkey: "recipient123",
|
||||
senderPubkey: "sender456",
|
||||
});
|
||||
|
||||
// Should have at least 3 recipient relays (MIN_RELAYS_PER_PARTY)
|
||||
const recipientCount = result.relays.filter((r) =>
|
||||
r.startsWith("wss://r"),
|
||||
).length;
|
||||
expect(recipientCount).toBeGreaterThanOrEqual(3);
|
||||
|
||||
// Should have at least 3 sender relays
|
||||
const senderCount = result.relays.filter((r) =>
|
||||
r.startsWith("wss://s"),
|
||||
).length;
|
||||
expect(senderCount).toBeGreaterThanOrEqual(3);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getZapRelays", () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it("should return just the relay URLs", async () => {
|
||||
const recipientRelays = ["wss://recipient1.com", "wss://recipient2.com"];
|
||||
|
||||
vi.mocked(relayListCache.getInboxRelays).mockResolvedValue(recipientRelays);
|
||||
|
||||
const relays = await getZapRelays("recipient123", "sender456");
|
||||
|
||||
expect(Array.isArray(relays)).toBe(true);
|
||||
expect(relays).toEqual(recipientRelays);
|
||||
});
|
||||
|
||||
it("should work without sender pubkey (anonymous)", async () => {
|
||||
const recipientRelays = ["wss://recipient1.com"];
|
||||
|
||||
vi.mocked(relayListCache.getInboxRelays).mockResolvedValue(recipientRelays);
|
||||
|
||||
const relays = await getZapRelays("recipient123");
|
||||
|
||||
expect(relays).toEqual(recipientRelays);
|
||||
});
|
||||
});
|
||||
138
src/lib/zap-relay-selection.ts
Normal file
138
src/lib/zap-relay-selection.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* Zap Relay Selection Utilities
|
||||
*
|
||||
* Provides optimal relay selection for zap receipts (kind 9735).
|
||||
* The relays tag in a zap request specifies where the zap receipt should be published.
|
||||
*
|
||||
* Priority order:
|
||||
* 1. Recipient's inbox (read) relays - so recipient sees the zap
|
||||
* 2. Sender's inbox (read) relays - so sender can verify the zap receipt
|
||||
* 3. Fallback aggregator relays - if neither party has relay preferences
|
||||
*/
|
||||
|
||||
import { relayListCache } from "@/services/relay-list-cache";
|
||||
import { AGGREGATOR_RELAYS } from "@/services/loaders";
|
||||
|
||||
/** Maximum number of relays to include in zap request */
|
||||
const MAX_ZAP_RELAYS = 10;
|
||||
|
||||
/** Minimum relays to ensure good coverage */
|
||||
const MIN_RELAYS_PER_PARTY = 3;
|
||||
|
||||
export interface ZapRelaySelectionParams {
|
||||
/** Pubkey of the zap recipient */
|
||||
recipientPubkey: string;
|
||||
/** Pubkey of the zap sender (undefined for anonymous zaps) */
|
||||
senderPubkey?: string;
|
||||
/** Explicit relays to use (overrides automatic selection) */
|
||||
explicitRelays?: string[];
|
||||
}
|
||||
|
||||
export interface ZapRelaySelectionResult {
|
||||
/** Selected relays for zap receipt publication */
|
||||
relays: string[];
|
||||
/** Debug info about relay sources */
|
||||
sources: {
|
||||
recipientInbox: string[];
|
||||
senderInbox: string[];
|
||||
fallback: string[];
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Select optimal relays for zap receipt publication
|
||||
*
|
||||
* Strategy:
|
||||
* - Prioritize recipient's inbox relays (they need to see the zap)
|
||||
* - Add sender's inbox relays (they want to verify/see the receipt)
|
||||
* - Use fallback aggregators if neither has preferences
|
||||
* - Deduplicate and limit to MAX_ZAP_RELAYS
|
||||
*/
|
||||
export async function selectZapRelays(
|
||||
params: ZapRelaySelectionParams,
|
||||
): Promise<ZapRelaySelectionResult> {
|
||||
const { recipientPubkey, senderPubkey, explicitRelays } = params;
|
||||
|
||||
// If explicit relays provided, use them directly
|
||||
if (explicitRelays && explicitRelays.length > 0) {
|
||||
return {
|
||||
relays: explicitRelays.slice(0, MAX_ZAP_RELAYS),
|
||||
sources: {
|
||||
recipientInbox: [],
|
||||
senderInbox: [],
|
||||
fallback: [],
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const sources = {
|
||||
recipientInbox: [] as string[],
|
||||
senderInbox: [] as string[],
|
||||
fallback: [] as string[],
|
||||
};
|
||||
|
||||
// Fetch relays in parallel
|
||||
const [recipientInbox, senderInbox] = await Promise.all([
|
||||
relayListCache.getInboxRelays(recipientPubkey),
|
||||
senderPubkey ? relayListCache.getInboxRelays(senderPubkey) : null,
|
||||
]);
|
||||
|
||||
if (recipientInbox && recipientInbox.length > 0) {
|
||||
sources.recipientInbox = recipientInbox;
|
||||
}
|
||||
|
||||
if (senderInbox && senderInbox.length > 0) {
|
||||
sources.senderInbox = senderInbox;
|
||||
}
|
||||
|
||||
// Build relay list with priority ordering
|
||||
const relaySet = new Set<string>();
|
||||
|
||||
// Priority 1: Recipient's inbox relays (take up to MIN_RELAYS_PER_PARTY first)
|
||||
for (const relay of sources.recipientInbox.slice(0, MIN_RELAYS_PER_PARTY)) {
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Priority 2: Sender's inbox relays (take up to MIN_RELAYS_PER_PARTY)
|
||||
for (const relay of sources.senderInbox.slice(0, MIN_RELAYS_PER_PARTY)) {
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Add remaining recipient relays
|
||||
for (const relay of sources.recipientInbox.slice(MIN_RELAYS_PER_PARTY)) {
|
||||
if (relaySet.size >= MAX_ZAP_RELAYS) break;
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Add remaining sender relays
|
||||
for (const relay of sources.senderInbox.slice(MIN_RELAYS_PER_PARTY)) {
|
||||
if (relaySet.size >= MAX_ZAP_RELAYS) break;
|
||||
relaySet.add(relay);
|
||||
}
|
||||
|
||||
// Fallback to aggregator relays if we don't have enough
|
||||
if (relaySet.size === 0) {
|
||||
sources.fallback = [...AGGREGATOR_RELAYS];
|
||||
for (const relay of AGGREGATOR_RELAYS) {
|
||||
if (relaySet.size >= MAX_ZAP_RELAYS) break;
|
||||
relaySet.add(relay);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
relays: Array.from(relaySet),
|
||||
sources,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a simple list of relays for zap receipt publication
|
||||
* Convenience wrapper that just returns the relay URLs
|
||||
*/
|
||||
export async function getZapRelays(
|
||||
recipientPubkey: string,
|
||||
senderPubkey?: string,
|
||||
): Promise<string[]> {
|
||||
const result = await selectZapRelays({ recipientPubkey, senderPubkey });
|
||||
return result.relays;
|
||||
}
|
||||
Reference in New Issue
Block a user