/// <summary> /// Lists all unspent transactions from all accounts in the wallet. /// This is distinct from the list of spendable transactions. A transaction can be unspent but not yet spendable due to coinbase/stake maturity, for example. /// </summary> /// <param name="currentChainHeight">Height of the current chain, used in calculating the number of confirmations.</param> /// <param name="confirmations">The number of confirmations required to consider a transaction spendable.</param> /// <param name="accountFilter">An optional filter for filtering the accounts being returned.</param> /// <returns>A collection of spendable outputs.</returns> public IEnumerable <UnspentOutputReference> GetAllUnspentTransactions(IWalletStore walletStore, int currentChainHeight, int confirmations = 0, Func <HdAccount, bool> accountFilter = null) { IEnumerable <HdAccount> accounts = this.GetAccounts(accountFilter); // The logic for retrieving unspent transactions is almost identical to determining spendable transactions, we just don't take coinbase/stake maturity into consideration. return(accounts.SelectMany(x => x.GetSpendableTransactions(walletStore, currentChainHeight, 0, confirmations))); }
/// <summary> /// Lists all spendable transactions in the current account. /// </summary> /// <param name="currentChainHeight">The current height of the chain. Used for calculating the number of confirmations a transaction has.</param> /// <param name="coinbaseMaturity">The coinbase maturity after which a coinstake transaction is spendable.</param> /// <param name="confirmations">The minimum number of confirmations required for transactions to be considered.</param> /// <returns>A collection of spendable outputs that belong to the given account.</returns> /// <remarks>Note that coinbase and coinstake transaction outputs also have to mature with a sufficient number of confirmations before /// they are considered spendable. This is independent of the confirmations parameter.</remarks> public IEnumerable <UnspentOutputReference> GetSpendableTransactions(IWalletStore walletStore, int currentChainHeight, long coinbaseMaturity, int confirmations = 0) { // This will take all the spendable coins that belong to the account and keep the reference to the HdAddress and HdAccount. // This is useful so later the private key can be calculated just from a given UTXO. foreach (HdAddress address in this.GetCombinedAddresses()) { // A block that is at the tip has 1 confirmation. // When calculating the confirmations the tip must be advanced by one. int countFrom = currentChainHeight + 1; foreach (TransactionOutputData transactionData in address.UnspentTransactions(walletStore)) { 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 ColdStake, CoinBase or CoinStake and has reached maturity. if ((!isCoinBase && !isCoinStake) || (confirmationCount > coinbaseMaturity)) { yield return(new UnspentOutputReference { Account = this, Address = address, Transaction = transactionData, Confirmations = confirmationCount.Value }); } } } }
/// <summary> /// Gets the first account that contains no transaction. /// </summary> /// <returns>An unused account.</returns> public HdAccount GetFirstUnusedAccount(IWalletStore walletStore) { // Get the accounts root for this type of coin. AccountRoot accountsRoot = this.AccountsRoot.Single(); if (accountsRoot.Accounts.Any()) { // Get an unused account. HdAccount firstUnusedAccount = accountsRoot.GetFirstUnusedAccount(walletStore); if (firstUnusedAccount != null) { return(firstUnusedAccount); } } return(null); }
/// <summary> /// Gets the last address that contains transactions. /// </summary> /// <param name="isChange">Whether the address is a change (internal) address or receiving (external) address.</param> /// <returns></returns> public HdAddress GetLastUsedAddress(IWalletStore walletStore, bool isChange) { IEnumerable <HdAddress> addresses = isChange ? this.InternalAddresses : this.ExternalAddresses; if (addresses == null) { return(null); } List <HdAddress> usedAddresses = addresses.Where(acc => walletStore.CountForAddress(acc.Address) > 0).ToList(); if (!usedAddresses.Any()) { return(null); } // gets the used address with the highest index int index = usedAddresses.Max(a => a.Index); return(usedAddresses.Single(a => a.Index == index)); }
/// <summary> /// Gets the first receiving address that contains no transaction. /// </summary> /// <returns>An unused address</returns> private HdAddress GetFirstUnusedAddress(IWalletStore walletStore, bool isChange) { IEnumerable <HdAddress> addresses = isChange ? this.InternalAddresses : this.ExternalAddresses; if (addresses == null) { return(null); } List <HdAddress> unusedAddresses = addresses.Where(acc => walletStore.CountForAddress(acc.Address) == 0).ToList(); if (!unusedAddresses.Any()) { return(null); } // gets the unused address with the lowest index int index = unusedAddresses.Min(a => a.Index); return(unusedAddresses.Single(a => a.Index == index)); }
/// <summary> /// Gets the first account that contains no transaction. /// </summary> /// <returns>An unused account</returns> public HdAccount GetFirstUnusedAccount(IWalletStore walletStore) { if (this.Accounts == null) { return(null); } List <HdAccount> unusedAccounts = this.Accounts .Where(Wallet.NormalAccounts) .Where(acc => !acc.ExternalAddresses.SelectMany(add => walletStore.GetForAddress(add.Address)).Any() && !acc.InternalAddresses.SelectMany(add => walletStore.GetForAddress(add.Address)).Any()).ToList(); if (!unusedAccounts.Any()) { return(null); } // gets the unused account with the lowest index int index = unusedAccounts.Min(a => a.Index); return(unusedAccounts.Single(a => a.Index == index)); }
/// <summary> /// Get the accounts total spendable value for both confirmed and unconfirmed UTXO. /// </summary> public (Money ConfirmedAmount, Money UnConfirmedAmount) GetBalances(IWalletStore walletStore, bool excludeColdStakeUtxo) { var res = walletStore.GetBalanceForAccount(this.Index, excludeColdStakeUtxo); return(res.AmountConfirmed, res.AmountUnconfirmed); }
/// <summary> /// Gets the first change address that contains no transaction. /// </summary> /// <returns>An unused address</returns> public HdAddress GetFirstUnusedChangeAddress(IWalletStore walletStore) { return(this.GetFirstUnusedAddress(walletStore, true)); }
/// <summary> /// Gets the first receiving address that contains no transaction. /// </summary> /// <returns>An unused address</returns> public HdAddress GetFirstUnusedReceivingAddress(IWalletStore walletStore) { return(this.GetFirstUnusedAddress(walletStore, false)); }
/// <summary> /// Lists all spendable transactions from all accounts in the wallet. /// </summary> /// <param name="currentChainHeight">Height of the current chain, used in calculating the number of confirmations.</param> /// <param name="confirmations">The number of confirmations required to consider a transaction spendable.</param> /// <param name="accountFilter">An optional filter for filtering the accounts being returned.</param> /// <returns>A collection of spendable outputs.</returns> public IEnumerable <UnspentOutputReference> GetAllSpendableTransactions(IWalletStore walletStore, int currentChainHeight, int confirmations = 0, Func <HdAccount, bool> accountFilter = null) { IEnumerable <HdAccount> accounts = this.GetAccounts(accountFilter); return(accounts.SelectMany(x => x.GetSpendableTransactions(walletStore, currentChainHeight, this.Network.Consensus.CoinbaseMaturity, confirmations))); }
/// <summary> /// Get the address total spendable value for both confirmed and unconfirmed UTXO. /// </summary> public (Money confirmedAmount, Money unConfirmedAmount, bool anyTrx) GetBalances(IWalletStore walletStore, bool excludeColdStakeUtxo) { var trx = walletStore.GetForAddress(this.Address).ToList(); List <TransactionOutputData> allTransactions = excludeColdStakeUtxo ? trx.Where(t => t.IsColdCoinStake != true).ToList() : trx; long confirmed = allTransactions.Sum(t => t.GetUnspentAmount(true)); long total = allTransactions.Sum(t => t.GetUnspentAmount(false)); return(confirmed, total - confirmed, trx.Any()); }
/// <summary> /// List all spendable transactions in an address. /// </summary> /// <returns>List of spendable transactions.</returns> public IEnumerable <TransactionOutputData> UnspentTransactions(IWalletStore walletStore) { return(walletStore.GetUnspentForAddress(this.Address)); }
public void WalletCanMineWithColdWalletCoins() { using (NodeBuilder builder = NodeBuilder.Create(this)) { var network = new StratisRegTest(); CoreNode stratisSender = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: false); CoreNode stratisHotStake = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: true); CoreNode stratisColdStake = CreatePowPosMiningNode(builder, network, TestBase.CreateTestDir(this), coldStakeNode: true); stratisSender.WithWallet().Start(); stratisHotStake.WithWallet().Start(); stratisColdStake.WithWallet().Start(); var senderWalletManager = stratisSender.FullNode.WalletManager() as ColdStakingManager; var coldWalletManager = stratisColdStake.FullNode.WalletManager() as ColdStakingManager; var hotWalletManager = stratisHotStake.FullNode.WalletManager() as ColdStakingManager; // Set up cold staking account on cold wallet. coldWalletManager.GetOrCreateColdStakingAccount(WalletName, true, Password); HdAddress coldWalletAddress = coldWalletManager.GetFirstUnusedColdStakingAddress(WalletName, true); IWalletStore walletStore = coldWalletManager.GetWalletByName(WalletName).walletStore; // Set up cold staking account on hot wallet. hotWalletManager.GetOrCreateColdStakingAccount(WalletName, false, Password); HdAddress hotWalletAddress = hotWalletManager.GetFirstUnusedColdStakingAddress(WalletName, false); int maturity = (int)stratisSender.FullNode.Network.Consensus.CoinbaseMaturity; TestHelper.MineBlocks(stratisSender, maturity + 16, true); // The mining should add coins to the wallet long total = stratisSender.FullNode.WalletManager().GetSpendableTransactionsInWallet(WalletName).Sum(s => s.Transaction.Amount); Assert.Equal(Money.COIN * 98000060, total); int confirmations = 10; var walletAccountReference = new WalletAccountReference(WalletName, Account); long total2 = stratisSender.FullNode.WalletManager().GetSpendableTransactionsInAccount(walletAccountReference, confirmations).Sum(s => s.Transaction.Amount); // Sync all nodes TestHelper.ConnectAndSync(stratisHotStake, stratisSender); TestHelper.ConnectAndSync(stratisHotStake, stratisColdStake); TestHelper.Connect(stratisSender, stratisColdStake); // Send coins to hot wallet. Money amountToSend = Money.COIN * 98000059; HdAddress sendto = hotWalletManager.GetUnusedAddress(new WalletAccountReference(WalletName, Account)); Transaction transaction1 = stratisSender.FullNode.WalletTransactionHandler().BuildTransaction(CreateContext(stratisSender.FullNode.Network, new WalletAccountReference(WalletName, Account), Password, sendto.ScriptPubKey, amountToSend, FeeType.Medium, confirmations)); // Broadcast to the other node stratisSender.FullNode.NodeController <WalletController>().SendTransaction(new SendTransactionRequest(transaction1.ToHex())); // Wait for the transaction to arrive TestBase.WaitLoop(() => stratisHotStake.CreateRPCClient().GetRawMempool().Length > 0); Assert.NotNull(stratisHotStake.CreateRPCClient().GetRawTransaction(transaction1.GetHash(), null, false)); TestBase.WaitLoop(() => stratisHotStake.FullNode.WalletManager().GetSpendableTransactionsInWallet(WalletName).Any()); long receivetotal = stratisHotStake.FullNode.WalletManager().GetSpendableTransactionsInWallet(WalletName).Sum(s => s.Transaction.Amount); Assert.Equal(amountToSend, (Money)receivetotal); Assert.Null(stratisHotStake.FullNode.WalletManager().GetSpendableTransactionsInWallet(WalletName).First().Transaction.BlockHeight); // Setup cold staking from the hot wallet. Money amountToSend2 = Money.COIN * 98000058; Transaction transaction2 = hotWalletManager.GetColdStakingSetupTransaction(stratisHotStake.FullNode.WalletTransactionHandler(), coldWalletAddress.Address, hotWalletAddress.Address, WalletName, Account, Password, amountToSend2, new Money(0.02m, MoneyUnit.BTC)); // Broadcast to the other node stratisHotStake.FullNode.NodeController <WalletController>().SendTransaction(new SendTransactionRequest(transaction2.ToHex())); // Wait for the transaction to arrive TestBase.WaitLoop(() => coldWalletManager.GetSpendableTransactionsInColdWallet(WalletName, true).Any()); long receivetotal2 = coldWalletManager.GetSpendableTransactionsInColdWallet(WalletName, true).Sum(s => s.Transaction.Amount); Assert.Equal(amountToSend2, (Money)receivetotal2); Assert.Null(coldWalletManager.GetSpendableTransactionsInColdWallet(WalletName, true).First().Transaction.BlockHeight); // Allow coins to reach maturity TestHelper.MineBlocks(stratisSender, maturity, true); // Start staking. var hotMiningFeature = stratisHotStake.FullNode.NodeFeature <MiningFeature>(); hotMiningFeature.StartStaking(WalletName, Password); TestBase.WaitLoop(() => { var stakingInfo = stratisHotStake.FullNode.NodeService <IPosMinting>().GetGetStakingInfoModel(); return(stakingInfo.Staking); }); // Wait for new cold wallet transaction. var cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(3)).Token; TestBase.WaitLoop(() => { // Keep mining to ensure that staking outputs reach maturity. TestHelper.MineBlocks(stratisSender, 1, true); return(walletStore.CountForAddress(coldWalletAddress.Address) > 1); }, cancellationToken: cancellationToken); // Wait for money from staking. cancellationToken = new CancellationTokenSource(TimeSpan.FromMinutes(3)).Token; TestBase.WaitLoop(() => { // Keep mining to ensure that staking outputs reach maturity. TestHelper.MineBlocks(stratisSender, 1, true); return(coldWalletManager.GetSpendableTransactionsInColdWallet(WalletName, true).Sum(s => s.Transaction.Amount) > receivetotal2); }, cancellationToken: cancellationToken); } }