Skip to content

Commit 1d57e4c

Browse files
authored
Add files via upload
1 parent 430b959 commit 1d57e4c

7 files changed

Lines changed: 263 additions & 102 deletions

File tree

app/src/main/java/github/aeonbtc/ibiswallet/data/boltz/BoltzBehaviorPort.kt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,9 @@ internal object BullBoltzBehavior : BoltzBehaviorPort {
8787
PendingLightningPaymentPhase.FUNDING,
8888
PendingLightningPaymentPhase.IN_PROGRESS,
8989
-> !session.fundingTxid.isNullOrBlank()
90-
PendingLightningPaymentPhase.FAILED -> false
90+
PendingLightningPaymentPhase.REFUNDING,
91+
PendingLightningPaymentPhase.FAILED,
92+
-> false
9193
}
9294
}
9395

app/src/main/java/github/aeonbtc/ibiswallet/data/local/SecureStorage.kt

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1991,6 +1991,10 @@ class SecureStorage private constructor(private val context: Context) {
19911991
put("phase", session.phase.name)
19921992
put("status", session.status)
19931993
put("fundingTxid", session.fundingTxid)
1994+
put("boltzClaimPublicKey", session.boltzClaimPublicKey)
1995+
put("timeoutBlockHeight", session.timeoutBlockHeight)
1996+
put("swapTree", session.swapTree)
1997+
put("blindingKey", session.blindingKey)
19941998
}.toString()
19951999
regularPrefs.edit(commit = true) {
19962000
putString("${KEY_PENDING_BOLTZ_LIGHTNING_PAYMENT_PREFIX}$walletId", json)
@@ -2046,6 +2050,22 @@ class SecureStorage private constructor(private val context: Context) {
20462050
json.optString("fundingTxid", "").takeIf {
20472051
it.isNotBlank() && !json.isNull("fundingTxid")
20482052
},
2053+
boltzClaimPublicKey =
2054+
json.optString("boltzClaimPublicKey", "").takeIf {
2055+
it.isNotBlank() && !json.isNull("boltzClaimPublicKey")
2056+
},
2057+
timeoutBlockHeight =
2058+
json.optInt("timeoutBlockHeight", -1).takeIf {
2059+
it >= 0 && !json.isNull("timeoutBlockHeight")
2060+
},
2061+
swapTree =
2062+
json.optString("swapTree", "").takeIf {
2063+
it.isNotBlank() && !json.isNull("swapTree")
2064+
},
2065+
blindingKey =
2066+
json.optString("blindingKey", "").takeIf {
2067+
it.isNotBlank() && !json.isNull("blindingKey")
2068+
},
20492069
)
20502070
} catch (_: Exception) {
20512071
null

app/src/main/java/github/aeonbtc/ibiswallet/data/model/LiquidModels.kt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ sealed interface LightningPaymentExecutionPlan {
107107
val estimatedLockupAmountSats: Long,
108108
val paymentAmountSats: Long,
109109
val swapFeeSats: Long,
110+
val refundAddress: String? = null,
110111
) : LightningPaymentExecutionPlan
111112

112113
data class BoltzSwap(
@@ -431,7 +432,7 @@ data class PendingLightningInvoiceSession(
431432
val createdAt: Long = System.currentTimeMillis(),
432433
)
433434

434-
enum class PendingLightningPaymentPhase { PREPARED, FUNDING, IN_PROGRESS, FAILED }
435+
enum class PendingLightningPaymentPhase { PREPARED, FUNDING, IN_PROGRESS, REFUNDING, FAILED }
435436

436437
enum class LightningPaymentBackend {
437438
LWK_PREPARE_PAY,
@@ -457,6 +458,10 @@ data class PendingLightningPaymentSession(
457458
val phase: PendingLightningPaymentPhase = PendingLightningPaymentPhase.PREPARED,
458459
val status: String = "",
459460
val fundingTxid: String? = null,
461+
val boltzClaimPublicKey: String? = null,
462+
val timeoutBlockHeight: Int? = null,
463+
val swapTree: String? = null,
464+
val blindingKey: String? = null,
460465
)
461466

462467
// ──────────────────────────────────────────────

app/src/main/java/github/aeonbtc/ibiswallet/data/remote/BoltzApiClient.kt

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,16 @@ data class BoltzFetchedBolt12Invoice(
4848
val invoice: String,
4949
)
5050

51+
data class BoltzRefundDetails(
52+
val pubNonce: String,
53+
val transactionHash: String,
54+
)
55+
56+
data class BoltzRefundResponse(
57+
val pubNonce: String,
58+
val partialSignature: String,
59+
)
60+
5161
/**
5262
* Client for the Boltz API v2 endpoints still used by the app.
5363
*
@@ -402,6 +412,44 @@ class BoltzApiClient(
402412
}
403413
}
404414

415+
// ── Submarine Swap: Cooperative Refund ──
416+
417+
/**
418+
* GET /v2/swap/submarine/{id}/refund
419+
* Fetch Boltz's partial signature details for a cooperative key path refund.
420+
* Only available when the swap is in a failed/refundable state.
421+
*/
422+
suspend fun getSubmarineRefundDetails(swapId: String): BoltzRefundDetails {
423+
val j = get("/v2/swap/submarine/$swapId/refund")
424+
return BoltzRefundDetails(
425+
pubNonce = j.getString("pubNonce"),
426+
transactionHash = j.getString("transactionHash"),
427+
)
428+
}
429+
430+
/**
431+
* POST /v2/swap/submarine/{id}/refund
432+
* Submit client's partial signature for a cooperative key path refund.
433+
* Returns Boltz's partial signature to complete the aggregated Schnorr signature.
434+
*/
435+
suspend fun postSubmarineCooperativeRefund(
436+
swapId: String,
437+
pubNonce: String,
438+
transaction: String,
439+
index: Int = 0,
440+
): BoltzRefundResponse {
441+
val body = JSONObject().apply {
442+
put("pubNonce", pubNonce)
443+
put("transaction", transaction)
444+
put("index", index)
445+
}
446+
val j = post("/v2/swap/submarine/$swapId/refund", body)
447+
return BoltzRefundResponse(
448+
pubNonce = j.getString("pubNonce"),
449+
partialSignature = j.getString("partialSignature"),
450+
)
451+
}
452+
405453
// ── Swap Status ──
406454

407455
/** Get current swap status via REST */

app/src/main/java/github/aeonbtc/ibiswallet/data/repository/LiquidRepository.kt

Lines changed: 103 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -3039,82 +3039,15 @@ class LiquidRepository(
30393039
getCachedLightningPaymentResolution(executionPlan.paymentInput, executionPlan.requestedAmountSats)
30403040
?: resolveBoltzLightningPaymentInput(executionPlan.paymentInput, executionPlan.requestedAmountSats)
30413041
try {
3042-
if (resolvedPayment.fetchedInvoice != null) {
3043-
val refundDetails = allocateBoltzSubmarineRefundDetails()
3044-
logBoltzTrace(
3045-
"prepare_rest_submarine",
3046-
trace.copy(backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE.name, source = "rest"),
3047-
"invoice" to summarizeValue(resolvedPayment.fetchedInvoice),
3048-
"refundAddress" to summarizeValue(refundDetails.refundAddress),
3049-
)
3050-
logDebug(
3051-
"prepareLightningPaymentExecutionPlan using cached resolution " +
3052-
"requestKey=$requestKey amount=${executionPlan.paymentAmountSats} " +
3053-
"refund=${summarizeValue(refundDetails.refundAddress)}",
3054-
)
3055-
val response =
3056-
boltzProvider.createLegacySubmarineSwap(
3057-
invoice = resolvedPayment.fetchedInvoice,
3058-
refundPublicKey = refundDetails.refundPublicKey,
3059-
)
3060-
val paymentAmountSats =
3061-
executionPlan.requestedAmountSats.takeIf { it != null && it > 0L } ?: executionPlan.paymentAmountSats
3062-
val lockupAmountSats = maxOf(response.expectedAmount, safeAddSats(paymentAmountSats, 0L))
3063-
val swapFeeSats = (lockupAmountSats - paymentAmountSats).coerceAtLeast(0L)
3064-
val session =
3065-
PendingLightningPaymentSession(
3066-
swapId = response.id,
3067-
backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE,
3068-
requestKey = requestKey,
3069-
paymentInput = executionPlan.paymentInput,
3070-
lockupAddress = response.address,
3071-
lockupAmountSats = lockupAmountSats,
3072-
refundAddress = refundDetails.refundAddress,
3073-
requestedAmountSats = executionPlan.requestedAmountSats,
3074-
resolvedPaymentInput = resolvedPayment.paymentInput,
3075-
fetchedInvoice = resolvedPayment.fetchedInvoice,
3076-
refundPublicKey = refundDetails.refundPublicKey,
3077-
paymentAmountSats = paymentAmountSats,
3078-
swapFeeSats = swapFeeSats,
3079-
phase = PendingLightningPaymentPhase.PREPARED,
3080-
status = "Prepared Lightning payment. Awaiting funding.",
3081-
)
3082-
persistPreparedLightningPaymentSession(session)
3083-
logBoltzTrace(
3084-
"prepared",
3085-
trace.copy(
3086-
swapId = response.id,
3087-
backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE.name,
3088-
source = "rest",
3089-
),
3090-
"elapsedMs" to boltzElapsedMs(traceStartedAt),
3091-
"lockupAmountSats" to lockupAmountSats,
3092-
"paymentAmountSats" to paymentAmountSats,
3093-
"swapFeeSats" to swapFeeSats,
3094-
)
3095-
return@withContext LightningPaymentExecutionPlan.BoltzSwap(
3096-
paymentInput = executionPlan.paymentInput,
3097-
backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE,
3098-
requestKey = requestKey,
3099-
resolvedPaymentInput = resolvedPayment.paymentInput,
3100-
requestedAmountSats = executionPlan.requestedAmountSats,
3101-
swapId = response.id,
3102-
lockupAddress = response.address,
3103-
lockupAmountSats = lockupAmountSats,
3104-
refundAddress = refundDetails.refundAddress,
3105-
fetchedInvoice = resolvedPayment.fetchedInvoice,
3106-
refundPublicKey = refundDetails.refundPublicKey,
3107-
paymentAmountSats = paymentAmountSats,
3108-
swapFeeSats = swapFeeSats,
3109-
)
3110-
}
3042+
val lwkPaymentInput = resolvedPayment.fetchedInvoice ?: executionPlan.resolvedPaymentInput
31113043
val refundAddress = getNewAddress() ?: throw Exception("No Liquid refund address available")
3112-
val payment = createLightningPayment(executionPlan.resolvedPaymentInput)
3044+
val payment = createLightningPayment(lwkPaymentInput)
31133045
logBoltzTrace(
31143046
"prepare_lwk",
31153047
trace.copy(backend = LightningPaymentBackend.LWK_PREPARE_PAY.name, source = "lwk"),
31163048
"refundAddress" to summarizeValue(refundAddress),
3117-
"payment" to summarizeValue(executionPlan.resolvedPaymentInput),
3049+
"payment" to summarizeValue(lwkPaymentInput),
3050+
"hasFetchedInvoice" to (resolvedPayment.fetchedInvoice != null),
31183051
)
31193052
return@withContext withBoltzOperationLock(operation = "prepareLightningPaymentExecutionPlan") {
31203053
val walletId = currentWalletId ?: throw Exception("Wallet not loaded")
@@ -3143,6 +3076,7 @@ class LiquidRepository(
31433076
snapshot = snapshot,
31443077
requestedAmountSats = executionPlan.requestedAmountSats,
31453078
resolvedPaymentInput = executionPlan.resolvedPaymentInput,
3079+
fetchedInvoice = resolvedPayment.fetchedInvoice,
31463080
paymentAmountSats = paymentAmountSats,
31473081
swapFeeSats = effectiveSwapFeeSats,
31483082
phase = PendingLightningPaymentPhase.PREPARED,
@@ -3160,6 +3094,7 @@ class LiquidRepository(
31603094
"lockupAmountSats" to lockupAmountSats,
31613095
"paymentAmountSats" to paymentAmountSats,
31623096
"swapFeeSats" to effectiveSwapFeeSats,
3097+
"hasFetchedInvoice" to (resolvedPayment.fetchedInvoice != null),
31633098
)
31643099
LightningPaymentExecutionPlan.BoltzSwap(
31653100
paymentInput = executionPlan.paymentInput,
@@ -3171,7 +3106,7 @@ class LiquidRepository(
31713106
lockupAddress = response.uriAddress().toString(),
31723107
lockupAmountSats = lockupAmountSats,
31733108
refundAddress = refundAddress,
3174-
fetchedInvoice = null,
3109+
fetchedInvoice = resolvedPayment.fetchedInvoice,
31753110
refundPublicKey = null,
31763111
paymentAmountSats = paymentAmountSats,
31773112
swapFeeSats = effectiveSwapFeeSats,
@@ -3192,6 +3127,22 @@ class LiquidRepository(
31923127
amountSats = direct.amountSats,
31933128
)
31943129
} catch (e: Exception) {
3130+
if (resolvedPayment.fetchedInvoice != null && e !is LwkException.MagicRoutingHint) {
3131+
logBoltzTrace(
3132+
"lwk_failed_falling_back_to_rest",
3133+
trace,
3134+
level = BoltzTraceLevel.WARN,
3135+
throwable = e,
3136+
"elapsedMs" to boltzElapsedMs(traceStartedAt),
3137+
)
3138+
return@withContext prepareRestSubmarineSwapFallback(
3139+
executionPlan = executionPlan,
3140+
resolvedPayment = resolvedPayment,
3141+
requestKey = requestKey,
3142+
trace = trace,
3143+
traceStartedAt = traceStartedAt,
3144+
)
3145+
}
31953146
logBoltzTrace(
31963147
"failed",
31973148
trace,
@@ -3205,6 +3156,85 @@ class LiquidRepository(
32053156
}
32063157
}
32073158

3159+
private suspend fun prepareRestSubmarineSwapFallback(
3160+
executionPlan: LightningPaymentExecutionPlan.BoltzQuote,
3161+
resolvedPayment: ResolvedLightningPaymentInput,
3162+
requestKey: String,
3163+
trace: BoltzTraceContext,
3164+
traceStartedAt: Long,
3165+
): LightningPaymentExecutionPlan.BoltzSwap {
3166+
val fetchedInvoice = resolvedPayment.fetchedInvoice
3167+
?: throw Exception("REST fallback requires a fetched invoice")
3168+
val refundDetails = allocateBoltzSubmarineRefundDetails()
3169+
logBoltzTrace(
3170+
"prepare_rest_submarine_fallback",
3171+
trace.copy(backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE.name, source = "rest"),
3172+
"invoice" to summarizeValue(fetchedInvoice),
3173+
"refundAddress" to summarizeValue(refundDetails.refundAddress),
3174+
)
3175+
val response =
3176+
boltzProvider.createLegacySubmarineSwap(
3177+
invoice = fetchedInvoice,
3178+
refundPublicKey = refundDetails.refundPublicKey,
3179+
)
3180+
val paymentAmountSats =
3181+
executionPlan.requestedAmountSats.takeIf { it != null && it > 0L }
3182+
?: executionPlan.paymentAmountSats
3183+
val lockupAmountSats = maxOf(response.expectedAmount, safeAddSats(paymentAmountSats, 0L))
3184+
val swapFeeSats = (lockupAmountSats - paymentAmountSats).coerceAtLeast(0L)
3185+
val session =
3186+
PendingLightningPaymentSession(
3187+
swapId = response.id,
3188+
backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE,
3189+
requestKey = requestKey,
3190+
paymentInput = executionPlan.paymentInput,
3191+
lockupAddress = response.address,
3192+
lockupAmountSats = lockupAmountSats,
3193+
refundAddress = refundDetails.refundAddress,
3194+
requestedAmountSats = executionPlan.requestedAmountSats,
3195+
resolvedPaymentInput = resolvedPayment.paymentInput,
3196+
fetchedInvoice = fetchedInvoice,
3197+
refundPublicKey = refundDetails.refundPublicKey,
3198+
paymentAmountSats = paymentAmountSats,
3199+
swapFeeSats = swapFeeSats,
3200+
phase = PendingLightningPaymentPhase.PREPARED,
3201+
status = "Prepared Lightning payment. Awaiting funding.",
3202+
boltzClaimPublicKey = response.claimPublicKey,
3203+
timeoutBlockHeight = response.timeoutBlockHeight,
3204+
swapTree = response.swapTree,
3205+
blindingKey = response.blindingKey,
3206+
)
3207+
persistPreparedLightningPaymentSession(session)
3208+
logBoltzTrace(
3209+
"prepared",
3210+
trace.copy(
3211+
swapId = response.id,
3212+
backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE.name,
3213+
source = "rest",
3214+
),
3215+
"elapsedMs" to boltzElapsedMs(traceStartedAt),
3216+
"lockupAmountSats" to lockupAmountSats,
3217+
"paymentAmountSats" to paymentAmountSats,
3218+
"swapFeeSats" to swapFeeSats,
3219+
"timeoutBlockHeight" to response.timeoutBlockHeight,
3220+
)
3221+
return LightningPaymentExecutionPlan.BoltzSwap(
3222+
paymentInput = executionPlan.paymentInput,
3223+
backend = LightningPaymentBackend.BOLTZ_REST_SUBMARINE,
3224+
requestKey = requestKey,
3225+
resolvedPaymentInput = resolvedPayment.paymentInput,
3226+
requestedAmountSats = executionPlan.requestedAmountSats,
3227+
swapId = response.id,
3228+
lockupAddress = response.address,
3229+
lockupAmountSats = lockupAmountSats,
3230+
refundAddress = refundDetails.refundAddress,
3231+
fetchedInvoice = fetchedInvoice,
3232+
refundPublicKey = refundDetails.refundPublicKey,
3233+
paymentAmountSats = paymentAmountSats,
3234+
swapFeeSats = swapFeeSats,
3235+
)
3236+
}
3237+
32083238
suspend fun resolveLightningPaymentPreview(
32093239
paymentInput: String,
32103240
requestedAmountSats: Long?,
@@ -3226,6 +3256,7 @@ class LiquidRepository(
32263256
estimatedLockupAmountSats = context.estimatedLockupAmountSats,
32273257
paymentAmountSats = context.paymentAmountSats,
32283258
swapFeeSats = context.estimatedSwapFeeSats,
3259+
refundAddress = _liquidState.value.currentAddress,
32293260
)
32303261
val preview =
32313262
buildLightningPreviewFromExecutionPlan(

0 commit comments

Comments
 (0)