Skip to content

Commit 07bd71e

Browse files
authored
Merge pull request #282 from synonymdev/graph-reset
Reset graph and scorer if failing to find a path
2 parents 0a7c8bb + bcfa5c7 commit 07bd71e

File tree

2 files changed

+239
-39
lines changed

2 files changed

+239
-39
lines changed

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

Lines changed: 131 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,6 @@ enum class LdkCallbackResponses {
123123
peer_already_connected,
124124
peer_currently_connecting,
125125
chain_sync_success,
126-
invoice_payment_success,
127126
tx_set_confirmed,
128127
tx_set_unconfirmed,
129128
process_pending_htlc_forwards_success,
@@ -185,6 +184,8 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
185184
private var currentNetwork: String? = null
186185
private var currentBlockchainTipHash: String? = null
187186
private var currentBlockchainHeight: Double? = null
187+
private var currentScorerDownloadUrl: String? = null
188+
private var currentRapidGossipSyncUrl: String? = null
188189

189190
//List of peers that "should" remain connected. Stores address: String, port: Double, pubKey: String
190191
private var addedPeers = ConcurrentLinkedQueue<Map<String, Any>>()
@@ -292,6 +293,9 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
292293
@ReactMethod
293294
fun downloadScorer(scorerSyncUrl: String, skipHoursThreshold: Double, promise: Promise) {
294295
val scorerFile = File(accountStoragePath + "/" + LdkFileNames.Scorer.fileName)
296+
297+
currentScorerDownloadUrl = scorerSyncUrl
298+
295299
//If old one is still recent, skip download. Else delete it.
296300
if (scorerFile.exists()) {
297301
val lastModifiedHours = (System.currentTimeMillis().toDouble() - scorerFile.lastModified().toDouble()) / 1000 / 60 / 60
@@ -329,6 +333,8 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
329333
return handleReject(promise, LdkErrors.already_init)
330334
}
331335

336+
currentRapidGossipSyncUrl = rapidGossipSyncUrl
337+
332338
val networkGraphFile = File(accountStoragePath + "/" + LdkFileNames.NetworkGraph.fileName)
333339
if (networkGraphFile.exists()) {
334340
(NetworkGraph.read(networkGraphFile.readBytes(), logger.logger) as? Result_NetworkGraphDecodeErrorZ.Result_NetworkGraphDecodeErrorZ_OK)?.let { res ->
@@ -427,7 +433,7 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
427433
var channelManagerSerialized: ByteArray? = null
428434
val channelManagerFile = File(accountStoragePath + "/" + LdkFileNames.ChannelManager.fileName)
429435
if (channelManagerFile.exists()) {
430-
channelManagerSerialized = channelManagerFile.readBytes()
436+
channelManagerSerialized = channelManagerFile.readBytes()
431437
}
432438

433439
//Scorer setup
@@ -558,11 +564,11 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
558564
LdkEventEmitter.send(EventTypes.channel_manager_restarted, "")
559565
LdkEventEmitter.send(EventTypes.native_log, "LDK restarted successfully")
560566
handleResolve(promise, LdkCallbackResponses.ldk_restart)
561-
},
567+
},
562568
{ reject ->
563569
LdkEventEmitter.send(EventTypes.native_log, "Error restarting LDK. Error: $reject")
564570
handleReject(promise, LdkErrors.unknown_error)
565-
})
571+
})
566572

