Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
229 changes: 229 additions & 0 deletions android/app/src/main/java/com/blixtwallet/BlixtToolsTurboModule.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package com.blixtwallet

import com.facebook.react.bridge.Promise
import com.facebook.react.bridge.ReactApplicationContext
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import java.util.TimeZone

class BlixtToolsTurboModule(private val appContext: ReactApplicationContext) :
NativeBlixtToolsSpec(appContext) {
override fun getName() = NAME

@Volatile private var gossipSyncCancelled = false
@Volatile private var activeConnection: HttpURLConnection? = null
@Volatile private var gossipSyncInProgress = false
private val gossipSyncLock = Any()
private val logLock = Any()

override fun startSyncWorker() {}

override fun scheduleSyncWorker() {}

override fun stopScheduleSyncWorker() {}

override fun getStatus(): Double {
return 0.0
// val lnd = LndNative()
// return lnd.getStatus().toDouble()
}

override fun gossipSync(serviceUrl: String, promise: Promise) {
synchronized(gossipSyncLock) {
if (gossipSyncInProgress) {
promise.reject("GOSSIP_SYNC_IN_PROGRESS", "Gossip sync is already in progress")
return
}
gossipSyncInProgress = true
}
gossipSyncCancelled = false

Thread {
var tempGraphDbFile: File? = null
val lastRunFile = File(appContext.cacheDir, LAST_RUN_FILE_NAME)
try {
logLine("gossipSync started with serviceUrl=$serviceUrl")

if (shouldSkipGossipSync(lastRunFile)) {
logLine("gossipSync skipped due to 24h time constraint")
promise.resolve(null)
return@Thread
}

val graphDir = File(appContext.filesDir, "data/graph/${BuildConfig.CHAIN}")
if (!graphDir.exists() && !graphDir.mkdirs()) {
throw IOException("Failed to create graph directory: ${graphDir.absolutePath}")
}

val graphDbFile = File(graphDir, "graph.db")
val backupGraphDbFile = File(graphDir, "graph.db.bak")
tempGraphDbFile = File(graphDir, "graph.db.download")

if (tempGraphDbFile.exists() && !tempGraphDbFile.delete()) {
throw IOException("Failed to clear previous temporary graph.db file")
}

val graphDbUrl = resolveGraphDbUrl(serviceUrl)
logLine("Downloading graph database from $graphDbUrl")
val connection = (URL(graphDbUrl).openConnection() as HttpURLConnection).apply {
requestMethod = "GET"
connectTimeout = 15000
readTimeout = 120000
doInput = true
instanceFollowRedirects = true
}
activeConnection = connection

connection.connect()
val statusCode = connection.responseCode
if (statusCode !in 200..299) {
throw IOException("Failed to download graph.db, HTTP $statusCode")
}

connection.inputStream.use { input ->
FileOutputStream(tempGraphDbFile).use { output ->
val buffer = ByteArray(DEFAULT_BUFFER_SIZE)
while (true) {
ensureNotCancelled()
val read = input.read(buffer)
if (read == -1) {
break
}
output.write(buffer, 0, read)
}
output.fd.sync()
}
}
ensureNotCancelled()

if (!tempGraphDbFile.exists() || tempGraphDbFile.length() == 0L) {
throw IOException("Downloaded graph.db is empty")
}
logLine("Downloaded graph database (${tempGraphDbFile.length()} bytes)")

if (backupGraphDbFile.exists() && !backupGraphDbFile.delete()) {
throw IOException("Failed to remove previous graph.db backup")
}
if (graphDbFile.exists() && !graphDbFile.renameTo(backupGraphDbFile)) {
throw IOException("Failed to create graph.db backup")
}

if (!tempGraphDbFile.renameTo(graphDbFile)) {
if (backupGraphDbFile.exists()) {
backupGraphDbFile.renameTo(graphDbFile)
}
throw IOException("Failed to replace graph.db")
}

if (backupGraphDbFile.exists()) {
backupGraphDbFile.delete()
}
touchLastRun(lastRunFile)
logLine("gossipSync completed successfully")
promise.resolve(null)
} catch (e: Exception) {
if (tempGraphDbFile?.exists() == true) {
tempGraphDbFile.delete()
}
if (gossipSyncCancelled || e.message == CANCELLATION_MESSAGE) {
logLine("gossipSync cancelled by user")
promise.reject("GOSSIP_SYNC_CANCELLED", CANCELLATION_MESSAGE, e)
} else {
logLine("gossipSync failed: ${e.message ?: "unknown error"}")
promise.reject("GOSSIP_SYNC_FAILED", e.message ?: "Unknown gossip sync failure", e)
}
} finally {
activeConnection?.disconnect()
activeConnection = null
gossipSyncCancelled = false
synchronized(gossipSyncLock) {
gossipSyncInProgress = false
}
}
}.start()
}

override fun cancelGossipSync() {
gossipSyncCancelled = true
activeConnection?.disconnect()
}

private fun ensureNotCancelled() {
if (gossipSyncCancelled) {
throw IOException(CANCELLATION_MESSAGE)
}
}

private fun shouldSkipGossipSync(lastRunFile: File): Boolean {
if (!lastRunFile.exists()) {
lastRunFile.parentFile?.mkdirs()
if (!lastRunFile.createNewFile()) {
throw IOException("Failed to create last-run marker file")
}
return false
}

val elapsedMillis = System.currentTimeMillis() - lastRunFile.lastModified()
return elapsedMillis <= LAST_RUN_INTERVAL_MS
}

private fun touchLastRun(lastRunFile: File) {
if (!lastRunFile.exists()) {
lastRunFile.parentFile?.mkdirs()
if (!lastRunFile.createNewFile()) {
throw IOException("Failed to create last-run marker file")
}
}
if (!lastRunFile.setLastModified(System.currentTimeMillis())) {
throw IOException("Failed to update last-run marker file")
}
}

// Accept either a base service URL (append /<chain>/graph/graph-001d.db)
// or a direct .db URL passed from settings/debug tooling.
private fun resolveGraphDbUrl(serviceUrl: String): String {
val trimmedServiceUrl = serviceUrl.trim().trimEnd('/')
if (trimmedServiceUrl.substringBefore('?').endsWith(".db")) {
return trimmedServiceUrl
}
val networkType = BuildConfig.CHAIN.lowercase()
return "$trimmedServiceUrl/$networkType/graph/graph-001d.db"
}

private fun logLine(message: String) {
synchronized(logLock) {
try {
val logDir = File(appContext.cacheDir, LOG_DIR_NAME)
if (!logDir.exists()) {
logDir.mkdirs()
}
val logFile = File(logDir, LOG_FILE_NAME)
val timestamp =
SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX", Locale.US).apply {
timeZone = TimeZone.getTimeZone("UTC")
}.format(Date())
FileOutputStream(logFile, true).bufferedWriter(Charsets.UTF_8).use { writer ->
writer.append(timestamp)
writer.append(" ")
writer.append(message)
writer.newLine()
}
} catch (_: IOException) {}
}
}

