2020-11-19 07:34:22 +01:00
using System ;
2019-07-31 15:38:49 +09:00
using System.Collections.Generic ;
2024-04-04 16:31:04 +09:00
using System.Globalization ;
2018-02-20 12:45:04 +09:00
using System.Linq ;
using System.Threading.Tasks ;
2024-04-04 16:31:04 +09:00
using AngleSharp.Dom ;
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.BIP78.Sender ;
2020-05-23 21:13:18 +02:00
using BTCPayServer.Client.Models ;
2018-02-20 12:45:04 +09:00
using BTCPayServer.Data ;
2020-03-30 00:28:22 +09:00
using BTCPayServer.HostedServices ;
2024-04-04 16:31:04 +09:00
using BTCPayServer.Lightning ;
2020-03-30 00:28:22 +09:00
using BTCPayServer.Logging ;
2019-05-29 14:33:31 +00:00
using BTCPayServer.Models ;
using BTCPayServer.Models.InvoicingModels ;
2018-02-20 12:45:04 +09:00
using BTCPayServer.Services ;
using BTCPayServer.Services.Invoices ;
using NBitcoin ;
2021-03-05 22:52:25 -06:00
using NBitcoin.DataEncoders ;
2024-04-04 16:31:04 +09:00
using NBitpayClient ;
using NBXplorer.DerivationStrategy ;
2020-01-18 06:12:27 +01:00
using NBXplorer.Models ;
2024-04-04 16:31:04 +09:00
using Newtonsoft.Json ;
using Newtonsoft.Json.Linq ;
using static Org . BouncyCastle . Math . EC . ECCurve ;
2020-05-23 21:13:18 +02:00
using StoreData = BTCPayServer . Data . StoreData ;
2018-02-20 12:45:04 +09:00
namespace BTCPayServer.Payments.Bitcoin
{
2024-04-04 16:31:04 +09:00
public interface IHasNetwork
{
BTCPayNetwork Network { get ; }
}
public class BitcoinLikePaymentHandler : IPaymentMethodHandler , IHasNetwork
2018-02-20 12:45:04 +09:00
{
2020-06-28 22:07:48 -05:00
readonly ExplorerClientProvider _ExplorerProvider ;
2024-04-04 16:31:04 +09:00
private readonly BTCPayNetwork _Network ;
2020-06-28 22:07:48 -05:00
private readonly IFeeProviderFactory _FeeRateProviderFactory ;
2020-03-30 00:28:22 +09:00
private readonly NBXplorerDashboard _dashboard ;
2024-04-04 16:31:04 +09:00
private readonly WalletRepository _walletRepository ;
2020-06-28 22:07:48 -05:00
private readonly Services . Wallets . BTCPayWalletProvider _WalletProvider ;
2024-04-04 16:31:04 +09:00
public JsonSerializer Serializer { get ; }
public PaymentMethodId PaymentMethodId { get ; private set ; }
public BTCPayNetwork Network = > _Network ;
2018-02-20 12:45:04 +09:00
2024-04-04 16:31:04 +09:00
public BitcoinLikePaymentHandler (
PaymentMethodId paymentMethodId ,
ExplorerClientProvider provider ,
BTCPayNetwork network ,
2019-05-29 14:33:31 +00:00
IFeeProviderFactory feeRateProviderFactory ,
2023-03-13 02:12:58 +01:00
DisplayFormatter displayFormatter ,
2020-03-30 00:28:22 +09:00
NBXplorerDashboard dashboard ,
2024-04-04 16:31:04 +09:00
WalletRepository walletRepository ,
2019-05-29 14:33:31 +00:00
Services . Wallets . BTCPayWalletProvider walletProvider )
2018-02-20 12:45:04 +09:00
{
2024-04-04 16:31:04 +09:00
Serializer = BlobSerializer . CreateSerializer ( network . NBXplorerNetwork ) . Serializer ;
2018-02-20 12:45:04 +09:00
_ExplorerProvider = provider ;
2024-04-04 16:31:04 +09:00
_Network = network ;
PaymentMethodId = paymentMethodId ;
2019-05-29 14:33:31 +00:00
_FeeRateProviderFactory = feeRateProviderFactory ;
2020-03-30 00:28:22 +09:00
_dashboard = dashboard ;
2024-04-04 16:31:04 +09:00
_walletRepository = walletRepository ;
2018-02-20 12:45:04 +09:00
_WalletProvider = walletProvider ;
}
2018-08-21 13:54:52 +09:00
class Prepare
{
2024-04-04 16:31:04 +09:00
public Task < FeeRate > GetRecommendedFeeRate ;
2019-11-07 13:35:47 +09:00
public Task < FeeRate > GetNetworkFeeRate ;
2020-01-18 06:12:27 +01:00
public Task < KeyPathInformation > ReserveAddress ;
2024-04-04 16:31:04 +09:00
public DerivationSchemeSettings DerivationSchemeSettings ;
2018-08-21 13:54:52 +09:00
}
2024-04-04 16:31:04 +09:00
object IPaymentMethodHandler . ParsePaymentPromptDetails ( JToken details )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return ParsePaymentPromptDetails ( details ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
public BitcoinPaymentPromptDetails ParsePaymentPromptDetails ( JToken details )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return details . ToObject < BitcoinPaymentPromptDetails > ( Serializer ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
public DerivationSchemeSettings ParsePaymentMethodConfig ( JToken config )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return config . ToObject < DerivationSchemeSettings > ( Serializer ) ? ? throw new FormatException ( $"Invalid {nameof(DerivationSchemeSettings)}" ) ;
2019-05-29 14:33:31 +00:00
}
2024-04-04 16:31:04 +09:00
object IPaymentMethodHandler . ParsePaymentMethodConfig ( JToken config )
2019-05-29 14:33:31 +00:00
{
2024-04-04 16:31:04 +09:00
return ParsePaymentMethodConfig ( config ) ;
2019-05-29 14:33:31 +00:00
}
2024-09-27 15:27:04 +09:00
public void StripDetailsForNonOwner ( object details )
{
( ( BitcoinPaymentPromptDetails ) details ) . AccountDerivation = null ;
}
2024-04-04 16:31:04 +09:00
public async Task AfterSavingInvoice ( PaymentMethodContext paymentMethodContext )
2018-08-21 13:54:52 +09:00
{
2024-04-04 16:31:04 +09:00
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 )
2018-08-21 13:54:52 +09:00
{
2024-04-04 16:31:04 +09:00
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 ) ;
2018-08-21 13:54:52 +09:00
}
2024-04-04 16:31:04 +09:00
public Task BeforeFetchingRates ( PaymentMethodContext paymentMethodContext )
2018-02-20 12:45:04 +09:00
{
2024-04-04 16:31:04 +09:00
paymentMethodContext . Prompt . Currency = _Network . CryptoCode ;
paymentMethodContext . Prompt . Divisibility = _Network . Divisibility ;
if ( paymentMethodContext . Prompt . Activated )
2021-04-07 06:08:42 +02:00
{
2024-04-04 16:31:04 +09:00
var settings = ParsePaymentMethodConfig ( paymentMethodContext . PaymentMethodConfig ) ;
var storeBlob = paymentMethodContext . StoreBlob ;
var store = paymentMethodContext . Store ;
paymentMethodContext . State = new Prepare ( )
2021-04-07 06:08:42 +02:00
{
2024-04-04 16:31:04 +09:00
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
2021-04-07 06:08:42 +02:00
} ;
}
2024-04-04 16:31:04 +09:00
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 )
2019-01-05 00:37:09 +09:00
{
case NetworkFeeMode . Always :
2024-04-04 16:31:04 +09:00
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
}
2019-01-05 00:37:09 +09:00
break ;
case NetworkFeeMode . Never :
2024-04-04 16:31:04 +09:00
onchainMethod . PaymentMethodFeeRate = FeeRate . Zero ;
2019-11-07 13:45:45 +09:00
break ;
2019-01-05 00:37:09 +09:00
}
2024-04-04 16:31:04 +09:00
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" ) ;
}
2020-01-06 13:57:32 +01:00
2020-08-09 16:00:58 +02:00
var reserved = await prepare . ReserveAddress ;
2023-01-06 14:18:07 +01:00
2024-04-04 16:31:04 +09:00
paymentMethod . Destination = reserved . Address . ToString ( ) ;
paymentContext . TrackedDestinations . Add ( Network . GetTrackedDestination ( reserved . Address . ScriptPubKey ) ) ;
2020-08-09 16:00:58 +02:00
onchainMethod . KeyPath = reserved . KeyPath ;
2024-04-04 16:31:04 +09:00
onchainMethod . AccountDerivation = accountDerivation ;
2020-03-30 00:28:22 +09:00
onchainMethod . PayjoinEnabled = blob . PayJoinEnabled & &
2024-04-04 16:31:04 +09:00
accountDerivation . ScriptPubKeyType ( ) ! = ScriptPubKeyType . Legacy & &
_Network . SupportPayJoin ;
var logs = paymentContext . Logs ;
2020-03-30 00:28:22 +09:00
if ( onchainMethod . PayjoinEnabled )
2020-01-06 13:57:32 +01:00
{
2024-04-04 16:31:04 +09:00
var isHotwallet = prepare . DerivationSchemeSettings . IsHotWallet ;
var nodeSupport = _dashboard ? . Get ( _Network . CryptoCode ) ? . Status ? . BitcoinStatus ? . Capabilities
2020-03-30 00:28:22 +09:00
? . CanSupportTransactionCheck is true ;
2024-04-04 16:31:04 +09:00
onchainMethod . PayjoinEnabled & = isHotwallet & & nodeSupport ;
if ( ! isHotwallet )
logs . Write ( "Payjoin should have been enabled, but your store is not a hotwallet" , InvoiceEventData . EventSeverity . Warning ) ;
2020-03-30 00:28:22 +09:00
if ( ! nodeSupport )
2024-04-04 16:31:04 +09:00
logs . Write ( "Payjoin should have been enabled, but your version of NBXplorer or full node does not support it." , InvoiceEventData . EventSeverity . Warning ) ;
2020-03-30 00:28:22 +09:00
if ( onchainMethod . PayjoinEnabled )
2024-04-04 16:31:04 +09:00
logs . Write ( "Payjoin is enabled for this invoice." , InvoiceEventData . EventSeverity . Info ) ;
2020-03-30 00:28:22 +09:00
}
2024-04-04 16:31:04 +09:00
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 ) ;
2018-02-20 12:45:04 +09:00
}
}
}