2018-07-26 22:32:24 +09:00
using System ;
using System.Collections.Generic ;
2019-05-14 16:03:48 +00:00
using System.ComponentModel.DataAnnotations ;
2018-07-26 22:32:24 +09: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 23:23:28 +09:00
using BTCPayServer.Services.Rates ;
2018-07-26 22:32:24 +09: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 16:03:48 +00:00
using Microsoft.EntityFrameworkCore.Metadata.Internal ;
2018-07-26 22:32:24 +09:00
using Microsoft.Extensions.Options ;
using NBitcoin ;
2019-05-13 00:13:55 +09:00
using NBitcoin.DataEncoders ;
2018-07-26 22:32:24 +09:00
using NBXplorer.DerivationStrategy ;
2018-10-26 23:07:39 +09:00
using NBXplorer.Models ;
2018-07-26 22:32:24 +09:00
using Newtonsoft.Json ;
using static BTCPayServer . Controllers . StoresController ;
namespace BTCPayServer.Controllers
{
[Route("wallets")]
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
[AutoValidateAntiforgeryToken]
2018-10-09 23:48:14 +09:00
public partial class WalletsController : Controller
2018-07-26 22:32:24 +09:00
{
2018-10-09 23:48:14 +09:00
public StoreRepository Repository { get ; }
2019-08-03 00:42:30 +09:00
public WalletRepository WalletRepository { get ; }
2018-10-09 23:48:14 +09:00
public BTCPayNetworkProvider NetworkProvider { get ; }
public ExplorerClientProvider ExplorerClientProvider { get ; }
2018-07-26 22:32:24 +09:00
private readonly UserManager < ApplicationUser > _userManager ;
2019-10-03 17:06:49 +09:00
private readonly JsonSerializerSettings _serializerSettings ;
2018-07-26 22:32:24 +09:00
private readonly NBXplorerDashboard _dashboard ;
2018-10-09 23:48:14 +09:00
2018-07-26 22:32:24 +09:00
private readonly IFeeProviderFactory _feeRateProvider ;
private readonly BTCPayWalletProvider _walletProvider ;
2018-10-09 23:48:14 +09:00
public RateFetcher RateFetcher { get ; }
2018-11-01 00:19:25 +09:00
[TempData]
public string StatusMessage { get ; set ; }
2018-07-26 23:23:28 +09:00
CurrencyNameTable _currencyTable ;
2018-07-26 22:32:24 +09:00
public WalletsController ( StoreRepository repo ,
2019-08-03 00:42:30 +09:00
WalletRepository walletRepository ,
2018-07-26 23:23:28 +09:00
CurrencyNameTable currencyTable ,
2018-07-26 22:32:24 +09:00
BTCPayNetworkProvider networkProvider ,
UserManager < ApplicationUser > userManager ,
2019-10-03 17:06:49 +09:00
MvcNewtonsoftJsonOptions mvcJsonOptions ,
2018-07-26 22:32:24 +09:00
NBXplorerDashboard dashboard ,
2018-08-22 16:53:40 +09:00
RateFetcher rateProvider ,
2018-07-26 22:32:24 +09:00
ExplorerClientProvider explorerProvider ,
IFeeProviderFactory feeRateProvider ,
BTCPayWalletProvider walletProvider )
{
2018-07-26 23:23:28 +09:00
_currencyTable = currencyTable ;
2018-10-09 23:48:14 +09:00
Repository = repo ;
2019-08-03 00:42:30 +09:00
WalletRepository = walletRepository ;
2018-10-09 23:48:14 +09:00
RateFetcher = rateProvider ;
NetworkProvider = networkProvider ;
2018-07-26 22:32:24 +09:00
_userManager = userManager ;
2019-10-03 17:06:49 +09:00
_serializerSettings = mvcJsonOptions . SerializerSettings ;
2018-07-26 22:32:24 +09:00
_dashboard = dashboard ;
2018-10-09 23:48:14 +09:00
ExplorerClientProvider = explorerProvider ;
2018-07-26 22:32:24 +09:00
_feeRateProvider = feeRateProvider ;
_walletProvider = walletProvider ;
}
2019-08-03 00:42:30 +09: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 22:03:49 +09:00
const int MaxLabelSize = 20 ;
const int MaxCommentSize = 200 ;
2019-08-03 00:42:30 +09: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 21:52:47 +09:00
WalletId walletId , string transactionId ,
string addlabel = null ,
string addlabelclick = null ,
string addcomment = null ,
string removelabel = null )
2019-08-03 00:42:30 +09:00
{
addlabel = addlabel ? ? addlabelclick ;
2019-08-03 12:41:12 +09: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-03 00:55:27 +09:00
{
2019-08-03 23:02:15 +09:00
if ( addcomment = = null & & Request ? . Form ? . TryGetValue ( nameof ( addcomment ) , out _ ) is true )
2019-08-03 12:41:12 +09:00
{
addcomment = string . Empty ;
}
2019-08-03 00:55:27 +09:00
}
2019-08-03 12:41:12 +09:00
catch { }
/////////
2019-08-03 00:42:30 +09: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 )
{
2019-08-03 22:06:14 +09:00
addlabel = addlabel . Trim ( ) . ToLowerInvariant ( ) . Replace ( ',' , ' ' ) . Truncate ( MaxLabelSize ) ;
2019-08-03 00:42:30 +09: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 22:03:49 +09:00
removelabel = removelabel . Trim ( ) . ToLowerInvariant ( ) . Truncate ( MaxLabelSize ) ;
2019-08-03 00:42:30 +09: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 22:03:49 +09:00
addcomment = addcomment . Trim ( ) . Truncate ( MaxCommentSize ) ;
2019-08-03 00:42:30 +09: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 ( ) } ) ;
}
2018-07-26 22:32:24 +09:00
public async Task < IActionResult > ListWallets ( )
{
var wallets = new ListWalletsViewModel ( ) ;
2018-10-09 23:48:14 +09:00
var stores = await Repository . GetStoresByUserId ( GetUserId ( ) ) ;
2018-07-26 22:32:24 +09:00
var onChainWallets = stores
2018-10-09 23:48:14 +09:00
. SelectMany ( s = > s . GetSupportedPaymentMethods ( NetworkProvider )
2019-05-08 23:39:11 +09:00
. OfType < DerivationSchemeSettings > ( )
2018-07-26 22:32:24 +09:00
. Select ( d = > ( ( Wallet : _walletProvider . GetWallet ( d . Network ) ,
2019-05-08 23:39:11 +09:00
DerivationStrategy : d . AccountDerivation ,
2018-07-26 22:32:24 +09: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-27 00:08:07 +09:00
public async Task < IActionResult > WalletTransactions (
2018-07-26 22:32:24 +09:00
[ModelBinder(typeof(WalletIdModelBinder))]
2019-08-03 23:10:45 +09:00
WalletId walletId , string labelFilter = null )
2018-07-26 22:32:24 +09:00
{
2019-05-12 11:07:41 +09:00
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings ( walletId ) ;
2018-07-27 00:08:07 +09:00
if ( paymentMethod = = null )
2018-07-26 22:32:24 +09:00
return NotFound ( ) ;
2018-07-27 00:08:07 +09:00
var wallet = _walletProvider . GetWallet ( paymentMethod . Network ) ;
2019-08-03 00:42:30 +09:00
var walletBlobAsync = WalletRepository . GetWalletInfo ( walletId ) ;
var walletTransactionsInfoAsync = WalletRepository . GetWalletTransactionsInfo ( walletId ) ;
2019-05-08 23:39:11 +09:00
var transactions = await wallet . FetchTransactions ( paymentMethod . AccountDerivation ) ;
2019-08-03 00:42:30 +09:00
var walletBlob = await walletBlobAsync ;
var walletTransactionsInfo = await walletTransactionsInfoAsync ;
2018-07-27 00:08:07 +09:00
var model = new ListTransactionsViewModel ( ) ;
2019-08-03 00:42:30 +09:00
foreach ( var tx in transactions . UnconfirmedTransactions . Transactions . Concat ( transactions . ConfirmedTransactions . Transactions ) . ToArray ( ) )
2018-07-27 00:08:07 +09: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 > = Money . Zero ;
vm . Balance = tx . BalanceChange . ToString ( ) ;
2018-11-05 17:26:49 +09:00
vm . IsConfirmed = tx . Confirmations ! = 0 ;
2019-08-03 00:42:30 +09: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 ;
}
2019-08-03 23:10:45 +09:00
if ( labelFilter = = null | | vm . Labels . Any ( l = > l . Value . Equals ( labelFilter , StringComparison . OrdinalIgnoreCase ) ) )
model . Transactions . Add ( vm ) ;
2018-07-27 00:08:07 +09:00
}
2018-07-27 12:03:56 +09:00
model . Transactions = model . Transactions . OrderByDescending ( t = > t . Timestamp ) . ToList ( ) ;
2018-07-27 00:08:07 +09:00
return View ( model ) ;
}
2018-07-26 22:32:24 +09:00
2019-08-03 00:42:30 +09:00
private static string GetLabelTarget ( WalletId walletId , uint256 txId )
{
return $"{walletId}:{txId}" ;
}
2018-07-27 00:08:07 +09:00
[HttpGet]
[Route("{walletId}/send")]
public async Task < IActionResult > WalletSend (
[ModelBinder(typeof(WalletIdModelBinder))]
2019-05-08 12:34:33 +09:00
WalletId walletId , string defaultDestination = null , string defaultAmount = null )
2018-07-27 00:08:07 +09:00
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2018-10-09 23:48:14 +09:00
var store = await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ;
2019-05-12 11:07:41 +09:00
DerivationSchemeSettings paymentMethod = GetDerivationSchemeSettings ( walletId , store ) ;
2018-07-26 22:32:24 +09:00
if ( paymentMethod = = null )
return NotFound ( ) ;
2019-05-29 09:43:50 +00:00
var network = this . NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId ? . CryptoCode ) ;
2018-11-01 00:19:25 +09:00
if ( network = = null )
return NotFound ( ) ;
2018-07-26 23:23:28 +09:00
var storeData = store . GetStoreBlob ( ) ;
2018-10-09 23:48:14 +09:00
var rateRules = store . GetStoreBlob ( ) . GetRateRules ( NetworkProvider ) ;
2018-08-01 18:38:46 +09:00
rateRules . Spread = 0.0 m ;
2018-07-27 01:17:43 +09:00
var currencyPair = new Rating . CurrencyPair ( paymentMethod . PaymentId . CryptoCode , GetCurrencyCode ( storeData . DefaultLang ) ? ? "USD" ) ;
2019-05-21 08:10:07 +00:00
double . TryParse ( defaultAmount , out var amount ) ;
var model = new WalletSendModel ( )
2018-10-09 23:48:14 +09:00
{
2019-05-21 08:10:07 +00:00
Outputs = new List < WalletSendModel . TransactionOutput > ( )
{
new WalletSendModel . TransactionOutput ( )
{
Amount = Convert . ToDecimal ( amount ) ,
DestinationAddress = defaultDestination
}
} ,
2018-11-01 00:19:25 +09:00
CryptoCode = walletId . CryptoCode
2018-10-09 23:48:14 +09:00
} ;
2019-05-21 08:10:07 +00:00
2018-11-01 00:19:25 +09:00
var feeProvider = _feeRateProvider . CreateFeeProvider ( network ) ;
var recommendedFees = feeProvider . GetFeeRateAsync ( ) ;
2019-05-08 23:39:11 +09:00
var balance = _walletProvider . GetWallet ( network ) . GetBalance ( paymentMethod . AccountDerivation ) ;
2018-11-01 00:19:25 +09:00
model . CurrentBalance = ( await balance ) . ToDecimal ( MoneyUnit . BTC ) ;
model . RecommendedSatoshiPerByte = ( int ) ( await recommendedFees ) . GetFee ( 1 ) . Satoshi ;
model . FeeSatoshiPerByte = model . RecommendedSatoshiPerByte ;
2019-05-08 15:24:20 +09:00
model . SupportRBF = network . SupportRBF ;
2018-07-26 23:23:28 +09:00
using ( CancellationTokenSource cts = new CancellationTokenSource ( ) )
{
try
{
cts . CancelAfter ( TimeSpan . FromSeconds ( 5 ) ) ;
2019-03-05 17:09:17 +09:00
var result = await RateFetcher . FetchRate ( currencyPair , rateRules , cts . Token ) . WithCancellation ( cts . Token ) ;
2018-07-27 18:04:41 +09:00
if ( result . BidAsk ! = null )
2018-07-26 23:23:28 +09:00
{
2018-07-27 18:04:41 +09:00
model . Rate = result . BidAsk . Center ;
2018-07-26 23:23:28 +09:00
model . Divisibility = _currencyTable . GetNumberFormatInfo ( currencyPair . Right , true ) . CurrencyDecimalDigits ;
model . Fiat = currencyPair . Right ;
}
2018-07-27 00:32:09 +09:00
else
{
model . RateError = $"{result.EvaluatedRule} ({string.Join(" , ", result.Errors.OfType<object>().ToArray())})" ;
}
2018-07-26 23:23:28 +09:00
}
2018-10-09 23:48:14 +09:00
catch ( Exception ex ) { model . RateError = ex . Message ; }
2018-07-26 23:23:28 +09:00
}
2018-07-26 22:32:24 +09:00
return View ( model ) ;
}
2018-11-01 00:19:25 +09:00
[HttpPost]
[Route("{walletId}/send")]
public async Task < IActionResult > WalletSend (
[ModelBinder(typeof(WalletIdModelBinder))]
2019-05-21 08:10:07 +00:00
WalletId walletId , WalletSendModel vm , string command = "" , CancellationToken cancellation = default )
2018-11-01 00:19:25 +09:00
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
var store = await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ;
if ( store = = null )
return NotFound ( ) ;
2019-05-29 09:43:50 +00:00
var network = this . NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId ? . CryptoCode ) ;
2018-11-01 00:19:25 +09:00
if ( network = = null )
return NotFound ( ) ;
2019-05-08 15:24:20 +09:00
vm . SupportRBF = network . SupportRBF ;
2019-05-21 08:10:07 +00:00
decimal transactionAmountSum = 0 ;
if ( command = = "add-output" )
{
2019-05-21 18:44:49 +09:00
ModelState . Clear ( ) ;
2019-05-21 08:10:07 +00:00
vm . Outputs . Add ( new WalletSendModel . TransactionOutput ( ) ) ;
return View ( vm ) ;
}
if ( command . StartsWith ( "remove-output" , StringComparison . InvariantCultureIgnoreCase ) )
2018-11-01 00:19:25 +09:00
{
2019-05-21 18:44:49 +09:00
ModelState . Clear ( ) ;
2019-05-21 08:10:07 +00:00
var index = int . Parse ( command . Substring ( command . IndexOf ( ":" , StringComparison . InvariantCultureIgnoreCase ) + 1 ) , CultureInfo . InvariantCulture ) ;
vm . Outputs . RemoveAt ( index ) ;
return View ( vm ) ;
2018-11-01 00:19:25 +09:00
}
2019-05-21 08:10:07 +00: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 19:04:39 +09:00
var substractFees = vm . Outputs . Any ( o = > o . SubtractFeesFromOutput ) ;
2019-05-21 08:10:07 +00: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" ,
2019-10-03 18:00:07 +09:00
this ) ;
2019-05-21 08:10:07 +00:00
}
}
if ( subtractFeesOutputsCount . Count > 1 )
{
foreach ( var subtractFeesOutput in subtractFeesOutputsCount )
{
vm . AddModelError ( model = > model . Outputs [ subtractFeesOutput ] . SubtractFeesFromOutput ,
2019-10-03 18:00:07 +09:00
"You can only subtract fees from one output" , this ) ;
2019-05-21 08:10:07 +00:00
}
2019-05-21 19:04:39 +09:00
} else if ( vm . CurrentBalance = = transactionAmountSum & & ! substractFees )
2019-05-21 08:10:07 +00: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 18:00:07 +09:00
"You are sending more than what you own" , this ) ;
2019-05-21 08:10:07 +00:00
}
}
2018-11-01 00:19:25 +09:00
if ( ! ModelState . IsValid )
return View ( vm ) ;
2019-05-12 11:07:41 +09:00
DerivationSchemeSettings derivationScheme = await GetDerivationSchemeSettings ( walletId ) ;
2019-05-12 00:05:30 +09:00
2019-05-13 08:55:26 +09:00
CreatePSBTResponse psbt = null ;
try
{
psbt = await CreatePSBT ( network , derivationScheme , vm , cancellation ) ;
}
catch ( NBXplorerException ex )
{
2019-05-21 08:10:07 +00:00
ModelState . AddModelError ( string . Empty , ex . Error . Message ) ;
2019-05-13 08:55:26 +09:00
return View ( vm ) ;
}
catch ( NotSupportedException )
{
2019-05-21 08:10:07 +00:00
ModelState . AddModelError ( string . Empty , "You need to update your version of NBXplorer" ) ;
2019-05-13 08:55:26 +09:00
return View ( vm ) ;
}
derivationScheme . RebaseKeyPaths ( psbt . PSBT ) ;
2019-05-21 08:10:07 +00:00
2019-05-14 16:03:48 +00:00
switch ( command )
2019-05-08 14:39:37 +09:00
{
2019-05-14 16:03:48 +00:00
case "ledger" :
return ViewWalletSendLedger ( psbt . PSBT , psbt . ChangeAddress ) ;
case "seed" :
2019-05-15 15:00:09 +09:00
return SignWithSeed ( walletId , psbt . PSBT . ToBase64 ( ) ) ;
2019-05-14 16:03:48 +00:00
case "analyze-psbt" :
2019-05-21 08:10:07 +00:00
var name =
$"Send-{string.Join('_', vm.Outputs.Select(output => $" { output . Amount } - > { output . DestinationAddress } { ( output . SubtractFeesFromOutput ? "-Fees" : string . Empty ) } "))}.psbt" ;
2019-05-31 00:00:20 +09:00
return RedirectToAction ( nameof ( WalletPSBT ) , new { walletId = walletId , psbt = psbt . PSBT . ToBase64 ( ) , FileName = name } ) ;
2019-05-14 16:03:48 +00:00
default :
return View ( vm ) ;
2019-05-08 14:39:37 +09:00
}
2019-05-21 08:10:07 +00:00
2019-05-08 14:39:37 +09:00
}
2019-05-11 20:26:31 +09: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-12 00:05:30 +09:00
SuccessPath = this . Url . Action ( nameof ( WalletPSBTReady ) )
2019-05-11 20:26:31 +09:00
} ) ;
}
2019-05-14 16:03:48 +00:00
[HttpGet("{walletId}/psbt/seed")]
public IActionResult SignWithSeed ( [ ModelBinder ( typeof ( WalletIdModelBinder ) ) ]
2019-05-15 15:00:09 +09:00
WalletId walletId , string psbt )
2019-05-14 16:03:48 +00:00
{
2019-05-15 15:00:09 +09:00
return View ( nameof ( SignWithSeed ) , new SignWithSeedViewModel ( )
2019-05-14 16:03:48 +00:00
{
2019-05-15 15:00:09 +09:00
PSBT = psbt
2019-05-14 16:03:48 +00: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 09:43:50 +00:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-14 16:03:48 +00:00
if ( network = = null )
throw new FormatException ( "Invalid value for crypto code" ) ;
2019-05-15 15:00:09 +09:00
ExtKey extKey = viewModel . GetExtKey ( network . NBitcoinNetwork ) ;
2019-05-14 16:03:48 +00:00
2019-05-15 15:00:09 +09:00
if ( extKey = = null )
2019-05-14 16:03:48 +00: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 19:00:26 +09:00
ExtKey signingKey = null ;
2019-05-15 15:00:09 +09:00
var settings = ( await GetDerivationSchemeSettings ( walletId ) ) ;
var signingKeySettings = settings . GetSigningAccountKeySettings ( ) ;
if ( signingKeySettings . RootFingerprint is null )
signingKeySettings . RootFingerprint = extKey . GetPublicKey ( ) . GetHDFingerPrint ( ) ;
2019-05-15 19:00:26 +09: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 16:03:48 +00:00
{
2019-05-15 19:00:26 +09: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 16:03:48 +00:00
}
2019-05-19 23:27:18 +09:00
var balanceChange = psbt . GetBalance ( settings . AccountDerivation , signingKey , rootedKeyPath ) ;
2019-05-15 15:00:09 +09:00
if ( balanceChange = = Money . Zero )
2019-05-14 16:03:48 +00:00
{
2019-09-08 00:18:30 +09:00
ModelState . AddModelError ( nameof ( viewModel . SeedOrKey ) , "This seed is unable to sign this transaction. Either the seed is incorrect, or the account path has not been properly configured in the Wallet Settings." ) ;
2019-05-15 15:00:09 +09:00
return View ( viewModel ) ;
}
2019-05-19 23:27:18 +09:00
psbt . SignAll ( settings . AccountDerivation , signingKey , rootedKeyPath ) ;
2019-05-15 15:00:09 +09:00
ModelState . Remove ( nameof ( viewModel . PSBT ) ) ;
2019-06-16 12:32:00 +09:00
return await WalletPSBTReady ( walletId , psbt . ToBase64 ( ) , signingKey . GetWif ( network . NBitcoinNetwork ) . ToString ( ) , rootedKeyPath ? . ToString ( ) ) ;
2019-05-15 15:00:09 +09:00
}
2019-05-29 09:43:50 +00:00
private string ValueToString ( Money v , BTCPayNetworkBase network )
2019-05-15 15:00:09 +09:00
{
return v . ToString ( ) + " " + network . CryptoCode ;
}
2019-05-11 20:26:31 +09:00
2018-11-01 00:19:25 +09:00
private IDestination [ ] ParseDestination ( string destination , Network network )
{
try
{
2018-11-01 12:54:25 +09:00
destination = destination ? . Trim ( ) ;
2018-11-01 00:19:25 +09:00
return new IDestination [ ] { BitcoinAddress . Create ( destination , network ) } ;
}
catch
{
return null ;
}
}
2019-05-12 00:05:30 +09:00
private async Task < IActionResult > RedirectToWalletTransaction ( WalletId walletId , Transaction transaction )
{
2019-05-29 09:43:50 +00:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-12 00:05:30 +09:00
if ( transaction ! = null )
{
var wallet = _walletProvider . GetWallet ( network ) ;
2019-05-12 11:07:41 +09:00
var derivationSettings = await GetDerivationSchemeSettings ( walletId ) ;
2019-05-12 00:05:30 +09:00
wallet . InvalidateCache ( derivationSettings . AccountDerivation ) ;
StatusMessage = $"Transaction broadcasted successfully ({transaction.GetHash().ToString()})" ;
}
return RedirectToAction ( nameof ( WalletTransactions ) ) ;
}
2018-10-26 23:07:39 +09:00
[HttpGet]
[Route("{walletId}/rescan")]
public async Task < IActionResult > WalletRescan (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId )
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2019-05-12 11:07:41 +09:00
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings ( walletId ) ;
2018-10-26 23:07:39 +09:00
if ( paymentMethod = = null )
return NotFound ( ) ;
var vm = new RescanWalletModel ( ) ;
2018-11-04 22:46:27 +09:00
vm . IsFullySync = _dashboard . IsFullySynched ( walletId . CryptoCode , out var unused ) ;
2018-10-26 23:07:39 +09: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 23:39:11 +09:00
var scanProgress = await explorer . GetScanUTXOSetInformationAsync ( paymentMethod . AccountDerivation ) ;
2018-11-01 00:19:25 +09:00
if ( scanProgress ! = null )
2018-10-26 23:07:39 +09: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 12:41:44 +09:00
[Authorize(Policy = Policies.CanModifyServerSettings.Key, AuthenticationSchemes = Policies.CookieAuthentication)]
2018-10-26 23:07:39 +09:00
public async Task < IActionResult > WalletRescan (
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId , RescanWalletModel vm )
{
if ( walletId ? . StoreId = = null )
return NotFound ( ) ;
2019-05-12 11:07:41 +09:00
DerivationSchemeSettings paymentMethod = await GetDerivationSchemeSettings ( walletId ) ;
2018-10-26 23:07:39 +09:00
if ( paymentMethod = = null )
return NotFound ( ) ;
var explorer = ExplorerClientProvider . GetExplorerClient ( walletId . CryptoCode ) ;
try
{
2019-05-08 23:39:11 +09:00
await explorer . ScanUTXOSetAsync ( paymentMethod . AccountDerivation , vm . BatchSize , vm . GapLimit , vm . StartingIndex ) ;
2018-10-26 23:07:39 +09:00
}
catch ( NBXplorerException ex ) when ( ex . Error . Code = = "scanutxoset-in-progress" )
{
}
return RedirectToAction ( ) ;
}
2018-07-27 01:17:43 +09:00
private string GetCurrencyCode ( string defaultLang )
{
if ( defaultLang = = null )
return null ;
try
{
var ri = new RegionInfo ( defaultLang ) ;
return ri . ISOCurrencySymbol ;
}
2018-10-09 23:48:14 +09:00
catch ( ArgumentException ) { }
2018-07-27 01:17:43 +09:00
return null ;
}
2019-05-12 11:07:41 +09:00
private DerivationSchemeSettings GetDerivationSchemeSettings ( WalletId walletId , StoreData store )
2018-07-27 00:08:07 +09:00
{
if ( store = = null | | ! store . HasClaim ( Policies . CanModifyStoreSettings . Key ) )
return null ;
var paymentMethod = store
2018-10-09 23:48:14 +09:00
. GetSupportedPaymentMethods ( NetworkProvider )
2019-05-08 23:39:11 +09:00
. OfType < DerivationSchemeSettings > ( )
2018-07-27 00:08:07 +09:00
. FirstOrDefault ( p = > p . PaymentId . PaymentType = = Payments . PaymentTypes . BTCLike & & p . PaymentId . CryptoCode = = walletId . CryptoCode ) ;
return paymentMethod ;
}
2019-05-12 11:07:41 +09:00
private async Task < DerivationSchemeSettings > GetDerivationSchemeSettings ( WalletId walletId )
{
var store = ( await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ) ;
return GetDerivationSchemeSettings ( walletId , store ) ;
}
2018-07-26 22:32:24 +09: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-11-01 00:19:25 +09:00
[Route("{walletId}/send/ledger/ws")]
2018-07-26 22:32:24 +09:00
public async Task < IActionResult > LedgerConnection (
2018-11-01 00:19:25 +09:00
[ModelBinder(typeof(WalletIdModelBinder))]
WalletId walletId ,
2018-07-26 22:32:24 +09:00
string command ,
// getinfo
// getxpub
int account = 0 ,
// sendtoaddress
2019-05-11 20:02:32 +09:00
string psbt = null ,
string hintChange = null
2018-07-26 22:32:24 +09:00
)
{
if ( ! HttpContext . WebSockets . IsWebSocketRequest )
return NotFound ( ) ;
2018-11-01 00:19:25 +09:00
2019-05-29 09:43:50 +00:00
var network = NetworkProvider . GetNetwork < BTCPayNetwork > ( walletId . CryptoCode ) ;
2019-05-11 20:02:32 +09:00
if ( network = = null )
throw new FormatException ( "Invalid value for crypto code" ) ;
2018-12-26 15:04:11 +09:00
var storeData = ( await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ) ;
2019-05-12 11:07:41 +09:00
var derivationSettings = GetDerivationSchemeSettings ( walletId , storeData ) ;
2018-11-01 00:19:25 +09:00
2018-07-26 22:32:24 +09: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 14:36:25 +09:00
var hw = new LedgerHardwareWalletService ( webSocket ) ;
2019-05-08 14:39:37 +09:00
var model = new WalletSendLedgerModel ( ) ;
2018-07-26 22:32:24 +09:00
object result = null ;
try
{
2018-11-01 00:19:25 +09:00
if ( command = = "test" )
{
result = await hw . Test ( normalOperationTimeout . Token ) ;
2018-07-26 22:32:24 +09: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 18:56:01 +09:00
2019-05-13 00:30:28 +09:00
var accountKey = derivationSettings . GetSigningAccountKeySettings ( ) ;
2019-05-10 01:05:37 +09:00
// Some deployment does not have the AccountKeyPath set, let's fix this...
2019-05-13 00:13:55 +09:00
if ( accountKey . AccountKeyPath = = null )
2018-07-26 22:32:24 +09:00
{
2018-12-26 15:10:00 +09: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 10:55:10 +09:00
var foundKeyPath = await hw . FindKeyPathFromDerivation ( network ,
derivationSettings . AccountDerivation ,
2019-05-10 10:48:30 +09:00
normalOperationTimeout . Token ) ;
2019-05-13 00:13:55 +09:00
accountKey . AccountKeyPath = foundKeyPath ? ? throw new HardwareWalletException ( $"This store is not configured to use this ledger" ) ;
2019-05-08 23:39:11 +09:00
storeData . SetSupportedPaymentMethod ( derivationSettings ) ;
2018-12-26 15:04:11 +09:00
await Repository . UpdateStore ( storeData ) ;
}
2019-05-10 10:48:30 +09: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-10 01:05:37 +09: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-13 00:13:55 +09:00
if ( accountKey . RootFingerprint = = null )
2019-05-10 01:05:37 +09:00
{
2019-05-12 00:05:30 +09:00
2019-05-13 00:13:55 +09:00
var actualPubKey = await hw . GetExtPubKey ( network , accountKey . AccountKeyPath , normalOperationTimeout . Token ) ;
2019-05-10 01:05:37 +09: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 10:48:30 +09:00
var actualPubKey = await hw . GetPubKey ( network , new KeyPath ( ) , normalOperationTimeout . Token ) ;
2019-05-13 00:13:55 +09:00
if ( actualPubKey . GetHDFingerPrint ( ) ! = accountKey . RootFingerprint . Value )
2019-05-10 01:05:37 +09:00
throw new HardwareWalletException ( $"This store is not configured to use this ledger" ) ;
}
}
2019-01-15 23:50:45 +09:00
2019-05-10 01:05:37 +09:00
// Some deployment does not have the RootFingerprint set, let's fix this...
2019-05-13 00:13:55 +09:00
if ( accountKey . RootFingerprint = = null )
2019-05-10 01:05:37 +09:00
{
2019-05-13 00:13:55 +09:00
accountKey . RootFingerprint = ( await hw . GetPubKey ( network , new KeyPath ( ) , normalOperationTimeout . Token ) ) . GetHDFingerPrint ( ) ;
2019-05-10 01:05:37 +09:00
storeData . SetSupportedPaymentMethod ( derivationSettings ) ;
await Repository . UpdateStore ( storeData ) ;
}
2018-07-26 22:32:24 +09:00
2019-05-11 20:02:32 +09:00
var psbtResponse = new CreatePSBTResponse ( )
{
PSBT = PSBT . Parse ( psbt , network . NBitcoinNetwork ) ,
ChangeAddress = string . IsNullOrEmpty ( hintChange ) ? null : BitcoinAddress . Create ( hintChange , network . NBitcoinNetwork )
} ;
2019-05-12 11:07:41 +09:00
derivationSettings . RebaseKeyPaths ( psbtResponse . PSBT ) ;
2018-07-26 22:32:24 +09:00
signTimeout . CancelAfter ( TimeSpan . FromMinutes ( 5 ) ) ;
2019-05-14 16:06:43 +09:00
psbtResponse . PSBT = await hw . SignTransactionAsync ( psbtResponse . PSBT , accountKey . GetRootedKeyPath ( ) , accountKey . AccountKey , psbtResponse . ChangeAddress ? . ScriptPubKey , signTimeout . Token ) ;
2019-05-12 00:05:30 +09:00
result = new SendToAddressResult ( ) { PSBT = psbtResponse . PSBT . ToBase64 ( ) } ;
2018-07-26 22:32:24 +09: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 17:06:49 +09:00
var bytes = UTF8NOBOM . GetBytes ( JsonConvert . SerializeObject ( result , _serializerSettings ) ) ;
2018-07-26 22:32:24 +09: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-13 00:13:55 +09: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-13 00:30:28 +09:00
var store = ( await Repository . FindStore ( walletId . StoreId , GetUserId ( ) ) ) ;
2019-05-13 00:13:55 +09:00
var vm = new WalletSettingsViewModel ( )
{
Label = derivationSchemeSettings . Label ,
DerivationScheme = derivationSchemeSettings . AccountDerivation . ToString ( ) ,
2019-05-13 00:30:28 +09:00
DerivationSchemeInput = derivationSchemeSettings . AccountOriginal ,
SelectedSigningKey = derivationSchemeSettings . SigningKey . ToString ( )
2019-05-13 00:13:55 +09: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 15:00:09 +09:00
derivationScheme . SigningKey = string . IsNullOrEmpty ( vm . SelectedSigningKey ) ? null : new BitcoinExtPubKey ( vm . SelectedSigningKey , derivationScheme . Network . NBitcoinNetwork ) ;
2019-05-13 00:13:55 +09: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 22:32:24 +09:00
}
public class GetInfoResult
{
}
public class SendToAddressResult
{
2019-05-12 00:05:30 +09:00
[JsonProperty("psbt")]
public string PSBT { get ; set ; }
2018-07-26 22:32:24 +09:00
}
}