Skip to content

Commit ce686fb

Browse files
committed
Backend,Frontend: import wallet using BIP39 seed
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.
1 parent f3d4f39 commit ce686fb

File tree

6 files changed

+156
-3
lines changed

6 files changed

+156
-3
lines changed

src/GWallet.Backend/Account.fs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ open System.Threading.Tasks
77

88
open GWallet.Backend.FSharpUtil.UwpHacks
99

10+
open NBitcoin
11+
1012
// this exception, if it happens, it would cause a crash because we don't handle it yet
1113
type UnhandledCurrencyServerException(currency: Currency,
1214
innerException: Exception) =
@@ -394,6 +396,41 @@ module Account =
394396
|> ignore<ArchivedAccount>
395397
Config.RemoveNormalAccount account
396398

399+
let CreateEphemeralAccountFromSeedMenmonic (mnemonic: string) : UtxoCoin.EphemeralUtxoAccount =
400+
let rootKey = Mnemonic(mnemonic).DeriveExtKey().Derive(KeyPath("m/84'/0'/0'"))
401+
let firstReceivingAddressKey = rootKey.Derive(0u).Derive(0u)
402+
403+
let currency = Currency.BTC
404+
let network = UtxoCoin.Account.GetNetwork currency
405+
let privateKeyString =
406+
firstReceivingAddressKey.PrivateKey.GetWif(network).ToWif()
407+
408+
let fromPublicKeyToPublicAddress (publicKey: PubKey) =
409+
publicKey.GetAddress(ScriptPubKeyType.Segwit, network).ToString()
410+
411+
let fromAccountFileToPrivateKey (accountConfigFile: FileRepresentation) =
412+
Key.Parse(accountConfigFile.Content(), network)
413+
414+
let fromAccountFileToPublicAddress (accountConfigFile: FileRepresentation) =
415+
fromPublicKeyToPublicAddress(fromAccountFileToPrivateKey(accountConfigFile).PubKey)
416+
417+
let fromAccountFileToPublicKey (accountConfigFile: FileRepresentation) =
418+
fromAccountFileToPrivateKey(accountConfigFile).PubKey
419+
420+
let fileName = fromPublicKeyToPublicAddress(firstReceivingAddressKey.GetPublicKey())
421+
let accountFileRepresentation = { Name = fileName; Content = fun _ -> privateKeyString }
422+
423+
UtxoCoin.EphemeralUtxoAccount(
424+
currency,
425+
accountFileRepresentation,
426+
fromAccountFileToPublicAddress,
427+
fromAccountFileToPublicKey)
428+
429+
let ConvertEphemeralAccountToArchivedAccount (ephemeralAccount: EphemeralAccount) (currency: Currency) : unit =
430+
// no need for removing account since we don't create any file to begin with (see CreateEphemeralAccountFromSeedMenmonic)
431+
let privateKeyAsString = ephemeralAccount.GetUnencryptedPrivateKey()
432+
CreateArchivedAccount currency privateKeyAsString |> ignore<ArchivedAccount>
433+
397434
let SweepArchivedFunds (account: ArchivedAccount)
398435
(balance: decimal)
399436
(destination: IAccount)

src/GWallet.Backend/AccountTypes.fs

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,11 @@ namespace GWallet.Backend
22

33
open System.IO
44

5+
type UtxoPublicKey = string
6+
57
type WatchWalletInfo =
68
{
7-
UtxoCoinPublicKey: string
9+
UtxoCoinPublicKey: UtxoPublicKey
810
EtherPublicAddress: string
911
}
1012

@@ -30,11 +32,13 @@ type AccountKind =
3032
| Normal
3133
| ReadOnly
3234
| Archived
35+
| Ephemeral
3336
static member All() =
3437
seq {
3538
yield Normal
3639
yield ReadOnly
3740
yield Archived
41+
yield Ephemeral
3842
}
3943

