Ejemplo n.º 1
0
        public async Task <IActionResult> WalletRescan(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            var store = await Repository.FindStore(walletId.StoreId, GetUserId());

            DerivationStrategy paymentMethod = GetPaymentMethod(walletId, store);

            if (paymentMethod == null)
            {
                return(NotFound());
            }

            var vm = new RescanWalletModel();

            vm.IsFullySync = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
            // We need to ensure it is segwit,
            // because hardware wallet support need the parent transactions to sign, which NBXplorer don't have. (Nor does a pruned node)
            vm.IsSegwit              = paymentMethod.DerivationStrategyBase.IsSegwit();
            vm.IsServerAdmin         = User.Claims.Any(c => c.Type == Policies.CanModifyServerSettings.Key && c.Value == "true");
            vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
            var explorer     = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
            var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.DerivationStrategyBase);

            if (scanProgress != null)
            {
                vm.PreviousError = scanProgress.Error;
                if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
                {
                    if (scanProgress.Progress == null)
                    {
                        vm.Progress = 0;
                    }
                    else
                    {
                        vm.Progress      = scanProgress.Progress.OverallProgress;
                        vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
                    }
                }
                if (scanProgress.Status == ScanUTXOStatus.Complete)
                {
                    vm.LastSuccess = scanProgress.Progress;
                    vm.TimeOfScan  = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
                }
            }
            return(View(vm));
        }
Ejemplo n.º 2
0
        public bool TaprootActivated(string crytoCode)
        {
            var network = (BTCPayNetwork)_NetworkProvider.GetNetwork(crytoCode);

#pragma warning disable CS0618
            if (!(network.IsBTC && network.NBitcoinNetwork.ChainName == ChainName.Mainnet))
            {
                // Consider it activated for everything that is not mainnet bitcoin
                return(true);
            }
#pragma warning restore CS0618
            var status = _Dashboard.Get(crytoCode).Status;
            return(status.ChainHeight >= 709632);
        }
