diff --git a/android/app/src/main/java/com/blixtwallet/LndMobileTools.java b/android/app/src/main/java/com/blixtwallet/LndMobileTools.java index 114818830..c18113779 100644 --- a/android/app/src/main/java/com/blixtwallet/LndMobileTools.java +++ b/android/app/src/main/java/com/blixtwallet/LndMobileTools.java @@ -7,6 +7,7 @@ import android.Manifest; import android.app.ActivityManager; import android.database.sqlite.SQLiteDatabase; +import android.net.Uri; import android.os.FileObserver; import android.os.Process; import android.util.Base64; @@ -765,6 +766,56 @@ public void generateSecureRandomAsBase64(int length, Promise promise) { promise.resolve(Base64.encodeToString(buffer, Base64.NO_WRAP)); } + @ReactMethod + public void saveChannelDbFile(Promise promise) { + // This promise will be resolved in MainActivity + MainActivity.tmpExportChannelDbPromise = promise; + Intent intent = new Intent(Intent.ACTION_CREATE_DOCUMENT); + intent.addCategory(Intent.CATEGORY_OPENABLE); + intent.setType("application/octet-stream"); + intent.putExtra(Intent.EXTRA_TITLE, "channel.db"); + getReactApplicationContext().getCurrentActivity().startActivityForResult(intent, MainActivity.INTENT_EXPORTCHANNELDBFILE); + } + + @ReactMethod + public void importChannelDbFile(String channelDbImportPath, Promise promise) { + Log.i(TAG, getReactApplicationContext().getFilesDir().toString() + "/data/graph/" + BuildConfig.CHAIN + "/channel.db"); + try { + File sourceFile = new File(channelDbImportPath); + + String channelDbFilePath = getReactApplicationContext().getFilesDir().toString() + "/data/graph/" + BuildConfig.CHAIN + "/channel.db"; + File destChannelDbFile = new File(channelDbFilePath); + + // Delete the channel.db file first if there is one + destChannelDbFile.delete(); + + File destFile = new File(channelDbFilePath); + if (!destFile.exists() && !destFile.createNewFile()) { + promise.reject(new IOException("Failed to create destination channel.db file")); + return; + } + + // Copy content + InputStream in = new FileInputStream(sourceFile); + OutputStream out = new FileOutputStream(destFile); + byte[] buffer = new byte[1024]; + int read; + while ((read = in.read(buffer)) != -1) { + out.write(buffer, 0, read); + } + in.close(); + out.flush(); + out.close(); + + // Delete the cached file + sourceFile.delete(); + + promise.resolve(true); + } catch (IOException error) { + promise.reject(error); + } + } + private void checkWriteExternalStoragePermission(@NonNull RequestWriteExternalStoragePermissionCallback successCallback, @NonNull Runnable failCallback, @NonNull Runnable failPermissionCheckcallback) { diff --git a/android/app/src/main/java/com/blixtwallet/MainActivity.kt b/android/app/src/main/java/com/blixtwallet/MainActivity.kt index 96a0f706a..82f252f39 100644 --- a/android/app/src/main/java/com/blixtwallet/MainActivity.kt +++ b/android/app/src/main/java/com/blixtwallet/MainActivity.kt @@ -5,6 +5,7 @@ import android.os.Bundle import android.widget.Toast import com.facebook.react.ReactActivity import com.facebook.react.ReactActivityDelegate +import com.facebook.react.bridge.Promise import com.facebook.react.defaults.DefaultNewArchitectureEntryPoint.fabricEnabled import com.facebook.react.defaults.DefaultReactActivityDelegate import dev.doubledot.doki.ui.DokiActivity @@ -92,6 +93,29 @@ class MainActivity : ReactActivity() { } catch (e: IOException) { Toast.makeText(this, "Error " + e.message, Toast.LENGTH_LONG).show() } + } else if (requestCode == INTENT_EXPORTCHANNELDBFILE && resultCode == RESULT_OK) { + val destUri = data!!.data + val sourceLocation = File(filesDir.toString() + "/data/graph/" + BuildConfig.CHAIN + "/channel.db") + try { + val `in`: InputStream = FileInputStream(sourceLocation) + val out = contentResolver.openOutputStream(destUri!!) + val buf = ByteArray(1024) + var len: Int + while (`in`.read(buf).also { len = it } > 0) { + out!!.write(buf, 0, len) + } + `in`.close() + out!!.close() + + if (tmpExportChannelDbPromise != null) { + tmpExportChannelDbPromise!!.resolve(true) + } else { + Toast.makeText(this, "promise is null", Toast.LENGTH_LONG).show() + } + } catch (e: IOException) { + Toast.makeText(this, "Error " + e.message, Toast.LENGTH_LONG).show() + tmpExportChannelDbPromise!!.reject(e) + } } } @@ -111,14 +135,19 @@ class MainActivity : ReactActivity() { @JvmField var INTENT_EXPORTCHANBACKUP = 101 @JvmField + var tmpChanBackup: ByteArray = ByteArray(0) + @JvmField var INTENT_EXPORTCHANBACKUPFILE = 102 @JvmField var INTENT_COPYSPEEDLOADERLOG = 103 @JvmField - var tmpChanBackup: ByteArray = ByteArray(0) + var INTENT_EXPORTCHANNELDBFILE = 104 + @JvmField + var tmpExportChannelDbPromise: Promise? = null + @JvmField var currentActivity: WeakReference? = null @JvmStatic val activity: MainActivity? get() = currentActivity!!.get() } -} +} \ No newline at end of file diff --git a/ios/LndMobile/Lnd.swift b/ios/LndMobile/Lnd.swift index 943cbf509..f9f749278 100644 --- a/ios/LndMobile/Lnd.swift +++ b/ios/LndMobile/Lnd.swift @@ -122,6 +122,7 @@ open class Lnd { "VerifyMessage": { bytes, cb in LndmobileVerifyMessage(bytes, cb) }, "SignMessage": { bytes, cb in LndmobileSignMessage(bytes, cb) }, "SignerSignMessage": { bytes, cb in LndmobileSignerSignMessage(bytes, cb) }, + "RestoreChannelBackups": { bytes, cb in LndmobileRestoreChannelBackups(bytes, cb) }, // autopilot "AutopilotStatus": { bytes, cb in LndmobileAutopilotStatus(bytes, cb) }, diff --git a/src/Main.tsx b/src/Main.tsx index d1e11f4b1..6357508ff 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, H2 } from "native-base"; +import { Spinner, H1, H2, Text } from "native-base"; import { createStackNavigator, CardStyleInterpolators, @@ -40,6 +40,7 @@ import useStackNavigationOptions from "./hooks/useStackNavigationOptions"; import { navigator } from "./utils/navigation"; import { PLATFORM } from "./utils/constants"; import Prompt, { IPromptNavigationProps } from "./windows/HelperWindows/Prompt"; +import { isInstanceBricked } from "./storage/app"; const RootStack = createStackNavigator(); @@ -86,6 +87,7 @@ export default function Main() { const appReady = useStoreState((store) => store.appReady); const lightningReady = useStoreState((store) => store.lightning.ready); const walletCreated = useStoreState((store) => store.walletCreated); + const importChannelDbOnStartup = useStoreState((store) => store.importChannelDbOnStartup); const loggedIn = useStoreState((store) => store.security.loggedIn); const initializeApp = useStoreActions((store) => store.initializeApp); const [initialRoute, setInitialRoute] = useState("Loading"); @@ -95,13 +97,22 @@ export default function Main() { (store) => store.settings.screenTransitionsEnabled, ); - const [state, setState] = useState<"init" | "authentication" | "onboarding" | "started">("init"); + console.log("walletCreated", walletCreated); + + const [state, setState] = useState< + "init" | "authentication" | "onboarding" | "started" | "bricked" + >("init"); useEffect(() => { // tslint:disable-next-line (async () => { if (!appReady) { try { + if (await isInstanceBricked()) { + setState("bricked"); + return; + } + await initializeApp(); } catch (e) { toast(e.message, 0, "danger"); @@ -120,7 +131,7 @@ export default function Main() { setState("authentication"); } else if (!lightningReady) { setState("started"); - if (!walletCreated) { + if (!walletCreated && !importChannelDbOnStartup) { setInitialRoute("Welcome"); } else { // try { @@ -225,6 +236,10 @@ export default function Main() { return ; } + if (state === "bricked") { + return Bricked; + } + return ( >; + saveChannelDbFile(): Promise; + importChannelDbFile(channelDbPath: string): Promise; // Android-specific getIntentStringData(): Promise; diff --git a/src/lndmobile/wallet.ts b/src/lndmobile/wallet.ts index 7f289cb04..8e701c0f2 100644 --- a/src/lndmobile/wallet.ts +++ b/src/lndmobile/wallet.ts @@ -9,7 +9,11 @@ import { lnrpc, signrpc } from "../../proto/lightning"; * TODO test */ export const genSeed = async (passphrase: string | undefined): Promise => { - const response = await sendCommand({ + const response = await sendCommand< + lnrpc.IGenSeedRequest, + lnrpc.GenSeedRequest, + lnrpc.GenSeedResponse + >({ request: lnrpc.GenSeedRequest, response: lnrpc.GenSeedResponse, method: "GenSeed", @@ -40,15 +44,19 @@ export const initWallet = async ( options.channelBackups = { multiChanBackup: { multiChanBackup: base64.toByteArray(channelBackupsBase64), - } - } + }, + }; } - const response = await sendCommand({ + const response = await sendCommand< + lnrpc.IInitWalletRequest, + lnrpc.InitWalletRequest, + lnrpc.InitWalletResponse + >({ request: lnrpc.InitWalletRequest, response: lnrpc.InitWalletResponse, method: "InitWallet", - options + options, }); return response; }; @@ -59,7 +67,11 @@ export const initWallet = async ( export const unlockWallet = async (password: string): Promise => { const start = new Date().getTime(); // await NativeModules.LndMobile.unlockWallet(password); - const response = await sendCommand({ + const response = await sendCommand< + lnrpc.IUnlockWalletRequest, + lnrpc.UnlockWalletRequest, + lnrpc.UnlockWalletResponse + >({ request: lnrpc.UnlockWalletRequest, response: lnrpc.UnlockWalletResponse, method: "UnlockWallet", @@ -74,8 +86,15 @@ export const unlockWallet = async (password: string): Promise => { - const response = await sendCommand({ +export const deriveKey = async ( + keyFamily: number, + keyIndex: number, +): Promise => { + const response = await sendCommand< + signrpc.IKeyLocator, + signrpc.KeyLocator, + signrpc.KeyDescriptor + >({ request: signrpc.KeyLocator, response: signrpc.KeyDescriptor, method: "WalletKitDeriveKey", @@ -90,8 +109,15 @@ export const deriveKey = async (keyFamily: number, keyIndex: number): Promise => { - const response = await sendCommand({ +export const derivePrivateKey = async ( + keyFamily: number, + keyIndex: number, +): Promise => { + const response = await sendCommand< + signrpc.IKeyDescriptor, + signrpc.KeyDescriptor, + signrpc.KeyDescriptor + >({ request: signrpc.KeyDescriptor, response: signrpc.KeyDescriptor, method: "WalletKitDerivePrivateKey", @@ -108,8 +134,15 @@ export const derivePrivateKey = async (keyFamily: number, keyIndex: number): Pro /** * @throws */ -export const verifyMessageNodePubkey = async (signature: string, msg: Uint8Array): Promise => { - const response = await sendCommand({ +export const verifyMessageNodePubkey = async ( + signature: string, + msg: Uint8Array, +): Promise => { + const response = await sendCommand< + lnrpc.IVerifyMessageRequest, + lnrpc.VerifyMessageRequest, + lnrpc.VerifyMessageResponse + >({ request: lnrpc.VerifyMessageRequest, response: lnrpc.VerifyMessageResponse, method: "VerifyMessage", @@ -124,8 +157,14 @@ export const verifyMessageNodePubkey = async (signature: string, msg: Uint8Array /** * @throws */ -export const signMessageNodePubkey = async (msg: Uint8Array): Promise => { - const response = await sendCommand({ +export const signMessageNodePubkey = async ( + msg: Uint8Array, +): Promise => { + const response = await sendCommand< + lnrpc.ISignMessageRequest, + lnrpc.SignMessageRequest, + lnrpc.SignMessageResponse + >({ request: lnrpc.SignMessageRequest, response: lnrpc.SignMessageResponse, method: "SignMessage", @@ -139,8 +178,16 @@ export const signMessageNodePubkey = async (msg: Uint8Array): Promise => { - const response = await sendCommand({ +export const signMessage = async ( + keyFamily: number, + keyIndex: number, + msg: Uint8Array, +): Promise => { + const response = await sendCommand< + signrpc.ISignMessageReq, + signrpc.SignMessageReq, + signrpc.SignMessageResp + >({ request: signrpc.SignMessageReq, response: signrpc.SignMessageResp, method: "SignerSignMessage", @@ -156,17 +203,43 @@ export const signMessage = async (keyFamily: number, keyIndex: number, msg: Uint return response; }; +/** + * @throws + */ +export const restoreChannelBackups = async ( + channelsBackupBase64: string, +): Promise => { + const response = await sendCommand< + lnrpc.IRestoreChanBackupRequest, + lnrpc.RestoreChanBackupRequest, + lnrpc.RestoreBackupResponse + >({ + request: lnrpc.RestoreChanBackupRequest, + response: lnrpc.RestoreBackupResponse, + method: "RestoreChannelBackups", + options: { + multiChanBackup: base64.toByteArray(channelsBackupBase64), + }, + }); + return response; +}; + // TODO exception? // TODO move to a more appropriate file? export const subscribeInvoices = async (): Promise => { try { - const response = await sendStreamCommand({ - request: lnrpc.InvoiceSubscription, - method: "SubscribeInvoices", - options: {}, - }, false); + const response = await sendStreamCommand( + { + request: lnrpc.InvoiceSubscription, + method: "SubscribeInvoices", + options: {}, + }, + false, + ); return response; - } catch (e) { throw e.message; } + } catch (e) { + throw e.message; + } }; // TODO error handling diff --git a/src/state/index.ts b/src/state/index.ts index 3fbab3623..05de14f1d 100644 --- a/src/state/index.ts +++ b/src/state/index.ts @@ -38,10 +38,14 @@ import { ISettingsModel, settings } from "./Settings"; import { ITransactionModel, transaction } from "./Transaction"; import { IWebLNModel, webln } from "./WebLN"; import { + IImportChannelDbOnStartup, StorageItem, + brickInstance, clearApp, getAppBuild, getAppVersion, + getBrickDeviceAndExportChannelDb, + getImportChannelDbOnStartup, getItem as getItemAsyncStorage, getItemObject as getItemObjectAsyncStorage, getLndCompactDb, @@ -49,6 +53,8 @@ import { getWalletCreated, setAppBuild, setAppVersion, + setBrickDeviceAndExportChannelDb, + setImportChannelDbOnStartup, setItem, setItemObject, setLndCompactDb, @@ -110,6 +116,7 @@ export interface IStoreModel { setTorEnabled: Action; setTorLoading: Action; setSpeedloaderLoading: Action; + setImportChannelDbOnStartup: Action; generateSeed: Thunk; writeConfig: Thunk; @@ -147,12 +154,13 @@ export interface IStoreModel { blixtLsp: IBlixtLsp; contacts: IContactsModel; lightningBox: ILightningBoxModel; + channelAcceptanceManager: IChannelAcceptanceManagerModel; walletSeed?: string[]; appVersion: number; appBuild: number; onboardingState: OnboardingState; - channelAcceptanceManager: IChannelAcceptanceManagerModel; + importChannelDbOnStartup: IImportChannelDbOnStartup | null; } export const model: IStoreModel = { @@ -175,6 +183,38 @@ export const model: IStoreModel = { } log.v("initializeApp()"); + const brickDeviceAndExportChannelDb = await getBrickDeviceAndExportChannelDb(); + if (brickDeviceAndExportChannelDb) { + await NativeModules.LndMobileTools.saveChannelDbFile(); + await brickInstance(); + await setBrickDeviceAndExportChannelDb(false); + Alert.alert( + "", + "This Blixt wallet instance is now stopped and disabled.\nUse your channel.db file to restore on another device.\nTake extreme caution and do not restore on more than one device.", + [ + { + text: "OK", + onPress() { + if (PLATFORM === "android") { + NativeModules.LndMobileTools.restartApp(); + } + }, + }, + ], + ); + return; + } + + const importChannelDbOnStartup = await getImportChannelDbOnStartup(); + if (importChannelDbOnStartup) { + actions.setImportChannelDbOnStartup(importChannelDbOnStartup); + const path = importChannelDbOnStartup.channelDbPath.replace(/^file:\/\//, ""); + toast("Beginning channel db import procedure", undefined, "warning"); + log.i("Beginning channel db import procedure", [path]); + await NativeModules.LndMobileTools.importChannelDbFile(path); + toast("Successfully imported channel.db"); + } + const { initialize, checkStatus, startLnd, gossipSync } = injections.lndMobile.index; const db = await actions.openDb(); const firstStartup = !(await getItemObjectAsyncStorage(StorageItem.app)); @@ -412,6 +452,27 @@ export const model: IStoreModel = { log.i("Current lnd state", [state]); if (state.state === lnrpc.WalletState.NON_EXISTING) { log.d("Got lnrpc.WalletState.NON_EXISTING"); + + // Continue channel db import restore + if (importChannelDbOnStartup) { + log.i("Continuing restoration with channel import"); + actions.setWalletSeed(importChannelDbOnStartup.seed); + await actions.createWallet({ + restore: { + restoreWallet: true, + aezeedPassphrase: importChannelDbOnStartup.passphrase, + }, + }); + + await Promise.all([ + actions.settings.changeAutopilotEnabled(false), + actions.scheduledSync.setSyncEnabled(true), // TODO test + actions.settings.changeScheduledSyncEnabled(true), + actions.changeOnboardingState("DONE"), + ]); + + setImportChannelDbOnStartup(null); + } } else if (state.state === lnrpc.WalletState.LOCKED) { log.d("Got lnrpc.WalletState.LOCKED"); log.d("Wallet locked, unlocking wallet"); @@ -702,6 +763,9 @@ routerrpc.estimator=${lndPathfindingAlgorithm} setSpeedloaderLoading: action((state, value) => { state.speedloaderLoading = value; }), + setImportChannelDbOnStartup: action((state, value) => { + state.importChannelDbOnStartup = value; + }), appReady: false, walletCreated: false, @@ -709,6 +773,7 @@ routerrpc.estimator=${lndPathfindingAlgorithm} appVersion: 0, appBuild: 0, onboardingState: "SEND_ONCHAIN", + importChannelDbOnStartup: null, torEnabled: false, torLoading: false, diff --git a/src/storage/app.ts b/src/storage/app.ts index 3c37d2daf..a0812e0aa 100644 --- a/src/storage/app.ts +++ b/src/storage/app.ts @@ -26,6 +26,9 @@ const APP_VERSION = appMigration.length - 1; export enum StorageItem { // const enums not supported in Babel 7... app = "app", + brickDeviceAndExportChannelDb = "brickDeviceAndExportChannelDb", // This is used to copy the channel.db file when lnd is down + bricked = "bricked", // This is used when channel.db migration is commenced + importChannelDbOnStartup = "importChannelDbOnStartup", appVersion = "appVersion", appBuild = "appBuild", databaseCreated = "databaseCreated", @@ -93,6 +96,12 @@ export enum StorageItem { // const enums not supported in Babel 7... randomizeSettingsOnStartup = "randomizeSettingsOnStartup", } +export interface IImportChannelDbOnStartup { + channelDbPath: string; + seed: string[]; + passphrase: string; +} + export const setItem = async (key: StorageItem, value: string) => await AsyncStorage.setItem(key, value); export const setItemObject = async (key: StorageItem, value: T) => @@ -128,11 +137,40 @@ export const setRescanWallet = async (rescan: boolean): Promise => { export const setLndCompactDb = async (rescan: boolean): Promise => { return await setItemObject(StorageItem.lndCompactDb, rescan); }; +export const setBrickDeviceAndExportChannelDb = async (brick: boolean): Promise => { + return await setItemObject(StorageItem.brickDeviceAndExportChannelDb, brick); +}; +export const getBrickDeviceAndExportChannelDb = async (): Promise => { + return await getItemObject(StorageItem.brickDeviceAndExportChannelDb); +}; +export const brickInstance = async (): Promise => { + return await setItemObject(StorageItem.bricked, true); +}; +export const isInstanceBricked = async (): Promise => { + return await getItemObject(StorageItem.bricked); +}; +export const setImportChannelDbOnStartup = async ( + value: IImportChannelDbOnStartup | null, +): Promise => { + return await setItemObject( + StorageItem.importChannelDbOnStartup, + value, + ); +}; +export const getImportChannelDbOnStartup = async (): Promise => { + return await getItemObject(StorageItem.importChannelDbOnStartup); +}; +export const removeImportChannelDbOnStartupKvItem = async (): Promise => { + return removeItem(StorageItem.importChannelDbOnStartup); +}; export const clearApp = async () => { // TODO use AsyncStorage.clear? await Promise.all([ removeItem(StorageItem.app), + removeItem(StorageItem.brickDeviceAndExportChannelDb), + removeItem(StorageItem.bricked), + removeItem(StorageItem.importChannelDbOnStartup), removeItem(StorageItem.appVersion), removeItem(StorageItem.appBuild), removeItem(StorageItem.walletCreated), @@ -226,6 +264,9 @@ export const setupApp = async () => { await Promise.all([ setItemObject(StorageItem.app, true), + setItemObject(StorageItem.brickDeviceAndExportChannelDb, false), + setItemObject(StorageItem.bricked, false), + setItemObject(StorageItem.bricked, null), setItemObject(StorageItem.appVersion, APP_VERSION), setItemObject(StorageItem.appBuild, VersionCode), setItemObject(StorageItem.walletCreated, false), diff --git a/src/windows/InitProcess/DEV_Commands.tsx b/src/windows/InitProcess/DEV_Commands.tsx index 3ed63670f..77c71956d 100644 --- a/src/windows/InitProcess/DEV_Commands.tsx +++ b/src/windows/InitProcess/DEV_Commands.tsx @@ -145,6 +145,22 @@ export default function DEV_Commands({ navigation, continueCallback }: IProps) { )} Random: + + - {PLATFORM === "android" && + {PLATFORM === "android" && ( - } - {(PLATFORM === "ios" && iCloudActive) && + )} + {PLATFORM === "ios" && iCloudActive && ( - } + )} + {/* */} + + )} + {(backupType === "file" || backupType === "macos") && ( + + {backupFile && backupFile.name} + - } - {(backupType === "file" || backupType === "macos") && - - {backupFile && backupFile.name} + )} + {backupType === "channeldb" && ( + + {channelDbFile && channelDbFile.name} - } - {backupType === "google_drive" && - + )} + {backupType === "google_drive" && ( + {t("backup.google")} - } - {backupType === "icloud" && - + )} + {backupType === "icloud" && ( + {t("backup.iCloud")} - } + )}

{t("restore.title")}

- {t("restore.msg")}{"\n"}{"\n"} + {t("restore.msg")} + {"\n"} + {"\n"} {t("restore.msg1")}
@@ -352,9 +415,7 @@ const style = StyleSheet.create({ textHeader: { marginBottom: 3, }, - card: { - - }, + card: {}, cardItem: { width: "100%", flex: 1,