4044
type IAccount =
@@ -77,3 +81,11 @@ type ArchivedAccount(currency: Currency, accountFile: FileRepresentation,
7781
accountFile.Content()
7882

7983
override __.Kind = AccountKind.Archived
84+
85+
/// Inherits from ArchivedAccount because SweepArchivedFunds expects ArchivedAccount instance
86+
/// and sweep funds functionality is needed for this kind of account.
87+
type EphemeralAccount(currency: Currency, accountFile: FileRepresentation,
88+
fromAccountFileToPublicAddress: FileRepresentation -> string) =
89+
inherit ArchivedAccount(currency, accountFile, fromAccountFileToPublicAddress)
90+
91+
override __.Kind = AccountKind.Ephemeral

src/GWallet.Backend/Config.fs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,8 @@ module Config =
110110
Path.Combine(accountConfigDir, "readonly")
111111
| AccountKind.Archived ->
112112
Path.Combine(accountConfigDir, "archived")
113+
| AccountKind.Ephemeral ->
114+
Path.Combine(accountConfigDir, "wip")
113115

114116
let configDir = Path.Combine(baseConfigDir, currency.ToString()) |> DirectoryInfo
115117
if not configDir.Exists then

src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ type ArchivedUtxoAccount(currency: Currency, accountFile: FileRepresentation,
5252
interface IUtxoAccount with
5353
member val PublicKey = fromAccountFileToPublicKey accountFile with get
5454

55+
type EphemeralUtxoAccount(currency: Currency, accountFile: FileRepresentation,
56+
fromAccountFileToPublicAddress: FileRepresentation -> string,
57+
fromAccountFileToPublicKey: FileRepresentation -> PubKey) =
58+
inherit GWallet.Backend.EphemeralAccount(currency, accountFile, fromAccountFileToPublicAddress)
59+
60+
interface IUtxoAccount with
61+
member val PublicKey = fromAccountFileToPublicKey accountFile with get
62+
5563
module Account =
5664

5765
let internal GetNetwork (currency: Currency) =

src/GWallet.Frontend.Console/Program.fs

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -326,6 +326,7 @@ module Program =
326326
| TestPaymentPassword
327327
| TestSeedPassphrase
328328
| WipeWallet
329+
| TransferFundsFromWalletUsingMenmonic
329330

330331
let rec TestPaymentPassword () =
331332
let password = UserInteraction.AskPassword false
@@ -348,13 +349,103 @@ module Program =
348349
Account.WipeAll()
349350
else
350351
()
352+
353+
let TransferFundsFromWalletUsingMenmonic() =
354+
let rec askForMnemonic() : UtxoCoin.EphemeralUtxoAccount =
355+
Console.WriteLine "Enter mnemonic seed phrase (12, 15, 18, 21 or 24 words):"
356+
let mnemonic = Console.ReadLine()
357+
try
358+
Account.CreateEphemeralAccountFromSeedMenmonic mnemonic
359+
with
360+
| :? FormatException as exn ->
361+
printfn "Error reading mnemonic seed phrase: %s" exn.Message
362+
askForMnemonic()
363+
364+
let importedAccount = askForMnemonic()
365+
let currency = BTC
366+
367+
let maybeTotalBalance, maybeUsdValue = UserInteraction.GetAccountBalance importedAccount |> Async.RunSynchronously
368+
match maybeTotalBalance with
369+
| NotFresh _ ->
370+
Console.WriteLine "Could not retrieve balance"
371+
UserInteraction.PressAnyKeyToContinue()
372+
| Fresh 0.0m ->
373+
Console.WriteLine "Balance on imported account is zero. No funds to transfer."
374+
UserInteraction.PressAnyKeyToContinue()
375+
| Fresh balance ->
376+
printfn
377+
"Balance on imported account: %s BTC (%s)"
378+
(balance.ToString())
379+
(UserInteraction.BalanceInUsdString balance maybeUsdValue)
380+
381+
let rec chooseAccount() =
382+
Console.WriteLine "Choose account to send funds to:"
383+
Console.WriteLine()
384+
let allAccounts = Account.GetAllActiveAccounts() |> Seq.toList
385+
let btcAccounts = allAccounts |> List.filter (fun acc -> acc.Currency = currency)
386+
387+
match btcAccounts with
388+
| [ singleAccount ] -> Some singleAccount
389+
| [] -> failwith "No BTC accounts found"
390+
| _ ->
391+
allAccounts |> Seq.iteri (fun i account ->
392+
if account.Currency = currency then
393+
let balance, maybeUsdValue =
394+
UserInteraction.GetAccountBalance account
395+
|> Async.RunSynchronously
396+
UserInteraction.DisplayAccountStatus (i + 1) account balance maybeUsdValue
397+
|> Seq.iter Console.WriteLine
398+
)
399+
400+
Console.Write("Write the account number (or 0 to cancel): ")
401+
let accountNumber = Console.ReadLine()
402+
match Int32.TryParse(accountNumber) with
403+
| false, _ -> chooseAccount()
404+
| true, 0 -> None
405+
| true, accountParsed ->
406+
let theAccountChosen =
407+
try
408+
let selectedAccount = allAccounts.[accountParsed - 1]
409+
if selectedAccount.Currency = BTC then
410+
Some selectedAccount
411+
else
412+
chooseAccount()
413+
with
414+
| _ -> chooseAccount()
415+
theAccountChosen
416+
417+
match chooseAccount() with
418+
| Some targetAccount ->
419+
let destination = targetAccount.PublicAddress
420+
let transferAmount = TransferAmount(balance, balance, currency) // send all funds
421+
let maybeFee = UserInteraction.AskFee importedAccount transferAmount destination
422+
match maybeFee with
423+
| None -> ()
424+
| Some(fee) ->
425+
let txId =
426+
Account.SweepArchivedFunds
427+
importedAccount
428+
balance
429+
targetAccount
430+
fee
431+
false
432+
|> Async.RunSynchronously
433+
let uri = BlockExplorer.GetTransaction currency txId
434+
printfn "Transaction successful:\n%s" (uri.ToString())
435+
Console.WriteLine()
436+
printf "Archiving imported account..."
437+
Account.ConvertEphemeralAccountToArchivedAccount importedAccount currency
438+
printfn " done"
439+
UserInteraction.PressAnyKeyToContinue()
440+
| None -> ()
351441

352442
let WalletOptions(): unit =
353443
let rec AskWalletOption(): GenericWalletOption =
354444
Console.WriteLine "0. Cancel, go back"
355445
Console.WriteLine "1. Check you still remember your payment password"
356446
Console.WriteLine "2. Check you still remember your secret recovery phrase"
357447
Console.WriteLine "3. Wipe your current wallet, in order to start from scratch"
448+
Console.WriteLine "4. Transfer all funds from another wallet (given mnemonic code)"
358449
Console.Write "Choose an option from the ones above: "
359450
let optIntroduced = Console.ReadLine ()
360451
match UInt32.TryParse optIntroduced with
@@ -365,6 +456,7 @@ module Program =
365456
| 1u -> GenericWalletOption.TestPaymentPassword
366457
| 2u -> GenericWalletOption.TestSeedPassphrase
367458
| 3u -> GenericWalletOption.WipeWallet
459+
| 4u -> GenericWalletOption.TransferFundsFromWalletUsingMenmonic
368460
| _ -> AskWalletOption()
369461

370462
let walletOption = AskWalletOption()
@@ -377,6 +469,8 @@ module Program =
377469
Console.WriteLine "Success!"
378470
| GenericWalletOption.WipeWallet ->
379471
WipeWallet()
472+
| GenericWalletOption.TransferFundsFromWalletUsingMenmonic ->
473+
TransferFundsFromWalletUsingMenmonic()
380474
| _ -> ()
381475

382476
let rec PerformOperation (numActiveAccounts: uint32) (numHotAccounts: uint32) =

src/GWallet.Frontend.Console/UserInteraction.fs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ module UserInteraction =
178178
password
179179

180180
// FIXME: share code between Frontend.Console and Frontend.XF
181-
let private BalanceInUsdString balance maybeUsdValue =
181+
let internal BalanceInUsdString balance maybeUsdValue =
182182
match maybeUsdValue with
183183
| NotFresh(NotAvailable) -> Presentation.ExchangeRateUnreachableMsg
184184
| Fresh(usdValue) ->
@@ -260,7 +260,7 @@ module UserInteraction =
260260
return (account,balance,usdValue)
261261
}
262262

263-
let private GetAccountBalance (account: IAccount): Async<MaybeCached<decimal>*MaybeCached<decimal>> =
263+
let internal GetAccountBalance (account: IAccount): Async<MaybeCached<decimal>*MaybeCached<decimal>> =
264264
async {
265265
let! (_, balance, maybeUsdValue) = GetAccountBalanceInner account false
266266
return (balance, maybeUsdValue)

0 commit comments

Comments
 (0)