Esempio n. 1
0
        public void GetFirstUnusedAccountReturnsAccountWithLowerIndexHavingNoAddresses()
        {
            AccountRoot       accountRoot = CreateAccountRoot(KnownCoinTypes.Stratis);
            WalletMemoryStore store       = new WalletMemoryStore();
            IHdAccount        unused      = CreateAccount("unused1");

            unused.Index = 2;
            accountRoot.Accounts.Add(unused);

            HdAccount unused2 = CreateAccount("unused2");

            unused2.Index = 1;
            accountRoot.Accounts.Add(unused2);

            HdAccount used = CreateAccount("used");

            used.ExternalAddresses.Add(CreateAddress());
            used.Index = 3;
            accountRoot.Accounts.Add(used);

            HdAccount used2 = CreateAccount("used2");

            used2.InternalAddresses.Add(CreateAddress());
            used2.Index = 4;
            accountRoot.Accounts.Add(used2);

            IHdAccount result = accountRoot.GetFirstUnusedAccount(store);

            Assert.NotNull(result);
            Assert.Equal(1, result.Index);
            Assert.Equal("unused2", result.Name);
        }
Esempio n. 2
0
        /// <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>
        /// <param name="accountIndex">The index at which an account will be created. If left null, a new account will be created after the last used one.</param>
        /// <param name="accountName">The name of the account to be created. If left null, an account will be created according to the <see cref="AccountNamePattern"/>.</param>
        /// <returns>A new HD account.</returns>
        public IHdAccount AddNewAccount(string password, string encryptedSeed, byte[] chainCode, Network network, DateTimeOffset accountCreationTime, int?accountIndex = null, string accountName = null)
        {
            Guard.NotEmpty(password, nameof(password));
            Guard.NotEmpty(encryptedSeed, nameof(encryptedSeed));
            Guard.NotNull(chainCode, nameof(chainCode));

            ICollection <IHdAccount> hdAccounts = this.Accounts;

            // If an account needs to be created at a specific index or with a specific name, make sure it doesn't already exist.
            if (hdAccounts.Any(a => a.Index == accountIndex || a.Name == accountName))
            {
                throw new WalletException($"An account at index {accountIndex} or with name {accountName} already exists.");
            }

            if (accountIndex == null)
            {
                if (hdAccounts.Any())
                {
                    // Hide account indexes used for cold staking from the "Max" calculation.
                    accountIndex = hdAccounts.Where(Wallet.NormalAccounts).Max(a => a.Index) + 1;
                }
                else
                {
                    accountIndex = 0;
                }
            }

            IHdAccount newAccount = this.CreateAccount(password, encryptedSeed, chainCode, network, accountCreationTime, accountIndex.Value, accountName);

            hdAccounts.Add(newAccount);
            this.Accounts = hdAccounts;

            return(newAccount);
        }
Esempio n. 3
0
        /// <summary>
        /// Finds first available wallet and its account.
        /// </summary>
        /// <returns>Reference to wallet account.</returns>
        internal WalletAccountReference GetAccount()
        {
            const string noWalletMessage  = "No wallet found";
            const string noAccountMessage = "No account found on wallet";


            string walletName = this.walletManager.GetWalletsNames().FirstOrDefault();

            if (walletName == null)
            {
                this.logger.LogError(ExceptionOccurredMessage, noWalletMessage);
                throw new Exception(noWalletMessage);
            }

            IHdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault();

            if (account == null)
            {
                this.logger.LogError(ExceptionOccurredMessage, noAccountMessage);
                throw new Exception(noAccountMessage);
            }

            var walletAccountReference = new WalletAccountReference(walletName, account.Name);

            return(walletAccountReference);
        }
        /// <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 the global override is null or empty.
            if (string.IsNullOrWhiteSpace(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 from class instance the wallet name.
                walletName = CurrentWalletName;
            }

            if (walletName == null)
            {
                throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found");
            }

            IHdAccount account = this.walletManager.GetAccounts(walletName).First();

            return(new WalletAccountReference(walletName, account.Name));
        }
