@@ -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