10 changed files with 187 additions and 32 deletions
@ -1,18 +0,0 @@
@@ -1,18 +0,0 @@
|
||||
#!/usr/bin/env bash |
||||
# Start a local ORLY Nostr relay for Jumble dev/testing (sibling repo ../next.orly.dev). |
||||
# Default: ws://127.0.0.1:4869/ — add that URL as a cache relay in settings. |
||||
set -euo pipefail |
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)" |
||||
ORLY_ROOT="${ORLY_ROOT:-$ROOT/../next.orly.dev}" |
||||
export ORLY_PORT="${ORLY_PORT:-4869}" |
||||
export ORLY_ACL_MODE="${ORLY_ACL_MODE:-none}" |
||||
export ORLY_DATA_DIR="${ORLY_DATA_DIR:-${TMPDIR:-/tmp}/orly-jumble-relay-$ORLY_PORT}" |
||||
mkdir -p "$ORLY_DATA_DIR" |
||||
if [[ ! -d "$ORLY_ROOT/cmd/orly" ]]; then |
||||
echo "ORLY repo not found at: $ORLY_ROOT" >&2 |
||||
echo "Set ORLY_ROOT to your next.orly.dev checkout." >&2 |
||||
exit 1 |
||||
fi |
||||
echo "Orly: ws://127.0.0.1:${ORLY_PORT}/ (ORLY_DATA_DIR=$ORLY_DATA_DIR)" |
||||
cd "$ORLY_ROOT" |
||||
exec go run ./cmd/orly |
||||
@ -0,0 +1,32 @@
@@ -0,0 +1,32 @@
|
||||
import { getConnectorConfig } from '@getalby/bitcoin-connect-react' |
||||
|
||||
export type TWalletConnectionDetails = { |
||||
connectorName: string |
||||
connectorType: string |
||||
nwcRelayUrl: string | null |
||||
} |
||||
|
||||
/** Parse `relay=` from a nostr+walletconnect:// connection string. */ |
||||
export function parseNwcRelayUrl(nwcUrl: string): string | null { |
||||
try { |
||||
const httpLike = nwcUrl |
||||
.replace(/^nostr\+walletconnect:\/\//i, 'http://') |
||||
.replace(/^nostrwalletconnect:\/\//i, 'http://') |
||||
.replace(/^nostr\+walletconnect:/i, 'http://') |
||||
.replace(/^nostrwalletconnect:/i, 'http://') |
||||
return new URL(httpLike).searchParams.get('relay') |
||||
} catch { |
||||
return null |
||||
} |
||||
} |
||||
|
||||
/** Read the active Bitcoin Connect wallet config (from localStorage-backed store). */ |
||||
export function getBitcoinConnectWalletDetails(): TWalletConnectionDetails | null { |
||||
const config = getConnectorConfig() |
||||
if (!config) return null |
||||
return { |
||||
connectorName: config.connectorName, |
||||
connectorType: config.connectorType, |
||||
nwcRelayUrl: config.nwcUrl ? parseNwcRelayUrl(config.nwcUrl) : null |
||||
} |
||||
} |
||||
@ -0,0 +1,47 @@
@@ -0,0 +1,47 @@
|
||||
import type { GetInfoResponse, WebLNProvider } from '@webbtc/webln-types' |
||||
|
||||
/** NWC clients fetch wallet service info (kind 13194) before the first NIP-47 request. */ |
||||
export const NWC_WALLET_SERVICE_INFO_ERROR = 'no info event (kind 13194) returned from relay' |
||||
|
||||
export function isNwcWalletServiceInfoError(error: unknown): boolean { |
||||
return error instanceof Error && error.message.includes(NWC_WALLET_SERVICE_INFO_ERROR) |
||||
} |
||||
|
||||
function delay(ms: number): Promise<void> { |
||||
return new Promise((resolve) => setTimeout(resolve, ms)) |
||||
} |
||||
|
||||
/** Enable WebLN and load wallet info so NWC encryption is negotiated before paying. */ |
||||
export async function prepareConnectedWebLNProvider( |
||||
provider: WebLNProvider |
||||
): Promise<GetInfoResponse> { |
||||
await provider.enable() |
||||
return provider.getInfo() |
||||
} |
||||
|
||||
export async function sendWebLNPaymentWithRetry( |
||||
provider: WebLNProvider, |
||||
invoice: string, |
||||
maxAttempts = 3 |
||||
): Promise<{ preimage: string }> { |
||||
let lastError: unknown |
||||
for (let attempt = 0; attempt < maxAttempts; attempt++) { |
||||
try { |
||||
if (attempt > 0) { |
||||
try { |
||||
await provider.getInfo() |
||||
} catch { |
||||
// Warm-up only; sendPayment below reports the actionable error.
|
||||
} |
||||
} |
||||
return await provider.sendPayment(invoice) |
||||
} catch (error) { |
||||
lastError = error |
||||
if (!isNwcWalletServiceInfoError(error) || attempt === maxAttempts - 1) { |
||||
throw error |
||||
} |
||||
await delay(400 * (attempt + 1)) |
||||
} |
||||
} |
||||
throw lastError |
||||
} |
||||
@ -0,0 +1,60 @@
@@ -0,0 +1,60 @@
|
||||
import { Button } from '@/components/ui/button' |
||||
import { getBitcoinConnectWalletDetails } from '@/lib/wallet-connection-details' |
||||
import { useZap } from '@/providers/ZapProvider' |
||||
import { Copy } from 'lucide-react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import { toast } from 'sonner' |
||||
|
||||
export default function WalletConnectionDetails() { |
||||
const { t } = useTranslation() |
||||
const { isWalletConnected } = useZap() |
||||
if (!isWalletConnected) return null |
||||
|
||||
const details = getBitcoinConnectWalletDetails() |
||||
|
||||
if (!details) return null |
||||
|
||||
const copyRelay = () => { |
||||
if (!details.nwcRelayUrl) return |
||||
navigator.clipboard.writeText(details.nwcRelayUrl) |
||||
toast.success(t('Copied to clipboard')) |
||||
} |
||||
|
||||
return ( |
||||
<div className="mb-3 space-y-2 rounded-lg border border-border/80 bg-muted/25 px-3 py-2.5 text-sm"> |
||||
<div> |
||||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> |
||||
{t('Wallet connector')} |
||||
</p> |
||||
<p className="mt-0.5 leading-snug"> |
||||
{details.connectorName} |
||||
<span className="text-muted-foreground"> ({details.connectorType})</span> |
||||
</p> |
||||
</div> |
||||
<div> |
||||
<p className="text-xs font-medium uppercase tracking-wider text-muted-foreground"> |
||||
{t('NWC relay')} |
||||
</p> |
||||
{details.nwcRelayUrl ? ( |
||||
<div className="mt-1 flex min-w-0 items-start gap-2"> |
||||
<p className="min-w-0 flex-1 break-all font-mono text-xs leading-relaxed text-foreground/90"> |
||||
{details.nwcRelayUrl} |
||||
</p> |
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
size="icon" |
||||
className="size-8 shrink-0" |
||||
onClick={copyRelay} |
||||
aria-label={t('Copy to clipboard')} |
||||
> |
||||
<Copy className="size-4" /> |
||||
</Button> |
||||
</div> |
||||
) : ( |
||||
<p className="mt-0.5 text-muted-foreground">{t('Not an NWC connection (no relay URL in config).')}</p> |
||||
)} |
||||
</div> |
||||
</div> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue