Skip to content
This repository was archived by the owner on Feb 9, 2026. It is now read-only.

Commit 2f30129

Browse files
committed
Write pending and confirmed sent payments natively
1 parent 63e4325 commit 2f30129

9 files changed

Lines changed: 248 additions & 28 deletions

File tree

lib/android/src/main/java/com/reactnativeldk/Helpers.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.reactnativeldk
22
import com.facebook.react.bridge.*
3+
import org.json.JSONObject
34
import org.ldk.enums.Currency
45
import org.ldk.enums.Network
56
import org.ldk.structs.*
@@ -381,4 +382,18 @@ fun getNetwork(chain: String): Pair<Network, Currency> {
381382
"mainnet" -> Pair(Network.LDKNetwork_Bitcoin, Currency.LDKCurrency_Bitcoin)
382383
else -> Pair(Network.LDKNetwork_Bitcoin, Currency.LDKCurrency_Bitcoin)
383384
}
385+
}
386+
387+
fun mergeObj(obj1: JSONObject, obj2: HashMap<String, Any>): HashMap<String, Any> {
388+
val newObj = HashMap<String, Any>()
389+
390+
obj1.keys().forEach { key ->
391+
newObj[key] = obj1[key]
392+
}
393+
394+
obj2.keys.forEach { key ->
395+
newObj[key] = obj2[key]!!
396+
}
397+
398+
return newObj
384399
}

lib/android/src/main/java/com/reactnativeldk/LdkModule.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import org.ldk.impl.bindings.get_ldk_version
1313
import org.ldk.structs.*
1414
import org.ldk.structs.Result_InvoiceParseOrSemanticErrorZ.Result_InvoiceParseOrSemanticErrorZ_OK
1515
import org.ldk.structs.Result_InvoiceSignOrCreationErrorZ.Result_InvoiceSignOrCreationErrorZ_OK
16+
import org.ldk.structs.Result_PaymentIdPaymentErrorZ.Result_PaymentIdPaymentErrorZ_OK
1617
import java.io.File
1718
import java.net.InetSocketAddress
1819
import java.nio.file.Files
@@ -113,6 +114,7 @@ enum class LdkFileNames(val fileName: String) {
113114
channel_manager("channel_manager.bin"),
114115
scorer("scorer.bin"),
115116
paymentsClaimed("payments_claimed.json"),
117+
paymentsSent("payments_sent.json"),
116118
}
117119

