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)); }
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); }
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); }
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); }