2022-08-04 05:07:59 +02:00
#nullable enable
2020-06-29 04:44:35 +02:00
using System ;
2018-07-26 15:32:24 +02:00
using System.Collections.Generic ;
using System.Globalization ;
using System.Linq ;
2022-05-20 02:35:31 +02:00
using System.Net.Mime ;
2018-07-26 15:32:24 +02:00
using System.Threading ;
using System.Threading.Tasks ;
2020-11-17 13:46:23 +01:00
using BTCPayServer.Abstractions.Constants ;
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2021-12-31 08:59:02 +01:00
using BTCPayServer.BIP78.Sender ;
2020-03-19 11:11:15 +01:00
using BTCPayServer.Client ;
2018-07-26 15:32:24 +02:00
using BTCPayServer.Data ;
using BTCPayServer.HostedServices ;
using BTCPayServer.ModelBinders ;
using BTCPayServer.Models ;
using BTCPayServer.Models.WalletViewModels ;
2020-01-18 06:12:27 +01:00
using BTCPayServer.Payments ;
2021-12-31 08:59:02 +01:00
using BTCPayServer.Payments.PayJoin ;
2018-07-26 15:32:24 +02:00
using BTCPayServer.Services ;
2020-04-28 09:53:34 +02:00
using BTCPayServer.Services.Labels ;
2018-07-26 16:23:28 +02:00
using BTCPayServer.Services.Rates ;
2018-07-26 15:32:24 +02:00
using BTCPayServer.Services.Stores ;
using BTCPayServer.Services.Wallets ;
2022-04-12 09:55:10 +02:00
using Dapper ;
2018-07-26 15:32:24 +02:00
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
2022-04-12 09:55:10 +02:00
using Microsoft.Extensions.DependencyInjection ;
2018-07-26 15:32:24 +02:00
using NBitcoin ;
2022-04-24 05:19:34 +02:00
using BTCPayServer.Client.Models ;
using BTCPayServer.Logging ;
2022-05-20 02:35:31 +02:00
using BTCPayServer.Services.Wallets.Export ;
2022-07-04 06:20:08 +02:00
using Microsoft.AspNetCore.Http ;
2020-01-21 09:33:12 +01:00
using NBXplorer ;
2022-04-12 09:55:10 +02:00
using NBXplorer.Client ;
2018-07-26 15:32:24 +02:00
using NBXplorer.DerivationStrategy ;
2018-10-26 16:07:39 +02:00
using NBXplorer.Models ;
2018-07-26 15:32:24 +02:00
using Newtonsoft.Json ;
2021-03-11 13:34:52 +01:00
using StoreData = BTCPayServer . Data . StoreData ;
2022-10-11 10:34:29 +02:00
using Microsoft.AspNetCore.Routing ;
using Newtonsoft.Json.Linq ;
2018-07-26 15:32:24 +02:00
namespace BTCPayServer.Controllers
{
[Route("wallets")]
2020-03-20 05:41:47 +01:00
[Authorize(Policy = Policies.CanModifyStoreSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2018-07-26 15:32:24 +02:00
[AutoValidateAntiforgeryToken]
2022-01-07 04:32:00 +01:00
public partial class UIWalletsController : Controller
2018-07-26 15:32:24 +02:00
{
2021-11-11 06:30:19 +01:00
private StoreRepository Repository { get ; }
private WalletRepository WalletRepository { get ; }
private BTCPayNetworkProvider NetworkProvider { get ; }
private ExplorerClientProvider ExplorerClientProvider { get ; }
2022-04-12 09:55:10 +02:00
public IServiceProvider ServiceProvider { get ; }
2021-11-11 06:30:19 +01:00
public RateFetcher RateFetcher { get ; }
2018-10-09 16:48:14 +02:00
2018-07-26 15:32:24 +02:00
private readonly UserManager < ApplicationUser > _userManager ;
private readonly NBXplorerDashboard _dashboard ;
2019-10-12 13:35:30 +02:00
private readonly IAuthorizationService _authorizationService ;
2018-07-26 15:32:24 +02:00
private readonly IFeeProviderFactory _feeRateProvider ;
private readonly BTCPayWalletProvider _walletProvider ;
2021-03-11 13:34:52 +01:00
private readonly WalletReceiveService _walletReceiveService ;
2020-01-21 09:33:12 +01:00
private readonly SettingsRepository _settingsRepository ;
2020-03-29 17:28:22 +02:00
private readonly DelayedTransactionBroadcaster _broadcaster ;
private readonly PayjoinClient _payjoinClient ;
2022-10-11 10:34:29 +02:00
private readonly LinkGenerator _linkGenerator ;
2022-04-24 05:19:34 +02:00
private readonly PullPaymentHostedService _pullPaymentHostedService ;
2022-07-23 13:26:13 +02:00
private readonly UTXOLocker _utxoLocker ;
2022-04-12 09:55:10 +02:00
private readonly WalletHistogramService _walletHistogramService ;
2020-05-24 21:55:28 +02:00
2020-06-29 05:07:48 +02:00
readonly CurrencyNameTable _currencyTable ;
2022-04-24 05:19:34 +02:00
2022-01-07 04:32:00 +01:00
public UIWalletsController ( StoreRepository repo ,
2019-08-02 17:42:30 +02:00
WalletRepository walletRepository ,
2018-07-26 16:23:28 +02:00
CurrencyNameTable currencyTable ,
2018-07-26 15:32:24 +02:00
BTCPayNetworkProvider networkProvider ,
UserManager < ApplicationUser > userManager ,
NBXplorerDashboard dashboard ,
2022-04-12 09:55:10 +02:00
WalletHistogramService walletHistogramService ,
2018-08-22 09:53:40 +02:00
RateFetcher rateProvider ,
2019-10-12 13:35:30 +02:00
IAuthorizationService authorizationService ,
2018-07-26 15:32:24 +02:00
ExplorerClientProvider explorerProvider ,
IFeeProviderFactory feeRateProvider ,
2020-01-18 06:12:27 +01:00
BTCPayWalletProvider walletProvider ,
2021-03-11 13:34:52 +01:00
WalletReceiveService walletReceiveService ,
2020-01-06 13:57:32 +01:00
SettingsRepository settingsRepository ,
2020-03-29 17:28:22 +02:00
DelayedTransactionBroadcaster broadcaster ,
2020-04-28 09:53:34 +02:00
PayjoinClient payjoinClient ,
2022-04-24 05:19:34 +02:00
IServiceProvider serviceProvider ,
2022-07-23 13:26:13 +02:00
PullPaymentHostedService pullPaymentHostedService ,
2022-10-11 10:34:29 +02:00
UTXOLocker utxoLocker ,
LinkGenerator linkGenerator )
2018-07-26 15:32:24 +02:00
{
2018-07-26 16:23:28 +02:00
_currencyTable = currencyTable ;
2022-10-11 10:34:29 +02:00
_linkGenerator = linkGenerator ;
2018-10-09 16:48:14 +02:00
Repository = repo ;
2019-08-02 17:42:30 +02:00
WalletRepository = walletRepository ;
2018-10-09 16:48:14 +02:00
RateFetcher = rateProvider ;
2019-10-12 13:35:30 +02:00
_authorizationService = authorizationService ;
2018-10-09 16:48:14 +02:00
NetworkProvider = networkProvider ;
2018-07-26 15:32:24 +02:00
_userManager = userManager ;
_dashboard = dashboard ;
2018-10-09 16:48:14 +02:00
ExplorerClientProvider = explorerProvider ;
2018-07-26 15:32:24 +02:00
_feeRateProvider = feeRateProvider ;
_walletProvider = walletProvider ;
2021-03-11 13:34:52 +01:00
_walletReceiveService = walletReceiveService ;
2020-01-21 09:33:12 +01:00
_settingsRepository = settingsRepository ;
2020-03-29 17:28:22 +02:00
_broadcaster = broadcaster ;
_payjoinClient = payjoinClient ;
2022-04-24 05:19:34 +02:00
_pullPaymentHostedService = pullPaymentHostedService ;
2022-07-23 13:26:13 +02:00
_utxoLocker = utxoLocker ;
2022-04-12 09:55:10 +02:00
ServiceProvider = serviceProvider ;
_walletHistogramService = walletHistogramService ;
2018-07-26 15:32:24 +02:00
}
2022-04-24 05:19:34 +02:00
2019-08-02 17:42:30 +02:00
[HttpPost]
[Route("{walletId}")]
public async Task < IActionResult > ModifyTransaction (
// We need addlabel and addlabelclick. addlabel is the + button if the label does not exists,
// addlabelclick is if the user click on existing label. For some reason, reusing the same name attribute for both
// does not work
[ModelBinder(typeof(WalletIdModelBinder))]
2020-05-24 23:27:01 +02:00
WalletId walletId , string transactionId ,
2022-08-04 05:07:59 +02:00
string? addlabel = null ,
string? addlabelclick = null ,
string? addcomment = null ,
string? removelabel = null )
2019-08-02 17:42:30 +02:00
{
addlabel = addlabel ? ? addlabelclick ;
2019-08-03 05:41:12 +02:00
// Hack necessary when the user enter a empty comment and submit.
// For some reason asp.net consider addcomment null instead of empty string...
try
2019-08-02 17:55:27 +02:00
{
2019-08-03 16:02:15 +02:00
if ( addcomment = = null & & Request ? . Form ? . TryGetValue ( nameof ( addcomment ) , out _ ) is true )
2019-08-03 05:41:12 +02:00
{
addcomment = string . Empty ;
}
2019-08-02 17:55:27 +02:00
}
2019-08-03 05:41:12 +02:00
catch { }
/////////
2020-05-24 23:27:01 +02:00
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2019-08-02 17:42:30 +02:00
if ( paymentMethod = = null )
return NotFound ( ) ;
2022-10-11 10:34:29 +02:00
var txObjId = new WalletObjectId ( walletId , WalletObjectData . Types . Tx , transactionId ) ;
2019-08-02 17:42:30 +02:00
var wallet = _walletProvider . GetWallet ( paymentMethod . Network ) ;
if ( addlabel ! = null )
{
2022-10-11 10:34:29 +02:00
await WalletRepository . AddWalletObjectLabels ( txObjId , addlabel ) ;
2019-08-02 17:42:30 +02:00
}
else if ( removelabel ! = null )
{
2022-10-11 10:34:29 +02:00
await WalletRepository . RemoveWalletObjectLabels ( txObjId , removelabel ) ;
2019-08-02 17:42:30 +02:00
}
else if ( addcomment ! = null )
{
2022-10-11 10:34:29 +02:00
await WalletRepository . SetWalletObjectComment ( txObjId , addcomment ) ;
2019-08-02 17:42:30 +02:00
}
return RedirectToAction ( nameof ( WalletTransactions ) , new { walletId = walletId . ToString ( ) } ) ;
}
2019-10-12 13:35:30 +02:00
[HttpGet]
[AllowAnonymous]
2018-07-26 15:32:24 +02:00
public async Task < IActionResult > ListWallets ( )
{
2019-10-12 13:35:30 +02:00
if ( GetUserId ( ) = = null )
{
return Challenge ( AuthenticationSchemes . Cookie ) ;
}
2018-07-26 15:32:24 +02:00
var wallets = new ListWalletsViewModel ( ) ;
2018-10-09 16:48:14 +02:00
var stores = await Repository . GetStoresByUserId ( GetUserId ( ) ) ;
2018-07-26 15:32:24 +02:00
var onChainWallets = stores
2022-04-24 05:19:34 +02:00
. SelectMany ( s = > s . GetSupportedPaymentMethods ( NetworkProvider )
. OfType < DerivationSchemeSettings > ( )
. Select ( d = > ( ( Wallet : _walletProvider . GetWallet ( d . Network ) ,
DerivationStrategy : d . AccountDerivation ,
Network : d . Network ) ) )
. Where ( _ = > _ . Wallet ! = null & & _ . Network . WalletSupported )
. Select ( _ = > ( Wallet : _ . Wallet ,
Store : s ,
Balance : GetBalanceString ( _ . Wallet , _ . DerivationStrategy ) ,
DerivationStrategy : _ . DerivationStrategy ,
Network : _ . Network ) ) )
. ToList ( ) ;
2018-07-26 15:32:24 +02:00
foreach ( var wallet in onChainWallets )
{
ListWalletsViewModel . WalletViewModel walletVm = new ListWalletsViewModel . WalletViewModel ( ) ;
wallets . Wallets . Add ( walletVm ) ;
walletVm . Balance = await wallet . Balance + " " + wallet . Wallet . Network . CryptoCode ;
2019-10-12 13:35:30 +02:00
walletVm . IsOwner = wallet . Store . Role = = StoreRoles . Owner ;
if ( ! walletVm . IsOwner )
2018-07-26 15:32:24 +02:00
{
walletVm . Balance = "" ;
}
2022-04-24 05:19:34 +02:00
2018-07-26 15:32:24 +02:00
walletVm . CryptoCode = wallet . Network . CryptoCode ;
walletVm . StoreId = wallet . Store . Id ;
walletVm . Id = new WalletId ( wallet . Store . Id , wallet . Network . CryptoCode ) ;
walletVm . StoreName = wallet . Store . StoreName ;
2021-09-15 16:52:57 +02:00
2021-09-29 11:52:17 +02:00
var money = await GetBalanceAsMoney ( wallet . Wallet , wallet . DerivationStrategy ) ;
wallets . BalanceForCryptoCode [ wallet . Network ] = wallets . BalanceForCryptoCode . ContainsKey ( wallet . Network )
? wallets . BalanceForCryptoCode [ wallet . Network ] . Add ( money )
: money ;
2018-07-26 15:32:24 +02:00
}
return View ( wallets ) ;
}
2021-11-11 06:30:19 +01:00
[HttpGet("{walletId}")]
[HttpGet("{walletId}/transactions")]
2018-07-26 17:08:07 +02:00
public async Task < IActionResult > WalletTransactions (
2018-07-26 15:32:24 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
2021-12-31 08:59:02 +01:00
WalletId walletId ,
2022-08-04 05:07:59 +02:00
string? labelFilter = null ,
2021-12-31 08:59:02 +01:00
int skip = 0 ,
2020-07-25 06:33:02 +02:00
int count = 50
)
2018-07-26 15:32:24 +02:00
{
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2018-07-26 17:08:07 +02:00
if ( paymentMethod = = null )
2018-07-26 15:32:24 +02:00
return NotFound ( ) ;
2018-07-26 17:08:07 +02:00
var wallet = _walletProvider . GetWallet ( paymentMethod . Network ) ;
2022-07-06 06:00:23 +02:00
// We can't filter at the database level if we need to apply label filter
var preFiltering = string . IsNullOrEmpty ( labelFilter ) ;
var transactions = await wallet . FetchTransactionHistory ( paymentMethod . AccountDerivation , preFiltering ? skip : null , preFiltering ? count : null ) ;
2022-10-11 10:34:29 +02:00
var walletTransactionsInfo = await WalletRepository . GetWalletTransactionsInfo ( walletId , transactions . Select ( t = > t . TransactionId . ToString ( ) ) . ToArray ( ) ) ;
2022-05-02 09:35:28 +02:00
var model = new ListTransactionsViewModel { Skip = skip , Count = count } ;
2022-10-11 10:34:29 +02:00
model . Labels . AddRange (
( await WalletRepository . GetWalletLabels ( walletId ) )
. Select ( c = > ( c . Label , c . Color , ColorPalette . Default . TextColor ( c . Color ) ) )
) ;
2020-12-12 07:21:37 +01:00
if ( labelFilter ! = null )
{
2022-04-24 05:19:34 +02:00
model . PaginationQuery = new Dictionary < string , object > { { "labelFilter" , labelFilter } } ;
2020-12-12 07:21:37 +01:00
}
2019-12-29 17:08:30 +01:00
if ( transactions = = null )
2018-07-26 17:08:07 +02:00
{
2019-12-29 17:08:30 +01:00
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Message =
"There was an error retrieving the transactions list. Is NBXplorer configured correctly?"
} ) ;
model . Transactions = new List < ListTransactionsViewModel . TransactionViewModel > ( ) ;
}
else
{
2022-07-04 08:50:56 +02:00
foreach ( var tx in transactions )
2019-08-02 17:42:30 +02:00
{
2019-12-29 17:08:30 +01:00
var vm = new ListTransactionsViewModel . TransactionViewModel ( ) ;
vm . Id = tx . TransactionId . ToString ( ) ;
vm . Link = string . Format ( CultureInfo . InvariantCulture , paymentMethod . Network . BlockExplorerLink ,
vm . Id ) ;
2022-07-04 08:50:56 +02:00
vm . Timestamp = tx . SeenAt ;
2019-12-29 17:08:30 +01:00
vm . Positive = tx . BalanceChange . GetValue ( wallet . Network ) > = 0 ;
2020-05-03 18:04:34 +02:00
vm . Balance = tx . BalanceChange . ShowMoney ( wallet . Network ) ;
2019-12-29 17:08:30 +01:00
vm . IsConfirmed = tx . Confirmations ! = 0 ;
if ( walletTransactionsInfo . TryGetValue ( tx . TransactionId . ToString ( ) , out var transactionInfo ) )
{
2022-10-11 10:34:29 +02:00
var labels = CreateTransactionTagModels ( transactionInfo ) ;
vm . Tags . AddRange ( labels ) ;
2019-12-29 17:08:30 +01:00
vm . Comment = transactionInfo . Comment ;
}
if ( labelFilter = = null | |
2022-10-11 10:34:29 +02:00
vm . Tags . Any ( l = > l . Text . Equals ( labelFilter , StringComparison . OrdinalIgnoreCase ) ) )
2019-12-29 17:08:30 +01:00
model . Transactions . Add ( vm ) ;
2019-08-02 17:42:30 +02:00
}
2019-08-03 16:10:45 +02:00
2022-07-15 05:35:57 +02:00
model . Total = preFiltering ? null : model . Transactions . Count ;
2022-09-22 03:39:48 +02:00
// if we couldn't filter at the db level, we need to apply skip and count
if ( ! preFiltering )
{
2022-09-27 14:24:53 +02:00
model . Transactions = model . Transactions . Skip ( skip ) . Take ( count ) . ToList ( ) ;
2022-09-22 03:39:48 +02:00
}
2018-07-26 17:08:07 +02:00
}
2019-12-29 17:08:30 +01:00
2021-04-08 15:32:42 +02:00
model . CryptoCode = walletId . CryptoCode ;
2022-09-27 14:24:53 +02:00
//If ajax call then load the partial view
return Request . Headers [ "X-Requested-With" ] = = "XMLHttpRequest"
? PartialView ( "_WalletTransactionsList" , model )
: View ( model ) ;
2018-07-26 17:08:07 +02:00
}
2022-04-12 09:55:10 +02:00
[HttpGet("{walletId}/histogram/{type}")]
public async Task < IActionResult > WalletHistogram (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId , WalletHistogramType type )
{
var store = GetCurrentStore ( ) ;
var data = await _walletHistogramService . GetHistogram ( store , walletId , type ) ;
return data = = null
? NotFound ( )
: Json ( data ) ;
}
2018-07-26 15:32:24 +02:00
2021-11-11 06:30:19 +01:00
[HttpGet("{walletId}/receive")]
2022-07-04 06:20:08 +02:00
public IActionResult WalletReceive ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ] WalletId walletId ,
2022-08-04 05:07:59 +02:00
[FromQuery] string? returnUrl = null )
2020-01-18 06:12:27 +01:00
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2020-01-18 06:12:27 +01:00
if ( paymentMethod = = null )
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2020-01-18 06:12:27 +01:00
if ( network = = null )
return NotFound ( ) ;
2021-12-20 15:15:32 +01:00
var store = GetCurrentStore ( ) ;
2021-03-11 13:34:52 +01:00
var address = _walletReceiveService . Get ( walletId ) ? . Address ;
2021-12-20 15:15:32 +01:00
var allowedPayjoin = paymentMethod . IsHotWallet & & store . GetStoreBlob ( ) . PayJoinEnabled ;
2021-07-30 11:47:02 +02:00
var bip21 = network . GenerateBIP21 ( address ? . ToString ( ) , null ) ;
2021-04-13 05:26:36 +02:00
if ( allowedPayjoin )
{
2022-04-24 05:19:34 +02:00
bip21 . QueryParams . Add ( PayjoinClient . BIP21EndpointKey ,
Request . GetAbsoluteUri ( Url . Action ( nameof ( PayJoinEndpointController . Submit ) , "PayJoinEndpoint" ,
new { walletId . CryptoCode } ) ) ) ;
2021-04-13 05:26:36 +02:00
}
2022-07-04 06:20:08 +02:00
return View ( new WalletReceiveViewModel
2020-01-18 06:12:27 +01:00
{
CryptoCode = walletId . CryptoCode ,
Address = address ? . ToString ( ) ,
2021-04-13 05:26:36 +02:00
CryptoImage = GetImage ( paymentMethod . PaymentId , network ) ,
2022-07-04 06:20:08 +02:00
PaymentLink = bip21 . ToString ( ) ,
ReturnUrl = returnUrl ? ? HttpContext . Request . GetTypedHeaders ( ) . Referer ? . AbsolutePath
2020-01-18 06:12:27 +01:00
} ) ;
}
2022-07-04 06:20:08 +02:00
[HttpPost("{walletId}/receive")]
2022-04-24 05:19:34 +02:00
public async Task < IActionResult > WalletReceive ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ] WalletId walletId ,
2022-07-04 06:20:08 +02:00
WalletReceiveViewModel vm , string command )
2020-01-18 06:12:27 +01:00
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2020-01-18 06:12:27 +01:00
if ( paymentMethod = = null )
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2020-01-18 06:12:27 +01:00
if ( network = = null )
return NotFound ( ) ;
switch ( command )
{
case "unreserve-current-address" :
2021-03-11 13:34:52 +01:00
var address = await _walletReceiveService . UnReserveAddress ( walletId ) ;
if ( ! string . IsNullOrEmpty ( address ) )
2020-01-18 06:12:27 +01:00
{
2022-07-04 06:20:08 +02:00
TempData . SetStatusMessageModel ( new StatusMessageModel
2021-03-11 13:34:52 +01:00
{
AllowDismiss = true ,
Message = $"Address {address} was unreserved." ,
Severity = StatusMessageModel . StatusSeverity . Success ,
} ) ;
2020-01-18 06:12:27 +01:00
}
break ;
case "generate-new-address" :
2021-03-11 13:34:52 +01:00
await _walletReceiveService . GetOrGenerate ( walletId , true ) ;
2020-01-18 06:12:27 +01:00
break ;
2022-04-12 09:55:10 +02:00
case "fill-wallet" :
var cheater = ServiceProvider . GetService < Cheater > ( ) ;
if ( cheater ! = null )
await SendFreeMoney ( cheater , walletId , paymentMethod ) ;
break ;
2020-01-18 06:12:27 +01:00
}
2022-07-04 06:20:08 +02:00
return RedirectToAction ( nameof ( WalletReceive ) , new { walletId , returnUrl = vm . ReturnUrl } ) ;
2020-01-18 06:12:27 +01:00
}
2022-04-12 09:55:10 +02:00
private async Task SendFreeMoney ( Cheater cheater , WalletId walletId , DerivationSchemeSettings paymentMethod )
{
var c = this . ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode ) ;
var addresses = Enumerable . Range ( 0 , 200 ) . Select ( _ = > c . GetUnusedAsync ( paymentMethod . AccountDerivation , DerivationFeature . Deposit , reserve : true ) ) . ToArray ( ) ;
await Task . WhenAll ( addresses ) ;
await cheater . CashCow . GenerateAsync ( addresses . Length / 8 ) ;
var b = cheater . CashCow . PrepareBatch ( ) ;
Random r = new Random ( ) ;
List < Task < uint256 > > sending = new List < Task < uint256 > > ( ) ;
foreach ( var a in addresses )
{
sending . Add ( b . SendToAddressAsync ( ( await a ) . Address , Money . Coins ( 0.1 m ) + Money . Satoshis ( r . Next ( 0 , 90_000_000 ) ) ) ) ;
}
await b . SendBatchAsync ( ) ;
await cheater . CashCow . GenerateAsync ( 1 ) ;
2022-08-04 05:07:59 +02:00
var factory = ServiceProvider . GetRequiredService < NBXplorerConnectionFactory > ( ) ;
2022-04-12 09:55:10 +02:00
// Wait it sync...
await Task . Delay ( 1000 ) ;
await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode ) . WaitServerStartedAsync ( ) ;
await Task . Delay ( 1000 ) ;
await using var conn = await factory . OpenConnection ( ) ;
var wallet_id = paymentMethod . GetNBXWalletId ( ) ;
var txIds = sending . Select ( s = > s . Result . ToString ( ) ) . ToArray ( ) ;
await conn . ExecuteAsync (
"UPDATE txs t SET seen_at=(NOW() - (random() * (interval '90 days'))) " +
"FROM unnest(@txIds) AS r (tx_id) WHERE r.tx_id=t.tx_id;" , new { txIds } ) ;
await Task . Delay ( 1000 ) ;
await conn . ExecuteAsync ( "REFRESH MATERIALIZED VIEW wallets_history;" ) ;
}
2020-01-21 09:33:12 +01:00
private async Task < bool > CanUseHotWallet ( )
{
var policies = await _settingsRepository . GetSettingAsync < PoliciesSettings > ( ) ;
2021-03-11 13:34:52 +01:00
return ( await _authorizationService . CanUseHotWallet ( policies , User ) ) . HotWallet ;
2020-01-21 09:33:12 +01:00
}
2020-05-24 23:27:01 +02:00
2021-06-14 07:06:56 +02:00
[HttpGet("{walletId}/send")]
2018-07-26 17:08:07 +02:00
public async Task < IActionResult > WalletSend (
2022-07-04 06:20:08 +02:00
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId ,
2022-08-04 05:07:59 +02:00
string? defaultDestination = null , string? defaultAmount = null , string [ ] ? bip21 = null ,
[FromQuery] string? returnUrl = null )
2018-07-26 17:08:07 +02:00
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2018-10-09 16:48:14 +02:00
var store = await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ;
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
if ( paymentMethod = = null | | store is null )
2018-07-26 15:32:24 +02:00
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var network = this . NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-12-29 17:08:30 +01:00
if ( network = = null | | network . ReadonlyWallet )
2018-10-31 16:19:25 +01:00
return NotFound ( ) ;
2018-07-26 16:23:28 +02:00
var storeData = store . GetStoreBlob ( ) ;
2018-10-09 16:48:14 +02:00
var rateRules = store . GetStoreBlob ( ) . GetRateRules ( NetworkProvider ) ;
2018-08-01 11:38:46 +02:00
rateRules . Spread = 0.0 m ;
2021-10-20 16:17:40 +02:00
var currencyPair = new Rating . CurrencyPair ( paymentMethod . PaymentId . CryptoCode , storeData . DefaultCurrency ) ;
2019-05-21 10:10:07 +02:00
double . TryParse ( defaultAmount , out var amount ) ;
2022-07-04 06:20:08 +02:00
var model = new WalletSendModel
{
CryptoCode = walletId . CryptoCode ,
ReturnUrl = returnUrl ? ? HttpContext . Request . GetTypedHeaders ( ) . Referer ? . AbsolutePath
} ;
2021-04-13 10:36:49 +02:00
if ( bip21 ? . Any ( ) is true )
{
foreach ( var link in bip21 )
2019-05-21 10:10:07 +02:00
{
2021-04-13 10:36:49 +02:00
if ( ! string . IsNullOrEmpty ( link ) )
2019-05-21 10:10:07 +02:00
{
2021-04-13 10:36:49 +02:00
LoadFromBIP21 ( model , link , network ) ;
2019-05-21 10:10:07 +02:00
}
2021-04-13 10:36:49 +02:00
}
}
if ( ! ( model . Outputs ? . Any ( ) is true ) )
2020-05-24 23:27:01 +02:00
{
2021-04-13 10:36:49 +02:00
model . Outputs = new List < WalletSendModel . TransactionOutput > ( )
{
new WalletSendModel . TransactionOutput ( )
{
Amount = Convert . ToDecimal ( amount ) , DestinationAddress = defaultDestination
}
} ;
2020-05-24 23:27:01 +02:00
}
var feeProvider = _feeRateProvider . CreateFeeProvider ( network ) ;
2020-05-07 22:34:39 +02:00
var recommendedFees =
new [ ]
{
TimeSpan . FromMinutes ( 10.0 ) , TimeSpan . FromMinutes ( 60.0 ) , TimeSpan . FromHours ( 6.0 ) ,
TimeSpan . FromHours ( 24.0 ) ,
} . Select ( async time = >
{
try
{
var result = await feeProvider . GetFeeRateAsync (
( int ) network . NBitcoinNetwork . Consensus . GetExpectedBlocksFor ( time ) ) ;
2022-04-24 05:19:34 +02:00
return new WalletSendModel . FeeRateOption ( )
{
Target = time , FeeRate = result . SatoshiPerByte
} ;
2020-05-07 22:34:39 +02:00
}
catch ( Exception )
{
return null ;
}
} )
. ToArray ( ) ;
2019-05-08 16:39:11 +02:00
var balance = _walletProvider . GetWallet ( network ) . GetBalance ( paymentMethod . AccountDerivation ) ;
2021-03-11 13:29:00 +01:00
model . NBXSeedAvailable = await GetSeed ( walletId , network ) ! = null ;
2021-12-31 08:59:02 +01:00
var Balance = await balance ;
2021-08-01 14:12:00 +02:00
model . CurrentBalance = ( Balance . Available ? ? Balance . Total ) . GetValue ( network ) ;
if ( Balance . Immature is null )
model . ImmatureBalance = 0 ;
else
model . ImmatureBalance = Balance . Immature . GetValue ( network ) ;
2021-12-31 08:59:02 +01:00
2020-05-07 22:34:39 +02:00
await Task . WhenAll ( recommendedFees ) ;
model . RecommendedSatoshiPerByte =
recommendedFees . Select ( tuple = > tuple . Result ) . Where ( option = > option ! = null ) . ToList ( ) ;
2020-05-05 12:06:59 +02:00
2020-05-07 22:34:39 +02:00
model . FeeSatoshiPerByte = model . RecommendedSatoshiPerByte . LastOrDefault ( ) ? . FeeRate ;
2019-05-08 08:24:20 +02:00
model . SupportRBF = network . SupportRBF ;
2020-09-11 09:23:08 +02:00
model . CryptoDivisibility = network . Divisibility ;
2018-07-26 16:23:28 +02:00
using ( CancellationTokenSource cts = new CancellationTokenSource ( ) )
{
try
{
cts . CancelAfter ( TimeSpan . FromSeconds ( 5 ) ) ;
2022-04-24 05:19:34 +02:00
var result = await RateFetcher . FetchRate ( currencyPair , rateRules , cts . Token )
. WithCancellation ( cts . Token ) ;
2018-07-27 11:04:41 +02:00
if ( result . BidAsk ! = null )
2018-07-26 16:23:28 +02:00
{
2018-07-27 11:04:41 +02:00
model . Rate = result . BidAsk . Center ;
2022-04-24 05:19:34 +02:00
model . FiatDivisibility = _currencyTable . GetNumberFormatInfo ( currencyPair . Right , true )
. CurrencyDecimalDigits ;
2018-07-26 16:23:28 +02:00
model . Fiat = currencyPair . Right ;
}
2018-07-26 17:32:09 +02:00
else
{
2022-04-24 05:19:34 +02:00
model . RateError =
$"{result.EvaluatedRule} ({string.Join(" , ", result.Errors.OfType<object>().ToArray())})" ;
2018-07-26 17:32:09 +02:00
}
2018-07-26 16:23:28 +02:00
}
2018-10-09 16:48:14 +02:00
catch ( Exception ex ) { model . RateError = ex . Message ; }
2018-07-26 16:23:28 +02:00
}
2022-04-24 05:19:34 +02:00
2018-07-26 15:32:24 +02:00
return View ( model ) ;
}
2020-05-24 23:27:01 +02:00
2022-08-04 05:07:59 +02:00
private async Task < string? > GetSeed ( WalletId walletId , BTCPayNetwork network )
2021-03-11 13:29:00 +01:00
{
return await CanUseHotWallet ( ) & &
2022-04-24 05:19:34 +02:00
GetDerivationSchemeSettings ( walletId ) is DerivationSchemeSettings s & &
s . IsHotWallet & &
ExplorerClientProvider . GetExplorerClient ( network ) is ExplorerClient client & &
await client . GetMetadataAsync < string > ( s . AccountDerivation , WellknownMetadataKeys . MasterHDKey ) is
string seed & &
! string . IsNullOrEmpty ( seed )
? seed
: null ;
2021-03-11 13:29:00 +01:00
}
2018-07-26 15:32:24 +02:00
2021-06-14 07:06:56 +02:00
[HttpPost("{walletId}/send")]
2018-10-31 16:19:25 +01:00
public async Task < IActionResult > WalletSend (
[ModelBinder(typeof(WalletIdModelBinder))]
2022-04-24 05:19:34 +02:00
WalletId walletId , WalletSendModel vm , string command = "" , CancellationToken cancellation = default ,
2022-08-04 05:42:15 +02:00
string? bip21 = "" )
2018-10-31 16:19:25 +01:00
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
var store = await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ;
if ( store = = null )
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-12-29 17:08:30 +01:00
if ( network = = null | | network . ReadonlyWallet )
2018-10-31 16:19:25 +01:00
return NotFound ( ) ;
2022-07-04 06:20:08 +02:00
2019-05-08 08:24:20 +02:00
vm . SupportRBF = network . SupportRBF ;
2021-03-11 13:29:00 +01:00
vm . NBXSeedAvailable = await GetSeed ( walletId , network ) ! = null ;
2020-02-13 09:18:43 +01:00
if ( ! string . IsNullOrEmpty ( bip21 ) )
{
2021-04-13 10:36:49 +02:00
vm . Outputs ? . Clear ( ) ;
2020-02-13 09:18:43 +01:00
LoadFromBIP21 ( vm , bip21 , network ) ;
}
2020-05-24 23:27:01 +02:00
decimal transactionAmountSum = 0 ;
2020-03-19 09:44:47 +01:00
if ( command = = "toggle-input-selection" )
{
2020-05-24 23:27:01 +02:00
vm . InputSelection = ! vm . InputSelection ;
2020-03-19 09:44:47 +01:00
}
if ( vm . InputSelection )
{
var schemeSettings = GetDerivationSchemeSettings ( walletId ) ;
2022-08-04 05:07:59 +02:00
if ( schemeSettings is null )
return NotFound ( ) ;
2020-03-19 09:44:47 +01:00
2022-04-24 05:19:34 +02:00
var utxos = await _walletProvider . GetWallet ( network )
2022-06-10 05:58:51 +02:00
. GetUnspentCoins ( schemeSettings . AccountDerivation , false , cancellation ) ;
2022-10-11 10:34:29 +02:00
2022-12-01 01:54:55 +01:00
var walletTransactionsInfoAsync = await this . WalletRepository . GetWalletTransactionsInfo ( walletId ,
utxos . SelectMany ( u = > GetWalletObjectsQuery . Get ( u ) ) . Distinct ( ) . ToArray ( ) ) ;
2020-03-19 09:44:47 +01:00
vm . InputsAvailable = utxos . Select ( coin = >
{
walletTransactionsInfoAsync . TryGetValue ( coin . OutPoint . Hash . ToString ( ) , out var info ) ;
2022-12-01 01:54:55 +01:00
walletTransactionsInfoAsync . TryGetValue ( coin . ScriptPubKey . ToHex ( ) , out var info2 ) ;
if ( info is not null & & info2 is not null )
{
info . Merge ( info2 ) ;
}
info ? ? = info2 ;
2020-03-19 09:44:47 +01:00
return new WalletSendModel . InputSelectionOption ( )
{
Outpoint = coin . OutPoint . ToString ( ) ,
Amount = coin . Value . GetValue ( network ) ,
Comment = info ? . Comment ,
2022-12-01 01:54:55 +01:00
Labels = CreateTransactionTagModels ( info ) ,
2022-04-24 05:19:34 +02:00
Link = string . Format ( CultureInfo . InvariantCulture , network . BlockExplorerLink ,
coin . OutPoint . Hash . ToString ( ) ) ,
2021-04-20 04:02:06 +02:00
Confirmations = coin . Confirmations
2020-03-19 09:44:47 +01:00
} ;
} ) . ToArray ( ) ;
}
if ( command = = "toggle-input-selection" )
{
ModelState . Clear ( ) ;
return View ( vm ) ;
}
2022-08-04 05:07:59 +02:00
vm . Outputs ? ? = new ( ) ;
2020-04-27 12:12:01 +02:00
if ( ! string . IsNullOrEmpty ( bip21 ) )
{
2021-04-13 10:36:49 +02:00
if ( ! vm . Outputs . Any ( ) )
{
vm . Outputs . Add ( new WalletSendModel . TransactionOutput ( ) ) ;
}
2020-04-27 12:12:01 +02:00
return View ( vm ) ;
}
2019-05-21 10:10:07 +02:00
if ( command = = "add-output" )
{
2019-05-21 11:44:49 +02:00
ModelState . Clear ( ) ;
2019-05-21 10:10:07 +02:00
vm . Outputs . Add ( new WalletSendModel . TransactionOutput ( ) ) ;
return View ( vm ) ;
}
if ( command . StartsWith ( "remove-output" , StringComparison . InvariantCultureIgnoreCase ) )
2018-10-31 16:19:25 +01:00
{
2019-05-21 11:44:49 +02:00
ModelState . Clear ( ) ;
2022-04-24 05:19:34 +02:00
var index = int . Parse (
command . Substring ( command . IndexOf ( ":" , StringComparison . InvariantCultureIgnoreCase ) + 1 ) ,
CultureInfo . InvariantCulture ) ;
2019-05-21 10:10:07 +02:00
vm . Outputs . RemoveAt ( index ) ;
return View ( vm ) ;
2018-10-31 16:19:25 +01:00
}
2019-05-21 10:10:07 +02:00
if ( ! vm . Outputs . Any ( ) )
{
ModelState . AddModelError ( string . Empty ,
"Please add at least one transaction output" ) ;
return View ( vm ) ;
}
2022-04-24 05:19:34 +02:00
var bypassBalanceChecks = command = = "schedule" ;
2019-05-21 10:10:07 +02:00
var subtractFeesOutputsCount = new List < int > ( ) ;
2019-05-21 12:04:39 +02:00
var substractFees = vm . Outputs . Any ( o = > o . SubtractFeesFromOutput ) ;
2019-05-21 10:10:07 +02:00
for ( var i = 0 ; i < vm . Outputs . Count ; i + + )
{
var transactionOutput = vm . Outputs [ i ] ;
if ( transactionOutput . SubtractFeesFromOutput )
{
subtractFeesOutputsCount . Add ( i ) ;
}
2019-12-11 05:05:59 +01:00
transactionOutput . DestinationAddress = transactionOutput . DestinationAddress ? . Trim ( ) ? ? string . Empty ;
2019-12-03 06:26:52 +01:00
2021-08-05 06:56:31 +02:00
var inputName =
2022-04-24 05:19:34 +02:00
string . Format ( CultureInfo . InvariantCulture , "Outputs[{0}]." ,
i . ToString ( CultureInfo . InvariantCulture ) ) +
nameof ( transactionOutput . DestinationAddress ) ;
2019-12-03 06:26:52 +01:00
try
{
2021-08-05 06:56:31 +02:00
var address = BitcoinAddress . Create ( transactionOutput . DestinationAddress , network . NBitcoinNetwork ) ;
if ( address is TaprootAddress )
{
2022-04-24 05:19:34 +02:00
var supportTaproot = _dashboard . Get ( network . CryptoCode ) ? . Status ? . BitcoinStatus ? . Capabilities
? . CanSupportTaproot ;
2021-08-05 06:56:31 +02:00
if ( ! ( supportTaproot is true ) )
{
2022-04-24 05:19:34 +02:00
ModelState . AddModelError ( inputName ,
"You need to update your full node, and/or NBXplorer (Version >= 2.1.56) to be able to send to a taproot address." ) ;
2021-08-05 06:56:31 +02:00
}
}
2019-12-03 06:26:52 +01:00
}
catch
{
2019-12-08 06:20:42 +01:00
ModelState . AddModelError ( inputName , "Invalid address" ) ;
2019-12-03 06:26:52 +01:00
}
2019-05-21 10:10:07 +02:00
2022-04-24 05:19:34 +02:00
if ( ! bypassBalanceChecks & & transactionOutput . Amount . HasValue )
2019-05-21 10:10:07 +02:00
{
transactionAmountSum + = transactionOutput . Amount . Value ;
if ( vm . CurrentBalance = = transactionOutput . Amount . Value & &
! transactionOutput . SubtractFeesFromOutput )
vm . AddModelError ( model = > model . Outputs [ i ] . SubtractFeesFromOutput ,
"You are sending your entire balance to the same destination, you should subtract the fees" ,
2019-10-03 11:00:07 +02:00
this ) ;
2019-05-21 10:10:07 +02:00
}
}
2022-04-24 05:19:34 +02:00
if ( ! bypassBalanceChecks )
2019-05-21 10:10:07 +02:00
{
2022-04-24 05:19:34 +02:00
if ( subtractFeesOutputsCount . Count > 1 )
2019-05-21 10:10:07 +02:00
{
2022-04-24 05:19:34 +02:00
foreach ( var subtractFeesOutput in subtractFeesOutputsCount )
{
vm . AddModelError ( model = > model . Outputs [ subtractFeesOutput ] . SubtractFeesFromOutput ,
"You can only subtract fees from one output" , this ) ;
}
}
else if ( vm . CurrentBalance = = transactionAmountSum & & ! substractFees )
{
ModelState . AddModelError ( string . Empty ,
"You are sending your entire balance, you should subtract the fees from an output" ) ;
2019-05-21 10:10:07 +02:00
}
2022-04-24 05:19:34 +02:00
if ( vm . CurrentBalance < transactionAmountSum )
2019-05-21 10:10:07 +02:00
{
2022-04-24 05:19:34 +02:00
for ( var i = 0 ; i < vm . Outputs . Count ; i + + )
{
vm . AddModelError ( model = > model . Outputs [ i ] . Amount ,
"You are sending more than what you own" , this ) ;
}
2019-05-21 10:10:07 +02:00
}
2022-04-24 05:19:34 +02:00
if ( vm . FeeSatoshiPerByte is decimal fee )
2020-05-05 12:06:59 +02:00
{
2022-04-24 05:19:34 +02:00
if ( fee < 0 )
{
vm . AddModelError ( model = > model . FeeSatoshiPerByte ,
2020-05-05 12:06:59 +02:00
"The fee rate should be above 0" , this ) ;
2022-04-24 05:19:34 +02:00
}
2020-05-05 12:06:59 +02:00
}
}
2022-04-24 05:19:34 +02:00
2020-05-24 23:27:01 +02:00
if ( ! ModelState . IsValid )
2018-10-31 16:19:25 +01:00
return View ( vm ) ;
2021-12-31 08:59:02 +01:00
2022-08-04 05:07:59 +02:00
var derivationScheme = GetDerivationSchemeSettings ( walletId ) ;
if ( derivationScheme is null )
return NotFound ( ) ;
2021-06-14 07:06:56 +02:00
CreatePSBTResponse psbtResponse ;
2022-04-24 05:19:34 +02:00
if ( command = = "schedule" )
{
var pmi = new PaymentMethodId ( walletId . CryptoCode , BitcoinPaymentType . Instance ) ;
var claims =
vm . Outputs . Where ( output = > string . IsNullOrEmpty ( output . PayoutId ) ) . Select ( output = > new ClaimRequest ( )
{
Destination = new AddressClaimDestination (
BitcoinAddress . Create ( output . DestinationAddress , network . NBitcoinNetwork ) ) ,
2022-08-04 05:07:59 +02:00
Value = output . Amount ,
2022-04-24 05:19:34 +02:00
PaymentMethodId = pmi ,
StoreId = walletId . StoreId ,
PreApprove = true ,
} ) . ToArray ( ) ;
var someFailed = false ;
2022-08-04 05:07:59 +02:00
string? message = null ;
string? errorMessage = null ;
2022-04-24 05:19:34 +02:00
var result = new Dictionary < ClaimRequest , ClaimRequest . ClaimResult > ( ) ;
foreach ( ClaimRequest claimRequest in claims )
{
var response = await _pullPaymentHostedService . Claim ( claimRequest ) ;
result . Add ( claimRequest , response . Result ) ;
if ( response . Result = = ClaimRequest . ClaimResult . Ok )
{
if ( message is null )
{
message = "Payouts scheduled:<br/>" ;
}
message + = $"{claimRequest.Value} to {claimRequest.Destination.ToString()}<br/>" ;
}
else
{
someFailed = true ;
if ( errorMessage is null )
{
errorMessage = "Payouts failed to be scheduled:<br/>" ;
}
switch ( response . Result )
{
case ClaimRequest . ClaimResult . Duplicate :
errorMessage + = $"{claimRequest.Value} to {claimRequest.Destination.ToString() } - address reuse<br/>" ;
break ;
case ClaimRequest . ClaimResult . AmountTooLow :
errorMessage + = $"{claimRequest.Value} to {claimRequest.Destination.ToString() } - amount too low<br/>" ;
break ;
}
}
}
if ( message is not null & & errorMessage is not null )
{
message + = $"<br/><br/>{errorMessage}" ;
}
else if ( message is null & & errorMessage is not null )
{
message = errorMessage ;
}
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = someFailed ? StatusMessageModel . StatusSeverity . Warning :
StatusMessageModel . StatusSeverity . Success ,
Html = message
} ) ;
return RedirectToAction ( "Payouts" , "UIStorePullPayments" ,
new
{
storeId = walletId . StoreId ,
PaymentMethodId = pmi . ToString ( ) ,
payoutState = PayoutState . AwaitingPayment ,
} ) ;
}
2019-05-13 01:55:26 +02:00
try
{
2021-06-14 07:06:56 +02:00
psbtResponse = await CreatePSBT ( network , derivationScheme , vm , cancellation ) ;
2019-05-13 01:55:26 +02:00
}
catch ( NBXplorerException ex )
{
2019-05-21 10:10:07 +02:00
ModelState . AddModelError ( string . Empty , ex . Error . Message ) ;
2019-05-13 01:55:26 +02:00
return View ( vm ) ;
}
catch ( NotSupportedException )
{
2019-05-21 10:10:07 +02:00
ModelState . AddModelError ( string . Empty , "You need to update your version of NBXplorer" ) ;
2019-05-13 01:55:26 +02:00
return View ( vm ) ;
}
2020-05-24 21:55:28 +02:00
2021-06-14 07:06:56 +02:00
var psbt = psbtResponse . PSBT ;
derivationScheme . RebaseKeyPaths ( psbt ) ;
2021-12-31 08:59:02 +01:00
2021-06-14 07:06:56 +02:00
var signingContext = new SigningContextModel
2020-05-24 21:55:28 +02:00
{
2020-06-17 14:43:56 +02:00
PayJoinBIP21 = vm . PayJoinBIP21 ,
2021-06-14 07:06:56 +02:00
EnforceLowR = psbtResponse . Suggestions ? . ShouldEnforceLowR ,
2022-02-10 04:24:28 +01:00
ChangeAddress = psbtResponse . ChangeAddress ? . ToString ( ) ,
PSBT = psbt . ToHex ( )
2021-06-14 07:06:56 +02:00
} ;
2019-05-14 18:03:48 +02:00
switch ( command )
2019-05-08 07:39:37 +02:00
{
2022-02-10 04:24:28 +01:00
case "sign" :
2022-07-04 06:20:08 +02:00
return await WalletSign ( walletId , new WalletPSBTViewModel
{
SigningContext = signingContext ,
ReturnUrl = vm . ReturnUrl ,
BackUrl = vm . BackUrl
} ) ;
2019-05-14 18:03:48 +02:00
case "analyze-psbt" :
2019-05-21 10:10:07 +02:00
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $" { output . Amount } - > { output . DestinationAddress } { ( output . SubtractFeesFromOutput ? "-Fees" : string . Empty ) } "))}.psbt" ;
2022-04-24 05:19:34 +02:00
return RedirectToWalletPSBT ( new WalletPSBTViewModel { PSBT = psbt . ToBase64 ( ) , FileName = name } ) ;
2019-05-14 18:03:48 +02:00
default :
return View ( vm ) ;
2019-05-08 07:39:37 +02:00
}
}
2022-04-24 05:19:34 +02:00
2020-02-13 09:18:43 +01:00
private void LoadFromBIP21 ( WalletSendModel vm , string bip21 , BTCPayNetwork network )
{
2022-08-04 05:07:59 +02:00
vm . Outputs ? ? = new ( ) ;
2020-02-13 09:18:43 +01:00
try
{
var uriBuilder = new NBitcoin . Payment . BitcoinUrlBuilder ( bip21 , network . NBitcoinNetwork ) ;
2021-04-13 10:36:49 +02:00
vm . Outputs . Add ( new WalletSendModel . TransactionOutput ( )
2020-02-13 09:18:43 +01:00
{
2021-04-13 10:36:49 +02:00
Amount = uriBuilder . Amount ? . ToDecimal ( MoneyUnit . BTC ) ,
2022-08-04 05:07:59 +02:00
DestinationAddress = uriBuilder . Address ? . ToString ( ) ,
2022-04-24 05:19:34 +02:00
SubtractFeesFromOutput = false ,
PayoutId = uriBuilder . UnknownParameters . ContainsKey ( "payout" )
? uriBuilder . UnknownParameters [ "payout" ]
: null
2021-04-13 10:36:49 +02:00
} ) ;
2020-02-13 09:18:43 +01:00
if ( ! string . IsNullOrEmpty ( uriBuilder . Label ) | | ! string . IsNullOrEmpty ( uriBuilder . Message ) )
{
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Info ,
Html =
$"Payment {(string.IsNullOrEmpty(uriBuilder.Label) ? string.Empty : $" to { uriBuilder . Label } ")} {(string.IsNullOrEmpty(uriBuilder.Message) ? string.Empty : $" for { uriBuilder . Message } ")}"
} ) ;
}
2020-06-17 14:43:56 +02:00
if ( uriBuilder . TryGetPayjoinEndpoint ( out _ ) )
vm . PayJoinBIP21 = uriBuilder . ToString ( ) ;
2020-02-13 09:18:43 +01:00
}
2020-03-26 11:59:28 +01:00
catch
2020-02-13 09:18:43 +01:00
{
2020-03-26 11:59:28 +01:00
try
2020-02-13 09:18:43 +01:00
{
2021-04-13 10:36:49 +02:00
vm . Outputs . Add ( new WalletSendModel . TransactionOutput ( )
2022-04-24 05:19:34 +02:00
{
DestinationAddress = BitcoinAddress . Create ( bip21 , network . NBitcoinNetwork ) . ToString ( )
}
2021-04-13 10:36:49 +02:00
) ;
2020-03-26 11:59:28 +01:00
}
catch
{
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Message = "The provided BIP21 payment URI was malformed"
} ) ;
}
2020-02-13 09:18:43 +01:00
}
ModelState . Clear ( ) ;
}
2022-07-04 06:20:08 +02:00
private IActionResult ViewVault ( WalletId walletId , WalletPSBTViewModel vm )
2019-11-11 06:22:04 +01:00
{
2022-04-24 05:19:34 +02:00
return View ( nameof ( WalletSendVault ) ,
2022-07-04 06:20:08 +02:00
new WalletSendVaultModel
2022-04-24 05:19:34 +02:00
{
2022-07-04 06:20:08 +02:00
SigningContext = vm . SigningContext ,
2022-04-24 05:19:34 +02:00
WalletId = walletId . ToString ( ) ,
2022-07-04 06:20:08 +02:00
WebsocketPath = Url . Action ( nameof ( UIVaultController . VaultBridgeConnection ) , "UIVault" ,
new { walletId = walletId . ToString ( ) } ) ,
ReturnUrl = vm . ReturnUrl ,
BackUrl = vm . BackUrl
2022-04-24 05:19:34 +02:00
} ) ;
2019-11-11 06:22:04 +01:00
}
2022-07-04 06:20:08 +02:00
[HttpPost("{walletId}/vault")]
2022-04-24 05:19:34 +02:00
public IActionResult WalletSendVault ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ] WalletId walletId ,
WalletSendVaultModel model )
2020-02-13 14:06:00 +01:00
{
2022-07-04 06:20:08 +02:00
return RedirectToWalletPSBTReady ( new WalletPSBTReadyViewModel
{
SigningContext = model . SigningContext ,
ReturnUrl = model . ReturnUrl ,
BackUrl = model . BackUrl
} ) ;
2020-02-13 14:06:00 +01:00
}
2021-12-31 08:59:02 +01:00
2020-05-24 21:55:28 +02:00
private IActionResult RedirectToWalletPSBTReady ( WalletPSBTReadyViewModel vm )
2020-02-13 14:06:00 +01:00
{
2021-09-01 17:31:42 +02:00
var redirectVm = new PostRedirectViewModel
2020-02-13 14:06:00 +01:00
{
2022-01-07 04:32:00 +01:00
AspController = "UIWallets" ,
2020-02-13 14:06:00 +01:00
AspAction = nameof ( WalletPSBTReady ) ,
2022-02-10 04:24:28 +01:00
RouteParameters = { { "walletId" , this . RouteData ? . Values [ "walletId" ] ? . ToString ( ) } } ,
FormParameters =
2020-02-13 14:06:00 +01:00
{
2022-02-10 04:24:28 +01:00
{ "SigningKey" , vm . SigningKey } ,
2022-02-17 09:58:56 +01:00
{ "SigningKeyPath" , vm . SigningKeyPath } ,
{ "command" , "decode" }
2020-02-13 14:06:00 +01:00
}
} ;
2020-05-24 23:27:01 +02:00
AddSigningContext ( redirectVm , vm . SigningContext ) ;
2020-07-13 15:02:51 +02:00
if ( ! string . IsNullOrEmpty ( vm . SigningContext . OriginalPSBT ) & &
! string . IsNullOrEmpty ( vm . SigningContext . PSBT ) )
{
//if a hw device signed a payjoin, we want it broadcast instantly
2022-02-17 09:58:56 +01:00
redirectVm . FormParameters . Remove ( "command" ) ;
2022-02-10 04:24:28 +01:00
redirectVm . FormParameters . Add ( "command" , "broadcast" ) ;
}
2022-07-04 06:20:08 +02:00
if ( vm . ReturnUrl ! = null )
2022-02-10 04:24:28 +01:00
{
2022-07-04 06:20:08 +02:00
redirectVm . FormParameters . Add ( "returnUrl" , vm . ReturnUrl ) ;
}
if ( vm . BackUrl ! = null )
{
redirectVm . FormParameters . Add ( "backUrl" , vm . BackUrl ) ;
2020-07-13 15:02:51 +02:00
}
2020-05-24 21:55:28 +02:00
return View ( "PostRedirect" , redirectVm ) ;
2020-02-13 14:06:00 +01:00
}
2020-05-24 23:27:01 +02:00
private void AddSigningContext ( PostRedirectViewModel redirectVm , SigningContextModel signingContext )
{
if ( signingContext is null )
return ;
2022-02-10 04:24:28 +01:00
redirectVm . FormParameters . Add ( "SigningContext.PSBT" , signingContext . PSBT ) ;
redirectVm . FormParameters . Add ( "SigningContext.OriginalPSBT" , signingContext . OriginalPSBT ) ;
redirectVm . FormParameters . Add ( "SigningContext.PayJoinBIP21" , signingContext . PayJoinBIP21 ) ;
2022-04-24 05:19:34 +02:00
redirectVm . FormParameters . Add ( "SigningContext.EnforceLowR" ,
signingContext . EnforceLowR ? . ToString ( CultureInfo . InvariantCulture ) ) ;
2022-02-10 04:24:28 +01:00
redirectVm . FormParameters . Add ( "SigningContext.ChangeAddress" , signingContext . ChangeAddress ) ;
2020-05-24 23:27:01 +02:00
}
2020-05-24 21:55:28 +02:00
private IActionResult RedirectToWalletPSBT ( WalletPSBTViewModel vm )
2019-11-08 12:21:33 +01:00
{
2021-11-11 06:30:19 +01:00
var redirectVm = new PostRedirectViewModel
2019-11-08 12:21:33 +01:00
{
2022-01-07 04:32:00 +01:00
AspController = "UIWallets" ,
2019-11-08 12:21:33 +01:00
AspAction = nameof ( WalletPSBT ) ,
2022-07-04 06:20:08 +02:00
RouteParameters = { { "walletId" , RouteData . Values [ "walletId" ] ? . ToString ( ) } } ,
FormParameters =
{
{ "psbt" , vm . PSBT } ,
{ "fileName" , vm . FileName } ,
{ "backUrl" , vm . BackUrl } ,
{ "returnUrl" , vm . ReturnUrl } ,
{ "command" , "decode" }
}
2019-11-08 12:21:33 +01:00
} ;
2020-05-24 21:55:28 +02:00
return View ( "PostRedirect" , redirectVm ) ;
2019-11-08 12:21:33 +01:00
}
2019-05-14 18:03:48 +02:00
[HttpGet("{walletId}/psbt/seed")]
2022-04-24 05:19:34 +02:00
public IActionResult SignWithSeed ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ] WalletId walletId ,
2022-07-04 06:20:08 +02:00
SigningContextModel signingContext , string returnUrl , string backUrl )
2019-05-14 18:03:48 +02:00
{
2022-07-04 06:20:08 +02:00
return View ( nameof ( SignWithSeed ) , new SignWithSeedViewModel
{
SigningContext = signingContext ,
ReturnUrl = returnUrl ,
BackUrl = backUrl
} ) ;
2019-05-14 18:03:48 +02:00
}
[HttpPost("{walletId}/psbt/seed")]
2022-04-24 05:19:34 +02:00
public async Task < IActionResult > SignWithSeed ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ] WalletId walletId ,
SignWithSeedViewModel viewModel )
2019-05-14 18:03:48 +02:00
{
if ( ! ModelState . IsValid )
{
2020-01-21 09:33:12 +01:00
return View ( "SignWithSeed" , viewModel ) ;
2019-05-14 18:03:48 +02:00
}
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-14 18:03:48 +02:00
if ( network = = null )
throw new FormatException ( "Invalid value for crypto code" ) ;
2019-05-15 08:00:09 +02:00
ExtKey extKey = viewModel . GetExtKey ( network . NBitcoinNetwork ) ;
2019-05-14 18:03:48 +02:00
2022-08-04 05:07:59 +02:00
if ( extKey is null )
2019-05-14 18:03:48 +02:00
{
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) ,
"Seed or Key was not in a valid format. It is either the 12/24 words or starts with xprv" ) ;
}
2020-05-24 23:27:01 +02:00
var psbt = PSBT . Parse ( viewModel . SigningContext . PSBT , network . NBitcoinNetwork ) ;
2019-05-14 18:03:48 +02:00
if ( ! psbt . IsReadyToSign ( ) )
{
2020-05-24 23:27:01 +02:00
ModelState . AddModelError ( nameof ( viewModel . SigningContext . PSBT ) , "PSBT is not ready to be signed" ) ;
2019-05-14 18:03:48 +02:00
}
if ( ! ModelState . IsValid )
{
2020-01-21 09:33:12 +01:00
return View ( "SignWithSeed" , viewModel ) ;
2019-05-14 18:03:48 +02:00
}
2022-08-04 05:07:59 +02:00
// It will never throw, this make nullable check below happy
ArgumentNullException . ThrowIfNull ( extKey ) ;
2019-05-14 18:03:48 +02:00
2022-08-04 05:07:59 +02:00
ExtKey ? signingKey = null ;
2019-10-12 13:35:30 +02:00
var settings = GetDerivationSchemeSettings ( walletId ) ;
2022-08-04 05:07:59 +02:00
if ( settings is null )
return NotFound ( ) ;
2019-05-15 08:00:09 +02:00
var signingKeySettings = settings . GetSigningAccountKeySettings ( ) ;
if ( signingKeySettings . RootFingerprint is null )
signingKeySettings . RootFingerprint = extKey . GetPublicKey ( ) . GetHDFingerPrint ( ) ;
2019-05-15 12:00:26 +02:00
RootedKeyPath rootedKeyPath = signingKeySettings . GetRootedKeyPath ( ) ;
2019-12-23 14:24:29 +01:00
if ( rootedKeyPath = = null )
{
2022-04-24 05:19:34 +02:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) ,
"The master fingerprint and/or account key path of your seed are not set in the wallet settings." ) ;
2020-05-24 23:27:01 +02:00
return View ( nameof ( SignWithSeed ) , viewModel ) ;
2019-12-23 14:24:29 +01:00
}
2019-05-15 12:00:26 +02:00
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
2019-12-23 14:24:29 +01:00
if ( rootedKeyPath . MasterFingerprint = = extKey . GetPublicKey ( ) . GetHDFingerPrint ( ) )
2019-05-14 18:03:48 +02:00
{
2019-05-15 12:00:26 +02:00
psbt . RebaseKeyPaths ( signingKeySettings . AccountKey , rootedKeyPath ) ;
signingKey = extKey . Derive ( rootedKeyPath . KeyPath ) ;
}
else
{
2022-04-24 05:19:34 +02:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) ,
"The master fingerprint does not match the one set in your wallet settings. Probable causes are: wrong seed, wrong passphrase or wrong fingerprint in your wallet settings." ) ;
2020-09-03 16:06:48 +02:00
return View ( nameof ( SignWithSeed ) , viewModel ) ;
2019-05-14 18:03:48 +02:00
}
2019-12-23 14:24:29 +01:00
2021-07-29 13:29:34 +02:00
psbt . Settings . SigningOptions = new SigningOptions ( )
2020-05-24 23:34:49 +02:00
{
EnforceLowR = ! ( viewModel . SigningContext ? . EnforceLowR is false )
2021-07-29 13:29:34 +02:00
} ;
2021-12-31 08:59:02 +01:00
var changed = psbt . PSBTChanged ( ( ) = > psbt . SignAll ( settings . AccountDerivation , signingKey , rootedKeyPath ) ) ;
2019-12-23 14:24:29 +01:00
if ( ! changed )
2019-05-14 18:03:48 +02:00
{
2022-04-24 05:19:34 +02:00
var update = new UpdatePSBTRequest ( ) { PSBT = psbt , DerivationScheme = settings . AccountDerivation } ;
2022-02-17 09:58:56 +01:00
update . RebaseKeyPaths = settings . GetPSBTRebaseKeyRules ( ) . ToList ( ) ;
psbt = ( await ExplorerClientProvider . GetExplorerClient ( network ) . UpdatePSBTAsync ( update ) ) ? . PSBT ;
2022-04-24 05:19:34 +02:00
changed = psbt is not null & & psbt . PSBTChanged ( ( ) = >
psbt . SignAll ( settings . AccountDerivation , signingKey , rootedKeyPath ) ) ;
2022-02-17 09:58:56 +01:00
if ( ! changed )
{
2022-04-24 05:19:34 +02:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) ,
"Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed." ) ;
2022-02-17 09:58:56 +01:00
return View ( nameof ( SignWithSeed ) , viewModel ) ;
}
2019-05-15 08:00:09 +02:00
}
2020-05-24 23:27:01 +02:00
ModelState . Remove ( nameof ( viewModel . SigningContext . PSBT ) ) ;
2022-08-04 05:07:59 +02:00
viewModel . SigningContext ? ? = new ( ) ;
viewModel . SigningContext . PSBT = psbt ? . ToBase64 ( ) ;
2021-09-01 17:31:42 +02:00
return RedirectToWalletPSBTReady ( new WalletPSBTReadyViewModel
2020-05-24 21:55:28 +02:00
{
SigningKey = signingKey . GetWif ( network . NBitcoinNetwork ) . ToString ( ) ,
SigningKeyPath = rootedKeyPath ? . ToString ( ) ,
2022-07-04 06:20:08 +02:00
SigningContext = viewModel . SigningContext ,
ReturnUrl = viewModel . ReturnUrl ,
BackUrl = viewModel . BackUrl
2020-05-24 21:55:28 +02:00
} ) ;
2019-05-15 08:00:09 +02:00
}
2020-05-24 23:27:01 +02:00
2019-05-29 11:43:50 +02:00
private string ValueToString ( Money v , BTCPayNetworkBase network )
2019-05-15 08:00:09 +02:00
{
return v . ToString ( ) + " " + network . CryptoCode ;
}
2019-05-11 13:26:31 +02:00
2021-11-11 06:30:19 +01:00
[HttpGet("{walletId}/rescan")]
2018-10-26 16:07:39 +02:00
public async Task < IActionResult > WalletRescan (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId )
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2018-10-26 16:07:39 +02:00
if ( paymentMethod = = null )
return NotFound ( ) ;
var vm = new RescanWalletModel ( ) ;
2018-11-04 14:46:27 +01:00
vm . IsFullySync = _dashboard . IsFullySynched ( walletId . CryptoCode , out var unused ) ;
2022-04-24 05:19:34 +02:00
vm . IsServerAdmin = ( await _authorizationService . AuthorizeAsync ( User , Policies . CanModifyServerSettings ) )
. Succeeded ;
vm . IsSupportedByCurrency =
_dashboard . Get ( walletId . CryptoCode ) ? . Status ? . BitcoinStatus ? . Capabilities ? . CanScanTxoutSet = = true ;
2018-10-26 16:07:39 +02:00
var explorer = ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode ) ;
2019-05-08 16:39:11 +02:00
var scanProgress = await explorer . GetScanUTXOSetInformationAsync ( paymentMethod . AccountDerivation ) ;
2018-10-31 16:19:25 +01:00
if ( scanProgress ! = null )
2018-10-26 16:07:39 +02:00
{
vm . PreviousError = scanProgress . Error ;
if ( scanProgress . Status = = ScanUTXOStatus . Queued | | scanProgress . Status = = ScanUTXOStatus . Pending )
{
if ( scanProgress . Progress = = null )
{
vm . Progress = 0 ;
}
else
{
vm . Progress = scanProgress . Progress . OverallProgress ;
vm . RemainingTime = TimeSpan . FromSeconds ( scanProgress . Progress . RemainingSeconds ) . PrettyPrint ( ) ;
}
}
2022-04-24 05:19:34 +02:00
2018-10-26 16:07:39 +02:00
if ( scanProgress . Status = = ScanUTXOStatus . Complete )
{
vm . LastSuccess = scanProgress . Progress ;
2022-08-04 05:07:59 +02:00
vm . TimeOfScan = ( scanProgress . Progress ! . CompletedAt ! . Value - scanProgress . Progress . StartedAt )
2022-04-24 05:19:34 +02:00
. PrettyPrint ( ) ;
2018-10-26 16:07:39 +02:00
}
}
2022-04-24 05:19:34 +02:00
2018-10-26 16:07:39 +02:00
return View ( vm ) ;
}
2021-11-11 06:30:19 +01:00
[HttpPost("{walletId}/rescan")]
2020-03-20 05:41:47 +01:00
[Authorize(Policy = Policies.CanModifyServerSettings, AuthenticationSchemes = AuthenticationSchemes.Cookie)]
2018-10-26 16:07:39 +02:00
public async Task < IActionResult > WalletRescan (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId , RescanWalletModel vm )
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2018-10-26 16:07:39 +02:00
if ( paymentMethod = = null )
return NotFound ( ) ;
var explorer = ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode ) ;
try
{
2022-04-24 05:19:34 +02:00
await explorer . ScanUTXOSetAsync ( paymentMethod . AccountDerivation , vm . BatchSize , vm . GapLimit ,
vm . StartingIndex ) ;
2022-05-27 09:26:31 +02:00
_walletProvider . GetWallet ( walletId . CryptoCode ) . InvalidateCache ( paymentMethod . AccountDerivation ) ;
2018-10-26 16:07:39 +02:00
}
catch ( NBXplorerException ex ) when ( ex . Error . Code = = "scanutxoset-in-progress" )
{
}
2022-04-24 05:19:34 +02:00
2018-10-26 16:07:39 +02:00
return RedirectToAction ( ) ;
}
2018-07-26 18:17:43 +02:00
2022-08-04 05:07:59 +02:00
internal DerivationSchemeSettings ? GetDerivationSchemeSettings ( WalletId walletId )
2019-10-12 13:35:30 +02:00
{
2021-12-20 15:15:32 +01:00
return GetCurrentStore ( ) . GetDerivationSchemeSettings ( NetworkProvider , walletId . CryptoCode ) ;
2018-07-26 17:08:07 +02:00
}
2022-04-24 05:19:34 +02:00
private static async Task < IMoney > GetBalanceAsMoney ( BTCPayWallet wallet ,
DerivationStrategyBase derivationStrategy )
2018-07-26 15:32:24 +02:00
{
2021-04-08 05:43:51 +02:00
using CancellationTokenSource cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 10 ) ) ;
try
2018-07-26 15:32:24 +02:00
{
2021-08-01 15:13:12 +02:00
var b = await wallet . GetBalance ( derivationStrategy , cts . Token ) ;
2021-11-11 06:30:19 +01:00
return b . Available ? ? b . Total ;
2021-09-15 16:52:57 +02:00
}
catch
{
2021-11-11 06:30:19 +01:00
return Money . Zero ;
2021-09-15 16:52:57 +02:00
}
}
2022-08-04 05:07:59 +02:00
internal async Task < string > GetBalanceString ( BTCPayWallet wallet , DerivationStrategyBase ? derivationStrategy )
2021-09-15 16:52:57 +02:00
{
2022-08-04 05:07:59 +02:00
if ( derivationStrategy is null )
return "--" ;
2021-09-15 16:52:57 +02:00
try
{
return ( await GetBalanceAsMoney ( wallet , derivationStrategy ) ) . ShowMoney ( wallet . Network ) ;
2021-04-08 05:43:51 +02:00
}
catch
{
return "--" ;
2018-07-26 15:32:24 +02:00
}
}
2021-12-31 08:59:02 +01:00
2021-11-11 06:30:19 +01:00
[HttpPost("{walletId}/actions")]
public async Task < IActionResult > WalletActions (
2020-04-29 08:28:13 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
2021-11-11 06:30:19 +01:00
WalletId walletId , string command ,
2022-02-10 04:24:28 +01:00
string [ ] selectedTransactions ,
2020-04-29 08:28:13 +02:00
CancellationToken cancellationToken = default )
2019-05-12 17:13:55 +02:00
{
2019-10-12 13:35:30 +02:00
var derivationScheme = GetDerivationSchemeSettings ( walletId ) ;
2019-12-29 17:08:30 +01:00
if ( derivationScheme = = null | | derivationScheme . Network . ReadonlyWallet )
2019-05-12 17:13:55 +02:00
return NotFound ( ) ;
2019-11-17 09:13:09 +01:00
2021-11-11 06:30:19 +01:00
switch ( command )
2019-11-17 09:13:09 +01:00
{
2022-02-10 04:24:28 +01:00
case "cpfp" :
2022-04-24 05:19:34 +02:00
{
selectedTransactions ? ? = Array . Empty < string > ( ) ;
if ( selectedTransactions . Length = = 0 )
2022-02-10 04:24:28 +01:00
{
2022-04-24 05:19:34 +02:00
TempData [ WellKnownTempData . ErrorMessage ] = $"No transaction selected" ;
return RedirectToAction ( nameof ( WalletTransactions ) , new { walletId } ) ;
}
var parameters = new MultiValueDictionary < string , string > ( ) ;
parameters . Add ( "walletId" , walletId . ToString ( ) ) ;
int i = 0 ;
foreach ( var tx in selectedTransactions )
{
parameters . Add ( $"transactionHashes[{i}]" , tx ) ;
i + + ;
}
2022-08-04 05:07:59 +02:00
var backUrl = Url . Action ( nameof ( WalletTransactions ) , new { walletId } ) ! ;
2022-07-04 06:20:08 +02:00
parameters . Add ( "returnUrl" , backUrl ) ;
parameters . Add ( "backUrl" , backUrl ) ;
2022-04-24 05:19:34 +02:00
return View ( "PostRedirect" ,
new PostRedirectViewModel
2022-02-10 04:24:28 +01:00
{
AspController = "UIWallets" ,
2022-07-04 06:20:08 +02:00
AspAction = nameof ( WalletCPFP ) ,
2022-02-10 04:24:28 +01:00
RouteParameters = { { "walletId" , walletId . ToString ( ) } } ,
FormParameters = parameters
} ) ;
2022-04-24 05:19:34 +02:00
}
2021-11-11 06:30:19 +01:00
case "prune" :
2022-04-24 05:19:34 +02:00
{
var result = await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode )
. PruneAsync ( derivationScheme . AccountDerivation , new PruneRequest ( ) , cancellationToken ) ;
if ( result . TotalPruned = = 0 )
2021-11-11 06:30:19 +01:00
{
2022-04-24 05:19:34 +02:00
TempData [ WellKnownTempData . SuccessMessage ] = "The wallet is already pruned" ;
}
else
{
TempData [ WellKnownTempData . SuccessMessage ] =
$"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)" ;
2021-11-11 06:30:19 +01:00
}
2022-04-24 05:19:34 +02:00
return RedirectToAction ( nameof ( WalletTransactions ) , new { walletId } ) ;
}
2021-12-31 08:59:02 +01:00
case "clear" when User . IsInRole ( Roles . ServerAdmin ) :
2022-04-24 05:19:34 +02:00
{
if ( Version . TryParse ( _dashboard . Get ( walletId . CryptoCode ) ? . Status ? . Version ? ? "0.0.0.0" ,
out var v ) & &
v < new Version ( 2 , 2 , 4 ) )
2021-03-11 13:29:00 +01:00
{
2022-04-24 05:19:34 +02:00
TempData [ WellKnownTempData . ErrorMessage ] =
"This version of NBXplorer doesn't support this operation, please upgrade to 2.2.4 or above" ;
}
else
{
await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode )
. WipeAsync ( derivationScheme . AccountDerivation , cancellationToken ) ;
TempData [ WellKnownTempData . SuccessMessage ] =
"The transactions have been wiped out, to restore your balance, rescan the wallet." ;
2021-11-11 06:30:19 +01:00
}
2022-04-24 05:19:34 +02:00
return RedirectToAction ( nameof ( WalletTransactions ) , new { walletId } ) ;
}
2021-11-11 06:30:19 +01:00
default :
return NotFound ( ) ;
2019-05-12 17:13:55 +02:00
}
}
2020-01-18 06:12:27 +01:00
2022-05-20 02:35:31 +02:00
[HttpGet("{walletId}/export")]
public async Task < IActionResult > Export (
[ModelBinder(typeof(WalletIdModelBinder))] WalletId walletId ,
2022-08-04 05:07:59 +02:00
string format , string? labelFilter = null )
2022-05-20 02:35:31 +02:00
{
2022-08-04 05:07:59 +02:00
var paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2022-05-20 02:35:31 +02:00
if ( paymentMethod = = null )
return NotFound ( ) ;
var wallet = _walletProvider . GetWallet ( paymentMethod . Network ) ;
2022-12-01 01:54:55 +01:00
var walletTransactionsInfoAsync = WalletRepository . GetWalletTransactionsInfo ( walletId , ( string [ ] ) null ) ;
2022-07-04 08:50:56 +02:00
var input = await wallet . FetchTransactionHistory ( paymentMethod . AccountDerivation , null , null ) ;
2022-05-20 02:35:31 +02:00
var walletTransactionsInfo = await walletTransactionsInfoAsync ;
var export = new TransactionsExport ( wallet , walletTransactionsInfo ) ;
var res = export . Process ( input , format ) ;
var cd = new ContentDisposition
{
FileName = $"btcpay-{walletId}-{DateTime.UtcNow.ToString(" yyyyMMdd - HHmmss ", CultureInfo.InvariantCulture)}.{format}" ,
Inline = true
} ;
Response . Headers . Add ( "Content-Disposition" , cd . ToString ( ) ) ;
Response . Headers . Add ( "X-Content-Type-Options" , "nosniff" ) ;
return Content ( res , "application/" + format ) ;
}
2020-01-18 06:12:27 +01:00
private string GetImage ( PaymentMethodId paymentMethodId , BTCPayNetwork network )
{
var res = paymentMethodId . PaymentType = = PaymentTypes . BTCLike
? Url . Content ( network . CryptoImagePath )
: Url . Content ( network . LightningImagePath ) ;
2021-01-07 14:49:53 +01:00
return Request . GetRelativePathOrAbsolute ( res ) ;
2020-01-18 06:12:27 +01:00
}
2021-12-20 15:15:32 +01:00
private string GetUserId ( ) = > _userManager . GetUserId ( User ) ;
private StoreData GetCurrentStore ( ) = > HttpContext . GetStoreData ( ) ;
2022-10-11 10:34:29 +02:00
public IEnumerable < TransactionTagModel > CreateTransactionTagModels ( WalletTransactionInfo ? transactionInfo )
{
if ( transactionInfo is null )
return Array . Empty < TransactionTagModel > ( ) ;
string PayoutTooltip ( IGrouping < string , string > ? payoutsByPullPaymentId = null )
{
if ( payoutsByPullPaymentId is null )
{
return "Paid a payout" ;
}
else if ( payoutsByPullPaymentId . Count ( ) = = 1 )
{
var pp = payoutsByPullPaymentId . Key ;
var payout = payoutsByPullPaymentId . First ( ) ;
if ( ! string . IsNullOrEmpty ( pp ) )
return $"Paid a payout ({payout}) of a pull payment ({pp})" ;
else
return $"Paid a payout {payout}" ;
}
else
{
var pp = payoutsByPullPaymentId . Key ;
if ( ! string . IsNullOrEmpty ( pp ) )
return $"Paid {payoutsByPullPaymentId.Count()} payouts of a pull payment ({pp})" ;
else
return $"Paid {payoutsByPullPaymentId.Count()} payouts" ;
}
}
var models = new Dictionary < string , TransactionTagModel > ( ) ;
foreach ( var tag in transactionInfo . Attachments )
{
if ( models . ContainsKey ( tag . Type ) )
continue ;
if ( ! transactionInfo . LabelColors . TryGetValue ( tag . Type , out var color ) )
continue ;
var model = new TransactionTagModel
{
Text = tag . Type ,
Color = color ,
TextColor = ColorPalette . Default . TextColor ( color )
} ;
models . Add ( tag . Type , model ) ;
2022-11-17 02:24:49 +01:00
if ( tag . Type = = WalletObjectData . Types . Payout )
2022-10-11 10:34:29 +02:00
{
var payoutsByPullPaymentId =
transactionInfo . Attachments . Where ( t = > t . Type = = "payout" )
. GroupBy ( t = > t . Data ? [ "pullPaymentId" ] ? . Value < string > ( ) ? ? "" ,
k = > k . Id ) . ToList ( ) ;
model . Tooltip = payoutsByPullPaymentId . Count switch
{
0 = > PayoutTooltip ( ) ,
1 = > PayoutTooltip ( payoutsByPullPaymentId . First ( ) ) ,
_ = >
$"<ul>{string.Join(string.Empty, payoutsByPullPaymentId.Select(pair => $" < li > { PayoutTooltip ( pair ) } < / li > "))}</ul>"
} ;
model . Link = _linkGenerator . PayoutLink ( transactionInfo . WalletId . ToString ( ) , null , PayoutState . Completed , Request . Scheme , Request . Host ,
Request . PathBase ) ;
}
2022-11-17 02:24:49 +01:00
else if ( tag . Type = = WalletObjectData . Types . Payjoin )
2022-10-11 10:34:29 +02:00
{
model . Tooltip = $"This UTXO was part of a PayJoin transaction." ;
}
2022-11-17 02:24:49 +01:00
else if ( tag . Type = = WalletObjectData . Types . Invoice )
2022-10-11 10:34:29 +02:00
{
model . Tooltip = $"Received through an invoice {tag.Id}" ;
model . Link = string . IsNullOrEmpty ( tag . Id )
? null
: _linkGenerator . InvoiceLink ( tag . Id , Request . Scheme , Request . Host , Request . PathBase ) ;
}
2022-11-17 02:24:49 +01:00
else if ( tag . Type = = WalletObjectData . Types . PaymentRequest )
2022-10-11 10:34:29 +02:00
{
model . Tooltip = $"Received through a payment request {tag.Id}" ;
model . Link = _linkGenerator . PaymentRequestLink ( tag . Id , Request . Scheme , Request . Host , Request . PathBase ) ;
}
2022-11-17 02:24:49 +01:00
else if ( tag . Type = = WalletObjectData . Types . App )
2022-10-11 10:34:29 +02:00
{
model . Tooltip = $"Received through an app {tag.Id}" ;
model . Link = _linkGenerator . AppLink ( tag . Id , Request . Scheme , Request . Host , Request . PathBase ) ;
}
2022-11-17 02:24:49 +01:00
else if ( tag . Type = = WalletObjectData . Types . PayjoinExposed )
2022-10-11 10:34:29 +02:00
{
if ( tag . Id . Length ! = 0 )
{
model . Tooltip = $"This UTXO was exposed through a PayJoin proposal for an invoice ({tag.Id})" ;
model . Link = _linkGenerator . InvoiceLink ( tag . Id , Request . Scheme , Request . Host , Request . PathBase ) ;
}
else
{
model . Tooltip = $"This UTXO was exposed through a PayJoin proposal" ;
}
}
2022-11-17 02:24:49 +01:00
else if ( tag . Type = = WalletObjectData . Types . Payjoin )
2022-10-11 10:34:29 +02:00
{
model . Tooltip = $"This UTXO was part of a PayJoin transaction." ;
}
}
foreach ( var label in transactionInfo . LabelColors )
models . TryAdd ( label . Key , new TransactionTagModel
{
Text = label . Key ,
Color = label . Value ,
TextColor = ColorPalette . Default . TextColor ( label . Value )
} ) ;
return models . Values . OrderBy ( v = > v . Text ) ;
}
2020-01-18 06:12:27 +01:00
}
public class WalletReceiveViewModel
{
2022-08-04 05:07:59 +02:00
public string? CryptoImage { get ; set ; }
public string? CryptoCode { get ; set ; }
public string? Address { get ; set ; }
public string? PaymentLink { get ; set ; }
public string? ReturnUrl { get ; set ; }
2018-07-26 15:32:24 +02:00
}
public class SendToAddressResult
{
2022-08-04 05:07:59 +02:00
[JsonProperty("psbt")] public string? PSBT { get ; set ; }
2018-07-26 15:32:24 +02:00
}
}