public static Transaction BuildTransaction( Network network, IWalletTransactionHandler walletTransactionHandler, IServiceNodeRegistrationConfig registrationConfig, RegistrationToken registrationToken, string walletName, string accountName, string password) { var accountReference = new WalletAccountReference() { AccountName = accountName, WalletName = walletName }; var context = new TransactionBuildContext(network) { AccountReference = accountReference, Recipients = GetRecipients(registrationConfig, registrationToken), Shuffle = false, Sign = true, OverrideFeeRate = new FeeRate(registrationConfig.TxFeeValue), WalletPassword = password }; context.TransactionBuilder.CoinSelector = new DefaultCoinSelector { GroupByScriptPubKey = false }; Transaction transaction = walletTransactionHandler.BuildTransaction(context); 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> /// <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) { TransactionBuildContext context = this.GetSetupTransactionBuildContext(walletTransactionHandler, coldWalletAddress, hotWalletAddress, walletName, walletAccount, walletPassword, amount, feeAmount, useSegwitChangeAddress); // Build the transaction. Transaction transaction = walletTransactionHandler.BuildTransaction(context); this.logger.LogTrace("(-)"); 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="subtractFeeFromAmount">Whether the transaction fee should be subtracted from the amount being transferred into the cold staking account.</param> /// <param name="offline">Whether the transaction should be left unsigned so that it can be transferred to an offline wallet for signing.</param> /// <param name="splitCount">The number of UTXOs of similar value the setup transaction will be split into. Defaults to 1.</param> /// <param name="useSegwitChangeAddress">Use a segwit style change address.</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, TransactionBuildContext) GetColdStakingSetupTransaction(IWalletTransactionHandler walletTransactionHandler, string coldWalletAddress, string hotWalletAddress, string walletName, string walletAccount, string walletPassword, Money amount, Money feeAmount, bool subtractFeeFromAmount, bool offline, int splitCount, bool useSegwitChangeAddress = false) { TransactionBuildContext context = this.GetSetupTransactionBuildContext(walletTransactionHandler, coldWalletAddress, hotWalletAddress, walletName, walletAccount, walletPassword, amount, feeAmount, subtractFeeFromAmount, offline, useSegwitChangeAddress, splitCount); context.Sign = !offline; // Build the transaction. Transaction transaction = walletTransactionHandler.BuildTransaction(context); this.logger.LogTrace("(-)"); return(transaction, context); }
public static JoinFederationRequestResult BuildTransaction(IWalletTransactionHandler walletTransactionHandler, Network network, JoinFederationRequest request, JoinFederationRequestEncoder encoder, string walletName, string walletAccount, string walletPassword) { byte[] encodedVotingRequest = encoder.Encode(request); var votingOutputScript = new Script(OpcodeType.OP_RETURN, Op.GetPushOp(encodedVotingRequest)); var context = new TransactionBuildContext(network) { AccountReference = new WalletAccountReference(walletName, walletAccount), MinConfirmations = 0, FeeType = FeeType.High, WalletPassword = walletPassword, Recipients = new[] { new Recipient { Amount = new Money(VotingRequestTransferAmount, MoneyUnit.BTC), ScriptPubKey = votingOutputScript } }.ToList() }; Transaction transaction = walletTransactionHandler.BuildTransaction(context); Guard.Assert(IsVotingRequestTransaction(transaction, encoder)); if (context.TransactionBuilder.Verify(transaction, out TransactionPolicyError[] errors))
/// <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> /// <param name="subtractFeeFromAmount">Set to <c>true</c> to subtract the <paramref name="feeAmount"/> from the <paramref name="amount"/>.</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, bool subtractFeeFromAmount) { (TransactionBuildContext context, HdAccount coldAccount, Script destination) = this.GetWithdrawalTransactionBuildContext(receivingAddress, walletName, amount, feeAmount, subtractFeeFromAmount); // Build the withdrawal transaction according to the settings recorded in the context. Transaction transaction = walletTransactionHandler.BuildTransaction(context); // Map OutPoint to UnspentOutputReference. var accountReference = new WalletAccountReference(walletName, coldAccount.Name); 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; } Wallet.Wallet wallet = this.GetWallet(walletName); // Add keys for signing inputs. This takes time so only add keys for distinct addresses. foreach (HdAddress address in transaction.Inputs.Select(i => mapOutPointToUnspentOutput[i.PrevOut].Address).Distinct()) { context.TransactionBuilder.AddKeys(wallet.GetExtendedPrivateKeyForAddress(walletPassword, address)); } // Sign the transaction. context.TransactionBuilder.SignTransactionInPlace(transaction); this.logger.LogTrace("(-):'{0}'", transaction.GetHash()); return(transaction); }
/// <summary> /// Builds an unsigned transaction template for a cold staking withdrawal transaction. /// This requires specialised logic due to the lack of a private key for the cold account. /// </summary> /// <param name="walletTransactionHandler">The <see cref="IWalletTransactionHandler"/>.</param> /// <param name="receivingAddress">The receiving address.</param> /// <param name="walletName">The spending wallet name.</param> /// <param name="accountName">The spending account name.</param> /// <param name="amount">The amount to spend.</param> /// <param name="feeAmount">The fee amount.</param> /// <param name="subtractFeeFromAmount">Set to <c>true</c> to subtract the <paramref name="feeAmount"/> from the <paramref name="amount"/>.</param> /// <returns>See <see cref="BuildOfflineSignResponse"/>.</returns> public BuildOfflineSignResponse BuildOfflineColdStakingWithdrawalRequest(IWalletTransactionHandler walletTransactionHandler, string receivingAddress, string walletName, string accountName, Money amount, Money feeAmount, bool subtractFeeFromAmount) { TransactionBuildContext context = this.GetOfflineWithdrawalBuildContext(receivingAddress, walletName, accountName, amount, feeAmount, subtractFeeFromAmount); Transaction transactionResult = walletTransactionHandler.BuildTransaction(context); var utxos = new List <UtxoDescriptor>(); var addresses = new List <AddressDescriptor>(); foreach (ICoin coin in context.TransactionBuilder.FindSpentCoins(transactionResult)) { utxos.Add(new UtxoDescriptor() { Amount = coin.TxOut.Value.ToUnit(MoneyUnit.BTC).ToString(), TransactionId = coin.Outpoint.Hash.ToString(), Index = coin.Outpoint.N.ToString(), ScriptPubKey = coin.TxOut.ScriptPubKey.ToHex() }); // We do not include address descriptors as the cold staking scripts are not really regarded as having addresses in the conventional sense. // There is also typically only a single script involved so the keypath hinting is of little use. } var hotAccountReference = new WalletAccountReference(walletName, accountName); // Return transaction hex and UTXO list. return(new BuildOfflineSignResponse() { WalletName = hotAccountReference.WalletName, WalletAccount = hotAccountReference.AccountName, Fee = context.TransactionFee.ToUnit(MoneyUnit.BTC).ToString(), UnsignedTransaction = transactionResult.ToHex(), Utxos = utxos, Addresses = addresses }); }
/// <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)); this.logger.LogTrace("({0}:'{1}',{2}:'{3}',{4}:'{5}',{6}:'{7}'", nameof(receivingAddress), receivingAddress, nameof(walletName), walletName, nameof(amount), amount, nameof(feeAmount), feeAmount ); Wallet.Wallet wallet = this.GetWalletByName(walletName); // Get the cold staking account. HdAccount 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).Select(a => a.Address.ToString()).Contains(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."); } HdAccount hotAccount = this.GetColdStakingAccount(wallet, false); if (hotAccount != null && hotAccount.ExternalAddresses.Concat(hotAccount.InternalAddresses).Select(a => a.Address.ToString()).Contains(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."); } // Send the money to the receiving address. Script 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, 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. foreach (TxIn input in transaction.Inputs) { UnspentOutputReference unspent = mapOutPointToUnspentOutput[input.PrevOut]; context.TransactionBuilder.AddKeys(wallet.GetExtendedPrivateKeyForAddress(walletPassword, unspent.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> /// <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) { 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)); this.logger.LogTrace("({0}:'{1}',{2}:'{3}',{4}:'{5}',{6}:'{7}',{8}:{9},{10}:{11})", nameof(coldWalletAddress), coldWalletAddress, nameof(hotWalletAddress), hotWalletAddress, nameof(walletName), walletName, nameof(walletAccount), walletAccount, nameof(amount), amount, nameof(feeAmount), feeAmount ); Wallet.Wallet wallet = this.GetWalletByName(walletName); // Get/create the cold staking accounts. HdAccount coldAccount = this.GetOrCreateColdStakingAccount(walletName, true, walletPassword); HdAccount hotAccount = this.GetOrCreateColdStakingAccount(walletName, false, walletPassword); bool thisIsColdWallet = coldAccount?.ExternalAddresses.Select(a => a.Address).Contains(coldWalletAddress) ?? false; bool thisIsHotWallet = hotAccount?.ExternalAddresses.Select(a => a.Address).Contains(hotWalletAddress) ?? false; this.logger.LogTrace("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."); } KeyId hotPubKeyHash = new BitcoinPubKeyAddress(hotWalletAddress, wallet.Network).Hash; KeyId coldPubKeyHash = new BitcoinPubKeyAddress(coldWalletAddress, wallet.Network).Hash; Script destination = ColdStakingScriptTemplate.Instance.GenerateScriptPubKey(hotPubKeyHash, coldPubKeyHash); // Only normal accounts should be allowed. if (!this.GetAccounts(walletName).Any(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, WalletPassword = walletPassword, Recipients = new List <Recipient>() { new Recipient { Amount = amount, ScriptPubKey = destination } } }; // 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); }
/// <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) { 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. HdAccount coldAccount = this.GetOrCreateColdStakingAccount(walletName, true, walletPassword); HdAccount hotAccount = this.GetOrCreateColdStakingAccount(walletName, false, walletPassword); 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; address.RedeemScript = 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; address.RedeemScript = 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); }
/// <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)); var wallet = GetWalletByName(walletName); // Get the cold staking account. var coldAccount = 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."); } var hotAccount = 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 _)) { 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. var transaction = walletTransactionHandler.BuildTransaction(context); // Map OutPoint to UnspentOutputReference. var mapOutPointToUnspentOutput = GetSpendableTransactionsInAccount(accountReference) .ToDictionary(unspent => unspent.ToOutPoint(), unspent => unspent); // Set the cold staking scriptPubKey on the change output. var changeOutput = transaction.Outputs.SingleOrDefault(output => output.ScriptPubKey != destination && output.Value != 0); if (changeOutput != null) { // Find the largest input. var 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 (var item in transaction.Inputs.Select(i => mapOutPointToUnspentOutput[i.PrevOut]).Distinct()) { var prevscript = item.Transaction.ScriptPubKey; if (prevscript.IsScriptType(ScriptType.P2SH) || prevscript.IsScriptType(ScriptType.P2WSH)) { if (item.Address.RedeemScript == null) { throw new WalletException("Missing redeem script"); } // Provide the redeem script to the builder var scriptCoin = ScriptCoin.Create(this.network, item.ToOutPoint(), new TxOut(item.Transaction.Amount, prevscript), item.Address.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); }