2020-06-29 04:44:35 +02:00
using System ;
2021-04-13 10:36:49 +02:00
using System.Collections.Generic ;
2020-06-24 03:34:09 +02:00
using System.Linq ;
2023-12-06 01:17:58 +01:00
using System.Security.Cryptography ;
using System.Text.RegularExpressions ;
2022-11-22 12:17:29 +01:00
using System.Threading ;
2020-06-24 03:34:09 +02:00
using System.Threading.Tasks ;
2023-12-06 01:17:58 +01:00
using Amazon.S3.Model ;
2022-08-11 14:30:42 +02:00
using BTCPayServer.Abstractions.Constants ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2022-08-11 14:30:42 +02:00
using BTCPayServer.Client ;
2021-04-13 10:36:49 +02:00
using BTCPayServer.Client.Models ;
2024-03-14 11:07:11 +01:00
using BTCPayServer.Controllers.Greenfield ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Data ;
using BTCPayServer.HostedServices ;
2023-12-06 01:17:58 +01:00
using BTCPayServer.Lightning ;
using BTCPayServer.ModelBinders ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Models ;
2022-08-11 14:30:42 +02:00
using BTCPayServer.Models.WalletViewModels ;
2023-12-06 01:17:58 +01:00
using BTCPayServer.NTag424 ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Payments ;
2024-05-01 03:22:07 +02:00
using BTCPayServer.Payouts ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Services ;
using BTCPayServer.Services.Rates ;
2023-01-30 09:23:49 +01:00
using BTCPayServer.Services.Stores ;
2023-12-06 01:17:58 +01:00
using Dapper ;
2020-06-24 06:44:26 +02:00
using Microsoft.AspNetCore.Authorization ;
2020-06-24 03:34:09 +02:00
using Microsoft.AspNetCore.Mvc ;
using Microsoft.EntityFrameworkCore ;
2022-05-25 12:59:28 +02:00
using NBitcoin ;
2023-12-06 01:17:58 +01:00
using NBitcoin.DataEncoders ;
using NdefLibrary.Ndef ;
using Newtonsoft.Json.Linq ;
2020-06-24 03:34:09 +02:00
namespace BTCPayServer.Controllers
{
2023-12-06 01:17:58 +01:00
public partial class UIPullPaymentController : Controller
2020-06-24 03:34:09 +02:00
{
private readonly ApplicationDbContextFactory _dbContextFactory ;
private readonly CurrencyNameTable _currencyNameTable ;
2023-03-13 02:12:58 +01:00
private readonly DisplayFormatter _displayFormatter ;
2020-06-24 03:34:09 +02:00
private readonly PullPaymentHostedService _pullPaymentHostedService ;
2023-06-16 03:56:17 +02:00
private readonly BTCPayNetworkProvider _networkProvider ;
2020-06-24 03:34:09 +02:00
private readonly BTCPayNetworkJsonSerializerSettings _serializerSettings ;
2024-05-01 03:22:07 +02:00
private readonly PayoutMethodHandlerDictionary _payoutHandlers ;
2023-01-30 09:23:49 +01:00
private readonly StoreRepository _storeRepository ;
2023-12-06 01:17:58 +01:00
private readonly BTCPayServerEnvironment _env ;
private readonly SettingsRepository _settingsRepository ;
2020-06-24 03:34:09 +02:00
2022-01-07 04:32:00 +01:00
public UIPullPaymentController ( ApplicationDbContextFactory dbContextFactory ,
2020-06-24 03:34:09 +02:00
CurrencyNameTable currencyNameTable ,
2023-03-13 02:12:58 +01:00
DisplayFormatter displayFormatter ,
2020-06-24 03:34:09 +02:00
PullPaymentHostedService pullPaymentHostedService ,
2023-06-16 03:56:17 +02:00
BTCPayNetworkProvider networkProvider ,
2021-04-13 10:36:49 +02:00
BTCPayNetworkJsonSerializerSettings serializerSettings ,
2024-05-01 03:22:07 +02:00
PayoutMethodHandlerDictionary payoutHandlers ,
2023-12-06 01:17:58 +01:00
StoreRepository storeRepository ,
BTCPayServerEnvironment env ,
SettingsRepository settingsRepository )
2020-06-24 03:34:09 +02:00
{
_dbContextFactory = dbContextFactory ;
_currencyNameTable = currencyNameTable ;
2023-03-13 02:12:58 +01:00
_displayFormatter = displayFormatter ;
2020-06-24 03:34:09 +02:00
_pullPaymentHostedService = pullPaymentHostedService ;
_serializerSettings = serializerSettings ;
2021-04-13 10:36:49 +02:00
_payoutHandlers = payoutHandlers ;
2023-01-30 09:23:49 +01:00
_storeRepository = storeRepository ;
2023-12-06 01:17:58 +01:00
_env = env ;
_settingsRepository = settingsRepository ;
2023-06-16 03:56:17 +02:00
_networkProvider = networkProvider ;
2020-06-24 03:34:09 +02:00
}
2021-12-31 08:59:02 +01:00
2022-08-11 14:30:42 +02:00
[AllowAnonymous]
[HttpGet("pull-payments/{pullPaymentId}")]
2020-06-24 03:34:09 +02:00
public async Task < IActionResult > ViewPullPayment ( string pullPaymentId )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
var pp = await ctx . PullPayments . FindAsync ( pullPaymentId ) ;
if ( pp is null )
return NotFound ( ) ;
var blob = pp . GetBlob ( ) ;
2023-01-30 09:23:49 +01:00
var store = await _storeRepository . FindStore ( pp . StoreId ) ;
if ( store is null )
return NotFound ( ) ;
2023-04-10 04:07:03 +02:00
2023-01-30 09:23:49 +01:00
var storeBlob = store . GetStoreBlob ( ) ;
2024-05-01 10:59:10 +02:00
var payouts = ( await ctx . Payouts . Where ( p = > p . PullPaymentDataId = = pp . Id )
2023-10-10 05:30:09 +02:00
. OrderByDescending ( o = > o . Date )
. ToListAsync ( ) )
. Select ( o = > new
{
Entity = o ,
Blob = o . GetBlob ( _serializerSettings ) ,
2024-05-01 03:22:07 +02:00
ProofBlob = _payoutHandlers . TryGet ( o . GetPayoutMethodId ( ) ) ? . ParseProof ( o )
2023-10-10 05:30:09 +02:00
} ) ;
2020-06-24 03:34:09 +02:00
var cd = _currencyNameTable . GetCurrencyData ( blob . Currency , false ) ;
var totalPaid = payouts . Where ( p = > p . Entity . State ! = PayoutState . Cancelled ) . Select ( p = > p . Blob . Amount ) . Sum ( ) ;
var amountDue = blob . Limit - totalPaid ;
2023-01-06 14:18:07 +01:00
ViewPullPaymentModel vm = new ( pp , DateTimeOffset . UtcNow )
2020-06-24 03:34:09 +02:00
{
AmountCollected = totalPaid ,
AmountDue = amountDue ,
ClaimedAmount = amountDue ,
CurrencyData = cd ,
2021-03-07 23:51:50 +01:00
StartDate = pp . StartDate ,
2021-12-17 07:31:06 +01:00
LastRefreshed = DateTime . UtcNow ,
2023-10-10 05:30:09 +02:00
Payouts = payouts . Select ( entity = > new ViewPullPaymentModel . PayoutLine
{
Id = entity . Entity . Id ,
Amount = entity . Blob . Amount ,
2023-10-25 13:51:27 +02:00
AmountFormatted = _displayFormatter . Currency ( entity . Blob . Amount , blob . Currency ) ,
2023-10-10 05:30:09 +02:00
Currency = blob . Currency ,
Status = entity . Entity . State ,
Destination = entity . Blob . Destination ,
PaymentMethod = PaymentMethodId . Parse ( entity . Entity . PaymentMethodId ) ,
Link = entity . ProofBlob ? . Link ,
TransactionId = entity . ProofBlob ? . Id
} ) . ToList ( )
2020-06-24 03:34:09 +02:00
} ;
vm . IsPending & = vm . AmountDue > 0.0 m ;
2023-12-01 16:13:44 +01:00
vm . StoreBranding = new StoreBrandingViewModel ( storeBlob )
{
EmbeddedCSS = blob . View . EmbeddedCSS ,
CustomCSSLink = blob . View . CustomCSSLink
} ;
2023-06-16 03:56:17 +02:00
if ( _pullPaymentHostedService . SupportsLNURL ( blob ) )
{
2024-03-14 11:07:11 +01:00
var url = Url . Action ( nameof ( UILNURLController . GetLNURLForPullPayment ) , "UILNURL" , new { cryptoCode = _networkProvider . DefaultNetwork . CryptoCode , pullPaymentId = vm . Id } , Request . Scheme , Request . Host . ToString ( ) ) ;
2023-06-16 03:56:17 +02:00
vm . LnurlEndpoint = url ! = null ? new Uri ( url ) : null ;
2024-03-14 11:07:11 +01:00
vm . SetupDeepLink = $"boltcard://program?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.UpdateVersion)}" ;
vm . ResetDeepLink = $"boltcard://reset?url={GetBoltcardDeeplinkUrl(vm, OnExistingBehavior.KeepVersion)}" ;
2023-06-16 03:56:17 +02:00
}
2023-12-06 01:17:58 +01:00
2020-06-24 03:34:09 +02:00
return View ( nameof ( ViewPullPayment ) , vm ) ;
}
2024-03-14 11:07:11 +01:00
private string GetBoltcardDeeplinkUrl ( ViewPullPaymentModel vm , OnExistingBehavior onExisting )
{
var registerUrl = Url . Action ( nameof ( GreenfieldPullPaymentController . RegisterBoltcard ) , "GreenfieldPullPayment" ,
new
{
pullPaymentId = vm . Id ,
onExisting = onExisting . ToString ( )
} , Request . Scheme , Request . Host . ToString ( ) ) ;
registerUrl = Uri . EscapeDataString ( registerUrl ) ;
return registerUrl ;
}
2022-08-11 14:30:42 +02:00
[HttpGet("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > EditPullPayment ( string storeId , string pullPaymentId )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
Data . PullPaymentData pp = await ctx . PullPayments . FindAsync ( pullPaymentId ) ;
if ( pp = = null & & ! string . IsNullOrEmpty ( pullPaymentId ) )
{
return NotFound ( ) ;
}
var vm = new UpdatePullPaymentModel ( pp ) ;
return View ( vm ) ;
}
[HttpPost("stores/{storeId}/pull-payments/edit/{pullPaymentId}")]
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
public async Task < IActionResult > EditPullPayment ( string storeId , string pullPaymentId , UpdatePullPaymentModel viewModel )
{
using var ctx = _dbContextFactory . CreateContext ( ) ;
var pp = await ctx . PullPayments . FindAsync ( pullPaymentId ) ;
if ( pp = = null & & ! string . IsNullOrEmpty ( pullPaymentId ) )
{
return NotFound ( ) ;
}
if ( ! ModelState . IsValid )
{
return View ( viewModel ) ;
}
var blob = pp . GetBlob ( ) ;
blob . Description = viewModel . Description ? ? string . Empty ;
blob . Name = viewModel . Name ? ? string . Empty ;
blob . View = new PullPaymentBlob . PullPaymentView ( )
{
Title = viewModel . Name ? ? string . Empty ,
Description = viewModel . Description ? ? string . Empty ,
CustomCSSLink = viewModel . CustomCSSLink ,
Email = null ,
EmbeddedCSS = viewModel . EmbeddedCSS ,
} ;
2023-01-06 14:18:07 +01:00
2022-08-11 14:30:42 +02:00
pp . SetBlob ( blob ) ;
ctx . PullPayments . Update ( pp ) ;
await ctx . SaveChangesAsync ( ) ;
2023-01-06 14:18:07 +01:00
2022-08-11 14:30:42 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Message = "Pull payment updated successfully" ,
Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
return RedirectToAction ( nameof ( UIStorePullPaymentsController . PullPayments ) , "UIStorePullPayments" , new { storeId , pullPaymentId } ) ;
}
[AllowAnonymous]
2022-05-25 12:59:28 +02:00
[HttpPost("pull-payments/{pullPaymentId}/claim")]
2022-11-22 12:17:29 +01:00
public async Task < IActionResult > ClaimPullPayment ( string pullPaymentId , ViewPullPaymentModel vm , CancellationToken cancellationToken )
2020-06-24 03:34:09 +02:00
{
2023-10-10 05:30:09 +02:00
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2020-06-24 03:34:09 +02:00
var pp = await ctx . PullPayments . FindAsync ( pullPaymentId ) ;
if ( pp is null )
{
ModelState . AddModelError ( nameof ( pullPaymentId ) , "This pull payment does not exists" ) ;
}
2021-12-31 08:59:02 +01:00
2023-10-10 05:30:09 +02:00
if ( string . IsNullOrEmpty ( vm . Destination ) )
2020-06-24 03:34:09 +02:00
{
2023-10-10 05:30:09 +02:00
ModelState . AddModelError ( nameof ( vm . Destination ) , "Please provide a destination" ) ;
2021-10-18 05:37:59 +02:00
return await ViewPullPayment ( pullPaymentId ) ;
}
2023-10-10 05:30:09 +02:00
var ppBlob = pp . GetBlob ( ) ;
var supported = ppBlob . SupportedPaymentMethods ;
2024-05-01 03:22:07 +02:00
PayoutMethodId payoutMethodId = null ;
2023-10-10 05:30:09 +02:00
IClaimDestination destination = null ;
2024-05-01 03:22:07 +02:00
IPayoutHandler payoutHandler = null ;
if ( string . IsNullOrEmpty ( vm . SelectedPayoutMethod ) )
2021-10-18 05:37:59 +02:00
{
2023-10-10 05:30:09 +02:00
foreach ( var pmId in supported )
{
2024-05-01 03:22:07 +02:00
var handler = _payoutHandlers . TryGet ( pmId ) ;
2023-10-10 05:30:09 +02:00
( IClaimDestination dst , string err ) = handler = = null
? ( null , "No payment handler found for this payment method" )
2024-05-01 03:22:07 +02:00
: await handler . ParseAndValidateClaimDestination ( vm . Destination , ppBlob , cancellationToken ) ;
2023-10-10 05:30:09 +02:00
if ( dst is not null & & err is null )
{
2024-05-01 03:22:07 +02:00
payoutMethodId = pmId ;
2023-10-10 05:30:09 +02:00
destination = dst ;
2024-05-01 03:22:07 +02:00
payoutHandler = handler ;
2023-10-10 05:30:09 +02:00
break ;
}
}
}
else
{
2024-05-01 03:22:07 +02:00
payoutMethodId = supported . FirstOrDefault ( id = > vm . SelectedPayoutMethod = = id . ToString ( ) ) ;
payoutHandler = payoutMethodId is null ? null : _payoutHandlers . TryGet ( payoutMethodId ) ;
destination = payoutHandler is null ? null : ( await payoutHandler . ParseAndValidateClaimDestination ( vm . Destination , ppBlob , cancellationToken ) ) . destination ;
2023-10-10 05:30:09 +02:00
}
if ( destination is null )
{
ModelState . AddModelError ( nameof ( vm . Destination ) , "Invalid destination or payment method" ) ;
2021-10-18 05:37:59 +02:00
return await ViewPullPayment ( pullPaymentId ) ;
}
2024-05-01 03:22:07 +02:00
var amtError = ClaimRequest . IsPayoutAmountOk ( destination , vm . ClaimedAmount = = 0 ? null : vm . ClaimedAmount , payoutHandler . Currency , ppBlob . Currency ) ;
2023-07-24 13:40:26 +02:00
if ( amtError . error is not null )
2021-10-18 05:37:59 +02:00
{
2023-12-06 01:17:58 +01:00
ModelState . AddModelError ( nameof ( vm . ClaimedAmount ) , amtError . error ) ;
2021-10-18 05:37:59 +02:00
}
2023-07-24 13:40:26 +02:00
else if ( amtError . amount is not null )
2021-10-18 05:37:59 +02:00
{
2023-07-24 13:40:26 +02:00
vm . ClaimedAmount = amtError . amount . Value ;
2020-06-24 03:34:09 +02:00
}
if ( ! ModelState . IsValid )
{
return await ViewPullPayment ( pullPaymentId ) ;
}
2023-10-10 05:30:09 +02:00
var result = await _pullPaymentHostedService . Claim ( new ClaimRequest
2020-06-24 03:34:09 +02:00
{
2023-10-10 05:30:09 +02:00
Destination = destination ,
2020-06-24 03:34:09 +02:00
PullPaymentId = pullPaymentId ,
Value = vm . ClaimedAmount ,
2024-05-01 03:22:07 +02:00
PayoutMethodId = payoutMethodId ,
2024-02-20 10:42:38 +01:00
StoreId = pp . StoreId
2020-06-24 03:34:09 +02:00
} ) ;
if ( result . Result ! = ClaimRequest . ClaimResult . Ok )
{
2023-10-10 05:30:09 +02:00
ModelState . AddModelError (
result . Result = = ClaimRequest . ClaimResult . AmountTooLow ? nameof ( vm . ClaimedAmount ) : string . Empty ,
ClaimRequest . GetErrorMessage ( result . Result ) ) ;
2020-06-24 03:34:09 +02:00
return await ViewPullPayment ( pullPaymentId ) ;
}
2023-01-06 14:18:07 +01:00
2022-05-18 14:40:26 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
2020-06-24 03:34:09 +02:00
{
2023-03-13 02:12:58 +01:00
Message = $"Your claim request of {_displayFormatter.Currency(vm.ClaimedAmount, ppBlob.Currency, DisplayFormatter.CurrencyFormat.Symbol)} to {vm.Destination} has been submitted and is awaiting {(result.PayoutData.State == PayoutState.AwaitingApproval ? " approval " : " payment ")}." ,
2022-05-18 14:40:26 +02:00
Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
2023-01-06 14:18:07 +01:00
2022-05-18 14:40:26 +02:00
return RedirectToAction ( nameof ( ViewPullPayment ) , new { pullPaymentId } ) ;
2020-06-24 03:34:09 +02:00
}
}
}