Example #1
0
 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);
            }