Skip to content

Commit

Permalink
WIP: use EphemeralAccount
Browse files Browse the repository at this point in the history
Introduce and use EphemeralAccount type for imported account.
Convert it to ArchivedAccount after sweeping funds.
  • Loading branch information
webwarrior-ws committed Jul 23, 2024
1 parent 33f52b0 commit 18ad190
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 90 deletions.
22 changes: 16 additions & 6 deletions src/GWallet.Backend/Account.fs
Original file line number Diff line number Diff line change
Expand Up @@ -397,31 +397,41 @@ module Account =
|> ignore<ArchivedAccount>
Config.RemoveNormalAccount account

let CreateArchivedAccountFromSeedMenmonic (mnemonic: string) : UtxoCoin.ArchivedUtxoAccount =
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 archivedAccount = CreateArchivedAccount currency privateKeyString


let fromPublicKeyToPublicAddress (publicKey: PubKey) =
publicKey.GetAddress(ScriptPubKeyType.Segwit, network).ToString()

let fromAccountFileToPrivateKey (accountConfigFile: FileRepresentation) =
Key.Parse(accountConfigFile.Content(), network)

let fromAccountFileToPublicAddress (accountConfigFile: FileRepresentation) =
fromAccountFileToPrivateKey(accountConfigFile).PubKey.GetAddress(ScriptPubKeyType.Segwit, network).ToString()
fromPublicKeyToPublicAddress(fromAccountFileToPrivateKey(accountConfigFile).PubKey)

let fromAccountFileToPublicKey (accountConfigFile: FileRepresentation) =
fromAccountFileToPrivateKey(accountConfigFile).PubKey

UtxoCoin.ArchivedUtxoAccount(
let fileName = fromPublicKeyToPublicAddress(firstReceivingAddressKey.GetPublicKey())
let accountFileRepresentation = { Name = fileName; Content = fun _ -> privateKeyString }

UtxoCoin.EphemeralUtxoAccount(
currency,
archivedAccount.AccountFile,
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
10 changes: 10 additions & 0 deletions src/GWallet.Backend/AccountTypes.fs
Original file line number Diff line number Diff line change
Expand Up @@ -32,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 @@ -79,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
6 changes: 3 additions & 3 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 Expand Up @@ -159,6 +161,7 @@ module Config =
let configDirForAccounts = GetConfigDirForAccounts()
Directory.Delete(configDirForAccounts.FullName, true)

// we don't expose this as public because we don't want to allow removing archived accounts
let private RemoveAccount (account: BaseAccount): unit =
let configFile = GetFile (account:>IAccount).Currency account
if not configFile.Exists then
Expand All @@ -172,9 +175,6 @@ module Config =
let RemoveReadOnlyAccount (account: ReadOnlyAccount): unit =
RemoveAccount account

let RemoveArchivedAccount (account: ArchivedAccount): unit =
RemoveAccount account

/// NOTE: the real initialization happens in Account.fs , see isInitialized
let internal Init() =
// In case a new token was added it will not have a config for an existing user
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
155 changes: 74 additions & 81 deletions src/GWallet.Frontend.Console/Program.fs
Original file line number Diff line number Diff line change
Expand Up @@ -351,11 +351,11 @@ module Program =
()

let TransferFundsFromWalletUsingMenmonic() =
let rec askForMnemonic() : UtxoCoin.ArchivedUtxoAccount =
let rec askForMnemonic() : UtxoCoin.EphemeralUtxoAccount =
Console.WriteLine "Enter mnemonic seed phrase (12, 15, 18, 21 or 24 words):"
let mnemonic = Console.ReadLine()
try
Account.CreateArchivedAccountFromSeedMenmonic mnemonic
Account.CreateEphemeralAccountFromSeedMenmonic mnemonic
with
| :? FormatException as exn ->
printfn "Error reading mnemonic seed phrase: %s" exn.Message
Expand All @@ -364,87 +364,80 @@ module Program =
let importedAccount = askForMnemonic()
let currency = BTC

let deleteImportedAccountFile() =
Config.RemoveArchivedAccount importedAccount
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
)

try
let maybeTotalBalance, maybeUsdValue = UserInteraction.GetAccountBalance importedAccount |> Async.RunSynchronously
match maybeTotalBalance with
| NotFresh _ ->
Console.WriteLine "Could not retrieve balance"
deleteImportedAccountFile()
UserInteraction.PressAnyKeyToContinue()
| Fresh 0.0m ->
Console.WriteLine "Balance on imported account is zero. No funds to transfer."
deleteImportedAccountFile()
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.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()
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())
UserInteraction.PressAnyKeyToContinue()
| None ->
deleteImportedAccountFile()
with
| _ ->
deleteImportedAccountFile()
reraise()
printf "Archiving imported account..."
Account.ConvertEphemeralAccountToArchivedAccount importedAccount currency
printfn " done"
UserInteraction.PressAnyKeyToContinue()
| None -> ()

let WalletOptions(): unit =
let rec AskWalletOption(): GenericWalletOption =
Expand Down

0 comments on commit 18ad190

Please sign in to comment.