10 changed files with 187 additions and 32 deletions
@ -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 @@ |
|||||||
|
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 @@ |
|||||||
|
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 @@ |
|||||||
|
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