Esempio n. 5
0
        /// <summary>
        /// Gets the first unused cold staking address. Creates a new address if required.
        /// </summary>
        /// <param name="walletName">The name of the wallet providing the cold staking address.</param>
        /// <param name="isColdWalletAddress">Indicates whether we need the cold wallet address (versus the hot wallet address).</param>
        /// <returns>The cold staking address or <c>null</c> if the required account does not exist.</returns>
        internal HdAddress GetFirstUnusedColdStakingAddress(string walletName, bool isColdWalletAddress)
        {
            Guard.NotNull(walletName, nameof(walletName));

            Wallet.Types.Wallet wallet  = this.GetWalletByName(walletName);
            IHdAccount          account = this.GetColdStakingAccount(wallet, isColdWalletAddress);

            if (account == null)
            {
                this.logger.LogTrace("(-)[ACCOUNT_DOES_NOT_EXIST]:null");
                return(null);
            }

            HdAddress address = account.GetFirstUnusedReceivingAddress(wallet.walletStore);

            if (address == null)
            {
                this.logger.LogDebug("No unused address exists on account '{0}'. Adding new address.", account.Name);
                IEnumerable <HdAddress> newAddresses = account.CreateAddresses(wallet.Network, 1);
                this.UpdateKeysLookup(wallet, newAddresses);
                address = newAddresses.First();
            }

            this.logger.LogTrace("(-):'{0}'", address.Address);
            return(address);
        }
Esempio n. 6
0
        public void GetAccountByNameWithMatchingNameReturnsAccount()
        {
            AccountRoot accountRoot = CreateAccountRootWithHdAccountHavingAddresses("Test", KnownCoinTypes.Stratis);

            IHdAccount result = accountRoot.GetAccountByName("Test");

            Assert.NotNull(result);
            Assert.Equal("Test", result.Name);
        }
Esempio n. 7
0
        public void GetFirstUnusedAccountWithoutAccountsReturnsNull()
        {
            AccountRoot       accountRoot = CreateAccountRoot(KnownCoinTypes.Stratis);
            WalletMemoryStore store       = new WalletMemoryStore();

            IHdAccount result = accountRoot.GetFirstUnusedAccount(store);

            Assert.Null(result);
        }
Esempio n. 8
0
        /// <summary>
        /// Gets a cold staking account.
        /// </summary>
        /// <remarks>
        /// <para>In order to keep track of cold staking addresses and balances we are using <see cref="HdAccount"/>'s
        /// with indexes starting from the value defined in <see cref="Wallet.SpecialPurposeAccountIndexesStart"/>.
        /// </para><para>
        /// We are using two such accounts, one when the wallet is in the role of cold wallet, and another one when
        /// the wallet is in the role of hot wallet. For this reason we specify the required account when calling this
        /// method.
        /// </para></remarks>
        /// <param name="wallet">The wallet where we wish to create the account.</param>
        /// <param name="isColdWalletAccount">Indicates whether we need the cold wallet account (versus the hot wallet account).</param>
        /// <returns>The cold staking account or <c>null</c> if the account does not exist.</returns>
        public IHdAccount GetColdStakingAccount(Wallet.Types.Wallet wallet, bool isColdWalletAccount)
        {
            var        coinType = wallet.Network.Consensus.CoinType;
            IHdAccount account  = wallet.GetAccount(isColdWalletAccount ? ColdWalletAccountName : HotWalletAccountName);

            if (account == null)
            {
                this.logger.LogTrace("(-)[ACCOUNT_DOES_NOT_EXIST]:null");
                return(null);
            }

            this.logger.LogTrace("(-):'{0}'", account.Name);
            return(account);
        }
Esempio n. 9
0
        /// <summary>
        /// Gets the first account that contains no transaction.
        /// </summary>
        /// <returns>An unused account.</returns>
        public IHdAccount GetFirstUnusedAccount(IWalletStore walletStore)
        {
            // Get the accounts root for this type of coin.
            IAccountRoot accountsRoot = this.AccountsRoot.Single();

            if (accountsRoot.Accounts.Any())
            {
                // Get an unused account.
                IHdAccount firstUnusedAccount = accountsRoot.GetFirstUnusedAccount(walletStore);
                if (firstUnusedAccount != null)
                {
                    return(firstUnusedAccount);
                }
            }

            return(null);
        }
Esempio n. 10
0
        /// <summary>
        /// Finds first available wallet and its account.
        /// </summary>
        /// <returns>Reference to wallet account.</returns>
        private WalletAccountReference GetAccount()
        {
            string walletName = this.walletManager.GetWalletsNames().FirstOrDefault();

            if (walletName == null)
            {
                throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No wallet found");
            }

            IHdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault();

            if (account == null)
            {
                throw new RPCServerException(RPCErrorCode.RPC_INVALID_REQUEST, "No account found on wallet");
            }

            var res = new WalletAccountReference(walletName, account.Name);

            return(res);
        }
