Files
bitvid/js/payments/zapSharedState.js
T

280 lines
7.0 KiB
JavaScript

import { resolvePlatformFeePercent } from "./platformFee.js";
import {
resolveLightningAddress as defaultResolveLightningAddress,
fetchPayServiceData as defaultFetchPayServiceData,
validateInvoiceAmount,
} from "./lnurl.js";
const METADATA_CACHE_TTL_MS = 5 * 60 * 1000;
const lightningMetadataCache = new Map();
const lightningMetadataByUrl = new Map();
let cachedPlatformLightningAddress = "";
export function normalizeLightningAddressKey(address) {
return typeof address === "string" ? address.trim().toLowerCase() : "";
}
export function isMetadataEntryFresh(entry) {
if (!entry || typeof entry.fetchedAt !== "number") {
return false;
}
return Date.now() - entry.fetchedAt < METADATA_CACHE_TTL_MS;
}
export function rememberLightningMetadata(entry) {
if (!entry || !entry.key) {
return entry;
}
lightningMetadataCache.set(entry.key, entry);
if (entry.resolved?.url) {
lightningMetadataByUrl.set(entry.resolved.url, entry);
}
return entry;
}
export function getCachedLightningEntry(address) {
const key = normalizeLightningAddressKey(address);
if (!key) {
return null;
}
const entry = lightningMetadataCache.get(key);
if (!entry) {
return null;
}
if (entry.metadata && isMetadataEntryFresh(entry)) {
return entry;
}
if (entry.metadata && !entry.promise) {
return entry;
}
return entry.metadata ? entry : null;
}
export function getCachedMetadataByUrl(url) {
if (!url) {
return null;
}
return lightningMetadataByUrl.get(url) || null;
}
export async function fetchLightningMetadata(
address,
{
resolveLightningAddress = defaultResolveLightningAddress,
fetchPayServiceData = defaultFetchPayServiceData,
} = {}
) {
const key = normalizeLightningAddressKey(address);
if (!key) {
throw new Error("Lightning address is required.");
}
const cached = lightningMetadataCache.get(key);
if (cached && cached.metadata && isMetadataEntryFresh(cached)) {
return cached;
}
if (cached?.promise) {
return cached.promise;
}
const fetchPromise = (async () => {
const resolved = cached?.resolved || (await resolveLightningAddress(address));
const metadata = await fetchPayServiceData(resolved.url);
const entry = {
key,
address: resolved.address || address,
resolved,
metadata,
fetchedAt: Date.now(),
};
rememberLightningMetadata(entry);
return entry;
})();
lightningMetadataCache.set(key, {
...(cached || {}),
key,
promise: fetchPromise,
});
try {
return await fetchPromise;
} catch (error) {
const current = lightningMetadataCache.get(key);
if (current?.promise === fetchPromise) {
lightningMetadataCache.delete(key);
}
throw error;
}
}
export function calculateZapShares(amount, overrideFee = null) {
const numericAmount = Math.max(0, Math.round(Number(amount) || 0));
const feePercent = resolvePlatformFeePercent(
typeof overrideFee === "undefined" ? null : overrideFee
);
const platformShare = Math.floor((numericAmount * feePercent) / 100);
const creatorShare = numericAmount - platformShare;
return {
total: numericAmount,
creatorShare,
platformShare,
feePercent,
};
}
export function describeShareType(type) {
if (type === "platform") {
return "Platform";
}
if (type === "creator") {
return "Creator";
}
return "Lightning";
}
export function formatMinRequirement(metadata) {
if (!metadata || typeof metadata.minSendable !== "number") {
return null;
}
if (metadata.minSendable <= 0) {
return null;
}
return Math.ceil(metadata.minSendable / 1000);
}
export function setCachedPlatformLightningAddress(address) {
cachedPlatformLightningAddress =
typeof address === "string" ? address.trim() : "";
return cachedPlatformLightningAddress;
}
export function getCachedPlatformLightningAddress() {
return cachedPlatformLightningAddress;
}
export function clearZapCaches() {
lightningMetadataCache.clear();
lightningMetadataByUrl.clear();
cachedPlatformLightningAddress = "";
}
export { validateInvoiceAmount };
function extractErrorMessage(error) {
if (!error) {
return "";
}
if (typeof error === "string") {
return error;
}
if (typeof error?.message === "string") {
return error.message;
}
if (typeof error?.reason === "string") {
return error.reason;
}
if (typeof error?.error?.message === "string") {
return error.error.message;
}
if (typeof error?.data?.error === "string") {
return error.data.error;
}
if (typeof error?.data?.error?.message === "string") {
return error.data.error.message;
}
return "";
}
const ALLOWANCE_ERROR_CODES = new Set([
"allowance_exceeded",
"allowance_exhausted",
"budget_exceeded",
"budget_exhausted",
"budget_limit_reached",
"budget_spent",
"quota_exceeded",
"spending_allowance_exceeded",
"spending_limit_reached",
"nwc_budget_exhausted",
]);
const ALLOWANCE_ERROR_PATTERNS = [
"budget exceeded",
"allowance exceeded",
"allowance exhausted",
"allowance spent",
"allowance depleted",
"allowance limit reached",
"spending allowance exceeded",
"spending allowance reached",
"spending limit reached",
"quota exceeded",
];
export function isZapAllowanceExhaustedError(error) {
const code =
typeof error?.code === "string"
? error.code.trim().toLowerCase().replace(/[^a-z0-9]+/g, "_")
: "";
if (code && ALLOWANCE_ERROR_CODES.has(code)) {
return true;
}
const message = extractErrorMessage(error);
if (!message) {
return false;
}
const normalized = message.toLowerCase();
if (ALLOWANCE_ERROR_PATTERNS.some((pattern) => normalized.includes(pattern))) {
return true;
}
if (normalized.includes("allowance") && normalized.includes("zap")) {
if (
normalized.includes("exceed") ||
normalized.includes("exhaust") ||
normalized.includes("spent") ||
normalized.includes("deplet")
) {
return true;
}
}
if (normalized.includes("budget") && normalized.includes("zap")) {
if (
normalized.includes("exceed") ||
normalized.includes("spent") ||
normalized.includes("deplet")
) {
return true;
}
}
return false;
}
const ALLOWANCE_GUIDANCE_SUFFIX =
"Increase your wallet zap limit or reduce the platform split to continue.";
const DEFAULT_ALLOWANCE_MESSAGE =
"Your wallet's zap allowance has been exhausted. " + ALLOWANCE_GUIDANCE_SUFFIX;
export function buildZapAllowanceExhaustedMessage(error) {
const baseMessage = extractErrorMessage(error).trim();
if (!baseMessage) {
return DEFAULT_ALLOWANCE_MESSAGE;
}
const normalized = baseMessage.toLowerCase();
const punctuation = /[.!?]$/.test(baseMessage) ? "" : ".";
if (
normalized.includes("zap allowance") &&
normalized.includes("increase") &&
normalized.includes("platform split")
) {
return baseMessage;
}
if (normalized.includes("zap") && normalized.includes("allowance")) {
return `${baseMessage}${punctuation} ${ALLOWANCE_GUIDANCE_SUFFIX}`;
}
return `${baseMessage}${punctuation} ${DEFAULT_ALLOWANCE_MESSAGE}`;
}