/// <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 is { }) // If allowedInputs are specified then select the coins from them.
private PSBT TryNegotiatePayjoin(IPayjoinClient payjoinClient, TransactionBuilder builder, PSBT psbt, HdPubKey changeHdPubKey) { try { Logger.LogInfo($"Negotiating payjoin payment with `{payjoinClient.PaymentUrl}`."); psbt = payjoinClient.RequestPayjoin( psbt, KeyManager.ExtPubKey, new RootedKeyPath(KeyManager.MasterFingerprint.Value, KeyManager.AccountKeyPath), changeHdPubKey, CancellationToken.None).GetAwaiter().GetResult(); // WTF??! builder.SignPSBT(psbt); Logger.LogInfo($"Payjoin payment was negotiated successfully."); } catch (HttpRequestException ex) when(ex.InnerException is TorConnectCommandFailedException innerEx) { if (innerEx.Message.Contains("HostUnreachable")) { Logger.LogWarning($"Payjoin server is not reachable. Ignoring..."); } // Ignore. } catch (HttpRequestException e) { Logger.LogWarning($"Payjoin server responded with {e.ToTypeMessageString()}. Ignoring..."); } catch (PayjoinException e) { Logger.LogWarning($"Payjoin server responded with {e.Message}. Ignoring..."); } return(psbt); }
/// <exception cref="ArgumentException"></exception> /// <exception cref="ArgumentNullException"></exception> /// <exception cref="ArgumentOutOfRangeException"></exception> public BuildTransactionResult BuildTransaction( PaymentIntent payments, FeeRate feeRate, IEnumerable <OutPoint> allowedInputs = null, IPayjoinClient payjoinClient = null) => BuildTransaction(payments, () => feeRate, allowedInputs, () => LockTime.Zero, payjoinClient);
/// <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 { })