diff --git a/src/GWallet.Backend/Account.fs b/src/GWallet.Backend/Account.fs index 9fad24398..bca8cbdaa 100644 --- a/src/GWallet.Backend/Account.fs +++ b/src/GWallet.Backend/Account.fs @@ -7,6 +7,8 @@ open System.Threading.Tasks open GWallet.Backend.FSharpUtil.UwpHacks +open NBitcoin + // this exception, if it happens, it would cause a crash because we don't handle it yet type UnhandledCurrencyServerException(currency: Currency, innerException: Exception) = @@ -394,6 +396,43 @@ module Account = |> ignore Config.RemoveNormalAccount account + let CreateEphemeralAccountFromSeedMenmonic (mnemonic: string) : UtxoCoin.EphemeralUtxoAccount = + let standardBip84DerivationPath = KeyPath("m/84'/0'/0'") + let rootKey = Mnemonic(mnemonic).DeriveExtKey().Derive(standardBip84DerivationPath) + let firstReceivingAddressKey = rootKey.Derive(0u).Derive(0u) + + let currency = Currency.BTC + let network = UtxoCoin.Account.GetNetwork currency + let privateKeyString = + firstReceivingAddressKey.PrivateKey.GetWif(network).ToWif() + + let fromPublicKeyToPublicAddress (publicKey: PubKey) = + publicKey.GetAddress(ScriptPubKeyType.Segwit, network).ToString() + + let fromAccountFileToPrivateKey (accountConfigFile: FileRepresentation) = + Key.Parse(accountConfigFile.Content(), network) + + let fromAccountFileToPublicAddress (accountConfigFile: FileRepresentation) = + fromPublicKeyToPublicAddress(fromAccountFileToPrivateKey(accountConfigFile).PubKey) + + let fromAccountFileToPublicKey (accountConfigFile: FileRepresentation) = + fromAccountFileToPrivateKey(accountConfigFile).PubKey + + let fileName = fromPublicKeyToPublicAddress(firstReceivingAddressKey.GetPublicKey()) + let accountFileRepresentation = { Name = fileName; Content = fun _ -> privateKeyString } + + UtxoCoin.EphemeralUtxoAccount( + currency, + accountFileRepresentation, + fromAccountFileToPublicAddress, + fromAccountFileToPublicKey + ) + + let ConvertEphemeralAccountToArchivedAccount (ephemeralAccount: EphemeralAccount) (currency: Currency) : unit = + // no need for removing account since we don't create any file to begin with (see CreateEphemeralAccountFromSeedMenmonic) + let privateKeyAsString = ephemeralAccount.GetUnencryptedPrivateKey() + CreateArchivedAccount currency privateKeyAsString |> ignore + let SweepArchivedFunds (account: ArchivedAccount) (balance: decimal) (destination: IAccount) diff --git a/src/GWallet.Backend/AccountTypes.fs b/src/GWallet.Backend/AccountTypes.fs index bd2aec6ba..a072d6a11 100644 --- a/src/GWallet.Backend/AccountTypes.fs +++ b/src/GWallet.Backend/AccountTypes.fs @@ -2,9 +2,11 @@ namespace GWallet.Backend open System.IO +type UtxoPublicKey = string + type WatchWalletInfo = { - UtxoCoinPublicKey: string + UtxoCoinPublicKey: UtxoPublicKey EtherPublicAddress: string } @@ -30,6 +32,7 @@ type AccountKind = | Normal | ReadOnly | Archived + | Ephemeral static member All() = seq { yield Normal @@ -77,3 +80,11 @@ type ArchivedAccount(currency: Currency, accountFile: FileRepresentation, accountFile.Content() override __.Kind = AccountKind.Archived + +/// Inherits from ArchivedAccount because SweepArchivedFunds expects ArchivedAccount instance +/// and sweep funds functionality is needed for this kind of account. +type EphemeralAccount(currency: Currency, accountFile: FileRepresentation, + fromAccountFileToPublicAddress: FileRepresentation -> string) = + inherit ArchivedAccount(currency, accountFile, fromAccountFileToPublicAddress) + + override __.Kind = AccountKind.Ephemeral diff --git a/src/GWallet.Backend/Config.fs b/src/GWallet.Backend/Config.fs index a0acf9d70..d04c5c79f 100644 --- a/src/GWallet.Backend/Config.fs +++ b/src/GWallet.Backend/Config.fs @@ -110,6 +110,8 @@ module Config = Path.Combine(accountConfigDir, "readonly") | AccountKind.Archived -> Path.Combine(accountConfigDir, "archived") + | AccountKind.Ephemeral -> + failwith "Ephemeral accounts are not supposed to be stored in file" let configDir = Path.Combine(baseConfigDir, currency.ToString()) |> DirectoryInfo if not configDir.Exists then diff --git a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs index ccaba5d12..e354f7627 100644 --- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs +++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs @@ -52,6 +52,14 @@ type ArchivedUtxoAccount(currency: Currency, accountFile: FileRepresentation, interface IUtxoAccount with member val PublicKey = fromAccountFileToPublicKey accountFile with get +type EphemeralUtxoAccount(currency: Currency, accountFile: FileRepresentation, + fromAccountFileToPublicAddress: FileRepresentation -> string, + fromAccountFileToPublicKey: FileRepresentation -> PubKey) = + inherit GWallet.Backend.EphemeralAccount(currency, accountFile, fromAccountFileToPublicAddress) + + interface IUtxoAccount with + member val PublicKey = fromAccountFileToPublicKey accountFile with get + module Account = let internal GetNetwork (currency: Currency) = diff --git a/src/GWallet.Frontend.Console/Program.fs b/src/GWallet.Frontend.Console/Program.fs index a337961f6..1344e89c4 100644 --- a/src/GWallet.Frontend.Console/Program.fs +++ b/src/GWallet.Frontend.Console/Program.fs @@ -326,6 +326,7 @@ module Program = | TestPaymentPassword | TestSeedPassphrase | WipeWallet + | TransferFundsFromWalletUsingMenmonic let rec TestPaymentPassword () = let password = UserInteraction.AskPassword false @@ -348,6 +349,99 @@ module Program = Account.WipeAll() else () + + let TransferFundsFromWalletUsingMenmonic() = + let rec askForMnemonic() : UtxoCoin.EphemeralUtxoAccount = + Console.WriteLine "Enter mnemonic seed phrase (12, 15, 18, 21 or 24 words):" + let mnemonic = Console.ReadLine() + try + Account.CreateEphemeralAccountFromSeedMenmonic mnemonic + with + | :? FormatException as exn -> + printfn "Error reading mnemonic seed phrase: %s" exn.Message + askForMnemonic() + + let importedAccount = askForMnemonic() + let currency = BTC + + let maybeTotalBalance, maybeUsdValue = UserInteraction.GetAccountBalance importedAccount |> Async.RunSynchronously + match maybeTotalBalance with + | NotFresh _ -> + Console.WriteLine "Could not retrieve balance." + UserInteraction.PressAnyKeyToContinue() + | Fresh 0.0m -> + Console.WriteLine "Balance on imported account is zero. No funds to transfer." + UserInteraction.PressAnyKeyToContinue() + | Fresh balance -> + printfn + "Balance on imported account: %s BTC (%s)" + (balance.ToString()) + (UserInteraction.BalanceInUsdString balance maybeUsdValue) + + let rec chooseAccount() = + Console.WriteLine "Choose account to send funds to:" + Console.WriteLine() + let allAccounts = Account.GetAllActiveAccounts() |> Seq.toList + let btcAccounts = allAccounts |> List.filter (fun acc -> acc.Currency = currency) + + match btcAccounts with + | [ singleAccount ] -> Some singleAccount + | [] -> + printfn "No BTC accounts found." + None + | _ -> + allAccounts |> Seq.iteri (fun i account -> + if account.Currency = currency then + let balance, maybeUsdValue = + UserInteraction.GetAccountBalance account + |> Async.RunSynchronously + UserInteraction.DisplayAccountStatus (i + 1) account balance maybeUsdValue + |> Seq.iter Console.WriteLine + ) + + Console.Write "Write the account number (or 0 to cancel): " + let accountNumber = Console.ReadLine() + match Int32.TryParse accountNumber with + | false, _ -> chooseAccount() + | true, 0 -> None + | true, accountParsed -> + let theAccountChosen = + try + let selectedAccount = allAccounts.[accountParsed - 1] + if selectedAccount.Currency = BTC then + Some selectedAccount + else + chooseAccount() + with + | _ -> chooseAccount() + theAccountChosen + + match chooseAccount() with + | Some targetAccount -> + let destination = targetAccount.PublicAddress + let transferAmount = TransferAmount(balance, balance, currency) // send all funds + let maybeFee = UserInteraction.AskFee importedAccount transferAmount destination + match maybeFee with + | None -> () + | Some fee -> + let txId = + Account.SweepArchivedFunds + importedAccount + balance + targetAccount + fee + false + |> Async.RunSynchronously + let uri = BlockExplorer.GetTransaction currency txId + printfn "Transaction successful:" + printfn "%s" (uri.ToString()) + Console.WriteLine() + printf "Archiving imported account..." + Account.ConvertEphemeralAccountToArchivedAccount importedAccount currency + printfn " done." + UserInteraction.PressAnyKeyToContinue() + | None -> + UserInteraction.PressAnyKeyToContinue() let WalletOptions(): unit = let rec AskWalletOption(): GenericWalletOption = @@ -355,6 +449,7 @@ module Program = Console.WriteLine "1. Check you still remember your payment password" Console.WriteLine "2. Check you still remember your secret recovery phrase" Console.WriteLine "3. Wipe your current wallet, in order to start from scratch" + Console.WriteLine "4. Transfer all funds from another wallet (given mnemonic code)" Console.Write "Choose an option from the ones above: " let optIntroduced = Console.ReadLine () match UInt32.TryParse optIntroduced with @@ -365,6 +460,7 @@ module Program = | 1u -> GenericWalletOption.TestPaymentPassword | 2u -> GenericWalletOption.TestSeedPassphrase | 3u -> GenericWalletOption.WipeWallet + | 4u -> GenericWalletOption.TransferFundsFromWalletUsingMenmonic | _ -> AskWalletOption() let walletOption = AskWalletOption() @@ -377,6 +473,8 @@ module Program = Console.WriteLine "Success!" | GenericWalletOption.WipeWallet -> WipeWallet() + | GenericWalletOption.TransferFundsFromWalletUsingMenmonic -> + TransferFundsFromWalletUsingMenmonic() | _ -> () let rec PerformOperation (numActiveAccounts: uint32) (numHotAccounts: uint32) = diff --git a/src/GWallet.Frontend.Console/UserInteraction.fs b/src/GWallet.Frontend.Console/UserInteraction.fs index c39b6db4a..625f2db1d 100644 --- a/src/GWallet.Frontend.Console/UserInteraction.fs +++ b/src/GWallet.Frontend.Console/UserInteraction.fs @@ -178,7 +178,7 @@ module UserInteraction = password // FIXME: share code between Frontend.Console and Frontend.XF - let private BalanceInUsdString balance maybeUsdValue = + let internal BalanceInUsdString balance maybeUsdValue = match maybeUsdValue with | NotFresh(NotAvailable) -> Presentation.ExchangeRateUnreachableMsg | Fresh(usdValue) -> @@ -260,7 +260,7 @@ module UserInteraction = return (account,balance,usdValue) } - let private GetAccountBalance (account: IAccount): Async*MaybeCached> = + let internal GetAccountBalance (account: IAccount): Async*MaybeCached> = async { let! (_, balance, maybeUsdValue) = GetAccountBalanceInner account false return (balance, maybeUsdValue)