118120
class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaModule(reactContext) {
@@ -685,6 +687,14 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
685687
UtilMethods.pay_zero_value_invoice(invoice, amountSats.toLong() * 1000, Retry.timeout(timeoutSeconds.toLong()), channelManager) else
686688
UtilMethods.pay_invoice(invoice, Retry.timeout(timeoutSeconds.toLong()), channelManager)
687689
if (res.is_ok) {
690+
channelManagerPersister.persistPaymentSent(hashMapOf(
691+
"payment_id" to (res as Result_PaymentIdPaymentErrorZ_OK).res.hexEncodedString(),
692+
"payment_hash" to invoice.payment_hash().hexEncodedString(),
693+
"amount_sat" to if (isZeroValueInvoice) amountSats.toLong() else ((invoice.amount_milli_satoshis() as Option_u64Z.Some).some.toInt() / 1000),
694+
"unix_timestamp" to (System.currentTimeMillis() / 1000).toInt(),
695+
"state" to "pending"
696+
))
697+
688698
return handleResolve(promise, LdkCallbackResponses.invoice_payment_success)
689699
}
690700

lib/android/src/main/java/com/reactnativeldk/classes/LdkChannelManagerPersister.kt

Lines changed: 69 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ class LdkChannelManagerPersister: ChannelManagerConstructor.EventHandler {
3333
body.putHexString("spontaneous_payment_preimage", it.spontaneous_payment)
3434
}
3535
body.putInt("unix_timestamp", (System.currentTimeMillis() / 1000).toInt())
36-
body.putBoolean("confirmed", false)
36+
body.putString("state", "pending")
3737

3838
persistPaymentClaimed(body)
3939
return LdkEventEmitter.send(EventTypes.channel_manager_payment_claimable, body)
@@ -45,6 +45,11 @@ class LdkChannelManagerPersister: ChannelManagerConstructor.EventHandler {
4545
body.putHexString("payment_preimage", paymentSent.payment_preimage)
4646
body.putHexString("payment_hash", paymentSent.payment_hash)
4747
body.putInt("fee_paid_sat", (paymentSent.fee_paid_msat as Option_u64Z.Some).some.toInt() / 1000)
48+
body.putInt("unix_timestamp", (System.currentTimeMillis() / 1000).toInt())
49+
body.putString("state", "successful")
50+
51+
persistPaymentSent(body.toHashMap())
52+
4853
return LdkEventEmitter.send(EventTypes.channel_manager_payment_sent, body)
4954
}
5055

@@ -82,13 +87,30 @@ class LdkChannelManagerPersister: ChannelManagerConstructor.EventHandler {
8287
// paymentPathFailed.path.iterator().forEach { path.pushMap(it.asJson) }
8388
// body.putArray("path_hops", path)
8489

90+
if (paymentPathFailed.payment_id != null) {
91+
persistPaymentSent(hashMapOf(
92+
"payment_id" to paymentPathFailed.payment_id!!.hexEncodedString(),
93+
"payment_hash" to paymentPathFailed.payment_hash.hexEncodedString(),
94+
"unix_timestamp" to (System.currentTimeMillis() / 1000).toInt(),
95+
"state" to if (paymentPathFailed.payment_failed_permanently) "failed" else "pending"
96+
))
97+
}
98+
8599
return LdkEventEmitter.send(EventTypes.channel_manager_payment_path_failed, body)
86100
}
87101

88102
(event as? Event.PaymentFailed)?.let { paymentFailed ->
89103
val body = Arguments.createMap()
90104
body.putHexString("payment_id", paymentFailed.payment_id)
91105
body.putHexString("payment_hash", paymentFailed.payment_hash)
106+
107+
persistPaymentSent(hashMapOf(
108+
"payment_id" to paymentFailed.payment_id!!.hexEncodedString(),
109+
"payment_hash" to paymentFailed.payment_hash.hexEncodedString(),
110+
"unix_timestamp" to (System.currentTimeMillis() / 1000).toInt(),
111+
"state" to "failed"
112+
))
113+
92114
return LdkEventEmitter.send(EventTypes.channel_manager_payment_failed, body)
93115
}
94116

@@ -139,7 +161,7 @@ class LdkChannelManagerPersister: ChannelManagerConstructor.EventHandler {
139161
body.putHexString("spontaneous_payment_preimage", it.spontaneous_payment)
140162
}
141163
body.putInt("unix_timestamp", (System.currentTimeMillis() / 1000).toInt())
142-
body.putBoolean("confirmed", true)
164+
body.putString("state", "successful")
143165

144166
persistPaymentClaimed(body)
145167
return LdkEventEmitter.send(EventTypes.channel_manager_payment_claimed, body)
@@ -182,14 +204,12 @@ class LdkChannelManagerPersister: ChannelManagerConstructor.EventHandler {
182204
try {
183205
if (File(LdkModule.accountStoragePath + "/" + LdkFileNames.paymentsClaimed.fileName).exists()) {
184206
val data = File(LdkModule.accountStoragePath + "/" + LdkFileNames.paymentsClaimed.fileName).readBytes()
185-
println(String(data))
186207
val existingPayments = JSONArray(String(data))
187208
for (i in 0 until existingPayments.length()) {
188209
val existingPayment = existingPayments.getJSONObject(i)
189-
190210
//Replace entry if payment hash exists (Confirmed payment replacing pending)
191211
if (existingPayment.getString("payment_hash") == payment.getString("payment_hash")) {
192-
payments[i] = payment.toHashMap()
212+
payments[i] = mergeObj(existingPayment, payment.toHashMap())
193213
paymentReplaced = true
194214
continue
195215
}
@@ -203,17 +223,57 @@ class LdkChannelManagerPersister: ChannelManagerConstructor.EventHandler {
203223
}
204224
}
205225
} catch (e: Exception) {
206-
LdkEventEmitter.send(EventTypes.native_log, "Error could not read exisitng claimed payments")
226+
LdkEventEmitter.send(EventTypes.native_log, "Error could not read existing claimed payments")
207227
}
208228

209229
//No existing payment found, append as new payment
210230
if (!paymentReplaced) {
211231
payments = payments.plus(payment.toHashMap())
212232
}
213233

214-
println("New content")
215-
payments.iterator().forEach { println(it) }
216-
217234
File(LdkModule.accountStoragePath + "/" + LdkFileNames.paymentsClaimed.fileName).writeText(JSONArray(payments).toString())
218235
}
236+
237+
fun persistPaymentSent(payment: HashMap<String, Any>) {
238+
if (LdkModule.accountStoragePath == "") {
239+
LdkEventEmitter.send(EventTypes.native_log, "Error. Failed to persist sent payment to disk (No set storage)")
240+
return
241+
}
242+
243+
var payments: Array<HashMap<String, Any>> = arrayOf()
244+
var paymentReplaced = false
245+
246+
try {
247+
if (File(LdkModule.accountStoragePath + "/" + LdkFileNames.paymentsSent.fileName).exists()) {
248+
val data = File(LdkModule.accountStoragePath + "/" + LdkFileNames.paymentsSent.fileName).readBytes()
249+
val existingPayments = JSONArray(String(data))
250+
for (i in 0 until existingPayments.length()) {
251+
val existingPayment = existingPayments.getJSONObject(i)
252+
//Replace entry if payment ID exists (Confirmed payment replacing pending)
253+
if (existingPayment.getString("payment_id") == payment["payment_id"]) {
254+
var merged = mergeObj(existingPayment, payment)
255+
payments = payments.plus(merged)
256+
paymentReplaced = true
257+
continue
258+
}
259+
260+
val map = HashMap<String, Any>()
261+
for (key in existingPayment.keys()) {
262+
map[key] = existingPayment.get(key)
263+
}
264+
265+
payments = payments.plus(map)
266+
}
267+
}
268+
} catch (e: Exception) {
269+
LdkEventEmitter.send(EventTypes.native_log, "Error could not read existing sent payments")
270+
}
271+
272+
//No existing payment found, append as new payment
273+
if (!paymentReplaced) {
274+
payments = payments.plus(payment)
275+
}
276+
277+
File(LdkModule.accountStoragePath + "/" + LdkFileNames.paymentsSent.fileName).writeText(JSONArray(payments).toString())
278+
}
219279
}

lib/ios/Classes/LdkChannelManagerPersister.swift

Lines changed: 106 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
5252
"payment_secret": Data(paymentSecret ?? []).hexEncodedString(),
5353
"spontaneous_payment_preimage": Data(spontaneousPayment ?? []).hexEncodedString(),
5454
"unix_timestamp": Int(Date().timeIntervalSince1970),
55-
"confirmed": false
55+
"state": "pending"
5656
]
5757

