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 ;
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 ;
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 ,
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 ;
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 ) ) ;
2021-12-31 16:59:02 +09:00
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 ( ) ;
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 ;
2021-12-31 16:59:02 +09:00
model . FeeRate = ( ( BitcoinLikeOnChainPaymentMethod ) paymentMethod . GetPaymentMethodDetails ( ) ) . GetFeeRate ( ) ;
2019-05-29 14:33:31 +00:00
model . PaymentMethodName = GetPaymentMethodName ( network ) ;
2020-11-09 00:23:09 -06:00
2020-11-09 02:05:17 -06:00
var lightningFallback = "" ;
2021-04-07 06:08:42 +02:00
if ( model . Activated & & network . SupportLightning & & storeBlob . OnChainWithLnInvoiceFallback )
2020-11-09 00:23:09 -06:00
{
var lightningInfo = invoiceResponse . CryptoInfo . FirstOrDefault ( a = >
a . GetpaymentMethodId ( ) = = new PaymentMethodId ( model . CryptoCode , PaymentTypes . LightningLike ) ) ;
2021-04-07 06:08:42 +02:00
if ( ! string . IsNullOrEmpty ( lightningInfo ? . PaymentUrls ? . BOLT11 ) )
2021-06-08 05:24:17 +02:00
lightningFallback = "&" + lightningInfo . PaymentUrls . BOLT11
. Replace ( "lightning:" , "lightning=" , StringComparison . OrdinalIgnoreCase )
. ToUpperInvariant ( ) ;
2020-11-09 00:23:09 -06:00
}
2021-04-07 06:08:42 +02:00
if ( model . Activated )
{
model . InvoiceBitcoinUrl = ( cryptoInfo . PaymentUrls ? . BIP21 ? ? "" ) + lightningFallback ;
2021-06-08 05:24:17 +02:00
model . InvoiceBitcoinUrlQR = ( cryptoInfo . PaymentUrls ? . BIP21 ? ? "" ) + lightningFallback
. Replace ( "LIGHTNING=" , "lightning=" , StringComparison . OrdinalIgnoreCase ) ;
2021-04-07 06:08:42 +02:00
}
else
{
model . InvoiceBitcoinUrl = "" ;
model . InvoiceBitcoinUrlQR = "" ;
}
2020-11-09 23:57:48 -06:00
2021-03-05 22:52:25 -06:00
// Most wallets still don't support BITCOIN: schema, so we're leaving this for better days
2020-12-03 23:39:18 -06:00
// Ref: https://github.com/btcpayserver/btcpayserver/pull/2060#issuecomment-723828348
//model.InvoiceBitcoinUrlQR = cryptoInfo.PaymentUrls.BIP21
// .Replace("bitcoin:", "BITCOIN:", StringComparison.OrdinalIgnoreCase)
2021-03-05 22:52:25 -06:00
// We're leading the way in Bitcoin community with adding UPPERCASE Bech32 addresses in QR Code
2021-12-31 16:59:02 +09:00
if ( network . CryptoCode . Equals ( "BTC" , StringComparison . InvariantCultureIgnoreCase ) & & _bech32Prefix . TryGetValue ( model . CryptoCode , out var prefix ) & & model . BtcAddress . StartsWith ( prefix , StringComparison . OrdinalIgnoreCase ) )
2021-03-05 22:52:25 -06:00
{
model . InvoiceBitcoinUrlQR = model . InvoiceBitcoinUrlQR . Replace (
2021-10-23 14:47:15 +09:00
$"{network.NBitcoinNetwork.UriScheme}:{model.BtcAddress}" , $"{network.NBitcoinNetwork.UriScheme}:{model.BtcAddress.ToUpperInvariant()}" ,
2021-03-05 22:52:25 -06:00
StringComparison . OrdinalIgnoreCase
) ;
}
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 )
. ReserveAddressAsync ( supportedPaymentMethod . AccountDerivation )
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 ,
2019-05-29 14:33:31 +00:00
BTCPayNetwork network , object preparePaymentObject )
2018-02-20 12:45:04 +09:00
{
2021-04-07 06:08:42 +02:00
if ( preparePaymentObject is null )
{
return new BitcoinLikeOnChainPaymentMethod ( )
{
Activated = false
} ;
}
2018-03-28 22:37:01 +09:00
if ( ! _ExplorerProvider . IsAvailable ( network ) )
throw new PaymentMethodUnavailableException ( $"Full node not available" ) ;
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 ;
2021-11-15 06:48:07 +02:00
if ( paymentMethod . ParentEntity . Type ! = InvoiceType . TopUp )
{
var txOut = network . NBitcoinNetwork . Consensus . ConsensusFactory . CreateTxOut ( ) ;
txOut . ScriptPubKey = reserved . Address . ScriptPubKey ;
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" ) ;
}
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 ;
}
}
}