Пример #1
0
        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);
        }
Пример #2
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>
        /// <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);
        }
Пример #3
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="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))
Пример #5
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>
        /// <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);
        }
Пример #6
0
        /// <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);
        }
Пример #9
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)
        {
            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);
        }
Пример #10
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));

            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);
        }