Esempio n. 11
0
        /// <summary>
        /// Creates a cold staking account and ensures that it has at least one address.
        /// If the account already exists then the existing account is returned.
        /// </summary>
        /// <remarks>
        /// <para>In order to keep track of cold staking addresses and balances we are using <see cref="HdAccount"/>'s
        /// with indexes starting from the value defined in <see cref="Wallet.SpecialPurposeAccountIndexesStart"/>.
        /// </para><para>
        /// We are using two such accounts, one when the wallet is in the role of cold wallet, and another one when
        /// the wallet is in the role of hot wallet. For this reason we specify the required account when calling this
        /// method.
        /// </para></remarks>
        /// <param name="walletName">The name of the wallet where we wish to create the account.</param>
        /// <param name="isColdWalletAccount">Indicates whether we need the cold wallet account (versus the hot wallet account).</param>
        /// <param name="walletPassword">The wallet password which will be used to create the account.</param>
        /// <returns>The new or existing cold staking account.</returns>
        public IHdAccount GetOrCreateColdStakingAccount(string walletName, bool isColdWalletAccount, string walletPassword)
        {
            Wallet.Types.Wallet wallet = this.GetWalletByName(walletName);

            IHdAccount account = this.GetColdStakingAccount(wallet, isColdWalletAccount);

            if (account != null)
            {
                this.logger.LogTrace("(-)[ACCOUNT_ALREADY_EXIST]:'{0}'", account.Name);
                return(account);
            }

            this.logger.LogDebug("The {0} wallet account for '{1}' does not exist and will now be created.", isColdWalletAccount ? "cold" : "hot", wallet.Name);

            int    accountIndex;
            string accountName;

            if (isColdWalletAccount)
            {
                accountIndex = ColdWalletAccountIndex;
                accountName  = ColdWalletAccountName;
            }
            else
            {
                accountIndex = HotWalletAccountIndex;
                accountName  = HotWalletAccountName;
            }

            account = wallet.AddNewAccount(walletPassword, this.dateTimeProvider.GetTimeOffset(), accountIndex, accountName);

            // Maintain at least one unused address at all times. This will ensure that wallet recovery will also work.
            IEnumerable <HdAddress> newAddresses = account.CreateAddresses(wallet.Network, 1, false);

            this.UpdateKeysLookup(wallet, newAddresses);

            // Save the changes to the file.
            this.SaveWallet(wallet);

            this.logger.LogTrace("(-):'{0}'", account.Name);
            return(account);
        }
Esempio n. 12
0
        private void AddComponentStats(StringBuilder benchLog)
        {
            IEnumerable <string> walletNames = this.coldStakingManager.GetWalletsNames();

            if (walletNames.Any())
            {
                benchLog.AppendLine();
                benchLog.AppendLine("======Wallets======");

                foreach (string walletName in walletNames)
                {
                    // Get all the accounts, including the ones used for cold staking.
                    // TODO: change GetAccounts to accept a filter.
                    foreach (IHdAccount account in this.coldStakingManager.GetAccounts(walletName))
                    {
                        AccountBalance accountBalance = this.coldStakingManager.GetBalances(walletName, account.Name).Single();
                        benchLog.AppendLine(($"{walletName}/{account.Name}" + ",").PadRight(LoggingConfiguration.ColumnLength + 20)
                                            + (" Confirmed balance: " + accountBalance.AmountConfirmed.ToString()).PadRight(LoggingConfiguration.ColumnLength + 20)
                                            + " Unconfirmed balance: " + accountBalance.AmountUnconfirmed.ToString());
                    }

                    IHdAccount coldStakingAccount = this.coldStakingManager.GetColdStakingAccount(this.coldStakingManager.GetWallet(walletName), true);
                    if (coldStakingAccount != null)
                    {
                        AccountBalance accountBalance = this.coldStakingManager.GetBalances(walletName, coldStakingAccount.Name).Single();
                        benchLog.AppendLine(($"{walletName}/{coldStakingAccount.Name}" + ",").PadRight(LoggingConfiguration.ColumnLength + 20)
                                            + (" Confirmed balance: " + accountBalance.AmountConfirmed.ToString()).PadRight(LoggingConfiguration.ColumnLength + 20)
                                            + " Unconfirmed balance: " + accountBalance.AmountUnconfirmed.ToString());
                    }

                    IHdAccount hotStakingAccount = this.coldStakingManager.GetColdStakingAccount(this.coldStakingManager.GetWallet(walletName), false);
                    if (hotStakingAccount != null)
                    {
                        AccountBalance accountBalance = this.coldStakingManager.GetBalances(walletName, hotStakingAccount.Name).Single();
                        benchLog.AppendLine(($"{walletName}/{hotStakingAccount.Name}" + ",").PadRight(LoggingConfiguration.ColumnLength + 20)
                                            + (" Confirmed balance: " + accountBalance.AmountConfirmed.ToString()).PadRight(LoggingConfiguration.ColumnLength + 20)
                                            + " Unconfirmed balance: " + accountBalance.AmountUnconfirmed.ToString());
                    }
                }
            }
        }
