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); }
/// <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); }
/// <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)); }
/// <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); }
public void GetAccountByNameWithMatchingNameReturnsAccount() { AccountRoot accountRoot = CreateAccountRootWithHdAccountHavingAddresses("Test", KnownCoinTypes.Stratis); IHdAccount result = accountRoot.GetAccountByName("Test"); Assert.NotNull(result); Assert.Equal("Test", result.Name); }
public void GetFirstUnusedAccountWithoutAccountsReturnsNull() { AccountRoot accountRoot = CreateAccountRoot(KnownCoinTypes.Stratis); WalletMemoryStore store = new WalletMemoryStore(); IHdAccount result = accountRoot.GetFirstUnusedAccount(store); Assert.Null(result); }
/// <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); }
/// <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); }
/// <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); }
/// <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); }
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()); } } } }
/// <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); }
/// <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); }
/// <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); }