Ejemplo n.º 3
0
        public async Task <IActionResult> WalletRescan(
            [ModelBinder(typeof(WalletIdModelBinder))]
            WalletId walletId)
        {
            if (walletId?.StoreId == null)
            {
                return(NotFound());
            }
            DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings(walletId);

            if (paymentMethod == null)
            {
                return(NotFound());
            }

            var vm = new RescanWalletModel();

            vm.IsFullySync           = _dashboard.IsFullySynched(walletId.CryptoCode, out var unused);
            vm.IsServerAdmin         = (await _authorizationService.AuthorizeAsync(User, Policies.CanModifyServerSettings.Key)).Succeeded;
            vm.IsSupportedByCurrency = _dashboard.Get(walletId.CryptoCode)?.Status?.BitcoinStatus?.Capabilities?.CanScanTxoutSet == true;
            var explorer     = ExplorerClientProvider.GetExplorerClient(walletId.CryptoCode);
            var scanProgress = await explorer.GetScanUTXOSetInformationAsync(paymentMethod.AccountDerivation);

            if (scanProgress != null)
            {
                vm.PreviousError = scanProgress.Error;
                if (scanProgress.Status == ScanUTXOStatus.Queued || scanProgress.Status == ScanUTXOStatus.Pending)
                {
                    if (scanProgress.Progress == null)
                    {
                        vm.Progress = 0;
                    }
                    else
                    {
                        vm.Progress      = scanProgress.Progress.OverallProgress;
                        vm.RemainingTime = TimeSpan.FromSeconds(scanProgress.Progress.RemainingSeconds).PrettyPrint();
                    }
                }
                if (scanProgress.Status == ScanUTXOStatus.Complete)
                {
                    vm.LastSuccess = scanProgress.Progress;
                    vm.TimeOfScan  = (scanProgress.Progress.CompletedAt.Value - scanProgress.Progress.StartedAt).PrettyPrint();
                }
            }
            return(View(vm));
        }
        public override async Task <IPaymentMethodDetails> CreatePaymentMethodDetails(
            InvoiceLogs logs,
            DerivationSchemeSettings supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store,
            BTCPayNetwork network, object preparePaymentObject)
        {
            if (!_ExplorerProvider.IsAvailable(network))
            {
                throw new PaymentMethodUnavailableException($"Full node not available");
            }
            var prepare = (Prepare)preparePaymentObject;

            Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod onchainMethod =
                new Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod();
            var blob = store.GetStoreBlob();

            onchainMethod.NetworkFeeMode = blob.NetworkFeeMode;
            onchainMethod.FeeRate        = await prepare.GetFeeRate;
            switch (onchainMethod.NetworkFeeMode)
            {
            case NetworkFeeMode.Always:
                onchainMethod.NetworkFeeRate = (await prepare.GetNetworkFeeRate);
                onchainMethod.NextNetworkFee =
                    onchainMethod.NetworkFeeRate.GetFee(100);     // assume price for 100 bytes
                break;

            case NetworkFeeMode.Never:
                onchainMethod.NetworkFeeRate = FeeRate.Zero;
                onchainMethod.NextNetworkFee = Money.Zero;
                break;

            case NetworkFeeMode.MultiplePaymentsOnly:
                onchainMethod.NetworkFeeRate = (await prepare.GetNetworkFeeRate);
                onchainMethod.NextNetworkFee = Money.Zero;
                break;
            }

            onchainMethod.DepositAddress = (await prepare.ReserveAddress).Address.ToString();
            onchainMethod.PayjoinEnabled = blob.PayJoinEnabled &&
                                           PayjoinClient.SupportedFormats.Contains(supportedPaymentMethod
                                                                                   .AccountDerivation.ScriptPubKeyType()) &&
                                           network.SupportPayJoin;
            if (onchainMethod.PayjoinEnabled)
            {
                var prefix      = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
                var nodeSupport = _dashboard?.Get(network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities
                                  ?.CanSupportTransactionCheck is true;
                onchainMethod.PayjoinEnabled &= supportedPaymentMethod.IsHotWallet && nodeSupport;
                if (!isHotwallet)
                {
                    logs.Write($"{prefix} Payjoin should have been enabled, but your store is not a hotwallet");
                }
                if (!nodeSupport)
                {
                    logs.Write($"{prefix} Payjoin should have been enabled, but your version of NBXplorer or full node does not support it.");
                }
                if (onchainMethod.PayjoinEnabled)
                {
                    logs.Write($"{prefix} Payjoin is enabled for this invoice.");
                }
            }

            return(onchainMethod);
        }
Ejemplo n.º 5
0
        public override async Task <IPaymentMethodDetails> CreatePaymentMethodDetails(
            InvoiceLogs logs,
            DerivationSchemeSettings supportedPaymentMethod, PaymentMethod paymentMethod, StoreData store,
            BTCPayNetwork network, object preparePaymentObject)
        {
            if (preparePaymentObject is null)
            {
                return(new BitcoinLikeOnChainPaymentMethod()
                {
                    Activated = false
                });
            }
            if (!_ExplorerProvider.IsAvailable(network))
            {
                throw new PaymentMethodUnavailableException($"Full node not available");
            }
            var prepare       = (Prepare)preparePaymentObject;
            var onchainMethod = new BitcoinLikeOnChainPaymentMethod();
            var blob          = store.GetStoreBlob();

            onchainMethod.Activated = true;
            // TODO: this needs to be refactored to move this logic into BitcoinLikeOnChainPaymentMethod
            // This is likely a constructor code
            onchainMethod.NetworkFeeMode = blob.NetworkFeeMode;
            onchainMethod.FeeRate        = await prepare.GetFeeRate;
            switch (onchainMethod.NetworkFeeMode)
            {
            case NetworkFeeMode.Always:
                onchainMethod.NetworkFeeRate = (await prepare.GetNetworkFeeRate);
                onchainMethod.NextNetworkFee =
                    onchainMethod.NetworkFeeRate.GetFee(100);     // assume price for 100 bytes
                break;

            case NetworkFeeMode.Never:
                onchainMethod.NetworkFeeRate = FeeRate.Zero;
                onchainMethod.NextNetworkFee = Money.Zero;
                break;

            case NetworkFeeMode.MultiplePaymentsOnly:
                onchainMethod.NetworkFeeRate = (await prepare.GetNetworkFeeRate);
                onchainMethod.NextNetworkFee = Money.Zero;
                break;
            }

            var reserved = await prepare.ReserveAddress;

            onchainMethod.DepositAddress = reserved.Address.ToString();
            onchainMethod.KeyPath        = reserved.KeyPath;
            onchainMethod.PayjoinEnabled = blob.PayJoinEnabled &&
                                           supportedPaymentMethod
                                           .AccountDerivation.ScriptPubKeyType() != ScriptPubKeyType.Legacy &&
                                           network.SupportPayJoin;
            if (onchainMethod.PayjoinEnabled)
            {
                var prefix      = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:";
                var nodeSupport = _dashboard?.Get(network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities
                                  ?.CanSupportTransactionCheck is true;
                onchainMethod.PayjoinEnabled &= supportedPaymentMethod.IsHotWallet && nodeSupport;
                if (!supportedPaymentMethod.IsHotWallet)
                {
                    logs.Write($"{prefix} Payjoin should have been enabled, but your store is not a hotwallet", InvoiceEventData.EventSeverity.Warning);
                }
                if (!nodeSupport)
                {
                    logs.Write($"{prefix} Payjoin should have been enabled, but your version of NBXplorer or full node does not support it.", InvoiceEventData.EventSeverity.Warning);
                }
                if (onchainMethod.PayjoinEnabled)
                {
                    logs.Write($"{prefix} Payjoin is enabled for this invoice.", InvoiceEventData.EventSeverity.Info);
                }
            }

            return(onchainMethod);
        }
        public async Task <IActionResult> CreateOnChainTransaction(string storeId, string cryptoCode,
                                                                   [FromBody] CreateOnChainTransactionRequest request)
        {
            if (IsInvalidWalletRequest(cryptoCode, out var network,
                                       out var derivationScheme, out var actionResult))
            {
                return(actionResult);
            }
            if (network.ReadonlyWallet)
            {
                return(this.CreateAPIError(503, "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(this.CreateAPIError(503, "not-available", $"You need to allow non-admins to use hotwallets for their stores (in /server/policies)"));
            }

            if (request.Destinations == null || !request.Destinations.Any())
            {
                ModelState.AddModelError(
                    nameof(request.Destinations),
                    "At least one destination must be specified"
                    );

                return(this.CreateValidationError(ModelState));
            }

            if (request.SelectedInputs != null && request.ExcludeUnconfirmed == true)
            {
                ModelState.AddModelError(
                    nameof(request.ExcludeUnconfirmed),
                    "Can't automatically exclude unconfirmed UTXOs while selection custom inputs"
                    );

                return(this.CreateValidationError(ModelState));
            }

            var explorerClient = _explorerClientProvider.GetExplorerClient(cryptoCode);
            var wallet         = _btcPayWalletProvider.GetWallet(network);

            var utxos = await wallet.GetUnspentCoins(derivationScheme.AccountDerivation, request.ExcludeUnconfirmed);

            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
                {
                    bip21 = new BitcoinUrlBuilder(destination.Destination, network.NBitcoinNetwork);
                    amount ??= bip21.Amount.GetValue(network);
                    if (bip21.Address is null)
                    {
                        request.AddModelError(transactionRequest => transactionRequest.Destinations[index],
                                              "This BIP21 destination is missing a bitcoin address", this);
                    }
                    else
                    {
                        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)
                    {
                        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?.UnknownParameters?.ContainsKey(PayjoinClient.BIP21EndpointKey) 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 = _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);
            }