5858
LdkEventEmitter.shared.send(
@@ -68,15 +68,22 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
6868
return handleEventError(event)
6969
}
7070

71+
let body: [String: Encodable] = [
72+
"payment_id": Data(paymentSent.getPaymentId()).hexEncodedString(),
73+
"payment_preimage": Data(paymentSent.getPaymentPreimage()).hexEncodedString(),
74+
"payment_hash": Data(paymentSent.getPaymentHash()).hexEncodedString(),
75+
"fee_paid_sat": (paymentSent.getFeePaidMsat() ?? 0) / 1000,
76+
"unix_timestamp": Int(Date().timeIntervalSince1970),
77+
"state": "successful"
78+
]
79+
7180
LdkEventEmitter.shared.send(
7281
withEvent: .channel_manager_payment_sent,
73-
body: [
74-
"payment_id": Data(paymentSent.getPaymentId()).hexEncodedString(),
75-
"payment_preimage": Data(paymentSent.getPaymentPreimage()).hexEncodedString(),
76-
"payment_hash": Data(paymentSent.getPaymentHash()).hexEncodedString(),
77-
"fee_paid_sat": (paymentSent.getFeePaidMsat() ?? 0) / 1000,
78-
]
82+
body: body
7983
)
84+
85+
//Save to disk for tx history
86+
persistPaymentSent(body)
8087
return
8188
case .OpenChannelRequest:
8289
//Use if we ever manually accept inbound channels. Setting in initConfig.
@@ -99,11 +106,14 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
99106
return handleEventError(event)
100107
}
101108

