diff --git a/src/components/nostr/kinds/P2pOrderDetailRenderer.tsx b/src/components/nostr/kinds/P2pOrderDetailRenderer.tsx new file mode 100644 index 0000000..81aa5a3 --- /dev/null +++ b/src/components/nostr/kinds/P2pOrderDetailRenderer.tsx @@ -0,0 +1,173 @@ +import { NostrEvent } from "@/types/nostr"; +import { UserName } from "../UserName"; +import { Copy } from "lucide-react"; +import { + getOrderType, + getFiatAmount, + getSatsAmount, + getCurrency, + getPaymentMethods, + getPlatform, + getPremium, + getOrderStatus, + getSource, + getBitcoinLayer, + getBitcoinNetwork, + getUsername, + getExpiration, +} from "@/lib/nip69-helpers"; +import { toast } from "sonner"; + +interface P2pOrderDetailRendererProps { + event: NostrEvent; +} + +/** + * Detail renderer for Kind 38383 - P2P Order + * Shows order details and links + */ +export function P2pOrderDetailRenderer({ event }: P2pOrderDetailRendererProps) { + const orderType = getOrderType(event); + const fiatAmount = getFiatAmount(event); + const satsAmount = getSatsAmount(event); + const currency = getCurrency(event); + const paymentMethods = getPaymentMethods(event); + const platform = getPlatform(event); + const premium = getPremium(event); + const orderStatus = getOrderStatus(event); + const source = getSource(event); + const bitcoinLayer = getBitcoinLayer(event); + const bitcoinNetwork = getBitcoinNetwork(event); + const username = getUsername(event); + const expiration = getExpiration(event); + + const handleCopy = (value: string) => { + navigator.clipboard.writeText(value); + toast.success(`External link copied to clipboard`); + }; + + const getTradetitle: () => string = () => { + if (!orderType) return "-"; + + let title = `${orderType?.toLocaleUpperCase()} `; + + const fiat = fiatAmount + ? fiatAmount.length < 2 + ? fiatAmount[0] + : `${fiatAmount[0]} - ${fiatAmount[1]}` + : ""; + + if (satsAmount) { + if (fiat) { + title += `${fiat} ${currency} (${satsAmount} sats)`; + } else { + title += `${fiat} sats (Premium ${premium}%)`; + } + } else { + title += `${fiat} ${currency} (Premium ${premium}%)`; + } + + return title; + }; + + const getExpirationDate = () => { + if (!expiration) return "-"; + + const date = new Date(expiration * 1000); + + return date.toLocaleString(); + }; + + return ( +
+ {/* Header Section */} +
+ {/* Order Title */} +
+
+

{getTradetitle()}

+ {orderStatus === "pending" && source && ( + + )} +
+
+
+ + {/* Metadata Grid */} +
+ {/* Publisher */} +
+

Profile

+ +
+ + {/* Host */} +
+

Host

+ + {platform ?? "-"} + +
+ + {/* Status */} +
+

Username

+ + {username ?? "-"} + +
+ + {/* Status */} +
+

Status

+ + {orderStatus ?? "-"} + +
+ + {/* Layer */} +
+

Layer

+ + {bitcoinLayer ?? "-"} + +
+ + {/* Network */} +
+

Network

+ + {bitcoinNetwork ?? "-"} + +
+
+ + {/* Platforms Section */} + {paymentMethods && paymentMethods.length > 0 && ( +
+

Payment Methods

+
+ {paymentMethods.map((pm) => ( +
+ {pm} +
+ ))} +
+
+ )} + + {/* Expiration */} +
+

Expiration Date

+
{getExpirationDate()}
+
+
+ ); +} diff --git a/src/components/nostr/kinds/P2pOrderRenderer.tsx b/src/components/nostr/kinds/P2pOrderRenderer.tsx new file mode 100644 index 0000000..be6c9fa --- /dev/null +++ b/src/components/nostr/kinds/P2pOrderRenderer.tsx @@ -0,0 +1,149 @@ +import { + BaseEventContainer, + BaseEventProps, + ClickableEventTitle, +} from "./BaseEventRenderer"; +import { + Ban, + Check, + ClockAlert, + Copy, + Layers, + Loader, + Scale, + Tickets, +} from "lucide-react"; +import { + getBitcoinLayer, + getCurrency, + getFiatAmount, + getOrderStatus, + getOrderType, + getPaymentMethods, + getPlatform, + getPremium, + getSatsAmount, + getSource, +} from "@/lib/nip69-helpers"; +import { toast } from "sonner"; + +/** + * Renderer for Kind 38383 - P2P Order + * Clean feed view with order details and links + */ +export function P2pOrderRenderer({ event }: BaseEventProps) { + const orderType = getOrderType(event); + const fiatAmount = getFiatAmount(event); + const satsAmount = getSatsAmount(event); + const currency = getCurrency(event); + const paymentMethods = getPaymentMethods(event); + const platform = getPlatform(event); + const premium = getPremium(event); + const orderStatus = getOrderStatus(event); + const source = getSource(event); + const bitcoinLayer = getBitcoinLayer(event); + + const handleCopy = (value: string) => { + navigator.clipboard.writeText(value); + toast.success(`External link copied to clipboard`); + }; + + const getTradetitle: () => string = () => { + let title = `${orderType?.toLocaleUpperCase()} `; + + const fiat = fiatAmount + ? fiatAmount.length < 2 + ? fiatAmount[0] + : `${fiatAmount[0]} - ${fiatAmount[1]}` + : ""; + + if (satsAmount) { + if (fiat) { + title += `${fiat} ${currency} (${satsAmount} sats)`; + } else { + title += `${fiat} sats (Premium ${premium}%)`; + } + } else { + title += `${fiat} ${currency} (Premium ${premium}%)`; + } + + return title; + }; + + const getStatusTag = () => { + const className = "size-3"; + const icon = { + pending: , + canceled: , + "in-progress": , + success: , + expired: , + }; + + return orderStatus ? ( + <> + {icon[orderStatus]} + {orderStatus} + + ) : ( + <> + ); + }; + + return ( + + {orderType && ( +
+
+ + {getTradetitle()} + +
+ {orderStatus === "pending" && source && ( + + )} +
+
+ + {orderType && ( +

+ {paymentMethods?.join(" ")} +

+ )} + + {(platform || bitcoinLayer) && ( +
+ {platform && ( + <> + + + {platform} + + + )} + {bitcoinLayer && ( + <> + + + {bitcoinLayer} + + + )} + {getStatusTag()} +
+ )} +
+ )} +
+ ); +} diff --git a/src/components/nostr/kinds/index.tsx b/src/components/nostr/kinds/index.tsx index fcec3b3..59b4b05 100644 --- a/src/components/nostr/kinds/index.tsx +++ b/src/components/nostr/kinds/index.tsx @@ -140,6 +140,8 @@ import { } from "./StarterPackRenderer"; import { NostrEvent } from "@/types/nostr"; import { BaseEventContainer, type BaseEventProps } from "./BaseEventRenderer"; +import { P2pOrderRenderer } from "./P2pOrderRenderer"; +import { P2pOrderDetailRenderer } from "./P2pOrderDetailRenderer"; /** * Registry of kind-specific renderers @@ -215,6 +217,7 @@ const kindRenderers: Record> = { 31989: HandlerRecommendationRenderer, // Handler Recommendation (NIP-89) 31990: ApplicationHandlerRenderer, // Application Handler (NIP-89) 32267: ZapstoreAppRenderer, // Zapstore App + 38383: P2pOrderRenderer, // P2P Orders 39000: GroupMetadataRenderer, // Group Metadata (NIP-29) 39089: StarterPackRenderer, // Starter Pack (NIP-51) 39092: MediaStarterPackRenderer, // Media Starter Pack (NIP-51) @@ -305,6 +308,7 @@ const detailRenderers: Record< 31989: HandlerRecommendationDetailRenderer, // Handler Recommendation Detail (NIP-89) 31990: ApplicationHandlerDetailRenderer, // Application Handler Detail (NIP-89) 32267: ZapstoreAppDetailRenderer, // Zapstore App Detail + 38383: P2pOrderDetailRenderer, // P2P Order Detail 39089: StarterPackDetailRenderer, // Starter Pack Detail (NIP-51) 39092: MediaStarterPackDetailRenderer, // Media Starter Pack Detail (NIP-51) }; diff --git a/src/lib/nip69-helpers.test.ts b/src/lib/nip69-helpers.test.ts new file mode 100644 index 0000000..b5a3441 --- /dev/null +++ b/src/lib/nip69-helpers.test.ts @@ -0,0 +1,547 @@ +import { describe, it, expect } from "vitest"; +import { + getOrderType, + getFiatAmount, + getSatsAmount, + getCurrency, + getBitcoinNetwork, + getUsername, + getPaymentMethods, + getPlatform, + getExpiration, + getPremium, + getOrderStatus, + getSource, + getBitcoinLayer, + ORDER_STATUSES, + type OrderStatus, +} from "./nip69-helpers"; +import { NostrEvent } from "@/types/nostr"; + +// Helper to create a minimal kind 38383 event (P2P Order) +function createP2POrderEvent(overrides?: Partial): NostrEvent { + return { + id: "test-id", + pubkey: "test-pubkey", + created_at: 1234567890, + kind: 38383, + tags: [], + content: "", + sig: "test-sig", + ...overrides, + }; +} + +describe("Kind 38383 (P2P Order) Helpers", () => { + describe("getOrderType", () => { + it("should extract order type from k tag (sell)", () => { + const event = createP2POrderEvent({ + tags: [["k", "sell"]], + }); + expect(getOrderType(event)).toBe("sell"); + }); + + it("should extract order type from k tag (buy)", () => { + const event = createP2POrderEvent({ + tags: [["k", "buy"]], + }); + expect(getOrderType(event)).toBe("buy"); + }); + + it("should return undefined if no k tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getOrderType(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["k", "sell"]], + }); + expect(getOrderType(event)).toBeUndefined(); + }); + }); + + describe("getFiatAmount", () => { + it("should extract fiat amount from fa tag (integer)", () => { + const event = createP2POrderEvent({ + tags: [["fa", "100"]], + }); + expect(getFiatAmount(event)).toStrictEqual(["100"]); + }); + + it("should parse large fiat amounts", () => { + const event = createP2POrderEvent({ + tags: [["fa", "1000000"]], + }); + expect(getFiatAmount(event)).toStrictEqual(["1000000"]); + }); + + it("should parse multiple fiat amounts", () => { + const event = createP2POrderEvent({ + tags: [["fa", "50", "100"]], + }); + expect(getFiatAmount(event)).toStrictEqual(["50", "100"]); + }); + + it("should return undefined if no fa tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getFiatAmount(event)).toStrictEqual([]); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["fa", "100"]], + }); + expect(getFiatAmount(event)).toBeUndefined(); + }); + + it("should parse integers from string values", () => { + const event = createP2POrderEvent({ + tags: [["fa", "100.99"]], + }); + expect(getFiatAmount(event)).toStrictEqual(["100"]); + }); + }); + + describe("getSatsAmount", () => { + it("should extract sats amount from amt tag", () => { + const event = createP2POrderEvent({ + tags: [["amt", "50000"]], + }); + expect(getSatsAmount(event)).toBe(50000); + }); + + it("should parse large sat amounts", () => { + const event = createP2POrderEvent({ + tags: [["amt", "100000000"]], + }); + expect(getSatsAmount(event)).toBe(100000000); + }); + + it("should return undefined if no amt tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getSatsAmount(event)).toBeUndefined(); + }); + + it("should return undefined if amt tag value is not a number", () => { + const event = createP2POrderEvent({ + tags: [["amt", "invalid"]], + }); + expect(getSatsAmount(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["amt", "50000"]], + }); + expect(getSatsAmount(event)).toBeUndefined(); + }); + }); + + describe("getCurrency", () => { + it("should extract currency from f tag (USD)", () => { + const event = createP2POrderEvent({ + tags: [["f", "USD"]], + }); + expect(getCurrency(event)).toBe("USD"); + }); + + it("should extract currency from f tag (EUR)", () => { + const event = createP2POrderEvent({ + tags: [["f", "EUR"]], + }); + expect(getCurrency(event)).toBe("EUR"); + }); + + it("should handle other ISO 4217 currencies", () => { + const event = createP2POrderEvent({ + tags: [["f", "JPY"]], + }); + expect(getCurrency(event)).toBe("JPY"); + }); + + it("should return undefined if no f tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getCurrency(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["f", "USD"]], + }); + expect(getCurrency(event)).toBeUndefined(); + }); + }); + + describe("getBitcoinNetwork", () => { + it("should extract bitcoin network from network tag (mainnet)", () => { + const event = createP2POrderEvent({ + tags: [["network", "mainnet"]], + }); + expect(getBitcoinNetwork(event)).toBe("mainnet"); + }); + + it("should extract bitcoin network from network tag (testnet)", () => { + const event = createP2POrderEvent({ + tags: [["network", "testnet"]], + }); + expect(getBitcoinNetwork(event)).toBe("testnet"); + }); + + it("should return undefined if no network tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getBitcoinNetwork(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["network", "mainnet"]], + }); + expect(getBitcoinNetwork(event)).toBeUndefined(); + }); + }); + + describe("getUsername", () => { + it("should extract username from name tag", () => { + const event = createP2POrderEvent({ + tags: [["name", "alice"]], + }); + expect(getUsername(event)).toBe("alice"); + }); + + it("should return undefined if no name tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getUsername(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["name", "alice"]], + }); + expect(getUsername(event)).toBeUndefined(); + }); + }); + + describe("getPaymentMethods", () => { + it("should extract all payment methods from pm tags", () => { + const event = createP2POrderEvent({ + tags: [ + ["pm", "revolut"], + ["pm", "strike"], + ["pm", "cash-app"], + ], + }); + expect(getPaymentMethods(event)).toEqual([ + "revolut", + "strike", + "cash-app", + ]); + }); + + it("should handle single payment method", () => { + const event = createP2POrderEvent({ + tags: [["pm", "paypal"]], + }); + expect(getPaymentMethods(event)).toEqual(["paypal"]); + }); + + it("should return empty array if no pm tags", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getPaymentMethods(event)).toEqual([]); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["pm", "paypal"]], + }); + expect(getPaymentMethods(event)).toBeUndefined(); + }); + + it("should handle pm tags with multiple values per tag", () => { + const event = createP2POrderEvent({ + tags: [ + ["pm", "revolut", "extra-value"], + ["pm", "strike"], + ], + }); + // getTagValues uses flatMap with slice(1), so all values after pm are included + expect(getPaymentMethods(event)).toEqual([ + "revolut", + "extra-value", + "strike", + ]); + }); + }); + + describe("getPlatform", () => { + it("should extract platform from y tag", () => { + const event = createP2POrderEvent({ + tags: [["y", "robosats"]], + }); + expect(getPlatform(event)).toBe("robosats"); + }); + + it("should handle different platforms", () => { + const event = createP2POrderEvent({ + tags: [["y", "hodlhodl"]], + }); + expect(getPlatform(event)).toBe("hodlhodl"); + }); + + it("should return undefined if no y tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getPlatform(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["y", "robosats"]], + }); + expect(getPlatform(event)).toBeUndefined(); + }); + }); + + describe("getExpiration", () => { + it("should extract expiration timestamp from expiration tag", () => { + const event = createP2POrderEvent({ + tags: [["expiration", "1704067200"]], + }); + expect(getExpiration(event)).toBe(1704067200); + }); + + it("should return undefined if no expiration tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getExpiration(event)).toBeUndefined(); + }); + + it("should return undefined if expiration tag value is not a number", () => { + const event = createP2POrderEvent({ + tags: [["expiration", "invalid"]], + }); + expect(getExpiration(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["expiration", "1704067200"]], + }); + expect(getExpiration(event)).toBeUndefined(); + }); + }); + + describe("getPremium", () => { + it("should extract premium value from premium tag (positive)", () => { + const event = createP2POrderEvent({ + tags: [["premium", "5"]], + }); + expect(getPremium(event)).toBe(5); + }); + + it("should extract premium value from premium tag (negative)", () => { + const event = createP2POrderEvent({ + tags: [["premium", "-3"]], + }); + expect(getPremium(event)).toBe(-3); + }); + + it("should extract premium value from premium tag (zero)", () => { + const event = createP2POrderEvent({ + tags: [["premium", "0"]], + }); + expect(getPremium(event)).toBe(0); + }); + + it("should return undefined if no premium tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getPremium(event)).toBeUndefined(); + }); + + it("should return undefined if premium tag value is not a number", () => { + const event = createP2POrderEvent({ + tags: [["premium", "invalid"]], + }); + expect(getPremium(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["premium", "5"]], + }); + expect(getPremium(event)).toBeUndefined(); + }); + }); + + describe("getOrderStatus", () => { + it("should extract status from s tag (pending)", () => { + const event = createP2POrderEvent({ + tags: [["s", "pending"]], + }); + expect(getOrderStatus(event)).toBe("pending"); + }); + + it("should extract status from s tag (canceled)", () => { + const event = createP2POrderEvent({ + tags: [["s", "canceled"]], + }); + expect(getOrderStatus(event)).toBe("canceled"); + }); + + it("should extract status from s tag (in-progress)", () => { + const event = createP2POrderEvent({ + tags: [["s", "in-progress"]], + }); + expect(getOrderStatus(event)).toBe("in-progress"); + }); + + it("should extract status from s tag (success)", () => { + const event = createP2POrderEvent({ + tags: [["s", "success"]], + }); + expect(getOrderStatus(event)).toBe("success"); + }); + + it("should extract status from s tag (expired)", () => { + const event = createP2POrderEvent({ + tags: [["s", "expired"]], + }); + expect(getOrderStatus(event)).toBe("expired"); + }); + + it("should return undefined if no s tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getOrderStatus(event)).toBeUndefined(); + }); + + it("should return undefined for invalid status value", () => { + const event = createP2POrderEvent({ + tags: [["s", "invalid-status"]], + }); + expect(getOrderStatus(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["s", "pending"]], + }); + expect(getOrderStatus(event)).toBeUndefined(); + }); + + it("should validate against ORDER_STATUSES constant", () => { + // Verify all valid statuses are recognized + ORDER_STATUSES.forEach((status) => { + const event = createP2POrderEvent({ + tags: [["s", status]], + }); + expect(getOrderStatus(event)).toBe(status); + }); + }); + }); + + describe("getSource", () => { + it("should extract source URL from source tag", () => { + const event = createP2POrderEvent({ + tags: [["source", "https://robosats.com/order/abc123"]], + }); + expect(getSource(event)).toBe("https://robosats.com/order/abc123"); + }); + + it("should return undefined if no source tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getSource(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["source", "https://example.com"]], + }); + expect(getSource(event)).toBeUndefined(); + }); + }); + + describe("getBitcoinLayer", () => { + it("should extract bitcoin layer from layer tag (lightning)", () => { + const event = createP2POrderEvent({ + tags: [["layer", "lightning"]], + }); + expect(getBitcoinLayer(event)).toBe("lightning"); + }); + + it("should extract bitcoin layer from layer tag (onchain)", () => { + const event = createP2POrderEvent({ + tags: [["layer", "onchain"]], + }); + expect(getBitcoinLayer(event)).toBe("onchain"); + }); + + it("should return undefined if no layer tag", () => { + const event = createP2POrderEvent({ + tags: [], + }); + expect(getBitcoinLayer(event)).toBeUndefined(); + }); + + it("should return undefined for non-38383 events", () => { + const event = createP2POrderEvent({ + kind: 1, + tags: [["layer", "lightning"]], + }); + expect(getBitcoinLayer(event)).toBeUndefined(); + }); + }); + + describe("ORDER_STATUSES constant", () => { + it("should contain all valid order statuses", () => { + expect(ORDER_STATUSES).toContain("pending"); + expect(ORDER_STATUSES).toContain("canceled"); + expect(ORDER_STATUSES).toContain("in-progress"); + expect(ORDER_STATUSES).toContain("success"); + expect(ORDER_STATUSES).toContain("expired"); + }); + + it("should have exactly 5 statuses", () => { + expect(ORDER_STATUSES.length).toBe(5); + }); + + it("should be readonly tuple", () => { + // TypeScript compile-time check - this tests the type definition + const status: OrderStatus = "pending"; + expect(ORDER_STATUSES.includes(status)).toBe(true); + }); + }); +}); diff --git a/src/lib/nip69-helpers.ts b/src/lib/nip69-helpers.ts new file mode 100644 index 0000000..d5ee587 --- /dev/null +++ b/src/lib/nip69-helpers.ts @@ -0,0 +1,185 @@ +import { NostrEvent } from "@/types/nostr"; +import { getTagValue } from "applesauce-core/helpers"; + +/** + * P2P Order Helper Functions + */ + +/** + * Status of the order + */ +export const ORDER_STATUSES = [ + "pending", + "canceled", + "in-progress", + "success", + "expired", +] as const; +export type OrderStatus = (typeof ORDER_STATUSES)[number]; + +/** + * Get all values for a tag name (plural version of getTagValue) + * Unlike getTagValue which returns first match, this returns all matches + */ +function getTagValues(event: NostrEvent, tagName: string): string[] { + return event.tags + .filter((tag) => tag[0] === tagName) + .flatMap((tag) => tag.slice(1)); +} + +// ============================================================================ +// Kind 38383 (P2P Orders) Helpers +// ============================================================================ + +/** + * Get order type from k tag (sell or buy) + */ +export function getOrderType(event: NostrEvent): string | undefined { + if (event.kind !== 38383) return undefined; + return getTagValue(event, "k"); +} + +/** + * Get amount in fiat from fa tag + */ +export function getFiatAmount( + event: NostrEvent, +): (string | undefined)[] | undefined { + if (event.kind !== 38383) return undefined; + const values = getTagValues(event, "fa"); + + return values.map((monto) => { + const number = parseFloat(monto); + + if (Number.isNaN(number)) return undefined; + + return number < 1 ? number.toString() : parseInt(monto, 10).toString(); + }); +} + +/** + * Get amount in sats from amt tag + */ +export function getSatsAmount(event: NostrEvent): number | undefined { + if (event.kind !== 38383) return undefined; + const value = getTagValue(event, "amt"); + + if (!value) return undefined; + + const number = parseInt(value, 10); + + if (Number.isNaN(number)) return undefined; + + return number; +} + +/** + * Get currency code using the ISO 4217 standard. + */ +export function getCurrency(event: NostrEvent): string | undefined { + if (event.kind !== 38383) return undefined; + + return getTagValue(event, "f"); +} + +/** + * Get the bitcoin network + */ +export function getBitcoinNetwork(event: NostrEvent): string | undefined { + if (event.kind !== 38383) return undefined; + + return getTagValue(event, "network"); +} + +/** + * Get the user name + */ +export function getUsername(event: NostrEvent): string | undefined { + if (event.kind !== 38383) return undefined; + + return getTagValue(event, "name"); +} + +/** + * Get accepted payment methods + */ +export function getPaymentMethods(event: NostrEvent): string[] | undefined { + if (event.kind !== 38383) return undefined; + + return getTagValues(event, "pm"); +} + +/** + * Get platform where the order is hosted + */ +export function getPlatform(event: NostrEvent): string | undefined { + if (event.kind !== 38383) return undefined; + + return getTagValue(event, "y"); +} + +/** + * Get rating + */ +export function getExpiration(event: NostrEvent): number | undefined { + if (event.kind !== 38383) return undefined; + + const value = getTagValue(event, "expiration"); + + if (!value) return undefined; + + const number = parseInt(value, 10); + + if (Number.isNaN(number)) return undefined; + + return number; +} + +/** + * Get premium value over market price + */ +export function getPremium(event: NostrEvent): number | undefined { + if (event.kind !== 38383) return undefined; + const value = getTagValue(event, "premium"); + + if (!value) return undefined; + + const number = parseInt(value, 10); + + if (Number.isNaN(number)) return undefined; + + return number; +} + +/** + * Get status of the order + */ +export function getOrderStatus(event: NostrEvent): OrderStatus | undefined { + if (event.kind !== 38383) return undefined; + + const status = getTagValue(event, "s"); + + if (!status) return undefined; + + if (!ORDER_STATUSES.includes(status as OrderStatus)) return undefined; + + return status as OrderStatus; +} + +/** + * Get link to the order view in host + */ +export function getSource(event: NostrEvent): string | undefined { + if (event.kind !== 38383) return undefined; + + return getTagValue(event, "source"); +} + +/** + * Get bitcoin layer + */ +export function getBitcoinLayer(event: NostrEvent): string | undefined { + if (event.kind !== 38383) return undefined; + + return getTagValue(event, "layer"); +}