feat: Add P2P orders renderer and detail (#116)

This commit is contained in:
KoalaSat
2026-01-16 23:06:07 +01:00
committed by GitHub
parent 97f18de358
commit 14d5255bce
5 changed files with 1058 additions and 0 deletions

View File

@@ -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 (
<div className="flex flex-col gap-6 p-6 max-w-4xl mx-auto">
{/* Header Section */}
<div className="flex gap-4">
{/* Order Title */}
<div className="flex flex-col g4p-2 flex-1 min-w-0">
<div className="flex items-start justify-between gap-4">
<h1 className="text-3xl font-bold">{getTradetitle()}</h1>
{orderStatus === "pending" && source && (
<button
onClick={() => handleCopy(source)}
title={`Order profile in ${platform}`}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-primary-foreground bg-primary rounded-lg hover:bg-primary/90 transition-colors flex-shrink-0"
>
<Copy className="size-3" />
{"Link"}
</button>
)}
</div>
</div>
</div>
{/* Metadata Grid */}
<div className="grid grid-cols-2 gap-4 text-sm">
{/* Publisher */}
<div className="flex flex-col gap-1">
<h3 className="text-muted-foreground">Profile</h3>
<UserName pubkey={event.pubkey} />
</div>
{/* Host */}
<div className="flex flex-col gap-1">
<h3 className="text-muted-foreground">Host</h3>
<code className="font-mono text-sm truncate" title={platform}>
{platform ?? "-"}
</code>
</div>
{/* Status */}
<div className="flex flex-col gap-1">
<h3 className="text-muted-foreground">Username</h3>
<code className="font-mono text-sm truncate" title={username}>
{username ?? "-"}
</code>
</div>
{/* Status */}
<div className="flex flex-col gap-1">
<h3 className="text-muted-foreground">Status</h3>
<code className="font-mono text-sm truncate" title={orderStatus}>
{orderStatus ?? "-"}
</code>
</div>
{/* Layer */}
<div className="flex flex-col gap-1">
<h3 className="text-muted-foreground">Layer</h3>
<code className="font-mono text-sm truncate" title={bitcoinLayer}>
{bitcoinLayer ?? "-"}
</code>
</div>
{/* Network */}
<div className="flex flex-col gap-1">
<h3 className="text-muted-foreground">Network</h3>
<code className="font-mono text-sm truncate" title={bitcoinNetwork}>
{bitcoinNetwork ?? "-"}
</code>
</div>
</div>
{/* Platforms Section */}
{paymentMethods && paymentMethods.length > 0 && (
<div className="flex flex-col gap-3">
<h2 className="text-xl font-semibold">Payment Methods</h2>
<div className="flex flex-wrap gap-2">
{paymentMethods.map((pm) => (
<div className="flex items-center gap-2 px-3 py-2 bg-muted/30 rounded-lg">
<span className="text-sm font-medium">{pm}</span>
</div>
))}
</div>
</div>
)}
{/* Expiration */}
<div className="flex flex-col gap-3">
<h2 className="text-xl font-semibold">Expiration Date</h2>
<div className="flex flex-wrap gap-2">{getExpirationDate()}</div>
</div>
</div>
);
}

View File

@@ -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: <Tickets className={className} />,
canceled: <Ban className={className} />,
"in-progress": <Loader className={className} />,
success: <Check className={className} />,
expired: <ClockAlert className={className} />,
};
return orderStatus ? (
<>
{icon[orderStatus]}
<span className="text-xs text-muted-foreground">{orderStatus}</span>
</>
) : (
<></>
);
};
return (
<BaseEventContainer event={event}>
{orderType && (
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-2">
<ClickableEventTitle
event={event}
className="text-base font-semibold text-foreground"
>
{getTradetitle()}
</ClickableEventTitle>
<div className="flex items-center gap-2">
{orderStatus === "pending" && source && (
<button
onClick={() => handleCopy(source)}
className="flex items-center gap-1.5 px-2 py-1 text-xs font-medium text-primary border border-primary/20 rounded hover:bg-primary/10 transition-colors flex-shrink-0"
title={`Order profile in ${platform}`}
>
<Copy className="size-3" />
{"Link"}
</button>
)}
</div>
</div>
{orderType && (
<p className="text-sm text-muted-foreground line-clamp-2">
{paymentMethods?.join(" ")}
</p>
)}
{(platform || bitcoinLayer) && (
<div className="flex items-center gap-2">
{platform && (
<>
<Scale className="size-3" />
<span className="text-xs text-muted-foreground">
{platform}
</span>
</>
)}
{bitcoinLayer && (
<>
<Layers className="size-3" />
<span className="text-xs text-muted-foreground">
{bitcoinLayer}
</span>
</>
)}
{getStatusTag()}
</div>
)}
</div>
)}
</BaseEventContainer>
);
}

View File

@@ -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<number, React.ComponentType<BaseEventProps>> = {
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)
};

View File

@@ -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>): 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);
});
});
});

185
src/lib/nip69-helpers.ts Normal file
View File

@@ -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");
}