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 ;
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 ;
2020-07-15 19:51:01 +02: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 ;
2018-07-26 15:32:24 +02:00
using BTCPayServer.Security ;
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 ;
2019-05-12 17:13:55 +02:00
using NBitcoin.DataEncoders ;
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 ;
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]
2018-10-09 16:48:14 +02:00
public partial class WalletsController : Controller
2018-07-26 15:32:24 +02:00
{
2018-10-09 16:48:14 +02:00
public StoreRepository Repository { get ; }
2019-08-02 17:42:30 +02:00
public WalletRepository WalletRepository { get ; }
2018-10-09 16:48:14 +02:00
public BTCPayNetworkProvider NetworkProvider { get ; }
public ExplorerClientProvider ExplorerClientProvider { get ; }
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 ;
2020-01-18 06:12:27 +01:00
private readonly WalletReceiveStateService _WalletReceiveStateService ;
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 ;
2020-05-24 21:55:28 +02:00
2018-10-09 16:48:14 +02:00
public RateFetcher RateFetcher { get ; }
2018-10-31 16:19:25 +01:00
2020-06-29 05:07:48 +02:00
readonly CurrencyNameTable _currencyTable ;
2018-07-26 15:32:24 +02:00
public WalletsController ( 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 ,
WalletReceiveStateService walletReceiveStateService ,
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 ,
HostedServices . PullPaymentHostedService pullPaymentService )
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 ;
2020-01-18 06:12:27 +01:00
_WalletReceiveStateService = walletReceiveStateService ;
_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 ;
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
2020-06-29 05:07:48 +02:00
readonly string [ ] LabelColorScheme = new string [ ]
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-04-28 09:53:34 +02:00
var labels = _labelFactory . GetLabels ( walletBlobInfo , Request ) ;
2019-08-02 17:42:30 +02:00
if ( ! walletTransactionsInfo . TryGetValue ( transactionId , out var walletTransactionInfo ) )
{
walletTransactionInfo = new WalletTransactionInfo ( ) ;
}
if ( ! labels . Any ( l = > l . Value . Equals ( addlabel , StringComparison . OrdinalIgnoreCase ) ) )
{
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 ( ) )
2020-12-02 07:38:05 +01:00
. ThenBy ( k = > {
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 ) ;
}
if ( walletTransactionInfo . Labels . Add ( addlabel ) )
{
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 ) )
{
var canDelete = ! walletTransactionsInfo . SelectMany ( txi = > txi . Value . Labels ) . Any ( l = > l = = removelabel ) ;
if ( canDelete )
{
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 ;
}
return View ( wallets ) ;
}
[HttpGet]
[Route("{walletId}")]
2020-01-18 06:12:27 +01:00
[Route("{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))]
2020-07-25 06:33:02 +02:00
WalletId walletId ,
string labelFilter = null ,
int skip = 0 ,
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
} ;
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-05-24 23:27:01 +02:00
var labels = _labelFactory . GetLabels ( 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 | |
vm . Labels . Any ( l = > l . Value . Equals ( labelFilter , StringComparison . OrdinalIgnoreCase ) ) )
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
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
2020-01-18 06:12:27 +01:00
[HttpGet]
[Route("{walletId}/receive")]
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 ( ) ;
var address = _WalletReceiveStateService . Get ( walletId ) ? . Address ;
return View ( new WalletReceiveViewModel ( )
{
CryptoCode = walletId . CryptoCode ,
Address = address ? . ToString ( ) ,
CryptoImage = GetImage ( paymentMethod . PaymentId , network )
} ) ;
}
[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 ( ) ;
var wallet = _walletProvider . GetWallet ( network ) ;
switch ( command )
{
case "unreserve-current-address" :
KeyPathInformation cachedAddress = _WalletReceiveStateService . Get ( walletId ) ;
if ( cachedAddress = = null )
{
break ;
}
var address = cachedAddress . ScriptPubKey . GetDestinationAddress ( network . NBitcoinNetwork ) ;
ExplorerClientProvider . GetExplorerClient ( network )
2020-05-24 23:27:01 +02:00
. CancelReservation ( cachedAddress . DerivationStrategy , new [ ] { cachedAddress . KeyPath } ) ;
2020-01-18 07:32:01 +01:00
this . TempData . SetStatusMessageModel ( new StatusMessageModel ( )
2020-01-18 06:12:27 +01:00
{
2020-01-18 07:32:01 +01:00
AllowDismiss = true ,
2020-01-18 06:12:27 +01:00
Message = $"Address {address} was unreserved." ,
Severity = StatusMessageModel . StatusSeverity . Success ,
2020-01-18 07:32:01 +01:00
} ) ;
2020-01-18 06:12:27 +01:00
_WalletReceiveStateService . Remove ( walletId ) ;
break ;
case "generate-new-address" :
var reserve = ( await wallet . ReserveAddressAsync ( paymentMethod . AccountDerivation ) ) ;
_WalletReceiveStateService . Set ( walletId , reserve ) ;
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 ( )
{
2020-03-20 05:44:02 +01:00
var isAdmin = ( await _authorizationService . AuthorizeAsync ( User , Policies . CanModifyServerSettings ) ) . Succeeded ;
2020-01-21 09:33:12 +01:00
if ( isAdmin )
return true ;
var policies = await _settingsRepository . GetSettingAsync < PoliciesSettings > ( ) ;
return policies ? . AllowHotWalletForAll is true ;
}
2020-05-24 23:27:01 +02:00
2018-07-26 17:08:07 +02:00
[HttpGet]
[Route("{walletId}/send")]
public async Task < IActionResult > WalletSend (
[ModelBinder(typeof(WalletIdModelBinder))]
2020-05-12 15:32:33 +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 ;
2018-07-26 18:17:43 +02:00
var currencyPair = new Rating . CurrencyPair ( paymentMethod . PaymentId . CryptoCode , GetCurrencyCode ( storeData . DefaultLang ) ? ? "USD" ) ;
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
{
2019-05-21 10:10:07 +02:00
Outputs = new List < WalletSendModel . TransactionOutput > ( )
{
new WalletSendModel . TransactionOutput ( )
{
Amount = Convert . ToDecimal ( amount ) ,
DestinationAddress = defaultDestination
}
} ,
2018-10-31 16:19:25 +01:00
CryptoCode = walletId . CryptoCode
2018-10-09 16:48:14 +02:00
} ;
2020-05-24 23:27:01 +02:00
if ( ! string . IsNullOrEmpty ( bip21 ) )
{
LoadFromBIP21 ( model , bip21 , network ) ;
}
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 ) ;
2020-01-21 09:33:12 +01:00
model . NBXSeedAvailable = await CanUseHotWallet ( ) & & ! string . IsNullOrEmpty ( await ExplorerClientProvider . GetExplorerClient ( network )
. GetMetadataAsync < string > ( GetDerivationSchemeSettings ( walletId ) . AccountDerivation ,
2020-02-13 14:06:00 +01:00
WellknownMetadataKeys . MasterHDKey ) ) ;
2019-12-02 09:57:38 +01:00
model . CurrentBalance = await balance ;
2020-05-24 23:27:01 +02: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
2018-07-26 15:32:24 +02:00
2018-10-31 16:19:25 +01:00
[HttpPost]
[Route("{walletId}/send")]
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 ( ) ;
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 ( ) ;
2019-05-08 08:24:20 +02:00
vm . SupportRBF = network . SupportRBF ;
2020-01-06 13:57:32 +01:00
vm . NBXSeedAvailable = await CanUseHotWallet ( ) & & ! string . IsNullOrEmpty ( await ExplorerClientProvider . GetExplorerClient ( network )
. GetMetadataAsync < string > ( GetDerivationSchemeSettings ( walletId ) . AccountDerivation ,
WellknownMetadataKeys . MasterHDKey , cancellation ) ) ;
2020-02-13 09:18:43 +01:00
if ( ! string . IsNullOrEmpty ( bip21 ) )
{
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-05-24 23:27:01 +02:00
Labels = info = = null ? null : _labelFactory . GetLabels ( walletBlobAsync , info , Request ) ,
2020-03-19 09:44:47 +01:00
Link = string . Format ( CultureInfo . InvariantCulture , network . BlockExplorerLink , coin . OutPoint . Hash . ToString ( ) )
} ;
} ) . ToArray ( ) ;
}
if ( command = = "toggle-input-selection" )
{
ModelState . Clear ( ) ;
return View ( vm ) ;
}
2020-04-27 12:12:01 +02:00
if ( ! string . IsNullOrEmpty ( bip21 ) )
{
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
try
{
2020-05-24 23:27:01 +02:00
BitcoinAddress . Create ( transactionOutput . DestinationAddress , network . NBitcoinNetwork ) ;
2019-12-03 06:26:52 +01:00
}
catch
{
2020-05-24 23:27:01 +02:00
var inputName =
string . Format ( CultureInfo . InvariantCulture , "Outputs[{0}]." , i . ToString ( CultureInfo . InvariantCulture ) ) +
2019-12-08 06:20:42 +01:00
nameof ( transactionOutput . DestinationAddress ) ;
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 ) ;
}
}
2019-05-21 10:10:07 +02:00
2020-05-24 23:27:01 +02:00
if ( ! ModelState . IsValid )
2018-10-31 16:19:25 +01:00
return View ( vm ) ;
2019-10-12 13:35:30 +02:00
DerivationSchemeSettings derivationScheme = GetDerivationSchemeSettings ( walletId ) ;
2019-05-11 17:05:30 +02:00
2019-05-13 01:55:26 +02:00
CreatePSBTResponse psbt = null ;
try
{
psbt = await CreatePSBT ( network , derivationScheme , vm , cancellation ) ;
}
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 ) ;
}
derivationScheme . RebaseKeyPaths ( psbt . PSBT ) ;
2020-05-24 21:55:28 +02:00
var signingContext = new SigningContextModel ( )
{
2020-06-17 14:43:56 +02:00
PayJoinBIP21 = vm . PayJoinBIP21 ,
2020-05-24 23:27:01 +02:00
EnforceLowR = psbt . Suggestions ? . ShouldEnforceLowR ,
ChangeAddress = psbt . ChangeAddress ? . ToString ( )
2020-05-24 21:55:28 +02:00
} ;
2020-05-24 23:27:01 +02:00
var res = await TryHandleSigningCommands ( walletId , psbt . PSBT , command , signingContext ) ;
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" ;
2020-05-24 21:55:28 +02:00
return RedirectToWalletPSBT ( new WalletPSBTViewModel ( )
{
PSBT = 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-05-24 23:27:01 +02:00
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 )
{
try
{
if ( bip21 . StartsWith ( network . UriScheme , StringComparison . InvariantCultureIgnoreCase ) )
{
bip21 = $"bitcoin{bip21.Substring(network.UriScheme.Length)}" ;
}
var uriBuilder = new NBitcoin . Payment . BitcoinUrlBuilder ( bip21 , network . NBitcoinNetwork ) ;
vm . Outputs = new List < WalletSendModel . TransactionOutput > ( )
{
new WalletSendModel . TransactionOutput ( )
{
Amount = uriBuilder . Amount . ToDecimal ( MoneyUnit . BTC ) ,
DestinationAddress = uriBuilder . Address . ToString ( ) ,
SubtractFeesFromOutput = false
}
} ;
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
{
2020-03-26 11:59:28 +01:00
vm . Outputs = new List < WalletSendModel . TransactionOutput > ( )
{
new WalletSendModel . TransactionOutput ( )
{
DestinationAddress = BitcoinAddress . Create ( bip21 , network . NBitcoinNetwork ) . ToString ( )
}
} ;
}
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 ( ) ,
WebsocketPath = this . Url . Action ( nameof ( VaultController . VaultBridgeConnection ) , "Vault" , new { walletId = walletId . ToString ( ) } )
} ) ;
}
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
}
2020-05-24 21:55:28 +02:00
private IActionResult RedirectToWalletPSBTReady ( WalletPSBTReadyViewModel vm )
2020-02-13 14:06:00 +01:00
{
2020-05-24 21:55:28 +02:00
var redirectVm = new PostRedirectViewModel ( )
2020-02-13 14:06:00 +01:00
{
AspController = "Wallets" ,
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
redirectVm . Parameters . Add ( new KeyValuePair < string , string > ( "command" , "broadcast" ) ) ;
}
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
{
2020-05-24 21:55:28 +02:00
var redirectVm = new PostRedirectViewModel ( )
2019-11-08 12:21:33 +01:00
{
AspController = "Wallets" ,
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
{
2019-05-15 08:00:09 +02: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
{
2019-12-23 14:24:29 +01:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) , "The master fingerprint does not match the one set in your wallet settings. Probable cause 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
2020-05-24 23:34:49 +02:00
var changed = PSBTChanged ( psbt , ( ) = > psbt . SignAll ( settings . AccountDerivation , signingKey , rootedKeyPath , new SigningOptions ( )
{
EnforceLowR = ! ( viewModel . SigningContext ? . EnforceLowR is false )
} ) ) ;
2019-12-23 14:24:29 +01:00
if ( ! changed )
2019-05-14 18:03:48 +02:00
{
2019-12-23 14:24:29 +01:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) , "Impossible to sign the transaction. Probable cause: Incorrect account key path in wallet settings, 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 ( ) ;
2020-05-24 21:55:28 +02:00
return RedirectToWalletPSBTReady ( new WalletPSBTReadyViewModel ( )
{
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-12-23 14:24:29 +01:00
private bool PSBTChanged ( PSBT psbt , Action act )
{
var before = psbt . ToBase64 ( ) ;
act ( ) ;
var after = psbt . ToBase64 ( ) ;
return before ! = after ;
}
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
}
2018-10-26 16:07:39 +02:00
[HttpGet]
[Route("{walletId}/rescan")]
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 ) ;
}
[HttpPost]
[Route("{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
private string GetCurrencyCode ( string defaultLang )
{
if ( defaultLang = = null )
return null ;
try
{
var ri = new RegionInfo ( defaultLang ) ;
return ri . ISOCurrencySymbol ;
}
2018-10-09 16:48:14 +02:00
catch ( ArgumentException ) { }
2018-07-26 18:17:43 +02:00
return null ;
}
2019-10-12 13:35:30 +02:00
public StoreData CurrentStore
2018-07-26 17:08:07 +02:00
{
2019-10-12 13:35:30 +02:00
get
{
return HttpContext . GetStoreData ( ) ;
}
}
2018-07-26 17:08:07 +02:00
2020-05-16 22:07:24 +02:00
internal DerivationSchemeSettings GetDerivationSchemeSettings ( WalletId walletId )
2019-10-12 13:35:30 +02:00
{
var paymentMethod = CurrentStore
2018-10-09 16:48:14 +02:00
. GetSupportedPaymentMethods ( NetworkProvider )
2019-05-08 16:39:11 +02:00
. OfType < DerivationSchemeSettings > ( )
2018-07-26 17:08:07 +02:00
. FirstOrDefault ( p = > p . PaymentId . PaymentType = = Payments . PaymentTypes . BTCLike & & p . PaymentId . CryptoCode = = walletId . CryptoCode ) ;
return paymentMethod ;
}
2018-07-26 15:32:24 +02:00
private static async Task < string > GetBalanceString ( BTCPayWallet wallet , DerivationStrategyBase derivationStrategy )
{
using ( CancellationTokenSource cts = new CancellationTokenSource ( TimeSpan . FromSeconds ( 10 ) ) )
{
try
{
2020-05-03 18:04:34 +02:00
return ( await wallet . GetBalance ( derivationStrategy , cts . Token ) ) . ShowMoney ( wallet . Network
. Divisibility ) ;
2018-07-26 15:32:24 +02:00
}
catch
{
return "--" ;
}
}
}
private string GetUserId ( )
{
return _userManager . GetUserId ( User ) ;
}
2019-05-12 17:13:55 +02:00
[Route("{walletId}/settings")]
public async Task < IActionResult > WalletSettings (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId )
{
2019-10-12 13:35:30 +02:00
var derivationSchemeSettings = GetDerivationSchemeSettings ( walletId ) ;
2019-12-29 17:08:30 +01:00
if ( derivationSchemeSettings = = null | | derivationSchemeSettings . Network . ReadonlyWallet )
2019-05-12 17:13:55 +02:00
return NotFound ( ) ;
2019-05-12 17:30:28 +02:00
var store = ( await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ) ;
2019-05-12 17:13:55 +02:00
var vm = new WalletSettingsViewModel ( )
{
2020-05-12 15:32:33 +02:00
StoreName = store . StoreName ,
UriScheme = derivationSchemeSettings . Network . UriScheme ,
2019-05-12 17:13:55 +02:00
Label = derivationSchemeSettings . Label ,
DerivationScheme = derivationSchemeSettings . AccountDerivation . ToString ( ) ,
2019-05-12 17:30:28 +02:00
DerivationSchemeInput = derivationSchemeSettings . AccountOriginal ,
2020-04-29 08:28:13 +02:00
SelectedSigningKey = derivationSchemeSettings . SigningKey . ToString ( ) ,
NBXSeedAvailable = await CanUseHotWallet ( ) & & ! string . IsNullOrEmpty ( await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode )
. GetMetadataAsync < string > ( GetDerivationSchemeSettings ( walletId ) . AccountDerivation ,
WellknownMetadataKeys . MasterHDKey ) )
2019-05-12 17:13:55 +02:00
} ;
vm . AccountKeys = derivationSchemeSettings . AccountKeySettings
. Select ( e = > new WalletSettingsAccountKeyViewModel ( )
{
AccountKey = e . AccountKey . ToString ( ) ,
MasterFingerprint = e . RootFingerprint is HDFingerprint fp ? fp . ToString ( ) : null ,
AccountKeyPath = e . AccountKeyPath = = null ? "" : $"m/{e.AccountKeyPath}"
} ) . ToList ( ) ;
return View ( vm ) ;
}
[Route("{walletId}/settings")]
[HttpPost]
public async Task < IActionResult > WalletSettings (
2020-04-29 08:28:13 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId , WalletSettingsViewModel vm , string command = "save" ,
CancellationToken cancellationToken = default )
2019-05-12 17:13:55 +02:00
{
if ( ! ModelState . IsValid )
return View ( vm ) ;
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
if ( command = = "save" )
2019-05-12 17:13:55 +02:00
{
2019-11-17 09:13:09 +01:00
derivationScheme . Label = vm . Label ;
2020-04-29 08:28:13 +02:00
derivationScheme . SigningKey = string . IsNullOrEmpty ( vm . SelectedSigningKey )
? null
: new BitcoinExtPubKey ( vm . SelectedSigningKey , derivationScheme . Network . NBitcoinNetwork ) ;
2019-11-17 09:13:09 +01:00
for ( int i = 0 ; i < derivationScheme . AccountKeySettings . Length ; i + + )
{
2020-04-29 08:28:13 +02:00
derivationScheme . AccountKeySettings [ i ] . AccountKeyPath =
string . IsNullOrWhiteSpace ( vm . AccountKeys [ i ] . AccountKeyPath )
? null
: new KeyPath ( vm . AccountKeys [ i ] . AccountKeyPath ) ;
derivationScheme . AccountKeySettings [ i ] . RootFingerprint =
string . IsNullOrWhiteSpace ( vm . AccountKeys [ i ] . MasterFingerprint )
? ( HDFingerprint ? ) null
: new HDFingerprint ( Encoders . Hex . DecodeData ( vm . AccountKeys [ i ] . MasterFingerprint ) ) ;
2019-11-17 09:13:09 +01:00
}
2020-04-29 08:28:13 +02:00
2019-11-17 09:13:09 +01:00
var store = ( await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ) ;
store . SetSupportedPaymentMethod ( derivationScheme ) ;
await Repository . UpdateStore ( store ) ;
TempData [ WellKnownTempData . SuccessMessage ] = "Wallet settings updated" ;
return RedirectToAction ( nameof ( WalletSettings ) ) ;
}
else if ( command = = "prune" )
{
2020-04-29 08:28:13 +02:00
var result = await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode )
. PruneAsync ( derivationScheme . AccountDerivation , new PruneRequest ( ) , cancellationToken ) ;
2019-11-17 09:13:09 +01:00
if ( result . TotalPruned = = 0 )
{
TempData [ WellKnownTempData . SuccessMessage ] = $"The wallet is already pruned" ;
}
else
{
2020-04-29 08:28:13 +02:00
TempData [ WellKnownTempData . SuccessMessage ] =
$"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)" ;
2019-11-17 09:13:09 +01:00
}
2020-04-29 08:28:13 +02:00
return RedirectToAction ( nameof ( WalletSettings ) ) ;
}
else if ( command = = "view-seed" & & await CanUseHotWallet ( ) )
{
2020-05-24 23:27:01 +02:00
var seed = await ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode )
2020-04-29 08:28:13 +02:00
. GetMetadataAsync < string > ( derivationScheme . AccountDerivation ,
WellknownMetadataKeys . Mnemonic , cancellationToken ) ;
if ( string . IsNullOrEmpty ( seed ) )
{
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
2020-05-24 23:27:01 +02:00
Severity = StatusMessageModel . StatusSeverity . Error ,
Message = "The seed was not found"
2020-04-29 08:28:13 +02:00
} ) ;
}
else
{
2020-07-15 19:51:01 +02:00
var recoveryVm = new RecoverySeedBackupViewModel ( )
{
CryptoCode = walletId . CryptoCode ,
Mnemonic = seed ,
IsStored = true ,
2020-08-05 11:20:34 +02:00
RequireConfirm = false ,
2020-07-15 19:51:01 +02:00
ReturnUrl = Url . Action ( nameof ( WalletSettings ) , new { walletId } )
} ;
return this . RedirectToRecoverySeedBackup ( recoveryVm ) ;
2020-04-29 08:28:13 +02:00
}
2020-05-24 23:27:01 +02:00
2019-11-17 09:13:09 +01:00
return RedirectToAction ( nameof ( WalletSettings ) ) ;
}
else
{
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 ) ;
return "/" + res ;
}
}
public class WalletReceiveViewModel
{
public string CryptoImage { get ; set ; }
public string CryptoCode { get ; set ; }
public string Address { get ; set ; }
2018-07-26 15:32:24 +02:00
}
public class GetInfoResult
{
}
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
}
}