mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-22 22:25:28 +01:00
257 lines
12 KiB
C#
257 lines
12 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.Linq;
|
|
using System.Threading.Tasks;
|
|
using AngleSharp.Dom;
|
|
using BTCPayServer.Abstractions.Extensions;
|
|
using BTCPayServer.BIP78.Sender;
|
|
using BTCPayServer.Client.Models;
|
|
using BTCPayServer.Data;
|
|
using BTCPayServer.HostedServices;
|
|
using BTCPayServer.Lightning;
|
|
using BTCPayServer.Logging;
|
|
using BTCPayServer.Models;
|
|
using BTCPayServer.Models.InvoicingModels;
|
|
using BTCPayServer.Services;
|
|
using BTCPayServer.Services.Invoices;
|
|
using NBitcoin;
|
|
using NBitcoin.DataEncoders;
|
|
using NBitpayClient;
|
|
using NBXplorer.DerivationStrategy;
|
|
using NBXplorer.Models;
|
|
using Newtonsoft.Json;
|
|
using Newtonsoft.Json.Linq;
|
|
using static Org.BouncyCastle.Math.EC.ECCurve;
|
|
using StoreData = BTCPayServer.Data.StoreData;
|
|
|
|
namespace BTCPayServer.Payments.Bitcoin
|
|
{
|
|
public interface IHasNetwork
|
|
{
|
|
BTCPayNetwork Network { get; }
|
|
}
|
|
public class BitcoinLikePaymentHandler : IPaymentMethodHandler, IHasNetwork
|
|
{
|
|
readonly ExplorerClientProvider _ExplorerProvider;
|
|
private readonly BTCPayNetwork _Network;
|
|
private readonly IFeeProviderFactory _FeeRateProviderFactory;
|
|
private readonly NBXplorerDashboard _dashboard;
|
|
private readonly WalletRepository _walletRepository;
|
|
private readonly Services.Wallets.BTCPayWalletProvider _WalletProvider;
|
|
|
|
public JsonSerializer Serializer { get; }
|
|
public PaymentMethodId PaymentMethodId { get; private set; }
|
|
public BTCPayNetwork Network => _Network;
|
|
|
|
public BitcoinLikePaymentHandler(
|
|
PaymentMethodId paymentMethodId,
|
|
ExplorerClientProvider provider,
|
|
BTCPayNetwork network,
|
|
IFeeProviderFactory feeRateProviderFactory,
|
|
DisplayFormatter displayFormatter,
|
|
NBXplorerDashboard dashboard,
|
|
WalletRepository walletRepository,
|
|
Services.Wallets.BTCPayWalletProvider walletProvider)
|
|
{
|
|
Serializer = BlobSerializer.CreateSerializer(network.NBXplorerNetwork).Serializer;
|
|
_ExplorerProvider = provider;
|
|
_Network = network;
|
|
PaymentMethodId = paymentMethodId;
|
|
_FeeRateProviderFactory = feeRateProviderFactory;
|
|
_dashboard = dashboard;
|
|
_walletRepository = walletRepository;
|
|
_WalletProvider = walletProvider;
|
|
}
|
|
|
|
class Prepare
|
|
{
|
|
public Task<FeeRate> GetRecommendedFeeRate;
|
|
public Task<FeeRate> GetNetworkFeeRate;
|
|
public Task<KeyPathInformation> ReserveAddress;
|
|
public DerivationSchemeSettings DerivationSchemeSettings;
|
|
}
|
|
|
|
object IPaymentMethodHandler.ParsePaymentPromptDetails(JToken details)
|
|
{
|
|
return ParsePaymentPromptDetails(details);
|
|
}
|
|
|
|
public BitcoinPaymentPromptDetails ParsePaymentPromptDetails(JToken details)
|
|
{
|
|
return details.ToObject<BitcoinPaymentPromptDetails>(Serializer);
|
|
}
|
|
public DerivationSchemeSettings ParsePaymentMethodConfig(JToken config)
|
|
{
|
|
return config.ToObject<DerivationSchemeSettings>(Serializer) ?? throw new FormatException($"Invalid {nameof(DerivationSchemeSettings)}");
|
|
}
|
|
object IPaymentMethodHandler.ParsePaymentMethodConfig(JToken config)
|
|
{
|
|
return ParsePaymentMethodConfig(config);
|
|
}
|
|
|
|
public async Task AfterSavingInvoice(PaymentMethodContext paymentMethodContext)
|
|
{
|
|
var paymentPrompt = paymentMethodContext.Prompt;
|
|
var store = paymentMethodContext.Store;
|
|
var entity = paymentMethodContext.InvoiceEntity;
|
|
var links = new List<WalletObjectLinkData>();
|
|
var walletId = new WalletId(store.Id, _Network.CryptoCode);
|
|
await _walletRepository.EnsureWalletObject(new WalletObjectId(
|
|
walletId,
|
|
WalletObjectData.Types.Invoice,
|
|
entity.Id
|
|
));
|
|
if (paymentPrompt.Destination is string)
|
|
{
|
|
links.Add(WalletRepository.NewWalletObjectLinkData(new WalletObjectId(
|
|
walletId,
|
|
WalletObjectData.Types.Address,
|
|
paymentPrompt.Destination),
|
|
new WalletObjectId(
|
|
walletId,
|
|
WalletObjectData.Types.Invoice,
|
|
entity.Id)));
|
|
}
|
|
await _walletRepository.EnsureCreated(null, links);
|
|
}
|
|
|
|
public Task BeforeFetchingRates(PaymentMethodContext paymentMethodContext)
|
|
{
|
|
paymentMethodContext.Prompt.Currency = _Network.CryptoCode;
|
|
paymentMethodContext.Prompt.Divisibility = _Network.Divisibility;
|
|
if (paymentMethodContext.Prompt.Activated)
|
|
{
|
|
var settings = ParsePaymentMethodConfig(paymentMethodContext.PaymentMethodConfig);
|
|
var storeBlob = paymentMethodContext.StoreBlob;
|
|
var store = paymentMethodContext.Store;
|
|
paymentMethodContext.State = new Prepare()
|
|
{
|
|
GetRecommendedFeeRate =
|
|
_FeeRateProviderFactory.CreateFeeProvider(_Network)
|
|
.GetFeeRateAsync(storeBlob.RecommendedFeeBlockTarget),
|
|
GetNetworkFeeRate = storeBlob.NetworkFeeMode == NetworkFeeMode.Never
|
|
? null
|
|
: _FeeRateProviderFactory.CreateFeeProvider(_Network).GetFeeRateAsync(),
|
|
ReserveAddress = _WalletProvider.GetWallet(_Network)
|
|
.ReserveAddressAsync(store.Id, settings.AccountDerivation, "invoice"),
|
|
DerivationSchemeSettings = settings
|
|
};
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
public async Task ConfigurePrompt(PaymentMethodContext paymentContext)
|
|
{
|
|
var prepare = (Prepare)paymentContext.State;
|
|
var accountDerivation = prepare.DerivationSchemeSettings.AccountDerivation;
|
|
if (!_ExplorerProvider.IsAvailable(_Network))
|
|
throw new PaymentMethodUnavailableException($"Full node not available");
|
|
var paymentMethod = paymentContext.Prompt;
|
|
var onchainMethod = new BitcoinPaymentPromptDetails();
|
|
var blob = paymentContext.StoreBlob;
|
|
|
|
onchainMethod.FeeMode = blob.NetworkFeeMode;
|
|
onchainMethod.RecommendedFeeRate = await prepare.GetRecommendedFeeRate;
|
|
switch (onchainMethod.FeeMode)
|
|
{
|
|
case NetworkFeeMode.Always:
|
|
case NetworkFeeMode.MultiplePaymentsOnly:
|
|
onchainMethod.PaymentMethodFeeRate = (await prepare.GetNetworkFeeRate);
|
|
if (onchainMethod.FeeMode == NetworkFeeMode.Always || paymentMethod.Calculate().TxCount > 0)
|
|
{
|
|
paymentMethod.PaymentMethodFee =
|
|
onchainMethod.PaymentMethodFeeRate.GetFee(100).GetValue(_Network); // assume price for 100 bytes
|
|
}
|
|
break;
|
|
case NetworkFeeMode.Never:
|
|
onchainMethod.PaymentMethodFeeRate = FeeRate.Zero;
|
|
break;
|
|
}
|
|
if (paymentContext.InvoiceEntity.Type != InvoiceType.TopUp)
|
|
{
|
|
var txOut = _Network.NBitcoinNetwork.Consensus.ConsensusFactory.CreateTxOut();
|
|
txOut.ScriptPubKey =
|
|
new Key().GetScriptPubKey(accountDerivation.ScriptPubKeyType());
|
|
var dust = txOut.GetDustThreshold();
|
|
var amount = paymentMethod.Calculate().Due;
|
|
if (amount < dust.ToDecimal(MoneyUnit.BTC))
|
|
throw new PaymentMethodUnavailableException("Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method");
|
|
}
|
|
|
|
var reserved = await prepare.ReserveAddress;
|
|
|
|
paymentMethod.Destination = reserved.Address.ToString();
|
|
paymentContext.TrackedDestinations.Add(Network.GetTrackedDestination(reserved.Address.ScriptPubKey));
|
|
onchainMethod.KeyPath = reserved.KeyPath;
|
|
onchainMethod.AccountDerivation = accountDerivation;
|
|
onchainMethod.PayjoinEnabled = blob.PayJoinEnabled &&
|
|
accountDerivation.ScriptPubKeyType() != ScriptPubKeyType.Legacy &&
|
|
_Network.SupportPayJoin;
|
|
var logs = paymentContext.Logs;
|
|
if (onchainMethod.PayjoinEnabled)
|
|
{
|
|
var isHotwallet = prepare.DerivationSchemeSettings.IsHotWallet;
|
|
var nodeSupport = _dashboard?.Get(_Network.CryptoCode)?.Status?.BitcoinStatus?.Capabilities
|
|
?.CanSupportTransactionCheck is true;
|
|
onchainMethod.PayjoinEnabled &= isHotwallet && nodeSupport;
|
|
if (!isHotwallet)
|
|
logs.Write("Payjoin should have been enabled, but your store is not a hotwallet", InvoiceEventData.EventSeverity.Warning);
|
|
if (!nodeSupport)
|
|
logs.Write("Payjoin should have been enabled, but your version of NBXplorer or full node does not support it.", InvoiceEventData.EventSeverity.Warning);
|
|
if (onchainMethod.PayjoinEnabled)
|
|
logs.Write("Payjoin is enabled for this invoice.", InvoiceEventData.EventSeverity.Info);
|
|
}
|
|
|
|
paymentMethod.Details = JObject.FromObject(onchainMethod, Serializer);
|
|
}
|
|
|
|
public static DerivationStrategyBase GetAccountDerivation(JToken activationData, BTCPayNetwork network)
|
|
{
|
|
if (activationData is JValue { Type: JTokenType.String, Value: string v })
|
|
{
|
|
var parser = network.GetDerivationSchemeParser();
|
|
return parser.Parse(v);
|
|
}
|
|
throw new FormatException($"{network.CryptoCode}: Invalid activation data, impossible to parse the derivation scheme");
|
|
}
|
|
public static DerivationStrategyBase GetAccountDerivation(IDictionary<PaymentMethodId, JToken> activationDataByPmi, BTCPayNetwork network)
|
|
{
|
|
var pmi = PaymentTypes.CHAIN.GetPaymentMethodId(network.CryptoCode);
|
|
activationDataByPmi.TryGetValue(pmi, out var value);
|
|
if (value is null)
|
|
return null;
|
|
return GetAccountDerivation(value, network);
|
|
}
|
|
public Task ValidatePaymentMethodConfig(PaymentMethodConfigValidationContext validationContext)
|
|
{
|
|
var parser = Network.GetDerivationSchemeParser();
|
|
DerivationSchemeSettings settings = new DerivationSchemeSettings();
|
|
if (parser.TryParseXpub(validationContext.Config.ToString(), ref settings))
|
|
{
|
|
validationContext.Config = JToken.FromObject(settings, Serializer);
|
|
return Task.CompletedTask;
|
|
}
|
|
var res = validationContext.Config.ToObject<DerivationSchemeSettings>(Serializer);
|
|
if (res is null)
|
|
{
|
|
validationContext.ModelState.AddModelError(nameof(validationContext.Config), "Invalid derivation scheme settings");
|
|
return Task.CompletedTask;
|
|
}
|
|
if (res.AccountDerivation is null)
|
|
{
|
|
validationContext.ModelState.AddModelError(nameof(res.AccountDerivation), "Invalid account derivation");
|
|
}
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
public BitcoinLikePaymentData ParsePaymentDetails(JToken details)
|
|
{
|
|
return details.ToObject<BitcoinLikePaymentData>(Serializer) ?? throw new FormatException($"Invalid {nameof(BitcoinLikePaymentData)}");
|
|
}
|
|
|
|
object IPaymentMethodHandler.ParsePaymentDetails(JToken details)
|
|
{
|
|
return ParsePaymentDetails(details);
|
|
}
|
|
}
|
|
}
|