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
Merged
+8,424
−119
Merged
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 e5c74f2
chore: sync allowances with the rewritten shared paykit runtime
ovitrif 1d66815
chore: sync allowances with the rewritten shared paykit runtime
ovitrif 21b3585
fix: pay allowances only to unused on-chain addresses
ovitrif ef44cb4
test: pin that allowances ignore the subscription clock offset
ovitrif fcabfd1
chore: sync allowances with the shared paykit runtime
ovitrif e5204fb
chore: sync allowances with the shared paykit runtime
ovitrif 60b20a8
chore: sync allowances with the shared paykit runtime
ovitrif 53e8bab
chore: sync allowances with the shared paykit runtime
ovitrif 848c99e
chore: sync allowances with the shared paykit runtime
ovitrif 002c2eb
chore: sync allowances with the shared paykit runtime
ovitrif 0322506
chore: sync allowances with the shared paykit runtime
ovitrif 54c305d
chore: sync allowances with the shared paykit runtime
ovitrif 44ee702
chore: sync allowances with the shared paykit runtime
ovitrif af1e7b4
chore: sync allowances with the shared paykit runtime
ovitrif e15f956
chore: sync allowances with the shared paykit runtime
ovitrif cd2748d
chore: sync allowances with the shared paykit runtime
ovitrif ed7eae3
chore: sync allowances with master
ovitrif 30bab90
test: keep allowance requests manual off bitcoin or behind a blocked …
ovitrif 39f4e77
fix: settle allowance attempts only on proven outcomes
ovitrif 84ca474
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 94c732d
fix: notify limit reached when a payment exceeds the per-payment max
ovitrif 1f2c365
feat: require the payment pin to grant or end an allowance
ovitrif ccedc30
fix: keep automatic allowance payments within visible limits
ovitrif a5b58ea
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 3bf876a
fix: pay allowance requests automatically only over lightning
ovitrif c54e3d1
fix: show allowee per-payment limit in whole dollars like the payer
ovitrif 9fc5bc4
fix: cap routing fees of automatic allowance payments
ovitrif c323a9a
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif dff5550
fix: drop allowance terms whose limits cannot be read
ovitrif b2adbda
fix: never merge allowances of different contacts
ovitrif ad41186
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 87f274a
chore: fix lint findings after the master sync
ovitrif fdf3fcc
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 5843254
fix: count allowance payments per contact
ovitrif 35aa914
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 215c4c0
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 1628436
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif f8aeffb
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 496dc65
fix: keep automatic allowance payments out of the user's send flow
ovitrif 481bec8
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif 7db49c1
fix: let manual retries of an allowance invoice settle as manual
ovitrif 46951c0
fix: announce each automatic allowance payment once
ovitrif 5915231
fix: treat a manual retry of an allowance invoice as the user's payment
ovitrif 1d8449c
Merge remote-tracking branch 'origin/master' into feat/paykit-allowances
ovitrif efa84ff
fix: auto-pay bitcoin requests that also offer a usdt rail
ovitrif 2f8ca79
fix: hold allowance coverage through the request refresh and release …
ovitrif bb31e95
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.