Show on-chain zappers in expanded reactions gallery
- Add OnchainZappedIcon and PendingClockBadge - move PendingClockBadge to TopStart to avoid follow-dot clash
This commit is contained in:
@@ -1772,13 +1772,13 @@ object LocalCache : ILocalCache, ICacheProvider {
|
|||||||
when (val result = verifier.verify(event)) {
|
when (val result = verifier.verify(event)) {
|
||||||
is VerifiedOnchainZap.Confirmed -> {
|
is VerifiedOnchainZap.Confirmed -> {
|
||||||
repliesTo.forEach {
|
repliesTo.forEach {
|
||||||
it.addOnchainZap(result.txid, result.verifiedSats, confirmed = true)
|
it.addOnchainZap(note, result.txid, result.verifiedSats, confirmed = true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is VerifiedOnchainZap.Pending -> {
|
is VerifiedOnchainZap.Pending -> {
|
||||||
repliesTo.forEach {
|
repliesTo.forEach {
|
||||||
it.addOnchainZap(result.txid, result.verifiedSats, confirmed = false)
|
it.addOnchainZap(note, result.txid, result.verifiedSats, confirmed = false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -420,3 +420,23 @@ fun VerticalDotsIcon() {
|
|||||||
tint = MaterialTheme.colorScheme.placeholderText,
|
tint = MaterialTheme.colorScheme.placeholderText,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun OnchainZappedIcon(modifier: Modifier) {
|
||||||
|
Icon(
|
||||||
|
symbol = MaterialSymbols.CurrencyBitcoin,
|
||||||
|
contentDescription = stringRes(R.string.onchain_zap_description),
|
||||||
|
tint = BitcoinOrange,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun PendingClockBadge(modifier: Modifier) {
|
||||||
|
Icon(
|
||||||
|
symbol = MaterialSymbols.Schedule,
|
||||||
|
contentDescription = stringRes(R.string.onchain_zap_pending),
|
||||||
|
tint = BitcoinOrange,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/*
|
||||||
|
* 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.ui.note
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.alpha
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.unit.sp
|
||||||
|
import com.vitorpamplona.amethyst.commons.model.OnchainZapEntry
|
||||||
|
import com.vitorpamplona.amethyst.model.Note
|
||||||
|
import com.vitorpamplona.amethyst.service.relayClient.reqCommand.event.observeNoteZaps
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.navs.INav
|
||||||
|
import com.vitorpamplona.amethyst.ui.navigation.routes.routeFor
|
||||||
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size25dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35Modifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.Size35dp
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.StdStartPadding
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.WidthAuthorPictureModifier
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.bitcoinColor
|
||||||
|
import com.vitorpamplona.amethyst.ui.theme.overPictureBackground
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import java.math.BigDecimal
|
||||||
|
|
||||||
|
private fun onOnchainZapEntryClick(
|
||||||
|
entry: OnchainZapEntry,
|
||||||
|
nav: INav,
|
||||||
|
) {
|
||||||
|
entry.source.author?.let { nav.nav(routeFor(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
internal fun WatchOnchainZapsAndRenderGallery(
|
||||||
|
baseNote: Note,
|
||||||
|
nav: INav,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
// Reuse the same flow the lightning gallery subscribes to. Note.addOnchainZap
|
||||||
|
// invalidates flowSet.zaps, so this composable refreshes when on-chain zaps
|
||||||
|
// arrive or upgrade pending → confirmed.
|
||||||
|
val zapsState by observeNoteZaps(baseNote, accountViewModel)
|
||||||
|
val entries =
|
||||||
|
zapsState
|
||||||
|
?.note
|
||||||
|
?.onchainZaps
|
||||||
|
?.values
|
||||||
|
?.toImmutableList() ?: persistentListOf()
|
||||||
|
|
||||||
|
if (entries.isNotEmpty()) {
|
||||||
|
RenderOnchainZapGallery(entries, nav, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun RenderOnchainZapGallery(
|
||||||
|
entries: ImmutableList<OnchainZapEntry>,
|
||||||
|
nav: INav,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
Row(Modifier.fillMaxWidth()) {
|
||||||
|
Box(modifier = WidthAuthorPictureModifier) {
|
||||||
|
OnchainZappedIcon(
|
||||||
|
modifier = Modifier.size(Size25dp).align(Alignment.TopEnd),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
OnchainZapAuthorGallery(entries, nav, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalLayoutApi::class)
|
||||||
|
@Composable
|
||||||
|
private fun OnchainZapAuthorGallery(
|
||||||
|
entries: ImmutableList<OnchainZapEntry>,
|
||||||
|
nav: INav,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
Column(modifier = StdStartPadding) {
|
||||||
|
FlowRow {
|
||||||
|
entries.forEach { entry ->
|
||||||
|
OnchainZapEntryRow(entry, nav, accountViewModel)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun OnchainZapEntryRow(
|
||||||
|
entry: OnchainZapEntry,
|
||||||
|
nav: INav,
|
||||||
|
accountViewModel: AccountViewModel,
|
||||||
|
) {
|
||||||
|
val user = entry.source.author
|
||||||
|
val amountText =
|
||||||
|
remember(entry.verifiedSats) {
|
||||||
|
showAmount(BigDecimal.valueOf(entry.verifiedSats))
|
||||||
|
}
|
||||||
|
val avatarAlpha = if (entry.confirmed) 1f else 0.6f
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier =
|
||||||
|
Modifier.clickable { onOnchainZapEntryClick(entry, nav) },
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Size35Modifier,
|
||||||
|
contentAlignment = Alignment.BottomCenter,
|
||||||
|
) {
|
||||||
|
// Only the avatar dims for pending entries. The amount overlay and clock
|
||||||
|
// badge stay at full opacity so they remain readable.
|
||||||
|
Box(modifier = Modifier.alpha(avatarAlpha)) {
|
||||||
|
WatchUserMetadataAndFollowsAndRenderUserProfilePictureOrDefaultAuthor(
|
||||||
|
user,
|
||||||
|
accountViewModel,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Amount overlay — same look as the lightning row's CrossfadeToDisplayAmount.
|
||||||
|
Box(
|
||||||
|
modifier =
|
||||||
|
Modifier
|
||||||
|
.size(Size35dp)
|
||||||
|
.clip(CircleShape),
|
||||||
|
contentAlignment = Alignment.BottomCenter,
|
||||||
|
) {
|
||||||
|
val overlayBg = MaterialTheme.colorScheme.overPictureBackground
|
||||||
|
Box(
|
||||||
|
modifier = remember(overlayBg) { Modifier.width(Size35dp).background(overlayBg) },
|
||||||
|
contentAlignment = Alignment.BottomCenter,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = amountText,
|
||||||
|
fontWeight = FontWeight.Bold,
|
||||||
|
color = MaterialTheme.colorScheme.bitcoinColor,
|
||||||
|
fontSize = 12.sp,
|
||||||
|
modifier = bottomPadding1dp,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!entry.confirmed) {
|
||||||
|
// TopStart so the badge doesn't collide with the FollowingIcon
|
||||||
|
// that WatchUserMetadataAndFollowsAndRenderUserProfilePicture
|
||||||
|
// paints at TopEnd for followed users.
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.align(Alignment.TopStart),
|
||||||
|
) {
|
||||||
|
PendingClockBadge(modifier = Modifier.size(14.dp))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -597,6 +597,7 @@ private fun ReactionDetailGallery(
|
|||||||
) {
|
) {
|
||||||
Column {
|
Column {
|
||||||
WatchZapAndRenderGallery(baseNote, backgroundColor, nav, accountViewModel)
|
WatchZapAndRenderGallery(baseNote, backgroundColor, nav, accountViewModel)
|
||||||
|
WatchOnchainZapsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||||
WatchBoostsAndRenderGallery(baseNote, nav, accountViewModel)
|
WatchBoostsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||||
WatchReactionsAndRenderGallery(baseNote, nav, accountViewModel)
|
WatchReactionsAndRenderGallery(baseNote, nav, accountViewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1887,6 +1887,8 @@
|
|||||||
<string name="boost_or_quote_description">Boost Or Quote</string>
|
<string name="boost_or_quote_description">Boost Or Quote</string>
|
||||||
<string name="like_description">Like</string>
|
<string name="like_description">Like</string>
|
||||||
<string name="zap_description">Zap</string>
|
<string name="zap_description">Zap</string>
|
||||||
|
<string name="onchain_zap_description">On-chain Bitcoin zap</string>
|
||||||
|
<string name="onchain_zap_pending">Pending confirmation</string>
|
||||||
|
|
||||||
<string name="change_reaction">Change Quick Reactions</string>
|
<string name="change_reaction">Change Quick Reactions</string>
|
||||||
|
|
||||||
|
|||||||
@@ -158,11 +158,13 @@ open class Note(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* NIP-BC verified onchain zaps targeting this note.
|
* NIP-BC verified onchain zaps targeting this note.
|
||||||
* Key: Bitcoin txid (lowercase 64-char hex). Value: verified satoshis paid to the recipient
|
* Key: Bitcoin txid (lowercase 64-char hex). Value: verified entry with the source
|
||||||
* (NOT the sender-claimed amount). Confirmed and pending entries live here together;
|
* OnchainZapEvent note (so `source.author` identifies the sender), the verified
|
||||||
* `updateZapTotal` only counts confirmed amounts.
|
* satoshis paid to the recipient (NOT the sender-claimed amount), and a confirmed
|
||||||
|
* flag. Confirmed and pending entries live here together; `updateZapTotal` only
|
||||||
|
* counts confirmed amounts.
|
||||||
*/
|
*/
|
||||||
var onchainZaps = mapOf<String, OnchainZapAmount>()
|
var onchainZaps = mapOf<String, OnchainZapEntry>()
|
||||||
private set
|
private set
|
||||||
|
|
||||||
var zapPayments = mapOf<Note, Note?>()
|
var zapPayments = mapOf<Note, Note?>()
|
||||||
@@ -430,27 +432,35 @@ open class Note(
|
|||||||
@Synchronized
|
@Synchronized
|
||||||
private fun innerAddOnchainZap(
|
private fun innerAddOnchainZap(
|
||||||
txid: String,
|
txid: String,
|
||||||
amount: OnchainZapAmount,
|
entry: OnchainZapEntry,
|
||||||
): Boolean {
|
): Boolean {
|
||||||
val existing = onchainZaps[txid]
|
val existing = onchainZaps[txid]
|
||||||
// Allow upgrading pending → confirmed but never reduce already-stored confirmations.
|
if (existing == null) {
|
||||||
if (existing != null && existing.confirmed && !amount.confirmed) return false
|
onchainZaps = onchainZaps + Pair(txid, entry)
|
||||||
if (existing == amount) return false
|
return true
|
||||||
onchainZaps = onchainZaps + Pair(txid, amount)
|
}
|
||||||
|
// Same-state duplicate: keep the first source and amount we got.
|
||||||
|
if (existing.confirmed == entry.confirmed) return false
|
||||||
|
// Downgrade confirmed → pending: never accept.
|
||||||
|
if (existing.confirmed && !entry.confirmed) return false
|
||||||
|
// Upgrade pending → confirmed: replace, so the upgrading event's source wins.
|
||||||
|
onchainZaps = onchainZaps + Pair(txid, entry)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Register a NIP-BC verified onchain zap targeting this note. `verifiedSats` MUST come
|
* Register a NIP-BC verified onchain zap targeting this note. `verifiedSats` MUST come
|
||||||
* from on-chain verification (sum of outputs paying the recipient's derived Taproot
|
* from on-chain verification (sum of outputs paying the recipient's derived Taproot
|
||||||
* address), not the sender-claimed `amount` tag.
|
* address), not the sender-claimed `amount` tag. `source` is the OnchainZapEvent's own
|
||||||
|
* note — `source.author` is the sender shown in the reactions gallery.
|
||||||
*/
|
*/
|
||||||
fun addOnchainZap(
|
fun addOnchainZap(
|
||||||
|
source: Note,
|
||||||
txid: String,
|
txid: String,
|
||||||
verifiedSats: Long,
|
verifiedSats: Long,
|
||||||
confirmed: Boolean,
|
confirmed: Boolean,
|
||||||
) {
|
) {
|
||||||
val inserted = innerAddOnchainZap(txid, OnchainZapAmount(verifiedSats, confirmed))
|
val inserted = innerAddOnchainZap(txid, OnchainZapEntry(source, verifiedSats, confirmed))
|
||||||
if (inserted) {
|
if (inserted) {
|
||||||
updateZapTotal()
|
updateZapTotal()
|
||||||
flowSet?.zaps?.invalidateData()
|
flowSet?.zaps?.invalidateData()
|
||||||
|
|||||||
+7
-3
@@ -20,11 +20,14 @@
|
|||||||
*/
|
*/
|
||||||
package com.vitorpamplona.amethyst.commons.model
|
package com.vitorpamplona.amethyst.commons.model
|
||||||
|
|
||||||
import androidx.compose.runtime.Immutable
|
import androidx.compose.runtime.Stable
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Per-(note, txid) verified NIP-BC onchain zap state.
|
* Per-(note, txid) verified NIP-BC onchain zap state.
|
||||||
*
|
*
|
||||||
|
* @property source The OnchainZapEvent note that contributed this entry. `source.author` is the
|
||||||
|
* sender shown in the reactions gallery. When pending → confirmed upgrades
|
||||||
|
* happen, the upgrading event's note replaces the existing `source`.
|
||||||
* @property verifiedSats Satoshis verified to have paid the recipient's derived Taproot
|
* @property verifiedSats Satoshis verified to have paid the recipient's derived Taproot
|
||||||
* address on chain. NEVER the sender-claimed `amount` tag.
|
* address on chain. NEVER the sender-claimed `amount` tag.
|
||||||
* @property confirmed True when the transaction has at least one confirmation. Unconfirmed
|
* @property confirmed True when the transaction has at least one confirmation. Unconfirmed
|
||||||
@@ -33,8 +36,9 @@ import androidx.compose.runtime.Immutable
|
|||||||
* SHOULD either exclude them from aggregate totals or clearly label
|
* SHOULD either exclude them from aggregate totals or clearly label
|
||||||
* them as pending").
|
* them as pending").
|
||||||
*/
|
*/
|
||||||
@Immutable
|
@Stable
|
||||||
data class OnchainZapAmount(
|
data class OnchainZapEntry(
|
||||||
|
val source: Note,
|
||||||
val verifiedSats: Long,
|
val verifiedSats: Long,
|
||||||
val confirmed: Boolean,
|
val confirmed: Boolean,
|
||||||
)
|
)
|
||||||
+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.model
|
||||||
|
|
||||||
|
import java.math.BigDecimal
|
||||||
|
import kotlin.test.Test
|
||||||
|
import kotlin.test.assertEquals
|
||||||
|
import kotlin.test.assertSame
|
||||||
|
import kotlin.test.assertTrue
|
||||||
|
|
||||||
|
class NoteOnchainZapTest {
|
||||||
|
private fun freshNote(idHex: String = "a".repeat(64)) = Note(idHex)
|
||||||
|
|
||||||
|
private fun sourceNote(idHex: String) = Note(idHex)
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pendingEntryIsStoredWithSourceAndDoesNotAffectTotal() {
|
||||||
|
val target = freshNote()
|
||||||
|
val src = sourceNote("b".repeat(64))
|
||||||
|
|
||||||
|
target.addOnchainZap(source = src, txid = "tx1", verifiedSats = 1000L, confirmed = false)
|
||||||
|
|
||||||
|
val entry = target.onchainZaps["tx1"]
|
||||||
|
assertEquals(1, target.onchainZaps.size)
|
||||||
|
assertSame(src, entry?.source)
|
||||||
|
assertEquals(1000L, entry?.verifiedSats)
|
||||||
|
assertEquals(false, entry?.confirmed)
|
||||||
|
assertEquals(BigDecimal.ZERO, target.zapsAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun confirmedEntryIsStoredAndAddsToTotal() {
|
||||||
|
val target = freshNote()
|
||||||
|
val src = sourceNote("c".repeat(64))
|
||||||
|
|
||||||
|
target.addOnchainZap(source = src, txid = "tx1", verifiedSats = 5000L, confirmed = true)
|
||||||
|
|
||||||
|
assertEquals(true, target.onchainZaps["tx1"]?.confirmed)
|
||||||
|
assertEquals(BigDecimal.valueOf(5000L), target.zapsAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun pendingThenConfirmedReplacesSourceAndUpdatesTotal() {
|
||||||
|
val target = freshNote()
|
||||||
|
val firstSrc = sourceNote("d".repeat(64))
|
||||||
|
val secondSrc = sourceNote("e".repeat(64))
|
||||||
|
|
||||||
|
target.addOnchainZap(firstSrc, "tx1", 2500L, confirmed = false)
|
||||||
|
target.addOnchainZap(secondSrc, "tx1", 2500L, confirmed = true)
|
||||||
|
|
||||||
|
val entry = target.onchainZaps["tx1"]
|
||||||
|
assertSame(secondSrc, entry?.source)
|
||||||
|
assertEquals(true, entry?.confirmed)
|
||||||
|
assertEquals(BigDecimal.valueOf(2500L), target.zapsAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun confirmedThenPendingIsIgnored() {
|
||||||
|
val target = freshNote()
|
||||||
|
val firstSrc = sourceNote("f".repeat(64))
|
||||||
|
val secondSrc = sourceNote("0".repeat(64))
|
||||||
|
|
||||||
|
target.addOnchainZap(firstSrc, "tx1", 7500L, confirmed = true)
|
||||||
|
target.addOnchainZap(secondSrc, "tx1", 7500L, confirmed = false)
|
||||||
|
|
||||||
|
val entry = target.onchainZaps["tx1"]
|
||||||
|
assertSame(firstSrc, entry?.source)
|
||||||
|
assertEquals(true, entry?.confirmed)
|
||||||
|
assertEquals(BigDecimal.valueOf(7500L), target.zapsAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun sameStateDuplicateIsIgnored() {
|
||||||
|
val target = freshNote()
|
||||||
|
val firstSrc = sourceNote("1".repeat(64))
|
||||||
|
val secondSrc = sourceNote("2".repeat(64))
|
||||||
|
|
||||||
|
target.addOnchainZap(firstSrc, "tx1", 100L, confirmed = true)
|
||||||
|
target.addOnchainZap(secondSrc, "tx1", 100L, confirmed = true)
|
||||||
|
|
||||||
|
assertSame(firstSrc, target.onchainZaps["tx1"]?.source)
|
||||||
|
assertEquals(BigDecimal.valueOf(100L), target.zapsAmount)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun updateZapTotalSumsConfirmedAcrossMultipleTxids() {
|
||||||
|
val target = freshNote()
|
||||||
|
target.addOnchainZap(sourceNote("3".repeat(64)), "tx1", 1000L, confirmed = true)
|
||||||
|
target.addOnchainZap(sourceNote("4".repeat(64)), "tx2", 2000L, confirmed = true)
|
||||||
|
target.addOnchainZap(sourceNote("5".repeat(64)), "tx3", 9999L, confirmed = false)
|
||||||
|
|
||||||
|
assertEquals(BigDecimal.valueOf(3000L), target.zapsAmount)
|
||||||
|
assertEquals(3, target.onchainZaps.size)
|
||||||
|
assertTrue(target.onchainZaps.containsKey("tx3"))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user