109+
let paymentId = Data(paymentPathSuccessful.getPaymentId()).hexEncodedString()
110+
let paymentHash = Data(paymentPathSuccessful.getPaymentHash()).hexEncodedString()
111+
102112
LdkEventEmitter.shared.send(
103113
withEvent: .channel_manager_payment_path_successful,
104114
body: [
105-
"payment_id": Data(paymentPathSuccessful.getPaymentId()).hexEncodedString(),
106-
"payment_hash": Data(paymentPathSuccessful.getPaymentHash()).hexEncodedString(),
115+
"payment_id": paymentId,
116+
"payment_hash": paymentHash,
107117
"path_hops": paymentPathSuccessful.getPath().getHops().map { $0.asJson },
108118
]
109119
)
@@ -113,27 +123,53 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
113123
return handleEventError(event)
114124
}
115125

126+
let paymentId = Data(paymentPathFailed.getPaymentId()).hexEncodedString()
127+
let paymentHash = Data(paymentPathFailed.getPaymentHash()).hexEncodedString()
128+
116129
LdkEventEmitter.shared.send(
117130
withEvent: .channel_manager_payment_path_failed,
118131
body: [
119-
"payment_id": Data(paymentPathFailed.getPaymentId()).hexEncodedString(),
120-
"payment_hash": Data(paymentPathFailed.getPaymentHash()).hexEncodedString(),
132+
"payment_id": paymentId,
133+
"payment_hash": paymentHash,
121134
"payment_failed_permanently": paymentPathFailed.getPaymentFailedPermanently(),
122135
"short_channel_id": String(paymentPathFailed.getShortChannelId() ?? 0),
123136
"path_hops": paymentPathFailed.getPath().getHops().map { $0.asJson }
124137
]
125138
)
139+
140+
persistPaymentSent(
141+
[
142+
"payment_id": paymentId,
143+
"payment_hash": paymentHash,
144+
"unix_timestamp": Int(Date().timeIntervalSince1970),
145+
"state": paymentPathFailed.getPaymentFailedPermanently() ? "failed" : "pending"
146+
]
147+
)
126148
return
127149
case .PaymentFailed:
128150
guard let paymentFailed = event.getValueAsPaymentFailed() else {
129151
return handleEventError(event)
130152
}
131153

154+
let paymentId = Data(paymentFailed.getPaymentId()).hexEncodedString()
155+
let paymentHash = Data(paymentFailed.getPaymentHash()).hexEncodedString()
156+
132157
LdkEventEmitter.shared.send(
133158
withEvent: .channel_manager_payment_failed,
134159
body: [
135-
"payment_id": Data(paymentFailed.getPaymentId()).hexEncodedString(),
136-
"payment_hash": Data(paymentFailed.getPaymentHash()).hexEncodedString(),
160+
"payment_id": paymentId,
161+
"payment_hash": paymentHash,
162+
]
163+
)
164+
165+
//MARK: Mark as failed
166+
167+
persistPaymentSent(
168+
[
169+
"payment_id": paymentId,
170+
"payment_hash": paymentHash,
171+
"unix_timestamp": Int(Date().timeIntervalSince1970),
172+
"state": "failed"
137173
]
138174
)
139175
return
@@ -208,7 +244,7 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
208244
"payment_secret": Data(paymentSecret ?? []).hexEncodedString(),
209245
"spontaneous_payment_preimage": Data(spontaneousPayment ?? []).hexEncodedString(),
210246
"unix_timestamp": Int(Date().timeIntervalSince1970),
211-
"confirmed": true
247+
"state": "successful"
212248
]
213249