companion object {
const val NAME = "BlixtTools"
private const val CANCELLATION_MESSAGE = "Gossip sync cancelled by user"
private const val LOG_DIR_NAME = "log"
private const val LOG_FILE_NAME = "speedloader.log"
private const val LAST_RUN_FILE_NAME = "lastrun"
private const val LAST_RUN_INTERVAL_MS = 24L * 60L * 60L * 1000L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,19 @@ import com.facebook.react.bridge.ReactApplicationContext
import com.facebook.react.module.model.ReactModuleInfo
import com.facebook.react.module.model.ReactModuleInfoProvider

class LndMobileToolsTurboPackage : BaseReactPackage() {
class BlixtToolsTurboPackage : BaseReactPackage() {
override fun getModule(name: String, reactContext: ReactApplicationContext): NativeModule? =
if (name == LndMobileToolsTurboModule.NAME) {
LndMobileToolsTurboModule(reactContext)
if (name == BlixtToolsTurboModule.NAME) {
BlixtToolsTurboModule(reactContext)
} else {
null
}

override fun getReactModuleInfoProvider() = ReactModuleInfoProvider {
mapOf(
LndMobileToolsTurboModule.NAME to ReactModuleInfo(
LndMobileToolsTurboModule.NAME,
LndMobileToolsTurboModule.NAME,
BlixtToolsTurboModule.NAME to ReactModuleInfo(
BlixtToolsTurboModule.NAME,
BlixtToolsTurboModule.NAME,
false, // canOverrideExistingModule
false, // needsEagerInit
false, // isCxxModule
Expand Down
40 changes: 0 additions & 40 deletions android/app/src/main/java/com/blixtwallet/LndMobile.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@
import java.io.OutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Date;
Expand Down Expand Up @@ -134,20 +133,6 @@ public void handleMessage(Message msg) {

break;
}
case LndMobileService.MSG_GOSSIP_SYNC_RESULT: {
final int request = msg.arg1;
final Promise promise = requests.remove(request);
if (bundle.containsKey("response")) {
final byte[] bytes = (byte[]) bundle.get("response");
promise.resolve("response=" + new String(bytes, StandardCharsets.UTF_8));
} else if (bundle.containsKey("error_code")) {
HyperLog.e(TAG, "ERROR" + msg);
promise.reject(bundle.getString("error_code"), bundle.getString("error_desc"));
} else {
promise.reject("noresponse");
}
break;
}
case LndMobileService.MSG_GRPC_STREAM_RESULT: {
// TODO EOF Stream error
final String method = (String) bundle.get("method");
Expand Down Expand Up @@ -400,31 +385,6 @@ public void stopLnd(Promise promise) {
}
}

@ReactMethod
public void gossipSync(String serviceUrl, String networkType, Promise promise) {
int req = new Random().nextInt();
requests.put(req, promise);

Message message = Message.obtain(null, LndMobileService.MSG_GOSSIP_SYNC, req, 0);
message.replyTo = messenger;
Bundle bundle = new Bundle();
bundle.putString(
"serviceUrl",
serviceUrl
);
bundle.putString(
"networkType",
networkType
);
message.setData(bundle);

try {
lndMobileServiceMessenger.send(message);
} catch (RemoteException e) {
promise.reject(TAG, "Could not Send MSG_GOSSIP_SYNC to LndMobileService", e);
}
}

@ReactMethod
public void sendCommand(String method, String payloadStr, final Promise promise) {
HyperLog.d(TAG, "sendCommand() " + method);
Expand Down
53 changes: 0 additions & 53 deletions android/app/src/main/java/com/blixtwallet/LndMobileService.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,8 +73,6 @@ public class LndMobileService extends Service {
static final int MSG_PONG = 20;
static final int MSG_GRPC_BIDI_STREAM_COMMAND = 21;
static final int MSG_GRPC_STREAM_WRITE_RESULT = 22;
static final int MSG_GOSSIP_SYNC = 23;
static final int MSG_GOSSIP_SYNC_RESULT = 24;

// private Map<String, Method> syncMethods = new HashMap<>();
// private Map<String, Method> streamMethods = new HashMap<>();
Expand Down Expand Up @@ -263,13 +261,6 @@ public void handleMessage(Message msg) {
stopLnd(msg.replyTo, request);
break;

case MSG_GOSSIP_SYNC:
HyperLog.i(TAG, "Got MSG_GOSSIP_SYNC");
final String serviceUrl = bundle.getString("serviceUrl", "");
final String networkType = bundle.getString("networkType", "");
gossipSync(msg.replyTo, serviceUrl, networkType, request);
break;

case MSG_PING:
HyperLog.d(TAG, "Got MSG_PING");
sendToClient(msg.replyTo, Message.obtain(null, MSG_PONG, request, 0));
Expand Down Expand Up @@ -415,50 +406,6 @@ public void handleMessage(Message msg) {
// }
// }

void gossipSync(Messenger recipient, String serviceUrl, String networkType, int request) {
HyperLog.i(TAG, "gossipSync()");
// Runnable gossipSync = new Runnable() {
// public void run() {
// Lndmobile.gossipSync(
// serviceUrl,
// getApplicationContext().getCacheDir().getAbsolutePath(),
// getApplicationContext().getFilesDir().getAbsolutePath(),
// networkType,
// new lndmobile.Callback() {

// @Override
// public void onError(Exception e) {
// HyperLog.e(TAG, "Could not invoke Lndmobile.gossipSync()", e);

// Message msg = Message.obtain(null, MSG_GOSSIP_SYNC_RESULT, request, 0);

// Bundle bundle = new Bundle();
// bundle.putString("error_code", "Gossip Error");
// bundle.putString("error_desc", e.toString());
// msg.setData(bundle);

// sendToClient(recipient, msg);
// // sendToClients(msg);
// }

// @Override
// public void onResponse(byte[] bytes) {
// Message msg = Message.obtain(null, MSG_GOSSIP_SYNC_RESULT, request, 0);

// Bundle bundle = new Bundle();
// bundle.putByteArray("response", bytes);
// msg.setData(bundle);

// sendToClient(recipient, msg);
// // sendToClients(msg);
// }
// });
// }
// };

// new Thread(gossipSync).start();
}

void startLnd(Messenger recipient, String args, int request) {
HyperLog.d(TAG, "startLnd(): Starting lnd");

Expand Down
Loading
Loading