2020-06-29 04:44:35 +02:00
using System ;
2020-06-24 03:34:09 +02:00
using System.Collections.Generic ;
2020-06-28 10:55:27 +02:00
using System.Globalization ;
2020-06-24 03:34:09 +02:00
using System.Linq ;
2020-06-28 10:55:27 +02:00
using System.Threading ;
2020-06-24 03:34:09 +02:00
using System.Threading.Tasks ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2021-04-13 10:36:49 +02:00
using BTCPayServer.Client.Models ;
2021-07-30 11:47:02 +02:00
using BTCPayServer.Common ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Data ;
2020-06-28 10:55:27 +02:00
using BTCPayServer.HostedServices ;
using BTCPayServer.ModelBinders ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Models ;
2020-06-28 10:55:27 +02:00
using BTCPayServer.Models.WalletViewModels ;
using BTCPayServer.Payments ;
using BTCPayServer.Rating ;
2020-06-24 03:34:09 +02:00
using BTCPayServer.Views ;
2020-06-28 10:55:27 +02:00
using Microsoft.AspNetCore.Mvc ;
using Microsoft.EntityFrameworkCore ;
using NBitcoin ;
2021-04-13 10:36:49 +02:00
using PayoutData = BTCPayServer . Data . PayoutData ;
2020-06-24 03:34:09 +02:00
namespace BTCPayServer.Controllers
{
public partial class WalletsController
{
2021-06-30 09:59:01 +02:00
[HttpGet("{walletId}/pull-payments/new")]
2020-06-24 03:34:09 +02:00
public IActionResult NewPullPayment ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
WalletId walletId )
{
2021-01-16 11:48:05 +01:00
if ( GetDerivationSchemeSettings ( walletId ) = = null )
return NotFound ( ) ;
return View ( new NewPullPaymentModel
2020-06-24 03:34:09 +02:00
{
Name = "" ,
2020-12-08 05:04:50 +01:00
Currency = "BTC" ,
CustomCSSLink = "" ,
EmbeddedCSS = "" ,
2020-06-24 03:34:09 +02:00
} ) ;
}
2021-06-30 09:59:01 +02:00
[HttpPost("{walletId}/pull-payments/new")]
2020-06-24 03:34:09 +02:00
public async Task < IActionResult > NewPullPayment ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
WalletId walletId , NewPullPaymentModel model )
{
2021-01-16 11:48:05 +01:00
if ( GetDerivationSchemeSettings ( walletId ) = = null )
return NotFound ( ) ;
2020-06-24 03:34:09 +02:00
model . Name ? ? = string . Empty ;
2020-06-24 06:44:26 +02:00
model . Currency = model . Currency . ToUpperInvariant ( ) . Trim ( ) ;
2020-06-24 03:34:09 +02:00
if ( _currencyTable . GetCurrencyData ( model . Currency , false ) is null )
{
ModelState . AddModelError ( nameof ( model . Currency ) , "Invalid currency" ) ;
}
if ( model . Amount < = 0.0 m )
{
ModelState . AddModelError ( nameof ( model . Amount ) , "The amount should be more than zero" ) ;
}
if ( model . Name . Length > 50 )
{
ModelState . AddModelError ( nameof ( model . Name ) , "The name should be maximum 50 characters." ) ;
}
2020-06-24 10:51:00 +02:00
var paymentMethodId = walletId . GetPaymentMethodId ( ) ;
var n = this . NetworkProvider . GetNetwork < BTCPayNetwork > ( paymentMethodId . CryptoCode ) ;
if ( n is null | | paymentMethodId . PaymentType ! = PaymentTypes . BTCLike | | n . ReadonlyWallet )
ModelState . AddModelError ( nameof ( model . Name ) , "Pull payments are not supported with this wallet" ) ;
2020-06-24 03:34:09 +02:00
if ( ! ModelState . IsValid )
return View ( model ) ;
await _pullPaymentService . CreatePullPayment ( new HostedServices . CreatePullPayment ( )
{
Name = model . Name ,
Amount = model . Amount ,
2020-06-24 06:44:26 +02:00
Currency = model . Currency ,
2020-06-24 03:34:09 +02:00
StoreId = walletId . StoreId ,
2020-12-08 05:04:50 +01:00
PaymentMethodIds = new [ ] { paymentMethodId } ,
EmbeddedCSS = model . EmbeddedCSS ,
CustomCSSLink = model . CustomCSSLink
2020-06-24 03:34:09 +02:00
} ) ;
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = "Pull payment request created" ,
Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
return RedirectToAction ( nameof ( PullPayments ) , new { walletId = walletId . ToString ( ) } ) ;
}
2021-06-30 09:59:01 +02:00
[HttpGet("{walletId}/pull-payments")]
2020-06-24 03:34:09 +02:00
public async Task < IActionResult > PullPayments (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId )
{
using var ctx = this . _dbContextFactory . CreateContext ( ) ;
var now = DateTimeOffset . UtcNow ;
var storeId = walletId . StoreId ;
var pps = await ctx . PullPayments . Where ( p = > p . StoreId = = storeId & & ! p . Archived )
. OrderByDescending ( p = > p . StartDate )
. Select ( o = > new
{
PullPayment = o ,
Awaiting = o . Payouts
2020-06-24 06:44:26 +02:00
. Where ( p = > p . State = = PayoutState . AwaitingPayment | | p . State = = PayoutState . AwaitingApproval ) ,
2020-06-24 03:34:09 +02:00
Completed = o . Payouts
. Where ( p = > p . State = = PayoutState . Completed | | p . State = = PayoutState . InProgress )
} )
. ToListAsync ( ) ;
2021-01-16 11:48:05 +01:00
var vm = new PullPaymentsModel
{ HasDerivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ! = null } ;
2020-06-24 03:34:09 +02:00
foreach ( var o in pps )
{
var pp = o . PullPayment ;
var totalCompleted = o . Completed . Where ( o = > o . IsInPeriod ( pp , now ) )
. Select ( o = > o . GetBlob ( _jsonSerializerSettings ) . Amount ) . Sum ( ) ;
var totalAwaiting = o . Awaiting . Where ( o = > o . IsInPeriod ( pp , now ) )
. Select ( o = > o . GetBlob ( _jsonSerializerSettings ) . Amount ) . Sum ( ) ;
var ppBlob = pp . GetBlob ( ) ;
var ni = _currencyTable . GetCurrencyData ( ppBlob . Currency , true ) ;
var nfi = _currencyTable . GetNumberFormatInfo ( ppBlob . Currency , true ) ;
var period = pp . GetPeriod ( now ) ;
vm . PullPayments . Add ( new PullPaymentsModel . PullPaymentModel ( )
{
StartDate = pp . StartDate ,
EndDate = pp . EndDate ,
Id = pp . Id ,
Name = ppBlob . Name ,
Progress = new PullPaymentsModel . PullPaymentModel . ProgressModel ( )
{
CompletedPercent = ( int ) ( totalCompleted / ppBlob . Limit * 100 m ) ,
AwaitingPercent = ( int ) ( totalAwaiting / ppBlob . Limit * 100 m ) ,
Awaiting = totalAwaiting . RoundToSignificant ( ni . Divisibility ) . ToString ( "C" , nfi ) ,
Completed = totalCompleted . RoundToSignificant ( ni . Divisibility ) . ToString ( "C" , nfi ) ,
Limit = _currencyTable . DisplayFormatCurrency ( ppBlob . Limit , ppBlob . Currency ) ,
ResetIn = period ? . End is DateTimeOffset nr ? ZeroIfNegative ( nr - now ) . TimeString ( ) : null ,
EndIn = pp . EndDate is DateTimeOffset end ? ZeroIfNegative ( end - now ) . TimeString ( ) : null
}
} ) ;
}
return View ( vm ) ;
}
public TimeSpan ZeroIfNegative ( TimeSpan time )
{
if ( time < TimeSpan . Zero )
time = TimeSpan . Zero ;
return time ;
}
2021-06-30 09:59:01 +02:00
[HttpGet("{walletId}/pull-payments/{pullPaymentId}/archive")]
2020-06-24 03:34:09 +02:00
public IActionResult ArchivePullPayment (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId ,
string pullPaymentId )
{
return View ( "Confirm" , new ConfirmModel ( )
{
Title = "Archive the pull payment" ,
Description = "Do you really want to archive this pull payment?" ,
ButtonClass = "btn-danger" ,
Action = "Archive"
} ) ;
}
2021-06-30 09:59:01 +02:00
[HttpPost("{walletId}/pull-payments/{pullPaymentId}/archive")]
2020-06-24 03:34:09 +02:00
public async Task < IActionResult > ArchivePullPaymentPost (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId ,
string pullPaymentId )
{
await _pullPaymentService . Cancel ( new HostedServices . PullPaymentHostedService . CancelRequest ( pullPaymentId ) ) ;
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = "Pull payment archived" ,
Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
return RedirectToAction ( nameof ( PullPayments ) , new { walletId = walletId . ToString ( ) } ) ;
}
2021-06-30 09:59:01 +02:00
[HttpPost("{walletId}/payouts")]
2020-06-24 03:34:09 +02:00
public async Task < IActionResult > PayoutsPost (
[ModelBinder(typeof(WalletIdModelBinder))]
2020-06-24 06:44:26 +02:00
WalletId walletId , PayoutsModel vm , CancellationToken cancellationToken )
2020-06-24 03:34:09 +02:00
{
2021-01-16 11:48:05 +01:00
if ( vm is null | | GetDerivationSchemeSettings ( walletId ) = = null )
2020-06-24 03:34:09 +02:00
return NotFound ( ) ;
2021-01-16 11:48:05 +01:00
2020-06-24 03:34:09 +02:00
var storeId = walletId . StoreId ;
var paymentMethodId = new PaymentMethodId ( walletId . CryptoCode , PaymentTypes . BTCLike ) ;
2021-04-13 10:36:49 +02:00
var commandState = Enum . Parse < PayoutState > ( vm . Command . Split ( "-" ) . First ( ) ) ;
var payoutIds = vm . GetSelectedPayouts ( commandState ) ;
2020-06-24 03:34:09 +02:00
if ( payoutIds . Length = = 0 )
{
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = "No payout selected" ,
Severity = StatusMessageModel . StatusSeverity . Error
} ) ;
return RedirectToAction ( nameof ( Payouts ) , new
{
walletId = walletId . ToString ( ) ,
pullPaymentId = vm . PullPaymentId
} ) ;
}
2021-04-13 10:36:49 +02:00
var command = vm . Command . Substring ( vm . Command . IndexOf ( '-' , StringComparison . InvariantCulture ) + 1 ) ;
switch ( command )
{
case "approve-pay" :
case "approve" :
2020-06-24 06:44:26 +02:00
{
2021-04-13 10:36:49 +02:00
await using var ctx = this . _dbContextFactory . CreateContext ( ) ;
ctx . ChangeTracker . QueryTrackingBehavior = QueryTrackingBehavior . NoTracking ;
var payouts = await GetPayoutsForPaymentMethod ( walletId . GetPaymentMethodId ( ) , ctx , payoutIds , storeId , cancellationToken ) ;
for ( int i = 0 ; i < payouts . Count ; i + + )
2020-06-24 06:44:26 +02:00
{
2021-04-13 10:36:49 +02:00
var payout = payouts [ i ] ;
if ( payout . State ! = PayoutState . AwaitingApproval )
continue ;
var rateResult = await _pullPaymentService . GetRate ( payout , null , cancellationToken ) ;
if ( rateResult . BidAsk = = null )
2020-06-24 06:44:26 +02:00
{
2021-04-13 10:36:49 +02:00
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = $"Rate unavailable: {rateResult.EvaluatedRule}" ,
Severity = StatusMessageModel . StatusSeverity . Error
} ) ;
return RedirectToAction ( nameof ( Payouts ) , new
{
walletId = walletId . ToString ( ) ,
pullPaymentId = vm . PullPaymentId
} ) ;
}
var approveResult = await _pullPaymentService . Approve ( new HostedServices . PullPaymentHostedService . PayoutApproval ( )
2020-06-24 06:44:26 +02:00
{
2021-04-13 10:36:49 +02:00
PayoutId = payout . Id ,
Revision = payout . GetBlob ( _jsonSerializerSettings ) . Revision ,
Rate = rateResult . BidAsk . Ask
2020-06-24 06:44:26 +02:00
} ) ;
2021-04-13 10:36:49 +02:00
if ( approveResult ! = HostedServices . PullPaymentHostedService . PayoutApproval . Result . Ok )
{
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = PullPaymentHostedService . PayoutApproval . GetErrorMessage ( approveResult ) ,
Severity = StatusMessageModel . StatusSeverity . Error
} ) ;
return RedirectToAction ( nameof ( Payouts ) , new
{
walletId = walletId . ToString ( ) ,
pullPaymentId = vm . PullPaymentId
} ) ;
}
2020-06-24 06:44:26 +02:00
}
2021-04-13 10:36:49 +02:00
if ( command = = "approve-pay" )
2020-06-24 06:44:26 +02:00
{
2021-04-13 10:36:49 +02:00
goto case "pay" ;
2020-06-24 06:44:26 +02:00
}
2021-04-13 10:36:49 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = "Payouts approved" , Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
return RedirectToAction ( nameof ( Payouts ) ,
new { walletId = walletId . ToString ( ) , pullPaymentId = vm . PullPaymentId } ) ;
2020-06-24 06:44:26 +02:00
}
2021-04-13 10:36:49 +02:00
case "pay" :
2020-06-24 03:34:09 +02:00
{
2021-04-13 10:36:49 +02:00
await using var ctx = this . _dbContextFactory . CreateContext ( ) ;
ctx . ChangeTracker . QueryTrackingBehavior = QueryTrackingBehavior . NoTracking ;
var payouts = await GetPayoutsForPaymentMethod ( walletId . GetPaymentMethodId ( ) , ctx , payoutIds , storeId , cancellationToken ) ;
var walletSend = ( WalletSendModel ) ( ( ViewResult ) ( await this . WalletSend ( walletId ) ) ) . Model ;
walletSend . Outputs . Clear ( ) ;
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
List < string > bip21 = new List < string > ( ) ;
2021-07-16 09:57:37 +02:00
2021-04-13 10:36:49 +02:00
foreach ( var payout in payouts )
2020-06-24 03:34:09 +02:00
{
2021-07-16 09:57:37 +02:00
if ( payout . Proof ! = null )
{
2021-04-13 10:36:49 +02:00
continue ;
2021-07-16 09:57:37 +02:00
}
var blob = payout . GetBlob ( _jsonSerializerSettings ) ;
2021-07-30 11:47:02 +02:00
bip21 . Add ( network . GenerateBIP21 ( payout . Destination , new Money ( blob . CryptoAmount . Value , MoneyUnit . BTC ) ) . ToString ( ) ) ;
2021-04-13 10:36:49 +02:00
}
2021-07-16 09:57:37 +02:00
if ( bip21 . Any ( ) )
return RedirectToAction ( nameof ( WalletSend ) , new { walletId , bip21 } ) ;
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Message = "There were no payouts eligible to pay from the selection. You may have selected payouts which have detected a transaction to the payout address with the payout amount that you need to accept or reject as the payout."
} ) ;
return RedirectToAction ( nameof ( Payouts ) , new
{
walletId = walletId . ToString ( ) ,
pullPaymentId = vm . PullPaymentId
} ) ;
2020-06-24 03:34:09 +02:00
}
2021-04-13 10:36:49 +02:00
2021-06-10 11:43:45 +02:00
case "mark-paid" :
{
await using var ctx = this . _dbContextFactory . CreateContext ( ) ;
ctx . ChangeTracker . QueryTrackingBehavior = QueryTrackingBehavior . NoTracking ;
var payouts = await GetPayoutsForPaymentMethod ( walletId . GetPaymentMethodId ( ) , ctx , payoutIds , storeId , cancellationToken ) ;
for ( int i = 0 ; i < payouts . Count ; i + + )
{
var payout = payouts [ i ] ;
if ( payout . State ! = PayoutState . AwaitingPayment )
continue ;
var result = await _pullPaymentService . MarkPaid ( new PayoutPaidRequest ( )
{
PayoutId = payout . Id
} ) ;
if ( result ! = PayoutPaidRequest . PayoutPaidResult . Ok )
{
2021-06-30 09:59:01 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
2021-06-10 11:43:45 +02:00
{
Message = PayoutPaidRequest . GetErrorMessage ( result ) ,
Severity = StatusMessageModel . StatusSeverity . Error
} ) ;
return RedirectToAction ( nameof ( Payouts ) , new
{
walletId = walletId . ToString ( ) ,
pullPaymentId = vm . PullPaymentId
} ) ;
}
}
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = "Payouts marked as paid" , Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
return RedirectToAction ( nameof ( Payouts ) ,
new { walletId = walletId . ToString ( ) , pullPaymentId = vm . PullPaymentId } ) ;
}
2021-04-13 10:36:49 +02:00
case "cancel" :
await _pullPaymentService . Cancel (
new HostedServices . PullPaymentHostedService . CancelRequest ( payoutIds ) ) ;
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Message = "Payouts archived" , Severity = StatusMessageModel . StatusSeverity . Success
} ) ;
return RedirectToAction ( nameof ( Payouts ) ,
new { walletId = walletId . ToString ( ) , pullPaymentId = vm . PullPaymentId } ) ;
2020-06-24 03:34:09 +02:00
}
2021-07-16 09:57:37 +02:00
var handler = _payoutHandlers
. FirstOrDefault ( handler = > handler . CanHandle ( paymentMethodId ) ) ;
2021-04-13 10:36:49 +02:00
2021-07-16 09:57:37 +02:00
if ( handler ! = null )
{
var result = await handler . DoSpecificAction ( command , payoutIds , walletId . StoreId ) ;
TempData . SetStatusMessageModel ( result ) ;
return RedirectToAction ( nameof ( Payouts ) , new
{
walletId = walletId . ToString ( ) ,
pullPaymentId = vm . PullPaymentId
} ) ;
}
2021-04-13 10:36:49 +02:00
return NotFound ( ) ;
}
private static async Task < List < PayoutData > > GetPayoutsForPaymentMethod ( PaymentMethodId paymentMethodId ,
ApplicationDbContext ctx , string [ ] payoutIds ,
string storeId , CancellationToken cancellationToken )
{
var payouts = ( await ctx . Payouts
. Include ( p = > p . PullPaymentData )
. Include ( p = > p . PullPaymentData . StoreData )
. Where ( p = > payoutIds . Contains ( p . Id ) )
. Where ( p = > p . PullPaymentData . StoreId = = storeId & & ! p . PullPaymentData . Archived )
. ToListAsync ( cancellationToken ) )
. Where ( p = > p . GetPaymentMethodId ( ) = = paymentMethodId )
. ToList ( ) ;
return payouts ;
2020-06-24 03:34:09 +02:00
}
2021-06-30 09:59:01 +02:00
[HttpGet("{walletId}/payouts")]
2020-06-24 03:34:09 +02:00
public async Task < IActionResult > Payouts (
[ModelBinder(typeof(WalletIdModelBinder))]
2021-06-30 09:59:01 +02:00
WalletId walletId , string pullPaymentId , PayoutState payoutState ,
int skip = 0 , int count = 50 )
2020-06-24 03:34:09 +02:00
{
2021-06-30 09:59:01 +02:00
var vm = this . ParseListQuery ( new PayoutsModel
{
PaymentMethodId = new PaymentMethodId ( walletId . CryptoCode , PaymentTypes . BTCLike ) ,
PullPaymentId = pullPaymentId ,
PayoutState = payoutState ,
Skip = skip ,
Count = count
} ) ;
vm . Payouts = new List < PayoutsModel . PayoutModel > ( ) ;
await using var ctx = _dbContextFactory . CreateContext ( ) ;
2020-06-24 03:34:09 +02:00
var storeId = walletId . StoreId ;
var payoutRequest = ctx . Payouts . Where ( p = > p . PullPaymentData . StoreId = = storeId & & ! p . PullPaymentData . Archived ) ;
if ( vm . PullPaymentId ! = null )
{
payoutRequest = payoutRequest . Where ( p = > p . PullPaymentDataId = = vm . PullPaymentId ) ;
2021-06-30 09:59:01 +02:00
vm . PullPaymentName = ( await ctx . PullPayments . FindAsync ( pullPaymentId ) ) . GetBlob ( ) . Name ;
}
if ( vm . PaymentMethodId ! = null )
{
var pmiStr = vm . PaymentMethodId . ToString ( ) ;
payoutRequest = payoutRequest . Where ( p = > p . PaymentMethodId = = pmiStr ) ;
}
vm . PayoutStateCount = payoutRequest . GroupBy ( data = > data . State )
. Select ( e = > new { e . Key , Count = e . Count ( ) } )
. ToDictionary ( arg = > arg . Key , arg = > arg . Count ) ;
foreach ( PayoutState value in Enum . GetValues ( typeof ( PayoutState ) ) )
{
if ( vm . PayoutStateCount . ContainsKey ( value ) )
continue ;
vm . PayoutStateCount . Add ( value , 0 ) ;
2020-06-24 03:34:09 +02:00
}
2021-06-30 09:59:01 +02:00
vm . PayoutStateCount = vm . PayoutStateCount . OrderBy ( pair = > pair . Key )
. ToDictionary ( pair = > pair . Key , pair = > pair . Value ) ;
payoutRequest = payoutRequest . Where ( p = > p . State = = vm . PayoutState ) ;
vm . Total = await payoutRequest . CountAsync ( ) ;
payoutRequest = payoutRequest . Skip ( vm . Skip ) . Take ( vm . Count ) ;
2020-06-24 03:34:09 +02:00
var payouts = await payoutRequest . OrderByDescending ( p = > p . Date )
2020-06-28 10:55:27 +02:00
. Select ( o = > new
{
2020-06-24 03:34:09 +02:00
Payout = o ,
PullPayment = o . PullPaymentData
} ) . ToListAsync ( ) ;
2021-06-30 09:59:01 +02:00
foreach ( var item in payouts )
2020-06-24 03:34:09 +02:00
{
2021-06-30 09:59:01 +02:00
var ppBlob = item . PullPayment . GetBlob ( ) ;
var payoutBlob = item . Payout . GetBlob ( _jsonSerializerSettings ) ;
var m = new PayoutsModel . PayoutModel
2020-06-24 03:34:09 +02:00
{
2021-06-30 09:59:01 +02:00
PullPaymentId = item . PullPayment . Id ,
PullPaymentName = ppBlob . Name ? ? item . PullPayment . Id ,
Date = item . Payout . Date ,
PayoutId = item . Payout . Id ,
Amount = _currencyTable . DisplayFormatCurrency ( payoutBlob . Amount , ppBlob . Currency ) ,
Destination = payoutBlob . Destination
} ;
var handler = _payoutHandlers
. FirstOrDefault ( handler = > handler . CanHandle ( item . Payout . GetPaymentMethodId ( ) ) ) ;
var proofBlob = handler ? . ParseProof ( item . Payout ) ;
m . ProofLink = proofBlob ? . Link ;
vm . Payouts . Add ( m ) ;
2020-06-24 03:34:09 +02:00
}
return View ( vm ) ;
}
}
}