예제 #1
0
        public Wallet(string dataDir, Network network, KeyManager keyManager)
        {
            DataDir    = Guard.NotNullOrEmptyOrWhitespace(nameof(dataDir), dataDir);
            Network    = Guard.NotNull(nameof(network), network);
            KeyManager = Guard.NotNull(nameof(keyManager), keyManager);

            RuntimeParams.SetDataDir(dataDir);
            HandleFiltersLock = new AsyncLock();

            BlockFolderLock = new AsyncLock();
            BlockFolderPath = Path.Combine(dataDir, "Blocks", Network.ToString());
            if (Directory.Exists(BlockFolderPath))
            {
                if (Network == Network.RegTest)
                {
                    Directory.Delete(BlockFolderPath, true);
                    Directory.CreateDirectory(BlockFolderPath);
                }
            }
            else
            {
                Directory.CreateDirectory(BlockFolderPath);
            }
            KeyManager.AssertCleanKeysIndexed();
            KeyManager.AssertLockedInternalKeysIndexed(14);
        }
예제 #2
0
        public WalletService(
            BitcoinStore bitcoinStore,
            KeyManager keyManager,
            WasabiSynchronizer syncer,
            CoinJoinClient chaumianClient,
            NodesGroup nodes,
            string workFolderDir,
            ServiceConfiguration serviceConfiguration,
            IFeeProvider feeProvider,
            CoreNode coreNode = null)
        {
            BitcoinStore         = Guard.NotNull(nameof(bitcoinStore), bitcoinStore);
            KeyManager           = Guard.NotNull(nameof(keyManager), keyManager);
            Nodes                = Guard.NotNull(nameof(nodes), nodes);
            Synchronizer         = Guard.NotNull(nameof(syncer), syncer);
            ChaumianClient       = Guard.NotNull(nameof(chaumianClient), chaumianClient);
            ServiceConfiguration = Guard.NotNull(nameof(serviceConfiguration), serviceConfiguration);
            FeeProvider          = Guard.NotNull(nameof(feeProvider), feeProvider);
            CoreNode             = coreNode;

            HandleFiltersLock = new AsyncLock();

            BlocksFolderPath = Path.Combine(workFolderDir, "Blocks", Network.ToString());
            RuntimeParams.SetDataDir(workFolderDir);

            BlockFolderLock = new AsyncLock();

            KeyManager.AssertCleanKeysIndexed();
            KeyManager.AssertLockedInternalKeysIndexed(14);

            TransactionProcessor = new TransactionProcessor(BitcoinStore.TransactionStore, KeyManager, ServiceConfiguration.DustThreshold, ServiceConfiguration.PrivacyLevelStrong);
            Coins = TransactionProcessor.Coins;

            TransactionProcessor.WalletRelevantTransactionProcessed += TransactionProcessor_WalletRelevantTransactionProcessedAsync;

            if (Directory.Exists(BlocksFolderPath))
            {
                if (Synchronizer.Network == Network.RegTest)
                {
                    Directory.Delete(BlocksFolderPath, true);
                    Directory.CreateDirectory(BlocksFolderPath);
                }
            }
            else
            {
                Directory.CreateDirectory(BlocksFolderPath);
            }

            var walletName = "UnnamedWallet";

            if (!string.IsNullOrWhiteSpace(KeyManager.FilePath))
            {
                walletName = Path.GetFileNameWithoutExtension(KeyManager.FilePath);
            }

            BitcoinStore.IndexStore.NewFilter += IndexDownloader_NewFilterAsync;
            BitcoinStore.IndexStore.Reorged   += IndexDownloader_ReorgedAsync;
            BitcoinStore.MempoolService.TransactionReceived += Mempool_TransactionReceived;
        }
예제 #3
0
        public Wallet(
            Network network,
            BitcoinStore bitcoinStore,
            KeyManager keyManager,
            WasabiSynchronizer syncer,
            NodesGroup nodes,
            string workFolderDir,
            ServiceConfiguration serviceConfiguration,
            IFeeProvider feeProvider,
            CoreNode coreNode = null)
        {
            Network              = Guard.NotNull(nameof(network), network);
            BitcoinStore         = Guard.NotNull(nameof(bitcoinStore), bitcoinStore);
            KeyManager           = Guard.NotNull(nameof(keyManager), keyManager);
            Nodes                = Guard.NotNull(nameof(nodes), nodes);
            Synchronizer         = Guard.NotNull(nameof(syncer), syncer);
            ServiceConfiguration = Guard.NotNull(nameof(serviceConfiguration), serviceConfiguration);
            FeeProvider          = Guard.NotNull(nameof(feeProvider), feeProvider);
            CoreNode             = coreNode;

            ChaumianClient    = new CoinJoinClient(Synchronizer, Network, keyManager);
            HandleFiltersLock = new AsyncLock();

            BlocksFolderPath = Path.Combine(workFolderDir, "Blocks", Network.ToString());
            RuntimeParams.SetDataDir(workFolderDir);

            BlockFolderLock = new AsyncLock();

            KeyManager.AssertCleanKeysIndexed();
            KeyManager.AssertLockedInternalKeysIndexed(14);

            TransactionProcessor = new TransactionProcessor(BitcoinStore.TransactionStore, KeyManager, ServiceConfiguration.DustThreshold, ServiceConfiguration.PrivacyLevelStrong);
            Coins = TransactionProcessor.Coins;

            TransactionProcessor.WalletRelevantTransactionProcessed += TransactionProcessor_WalletRelevantTransactionProcessedAsync;
            ChaumianClient.OnDequeue += ChaumianClient_OnDequeue;

            if (Directory.Exists(BlocksFolderPath))
            {
                if (Network == Network.RegTest)
                {
                    Directory.Delete(BlocksFolderPath, true);
                    Directory.CreateDirectory(BlocksFolderPath);
                }
            }
            else
            {
                Directory.CreateDirectory(BlocksFolderPath);
            }

            BitcoinStore.IndexStore.NewFilter += IndexDownloader_NewFilterAsync;
            BitcoinStore.IndexStore.Reorged   += IndexDownloader_ReorgedAsync;
            BitcoinStore.MempoolService.TransactionReceived += Mempool_TransactionReceived;

            State = WalletState.Initialized;
        }
예제 #4
0
        public Wallet(string dataDir, Network network, KeyManager keyManager)
        {
            DataDir    = Guard.NotNullOrEmptyOrWhitespace(nameof(dataDir), dataDir);
            Network    = Guard.NotNull(nameof(network), network);
            KeyManager = Guard.NotNull(nameof(keyManager), keyManager);

            RuntimeParams.SetDataDir(dataDir);
            HandleFiltersLock = new AsyncLock();

            KeyManager.AssertCleanKeysIndexed();
            KeyManager.AssertLockedInternalKeysIndexed(14);
        }
예제 #5
0
        public bool Process(SmartTransaction tx)
        {
            if (!tx.Transaction.PossiblyP2WPKHInvolved())
            {
                return(false);                // We do not care about non-witness transactions for other than mempool cleanup.
            }

            uint256 txId           = tx.GetHash();
            var     walletRelevant = false;

            if (tx.Confirmed)
            {
                foreach (var coin in Coins.Where(x => x.TransactionId == txId))
                {
                    coin.Height    = tx.Height;
                    walletRelevant = true;                     // relevant
                }
            }

            if (!tx.Transaction.IsCoinBase)             // Transactions we already have and processed would be "double spends" but they shouldn't.
            {
                var doubleSpends = new List <SmartCoin>();
                foreach (SmartCoin coin in Coins)
                {
                    var spent = false;
                    foreach (TxoRef spentOutput in coin.SpentOutputs)
                    {
                        foreach (TxIn txIn in tx.Transaction.Inputs)
                        {
                            if (spentOutput.TransactionId == txIn.PrevOut.Hash && spentOutput.Index == txIn.PrevOut.N)                             // Do not do (spentOutput == txIn.PrevOut), it's faster this way, because it won't check for null.
                            {
                                doubleSpends.Add(coin);
                                spent          = true;
                                walletRelevant = true;
                                break;
                            }
                        }
                        if (spent)
                        {
                            break;
                        }
                    }
                }

                if (doubleSpends.Any())
                {
                    if (tx.Height == Height.Mempool)
                    {
                        // if the received transaction is spending at least one input already
                        // spent by a previous unconfirmed transaction signaling RBF then it is not a double
                        // spanding transaction but a replacement transaction.
                        if (doubleSpends.Any(x => x.IsReplaceable))
                        {
                            // remove double spent coins (if other coin spends it, remove that too and so on)
                            // will add later if they came to our keys
                            foreach (SmartCoin doubleSpentCoin in doubleSpends.Where(x => !x.Confirmed))
                            {
                                Coins.TryRemove(doubleSpentCoin);
                            }
                            tx.SetReplacement();
                            walletRelevant = true;
                        }
                        else
                        {
                            return(false);
                        }
                    }
                    else                     // new confirmation always enjoys priority
                    {
                        // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys
                        foreach (SmartCoin doubleSpentCoin in doubleSpends)
                        {
                            Coins.TryRemove(doubleSpentCoin);
                        }
                        walletRelevant = true;
                    }
                }
            }

            var  isLikelyCoinJoinOutput = false;
            bool hasEqualOutputs        = tx.Transaction.GetIndistinguishableOutputs(includeSingle: false).FirstOrDefault() != default;

            if (hasEqualOutputs)
            {
                var  receiveKeys         = KeyManager.GetKeys(x => tx.Transaction.Outputs.Any(y => y.ScriptPubKey == x.P2wpkhScript));
                bool allReceivedInternal = receiveKeys.All(x => x.IsInternal);
                if (allReceivedInternal)
                {
                    // It is likely a coinjoin if the diff between receive and sent amount is small and have at least 2 equal outputs.
                    Money spentAmount    = Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).Sum(x => x.Amount);
                    Money receivedAmount = tx.Transaction.Outputs.Where(x => receiveKeys.Any(y => y.P2wpkhScript == x.ScriptPubKey)).Sum(x => x.Value);
                    bool  receivedAlmostAsMuchAsSpent = spentAmount.Almost(receivedAmount, Money.Coins(0.005m));

                    if (receivedAlmostAsMuchAsSpent)
                    {
                        isLikelyCoinJoinOutput = true;
                    }
                }
            }

            List <SmartCoin> spentOwnCoins = null;

            for (var i = 0U; i < tx.Transaction.Outputs.Count; i++)
            {
                // If transaction received to any of the wallet keys:
                var      output   = tx.Transaction.Outputs[i];
                HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey);
                if (foundKey != default)
                {
                    walletRelevant = true;

                    if (output.Value <= DustThreshold)
                    {
                        continue;
                    }

                    foundKey.SetKeyState(KeyState.Used, KeyManager);
                    spentOwnCoins ??= Coins.Where(x => tx.Transaction.Inputs.Any(y => y.PrevOut.Hash == x.TransactionId && y.PrevOut.N == x.Index)).ToList();
                    var anonset = tx.Transaction.GetAnonymitySet(i);
                    if (spentOwnCoins.Count != 0)
                    {
                        anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1;                         // Minus 1, because do not count own.
                    }

                    SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, isLikelyCoinJoinOutput, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey);                     // Do not inherit locked status from key, that's different.
                    // If we did not have it.
                    if (Coins.TryAdd(newCoin))
                    {
                        CoinReceived?.Invoke(this, newCoin);

                        // Make sure there's always 21 clean keys generated and indexed.
                        KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal);

                        if (foundKey.IsInternal)
                        {
                            // Make sure there's always 14 internal locked keys generated and indexed.
                            KeyManager.AssertLockedInternalKeysIndexed(14);
                        }
                    }
                    else                                      // If we had this coin already.
                    {
                        if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had.
                        {
                            SmartCoin oldCoin = Coins.FirstOrDefault(x => x.TransactionId == txId && x.Index == i);
                            if (oldCoin != null)                             // Just to be sure, it is a concurrent collection.
                            {
                                oldCoin.Height = newCoin.Height;
                            }
                        }
                    }
                }
            }

            // If spends any of our coin
            for (var i = 0; i < tx.Transaction.Inputs.Count; i++)
            {
                var input = tx.Transaction.Inputs[i];

                var foundCoin = Coins.FirstOrDefault(x => x.TransactionId == input.PrevOut.Hash && x.Index == input.PrevOut.N);
                if (foundCoin != null)
                {
                    walletRelevant = true;
                    var alreadyKnown = foundCoin.SpenderTransactionId == txId;
                    foundCoin.SpenderTransactionId = txId;

                    if (!alreadyKnown)
                    {
                        CoinSpent?.Invoke(this, foundCoin);
                    }

                    if (tx.Confirmed)
                    {
                        SpenderConfirmed?.Invoke(this, foundCoin);
                    }
                }
            }

            if (walletRelevant)
            {
                TransactionStore.AddOrUpdate(tx);
            }

            return(walletRelevant);
        }
