From 44a6ab6bd40ccdebc333f7e2f05a679cb7027169 Mon Sep 17 00:00:00 2001 From: Vitor Pamplona Date: Thu, 11 Jun 2026 14:00:32 -0400 Subject: [PATCH] fix(clink): keep offer/debit payments on clearnet + short subscription id Two bugs kept CLINK offer/debit round-trips from completing over the shared account relay client: - The offer relay was treated as a generic "new" relay, so with Tor on it was dialed through the proxy and failed on services that block Tor exits. Register the offer/debit relays as money-operation relays for the duration of the round-trip; the subscribe()-triggered reconnect plus the BasicRelayClient wrong-transport rebuild then move the socket to clearnet. - The subscription id "clink-offer-" was 76 chars; relays cap REQ subscription ids at 64 (NIP-01) and reject the over-long REQ outright, so the reply never arrived. Use newSubId(); the reply is matched by request id in the listener, not by subscription id. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../amethyst/service/ClinkDebitPayer.kt | 13 ++++++++++++- .../amethyst/service/ClinkOfferPayer.kt | 15 ++++++++++++++- 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt index a15ba45683..664b55ed61 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkDebitPayer.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.experimental.clink.client.DebitClient import com.vitorpamplona.quartz.experimental.clink.debits.DebitEvent @@ -28,6 +29,7 @@ import com.vitorpamplona.quartz.experimental.clink.debits.DebitResponse import com.vitorpamplona.quartz.experimental.clink.pointers.NDebit import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import kotlinx.coroutines.CancellationException @@ -104,7 +106,9 @@ object ClinkDebitPayer { val relays = client.pointer.relays.toSet() val reply = CompletableDeferred() - val subId = "clink-debit-${request.id}" + // A random short id: relays cap subscription ids at 64 chars (NIP-01); the reply is matched + // by request id in the listener, not by subId. + val subId = newSubId() val filters: Map> = relays.associateWith { listOf(client.responseFilter(request.id)) } val listener = @@ -121,6 +125,12 @@ object ClinkDebitPayer { } } + // Saved debit wallets are already fed into the money-op relay set by AccountsTorStateConnector, + // but register here too so a freshly-added wallet paid before that flow propagates — and any + // non-saved debit pointer — still routes under the money-operations Tor preference rather than + // the generic `newRelaysViaTor` policy. + val torState = Amethyst.instance.torEvaluatorFlow + torState.registerMoneyOpRelays(relays) account.client.subscribe(subId, filters, listener) return try { account.client.publish(request, relays) @@ -136,6 +146,7 @@ object ClinkDebitPayer { } } finally { account.client.unsubscribe(subId) + torState.unregisterMoneyOpRelays(relays) } } } diff --git a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt index a835db93e8..28a0d64fb0 100644 --- a/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt +++ b/amethyst/src/main/java/com/vitorpamplona/amethyst/service/ClinkOfferPayer.kt @@ -20,6 +20,7 @@ */ package com.vitorpamplona.amethyst.service +import com.vitorpamplona.amethyst.Amethyst import com.vitorpamplona.amethyst.model.Account import com.vitorpamplona.quartz.experimental.clink.client.OfferClient import com.vitorpamplona.quartz.experimental.clink.offers.OfferEvent @@ -28,6 +29,7 @@ import com.vitorpamplona.quartz.experimental.clink.pointers.NOffer import com.vitorpamplona.quartz.nip01Core.core.Event import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair import com.vitorpamplona.quartz.nip01Core.relay.client.reqs.SubscriptionListener +import com.vitorpamplona.quartz.nip01Core.relay.client.single.newSubId import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal @@ -76,7 +78,9 @@ object ClinkOfferPayer { val request = client.requestInvoice(amountSats = amountSats) val reply = CompletableDeferred() - val subId = "clink-offer-${request.id}" + // A random short id: relays cap subscription ids at 64 chars (NIP-01) and reject an + // over-long REQ outright. The reply is matched by request id in the listener, not by subId. + val subId = newSubId() val filters: Map> = relays.associateWith { listOf(client.responseFilter(request.id)) } val listener = @@ -93,6 +97,14 @@ object ClinkOfferPayer { } } + // The offer's relays are an ad-hoc payment endpoint, not a saved wallet, so register them + // as money-operation relays for the duration of the round-trip. Otherwise account.client + // would treat them as generic "new" relays and route them per `newRelaysViaTor`, silently + // pushing the payment through Tor (and failing on services that block Tor exits) even when + // the user disabled Tor for money operations. The subscribe() below triggers a reconnect, and + // BasicRelayClient rebuilds any socket left on the now-wrong (Tor) transport onto clearnet. + val torState = Amethyst.instance.torEvaluatorFlow + torState.registerMoneyOpRelays(relays) account.client.subscribe(subId, filters, listener) try { account.client.publish(request, relays) @@ -109,6 +121,7 @@ object ClinkOfferPayer { } } finally { account.client.unsubscribe(subId) + torState.unregisterMoneyOpRelays(relays) } } }