567573
initChannelManager(
568574
currentNetwork,
@@ -687,7 +693,7 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
687693
if (currentlyConnectingPeers.contains(pubKey)) {
688694
return handleResolve(promise, LdkCallbackResponses.peer_currently_connecting)
689695
}
690-
696+
691697
try {
692698
currentlyConnectingPeers.add(pubKey)
693699
peerHandler!!.connect(pubKey.hexa(), InetSocketAddress(address, port.toInt()), timeout.toInt())
@@ -925,26 +931,131 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
925931
promise.resolve(parsedInvoice.res.asJson)
926932
}
927933

934+
private fun resetGraphAndScorerAndRetryPayment(
935+
originalError: LdkErrors,
936+
paymentRequest: String,
937+
amountSats: Double,
938+
timeoutSeconds: Double,
939+
promise: Promise
940+
) {
941+
if (accountStoragePath == "") {
942+
LdkEventEmitter.send(EventTypes.native_log, "Failed to reset graph: account storage path not set")
943+
return handleReject(promise, originalError)
944+
}
945+
946+
// Check required data and URLs
947+
val currentNetwork = currentNetwork ?: return handleReject(promise, originalError)
948+
949+
if (currentRapidGossipSyncUrl.isNullOrEmpty() || currentScorerDownloadUrl.isNullOrEmpty()) {
950+
val missingUrl = if (currentRapidGossipSyncUrl.isNullOrEmpty()) "rapid gossip sync" else "scorer download"
951+
LdkEventEmitter.send(EventTypes.native_log, "Failed to reset graph: $missingUrl URL not set")
952+
return handleReject(promise, originalError)
953+
}
954+
955+
val scorerFile = File("$accountStoragePath/${LdkFileNames.Scorer.fileName}")
956+
val networkGraphFile = File("$accountStoragePath/${LdkFileNames.NetworkGraph.fileName}")
957+
958+
// Delete scorer if exists
959+
if (scorerFile.exists()) {
960+
try {
961+
scorerFile.delete()
962+
LdkEventEmitter.send(EventTypes.native_log, "Deleted scorer file")
963+
} catch (e: Exception) {
964+
LdkEventEmitter.send(EventTypes.native_log, "Failed to delete scorer file: ${e.localizedMessage}")
965+
}
966+
}
967+
968+
// Delete network graph if exists
969+
if (networkGraphFile.exists()) {
970+
try {
971+
networkGraphFile.delete()
972+
LdkEventEmitter.send(EventTypes.native_log, "Deleted network graph file")
973+
networkGraph = null
974+
} catch (e: Exception) {
975+
LdkEventEmitter.send(EventTypes.native_log, "Failed to delete network graph file: ${e.localizedMessage}")
976+
}
977+
}
978+
979+
LdkEventEmitter.send(EventTypes.native_log, "Deleted scorer and network graph, resyncing from scratch so we can retry payment")
980+
981+
// Download everything again and retry
982+
downloadScorer(currentScorerDownloadUrl!!, 1.0, object : PromiseImpl(
983+
{ _ ->
984+
LdkEventEmitter.send(EventTypes.native_log, "Scorer downloaded, initializing network graph...")
985+
initNetworkGraph(currentNetwork, currentRapidGossipSyncUrl!!, 1.0, object : PromiseImpl(
986+
{ _ ->
987+
LdkEventEmitter.send(EventTypes.native_log, "Network graph initialized, restarting channel manager...")
988+
restart(object : PromiseImpl(
989+
{ _ ->
990+
// Run handleDroppedPeers on a background thread (can't work in the UI thread)
991+
Thread {
992+
handleDroppedPeers()
993+
}.start()
994+
995+
Thread.sleep(2500) //Wait a little as android peer connections happen async so we're just making sure they're all connected
996+
val channelsInGraph = networkGraph?.read_only()?.list_channels()?.size
997+
LdkEventEmitter.send(EventTypes.native_log, "Channels found in graph: $channelsInGraph")
998+
LdkEventEmitter.send(EventTypes.native_log, "Peers connected: ${peerManager?.list_peers()?.size}")
999+
LdkEventEmitter.send(EventTypes.native_log, "Restart complete. Attempting to retry payment after graph reset...")
1000+
val (paymentId2, error2) = handlePayment(paymentRequest, amountSats, timeoutSeconds)
1001+
1002+
if (error2 != null) {
1003+
LdkEventEmitter.send(EventTypes.native_log, "Failed to retry payment after graph reset: $error2")
1004+
handleReject(promise, error2)
1005+
} else {
1006+
LdkEventEmitter.send(EventTypes.native_log, "Successfully retried payment after graph reset")
1007+
// 2nd attempt found a path with fresh graph
1008+
promise.resolve(paymentId2)
1009+
}
1010+
},
1011+
{ _ -> handleReject(promise, originalError) }
1012+
) {})
1013+
},
1014+
{ _ -> handleReject(promise, originalError) }
1015+
) {})
1016+
},
1017+
{ _ -> handleReject(promise, originalError) }
1018+
) {})
1019+
}
1020+
9281021
@ReactMethod
9291022
fun pay(paymentRequest: String, amountSats: Double, timeoutSeconds: Double, promise: Promise) {
930-
channelManager ?: return handleReject(promise, LdkErrors.init_channel_manager)
1023+
val (paymentId, error) = handlePayment(paymentRequest, amountSats, timeoutSeconds)
1024+
if (error != null) {
1025+
// If error is route not found, maybe a problem with the graph, so reset it, download all again and try payment one more time
1026+
if (error == LdkErrors.invoice_payment_fail_route_not_found) {
1027+
return resetGraphAndScorerAndRetryPayment(
1028+
error,
1029+
paymentRequest,
1030+
amountSats,
1031+
timeoutSeconds,
1032+
promise
1033+
)
1034+
}
1035+
return handleReject(promise, error)
1036+
}
1037+
return promise.resolve(paymentId)
1038+
}
1039+
1040+
private fun handlePayment(paymentRequest: String, amountSats: Double, timeoutSeconds: Double): Pair<String?, LdkErrors?> {
1041+
channelManager ?: return Pair(null, LdkErrors.init_channel_manager)
9311042

9321043
val invoiceParse = Bolt11Invoice.from_str(paymentRequest)
9331044
if (!invoiceParse.is_ok) {
934-
return handleReject(promise, LdkErrors.decode_invoice_fail)
1045+
return Pair(null, LdkErrors.decode_invoice_fail)
9351046
}
9361047
val invoice = (invoiceParse as Result_Bolt11InvoiceParseOrSemanticErrorZ_OK).res
9371048

9381049
val isZeroValueInvoice = invoice.amount_milli_satoshis() is Option_u64Z.None
9391050

9401051
//If it's a zero invoice and we don't have an amount then don't proceed
9411052
if (isZeroValueInvoice && amountSats == 0.0) {
942-
return handleReject(promise, LdkErrors.invoice_payment_fail_must_specify_amount)
1053+
return Pair(null, LdkErrors.invoice_payment_fail_must_specify_amount)
9431054
}
9441055

9451056
//Amount was set but not allowed to set own amount
9461057
if (amountSats > 0 && !isZeroValueInvoice) {
947-
return handleReject(promise, LdkErrors.invoice_payment_fail_must_not_specify_amount)
1058+
return Pair(null, LdkErrors.invoice_payment_fail_must_not_specify_amount)
9481059
}
9491060

9501061
val paymentId = invoice.payment_hash()
@@ -953,7 +1064,7 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
9531064
UtilMethods.payment_parameters_from_invoice(invoice)
9541065

9551066
if (!detailsRes.is_ok) {
956-
return handleReject(promise, LdkErrors.invoice_payment_fail_invoice)
1067+
return Pair(null, LdkErrors.invoice_payment_fail_invoice)
9571068
}
9581069

9591070
val sendDetails = detailsRes as Result_C3Tuple_ThirtyTwoBytesRecipientOnionFieldsRouteParametersZNoneZ_OK
@@ -974,26 +1085,23 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
9741085
"state" to "pending"
9751086
))
9761087

