From ce8b3d581008aca77ced9912352c22f5f096b630 Mon Sep 17 00:00:00 2001 From: Kevin Cai Date: Wed, 21 Jun 2023 13:05:41 -0400 Subject: [PATCH] Speedloader MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hampus Sjöberg --- android/app/build.gradle | 1 + .../blixtwallet/GossipFileScheduledSync.java | 81 +++++++++ .../GossipFileScheduledSyncPackage.java | 22 +++ .../GossipFileScheduledSyncWorker.java | 169 ++++++++++++++++++ .../main/java/com/blixtwallet/LndMobile.java | 36 ++++ .../com/blixtwallet/LndMobileService.java | 51 ++++++ .../java/com/blixtwallet/LndMobileTools.java | 17 ++ .../java/com/blixtwallet/MainApplication.java | 1 + contrib/log-scheduler | 2 +- ios/LndMobile/Lnd.swift | 4 + ios/LndMobile/LndMobile.m | 6 + ios/LndMobile/LndMobile.swift | 17 ++ ios/LndMobile/LndMobileTools.m | 10 ++ ios/LndMobile/LndMobileTools.swift | 44 ++++- locales/en.json | 10 ++ package.json | 1 + src/Main.tsx | 18 +- src/lndmobile/LndMobile.d.ts | 10 ++ src/lndmobile/index.ts | 7 + src/lndmobile/scheduled-gossip-sync.ts | 7 + src/migration/app-migration.ts | 8 + src/state/Lightning.ts | 1 + src/state/LndMobileInjection.ts | 18 +- src/state/ScheduledGossip.ts | 73 ++++++++ src/state/Settings.ts | 32 +++- src/state/index.ts | 63 ++++++- src/storage/app.ts | 9 + src/windows/InitProcess/DEV_Commands.tsx | 17 ++ src/windows/Settings/Settings.tsx | 90 +++++++++- yarn.lock | 5 + 30 files changed, 807 insertions(+), 23 deletions(-) create mode 100644 android/app/src/main/java/com/blixtwallet/GossipFileScheduledSync.java create mode 100644 android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncPackage.java create mode 100644 android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncWorker.java create mode 100644 src/lndmobile/scheduled-gossip-sync.ts create mode 100644 src/state/ScheduledGossip.ts diff --git a/android/app/build.gradle b/android/app/build.gradle index af5da31ed..406b4bf47 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -325,6 +325,7 @@ dependencies { implementation "net.freehaven.tor.control:jtorctl:0.2" // controlling tor instance via its control port implementation "com.jakewharton:process-phoenix:2.0.0" + implementation 'org.brotli:dec:0.1.2' } configurations { diff --git a/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSync.java b/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSync.java new file mode 100644 index 000000000..976cc1028 --- /dev/null +++ b/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSync.java @@ -0,0 +1,81 @@ +package com.blixtwallet; + +import androidx.work.Constraints; +import androidx.work.ExistingPeriodicWorkPolicy; +import androidx.work.PeriodicWorkRequest; +import androidx.work.NetworkType; +import androidx.work.WorkInfo; +import androidx.work.WorkManager; + +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.bridge.ReactContextBaseJavaModule; +import com.facebook.react.bridge.ReactMethod; +import com.facebook.react.bridge.Promise; + +import com.google.common.util.concurrent.ListenableFuture; +import java.util.List; +import java.util.concurrent.TimeUnit; + +import com.hypertrack.hyperlog.HyperLog; + +class GossipFileScheduledSync extends ReactContextBaseJavaModule { + private final String TAG = "GossipFileScheduledSync"; + private final String GOSSIP_FILE_SCHEDULED_SYNC_WORK_NAME = "GOSSIP_FILE_SCHEDULED_SYNC_WORK"; + private WorkManager workManager; + private PeriodicWorkRequest periodicWorkRequest; + + public GossipFileScheduledSync(ReactApplicationContext reactContext) { + super(reactContext); + + workManager = WorkManager.getInstance(getReactApplicationContext()); + periodicWorkRequest = BuildConfig.DEBUG + ? new PeriodicWorkRequest.Builder(GossipFileScheduledSyncWorker.class, 15, TimeUnit.MINUTES) + .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED).build()) + .build() + : new PeriodicWorkRequest.Builder(GossipFileScheduledSyncWorker.class, 1, TimeUnit.DAYS) + .setConstraints(new Constraints.Builder().setRequiredNetworkType(NetworkType.UNMETERED).build()) + .build(); + } + + @Override + public String getName() { + return "GossipFileScheduledSync"; + } + + @ReactMethod + public void setupScheduledSyncWork(Promise promise) { + workManager.enqueueUniquePeriodicWork(GOSSIP_FILE_SCHEDULED_SYNC_WORK_NAME, ExistingPeriodicWorkPolicy.REPLACE, periodicWorkRequest); + promise.resolve(true); + } + + @ReactMethod + public void removeScheduledSyncWork(Promise promise) { + workManager.cancelUniqueWork(GOSSIP_FILE_SCHEDULED_SYNC_WORK_NAME); + promise.resolve(true); + } + + @ReactMethod + public void checkScheduledSyncWorkStatus(Promise promise) { + try { + HyperLog.d(TAG, "Checking unique periodic work"); + + ListenableFuture> future = workManager.getWorkInfosForUniqueWork(GOSSIP_FILE_SCHEDULED_SYNC_WORK_NAME); + List workInfoList = future.get(); + if (workInfoList.size() == 0) { + promise.resolve("WORK_NOT_EXIST"); + } + else if (workInfoList.size() > 1) { + HyperLog.w(TAG, "Found more than 1 work"); + } + + for (WorkInfo workInfo : workInfoList) { + WorkInfo.State state = workInfo.getState(); + promise.resolve(state.toString()); + return; + } + } catch (Throwable e) { + HyperLog.e(TAG, "Could not create periodic work", e); + promise.reject("Could not create periodic work", e); + } + } +} diff --git a/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncPackage.java b/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncPackage.java new file mode 100644 index 000000000..272e8329c --- /dev/null +++ b/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncPackage.java @@ -0,0 +1,22 @@ +package com.blixtwallet; + +import com.facebook.react.ReactPackage; +import com.facebook.react.bridge.NativeModule; +import com.facebook.react.bridge.ReactApplicationContext; +import com.facebook.react.uimanager.ViewManager; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +public class GossipFileScheduledSyncPackage implements ReactPackage { + @Override + public List createViewManagers(ReactApplicationContext reactContext) { + return Collections.emptyList(); + } + + @Override + public List createNativeModules(ReactApplicationContext reactContext) { + return Arrays.asList(new GossipFileScheduledSync(reactContext)); + } +} diff --git a/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncWorker.java b/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncWorker.java new file mode 100644 index 000000000..a88569e0e --- /dev/null +++ b/android/app/src/main/java/com/blixtwallet/GossipFileScheduledSyncWorker.java @@ -0,0 +1,169 @@ +package com.blixtwallet; + +import android.annotation.SuppressLint; +import android.content.Context; +import android.content.Intent; +import android.os.Bundle; +import android.os.Handler; +import android.os.IBinder; +import android.os.Message; +import android.os.Process; +import android.os.RemoteException; +import android.util.Log; +import android.database.sqlite.SQLiteDatabase; +import android.database.sqlite.SQLiteStatement; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.concurrent.futures.CallbackToFutureAdapter; +import com.google.common.util.concurrent.ListenableFuture; +import androidx.work.ListenableWorker; +import androidx.work.WorkerParameters; + +import com.facebook.react.modules.storage.ReactDatabaseSupplier; +import com.facebook.react.modules.storage.AsyncLocalStorageUtil; +import com.facebook.react.bridge.Arguments; +import com.facebook.react.bridge.WritableMap; +import com.facebook.react.bridge.ReadableMap; + +import com.oblador.keychain.KeychainModule; +import com.google.protobuf.ByteString; +import com.hypertrack.hyperlog.HyperLog; + +import org.brotli.dec.BrotliInputStream; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.net.URL; +import java.net.HttpURLConnection; +import java.util.zip.GZIPInputStream; + +public class GossipFileScheduledSyncWorker extends ListenableWorker { + private final String TAG = "GossipFileScheduledSyncWorker"; + private ReactDatabaseSupplier dbSupplier; + private boolean persistentServicesEnabled = false; + + public GossipFileScheduledSyncWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) { + super(context, workerParams); + dbSupplier = ReactDatabaseSupplier.getInstance(getApplicationContext()); + } + + @Override + public ListenableFuture startWork() { + persistentServicesEnabled = getPersistentServicesEnabled(); + + return CallbackToFutureAdapter.getFuture(completer -> { + HyperLog.i(TAG, "------------------------------------"); + HyperLog.i(TAG, "Starting scheduled sync work"); + HyperLog.i(TAG, "I am " + getApplicationContext().getPackageName()); + writeLastScheduledSyncAttemptToDb(); + + if (persistentServicesEnabled) { + HyperLog.i(TAG, "persistentServicesEnabled = " + persistentServicesEnabled + ", quitting job"); + completer.set(Result.success()); + return null; + } + HyperLog.i(TAG, "Starting gossip file download"); + startGossipWorkThread(completer); + return null; + }); + } + + private void startGossipWorkThread(CallbackToFutureAdapter.Completer completer) { + Thread thread = new Thread(new Runnable() { + @Override + public void run() { + HyperLog.i(TAG, "Handling periodic gossip file download"); + try { + URL url = new URL("https://maps.eldamar.icu/mainnet/graph/graph-001d.db"); + File dgraph = new File(getApplicationContext().getCacheDir().getAbsolutePath() + "/dgraph"); + dgraph.mkdirs(); + FileOutputStream out = new FileOutputStream(new File(getApplicationContext().getCacheDir().getAbsolutePath() + "/dgraph/channel.db")); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setRequestProperty("Accept-Encoding", "br, gzip"); + InputStream stream = null; + if ("gzip".equals(con.getContentEncoding())) { + stream = new GZIPInputStream(con.getInputStream()); + } else if ("br".equals(con.getContentEncoding())) { + stream = new BrotliInputStream(con.getInputStream()); + } else { + stream = con.getInputStream(); + } + out.write(stream.readAllBytes()); + out.close(); + stream.close(); + } catch (Throwable e) { + Log.e(TAG, e.getMessage()); + HyperLog.e(TAG, e.getMessage()); + completer.set(Result.failure()); + return; + } + HyperLog.i(TAG, "Periodic gossip file download finished"); + completer.set(Result.success()); + writeLastScheduledSyncToDb(); + } + }); + thread.start(); + } + + private boolean getPersistentServicesEnabled() { + SQLiteDatabase db = dbSupplier.get(); + String persistentServicesEnabled = AsyncLocalStorageUtil.getItemImpl(db, "persistentServicesEnabled"); + if (persistentServicesEnabled != null) { + return persistentServicesEnabled.equals("true"); + } + HyperLog.w(TAG, "Could not find persistentServicesEnabled in asyncStorage"); + return false; + } + + private void writeLastScheduledSyncAttemptToDb() { + SQLiteDatabase db = dbSupplier.get(); + String key = "lastScheduledGossipSyncAttempt"; + Long tsLong = System.currentTimeMillis() / 1000; + String value = tsLong.toString(); + String sql = "INSERT OR REPLACE INTO catalystLocalStorage VALUES (?, ?);"; + SQLiteStatement statement = db.compileStatement(sql); + try { + db.beginTransaction(); + statement.clearBindings(); + statement.bindString(1, key); + statement.bindString(2, value); + statement.execute(); + db.setTransactionSuccessful(); + } catch (Exception e) { + HyperLog.w(TAG, e.getMessage(), e); + } finally { + try { + db.endTransaction(); + } catch (Exception e) { + HyperLog.w(TAG, e.getMessage(), e); + } + } + } + + private void writeLastScheduledSyncToDb() { + SQLiteDatabase db = dbSupplier.get(); + String key = "lastScheduledGossipSync"; + Long tsLong = System.currentTimeMillis() / 1000; + String value = tsLong.toString(); + String sql = "INSERT OR REPLACE INTO catalystLocalStorage VALUES (?, ?);"; + SQLiteStatement statement = db.compileStatement(sql); + try { + db.beginTransaction(); + statement.clearBindings(); + statement.bindString(1, key); + statement.bindString(2, value); + statement.execute(); + db.setTransactionSuccessful(); + } catch (Exception e) { + HyperLog.w(TAG, e.getMessage(), e); + } finally { + try { + db.endTransaction(); + } catch (Exception e) { + HyperLog.w(TAG, e.getMessage(), e); + } + } + } +} diff --git a/android/app/src/main/java/com/blixtwallet/LndMobile.java b/android/app/src/main/java/com/blixtwallet/LndMobile.java index 2b6bb1215..670eb857f 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobile.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobile.java @@ -44,6 +44,7 @@ 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; @@ -138,6 +139,20 @@ 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"); @@ -377,6 +392,27 @@ public void stopLnd(Promise promise) { } } + @ReactMethod + public void gossipSync(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( + "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); diff --git a/android/app/src/main/java/com/blixtwallet/LndMobileService.java b/android/app/src/main/java/com/blixtwallet/LndMobileService.java index 30303fe0c..3bdf9c9f3 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobileService.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobileService.java @@ -63,6 +63,8 @@ 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 syncMethods = new HashMap<>(); private Map streamMethods = new HashMap<>(); @@ -260,6 +262,12 @@ public void handleMessage(Message msg) { stopLnd(msg.replyTo, request); break; + case MSG_GOSSIP_SYNC: + HyperLog.i(TAG, "Got MSG_GOSSIP_SYNC"); + final String networkType = bundle.getString("networkType", ""); + gossipSync(msg.replyTo, networkType, request); + break; + case MSG_PING: HyperLog.d(TAG, "Got MSG_PING"); sendToClient(msg.replyTo, Message.obtain(null, MSG_PONG, request, 0)); @@ -405,6 +413,49 @@ public void onResponse(byte[] bytes) { } } + void gossipSync(Messenger recipient, String networkType, int request) { + HyperLog.i(TAG, "gossipSync()"); + Runnable gossipSync = new Runnable() { + public void run() { + Lndmobile.gossipSync( + 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"); Runnable startLnd = new Runnable() { diff --git a/android/app/src/main/java/com/blixtwallet/LndMobileTools.java b/android/app/src/main/java/com/blixtwallet/LndMobileTools.java index b2ea6f581..07155aea6 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobileTools.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobileTools.java @@ -570,6 +570,23 @@ void deleteRecursive(File fileOrDirectory) { HyperLog.d(TAG, "Delete file " + fileOrDirectory.getName() + " : " + fileOrDirectory.delete()); } + @ReactMethod + public void DEBUG_deleteSpeedloaderLastrunFile(Promise promise) { + HyperLog.i(TAG, "DEBUG cache lastrun"); + String filename = getReactApplicationContext().getCacheDir().toString() + "/lastrun"; + File file = new File(filename); + promise.resolve(file.delete()); + } + + @ReactMethod + public void DEBUG_deleteSpeedloaderDgraphDirectory(Promise promise) { + HyperLog.i(TAG, "DEBUG cache lastrun"); + String filename = getReactApplicationContext().getCacheDir().toString() + "/dgraph"; + File file = new File(filename); + deleteRecursive(file); + promise.resolve(null); + } + @ReactMethod public void DEBUG_listProcesses(Promise promise) { String processes = ""; diff --git a/android/app/src/main/java/com/blixtwallet/MainApplication.java b/android/app/src/main/java/com/blixtwallet/MainApplication.java index c83ded655..5f1938df3 100644 --- a/android/app/src/main/java/com/blixtwallet/MainApplication.java +++ b/android/app/src/main/java/com/blixtwallet/MainApplication.java @@ -30,6 +30,7 @@ protected List getPackages() { List packages = new PackageList(this).getPackages(); packages.add(new LndMobilePackage()); packages.add(new LndMobileToolsPackage()); + packages.add(new GossipFileScheduledSyncPackage()); packages.add(new LndMobileScheduledSyncPackage()); packages.add(new BlixtTorPackage()); packages.add(new RealTimeBlurPackage()); diff --git a/contrib/log-scheduler b/contrib/log-scheduler index e8bb54f4b..5b10d5304 100755 --- a/contrib/log-scheduler +++ b/contrib/log-scheduler @@ -1 +1 @@ -adb logcat LndMobile:V LndMobileService:V LndMobileScheduledSync:V LndScheduledSyncWorker:V BlixtTor:V WM-WorkerWrapper:V *:S +adb logcat LndMobile:V LndMobileService:V LndMobileScheduledSync:V LndScheduledSyncWorker:V GossipFileScheduledSyncWorker:V BlixtTor:V WM-WorkerWrapper:V *:S diff --git a/ios/LndMobile/Lnd.swift b/ios/LndMobile/Lnd.swift index 08b0919c4..eea650eeb 100644 --- a/ios/LndMobile/Lnd.swift +++ b/ios/LndMobile/Lnd.swift @@ -292,4 +292,8 @@ open class Lnd { callback(nil, error) } } + + func gossipSync(_ cacheDir: String, dataDir: String, networkType: String, callback: @escaping Callback) { + LndmobileGossipSync(cacheDir, dataDir, networkType, LndmobileCallback(method: "blixt_gossipSync", callback: callback)) + } } diff --git a/ios/LndMobile/LndMobile.m b/ios/LndMobile/LndMobile.m index d47a34719..34444c7e5 100644 --- a/ios/LndMobile/LndMobile.m +++ b/ios/LndMobile/LndMobile.m @@ -69,4 +69,10 @@ @interface RCT_EXTERN_MODULE(LndMobile, RCTEventEmitter) rejecter: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + gossipSync: (NSString *)networkType + resolver: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject +) + @end diff --git a/ios/LndMobile/LndMobile.swift b/ios/LndMobile/LndMobile.swift index b6fa532c3..d86f5a6c8 100644 --- a/ios/LndMobile/LndMobile.swift +++ b/ios/LndMobile/LndMobile.swift @@ -273,4 +273,21 @@ class LndMobile: RCTEventEmitter { } } } + + @objc(gossipSync:resolver:rejecter:) + func gossipSync(networkType: String, resolver resolve: @escaping RCTPromiseResolveBlock, rejecter reject: @escaping RCTPromiseRejectBlock) { + let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0] + let lndPath = applicationSupport.appendingPathComponent("lnd", isDirectory: true) + let cachePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + + Lnd.shared.gossipSync(cachePath.path, dataDir: lndPath.path, networkType: networkType, callback: { (data, error) in + if let e = error { + reject("error", e.localizedDescription, e) + return + } + resolve([ + "data": data?.base64EncodedString() + ]) + }) + } } diff --git a/ios/LndMobile/LndMobileTools.m b/ios/LndMobile/LndMobileTools.m index 2cf1b540b..2215d9066 100644 --- a/ios/LndMobile/LndMobileTools.m +++ b/ios/LndMobile/LndMobileTools.m @@ -57,6 +57,16 @@ @interface RCT_EXTERN_MODULE(LndMobileTools, RCTEventEmitter) rejecter: (RCTPromiseRejectBlock)reject ) +RCT_EXTERN_METHOD( + DEBUG_deleteSpeedloaderLastrunFile: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject +) + +RCT_EXTERN_METHOD( + DEBUG_deleteSpeedloaderDgraphDirectory: (RCTPromiseResolveBlock)resolve + rejecter: (RCTPromiseRejectBlock)reject +) + RCT_EXTERN_METHOD( checkApplicationSupportExists: (RCTPromiseResolveBlock)resolve rejecter: (RCTPromiseRejectBlock)reject diff --git a/ios/LndMobile/LndMobileTools.swift b/ios/LndMobile/LndMobileTools.swift index 5c00515bc..90facc107 100644 --- a/ios/LndMobile/LndMobileTools.swift +++ b/ios/LndMobile/LndMobileTools.swift @@ -172,13 +172,13 @@ autopilot.heuristic=preferential:0.05 func DEBUG_getWalletPasswordFromKeychain(resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) { let server = "password" - let query = [ - kSecClass as String: kSecClassInternetPassword, - kSecAttrServer as String: server, - kSecReturnAttributes as String: kCFBooleanTrue!, - kSecReturnData as String: kCFBooleanTrue!, - kSecMatchLimit as String: kSecMatchLimitOne as String - ] as CFDictionary + let query: CFDictionary = [ + kSecClass: kSecClassInternetPassword, + kSecAttrServer: server, + kSecReturnAttributes: kCFBooleanTrue!, + kSecReturnData: kCFBooleanTrue!, + kSecMatchLimit: kSecMatchLimitOne as String + ] as [CFString: Any] as CFDictionary var result: AnyObject? let osStatus = SecItemCopyMatching(query, &result) @@ -343,6 +343,36 @@ autopilot.heuristic=preferential:0.05 } } + @objc(DEBUG_deleteSpeedloaderLastrunFile:rejecter:) + func DEBUG_deleteSpeedloaderLastrunFile(resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) { + let cachePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let lastrunPath = cachePath.appendingPathComponent("lastrun") + + do { + try FileManager.default.removeItem(at: lastrunPath) + } catch { + reject("error", error.localizedDescription, error) + return + } + + resolve(true) + } + + @objc(DEBUG_deleteSpeedloaderDgraphDirectory:rejecter:) + func DEBUG_deleteSpeedloaderDgraphDirectory(resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) { + let cachePath = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0] + let dgraphPath = cachePath.appendingPathComponent("dgraph", isDirectory: true) + + do { + try FileManager.default.removeItem(at: dgraphPath) + } catch { + reject("error", error.localizedDescription, error) + return + } + + resolve(nil) + } + @objc(checkApplicationSupportExists:rejecter:) func checkApplicationSupportExists(resolver resolve: RCTPromiseResolveBlock, rejecter reject: RCTPromiseRejectBlock) { let applicationSupport = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask).first! diff --git a/locales/en.json b/locales/en.json index d110879f0..08876a251 100644 --- a/locales/en.json +++ b/locales/en.json @@ -686,6 +686,13 @@ "title": "Not recommended", "msg": "Warning. It is not recommended to disable scheduled chain sync.\n\nMake sure you keep up-to-date with the network otherwise you risk losing your funds.\n\nOnly do this if you're know what you're doing." } + }, + "gossipSync": { + "title": "Sync LN channels on startup" + }, + "gossipSyncAndroid": { + "title": "Scheduled LN channel sync", + "subtitle": "Runs in background every {{hours}} hours" } }, "display": { @@ -970,6 +977,9 @@ }, "compactLndDatabases": { "title": "Compact lnd's databases" + }, + "enforceSpeedloaderOnStartup": { + "title": "Enforce gossip sync on startup" } } }, diff --git a/package.json b/package.json index f2a0d1b72..89c29e6d1 100755 --- a/package.json +++ b/package.json @@ -59,6 +59,7 @@ "@react-native-community/clipboard": "1.5.1", "@react-native-community/geolocation": "^3.0.5", "@react-native-community/masked-view": "^0.1.11", + "@react-native-community/netinfo": "^9.3.10", "@react-native-community/push-notification-ios": "^1.11.0", "@react-native-community/slider": "^4.4.2", "@react-native-google-signin/google-signin": "^9.0.2", diff --git a/src/Main.tsx b/src/Main.tsx index d786f4e9d..0c3c2be79 100644 --- a/src/Main.tsx +++ b/src/Main.tsx @@ -1,6 +1,6 @@ import React, { useEffect, useState } from "react"; import { StatusBar, Alert, NativeModules } from "react-native"; -import { Spinner, H1 } from "native-base"; +import { Spinner, H1, H2 } from "native-base"; import { createStackNavigator, CardStyleInterpolators, StackNavigationOptions } from "@react-navigation/stack"; import Overview from "./windows/Overview"; @@ -82,6 +82,7 @@ export default function Main() { const initializeApp = useStoreActions((store) => store.initializeApp); const [initialRoute, setInitialRoute] = useState("Loading"); const torLoading = useStoreState((store) => store.torLoading); + const speedloaderLoading = useStoreState((store) => store.speedloaderLoading); const [state, setState] = useState<"init" | "authentication" | "onboarding" | "started">("init"); @@ -185,6 +186,21 @@ export default function Main() { ); } + if (speedloaderLoading) { + return ( + + + ); + } return ( <> ; // TODO(hsjoberg): function looks broken sendPongToLndMobileservice(): Promise<{ data: string }>; checkLndMobileServiceConnected(): Promise; + gossipSync(networkType: string): Promise<{ data: string }>; } export interface ILndMobileTools { @@ -39,6 +40,8 @@ export interface ILndMobileTools { saveChannelBackupFile(): Promise; DEBUG_getWalletPasswordFromKeychain(): Promise; getTorEnabled(): Promise; + DEBUG_deleteSpeedloaderLastrunFile(): boolean; + DEBUG_deleteSpeedloaderDgraphDirectory(): null; // Android-specific getIntentStringData(): Promise; @@ -70,10 +73,17 @@ export interface ILndMobileScheduledSync { checkScheduledSyncWorkStatus: () => Promise; } +export interface IGossipFileScheduledSync { + setupScheduledSyncWork: () => Promise; + removeScheduledSyncWork: () => Promise; + checkScheduledSyncWorkStatus: () => Promise; +} + declare module "react-native" { interface NativeModulesStatic { LndMobile: ILndMobile; LndMobileTools: ILndMobileTools; LndMobileScheduledSync: ILndMobileScheduledSync; + GossipFileScheduledSync: IGossipFileScheduledSync; } } diff --git a/src/lndmobile/index.ts b/src/lndmobile/index.ts index 91bba1790..41c9fb7c6 100644 --- a/src/lndmobile/index.ts +++ b/src/lndmobile/index.ts @@ -66,6 +66,13 @@ export const startLnd = async (torEnabled: boolean, args?: string): Promise<{dat return await LndMobile.startLnd(torEnabled, args); }; +/** + * @throws + */ +export const gossipSync = async (networkType: string): Promise<{ data: string } > => { + return await LndMobile.gossipSync(networkType); +} + export const checkICloudEnabled = async (): Promise => { return await LndMobileTools.checkICloudEnabled(); }; diff --git a/src/lndmobile/scheduled-gossip-sync.ts b/src/lndmobile/scheduled-gossip-sync.ts new file mode 100644 index 000000000..f4d6deb4a --- /dev/null +++ b/src/lndmobile/scheduled-gossip-sync.ts @@ -0,0 +1,7 @@ +import { NativeModules } from "react-native"; +import { WorkInfo } from "./LndMobile"; +const { GossipFileScheduledSync } = NativeModules; + +export const checkScheduledGossipSyncWorkStatus = async (): Promise => { + return await GossipFileScheduledSync.checkScheduledSyncWorkStatus(); +}; \ No newline at end of file diff --git a/src/migration/app-migration.ts b/src/migration/app-migration.ts index 6067a41fc..3ca4b335d 100644 --- a/src/migration/app-migration.ts +++ b/src/migration/app-migration.ts @@ -292,4 +292,12 @@ export const appMigration: IAppMigration[] = [ await db.executeSql("ALTER TABLE contact ADD label TEXT NULL"); }, }, + // Version 35 + { + async beforeLnd(db, i) { + setItemObject(StorageItem.scheduledGossipSyncEnabled, false); + await setItemObject(StorageItem.lastScheduledGossipSync, 0); + await setItemObject(StorageItem.lastScheduledGossipSyncAttempt, 0); + } + } ]; diff --git a/src/state/Lightning.ts b/src/state/Lightning.ts index 578a5cb53..e0ff30316 100644 --- a/src/state/Lightning.ts +++ b/src/state/Lightning.ts @@ -159,6 +159,7 @@ export const lightning: ILightningModel = { dispatch.onChain.initialize(), dispatch.transaction.checkOpenTransactions(), dispatch.scheduledSync.initialize(), + dispatch.scheduledGossipSync.initialize(), dispatch.notificationManager.initialize(), dispatch.clipboardManager.initialize(), dispatch.deeplinkManager.initialize(), diff --git a/src/state/LndMobileInjection.ts b/src/state/LndMobileInjection.ts index 478de07a2..8f9ca15db 100644 --- a/src/state/LndMobileInjection.ts +++ b/src/state/LndMobileInjection.ts @@ -1,6 +1,7 @@ import { IAddInvoiceBlixtLspArgs, IReadLndLogResponse, + gossipSync, TEMP_moveLndToApplicationSupport, addInvoice, addInvoiceBlixtLsp, @@ -49,7 +50,6 @@ import { pendingChannels, subscribeChannelEvents, } from "../lndmobile/channel"; -import { autopilotrpc, invoicesrpc, lnrpc, routerrpc, signrpc } from "../../proto/lightning"; import { decodeInvoiceResult, deriveKey, @@ -70,10 +70,11 @@ import { subscribeTransactions, walletBalance, } from "../lndmobile/onchain"; -import { modifyStatus, queryScores, setScores, status } from "../lndmobile/autopilot"; - -import { WorkInfo } from "../lndmobile/LndMobile"; +import { status, modifyStatus, queryScores, setScores } from "../lndmobile/autopilot"; import { checkScheduledSyncWorkStatus } from "../lndmobile/scheduled-sync"; // TODO(hsjoberg): This could be its own injection "LndMobileScheduledSync" +import { checkScheduledGossipSyncWorkStatus } from "../lndmobile/scheduled-gossip-sync"; +import { lnrpc, signrpc, invoicesrpc, autopilotrpc, routerrpc } from "../../proto/lightning"; +import { WorkInfo } from "../lndmobile/LndMobile"; export interface ILndMobileInjections { index: { @@ -84,13 +85,13 @@ export interface ILndMobileInjections { decodeState: (data: string) => lnrpc.SubscribeStateResponse; checkStatus: () => Promise; startLnd: (torEnabled: boolean, args: string) => Promise; + gossipSync: (networkType: string) => Promise<{ data: string }>; checkICloudEnabled: () => Promise; checkApplicationSupportExists: () => Promise; checkLndFolderExists: () => Promise; createIOSApplicationSupportAndLndDirectories: () => Promise; TEMP_moveLndToApplicationSupport: () => Promise; excludeLndICloudBackup: () => Promise; - addInvoice: ( amount: number, memo: string, @@ -203,6 +204,9 @@ export interface ILndMobileInjections { scheduledSync: { checkScheduledSyncWorkStatus: () => Promise; }; + scheduledGossipSync: { + checkScheduledGossipSyncWorkStatus: () => Promise; + }; } export default { @@ -214,6 +218,7 @@ export default { subscribeState, decodeState, startLnd, + gossipSync, checkICloudEnabled, checkApplicationSupportExists, checkLndFolderExists, @@ -285,4 +290,7 @@ export default { scheduledSync: { checkScheduledSyncWorkStatus, }, + scheduledGossipSync: { + checkScheduledGossipSyncWorkStatus, + }, } as unknown as ILndMobileInjections; diff --git a/src/state/ScheduledGossip.ts b/src/state/ScheduledGossip.ts new file mode 100644 index 000000000..20f3f59e2 --- /dev/null +++ b/src/state/ScheduledGossip.ts @@ -0,0 +1,73 @@ +import { NativeModules } from "react-native" +import { Action, action, Thunk, thunk, computed, Computed } from "easy-peasy"; +import { StorageItem, getItemObject } from "../storage/app"; +import { WorkInfo } from "../lndmobile/LndMobile"; +import { IStoreInjections } from "./store"; +import { PLATFORM } from "../utils/constants"; + +import logger from "./../utils/log"; +const log = logger("ScheduledSync"); + +const { GossipFileScheduledSync } = NativeModules; + +export interface IScheduledGossipSyncModel { + initialize: Thunk; + + retrieveSyncInfo: Thunk; + setSyncEnabled: Thunk; + + setLastScheduledSync: Action; + setLastScheduledSyncAttempt: Action; + setWorkInfo: Action; + + syncEnabled: Computed; + lastScheduledSync: number; + lastScheduledSyncAttempt: number; + workInfo: WorkInfo | null; +} + +export const scheduledGossipSync: IScheduledGossipSyncModel = { + initialize: thunk(async (actions) => { + if (PLATFORM !== "android") { + log.i("initialize(): Platform does not support scheduled gossip sync yet"); + return; + } + await actions.retrieveSyncInfo(); + }), + + retrieveSyncInfo: thunk(async (actions, _, { injections }) => { + if (PLATFORM !== "android") { + log.w("retrieveSyncInfo(): Platform does not support scheduled gossip sync yet"); + return; + } + + try { + actions.setLastScheduledSync(await getItemObject(StorageItem.lastScheduledGossipSync)); + actions.setLastScheduledSyncAttempt(await getItemObject(StorageItem.lastScheduledGossipSyncAttempt)); + actions.setWorkInfo(await injections.lndMobile.scheduledGossipSync.checkScheduledGossipSyncWorkStatus()); + } catch (e) { + log.e("Error retrieving gossip file sync info", [e]); + } + }), + + setSyncEnabled: thunk(async (actions, enabled) => { + if (PLATFORM !== "android") { + log.w("setSyncEnabled(): Platform does not support scheduled sync yet"); + return; + } + + enabled + ? await GossipFileScheduledSync.setupScheduledSyncWork() + : await GossipFileScheduledSync.removeScheduledSyncWork(); + actions.setWorkInfo(await GossipFileScheduledSync.checkScheduledSyncWorkStatus()); + }), + + setLastScheduledSync: action((state, payload) => { state.lastScheduledSync = payload; }), + setLastScheduledSyncAttempt: action((state, payload) => { state.lastScheduledSyncAttempt = payload; }), + setWorkInfo: action((state, payload) => { state.workInfo = payload; }), + + syncEnabled: computed((store) => ["BLOCKED", "ENQUEUED", "FAILED", "RUNNING", "SUCCEEDED"].includes(store.workInfo!)), + lastScheduledSync: 0, + lastScheduledSyncAttempt: 0, + workInfo: "WORK_NOT_EXIST", +}; diff --git a/src/state/Settings.ts b/src/state/Settings.ts index 916716fe1..5b8035b82 100644 --- a/src/state/Settings.ts +++ b/src/state/Settings.ts @@ -50,6 +50,7 @@ export interface ISettingsModel { changePushNotificationsEnabled: Thunk; changeClipboardInvoiceCheckEnabled: Thunk; changeScheduledSyncEnabled: Thunk; + changeScheduledGossipSyncEnabled: Thunk; changeDebugShowStartupInfo: Thunk; changeGoogleDriveBackupEnabled: Thunk; changePreferFiat: Thunk; @@ -79,6 +80,7 @@ export interface ISettingsModel { changeMaxLNFeePercentage: Thunk; changeLndLogLevel: Thunk; changeLndCompactDb: Thunk; + changeEnforceSpeedloaderOnStartup: Thunk; setBitcoinUnit: Action; setFiatUnit: Action; @@ -88,6 +90,7 @@ export interface ISettingsModel { setPushNotificationsEnabled: Action; setClipboardInvoiceCheckInvoicesEnabled: Action; setScheduledSyncEnabled: Action; + setScheduledGossipSyncEnabled: Action; setDebugShowStartupInfo: Action; setGoogleDriveBackupEnabled: Action; setPreferFiat: Action; @@ -117,6 +120,7 @@ export interface ISettingsModel { setMaxLNFeePercentage: Action; setLndLogLevel: Action; setLndCompactDb: Action; + setEnforceSpeedloaderOnStartup: Action; bitcoinUnit: keyof IBitcoinUnits; fiatUnit: keyof IFiatRates; @@ -126,6 +130,7 @@ export interface ISettingsModel { pushNotificationsEnabled: boolean; clipboardInvoiceCheckEnabled: boolean; scheduledSyncEnabled: boolean; + scheduledGossipSyncEnabled: boolean; debugShowStartupInfo: boolean; googleDriveBackupEnabled: boolean; preferFiat: boolean; @@ -155,6 +160,7 @@ export interface ISettingsModel { lndLogLevel: LndLogLevel; lndCompactDb: boolean; zeroConfPeers: string[]; + enforceSpeedloaderOnStartup: boolean; } export const settings: ISettingsModel = { @@ -174,6 +180,9 @@ export const settings: ISettingsModel = { actions.setScheduledSyncEnabled( (await getItemObject(StorageItem.scheduledSyncEnabled)) || false, ); + actions.setScheduledGossipSyncEnabled( + (await getItemObject(StorageItem.scheduledGossipSyncEnabled)) || false, + ); actions.setDebugShowStartupInfo( (await getItemObject(StorageItem.debugShowStartupInfo)) || false, ); @@ -223,6 +232,9 @@ export const settings: ISettingsModel = { actions.setMaxLNFeePercentage((await getItemObject(StorageItem.maxLNFeePercentage)) ?? 2); actions.setLndLogLevel(((await getItem(StorageItem.lndLogLevel)) ?? "info") as LndLogLevel); actions.setLndCompactDb(await getLndCompactDb()); + actions.setEnforceSpeedloaderOnStartup( + await getItemObject(StorageItem.enforceSpeedloaderOnStartup || false), + ); log.d("Done"); }), @@ -271,6 +283,11 @@ export const settings: ISettingsModel = { actions.setScheduledSyncEnabled(payload); }), + changeScheduledGossipSyncEnabled: thunk(async (actions, payload) => { + await setItemObject(StorageItem.scheduledGossipSyncEnabled, payload); + actions.setScheduledGossipSyncEnabled(payload); + }), + changeDebugShowStartupInfo: thunk(async (actions, payload) => { await setItemObject(StorageItem.debugShowStartupInfo, payload); actions.setDebugShowStartupInfo(payload); @@ -417,6 +434,11 @@ export const settings: ISettingsModel = { actions.setLndCompactDb(payload); }), + changeEnforceSpeedloaderOnStartup: thunk(async (actions, payload) => { + await setItemObject(StorageItem.enforceSpeedloaderOnStartup, payload); + actions.setEnforceSpeedloaderOnStartup(payload); + }), + setBitcoinUnit: action((state, payload) => { state.bitcoinUnit = payload; }), @@ -441,6 +463,9 @@ export const settings: ISettingsModel = { setScheduledSyncEnabled: action((state, payload) => { state.scheduledSyncEnabled = payload; }), + setScheduledGossipSyncEnabled: action((state, payload) => { + state.scheduledGossipSyncEnabled = payload; + }), setDebugShowStartupInfo: action((state, payload) => { state.debugShowStartupInfo = payload; }), @@ -528,6 +553,9 @@ export const settings: ISettingsModel = { setLndCompactDb: action((state, payload) => { state.lndCompactDb = payload; }), + setEnforceSpeedloaderOnStartup: action((state, payload) => { + state.enforceSpeedloaderOnStartup = payload; + }), bitcoinUnit: "bitcoin", fiatUnit: "USD", @@ -537,6 +565,7 @@ export const settings: ISettingsModel = { pushNotificationsEnabled: false, clipboardInvoiceCheckEnabled: false, scheduledSyncEnabled: false, + scheduledGossipSyncEnabled: false, debugShowStartupInfo: false, googleDriveBackupEnabled: false, preferFiat: false, @@ -562,8 +591,9 @@ export const settings: ISettingsModel = { rescanWallet: false, receiveViaP2TR: false, strictGraphPruningEnabled: false, + lndPathfindingAlgorithm: DEFAULT_PATHFINDING_ALGORITHM, maxLNFeePercentage: DEFAULT_MAX_LN_FEE_PERCENTAGE, lndLogLevel: DEFAULT_LND_LOG_LEVEL, - lndPathfindingAlgorithm: DEFAULT_PATHFINDING_ALGORITHM, lndCompactDb: false, + enforceSpeedloaderOnStartup: false, }; diff --git a/src/state/index.ts b/src/state/index.ts index f2107c6a4..4dd3ffdf2 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -1,9 +1,12 @@ import * as base64 from "base64-js"; +import Tor from "react-native-tor"; +import NetInfo from "@react-native-community/netinfo"; import { Action, Thunk, action, thunk } from "easy-peasy"; import { AlertButton, NativeModules } from "react-native"; -import { Chain, VersionCode } from "../utils/build"; + import { DEFAULT_PATHFINDING_ALGORITHM, PLATFORM } from "../utils/constants"; +import { Chain, VersionCode } from "../utils/build"; import { IBlixtLsp, blixtLsp } from "./BlixtLsp"; import { IChannelModel, channel } from "./Channel"; import { IChannelRpcInterceptorModel, channelRpcInterceptor } from "./ChannelRpcInterceptor"; @@ -21,6 +24,7 @@ import { INotificationManagerModel, notificationManager } from "./NotificationMa import { IOnChainModel, onChain } from "./OnChain"; import { IReceiveModel, receive } from "./Receive"; import { IScheduledSyncModel, scheduledSync } from "./ScheduledSync"; +import { IScheduledGossipSyncModel, scheduledGossipSync } from "./ScheduledGossip"; import { ISecurityModel, security } from "./Security"; import { ISendModel, send } from "./Send"; import { ISettingsModel, settings } from "./Settings"; @@ -58,7 +62,6 @@ import { IStoreInjections } from "./store"; import { LndMobileEventEmitter } from "../utils/event-listener"; import { SQLiteDatabase } from "react-native-sqlite-storage"; import SetupBlixtDemo from "../utils/setup-demo"; -import Tor from "react-native-tor"; import { appMigration } from "../migration/app-migration"; import { checkLndStreamErrorResponse } from "../utils/lndmobile"; import { clearTransactions } from "../storage/database/transaction"; @@ -100,6 +103,7 @@ export interface IStoreModel { setOnboardingState: Action; setTorEnabled: Action; setTorLoading: Action; + setSpeedloaderLoading: Action; generateSeed: Thunk; writeConfig: Thunk; @@ -112,6 +116,7 @@ export interface IStoreModel { walletCreated: boolean; holdOnboarding: boolean; torLoading: boolean; + speedloaderLoading: boolean; torEnabled: boolean; lightning: ILightningModel; @@ -125,6 +130,7 @@ export interface IStoreModel { settings: ISettingsModel; clipboardManager: IClipboardManagerModel; scheduledSync: IScheduledSyncModel; + scheduledGossipSync: IScheduledGossipSyncModel; lnUrl: ILNUrlModel; google: IGoogleModel; googleDriveBackup: IGoogleDriveBackupModel; @@ -163,7 +169,7 @@ export const model: IStoreModel = { } log.v("initializeApp()"); - const { initialize, checkStatus, startLnd } = injections.lndMobile.index; + const { initialize, checkStatus, startLnd, gossipSync } = injections.lndMobile.index; const db = await actions.openDb(); if (!(await getItemObjectAsyncStorage(StorageItem.app))) { log.i("Initializing app for the first time"); @@ -222,6 +228,10 @@ export const model: IStoreModel = { ); actions.setWalletCreated(await getWalletCreated()); + const debugShowStartupInfo = + (await getItemObjectAsyncStorage(StorageItem.debugShowStartupInfo)) ?? false; + const start = new Date(); + try { let torEnabled = (await getItemObjectAsyncStorage(StorageItem.torEnabled)) ?? false; actions.setTorEnabled(torEnabled); @@ -242,6 +252,8 @@ export const model: IStoreModel = { if (socksPort === 0 && PLATFORM === "ios") { throw new Error("Unable to obtain SOCKS port"); } + debugShowStartupInfo && + toast("Tor initialized " + (new Date().getTime() - start.getTime()) / 1000 + "s", 1000); } catch (e) { const restartText = "Restart app and try again with Tor"; const continueText = "Continue without Tor"; @@ -276,7 +288,13 @@ export const model: IStoreModel = { log.v("Running LndMobile.initialize()"); const initReturn = await initialize(); - log.v("initialize done", [initReturn]); + log.i("initialize done", [initReturn]); + const gossipSyncEnabled = + (await getItemObjectAsyncStorage(StorageItem.scheduledGossipSyncEnabled)) ?? false; + const enforceSpeedloaderOnStartup = + (await getItemObjectAsyncStorage(StorageItem.enforceSpeedloaderOnStartup)) ?? + false; + let gossipStatus: unknown = null; const status = await checkStatus(); log.d("status", [status]); @@ -284,7 +302,36 @@ export const model: IStoreModel = { (status & ELndMobileStatusCodes.STATUS_PROCESS_STARTED) !== ELndMobileStatusCodes.STATUS_PROCESS_STARTED ) { - log.i("Starting lnd"); + const speed = setTimeout(() => { + actions.setSpeedloaderLoading(true); + }, 3000); + if (gossipSyncEnabled) { + if (enforceSpeedloaderOnStartup) { + log.d("Clearing speedloader files"); + try { + // TODO(hsjoberg): LndMobileTools should be injected + await NativeModules.LndMobileTools.DEBUG_deleteSpeedloaderLastrunFile(); + await NativeModules.LndMobileTools.DEBUG_deleteSpeedloaderDgraphDirectory(); + } catch (error) { + log.e("Gossip files deletion failed", [error]); + } + } + try { + let connectionState = await NetInfo.fetch(); + log.i("connectionState", [connectionState.type]); + gossipStatus = await gossipSync(connectionState.type); + debugShowStartupInfo && + toast( + "Gossip sync done " + (new Date().getTime() - start.getTime()) / 1000 + "s", + 1000, + ); + } catch (e) { + log.e("GossipSync exception!", [e]); + } + } + clearTimeout(speed); + actions.setSpeedloaderLoading(false); + log.i("Starting lnd, gossipStatus", [gossipStatus]); try { let args = ""; if (socksPort > 0) { @@ -330,8 +377,6 @@ export const model: IStoreModel = { await dispatch.channel.setupCachedBalance(); log.d("Done starting up stores"); - const debugShowStartupInfo = getState().settings.debugShowStartupInfo; - const start = new Date(); LndMobileEventEmitter.addListener("SubscribeState", async (e: any) => { try { log.d("SubscribeState", [e]); @@ -607,6 +652,9 @@ routerrpc.estimator=${lndPathfindingAlgorithm} setTorLoading: action((state, value) => { state.torLoading = value; }), + setSpeedloaderLoading: action((state, value) => { + state.speedloaderLoading = value; + }), appReady: false, walletCreated: false, @@ -628,6 +676,7 @@ routerrpc.estimator=${lndPathfindingAlgorithm} settings, clipboardManager, scheduledSync, + scheduledGossipSync, lnUrl, google, googleDriveBackup, diff --git a/src/storage/app.ts b/src/storage/app.ts index 54fdf0c2e..83ccb4d5a 100644 --- a/src/storage/app.ts +++ b/src/storage/app.ts @@ -40,8 +40,11 @@ export enum StorageItem { // const enums not supported in Babel 7... pushNotificationsEnabled = "pushNotificationsEnabled", clipboardInvoiceCheck = "clipboardInvoiceCheck", scheduledSyncEnabled = "scheduledSyncEnabled", + scheduledGossipSyncEnabled = "scheduledGossipSyncEnabled", lastScheduledSync = "lastScheduledSync", lastScheduledSyncAttempt = "lastScheduledSyncAttempt", + lastScheduledGossipSync = "lastScheduledGossipSync", + lastScheduledGossipSyncAttempt = "lastScheduledGossipSyncAttempt", debugShowStartupInfo = "debugShowStartupInfo", googleDriveBackupEnabled = "googleDriveBackupEnabled", preferFiat = "preferFiat", @@ -77,6 +80,7 @@ export enum StorageItem { // const enums not supported in Babel 7... lndLogLevel = "lndLogLevel", lndCompactDb = "lndCompactDb", zeroConfPeers = "zeroConfPeers", + enforceSpeedloaderOnStartup = "enforceSpeedloaderOnStartup", } export const setItem = async (key: StorageItem, value: string) => @@ -138,6 +142,9 @@ export const clearApp = async () => { removeItem(StorageItem.scheduledSyncEnabled), removeItem(StorageItem.lastScheduledSync), removeItem(StorageItem.lastScheduledSyncAttempt), + removeItem(StorageItem.scheduledGossipSyncEnabled), + removeItem(StorageItem.lastScheduledGossipSync), + removeItem(StorageItem.lastScheduledGossipSyncAttempt), removeItem(StorageItem.debugShowStartupInfo), removeItem(StorageItem.googleDriveBackupEnabled), removeItem(StorageItem.preferFiat), @@ -173,6 +180,7 @@ export const clearApp = async () => { removeItem(StorageItem.maxLNFeePercentage), removeItem(StorageItem.lndLogLevel), removeItem(StorageItem.lndCompactDb), + removeItem(StorageItem.enforceSpeedloaderOnStartup), ]); }; @@ -255,5 +263,6 @@ export const setupApp = async () => { setItemObject(StorageItem.maxLNFeePercentage, DEFAULT_MAX_LN_FEE_PERCENTAGE), setItem(StorageItem.lndLogLevel, DEFAULT_LND_LOG_LEVEL), setItemObject(StorageItem.lndCompactDb, false), + setItemObject(StorageItem.enforceSpeedloaderOnStartup, false), ]); }; diff --git a/src/windows/InitProcess/DEV_Commands.tsx b/src/windows/InitProcess/DEV_Commands.tsx index f1dbf2022..d6c8ce8bd 100644 --- a/src/windows/InitProcess/DEV_Commands.tsx +++ b/src/windows/InitProcess/DEV_Commands.tsx @@ -186,6 +186,12 @@ export default function DEV_Commands({ navigation, continueCallback }: IProps) { + + + Speedloader: + + + lndmobile: + + +