Skip to content

Commit

Permalink
Backend,Frontend: import wallet using BIP39 seed
Browse files Browse the repository at this point in the history
Added option to import  wallet using BIP39 seed phrase
(mnemonic). All funds from imported wallet (currently only
first receiving address is used) are sent to BTC account of
choice. After that, imported account is converted to archived
account so geewallet will warn the user if more funds arrive
to it in the future.
  • Loading branch information
webwarrior-ws committed Jul 23, 2024
1 parent f3d4f39 commit ce686fb
Show file tree
Hide file tree
Showing 6 changed files with 156 additions and 3 deletions.
37 changes: 37 additions & 0 deletions src/GWallet.Backend/Account.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down Expand Up @@ -394,6 +396,41 @@ module Account =
|> ignore<ArchivedAccount>
Config.RemoveNormalAccount account

let CreateEphemeralAccountFromSeedMenmonic (mnemonic: string) : UtxoCoin.EphemeralUtxoAccount =
let rootKey = Mnemonic(mnemonic).DeriveExtKey().Derive(KeyPath("m/84'/0'/0'"))
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<ArchivedAccount>

let SweepArchivedFunds (account: ArchivedAccount)
(balance: decimal)
(destination: IAccount)
Expand Down
14 changes: 13 additions & 1 deletion src/GWallet.Backend/AccountTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,11 @@ namespace GWallet.Backend

open System.IO

type UtxoPublicKey = string

type WatchWalletInfo =
{
UtxoCoinPublicKey: string
UtxoCoinPublicKey: UtxoPublicKey
EtherPublicAddress: string
}

Expand All @@ -30,11 +32,13 @@ type AccountKind =
| Normal
| ReadOnly
| Archived
| Ephemeral
static member All() =
seq {
yield Normal
yield ReadOnly
yield Archived
yield Ephemeral
}

type IAccount =
Expand Down Expand Up @@ -77,3 +81,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
2 changes: 2 additions & 0 deletions src/GWallet.Backend/Config.fs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,8 @@ module Config =
Path.Combine(accountConfigDir, "readonly")
| AccountKind.Archived ->
Path.Combine(accountConfigDir, "archived")
| AccountKind.Ephemeral ->
Path.Combine(accountConfigDir, "wip")

let configDir = Path.Combine(baseConfigDir, currency.ToString()) |> DirectoryInfo
if not configDir.Exists then
Expand Down
8 changes: 8 additions & 0 deletions src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
Expand Down
94 changes: 94 additions & 0 deletions src/GWallet.Frontend.Console/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,7 @@ module Program =
| TestPaymentPassword
| TestSeedPassphrase
| WipeWallet
| TransferFundsFromWalletUsingMenmonic

let rec TestPaymentPassword () =
let password = UserInteraction.AskPassword false
Expand All @@ -348,13 +349,103 @@ 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
| [] -> failwith "No BTC accounts found"
| _ ->
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:\n%s" (uri.ToString())
Console.WriteLine()
printf "Archiving imported account..."
Account.ConvertEphemeralAccountToArchivedAccount importedAccount currency
printfn " done"
UserInteraction.PressAnyKeyToContinue()
| None -> ()

let WalletOptions(): unit =
let rec AskWalletOption(): GenericWalletOption =
Console.WriteLine "0. Cancel, go back"
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
Expand All @@ -365,6 +456,7 @@ module Program =
| 1u -> GenericWalletOption.TestPaymentPassword
| 2u -> GenericWalletOption.TestSeedPassphrase
| 3u -> GenericWalletOption.WipeWallet
| 4u -> GenericWalletOption.TransferFundsFromWalletUsingMenmonic
| _ -> AskWalletOption()

let walletOption = AskWalletOption()
Expand All @@ -377,6 +469,8 @@ module Program =
Console.WriteLine "Success!"
| GenericWalletOption.WipeWallet ->
WipeWallet()
| GenericWalletOption.TransferFundsFromWalletUsingMenmonic ->
TransferFundsFromWalletUsingMenmonic()
| _ -> ()

let rec PerformOperation (numActiveAccounts: uint32) (numHotAccounts: uint32) =
Expand Down
4 changes: 2 additions & 2 deletions src/GWallet.Frontend.Console/UserInteraction.fs
Original file line number Diff line number Diff line change
Expand Up @@ -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) ->
Expand Down Expand Up @@ -260,7 +260,7 @@ module UserInteraction =
return (account,balance,usdValue)
}

let private GetAccountBalance (account: IAccount): Async<MaybeCached<decimal>*MaybeCached<decimal>> =
let internal GetAccountBalance (account: IAccount): Async<MaybeCached<decimal>*MaybeCached<decimal>> =
async {
let! (_, balance, maybeUsdValue) = GetAccountBalanceInner account false
return (balance, maybeUsdValue)
Expand Down

0 comments on commit ce686fb

Please sign in to comment.