public virtual async Task<Transaction> CreateOnChainTransactionButDoNotBroadcast(string storeId, string cryptoCode, CreateOnChainTransactionRequest request, Network network, CancellationToken token = default) { if (request.ProceedWithBroadcast) { throw new ArgumentOutOfRangeException(nameof(request.ProceedWithBroadcast), "Please use CreateOnChainTransaction when wanting to also broadcast the transaction"); } var response = await _httpClient.SendAsync( CreateHttpRequest($"api/v1/stores/{storeId}/payment-methods/onchain/{cryptoCode}/wallet/transactions", null, request, HttpMethod.Post), token); return Transaction.Parse(await HandleResponse<string>(response), network); }
public async Task <IActionResult> CreateOnChainTransaction(string storeId, string cryptoCode, [FromBody] CreateOnChainTransactionRequest request) { if (IsInvalidWalletRequest(cryptoCode, out BTCPayNetwork network, out DerivationSchemeSettings derivationScheme, out IActionResult actionResult)) { return(actionResult); } if (network.ReadonlyWallet) { return(this.CreateAPIError("not-available", $"{cryptoCode} sending services are not currently available")); } //This API is only meant for hot wallet usage for now. We can expand later when we allow PSBT manipulation. if (!(await CanUseHotWallet()).HotWallet) { return(Unauthorized()); } var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode); var wallet = _btcPayWalletProvider.GetWallet(network); var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation); if (request.SelectedInputs != null || !utxos.Any()) { utxos = utxos.Where(coin => request.SelectedInputs?.Contains(coin.OutPoint) ?? true) .ToArray(); if (utxos.Any() is false) { //no valid utxos selected request.AddModelError(transactionRequest => transactionRequest.SelectedInputs, "There are no available utxos based on your request", this); } } var balanceAvailable = utxos.Sum(coin => coin.Value.GetValue(network)); var subtractFeesOutputsCount = new List <int>(); var subtractFees = request.Destinations.Any(o => o.SubtractFromAmount); int?payjoinOutputIndex = null; var sum = 0m; var outputs = new List <WalletSendModel.TransactionOutput>(); for (var index = 0; index < request.Destinations.Count; index++) { var destination = request.Destinations[index]; if (destination.SubtractFromAmount) { subtractFeesOutputsCount.Add(index); } BitcoinUrlBuilder bip21 = null; var amount = destination.Amount; if (amount.GetValueOrDefault(0) <= 0) { amount = null; } var address = string.Empty; try { destination.Destination = destination.Destination.Replace(network.UriScheme + ":", "bitcoin:", StringComparison.InvariantCultureIgnoreCase); bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork); amount ??= bip21.Amount.GetValue(network); address = bip21.Address.ToString(); if (destination.SubtractFromAmount) { request.AddModelError(transactionRequest => transactionRequest.Destinations[index], "You cannot use a BIP21 destination along with SubtractFromAmount", this); } } catch (FormatException) { try { address = BitcoinAddress.Create(destination.Destination, network.NBitcoinNetwork).ToString(); } catch (Exception e) { request.AddModelError(transactionRequest => transactionRequest.Destinations[index], "Destination must be a BIP21 payment link or an address", this); } } if (amount is null || amount <= 0) { request.AddModelError(transactionRequest => transactionRequest.Destinations[index], "Amount must be specified or destination must be a BIP21 payment link, and greater than 0", this); } if (request.ProceedWithPayjoin && bip21?.UnknowParameters?.ContainsKey("pj") is true) { payjoinOutputIndex = index; } outputs.Add(new WalletSendModel.TransactionOutput() { DestinationAddress = address, Amount = amount, SubtractFeesFromOutput = destination.SubtractFromAmount }); sum += destination.Amount ?? 0; } if (subtractFeesOutputsCount.Count > 1) { foreach (var subtractFeesOutput in subtractFeesOutputsCount) { request.AddModelError(model => model.Destinations[subtractFeesOutput].SubtractFromAmount, "You can only subtract fees from one destination", this); } } if (balanceAvailable < sum) { request.AddModelError(transactionRequest => transactionRequest.Destinations, "You are attempting to send more than is available", this); } else if (balanceAvailable == sum && !subtractFees) { request.AddModelError(transactionRequest => transactionRequest.Destinations, "You are sending your entire balance, you should subtract the fees from a destination", this); } var minRelayFee = this._nbXplorerDashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); if (request.FeeRate != null && request.FeeRate < minRelayFee) { ModelState.AddModelError(nameof(request.FeeRate), "The fee rate specified is lower than the current minimum relay fee"); } if (!ModelState.IsValid) { return(this.CreateValidationError(ModelState)); } CreatePSBTResponse psbt; try { psbt = await _walletsController.CreatePSBT(network, derivationScheme, new WalletSendModel() { SelectedInputs = request.SelectedInputs?.Select(point => point.ToString()), Outputs = outputs, AlwaysIncludeNonWitnessUTXO = true, InputSelection = request.SelectedInputs?.Any() is true, AllowFeeBump = !request.RBF.HasValue ? WalletSendModel.ThreeStateBool.Maybe : request.RBF.Value ? WalletSendModel.ThreeStateBool.Yes : WalletSendModel.ThreeStateBool.No, FeeSatoshiPerByte = request.FeeRate?.SatoshiPerByte, NoChange = request.NoChange }, CancellationToken.None); }