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 29, 2024
1 parent 600cccf commit 0de1386
Show file tree
Hide file tree
Showing 6 changed files with 154 additions and 3 deletions.
39 changes: 39 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,43 @@ module Account =
|> ignore<ArchivedAccount>
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: UtxoCoin.EphemeralUtxoAccount) (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
5 changes: 4 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,6 +32,7 @@ type AccountKind =
| Normal
| ReadOnly
| Archived
| Ephemeral
static member All() =
seq {
yield Normal
Expand Down
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 ->
failwith "Ephemeral accounts are not supposed to be stored in file"

let configDir = Path.Combine(baseConfigDir, currency.ToString()) |> DirectoryInfo
if not configDir.Exists then
Expand Down
9 changes: 9 additions & 0 deletions src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,15 @@ type ArchivedUtxoAccount(currency: Currency, accountFile: FileRepresentation,
interface IUtxoAccount with
member val PublicKey = fromAccountFileToPublicKey accountFile with get

/// Inherits from ArchivedUtxoAccount because SweepArchivedFunds expects ArchivedUtxoAccount instance
/// and sweep funds functionality is needed for this kind of account.
type EphemeralUtxoAccount(currency: Currency, accountFile: FileRepresentation,
fromAccountFileToPublicAddress: FileRepresentation -> string,
fromAccountFileToPublicKey: FileRepresentation -> PubKey) =
inherit ArchivedUtxoAccount(currency, accountFile, fromAccountFileToPublicAddress, fromAccountFileToPublicKey)

override self.Kind = AccountKind.Ephemeral

module Account =

let internal GetNetwork (currency: Currency) =
Expand Down
98 changes: 98 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,107 @@ 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 =
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 +460,7 @@ module Program =
| 1u -> GenericWalletOption.TestPaymentPassword
| 2u -> GenericWalletOption.TestSeedPassphrase
| 3u -> GenericWalletOption.WipeWallet
| 4u -> GenericWalletOption.TransferFundsFromWalletUsingMenmonic
| _ -> AskWalletOption()

let walletOption = AskWalletOption()
Expand All @@ -377,6 +473,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 0de1386

Please sign in to comment.