977-
return handleResolve(promise, LdkCallbackResponses.invoice_payment_success)
1088+
return Pair(paymentId.hexEncodedString(), null)
9781089
}
9791090

9801091
val error = res as? Result_NoneRetryableSendFailureZ_Err
981-
?: return handleReject(promise, LdkErrors.invoice_payment_fail_unknown)
1092+
?: return Pair(null, LdkErrors.invoice_payment_fail_unknown)
9821093

983-
when (error.err) {
1094+
return when (error.err) {
9841095
RetryableSendFailure.LDKRetryableSendFailure_DuplicatePayment -> {
985-
handleReject(promise, LdkErrors.invoice_payment_fail_duplicate_payment)
1096+
Pair(null, LdkErrors.invoice_payment_fail_duplicate_payment)
9861097
}
987-
9881098
RetryableSendFailure.LDKRetryableSendFailure_PaymentExpired -> {
989-
handleReject(promise, LdkErrors.invoice_payment_fail_payment_expired)
1099+
Pair(null, LdkErrors.invoice_payment_fail_payment_expired)
9901100
}
991-
9921101
RetryableSendFailure.LDKRetryableSendFailure_RouteNotFound -> {
993-
handleReject(promise, LdkErrors.invoice_payment_fail_route_not_found)
1102+
Pair(null, LdkErrors.invoice_payment_fail_route_not_found)
9941103
}
995-
996-
else -> handleReject(promise, LdkErrors.invoice_payment_fail_unknown)
1104+
else -> Pair(null, LdkErrors.invoice_payment_fail_unknown)
9971105
}
9981106
}
9991107

@@ -1409,10 +1517,10 @@ class LdkModule(reactContext: ReactApplicationContext) : ReactContextBaseJavaMod
14091517

14101518
keysManager?.inner?.as_NodeSigner()
14111519
?.get_node_id(Recipient.LDKRecipient_Node)?.let { pubKeyRes ->
1412-
if (pubKeyRes.is_ok) {
1413-
logDump.add("NodeID: ${(pubKeyRes as Result_PublicKeyNoneZ_OK).res.hexEncodedString()}")
1520+
if (pubKeyRes.is_ok) {
1521+
logDump.add("NodeID: ${(pubKeyRes as Result_PublicKeyNoneZ_OK).res.hexEncodedString()}")
1522+
}
14141523
}
1415-
}
14161524

14171525
channelManager?.list_channels()?.forEach { channel ->
14181526
logDump.add("Open channel:")

0 commit comments

Comments
 (0)