Esempio n. 13
0
        /// <summary>Gets scriptPubKey from the wallet.</summary>
        private Script GetScriptPubKeyFromWallet()
        {
            string walletName = this.walletManager.GetWalletsNames().FirstOrDefault();

            if (walletName == null)
            {
                return(null);
            }

            IHdAccount account = this.walletManager.GetAccounts(walletName).FirstOrDefault();

            if (account == null)
            {
                return(null);
            }

            var walletAccountReference = new WalletAccountReference(walletName, account.Name);

            HdAddress address = this.walletManager.GetUnusedAddress(walletAccountReference);

            return(address.Pubkey);
        }
Esempio n. 14
0
        /// <summary>
        /// Creates a cold staking withdrawal <see cref="Transaction"/>.
        /// </summary>
        /// <remarks>
        /// Cold staking withdrawal is performed on the wallet that is in the role of the cold staking cold wallet.
        /// </remarks>
        /// <param name="walletTransactionHandler">The wallet transaction handler used to build the transaction.</param>
        /// <param name="receivingAddress">The address that will receive the withdrawal.</param>
        /// <param name="walletName">The name of the wallet in the role of cold wallet.</param>
        /// <param name="walletPassword">The wallet password.</param>
        /// <param name="amount">The amount to remove from cold staking.</param>
        /// <param name="feeAmount">The fee to pay for cold staking transaction withdrawal.</param>
        /// <returns>The <see cref="Transaction"/> for cold staking withdrawal.</returns>
        /// <exception cref="WalletException">Thrown if the receiving address is in a cold staking account in this wallet.</exception>
        /// <exception cref="ArgumentNullException">Thrown if the receiving address is invalid.</exception>
        internal Transaction GetColdStakingWithdrawalTransaction(IWalletTransactionHandler walletTransactionHandler, string receivingAddress,
                                                                 string walletName, string walletPassword, Money amount, Money feeAmount)
        {
            Guard.NotEmpty(receivingAddress, nameof(receivingAddress));
            Guard.NotEmpty(walletName, nameof(walletName));
            Guard.NotNull(amount, nameof(amount));
            Guard.NotNull(feeAmount, nameof(feeAmount));

            Wallet.Types.Wallet wallet = this.GetWalletByName(walletName);

            // Get the cold staking account.
            IHdAccount coldAccount = this.GetColdStakingAccount(wallet, true);

            if (coldAccount == null)
            {
                this.logger.LogTrace("(-)[COLDSTAKE_ACCOUNT_DOES_NOT_EXIST]");
                throw new WalletException("The cold wallet account does not exist.");
            }

            // Prevent reusing cold stake addresses as regular withdrawal addresses.
            if (coldAccount.ExternalAddresses.Concat(coldAccount.InternalAddresses).Any(s => s.Address == receivingAddress || s.Bech32Address == receivingAddress))
            {
                this.logger.LogTrace("(-)[COLDSTAKE_INVALID_COLD_WALLET_ADDRESS_USAGE]");
                throw new WalletException("You can't send the money to a cold staking cold wallet account.");
            }

            IHdAccount hotAccount = this.GetColdStakingAccount(wallet, false);

            if (hotAccount != null && hotAccount.ExternalAddresses.Concat(hotAccount.InternalAddresses).Any(s => s.Address == receivingAddress || s.Bech32Address == receivingAddress))
            {
                this.logger.LogTrace("(-)[COLDSTAKE_INVALID_HOT_WALLET_ADDRESS_USAGE]");
                throw new WalletException("You can't send the money to a cold staking hot wallet account.");
            }

            Script destination = null;

            if (BitcoinWitPubKeyAddress.IsValid(receivingAddress, this.network, out Exception _))
            {
                destination = new BitcoinWitPubKeyAddress(receivingAddress, wallet.Network).ScriptPubKey;
            }
            else
            {
                // Send the money to the receiving address.
                destination = BitcoinAddress.Create(receivingAddress, wallet.Network).ScriptPubKey;
            }

            // Create the transaction build context (used in BuildTransaction).
            var accountReference = new WalletAccountReference(walletName, coldAccount.Name);
            var context          = new TransactionBuildContext(wallet.Network)
            {
                AccountReference = accountReference,
                // Specify a dummy change address to prevent a change (internal) address from being created.
                // Will be changed after the transacton is built and before it is signed.
                ChangeAddress    = coldAccount.ExternalAddresses.First(),
                TransactionFee   = feeAmount,
                MinConfirmations = 0,
                Shuffle          = false,
                Sign             = false,
                Recipients       = new[] { new Recipient {
                                               Amount = amount, ScriptPubKey = destination
                                           } }.ToList()
            };

            // Register the cold staking builder extension with the transaction builder.
            context.TransactionBuilder.Extensions.Add(new ColdStakingBuilderExtension(false));

            // Avoid script errors due to missing scriptSig.
            context.TransactionBuilder.StandardTransactionPolicy.ScriptVerify = null;

            // Build the transaction according to the settings recorded in the context.
            Transaction transaction = walletTransactionHandler.BuildTransaction(context);

            // Map OutPoint to UnspentOutputReference.
            Dictionary <OutPoint, UnspentOutputReference> mapOutPointToUnspentOutput = this.GetSpendableTransactionsInAccount(accountReference)
                                                                                       .ToDictionary(unspent => unspent.ToOutPoint(), unspent => unspent);

            // Set the cold staking scriptPubKey on the change output.
            TxOut changeOutput = transaction.Outputs.SingleOrDefault(output => (output.ScriptPubKey != destination) && (output.Value != 0));

            if (changeOutput != null)
            {
                // Find the largest input.
                TxIn largestInput = transaction.Inputs.OrderByDescending(input => mapOutPointToUnspentOutput[input.PrevOut].Transaction.Amount).Take(1).Single();

                // Set the scriptPubKey of the change output to the scriptPubKey of the largest input.
                changeOutput.ScriptPubKey = mapOutPointToUnspentOutput[largestInput.PrevOut].Transaction.ScriptPubKey;
            }

            // Add keys for signing inputs. This takes time so only add keys for distinct addresses.
            foreach (UnspentOutputReference item in transaction.Inputs.Select(i => mapOutPointToUnspentOutput[i.PrevOut]).Distinct())
            {
                Script prevscript = item.Transaction.ScriptPubKey;

                if (prevscript.IsScriptType(ScriptType.P2SH) || prevscript.IsScriptType(ScriptType.P2WSH))
                {
                    if (item.Address.RedeemScripts == null)
                    {
                        throw new WalletException("Wallet has no redeem scripts");
                    }

                    Script redeemScript = item.Address.RedeemScripts.FirstOrDefault(r => r.Hash.ScriptPubKey == item.Transaction.ScriptPubKey || r.WitHash.ScriptPubKey == item.Transaction.ScriptPubKey);

                    if (redeemScript == null)
                    {
                        throw new WalletException($"RedeemScript was not found for address '{item.Address.Address}' with output '{item.Transaction.ScriptPubKey}'");
                    }

                    // Provide the redeem script to the builder
                    var scriptCoin = ScriptCoin.Create(this.network, item.ToOutPoint(), new TxOut(item.Transaction.Amount, prevscript), redeemScript);
                    context.TransactionBuilder.AddCoins(scriptCoin);
                }

                context.TransactionBuilder.AddKeys(wallet.GetExtendedPrivateKeyForAddress(walletPassword, item.Address));
            }

            // Sign the transaction.
            context.TransactionBuilder.SignTransactionInPlace(transaction);

            this.logger.LogTrace("(-):'{0}'", transaction.GetHash());
            return(transaction);
        }
