2020-11-19 07:34:22 +01:00
using System ;
2019-07-31 15:38:49 +09:00
using System.Collections.Generic ;
2018-02-20 12:45:04 +09:00
using System.Linq ;
using System.Threading.Tasks ;
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 ;
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 ;
2023-03-09 21:36:11 +01:00
using BTCPayServer.Services.Rates ;
2018-02-20 12:45:04 +09:00
using NBitcoin ;
2021-03-05 22:52:25 -06:00
using NBitcoin.DataEncoders ;
2020-01-18 06:12:27 +01:00
using NBXplorer.Models ;
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
{
2019-05-29 14:33:31 +00:00
public class BitcoinLikePaymentHandler : PaymentMethodHandlerBase < DerivationSchemeSettings , BTCPayNetwork >
2018-02-20 12:45:04 +09:00
{
2020-06-28 22:07:48 -05:00
readonly ExplorerClientProvider _ExplorerProvider ;
2019-05-29 14:33:31 +00:00
private readonly BTCPayNetworkProvider _networkProvider ;
2020-06-28 22:07:48 -05:00
private readonly IFeeProviderFactory _FeeRateProviderFactory ;
2020-03-30 00:28:22 +09:00
private readonly NBXplorerDashboard _dashboard ;
2023-03-09 21:36:11 +01:00
private readonly CurrencyNameTable _currencyNameTable ;
2020-06-28 22:07:48 -05:00
private readonly Services . Wallets . BTCPayWalletProvider _WalletProvider ;
2021-03-09 04:45:56 +01:00
private readonly Dictionary < string , string > _bech32Prefix ;
2018-02-20 12:45:04 +09:00
public BitcoinLikePaymentHandler ( ExplorerClientProvider provider ,
2019-05-29 14:33:31 +00:00
BTCPayNetworkProvider networkProvider ,
IFeeProviderFactory feeRateProviderFactory ,
2023-03-09 21:36:11 +01:00
CurrencyNameTable currencyNameTable ,
2020-03-30 00:28:22 +09:00
NBXplorerDashboard dashboard ,
2019-05-29 14:33:31 +00:00
Services . Wallets . BTCPayWalletProvider walletProvider )
2018-02-20 12:45:04 +09:00
{
_ExplorerProvider = provider ;
2019-05-29 14:33:31 +00:00
_networkProvider = networkProvider ;
_FeeRateProviderFactory = feeRateProviderFactory ;
2020-03-30 00:28:22 +09:00
_dashboard = dashboard ;
2018-02-20 12:45:04 +09:00
_WalletProvider = walletProvider ;
2023-03-09 21:36:11 +01:00
_currencyNameTable = currencyNameTable ;
2021-03-05 22:52:25 -06:00
2021-03-09 04:45:56 +01:00
_bech32Prefix = networkProvider . GetAll ( ) . OfType < BTCPayNetwork > ( )
. Where ( network = > network . NBitcoinNetwork ? . Consensus ? . SupportSegwit is true ) . ToDictionary ( network = > network . CryptoCode ,
network = > Encoders . ASCII . EncodeData (
network . NBitcoinNetwork . GetBech32Encoder ( Bech32Type . WITNESS_PUBKEY_ADDRESS , false )
. HumanReadablePart ) ) ;
2018-02-20 12:45:04 +09:00
}
2018-08-21 13:54:52 +09:00
class Prepare
{
public Task < FeeRate > GetFeeRate ;
2019-11-07 13:35:47 +09:00
public Task < FeeRate > GetNetworkFeeRate ;
2020-01-18 06:12:27 +01:00
public Task < KeyPathInformation > ReserveAddress ;
2018-08-21 13:54:52 +09:00
}
2019-09-11 07:49:06 +02:00
public override void PreparePaymentModel ( PaymentModel model , InvoiceResponse invoiceResponse ,
2020-11-06 11:09:17 +01:00
StoreBlob storeBlob , IPaymentMethod paymentMethod )
2019-05-29 14:33:31 +00:00
{
2020-11-06 11:09:17 +01:00
var paymentMethodId = paymentMethod . GetId ( ) ;
2023-02-10 03:23:48 +01:00
var paymentMethodDetails = ( BitcoinLikeOnChainPaymentMethod ) paymentMethod . GetPaymentMethodDetails ( ) ;
2019-05-29 14:33:31 +00:00
var cryptoInfo = invoiceResponse . CryptoInfo . First ( o = > o . GetpaymentMethodId ( ) = = paymentMethodId ) ;
var network = _networkProvider . GetNetwork < BTCPayNetwork > ( model . CryptoCode ) ;
2020-11-06 11:09:17 +01:00
model . ShowRecommendedFee = storeBlob . ShowRecommendedFee ;
2023-02-10 03:23:48 +01:00
model . FeeRate = paymentMethodDetails . GetFeeRate ( ) ;
2019-05-29 14:33:31 +00:00
model . PaymentMethodName = GetPaymentMethodName ( network ) ;
2020-11-09 00:23:09 -06:00
2023-03-09 21:36:11 +01:00
var bip21Case = network . SupportLightning & & storeBlob . OnChainWithLnInvoiceFallback ;
var amountInSats = bip21Case & & storeBlob . LightningAmountInSatoshi & & model . CryptoCode = = "BTC" ;
2023-01-24 01:44:39 +01:00
string lightningFallback = null ;
2023-03-09 21:36:11 +01:00
if ( model . Activated & & bip21Case )
2020-11-09 00:23:09 -06:00
{
var lightningInfo = invoiceResponse . CryptoInfo . FirstOrDefault ( a = >
a . GetpaymentMethodId ( ) = = new PaymentMethodId ( model . CryptoCode , PaymentTypes . LightningLike ) ) ;
2023-02-08 07:47:38 +01:00
if ( lightningInfo is not null & & ! string . IsNullOrEmpty ( lightningInfo . PaymentUrls ? . BOLT11 ) )
{
lightningFallback = lightningInfo . PaymentUrls . BOLT11 ;
}
else
{
2023-02-10 03:23:48 +01:00
var lnurlInfo = invoiceResponse . CryptoInfo . FirstOrDefault ( a = >
2023-02-08 07:47:38 +01:00
a . GetpaymentMethodId ( ) = = new PaymentMethodId ( model . CryptoCode , PaymentTypes . LNURLPay ) ) ;
2023-02-10 03:23:48 +01:00
if ( lnurlInfo is not null )
2023-02-08 07:47:38 +01:00
{
2023-02-10 03:23:48 +01:00
lightningFallback = lnurlInfo . PaymentUrls ? . AdditionalData [ "LNURLP" ] . ToObject < string > ( ) ;
// This seems to be an edge case in the Selenium tests, in which the LNURLP isn't populated.
// I have come across it only in the tests and this is supposed to make them happy.
if ( string . IsNullOrEmpty ( lightningFallback ) )
{
var serverUrl = new Uri ( lnurlInfo . Url [ . . lnurlInfo . Url . IndexOf ( "/i/" , StringComparison . InvariantCultureIgnoreCase ) ] ) ;
var uri = new Uri ( $"{serverUrl}{network.CryptoCode}/lnurl/pay/i/{invoiceResponse.Id}" ) ;
lightningFallback = LNURL . LNURL . EncodeUri ( uri , "payRequest" , true ) . ToString ( ) ;
}
2023-02-08 07:47:38 +01:00
}
}
if ( ! string . IsNullOrEmpty ( lightningFallback ) )
{
lightningFallback = lightningFallback
. Replace ( "lightning:" , "lightning=" , StringComparison . OrdinalIgnoreCase ) ;
}
2020-11-09 00:23:09 -06:00
}
2021-04-07 06:08:42 +02:00
if ( model . Activated )
{
2023-01-24 01:44:39 +01:00
// We're leading the way in Bitcoin community with adding UPPERCASE Bech32 addresses in QR Code
//
// Correct casing: Addresses in payment URI need to be …
// - lowercase in link version
// - uppercase in QR version
//
// The keys (e.g. "bitcoin:" or "lightning=" should be lowercase!
2023-01-06 14:18:07 +01:00
2023-01-24 01:44:39 +01:00
// cryptoInfo.PaymentUrls?.BIP21: bitcoin:bcrt1qxp2qa5?amount=0.00044007
model . InvoiceBitcoinUrl = model . InvoiceBitcoinUrlQR = cryptoInfo . PaymentUrls ? . BIP21 ? ? "" ;
// model.InvoiceBitcoinUrl: bitcoin:bcrt1qxp2qa5?amount=0.00044007
// model.InvoiceBitcoinUrlQR: bitcoin:bcrt1qxp2qa5?amount=0.00044007
if ( ! string . IsNullOrEmpty ( lightningFallback ) )
{
var delimiterUrl = model . InvoiceBitcoinUrl . Contains ( "?" ) ? "&" : "?" ;
model . InvoiceBitcoinUrl + = $"{delimiterUrl}{lightningFallback}" ;
// model.InvoiceBitcoinUrl: bitcoin:bcrt1qxp2qa5dhn7?amount=0.00044007&lightning=lnbcrt440070n1...
var delimiterUrlQR = model . InvoiceBitcoinUrlQR . Contains ( "?" ) ? "&" : "?" ;
model . InvoiceBitcoinUrlQR + = $"{delimiterUrlQR}{lightningFallback.ToUpperInvariant().Replace(" LIGHTNING = ", " lightning = ", StringComparison.OrdinalIgnoreCase)}" ;
// model.InvoiceBitcoinUrlQR: bitcoin:bcrt1qxp2qa5dhn7?amount=0.00044007&lightning=LNBCRT4400...
}
2022-11-16 01:04:51 +01:00
if ( network . CryptoCode . Equals ( "BTC" , StringComparison . InvariantCultureIgnoreCase ) & & _bech32Prefix . TryGetValue ( model . CryptoCode , out var prefix ) & & model . BtcAddress . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) )
{
model . InvoiceBitcoinUrlQR = model . InvoiceBitcoinUrlQR . Replace (
$"{network.NBitcoinNetwork.UriScheme}:{model.BtcAddress}" , $"{network.NBitcoinNetwork.UriScheme}:{model.BtcAddress.ToUpperInvariant()}" ,
2023-01-24 01:44:39 +01:00
StringComparison . OrdinalIgnoreCase ) ;
// model.InvoiceBitcoinUrlQR: bitcoin:BCRT1QXP2QA5DHN...?amount=0.00044007&lightning=LNBCRT4400...
2022-11-16 01:04:51 +01:00
}
2021-04-07 06:08:42 +02:00
}
else
{
2023-01-24 01:44:39 +01:00
model . InvoiceBitcoinUrl = model . InvoiceBitcoinUrlQR = string . Empty ;
2021-04-07 06:08:42 +02:00
}
2023-03-09 21:36:11 +01:00
if ( model . Activated & & amountInSats )
{
base . PreparePaymentModelForAmountInSats ( model , paymentMethod , _currencyNameTable ) ;
}
2019-05-29 14:33:31 +00:00
}
public override string GetCryptoImage ( PaymentMethodId paymentMethodId )
{
var network = _networkProvider . GetNetwork < BTCPayNetwork > ( paymentMethodId . CryptoCode ) ;
return GetCryptoImage ( network ) ;
}
private string GetCryptoImage ( BTCPayNetworkBase network )
{
return network . CryptoImagePath ;
}
public override string GetPaymentMethodName ( PaymentMethodId paymentMethodId )
{
var network = _networkProvider . GetNetwork < BTCPayNetwork > ( paymentMethodId . CryptoCode ) ;
return GetPaymentMethodName ( network ) ;
}
public override IEnumerable < PaymentMethodId > GetSupportedPaymentMethods ( )
{
2019-09-21 16:39:44 +02:00
return _networkProvider
. GetAll ( )
. OfType < BTCPayNetwork > ( )
2019-05-29 14:33:31 +00:00
. Select ( network = > new PaymentMethodId ( network . CryptoCode , PaymentTypes . BTCLike ) ) ;
}
private string GetPaymentMethodName ( BTCPayNetworkBase network )
{
return network . DisplayName ;
}
public override object PreparePayment ( DerivationSchemeSettings supportedPaymentMethod , StoreData store ,
BTCPayNetworkBase network )
2018-08-21 13:54:52 +09:00
{
2019-11-06 16:21:33 -08:00
var storeBlob = store . GetStoreBlob ( ) ;
2018-08-21 13:54:52 +09:00
return new Prepare ( )
{
2020-03-30 00:28:22 +09:00
GetFeeRate =
_FeeRateProviderFactory . CreateFeeProvider ( network )
. GetFeeRateAsync ( storeBlob . RecommendedFeeBlockTarget ) ,
GetNetworkFeeRate = storeBlob . NetworkFeeMode = = NetworkFeeMode . Never
? null
: _FeeRateProviderFactory . CreateFeeProvider ( network ) . GetFeeRateAsync ( ) ,
2019-05-29 14:33:31 +00:00
ReserveAddress = _WalletProvider . GetWallet ( network )
2022-12-08 13:16:18 +09:00
. ReserveAddressAsync ( store . Id , supportedPaymentMethod . AccountDerivation , "invoice" )
2018-08-21 13:54:52 +09:00
} ;
}
2019-06-04 08:59:01 +09:00
public override PaymentType PaymentType = > PaymentTypes . BTCLike ;
2019-05-24 06:38:47 +00:00
2019-05-29 09:43:50 +00:00
public override async Task < IPaymentMethodDetails > CreatePaymentMethodDetails (
2020-03-30 00:28:22 +09:00
InvoiceLogs logs ,
2019-05-29 09:43:50 +00:00
DerivationSchemeSettings supportedPaymentMethod , PaymentMethod paymentMethod , StoreData store ,
2022-07-06 15:09:05 +02:00
BTCPayNetwork network , object preparePaymentObject , IEnumerable < PaymentMethodId > invoicePaymentMethods )
2018-02-20 12:45:04 +09:00
{
2023-01-06 14:18:07 +01:00
2022-11-16 01:04:51 +01:00
if ( ! _ExplorerProvider . IsAvailable ( network ) )
throw new PaymentMethodUnavailableException ( $"Full node not available" ) ;
if ( paymentMethod . ParentEntity . Type ! = InvoiceType . TopUp )
{
var txOut = network . NBitcoinNetwork . Consensus . ConsensusFactory . CreateTxOut ( ) ;
txOut . ScriptPubKey =
new Key ( ) . GetScriptPubKey ( supportedPaymentMethod . AccountDerivation . ScriptPubKeyType ( ) ) ;
var dust = txOut . GetDustThreshold ( ) ;
var amount = paymentMethod . Calculate ( ) . Due ;
if ( amount < dust )
throw new PaymentMethodUnavailableException ( "Amount below the dust threshold. For amounts of this size, it is recommended to enable an off-chain (Lightning) payment method" ) ;
}
2021-04-07 06:08:42 +02:00
if ( preparePaymentObject is null )
{
return new BitcoinLikeOnChainPaymentMethod ( )
{
Activated = false
} ;
}
2018-08-21 13:54:52 +09:00
var prepare = ( Prepare ) preparePaymentObject ;
2021-03-01 09:56:57 -06:00
var onchainMethod = new BitcoinLikeOnChainPaymentMethod ( ) ;
2020-01-06 13:57:32 +01:00
var blob = store . GetStoreBlob ( ) ;
2021-04-07 06:08:42 +02:00
onchainMethod . Activated = true ;
2021-03-01 09:56:57 -06:00
// TODO: this needs to be refactored to move this logic into BitcoinLikeOnChainPaymentMethod
// This is likely a constructor code
2020-01-06 13:57:32 +01:00
onchainMethod . NetworkFeeMode = blob . NetworkFeeMode ;
2018-08-21 13:54:52 +09:00
onchainMethod . FeeRate = await prepare . GetFeeRate ;
2019-01-05 00:37:09 +09:00
switch ( onchainMethod . NetworkFeeMode )
{
case NetworkFeeMode . Always :
2019-11-07 13:45:45 +09:00
onchainMethod . NetworkFeeRate = ( await prepare . GetNetworkFeeRate ) ;
2020-03-30 00:28:22 +09:00
onchainMethod . NextNetworkFee =
onchainMethod . NetworkFeeRate . GetFee ( 100 ) ; // assume price for 100 bytes
2019-01-05 00:37:09 +09:00
break ;
case NetworkFeeMode . Never :
2019-11-07 13:45:45 +09:00
onchainMethod . NetworkFeeRate = FeeRate . Zero ;
2019-01-07 15:35:18 +09:00
onchainMethod . NextNetworkFee = Money . Zero ;
2019-01-05 00:37:09 +09:00
break ;
2019-11-07 13:45:45 +09:00
case NetworkFeeMode . MultiplePaymentsOnly :
onchainMethod . NetworkFeeRate = ( await prepare . GetNetworkFeeRate ) ;
2020-03-30 00:28:22 +09:00
onchainMethod . NextNetworkFee = Money . Zero ;
2019-11-07 13:45:45 +09:00
break ;
2019-01-05 00:37:09 +09:00
}
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
2020-08-09 16:00:58 +02:00
onchainMethod . DepositAddress = reserved . Address . ToString ( ) ;
onchainMethod . KeyPath = reserved . KeyPath ;
2020-03-30 00:28:22 +09:00
onchainMethod . PayjoinEnabled = blob . PayJoinEnabled & &
2021-03-01 14:44:53 +01:00
supportedPaymentMethod
. AccountDerivation . ScriptPubKeyType ( ) ! = ScriptPubKeyType . Legacy & &
2020-03-30 00:28:22 +09:00
network . SupportPayJoin ;
if ( onchainMethod . PayjoinEnabled )
2020-01-06 13:57:32 +01:00
{
2020-04-10 09:00:41 +02:00
var prefix = $"{supportedPaymentMethod.PaymentId.ToPrettyString()}:" ;
2020-03-30 00:28:22 +09:00
var nodeSupport = _dashboard ? . Get ( network . CryptoCode ) ? . Status ? . BitcoinStatus ? . Capabilities
? . CanSupportTransactionCheck is true ;
2020-06-07 09:38:15 +09:00
onchainMethod . PayjoinEnabled & = supportedPaymentMethod . IsHotWallet & & nodeSupport ;
2020-06-06 23:52:21 -05:00
if ( ! supportedPaymentMethod . IsHotWallet )
2020-08-28 08:49:13 +02:00
logs . Write ( $"{prefix} 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 )
2020-08-28 08:49:13 +02:00
logs . Write ( $"{prefix} 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 )
2020-08-28 08:49:13 +02:00
logs . Write ( $"{prefix} Payjoin is enabled for this invoice." , InvoiceEventData . EventSeverity . Info ) ;
2020-03-30 00:28:22 +09:00
}
2018-02-20 12:45:04 +09:00
return onchainMethod ;
}
}
}