/// <inheritdoc /> public Mnemonic CreateWallet(string password, string name, string passphrase = null, string mnemonicList = null) { Guard.NotEmpty(password, nameof(password)); Guard.NotEmpty(name, nameof(name)); // for now the passphrase is set to be the password by default. if (passphrase == null) { passphrase = password; } // generate the root seed used to generate keys from a mnemonic picked at random // and a passphrase optionally provided by the user Mnemonic mnemonic = string.IsNullOrEmpty(mnemonicList) ? new Mnemonic(Wordlist.English, WordCount.Twelve) : new Mnemonic(mnemonicList); ExtKey extendedKey = HdOperations.GetHdPrivateKey(mnemonic, passphrase); // create a wallet file Wallet wallet = this.GenerateWalletFile(password, name, extendedKey); // generate multiple accounts and addresses from the get-go for (int i = 0; i < WalletCreationAccountsCount; i++) { HdAccount account = wallet.AddNewAccount(password, this.coinType); account.CreateAddresses(this.network, UnusedAddressesBuffer); account.CreateAddresses(this.network, UnusedAddressesBuffer, true); } // update the height of the we start syncing from this.UpdateLastBlockSyncedHeight(wallet, this.chain.Tip); // save the changes to the file and add addresses to be tracked this.SaveToFile(wallet); this.Load(wallet); this.LoadKeysLookup(); return(mnemonic); }
/// <inheritdoc /> public HdAccount CreateNewAccount(Wallet wallet, string password) { Guard.NotNull(wallet, nameof(wallet)); Guard.NotEmpty(password, nameof(password)); // get the accounts for this type of coin var accounts = wallet.AccountsRoot.Single(a => a.CoinType == this.coinType).Accounts.ToList(); int newAccountIndex = 0; if (accounts.Any()) { newAccountIndex = accounts.Max(a => a.Index) + 1; } // get the extended pub key used to generate addresses for this account var privateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network); var seedExtKey = new ExtKey(privateKey, wallet.ChainCode); var accountHdPath = $"m/44'/{(int)this.coinType}'/{newAccountIndex}'"; KeyPath keyPath = new KeyPath(accountHdPath); ExtKey accountExtKey = seedExtKey.Derive(keyPath); ExtPubKey accountExtPubKey = accountExtKey.Neuter(); var newAccount = new HdAccount { Index = newAccountIndex, ExtendedPubKey = accountExtPubKey.ToString(wallet.Network), ExternalAddresses = new List <HdAddress>(), InternalAddresses = new List <HdAddress>(), Name = $"account {newAccountIndex}", HdPath = accountHdPath, CreationTime = DateTimeOffset.Now }; accounts.Add(newAccount); wallet.AccountsRoot.Single(a => a.CoinType == this.coinType).Accounts = accounts; return(newAccount); }
/// <summary> /// Adds an account to the current account root using encrypted seed and password. /// </summary> /// <remarks>The name given to the account is of the form "account (i)" by default, where (i) is an incremental index starting at 0. /// According to BIP44, an account at index (i) can only be created when the account at index (i - 1) contains transactions. /// <seealso cref="https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki"/></remarks> /// <param name="password">The password used to decrypt the wallet's encrypted seed.</param> /// <param name="encryptedSeed">The encrypted private key for this wallet.</param> /// <param name="chainCode">The chain code for this wallet.</param> /// <param name="network">The network for which this account will be created.</param> /// <param name="accountCreationTime">Creation time of the account to be created.</param> /// <returns>A new HD account.</returns> public HdAccount AddNewAccount(string password, string encryptedSeed, byte[] chainCode, Network network, DateTimeOffset accountCreationTime) { Guard.NotEmpty(password, nameof(password)); Guard.NotEmpty(encryptedSeed, nameof(encryptedSeed)); Guard.NotNull(chainCode, nameof(chainCode)); int newAccountIndex = 0; ICollection <HdAccount> hdAccounts = this.Accounts.ToList(); if (hdAccounts.Any()) { // Hide account indexes used for cold staking from the "Max" calculation. newAccountIndex = hdAccounts.Where(Wallet.NormalAccounts).Max(a => a.Index) + 1; } HdAccount newAccount = this.CreateAccount(password, encryptedSeed, chainCode, network, accountCreationTime, newAccountIndex); hdAccounts.Add(newAccount); this.Accounts = hdAccounts; return(newAccount); }
/// <summary> /// Gets the first account from the "default" wallet if it specified, otherwise returns the first available account in the existing wallets. /// </summary> /// <returns>Reference to the default wallet account, or the first available if no default wallet is specified.</returns> private WalletAccountReference GetAccount() { string walletName; 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(); } if (walletName == null) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found"); } HdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); return(new WalletAccountReference(walletName, account.Name)); }
/// <inheritdoc /> public Wallet RecoverWallet(string password, string name, string mnemonic, DateTime creationTime, string passphrase = null) { Guard.NotEmpty(password, nameof(password)); Guard.NotEmpty(name, nameof(name)); Guard.NotEmpty(mnemonic, nameof(mnemonic)); // for now the passphrase is set to be the password by default. if (passphrase == null) { passphrase = password; } // generate the root seed used to generate keys ExtKey extendedKey = HdOperations.GetHdPrivateKey(mnemonic, passphrase); // create a wallet file Wallet wallet = this.GenerateWalletFile(password, name, extendedKey, creationTime); // generate multiple accounts and addresses from the get-go for (int i = 0; i < WalletRecoveryAccountsCount; i++) { HdAccount account = wallet.AddNewAccount(password, this.coinType); account.CreateAddresses(this.network, UnusedAddressesBuffer); account.CreateAddresses(this.network, UnusedAddressesBuffer, true); } int blockSyncStart = this.chain.GetHeightAtTime(creationTime); this.UpdateLastBlockSyncedHeight(wallet, this.chain.GetBlock(blockSyncStart)); // save the changes to the file and add addresses to be tracked this.SaveToFile(wallet); this.Load(wallet); this.LoadKeysLookup(); return(wallet); }
/// <summary> /// Adds an account to the current account root. /// </summary> /// <remarks>The name given to the account is of the form "account (i)" by default, where (i) is an incremental index starting at 0. /// According to BIP44, an account at index (i) can only be created when the account at index (i - 1) contains transactions. /// <seealso cref="https://github.com/bitcoin/bips/blob/master/bip-0044.mediawiki"/></remarks> /// <param name="password">The password used to decrypt the wallet's encrypted seed.</param> /// <param name="encryptedSeed">The encrypted private key for this wallet.</param> /// <param name="chainCode">The chain code for this wallet.</param> /// <param name="network">The network for which this account will be created.</param> /// <returns>A new HD account.</returns> public HdAccount AddNewAccount(string password, string encryptedSeed, byte[] chainCode, Network network) { Guard.NotEmpty(password, nameof(password)); Guard.NotEmpty(encryptedSeed, nameof(encryptedSeed)); Guard.NotNull(chainCode, nameof(chainCode)); // Get the current collection of accounts. var accounts = this.Accounts.ToList(); int newAccountIndex = 0; if (accounts.Any()) { newAccountIndex = accounts.Max(a => a.Index) + 1; } // Get the extended pub key used to generate addresses for this account. string accountHdPath = HdOperations.GetAccountHdPath((int)this.CoinType, newAccountIndex); Key privateKey = HdOperations.DecryptSeed(encryptedSeed, password, network); ExtPubKey accountExtPubKey = HdOperations.GetExtendedPublicKey(privateKey, chainCode, accountHdPath); var newAccount = new HdAccount { Index = newAccountIndex, ExtendedPubKey = accountExtPubKey.ToString(network), ExternalAddresses = new List <HdAddress>(), InternalAddresses = new List <HdAddress>(), Name = $"account {newAccountIndex}", HdPath = accountHdPath, CreationTime = DateTimeOffset.Now }; accounts.Add(newAccount); this.Accounts = accounts; return(newAccount); }
/// <summary> /// Gets the first account from the "default" wallet if it specified, /// otherwise returns the first available account in the existing wallets. /// </summary> /// <returns>Reference to the default wallet account, or the first available if no default wallet is specified.</returns> private WalletAccountReference GetWalletAccountReference() { string walletName = null; 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 { // Read the wallet name from the class instance. walletName = WalletRPCController.CurrentWalletName; } if (walletName == null) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found"); } HdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault(); if (account == null) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "Account not found"); } return(new WalletAccountReference(walletName, account.Name)); }
public async Task <GetTransactionModel> GetTransactionAsync(string txid) { if (!uint256.TryParse(txid, out uint256 trxid)) { throw new ArgumentException(nameof(txid)); } WalletAccountReference accountReference = this.GetAccount(); HdAccount account = this.walletManager.GetAccounts(accountReference.WalletName).Single(a => a.Name == accountReference.AccountName); // Get the transaction from the wallet by looking into received and send transactions. List <HdAddress> addresses = account.GetCombinedAddresses().ToList(); List <TransactionData> receivedTransactions = addresses.Where(r => !r.IsChangeAddress() && r.Transactions != null).SelectMany(a => a.Transactions.Where(t => t.Id == trxid)).ToList(); List <TransactionData> sendTransactions = addresses.Where(r => r.Transactions != null).SelectMany(a => a.Transactions.Where(t => t.SpendingDetails != null && t.SpendingDetails.TransactionId == trxid)).ToList(); if (!receivedTransactions.Any() && !sendTransactions.Any()) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id."); } // Get the block hash from the transaction in the wallet. TransactionData transactionFromWallet = null; uint256 blockHash = null; int? blockHeight; if (receivedTransactions.Any()) { blockHeight = receivedTransactions.First().BlockHeight; blockHash = receivedTransactions.First().BlockHash; transactionFromWallet = receivedTransactions.First(); } else { blockHeight = sendTransactions.First().SpendingDetails.BlockHeight; blockHash = blockHeight != null?this.Chain.GetBlock(blockHeight.Value).HashBlock : null; } // Get the block containing the transaction (if it has been confirmed). ChainedHeaderBlock chainedHeaderBlock = null; if (blockHash != null) { await this.ConsensusManager.GetOrDownloadBlocksAsync(new List <uint256> { blockHash }, b => { chainedHeaderBlock = b; }); } Block block = null; Transaction transactionFromStore = null; if (chainedHeaderBlock != null) { block = chainedHeaderBlock.Block; transactionFromStore = block.Transactions.Single(t => t.GetHash() == trxid); } DateTimeOffset transactionTime; bool isGenerated; string hex; if (transactionFromStore != null) { transactionTime = Utils.UnixTimeToDateTime(transactionFromStore.Time); isGenerated = transactionFromStore.IsCoinBase || transactionFromStore.IsCoinStake; hex = transactionFromStore.ToHex(); } else if (transactionFromWallet != null) { transactionTime = transactionFromWallet.CreationTime; isGenerated = transactionFromWallet.IsCoinBase == true || transactionFromWallet.IsCoinStake == true; hex = transactionFromWallet.Hex; } else { transactionTime = sendTransactions.First().SpendingDetails.CreationTime; isGenerated = false; hex = null; // TODO get from mempool } Money amountSent = sendTransactions.Select(s => s.SpendingDetails).SelectMany(sds => sds.Payments).GroupBy(p => p.DestinationAddress).Select(g => g.First()).Sum(p => p.Amount); Money totalAmount = receivedTransactions.Sum(t => t.Amount) - amountSent; var model = new GetTransactionModel { Amount = totalAmount.ToDecimal(MoneyUnit.BTC), Fee = null,// TODO this still needs to be worked on. Confirmations = blockHeight != null ? this.ConsensusManager.Tip.Height - blockHeight.Value + 1 : 0, Isgenerated = isGenerated ? true : (bool?)null, BlockHash = blockHash, BlockIndex = block?.Transactions.FindIndex(t => t.GetHash() == trxid), BlockTime = block?.Header.BlockTime.ToUnixTimeSeconds(), TransactionId = uint256.Parse(txid), TransactionTime = transactionTime.ToUnixTimeSeconds(), TimeReceived = transactionTime.ToUnixTimeSeconds(), Details = new List <GetTransactionDetailsModel>(), Hex = hex }; // Send transactions details. foreach (PaymentDetails paymentDetail in sendTransactions.Select(s => s.SpendingDetails).SelectMany(sd => sd.Payments)) { // Only a single item should appear per destination address. if (model.Details.SingleOrDefault(d => d.Address == paymentDetail.DestinationAddress) == null) { model.Details.Add(new GetTransactionDetailsModel { Address = paymentDetail.DestinationAddress, Category = GetTransactionDetailsCategoryModel.Send, Amount = -paymentDetail.Amount.ToDecimal(MoneyUnit.BTC), Fee = null, // TODO this still needs to be worked on. OutputIndex = paymentDetail.OutputIndex }); } } // Receive transactions details. foreach (TransactionData trxInWallet in receivedTransactions) { GetTransactionDetailsCategoryModel category; if (isGenerated) { category = model.Confirmations > this.FullNode.Network.Consensus.CoinbaseMaturity ? GetTransactionDetailsCategoryModel.Generate : GetTransactionDetailsCategoryModel.Immature; } else { category = GetTransactionDetailsCategoryModel.Receive; } model.Details.Add(new GetTransactionDetailsModel { Address = addresses.First(a => a.Transactions.Contains(trxInWallet)).Address, Category = category, Amount = trxInWallet.Amount.ToDecimal(MoneyUnit.BTC), OutputIndex = trxInWallet.Index }); } return(model); }
public GetTransactionModel GetTransaction(string txid) { if (!uint256.TryParse(txid, out uint256 trxid)) { throw new ArgumentException(nameof(txid)); } WalletAccountReference accountReference = this.GetWalletAccountReference(); HdAccount account = this.walletManager.GetAccounts(accountReference.WalletName).Single(a => a.Name == accountReference.AccountName); // Get the transaction from the wallet by looking into received and send transactions. List <HdAddress> addresses = account.GetCombinedAddresses().ToList(); List <TransactionData> receivedTransactions = addresses.Where(r => !r.IsChangeAddress() && r.Transactions != null).SelectMany(a => a.Transactions.Where(t => t.Id == trxid)).ToList(); List <TransactionData> sendTransactions = addresses.Where(r => r.Transactions != null).SelectMany(a => a.Transactions.Where(t => t.SpendingDetails != null && t.SpendingDetails.TransactionId == trxid)).ToList(); if (!receivedTransactions.Any() && !sendTransactions.Any()) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id."); } // Get the block hash from the transaction in the wallet. TransactionData transactionFromWallet = null; uint256 blockHash = null; int? blockHeight, blockIndex; if (receivedTransactions.Any()) { blockHeight = receivedTransactions.First().BlockHeight; blockIndex = receivedTransactions.First().BlockIndex; blockHash = receivedTransactions.First().BlockHash; transactionFromWallet = receivedTransactions.First(); } else { blockHeight = sendTransactions.First().SpendingDetails.BlockHeight; blockIndex = sendTransactions.First().SpendingDetails.BlockIndex; blockHash = blockHeight != null?this.ChainIndexer.GetHeader(blockHeight.Value).HashBlock : null; } // Get the block containing the transaction (if it has been confirmed). ChainedHeaderBlock chainedHeaderBlock = null; if (blockHash != null) { this.ConsensusManager.GetOrDownloadBlocks(new List <uint256> { blockHash }, b => { chainedHeaderBlock = b; }); } Block block = null; Transaction transactionFromStore = null; if (chainedHeaderBlock != null) { block = chainedHeaderBlock.Block; transactionFromStore = block.Transactions.Single(t => t.GetHash() == trxid); } DateTimeOffset transactionTime; bool isGenerated; string hex; if (transactionFromStore != null) { transactionTime = Utils.UnixTimeToDateTime(transactionFromStore.Time); isGenerated = transactionFromStore.IsCoinBase || transactionFromStore.IsCoinStake; hex = transactionFromStore.ToHex(); } else if (transactionFromWallet != null) { transactionTime = transactionFromWallet.CreationTime; isGenerated = transactionFromWallet.IsCoinBase == true || transactionFromWallet.IsCoinStake == true; hex = transactionFromWallet.Hex; } else { transactionTime = sendTransactions.First().SpendingDetails.CreationTime; isGenerated = false; hex = null; // TODO get from mempool } var model = new GetTransactionModel { Confirmations = blockHeight != null ? this.ConsensusManager.Tip.Height - blockHeight.Value + 1 : 0, Isgenerated = isGenerated ? true : (bool?)null, BlockHash = blockHash, BlockIndex = blockIndex ?? block?.Transactions.FindIndex(t => t.GetHash() == trxid), BlockTime = block?.Header.BlockTime.ToUnixTimeSeconds(), TransactionId = uint256.Parse(txid), TransactionTime = transactionTime.ToUnixTimeSeconds(), TimeReceived = transactionTime.ToUnixTimeSeconds(), Details = new List <GetTransactionDetailsModel>(), Hex = hex }; Money feeSent = Money.Zero; if (sendTransactions.Any()) { Wallet wallet = this.walletManager.GetWallet(accountReference.WalletName); feeSent = wallet.GetSentTransactionFee(trxid); } // Send transactions details. foreach (PaymentDetails paymentDetail in sendTransactions.Select(s => s.SpendingDetails).SelectMany(sd => sd.Payments)) { // Only a single item should appear per destination address. if (model.Details.SingleOrDefault(d => d.Address == paymentDetail.DestinationAddress) == null) { model.Details.Add(new GetTransactionDetailsModel { Address = paymentDetail.DestinationAddress, Category = GetTransactionDetailsCategoryModel.Send, Amount = -paymentDetail.Amount.ToDecimal(MoneyUnit.BTC), Fee = -feeSent.ToDecimal(MoneyUnit.BTC), OutputIndex = paymentDetail.OutputIndex }); } } // Get the ColdStaking script template if available. Dictionary <string, ScriptTemplate> 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. if (coldStakingTemplate != null && coldStakingTemplate.CheckScriptPubKey(trxInWallet.ScriptPubKey)) { continue; } GetTransactionDetailsCategoryModel category; if (isGenerated) { category = model.Confirmations > this.FullNode.Network.Consensus.CoinbaseMaturity ? GetTransactionDetailsCategoryModel.Generate : GetTransactionDetailsCategoryModel.Immature; } else { category = GetTransactionDetailsCategoryModel.Receive; } model.Details.Add(new GetTransactionDetailsModel { Address = addresses.First(a => a.Transactions.Contains(trxInWallet)).Address, Category = category, Amount = trxInWallet.Amount.ToDecimal(MoneyUnit.BTC), OutputIndex = trxInWallet.Index }); } model.Amount = model.Details.Sum(d => d.Amount); model.Fee = model.Details.FirstOrDefault(d => d.Category == GetTransactionDetailsCategoryModel.Send)?.Fee; return(model); }
/// <inheritdoc /> public (string hex, uint256 transactionId, Money fee) BuildTransaction(WalletAccountReference accountReference, string password, Script destinationScript, Money amount, FeeType feeType, int minConfirmations) { if (amount == Money.Zero) { throw new WalletException($"Cannot send transaction with 0 {this.coinType}"); } // get the wallet and the account Wallet wallet = this.GetWalletByName(accountReference.WalletName); HdAccount account = this.GetAccounts(wallet).GetAccountByName(accountReference.AccountName); // get a list of transactions outputs that have not been spent var spendableTransactions = account.GetSpendableTransactions().ToList(); // remove whats under min confirmations var currentHeight = this.chain.Height; spendableTransactions = spendableTransactions.Where(s => currentHeight - s.BlockHeight >= minConfirmations).ToList(); // get total spendable balance in the account. var balance = spendableTransactions.Sum(t => t.Amount); // make sure we have enough funds if (balance < amount) { throw new WalletException("Not enough funds."); } // calculate which addresses needs to be used as well as the fee to be charged var calculationResult = this.CalculateFees(spendableTransactions, amount, feeType.ToConfirmations()); // get extended private key var privateKey = Key.Parse(wallet.EncryptedSeed, password, wallet.Network); var seedExtKey = new ExtKey(privateKey, wallet.ChainCode); var signingKeys = new HashSet <ISecret>(); var coins = new List <Coin>(); foreach (var transactionToUse in calculationResult.transactionsToUse) { var address = account.FindAddressesForTransaction(t => t.Id == transactionToUse.Id && t.Index == transactionToUse.Index && t.Amount > 0).Single(); ExtKey addressExtKey = seedExtKey.Derive(new KeyPath(address.HdPath)); BitcoinExtKey addressPrivateKey = addressExtKey.GetWif(wallet.Network); signingKeys.Add(addressPrivateKey); coins.Add(new Coin(transactionToUse.Id, (uint)transactionToUse.Index, transactionToUse.Amount, transactionToUse.ScriptPubKey)); } // get address to send the change to var changeAddress = account.GetFirstUnusedChangeAddress(); // build transaction var builder = new TransactionBuilder(); Transaction tx = builder .AddCoins(coins) .AddKeys(signingKeys.ToArray()) .Send(destinationScript, amount) .SetChange(changeAddress.ScriptPubKey) .SendFees(calculationResult.fee) .BuildTransaction(true); if (!builder.Verify(tx)) { throw new WalletException("Could not build transaction, please make sure you entered the correct data."); } return(tx.ToHex(), tx.GetHash(), calculationResult.fee); }
public AddressCollection(HdAccount account, int addressType) : this(account, addressType, new List <HdAddress>()) { }
public GetTransactionModel GetTransaction(string txid) { if (!uint256.TryParse(txid, out uint256 trxid)) { throw new ArgumentException(nameof(txid)); } WalletAccountReference accountReference = this.GetWalletAccountReference(); Wallet hdWallet = this.walletManager.WalletRepository.GetWallet(accountReference.WalletName); HdAccount hdAccount = this.walletManager.WalletRepository.GetAccounts(hdWallet, accountReference.AccountName).First(); IWalletAddressReadOnlyLookup addressLookup = this.walletManager.WalletRepository.GetWalletAddressLookup(accountReference.WalletName); bool IsChangeAddress(Script scriptPubKey) { return(addressLookup.Contains(scriptPubKey, out AddressIdentifier addressIdentifier) && addressIdentifier.AddressType == 1); } // Get the transaction from the wallet by looking into received and send transactions. List <TransactionData> receivedTransactions = this.walletManager.WalletRepository.GetTransactionOutputs(hdAccount, null, trxid, true) .Where(td => !IsChangeAddress(td.ScriptPubKey)).ToList(); List <TransactionData> sentTransactions = this.walletManager.WalletRepository.GetTransactionInputs(hdAccount, null, trxid, true).ToList(); TransactionData firstReceivedTransaction = receivedTransactions.FirstOrDefault(); TransactionData firstSendTransaction = sentTransactions.FirstOrDefault(); if (firstReceivedTransaction == null && firstSendTransaction == null) { throw new RPCServerException(RPCErrorCode.RPC_INVALID_ADDRESS_OR_KEY, "Invalid or non-wallet transaction id."); } uint256 blockHash = null; int? blockHeight, blockIndex; DateTimeOffset transactionTime; SpendingDetails spendingDetails = firstSendTransaction?.SpendingDetails; if (firstReceivedTransaction != null) { blockHeight = firstReceivedTransaction.BlockHeight; blockIndex = firstReceivedTransaction.BlockIndex; blockHash = firstReceivedTransaction.BlockHash; transactionTime = firstReceivedTransaction.CreationTime; } else { blockHeight = spendingDetails.BlockHeight; blockIndex = spendingDetails.BlockIndex; blockHash = spendingDetails.BlockHash; transactionTime = spendingDetails.CreationTime; } // Get the block containing the transaction (if it has been confirmed). ChainedHeaderBlock chainedHeaderBlock = null; if (blockHash != null) { this.ConsensusManager.GetOrDownloadBlocks(new List <uint256> { blockHash }, b => { chainedHeaderBlock = b; }); } Block block = null; Transaction transactionFromStore = null; if (chainedHeaderBlock != null) { block = chainedHeaderBlock.Block; if (block != null) { if (blockIndex == null) { blockIndex = block.Transactions.FindIndex(t => t.GetHash() == trxid); } transactionFromStore = block.Transactions[(int)blockIndex]; } } bool isGenerated; string hex; if (transactionFromStore != null) { transactionTime = Utils.UnixTimeToDateTime(chainedHeaderBlock.ChainedHeader.Header.Time); isGenerated = transactionFromStore.IsCoinBase || transactionFromStore.IsCoinStake; hex = transactionFromStore.ToHex(); } else { isGenerated = false; hex = null; // TODO get from mempool } var model = new GetTransactionModel { Confirmations = blockHeight != null ? this.ConsensusManager.Tip.Height - blockHeight.Value + 1 : 0, Isgenerated = isGenerated ? true : (bool?)null, BlockHash = blockHash, BlockIndex = blockIndex, BlockTime = block?.Header.BlockTime.ToUnixTimeSeconds(), TransactionId = uint256.Parse(txid), TransactionTime = transactionTime.ToUnixTimeSeconds(), TimeReceived = transactionTime.ToUnixTimeSeconds(), Details = new List <GetTransactionDetailsModel>(), Hex = hex }; // Send transactions details. if (spendingDetails != null) { Money feeSent = Money.Zero; if (firstSendTransaction != null) { // Get the change. long change = spendingDetails.Change.Sum(o => o.Amount); Money inputsAmount = new Money(sentTransactions.Sum(i => i.Amount)); Money outputsAmount = new Money(spendingDetails.Payments.Sum(p => p.Amount) + change); feeSent = inputsAmount - outputsAmount; } var details = spendingDetails.Payments .GroupBy(detail => detail.DestinationAddress) .Select(p => new GetTransactionDetailsModel() { Address = p.Key, Category = GetTransactionDetailsCategoryModel.Send, OutputIndex = p.First().OutputIndex, Amount = 0 - p.Sum(detail => detail.Amount.ToDecimal(MoneyUnit.BTC)), Fee = -feeSent.ToDecimal(MoneyUnit.BTC) }); model.Details.AddRange(details); } // Get the ColdStaking script template if available. Dictionary <string, ScriptTemplate> templates = this.walletManager.GetValidStakingTemplates(); ScriptTemplate coldStakingTemplate = templates.ContainsKey("ColdStaking") ? templates["ColdStaking"] : null; // Receive transactions details. IScriptAddressReader scriptAddressReader = this.FullNode.NodeService <IScriptAddressReader>(); 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 recognize 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; } else { category = GetTransactionDetailsCategoryModel.Receive; } string address = scriptAddressReader.GetAddressFromScriptPubKey(this.FullNode.Network, trxInWallet.ScriptPubKey); model.Details.Add(new GetTransactionDetailsModel { Address = address, Category = category, Amount = trxInWallet.Amount.ToDecimal(MoneyUnit.BTC), OutputIndex = trxInWallet.Index }); } model.Amount = model.Details.Sum(d => d.Amount); model.Fee = model.Details.FirstOrDefault(d => d.Category == GetTransactionDetailsCategoryModel.Send)?.Fee; return(model); }