mirror of
https://github.com/vitorpamplona/amethyst.git
synced 2026-08-11 16:57:39 +00:00
feat(amethyst): scaffold NIP-60 Cashu wallet UI + state
Adds the user-visible scaffolding for a Cashu wallet alongside the
existing NWC wallets. View-only for now — minting, send/receive, and
NIP-61 nutzaps land in a follow-up commit on this branch.
UI
* AddWalletScreen is now a wallet-type chooser. The existing NWC
flow moves verbatim to AddNwcWalletScreen; AddCashuWalletScreen
is new: takes one or more mint URLs, auto-generates a separate
P2PK key for nutzap receiving (or accepts a pasted hex key), and
publishes a kind:17375 wallet event via the account's signer
using CashuWalletEvent.build(mints, privkey).
* CashuWalletScreen renders the wallet's mint list, total balance
in sats (summed across all unspent kind:7375 token events the
signer can decrypt, with rollover applied via the `del` field),
and a chronological history view sourced from kind:7376.
* WalletScreen surfaces the Cashu wallet as a card under "Your
Wallets" when one exists, so the Wallets entry point shows both
wallet kinds side by side.
Relay subscription
* CashuWalletFilterAssembler (commons) subscribes one filter per
relay covering kinds 17375/7375/7376/7374/10019 by author and
one targeting inbound kind:9321 via #p. Not yet wired into
Account.kt — the view path works because we feed our own writes
through cache.justConsumeMyOwnEvent. Cross-device sync requires
the assembler subscription wiring, which comes next.
Plumbing
* Routes.WalletAddNwc / WalletAddCashu / CashuWallet added and
registered in AppNavigation.
* CashuWalletEvent.createAddress(pubKey) mirrors MetadataEvent for
looking up the replaceable wallet event from LocalCache.
Compiles clean on playDebug + fdroidDebug; BDHKE jvm tests still
pass (7/7).
https://claude.ai/code/session_01MdWddiar819f8XYt5N8BjP
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* 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.commons.relayClient.assemblers
|
||||
|
||||
import com.vitorpamplona.amethyst.commons.relayClient.eoseManagers.SingleSubEoseManager
|
||||
import com.vitorpamplona.amethyst.commons.relays.SincePerRelayMap
|
||||
import com.vitorpamplona.quartz.nip01Core.core.HexKey
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.INostrClient
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.client.pool.RelayBasedFilter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.filters.Filter
|
||||
import com.vitorpamplona.quartz.nip01Core.relay.normalizer.NormalizedRelayUrl
|
||||
import com.vitorpamplona.quartz.nip60Cashu.history.CashuSpendingHistoryEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.quote.CashuMintQuoteEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.token.CashuTokenEvent
|
||||
import com.vitorpamplona.quartz.nip60Cashu.wallet.CashuWalletEvent
|
||||
import com.vitorpamplona.quartz.nip61Nutzaps.info.NutzapInfoEvent
|
||||
import com.vitorpamplona.quartz.nip61Nutzaps.nutzap.NutzapEvent
|
||||
|
||||
/**
|
||||
* Query state for the NIP-60 / NIP-61 wallet subscription.
|
||||
*
|
||||
* `pubkey` is the wallet owner — used both as `authors=` for their own
|
||||
* NIP-60 events and as the `#p` tag value for inbound nutzaps.
|
||||
*/
|
||||
data class CashuWalletQueryState(
|
||||
val pubkey: HexKey,
|
||||
val relays: Set<NormalizedRelayUrl>,
|
||||
)
|
||||
|
||||
/**
|
||||
* Subscribes to all NIP-60 / NIP-61 events that participate in this
|
||||
* account's Cashu wallet:
|
||||
*
|
||||
* - kind 17375 — the wallet event (replaceable)
|
||||
* - kind 7375 — unspent proofs (token events)
|
||||
* - kind 7376 — spending history
|
||||
* - kind 7374 — quote state events
|
||||
* - kind 10019 — nutzap info (replaceable, for incoming nutzaps)
|
||||
* - kind 9321 — inbound nutzaps tagged with the user's pubkey
|
||||
*
|
||||
* All authored events are queried by `authors=[pubkey]`; nutzaps are queried by
|
||||
* `#p=[pubkey]` (we receive them, not send them, from this filter's perspective).
|
||||
*/
|
||||
class CashuWalletFilterAssembler(
|
||||
client: INostrClient,
|
||||
allKeys: () -> Set<CashuWalletQueryState>,
|
||||
) : SingleSubEoseManager<CashuWalletQueryState>(client, allKeys, invalidateAfterEose = true) {
|
||||
override fun distinct(key: CashuWalletQueryState): Any = key.pubkey
|
||||
|
||||
override fun updateFilter(
|
||||
keys: List<CashuWalletQueryState>,
|
||||
since: SincePerRelayMap?,
|
||||
): List<RelayBasedFilter>? {
|
||||
if (keys.isEmpty()) return null
|
||||
|
||||
val pubkey = keys.first().pubkey
|
||||
val relays =
|
||||
keys
|
||||
.flatMap { it.relays }
|
||||
.toSet()
|
||||
.ifEmpty { return null }
|
||||
|
||||
val ownedFilter =
|
||||
Filter(
|
||||
kinds =
|
||||
listOf(
|
||||
CashuWalletEvent.KIND,
|
||||
CashuTokenEvent.KIND,
|
||||
CashuSpendingHistoryEvent.KIND,
|
||||
CashuMintQuoteEvent.KIND,
|
||||
NutzapInfoEvent.KIND,
|
||||
),
|
||||
authors = listOf(pubkey),
|
||||
)
|
||||
|
||||
val inboundNutzapsFilter =
|
||||
Filter(
|
||||
kinds = listOf(NutzapEvent.KIND),
|
||||
tags = mapOf("p" to listOf(pubkey)),
|
||||
)
|
||||
|
||||
return relays.flatMap { relay ->
|
||||
val sinceTime = since?.get(relay)?.time
|
||||
listOf(
|
||||
RelayBasedFilter(
|
||||
relay,
|
||||
if (sinceTime != null) ownedFilter.copy(since = sinceTime) else ownedFilter,
|
||||
),
|
||||
RelayBasedFilter(
|
||||
relay,
|
||||
if (sinceTime != null) inboundNutzapsFilter.copy(since = sinceTime) else inboundNutzapsFilter,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user