214250
LdkEventEmitter.shared.send(
@@ -271,7 +307,9 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
271307
return Result_NoneErrorZ.initWithErr(e: .Other)
272308
}
273309
}
274-
310+
311+
/// Saves claiming/claimed payment to disk. If payment hash exists already then the payment values are merged into the existing entry as an update
312+
/// - Parameter payment: payment obj
275313
private func persistPaymentClaimed(_ payment: [String: Any]) {
276314
guard let claimedPaymentsStorage = Ldk.accountStoragePath?.appendingPathComponent(LdkFileNames.paymentsClaimed.rawValue) else {
277315
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to persist claimed payment to disk (No set storage)")
@@ -296,7 +334,7 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
296334
for (index, existingPayment) in payments.enumerated() {
297335
if let existingPaymentHash = existingPayment["payment_hash"] as? String, let newPaymentHash = payment["payment_hash"] as? String {
298336
if existingPaymentHash == newPaymentHash {
299-
payments[index] = payment
337+
payments[index] = mergeObj(payments[index], payment) //Merges update into orginal entry
300338
paymentReplaced = true
301339
}
302340
}
@@ -321,4 +359,56 @@ class LdkChannelManagerPersister: Persister, ExtendedChannelManagerPersister {
321359
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error writing payment claimed to file: \(error)")
322360
}
323361
}
362+
363+
/// Saves sending/sent payment to disk. If payment ID exists already then the payment values are merged into the existing entry as an update
364+
/// - Parameter payment: payment obj
365+
func persistPaymentSent(_ payment: [String: Any]) {
366+
guard let sentPaymentsStorage = Ldk.accountStoragePath?.appendingPathComponent(LdkFileNames.paymentsSent.rawValue) else {
367+
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error. Failed to persist sent payment to disk (No set storage)")
368+
return
369+
}
370+
371+
var payments: [[String: Any]] = []
372+
373+
do {
374+
if FileManager.default.fileExists(atPath: sentPaymentsStorage.path) {
375+
let data = try Data(contentsOf: URL(fileURLWithPath: sentPaymentsStorage.path), options: .mappedIfSafe)
376+
377+
if let existingContent = try JSONSerialization.jsonObject(with: data, options: []) as? [[String: Any]] {
378+
payments = existingContent
379+
} else {
380+
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error could not read existing sent payments")
381+
}
382+
}
383+
384+
//Replace entry if payment hash exists (Confirmed payment replacing pending)
385+
var paymentReplaced = false
386+
for (index, existingPayment) in payments.enumerated() {
387+
if let existingPaymentId = existingPayment["payment_id"] as? String, let newPaymentId = payment["payment_id"] as? String {
388+
if existingPaymentId == newPaymentId {
389+
payments[index] = mergeObj(payments[index], payment) //Merges update into orginal entry
390+
paymentReplaced = true
391+
}
392+
}
393+
}
394+
395+
//No existing payment found, append as new payment
396+
if !paymentReplaced {
397+
payments.append(payment)
398+
}
399+
400+
guard let jsonData = try? JSONSerialization.data(withJSONObject: payments, options: []) else {
401+
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error could not serialize sent payments")
402+
return
403+
}
404+
405+
guard let jsonString = String(data: jsonData, encoding: .utf8) else {
406+
return
407+
}
408+
409+
try jsonString.write(to: sentPaymentsStorage, atomically: true, encoding: .utf8)
410+
} catch {
411+
LdkEventEmitter.shared.send(withEvent: .native_log, body: "Error writing payment sent to file: \(error)")
412+
}
413+
}
324414
}

0 commit comments

Comments
 (0)