2018-07-26 15:32:24 +02:00
using System ;
using System.Collections.Generic ;
2019-05-14 18:03:48 +02:00
using System.ComponentModel.DataAnnotations ;
2018-07-26 15:32:24 +02:00
using System.Globalization ;
using System.Linq ;
using System.Net.WebSockets ;
using System.Text ;
using System.Threading ;
using System.Threading.Tasks ;
using BTCPayServer.Data ;
2020-01-18 06:12:27 +01:00
using BTCPayServer.Events ;
2018-07-26 15:32:24 +02:00
using BTCPayServer.HostedServices ;
using BTCPayServer.ModelBinders ;
using BTCPayServer.Models ;
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 ;
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 LedgerWallet ;
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Http ;
using Microsoft.AspNetCore.Identity ;
using Microsoft.AspNetCore.Mvc ;
2019-05-14 18:03:48 +02:00
using Microsoft.EntityFrameworkCore.Metadata.Internal ;
2020-01-18 06:12:27 +01:00
using Microsoft.Extensions.Caching.Memory ;
2018-07-26 15:32:24 +02:00
using Microsoft.Extensions.Options ;
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 ;
using static BTCPayServer . Controllers . StoresController ;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
2019-10-12 13:35:30 +02:00
[Authorize(Policy = Policies.CanModifyStoreSettings.Key, 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 ;
2018-10-09 16:48:14 +02:00
public RateFetcher RateFetcher { get ; }
2018-10-31 16:19:25 +01:00
2018-07-26 16:23:28 +02:00
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 ,
SettingsRepository settingsRepository )
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 ;
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
string [ ] LabelColorScheme = new string [ ]
{
"#fbca04" ,
"#0e8a16" ,
"#ff7619" ,
"#84b6eb" ,
"#5319e7" ,
"#000000" ,
"#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))]
2019-08-03 14:52:47 +02:00
WalletId walletId , string transactionId ,
string addlabel = null ,
string addlabelclick = null ,
string addcomment = null ,
string removelabel = null )
2019-08-02 17:42:30 +02:00
{
addlabel = addlabel ? ? addlabelclick ;
2019-08-03 05:41:12 +02:00
// Hack necessary when the user enter a empty comment and submit.
// For some reason asp.net consider addcomment null instead of empty string...
try
2019-08-02 17:55:27 +02:00
{
2019-08-03 16:02:15 +02:00
if ( addcomment = = null & & Request ? . Form ? . TryGetValue ( nameof ( addcomment ) , out _ ) is true )
2019-08-03 05:41:12 +02:00
{
addcomment = string . Empty ;
}
2019-08-02 17:55:27 +02:00
}
2019-08-03 05:41:12 +02:00
catch { }
/////////
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 )
{
2019-08-03 15:06:14 +02:00
addlabel = addlabel . Trim ( ) . ToLowerInvariant ( ) . Replace ( ',' , ' ' ) . Truncate ( MaxLabelSize ) ;
2019-08-02 17:42:30 +02:00
var labels = walletBlobInfo . GetLabels ( ) ;
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 ( ) )
. ThenBy ( k = > Array . IndexOf ( LabelColorScheme , k . Key ) )
. 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 )
{
2019-08-03 15:03:49 +02:00
removelabel = removelabel . Trim ( ) . ToLowerInvariant ( ) . Truncate ( MaxLabelSize ) ;
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))]
2019-08-03 16:10:45 +02:00
WalletId walletId , string labelFilter = null )
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 ;
2018-07-26 17:08:07 +02:00
var model = new ListTransactionsViewModel ( ) ;
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 ;
vm . Balance = tx . BalanceChange . ToString ( ) ;
vm . IsConfirmed = tx . Confirmations ! = 0 ;
if ( walletTransactionsInfo . TryGetValue ( tx . TransactionId . ToString ( ) , out var transactionInfo ) )
{
var labels = walletBlob . GetLabels ( transactionInfo ) ;
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
2019-12-29 17:08:30 +01:00
model . Transactions = model . Transactions . OrderByDescending ( t = > t . Timestamp ) . 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 )
. 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-01-18 07:32:01 +01: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 isAdmin = ( await _authorizationService . AuthorizeAsync ( User , Policies . CanModifyServerSettings . Key ) ) . Succeeded ;
if ( isAdmin )
return true ;
var policies = await _settingsRepository . GetSettingAsync < PoliciesSettings > ( ) ;
return policies ? . AllowHotWalletForAll is true ;
}
2018-07-26 17:08:07 +02:00
[HttpGet]
[Route("{walletId}/send")]
public async Task < IActionResult > WalletSend (
[ModelBinder(typeof(WalletIdModelBinder))]
2019-05-08 05:34:33 +02:00
WalletId walletId , string defaultDestination = null , string defaultAmount = 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 ) ;
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
} ;
2019-05-21 10:10:07 +02:00
2018-10-31 16:19:25 +01:00
var feeProvider = _feeRateProvider . CreateFeeProvider ( network ) ;
var recommendedFees = feeProvider . GetFeeRateAsync ( ) ;
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 ,
WellknownMetadataKeys . Mnemonic ) ) ;
2019-12-02 09:57:38 +01:00
model . CurrentBalance = await balance ;
2018-10-31 16:19:25 +01:00
model . RecommendedSatoshiPerByte = ( int ) ( await recommendedFees ) . GetFee ( 1 ) . Satoshi ;
model . FeeSatoshiPerByte = model . RecommendedSatoshiPerByte ;
2019-05-08 08:24:20 +02:00
model . SupportRBF = network . SupportRBF ;
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 ;
2018-07-26 16:23:28 +02:00
model . Divisibility = _currencyTable . GetNumberFormatInfo ( currencyPair . Right , true ) . CurrencyDecimalDigits ;
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 ) ;
}
2018-10-31 16:19:25 +01:00
[HttpPost]
[Route("{walletId}/send")]
public async Task < IActionResult > WalletSend (
[ModelBinder(typeof(WalletIdModelBinder))]
2019-05-21 10:10:07 +02:00
WalletId walletId , WalletSendModel vm , string command = "" , CancellationToken cancellation = default )
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 ;
2019-05-21 10:10:07 +02:00
decimal transactionAmountSum = 0 ;
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 ( ) ;
2019-05-21 10:10:07 +02:00
var index = int . Parse ( command . Substring ( command . IndexOf ( ":" , StringComparison . InvariantCultureIgnoreCase ) + 1 ) , CultureInfo . InvariantCulture ) ;
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
{
BitcoinAddress . Create ( transactionOutput . DestinationAddress , network . NBitcoinNetwork ) ;
}
catch
{
2019-12-08 06:20:42 +01:00
var inputName =
string . Format ( CultureInfo . InvariantCulture , "Outputs[{0}]." , i . ToString ( CultureInfo . InvariantCulture ) ) +
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
}
2019-05-21 12:04:39 +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
}
}
2019-12-08 06:20:42 +01: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 ) ;
2019-05-21 10:10:07 +02:00
2019-05-14 18:03:48 +02:00
switch ( command )
2019-05-08 07:39:37 +02:00
{
2019-11-11 06:22:04 +01:00
case "vault" :
return ViewVault ( walletId , psbt . PSBT ) ;
2020-01-21 09:33:12 +01:00
case "nbx-seed" :
var extKey = await ExplorerClientProvider . GetExplorerClient ( network )
. GetMetadataAsync < string > ( derivationScheme . AccountDerivation , WellknownMetadataKeys . MasterHDKey , cancellation ) ;
return await SignWithSeed ( walletId , new SignWithSeedViewModel ( )
{
SeedOrKey = extKey ,
PSBT = psbt . PSBT . ToBase64 ( )
} ) ;
2019-05-14 18:03:48 +02:00
case "ledger" :
2020-01-21 13:00:34 +01:00
return ViewWalletSendLedger ( walletId , psbt . PSBT , psbt . ChangeAddress ) ;
2019-05-14 18:03:48 +02:00
case "seed" :
2019-05-15 08:00:09 +02:00
return SignWithSeed ( walletId , psbt . PSBT . ToBase64 ( ) ) ;
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" ;
2019-11-08 12:21:33 +01:00
return RedirectToWalletPSBT ( walletId , psbt . PSBT , name ) ;
2019-05-14 18:03:48 +02:00
default :
return View ( vm ) ;
2019-05-08 07:39:37 +02:00
}
2019-05-21 10:10:07 +02:00
2019-05-08 07:39:37 +02:00
}
2019-11-11 06:22:04 +01:00
private IActionResult ViewVault ( WalletId walletId , PSBT psbt )
{
return View ( "WalletSendVault" , new WalletSendVaultModel ( )
{
WalletId = walletId . ToString ( ) ,
PSBT = psbt . ToBase64 ( ) ,
WebsocketPath = this . Url . Action ( nameof ( VaultController . VaultBridgeConnection ) , "Vault" , new { walletId = walletId . ToString ( ) } )
} ) ;
}
2019-11-08 12:21:33 +01:00
private IActionResult RedirectToWalletPSBT ( WalletId walletId , PSBT psbt , string fileName = null )
{
2019-11-08 13:42:34 +01:00
var vm = new PostRedirectViewModel ( )
2019-11-08 12:21:33 +01:00
{
AspController = "Wallets" ,
AspAction = nameof ( WalletPSBT ) ,
Parameters =
{
new KeyValuePair < string , string > ( "psbt" , psbt . ToBase64 ( ) )
}
} ;
if ( ! string . IsNullOrEmpty ( fileName ) )
vm . Parameters . Add ( new KeyValuePair < string , string > ( "fileName" , fileName ) ) ;
return View ( "PostRedirect" , vm ) ;
}
void SetAmbientPSBT ( PSBT psbt )
{
if ( psbt ! = null )
TempData [ "AmbientPSBT" ] = psbt . ToBase64 ( ) ;
else
TempData . Remove ( "AmbientPSBT" ) ;
}
PSBT GetAmbientPSBT ( Network network , bool peek )
{
if ( network = = null )
throw new ArgumentNullException ( nameof ( network ) ) ;
if ( ( peek ? TempData . Peek ( "AmbientPSBT" ) : TempData [ "AmbientPSBT" ] ) is string str )
{
try
{
return PSBT . Parse ( str , network ) ;
}
catch { }
}
return null ;
}
2020-01-21 13:00:34 +01:00
private ViewResult ViewWalletSendLedger ( WalletId walletId , PSBT psbt , BitcoinAddress hintChange = null )
2019-05-11 13:26:31 +02:00
{
2019-11-08 12:21:33 +01:00
SetAmbientPSBT ( psbt ) ;
2019-05-11 13:26:31 +02:00
return View ( "WalletSendLedger" , new WalletSendLedgerModel ( )
{
PSBT = psbt . ToBase64 ( ) ,
HintChange = hintChange ? . ToString ( ) ,
2020-01-21 13:00:34 +01:00
WebsocketPath = this . Url . Action ( nameof ( LedgerConnection ) , new { walletId = walletId . ToString ( ) } )
2019-05-11 13:26:31 +02:00
} ) ;
}
2019-05-14 18:03:48 +02:00
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2019-05-15 08:00:09 +02:00
WalletId walletId , string psbt )
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
{
2019-05-15 08:00:09 +02:00
PSBT = psbt
2019-05-14 18:03:48 +02:00
} ) ;
}
[HttpPost("{walletId}/psbt/seed")]
public async Task < IActionResult > SignWithSeed ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
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" ) ;
}
var psbt = PSBT . Parse ( viewModel . PSBT , network . NBitcoinNetwork ) ;
if ( ! psbt . IsReadyToSign ( ) )
{
ModelState . AddModelError ( nameof ( viewModel . PSBT ) , "PSBT is not ready to be signed" ) ;
}
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-01-21 09:33:12 +01:00
return View ( "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." ) ;
return View ( viewModel ) ;
2019-05-14 18:03:48 +02:00
}
2019-12-23 14:24:29 +01:00
var changed = PSBTChanged ( psbt , ( ) = > psbt . SignAll ( settings . AccountDerivation , signingKey , rootedKeyPath ) ) ;
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." ) ;
2019-05-15 08:00:09 +02:00
return View ( viewModel ) ;
}
ModelState . Remove ( nameof ( viewModel . PSBT ) ) ;
2019-06-16 05:32:00 +02:00
return await WalletPSBTReady ( walletId , psbt . ToBase64 ( ) , signingKey . GetWif ( network . NBitcoinNetwork ) . ToString ( ) , rootedKeyPath ? . ToString ( ) ) ;
2019-05-15 08:00:09 +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-31 04:29:59 +01:00
TempData [ WellKnownTempData . SuccessMessage ] = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})" ;
2019-05-11 17:05:30 +02:00
}
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 ) ;
2019-10-12 13:35:30 +02:00
vm . IsServerAdmin = ( await _authorizationService . AuthorizeAsync ( User , Policies . CanModifyServerSettings . Key ) ) . 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")]
2019-10-12 13:35:30 +02:00
[Authorize(Policy = Policies.CanModifyServerSettings.Key, 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
2019-10-12 13:35:30 +02:00
private DerivationSchemeSettings GetDerivationSchemeSettings ( WalletId walletId )
{
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
{
2019-12-03 05:53:50 +01:00
return ( await wallet . GetBalance ( derivationStrategy , cts . Token ) ) . ToString ( CultureInfo . InvariantCulture ) ;
2018-07-26 15:32:24 +02:00
}
catch
{
return "--" ;
}
}
}
private string GetUserId ( )
{
return _userManager . GetUserId ( User ) ;
}
[HttpGet]
2018-10-31 16:19:25 +01:00
[Route("{walletId}/send/ledger/ws")]
2018-07-26 15:32:24 +02:00
public async Task < IActionResult > LedgerConnection (
2018-10-31 16:19:25 +01:00
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId ,
2018-07-26 15:32:24 +02:00
string command ,
// getinfo
// getxpub
int account = 0 ,
// sendtoaddress
2019-05-11 13:02:32 +02:00
string hintChange = null
2018-07-26 15:32:24 +02:00
)
{
if ( ! HttpContext . WebSockets . IsWebSocketRequest )
return NotFound ( ) ;
2019-10-12 13:35:30 +02:00
var storeData = CurrentStore ;
2019-05-29 11:43:50 +02:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-11 13:02:32 +02:00
if ( network = = null )
throw new FormatException ( "Invalid value for crypto code" ) ;
2019-11-08 12:21:33 +01:00
PSBT psbt = GetAmbientPSBT ( network . NBitcoinNetwork , true ) ;
2019-10-12 13:35:30 +02:00
var derivationSettings = GetDerivationSchemeSettings ( walletId ) ;
2018-10-31 16:19:25 +01:00
2018-07-26 15:32:24 +02:00
var webSocket = await HttpContext . WebSockets . AcceptWebSocketAsync ( ) ;
using ( var normalOperationTimeout = new CancellationTokenSource ( ) )
using ( var signTimeout = new CancellationTokenSource ( ) )
{
normalOperationTimeout . CancelAfter ( TimeSpan . FromMinutes ( 30 ) ) ;
2019-05-10 07:36:25 +02:00
var hw = new LedgerHardwareWalletService ( webSocket ) ;
2018-07-26 15:32:24 +02:00
object result = null ;
try
{
2018-10-31 16:19:25 +01:00
if ( command = = "test" )
{
result = await hw . Test ( normalOperationTimeout . Token ) ;
2018-07-26 15:32:24 +02:00
}
if ( command = = "sendtoaddress" )
{
if ( ! _dashboard . IsFullySynched ( network . CryptoCode , out var summary ) )
throw new Exception ( $"{network.CryptoCode}: not started or fully synched" ) ;
2019-05-02 11:56:01 +02:00
2019-05-12 17:30:28 +02:00
var accountKey = derivationSettings . GetSigningAccountKeySettings ( ) ;
2019-05-09 18:05:37 +02:00
// Some deployment does not have the AccountKeyPath set, let's fix this...
2019-05-12 17:13:55 +02:00
if ( accountKey . AccountKeyPath = = null )
2018-07-26 15:32:24 +02:00
{
2018-12-26 07:10:00 +01:00
// If the saved wallet key path is not present or incorrect, let's scan the wallet to see if it can sign strategy
2019-05-10 03:55:10 +02:00
var foundKeyPath = await hw . FindKeyPathFromDerivation ( network ,
derivationSettings . AccountDerivation ,
2019-05-10 03:48:30 +02:00
normalOperationTimeout . Token ) ;
2019-05-12 17:13:55 +02:00
accountKey . AccountKeyPath = foundKeyPath ? ? throw new HardwareWalletException ( $"This store is not configured to use this ledger" ) ;
2019-05-08 16:39:11 +02:00
storeData . SetSupportedPaymentMethod ( derivationSettings ) ;
2018-12-26 07:04:11 +01:00
await Repository . UpdateStore ( storeData ) ;
}
2019-05-10 03:48:30 +02:00
// If it has already the AccountKeyPath, we did not looked up for it, so we need to check if we are on the right ledger
2019-05-09 18:05:37 +02:00
else
{
// Checking if ledger is right with the RootFingerprint is faster as it does not need to make a query to the parent xpub,
// but some deployment does not have it, so let's use AccountKeyPath instead
2019-05-12 17:13:55 +02:00
if ( accountKey . RootFingerprint = = null )
2019-05-09 18:05:37 +02:00
{
2019-05-11 17:05:30 +02:00
2019-05-12 17:13:55 +02:00
var actualPubKey = await hw . GetExtPubKey ( network , accountKey . AccountKeyPath , normalOperationTimeout . Token ) ;
2019-05-09 18:05:37 +02:00
if ( ! derivationSettings . AccountDerivation . GetExtPubKeys ( ) . Any ( p = > p . GetPublicKey ( ) = = actualPubKey . GetPublicKey ( ) ) )
throw new HardwareWalletException ( $"This store is not configured to use this ledger" ) ;
}
// We have the root fingerprint, we can check the root from it
else
{
2019-05-10 03:48:30 +02:00
var actualPubKey = await hw . GetPubKey ( network , new KeyPath ( ) , normalOperationTimeout . Token ) ;
2019-05-12 17:13:55 +02:00
if ( actualPubKey . GetHDFingerPrint ( ) ! = accountKey . RootFingerprint . Value )
2019-05-09 18:05:37 +02:00
throw new HardwareWalletException ( $"This store is not configured to use this ledger" ) ;
}
}
2019-01-15 15:50:45 +01:00
2019-05-09 18:05:37 +02:00
// Some deployment does not have the RootFingerprint set, let's fix this...
2019-05-12 17:13:55 +02:00
if ( accountKey . RootFingerprint = = null )
2019-05-09 18:05:37 +02:00
{
2019-05-12 17:13:55 +02:00
accountKey . RootFingerprint = ( await hw . GetPubKey ( network , new KeyPath ( ) , normalOperationTimeout . Token ) ) . GetHDFingerPrint ( ) ;
2019-05-09 18:05:37 +02:00
storeData . SetSupportedPaymentMethod ( derivationSettings ) ;
await Repository . UpdateStore ( storeData ) ;
}
2018-07-26 15:32:24 +02:00
2019-11-08 12:21:33 +01:00
derivationSettings . RebaseKeyPaths ( psbt ) ;
var changeAddress = string . IsNullOrEmpty ( hintChange ) ? null : BitcoinAddress . Create ( hintChange , network . NBitcoinNetwork ) ;
2018-07-26 15:32:24 +02:00
signTimeout . CancelAfter ( TimeSpan . FromMinutes ( 5 ) ) ;
2019-11-08 12:21:33 +01:00
psbt = await hw . SignTransactionAsync ( psbt , accountKey . GetRootedKeyPath ( ) , accountKey . AccountKey , changeAddress ? . ScriptPubKey , signTimeout . Token ) ;
SetAmbientPSBT ( null ) ;
result = new SendToAddressResult ( ) { PSBT = psbt . ToBase64 ( ) } ;
2018-07-26 15:32:24 +02:00
}
}
catch ( OperationCanceledException )
{ result = new LedgerTestResult ( ) { Success = false , Error = "Timeout" } ; }
catch ( Exception ex )
{ result = new LedgerTestResult ( ) { Success = false , Error = ex . Message } ; }
finally { hw . Dispose ( ) ; }
try
{
if ( result ! = null )
{
UTF8Encoding UTF8NOBOM = new UTF8Encoding ( false ) ;
2019-10-03 10:06:49 +02:00
var bytes = UTF8NOBOM . GetBytes ( JsonConvert . SerializeObject ( result , _serializerSettings ) ) ;
2018-07-26 15:32:24 +02:00
await webSocket . SendAsync ( new ArraySegment < byte > ( bytes ) , WebSocketMessageType . Text , true , new CancellationTokenSource ( 2000 ) . Token ) ;
}
}
catch { }
finally
{
await webSocket . CloseSocket ( ) ;
}
}
return new EmptyResult ( ) ;
}
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 ( )
{
Label = derivationSchemeSettings . Label ,
DerivationScheme = derivationSchemeSettings . AccountDerivation . ToString ( ) ,
2019-05-12 17:30:28 +02:00
DerivationSchemeInput = derivationSchemeSettings . AccountOriginal ,
SelectedSigningKey = derivationSchemeSettings . SigningKey . ToString ( )
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 (
[ModelBinder(typeof(WalletIdModelBinder))]
2019-11-17 09:13:09 +01:00
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 ;
derivationScheme . SigningKey = string . IsNullOrEmpty ( vm . SelectedSigningKey ) ? null : new BitcoinExtPubKey ( vm . SelectedSigningKey , derivationScheme . Network . NBitcoinNetwork ) ;
for ( int i = 0 ; i < derivationScheme . AccountKeySettings . Length ; i + + )
{
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 ) ) ;
}
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" )
{
2019-12-01 15:30:56 +01: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
{
TempData [ WellKnownTempData . SuccessMessage ] = $"The wallet has been successfully pruned ({result.TotalPruned} transactions have been removed from the history)" ;
}
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
}
}