Пример #1
0

        
        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));
        }
Пример #3
0
        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 }));
        }
Пример #5
0
    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);
    }
Пример #6
0
 public bool CanHandle(PaymentMethodId paymentMethod)
 {
     return(paymentMethod?.PaymentType == BitcoinPaymentType.Instance &&
            _btcPayNetworkProvider.GetNetwork <BTCPayNetwork>(paymentMethod.CryptoCode)?.ReadonlyWallet is false);
 }
Пример #7
0
 public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId)
 {
     GetPaymentMethods().TryGetValue(paymentMethodId, out var data);
     return(data);
 }
Пример #8
0
 public static void SetDefaultPaymentId(this StoreData storeData, PaymentMethodId defaultPaymentId)
 {
     storeData.DefaultCrypto = defaultPaymentId.ToString();
 }
Пример #9
0
        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);
        }
Пример #10
0
        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));
        }
Пример #11
0
        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));
        }
Пример #12
0
        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);
 }
Пример #14
0
 public static PaymentMethodId?GetDefaultPaymentId(this StoreData storeData)
 {
     PaymentMethodId.TryParse(storeData.DefaultCrypto, out var defaultPaymentId);
     return(defaultPaymentId);
 }
Пример #15
0
        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
            }));
        }
Пример #16
0
#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);
        }
Пример #17
0
        internal bool Support(PaymentMethodId paymentMethodId)
        {
            var rates = GetPaymentMethods(null);

            return(rates.TryGet(paymentMethodId) != null);
        }
Пример #18
0
 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;
     }
 }
Пример #19
0
 public PaymentMethod GetPaymentMethod(PaymentMethodId paymentMethodId, BTCPayNetworkProvider networkProvider)
 {
     GetPaymentMethods(networkProvider).TryGetValue(paymentMethodId, out var data);
     return(data);
 }
Пример #20
0
    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);
        }
Пример #22
0
    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")));
        }
    }
Пример #23
0
        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));
            }
        }
Пример #25
0
        public override string GetPaymentMethodName(PaymentMethodId paymentMethodId)
        {
            var network = _networkProvider.GetNetwork <BTCPayNetwork>(paymentMethodId.CryptoCode);

            return(GetPaymentMethodName(network));
        }
Пример #26
0

        
 public InvoiceNewPaymentDetailsEvent(string invoiceId, IPaymentMethodDetails details, PaymentMethodId paymentMethodId)
 {
     InvoiceId       = invoiceId;
     Details         = details;
     PaymentMethodId = paymentMethodId;
 }
Пример #28
0
        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)));
        }
Пример #29
0
        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"
            });
        }
Пример #30
0
 /// <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;
 }
Пример #31
0
 public static PaymentMethodId GetPaymentMethodId(this PayoutData data)
 {
     return(PaymentMethodId.TryParse(data.PaymentMethodId, out var paymentMethodId) ? paymentMethodId : null);
 }