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 ;
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 ;
2021-12-31 08:59:02 +01:00
using BTCPayServer.Models.StoreViewModels ;
2018-07-26 15:32:24 +02:00
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 ;
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
using NBitcoin ;
2020-01-21 09:33:12 +01:00
using NBXplorer ;
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 ;
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 ; }
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 ;
2019-10-03 10:06:49 +02:00
private readonly JsonSerializerSettings _serializerSettings ;
2018-07-26 15:32:24 +02:00
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-18 06:12:27 +01:00
private readonly EventAggregator _EventAggregator ;
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 ;
2020-04-28 09:53:34 +02:00
private readonly LabelFactory _labelFactory ;
2020-06-24 03:34:09 +02:00
private readonly ApplicationDbContextFactory _dbContextFactory ;
private readonly BTCPayNetworkJsonSerializerSettings _jsonSerializerSettings ;
private readonly PullPaymentHostedService _pullPaymentService ;
2021-04-13 10:36:49 +02:00
private readonly IEnumerable < IPayoutHandler > _payoutHandlers ;
2020-05-24 21:55:28 +02:00
2020-06-29 05:07:48 +02:00
readonly CurrencyNameTable _currencyTable ;
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 ,
2019-10-03 10:06:49 +02:00
MvcNewtonsoftJsonOptions mvcJsonOptions ,
2018-07-26 15:32:24 +02:00
NBXplorerDashboard dashboard ,
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-21 09:33:12 +01:00
EventAggregator eventAggregator ,
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 ,
2020-06-24 03:34:09 +02:00
LabelFactory labelFactory ,
ApplicationDbContextFactory dbContextFactory ,
BTCPayNetworkJsonSerializerSettings jsonSerializerSettings ,
2021-11-11 06:30:19 +01:00
PullPaymentHostedService pullPaymentService ,
2021-04-13 10:36:49 +02:00
IEnumerable < IPayoutHandler > payoutHandlers )
2018-07-26 15:32:24 +02:00
{
2018-07-26 16:23:28 +02:00
_currencyTable = currencyTable ;
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 ;
2019-10-03 10:06:49 +02:00
_serializerSettings = mvcJsonOptions . SerializerSettings ;
2018-07-26 15:32:24 +02:00
_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-18 06:12:27 +01:00
_EventAggregator = eventAggregator ;
2020-01-21 09:33:12 +01:00
_settingsRepository = settingsRepository ;
2020-03-29 17:28:22 +02:00
_broadcaster = broadcaster ;
_payjoinClient = payjoinClient ;
2020-04-28 09:53:34 +02:00
_labelFactory = labelFactory ;
2020-06-24 03:34:09 +02:00
_dbContextFactory = dbContextFactory ;
_jsonSerializerSettings = jsonSerializerSettings ;
_pullPaymentService = pullPaymentService ;
2021-04-13 10:36:49 +02:00
_payoutHandlers = payoutHandlers ;
2018-07-26 15:32:24 +02:00
}
2019-08-02 17:42:30 +02:00
// Borrowed from https://github.com/ManageIQ/guides/blob/master/labels.md
2021-12-31 08:59:02 +01:00
readonly string [ ] LabelColorScheme =
2019-08-02 17:42:30 +02:00
{
"#fbca04" ,
"#0e8a16" ,
"#ff7619" ,
"#84b6eb" ,
"#5319e7" ,
2020-12-02 07:38:05 +01:00
"#cdcdcd" ,
2019-08-02 17:42:30 +02:00
"#cc317c" ,
} ;
2019-08-03 15:03:49 +02:00
const int MaxLabelSize = 20 ;
const int MaxCommentSize = 200 ;
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 ,
string addlabel = null ,
2019-08-03 14:52:47 +02:00
string addlabelclick = null ,
2020-05-24 23:27:01 +02:00
string addcomment = null ,
2019-08-03 14:52:47 +02:00
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
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2019-08-02 17:42:30 +02:00
if ( paymentMethod = = null )
return NotFound ( ) ;
var walletBlobInfoAsync = WalletRepository . GetWalletInfo ( walletId ) ;
var walletTransactionsInfoAsync = WalletRepository . GetWalletTransactionsInfo ( walletId ) ;
var wallet = _walletProvider . GetWallet ( paymentMethod . Network ) ;
var walletBlobInfo = await walletBlobInfoAsync ;
var walletTransactionsInfo = await walletTransactionsInfoAsync ;
if ( addlabel ! = null )
{
2020-05-24 23:27:01 +02:00
addlabel = addlabel . Trim ( ) . TrimStart ( '{' ) . ToLowerInvariant ( ) . Replace ( ',' , ' ' ) . Truncate ( MaxLabelSize ) ;
2020-12-12 06:10:47 +01:00
var labels = _labelFactory . GetWalletColoredLabels ( walletBlobInfo , Request ) ;
2019-08-02 17:42:30 +02:00
if ( ! walletTransactionsInfo . TryGetValue ( transactionId , out var walletTransactionInfo ) )
{
walletTransactionInfo = new WalletTransactionInfo ( ) ;
}
2020-12-12 06:10:47 +01:00
if ( ! labels . Any ( l = > l . Text . Equals ( addlabel , StringComparison . OrdinalIgnoreCase ) ) )
2019-08-02 17:42:30 +02:00
{
List < string > allColors = new List < string > ( ) ;
allColors . AddRange ( LabelColorScheme ) ;
allColors . AddRange ( labels . Select ( l = > l . Color ) ) ;
var chosenColor =
allColors
. GroupBy ( k = > k )
. OrderBy ( k = > k . Count ( ) )
2021-12-31 08:59:02 +01:00
. ThenBy ( k = >
{
2020-12-02 07:38:05 +01:00
var indexInColorScheme = Array . IndexOf ( LabelColorScheme , k . Key ) ;
// Ensures that any label color which may not be in our label color scheme is given the least priority
return indexInColorScheme = = - 1 ? double . PositiveInfinity : indexInColorScheme ;
} )
2019-08-02 17:42:30 +02:00
. First ( ) . Key ;
walletBlobInfo . LabelColors . Add ( addlabel , chosenColor ) ;
await WalletRepository . SetWalletInfo ( walletId , walletBlobInfo ) ;
}
2020-12-12 06:10:47 +01:00
var rawLabel = new RawLabel ( addlabel ) ;
if ( walletTransactionInfo . Labels . TryAdd ( rawLabel . Text , rawLabel ) )
2019-08-02 17:42:30 +02:00
{
await WalletRepository . SetWalletTransactionInfo ( walletId , transactionId , walletTransactionInfo ) ;
}
}
else if ( removelabel ! = null )
{
2020-04-28 08:06:28 +02:00
removelabel = removelabel . Trim ( ) ;
2019-08-02 17:42:30 +02:00
if ( walletTransactionsInfo . TryGetValue ( transactionId , out var walletTransactionInfo ) )
{
if ( walletTransactionInfo . Labels . Remove ( removelabel ) )
{
2020-12-12 06:10:47 +01:00
var canDeleteColor = ! walletTransactionsInfo . Any ( txi = > txi . Value . Labels . ContainsKey ( removelabel ) ) ;
if ( canDeleteColor )
2019-08-02 17:42:30 +02:00
{
walletBlobInfo . LabelColors . Remove ( removelabel ) ;
await WalletRepository . SetWalletInfo ( walletId , walletBlobInfo ) ;
}
await WalletRepository . SetWalletTransactionInfo ( walletId , transactionId , walletTransactionInfo ) ;
}
}
}
else if ( addcomment ! = null )
{
2019-08-03 15:03:49 +02:00
addcomment = addcomment . Trim ( ) . Truncate ( MaxCommentSize ) ;
2019-08-02 17:42:30 +02:00
if ( ! walletTransactionsInfo . TryGetValue ( transactionId , out var walletTransactionInfo ) )
{
walletTransactionInfo = new WalletTransactionInfo ( ) ;
}
walletTransactionInfo . Comment = addcomment ;
await WalletRepository . SetWalletTransactionInfo ( walletId , transactionId , walletTransactionInfo ) ;
}
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
2018-10-09 16:48:14 +02:00
. SelectMany ( s = > s . GetSupportedPaymentMethods ( NetworkProvider )
2019-05-08 16:39:11 +02:00
. OfType < DerivationSchemeSettings > ( )
2018-07-26 15:32:24 +02:00
. Select ( d = > ( ( Wallet : _walletProvider . GetWallet ( d . Network ) ,
2019-05-08 16:39:11 +02:00
DerivationStrategy : d . AccountDerivation ,
2018-07-26 15:32:24 +02:00
Network : d . Network ) ) )
2019-12-24 08:20:44 +01:00
. Where ( _ = > _ . Wallet ! = null & & _ . Network . WalletSupported )
2018-07-26 15:32:24 +02:00
. Select ( _ = > ( Wallet : _ . Wallet ,
Store : s ,
Balance : GetBalanceString ( _ . Wallet , _ . DerivationStrategy ) ,
DerivationStrategy : _ . DerivationStrategy ,
Network : _ . Network ) ) )
. ToList ( ) ;
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 = "" ;
}
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 ,
2020-07-25 06:33:02 +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
{
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings 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 ) ;
2019-08-02 17:42:30 +02:00
var walletBlobAsync = WalletRepository . GetWalletInfo ( walletId ) ;
var walletTransactionsInfoAsync = WalletRepository . GetWalletTransactionsInfo ( walletId ) ;
2019-05-08 16:39:11 +02:00
var transactions = await wallet . FetchTransactions ( paymentMethod . AccountDerivation ) ;
2019-08-02 17:42:30 +02:00
var walletBlob = await walletBlobAsync ;
var walletTransactionsInfo = await walletTransactionsInfoAsync ;
2020-07-25 06:33:02 +02:00
var model = new ListTransactionsViewModel
{
Skip = skip ,
Count = count ,
Total = 0
} ;
2020-12-12 07:21:37 +01:00
if ( labelFilter ! = null )
{
model . PaginationQuery = new Dictionary < string , object >
{
{ "labelFilter" , labelFilter }
} ;
}
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
{
foreach ( var tx in transactions . UnconfirmedTransactions . Transactions
. Concat ( transactions . ConfirmedTransactions . Transactions ) . ToArray ( ) )
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 ) ;
vm . Timestamp = tx . Timestamp ;
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 ) )
{
2020-12-12 06:10:47 +01:00
var labels = _labelFactory . ColorizeTransactionLabels ( walletBlob , transactionInfo , Request ) ;
2019-12-29 17:08:30 +01:00
vm . Labels . AddRange ( labels ) ;
model . Labels . AddRange ( labels ) ;
vm . Comment = transactionInfo . Comment ;
}
if ( labelFilter = = null | |
2020-12-12 06:10:47 +01:00
vm . Labels . 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
2020-07-25 06:33:02 +02:00
model . Total = model . Transactions . Count ;
model . Transactions = model . Transactions . OrderByDescending ( t = > t . Timestamp ) . Skip ( skip ) . Take ( count ) . ToList ( ) ;
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 ;
2018-07-26 17:08:07 +02:00
return View ( model ) ;
}
2018-07-26 15:32:24 +02:00
2019-08-02 17:42:30 +02:00
private static string GetLabelTarget ( WalletId walletId , uint256 txId )
{
return $"{walletId}:{txId}" ;
}
2018-07-26 17:08:07 +02:00
2021-11-11 06:30:19 +01:00
[HttpGet("{walletId}/receive")]
2020-01-18 06:12:27 +01:00
public IActionResult WalletReceive ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2020-01-18 07:32:01 +01:00
WalletId walletId )
2020-01-18 06:12:27 +01:00
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
if ( paymentMethod = = null )
return NotFound ( ) ;
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId ? . CryptoCode ) ;
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 )
{
2021-07-30 11:47:02 +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
}
2020-01-18 06:12:27 +01:00
return View ( new WalletReceiveViewModel ( )
{
CryptoCode = walletId . CryptoCode ,
Address = address ? . ToString ( ) ,
2021-04-13 05:26:36 +02:00
CryptoImage = GetImage ( paymentMethod . PaymentId , network ) ,
2021-07-30 11:47:02 +02:00
PaymentLink = bip21 . ToString ( )
2020-01-18 06:12:27 +01:00
} ) ;
}
[HttpPost]
[Route("{walletId}/receive")]
public async Task < IActionResult > WalletReceive ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
WalletId walletId , WalletReceiveViewModel viewModel , string command )
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
if ( paymentMethod = = null )
return NotFound ( ) ;
var network = this . NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId ? . CryptoCode ) ;
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
{
2021-03-11 13:34:52 +01:00
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
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 ;
}
2020-05-24 23:27:01 +02:00
return RedirectToAction ( nameof ( WalletReceive ) , new { walletId } ) ;
2020-01-18 06:12:27 +01:00
}
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 (
[ModelBinder(typeof(WalletIdModelBinder))]
2021-04-13 10:36:49 +02:00
WalletId walletId , string defaultDestination = null , string defaultAmount = null , string [ ] bip21 = 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 ( ) ) ;
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2018-07-26 15:32:24 +02:00
if ( paymentMethod = = null )
return NotFound ( ) ;
2019-05-29 11:43:50 +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 ) ;
2020-05-24 23:27:01 +02:00
var model = new WalletSendModel ( )
2018-10-09 16:48:14 +02:00
{
2021-04-13 10:36:49 +02:00
CryptoCode = walletId . CryptoCode
} ;
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-12-31 08:59:02 +01: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 ) ) ;
2020-05-24 23:27:01 +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 ) ) ;
2019-03-05 09:09:17 +01: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 ;
2020-09-11 09:23:08 +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
{
model . RateError = $"{result.EvaluatedRule} ({string.Join(" , ", result.Errors.OfType<object>().ToArray())})" ;
}
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
}
2018-07-26 15:32:24 +02:00
return View ( model ) ;
}
2020-05-24 23:27:01 +02:00
2021-03-11 13:29:00 +01:00
private async Task < string > GetSeed ( WalletId walletId , BTCPayNetwork network )
{
return await CanUseHotWallet ( ) & &
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 ;
}
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))]
2020-02-13 09:18:43 +01:00
WalletId walletId , WalletSendModel vm , string command = "" , CancellationToken cancellation = default , 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 ( ) ;
2021-06-14 07:06:56 +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 ( ) ;
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 ) ;
var walletBlobAsync = await WalletRepository . GetWalletInfo ( walletId ) ;
var walletTransactionsInfoAsync = await WalletRepository . GetWalletTransactionsInfo ( walletId ) ;
2020-05-24 23:27:01 +02:00
var utxos = await _walletProvider . GetWallet ( network ) . GetUnspentCoins ( schemeSettings . AccountDerivation , cancellation ) ;
2020-03-19 09:44:47 +01:00
vm . InputsAvailable = utxos . Select ( coin = >
{
walletTransactionsInfoAsync . TryGetValue ( coin . OutPoint . Hash . ToString ( ) , out var info ) ;
return new WalletSendModel . InputSelectionOption ( )
{
Outpoint = coin . OutPoint . ToString ( ) ,
Amount = coin . Value . GetValue ( network ) ,
Comment = info ? . Comment ,
2020-12-12 06:10:47 +01:00
Labels = info = = null ? null : _labelFactory . ColorizeTransactionLabels ( walletBlobAsync , info , Request ) ,
2021-04-20 04:02:06 +02:00
Link = string . Format ( CultureInfo . InvariantCulture , network . BlockExplorerLink , coin . OutPoint . Hash . ToString ( ) ) ,
Confirmations = coin . Confirmations
2020-03-19 09:44:47 +01:00
} ;
} ) . ToArray ( ) ;
}
if ( command = = "toggle-input-selection" )
{
ModelState . Clear ( ) ;
return View ( vm ) ;
}
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 ( ) ;
2020-05-24 23:27:01 +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 ) ;
}
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 =
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 )
{
var supportTaproot = _dashboard . Get ( network . CryptoCode ) ? . Status ? . BitcoinStatus ? . Capabilities ? . CanSupportTaproot ;
if ( ! ( supportTaproot is true ) )
{
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." ) ;
}
}
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
if ( transactionOutput . Amount . HasValue )
{
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
}
}
if ( subtractFeesOutputsCount . Count > 1 )
{
foreach ( var subtractFeesOutput in subtractFeesOutputsCount )
{
vm . AddModelError ( model = > model . Outputs [ subtractFeesOutput ] . SubtractFeesFromOutput ,
2019-10-03 11:00:07 +02:00
"You can only subtract fees from one output" , this ) ;
2019-05-21 10:10:07 +02:00
}
2020-05-24 23:27:01 +02:00
}
else if ( vm . CurrentBalance = = transactionAmountSum & & ! substractFees )
2019-05-21 10:10:07 +02:00
{
ModelState . AddModelError ( string . Empty ,
"You are sending your entire balance, you should subtract the fees from an output" ) ;
}
if ( vm . CurrentBalance < transactionAmountSum )
{
for ( var i = 0 ; i < vm . Outputs . Count ; i + + )
{
vm . AddModelError ( model = > model . Outputs [ i ] . Amount ,
2019-10-03 11:00:07 +02:00
"You are sending more than what you own" , this ) ;
2019-05-21 10:10:07 +02:00
}
}
2020-05-05 12:06:59 +02:00
if ( vm . FeeSatoshiPerByte is decimal fee )
{
if ( fee < 0 )
{
vm . AddModelError ( model = > model . FeeSatoshiPerByte ,
"The fee rate should be above 0" , this ) ;
}
}
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
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings ( walletId ) ;
2021-06-14 07:06:56 +02:00
CreatePSBTResponse psbtResponse ;
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 ,
2021-06-15 10:35:23 +02:00
ChangeAddress = psbtResponse . ChangeAddress ? . ToString ( )
2021-06-14 07:06:56 +02:00
} ;
2020-05-24 21:55:28 +02:00
2021-08-03 12:43:16 +02:00
var res = await TryHandleSigningCommands ( walletId , psbt , command , signingContext , nameof ( WalletSend ) ) ;
2020-05-23 21:31:21 +02:00
if ( res ! = null )
{
return res ;
}
2020-05-24 23:27:01 +02:00
2019-05-14 18:03:48 +02:00
switch ( command )
2019-05-08 07:39:37 +02:00
{
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" ;
2021-06-14 07:06:56 +02:00
return RedirectToWalletPSBT ( new WalletPSBTViewModel
2020-05-24 21:55:28 +02:00
{
2021-06-14 07:06:56 +02:00
PSBT = psbt . ToBase64 ( ) ,
2020-05-25 00:05:01 +02:00
FileName = name
2020-05-24 21:55:28 +02:00
} ) ;
2019-05-14 18:03:48 +02:00
default :
return View ( vm ) ;
2019-05-08 07:39:37 +02:00
}
}
2020-02-13 09:18:43 +01:00
private void LoadFromBIP21 ( WalletSendModel vm , string bip21 , BTCPayNetwork network )
{
2021-04-13 10:36:49 +02:00
vm . Outputs ? ? = new List < WalletSendModel . TransactionOutput > ( ) ;
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 ) ,
DestinationAddress = uriBuilder . Address . ToString ( ) ,
SubtractFeesFromOutput = false
} ) ;
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 ( )
2021-12-31 08:59:02 +01: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 ( ) ;
}
2020-05-24 23:27:01 +02:00
private IActionResult ViewVault ( WalletId walletId , SigningContextModel signingContext )
2019-11-11 06:22:04 +01:00
{
2020-03-26 12:39:19 +01:00
return View ( nameof ( WalletSendVault ) , new WalletSendVaultModel ( )
2019-11-11 06:22:04 +01:00
{
2020-05-24 21:55:28 +02:00
SigningContext = signingContext ,
2019-11-11 06:22:04 +01:00
WalletId = walletId . ToString ( ) ,
2022-01-07 04:32:00 +01:00
WebsocketPath = this . Url . Action ( nameof ( UIVaultController . VaultBridgeConnection ) , "UIVault" , new { walletId = walletId . ToString ( ) } )
2019-11-11 06:22:04 +01:00
} ) ;
}
2020-02-13 14:06:00 +01:00
[HttpPost]
[Route("{walletId}/vault")]
2020-04-09 13:25:17 +02:00
public IActionResult WalletSendVault ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2020-02-13 14:06:00 +01:00
WalletId walletId , WalletSendVaultModel model )
{
2020-05-24 21:55:28 +02:00
return RedirectToWalletPSBTReady ( new WalletPSBTReadyViewModel ( )
{
SigningContext = model . SigningContext
} ) ;
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 ) ,
Parameters =
{
2020-05-24 21:55:28 +02:00
new KeyValuePair < string , string > ( "SigningKey" , vm . SigningKey ) ,
new KeyValuePair < string , string > ( "SigningKeyPath" , vm . SigningKeyPath )
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
2021-12-31 08:59:02 +01:00
redirectVm . Parameters . Add ( new KeyValuePair < string , string > ( "command" , "broadcast" ) ) ;
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 ;
redirectVm . Parameters . Add ( new KeyValuePair < string , string > ( "SigningContext.PSBT" , signingContext . PSBT ) ) ;
redirectVm . Parameters . Add ( new KeyValuePair < string , string > ( "SigningContext.OriginalPSBT" , signingContext . OriginalPSBT ) ) ;
2020-06-17 14:43:56 +02:00
redirectVm . Parameters . Add ( new KeyValuePair < string , string > ( "SigningContext.PayJoinBIP21" , signingContext . PayJoinBIP21 ) ) ;
2020-05-24 23:27:01 +02:00
redirectVm . Parameters . Add ( new KeyValuePair < string , string > ( "SigningContext.EnforceLowR" , signingContext . EnforceLowR ? . ToString ( CultureInfo . InvariantCulture ) ) ) ;
redirectVm . Parameters . Add ( new KeyValuePair < string , string > ( "SigningContext.ChangeAddress" , signingContext . ChangeAddress ) ) ;
}
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 ) ,
Parameters =
{
2020-05-24 21:55:28 +02:00
new KeyValuePair < string , string > ( "psbt" , vm . PSBT ) ,
2020-05-24 23:27:01 +02:00
new KeyValuePair < string , string > ( "fileName" , vm . FileName )
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")]
public IActionResult SignWithSeed ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2020-05-24 23:27:01 +02:00
WalletId walletId , SigningContextModel signingContext )
2019-05-14 18:03:48 +02:00
{
2021-11-11 06:30:19 +01:00
return View ( nameof ( SignWithSeed ) , new SignWithSeedViewModel
2019-05-14 18:03:48 +02:00
{
2020-05-24 21:55:28 +02:00
SigningContext = signingContext ,
2019-05-14 18:03:48 +02:00
} ) ;
}
[HttpPost("{walletId}/psbt/seed")]
2020-04-09 13:25:17 +02:00
public IActionResult SignWithSeed ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2019-05-14 18:03:48 +02:00
WalletId walletId , SignWithSeedViewModel viewModel )
{
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
2019-05-15 08:00:09 +02:00
if ( extKey = = 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
}
2019-05-15 12:00:26 +02:00
ExtKey signingKey = null ;
2019-10-12 13:35:30 +02:00
var settings = GetDerivationSchemeSettings ( walletId ) ;
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 )
{
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
{
2021-03-08 07:30:40 +01: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
{
2021-03-08 07:30:40 +01:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) , "Impossible to sign the transaction. Probable causes: Incorrect account key path in wallet settings or PSBT already signed." ) ;
2020-09-03 16:06:48 +02: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 ) ) ;
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 ( ) ,
SigningContext = viewModel . SigningContext
} ) ;
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
2019-10-12 13:35:30 +02:00
private IActionResult RedirectToWalletTransaction ( WalletId walletId , Transaction transaction )
2019-05-11 17:05:30 +02:00
{
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-11 17:05:30 +02:00
if ( transaction ! = null )
{
var wallet = _walletProvider . GetWallet ( network ) ;
2019-10-12 13:35:30 +02:00
var derivationSettings = GetDerivationSchemeSettings ( walletId ) ;
2019-05-11 17:05:30 +02:00
wallet . InvalidateCache ( derivationSettings . AccountDerivation ) ;
}
2019-10-21 10:54:12 +02:00
return RedirectToAction ( nameof ( WalletTransactions ) , new { walletId = walletId . ToString ( ) } ) ;
2019-05-11 17:05:30 +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 ( ) ;
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings 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 ) ;
2020-03-20 05:41:47 +01:00
vm . IsServerAdmin = ( await _authorizationService . AuthorizeAsync ( User , Policies . CanModifyServerSettings ) ) . Succeeded ;
2018-10-26 16:07:39 +02:00
vm . IsSupportedByCurrency = _dashboard . Get ( walletId . CryptoCode ) ? . Status ? . BitcoinStatus ? . Capabilities ? . CanScanTxoutSet = = true ;
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 ( ) ;
}
}
if ( scanProgress . Status = = ScanUTXOStatus . Complete )
{
vm . LastSuccess = scanProgress . Progress ;
vm . TimeOfScan = ( scanProgress . Progress . CompletedAt . Value - scanProgress . Progress . StartedAt ) . PrettyPrint ( ) ;
}
}
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 ( ) ;
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings ( walletId ) ;
2018-10-26 16:07:39 +02:00
if ( paymentMethod = = null )
return NotFound ( ) ;
var explorer = ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode ) ;
try
{
2019-05-08 16:39:11 +02:00
await explorer . ScanUTXOSetAsync ( paymentMethod . AccountDerivation , vm . BatchSize , vm . GapLimit , vm . StartingIndex ) ;
2018-10-26 16:07:39 +02:00
}
catch ( NBXplorerException ex ) when ( ex . Error . Code = = "scanutxoset-in-progress" )
{
}
return RedirectToAction ( ) ;
}
2018-07-26 18:17:43 +02:00
2020-05-16 22:07:24 +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
}
2021-09-15 16:52:57 +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
}
}
private static async Task < string > GetBalanceString ( BTCPayWallet wallet , DerivationStrategyBase derivationStrategy )
{
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 ,
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
{
2021-11-11 06:30:19 +01:00
case "prune" :
{
2021-12-31 08:59:02 +01:00
var result = await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode ) . PruneAsync ( derivationScheme . AccountDerivation , new PruneRequest ( ) , cancellationToken ) ;
if ( result . TotalPruned = = 0 )
{
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)" ;
}
2020-04-29 08:28:13 +02:00
2021-12-31 08:59:02 +01:00
return RedirectToAction ( nameof ( WalletTransactions ) , new { walletId } ) ;
2021-11-11 06:30:19 +01:00
}
2021-12-31 08:59:02 +01:00
case "clear" when User . IsInRole ( Roles . ServerAdmin ) :
2021-03-11 13:29:00 +01:00
{
2021-12-31 08:59:02 +01:00
if ( Version . TryParse ( _dashboard . Get ( walletId . CryptoCode ) ? . Status ? . Version ? ? "0.0.0.0" , out var v ) & &
v < new Version ( 2 , 2 , 4 ) )
{
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." ;
}
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
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 ( ) ;
2020-01-18 06:12:27 +01:00
}
public class WalletReceiveViewModel
{
public string CryptoImage { get ; set ; }
public string CryptoCode { get ; set ; }
public string Address { get ; set ; }
2021-04-13 05:26:36 +02:00
public string PaymentLink { get ; set ; }
2018-07-26 15:32:24 +02:00
}
public class SendToAddressResult
{
2019-05-11 17:05:30 +02:00
[JsonProperty("psbt")]
public string PSBT { get ; set ; }
2018-07-26 15:32:24 +02:00
}
}