public async Task <IActionResult> AddDerivationScheme(string storeId, [FromForm] DerivationSchemeViewModel vm, string cryptoCode) { vm.CryptoCode = cryptoCode; var store = HttpContext.GetStoreData(); if (store == null) { return(NotFound()); } var network = cryptoCode == null ? null : _ExplorerProvider.GetNetwork(cryptoCode); if (network == null) { return(NotFound()); } vm.Network = network; vm.RootKeyPath = network.GetRootKeyPath(); DerivationSchemeSettings strategy = null; var wallet = _WalletProvider.GetWallet(network); if (wallet == null) { return(NotFound()); } if (!string.IsNullOrEmpty(vm.Config)) { if (!DerivationSchemeSettings.TryParseFromJson(vm.Config, network, out strategy)) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Error, Message = "Config file was not in the correct format" }); vm.Confirmation = false; return(View(nameof(AddDerivationScheme), vm)); } } if (vm.WalletFile != null) { if (!DerivationSchemeSettings.TryParseFromWalletFile(await ReadAllText(vm.WalletFile), network, out strategy)) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Error, Message = "Wallet file was not in the correct format" }); vm.Confirmation = false; return(View(nameof(AddDerivationScheme), vm)); } } else if (!string.IsNullOrEmpty(vm.WalletFileContent)) { if (!DerivationSchemeSettings.TryParseFromWalletFile(vm.WalletFileContent, network, out strategy)) { TempData.SetStatusMessageModel(new StatusMessageModel() { Severity = StatusMessageModel.StatusSeverity.Error, Message = "QR import was not in the correct format" }); vm.Confirmation = false; return(View(nameof(AddDerivationScheme), vm)); } } else { try { if (!string.IsNullOrEmpty(vm.DerivationScheme)) { var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, null, network); if (newStrategy.AccountDerivation != strategy?.AccountDerivation) { var accountKey = string.IsNullOrEmpty(vm.AccountKey) ? null : new BitcoinExtPubKey(vm.AccountKey, network.NBitcoinNetwork); if (accountKey != null) { var accountSettings = newStrategy.AccountKeySettings.FirstOrDefault(a => a.AccountKey == accountKey); if (accountSettings != null) { accountSettings.AccountKeyPath = vm.KeyPath == null ? null : KeyPath.Parse(vm.KeyPath); accountSettings.RootFingerprint = string.IsNullOrEmpty(vm.RootFingerprint) ? (HDFingerprint?)null : new HDFingerprint( NBitcoin.DataEncoders.Encoders.Hex.DecodeData(vm.RootFingerprint)); } } strategy = newStrategy; strategy.Source = vm.Source; vm.DerivationScheme = strategy.AccountDerivation.ToString(); } } else { strategy = null; } } catch { ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme"); vm.Confirmation = false; return(View(nameof(AddDerivationScheme), vm)); } } var oldConfig = vm.Config; vm.Config = strategy == null ? null : strategy.ToJson(); PaymentMethodId paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); var exisingStrategy = store.GetSupportedPaymentMethods(_NetworkProvider) .Where(c => c.PaymentId == paymentMethodId) .OfType <DerivationSchemeSettings>() .FirstOrDefault(); var storeBlob = store.GetStoreBlob(); var wasExcluded = storeBlob.GetExcludedPaymentMethods().Match(paymentMethodId); var willBeExcluded = !vm.Enabled; var showAddress = // Show addresses if: // - If the user is testing the hint address in confirmation screen (vm.Confirmation && !string.IsNullOrWhiteSpace(vm.HintAddress)) || // - The user is clicking on continue after changing the config (!vm.Confirmation && oldConfig != vm.Config) || // - The user is clicking on continue without changing config nor enabling/disabling (!vm.Confirmation && oldConfig == vm.Config && willBeExcluded == wasExcluded); showAddress = showAddress && strategy != null; if (!showAddress) { try { if (strategy != null) { await wallet.TrackAsync(strategy.AccountDerivation); } store.SetSupportedPaymentMethod(paymentMethodId, strategy); storeBlob.SetExcluded(paymentMethodId, willBeExcluded); storeBlob.Hints.Wallet = false; store.SetStoreBlob(storeBlob); } catch { ModelState.AddModelError(nameof(vm.DerivationScheme), "Invalid Derivation Scheme"); return(View(vm)); } await _Repo.UpdateStore(store); _EventAggregator.Publish(new WalletChangedEvent() { WalletId = new WalletId(storeId, cryptoCode) }); if (willBeExcluded != wasExcluded) { var label = willBeExcluded ? "disabled" : "enabled"; TempData[WellKnownTempData.SuccessMessage] = $"On-Chain payments for {network.CryptoCode} has been {label}."; } else { TempData[WellKnownTempData.SuccessMessage] = $"Derivation settings for {network.CryptoCode} has been modified."; } // This is success case when derivation scheme is added to the store return(RedirectToAction(nameof(UpdateStore), new { storeId = storeId })); } else if (!string.IsNullOrEmpty(vm.HintAddress)) { BitcoinAddress address = null; try { address = BitcoinAddress.Create(vm.HintAddress, network.NBitcoinNetwork); } catch { ModelState.AddModelError(nameof(vm.HintAddress), "Invalid hint address"); return(ShowAddresses(vm, strategy)); } try { var newStrategy = ParseDerivationStrategy(vm.DerivationScheme, address.ScriptPubKey, network); if (newStrategy.AccountDerivation != strategy.AccountDerivation) { strategy.AccountDerivation = newStrategy.AccountDerivation; strategy.AccountOriginal = null; } } catch { ModelState.AddModelError(nameof(vm.HintAddress), "Impossible to find a match with this address"); return(ShowAddresses(vm, strategy)); } vm.HintAddress = ""; TempData[WellKnownTempData.SuccessMessage] = "Address successfully found, please verify that the rest is correct and click on \"Confirm\""; ModelState.Remove(nameof(vm.HintAddress)); ModelState.Remove(nameof(vm.DerivationScheme)); } return(ShowAddresses(vm, strategy)); }
public async Task <IActionResult> Submit(string cryptoCode, long?maxadditionalfeecontribution, int?additionalfeeoutputindex, decimal minfeerate = -1.0m, bool disableoutputsubstitution = false, int v = 1) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network == null) { return(NotFound()); } if (v != 1) { return(BadRequest(new JObject { new JProperty("errorCode", "version-unsupported"), new JProperty("supported", new JArray(1)), new JProperty("message", "This version of payjoin is not supported.") })); } await using var ctx = new PayjoinReceiverContext(_invoiceRepository, _explorerClientProvider.GetExplorerClient(network), _payJoinRepository); ObjectResult CreatePayjoinErrorAndLog(int httpCode, PayjoinReceiverWellknownErrors err, string debug) { ctx.Logs.Write($"Payjoin error: {debug}", InvoiceEventData.EventSeverity.Error); return(StatusCode(httpCode, CreatePayjoinError(err, debug))); } var explorer = _explorerClientProvider.GetExplorerClient(network); if (Request.ContentLength is long length) { if (length > 1_000_000) { return(this.StatusCode(413, CreatePayjoinError("payload-too-large", "The transaction is too big to be processed"))); } } else { return(StatusCode(411, CreatePayjoinError("missing-content-length", "The http header Content-Length should be filled"))); } string rawBody; using (StreamReader reader = new StreamReader(Request.Body, Encoding.UTF8)) { rawBody = (await reader.ReadToEndAsync()) ?? string.Empty; } FeeRate originalFeeRate = null; bool psbtFormat = true; if (PSBT.TryParse(rawBody, network.NBitcoinNetwork, out var psbt)) { if (!psbt.IsAllFinalized()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT should be finalized"))); } ctx.OriginalTransaction = psbt.ExtractTransaction(); } // BTCPay Server implementation support a transaction instead of PSBT else { psbtFormat = false; if (!Transaction.TryParse(rawBody, network.NBitcoinNetwork, out var tx)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "invalid transaction or psbt"))); } ctx.OriginalTransaction = tx; psbt = PSBT.FromTransaction(tx, network.NBitcoinNetwork); psbt = (await explorer.UpdatePSBTAsync(new UpdatePSBTRequest() { PSBT = psbt })).PSBT; for (int i = 0; i < tx.Inputs.Count; i++) { psbt.Inputs[i].FinalScriptSig = tx.Inputs[i].ScriptSig; psbt.Inputs[i].FinalScriptWitness = tx.Inputs[i].WitScript; } } FeeRate senderMinFeeRate = minfeerate >= 0.0m ? new FeeRate(minfeerate) : null; Money allowedSenderFeeContribution = Money.Satoshis(maxadditionalfeecontribution is long t && t >= 0 ? t : 0); var sendersInputType = psbt.GetInputsScriptPubKeyType(); if (psbt.CheckSanity() is var errors && errors.Count != 0) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"This PSBT is insane ({errors[0]})"))); } if (!psbt.TryGetEstimatedFeeRate(out originalFeeRate)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "You need to provide Witness UTXO information to the PSBT."))); } // This is actually not a mandatory check, but we don't want implementers // to leak global xpubs if (psbt.GlobalXPubs.Any()) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "GlobalXPubs should not be included in the PSBT"))); } if (psbt.Outputs.Any(o => o.HDKeyPaths.Count != 0) || psbt.Inputs.Any(o => o.HDKeyPaths.Count != 0)) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "Keypath information should not be included in the PSBT"))); } if (psbt.Inputs.Any(o => !o.IsFinalized())) { return(BadRequest(CreatePayjoinError("original-psbt-rejected", "The PSBT Should be finalized"))); } //////////// var mempool = await explorer.BroadcastAsync(ctx.OriginalTransaction, true); if (!mempool.Success) { ctx.DoNotBroadcast(); return(BadRequest(CreatePayjoinError("original-psbt-rejected", $"Provided transaction isn't mempool eligible {mempool.RPCCodeMessage}"))); } var enforcedLowR = ctx.OriginalTransaction.Inputs.All(IsLowR); var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); bool paidSomething = false; Money due = null; Dictionary <OutPoint, UTXO> selectedUTXOs = new Dictionary <OutPoint, UTXO>(); PSBTOutput originalPaymentOutput = null; BitcoinAddress paymentAddress = null; KeyPath paymentAddressIndex = null; InvoiceEntity invoice = null; DerivationSchemeSettings derivationSchemeSettings = null; WalletId walletId = null; foreach (var output in psbt.Outputs) { var walletReceiveMatch = _walletReceiveService.GetByScriptPubKey(network.CryptoCode, output.ScriptPubKey); if (walletReceiveMatch is null) { var key = output.ScriptPubKey.Hash + "#" + network.CryptoCode.ToUpperInvariant(); invoice = (await _invoiceRepository.GetInvoicesFromAddresses(new[] { key })).FirstOrDefault(); if (invoice is null) { continue; } derivationSchemeSettings = invoice .GetSupportedPaymentMethod <DerivationSchemeSettings>(paymentMethodId) .SingleOrDefault(); walletId = new WalletId(invoice.StoreId, network.CryptoCode.ToUpperInvariant()); } else { var store = await _storeRepository.FindStore(walletReceiveMatch.Item1.StoreId); derivationSchemeSettings = store.GetDerivationSchemeSettings(_btcPayNetworkProvider, walletReceiveMatch.Item1.CryptoCode); walletId = walletReceiveMatch.Item1; } if (derivationSchemeSettings is null) { continue; } var receiverInputsType = derivationSchemeSettings.AccountDerivation.ScriptPubKeyType(); if (receiverInputsType == ScriptPubKeyType.Legacy) { //this should never happen, unless the store owner changed the wallet mid way through an invoice return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Our wallet does not support payjoin")); } if (sendersInputType is ScriptPubKeyType t1 && t1 != receiverInputsType) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for making a payjoin with the sender's inputs type")); } if (walletReceiveMatch is null) { var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentDetails = paymentMethod.GetPaymentMethodDetails() as Payments.Bitcoin.BitcoinLikeOnChainPaymentMethod; if (paymentDetails is null || !paymentDetails.PayjoinEnabled) { continue; } paidSomething = true; due = paymentMethod.Calculate().TotalDue - output.Value; if (due > Money.Zero) { break; } paymentAddress = paymentDetails.GetDepositAddress(network.NBitcoinNetwork); paymentAddressIndex = paymentDetails.KeyPath; if (invoice.GetAllBitcoinPaymentData(false).Any()) { ctx.DoNotBroadcast(); return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The invoice this PSBT is paying has already been partially or completely paid"))); } } else { paidSomething = true; due = Money.Zero; paymentAddress = walletReceiveMatch.Item2.Address; paymentAddressIndex = walletReceiveMatch.Item2.KeyPath; } if (!await _payJoinRepository.TryLockInputs(ctx.OriginalTransaction.Inputs.Select(i => i.PrevOut).ToArray())) { // We do not broadcast, since we might double spend a delayed transaction of a previous payjoin ctx.DoNotBroadcast(); return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "Some of those inputs have already been used to make another payjoin transaction")); } var utxos = (await explorer.GetUTXOsAsync(derivationSchemeSettings.AccountDerivation)) .GetUnspentUTXOs(false); // In case we are paying ourselves, be need to make sure // we can't take spent outpoints. var prevOuts = ctx.OriginalTransaction.Inputs.Select(o => o.PrevOut).ToHashSet(); utxos = utxos.Where(u => !prevOuts.Contains(u.Outpoint)).ToArray(); Array.Sort(utxos, UTXODeterministicComparer.Instance); foreach (var utxo in (await SelectUTXO(network, utxos, psbt.Inputs.Select(input => input.WitnessUtxo.Value.ToDecimal(MoneyUnit.BTC)), output.Value.ToDecimal(MoneyUnit.BTC), psbt.Outputs.Where(psbtOutput => psbtOutput.Index != output.Index).Select(psbtOutput => psbtOutput.Value.ToDecimal(MoneyUnit.BTC)))).selectedUTXO) { selectedUTXOs.Add(utxo.Outpoint, utxo); } ctx.LockedUTXOs = selectedUTXOs.Select(u => u.Key).ToArray(); originalPaymentOutput = output; break; } if (!paidSomething) { return(BadRequest(CreatePayjoinError("invoice-not-found", "This transaction does not pay any invoice with payjoin"))); } if (due is null || due > Money.Zero) { return(BadRequest(CreatePayjoinError("invoice-not-fully-paid", "The transaction must pay the whole invoice"))); } if (selectedUTXOs.Count == 0) { return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "We do not have any UTXO available for contributing to a payjoin")); } var originalPaymentValue = originalPaymentOutput.Value; await _broadcaster.Schedule(DateTimeOffset.UtcNow + TimeSpan.FromMinutes(2.0), ctx.OriginalTransaction, network); //check if wallet of store is configured to be hot wallet var extKeyStr = await explorer.GetMetadataAsync <string>( derivationSchemeSettings.AccountDerivation, WellknownMetadataKeys.AccountHDKey); if (extKeyStr == null) { // This should not happen, as we check the existance of private key before creating invoice with payjoin return(CreatePayjoinErrorAndLog(503, PayjoinReceiverWellknownErrors.Unavailable, "The HD Key of the store changed")); } Money contributedAmount = Money.Zero; var newTx = ctx.OriginalTransaction.Clone(); var ourNewOutput = newTx.Outputs[originalPaymentOutput.Index]; HashSet <TxOut> isOurOutput = new HashSet <TxOut>(); isOurOutput.Add(ourNewOutput); TxOut feeOutput = additionalfeeoutputindex is int feeOutputIndex && maxadditionalfeecontribution is long v3 && v3 >= 0 && feeOutputIndex >= 0 && feeOutputIndex < newTx.Outputs.Count && !isOurOutput.Contains(newTx.Outputs[feeOutputIndex]) ? newTx.Outputs[feeOutputIndex] : null; int senderInputCount = newTx.Inputs.Count; foreach (var selectedUTXO in selectedUTXOs.Select(o => o.Value)) { contributedAmount += (Money)selectedUTXO.Value; var newInput = newTx.Inputs.Add(selectedUTXO.Outpoint); newInput.Sequence = newTx.Inputs[(int)(RandomUtils.GetUInt32() % senderInputCount)].Sequence; } ourNewOutput.Value += contributedAmount; var minRelayTxFee = this._dashboard.Get(network.CryptoCode).Status.BitcoinStatus?.MinRelayTxFee ?? new FeeRate(1.0m); // Remove old signatures as they are not valid anymore foreach (var input in newTx.Inputs) { input.WitScript = WitScript.Empty; } Money ourFeeContribution = Money.Zero; // We need to adjust the fee to keep a constant fee rate var txBuilder = network.NBitcoinNetwork.CreateTransactionBuilder(); var coins = psbt.Inputs.Select(i => i.GetSignableCoin()) .Concat(selectedUTXOs.Select(o => o.Value.AsCoin(derivationSchemeSettings.AccountDerivation))).ToArray(); txBuilder.AddCoins(coins); Money expectedFee = txBuilder.EstimateFees(newTx, originalFeeRate); Money actualFee = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); Money additionalFee = expectedFee - actualFee; if (additionalFee > Money.Zero) { // If the user overpaid, taking fee on our output (useful if sender dump a full UTXO for privacy) for (int i = 0; i < newTx.Outputs.Count && additionalFee > Money.Zero && due < Money.Zero && !invoice.IsUnsetTopUp(); i++) { if (disableoutputsubstitution) { break; } if (isOurOutput.Contains(newTx.Outputs[i])) { var outputContribution = Money.Min(additionalFee, -due); outputContribution = Money.Min(outputContribution, newTx.Outputs[i].Value - newTx.Outputs[i].GetDustThreshold(minRelayTxFee)); newTx.Outputs[i].Value -= outputContribution; additionalFee -= outputContribution; due += outputContribution; ourFeeContribution += outputContribution; } } // The rest, we take from user's change if (feeOutput != null) { var outputContribution = Money.Min(additionalFee, feeOutput.Value); outputContribution = Money.Min(outputContribution, feeOutput.Value - feeOutput.GetDustThreshold(minRelayTxFee)); outputContribution = Money.Min(outputContribution, allowedSenderFeeContribution); feeOutput.Value -= outputContribution; additionalFee -= outputContribution; allowedSenderFeeContribution -= outputContribution; } if (additionalFee > Money.Zero) { // We could not pay fully the additional fee, however, as long as // we are not under the relay fee, it should be OK. var newVSize = txBuilder.EstimateSize(newTx, true); var newFeePaid = newTx.GetFee(txBuilder.FindSpentCoins(newTx)); if (new FeeRate(newFeePaid, newVSize) < (senderMinFeeRate ?? minRelayTxFee)) { return(CreatePayjoinErrorAndLog(422, PayjoinReceiverWellknownErrors.NotEnoughMoney, "Not enough money is sent to pay for the additional payjoin inputs")); } } } var accountKey = ExtKey.Parse(extKeyStr, network.NBitcoinNetwork); var newPsbt = PSBT.FromTransaction(newTx, network.NBitcoinNetwork); foreach (var selectedUtxo in selectedUTXOs.Select(o => o.Value)) { var signedInput = newPsbt.Inputs.FindIndexedInput(selectedUtxo.Outpoint); var coin = selectedUtxo.AsCoin(derivationSchemeSettings.AccountDerivation); signedInput.UpdateFromCoin(coin); var privateKey = accountKey.Derive(selectedUtxo.KeyPath).PrivateKey; signedInput.PSBT.Settings.SigningOptions = new SigningOptions() { EnforceLowR = enforcedLowR }; signedInput.Sign(privateKey); signedInput.FinalizeInput(); newTx.Inputs[signedInput.Index].WitScript = newPsbt.Inputs[(int)signedInput.Index].FinalScriptWitness; } // Add the transaction to the payments with a confirmation of -1. // This will make the invoice paid even if the user do not // broadcast the payjoin. var originalPaymentData = new BitcoinLikePaymentData(paymentAddress, originalPaymentOutput.Value, new OutPoint(ctx.OriginalTransaction.GetHash(), originalPaymentOutput.Index), ctx.OriginalTransaction.RBF, paymentAddressIndex); originalPaymentData.ConfirmationCount = -1; originalPaymentData.PayjoinInformation = new PayjoinInformation() { CoinjoinTransactionHash = GetExpectedHash(newPsbt, coins), CoinjoinValue = originalPaymentValue - ourFeeContribution, ContributedOutPoints = selectedUTXOs.Select(o => o.Key).ToArray() }; if (invoice != null) { var payment = await _invoiceRepository.AddPayment(invoice.Id, DateTimeOffset.UtcNow, originalPaymentData, network, true); if (payment is null) { return(UnprocessableEntity(CreatePayjoinError("already-paid", $"The original transaction has already been accounted"))); } _eventAggregator.Publish(new InvoiceEvent(invoice, InvoiceEvent.ReceivedPayment) { Payment = payment }); } await _btcPayWalletProvider.GetWallet(network).SaveOffchainTransactionAsync(ctx.OriginalTransaction); _eventAggregator.Publish(new UpdateTransactionLabel() { WalletId = walletId, TransactionLabels = selectedUTXOs.GroupBy(pair => pair.Key.Hash).Select(utxo => new KeyValuePair <uint256, List <(string color, Label label)> >(utxo.Key, new List <(string color, Label label)>() { UpdateTransactionLabel.PayjoinExposedLabelTemplate(invoice?.Id) })) .ToDictionary(pair => pair.Key, pair => pair.Value) });
public async Task <IActionResult> LightningSettings(LightningSettingsViewModel vm) { var store = HttpContext.GetStoreData(); if (store == null) { return(NotFound()); } if (vm.CryptoCode == null) { ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network"); return(View(vm)); } var network = _ExplorerProvider.GetNetwork(vm.CryptoCode); var needUpdate = false; var blob = store.GetStoreBlob(); blob.LightningDescriptionTemplate = vm.LightningDescriptionTemplate ?? string.Empty; blob.LightningAmountInSatoshi = vm.LightningAmountInSatoshi; blob.LightningPrivateRouteHints = vm.LightningPrivateRouteHints; blob.OnChainWithLnInvoiceFallback = vm.OnChainWithLnInvoiceFallback; var disableBolt11PaymentMethod = vm.LNURLEnabled && vm.LNURLStandardInvoiceEnabled && vm.DisableBolt11PaymentMethod; var lnurlId = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay); blob.SetExcluded(lnurlId, !vm.LNURLEnabled); var lightning = GetExistingLightningSupportedPaymentMethod(vm.CryptoCode, store); // Going to mark "lightning" as non-null here assuming that if we are POSTing here it's because we have a Lightning Node set-up if (lightning !.DisableBOLT11PaymentOption != disableBolt11PaymentMethod) { needUpdate = true; lightning.DisableBOLT11PaymentOption = disableBolt11PaymentMethod; store.SetSupportedPaymentMethod(lightning); } var lnurl = GetExistingLNURLSupportedPaymentMethod(vm.CryptoCode, store); if (lnurl is null || ( lnurl.EnableForStandardInvoices != vm.LNURLStandardInvoiceEnabled || lnurl.UseBech32Scheme != vm.LNURLBech32Mode || lnurl.LUD12Enabled != vm.LUD12Enabled)) { needUpdate = true; } store.SetSupportedPaymentMethod(new LNURLPaySupportedPaymentMethod { CryptoCode = vm.CryptoCode, EnableForStandardInvoices = vm.LNURLStandardInvoiceEnabled, UseBech32Scheme = vm.LNURLBech32Mode, LUD12Enabled = vm.LUD12Enabled }); if (store.SetStoreBlob(blob)) { needUpdate = true; } if (needUpdate) { await _Repo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning settings successfully updated."; } return(RedirectToAction(nameof(LightningSettings), new { vm.StoreId, vm.CryptoCode })); }
public async Task <StatusMessageModel> DoSpecificAction(string action, string[] payoutIds, string storeId) { switch (action) { case "mark-paid": await using (var context = _dbContextFactory.CreateContext()) { var payouts = (await context.Payouts .Include(p => p.PullPaymentData) .Include(p => p.PullPaymentData.StoreData) .Where(p => payoutIds.Contains(p.Id)) .Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingPayment) .ToListAsync()).Where(data => PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) && CanHandle(paymentMethodId)) .Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == false); foreach (var valueTuple in payouts) { valueTuple.Item2.Accounted = true; valueTuple.data.State = PayoutState.InProgress; SetProofBlob(valueTuple.data, valueTuple.Item2); } await context.SaveChangesAsync(); } return(new StatusMessageModel() { Message = "Payout payments have been marked confirmed", Severity = StatusMessageModel.StatusSeverity.Success }); case "reject-payment": await using (var context = _dbContextFactory.CreateContext()) { var payouts = (await context.Payouts .Include(p => p.PullPaymentData) .Include(p => p.PullPaymentData.StoreData) .Where(p => payoutIds.Contains(p.Id)) .Where(p => p.PullPaymentData.StoreId == storeId && !p.PullPaymentData.Archived && p.State == PayoutState.AwaitingPayment) .ToListAsync()).Where(data => PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) && CanHandle(paymentMethodId)) .Select(data => (data, ParseProof(data) as PayoutTransactionOnChainBlob)).Where(tuple => tuple.Item2 != null && tuple.Item2.TransactionId != null && tuple.Item2.Accounted == true); foreach (var valueTuple in payouts) { valueTuple.Item2.TransactionId = null; SetProofBlob(valueTuple.data, valueTuple.Item2); } await context.SaveChangesAsync(); } return(new StatusMessageModel() { Message = "Payout payments have been unmarked", Severity = StatusMessageModel.StatusSeverity.Success }); } return(null); }
public bool CanHandle(PaymentMethodId paymentMethod) { return(paymentMethod?.PaymentType == BitcoinPaymentType.Instance && _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false); }
public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId) { GetPaymentMethods().TryGetValue(paymentMethodId, out var data); return(data); }
public static void SetDefaultPaymentId(this StoreData storeData, PaymentMethodId defaultPaymentId) { storeData.DefaultCrypto = defaultPaymentId.ToString(); }
private async Task <PaymentModel> GetInvoiceModel(string invoiceId, PaymentMethodId paymentMethodId) { var invoice = await _InvoiceRepository.GetInvoice(invoiceId); if (invoice == null) { return(null); } var store = await _StoreRepository.FindStore(invoice.StoreId); bool isDefaultPaymentId = false; if (paymentMethodId == null) { paymentMethodId = store.GetDefaultPaymentId(_NetworkProvider); isDefaultPaymentId = true; } BTCPayNetworkBase network = _NetworkProvider.GetNetwork <BTCPayNetworkBase>(paymentMethodId.CryptoCode); if (network == null && isDefaultPaymentId) { //TODO: need to look into a better way for this as it does not scale network = _NetworkProvider.GetAll().OfType <BTCPayNetwork>().FirstOrDefault(); paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.BTCLike); } if (invoice == null || network == null) { return(null); } if (!invoice.Support(paymentMethodId)) { if (!isDefaultPaymentId) { return(null); } var paymentMethodTemp = invoice.GetPaymentMethods() .Where(c => paymentMethodId.CryptoCode == c.GetId().CryptoCode) .FirstOrDefault(); if (paymentMethodTemp == null) { paymentMethodTemp = invoice.GetPaymentMethods().First(); } network = paymentMethodTemp.Network; paymentMethodId = paymentMethodTemp.GetId(); } var paymentMethod = invoice.GetPaymentMethod(paymentMethodId); var paymentMethodDetails = paymentMethod.GetPaymentMethodDetails(); var dto = invoice.EntityToDTO(); var cryptoInfo = dto.CryptoInfo.First(o => o.GetpaymentMethodId() == paymentMethodId); var storeBlob = store.GetStoreBlob(); var currency = invoice.ProductInformation.Currency; var accounting = paymentMethod.Calculate(); ChangellySettings changelly = (storeBlob.ChangellySettings != null && storeBlob.ChangellySettings.Enabled && storeBlob.ChangellySettings.IsConfigured()) ? storeBlob.ChangellySettings : null; CoinSwitchSettings coinswitch = (storeBlob.CoinSwitchSettings != null && storeBlob.CoinSwitchSettings.Enabled && storeBlob.CoinSwitchSettings.IsConfigured()) ? storeBlob.CoinSwitchSettings : null; var changellyAmountDue = changelly != null ? (accounting.Due.ToDecimal(MoneyUnit.BTC) * (1m + (changelly.AmountMarkupPercentage / 100m))) : (decimal?)null; var paymentMethodHandler = _paymentMethodHandlerDictionary[paymentMethodId]; var model = new PaymentModel() { CryptoCode = network.CryptoCode, RootPath = this.Request.PathBase.Value.WithTrailingSlash(), OrderId = invoice.OrderId, InvoiceId = invoice.Id, DefaultLang = storeBlob.DefaultLang ?? "en", HtmlTitle = storeBlob.HtmlTitle ?? "BTCPay Invoice", CustomCSSLink = storeBlob.CustomCSS, CustomLogoLink = storeBlob.CustomLogo, CryptoImage = Request.GetRelativePathOrAbsolute(paymentMethodHandler.GetCryptoImage(paymentMethodId)), BtcAddress = paymentMethodDetails.GetPaymentDestination(), BtcDue = accounting.Due.ToString(), OrderAmount = (accounting.TotalDue - accounting.NetworkFee).ToString(), OrderAmountFiat = OrderAmountFromInvoice(network.CryptoCode, invoice.ProductInformation), CustomerEmail = invoice.RefundMail, RequiresRefundEmail = storeBlob.RequiresRefundEmail, ShowRecommendedFee = storeBlob.ShowRecommendedFee, FeeRate = paymentMethodDetails.GetFeeRate(), ExpirationSeconds = Math.Max(0, (int)(invoice.ExpirationTime - DateTimeOffset.UtcNow).TotalSeconds), MaxTimeSeconds = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalSeconds, MaxTimeMinutes = (int)(invoice.ExpirationTime - invoice.InvoiceTime).TotalMinutes, ItemDesc = invoice.ProductInformation.ItemDesc, Rate = ExchangeRate(paymentMethod), MerchantRefLink = invoice.RedirectURL?.AbsoluteUri ?? "/", RedirectAutomatically = invoice.RedirectAutomatically, StoreName = store.StoreName, PeerInfo = (paymentMethodDetails as LightningLikePaymentMethodDetails)?.NodeInfo, TxCount = accounting.TxRequired, BtcPaid = accounting.Paid.ToString(), #pragma warning disable CS0618 // Type or member is obsolete Status = invoice.StatusString, #pragma warning restore CS0618 // Type or member is obsolete NetworkFee = paymentMethodDetails.GetNextNetworkFee(), IsMultiCurrency = invoice.GetPayments().Select(p => p.GetPaymentMethodId()).Concat(new[] { paymentMethod.GetId() }).Distinct().Count() > 1, ChangellyEnabled = changelly != null, ChangellyMerchantId = changelly?.ChangellyMerchantId, ChangellyAmountDue = changellyAmountDue, CoinSwitchEnabled = coinswitch != null, CoinSwitchAmountMarkupPercentage = coinswitch?.AmountMarkupPercentage ?? 0, CoinSwitchMerchantId = coinswitch?.MerchantId, CoinSwitchMode = coinswitch?.Mode, StoreId = store.Id, AvailableCryptos = invoice.GetPaymentMethods() .Where(i => i.Network != null) .Select(kv => { var availableCryptoPaymentMethodId = kv.GetId(); var availableCryptoHandler = _paymentMethodHandlerDictionary[availableCryptoPaymentMethodId]; return(new PaymentModel.AvailableCrypto() { PaymentMethodId = kv.GetId().ToString(), CryptoCode = kv.GetId().CryptoCode, PaymentMethodName = availableCryptoHandler.GetPaymentMethodName(availableCryptoPaymentMethodId), IsLightning = kv.GetId().PaymentType == PaymentTypes.LightningLike, CryptoImage = Request.GetRelativePathOrAbsolute(availableCryptoHandler.GetCryptoImage(availableCryptoPaymentMethodId)), Link = Url.Action(nameof(Checkout), new { invoiceId = invoiceId, paymentMethodId = kv.GetId().ToString() }) }); }).Where(c => c.CryptoImage != "/") .OrderByDescending(a => a.CryptoCode == "BTC").ThenBy(a => a.PaymentMethodName).ThenBy(a => a.IsLightning ? 1 : 0) .ToList() }; paymentMethodHandler.PreparePaymentModel(model, dto, storeBlob); if (model.IsLightning && storeBlob.LightningAmountInSatoshi && model.CryptoCode == "Sats") { model.Rate = _CurrencyNameTable.DisplayFormatCurrency(paymentMethod.Rate / 100_000_000, paymentMethod.ParentEntity.ProductInformation.Currency); } model.UISettings = paymentMethodHandler.GetCheckoutUISettings(); model.PaymentMethodId = paymentMethodId.ToString(); var expiration = TimeSpan.FromSeconds(model.ExpirationSeconds); model.TimeLeft = expiration.PrettyPrint(); return(model); }
public async Task <IActionResult> GetStatus(string invoiceId, string paymentMethodId = null) { var model = await GetInvoiceModel(invoiceId, paymentMethodId == null?null : PaymentMethodId.Parse(paymentMethodId)); if (model == null) { return(NotFound()); } return(Json(model)); }
public async Task <IActionResult> CheckoutNoScript(string invoiceId, string id = null, string paymentMethodId = null) { //Keep compatibility with Bitpay invoiceId = invoiceId ?? id; id = invoiceId; // var model = await GetInvoiceModel(invoiceId, paymentMethodId == null?null : PaymentMethodId.Parse(paymentMethodId)); if (model == null) { return(NotFound()); } return(View(model)); }
public async Task <IActionResult> Checkout(string invoiceId, string id = null, string paymentMethodId = null, [FromQuery] string view = null) { //Keep compatibility with Bitpay invoiceId = invoiceId ?? id; id = invoiceId; // var model = await GetInvoiceModel(invoiceId, paymentMethodId == null?null : PaymentMethodId.Parse(paymentMethodId)); if (model == null) { return(NotFound()); } if (view == "modal") { model.IsModal = true; } _CSP.Add(new ConsentSecurityPolicy("script-src", "'unsafe-eval'")); // Needed by Vue if (!string.IsNullOrEmpty(model.CustomCSSLink) && Uri.TryCreate(model.CustomCSSLink, UriKind.Absolute, out var uri)) { _CSP.Clear(); } if (!string.IsNullOrEmpty(model.CustomLogoLink) && Uri.TryCreate(model.CustomLogoLink, UriKind.Absolute, out uri)) { _CSP.Clear(); } return(View(nameof(Checkout), model)); }
public AddressInvoiceData Set(string address, PaymentMethodId paymentMethodId) { Address = address + "#" + paymentMethodId?.ToString(); return(this); }
public static PaymentMethodId?GetDefaultPaymentId(this StoreData storeData) { PaymentMethodId.TryParse(storeData.DefaultCrypto, out var defaultPaymentId); return(defaultPaymentId); }
public async Task <IActionResult> CheckoutExperience(CheckoutExperienceViewModel model) { CurrencyValue lightningMaxValue = null; if (!string.IsNullOrWhiteSpace(model.LightningMaxValue)) { if (!CurrencyValue.TryParse(model.LightningMaxValue, out lightningMaxValue)) { ModelState.AddModelError(nameof(model.LightningMaxValue), "Invalid lightning max value"); } } CurrencyValue onchainMinValue = null; if (!string.IsNullOrWhiteSpace(model.OnChainMinValue)) { if (!CurrencyValue.TryParse(model.OnChainMinValue, out onchainMinValue)) { ModelState.AddModelError(nameof(model.OnChainMinValue), "Invalid on chain min value"); } } bool needUpdate = false; var blob = CurrentStore.GetStoreBlob(); var defaultPaymentMethodId = model.DefaultPaymentMethod == null ? null : PaymentMethodId.Parse(model.DefaultPaymentMethod); if (CurrentStore.GetDefaultPaymentId(_NetworkProvider) != defaultPaymentMethodId) { needUpdate = true; CurrentStore.SetDefaultPaymentId(defaultPaymentMethodId); } SetCryptoCurrencies(model, CurrentStore); model.SetLanguages(_LangService, model.DefaultLang); if (!ModelState.IsValid) { return(View(model)); } blob.CustomLogo = string.IsNullOrWhiteSpace(model.CustomLogo) ? null : new Uri(model.CustomLogo, UriKind.Absolute); blob.CustomCSS = string.IsNullOrWhiteSpace(model.CustomCSS) ? null : new Uri(model.CustomCSS, UriKind.Absolute); blob.HtmlTitle = string.IsNullOrWhiteSpace(model.HtmlTitle) ? null : model.HtmlTitle; blob.DefaultLang = model.DefaultLang; blob.RequiresRefundEmail = model.RequiresRefundEmail; blob.OnChainMinValue = onchainMinValue; blob.LightningMaxValue = lightningMaxValue; blob.LightningAmountInSatoshi = model.LightningAmountInSatoshi; blob.RedirectAutomatically = model.RedirectAutomatically; if (CurrentStore.SetStoreBlob(blob)) { needUpdate = true; } if (needUpdate) { await _Repo.UpdateStore(CurrentStore); StatusMessage = "Store successfully updated"; } return(RedirectToAction(nameof(CheckoutExperience), new { storeId = CurrentStore.Id })); }
#pragma warning disable CS0618 public static PaymentMethodId GetDefaultPaymentId(this StoreData storeData, BTCPayNetworkProvider networks) { PaymentMethodId[] paymentMethodIds = storeData.GetEnabledPaymentIds(networks); var defaultPaymentId = string.IsNullOrEmpty(storeData.DefaultCrypto) ? null : PaymentMethodId.Parse(storeData.DefaultCrypto); var chosen = paymentMethodIds.FirstOrDefault(f => f == defaultPaymentId) ?? paymentMethodIds.FirstOrDefault(f => f.CryptoCode == defaultPaymentId?.CryptoCode) ?? paymentMethodIds.FirstOrDefault(); return(chosen); }
internal bool Support(PaymentMethodId paymentMethodId) { var rates = GetPaymentMethods(null); return(rates.TryGet(paymentMethodId) != null); }
private static void MarkUnassigned(string invoiceId, InvoiceEntity entity, ApplicationDbContext context, PaymentMethodId paymentMethodId) { foreach (var address in entity.GetPaymentMethods()) { if (paymentMethodId != null && paymentMethodId != address.GetId()) { continue; } var historical = new HistoricalAddressInvoiceData(); historical.InvoiceDataId = invoiceId; historical.SetAddress(address.GetPaymentMethodDetails().GetPaymentDestination(), address.GetId().ToString()); historical.UnAssigned = DateTimeOffset.UtcNow; context.Attach(historical); context.Entry(historical).Property(o => o.UnAssigned).IsModified = true; } }
public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networkProvider) { GetPaymentMethods(networkProvider).TryGetValue(paymentMethodId, out var data); return(data); }
private async Task UpdatePayoutsAwaitingForPayment(NewOnChainTransactionEvent newTransaction, AddressTrackedSource addressTrackedSource) { try { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(newTransaction.CryptoCode); var destinationSum = newTransaction.NewTransactionEvent.Outputs.Sum(output => output.Value.GetValue(network)); var destination = addressTrackedSource.Address.ToString(); var paymentMethodId = new PaymentMethodId(newTransaction.CryptoCode, BitcoinPaymentType.Instance); await using var ctx = _dbContextFactory.CreateContext(); var payouts = await ctx.Payouts .Include(o => o.PullPaymentData) .ThenInclude(o => o.StoreData) .Where(p => p.State == PayoutState.AwaitingPayment) .Where(p => p.PaymentMethodId == paymentMethodId.ToString()) #pragma warning disable CA1307 // Specify StringComparison .Where(p => destination.Equals(p.Destination)) #pragma warning restore CA1307 // Specify StringComparison .ToListAsync(); var payoutByDestination = payouts.ToDictionary(p => p.Destination); if (!payoutByDestination.TryGetValue(destination, out var payout)) { return; } var payoutBlob = payout.GetBlob(_jsonSerializerSettings); if (payoutBlob.CryptoAmount is null || // The round up here is not strictly necessary, this is temporary to fix existing payout before we // were properly roundup the crypto amount destinationSum != BTCPayServer.Extensions.RoundUp(payoutBlob.CryptoAmount.Value, network.Divisibility)) { return; } var derivationSchemeSettings = payout.PullPaymentData.StoreData .GetDerivationSchemeSettings(_btcPayNetworkProvider, newTransaction.CryptoCode).AccountDerivation; var storeWalletMatched = (await _explorerClientProvider.GetExplorerClient(newTransaction.CryptoCode) .GetTransactionAsync(derivationSchemeSettings, newTransaction.NewTransactionEvent.TransactionData.TransactionHash)); //if the wallet related to the store related to the payout does not have the tx: it is external var isInternal = storeWalletMatched is { }; var proof = ParseProof(payout) as PayoutTransactionOnChainBlob ?? new PayoutTransactionOnChainBlob() { Accounted = isInternal }; var txId = newTransaction.NewTransactionEvent.TransactionData.TransactionHash; if (!proof.Candidates.Add(txId)) { return; } if (isInternal) { payout.State = PayoutState.InProgress; var walletId = new WalletId(payout.PullPaymentData.StoreId, newTransaction.CryptoCode); _eventAggregator.Publish(new UpdateTransactionLabel(walletId, newTransaction.NewTransactionEvent.TransactionData.TransactionHash, UpdateTransactionLabel.PayoutTemplate(payout.Id, payout.PullPaymentDataId, walletId.ToString()))); } else { await _notificationSender.SendNotification(new StoreScope(payout.PullPaymentData.StoreId), new ExternalPayoutTransactionNotification() { PaymentMethod = payout.PaymentMethodId, PayoutId = payout.Id, StoreId = payout.PullPaymentData.StoreId }); } proof.TransactionId ??= txId; SetProofBlob(payout, proof); await ctx.SaveChangesAsync(); } catch (Exception ex) { Logs.PayServer.LogWarning(ex, "Error while processing a transaction in the pull payment hosted service"); } }
public override async Task <string> IsPaymentMethodAllowedBasedOnInvoiceAmount(StoreBlob storeBlob, Dictionary <CurrencyPair, Task <RateResult> > rate, Money amount, PaymentMethodId paymentMethodId) { if (storeBlob.LightningMaxValue != null) { var currentRateToCrypto = await rate[new CurrencyPair(paymentMethodId.CryptoCode, storeBlob.LightningMaxValue.Currency)]; if (currentRateToCrypto?.BidAsk != null) { var limitValueCrypto = Money.Coins(storeBlob.LightningMaxValue.Value / currentRateToCrypto.BidAsk.Bid); if (amount > limitValueCrypto) { return("The amount of the invoice is too high to be paid with lightning"); } } } return(string.Empty); }
public Task <(IClaimDestination destination, string error)> ParseClaimDestination(PaymentMethodId paymentMethodId, string destination) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(paymentMethodId.CryptoCode); destination = destination.Trim(); try { if (destination.StartsWith($"{network.NBitcoinNetwork.UriScheme}:", StringComparison.OrdinalIgnoreCase)) { return(Task.FromResult <(IClaimDestination, string)>((new UriClaimDestination(new BitcoinUrlBuilder(destination, network.NBitcoinNetwork)), null))); } return(Task.FromResult <(IClaimDestination, string)>((new AddressClaimDestination(BitcoinAddress.Create(destination, network.NBitcoinNetwork)), null))); } catch { return(Task.FromResult <(IClaimDestination, string)>( (null, "A valid address was not provided"))); } }
internal async Task <InvoiceEntity> CreateInvoiceCoreRaw(BitpayCreateInvoiceRequest invoice, StoreData store, string serverUrl, List <string> additionalTags = null, CancellationToken cancellationToken = default) { var storeBlob = store.GetStoreBlob(); var entity = _InvoiceRepository.CreateNewInvoice(); entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime + storeBlob.InvoiceExpiration; entity.MonitoringExpiration = entity.ExpirationTime + storeBlob.MonitoringExpiration; if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime) { throw new BitpayHttpException(400, "The expirationTime is set too soon"); } invoice.Currency = invoice.Currency?.Trim().ToUpperInvariant() ?? "USD"; entity.Metadata.OrderId = invoice.OrderId; entity.Metadata.PosData = invoice.PosData; entity.ServerUrl = serverUrl; entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications; entity.ExtendedNotifications = invoice.ExtendedNotifications; entity.NotificationURLTemplate = invoice.NotificationURL; entity.NotificationEmail = invoice.NotificationEmail; if (additionalTags != null) { entity.InternalTags.AddRange(additionalTags); } FillBuyerInfo(invoice, entity); var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m; var price = invoice.Price; entity.Metadata.ItemCode = invoice.ItemCode; entity.Metadata.ItemDesc = invoice.ItemDesc; entity.Metadata.Physical = invoice.Physical; entity.Metadata.TaxIncluded = invoice.TaxIncluded; entity.Currency = invoice.Currency; entity.Price = price; entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite; entity.RedirectAutomatically = invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically); entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); IPaymentFilter excludeFilter = null; if (invoice.PaymentCurrencies?.Any() is true) { invoice.SupportedTransactionCurrencies ??= new Dictionary <string, InvoiceSupportedTransactionCurrency>(); foreach (string paymentCurrency in invoice.PaymentCurrencies) { invoice.SupportedTransactionCurrencies.TryAdd(paymentCurrency, new InvoiceSupportedTransactionCurrency() { Enabled = true }); } } if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0) { var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies .Where(c => c.Value.Enabled) .Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null) .Where(c => c != null) .ToHashSet(); excludeFilter = PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p)); } entity.PaymentTolerance = storeBlob.PaymentTolerance; return(await CreateInvoiceCoreRaw(entity, store, excludeFilter, null, cancellationToken)); }
public async Task <IActionResult> SetupLightningNode(string storeId, LightningNodeViewModel vm, string command, string cryptoCode) { vm.CryptoCode = cryptoCode; var store = HttpContext.GetStoreData(); if (store == null) { return(NotFound()); } vm.CanUseInternalNode = await CanUseInternalLightning(); if (vm.CryptoCode == null) { ModelState.AddModelError(nameof(vm.CryptoCode), "Invalid network"); return(View(vm)); } var network = _ExplorerProvider.GetNetwork(vm.CryptoCode); var paymentMethodId = new PaymentMethodId(network.CryptoCode, PaymentTypes.LightningLike); LightningSupportedPaymentMethod?paymentMethod = null; if (vm.LightningNodeType == LightningNodeType.Internal) { if (!await CanUseInternalLightning()) { ModelState.AddModelError(nameof(vm.ConnectionString), "You are not authorized to use the internal lightning node"); return(View(vm)); } paymentMethod = new LightningSupportedPaymentMethod { CryptoCode = paymentMethodId.CryptoCode }; paymentMethod.SetInternalNode(); } else { if (string.IsNullOrEmpty(vm.ConnectionString)) { ModelState.AddModelError(nameof(vm.ConnectionString), "Please provide a connection string"); return(View(vm)); } if (!LightningConnectionString.TryParse(vm.ConnectionString, false, out var connectionString, out var error)) { ModelState.AddModelError(nameof(vm.ConnectionString), $"Invalid URL ({error})"); return(View(vm)); } if (connectionString.ConnectionType == LightningConnectionType.LndGRPC) { ModelState.AddModelError(nameof(vm.ConnectionString), $"GRSPay does not support gRPC connections"); return(View(vm)); } if (!User.IsInRole(Roles.ServerAdmin) && !connectionString.IsSafe()) { ModelState.AddModelError(nameof(vm.ConnectionString), "You are not a server admin, so the connection string should not contain 'cookiefilepath', 'macaroondirectorypath', 'macaroonfilepath', and should not point to a local ip or to a dns name ending with '.internal', '.local', '.lan' or '.'."); return(View(vm)); } paymentMethod = new LightningSupportedPaymentMethod { CryptoCode = paymentMethodId.CryptoCode }; paymentMethod.SetLightningUrl(connectionString); } switch (command) { case "save": var lnurl = new PaymentMethodId(vm.CryptoCode, PaymentTypes.LNURLPay); store.SetSupportedPaymentMethod(paymentMethodId, paymentMethod); store.SetSupportedPaymentMethod(lnurl, new LNURLPaySupportedPaymentMethod() { CryptoCode = vm.CryptoCode, UseBech32Scheme = true, EnableForStandardInvoices = false, LUD12Enabled = false }); await _Repo.UpdateStore(store); TempData[WellKnownTempData.SuccessMessage] = $"{network.CryptoCode} Lightning node updated."; return(RedirectToAction(nameof(LightningSettings), new { storeId, cryptoCode })); case "test": var handler = _ServiceProvider.GetRequiredService <LightningLikePaymentHandler>(); try { var info = await handler.GetNodeInfo(paymentMethod, network, new InvoiceLogs(), Request.IsOnion(), true); if (!vm.SkipPortTest) { using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(20)); await handler.TestConnection(info.First(), cts.Token); } TempData[WellKnownTempData.SuccessMessage] = $"Connection to the Lightning node successful. Your node address: {info.First()}"; } catch (Exception ex) { TempData[WellKnownTempData.ErrorMessage] = ex.Message; return(View(vm)); } return(View(vm)); default: return(View(vm)); } }
public override string GetPaymentMethodName(PaymentMethodId paymentMethodId) { var network = _networkProvider.GetNetwork <BTCPayNetwork>(paymentMethodId.CryptoCode); return(GetPaymentMethodName(network)); }
public InvoiceNewPaymentDetailsEvent(string invoiceId, IPaymentMethodDetails details, PaymentMethodId paymentMethodId) { InvoiceId = invoiceId; Details = details; PaymentMethodId = paymentMethodId; }
public async Task <IActionResult> GetLNURLForApp(string cryptoCode, string appId, string itemCode = null) { var network = _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(cryptoCode); if (network is null || !network.SupportLightning) { return(NotFound()); } var app = await _appService.GetApp(appId, null, true); if (app is null) { return(NotFound()); } var store = app.StoreData; if (store is null) { return(NotFound()); } if (string.IsNullOrEmpty(itemCode)) { return(NotFound()); } var pmi = new PaymentMethodId(cryptoCode, PaymentTypes.LNURLPay); var lnpmi = new PaymentMethodId(cryptoCode, PaymentTypes.LightningLike); var methods = store.GetSupportedPaymentMethods(_btcPayNetworkProvider); var lnUrlMethod = methods.FirstOrDefault(method => method.PaymentId == pmi) as LNURLPaySupportedPaymentMethod; var lnMethod = methods.FirstOrDefault(method => method.PaymentId == lnpmi); if (lnUrlMethod is null || lnMethod is null) { return(NotFound()); } ViewPointOfSaleViewModel.Item[] items = null; string currencyCode = null; switch (app.AppType) { case nameof(AppType.Crowdfund): var cfS = app.GetSettings <CrowdfundSettings>(); currencyCode = cfS.TargetCurrency; items = _appService.Parse(cfS.PerksTemplate, cfS.TargetCurrency); break; case nameof(AppType.PointOfSale): var posS = app.GetSettings <UIAppsController.PointOfSaleSettings>(); currencyCode = posS.Currency; items = _appService.Parse(posS.Template, posS.Currency); break; } var item = items.FirstOrDefault(item1 => item1.Id.Equals(itemCode, StringComparison.InvariantCultureIgnoreCase)); if (item is null || item.Inventory <= 0 || (item.PaymentMethods?.Any() is true && item.PaymentMethods?.Any(s => PaymentMethodId.Parse(s) == pmi) is false)) { return(NotFound()); } return(await GetLNURL(cryptoCode, app.StoreDataId, currencyCode, null, null, () => (null, new List <string> { AppService.GetAppInternalTag(appId) }, item.Price.Value, true))); }
internal async Task <DataWrapper <InvoiceResponse> > CreateInvoiceCore(CreateInvoiceRequest invoice, StoreData store, string serverUrl, List <string> additionalTags = null, CancellationToken cancellationToken = default) { invoice.Currency = invoice.Currency?.ToUpperInvariant() ?? "USD"; InvoiceLogs logs = new InvoiceLogs(); logs.Write("Creation of invoice starting"); var entity = _InvoiceRepository.CreateNewInvoice(); var getAppsTaggingStore = _InvoiceRepository.GetAppsTaggingStore(store.Id); var storeBlob = store.GetStoreBlob(); EmailAddressAttribute emailValidator = new EmailAddressAttribute(); entity.ExpirationTime = invoice.ExpirationTime is DateTimeOffset v ? v : entity.InvoiceTime.AddMinutes(storeBlob.InvoiceExpiration); if (entity.ExpirationTime - TimeSpan.FromSeconds(30.0) < entity.InvoiceTime) { throw new BitpayHttpException(400, "The expirationTime is set too soon"); } entity.MonitoringExpiration = entity.ExpirationTime + TimeSpan.FromMinutes(storeBlob.MonitoringExpiration); entity.OrderId = invoice.OrderId; entity.ServerUrl = serverUrl; entity.FullNotifications = invoice.FullNotifications || invoice.ExtendedNotifications; entity.ExtendedNotifications = invoice.ExtendedNotifications; entity.NotificationURLTemplate = invoice.NotificationURL; entity.NotificationEmail = invoice.NotificationEmail; entity.BuyerInformation = Map <CreateInvoiceRequest, BuyerInformation>(invoice); entity.PaymentTolerance = storeBlob.PaymentTolerance; if (additionalTags != null) { entity.InternalTags.AddRange(additionalTags); } //Another way of passing buyer info to support FillBuyerInfo(invoice.Buyer, entity.BuyerInformation); if (entity?.BuyerInformation?.BuyerEmail != null) { if (!EmailValidator.IsEmail(entity.BuyerInformation.BuyerEmail)) { throw new BitpayHttpException(400, "Invalid email"); } entity.RefundMail = entity.BuyerInformation.BuyerEmail; } var taxIncluded = invoice.TaxIncluded.HasValue ? invoice.TaxIncluded.Value : 0m; var currencyInfo = _CurrencyNameTable.GetNumberFormatInfo(invoice.Currency, false); if (currencyInfo != null) { int divisibility = currencyInfo.CurrencyDecimalDigits; invoice.Price = invoice.Price.RoundToSignificant(ref divisibility); divisibility = currencyInfo.CurrencyDecimalDigits; invoice.TaxIncluded = taxIncluded.RoundToSignificant(ref divisibility); } invoice.Price = Math.Max(0.0m, invoice.Price); invoice.TaxIncluded = Math.Max(0.0m, taxIncluded); invoice.TaxIncluded = Math.Min(taxIncluded, invoice.Price); entity.ProductInformation = Map <CreateInvoiceRequest, ProductInformation>(invoice); entity.RedirectURLTemplate = invoice.RedirectURL ?? store.StoreWebsite; entity.RedirectAutomatically = invoice.RedirectAutomatically.GetValueOrDefault(storeBlob.RedirectAutomatically); entity.Status = InvoiceStatus.New; entity.SpeedPolicy = ParseSpeedPolicy(invoice.TransactionSpeed, store.SpeedPolicy); HashSet <CurrencyPair> currencyPairsToFetch = new HashSet <CurrencyPair>(); var rules = storeBlob.GetRateRules(_NetworkProvider); var excludeFilter = storeBlob.GetExcludedPaymentMethods(); // Here we can compose filters from other origin with PaymentFilter.Any() if (invoice.PaymentCurrencies?.Any() is true) { foreach (string paymentCurrency in invoice.PaymentCurrencies) { invoice.SupportedTransactionCurrencies.TryAdd(paymentCurrency, new InvoiceSupportedTransactionCurrency() { Enabled = true }); } } if (invoice.SupportedTransactionCurrencies != null && invoice.SupportedTransactionCurrencies.Count != 0) { var supportedTransactionCurrencies = invoice.SupportedTransactionCurrencies .Where(c => c.Value.Enabled) .Select(c => PaymentMethodId.TryParse(c.Key, out var p) ? p : null) .ToHashSet(); excludeFilter = PaymentFilter.Or(excludeFilter, PaymentFilter.Where(p => !supportedTransactionCurrencies.Contains(p))); } foreach (var network in store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId)) .Select(c => _NetworkProvider.GetNetwork <BTCPayNetworkBase>(c.PaymentId.bitcoinCode)) .Where(c => c != null)) { currencyPairsToFetch.Add(new CurrencyPair(network.bitcoinCode, invoice.Currency)); //TODO: abstract if (storeBlob.LightningMaxValue != null) { currencyPairsToFetch.Add(new CurrencyPair(network.bitcoinCode, storeBlob.LightningMaxValue.Currency)); } if (storeBlob.OnChainMinValue != null) { currencyPairsToFetch.Add(new CurrencyPair(network.bitcoinCode, storeBlob.OnChainMinValue.Currency)); } } var rateRules = storeBlob.GetRateRules(_NetworkProvider); var fetchingByCurrencyPair = _RateProvider.FetchRates(currencyPairsToFetch, rateRules, cancellationToken); var fetchingAll = WhenAllFetched(logs, fetchingByCurrencyPair); var supportedPaymentMethods = store.GetSupportedPaymentMethods(_NetworkProvider) .Where(s => !excludeFilter.Match(s.PaymentId) && _paymentMethodHandlerDictionary.Support(s.PaymentId)) .Select(c => (Handler: _paymentMethodHandlerDictionary[c.PaymentId], SupportedPaymentMethod: c, Network: _NetworkProvider.GetNetwork <BTCPayNetworkBase>(c.PaymentId.bitcoinCode))) .Where(c => c.Network != null) .Select(o => (SupportedPaymentMethod: o.SupportedPaymentMethod, PaymentMethod: CreatePaymentMethodAsync(fetchingByCurrencyPair, o.Handler, o.SupportedPaymentMethod, o.Network, entity, store, logs))) .ToList(); List <ISupportedPaymentMethod> supported = new List <ISupportedPaymentMethod>(); var paymentMethods = new PaymentMethodDictionary(); foreach (var o in supportedPaymentMethods) { var paymentMethod = await o.PaymentMethod; if (paymentMethod == null) { continue; } supported.Add(o.SupportedPaymentMethod); paymentMethods.Add(paymentMethod); } if (supported.Count == 0) { StringBuilder errors = new StringBuilder(); if (!store.GetSupportedPaymentMethods(_NetworkProvider).Any()) { errors.AppendLine("Warning: No wallet has been linked to your BTCPay Store. See the following link for more information on how to connect your store and wallet. (https://docs.btcpayserver.org/WalletSetup/)"); } foreach (var error in logs.ToList()) { errors.AppendLine(error.ToString()); } throw new BitpayHttpException(400, errors.ToString()); } entity.SetSupportedPaymentMethods(supported); entity.SetPaymentMethods(paymentMethods); entity.PosData = invoice.PosData; foreach (var app in await getAppsTaggingStore) { entity.InternalTags.Add(AppService.GetAppInternalTag(app.Id)); } using (logs.Measure("Saving invoice")) { entity = await _InvoiceRepository.CreateInvoiceAsync(store.Id, entity); } _ = Task.Run(async() => { try { await fetchingAll; } catch (AggregateException ex) { ex.Handle(e => { logs.Write($"Error while fetching rates {ex}"); return(true); }); } await _InvoiceRepository.AddInvoiceLogs(entity.Id, logs); }); _EventAggregator.Publish(new Events.InvoiceEvent(entity, 1001, InvoiceEvent.Created)); var resp = entity.EntityToDTO(); return(new DataWrapper <InvoiceResponse>(resp) { Facade = "pos/invoice" }); }
/// <summary> /// Return service information. /// This API returns merchant info and all the available payment options per country for a given service. /// This is an important API if you want to build your own payment screens. /// </summary> /// <param name="paymentMethodId">Paymentmethod ID</param> /// <returns>FUll response with all service information</returns> public static PAYNLSDK.API.Transaction.GetService.Response GetService(PaymentMethodId? paymentMethodId) { TransactionGetService request = new TransactionGetService(); request.PaymentMethodId = paymentMethodId; Client c = new Client(); c.PerformRequest(request); return request.Response; }
public static PaymentMethodId GetPaymentMethodId(this PayoutData data) { return(PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) ? paymentMethodId : null); }