refactor: extract AccountZapActions from Account
Moves the ~270-line zap/payment orchestration (NIP-57 zap requests, NWC wallet requests with spoof tracking, NIP-B1 BOLT12 zaps, NIP-BC onchain zaps/sends/splits) into AccountZapActions, exposed as account.zaps. The onchain backend-not-configured constant moves with it. External callers (ZapPaymentHandler, V4VPaymentHandler, wallet viewmodels, blossom payments, app functions) now call account.zaps.* directly. Moved code is unchanged except for account. qualification. Completes the Account decoupling series: Account.kt went from 6228 to 3618 lines across EventBroadcaster, AccountConcordActions, AccountMarmotActions, AccountRelayGroupActions, and AccountZapActions. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012sJgKJ4FAjcZqvkMc7U3EA
This commit is contained in:
@@ -24,7 +24,6 @@ import androidx.compose.runtime.Stable
|
||||
import com.vitorpamplona.amethyst.Amethyst
|
||||
import com.vitorpamplona.amethyst.BuildConfig
|
||||
import com.vitorpamplona.amethyst.LocalPreferences
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.audio.VisualizerStyle
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.InMemoryNip46ClientStore
|
||||
import com.vitorpamplona.amethyst.commons.connectedApps.nip46.Nip46ClientStore
|
||||
@@ -60,11 +59,6 @@ import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCa
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.ContactCardsState
|
||||
import com.vitorpamplona.amethyst.commons.model.nip85TrustedAssertions.TrustProviderListDecryptionCache
|
||||
import com.vitorpamplona.amethyst.commons.model.privateChats.hasEncryptedContent
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendError
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSender
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapShare
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthCustomToggles
|
||||
import com.vitorpamplona.amethyst.commons.relayauth.RelayAuthPermissionStore
|
||||
import com.vitorpamplona.amethyst.commons.richtext.RichTextParser
|
||||
@@ -254,12 +248,6 @@ import com.vitorpamplona.quartz.nip37Drafts.DraftWrapEvent
|
||||
import com.vitorpamplona.quartz.nip42RelayAuth.RelayAuthEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.events.NwcInfoEvent
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.IErrorResponseLike
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.BookmarkListEvent
|
||||
import com.vitorpamplona.quartz.nip51Lists.bookmarkList.tags.AddressBookmark
|
||||
import com.vitorpamplona.quartz.nip56Reports.ReportEvent
|
||||
@@ -319,8 +307,6 @@ import com.vitorpamplona.quartz.nipA0VoiceMessages.BaseVoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceEvent
|
||||
import com.vitorpamplona.quartz.nipA0VoiceMessages.VoiceReplyEvent
|
||||
import com.vitorpamplona.quartz.nipB0WebBookmarks.WebBookmarkEvent
|
||||
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.builder.Bolt12ZapBuilder
|
||||
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidation
|
||||
import com.vitorpamplona.quartz.nipC7Chats.ChatEvent
|
||||
import com.vitorpamplona.quartz.utils.DualCase
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
@@ -347,8 +333,6 @@ import kotlin.coroutines.cancellation.CancellationException
|
||||
import com.vitorpamplona.quartz.experimental.nip95.header.thumbhash as nip95thumbhash
|
||||
import com.vitorpamplona.quartz.experimental.profileGallery.thumbhash as galleryThumbhash
|
||||
|
||||
private const val ONCHAIN_BACKEND_NOT_CONFIGURED = "Bitcoin chain backend is not configured"
|
||||
|
||||
@OptIn(DelicateCoroutinesApi::class)
|
||||
@Stable
|
||||
class Account(
|
||||
@@ -726,6 +710,9 @@ class Account(
|
||||
/** NIP-29 relay-group + Buzz workspace orchestration. */
|
||||
val relayGroups = AccountRelayGroupActions(this)
|
||||
|
||||
/** Zap/payment orchestration (NIP-57, NWC, BOLT12, onchain). */
|
||||
val zaps = AccountZapActions(this)
|
||||
|
||||
/**
|
||||
* Relay routing + sign-and-publish choke point: computes which relays an event
|
||||
* should go to (outbox model, hints, channels, broadcast lists) and owns every
|
||||
@@ -1339,278 +1326,6 @@ class Account(
|
||||
cache.justConsumeMyOwnEvent(event)
|
||||
}
|
||||
|
||||
suspend fun createZapRequestFor(
|
||||
event: Event,
|
||||
pollOption: Int?,
|
||||
message: String = "",
|
||||
zapType: LnZapEvent.ZapType,
|
||||
toUser: User?,
|
||||
additionalRelays: Set<NormalizedRelayUrl>? = null,
|
||||
amountMillisats: Long? = null,
|
||||
lnurl: String? = null,
|
||||
) = LnZapRequestEvent.create(
|
||||
zappedEvent = event,
|
||||
relays = nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()),
|
||||
signer = signer,
|
||||
pollOption = pollOption,
|
||||
message = message,
|
||||
zapType = zapType,
|
||||
toUserPubHex = toUser?.pubkeyHex,
|
||||
amountMillisats = amountMillisats,
|
||||
lnurl = lnurl,
|
||||
)
|
||||
|
||||
suspend fun calculateIfNoteWasZappedByAccount(
|
||||
zappedNote: Note?,
|
||||
afterTimeInSeconds: Long,
|
||||
): Boolean = zappedNote?.isZappedBy(userProfile(), afterTimeInSeconds, this) == true
|
||||
|
||||
suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(nip47SignerState)
|
||||
|
||||
suspend fun sendNwcRequest(
|
||||
request: Request,
|
||||
onResponse: (Response?) -> Unit,
|
||||
) {
|
||||
val (event, relay) = nip47SignerState.sendNwcRequest(request, onResponse)
|
||||
client.publish(event, setOf(relay))
|
||||
}
|
||||
|
||||
suspend fun sendNwcRequestToWallet(
|
||||
walletUri: Nip47WalletConnect.Nip47URINorm,
|
||||
request: Request,
|
||||
onResponse: (Response?) -> Unit,
|
||||
): HexKey {
|
||||
val (event, relay) = nip47SignerState.sendNwcRequestToWallet(walletUri, request, onResponse)
|
||||
client.publish(event, setOf(relay))
|
||||
return event.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of spoofed (wrong-author) NIP-47 replies that have arrived for
|
||||
* the given request id. 0 if the request is unknown or already resolved.
|
||||
*/
|
||||
fun nwcSpoofAttempts(requestId: HexKey): Int = LocalCache.paymentTracker.spoofAttemptsFor(requestId)
|
||||
|
||||
/**
|
||||
* Removes a pending NIP-47 request from the tracker. Call this when the
|
||||
* UI gives up waiting (timeout) so the entry doesn't stick around.
|
||||
*/
|
||||
fun cleanupNwcRequest(requestId: HexKey) = LocalCache.paymentTracker.cleanup(requestId)
|
||||
|
||||
suspend fun sendZapPaymentRequestFor(
|
||||
bolt11: String,
|
||||
zappedNote: Note?,
|
||||
onResponse: (Response?) -> Unit,
|
||||
) {
|
||||
val (event, relay) = nip47SignerState.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
|
||||
client.publish(event, setOf(relay))
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the default NWC wallet advertises the nwc#2 `pay` method — the rail a
|
||||
* BOLT12 zap needs to obtain a payer proof. Read from the wallet's cached kind:13194
|
||||
* info event (its capability advertisement), which [NwcSignerState] already refreshes
|
||||
* on wallet change. A missing/unfetched info event reads as false, so the zap path
|
||||
* falls back to lightning rather than attempting a `pay` the wallet can't honor.
|
||||
*/
|
||||
fun defaultWalletSupportsBolt12Pay(): Boolean {
|
||||
val uri = nip47SignerState.defaultWalletUri.value ?: return false
|
||||
return nip47SignerState.infoCache?.current(uri)?.supportsMethod(NwcMethod.PAY) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a NIP-B1 BOLT12 zap to [recipientPubKey] over the default NWC wallet.
|
||||
*
|
||||
* Signs a kind 9737 intent, pays [offer] via the nwc#2 `pay` method with the
|
||||
* intent-bound `payer_note`, then — only if the wallet returns a payer proof that
|
||||
* validates — builds, self-consumes, and publishes the kind 9736 zap. Validation
|
||||
* is the fail-safe: a wallet that drops or misroutes the note yields a proof that
|
||||
* fails the binding check, so no invalid receipt is ever published (the payment
|
||||
* still happened; [onError] reports "paid, no receipt"). [zappedEvent] is null for
|
||||
* a profile zap. Requires an NWC wallet (see [hasNwcWallet]); BOLT12 zaps have no
|
||||
* external-wallet or LNURL fallback because only NWC returns the proof.
|
||||
*/
|
||||
suspend fun sendBolt12Zap(
|
||||
zappedEvent: Event?,
|
||||
recipientPubKey: HexKey,
|
||||
offer: String,
|
||||
amountMillisats: Long,
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
// (messageResId, detail) — the caller localizes; detail carries a wallet error, if any.
|
||||
onError: (Int, String?) -> Unit,
|
||||
onProcessed: () -> Unit,
|
||||
) {
|
||||
// NONZAP means "pay, but publish no receipt" — settle the offer without binding
|
||||
// a zap intent or emitting a 9736, matching the privacy of a bolt11 NONZAP.
|
||||
if (zapType == LnZapEvent.ZapType.NONZAP) {
|
||||
sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats)) { response ->
|
||||
scope.launch {
|
||||
if (response is IErrorResponseLike) onError(R.string.bolt12_payment_failed, response.errorMessage())
|
||||
onProcessed()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val anonymous = zapType == LnZapEvent.ZapType.ANONYMOUS
|
||||
// The 9737 intent and the 9736 zap MUST be signed by the same key. An anonymous
|
||||
// zap uses a fresh ephemeral key so it carries no `P` tag and isn't traceable.
|
||||
val zapSigner = if (anonymous) NostrSignerInternal(KeyPair()) else signer
|
||||
|
||||
val intent =
|
||||
if (zappedEvent == null) {
|
||||
Bolt12ZapBuilder.buildProfileIntent(zapSigner, recipientPubKey, amountMillisats, offer, message)
|
||||
} else {
|
||||
Bolt12ZapBuilder.buildIntent(zapSigner, recipientPubKey, amountMillisats, offer, EventHintBundle(zappedEvent), message)
|
||||
}
|
||||
|
||||
val payerNote = Bolt12ZapBuilder.payerNote(intent)
|
||||
|
||||
sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats, payerNote)) { response ->
|
||||
scope.launch {
|
||||
// try/finally so a failure while assembling/publishing the receipt (e.g. a
|
||||
// remote signer error) still steps progress and surfaces an error, instead
|
||||
// of vanishing as an uncaught coroutine exception. The payment already
|
||||
// settled at this point, so such a failure means "paid, no receipt".
|
||||
try {
|
||||
when (response) {
|
||||
is PaySuccessResponse -> {
|
||||
val proof = response.result?.payer_proof
|
||||
if (proof.isNullOrBlank()) {
|
||||
onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
} else {
|
||||
val zap = Bolt12ZapBuilder.buildZap(zapSigner, intent, proof, anonymous)
|
||||
if (cache.bolt12ZapValidator.validate(zap, verifyEventSignature = false) is Bolt12ZapValidation.Valid) {
|
||||
cache.justConsumeMyOwnEvent(zap)
|
||||
client.publish(zap, computeRelayListToBroadcast(zap))
|
||||
} else {
|
||||
onError(R.string.bolt12_zap_invalid_receipt, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is IErrorResponseLike -> onError(R.string.bolt12_payment_failed, response.errorMessage())
|
||||
|
||||
else -> onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w("Account", "BOLT12 zap receipt assembly failed after payment", e)
|
||||
onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
} finally {
|
||||
onProcessed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createZapRequestFor(
|
||||
user: User,
|
||||
message: String = "",
|
||||
zapType: LnZapEvent.ZapType,
|
||||
amountMillisats: Long? = null,
|
||||
lnurl: String? = null,
|
||||
): LnZapRequestEvent {
|
||||
val zapRequest =
|
||||
LnZapRequestEvent.create(
|
||||
userHex = user.pubkeyHex,
|
||||
relays = nip65RelayList.inboxFlow.value + (user.inboxRelays() ?: emptyList()),
|
||||
signer = signer,
|
||||
message = message,
|
||||
zapType = zapType,
|
||||
amountMillisats = amountMillisats,
|
||||
lnurl = lnurl,
|
||||
)
|
||||
|
||||
cache.justConsumeMyOwnEvent(zapRequest)
|
||||
return zapRequest
|
||||
}
|
||||
|
||||
private fun onchainBackendNotConfigured() =
|
||||
OnchainZapSendResult.Failure(
|
||||
OnchainZapSendStage.LOADING_UTXOS,
|
||||
OnchainZapSendError.BACKEND_NOT_CONFIGURED,
|
||||
ONCHAIN_BACKEND_NOT_CONFIGURED,
|
||||
)
|
||||
|
||||
/**
|
||||
* Send a NIP-BC onchain zap: build a Bitcoin transaction paying the recipient's
|
||||
* derived Taproot address, sign it, broadcast it, and publish the kind:8333
|
||||
* zap receipt. Pass [zappedEvent] to attribute the zap to a specific event, or
|
||||
* leave it null for a profile zap.
|
||||
*/
|
||||
suspend fun sendOnchainZap(
|
||||
recipientPubKey: HexKey,
|
||||
amountSats: Long,
|
||||
feeRateSatPerVByte: Double,
|
||||
comment: String = "",
|
||||
zappedEvent: EventHintBundle<out Event>? = null,
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
cache.onchainBackend
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.send(
|
||||
backend = backend,
|
||||
signer = signer,
|
||||
senderPubKey = signer.pubKey,
|
||||
recipientPubKey = recipientPubKey,
|
||||
amountSats = amountSats,
|
||||
feeRateSatPerVByte = feeRateSatPerVByte,
|
||||
comment = comment,
|
||||
zappedEvent = zappedEvent,
|
||||
) { template -> signAndComputeBroadcast(template) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Pay an explicit Bitcoin address (e.g. a profile's NIP-A3 `bitcoin`
|
||||
* payment target) from the NIP-BC Taproot wallet. A plain wallet send —
|
||||
* no kind:8333 receipt is published. See [OnchainZapSender.sendToAddress].
|
||||
*/
|
||||
suspend fun sendOnchainToAddress(
|
||||
recipientAddress: String,
|
||||
amountSats: Long,
|
||||
feeRateSatPerVByte: Double,
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
cache.onchainBackend
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.sendToAddress(
|
||||
backend = backend,
|
||||
signer = signer,
|
||||
senderPubKey = signer.pubKey,
|
||||
recipientAddress = recipientAddress,
|
||||
amountSats = amountSats,
|
||||
feeRateSatPerVByte = feeRateSatPerVByte,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a NIP-BC onchain split zap: a single Bitcoin transaction paying
|
||||
* each recipient their precomputed share, plus one kind:8333 receipt per
|
||||
* recipient. See [OnchainZapSender.sendSplit] for failure semantics.
|
||||
*/
|
||||
suspend fun sendOnchainZapWithSplits(
|
||||
recipients: List<OnchainZapShare>,
|
||||
feeRateSatPerVByte: Double,
|
||||
comment: String = "",
|
||||
zappedEvent: EventHintBundle<out Event>? = null,
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
cache.onchainBackend
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.sendSplit(
|
||||
backend = backend,
|
||||
signer = signer,
|
||||
senderPubKey = signer.pubKey,
|
||||
recipients = recipients,
|
||||
feeRateSatPerVByte = feeRateSatPerVByte,
|
||||
comment = comment,
|
||||
zappedEvent = zappedEvent,
|
||||
) { template -> signAndComputeBroadcast(template) }
|
||||
}
|
||||
|
||||
suspend fun report(
|
||||
note: Note,
|
||||
type: ReportType,
|
||||
|
||||
@@ -0,0 +1,337 @@
|
||||
/*
|
||||
* Copyright (c) 2025 Vitor Pamplona
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining a copy of
|
||||
* this software and associated documentation files (the "Software"), to deal in
|
||||
* the Software without restriction, including without limitation the rights to use,
|
||||
* copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the
|
||||
* Software, and to permit persons to whom the Software is furnished to do so,
|
||||
* subject to the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included in all
|
||||
* copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
|
||||
* FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
|
||||
* COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
|
||||
* AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
* WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
package com.vitorpamplona.amethyst.model
|
||||
|
||||
import com.vitorpamplona.amethyst.R
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendError
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendResult
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSendStage
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapSender
|
||||
import com.vitorpamplona.amethyst.commons.onchain.OnchainZapShare
|
||||
import com.vitorpamplona.amethyst.model.nip47WalletConnect.NwcSignerState
|
||||
import com.vitorpamplona.quartz.nip01Core.core.Event
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.crypto.KeyPair
|
||||
import com.vitorpamplona.quartz.nip01Core.hints.EventHintBundle
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip01Core.signers.NostrSignerInternal
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.Nip47WalletConnect
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.IErrorResponseLike
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.NwcMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PayMethod
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.PaySuccessResponse
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Request
|
||||
import com.vitorpamplona.quartz.nip47WalletConnect.rpc.Response
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapEvent
|
||||
import com.vitorpamplona.quartz.nip57Zaps.LnZapRequestEvent
|
||||
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.builder.Bolt12ZapBuilder
|
||||
import com.vitorpamplona.quartz.nipB1Bolt12Zaps.verify.Bolt12ZapValidation
|
||||
import com.vitorpamplona.quartz.utils.Log
|
||||
import kotlinx.coroutines.launch
|
||||
import java.math.BigDecimal
|
||||
import kotlin.coroutines.cancellation.CancellationException
|
||||
|
||||
private const val ONCHAIN_BACKEND_NOT_CONFIGURED = "Bitcoin chain backend is not configured"
|
||||
|
||||
/**
|
||||
* Zap and payment orchestration for an [Account]: NIP-57 zap requests, NIP-47
|
||||
* NWC wallet requests (with spoof tracking), NIP-B1 BOLT12 zaps, and NIP-BC
|
||||
* onchain zaps/sends. Event building lives in the commons ZapActions/
|
||||
* Bolt12ZapActions; this class wires wallet selection, signing, and relay
|
||||
* routing to the account.
|
||||
*/
|
||||
class AccountZapActions(
|
||||
private val account: Account,
|
||||
) {
|
||||
suspend fun createZapRequestFor(
|
||||
event: Event,
|
||||
pollOption: Int?,
|
||||
message: String = "",
|
||||
zapType: LnZapEvent.ZapType,
|
||||
toUser: User?,
|
||||
additionalRelays: Set<NormalizedRelayUrl>? = null,
|
||||
amountMillisats: Long? = null,
|
||||
lnurl: String? = null,
|
||||
) = LnZapRequestEvent.create(
|
||||
zappedEvent = event,
|
||||
relays = account.nip65RelayList.inboxFlow.value + (additionalRelays ?: emptySet()),
|
||||
signer = account.signer,
|
||||
pollOption = pollOption,
|
||||
message = message,
|
||||
zapType = zapType,
|
||||
toUserPubHex = toUser?.pubkeyHex,
|
||||
amountMillisats = amountMillisats,
|
||||
lnurl = lnurl,
|
||||
)
|
||||
|
||||
suspend fun calculateIfNoteWasZappedByAccount(
|
||||
zappedNote: Note?,
|
||||
afterTimeInSeconds: Long,
|
||||
): Boolean = zappedNote?.isZappedBy(account.userProfile(), afterTimeInSeconds, account) == true
|
||||
|
||||
suspend fun calculateZappedAmount(zappedNote: Note): BigDecimal = zappedNote.zappedAmountWithNWCPayments(account.nip47SignerState)
|
||||
|
||||
suspend fun sendNwcRequest(
|
||||
request: Request,
|
||||
onResponse: (Response?) -> Unit,
|
||||
) {
|
||||
val (event, relay) = account.nip47SignerState.sendNwcRequest(request, onResponse)
|
||||
account.client.publish(event, setOf(relay))
|
||||
}
|
||||
|
||||
suspend fun sendNwcRequestToWallet(
|
||||
walletUri: Nip47WalletConnect.Nip47URINorm,
|
||||
request: Request,
|
||||
onResponse: (Response?) -> Unit,
|
||||
): HexKey {
|
||||
val (event, relay) = account.nip47SignerState.sendNwcRequestToWallet(walletUri, request, onResponse)
|
||||
account.client.publish(event, setOf(relay))
|
||||
return event.id
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of spoofed (wrong-author) NIP-47 replies that have arrived for
|
||||
* the given request id. 0 if the request is unknown or already resolved.
|
||||
*/
|
||||
fun nwcSpoofAttempts(requestId: HexKey): Int = LocalCache.paymentTracker.spoofAttemptsFor(requestId)
|
||||
|
||||
/**
|
||||
* Removes a pending NIP-47 request from the tracker. Call this when the
|
||||
* UI gives up waiting (timeout) so the entry doesn't stick around.
|
||||
*/
|
||||
fun cleanupNwcRequest(requestId: HexKey) = LocalCache.paymentTracker.cleanup(requestId)
|
||||
|
||||
suspend fun sendZapPaymentRequestFor(
|
||||
bolt11: String,
|
||||
zappedNote: Note?,
|
||||
onResponse: (Response?) -> Unit,
|
||||
) {
|
||||
val (event, relay) = account.nip47SignerState.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
|
||||
account.client.publish(event, setOf(relay))
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the default NWC wallet advertises the nwc#2 `pay` method — the rail a
|
||||
* BOLT12 zap needs to obtain a payer proof. Read from the wallet's cached kind:13194
|
||||
* info event (its capability advertisement), which [NwcSignerState] already refreshes
|
||||
* on wallet change. A missing/unfetched info event reads as false, so the zap path
|
||||
* falls back to lightning rather than attempting a `pay` the wallet can't honor.
|
||||
*/
|
||||
fun defaultWalletSupportsBolt12Pay(): Boolean {
|
||||
val uri = account.nip47SignerState.defaultWalletUri.value ?: return false
|
||||
return account.nip47SignerState.infoCache
|
||||
?.current(uri)
|
||||
?.supportsMethod(NwcMethod.PAY) == true
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a NIP-B1 BOLT12 zap to [recipientPubKey] over the default NWC wallet.
|
||||
*
|
||||
* Signs a kind 9737 intent, pays [offer] via the nwc#2 `pay` method with the
|
||||
* intent-bound `payer_note`, then — only if the wallet returns a payer proof that
|
||||
* validates — builds, self-consumes, and publishes the kind 9736 zap. Validation
|
||||
* is the fail-safe: a wallet that drops or misroutes the note yields a proof that
|
||||
* fails the binding check, so no invalid receipt is ever published (the payment
|
||||
* still happened; [onError] reports "paid, no receipt"). [zappedEvent] is null for
|
||||
* a profile zap. Requires an NWC wallet (see [hasNwcWallet]); BOLT12 zaps have no
|
||||
* external-wallet or LNURL fallback because only NWC returns the proof.
|
||||
*/
|
||||
suspend fun sendBolt12Zap(
|
||||
zappedEvent: Event?,
|
||||
recipientPubKey: HexKey,
|
||||
offer: String,
|
||||
amountMillisats: Long,
|
||||
message: String,
|
||||
zapType: LnZapEvent.ZapType,
|
||||
// (messageResId, detail) — the caller localizes; detail carries a wallet error, if any.
|
||||
onError: (Int, String?) -> Unit,
|
||||
onProcessed: () -> Unit,
|
||||
) {
|
||||
// NONZAP means "pay, but publish no receipt" — settle the offer without binding
|
||||
// a zap intent or emitting a 9736, matching the privacy of a bolt11 NONZAP.
|
||||
if (zapType == LnZapEvent.ZapType.NONZAP) {
|
||||
sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats)) { response ->
|
||||
account.scope.launch {
|
||||
if (response is IErrorResponseLike) onError(R.string.bolt12_payment_failed, response.errorMessage())
|
||||
onProcessed()
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
val anonymous = zapType == LnZapEvent.ZapType.ANONYMOUS
|
||||
// The 9737 intent and the 9736 zap MUST be signed by the same key. An anonymous
|
||||
// zap uses a fresh ephemeral key so it carries no `P` tag and isn't traceable.
|
||||
val zapSigner = if (anonymous) NostrSignerInternal(KeyPair()) else account.signer
|
||||
|
||||
val intent =
|
||||
if (zappedEvent == null) {
|
||||
Bolt12ZapBuilder.buildProfileIntent(zapSigner, recipientPubKey, amountMillisats, offer, message)
|
||||
} else {
|
||||
Bolt12ZapBuilder.buildIntent(zapSigner, recipientPubKey, amountMillisats, offer, EventHintBundle(zappedEvent), message)
|
||||
}
|
||||
|
||||
val payerNote = Bolt12ZapBuilder.payerNote(intent)
|
||||
|
||||
sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats, payerNote)) { response ->
|
||||
account.scope.launch {
|
||||
// try/finally so a failure while assembling/publishing the receipt (e.g. a
|
||||
// remote signer error) still steps progress and surfaces an error, instead
|
||||
// of vanishing as an uncaught coroutine exception. The payment already
|
||||
// settled at this point, so such a failure means "paid, no receipt".
|
||||
try {
|
||||
when (response) {
|
||||
is PaySuccessResponse -> {
|
||||
val proof = response.result?.payer_proof
|
||||
if (proof.isNullOrBlank()) {
|
||||
onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
} else {
|
||||
val zap = Bolt12ZapBuilder.buildZap(zapSigner, intent, proof, anonymous)
|
||||
if (account.cache.bolt12ZapValidator.validate(zap, verifyEventSignature = false) is Bolt12ZapValidation.Valid) {
|
||||
account.cache.justConsumeMyOwnEvent(zap)
|
||||
account.client.publish(zap, account.broadcaster.computeRelayListToBroadcast(zap))
|
||||
} else {
|
||||
onError(R.string.bolt12_zap_invalid_receipt, null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
is IErrorResponseLike -> onError(R.string.bolt12_payment_failed, response.errorMessage())
|
||||
|
||||
else -> onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
}
|
||||
} catch (e: CancellationException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
Log.w("Account", "BOLT12 zap receipt assembly failed after payment", e)
|
||||
onError(R.string.bolt12_zap_paid_no_receipt, null)
|
||||
} finally {
|
||||
onProcessed()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun createZapRequestFor(
|
||||
user: User,
|
||||
message: String = "",
|
||||
zapType: LnZapEvent.ZapType,
|
||||
amountMillisats: Long? = null,
|
||||
lnurl: String? = null,
|
||||
): LnZapRequestEvent {
|
||||
val zapRequest =
|
||||
LnZapRequestEvent.create(
|
||||
userHex = user.pubkeyHex,
|
||||
relays = account.nip65RelayList.inboxFlow.value + (user.inboxRelays() ?: emptyList()),
|
||||
signer = account.signer,
|
||||
message = message,
|
||||
zapType = zapType,
|
||||
amountMillisats = amountMillisats,
|
||||
lnurl = lnurl,
|
||||
)
|
||||
|
||||
account.cache.justConsumeMyOwnEvent(zapRequest)
|
||||
return zapRequest
|
||||
}
|
||||
|
||||
private fun onchainBackendNotConfigured() =
|
||||
OnchainZapSendResult.Failure(
|
||||
OnchainZapSendStage.LOADING_UTXOS,
|
||||
OnchainZapSendError.BACKEND_NOT_CONFIGURED,
|
||||
ONCHAIN_BACKEND_NOT_CONFIGURED,
|
||||
)
|
||||
|
||||
/**
|
||||
* Send a NIP-BC onchain zap: build a Bitcoin transaction paying the recipient's
|
||||
* derived Taproot address, sign it, broadcast it, and publish the kind:8333
|
||||
* zap receipt. Pass [zappedEvent] to attribute the zap to a specific event, or
|
||||
* leave it null for a profile zap.
|
||||
*/
|
||||
suspend fun sendOnchainZap(
|
||||
recipientPubKey: HexKey,
|
||||
amountSats: Long,
|
||||
feeRateSatPerVByte: Double,
|
||||
comment: String = "",
|
||||
zappedEvent: EventHintBundle<out Event>? = null,
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
account.cache.onchainBackend
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.send(
|
||||
backend = backend,
|
||||
signer = account.signer,
|
||||
senderPubKey = account.signer.pubKey,
|
||||
recipientPubKey = recipientPubKey,
|
||||
amountSats = amountSats,
|
||||
feeRateSatPerVByte = feeRateSatPerVByte,
|
||||
comment = comment,
|
||||
zappedEvent = zappedEvent,
|
||||
) { template -> account.broadcaster.signAndComputeBroadcast(template) }
|
||||
}
|
||||
|
||||
/**
|
||||
* Pay an explicit Bitcoin address (e.g. a profile's NIP-A3 `bitcoin`
|
||||
* payment target) from the NIP-BC Taproot wallet. A plain wallet send —
|
||||
* no kind:8333 receipt is published. See [OnchainZapSender.sendToAddress].
|
||||
*/
|
||||
suspend fun sendOnchainToAddress(
|
||||
recipientAddress: String,
|
||||
amountSats: Long,
|
||||
feeRateSatPerVByte: Double,
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
account.cache.onchainBackend
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.sendToAddress(
|
||||
backend = backend,
|
||||
signer = account.signer,
|
||||
senderPubKey = account.signer.pubKey,
|
||||
recipientAddress = recipientAddress,
|
||||
amountSats = amountSats,
|
||||
feeRateSatPerVByte = feeRateSatPerVByte,
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a NIP-BC onchain split zap: a single Bitcoin transaction paying
|
||||
* each recipient their precomputed share, plus one kind:8333 receipt per
|
||||
* recipient. See [OnchainZapSender.sendSplit] for failure semantics.
|
||||
*/
|
||||
suspend fun sendOnchainZapWithSplits(
|
||||
recipients: List<OnchainZapShare>,
|
||||
feeRateSatPerVByte: Double,
|
||||
comment: String = "",
|
||||
zappedEvent: EventHintBundle<out Event>? = null,
|
||||
): OnchainZapSendResult {
|
||||
val backend =
|
||||
account.cache.onchainBackend
|
||||
?: return onchainBackendNotConfigured()
|
||||
return OnchainZapSender.sendSplit(
|
||||
backend = backend,
|
||||
signer = account.signer,
|
||||
senderPubKey = account.signer.pubKey,
|
||||
recipients = recipients,
|
||||
feeRateSatPerVByte = feeRateSatPerVByte,
|
||||
comment = comment,
|
||||
zappedEvent = zappedEvent,
|
||||
) { template -> account.broadcaster.signAndComputeBroadcast(template) }
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -279,7 +279,7 @@ class AccountNappletGateways(
|
||||
}
|
||||
|
||||
val result = CompletableDeferred<String?>()
|
||||
account.sendZapPaymentRequestFor(invoice, null) { response ->
|
||||
account.zaps.sendZapPaymentRequestFor(invoice, null) { response ->
|
||||
when (response) {
|
||||
is PayInvoiceSuccessResponse -> result.complete(response.result?.preimage)
|
||||
is PayInvoiceErrorResponse -> result.completeExceptionally(RuntimeException(response.error?.message ?: "Payment failed."))
|
||||
|
||||
@@ -159,7 +159,7 @@ class V4VPaymentHandler(
|
||||
tlvRecords = tlvRecords,
|
||||
)
|
||||
|
||||
account.sendNwcRequest(request) { response: Response? ->
|
||||
account.zaps.sendNwcRequest(request) { response: Response? ->
|
||||
if (response is IErrorResponseLike) {
|
||||
onError(
|
||||
stringRes(context, R.string.error_dialog_pay_invoice_error),
|
||||
@@ -195,7 +195,7 @@ class V4VPaymentHandler(
|
||||
try {
|
||||
val nostrRequest =
|
||||
if (asZap && noteEvent != null) {
|
||||
account.createZapRequestFor(
|
||||
account.zaps.createZapRequestFor(
|
||||
event = noteEvent,
|
||||
pollOption = null,
|
||||
message = message,
|
||||
@@ -250,7 +250,7 @@ class V4VPaymentHandler(
|
||||
is PaymentSource.Nwc -> {
|
||||
var done = 0
|
||||
payables.forEach { payable ->
|
||||
account.sendZapPaymentRequestFor(payable.invoice, zappedNote) { response ->
|
||||
account.zaps.sendZapPaymentRequestFor(payable.invoice, zappedNote) { response ->
|
||||
if (response is IErrorResponseLike) {
|
||||
onError(
|
||||
stringRes(context, R.string.error_dialog_pay_invoice_error),
|
||||
|
||||
@@ -163,7 +163,7 @@ class ZapPaymentHandler(
|
||||
val canBolt12 =
|
||||
account.settings.nwcWallets.value
|
||||
.isNotEmpty() &&
|
||||
account.defaultWalletSupportsBolt12Pay()
|
||||
account.zaps.defaultWalletSupportsBolt12Pay()
|
||||
|
||||
val bolt12Recipients =
|
||||
unverifiedZapsToSend.mapNotNull {
|
||||
@@ -330,7 +330,7 @@ class ZapPaymentHandler(
|
||||
|
||||
val zapRequest =
|
||||
if (zapType != LnZapEvent.ZapType.NONZAP && noteEvent != null) {
|
||||
account.createZapRequestFor(
|
||||
account.zaps.createZapRequestFor(
|
||||
event = noteEvent,
|
||||
pollOption = pollOption,
|
||||
message = message,
|
||||
@@ -414,7 +414,7 @@ class ZapPaymentHandler(
|
||||
return mapNotNullAsync(
|
||||
items = payables,
|
||||
runRequestFor = { payable: Payable ->
|
||||
account.sendZapPaymentRequestFor(
|
||||
account.zaps.sendZapPaymentRequestFor(
|
||||
bolt11 = payable.invoice,
|
||||
zappedNote = note,
|
||||
onResponse = { response ->
|
||||
@@ -462,7 +462,7 @@ class ZapPaymentHandler(
|
||||
val progress = PaymentProgress(recipients.size, onProgress)
|
||||
|
||||
mapNotNullAsync(recipients) { recipient: Bolt12Recipient ->
|
||||
account.sendBolt12Zap(
|
||||
account.zaps.sendBolt12Zap(
|
||||
zappedEvent = note.event,
|
||||
recipientPubKey = recipient.user.pubkeyHex,
|
||||
offer = recipient.offer,
|
||||
|
||||
+1
-1
@@ -166,7 +166,7 @@ object BlossomPaymentHandler {
|
||||
|
||||
val preimageResult = CompletableDeferred<String?>()
|
||||
try {
|
||||
account.sendZapPaymentRequestFor(invoice, null) { response ->
|
||||
account.zaps.sendZapPaymentRequestFor(invoice, null) { response ->
|
||||
// CompletableDeferred.complete is idempotent, so extra callbacks are harmless.
|
||||
preimageResult.complete((response as? PayInvoiceSuccessResponse)?.result?.preimage)
|
||||
}
|
||||
|
||||
@@ -103,7 +103,7 @@ class PollNoteViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
totalZapped = totalZapped()
|
||||
wasZappedByLoggedInAccount = false
|
||||
wasZappedByLoggedInAccount = account.calculateIfNoteWasZappedByAccount(pollNote, 0)
|
||||
wasZappedByLoggedInAccount = account.zaps.calculateIfNoteWasZappedByAccount(pollNote, 0)
|
||||
canZap.value = checkIfCanZap()
|
||||
|
||||
tallies.forEach {
|
||||
|
||||
@@ -150,7 +150,7 @@ fun GoalProgressBar(
|
||||
|
||||
LaunchedEffect(key1 = zapsState) {
|
||||
zapsState?.note?.let {
|
||||
val newZapAmount = accountViewModel.account.calculateZappedAmount(note)
|
||||
val newZapAmount = accountViewModel.account.zaps.calculateZappedAmount(note)
|
||||
var percentage = newZapAmount.div(goalAmountSats.toBigDecimal()).toFloat()
|
||||
if (percentage > 1) percentage = 1f
|
||||
|
||||
|
||||
+7
-7
@@ -843,7 +843,7 @@ class AccountViewModel(
|
||||
afterTimeInSeconds: Long,
|
||||
): Boolean =
|
||||
withContext(Dispatchers.IO) {
|
||||
account.calculateIfNoteWasZappedByAccount(zappedNote, afterTimeInSeconds)
|
||||
account.zaps.calculateIfNoteWasZappedByAccount(zappedNote, afterTimeInSeconds)
|
||||
}
|
||||
|
||||
suspend fun calculateZapAmount(zappedNote: Note): String {
|
||||
@@ -854,7 +854,7 @@ class AccountViewModel(
|
||||
val ownPendingOnchain = zappedNote.extraOwnPendingOnchainSats(account.userProfile().pubkeyHex)
|
||||
return if (zappedNote.zapPayments.isNotEmpty()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val nwc = account.calculateZappedAmount(zappedNote)
|
||||
val nwc = account.zaps.calculateZappedAmount(zappedNote)
|
||||
showAmount(nwc + java.math.BigDecimal(ownPendingOnchain))
|
||||
}
|
||||
} else {
|
||||
@@ -866,7 +866,7 @@ class AccountViewModel(
|
||||
val zapraiserAmount = zappedNote.event?.zapraiserAmount() ?: 0
|
||||
return if (zappedNote.zapPayments.isNotEmpty()) {
|
||||
withContext(Dispatchers.IO) {
|
||||
val newZapAmount = account.calculateZappedAmount(zappedNote)
|
||||
val newZapAmount = account.zaps.calculateZappedAmount(zappedNote)
|
||||
var percentage = newZapAmount.div(zapraiserAmount.toBigDecimal()).toFloat()
|
||||
|
||||
if (percentage > 1) {
|
||||
@@ -1202,7 +1202,7 @@ class AccountViewModel(
|
||||
.isNotEmpty()
|
||||
|
||||
/** True when a BOLT12 offer can be paid in-app: an NWC wallet is set and advertises `pay` (nwc#2). */
|
||||
fun canPayBolt12ViaNwc(): Boolean = hasNwcWallet() && account.defaultWalletSupportsBolt12Pay()
|
||||
fun canPayBolt12ViaNwc(): Boolean = hasNwcWallet() && account.zaps.defaultWalletSupportsBolt12Pay()
|
||||
|
||||
/**
|
||||
* Pays a recipient's BOLT12 [offer] over the default NWC wallet using the nwc#2
|
||||
@@ -1214,7 +1214,7 @@ class AccountViewModel(
|
||||
offer: String,
|
||||
amountMillisats: Long,
|
||||
) = launchSigner {
|
||||
account.sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats)) { response ->
|
||||
account.zaps.sendNwcRequest(PayMethod.create("bitcoin:?lno=$offer", amountMillisats)) { response ->
|
||||
when (response) {
|
||||
is PaySuccessResponse -> toastManager.toast(R.string.bolt12_offers, R.string.bolt12_payment_sent)
|
||||
is IErrorResponseLike ->
|
||||
@@ -2745,7 +2745,7 @@ class AccountViewModel(
|
||||
onSent: () -> Unit = {},
|
||||
onResponse: (Response?) -> Unit,
|
||||
) = launchSigner {
|
||||
account.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
|
||||
account.zaps.sendZapPaymentRequestFor(bolt11, zappedNote, onResponse)
|
||||
onSent()
|
||||
}
|
||||
|
||||
@@ -2801,7 +2801,7 @@ class AccountViewModel(
|
||||
if (effectiveZapType != LnZapEvent.ZapType.NONZAP) {
|
||||
// NIP-57 Appendix F: include amount + lnurl so the receipt can be validated.
|
||||
val splitLnurl = LnurlForm.toUrl(lnAddress)?.let(LnurlForm::urlToBech32)
|
||||
account.createZapRequestFor(
|
||||
account.zaps.createZapRequestFor(
|
||||
user = user,
|
||||
message = message,
|
||||
zapType = effectiveZapType,
|
||||
|
||||
+2
-2
@@ -509,13 +509,13 @@ private fun SendPaymentLoaded(
|
||||
if (onchainAddressTarget != null) {
|
||||
// Pays the profile's announced bitcoin address directly —
|
||||
// a plain wallet send, no NIP-BC receipt exists for it.
|
||||
accountViewModel.account.sendOnchainToAddress(
|
||||
accountViewModel.account.zaps.sendOnchainToAddress(
|
||||
recipientAddress = onchainAddressTarget,
|
||||
amountSats = amount,
|
||||
feeRateSatPerVByte = feeRate,
|
||||
)
|
||||
} else {
|
||||
accountViewModel.account.sendOnchainZap(
|
||||
accountViewModel.account.zaps.sendOnchainZap(
|
||||
recipientPubKey = user.pubkeyHex,
|
||||
amountSats = amount,
|
||||
feeRateSatPerVByte = feeRate,
|
||||
|
||||
+2
-2
@@ -425,7 +425,7 @@ fun OnchainZapSendDialog(
|
||||
)
|
||||
return@launch
|
||||
}
|
||||
accountViewModel.account.sendOnchainZapWithSplits(
|
||||
accountViewModel.account.zaps.sendOnchainZapWithSplits(
|
||||
recipients = shares,
|
||||
feeRateSatPerVByte = feeRate,
|
||||
comment = comment.trim(),
|
||||
@@ -433,7 +433,7 @@ fun OnchainZapSendDialog(
|
||||
)
|
||||
} else {
|
||||
val recipient = resolvedRecipient ?: return@launch
|
||||
accountViewModel.account.sendOnchainZap(
|
||||
accountViewModel.account.zaps.sendOnchainZap(
|
||||
recipientPubKey = recipient,
|
||||
amountSats = amount,
|
||||
feeRateSatPerVByte = feeRate,
|
||||
|
||||
+1
-1
@@ -347,7 +347,7 @@ class ReloadMintViewModel : ViewModel() {
|
||||
// Fire-and-forget: the mint-quote poll below is the source of truth for
|
||||
// whether the payment actually landed.
|
||||
runCatching {
|
||||
vm.account.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(flow.invoice)) { }
|
||||
vm.account.zaps.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(flow.invoice)) { }
|
||||
}
|
||||
} else {
|
||||
// No NWC — surface the invoice for an external wallet and keep polling.
|
||||
|
||||
+1
-1
@@ -209,7 +209,7 @@ class TopUpMintViewModel : ViewModel() {
|
||||
// Fire-and-forget: the mint-quote poll below is the source of truth for
|
||||
// whether the payment actually landed.
|
||||
runCatching {
|
||||
vm.account.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(flow.invoice)) { }
|
||||
vm.account.zaps.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(flow.invoice)) { }
|
||||
}
|
||||
} else {
|
||||
// No NWC — surface the invoice for an external wallet and keep polling.
|
||||
|
||||
+10
-10
@@ -222,7 +222,7 @@ class WalletViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
delay(NWC_TIMEOUT_MS)
|
||||
val requestId = requestIdProvider()
|
||||
val spoofs = requestId?.let { account?.nwcSpoofAttempts(it) ?: 0 } ?: 0
|
||||
val spoofs = requestId?.let { account?.zaps?.nwcSpoofAttempts(it) ?: 0 } ?: 0
|
||||
_error.value =
|
||||
if (spoofs > 0) {
|
||||
"Wallet request timed out — $spoofs ${if (spoofs == 1) "reply was" else "replies were"} rejected because " +
|
||||
@@ -230,7 +230,7 @@ class WalletViewModel : ViewModel() {
|
||||
} else {
|
||||
"Wallet request timed out"
|
||||
}
|
||||
requestId?.let { account?.cleanupNwcRequest(it) }
|
||||
requestId?.let { account?.zaps?.cleanupNwcRequest(it) }
|
||||
onTimeout()
|
||||
}
|
||||
|
||||
@@ -406,7 +406,7 @@ class WalletViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
updateWalletInfo(walletId) { it.copy(isLoading = true, error = null) }
|
||||
try {
|
||||
acc.sendNwcRequestToWallet(walletUri, GetBalanceMethod.create()) { response ->
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetBalanceMethod.create()) { response ->
|
||||
when (response) {
|
||||
is GetBalanceSuccessResponse -> {
|
||||
val sats = (response.result?.balance ?: 0L) / 1000L
|
||||
@@ -437,7 +437,7 @@ class WalletViewModel : ViewModel() {
|
||||
val walletUri = getWalletUri(walletId) ?: return
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
acc.sendNwcRequestToWallet(walletUri, GetInfoMethod.create()) { response ->
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetInfoMethod.create()) { response ->
|
||||
when (response) {
|
||||
is GetInfoSuccessResponse -> {
|
||||
updateWalletInfo(walletId) { it.copy(alias = response.result?.alias) }
|
||||
@@ -479,7 +479,7 @@ class WalletViewModel : ViewModel() {
|
||||
val timeoutJob = launchTimeout({ requestId }) { _isLoading.value = false }
|
||||
try {
|
||||
requestId =
|
||||
acc.sendNwcRequestToWallet(walletUri, GetBalanceMethod.create()) { response ->
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetBalanceMethod.create()) { response ->
|
||||
timeoutJob.cancel()
|
||||
when (response) {
|
||||
is GetBalanceSuccessResponse -> {
|
||||
@@ -512,7 +512,7 @@ class WalletViewModel : ViewModel() {
|
||||
val walletUri = getWalletUri(walletId) ?: return
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
try {
|
||||
acc.sendNwcRequestToWallet(walletUri, GetInfoMethod.create()) { response ->
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, GetInfoMethod.create()) { response ->
|
||||
when (response) {
|
||||
is GetInfoSuccessResponse -> {
|
||||
_walletAlias.value = response.result?.alias
|
||||
@@ -538,7 +538,7 @@ class WalletViewModel : ViewModel() {
|
||||
val timeoutJob = launchTimeout({ requestId }) { _isLoading.value = false }
|
||||
try {
|
||||
requestId =
|
||||
acc.sendNwcRequestToWallet(
|
||||
acc.zaps.sendNwcRequestToWallet(
|
||||
walletUri,
|
||||
ListTransactionsMethod.create(
|
||||
limit = pageSize,
|
||||
@@ -591,7 +591,7 @@ class WalletViewModel : ViewModel() {
|
||||
val timeoutJob = launchTimeout({ requestId }) { _isLoadingMore.value = false }
|
||||
try {
|
||||
requestId =
|
||||
acc.sendNwcRequestToWallet(
|
||||
acc.zaps.sendNwcRequestToWallet(
|
||||
walletUri,
|
||||
ListTransactionsMethod.create(
|
||||
limit = pageSize,
|
||||
@@ -638,7 +638,7 @@ class WalletViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_sendState.value = SendState.Sending
|
||||
try {
|
||||
acc.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(bolt11)) { response ->
|
||||
acc.zaps.sendNwcRequestToWallet(walletUri, PayInvoiceMethod.create(bolt11)) { response ->
|
||||
when (response) {
|
||||
is PayInvoiceSuccessResponse -> {
|
||||
_sendState.value = SendState.Success(response.result?.preimage)
|
||||
@@ -676,7 +676,7 @@ class WalletViewModel : ViewModel() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
_receiveState.value = ReceiveState.Creating
|
||||
try {
|
||||
acc.sendNwcRequestToWallet(
|
||||
acc.zaps.sendNwcRequestToWallet(
|
||||
walletUri,
|
||||
MakeInvoiceMethod.create(
|
||||
amount = amountSats * 1000L,
|
||||
|
||||
+2
-2
@@ -1467,7 +1467,7 @@ class AmethystAppFunctions {
|
||||
}
|
||||
|
||||
val result =
|
||||
account.sendOnchainZap(
|
||||
account.zaps.sendOnchainZap(
|
||||
recipientPubKey = recipientPub,
|
||||
amountSats = sats,
|
||||
feeRateSatPerVByte = feeRateSatPerVByte,
|
||||
@@ -1751,7 +1751,7 @@ class AmethystAppFunctions {
|
||||
val deferred = CompletableDeferred<Response?>()
|
||||
// sendZapPaymentRequestFor fires onResponse exactly once when the wallet replies
|
||||
// (success, error, or NwcError). On timeout we discard the late response.
|
||||
account.sendZapPaymentRequestFor(bolt11, zappedNote) { response ->
|
||||
account.zaps.sendZapPaymentRequestFor(bolt11, zappedNote) { response ->
|
||||
if (!deferred.isCompleted) deferred.complete(response)
|
||||
}
|
||||
val response =
|
||||
|
||||
Reference in New Issue
Block a user