From bc88cdc4ec0eb8867c84965cb1b23396390e4ccc Mon Sep 17 00:00:00 2001 From: webwarrior Date: Tue, 16 Jul 2024 10:11:07 +0200 Subject: [PATCH 1/3] scripts,Backend(Tests): default to native segwit Make native segwit default by always adding NATIVE_SEGWIT to defined constants in configure.fsx. To make tests work properly with native segwit, rename deprecated OriginAddress property to OriginMainAddress in test data. --- scripts/configure.fsx | 11 +++-------- .../data/signedAndFormattedBtcTransaction.json | 2 +- .../data/signedAndFormattedEtherTransaction.json | 2 +- .../data/signedAndFormattedSaiTransaction.json | 2 +- .../data/unsignedAndFormattedBtcTransaction.json | 2 +- .../data/unsignedAndFormattedEtherTransaction.json | 2 +- .../data/unsignedAndFormattedSaiTransaction.json | 2 +- src/GWallet.Backend/Transaction.fs | 6 ------ 8 files changed, 9 insertions(+), 20 deletions(-) diff --git a/scripts/configure.fsx b/scripts/configure.fsx index 8a9c78159..1de5d5ae4 100644 --- a/scripts/configure.fsx +++ b/scripts/configure.fsx @@ -217,14 +217,9 @@ let configFileToBeWritten = | None -> configFileStageTwo let finalConfigFile = - let nativeSegwitEnabled = - argsToThisFsxScript - |> List.contains "--native-segwit" - if nativeSegwitEnabled then - configFileStageThree - |> AddToDefinedConstants "NATIVE_SEGWIT" - else - configFileStageThree + // native segwit is enabled by default + configFileStageThree + |> AddToDefinedConstants "NATIVE_SEGWIT" finalConfigFile diff --git a/src/GWallet.Backend.Tests/data/signedAndFormattedBtcTransaction.json b/src/GWallet.Backend.Tests/data/signedAndFormattedBtcTransaction.json index 05ec34f6b..d88f96f62 100644 --- a/src/GWallet.Backend.Tests/data/signedAndFormattedBtcTransaction.json +++ b/src/GWallet.Backend.Tests/data/signedAndFormattedBtcTransaction.json @@ -4,7 +4,7 @@ "Value": { "TransactionInfo": { "Proposal": { - "OriginAddress": "16pKBjGGZkUXo1afyBNf5ttFvV9hauS1kR", + "OriginMainAddress": "16pKBjGGZkUXo1afyBNf5ttFvV9hauS1kR", "Amount": { "ValueToSend": 10.01, "BalanceAtTheMomentOfSending": 12.02, diff --git a/src/GWallet.Backend.Tests/data/signedAndFormattedEtherTransaction.json b/src/GWallet.Backend.Tests/data/signedAndFormattedEtherTransaction.json index 68b8e4e3c..e4db17589 100644 --- a/src/GWallet.Backend.Tests/data/signedAndFormattedEtherTransaction.json +++ b/src/GWallet.Backend.Tests/data/signedAndFormattedEtherTransaction.json @@ -4,7 +4,7 @@ "Value": { "TransactionInfo": { "Proposal": { - "OriginAddress": "0xf3j4m0rjx94sushh03j", + "OriginMainAddress": "0xf3j4m0rjx94sushh03j", "Amount": { "ValueToSend": 10.01, "BalanceAtTheMomentOfSending": 12.02, diff --git a/src/GWallet.Backend.Tests/data/signedAndFormattedSaiTransaction.json b/src/GWallet.Backend.Tests/data/signedAndFormattedSaiTransaction.json index 70e6e14c1..fa7595f48 100644 --- a/src/GWallet.Backend.Tests/data/signedAndFormattedSaiTransaction.json +++ b/src/GWallet.Backend.Tests/data/signedAndFormattedSaiTransaction.json @@ -4,7 +4,7 @@ "Value": { "TransactionInfo": { "Proposal": { - "OriginAddress": "0xba766d6d13E2Cc921Bf6e896319D32502af9e37E", + "OriginMainAddress": "0xba766d6d13E2Cc921Bf6e896319D32502af9e37E", "Amount": { "ValueToSend": 1.0, "BalanceAtTheMomentOfSending": 7.08, diff --git a/src/GWallet.Backend.Tests/data/unsignedAndFormattedBtcTransaction.json b/src/GWallet.Backend.Tests/data/unsignedAndFormattedBtcTransaction.json index 35781105e..8aaefb17f 100644 --- a/src/GWallet.Backend.Tests/data/unsignedAndFormattedBtcTransaction.json +++ b/src/GWallet.Backend.Tests/data/unsignedAndFormattedBtcTransaction.json @@ -3,7 +3,7 @@ "TypeName": "GWallet.Backend.UnsignedTransaction`1[GWallet.Backend.UtxoCoin.TransactionMetadata]", "Value": { "Proposal": { - "OriginAddress": "16pKBjGGZkUXo1afyBNf5ttFvV9hauS1kR", + "OriginMainAddress": "16pKBjGGZkUXo1afyBNf5ttFvV9hauS1kR", "Amount": { "ValueToSend": 10.01, "BalanceAtTheMomentOfSending": 12.02, diff --git a/src/GWallet.Backend.Tests/data/unsignedAndFormattedEtherTransaction.json b/src/GWallet.Backend.Tests/data/unsignedAndFormattedEtherTransaction.json index e7ac16878..1c572b0d6 100644 --- a/src/GWallet.Backend.Tests/data/unsignedAndFormattedEtherTransaction.json +++ b/src/GWallet.Backend.Tests/data/unsignedAndFormattedEtherTransaction.json @@ -3,7 +3,7 @@ "TypeName": "GWallet.Backend.UnsignedTransaction`1[GWallet.Backend.Ether.TransactionMetadata]", "Value": { "Proposal": { - "OriginAddress": "0xf3j4m0rjx94sushh03j", + "OriginMainAddress": "0xf3j4m0rjx94sushh03j", "Amount": { "ValueToSend": 10.01, "BalanceAtTheMomentOfSending": 12.02, diff --git a/src/GWallet.Backend.Tests/data/unsignedAndFormattedSaiTransaction.json b/src/GWallet.Backend.Tests/data/unsignedAndFormattedSaiTransaction.json index 3b3e067c2..4522bc557 100644 --- a/src/GWallet.Backend.Tests/data/unsignedAndFormattedSaiTransaction.json +++ b/src/GWallet.Backend.Tests/data/unsignedAndFormattedSaiTransaction.json @@ -3,7 +3,7 @@ "TypeName": "GWallet.Backend.UnsignedTransaction`1[GWallet.Backend.Ether.TransactionMetadata]", "Value": { "Proposal": { - "OriginAddress": "0xba766d6d13E2Cc921Bf6e896319D32502af9e37E", + "OriginMainAddress": "0xba766d6d13E2Cc921Bf6e896319D32502af9e37E", "Amount": { "ValueToSend": 1.0, "BalanceAtTheMomentOfSending": 7.08, diff --git a/src/GWallet.Backend/Transaction.fs b/src/GWallet.Backend/Transaction.fs index 17f17669b..b605e5884 100644 --- a/src/GWallet.Backend/Transaction.fs +++ b/src/GWallet.Backend/Transaction.fs @@ -10,9 +10,6 @@ type ITransactionDetails = type internal SignedTransactionDetails = { -#if !NATIVE_SEGWIT - [] -#endif OriginMainAddress: string Amount: decimal @@ -27,9 +24,6 @@ type internal SignedTransactionDetails = type UnsignedTransactionProposal = { -#if !NATIVE_SEGWIT - [] -#endif OriginMainAddress: string Amount: TransferAmount; From 600cccf1158e994104a849169c5c760df50cc87a Mon Sep 17 00:00:00 2001 From: webwarrior Date: Tue, 16 Jul 2024 12:41:02 +0200 Subject: [PATCH 2/3] Backend,scripts: remove nested segwit code Remove NATIVE_SEGWIT compile-time constant and all code that was disabled when NATIVE_SEGWIT was defined. Also remove readonly accounts migration to native segwit. --- scripts/configure.fsx | 7 +--- src/GWallet.Backend/Account.fs | 13 +------- src/GWallet.Backend/Config.fs | 4 --- .../UtxoCoin/UtxoCoinAccount.fs | 32 ------------------- 4 files changed, 2 insertions(+), 54 deletions(-) diff --git a/scripts/configure.fsx b/scripts/configure.fsx index 1de5d5ae4..8559b56e6 100644 --- a/scripts/configure.fsx +++ b/scripts/configure.fsx @@ -211,16 +211,11 @@ let configFileToBeWritten = | Some theTool -> initialConfigFile.Add("LegacyBuildTool", theTool) | None -> initialConfigFile - let configFileStageThree = + let finalConfigFile = match buildTool with | Some theTool -> configFileStageTwo.Add("BuildTool", theTool) | None -> configFileStageTwo - let finalConfigFile = - // native segwit is enabled by default - configFileStageThree - |> AddToDefinedConstants "NATIVE_SEGWIT" - finalConfigFile let lines = diff --git a/src/GWallet.Backend/Account.fs b/src/GWallet.Backend/Account.fs index 6f39dc7d4..9fad24398 100644 --- a/src/GWallet.Backend/Account.fs +++ b/src/GWallet.Backend/Account.fs @@ -17,19 +17,8 @@ type UnhandledCurrencyServerException(currency: Currency, module Account = - let private isInitialized (accounts: seq) = lazy( + let private isInitialized (_accounts: seq) = lazy( Config.Init() - -#if !NATIVE_SEGWIT - let _readonlyUtxoAccounts = -#else - let readonlyUtxoAccounts = -#endif - accounts.Where(fun acc -> acc.Currency.IsUtxo()).OfType() -#if NATIVE_SEGWIT - UtxoCoin.Account.MigrateReadOnlyAccountsToNativeSegWit readonlyUtxoAccounts -#endif - () ) let private GetShowableBalanceAndImminentPaymentInternal (account: IAccount) diff --git a/src/GWallet.Backend/Config.fs b/src/GWallet.Backend/Config.fs index 9ccbf1ccf..a0acf9d70 100644 --- a/src/GWallet.Backend/Config.fs +++ b/src/GWallet.Backend/Config.fs @@ -38,11 +38,7 @@ module Config = let internal NoNetworkBalanceForDebuggingPurposes = false let internal UseNativeSegwit = -#if NATIVE_SEGWIT true -#else - false -#endif let IsWindowsPlatform() = RuntimeInformation.IsOSPlatform OSPlatform.Windows diff --git a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs index 6adb43f4c..ccaba5d12 100644 --- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs +++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs @@ -700,38 +700,6 @@ module Account = Config.AddAccount conceptAccountForReadOnlyAccount AccountKind.ReadOnly |> ignore -#if NATIVE_SEGWIT - let internal MigrateReadOnlyAccountsToNativeSegWit (readOnlyUtxoAccounts: seq): unit = - let utxoAccountsToMigrate = - seq { - for utxoAccount in readOnlyUtxoAccounts do - let accountFile = utxoAccount.AccountFile - let prefix = - match (utxoAccount :> IAccount).Currency with - | Currency.BTC -> - BITCOIN_ADDRESS_BECH32_PREFIX - | Currency.LTC -> - LITECOIN_ADDRESS_BECH32_PREFIX - | otherCurrency -> failwith <| SPrintF1 "Missed UTXO currency %A when implementing NativeSegwit migration?" otherCurrency - if not (accountFile.Name.StartsWith prefix) then - yield utxoAccount - } - - let utxoPublicKeys = - seq { - for utxoReadOnlyAccount in utxoAccountsToMigrate do - let accountFile = utxoReadOnlyAccount.AccountFile - let utxoPublicKey = accountFile.Content() - yield utxoPublicKey - } |> Set.ofSeq - - for utxoPublicKey in utxoPublicKeys do - CreateReadOnlyAccounts utxoPublicKey - - for utxoReadOnlyAccount in utxoAccountsToMigrate do - Config.RemoveReadOnlyAccount utxoReadOnlyAccount -#endif - let GetSignedTransactionDetails<'T when 'T :> IBlockchainFeeInfo>(rawTransaction: string) (currency: Currency) (readonlyUtxoAccounts: seq) From f9f214661f45202ad0aa31cb6b2d0bd0bb925b6a Mon Sep 17 00:00:00 2001 From: webwarrior Date: Tue, 16 Jul 2024 14:41:34 +0200 Subject: [PATCH 3/3] 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. --- src/GWallet.Backend/Account.fs | 39 +++++++ src/GWallet.Backend/AccountTypes.fs | 5 +- src/GWallet.Backend/Config.fs | 2 + .../UtxoCoin/UtxoCoinAccount.fs | 9 ++ src/GWallet.Frontend.Console/Program.fs | 102 ++++++++++++++++++ .../UserInteraction.fs | 4 +- 6 files changed, 158 insertions(+), 3 deletions(-) diff --git a/src/GWallet.Backend/Account.fs b/src/GWallet.Backend/Account.fs index 9fad24398..80ebbb60b 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: 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 + let SweepArchivedFunds (account: ArchivedAccount) (balance: decimal) (destination: IAccount) diff --git a/src/GWallet.Backend/AccountTypes.fs b/src/GWallet.Backend/AccountTypes.fs index bd2aec6ba..f025930ae 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 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..de0c11c47 100644 --- a/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs +++ b/src/GWallet.Backend/UtxoCoin/UtxoCoinAccount.fs @@ -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) = diff --git a/src/GWallet.Frontend.Console/Program.fs b/src/GWallet.Frontend.Console/Program.fs index a337961f6..bb60df1cf 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,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 + + Console.WriteLine() + + 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 "Imported account address: %s" (importedAccount :> IAccount).PublicAddress + printfn + "Balance on imported account: %s BTC (%s)" + (balance.ToString()) + (UserInteraction.BalanceInUsdString balance maybeUsdValue) + Console.WriteLine() + + 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 +453,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 +464,7 @@ module Program = | 1u -> GenericWalletOption.TestPaymentPassword | 2u -> GenericWalletOption.TestSeedPassphrase | 3u -> GenericWalletOption.WipeWallet + | 4u -> GenericWalletOption.TransferFundsFromWalletUsingMenmonic | _ -> AskWalletOption() let walletOption = AskWalletOption() @@ -377,6 +477,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)