From 0aaf03e031f51054a0d7f9d9f129f5bda282f1d1 Mon Sep 17 00:00:00 2001 From: SondreB Date: Thu, 23 Apr 2020 19:26:01 +0200 Subject: [PATCH] Add signal when transaction found in wallet, and filtering on wallet - Add a signal when transactoin is found in wallet. - Add options to filter what is returned from Accounts, Transactions and Balances. --- .../ColdStakingManager.cs | 3 + .../Broadcasters/WalletInfoBroadcaster.cs | 2 +- .../Controllers/WalletController.cs | 19 ++-- .../Controllers/WalletRPCController.cs | 81 ++++++++++++++- .../Events/TransactionFound.cs | 22 +++++ .../Models/GetWalletInfoModel.cs | 28 ++++++ .../Blockcore.Features.Wallet/Wallet.cs | 94 +++++++++++++++--- .../WalletManager.cs | 38 +++++-- src/Networks/City/City/Networks/CityMain.cs | 4 +- .../Consensus/CityPosConsensusOptions.cs | 63 ++++++++++++ .../ColdStakingControllerTest.cs | 99 +++++++++++++++++++ .../ColdStakingManagerTest.cs | 7 +- .../WalletManagerTest.cs | 20 ++-- .../API/ApiSteps.cs | 33 ++++--- 14 files changed, 453 insertions(+), 60 deletions(-) create mode 100644 src/Features/Blockcore.Features.Wallet/Events/TransactionFound.cs create mode 100644 src/Features/Blockcore.Features.Wallet/Models/GetWalletInfoModel.cs create mode 100644 src/Networks/City/City/Networks/Consensus/CityPosConsensusOptions.cs diff --git a/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs b/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs index 988ccbc4f..55aa083aa 100644 --- a/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs +++ b/src/Features/Blockcore.Features.ColdStaking/ColdStakingManager.cs @@ -7,6 +7,7 @@ using Blockcore.Features.Wallet; using Blockcore.Features.Wallet.Interfaces; using Blockcore.Interfaces; +using Blockcore.Signals; using Blockcore.Utilities; using Microsoft.Extensions.Logging; using NBitcoin; @@ -79,6 +80,7 @@ public ColdStakingManager( IScriptAddressReader scriptAddressReader, ILoggerFactory loggerFactory, IDateTimeProvider dateTimeProvider, + ISignals signals = null, IBroadcasterManager broadcasterManager = null) : base( loggerFactory, network, @@ -90,6 +92,7 @@ public ColdStakingManager( nodeLifeTime, dateTimeProvider, scriptAddressReader, + signals, broadcasterManager ) { diff --git a/src/Features/Blockcore.Features.SignalR/Broadcasters/WalletInfoBroadcaster.cs b/src/Features/Blockcore.Features.SignalR/Broadcasters/WalletInfoBroadcaster.cs index 40100807e..4fd370db7 100644 --- a/src/Features/Blockcore.Features.SignalR/Broadcasters/WalletInfoBroadcaster.cs +++ b/src/Features/Blockcore.Features.SignalR/Broadcasters/WalletInfoBroadcaster.cs @@ -62,7 +62,7 @@ protected override IEnumerable GetMessages() SpendableAmount = balance.SpendableAmount, Addresses = account.GetCombinedAddresses().Select(address => { - (Money confirmedAmount, Money unConfirmedAmount) = address.GetBalances(); + (Money confirmedAmount, Money unConfirmedAmount) = address.GetBalances(account.IsNormalAccount()); return new AddressModel { Address = address.Address, diff --git a/src/Features/Blockcore.Features.Wallet/Controllers/WalletController.cs b/src/Features/Blockcore.Features.Wallet/Controllers/WalletController.cs index 525e1373b..47803f4c1 100644 --- a/src/Features/Blockcore.Features.Wallet/Controllers/WalletController.cs +++ b/src/Features/Blockcore.Features.Wallet/Controllers/WalletController.cs @@ -661,7 +661,7 @@ public IActionResult GetBalance([FromQuery] WalletBalanceRequest request) SpendableAmount = balance.SpendableAmount, Addresses = account.GetCombinedAddresses().Select(address => { - (Money confirmedAmount, Money unConfirmedAmount) = address.GetBalances(); + (Money confirmedAmount, Money unConfirmedAmount) = address.GetBalances(account.IsNormalAccount()); return new AddressModel { Address = address.Address, @@ -1197,7 +1197,7 @@ public IActionResult GetAllAddresses([FromQuery]GetAllAddressesModel request) { Addresses = account.GetCombinedAddresses().Select(address => { - (Money confirmedAmount, Money unConfirmedAmount) = address.GetBalances(); + (Money confirmedAmount, Money unConfirmedAmount) = address.GetBalances(account.IsNormalAccount()); return new AddressModel { @@ -1271,19 +1271,20 @@ public IActionResult RemoveTransactions([FromQuery]RemoveTransactionsModel reque } // If the user chose to resync the wallet after removing transactions. - if (result.Any() && request.ReSync) + if (request.ReSync) { - // From the list of removed transactions, check which one is the oldest and retrieve the block right before that time. - DateTimeOffset earliestDate = result.Min(r => r.creationTime); - ChainedHeader chainedHeader = this.chainIndexer.GetHeader(this.chainIndexer.GetHeightAtTime(earliestDate.DateTime)); + Wallet wallet = this.walletManager.GetWallet(request.WalletName); + + // Initiate the scan one day ahead of wallet creation. + // If the creation time is DateTime.MinValue, don't remove one day as that throws exception. + ChainedHeader chainedHeader = this.chainIndexer.GetHeader(this.chainIndexer.GetHeightAtTime(wallet.CreationTime.DateTime != DateTime.MinValue ? wallet.CreationTime.DateTime.AddDays(-1) : wallet.CreationTime.DateTime)); // Update the wallet and save it to the file system. - Wallet wallet = this.walletManager.GetWallet(request.WalletName); wallet.SetLastBlockDetails(chainedHeader); this.walletManager.SaveWallet(wallet); - // Start the syncing process from the block before the earliest transaction was seen. - this.walletSyncManager.SyncFromHeight(chainedHeader.Height - 1); + // Start the sync from the day before it was created. + this.walletSyncManager.SyncFromHeight(chainedHeader.Height); } IEnumerable model = result.Select(r => new RemovedTransactionModel diff --git a/src/Features/Blockcore.Features.Wallet/Controllers/WalletRPCController.cs b/src/Features/Blockcore.Features.Wallet/Controllers/WalletRPCController.cs index f39923b75..77733426b 100644 --- a/src/Features/Blockcore.Features.Wallet/Controllers/WalletRPCController.cs +++ b/src/Features/Blockcore.Features.Wallet/Controllers/WalletRPCController.cs @@ -46,6 +46,11 @@ public class WalletRPCController : FeatureController /// Wallet related configuration. private readonly WalletSettings walletSettings; + /// + /// The wallet name set by selectwallet method. This is static since the controller is a stateless type. This value should probably be cached by an injected service in the future. + /// + private static string CurrentWalletName; + public WalletRPCController( IBlockStore blockStore, IBroadcasterManager broadcasterManager, @@ -70,6 +75,14 @@ public WalletRPCController( this.walletTransactionHandler = walletTransactionHandler; } + [ActionName("setwallet")] + [ActionDescription("Selects the active wallet on RPC based on the name of the wallet supplied.")] + public bool SetWallet(string walletname) + { + WalletRPCController.CurrentWalletName = walletname; + return true; + } + [ActionName("walletpassphrase")] [ActionDescription("Stores the wallet decryption key in memory for the indicated number of seconds. Issuing the walletpassphrase command while the wallet is already unlocked will set a new unlock time that overrides the old one.")] public bool UnlockWallet(string passphrase, int timeout) @@ -419,10 +432,22 @@ public GetTransactionModel GetTransaction(string txid) } } + // Get the ColdStaking script template if available. + Dictionary templates = this.walletManager.GetValidStakingTemplates(); + ScriptTemplate coldStakingTemplate = templates.ContainsKey("ColdStaking") ? templates["ColdStaking"] : null; + // Receive transactions details. foreach (TransactionData trxInWallet in receivedTransactions) { + // Skip the details if the script pub key is cold staking. + // TODO: Verify if we actually need this any longer, after changing the internals to recognice account type! + if (coldStakingTemplate != null && coldStakingTemplate.CheckScriptPubKey(trxInWallet.ScriptPubKey)) + { + continue; + } + GetTransactionDetailsCategoryModel category; + if (isGenerated) { category = model.Confirmations > this.FullNode.Network.Consensus.CoinbaseMaturity ? GetTransactionDetailsCategoryModel.Generate : GetTransactionDetailsCategoryModel.Immature; @@ -792,6 +817,43 @@ public async Task SendManyAsync(string fromAccount, string addressesJso } } + [ActionName("getwalletinfo")] + [ActionDescription("Provides information about the wallet.")] + public GetWalletInfoModel GetWalletInfo() + { + var accountReference = this.GetWalletAccountReference(); + var account = this.walletManager.GetAccounts(accountReference.WalletName) + .Where(i => i.Name.Equals(accountReference.AccountName)) + .Single(); + + (Money confirmedAmount, Money unconfirmedAmount) = account.GetBalances(account.IsNormalAccount()); + + var balance = Money.Coins(GetBalance(string.Empty)); + var immature = Money.Coins(balance.ToDecimal(MoneyUnit.BTC) - GetBalance(string.Empty, (int)this.FullNode.Network.Consensus.CoinbaseMaturity)); // Balance - Balance(AtHeight) + + var model = new GetWalletInfoModel + { + Balance = balance, + WalletName = accountReference.WalletName + ".wallet.json", + WalletVersion = 1, + UnConfirmedBalance = unconfirmedAmount, + ImmatureBalance = immature + }; + + return model; + } + + private int GetConformationCount(TransactionData transaction) + { + if (transaction.BlockHeight.HasValue) + { + var blockCount = this.ConsensusManager?.Tip.Height ?? -1; // TODO: This is available in FullNodeController, should refactor and reuse the logic. + return blockCount - transaction.BlockHeight.Value; + } + + return -1; + } + /// /// Gets the first account from the "default" wallet if it specified, /// otherwise returns the first available account in the existing wallets. @@ -801,16 +863,27 @@ private WalletAccountReference GetWalletAccountReference() { string walletName = null; - if (this.walletSettings.IsDefaultWalletEnabled()) - walletName = this.walletManager.GetWalletsNames().FirstOrDefault(w => w == this.walletSettings.DefaultWalletName); + // If the global override is null or empty. + if (string.IsNullOrWhiteSpace(WalletRPCController.CurrentWalletName)) + { + if (this.walletSettings.IsDefaultWalletEnabled()) + walletName = this.walletManager.GetWalletsNames().FirstOrDefault(w => w == this.walletSettings.DefaultWalletName); + else + { + //TODO: Support multi wallet like core by mapping passed RPC credentials to a wallet/account + walletName = this.walletManager.GetWalletsNames().FirstOrDefault(); + } + } else { - //TODO: Support multi wallet like core by mapping passed RPC credentials to a wallet/account - walletName = this.walletManager.GetWalletsNames().FirstOrDefault(); + // Read from class instance the wallet name. + walletName = WalletRPCController.CurrentWalletName; } if (walletName == null) + { throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found"); + } HdAccount account = this.walletManager.GetAccounts(walletName).First(); return new WalletAccountReference(walletName, account.Name); diff --git a/src/Features/Blockcore.Features.Wallet/Events/TransactionFound.cs b/src/Features/Blockcore.Features.Wallet/Events/TransactionFound.cs new file mode 100644 index 000000000..ef68bd669 --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Events/TransactionFound.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Blockcore.EventBus; +using NBitcoin; + +namespace Blockcore.Features.Wallet.Events +{ + /// + /// Event that is executed when a transaction is found in the wallet. + /// + /// + public class TransactionFound : EventBase + { + public Transaction FoundTransaction { get; } + + public TransactionFound(Transaction foundTransaction) + { + this.FoundTransaction = foundTransaction; + } + } +} diff --git a/src/Features/Blockcore.Features.Wallet/Models/GetWalletInfoModel.cs b/src/Features/Blockcore.Features.Wallet/Models/GetWalletInfoModel.cs new file mode 100644 index 000000000..a33adc894 --- /dev/null +++ b/src/Features/Blockcore.Features.Wallet/Models/GetWalletInfoModel.cs @@ -0,0 +1,28 @@ +using Blockcore.Utilities.JsonConverters; +using NBitcoin; +using Newtonsoft.Json; + +namespace Blockcore.Features.Wallet.Models +{ + /// Model for RPC method getwalletinfo. + public class GetWalletInfoModel + { + [JsonProperty("walletname")] + public string WalletName { get; set; } + + [JsonProperty("walletversion")] + public int WalletVersion { get; set; } + + [JsonProperty("balance")] + [JsonConverter(typeof(MoneyInCoinsJsonConverter))] + public Money Balance { get; set; } + + [JsonProperty("unconfirmed_balance")] + [JsonConverter(typeof(MoneyInCoinsJsonConverter))] + public Money UnConfirmedBalance { get; set; } + + [JsonProperty("immature_balance")] + [JsonConverter(typeof(MoneyInCoinsJsonConverter))] + public Money ImmatureBalance { get; set; } + } +} diff --git a/src/Features/Blockcore.Features.Wallet/Wallet.cs b/src/Features/Blockcore.Features.Wallet/Wallet.cs index a292e6135..84454d110 100644 --- a/src/Features/Blockcore.Features.Wallet/Wallet.cs +++ b/src/Features/Blockcore.Features.Wallet/Wallet.cs @@ -22,6 +22,9 @@ public class Wallet /// Filter for identifying normal wallet accounts. public static Func NormalAccounts = a => a.Index < SpecialPurposeAccountIndexesStart; + /// Filter for all wallet accounts. + public static Func AllAccounts = a => true; + /// /// Initializes a new instance of the wallet. /// @@ -119,16 +122,40 @@ public void SetLastBlockDetails(ChainedHeader block) /// Gets all the transactions in the wallet. /// /// A list of all the transactions in the wallet. - public IEnumerable GetAllTransactions() + public IEnumerable GetAllTransactions(Func accountFilter = null) { - List accounts = this.GetAccounts().ToList(); + List accounts = this.GetAccounts(accountFilter).ToList(); + + // First we iterate normal accounts + foreach (TransactionData txData in accounts.Where(a => a.IsNormalAccount()).SelectMany(x => x.ExternalAddresses).SelectMany(x => x.Transactions)) + { + // If this is a cold coin stake UTXO, we won't return it for a normal account. + if (txData.IsColdCoinStake.HasValue && txData.IsColdCoinStake.Value == true) + { + continue; + } + + yield return txData; + } + + foreach (TransactionData txData in accounts.Where(a => a.IsNormalAccount()).SelectMany(x => x.InternalAddresses).SelectMany(x => x.Transactions)) + { + // If this is a cold coin stake UTXO, we won't return it for a normal account. + if (txData.IsColdCoinStake.HasValue && txData.IsColdCoinStake.Value == true) + { + continue; + } + + yield return txData; + } - foreach (TransactionData txData in accounts.SelectMany(x => x.ExternalAddresses).SelectMany(x => x.Transactions)) + // Then we iterate special accounts. + foreach (TransactionData txData in accounts.Where(a => !a.IsNormalAccount()).SelectMany(x => x.ExternalAddresses).SelectMany(x => x.Transactions)) { yield return txData; } - foreach (TransactionData txData in accounts.SelectMany(x => x.InternalAddresses).SelectMany(x => x.Transactions)) + foreach (TransactionData txData in accounts.Where(a => !a.IsNormalAccount()).SelectMany(x => x.InternalAddresses).SelectMany(x => x.Transactions)) { yield return txData; } @@ -307,7 +334,7 @@ public IEnumerable GetAllUnspentTransactions(int current /// The fee paid. public Money GetSentTransactionFee(uint256 transactionId) { - List allTransactions = this.GetAllTransactions().ToList(); + List allTransactions = this.GetAllTransactions(Wallet.NormalAccounts).ToList(); // Get a list of all the inputs spent in this transaction. List inputsSpentInTransaction = allTransactions.Where(t => t.SpendingDetails?.TransactionId == transactionId).ToList(); @@ -606,6 +633,15 @@ public int GetCoinType() return HdOperations.GetCoinType(this.HdPath); } + /// + /// Check if the current account is a normal or special purpose one. + /// + /// True if this is a normal account (index below the SpecialPurposeAccountIndexesStart). + public bool IsNormalAccount() + { + return this.Index < Wallet.SpecialPurposeAccountIndexesStart; + } + /// /// Gets the first receiving address that contains no transaction. /// @@ -683,15 +719,26 @@ public IEnumerable GetTransactionsById(uint256 id) /// /// Get the accounts total spendable value for both confirmed and unconfirmed UTXO. /// - public (Money ConfirmedAmount, Money UnConfirmedAmount) GetBalances() + public (Money ConfirmedAmount, Money UnConfirmedAmount) GetBalances(bool excludeColdStakeUtxo) { List allTransactions = this.ExternalAddresses.SelectMany(a => a.Transactions) .Concat(this.InternalAddresses.SelectMany(i => i.Transactions)).ToList(); - long confirmed = allTransactions.Sum(t => t.GetUnspentAmount(true)); - long total = allTransactions.Sum(t => t.GetUnspentAmount(false)); + if (excludeColdStakeUtxo) + { + // If this is a normal account, we must exclude the cold coin stake data. + long confirmed = allTransactions.Where(t => t.IsColdCoinStake != true).Sum(t => t.GetUnspentAmount(true)); + long total = allTransactions.Where(t => t.IsColdCoinStake != true).Sum(t => t.GetUnspentAmount(false)); - return (confirmed, total - confirmed); + return (confirmed, total - confirmed); + } + else + { + long confirmed = allTransactions.Sum(t => t.GetUnspentAmount(true)); + long total = allTransactions.Sum(t => t.GetUnspentAmount(false)); + + return (confirmed, total - confirmed); + } } /// @@ -812,17 +859,34 @@ public IEnumerable GetSpendableTransactions(int currentC foreach (TransactionData transactionData in address.UnspentTransactions()) { int? confirmationCount = 0; + if (transactionData.BlockHeight != null) + { confirmationCount = countFrom >= transactionData.BlockHeight ? countFrom - transactionData.BlockHeight : 0; + } if (confirmationCount < confirmations) + { continue; + } bool isCoinBase = transactionData.IsCoinBase ?? false; bool isCoinStake = transactionData.IsCoinStake ?? false; + // Check if this wallet is a normal purpose wallet (not cold staking, etc). + if (this.IsNormalAccount()) + { + bool isColdCoinStake = transactionData.IsColdCoinStake ?? false; + + // Skip listing the UTXO if this is a normal wallet, and the UTXO is marked as an cold coin stake. + if (isColdCoinStake) + { + continue; + } + } + // This output can unconditionally be included in the results. - // Or this output is a CoinBase or CoinStake and has reached maturity. + // Or this output is a ColdStake, CoinBase or CoinStake and has reached maturity. if ((!isCoinBase && !isCoinStake) || (confirmationCount > coinbaseMaturity)) { yield return new UnspentOutputReference @@ -939,9 +1003,9 @@ public IEnumerable UnspentTransactions() /// /// Get the address total spendable value for both confirmed and unconfirmed UTXO. /// - public (Money confirmedAmount, Money unConfirmedAmount) GetBalances() + public (Money confirmedAmount, Money unConfirmedAmount) GetBalances(bool excludeColdStakeUtxo) { - List allTransactions = this.Transactions.ToList(); + List allTransactions = excludeColdStakeUtxo ? this.Transactions.Where(t => t.IsColdCoinStake != true).ToList() : this.Transactions.ToList(); long confirmed = allTransactions.Sum(t => t.GetUnspentAmount(true)); long total = allTransactions.Sum(t => t.GetUnspentAmount(false)); @@ -981,6 +1045,12 @@ public class TransactionData [JsonProperty(PropertyName = "isCoinStake", NullValueHandling = NullValueHandling.Ignore)] public bool? IsCoinStake { get; set; } + /// + /// A value indicating whether this is a coldstake transaction or not. + /// + [JsonProperty(PropertyName = "isColdCoinStake", NullValueHandling = NullValueHandling.Ignore)] + public bool? IsColdCoinStake { get; set; } + /// /// The index of this scriptPubKey in the transaction it is contained. /// diff --git a/src/Features/Blockcore.Features.Wallet/WalletManager.cs b/src/Features/Blockcore.Features.Wallet/WalletManager.cs index 7cad85cc0..8668ebf85 100644 --- a/src/Features/Blockcore.Features.Wallet/WalletManager.cs +++ b/src/Features/Blockcore.Features.Wallet/WalletManager.cs @@ -10,6 +10,7 @@ using Blockcore.Features.Wallet.Broadcasting; using Blockcore.Features.Wallet.Interfaces; using Blockcore.Interfaces; +using Blockcore.Signals; using Blockcore.Utilities; using Blockcore.Utilities.Extensions; using Microsoft.Extensions.Caching.Memory; @@ -94,7 +95,10 @@ public class WalletManager : IWalletManager /// The private key cache for unlocked wallets. private readonly MemoryCache privateKeyCache; + private readonly ISignals signals; + public uint256 WalletTipHash { get; set; } + public int WalletTipHeight { get; set; } // In order to allow faster look-ups of transactions affecting the wallets' addresses, @@ -118,6 +122,7 @@ public WalletManager( INodeLifetime nodeLifetime, IDateTimeProvider dateTimeProvider, IScriptAddressReader scriptAddressReader, + ISignals signals = null, IBroadcasterManager broadcasterManager = null) // no need to know about transactions the node will broadcast to. { Guard.NotNull(loggerFactory, nameof(loggerFactory)); @@ -142,6 +147,7 @@ public WalletManager( this.asyncProvider = asyncProvider; this.nodeLifetime = nodeLifetime; this.fileStorage = new FileStorage(dataFolder.WalletPath); + this.signals = signals; this.broadcasterManager = broadcasterManager; this.scriptAddressReader = scriptAddressReader; this.dateTimeProvider = dateTimeProvider; @@ -694,12 +700,21 @@ public AccountHistory GetHistory(HdAccount account) { Guard.NotNull(account, nameof(account)); FlatHistory[] items; + lock (this.lockObject) { // Get transactions contained in the account. - items = account.GetCombinedAddresses() - .Where(a => a.Transactions.Any()) - .SelectMany(s => s.Transactions.Select(t => new FlatHistory { Address = s, Transaction = t })).ToArray(); + var query = account.GetCombinedAddresses().Where(a => a.Transactions.Any()); + + if (account.IsNormalAccount()) + { + // When the account is a normal one, we want to filter out all cold stake UTXOs. + items = query.SelectMany(s => s.Transactions.Where(t => t.IsColdCoinStake == null || t.IsColdCoinStake == false).Select(t => new FlatHistory { Address = s, Transaction = t })).ToArray(); + } + else + { + items = query.SelectMany(s => s.Transactions.Select(t => new FlatHistory { Address = s, Transaction = t })).ToArray(); + } } return new AccountHistory { Account = account, History = items }; @@ -739,7 +754,7 @@ public IEnumerable GetBalances(string walletName, string account } // Get the total balances. - (Money amountConfirmed, Money amountUnconfirmed) result = account.GetBalances(); + (Money amountConfirmed, Money amountUnconfirmed) result = account.GetBalances(account.IsNormalAccount()); balances.Add(new AccountBalance { @@ -774,7 +789,8 @@ public AddressBalance GetAddressBalance(string address) hdAddress = wallet.GetAllAddresses().FirstOrDefault(a => a.Address == address); if (hdAddress == null) continue; - (Money amountConfirmed, Money amountUnconfirmed) result = hdAddress.GetBalances(); + // When this query to get balance on specific address, we will exclude the cold staking UTXOs. + (Money amountConfirmed, Money amountUnconfirmed) result = hdAddress.GetBalances(true); Money spendableAmount = wallet .GetAllSpendableTransactions(this.ChainIndexer.Tip.Height) @@ -1071,6 +1087,11 @@ public virtual bool ProcessTransaction(Transaction transaction, int? blockHeight this.AddTransactionToWallet(transaction, utxo, blockHeight, block, isPropagated); foundReceivingTrx = true; this.logger.LogDebug("Transaction '{0}' contained funds received by the user's wallet(s).", hash); + + if (this.signals != null) + { + this.signals.Publish(new Events.TransactionFound(transaction)); + } } } @@ -1127,6 +1148,10 @@ private void AddTransactionToWallet(Transaction transaction, TxOut utxo, int? bl uint256 transactionHash = transaction.GetHash(); + // Get the ColdStaking script template if available. + Dictionary templates = this.GetValidStakingTemplates(); + ScriptTemplate coldStakingTemplate = templates.ContainsKey("ColdStaking") ? templates["ColdStaking"] : null; + // Get the collection of transactions to add to. Script script = utxo.ScriptPubKey; this.scriptToAddressLookup.TryGetValue(script, out HdAddress address); @@ -1145,6 +1170,7 @@ private void AddTransactionToWallet(Transaction transaction, TxOut utxo, int? bl Amount = amount, IsCoinBase = transaction.IsCoinBase == false ? (bool?)null : true, IsCoinStake = transaction.IsCoinStake == false ? (bool?)null : true, + IsColdCoinStake = (coldStakingTemplate != null && coldStakingTemplate.CheckScriptPubKey(script)) == false ? (bool?)null : true, BlockHeight = blockHeight, BlockHash = block?.GetHash(), BlockIndex = block?.Transactions.FindIndex(t => t.GetHash() == transactionHash), @@ -1755,7 +1781,7 @@ private void RemoveTransactionsByIds(IEnumerable transactionsIds) lock (this.lockObject) { - IEnumerable accounts = wallet.GetAccounts(); + IEnumerable accounts = wallet.GetAccounts(Wallet.AllAccounts); foreach (HdAccount account in accounts) { foreach (HdAddress address in account.GetCombinedAddresses()) diff --git a/src/Networks/City/City/Networks/CityMain.cs b/src/Networks/City/City/Networks/CityMain.cs index 7f41c5751..c0b75975e 100644 --- a/src/Networks/City/City/Networks/CityMain.cs +++ b/src/Networks/City/City/Networks/CityMain.cs @@ -4,6 +4,7 @@ using Blockcore.Features.Consensus.Rules.ProvenHeaderRules; using Blockcore.Features.Consensus.Rules.UtxosetRules; using Blockcore.Features.MemoryPool.Rules; +using Blockcore.Networks.City.Consensus; using City.Networks.Policies; using City.Networks.Rules; using NBitcoin; @@ -49,8 +50,7 @@ public CityMain() Genesis = genesisBlock; - // Taken from StratisX. - var consensusOptions = new PosConsensusOptions( + var consensusOptions = new CityPosConsensusOptions( maxBlockBaseSize: 1_000_000, maxStandardVersion: 2, maxStandardTxWeight: 100_000, diff --git a/src/Networks/City/City/Networks/Consensus/CityPosConsensusOptions.cs b/src/Networks/City/City/Networks/Consensus/CityPosConsensusOptions.cs new file mode 100644 index 000000000..6cdb1945a --- /dev/null +++ b/src/Networks/City/City/Networks/Consensus/CityPosConsensusOptions.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.Text; +using NBitcoin; + +namespace Blockcore.Networks.City.Consensus +{ + public class CityPosConsensusOptions : PosConsensusOptions + { + /// Coinstake minimal confirmations softfork activation height for mainnet. + public const int CityCoinstakeMinConfirmationActivationHeightMainnet = 500000; + + /// Coinstake minimal confirmations softfork activation height for testnet. + public const int CityCoinstakeMinConfirmationActivationHeightTestnet = 15000; + + /// + /// Initializes the default values. + /// + public CityPosConsensusOptions() + { + } + + /// + /// Initializes all values. Used by networks that use block weight rules. + /// + public CityPosConsensusOptions( + uint maxBlockBaseSize, + uint maxBlockWeight, + uint maxBlockSerializedSize, + int witnessScaleFactor, + int maxStandardVersion, + int maxStandardTxWeight, + int maxBlockSigopsCost, + int maxStandardTxSigopsCost) : base(maxBlockBaseSize, maxBlockWeight, maxBlockSerializedSize, witnessScaleFactor, maxStandardVersion, maxStandardTxWeight, maxBlockSigopsCost, maxStandardTxSigopsCost) + { + } + + /// + /// Initializes values for networks that use block size rules. + /// + public CityPosConsensusOptions( + uint maxBlockBaseSize, + int maxStandardVersion, + int maxStandardTxWeight, + int maxBlockSigopsCost, + int maxStandardTxSigopsCost, + int witnessScaleFactor + ) : base(maxBlockBaseSize, maxStandardVersion, maxStandardTxWeight, maxBlockSigopsCost, maxStandardTxSigopsCost, witnessScaleFactor) + { + } + + public override int GetStakeMinConfirmations(int height, Network network) + { + if (network.Name.ToLowerInvariant().Contains("test")) + { + return height < CityCoinstakeMinConfirmationActivationHeightTestnet ? 10 : 20; + } + + // The coinstake confirmation minimum should be 50 until activation at height 500K (~347 days). + return height < CityCoinstakeMinConfirmationActivationHeightMainnet ? 50 : 500; + } + } +} diff --git a/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingControllerTest.cs b/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingControllerTest.cs index a8bf64c0a..bdded5c75 100644 --- a/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingControllerTest.cs +++ b/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingControllerTest.cs @@ -591,6 +591,62 @@ public void SetupColdStakingWithHotWalletSegwitSucceeds() Assert.True(this.mempoolManager.Validator.AcceptToMemoryPool(state, transaction).GetAwaiter().GetResult(), "Transaction failed mempool validation."); } + [Fact] + public void VerifyThatColdStakeTransactionCanBeFiltered() + { + this.Initialize(); + this.CreateMempoolManager(); + + this.coldStakingManager.CreateWallet(walletPassword, walletName1, walletPassphrase, new Mnemonic(walletMnemonic1)); + + Wallet.Wallet wallet1 = this.coldStakingManager.GetWalletByName(walletName1); + + // This will add a normal account to our wallet. + Transaction trx1 = this.AddSpendableTransactionToWallet(wallet1); + + // This will add a secondary account to our wallet. + Transaction trx2 = this.AddSpendableColdstakingTransactionToWallet(wallet1); + + // THis will add a cold staking transaction to the secondary normal account address. This simulates activation of cold staking onto any normal address. + Transaction trx3 = this.AddSpendableColdstakingTransactionToNormalWallet(wallet1); + + var accounts = wallet1.GetAccounts(Wallet.Wallet.AllAccounts).ToArray(); + + // We should have 2 accounts in our wallet. + Assert.Equal(2, accounts.Length); + + // But not if we use default or specify to only return normal accounts. + Assert.Single(wallet1.GetAccounts().ToArray()); // Defaults to NormalAccounts + Assert.Single(wallet1.GetAccounts(Wallet.Wallet.NormalAccounts).ToArray()); + + // Verify that we actually have an cold staking activation UTXO in the wallet of 202 coins. + // This should normally not be returned by the GetAllTransactions, and should never be included in balance calculations. + Assert.True(accounts[0].ExternalAddresses.ToArray()[1].Transactions.ToArray()[0].IsColdCoinStake); + Assert.Equal(new Money(202, MoneyUnit.BTC), accounts[0].ExternalAddresses.ToArray()[1].Transactions.ToArray()[0].Amount); + + Assert.Single(wallet1.GetAllTransactions().ToArray()); // Default to NormalAccounts, should filter out cold staking (trx3) from normal wallet. + Assert.Single(wallet1.GetAllTransactions(Wallet.Wallet.NormalAccounts).ToArray()); + Assert.Single(wallet1.GetAllSpendableTransactions(5, 0, Wallet.Wallet.NormalAccounts).ToArray()); // Default to NormalAccounts + Assert.Equal(2, wallet1.GetAllTransactions(Wallet.Wallet.AllAccounts).ToArray().Length); + Assert.Equal(2, wallet1.GetAllSpendableTransactions(5, 0, Wallet.Wallet.AllAccounts).ToArray().Length); // Specified AllAccounts, should include cold-staking transaction. + + // Verify balance on normal account + var balance1 = accounts[0].GetBalances(true); + var balance2 = accounts[0].GetBalances(false); + + Assert.Equal(new Money(101, MoneyUnit.BTC), balance1.ConfirmedAmount); + Assert.Equal(new Money(303, MoneyUnit.BTC), balance2.ConfirmedAmount); + + // Verify balance on special account. + // Verify balance on normal account + var balance3 = accounts[1].GetBalances(true); + var balance4 = accounts[1].GetBalances(false); + + // The only transaction that exists in the cold staking wallet, is a normal one, and should be returned for both balance queries. + Assert.Equal(new Money(101, MoneyUnit.BTC), balance3.ConfirmedAmount); + Assert.Equal(new Money(101, MoneyUnit.BTC), balance4.ConfirmedAmount); + } + /// /// Confirms that cold staking setup with the cold wallet will succeed if no issues (as per above test cases) are encountered. /// @@ -820,6 +876,49 @@ private Transaction AddSpendableColdstakingTransactionToWallet(Wallet.Wallet wal return transaction; } + /// + /// Adds a spendable cold staking transaction to a normal account, as oppose to dedicated special account. + /// + /// Wallet to add the transaction to. + /// The spendable transaction that was added to the wallet. + private Transaction AddSpendableColdstakingTransactionToNormalWallet(Wallet.Wallet wallet, bool script = false) + { + // This will always be added to the secondary address. + HdAddress address = wallet.GetAllAddresses().ToArray()[1]; + + var transaction = this.Network.CreateTransaction(); + + // Use the normal wallet address here. + TxDestination hotPubKey = BitcoinAddress.Create(address.Address, wallet.Network).ScriptPubKey.GetDestination(wallet.Network); + TxDestination coldPubKey = BitcoinAddress.Create(coldWalletAddress2, wallet.Network).ScriptPubKey.GetDestination(wallet.Network); + + var scriptPubKey = new Script(OpcodeType.OP_DUP, OpcodeType.OP_HASH160, OpcodeType.OP_ROT, OpcodeType.OP_IF, + OpcodeType.OP_CHECKCOLDSTAKEVERIFY, Op.GetPushOp(hotPubKey.ToBytes()), OpcodeType.OP_ELSE, Op.GetPushOp(coldPubKey.ToBytes()), + OpcodeType.OP_ENDIF, OpcodeType.OP_EQUALVERIFY, OpcodeType.OP_CHECKSIG); + + transaction.Outputs.Add(new TxOut(Money.Coins(202), script ? scriptPubKey.WitHash.ScriptPubKey : scriptPubKey)); + + if (script) + address.RedeemScript = scriptPubKey; + + address.Transactions.Add(new TransactionData() + { + Hex = transaction.ToHex(this.Network), + Amount = transaction.Outputs[0].Value, + Id = transaction.GetHash(), + BlockHeight = 0, + Index = 0, + IsCoinBase = false, + IsCoinStake = false, + IsColdCoinStake = true, + IsPropagated = true, + BlockHash = this.Network.GenesisHash, + ScriptPubKey = script ? scriptPubKey.WitHash.ScriptPubKey : scriptPubKey, + }); + + return transaction; + } + /// /// Confirms that cold staking setup with the cold wallet will succeed if no issues (as per above test cases) are encountered. /// diff --git a/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingManagerTest.cs b/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingManagerTest.cs index 93589cd83..ccf0bba57 100644 --- a/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingManagerTest.cs +++ b/src/Tests/Blockcore.Features.ColdStaking.Tests/ColdStakingManagerTest.cs @@ -7,6 +7,7 @@ using Blockcore.Features.Wallet; using Blockcore.Features.Wallet.Interfaces; using Blockcore.Interfaces; +using Blockcore.Signals; using Blockcore.Tests.Common.Logging; using Blockcore.Tests.Wallet.Common; using Blockcore.Utilities; @@ -190,14 +191,14 @@ public void ProcessTransactionWithValidColdStakingSetupLoadsTransactionsIntoWall var walletSettings = new WalletSettings(new NodeSettings(network: this.Network)); var coldWalletManager = new ColdStakingManager(this.Network, chainInfo.chain, walletSettings, dataFolder, walletFeePolicy.Object, - new Mock().Object, new NodeLifetime(), new ScriptAddressReader(), this.LoggerFactory.Object, DateTimeProvider.Default, new Mock().Object); + new Mock().Object, new NodeLifetime(), new ScriptAddressReader(), this.LoggerFactory.Object, DateTimeProvider.Default, new Mock().Object, new Mock().Object); coldWalletManager.Wallets.Add(wallet); coldWalletManager.Wallets.Add(coldWallet); coldWalletManager.LoadKeysLookupLock(); // Create another instance for the hot wallet as it is not allowed to have both wallets on the same instance. var hotWalletManager = new ColdStakingManager(this.Network, chainInfo.chain, walletSettings, dataFolder, walletFeePolicy.Object, - new Mock().Object, new NodeLifetime(), new ScriptAddressReader(), this.LoggerFactory.Object, DateTimeProvider.Default, new Mock().Object); + new Mock().Object, new NodeLifetime(), new ScriptAddressReader(), this.LoggerFactory.Object, DateTimeProvider.Default, new Mock().Object, new Mock().Object); hotWalletManager.Wallets.Add(hotWallet); hotWalletManager.LoadKeysLookupLock(); @@ -269,7 +270,7 @@ public void ProcessTransactionWithValidColdStakingSetupLoadsTransactionsIntoWall // Wallet manager for the wallet receiving the funds. var receivingWalletManager = new ColdStakingManager(this.Network, chainInfo.chain, walletSettings, dataFolder, walletFeePolicy.Object, - new Mock().Object, new NodeLifetime(), new ScriptAddressReader(), this.LoggerFactory.Object, DateTimeProvider.Default, new Mock().Object); + new Mock().Object, new NodeLifetime(), new ScriptAddressReader(), this.LoggerFactory.Object, DateTimeProvider.Default, new Mock().Object, new Mock().Object); receivingWalletManager.Wallets.Add(withdrawalWallet); receivingWalletManager.LoadKeysLookupLock(); diff --git a/src/Tests/Blockcore.Features.Wallet.Tests/WalletManagerTest.cs b/src/Tests/Blockcore.Features.Wallet.Tests/WalletManagerTest.cs index 7d6346a85..0b261a0bb 100644 --- a/src/Tests/Blockcore.Features.Wallet.Tests/WalletManagerTest.cs +++ b/src/Tests/Blockcore.Features.Wallet.Tests/WalletManagerTest.cs @@ -2475,8 +2475,8 @@ public void CheckWalletBalanceEstimationWithConfirmedTransactions() firstAccount.ExternalAddresses.ElementAt(i).Transactions.Add(new TransactionData { Amount = 10 }); } - Assert.Equal(0, firstAccount.GetBalances().ConfirmedAmount); - Assert.Equal(40, firstAccount.GetBalances().UnConfirmedAmount); + Assert.Equal(0, firstAccount.GetBalances(firstAccount.IsNormalAccount()).ConfirmedAmount); + Assert.Equal(40, firstAccount.GetBalances(firstAccount.IsNormalAccount()).UnConfirmedAmount); } [Fact] @@ -2569,8 +2569,8 @@ public void CheckWalletBalanceEstimationWithUnConfirmedTransactions() firstAccount.ExternalAddresses.ElementAt(i).Transactions.Add(new TransactionData { Amount = 10, BlockHeight = 10 }); } - Assert.Equal(40, firstAccount.GetBalances().ConfirmedAmount); - Assert.Equal(0, firstAccount.GetBalances().UnConfirmedAmount); + Assert.Equal(40, firstAccount.GetBalances(firstAccount.IsNormalAccount()).ConfirmedAmount); + Assert.Equal(0, firstAccount.GetBalances(firstAccount.IsNormalAccount()).UnConfirmedAmount); } [Fact] @@ -2594,8 +2594,8 @@ public void CheckWalletBalanceEstimationWithSpentTransactions() firstAccount.ExternalAddresses.ElementAt(i).Transactions.Add(new TransactionData { Amount = 10, BlockHeight = 10, SpendingDetails = new SpendingDetails() }); } - Assert.Equal(0, firstAccount.GetBalances().ConfirmedAmount); - Assert.Equal(0, firstAccount.GetBalances().UnConfirmedAmount); + Assert.Equal(0, firstAccount.GetBalances(firstAccount.IsNormalAccount()).ConfirmedAmount); + Assert.Equal(0, firstAccount.GetBalances(firstAccount.IsNormalAccount()).UnConfirmedAmount); } [Fact] @@ -2625,8 +2625,8 @@ public void CheckWalletBalanceEstimationWithSpentAndConfirmedTransactions() firstAccount.ExternalAddresses.ElementAt(i).Transactions.Add(new TransactionData { Amount = 10, BlockHeight = 10 }); } - Assert.Equal(40, firstAccount.GetBalances().ConfirmedAmount); - Assert.Equal(0, firstAccount.GetBalances().UnConfirmedAmount); + Assert.Equal(40, firstAccount.GetBalances(firstAccount.IsNormalAccount()).ConfirmedAmount); + Assert.Equal(0, firstAccount.GetBalances(firstAccount.IsNormalAccount()).UnConfirmedAmount); } [Fact] @@ -2656,8 +2656,8 @@ public void CheckWalletBalanceEstimationWithSpentAndUnConfirmedTransactions() firstAccount.ExternalAddresses.ElementAt(i).Transactions.Add(new TransactionData { Amount = 10 }); } - Assert.Equal(0, firstAccount.GetBalances().ConfirmedAmount); - Assert.Equal(40, firstAccount.GetBalances().UnConfirmedAmount); + Assert.Equal(0, firstAccount.GetBalances(firstAccount.IsNormalAccount()).ConfirmedAmount); + Assert.Equal(40, firstAccount.GetBalances(firstAccount.IsNormalAccount()).UnConfirmedAmount); } [Fact] diff --git a/src/Tests/Blockcore.IntegrationTests/API/ApiSteps.cs b/src/Tests/Blockcore.IntegrationTests/API/ApiSteps.cs index c4b7e6fce..d75dccd43 100644 --- a/src/Tests/Blockcore.IntegrationTests/API/ApiSteps.cs +++ b/src/Tests/Blockcore.IntegrationTests/API/ApiSteps.cs @@ -433,36 +433,43 @@ private void a_full_list_of_available_commands_is_returned() { var commands = JsonDataSerializer.Instance.Deserialize>(this.responseText); - commands.Count.Should().Be(33); + commands.Count.Should().Be(35); + + commands.Count.Should().Be(35); commands.Should().Contain(x => x.Command == "stop"); commands.Should().Contain(x => x.Command == "getrawtransaction [] []"); + commands.Should().Contain(x => x.Command == "decoderawtransaction "); commands.Should().Contain(x => x.Command == "gettxout []"); commands.Should().Contain(x => x.Command == "getblockcount"); commands.Should().Contain(x => x.Command == "getinfo"); commands.Should().Contain(x => x.Command == "getblockheader []"); commands.Should().Contain(x => x.Command == "validateaddress
"); + commands.Should().Contain(x => x.Command == "getblock []"); + commands.Should().Contain(x => x.Command == "getnetworkinfo"); + commands.Should().Contain(x => x.Command == "getblockchaininfo"); commands.Should().Contain(x => x.Command == "addnode "); commands.Should().Contain(x => x.Command == "getpeerinfo"); commands.Should().Contain(x => x.Command == "getbestblockhash"); commands.Should().Contain(x => x.Command == "getblockhash "); commands.Should().Contain(x => x.Command == "getrawmempool"); - commands.Should().Contain(x => x.Command == "generate "); - commands.Should().Contain(x => x.Command == "startstaking "); - commands.Should().Contain(x => x.Command == "getstakinginfo []"); + commands.Should().Contain(x => x.Command == "setwallet "); + commands.Should().Contain(x => x.Command == "walletpassphrase "); + commands.Should().Contain(x => x.Command == "walletlock"); + commands.Should().Contain(x => x.Command == "getwalletinfo"); commands.Should().Contain(x => x.Command == "sendtoaddress
"); + commands.Should().Contain(x => x.Command == "sendrawtransaction "); commands.Should().Contain(x => x.Command == "getnewaddress [] []"); commands.Should().Contain(x => x.Command == "getunusedaddress "); - commands.Should().Contain(x => x.Command == "sendrawtransaction "); - commands.Should().Contain(x => x.Command == "decoderawtransaction "); - commands.Should().Contain(x => x.Command == "getblock []"); - commands.Should().Contain(x => x.Command == "walletlock"); - commands.Should().Contain(x => x.Command == "walletpassphrase "); - commands.Should().Contain(x => x.Command == "listunspent [] [] []"); + commands.Should().Contain(x => x.Command == "getbalance []"); commands.Should().Contain(x => x.Command == "listsinceblock []"); - commands.Should().Contain(x => x.Command == "sendmany [] [] [] [] [] []"); - commands.Should().Contain(x => x.Command == "getblockchaininfo"); - commands.Should().Contain(x => x.Command == "getnetworkinfo"); + commands.Should().Contain(x => x.Command == "gettransaction "); commands.Should().Contain(x => x.Command == "listaddressgroupings"); + commands.Should().Contain(x => x.Command == "listunspent [] [] []"); + commands.Should().Contain(x => x.Command == "sendmany [] [] [] [] [] []"); + commands.Should().Contain(x => x.Command == "generate "); + commands.Should().Contain(x => x.Command == "generatetoaddress
"); + commands.Should().Contain(x => x.Command == "startstaking "); + commands.Should().Contain(x => x.Command == "getstakinginfo []"); } private void status_information_is_returned()