예제 #6
0
        private async Task TryRegisterCoinsAsync(CcjClientRound inputRegistrableRound)
        {
            try
            {
                List <(uint256 txid, uint index)> registrableCoins = State.GetRegistrableCoins(
                    inputRegistrableRound.State.MaximumInputCountPerPeer,
                    inputRegistrableRound.State.Denomination,
                    inputRegistrableRound.State.FeePerInputs,
                    inputRegistrableRound.State.FeePerOutputs).ToList();

                if (registrableCoins.Any())
                {
                    BitcoinAddress changeAddress = null;
                    BitcoinAddress activeAddress = null;
                    lock (CustomChangeAddressesLock)
                    {
                        if (CustomChangeAddresses.Count > 0)
                        {
                            changeAddress = CustomChangeAddresses.First();
                            CustomChangeAddresses.RemoveFirst();
                        }
                    }
                    lock (CustomActiveAddressesLock)
                    {
                        if (CustomActiveAddresses.Count > 0)
                        {
                            activeAddress = CustomActiveAddresses.First();
                            CustomActiveAddresses.RemoveFirst();
                        }
                    }

                    if (changeAddress is null || activeAddress is null)
                    {
                        IEnumerable <HdPubKey> allUnusedInternalKeys = KeyManager.GetKeys(keyState: null, isInternal: true).Where(x => x.KeyState != KeyState.Used);

                        if (changeAddress is null)
                        {
                            string changeLabel = "ZeroLink Change";
                            IEnumerable <HdPubKey> allChangeKeys = allUnusedInternalKeys.Where(x => x.Label == changeLabel);
                            HdPubKey changeKey = null;

                            KeyManager.AssertLockedInternalKeysIndexed(14);
                            IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys);

                            if (allChangeKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any())                             // Then don't generate new keys, because it'd bloat the wallet.
                            {
                                // Find the first one that we did not try to register in the current session.
                                changeKey = allChangeKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x));
                                // If there is no such a key, then use the oldest.
                                if (changeKey == default)
                                {
                                    changeKey = AccessCache.Where(x => allChangeKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key;
                                }
                                changeKey.SetLabel(changeLabel);
                                changeKey.SetKeyState(KeyState.Locked);
                            }
                            else
                            {
                                changeKey = internalNotCachedLockedKeys.RandomElement();
                                changeKey.SetLabel(changeLabel);
                            }
                            changeAddress = changeKey.GetP2wpkhAddress(Network);
                            AccessCache.AddOrReplace(changeKey, DateTimeOffset.UtcNow);
                        }

                        if (activeAddress is null)
                        {
                            string activeLabel = "ZeroLink Mixed Coin";
                            IEnumerable <HdPubKey> allActiveKeys = allUnusedInternalKeys.Where(x => x.Label == activeLabel);
                            HdPubKey activeKey = null;

                            KeyManager.AssertLockedInternalKeysIndexed(14);
                            IEnumerable <HdPubKey> internalNotCachedLockedKeys = KeyManager.GetKeys(KeyState.Locked, isInternal: true).Except(AccessCache.Keys);

                            if (allActiveKeys.Count() >= 7 || !internalNotCachedLockedKeys.Any())                             // Then don't generate new keys, because it'd bloat the wallet.
                            {
                                // Find the first one that we did not try to register in the current session.
                                activeKey = allActiveKeys.FirstOrDefault(x => !AccessCache.ContainsKey(x));
                                // If there is no such a key, then use the oldest.
                                if (activeKey == default)
                                {
                                    activeKey = AccessCache.Where(x => allActiveKeys.Contains(x.Key)).OrderBy(x => x.Value).First().Key;
                                }
                                activeKey.SetLabel(activeLabel);
                                activeKey.SetKeyState(KeyState.Locked);
                                activeAddress = activeKey.GetP2wpkhAddress(Network);
                            }
                            else
                            {
                                activeKey = internalNotCachedLockedKeys.RandomElement();
                                activeKey.SetLabel(activeLabel);
                            }
                            activeAddress = activeKey.GetP2wpkhAddress(Network);
                            AccessCache.AddOrReplace(activeKey, DateTimeOffset.UtcNow);
                        }
                    }

                    KeyManager.ToFile();

                    var blind = CoordinatorPubKey.Blind(activeAddress.ScriptPubKey.ToBytes());

                    var inputProofs = new List <InputProofModel>();
                    foreach ((uint256 txid, uint index)coinReference in registrableCoins)
                    {
                        SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin is null)
                        {
                            throw new NotSupportedException("This is impossible.");
                        }
                        coin.Secret = coin.Secret ?? KeyManager.GetSecrets(OnePiece, coin.ScriptPubKey).Single();
                        var inputProof = new InputProofModel
                        {
                            Input = coin.GetTxoRef(),
                            Proof = coin.Secret.PrivateKey.SignMessage(ByteHelpers.ToHex(blind.BlindedData))
                        };
                        inputProofs.Add(inputProof);
                    }
                    AliceClient aliceClient = await AliceClient.CreateNewAsync(Network, changeAddress, blind.BlindedData, inputProofs, CcjHostUri, TorSocks5EndPoint);

                    byte[] unblindedSignature = CoordinatorPubKey.UnblindSignature(aliceClient.BlindedOutputSignature, blind.BlindingFactor);

                    if (!CoordinatorPubKey.Verify(unblindedSignature, activeAddress.ScriptPubKey.ToBytes()))
                    {
                        throw new NotSupportedException("Coordinator did not sign the blinded output properly.");
                    }

                    CcjClientRound roundRegistered = State.GetSingleOrDefaultRound(aliceClient.RoundId);
                    if (roundRegistered is null)
                    {
                        // If our SatoshiClient doesn't yet know about the round because of the dealy create it.
                        // Make its state as it'd be the same as our assumed round was, except the roundId and registeredPeerCount, it'll be updated later.
                        roundRegistered = new CcjClientRound(CcjRunningRoundState.CloneExcept(inputRegistrableRound.State, aliceClient.RoundId, registeredPeerCount: 1));
                        State.AddOrReplaceRound(roundRegistered);
                    }

                    foreach ((uint256 txid, uint index)coinReference in registrableCoins)
                    {
                        var coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                        if (coin is null)
                        {
                            throw new NotSupportedException("This is impossible.");
                        }
                        roundRegistered.CoinsRegistered.Add(coin);
                        State.RemoveCoinFromWaitingList(coin);
                    }
                    roundRegistered.ActiveOutputAddress = activeAddress;
                    roundRegistered.ChangeOutputAddress = changeAddress;
                    roundRegistered.UnblindedSignature  = unblindedSignature;
                    roundRegistered.AliceClient         = aliceClient;
                }
            }
            catch (Exception ex)
            {
                Logger.LogError <CcjClient>(ex);
            }
        }