Esempio n. 15
0
        /// <summary>
        /// Creates cold staking setup <see cref="Transaction"/>.
        /// </summary>
        /// <remarks>
        /// The <paramref name="coldWalletAddress"/> and <paramref name="hotWalletAddress"/> would be expected to be
        /// from different wallets and typically also different physical machines under normal circumstances. The following
        /// rules are enforced by this method and would lead to a <see cref="WalletException"/> otherwise:
        /// <list type="bullet">
        /// <item><description>The cold and hot wallet addresses are expected to belong to different wallets.</description></item>
        /// <item><description>Either the cold or hot wallet address must belong to a cold staking account in the wallet identified
        /// by <paramref name="walletName"/></description></item>
        /// <item><description>The account specified in <paramref name="walletAccount"/> can't be a cold staking account.</description></item>
        /// </list>
        /// </remarks>
        /// <param name="walletTransactionHandler">The wallet transaction handler. Contains the <see cref="WalletTransactionHandler.BuildTransaction"/> method.</param>
        /// <param name="coldWalletAddress">The cold wallet address generated by <see cref="GetColdStakingAddress"/>.</param>
        /// <param name="hotWalletAddress">The hot wallet address generated by <see cref="GetColdStakingAddress"/>.</param>
        /// <param name="walletName">The name of the wallet.</param>
        /// <param name="walletAccount">The wallet account.</param>
        /// <param name="walletPassword">The wallet password.</param>
        /// <param name="amount">The amount to cold stake.</param>
        /// <param name="feeAmount">The fee to pay for the cold staking setup transaction.</param>
        /// <param name="useSegwitChangeAddress">Use a segwit style change address.</param>
        /// <param name="payToScript">Indicate script staking (P2SH or P2WSH outputs).</param>
        /// <returns>The <see cref="Transaction"/> for setting up cold staking.</returns>
        /// <exception cref="WalletException">Thrown if any of the rules listed in the remarks section of this method are broken.</exception>
        ///

        internal Transaction GetColdStakingSetupTransaction(IWalletTransactionHandler walletTransactionHandler,
                                                            string coldWalletAddress, string hotWalletAddress, string walletName, string walletAccount,
                                                            string walletPassword, Money amount, Money feeAmount, bool useSegwitChangeAddress = false, bool payToScript = false, bool createHotAccount = true)
        {
            Guard.NotNull(walletTransactionHandler, nameof(walletTransactionHandler));
            Guard.NotEmpty(coldWalletAddress, nameof(coldWalletAddress));
            Guard.NotEmpty(hotWalletAddress, nameof(hotWalletAddress));
            Guard.NotEmpty(walletName, nameof(walletName));
            Guard.NotEmpty(walletAccount, nameof(walletAccount));
            Guard.NotNull(amount, nameof(amount));
            Guard.NotNull(feeAmount, nameof(feeAmount));

            Wallet.Types.Wallet wallet = this.GetWalletByName(walletName);

            // Get/create the cold staking accounts.
            IHdAccount coldAccount = this.GetOrCreateColdStakingAccount(walletName, true, walletPassword);
            IHdAccount hotAccount;

            if (createHotAccount)
            {
                hotAccount = this.GetOrCreateColdStakingAccount(walletName, false, walletPassword);
            }
            else
            {
                hotAccount = this.GetColdStakingAccount(this.GetWalletByName(walletName), false);
            }

            HdAddress coldAddress = coldAccount?.ExternalAddresses.FirstOrDefault(s => s.Address == coldWalletAddress || s.Bech32Address == coldWalletAddress);
            HdAddress hotAddress  = hotAccount?.ExternalAddresses.FirstOrDefault(s => s.Address == hotWalletAddress || s.Bech32Address == hotWalletAddress);

            bool thisIsColdWallet = coldAddress != null;
            bool thisIsHotWallet  = hotAddress != null;

            this.logger.LogDebug("Local wallet '{0}' does{1} contain cold wallet address '{2}' and does{3} contain hot wallet address '{4}'.",
                                 walletName, thisIsColdWallet ? "" : " NOT", coldWalletAddress, thisIsHotWallet ? "" : " NOT", hotWalletAddress);

            if (thisIsColdWallet && thisIsHotWallet)
            {
                this.logger.LogTrace("(-)[COLDSTAKE_BOTH_HOT_AND_COLD]");
                throw new WalletException("You can't use this wallet as both hot wallet and cold wallet.");
            }

            if (!thisIsColdWallet && !thisIsHotWallet)
            {
                this.logger.LogTrace("(-)[COLDSTAKE_ADDRESSES_NOT_IN_ACCOUNTS]");
                throw new WalletException("The hot and cold wallet addresses could not be found in the corresponding accounts.");
            }

            Script destination    = null;
            KeyId  hotPubKeyHash  = null;
            KeyId  coldPubKeyHash = null;

            // Check if this is a segwit address
            if (coldAddress?.Bech32Address == coldWalletAddress || hotAddress?.Bech32Address == hotWalletAddress)
            {
                hotPubKeyHash  = new BitcoinWitPubKeyAddress(hotWalletAddress, wallet.Network).Hash.AsKeyId();
                coldPubKeyHash = new BitcoinWitPubKeyAddress(coldWalletAddress, wallet.Network).Hash.AsKeyId();
                destination    = ColdStakingScriptTemplate.Instance.GenerateScriptPubKey(hotPubKeyHash, coldPubKeyHash);

                if (payToScript)
                {
                    HdAddress address = coldAddress ?? hotAddress;
                    if (address.RedeemScripts == null)
                    {
                        address.RedeemScripts = new List <Script>();
                    }
                    address.RedeemScripts.Add(destination);
                    destination = destination.WitHash.ScriptPubKey;
                }
            }
            else
            {
                hotPubKeyHash  = new BitcoinPubKeyAddress(hotWalletAddress, wallet.Network).Hash;
                coldPubKeyHash = new BitcoinPubKeyAddress(coldWalletAddress, wallet.Network).Hash;
                destination    = ColdStakingScriptTemplate.Instance.GenerateScriptPubKey(hotPubKeyHash, coldPubKeyHash);

                if (payToScript)
                {
                    HdAddress address = coldAddress ?? hotAddress;
                    if (address.RedeemScripts == null)
                    {
                        address.RedeemScripts = new List <Script>();
                    }
                    address.RedeemScripts.Add(destination);
                    destination = destination.Hash.ScriptPubKey;
                }
            }

            // Only normal accounts should be allowed.
            if (this.GetAccounts(walletName).All(a => a.Name != walletAccount))
            {
                this.logger.LogTrace("(-)[COLDSTAKE_ACCOUNT_NOT_FOUND]");
                throw new WalletException($"Can't find wallet account '{walletAccount}'.");
            }

            var context = new TransactionBuildContext(wallet.Network)
            {
                AccountReference       = new WalletAccountReference(walletName, walletAccount),
                TransactionFee         = feeAmount,
                MinConfirmations       = 0,
                Shuffle                = false,
                UseSegwitChangeAddress = useSegwitChangeAddress,
                WalletPassword         = walletPassword,
                Recipients             = new List <Recipient>()
                {
                    new Recipient {
                        Amount = amount, ScriptPubKey = destination
                    }
                }
            };

            if (payToScript)
            {
                // In the case of P2SH and P2WSH, to avoid the possibility of lose track of funds
                // we add an opreturn with the hot and cold key hashes to the setup transaction
                // this will allow a user to recreate the redeem script of the output in case they lose
                // access to one of the keys.
                // The special marker will help a wallet that is tracking cold staking accounts to monitor
                // the hot and cold keys, if a special marker is found then the keys are in the opreturn are checked
                // against the current wallet, if found and validated the wallet will track that ScriptPubKey

                var opreturnKeys = new List <byte>();
                opreturnKeys.AddRange(hotPubKeyHash.ToBytes());
                opreturnKeys.AddRange(coldPubKeyHash.ToBytes());

                context.OpReturnRawData = opreturnKeys.ToArray();
                TxNullDataTemplate template = this.network.StandardScriptsRegistry.GetScriptTemplates.OfType <TxNullDataTemplate>().First();
                if (template.MinRequiredSatoshiFee > 0)
                {
                    context.OpReturnAmount = Money.Satoshis(template.MinRequiredSatoshiFee); // mandatory fee must be paid.
                }
                // The P2SH and P2WSH hide the cold stake keys in the script hash so the wallet cannot track
                // the ouputs based on the derived keys when the trx is subbmited to the network.
                // So we add the output script manually.
            }

            // Register the cold staking builder extension with the transaction builder.
            context.TransactionBuilder.Extensions.Add(new ColdStakingBuilderExtension(false));

            // Build the transaction.
            Transaction transaction = walletTransactionHandler.BuildTransaction(context);

            this.logger.LogTrace("(-)");
            return(transaction);
        }