applesauce-wallet-connect
Version:
NIP-47 Nostr Wallet Connect implementation for both clients and services.
114 lines (113 loc) • 5.04 kB
JavaScript
import { mergeRelaySets } from "applesauce-core/helpers";
/**
* Parses a nostr+walletauth URI
* @throws {Error} if the authorization URI is invalid
*/
export function parseWalletAuthURI(authURI) {
const { host, pathname, searchParams, protocol } = new URL(authURI);
// Check if it's a valid wallet auth protocol
if (!protocol.startsWith("nostr+walletauth")) {
throw new Error("invalid wallet auth uri protocol");
}
// Extract wallet name if present (nostr+walletauth+walletname://)
const walletName = protocol.includes("+") && protocol.split("+").length > 2 ? protocol.split("+")[2]?.replace(/:$/, "") : undefined;
// The client pubkey is in the pathname or host
const client = pathname || host;
if (!client)
throw new Error("missing client public key in authorization URI");
// Relay is required
const relays = mergeRelaySets(searchParams.getAll("relay"));
if (relays.length === 0)
throw new Error("missing required relay parameter in authorization URI");
// Parse optional parameters
const name = searchParams.get("name") ?? undefined;
const icon = searchParams.get("icon") ?? undefined;
const returnTo = searchParams.get("return_to") ?? undefined;
const expiresAtParam = searchParams.get("expires_at");
const expiresAt = expiresAtParam ? parseInt(expiresAtParam, 10) : undefined;
const maxAmountParam = searchParams.get("max_amount");
const maxAmount = maxAmountParam ? parseInt(maxAmountParam, 10) : undefined;
const budgetRenewal = searchParams.get("budget_renewal");
const methodsParam = searchParams.get("request_methods");
const methods = methodsParam ? methodsParam.split(" ") : undefined;
const notificationsParam = searchParams.get("notification_types");
const notifications = notificationsParam ? notificationsParam.split(" ") : undefined;
const isolatedParam = searchParams.get("isolated");
const isolated = isolatedParam ? isolatedParam === "true" : undefined;
const metadataParam = searchParams.get("metadata");
let metadata;
if (metadataParam) {
try {
metadata = JSON.parse(decodeURIComponent(metadataParam));
}
catch (error) {
throw new Error("invalid metadata parameter in authorization URI");
}
}
return {
client,
relays,
name,
icon,
returnTo,
expiresAt,
maxAmount,
budgetRenewal: budgetRenewal || undefined,
methods,
notifications,
isolated,
metadata,
walletName,
};
}
/**
* Creates a nostr+walletauth URI from a WalletAuthURI object
*/
export function createWalletAuthURI(parts) {
validateWalletAuthURI(parts);
// Determine the protocol based on whether wallet name is specified
const protocol = parts.walletName ? `nostr+walletauth+${parts.walletName}` : "nostr+walletauth";
const url = new URL(`${protocol}://${parts.client}`);
// Add required relay parameter
for (const relay of parts.relays)
url.searchParams.append("relay", relay);
// Add optional parameters
if (parts.name)
url.searchParams.append("name", parts.name);
if (parts.icon)
url.searchParams.append("icon", parts.icon);
if (parts.returnTo)
url.searchParams.append("return_to", parts.returnTo);
if (parts.expiresAt)
url.searchParams.append("expires_at", parts.expiresAt.toString());
if (parts.maxAmount)
url.searchParams.append("max_amount", parts.maxAmount.toString());
if (parts.budgetRenewal && parts.budgetRenewal !== "never")
url.searchParams.append("budget_renewal", parts.budgetRenewal);
if (parts.methods && parts.methods.length > 0)
url.searchParams.append("request_methods", parts.methods.join(" "));
if (parts.notifications && parts.notifications.length > 0)
url.searchParams.append("notification_types", parts.notifications.join(" "));
if (parts.isolated !== undefined)
url.searchParams.append("isolated", parts.isolated.toString());
if (parts.metadata)
url.searchParams.append("metadata", encodeURIComponent(JSON.stringify(parts.metadata)));
return url.toString();
}
/**
* Validates a WalletAuthURI object
* @returns true if valid, throws Error if invalid
*/
export function validateWalletAuthURI(parts) {
if (!parts.client || parts.client.length === 0)
throw new Error("client public key is required");
if (!parts.relays || parts.relays.length === 0)
throw new Error("at least one relay is required");
if (parts.expiresAt && parts.expiresAt <= Math.floor(Date.now() / 1000))
throw new Error("expires_at must be in the future");
if (parts.maxAmount && parts.maxAmount <= 0)
throw new Error("max_amount must be positive");
if (parts.budgetRenewal && !["never", "daily", "weekly", "monthly", "yearly"].includes(parts.budgetRenewal))
throw new Error("invalid budget_renewal value");
return true;
}