예제 #7
0
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        public BuildTransactionResult BuildTransaction(
            PaymentIntent payments,
            Func <FeeRate> feeRateFetcher,
            IEnumerable <OutPoint> allowedInputs = null,
            Func <LockTime> lockTimeSelector     = null,
            IPayjoinClient payjoinClient         = null)
        {
            payments = Guard.NotNull(nameof(payments), payments);
            lockTimeSelector ??= () => LockTime.Zero;

            long totalAmount = payments.TotalAmount.Satoshi;

            if (totalAmount < 0 || totalAmount > Constants.MaximumNumberOfSatoshis)
            {
                throw new ArgumentOutOfRangeException($"{nameof(payments)}.{nameof(payments.TotalAmount)} sum cannot be smaller than 0 or greater than {Constants.MaximumNumberOfSatoshis}.");
            }

            // Get allowed coins to spend.
            var availableCoinsView = Coins.Available();
            List <SmartCoin> allowedSmartCoinInputs = AllowUnconfirmed            // Inputs that can be used to build the transaction.
                                        ? availableCoinsView.ToList()
                                        : availableCoinsView.Confirmed().ToList();

            if (allowedInputs != null)             // If allowedInputs are specified then select the coins from them.
            {
                if (!allowedInputs.Any())
                {
                    throw new ArgumentException($"{nameof(allowedInputs)} is not null, but empty.");
                }

                allowedSmartCoinInputs = allowedSmartCoinInputs
                                         .Where(x => allowedInputs.Any(y => y.Hash == x.TransactionId && y.N == x.Index))
                                         .ToList();

                // Add those that have the same script, because common ownership is already exposed.
                // But only if the user didn't click the "max" button. In this case he'd send more money than what he'd think.
                if (payments.ChangeStrategy != ChangeStrategy.AllRemainingCustom)
                {
                    var allScripts = allowedSmartCoinInputs.Select(x => x.ScriptPubKey).ToHashSet();
                    foreach (var coin in availableCoinsView.Where(x => !allowedSmartCoinInputs.Any(y => x.TransactionId == y.TransactionId && x.Index == y.Index)))
                    {
                        if (!(AllowUnconfirmed || coin.Confirmed))
                        {
                            continue;
                        }

                        if (allScripts.Contains(coin.ScriptPubKey))
                        {
                            allowedSmartCoinInputs.Add(coin);
                        }
                    }
                }
            }

            // Get and calculate fee
            Logger.LogInfo("Calculating dynamic transaction fee...");

            TransactionBuilder builder = Network.CreateTransactionBuilder();

            builder.SetCoinSelector(new SmartCoinSelector(allowedSmartCoinInputs));
            builder.AddCoins(allowedSmartCoinInputs.Select(c => c.GetCoin()));
            builder.SetLockTime(lockTimeSelector());

            foreach (var request in payments.Requests.Where(x => x.Amount.Type == MoneyRequestType.Value))
            {
                var amountRequest = request.Amount;

                builder.Send(request.Destination, amountRequest.Amount);
                if (amountRequest.SubtractFee)
                {
                    builder.SubtractFees();
                }
            }

            HdPubKey changeHdPubKey = null;

            if (payments.TryGetCustomRequest(out DestinationRequest custChange))
            {
                var changeScript = custChange.Destination.ScriptPubKey;
                changeHdPubKey = KeyManager.GetKeyForScriptPubKey(changeScript);

                var changeStrategy = payments.ChangeStrategy;
                if (changeStrategy == ChangeStrategy.Custom)
                {
                    builder.SetChange(changeScript);
                }
                else if (changeStrategy == ChangeStrategy.AllRemainingCustom)
                {
                    builder.SendAllRemaining(changeScript);
                }
                else
                {
                    throw new NotSupportedException(payments.ChangeStrategy.ToString());
                }
            }
            else
            {
                KeyManager.AssertCleanKeysIndexed(isInternal: true);
                KeyManager.AssertLockedInternalKeysIndexed(14);
                changeHdPubKey = KeyManager.GetKeys(KeyState.Clean, true).RandomElement();

                builder.SetChange(changeHdPubKey.P2wpkhScript);
            }

            builder.OptInRBF = new Random().NextDouble() < Constants.TransactionRBFSignalRate;

            FeeRate feeRate = feeRateFetcher();

            builder.SendEstimatedFees(feeRate);

            var psbt = builder.BuildPSBT(false);

            var spentCoins = psbt.Inputs.Select(txin => allowedSmartCoinInputs.First(y => y.OutPoint == txin.PrevOut)).ToArray();

            var realToSend = payments.Requests
                             .Select(t =>
                                     (label: t.Label,
                                      destination: t.Destination,
                                      amount: psbt.Outputs.FirstOrDefault(o => o.ScriptPubKey == t.Destination.ScriptPubKey)?.Value))
                             .Where(i => i.amount != null);

            if (!psbt.TryGetFee(out var fee))
            {
                throw new InvalidOperationException("Impossible to get the fees of the PSBT, this should never happen.");
            }
            Logger.LogInfo($"Fee: {fee.Satoshi} Satoshi.");

            var vSize = builder.EstimateSize(psbt.GetOriginalTransaction(), true);

            Logger.LogInfo($"Estimated tx size: {vSize} vBytes.");

            // Do some checks
            Money totalSendAmountNoFee = realToSend.Sum(x => x.amount);

            if (totalSendAmountNoFee == Money.Zero)
            {
                throw new InvalidOperationException("The amount after subtracting the fee is too small to be sent.");
            }

            Money totalOutgoingAmountNoFee;

            if (changeHdPubKey is null)
            {
                totalOutgoingAmountNoFee = totalSendAmountNoFee;
            }
            else
            {
                totalOutgoingAmountNoFee = realToSend.Where(x => !changeHdPubKey.ContainsScript(x.destination.ScriptPubKey)).Sum(x => x.amount);
            }
            decimal totalOutgoingAmountNoFeeDecimal = totalOutgoingAmountNoFee.ToDecimal(MoneyUnit.BTC);
            // Cannot divide by zero, so use the closest number we have to zero.
            decimal totalOutgoingAmountNoFeeDecimalDivisor = totalOutgoingAmountNoFeeDecimal == 0 ? decimal.MinValue : totalOutgoingAmountNoFeeDecimal;
            decimal feePc = 100 * fee.ToDecimal(MoneyUnit.BTC) / totalOutgoingAmountNoFeeDecimalDivisor;

            if (feePc > 1)
            {
                Logger.LogInfo($"The transaction fee is {feePc:0.#}% of the sent amount.{Environment.NewLine}"
                               + $"Sending:\t {totalOutgoingAmountNoFee.ToString(fplus: false, trimExcessZero: true)} BTC.{Environment.NewLine}"
                               + $"Fee:\t\t {fee.Satoshi} Satoshi.");
            }
            if (feePc > 100)
            {
                throw new InvalidOperationException($"The transaction fee is more than twice the sent amount: {feePc:0.#}%.");
            }

            if (spentCoins.Any(u => !u.Confirmed))
            {
                Logger.LogInfo("Unconfirmed transaction is spent.");
            }

            // Build the transaction
            Logger.LogInfo("Signing transaction...");
            // It must be watch only, too, because if we have the key and also hardware wallet, we do not care we can sign.

            Transaction tx = null;

            if (KeyManager.IsWatchOnly)
            {
                tx = psbt.GetGlobalTransaction();
            }
            else
            {
                IEnumerable <ExtKey> signingKeys = KeyManager.GetSecrets(Password, spentCoins.Select(x => x.ScriptPubKey).ToArray());
                builder = builder.AddKeys(signingKeys.ToArray());
                builder.SignPSBT(psbt);

                UpdatePSBTInfo(psbt, spentCoins, changeHdPubKey);

                if (!KeyManager.IsWatchOnly)
                {
                    // Try to pay using payjoin
                    if (payjoinClient is { })
예제 #8
0
        private (HdPubKey change, IEnumerable <HdPubKey> active) GetOutputsToRegister(Money baseDenomination, int mixingLevelCount, IEnumerable <TxoRef> coinsToRegister)
        {
            // Figure out how many mixing level we need to register active outputs.
            Money inputSum = Money.Zero;

            foreach (TxoRef coinReference in coinsToRegister)
            {
                SmartCoin coin = State.GetSingleOrDefaultFromWaitingList(coinReference);
                inputSum += coin.Amount;
            }

            int maximumMixingLevelCount = 1;
            var denominations           = new List <Money>
            {
                baseDenomination
            };

            for (int i = 1; i < mixingLevelCount; i++)
            {
                Money denom = denominations.Last() * 2;
                denominations.Add(denom);
                if (inputSum > denom)
                {
                    maximumMixingLevelCount = i + 1;
                }
            }

            string changeLabel = "ZeroLink Change";
            string activeLabel = "ZeroLink Mixed Coin";

            var keysToSurelyRegister = ExposedLinks.Where(x => coinsToRegister.Contains(x.Key)).SelectMany(x => x.Value).Select(x => x.Key).ToArray();
            var keysTryNotToRegister = ExposedLinks.SelectMany(x => x.Value).Select(x => x.Key).Except(keysToSurelyRegister).ToArray();

            // Get all locked internal keys we have and assert we have enough.
            KeyManager.AssertLockedInternalKeysIndexed(howMany: maximumMixingLevelCount + 1);
            IEnumerable <HdPubKey> allLockedInternalKeys = KeyManager.GetKeys(x => x.IsInternal && x.KeyState == KeyState.Locked && !keysTryNotToRegister.Contains(x));

            // If any of our inputs have exposed address relationship then prefer that.
            allLockedInternalKeys = keysToSurelyRegister.Concat(allLockedInternalKeys).Distinct();

            // Prefer not to bloat the wallet:
            if (allLockedInternalKeys.Count() <= maximumMixingLevelCount)
            {
                allLockedInternalKeys = allLockedInternalKeys.Concat(keysTryNotToRegister).Distinct();
            }

            var newKeys = new List <HdPubKey>();

            for (int i = allLockedInternalKeys.Count(); i <= maximumMixingLevelCount + 1; i++)
            {
                HdPubKey k = KeyManager.GenerateNewKey("", KeyState.Locked, isInternal: true, toFile: false);
                newKeys.Add(k);
            }
            allLockedInternalKeys = allLockedInternalKeys.Concat(newKeys);

            // Select the change and active keys to register and label them accordingly.
            HdPubKey change = allLockedInternalKeys.First();

            change.SetLabel(changeLabel);

            var actives = new List <HdPubKey>();

            foreach (HdPubKey active in allLockedInternalKeys.Skip(1).Take(maximumMixingLevelCount))
            {
                actives.Add(active);
                active.SetLabel(activeLabel);
            }

            // Remember which links we are exposing.
            var outLinks = new List <HdPubKeyBlindedPair>
            {
                new HdPubKeyBlindedPair(change, isBlinded: false)
            };

            foreach (var active in actives)
            {
                outLinks.Add(new HdPubKeyBlindedPair(active, isBlinded: true));
            }
            foreach (TxoRef coin in coinsToRegister)
            {
                if (!ExposedLinks.TryAdd(coin, outLinks))
                {
                    var newOutLinks = new List <HdPubKeyBlindedPair>();
                    foreach (HdPubKeyBlindedPair link in ExposedLinks[coin])
                    {
                        newOutLinks.Add(link);
                    }
                    foreach (HdPubKeyBlindedPair link in outLinks)
                    {
                        HdPubKeyBlindedPair found = newOutLinks.FirstOrDefault(x => x == link);

                        if (found == default)
                        {
                            newOutLinks.Add(link);
                        }
                        else                         // If already in it then update the blinded value if it's getting exposed just now. (eg. the change)
                        {
                            if (found.IsBlinded)
                            {
                                found.IsBlinded = link.IsBlinded;
                            }
                        }
                    }

                    ExposedLinks[coin] = newOutLinks;
                }
            }

            // Save our modifications in the keymanager before we give back the selected keys.
            KeyManager.ToFile();
            return(change, actives);
        }
        /// <exception cref="ArgumentException"></exception>
        /// <exception cref="ArgumentNullException"></exception>
        /// <exception cref="ArgumentOutOfRangeException"></exception>
        public BuildTransactionResult BuildTransaction(
            PaymentIntent payments,
            Func <FeeRate> feeRateFetcher,
            IEnumerable <TxoRef> allowedInputs = null)
        {
            payments = Guard.NotNull(nameof(payments), payments);

            long totalAmount = payments.TotalAmount.Satoshi;

            if (totalAmount < 0 || totalAmount > Constants.MaximumNumberOfSatoshis)
            {
                throw new ArgumentOutOfRangeException($"{nameof(payments)}.{nameof(payments.TotalAmount)} sum cannot be smaller than 0 or greater than {Constants.MaximumNumberOfSatoshis}.");
            }

            // Get allowed coins to spend.
            List <SmartCoin> allowedSmartCoinInputs; // Inputs that can be used to build the transaction.

            if (allowedInputs != null)               // If allowedInputs are specified then select the coins from them.
            {
                if (!allowedInputs.Any())
                {
                    throw new ArgumentException($"{nameof(allowedInputs)} is not null, but empty.");
                }

                allowedSmartCoinInputs = AllowUnconfirmed
                                        ? Coins.Where(x => !x.Unavailable && allowedInputs.Any(y => y.TransactionId == x.TransactionId && y.Index == x.Index)).ToList()
                                        : Coins.Where(x => !x.Unavailable && x.Confirmed && allowedInputs.Any(y => y.TransactionId == x.TransactionId && y.Index == x.Index)).ToList();

                // Add those that have the same script, because common ownership is already exposed.
                // But only if the user didn't click the "max" button. In this case he'd send more money than what he'd think.
                if (payments.ChangeStrategy != ChangeStrategy.AllRemainingCustom)
                {
                    var allScripts = allowedSmartCoinInputs.Select(x => x.ScriptPubKey).ToHashSet();
                    foreach (var coin in Coins.Where(x => !x.Unavailable && !allowedSmartCoinInputs.Any(y => x.TransactionId == y.TransactionId && x.Index == y.Index)))
                    {
                        if (!(AllowUnconfirmed || coin.Confirmed))
                        {
                            continue;
                        }

                        if (allScripts.Contains(coin.ScriptPubKey))
                        {
                            allowedSmartCoinInputs.Add(coin);
                        }
                    }
                }
            }
            else
            {
                allowedSmartCoinInputs = AllowUnconfirmed ? Coins.Where(x => !x.Unavailable).ToList() : Coins.Where(x => !x.Unavailable && x.Confirmed).ToList();
            }

            // Get and calculate fee
            Logger.LogInfo("Calculating dynamic transaction fee...");

            TransactionBuilder builder = Network.CreateTransactionBuilder();

            builder.SetCoinSelector(new SmartCoinSelector(allowedSmartCoinInputs));
            builder.AddCoins(allowedSmartCoinInputs.Select(c => c.GetCoin()));

            foreach (var request in payments.Requests.Where(x => x.Amount.Type == MoneyRequestType.Value))
            {
                var amountRequest = request.Amount;

                builder.Send(request.Destination, amountRequest.Amount);
                if (amountRequest.SubtractFee)
                {
                    builder.SubtractFees();
                }
            }

            HdPubKey changeHdPubKey = null;

            if (payments.TryGetCustomRequest(out DestinationRequest custChange))
            {
                var changeScript = custChange.Destination.ScriptPubKey;
                changeHdPubKey = KeyManager.GetKeyForScriptPubKey(changeScript);

                var changeStrategy = payments.ChangeStrategy;
                if (changeStrategy == ChangeStrategy.Custom)
                {
                    builder.SetChange(changeScript);
                }
                else if (changeStrategy == ChangeStrategy.AllRemainingCustom)
                {
                    builder.SendAllRemaining(changeScript);
                }
                else
                {
                    throw new NotSupportedException(payments.ChangeStrategy.ToString());
                }
            }
            else
            {
                KeyManager.AssertCleanKeysIndexed(isInternal: true);
                KeyManager.AssertLockedInternalKeysIndexed(14);
                changeHdPubKey = KeyManager.GetKeys(KeyState.Clean, true).RandomElement();

                builder.SetChange(changeHdPubKey.P2wpkhScript);
            }

            FeeRate feeRate = feeRateFetcher();

            builder.SendEstimatedFees(feeRate);

            var psbt = builder.BuildPSBT(false);

            var spentCoins = psbt.Inputs.Select(txin => allowedSmartCoinInputs.First(y => y.GetOutPoint() == txin.PrevOut)).ToArray();

            var realToSend = payments.Requests
                             .Select(t =>
                                     (label: t.Label,
                                      destination: t.Destination,
                                      amount: psbt.Outputs.FirstOrDefault(o => o.ScriptPubKey == t.Destination.ScriptPubKey)?.Value))
                             .Where(i => i.amount != null);

            if (!psbt.TryGetFee(out var fee))
            {
                throw new InvalidOperationException("Impossible to get the fees of the PSBT, this should never happen.");
            }
            Logger.LogInfo($"Fee: {fee.Satoshi} Satoshi.");

            var vSize = builder.EstimateSize(psbt.GetOriginalTransaction(), true);

            Logger.LogInfo($"Estimated tx size: {vSize} vbytes.");

            // Do some checks
            Money totalSendAmountNoFee = realToSend.Sum(x => x.amount);

            if (totalSendAmountNoFee == Money.Zero)
            {
                throw new InvalidOperationException("The amount after subtracting the fee is too small to be sent.");
            }
            Money totalSendAmount = totalSendAmountNoFee + fee;

            Money totalOutgoingAmountNoFee;

            if (changeHdPubKey is null)
            {
                totalOutgoingAmountNoFee = totalSendAmountNoFee;
            }
            else
            {
                totalOutgoingAmountNoFee = realToSend.Where(x => !changeHdPubKey.ContainsScript(x.destination.ScriptPubKey)).Sum(x => x.amount);
            }
            decimal totalOutgoingAmountNoFeeDecimal = totalOutgoingAmountNoFee.ToDecimal(MoneyUnit.BTC);
            // Cannot divide by zero, so use the closest number we have to zero.
            decimal totalOutgoingAmountNoFeeDecimalDivisor = totalOutgoingAmountNoFeeDecimal == 0 ? decimal.MinValue : totalOutgoingAmountNoFeeDecimal;
            decimal feePc = (100 * fee.ToDecimal(MoneyUnit.BTC)) / totalOutgoingAmountNoFeeDecimalDivisor;

            if (feePc > 1)
            {
                Logger.LogInfo($"The transaction fee is {totalOutgoingAmountNoFee:0.#}% of your transaction amount.{Environment.NewLine}"
                               + $"Sending:\t {totalSendAmount.ToString(fplus: false, trimExcessZero: true)} BTC.{Environment.NewLine}"
                               + $"Fee:\t\t {fee.Satoshi} Satoshi.");
            }
            if (feePc > 100)
            {
                throw new InvalidOperationException($"The transaction fee is more than twice the transaction amount: {feePc:0.#}%.");
            }

            if (spentCoins.Any(u => !u.Confirmed))
            {
                Logger.LogInfo("Unconfirmed transaction is spent.");
            }

            // Build the transaction
            Logger.LogInfo("Signing transaction...");
            // It must be watch only, too, because if we have the key and also hardware wallet, we do not care we can sign.

            Transaction tx = null;

            if (KeyManager.IsWatchOnly)
            {
                tx = psbt.GetGlobalTransaction();
            }
            else
            {
                IEnumerable <ExtKey> signingKeys = KeyManager.GetSecrets(Password, spentCoins.Select(x => x.ScriptPubKey).ToArray());
                builder = builder.AddKeys(signingKeys.ToArray());
                builder.SignPSBT(psbt);
                psbt.Finalize();
                tx = psbt.ExtractTransaction();

                var checkResults = builder.Check(tx).ToList();
                if (!psbt.TryGetEstimatedFeeRate(out FeeRate actualFeeRate))
                {
                    throw new InvalidOperationException("Impossible to get the fee rate of the PSBT, this should never happen.");
                }

                // Manually check the feerate, because some inaccuracy is possible.
                var sb1 = feeRate.SatoshiPerByte;
                var sb2 = actualFeeRate.SatoshiPerByte;
                if (Math.Abs(sb1 - sb2) > 2)                 // 2s/b inaccuracy ok.
                {
                    // So it'll generate a transactionpolicy error thrown below.
                    checkResults.Add(new NotEnoughFundsPolicyError("Fees different than expected"));
                }
                if (checkResults.Count > 0)
                {
                    throw new InvalidTxException(tx, checkResults);
                }
            }

            if (KeyManager.MasterFingerprint is HDFingerprint fp)
            {
                foreach (var coin in spentCoins)
                {
                    var rootKeyPath = new RootedKeyPath(fp, coin.HdPubKey.FullKeyPath);
                    psbt.AddKeyPath(coin.HdPubKey.PubKey, rootKeyPath, coin.ScriptPubKey);
                }
            }

            var label = SmartLabel.Merge(payments.Requests.Select(x => x.Label).Concat(spentCoins.Select(x => x.Label)));
            var outerWalletOutputs = new List <SmartCoin>();
            var innerWalletOutputs = new List <SmartCoin>();

            for (var i = 0U; i < tx.Outputs.Count; i++)
            {
                TxOut output   = tx.Outputs[i];
                var   anonset  = (tx.GetAnonymitySet(i) + spentCoins.Min(x => x.AnonymitySet)) - 1;              // Minus 1, because count own only once.
                var   foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey);
                var   coin     = new SmartCoin(tx.GetHash(), i, output.ScriptPubKey, output.Value, tx.Inputs.ToTxoRefs().ToArray(), Height.Unknown, tx.RBF, anonset, isLikelyCoinJoinOutput: false, pubKey: foundKey);
                label = SmartLabel.Merge(label, coin.Label);                 // foundKey's label is already added to the coinlabel.

                if (foundKey is null)
                {
                    outerWalletOutputs.Add(coin);
                }
                else
                {
                    innerWalletOutputs.Add(coin);
                }
            }

            foreach (var coin in outerWalletOutputs.Concat(innerWalletOutputs))
            {
                var foundPaymentRequest = payments.Requests.FirstOrDefault(x => x.Destination.ScriptPubKey == coin.ScriptPubKey);

                // If change then we concatenate all the labels.
                if (foundPaymentRequest is null)                 // Then it's autochange.
                {
                    coin.Label = label;
                }
                else
                {
                    coin.Label = SmartLabel.Merge(coin.Label, foundPaymentRequest.Label);
                }

                var foundKey = KeyManager.GetKeyForScriptPubKey(coin.ScriptPubKey);
                foundKey?.SetLabel(coin.Label);                 // The foundkeylabel has already been added previously, so no need to concatenate.
            }

            Logger.LogInfo($"Transaction is successfully built: {tx.GetHash()}.");
            var sign = !KeyManager.IsWatchOnly;
            var spendsUnconfirmed = spentCoins.Any(c => !c.Confirmed);

            return(new BuildTransactionResult(new SmartTransaction(tx, Height.Unknown), psbt, spendsUnconfirmed, sign, fee, feePc, outerWalletOutputs, innerWalletOutputs, spentCoins));
        }
예제 #10
0
        private ProcessedResult ProcessNoLock(SmartTransaction tx)
        {
            var result = new ProcessedResult(tx);

            // We do not care about non-witness transactions for other than mempool cleanup.
            if (tx.Transaction.PossiblyP2WPKHInvolved())
            {
                uint256 txId = tx.GetHash();

                // Performance ToDo: txids could be cached in a hashset here by the AllCoinsView and then the contains would be fast.
                if (!tx.Transaction.IsCoinBase && !Coins.AsAllCoinsView().CreatedBy(txId).Any())                 // Transactions we already have and processed would be "double spends" but they shouldn't.
                {
                    var doubleSpends = new List <SmartCoin>();
                    foreach (var txin in tx.Transaction.Inputs)
                    {
                        if (Coins.TryGetSpenderSmartCoinsByOutPoint(txin.PrevOut, out var coins))
                        {
                            doubleSpends.AddRange(coins);
                        }
                    }

                    if (doubleSpends.Any())
                    {
                        if (tx.Height == Height.Mempool)
                        {
                            // if the received transaction is spending at least one input already
                            // spent by a previous unconfirmed transaction signaling RBF then it is not a double
                            // spending transaction but a replacement transaction.
                            var isReplacemenetTx = doubleSpends.Any(x => x.IsReplaceable && !x.Confirmed);
                            if (isReplacemenetTx)
                            {
                                // Undo the replaced transaction by removing the coins it created (if other coin
                                // spends it, remove that too and so on) and restoring those that it replaced.
                                // After undoing the replaced transaction it will process the replacement transaction.
                                var replacedTxId = doubleSpends.First().TransactionId;
                                var(replaced, restored) = Coins.Undo(replacedTxId);

                                result.ReplacedCoins.AddRange(replaced);
                                result.RestoredCoins.AddRange(restored);

                                foreach (var replacedTransactionId in replaced.Select(coin => coin.TransactionId))
                                {
                                    TransactionStore.MempoolStore.TryRemove(replacedTransactionId, out _);
                                }

                                tx.SetReplacement();
                            }
                            else
                            {
                                return(result);
                            }
                        }
                        else                         // new confirmation always enjoys priority
                        {
                            // remove double spent coins recursively (if other coin spends it, remove that too and so on), will add later if they came to our keys
                            foreach (SmartCoin doubleSpentCoin in doubleSpends)
                            {
                                Coins.Remove(doubleSpentCoin);
                            }

                            result.SuccessfullyDoubleSpentCoins.AddRange(doubleSpends);

                            var unconfirmedDoubleSpentTxId = doubleSpends.First().TransactionId;
                            TransactionStore.MempoolStore.TryRemove(unconfirmedDoubleSpentTxId, out _);
                        }
                    }
                }

                List <SmartCoin> spentOwnCoins = null;
                for (var i = 0U; i < tx.Transaction.Outputs.Count; i++)
                {
                    // If transaction received to any of the wallet keys:
                    var      output   = tx.Transaction.Outputs[i];
                    HdPubKey foundKey = KeyManager.GetKeyForScriptPubKey(output.ScriptPubKey);
                    if (foundKey != default)
                    {
                        if (output.Value <= DustThreshold)
                        {
                            result.ReceivedDusts.Add(output);
                            continue;
                        }

                        foundKey.SetKeyState(KeyState.Used, KeyManager);
                        spentOwnCoins ??= Coins.OutPoints(tx.Transaction.Inputs.ToTxoRefs()).ToList();
                        var anonset = tx.Transaction.GetAnonymitySet(i);
                        if (spentOwnCoins.Count != 0)
                        {
                            anonset += spentOwnCoins.Min(x => x.AnonymitySet) - 1;                             // Minus 1, because do not count own.
                        }

                        SmartCoin newCoin = new SmartCoin(txId, i, output.ScriptPubKey, output.Value, tx.Transaction.Inputs.ToTxoRefs().ToArray(), tx.Height, tx.IsRBF, anonset, foundKey.Label, spenderTransactionId: null, false, pubKey: foundKey);                         // Do not inherit locked status from key, that's different.

                        result.ReceivedCoins.Add(newCoin);
                        // If we did not have it.
                        if (Coins.TryAdd(newCoin))
                        {
                            result.NewlyReceivedCoins.Add(newCoin);

                            // Make sure there's always 21 clean keys generated and indexed.
                            KeyManager.AssertCleanKeysIndexed(isInternal: foundKey.IsInternal);

                            if (foundKey.IsInternal)
                            {
                                // Make sure there's always 14 internal locked keys generated and indexed.
                                KeyManager.AssertLockedInternalKeysIndexed(14);
                            }
                        }
                        else                                      // If we had this coin already.
                        {
                            if (newCoin.Height != Height.Mempool) // Update the height of this old coin we already had.
                            {
                                SmartCoin oldCoin = Coins.AsAllCoinsView().GetByOutPoint(new OutPoint(txId, i));
                                if (oldCoin is { })                                 // Just to be sure, it is a concurrent collection.
    /// <exception cref="ArgumentException"/>
    /// <exception cref="ArgumentNullException"/>
    /// <exception cref="ArgumentOutOfRangeException"/>
    public BuildTransactionResult BuildTransaction(
        PaymentIntent payments,
        Func <FeeRate> feeRateFetcher,
        IEnumerable <OutPoint>?allowedInputs = null,
        Func <LockTime>?lockTimeSelector     = null,
        IPayjoinClient?payjoinClient         = null,
        bool tryToSign = true)
    {
        lockTimeSelector ??= () => LockTime.Zero;

        long totalAmount = payments.TotalAmount.Satoshi;

        if (totalAmount is < 0 or > Constants.MaximumNumberOfSatoshis)
        {
            throw new ArgumentOutOfRangeException($"{nameof(payments)}.{nameof(payments.TotalAmount)} sum cannot be smaller than 0 or greater than {Constants.MaximumNumberOfSatoshis}.");
        }

        // Get allowed coins to spend.
        var availableCoinsView = Coins.Unspent();
        List <SmartCoin> allowedSmartCoinInputs = AllowUnconfirmed        // Inputs that can be used to build the transaction.
                                ? availableCoinsView.ToList()
                                : availableCoinsView.Confirmed().ToList();

        if (allowedInputs is not null)         // If allowedInputs are specified then select the coins from them.
        {
            if (!allowedInputs.Any())
            {
                throw new ArgumentException($"{nameof(allowedInputs)} is not null, but empty.");
            }

            allowedSmartCoinInputs = allowedSmartCoinInputs
                                     .Where(x => allowedInputs.Any(y => y.Hash == x.TransactionId && y.N == x.Index))
                                     .ToList();

            // Add those that have the same script, because common ownership is already exposed.
            // But only if the user didn't click the "max" button. In this case he'd send more money than what he'd think.
            if (payments.ChangeStrategy != ChangeStrategy.AllRemainingCustom)
            {
                var allScripts = allowedSmartCoinInputs.Select(x => x.ScriptPubKey).ToHashSet();
                foreach (var coin in availableCoinsView.Where(x => !allowedSmartCoinInputs.Any(y => x.TransactionId == y.TransactionId && x.Index == y.Index)))
                {
                    if (!(AllowUnconfirmed || coin.Confirmed))
                    {
                        continue;
                    }

                    if (allScripts.Contains(coin.ScriptPubKey))
                    {
                        allowedSmartCoinInputs.Add(coin);
                    }
                }
            }
        }

        // Get and calculate fee
        Logger.LogInfo("Calculating dynamic transaction fee...");

        TransactionBuilder builder = Network.CreateTransactionBuilder();

        builder.SetCoinSelector(new SmartCoinSelector(allowedSmartCoinInputs));
        builder.AddCoins(allowedSmartCoinInputs.Select(c => c.Coin));
        builder.SetLockTime(lockTimeSelector());

        foreach (var request in payments.Requests.Where(x => x.Amount.Type == MoneyRequestType.Value))
        {
            var amountRequest = request.Amount;

            builder.Send(request.Destination, amountRequest.Amount);
            if (amountRequest.SubtractFee)
            {
                builder.SubtractFees();
            }
        }

        HdPubKey?changeHdPubKey;

        if (payments.TryGetCustomRequest(out DestinationRequest? custChange))
        {
            var changeScript = custChange.Destination.ScriptPubKey;
            KeyManager.TryGetKeyForScriptPubKey(changeScript, out HdPubKey? hdPubKey);
            changeHdPubKey = hdPubKey;

            var changeStrategy = payments.ChangeStrategy;
            if (changeStrategy == ChangeStrategy.Custom)
            {
                builder.SetChange(changeScript);
            }
            else if (changeStrategy == ChangeStrategy.AllRemainingCustom)
            {
                builder.SendAllRemaining(changeScript);
            }
            else
            {
                throw new NotSupportedException(payments.ChangeStrategy.ToString());
            }
        }
        else
        {
            KeyManager.AssertCleanKeysIndexed(isInternal: true);
            KeyManager.AssertLockedInternalKeysIndexed(14);
            changeHdPubKey = KeyManager.GetKeys(KeyState.Clean, true).First();

            builder.SetChange(changeHdPubKey.P2wpkhScript);
        }

        builder.OptInRBF = true;

        builder.SendEstimatedFees(feeRateFetcher());

        var psbt = builder.BuildPSBT(false);

        var spentCoins = psbt.Inputs.Select(txin => allowedSmartCoinInputs.First(y => y.OutPoint == txin.PrevOut)).ToArray();

        var realToSend = payments.Requests
                         .Select(t =>
                                 (label: t.Label,
                                  destination: t.Destination,
                                  amount: psbt.Outputs.FirstOrDefault(o => o.ScriptPubKey == t.Destination.ScriptPubKey)?.Value))
                         .Where(i => i.amount is not null);

        if (!psbt.TryGetFee(out var fee))
        {
            throw new InvalidOperationException("Impossible to get the fees of the PSBT, this should never happen.");
        }
        Logger.LogInfo($"Fee: {fee.Satoshi} Satoshi.");

        var vSize = builder.EstimateSize(psbt.GetOriginalTransaction(), true);

        Logger.LogInfo($"Estimated tx size: {vSize} vBytes.");

        // Do some checks
        Money totalSendAmountNoFee = realToSend.Sum(x => x.amount);

        if (totalSendAmountNoFee == Money.Zero)
        {
            throw new InvalidOperationException("The amount after subtracting the fee is too small to be sent.");
        }

        Money totalOutgoingAmountNoFee;

        if (changeHdPubKey is null)
        {
            totalOutgoingAmountNoFee = totalSendAmountNoFee;
        }
        else
        {
            totalOutgoingAmountNoFee = realToSend.Where(x => !changeHdPubKey.ContainsScript(x.destination.ScriptPubKey)).Sum(x => x.amount);
        }
        decimal totalOutgoingAmountNoFeeDecimal = totalOutgoingAmountNoFee.ToDecimal(MoneyUnit.BTC);

        // Cannot divide by zero, so use the closest number we have to zero.
        decimal totalOutgoingAmountNoFeeDecimalDivisor = totalOutgoingAmountNoFeeDecimal == 0 ? decimal.MinValue : totalOutgoingAmountNoFeeDecimal;
        decimal feePc = 100 * fee.ToDecimal(MoneyUnit.BTC) / totalOutgoingAmountNoFeeDecimalDivisor;

        if (feePc > 1)
        {
            Logger.LogInfo($"The transaction fee is {feePc:0.#}% of the sent amount.{Environment.NewLine}"
                           + $"Sending:\t {totalOutgoingAmountNoFee.ToString(fplus: false, trimExcessZero: true)} BTC.{Environment.NewLine}"
                           + $"Fee:\t\t {fee.Satoshi} Satoshi.");
        }
        if (feePc > 100)
        {
            throw new TransactionFeeOverpaymentException(feePc);
        }

        if (spentCoins.Any(u => !u.Confirmed))
        {
            Logger.LogInfo("Unconfirmed transaction is spent.");
        }

        // Build the transaction

        // It must be watch only, too, because if we have the key and also hardware wallet, we do not care we can sign.
        psbt.AddKeyPaths(KeyManager);
        psbt.AddPrevTxs(TransactionStore);

        Transaction tx;

        if (KeyManager.IsWatchOnly || !tryToSign)
        {
            tx = psbt.GetGlobalTransaction();
        }
        else
        {
            Logger.LogInfo("Signing transaction...");
            IEnumerable <ExtKey> signingKeys = KeyManager.GetSecrets(Password, spentCoins.Select(x => x.ScriptPubKey).ToArray());
            builder = builder.AddKeys(signingKeys.ToArray());
            builder.SignPSBT(psbt);

            // Try to pay using payjoin
            if (payjoinClient is not null)
            {
                psbt = TryNegotiatePayjoin(payjoinClient, builder, psbt, changeHdPubKey);
                psbt.AddKeyPaths(KeyManager);
                psbt.AddPrevTxs(TransactionStore);
            }

            psbt.Finalize();
            tx = psbt.ExtractTransaction();

            if (payjoinClient is not null)
            {
                builder.CoinFinder = (outpoint) => psbt.Inputs.Select(x => x.GetCoin()).Single(x => x?.Outpoint == outpoint) !;
            }

            var checkResults = builder.Check(tx).ToList();
            if (checkResults.Count > 0)
            {
                Logger.LogDebug($"Found policy error(s)! First error: '{checkResults[0]}'.");
                throw new InvalidTxException(tx, checkResults);
            }
        }

        var smartTransaction = new SmartTransaction(tx, Height.Unknown, label: SmartLabel.Merge(payments.Requests.Select(x => x.Label)));

        foreach (var coin in spentCoins)
        {
            smartTransaction.WalletInputs.Add(coin);
        }
        var label = SmartLabel.Merge(payments.Requests.Select(x => x.Label).Concat(smartTransaction.WalletInputs.Select(x => x.HdPubKey.Label)));

        for (var i = 0U; i < tx.Outputs.Count; i++)
        {
            TxOut output = tx.Outputs[i];
            if (KeyManager.TryGetKeyForScriptPubKey(output.ScriptPubKey, out HdPubKey? foundKey))
            {
                var smartCoin = new SmartCoin(smartTransaction, i, foundKey);
                label = SmartLabel.Merge(label, smartCoin.HdPubKey.Label);                 // foundKey's label is already added to the coinlabel.
                smartTransaction.WalletOutputs.Add(smartCoin);
            }
        }

        foreach (var coin in smartTransaction.WalletOutputs)
        {
            var foundPaymentRequest = payments.Requests.FirstOrDefault(x => x.Destination.ScriptPubKey == coin.ScriptPubKey);

            // If change then we concatenate all the labels.
            // The foundkeylabel has already been added previously, so no need to concatenate.
            if (foundPaymentRequest is null)             // Then it's autochange.
            {
                coin.HdPubKey.SetLabel(label);
            }
            else
            {
                coin.HdPubKey.SetLabel(SmartLabel.Merge(coin.HdPubKey.Label, foundPaymentRequest.Label));
            }
        }

        Logger.LogInfo($"Transaction is successfully built: {tx.GetHash()}.");
        var sign = !KeyManager.IsWatchOnly;

        return(new BuildTransactionResult(smartTransaction, psbt, sign, fee, feePc));
    }