2018-08-30 18:34:39 +02:00
using System ;
2017-09-13 08:47:34 +02:00
using System.Collections.Generic ;
2018-08-30 18:34:39 +02:00
using System.ComponentModel.DataAnnotations ;
using System.Linq ;
2017-09-13 08:47:34 +02:00
using System.Text ;
using System.Threading.Tasks ;
using BTCPayServer.Data ;
2019-01-06 10:12:45 +01:00
using BTCPayServer.Events ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Logging ;
using BTCPayServer.Models ;
using BTCPayServer.Payments ;
using BTCPayServer.Rating ;
using BTCPayServer.Security ;
2017-10-20 21:06:37 +02:00
using BTCPayServer.Services.Invoices ;
2017-09-15 09:06:57 +02:00
using BTCPayServer.Services.Rates ;
2018-08-30 18:34:39 +02:00
using BTCPayServer.Services.Stores ;
2017-09-15 09:06:57 +02:00
using BTCPayServer.Services.Wallets ;
2018-11-06 08:08:42 +01:00
using BTCPayServer.Validation ;
2018-08-30 18:34:39 +02:00
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
using NBitcoin ;
using NBitpayClient ;
using Newtonsoft.Json ;
2017-09-13 08:47:34 +02:00
namespace BTCPayServer.Controllers
{
2017-10-27 10:53:04 +02:00
public partial class InvoiceController : Controller
{
InvoiceRepository _InvoiceRepository ;
2018-07-12 10:38:21 +02:00
ContentSecurityPolicies _CSP ;
2018-08-22 09:53:40 +02:00
RateFetcher _RateProvider ;
2017-10-27 10:53:04 +02:00
StoreRepository _StoreRepository ;
UserManager < ApplicationUser > _UserManager ;
2017-10-27 11:58:43 +02:00
private CurrencyNameTable _CurrencyNameTable ;
2017-12-17 11:58:55 +01:00
EventAggregator _EventAggregator ;
2017-12-21 07:52:04 +01:00
BTCPayNetworkProvider _NetworkProvider ;
2018-02-20 04:45:04 +01:00
private readonly BTCPayWalletProvider _WalletProvider ;
IServiceProvider _ServiceProvider ;
public InvoiceController (
IServiceProvider serviceProvider ,
InvoiceRepository invoiceRepository ,
2017-10-27 11:58:43 +02:00
CurrencyNameTable currencyNameTable ,
2017-10-27 10:53:04 +02:00
UserManager < ApplicationUser > userManager ,
2018-08-22 09:53:40 +02:00
RateFetcher rateProvider ,
2017-10-27 10:53:04 +02:00
StoreRepository storeRepository ,
2017-12-17 11:58:55 +01:00
EventAggregator eventAggregator ,
2018-02-20 04:45:04 +01:00
BTCPayWalletProvider walletProvider ,
2018-07-12 10:38:21 +02:00
ContentSecurityPolicies csp ,
2018-02-20 04:45:04 +01:00
BTCPayNetworkProvider networkProvider )
2017-10-27 10:53:04 +02:00
{
2018-02-20 04:45:04 +01:00
_ServiceProvider = serviceProvider ;
2017-10-27 11:58:43 +02:00
_CurrencyNameTable = currencyNameTable ? ? throw new ArgumentNullException ( nameof ( currencyNameTable ) ) ;
2017-10-27 10:53:04 +02:00
_StoreRepository = storeRepository ? ? throw new ArgumentNullException ( nameof ( storeRepository ) ) ;
_InvoiceRepository = invoiceRepository ? ? throw new ArgumentNullException ( nameof ( invoiceRepository ) ) ;
2018-05-02 20:32:42 +02:00
_RateProvider = rateProvider ? ? throw new ArgumentNullException ( nameof ( rateProvider ) ) ;
2017-10-27 10:53:04 +02:00
_UserManager = userManager ;
2017-12-17 11:58:55 +01:00
_EventAggregator = eventAggregator ;
2017-12-21 07:52:04 +01:00
_NetworkProvider = networkProvider ;
2018-02-20 04:45:04 +01:00
_WalletProvider = walletProvider ;
2018-07-12 10:38:21 +02:00
_CSP = csp ;
2017-10-27 10:53:04 +02:00
}
2017-09-13 08:47:34 +02:00
2017-12-21 07:52:04 +01:00
2018-01-17 07:11:05 +01:00
internal async Task < DataWrapper < InvoiceResponse > > CreateInvoiceCore ( Invoice invoice , StoreData store , string serverUrl )
2017-10-27 10:53:04 +02:00
{
2018-09-08 07:32:26 +02:00
if ( ! store . HasClaim ( Policies . CanCreateInvoice . Key ) )
throw new UnauthorizedAccessException ( ) ;
2018-07-24 05:19:43 +02:00
InvoiceLogs logs = new InvoiceLogs ( ) ;
logs . Write ( "Creation of invoice starting" ) ;
2017-10-27 10:53:04 +02:00
var entity = new InvoiceEntity
{
2018-01-06 10:57:56 +01:00
InvoiceTime = DateTimeOffset . UtcNow
2017-10-27 10:53:04 +02:00
} ;
2017-12-21 07:52:04 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
2017-10-27 10:53:04 +02:00
Uri notificationUri = Uri . IsWellFormedUriString ( invoice . NotificationURL , UriKind . Absolute ) ? new Uri ( invoice . NotificationURL , UriKind . Absolute ) : null ;
if ( notificationUri = = null | | ( notificationUri . Scheme ! = "http" & & notificationUri . Scheme ! = "https" ) ) //TODO: Filer non routable addresses ?
notificationUri = null ;
EmailAddressAttribute emailValidator = new EmailAddressAttribute ( ) ;
2018-01-17 07:11:05 +01:00
entity . ExpirationTime = entity . InvoiceTime . AddMinutes ( storeBlob . InvoiceExpiration ) ;
2017-12-03 06:43:52 +01:00
entity . MonitoringExpiration = entity . ExpirationTime + TimeSpan . FromMinutes ( storeBlob . MonitoringExpiration ) ;
2017-10-27 10:53:04 +02:00
entity . OrderId = invoice . OrderId ;
entity . ServerUrl = serverUrl ;
2018-01-07 20:14:35 +01:00
entity . FullNotifications = invoice . FullNotifications | | invoice . ExtendedNotifications ;
entity . ExtendedNotifications = invoice . ExtendedNotifications ;
2017-10-27 10:53:04 +02:00
entity . NotificationURL = notificationUri ? . AbsoluteUri ;
2018-10-12 03:09:13 +02:00
entity . NotificationEmail = invoice . NotificationEmail ;
2017-10-27 10:53:04 +02:00
entity . BuyerInformation = Map < Invoice , BuyerInformation > ( invoice ) ;
2018-05-04 16:15:34 +02:00
entity . PaymentTolerance = storeBlob . PaymentTolerance ;
2017-10-27 10:53:04 +02:00
//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 ;
}
2019-01-15 14:12:29 +01:00
var currencyInfo = _CurrencyNameTable . GetNumberFormatInfo ( invoice . Currency , false ) ;
if ( currencyInfo ! = null )
{
invoice . Price = Math . Round ( invoice . Price , currencyInfo . CurrencyDecimalDigits ) ;
}
invoice . Price = Math . Max ( 0.0 m , invoice . Price ) ;
2017-10-27 10:53:04 +02:00
entity . ProductInformation = Map < Invoice , ProductInformation > ( invoice ) ;
2019-01-15 14:12:29 +01:00
2017-10-27 10:53:04 +02:00
entity . RedirectURL = invoice . RedirectURL ? ? store . StoreWebsite ;
2018-05-24 16:54:48 +02:00
if ( ! Uri . IsWellFormedUriString ( entity . RedirectURL , UriKind . Absolute ) )
entity . RedirectURL = null ;
2018-12-10 13:48:28 +01:00
entity . Status = InvoiceStatus . New ;
2017-10-27 10:53:04 +02:00
entity . SpeedPolicy = ParseSpeedPolicy ( invoice . TransactionSpeed , store . SpeedPolicy ) ;
2017-11-12 15:23:21 +01:00
2018-05-02 20:32:42 +02:00
HashSet < CurrencyPair > currencyPairsToFetch = new HashSet < CurrencyPair > ( ) ;
var rules = storeBlob . GetRateRules ( _NetworkProvider ) ;
2018-07-27 13:37:16 +02:00
var excludeFilter = storeBlob . GetExcludedPaymentMethods ( ) ; // Here we can compose filters from other origin with PaymentFilter.Any()
2018-05-02 20:32:42 +02:00
foreach ( var network in store . GetSupportedPaymentMethods ( _NetworkProvider )
2018-07-27 13:37:16 +02:00
. Where ( s = > ! excludeFilter . Match ( s . PaymentId ) )
. Select ( c = > _NetworkProvider . GetNetwork ( c . PaymentId . CryptoCode ) )
2018-05-02 20:32:42 +02:00
. Where ( c = > c ! = null ) )
{
currencyPairsToFetch . Add ( new CurrencyPair ( network . CryptoCode , invoice . Currency ) ) ;
if ( storeBlob . LightningMaxValue ! = null )
currencyPairsToFetch . Add ( new CurrencyPair ( network . CryptoCode , storeBlob . LightningMaxValue . Currency ) ) ;
if ( storeBlob . OnChainMinValue ! = null )
currencyPairsToFetch . Add ( new CurrencyPair ( network . CryptoCode , storeBlob . OnChainMinValue . Currency ) ) ;
}
var rateRules = storeBlob . GetRateRules ( _NetworkProvider ) ;
var fetchingByCurrencyPair = _RateProvider . FetchRates ( currencyPairsToFetch , rateRules ) ;
2018-07-24 05:19:43 +02:00
var fetchingAll = WhenAllFetched ( logs , fetchingByCurrencyPair ) ;
2018-03-28 15:37:01 +02:00
var supportedPaymentMethods = store . GetSupportedPaymentMethods ( _NetworkProvider )
2018-07-27 13:37:16 +02:00
. Where ( s = > ! excludeFilter . Match ( s . PaymentId ) )
2018-03-28 15:37:01 +02:00
. Select ( c = >
( Handler : ( IPaymentMethodHandler ) _ServiceProvider . GetService ( typeof ( IPaymentMethodHandler < > ) . MakeGenericType ( c . GetType ( ) ) ) ,
SupportedPaymentMethod : c ,
Network : _NetworkProvider . GetNetwork ( c . PaymentId . CryptoCode ) ) )
. Where ( c = > c . Network ! = null )
. Select ( o = >
( SupportedPaymentMethod : o . SupportedPaymentMethod ,
2018-07-24 05:19:43 +02:00
PaymentMethod : CreatePaymentMethodAsync ( fetchingByCurrencyPair , o . Handler , o . SupportedPaymentMethod , o . Network , entity , store , logs ) ) )
2018-03-28 15:37:01 +02:00
. ToList ( ) ;
2018-03-25 18:57:44 +02:00
List < ISupportedPaymentMethod > supported = new List < ISupportedPaymentMethod > ( ) ;
2018-03-28 15:37:01 +02:00
var paymentMethods = new PaymentMethodDictionary ( ) ;
foreach ( var o in supportedPaymentMethods )
2018-02-20 04:45:04 +01:00
{
2018-07-24 05:19:43 +02:00
var paymentMethod = await o . PaymentMethod ;
if ( paymentMethod = = null )
continue ;
supported . Add ( o . SupportedPaymentMethod ) ;
paymentMethods . Add ( paymentMethod ) ;
2018-03-25 18:57:44 +02:00
}
2018-03-28 15:37:01 +02:00
if ( supported . Count = = 0 )
2018-03-25 18:57:44 +02:00
{
2018-03-28 15:37:01 +02:00
StringBuilder errors = new StringBuilder ( ) ;
2019-01-18 11:15:31 +01:00
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/btcpay-basics/gettingstarted#connecting-btcpay-store-to-your-wallet)" ) ;
2018-07-24 05:19:43 +02:00
foreach ( var error in logs . ToList ( ) )
2018-03-28 15:37:01 +02:00
{
2018-07-24 05:19:43 +02:00
errors . AppendLine ( error . ToString ( ) ) ;
2018-03-28 15:37:01 +02:00
}
throw new BitpayHttpException ( 400 , errors . ToString ( ) ) ;
2017-12-21 07:52:04 +01:00
}
2018-03-25 18:57:44 +02:00
entity . SetSupportedPaymentMethods ( supported ) ;
entity . SetPaymentMethods ( paymentMethods ) ;
2017-10-27 10:53:04 +02:00
entity . PosData = invoice . PosData ;
2018-07-24 05:19:43 +02:00
entity = await _InvoiceRepository . CreateInvoiceAsync ( store . Id , entity , logs , _NetworkProvider ) ;
await fetchingAll ;
2019-01-06 10:12:45 +01:00
_EventAggregator . Publish ( new Events . InvoiceEvent ( entity . EntityToDTO ( _NetworkProvider ) , 1001 , InvoiceEvent . Created ) ) ;
2017-12-21 07:52:04 +01:00
var resp = entity . EntityToDTO ( _NetworkProvider ) ;
2017-10-27 10:53:04 +02:00
return new DataWrapper < InvoiceResponse > ( resp ) { Facade = "pos/invoice" } ;
}
2017-09-13 08:47:34 +02:00
2018-07-24 05:19:43 +02:00
private Task WhenAllFetched ( InvoiceLogs logs , Dictionary < CurrencyPair , Task < RateResult > > fetchingByCurrencyPair )
2018-03-28 15:37:01 +02:00
{
2018-07-24 05:19:43 +02:00
return Task . WhenAll ( fetchingByCurrencyPair . Select ( async pair = >
2018-03-28 16:15:10 +02:00
{
2018-07-24 05:19:43 +02:00
var rateResult = await pair . Value ;
logs . Write ( $"{pair.Key}: The rating rule is {rateResult.Rule}" ) ;
logs . Write ( $"{pair.Key}: The evaluated rating rule is {rateResult.EvaluatedRule}" ) ;
if ( rateResult . Errors . Count ! = 0 )
{
var allRateRuleErrors = string . Join ( ", " , rateResult . Errors . ToArray ( ) ) ;
logs . Write ( $"{pair.Key}: Rate rule error ({allRateRuleErrors})" ) ;
}
2018-08-22 17:24:33 +02:00
foreach ( var ex in rateResult . ExchangeExceptions )
2018-07-24 05:19:43 +02:00
{
2018-08-22 17:24:33 +02:00
logs . Write ( $"{pair.Key}: Exception reaching exchange {ex.ExchangeName} ({ex.Exception.Message})" ) ;
2018-07-24 05:19:43 +02:00
}
} ) . ToArray ( ) ) ;
}
2018-04-03 10:39:28 +02:00
2018-07-24 05:19:43 +02:00
private async Task < PaymentMethod > CreatePaymentMethodAsync ( Dictionary < CurrencyPair , Task < RateResult > > fetchingByCurrencyPair , IPaymentMethodHandler handler , ISupportedPaymentMethod supportedPaymentMethod , BTCPayNetwork network , InvoiceEntity entity , StoreData store , InvoiceLogs logs )
{
try
2018-04-03 10:39:28 +02:00
{
2018-07-24 05:19:43 +02:00
var storeBlob = store . GetStoreBlob ( ) ;
2018-08-21 06:54:52 +02:00
var preparePayment = handler . PreparePayment ( supportedPaymentMethod , store , network ) ;
2018-07-24 05:19:43 +02:00
var rate = await fetchingByCurrencyPair [ new CurrencyPair ( network . CryptoCode , entity . ProductInformation . Currency ) ] ;
2018-07-27 11:04:41 +02:00
if ( rate . BidAsk = = null )
2018-07-24 05:19:43 +02:00
{
return null ;
}
PaymentMethod paymentMethod = new PaymentMethod ( ) ;
paymentMethod . ParentEntity = entity ;
paymentMethod . Network = network ;
paymentMethod . SetId ( supportedPaymentMethod . PaymentId ) ;
2018-07-27 11:04:41 +02:00
paymentMethod . Rate = rate . BidAsk . Bid ;
2018-08-21 06:54:52 +02:00
var paymentDetails = await handler . CreatePaymentMethodDetails ( supportedPaymentMethod , paymentMethod , store , network , preparePayment ) ;
2018-07-24 05:19:43 +02:00
paymentMethod . SetPaymentMethodDetails ( paymentDetails ) ;
Func < Money , Money , bool > compare = null ;
CurrencyValue limitValue = null ;
string errorMessage = null ;
if ( supportedPaymentMethod . PaymentId . PaymentType = = PaymentTypes . LightningLike & &
storeBlob . LightningMaxValue ! = null )
{
compare = ( a , b ) = > a > b ;
limitValue = storeBlob . LightningMaxValue ;
errorMessage = "The amount of the invoice is too high to be paid with lightning" ;
}
else if ( supportedPaymentMethod . PaymentId . PaymentType = = PaymentTypes . BTCLike & &
storeBlob . OnChainMinValue ! = null )
2018-03-28 16:15:10 +02:00
{
2018-07-24 05:19:43 +02:00
compare = ( a , b ) = > a < b ;
limitValue = storeBlob . OnChainMinValue ;
errorMessage = "The amount of the invoice is too low to be paid on chain" ;
}
if ( compare ! = null )
{
var limitValueRate = await fetchingByCurrencyPair [ new CurrencyPair ( network . CryptoCode , limitValue . Currency ) ] ;
2018-07-27 11:04:41 +02:00
if ( limitValueRate . BidAsk ! = null )
2018-05-02 20:32:42 +02:00
{
2018-07-27 11:04:41 +02:00
var limitValueCrypto = Money . Coins ( limitValue . Value / limitValueRate . BidAsk . Bid ) ;
2018-07-24 05:19:43 +02:00
if ( compare ( paymentMethod . Calculate ( ) . Due , limitValueCrypto ) )
{
logs . Write ( $"{supportedPaymentMethod.PaymentId.CryptoCode}: {errorMessage}" ) ;
return null ;
}
2018-05-02 20:32:42 +02:00
}
2018-03-28 16:15:10 +02:00
}
2018-07-24 05:19:43 +02:00
///////////////
2018-03-28 16:15:10 +02:00
2018-03-28 15:37:01 +02:00
#pragma warning disable CS0618
2018-07-24 05:19:43 +02:00
if ( paymentMethod . GetId ( ) . IsBTCOnChain )
{
2019-01-07 07:35:18 +01:00
entity . TxFee = paymentMethod . NextNetworkFee ;
2018-07-24 05:19:43 +02:00
entity . Rate = paymentMethod . Rate ;
entity . DepositAddress = paymentMethod . DepositAddress ;
}
#pragma warning restore CS0618
return paymentMethod ;
}
catch ( PaymentMethodUnavailableException ex )
2018-03-28 15:37:01 +02:00
{
2018-07-24 05:19:43 +02:00
logs . Write ( $"{supportedPaymentMethod.PaymentId.CryptoCode}: Payment method unavailable ({ex.Message})" ) ;
2018-03-28 15:37:01 +02:00
}
2018-07-24 05:19:43 +02:00
catch ( Exception ex )
{
logs . Write ( $"{supportedPaymentMethod.PaymentId.CryptoCode}: Unexpected exception ({ex.ToString()})" ) ;
}
return null ;
2018-03-28 15:37:01 +02:00
}
2017-10-27 10:53:04 +02:00
private SpeedPolicy ParseSpeedPolicy ( string transactionSpeed , SpeedPolicy defaultPolicy )
{
if ( transactionSpeed = = null )
return defaultPolicy ;
var mappings = new Dictionary < string , SpeedPolicy > ( ) ;
mappings . Add ( "low" , SpeedPolicy . LowSpeed ) ;
2018-05-11 15:12:45 +02:00
mappings . Add ( "low-medium" , SpeedPolicy . LowMediumSpeed ) ;
2017-10-27 10:53:04 +02:00
mappings . Add ( "medium" , SpeedPolicy . MediumSpeed ) ;
mappings . Add ( "high" , SpeedPolicy . HighSpeed ) ;
if ( ! mappings . TryGetValue ( transactionSpeed , out SpeedPolicy policy ) )
policy = defaultPolicy ;
return policy ;
}
2017-10-23 07:51:21 +02:00
2017-10-27 10:53:04 +02:00
private void FillBuyerInfo ( Buyer buyer , BuyerInformation buyerInformation )
{
if ( buyer = = null )
return ;
buyerInformation . BuyerAddress1 = buyerInformation . BuyerAddress1 ? ? buyer . Address1 ;
buyerInformation . BuyerAddress2 = buyerInformation . BuyerAddress2 ? ? buyer . Address2 ;
buyerInformation . BuyerCity = buyerInformation . BuyerCity ? ? buyer . City ;
buyerInformation . BuyerCountry = buyerInformation . BuyerCountry ? ? buyer . country ;
buyerInformation . BuyerEmail = buyerInformation . BuyerEmail ? ? buyer . email ;
buyerInformation . BuyerName = buyerInformation . BuyerName ? ? buyer . Name ;
buyerInformation . BuyerPhone = buyerInformation . BuyerPhone ? ? buyer . phone ;
buyerInformation . BuyerState = buyerInformation . BuyerState ? ? buyer . State ;
buyerInformation . BuyerZip = buyerInformation . BuyerZip ? ? buyer . zip ;
}
2017-10-13 09:44:55 +02:00
2017-10-27 10:53:04 +02:00
private TDest Map < TFrom , TDest > ( TFrom data )
{
return JsonConvert . DeserializeObject < TDest > ( JsonConvert . SerializeObject ( data ) ) ;
}
}
2017-09-13 08:47:34 +02:00
}