Repository navigation
feat: add allowances for automatic paykit payments #1340
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ovitrif
wants to merge
103
commits into
master
Choose a base branch
from
feat/paykit-allowances
base: master
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+8,424
−119
Open
Changes from all commits
Commits
Show all changes
103 commits
Select commit
Hold shift + click to select a range
8bd3504
chore: build paykit from the local allowances stack
ovitrif df25bea
chore: let an explicit e2e homegate url win on every backend
ovitrif eed5280
feat: add the allowance model, sdk calls and repo contract
ovitrif 1efc749
feat: keep rc55 paykit state readable across the allowance sdk
ovitrif 4d5c4e6
feat: pay covered paykit requests through allowances
ovitrif feeb0b6
feat: add the allowances tab and allowance sheets
ovitrif f13ccf3
feat: run allowances from the app lifecycle and send flow
ovitrif 7bf7640
fix: wait for the payee's next payment list instead of asking the payer
ovitrif 3d7e2af
fix: hold automatic payments until the node's channels are usable
ovitrif 8ac84fc
fix: notify allowance payments and limits over the request sheet
ovitrif e099551
docs: add the allowance journeys
ovitrif faca50b
chore: add changelog fragment
ovitrif 4dc9055
fix: extend an allowance to a contact link that links after the grant
ovitrif 145330c
fix: finish an allowance payment once it starts
ovitrif d701ee5
feat: share paykit state across apps
ben-kaufman 2bba381
chore: rename changelog fragment
ben-kaufman 5f959d0
docs: clarify paykit integration contracts
ben-kaufman 354471e
fix: tighten paykit attribution and cleanup reporting
ben-kaufman d465536
test: cover paykit key generation and rotation
ben-kaufman bca62da
fix: guard paykit execution and cache contact backfills
ben-kaufman ae0764a
fix: prune completed paykit acceptance records
ben-kaufman d2c5a03
fix: preserve paykit payment recovery and contact choices
ben-kaufman bef908f
refactor: simplify paykit contact backfill
ben-kaufman d2525a5
chore: update Paykit to rc59
ben-kaufman 2077863
chore: stack allowances on the shared paykit runtime
ovitrif af7c2bf
fix: preserve private sharing settings during contact cleanup
ben-kaufman 0b131e4
Merge branch 'master' into codex/paykit-shared-runtime-local-20260930
ben-kaufman b657fd0
refactor: bind allowances to the shared paykit identity
ovitrif 0b1f266
fix: keep an automatically accepted request payable on this install
ovitrif 455bb90
test: align request presentation with shared paykit
ben-kaufman 6d42b5a
test: align private paykit settings stub
ben-kaufman c80b941
chore: sync allowances with the shared paykit runtime
ovitrif b55a79d
docs: align the end allowance journey with the ended row
ovitrif 5aa675e
style: order the allowance imports
ovitrif 16d44b5
fix: satisfy detekt in the allowance acceptance
ovitrif 1adde2a
fix: reduce paykit sync overhead and retry session setup
ben-kaufman b929cce
chore: sync allowances with the shared paykit runtime
ovitrif 5fb0193
chore: merge master into paykit shared runtime
ben-kaufman c642d32
chore: sync allowances with the shared paykit runtime
ovitrif 7026f86
fix: keep private message sync off frequent request polls
ben-kaufman f7338f5
chore: sync allowances with the shared paykit runtime
ovitrif 3733d2a
feat: share paykit state across apps
ben-kaufman ce9910c
chore: rename changelog fragment
ben-kaufman 44fae4b
docs: clarify paykit integration contracts
ben-kaufman ffb3755
fix: tighten paykit attribution and cleanup reporting
ben-kaufman c4fb1db
test: cover paykit key generation and rotation
ben-kaufman 925256b
fix: guard paykit execution and cache contact backfills
ben-kaufman 963d3ea
fix: prune completed paykit acceptance records
ben-kaufman 5266e83
fix: preserve paykit payment recovery and contact choices
ben-kaufman 011c673
refactor: simplify paykit contact backfill
ben-kaufman 81c1865
chore: update Paykit to rc59
ben-kaufman 1f41d51
fix: preserve private sharing settings during contact cleanup
ben-kaufman 8f05585
test: align request presentation with shared paykit
ben-kaufman 0af40d5
test: align private paykit settings stub
ben-kaufman 6125336
fix: reduce paykit sync overhead and retry session setup
ben-kaufman 14d5383
fix: keep private message sync off frequent request polls
ben-kaufman 573123b
chore: sync allowances with the rewritten shared paykit runtime
ovitrif 8237983
chore: sync allowances with the rewritten shared paykit runtime
ovitrif 56dbcf7
fix: pay allowances only to unused on-chain addresses
ovitrif a2f33ad
test: pin that allowances ignore the subscription clock offset
ovitrif cda2de2
chore: sync allowances with the shared paykit runtime
ovitrif 0e4e239
chore: sync allowances with the shared paykit runtime
ovitrif 3472b26
chore: sync allowances with the shared paykit runtime
ovitrif 44af685
chore: sync allowances with the shared paykit runtime
ovitrif de33cd3
chore: sync allowances with the shared paykit runtime
ovitrif 681f0fb
chore: sync allowances with the shared paykit runtime
ovitrif a63faa4
chore: sync allowances with the shared paykit runtime
ovitrif 9879897
chore: sync allowances with the shared paykit runtime
ovitrif 1962193
chore: sync allowances with the shared paykit runtime
ovitrif c71b8df
chore: sync allowances with the shared paykit runtime
ovitrif 07b5b6f
chore: sync allowances with the shared paykit runtime
ovitrif b7f4c8b
chore: sync allowances with the shared paykit runtime
ovitrif 8ec17ba
chore: sync allowances with master
ovitrif 42fdf61
test: keep allowance requests manual off bitcoin or behind a blocked …
ovitrif 0098411
fix: settle allowance attempts only on proven outcomes
ovitrif cf564fc
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 876a5df
fix: notify limit reached when a payment exceeds the per-payment max
ovitrif e1e5d4e
feat: require the payment pin to grant or end an allowance
ovitrif deada17
fix: keep automatic allowance payments within visible limits
ovitrif dadaeb9
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 6e36ce4
fix: pay allowance requests automatically only over lightning
ovitrif 9f7ca64
fix: show allowee per-payment limit in whole dollars like the payer
ovitrif f20cb57
fix: cap routing fees of automatic allowance payments
ovitrif d33d4dd
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif a8d73c1
fix: drop allowance terms whose limits cannot be read
ovitrif e7c076a
fix: never merge allowances of different contacts
ovitrif da0719c
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 5b0bfe8
chore: fix lint findings after the master sync
ovitrif a04b91a
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif f561e63
fix: count allowance payments per contact
ovitrif e7b4477
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif f657def
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 7ba1300
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 39a0918
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 2611a18
fix: keep automatic allowance payments out of the user's send flow
ovitrif 61fd53a
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif fb3a7ab
fix: let manual retries of an allowance invoice settle as manual
ovitrif 2d5e2b5
fix: announce each automatic allowance payment once
ovitrif c493635
fix: treat a manual retry of an allowance invoice as the user's payment
ovitrif 70482d0
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 8a7b309
fix: auto-pay bitcoin requests that also offer a usdt rail
ovitrif d6b83b5
fix: hold allowance coverage through the request refresh and release …
ovitrif 480ae3a
fix: release an allowance payment list only from the path that claims it
ovitrif File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
299 changes: 299 additions & 0 deletions
299
app/src/main/java/to/bitkit/repositories/PaykitAllowance.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,299 @@ | ||
| package to.bitkit.repositories | ||
|
|
||
| import androidx.compose.runtime.Immutable | ||
| import com.synonym.paykit.AllowanceAccountingHistory | ||
| import com.synonym.paykit.AllowanceAmountRange | ||
| import com.synonym.paykit.AllowanceLifecycleState | ||
| import com.synonym.paykit.AllowanceLocalRole | ||
| import com.synonym.paykit.AllowancePeriod | ||
| import com.synonym.paykit.AllowancePeriodLimit | ||
| import com.synonym.paykit.AllowanceRecord | ||
| import com.synonym.paykit.AllowanceTerms | ||
| import com.synonym.paykit.PaymentExecutionMode | ||
| import com.synonym.paykit.PaymentExecutionStatus | ||
| import kotlinx.serialization.Serializable | ||
| import to.bitkit.models.PubkyPublicKeyFormat | ||
| import to.bitkit.models.safe | ||
| import java.math.BigDecimal | ||
| import java.time.ZoneOffset | ||
| import java.time.ZonedDateTime | ||
| import java.time.format.DateTimeFormatter | ||
| import java.time.temporal.ChronoUnit | ||
| import kotlin.time.Instant | ||
| import kotlin.time.toJavaInstant | ||
| import kotlin.time.toKotlinInstant | ||
|
|
||
| /** | ||
| * An Allowance between this wallet's identity and one contact identity, built from the SDK record. | ||
| * Eligibility always runs on real time. | ||
| */ | ||
| @Immutable | ||
| data class PaykitAllowance( | ||
| val id: Id, | ||
| val role: Role, | ||
| val lifecycleState: AllowanceLifecycleState, | ||
| val isProposedByMe: Boolean, | ||
| val perPaymentMaxSats: ULong?, | ||
| val monthlyLimitSats: ULong?, | ||
| val monthlyAnchor: Instant?, | ||
| val activeFrom: Instant? = null, | ||
| val expiresAt: Instant? = null, | ||
| val allowedPaymentEndpointIdentifiers: List<String>? = null, | ||
| val lastEventAt: Instant? = null, | ||
| /** The received proposal's private stream item; accepting it is bound to this item. */ | ||
| val proposalStreamItemId: ULong? = null, | ||
| ) { | ||
| @Serializable | ||
| data class Id( | ||
| val counterparty: String, | ||
| val allowanceId: String, | ||
| ) | ||
|
|
||
| @Serializable | ||
| enum class Role { ALLOWER, ALLOWEE } | ||
|
|
||
| enum class Status { | ||
| /** Sent by this wallet; the other side has not answered yet. */ | ||
| AWAITING_ANSWER, | ||
|
|
||
| /** Received; this wallet must accept or decline. */ | ||
| AWAITING_MY_ANSWER, | ||
| ACTIVE, | ||
| NOT_YET_ACTIVE, | ||
| EXPIRED, | ||
| DECLINED, | ||
| ENDED, | ||
| CONFLICTED, | ||
| } | ||
|
|
||
| val counterparty: String get() = id.counterparty | ||
| val allowanceId: String get() = id.allowanceId | ||
|
|
||
| /** The payer side: this wallet pays the counterparty's requests automatically. */ | ||
| val isAllower: Boolean get() = role == Role.ALLOWER | ||
|
|
||
| /** Allowance ids are unique per contact only, so this identifies one Allowance across all contacts. */ | ||
| val key: String get() = key(counterparty, allowanceId) | ||
|
|
||
| val canEnd: Boolean | ||
| get() = when (lifecycleState) { | ||
| AllowanceLifecycleState.ACCEPTED -> true | ||
| AllowanceLifecycleState.PROPOSED -> isProposedByMe | ||
| else -> false | ||
| } | ||
|
|
||
| val isAnswerable: Boolean get() = lifecycleState == AllowanceLifecycleState.PROPOSED && !isProposedByMe | ||
|
|
||
| fun status(now: Instant): Status = when (lifecycleState) { | ||
| AllowanceLifecycleState.PROPOSED -> if (isProposedByMe) Status.AWAITING_ANSWER else Status.AWAITING_MY_ANSWER | ||
| AllowanceLifecycleState.ACCEPTED -> when { | ||
| expiresAt != null && now >= expiresAt -> Status.EXPIRED | ||
| activeFrom != null && now < activeFrom -> Status.NOT_YET_ACTIVE | ||
| else -> Status.ACTIVE | ||
| } | ||
| AllowanceLifecycleState.REJECTED -> Status.DECLINED | ||
| AllowanceLifecycleState.ENDED -> Status.ENDED | ||
| AllowanceLifecycleState.CONFLICTED, AllowanceLifecycleState.UNKNOWN -> Status.CONFLICTED | ||
| } | ||
|
|
||
| companion object { | ||
| fun key(counterparty: String, allowanceId: String?): String = | ||
| "${PubkyPublicKeyFormat.normalized(counterparty) ?: counterparty}/$allowanceId" | ||
|
|
||
| fun from(record: AllowanceRecord): PaykitAllowance? { | ||
| val role = when (record.localRole) { | ||
| AllowanceLocalRole.ALLOWER -> Role.ALLOWER | ||
| AllowanceLocalRole.ALLOWEE -> Role.ALLOWEE | ||
| else -> return null | ||
| } | ||
| val terms = record.terms ?: return null | ||
| if (terms.asset() != PaykitIssuerInterop.BITCOIN_ASSET) return null | ||
| val monthly = terms.periodLimits().firstOrNull { isMonthly(it.period()) } | ||
| val perPaymentMaxSats = terms.perPaymentAmount()?.let { satsFromBitcoinAmount(it.maximum()) } | ||
| val monthlyLimitSats = monthly?.amountLimit()?.let(::satsFromBitcoinAmount) | ||
| val monthlyAnchor = monthly?.period()?.anchor()?.let(PaykitAllowanceTime::parse) | ||
| // This wallet pays only under limits it can show: terms without a readable per-payment and monthly cap | ||
| // are dropped. | ||
| val isBounded = perPaymentMaxSats != null && monthlyLimitSats != null && monthlyAnchor != null | ||
| if (role == Role.ALLOWER && !isBounded) return null | ||
| return PaykitAllowance( | ||
| id = Id(record.counterparty, record.allowanceId), | ||
| role = role, | ||
| lifecycleState = record.state, | ||
| isProposedByMe = record.proposalOutboundMessageId != null, | ||
| perPaymentMaxSats = perPaymentMaxSats, | ||
| monthlyLimitSats = monthlyLimitSats, | ||
| monthlyAnchor = monthlyAnchor, | ||
| activeFrom = terms.activeFrom()?.let(PaykitAllowanceTime::parse), | ||
| expiresAt = terms.expiresAt()?.let(PaykitAllowanceTime::parse), | ||
| allowedPaymentEndpointIdentifiers = terms.allowedPaymentEndpointIdentifiers(), | ||
| lastEventAt = record.lastEventAt?.let(PaykitAllowanceTime::parse), | ||
| proposalStreamItemId = record.proposalStreamItemId, | ||
| ) | ||
| } | ||
|
|
||
| fun isMonthly(period: AllowancePeriod): Boolean = | ||
| period.kind() == "anchored" && period.every() == 1uL && period.unit() == "month" | ||
|
|
||
| /** BTC decimal string to sats; unlike [toPaykitSats] a zero amount is valid here. */ | ||
| fun satsFromBitcoinAmount(amount: String): ULong? { | ||
| if (amount.split('.').all { part -> part.all { it == '0' } }) return 0uL | ||
| return amount.toPaykitSats() | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** One grant as the user sees it: the Allowance with a contact identity and the limits picked for it. */ | ||
| @Immutable | ||
| data class PaykitAllowanceEntry( | ||
| val id: String, | ||
| val allowances: List<PaykitAllowance>, | ||
| val limits: PaykitAllowanceLimits?, | ||
| ) { | ||
| /** An accepted Allowance before any other. */ | ||
| val primary: PaykitAllowance | ||
| get() = allowances.firstOrNull { it.lifecycleState == AllowanceLifecycleState.ACCEPTED } ?: allowances.first() | ||
| val counterparty: String get() = primary.counterparty | ||
| val role: PaykitAllowance.Role get() = primary.role | ||
| val perPaymentMaxSats: ULong? get() = primary.perPaymentMaxSats | ||
| val monthlyLimitSats: ULong? get() = primary.monthlyLimitSats | ||
| val canEnd: Boolean get() = allowances.any { it.canEnd } | ||
| val isAnswerable: Boolean get() = allowances.any { it.isAnswerable } | ||
|
|
||
| fun status(now: Instant): PaykitAllowance.Status = primary.status(now) | ||
| } | ||
|
|
||
| /** Limits picked in USD on the Set Allowance sheet, converted once to whole-sat BTC terms. */ | ||
| @Serializable | ||
| @Immutable | ||
| data class PaykitAllowanceLimits( | ||
| val perPaymentUsd: Int, | ||
| val monthlyUsd: Int, | ||
| val perPaymentSats: ULong, | ||
| val monthlySats: ULong, | ||
| ) { | ||
| /** | ||
| * Terms Bitkit proposes: per-payment range 0...max, an anchored UTC calendar month, and the endpoints Bitkit pays. | ||
| */ | ||
| fun terms(monthAnchor: Instant, allowedPaymentEndpointIdentifiers: List<String>): AllowanceTerms { | ||
| val perPayment = AllowanceAmountRange(minimum = "0", maximum = perPaymentSats.toBitcoinDecimal()) | ||
| val month = AllowancePeriod( | ||
| kind = "anchored", | ||
| every = 1uL, | ||
| unit = "month", | ||
| anchor = PaykitAllowanceTime.format(monthAnchor), | ||
| ) | ||
| val monthly = AllowancePeriodLimit( | ||
| amountLimit = monthlySats.toBitcoinDecimal(), | ||
| paymentCountLimit = null, | ||
| period = month, | ||
| ) | ||
| return AllowanceTerms( | ||
| asset = PaykitIssuerInterop.BITCOIN_ASSET, | ||
| perPaymentAmount = perPayment, | ||
| periodLimits = listOf(monthly), | ||
| lifetimeAmountLimit = null, | ||
| activeFrom = null, | ||
| expiresAt = null, | ||
| allowedPaymentEndpointIdentifiers = allowedPaymentEndpointIdentifiers, | ||
| ) | ||
| } | ||
|
|
||
| companion object { | ||
| /** Slider stops on the Set Allowance sheet, in whole US dollars. */ | ||
| val PER_PAYMENT_STOPS_USD = listOf(1, 5, 10, 20, 50) | ||
|
|
||
| /** Slider stops on the Set Allowance sheet, in whole US dollars. */ | ||
| val MONTHLY_STOPS_USD = listOf(10, 50, 100, 200, 500) | ||
| } | ||
| } | ||
|
|
||
| internal fun ULong.toBitcoinDecimal(): String = | ||
| BigDecimal(toString()).movePointLeft(8).stripTrailingZeros().toPlainString() | ||
|
|
||
| object PaykitAllowanceTime { | ||
| private val utc = ZoneOffset.UTC | ||
|
|
||
| fun format(instant: Instant): String = | ||
| DateTimeFormatter.ISO_INSTANT.format(instant.toJavaInstant().truncatedTo(ChronoUnit.MILLIS)) | ||
|
|
||
| fun parse(value: String): Instant? = runCatching { Instant.parse(value) }.getOrNull() | ||
|
|
||
| /** First instant of the UTC calendar month that contains [instant]. */ | ||
| fun monthStart(containing: Instant): Instant = ZonedDateTime.ofInstant(containing.toJavaInstant(), utc) | ||
| .withDayOfMonth(1) | ||
| .truncatedTo(ChronoUnit.DAYS) | ||
| .toInstant() | ||
| .toKotlinInstant() | ||
|
|
||
| /** | ||
| * The anchored monthly window `[start, end)` that contains [instant]. Anchors on a day that a short month lacks | ||
| * clamp to that month's last day, as the spec's anchored-period arithmetic does. | ||
| */ | ||
| fun monthlyWindow(anchor: Instant, containing: Instant): Pair<Instant, Instant> { | ||
| val anchorTime = ZonedDateTime.ofInstant(anchor.toJavaInstant(), utc) | ||
| val dateTime = ZonedDateTime.ofInstant(containing.toJavaInstant(), utc) | ||
| // Every boundary counts from the original anchor, so a clamped February never shortens later months. | ||
| val boundary = { index: Long -> anchorTime.plusMonths(index).toInstant().toKotlinInstant() } | ||
| var index = (dateTime.year - anchorTime.year) * 12L + dateTime.monthValue - anchorTime.monthValue | ||
| while (boundary(index) > containing) index-- | ||
| while (boundary(index + 1) <= containing) index++ | ||
| return boundary(index) to boundary(index + 1) | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Wallet-side capacity preflight. The SDK checks capacity only after automatic Acceptance, so Bitkit sums this | ||
| * Allowance's live automatic attempts in the current month first and keeps an over-cap request on the manual flow. | ||
| */ | ||
| object PaykitAllowanceCapacity { | ||
| /** Allowance ids are unique per contact only, so an attempt keeps the contact it paid. */ | ||
| data class Attempt( | ||
| val counterparty: String, | ||
| val allowanceId: String, | ||
| val amountSats: ULong, | ||
| val admittedAt: Instant, | ||
| val isLive: Boolean, | ||
| ) | ||
|
|
||
| fun usedSats( | ||
| counterparty: String, | ||
| allowanceId: String, | ||
| attempts: List<Attempt>, | ||
| anchor: Instant, | ||
| now: Instant, | ||
| ): ULong { | ||
| val (start, end) = PaykitAllowanceTime.monthlyWindow(anchor, now) | ||
| return attempts | ||
| .filter { | ||
| it.allowanceId == allowanceId && PubkyPublicKeyFormat.matches(it.counterparty, counterparty) && | ||
| it.isLive && it.admittedAt >= start && it.admittedAt < end | ||
| } | ||
| .fold(0uL) { total, attempt -> total.safe() + attempt.amountSats.safe() } | ||
| } | ||
|
|
||
| fun fits(amountSats: ULong, allowance: PaykitAllowance, attempts: List<Attempt>, now: Instant): Boolean { | ||
| val perPaymentMax = allowance.perPaymentMaxSats ?: return false | ||
| if (amountSats > perPaymentMax) return false | ||
| val monthlyLimit = allowance.monthlyLimitSats ?: return false | ||
| val anchor = allowance.monthlyAnchor ?: return false | ||
| val used = usedSats(allowance.counterparty, allowance.allowanceId, attempts, anchor, now) | ||
| return used.safe() + amountSats.safe() <= monthlyLimit | ||
| } | ||
|
|
||
| fun attempts(history: AllowanceAccountingHistory): List<Attempt> = history.occurrences | ||
| .flatMap { occurrence -> occurrence.attempts.map { occurrence.key.request.counterparty to it } } | ||
| .mapNotNull { (counterparty, attempt) -> | ||
| if (attempt.mode != PaymentExecutionMode.AUTOMATIC) return@mapNotNull null | ||
| val allowanceId = attempt.allowanceId ?: return@mapNotNull null | ||
| val admittedAt = PaykitAllowanceTime.parse(attempt.admittedAt) ?: return@mapNotNull null | ||
| val amountSats = PaykitAllowance.satsFromBitcoinAmount(attempt.amount.value()) ?: return@mapNotNull null | ||
| Attempt( | ||
| counterparty = counterparty, | ||
| allowanceId = allowanceId, | ||
| amountSats = amountSats, | ||
| admittedAt = admittedAt, | ||
| isLive = attempt.status != PaymentExecutionStatus.FAILED, | ||
| ) | ||
| } | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.