Browse Source

bug-fix payment handlers

remove orly
imwald
Silberengel 4 weeks ago
parent
commit
0681c4ff7f
  1. 1
      nip66-cron/index.mjs
  2. 3
      package.json
  3. 18
      scripts/start-orly-cache-relay.sh
  4. 3
      src/i18n/locales/en.ts
  5. 32
      src/lib/wallet-connection-details.ts
  6. 47
      src/lib/webln-payment.ts
  7. 60
      src/pages/secondary/WalletPage/WalletConnectionDetails.tsx
  8. 2
      src/pages/secondary/WalletPage/WalletZapSendingSettings.tsx
  9. 25
      src/providers/ZapProvider.tsx
  10. 28
      src/services/lightning.service.ts

1
nip66-cron/index.mjs

@ -41,7 +41,6 @@ const MONITOR_BOT_TAG = ['bot', 'true'] @@ -41,7 +41,6 @@ const MONITOR_BOT_TAG = ['bot', 'true']
// Deduplicated list of default relays to monitor (normalized URLs, first occurrence preserved)
const DEFAULT_RELAYS_TO_MONITOR = [
'wss://theforest.nostr1.com',
'wss://orly-relay.imwald.eu',
'wss://nostr.land',
'wss://thecitadel.nostr1.com',
'wss://relay.nostr.watch',

3
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "imwald",
"version": "23.13.3",
"version": "23.13.4",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true,
"type": "module",
@ -14,7 +14,6 @@ @@ -14,7 +14,6 @@
"homepage": "https://github.com/Silberengel/jumble",
"scripts": {
"dev": "vite --host",
"orly:relay": "bash scripts/start-orly-cache-relay.sh",
"dev:all": "bash scripts/dev-all-local.sh",
"stack:remote": "bash scripts/stack-remote.sh",
"dev:refresh": "rm -rf node_modules/.vite && vite --host",

18
scripts/start-orly-cache-relay.sh

@ -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

3
src/i18n/locales/en.ts

@ -1370,6 +1370,9 @@ export default { @@ -1370,6 +1370,9 @@ export default {
"most popular": "most popular",
"least popular": "least popular",
"Connected to": "Connected to",
"Wallet connector": "Wallet connector",
"NWC relay": "NWC relay",
"Not an NWC connection (no relay URL in config).": "Not an NWC connection (no relay URL in config).",
"Disconnect Wallet": "Disconnect Wallet",
"Are you absolutely sure?": "Are you absolutely sure?",
"You will not be able to send zaps to others.": "You will not be able to send zaps to others.",

32
src/lib/wallet-connection-details.ts

@ -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
}
}

47
src/lib/webln-payment.ts

@ -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
}

60
src/pages/secondary/WalletPage/WalletConnectionDetails.tsx

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

2
src/pages/secondary/WalletPage/WalletZapSendingSettings.tsx

@ -18,6 +18,7 @@ import DefaultZapAmountInput from './DefaultZapAmountInput' @@ -18,6 +18,7 @@ import DefaultZapAmountInput from './DefaultZapAmountInput'
import DefaultZapCommentInput from './DefaultZapCommentInput'
import QuickZapSwitch from './QuickZapSwitch'
import IncludePublicZapReceiptSwitch from './IncludePublicZapReceiptSwitch'
import WalletConnectionDetails from './WalletConnectionDetails'
export default function WalletZapSendingSettings() {
const { t } = useTranslation()
@ -32,6 +33,7 @@ export default function WalletZapSendingSettings() { @@ -32,6 +33,7 @@ export default function WalletZapSendingSettings() {
{t('Connected to')} <strong>{walletInfo.node.alias}</strong>
</div>
)}
<WalletConnectionDetails />
<AlertDialog>
<AlertDialogTrigger asChild>
<Button variant="destructive">{t('Disconnect Wallet')}</Button>

25
src/providers/ZapProvider.tsx

@ -1,9 +1,12 @@ @@ -1,9 +1,12 @@
import { LIGHTNING_WALLET_PAY_ENABLED } from '@/constants'
import { prepareConnectedWebLNProvider } from '@/lib/webln-payment'
import lightningService from '@/services/lightning.service'
import storage from '@/services/local-storage.service'
import { onConnected, onDisconnected } from '@getalby/bitcoin-connect-react'
import { disconnect, onConnected, onDisconnected } from '@getalby/bitcoin-connect-react'
import { GetInfoResponse, WebLNProvider } from '@webbtc/webln-types'
import { createContext, useContext, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
type TZapContext = {
isWalletConnected: boolean
@ -32,6 +35,7 @@ export const useZap = () => { @@ -32,6 +35,7 @@ export const useZap = () => {
}
export function ZapProvider({ children }: { children: React.ReactNode }) {
const { t } = useTranslation()
const [defaultZapSats, setDefaultZapSats] = useState<number>(storage.getDefaultZapSats())
const [defaultZapComment, setDefaultZapComment] = useState<string>(storage.getDefaultZapComment())
const [quickZap, setQuickZap] = useState<boolean>(storage.getQuickZap())
@ -47,11 +51,22 @@ export function ZapProvider({ children }: { children: React.ReactNode }) { @@ -47,11 +51,22 @@ export function ZapProvider({ children }: { children: React.ReactNode }) {
if (!LIGHTNING_WALLET_PAY_ENABLED) return
const unSubOnConnected = onConnected((provider) => {
setIsWalletConnected(true)
setIsWalletConnected(false)
setWalletInfo(null)
setProvider(provider)
lightningService.provider = provider
provider.getInfo().then(setWalletInfo)
void prepareConnectedWebLNProvider(provider)
.then((info) => {
setProvider(provider)
lightningService.provider = provider
setWalletInfo(info)
setIsWalletConnected(true)
})
.catch((error) => {
setProvider(null)
lightningService.provider = null
setIsWalletConnected(false)
disconnect()
toast.error(`${t('Lightning payment failed')}: ${(error as Error).message}`)
})
})
const unSubOnDisconnected = onDisconnected(() => {
setIsWalletConnected(false)

28
src/services/lightning.service.ts

@ -8,6 +8,10 @@ import { @@ -8,6 +8,10 @@ import {
import { getZapInfoFromEvent } from '@/lib/event-metadata'
import { TProfile } from '@/types'
import { init, launchPaymentModal } from '@getalby/bitcoin-connect-react'
import {
isNwcWalletServiceInfoError,
sendWebLNPaymentWithRetry
} from '@/lib/webln-payment'
import { Invoice } from '@getalby/lightning-tools'
import { bech32 } from '@scure/base'
import { WebLNProvider } from '@webbtc/webln-types'
@ -124,9 +128,15 @@ class LightningService { @@ -124,9 +128,15 @@ class LightningService {
}
if (this.provider) {
const { preimage } = await this.provider.sendPayment(pr)
closeOuterModel?.()
return { preimage, invoice: pr }
try {
const { preimage } = await sendWebLNPaymentWithRetry(this.provider, pr)
closeOuterModel?.()
return { preimage, invoice: pr }
} catch (error) {
if (!isNwcWalletServiceInfoError(error)) {
throw error
}
}
}
return new Promise((resolve) => {
@ -191,9 +201,15 @@ class LightningService { @@ -191,9 +201,15 @@ class LightningService {
closeOuterModel?: () => void
): Promise<{ preimage: string; invoice: string } | null> {
if (this.provider) {
const { preimage } = await this.provider.sendPayment(invoice)
closeOuterModel?.()
return { preimage, invoice: invoice }
try {
const { preimage } = await sendWebLNPaymentWithRetry(this.provider, invoice)
closeOuterModel?.()
return { preimage, invoice }
} catch (error) {
if (!isNwcWalletServiceInfoError(error)) {
throw error
}
}
}
return new Promise((resolve) => {

Loading…
Cancel
Save