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 ;
using BTCPayServer.HostedServices ;
using BTCPayServer.ModelBinders ;
using BTCPayServer.Models ;
using BTCPayServer.Models.WalletViewModels ;
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 ;
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 ;
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")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[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 ;
private readonly IOptions < MvcJsonOptions > _mvcJsonOptions ;
private readonly NBXplorerDashboard _dashboard ;
2018-10-09 16:48:14 +02:00
2018-07-26 15:32:24 +02:00
private readonly IFeeProviderFactory _feeRateProvider ;
private readonly BTCPayWalletProvider _walletProvider ;
2018-10-09 16:48:14 +02:00
public RateFetcher RateFetcher { get ; }
2018-10-31 16:19:25 +01:00
[TempData]
public string StatusMessage { get ; set ; }
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 ,
IOptions < MvcJsonOptions > mvcJsonOptions ,
NBXplorerDashboard dashboard ,
2018-08-22 09:53:40 +02:00
RateFetcher rateProvider ,
2018-07-26 15:32:24 +02:00
ExplorerClientProvider explorerProvider ,
IFeeProviderFactory feeRateProvider ,
BTCPayWalletProvider walletProvider )
{
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 ;
NetworkProvider = networkProvider ;
2018-07-26 15:32:24 +02:00
_userManager = userManager ;
_mvcJsonOptions = mvcJsonOptions ;
_dashboard = dashboard ;
2018-10-09 16:48:14 +02:00
ExplorerClientProvider = explorerProvider ;
2018-07-26 15:32:24 +02:00
_feeRateProvider = feeRateProvider ;
_walletProvider = walletProvider ;
}
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" ,
} ;
[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 05:41:12 +02:00
if ( Request ? . Form ? . TryGetValue ( nameof ( addcomment ) , out _ ) is true )
{
addcomment = string . Empty ;
}
2019-08-02 17:55:27 +02:00
}
2019-08-03 05:41:12 +02:00
catch { }
/////////
2019-08-02 17:42:30 +02:00
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings ( walletId ) ;
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 )
{
addlabel = addlabel . Trim ( ) . ToLowerInvariant ( ) ;
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 )
{
removelabel = removelabel . Trim ( ) . ToLowerInvariant ( ) ;
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 )
{
addcomment = addcomment . Trim ( ) ;
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 ( ) } ) ;
}
2018-07-26 15:32:24 +02:00
public async Task < IActionResult > ListWallets ( )
{
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 ) ) )
. Where ( _ = > _ . Wallet ! = null )
. 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 ;
if ( ! wallet . Store . HasClaim ( Policies . CanModifyStoreSettings . Key ) )
{
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 ;
walletVm . IsOwner = wallet . Store . HasClaim ( Policies . CanModifyStoreSettings . Key ) ;
}
return View ( wallets ) ;
}
[HttpGet]
[Route("{walletId}")]
2018-07-26 17:08:07 +02:00
public async Task < IActionResult > WalletTransactions (
2018-07-26 15:32:24 +02:00
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId )
{
2019-05-12 04:07:41 +02:00
DerivationSchemeSettings paymentMethod = await 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-08-02 17:42:30 +02:00
foreach ( var tx in transactions . UnconfirmedTransactions . Transactions . Concat ( transactions . ConfirmedTransactions . Transactions ) . ToArray ( ) )
2018-07-26 17:08:07 +02:00
{
var vm = new ListTransactionsViewModel . TransactionViewModel ( ) ;
model . Transactions . Add ( vm ) ;
vm . Id = tx . TransactionId . ToString ( ) ;
vm . Link = string . Format ( CultureInfo . InvariantCulture , paymentMethod . Network . BlockExplorerLink , vm . Id ) ;
vm . Timestamp = tx . Timestamp ;
vm . Positive = tx . BalanceChange > = Money . Zero ;
vm . Balance = tx . BalanceChange . ToString ( ) ;
2018-11-05 09:26:49 +01:00
vm . IsConfirmed = tx . Confirmations ! = 0 ;
2019-08-02 17:42:30 +02:00
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 ;
}
2018-07-26 17:08:07 +02:00
}
2018-07-27 05:03:56 +02:00
model . Transactions = model . Transactions . OrderByDescending ( t = > t . Timestamp ) . ToList ( ) ;
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
[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-05-12 04:07:41 +02:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings ( walletId , store ) ;
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 ) ;
2018-10-31 16:19:25 +01:00
if ( network = = null )
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 ) ;
2018-10-31 16:19:25 +01:00
model . CurrentBalance = ( await balance ) . ToDecimal ( MoneyUnit . BTC ) ;
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 ) ;
2018-10-31 16:19:25 +01:00
if ( network = = null )
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 ) ;
}
var destination = ParseDestination ( transactionOutput . DestinationAddress , network . NBitcoinNetwork ) ;
if ( destination = = null )
ModelState . AddModelError ( nameof ( transactionOutput . DestinationAddress ) , "Invalid address" ) ;
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" ,
ModelState ) ;
}
}
if ( subtractFeesOutputsCount . Count > 1 )
{
foreach ( var subtractFeesOutput in subtractFeesOutputsCount )
{
vm . AddModelError ( model = > model . Outputs [ subtractFeesOutput ] . SubtractFeesFromOutput ,
"You can only subtract fees from one output" , ModelState ) ;
}
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 ,
"You are sending more than what you own" , ModelState ) ;
}
}
2018-10-31 16:19:25 +01:00
if ( ! ModelState . IsValid )
return View ( vm ) ;
2019-05-12 04:07:41 +02:00
DerivationSchemeSettings derivationScheme = await 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-05-14 18:03:48 +02:00
case "ledger" :
return ViewWalletSendLedger ( psbt . PSBT , psbt . ChangeAddress ) ;
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-05-30 17:00:20 +02:00
return RedirectToAction ( nameof ( WalletPSBT ) , new { walletId = walletId , psbt = psbt . PSBT . ToBase64 ( ) , FileName = name } ) ;
2019-05-14 18:03:48 +02:00
default :
return View ( vm ) ;
2019-05-08 07:39:37 +02:00
}
2019-05-21 10:10:07 +02:00
2019-05-08 07:39:37 +02:00
}
2019-05-11 13:26:31 +02:00
private ViewResult ViewWalletSendLedger ( PSBT psbt , BitcoinAddress hintChange = null )
{
return View ( "WalletSendLedger" , new WalletSendLedgerModel ( )
{
PSBT = psbt . ToBase64 ( ) ,
HintChange = hintChange ? . ToString ( ) ,
WebsocketPath = this . Url . Action ( nameof ( LedgerConnection ) ) ,
2019-05-11 17:05:30 +02:00
SuccessPath = this . Url . Action ( nameof ( WalletPSBTReady ) )
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 )
{
return View ( viewModel ) ;
}
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 )
{
return View ( viewModel ) ;
}
2019-05-15 12:00:26 +02:00
ExtKey signingKey = null ;
2019-05-15 08:00:09 +02:00
var settings = ( await GetDerivationSchemeSettings ( walletId ) ) ;
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 ( ) ;
// The user gave the root key, let's try to rebase the PSBT, and derive the account private key
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 ) ;
}
// The user maybe gave the account key, let's try to sign with it
else
{
signingKey = extKey ;
2019-05-14 18:03:48 +02:00
}
2019-05-19 16:27:18 +02:00
var balanceChange = psbt . GetBalance ( settings . AccountDerivation , signingKey , rootedKeyPath ) ;
2019-05-15 08:00:09 +02:00
if ( balanceChange = = Money . Zero )
2019-05-14 18:03:48 +02:00
{
2019-05-15 08:00:09 +02:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) , "This seed does not seem to be able to sign this transaction. Either this is the wrong key, or Wallet Settings have not the correct account path in the wallet settings." ) ;
return View ( viewModel ) ;
}
2019-05-19 16:27:18 +02:00
psbt . SignAll ( settings . AccountDerivation , signingKey , rootedKeyPath ) ;
2019-05-15 08:00:09 +02:00
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-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
2018-10-31 16:19:25 +01:00
private IDestination [ ] ParseDestination ( string destination , Network network )
{
try
{
2018-11-01 04:54:25 +01:00
destination = destination ? . Trim ( ) ;
2018-10-31 16:19:25 +01:00
return new IDestination [ ] { BitcoinAddress . Create ( destination , network ) } ;
}
catch
{
return null ;
}
}
2019-05-11 17:05:30 +02:00
private async Task < IActionResult > RedirectToWalletTransaction ( WalletId walletId , Transaction transaction )
{
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-05-12 04:07:41 +02:00
var derivationSettings = await GetDerivationSchemeSettings ( walletId ) ;
2019-05-11 17:05:30 +02:00
wallet . InvalidateCache ( derivationSettings . AccountDerivation ) ;
StatusMessage = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})" ;
}
return RedirectToAction ( nameof ( WalletTransactions ) ) ;
}
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-05-12 04:07:41 +02:00
DerivationSchemeSettings paymentMethod = await 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 ) ;
2018-10-26 16:07:39 +02:00
vm . IsServerAdmin = User . Claims . Any ( c = > c . Type = = Policies . CanModifyServerSettings . Key & & c . Value = = "true" ) ;
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-06-08 05:41:44 +02:00
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)]
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-05-12 04:07:41 +02:00
DerivationSchemeSettings paymentMethod = await 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-05-12 04:07:41 +02:00
private DerivationSchemeSettings GetDerivationSchemeSettings ( WalletId walletId , StoreData store )
2018-07-26 17:08:07 +02:00
{
if ( store = = null | | ! store . HasClaim ( Policies . CanModifyStoreSettings . Key ) )
return null ;
var paymentMethod = store
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 ;
}
2019-05-12 04:07:41 +02:00
private async Task < DerivationSchemeSettings > GetDerivationSchemeSettings ( WalletId walletId )
{
var store = ( await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ) ;
return GetDerivationSchemeSettings ( walletId , store ) ;
}
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
{
return ( await wallet . GetBalance ( derivationStrategy , cts . Token ) ) . ToString ( ) ;
}
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 psbt = null ,
string hintChange = null
2018-07-26 15:32:24 +02:00
)
{
if ( ! HttpContext . WebSockets . IsWebSocketRequest )
return NotFound ( ) ;
2018-10-31 16:19:25 +01:00
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" ) ;
2018-12-26 07:04:11 +01:00
var storeData = ( await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ) ;
2019-05-12 04:07:41 +02:00
var derivationSettings = GetDerivationSchemeSettings ( walletId , storeData ) ;
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 ) ;
2019-05-08 07:39:37 +02:00
var model = new WalletSendLedgerModel ( ) ;
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-05-11 13:02:32 +02:00
var psbtResponse = new CreatePSBTResponse ( )
{
PSBT = PSBT . Parse ( psbt , network . NBitcoinNetwork ) ,
ChangeAddress = string . IsNullOrEmpty ( hintChange ) ? null : BitcoinAddress . Create ( hintChange , network . NBitcoinNetwork )
} ;
2019-05-12 04:07:41 +02:00
derivationSettings . RebaseKeyPaths ( psbtResponse . PSBT ) ;
2018-07-26 15:32:24 +02:00
signTimeout . CancelAfter ( TimeSpan . FromMinutes ( 5 ) ) ;
2019-05-14 09:06:43 +02:00
psbtResponse . PSBT = await hw . SignTransactionAsync ( psbtResponse . PSBT , accountKey . GetRootedKeyPath ( ) , accountKey . AccountKey , psbtResponse . ChangeAddress ? . ScriptPubKey , signTimeout . Token ) ;
2019-05-11 17:05:30 +02:00
result = new SendToAddressResult ( ) { PSBT = psbtResponse . 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 ) ;
var bytes = UTF8NOBOM . GetBytes ( JsonConvert . SerializeObject ( result , _mvcJsonOptions . Value . SerializerSettings ) ) ;
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 )
{
var derivationSchemeSettings = await GetDerivationSchemeSettings ( walletId ) ;
if ( derivationSchemeSettings = = null )
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))]
WalletId walletId , WalletSettingsViewModel vm )
{
if ( ! ModelState . IsValid )
return View ( vm ) ;
var derivationScheme = await GetDerivationSchemeSettings ( walletId ) ;
if ( derivationScheme = = null )
return NotFound ( ) ;
derivationScheme . Label = vm . Label ;
2019-05-15 08:00:09 +02:00
derivationScheme . SigningKey = string . IsNullOrEmpty ( vm . SelectedSigningKey ) ? null : new BitcoinExtPubKey ( vm . SelectedSigningKey , derivationScheme . Network . NBitcoinNetwork ) ;
2019-05-12 17:13:55 +02:00
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 ) ;
StatusMessage = "Wallet settings updated" ;
return RedirectToAction ( nameof ( WalletSettings ) ) ;
}
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
}
}