2021-02-11 11:48:54 +01:00
using System ;
2021-02-17 18:43:12 +01:00
using System.IO ;
2021-02-11 11:48:54 +01:00
using System.Linq ;
2021-06-17 11:27:17 +02:00
using System.Text ;
2021-11-11 06:30:19 +01:00
using System.Threading ;
2021-02-11 11:48:54 +01:00
using System.Threading.Tasks ;
using BTCPayServer.Abstractions.Extensions ;
using BTCPayServer.Abstractions.Models ;
2021-02-17 18:43:12 +01:00
using BTCPayServer.Client ;
2021-02-11 11:48:54 +01:00
using BTCPayServer.Data ;
using BTCPayServer.Events ;
using BTCPayServer.Models ;
using BTCPayServer.Models.StoreViewModels ;
using BTCPayServer.Payments ;
2021-02-17 18:43:12 +01:00
using BTCPayServer.Services ;
2021-06-17 09:02:47 +02:00
using ExchangeSharp ;
2021-02-17 18:43:12 +01:00
using Microsoft.AspNetCore.Authorization ;
using Microsoft.AspNetCore.Http ;
2021-02-11 11:48:54 +01:00
using Microsoft.AspNetCore.Mvc ;
2021-06-17 08:36:22 +02:00
using Microsoft.AspNetCore.Mvc.ModelBinding.Metadata ;
2021-02-11 11:48:54 +01:00
using NBitcoin ;
2021-11-11 06:30:19 +01:00
using NBitcoin.DataEncoders ;
2021-03-01 12:43:25 +01:00
using NBXplorer ;
2021-02-11 11:48:54 +01:00
using NBXplorer.DerivationStrategy ;
using NBXplorer.Models ;
namespace BTCPayServer.Controllers
{
public partial class StoresController
{
[HttpGet("{storeId}/onchain/{cryptoCode}")]
public ActionResult SetupWallet ( WalletSetupViewModel vm )
{
var checkResult = IsAvailable ( vm . CryptoCode , out var store , out _ ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( vm . CryptoCode , store ) ;
vm . DerivationScheme = derivation ? . AccountDerivation . ToString ( ) ;
return View ( vm ) ;
}
[HttpGet("{storeId}/onchain/{cryptoCode}/import/{method?}")]
public async Task < IActionResult > ImportWallet ( WalletSetupViewModel vm )
{
var checkResult = IsAvailable ( vm . CryptoCode , out _ , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var ( hotWallet , rpcImport ) = await CanUseHotWallet ( ) ;
vm . Network = network ;
vm . CanUseHotWallet = hotWallet ;
vm . CanUseRPCImport = rpcImport ;
2021-09-04 15:07:09 +02:00
vm . SupportTaproot = network . NBitcoinNetwork . Consensus . SupportTaproot ;
vm . SupportSegwit = network . NBitcoinNetwork . Consensus . SupportSegwit ;
2021-02-11 11:48:54 +01:00
if ( vm . Method = = null )
{
vm . Method = WalletSetupMethod . ImportOptions ;
}
else if ( vm . Method = = WalletSetupMethod . Seed )
{
2021-06-18 03:25:17 +02:00
vm . SetupRequest = new WalletSetupRequest ( ) ;
2021-02-11 11:48:54 +01:00
}
return View ( vm . ViewName , vm ) ;
}
[HttpPost("{storeId}/onchain/{cryptoCode}/modify")]
[HttpPost("{storeId}/onchain/{cryptoCode}/import/{method}")]
public async Task < IActionResult > UpdateWallet ( WalletSetupViewModel vm )
{
var checkResult = IsAvailable ( vm . CryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
vm . Network = network ;
DerivationSchemeSettings strategy = null ;
var wallet = _WalletProvider . GetWallet ( network ) ;
if ( wallet = = null )
{
return NotFound ( ) ;
}
if ( vm . WalletFile ! = null )
{
if ( ! DerivationSchemeSettings . TryParseFromWalletFile ( await ReadAllText ( vm . WalletFile ) , network , out strategy ) )
{
ModelState . AddModelError ( nameof ( vm . WalletFile ) , "Wallet file was not in the correct format" ) ;
return View ( vm . ViewName , vm ) ;
}
}
else if ( ! string . IsNullOrEmpty ( vm . WalletFileContent ) )
{
if ( ! DerivationSchemeSettings . TryParseFromWalletFile ( vm . WalletFileContent , network , out strategy ) )
{
ModelState . AddModelError ( nameof ( vm . WalletFileContent ) , "QR import was not in the correct format" ) ;
return View ( vm . ViewName , vm ) ;
}
}
2021-02-18 15:58:35 +01:00
else if ( ! string . IsNullOrEmpty ( vm . DerivationScheme ) )
2021-02-11 11:48:54 +01:00
{
try
{
2021-06-17 09:02:47 +02:00
strategy = ParseDerivationStrategy ( vm . DerivationScheme , network ) ;
strategy . Source = "ManualDerivationScheme" ;
if ( ! string . IsNullOrEmpty ( vm . AccountKey ) )
2021-02-11 11:48:54 +01:00
{
2021-06-17 09:02:47 +02:00
var accountKey = new BitcoinExtPubKey ( vm . AccountKey , network . NBitcoinNetwork ) ;
var accountSettings =
strategy . AccountKeySettings . FirstOrDefault ( a = > a . AccountKey = = accountKey ) ;
if ( accountSettings ! = null )
2021-02-11 11:48:54 +01:00
{
2021-06-17 09:02:47 +02:00
accountSettings . AccountKeyPath =
vm . KeyPath = = null ? null : KeyPath . Parse ( vm . KeyPath ) ;
accountSettings . RootFingerprint = string . IsNullOrEmpty ( vm . RootFingerprint )
? ( HDFingerprint ? ) null
: new HDFingerprint (
NBitcoin . DataEncoders . Encoders . Hex . DecodeData ( vm . RootFingerprint ) ) ;
2021-02-11 11:48:54 +01:00
}
}
2021-06-17 09:02:47 +02:00
vm . DerivationScheme = strategy . AccountDerivation . ToString ( ) ;
ModelState . Remove ( nameof ( vm . DerivationScheme ) ) ;
2021-02-11 11:48:54 +01:00
}
catch
{
ModelState . AddModelError ( nameof ( vm . DerivationScheme ) , "Invalid wallet format" ) ;
return View ( vm . ViewName , vm ) ;
}
}
2021-06-17 09:02:47 +02:00
else if ( ! string . IsNullOrEmpty ( vm . Config ) )
{
2021-06-17 11:27:17 +02:00
if ( ! DerivationSchemeSettings . TryParseFromJson ( UnprotectString ( vm . Config ) , network , out strategy ) )
2021-06-17 09:02:47 +02:00
{
ModelState . AddModelError ( nameof ( vm . Config ) , "Config file was not in the correct format" ) ;
return View ( vm . ViewName , vm ) ;
}
}
if ( strategy is null )
2021-02-18 15:58:35 +01:00
{
ModelState . AddModelError ( nameof ( vm . DerivationScheme ) , "Please provide your extended public key" ) ;
return View ( vm . ViewName , vm ) ;
}
2021-02-11 11:48:54 +01:00
2021-06-17 11:27:17 +02:00
vm . Config = ProtectString ( strategy . ToJson ( ) ) ;
2021-06-17 09:02:47 +02:00
ModelState . Remove ( nameof ( vm . Config ) ) ;
2021-02-11 11:48:54 +01:00
PaymentMethodId paymentMethodId = new PaymentMethodId ( network . CryptoCode , PaymentTypes . BTCLike ) ;
var storeBlob = store . GetStoreBlob ( ) ;
2021-06-17 09:02:47 +02:00
if ( vm . Confirmation )
2021-02-11 11:48:54 +01:00
{
try
{
2021-06-17 09:02:47 +02:00
await wallet . TrackAsync ( strategy . AccountDerivation ) ;
2021-02-11 11:48:54 +01:00
store . SetSupportedPaymentMethod ( paymentMethodId , strategy ) ;
2021-06-17 07:17:14 +02:00
storeBlob . SetExcluded ( paymentMethodId , false ) ;
2021-02-11 11:48:54 +01:00
storeBlob . Hints . Wallet = false ;
2021-10-12 11:37:13 +02:00
storeBlob . PayJoinEnabled = strategy . IsHotWallet & & ! ( vm . SetupRequest ? . PayJoinEnabled is false ) ;
2021-02-11 11:48:54 +01:00
store . SetStoreBlob ( storeBlob ) ;
}
catch
{
ModelState . AddModelError ( nameof ( vm . DerivationScheme ) , "Invalid derivation scheme" ) ;
return View ( vm . ViewName , vm ) ;
}
await _Repo . UpdateStore ( store ) ;
2021-06-17 09:02:47 +02:00
_EventAggregator . Publish ( new WalletChangedEvent { WalletId = new WalletId ( vm . StoreId , vm . CryptoCode ) } ) ;
2021-02-11 11:48:54 +01:00
2021-06-17 12:40:08 +02:00
TempData [ WellKnownTempData . SuccessMessage ] = $"Wallet settings for {network.CryptoCode} have been updated." ;
2021-02-11 11:48:54 +01:00
// This is success case when derivation scheme is added to the store
2021-10-29 08:25:43 +02:00
return RedirectToAction ( nameof ( PaymentMethods ) , new { storeId = vm . StoreId } ) ;
2021-02-11 11:48:54 +01:00
}
return ConfirmAddresses ( vm , strategy ) ;
}
2021-06-17 11:27:17 +02:00
private string ProtectString ( string str )
{
return Convert . ToBase64String ( DataProtector . Protect ( Encoding . UTF8 . GetBytes ( str ) ) ) ;
}
private string UnprotectString ( string str )
{
return Encoding . UTF8 . GetString ( DataProtector . Unprotect ( Convert . FromBase64String ( str ) ) ) ;
}
2021-02-11 11:48:54 +01:00
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/{method?}")]
public async Task < IActionResult > GenerateWallet ( WalletSetupViewModel vm )
{
var checkResult = IsAvailable ( vm . CryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var isHotWallet = vm . Method = = WalletSetupMethod . HotWallet ;
var ( hotWallet , rpcImport ) = await CanUseHotWallet ( ) ;
if ( isHotWallet & & ! hotWallet )
{
return NotFound ( ) ;
}
vm . CanUseHotWallet = hotWallet ;
vm . CanUseRPCImport = rpcImport ;
2021-09-04 15:07:09 +02:00
vm . SupportTaproot = network . NBitcoinNetwork . Consensus . SupportTaproot ;
vm . SupportSegwit = network . NBitcoinNetwork . Consensus . SupportSegwit ;
2021-02-11 11:48:54 +01:00
vm . Network = network ;
if ( vm . Method = = null )
{
vm . Method = WalletSetupMethod . GenerateOptions ;
}
else
{
2021-06-18 03:25:17 +02:00
var canUsePayJoin = hotWallet & & isHotWallet & & network . SupportPayJoin ;
vm . SetupRequest = new WalletSetupRequest
{
SavePrivateKeys = isHotWallet ,
CanUsePayJoin = canUsePayJoin ,
PayJoinEnabled = canUsePayJoin
} ;
2021-02-11 11:48:54 +01:00
}
return View ( vm . ViewName , vm ) ;
}
2021-06-17 11:27:17 +02:00
internal GenerateWalletResponse GenerateWalletResponse ;
2021-02-11 11:48:54 +01:00
[HttpPost("{storeId}/onchain/{cryptoCode}/generate/{method}")]
2021-06-18 03:25:17 +02:00
public async Task < IActionResult > GenerateWallet ( string storeId , string cryptoCode , WalletSetupMethod method , WalletSetupRequest request )
2021-02-11 11:48:54 +01:00
{
var checkResult = IsAvailable ( cryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var ( hotWallet , rpcImport ) = await CanUseHotWallet ( ) ;
if ( ! hotWallet & & request . SavePrivateKeys | | ! rpcImport & & request . ImportKeysToRPC )
{
return NotFound ( ) ;
}
var client = _ExplorerProvider . GetExplorerClient ( cryptoCode ) ;
var isImport = method = = WalletSetupMethod . Seed ;
var vm = new WalletSetupViewModel
{
StoreId = storeId ,
CryptoCode = cryptoCode ,
Method = method ,
SetupRequest = request ,
Confirmation = string . IsNullOrEmpty ( request . ExistingMnemonic ) ,
Network = network ,
2021-06-17 08:36:22 +02:00
Source = isImport ? "SeedImported" : "NBXplorerGenerated" ,
IsHotWallet = isImport ? request . SavePrivateKeys : method = = WalletSetupMethod . HotWallet ,
2021-02-11 11:48:54 +01:00
DerivationSchemeFormat = "BTCPay" ,
2021-06-18 03:25:17 +02:00
CanUseHotWallet = hotWallet ,
2021-09-04 15:07:09 +02:00
CanUseRPCImport = rpcImport ,
SupportTaproot = network . NBitcoinNetwork . Consensus . SupportTaproot ,
SupportSegwit = network . NBitcoinNetwork . Consensus . SupportSegwit
2021-02-11 11:48:54 +01:00
} ;
2021-09-03 08:37:12 +02:00
2021-02-11 11:48:54 +01:00
if ( isImport & & string . IsNullOrEmpty ( request . ExistingMnemonic ) )
{
ModelState . AddModelError ( nameof ( request . ExistingMnemonic ) , "Please provide your existing seed" ) ;
return View ( vm . ViewName , vm ) ;
}
GenerateWalletResponse response ;
try
{
response = await client . GenerateWalletAsync ( request ) ;
if ( response = = null )
{
throw new Exception ( "Node unavailable" ) ;
}
}
catch ( Exception e )
{
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Html = $"There was an error generating your wallet: {e.Message}"
} ) ;
return View ( vm . ViewName , vm ) ;
}
2021-06-17 09:02:47 +02:00
var derivationSchemeSettings = new DerivationSchemeSettings ( response . DerivationScheme , network ) ;
if ( method = = WalletSetupMethod . Seed )
{
derivationSchemeSettings . Source = "ImportedSeed" ;
derivationSchemeSettings . IsHotWallet = request . SavePrivateKeys ;
}
else
{
derivationSchemeSettings . Source = "NBXplorerGenerated" ;
derivationSchemeSettings . IsHotWallet = method = = WalletSetupMethod . HotWallet ;
}
var accountSettings = derivationSchemeSettings . GetSigningAccountKeySettings ( ) ;
accountSettings . AccountKeyPath = response . AccountKeyPath . KeyPath ;
accountSettings . RootFingerprint = response . AccountKeyPath . MasterFingerprint ;
derivationSchemeSettings . AccountOriginal = response . DerivationScheme . ToString ( ) ;
2021-02-11 11:48:54 +01:00
// Set wallet properties from generate response
2021-08-03 07:27:04 +02:00
vm . RootFingerprint = response . AccountKeyPath . MasterFingerprint . ToString ( ) ;
vm . AccountKey = response . AccountHDKey . Neuter ( ) . ToWif ( ) ;
vm . KeyPath = response . AccountKeyPath . KeyPath . ToString ( ) ;
2021-06-17 11:27:17 +02:00
vm . Config = ProtectString ( derivationSchemeSettings . ToJson ( ) ) ;
2021-06-17 09:02:47 +02:00
2021-02-11 11:48:54 +01:00
var result = await UpdateWallet ( vm ) ;
if ( ! ModelState . IsValid | | ! ( result is RedirectToActionResult ) )
return result ;
if ( ! isImport )
{
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Success ,
Html = "<span class='text-centered'>Your wallet has been generated.</span>"
} ) ;
var seedVm = new RecoverySeedBackupViewModel
{
CryptoCode = cryptoCode ,
Mnemonic = response . Mnemonic ,
Passphrase = response . Passphrase ,
IsStored = request . SavePrivateKeys ,
2021-06-17 09:02:47 +02:00
ReturnUrl = Url . Action ( nameof ( GenerateWalletConfirm ) , new { storeId , cryptoCode } )
2021-02-11 11:48:54 +01:00
} ;
2021-11-11 06:30:19 +01:00
if ( _BTCPayEnv . IsDeveloping )
2021-06-17 09:02:47 +02:00
{
2021-06-17 11:27:17 +02:00
GenerateWalletResponse = response ;
2021-06-17 09:02:47 +02:00
}
2021-02-11 11:48:54 +01:00
return this . RedirectToRecoverySeedBackup ( seedVm ) ;
}
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Warning ,
Html = "Please check your addresses and confirm."
} ) ;
return result ;
}
// The purpose of this action is to show the user a success message, which confirms
// that the store settings have been updated after generating a new wallet.
[HttpGet("{storeId}/onchain/{cryptoCode}/generate/confirm")]
public ActionResult GenerateWalletConfirm ( string storeId , string cryptoCode )
{
var checkResult = IsAvailable ( cryptoCode , out _ , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
2021-06-17 12:40:08 +02:00
TempData [ WellKnownTempData . SuccessMessage ] = $"Wallet settings for {network.CryptoCode} have been updated." ;
2021-02-11 11:48:54 +01:00
2021-10-29 08:25:43 +02:00
return RedirectToAction ( nameof ( PaymentMethods ) , new { storeId } ) ;
2021-02-11 11:48:54 +01:00
}
2021-10-29 08:25:43 +02:00
[HttpGet("{storeId}/onchain/{cryptoCode}/settings")]
public async Task < IActionResult > WalletSettings ( string storeId , string cryptoCode )
2021-02-11 11:48:54 +01:00
{
2021-10-29 08:25:43 +02:00
var checkResult = IsAvailable ( cryptoCode , out var store , out var network ) ;
2021-02-11 11:48:54 +01:00
if ( checkResult ! = null )
{
return checkResult ;
}
2021-10-29 08:25:43 +02:00
var derivation = GetExistingDerivationStrategy ( cryptoCode , store ) ;
2021-02-11 11:48:54 +01:00
if ( derivation = = null )
{
return NotFound ( ) ;
}
2021-11-11 06:30:19 +01:00
2021-10-29 08:25:43 +02:00
var storeBlob = store . GetStoreBlob ( ) ;
( bool canUseHotWallet , bool rpcImport ) = await CanUseHotWallet ( ) ;
2021-11-11 06:30:19 +01:00
var client = _ExplorerProvider . GetExplorerClient ( network ) ;
2021-02-11 11:48:54 +01:00
2021-10-29 08:25:43 +02:00
var vm = new WalletSettingsViewModel
2021-11-11 06:30:19 +01:00
{
StoreId = storeId ,
CryptoCode = cryptoCode ,
WalletId = new WalletId ( storeId , cryptoCode ) ,
Network = network ,
IsHotWallet = derivation . IsHotWallet ,
Source = derivation . Source ,
RootFingerprint = derivation . GetSigningAccountKeySettings ( ) . RootFingerprint . ToString ( ) ,
DerivationScheme = derivation . AccountDerivation . ToString ( ) ,
DerivationSchemeInput = derivation . AccountOriginal ,
KeyPath = derivation . GetSigningAccountKeySettings ( ) . AccountKeyPath ? . ToString ( ) ,
UriScheme = derivation . Network . NBitcoinNetwork . UriScheme ,
Label = derivation . Label ,
SelectedSigningKey = derivation . SigningKey . ToString ( ) ,
NBXSeedAvailable = derivation . IsHotWallet & &
canUseHotWallet & &
! string . IsNullOrEmpty ( await client . GetMetadataAsync < string > ( derivation . AccountDerivation ,
WellknownMetadataKeys . MasterHDKey ) ) ,
AccountKeys = derivation . 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 ( ) ,
Config = ProtectString ( derivation . ToJson ( ) ) ,
PayJoinEnabled = storeBlob . PayJoinEnabled ,
MonitoringExpiration = ( int ) storeBlob . MonitoringExpiration . TotalMinutes ,
SpeedPolicy = store . SpeedPolicy ,
ShowRecommendedFee = storeBlob . ShowRecommendedFee ,
RecommendedFeeBlockTarget = storeBlob . RecommendedFeeBlockTarget ,
CanUseHotWallet = canUseHotWallet ,
CanUseRPCImport = rpcImport ,
CanUsePayJoin = canUseHotWallet & & store
. GetSupportedPaymentMethods ( _NetworkProvider )
. OfType < DerivationSchemeSettings > ( )
. Any ( settings = > settings . Network . SupportPayJoin & & settings . IsHotWallet ) ,
StoreName = store . StoreName ,
} ;
2021-02-11 11:48:54 +01:00
2021-09-07 04:55:53 +02:00
ViewData [ "ReplaceDescription" ] = WalletReplaceWarning ( derivation . IsHotWallet ) ;
ViewData [ "RemoveDescription" ] = WalletRemoveWarning ( derivation . IsHotWallet , network . CryptoCode ) ;
2021-02-11 11:48:54 +01:00
return View ( vm ) ;
}
2021-10-29 08:25:43 +02:00
2021-11-11 06:30:19 +01:00
[HttpPost("{storeId}/onchain/{cryptoCode}/settings/wallet")]
2021-10-29 08:25:43 +02:00
public async Task < IActionResult > UpdateWalletSettings ( WalletSettingsViewModel vm )
{
2021-11-11 06:30:19 +01:00
var checkResult = IsAvailable ( vm . CryptoCode , out var store , out _ ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( vm . CryptoCode , store ) ;
if ( derivation = = null )
{
return NotFound ( ) ;
}
2021-10-29 08:25:43 +02:00
bool needUpdate = false ;
2021-11-11 06:30:19 +01:00
string errorMessage = null ;
if ( derivation . Label ! = vm . Label )
{
needUpdate = true ;
derivation . Label = vm . Label ;
}
var signingKey = string . IsNullOrEmpty ( vm . SelectedSigningKey )
? null
: new BitcoinExtPubKey ( vm . SelectedSigningKey , derivation . Network . NBitcoinNetwork ) ;
if ( derivation . SigningKey ! = signingKey & & signingKey ! = null )
2021-10-29 08:25:43 +02:00
{
needUpdate = true ;
2021-11-11 06:30:19 +01:00
derivation . SigningKey = signingKey ;
2021-10-29 08:25:43 +02:00
}
2021-11-11 06:30:19 +01:00
for ( int i = 0 ; i < derivation . AccountKeySettings . Length ; i + + )
{
KeyPath accountKeyPath = null ;
HDFingerprint ? rootFingerprint = null ;
try
{
accountKeyPath = string . IsNullOrWhiteSpace ( vm . AccountKeys [ i ] . AccountKeyPath )
? null
: new KeyPath ( vm . AccountKeys [ i ] . AccountKeyPath ) ;
if ( accountKeyPath ! = null & & derivation . AccountKeySettings [ i ] . AccountKeyPath ! = accountKeyPath )
{
needUpdate = true ;
derivation . AccountKeySettings [ i ] . AccountKeyPath = accountKeyPath ;
}
}
catch ( Exception ex )
{
errorMessage = $"{ex.Message}: {vm.AccountKeys[i].AccountKeyPath}" ;
}
try
{
rootFingerprint = string . IsNullOrWhiteSpace ( vm . AccountKeys [ i ] . MasterFingerprint )
? ( HDFingerprint ? ) null
: new HDFingerprint ( Encoders . Hex . DecodeData ( vm . AccountKeys [ i ] . MasterFingerprint ) ) ;
if ( rootFingerprint ! = null & & derivation . AccountKeySettings [ i ] . RootFingerprint ! = rootFingerprint )
{
needUpdate = true ;
derivation . AccountKeySettings [ i ] . RootFingerprint = rootFingerprint ;
}
}
catch ( Exception ex )
{
errorMessage = $"{ex.Message}: {vm.AccountKeys[i].MasterFingerprint}" ;
}
}
if ( needUpdate )
{
store . SetSupportedPaymentMethod ( derivation ) ;
await _Repo . UpdateStore ( store ) ;
2021-10-29 08:25:43 +02:00
2021-11-11 06:30:19 +01:00
if ( string . IsNullOrEmpty ( errorMessage ) )
{
TempData [ WellKnownTempData . SuccessMessage ] = "Wallet settings successfully updated" ;
}
else
{
TempData [ WellKnownTempData . ErrorMessage ] = errorMessage ;
}
}
return RedirectToAction ( nameof ( WalletSettings ) , new { vm . StoreId , vm . CryptoCode } ) ;
}
[HttpPost("{storeId}/onchain/{cryptoCode}/settings/payment")]
public async Task < IActionResult > UpdatePaymentSettings ( WalletSettingsViewModel vm )
{
var checkResult = IsAvailable ( vm . CryptoCode , out var store , out _ ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( vm . CryptoCode , store ) ;
if ( derivation = = null )
{
return NotFound ( ) ;
}
2021-10-29 08:25:43 +02:00
2021-11-11 06:30:19 +01:00
bool needUpdate = false ;
if ( store . SpeedPolicy ! = vm . SpeedPolicy )
{
needUpdate = true ;
store . SpeedPolicy = vm . SpeedPolicy ;
}
var blob = store . GetStoreBlob ( ) ;
var payjoinChanged = blob . PayJoinEnabled ! = vm . PayJoinEnabled ;
2021-10-29 08:25:43 +02:00
blob . MonitoringExpiration = TimeSpan . FromMinutes ( vm . MonitoringExpiration ) ;
blob . ShowRecommendedFee = vm . ShowRecommendedFee ;
blob . RecommendedFeeBlockTarget = vm . RecommendedFeeBlockTarget ;
blob . PayJoinEnabled = vm . PayJoinEnabled ;
2021-11-11 06:30:19 +01:00
if ( store . SetStoreBlob ( blob ) )
2021-10-29 08:25:43 +02:00
{
needUpdate = true ;
}
if ( needUpdate )
{
2021-11-11 06:30:19 +01:00
await _Repo . UpdateStore ( store ) ;
2021-10-29 08:25:43 +02:00
TempData [ WellKnownTempData . SuccessMessage ] = "Payment settings successfully updated" ;
if ( payjoinChanged & & blob . PayJoinEnabled )
{
2021-11-11 06:30:19 +01:00
var problematicPayjoinEnabledMethods = store . GetSupportedPaymentMethods ( _NetworkProvider )
2021-10-29 08:25:43 +02:00
. OfType < DerivationSchemeSettings > ( )
. Where ( settings = > settings . Network . SupportPayJoin & & ! settings . IsHotWallet )
. Select ( settings = > settings . PaymentId . CryptoCode )
. ToArray ( ) ;
if ( problematicPayjoinEnabledMethods . Any ( ) )
{
TempData . Remove ( WellKnownTempData . SuccessMessage ) ;
TempData . SetStatusMessageModel ( new StatusMessageModel ( )
{
Severity = StatusMessageModel . StatusSeverity . Warning ,
2021-11-11 06:30:19 +01:00
Html = $"The payment settings were updated successfully. However, PayJoin will not work for {string.Join(" , ", problematicPayjoinEnabledMethods)} until you configure them to be a <a href='https://docs.btcpayserver.org/HotWallet/' class='alert-link' target='_blank'>hot wallet</a>."
2021-10-29 08:25:43 +02:00
} ) ;
}
}
}
2021-11-11 06:30:19 +01:00
return RedirectToAction ( nameof ( WalletSettings ) , new { vm . StoreId , vm . CryptoCode } ) ;
}
[HttpGet("{storeId}/onchain/{cryptoCode}/seed")]
public async Task < IActionResult > WalletSeed ( string storeId , string cryptoCode , CancellationToken cancellationToken = default )
{
var checkResult = IsAvailable ( cryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
2021-10-29 08:25:43 +02:00
2021-11-11 06:30:19 +01:00
var derivation = GetExistingDerivationStrategy ( cryptoCode , store ) ;
if ( derivation = = null )
2021-10-29 08:25:43 +02:00
{
2021-11-11 06:30:19 +01:00
return NotFound ( ) ;
}
( bool canUseHotWallet , bool _ ) = await CanUseHotWallet ( ) ;
if ( ! canUseHotWallet )
{
return NotFound ( ) ;
}
var client = _ExplorerProvider . GetExplorerClient ( network ) ;
if ( await GetSeed ( client , derivation ) ! = null )
{
var mnemonic = await client . GetMetadataAsync < string > ( derivation . AccountDerivation ,
WellknownMetadataKeys . Mnemonic , cancellationToken ) ;
var recoveryVm = new RecoverySeedBackupViewModel
{
CryptoCode = cryptoCode ,
Mnemonic = mnemonic ,
IsStored = true ,
RequireConfirm = false ,
ReturnUrl = Url . Action ( nameof ( WalletSettings ) , new { storeId , cryptoCode } )
} ;
return this . RedirectToRecoverySeedBackup ( recoveryVm ) ;
}
TempData . SetStatusMessageModel ( new StatusMessageModel
{
Severity = StatusMessageModel . StatusSeverity . Error ,
Message = "The seed was not found"
2021-10-29 08:25:43 +02:00
} ) ;
2021-11-11 06:30:19 +01:00
return RedirectToAction ( nameof ( WalletSettings ) ) ;
2021-10-29 08:25:43 +02:00
}
2021-02-11 11:48:54 +01:00
2021-03-01 12:43:25 +01:00
[HttpGet("{storeId}/onchain/{cryptoCode}/replace")]
2021-06-18 03:25:17 +02:00
public ActionResult ReplaceWallet ( string storeId , string cryptoCode )
2021-03-01 12:43:25 +01:00
{
var checkResult = IsAvailable ( cryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( cryptoCode , store ) ;
2021-09-07 04:55:53 +02:00
2021-03-01 12:43:25 +01:00
return View ( "Confirm" , new ConfirmModel
{
Title = $"Replace {network.CryptoCode} wallet" ,
2021-09-07 04:55:53 +02:00
Description = WalletReplaceWarning ( derivation . IsHotWallet ) ,
2021-03-01 12:43:25 +01:00
DescriptionHtml = true ,
Action = "Setup new wallet"
} ) ;
}
[HttpPost("{storeId}/onchain/{cryptoCode}/replace")]
public IActionResult ConfirmReplaceWallet ( string storeId , string cryptoCode )
{
var checkResult = IsAvailable ( cryptoCode , out var store , out _ ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( cryptoCode , store ) ;
if ( derivation = = null )
{
return NotFound ( ) ;
}
2021-06-17 09:02:47 +02:00
return RedirectToAction ( nameof ( SetupWallet ) , new { storeId , cryptoCode } ) ;
2021-03-01 12:43:25 +01:00
}
2021-02-11 11:48:54 +01:00
[HttpGet("{storeId}/onchain/{cryptoCode}/delete")]
2021-06-18 03:25:17 +02:00
public ActionResult DeleteWallet ( string storeId , string cryptoCode )
2021-02-11 11:48:54 +01:00
{
var checkResult = IsAvailable ( cryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( cryptoCode , store ) ;
return View ( "Confirm" , new ConfirmModel
{
Title = $"Remove {network.CryptoCode} wallet" ,
2021-09-07 04:55:53 +02:00
Description = WalletRemoveWarning ( derivation . IsHotWallet , network . CryptoCode ) ,
2021-02-11 11:48:54 +01:00
DescriptionHtml = true ,
Action = "Remove"
} ) ;
}
2021-04-16 15:31:09 +02:00
[HttpPost("{storeId}/onchain/{cryptoCode}/status")]
public async Task < IActionResult > SetWalletEnabled ( string storeId , string cryptoCode , bool enabled )
{
var checkResult = IsAvailable ( cryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( cryptoCode , store ) ;
if ( derivation = = null )
{
return NotFound ( ) ;
}
var wallet = _WalletProvider . GetWallet ( network ) ;
if ( wallet = = null )
{
return NotFound ( ) ;
}
var paymentMethodId = new PaymentMethodId ( network . CryptoCode , PaymentTypes . BTCLike ) ;
var storeBlob = store . GetStoreBlob ( ) ;
storeBlob . SetExcluded ( paymentMethodId , ! enabled ) ;
store . SetStoreBlob ( storeBlob ) ;
await _Repo . UpdateStore ( store ) ;
2021-06-17 09:02:47 +02:00
_EventAggregator . Publish ( new WalletChangedEvent { WalletId = new WalletId ( storeId , cryptoCode ) } ) ;
2021-04-16 15:31:09 +02:00
TempData [ WellKnownTempData . SuccessMessage ] =
$"{network.CryptoCode} on-chain payments are now {(enabled ? " enabled " : " disabled ")} for this store." ;
2021-10-29 08:25:43 +02:00
return RedirectToAction ( nameof ( PaymentMethods ) , new { storeId } ) ;
2021-04-16 15:31:09 +02:00
}
2021-02-11 11:48:54 +01:00
[HttpPost("{storeId}/onchain/{cryptoCode}/delete")]
public async Task < IActionResult > ConfirmDeleteWallet ( string storeId , string cryptoCode )
{
var checkResult = IsAvailable ( cryptoCode , out var store , out var network ) ;
if ( checkResult ! = null )
{
return checkResult ;
}
var derivation = GetExistingDerivationStrategy ( cryptoCode , store ) ;
if ( derivation = = null )
{
return NotFound ( ) ;
}
PaymentMethodId paymentMethodId = new PaymentMethodId ( network . CryptoCode , PaymentTypes . BTCLike ) ;
store . SetSupportedPaymentMethod ( paymentMethodId , null ) ;
await _Repo . UpdateStore ( store ) ;
2021-06-17 09:02:47 +02:00
_EventAggregator . Publish ( new WalletChangedEvent { WalletId = new WalletId ( storeId , cryptoCode ) } ) ;
2021-02-11 11:48:54 +01:00
TempData [ WellKnownTempData . SuccessMessage ] =
$"On-Chain payment for {network.CryptoCode} has been removed." ;
2021-10-29 08:25:43 +02:00
return RedirectToAction ( nameof ( PaymentMethods ) , new { storeId } ) ;
2021-02-11 11:48:54 +01:00
}
private IActionResult ConfirmAddresses ( WalletSetupViewModel vm , DerivationSchemeSettings strategy )
{
vm . DerivationScheme = strategy . AccountDerivation . ToString ( ) ;
2021-03-01 12:43:25 +01:00
var deposit = new KeyPathTemplates ( null ) . GetKeyPathTemplate ( DerivationFeature . Deposit ) ;
2021-02-11 11:48:54 +01:00
if ( ! string . IsNullOrEmpty ( vm . DerivationScheme ) )
{
var line = strategy . AccountDerivation . GetLineFor ( deposit ) ;
for ( uint i = 0 ; i < 10 ; i + + )
{
var keyPath = deposit . GetKeyPath ( i ) ;
var rootedKeyPath = vm . GetAccountKeypath ( ) ? . Derive ( keyPath ) ;
var derivation = line . Derive ( i ) ;
var address = strategy . Network . NBXplorerNetwork . CreateAddress ( strategy . AccountDerivation ,
line . KeyPathTemplate . GetKeyPath ( i ) ,
derivation . ScriptPubKey ) . ToString ( ) ;
vm . AddressSamples . Add ( ( keyPath . ToString ( ) , address , rootedKeyPath ) ) ;
}
}
vm . Confirmation = true ;
ModelState . Remove ( nameof ( vm . Config ) ) ; // Remove the cached value
return View ( "ImportWallet/ConfirmAddresses" , vm ) ;
}
private ActionResult IsAvailable ( string cryptoCode , out StoreData store , out BTCPayNetwork network )
{
store = HttpContext . GetStoreData ( ) ;
network = cryptoCode = = null ? null : _ExplorerProvider . GetNetwork ( cryptoCode ) ;
return store = = null | | network = = null ? NotFound ( ) : null ;
}
2021-02-17 18:43:12 +01:00
private DerivationSchemeSettings GetExistingDerivationStrategy ( string cryptoCode , StoreData store )
{
var id = new PaymentMethodId ( cryptoCode , PaymentTypes . BTCLike ) ;
var existing = store . GetSupportedPaymentMethods ( _NetworkProvider )
. OfType < DerivationSchemeSettings > ( )
. FirstOrDefault ( d = > d . PaymentId = = id ) ;
return existing ;
}
2021-11-11 06:30:19 +01:00
private async Task < string > GetSeed ( ExplorerClient client , DerivationSchemeSettings derivation )
{
return derivation . IsHotWallet & &
await client . GetMetadataAsync < string > ( derivation . AccountDerivation , WellknownMetadataKeys . MasterHDKey ) is string seed & &
! string . IsNullOrEmpty ( seed ) ? seed : null ;
}
2021-02-17 18:43:12 +01:00
private async Task < ( bool HotWallet , bool RPCImport ) > CanUseHotWallet ( )
{
var policies = await _settingsRepository . GetSettingAsync < PoliciesSettings > ( ) ;
2021-03-11 13:34:52 +01:00
return await _authorizationService . CanUseHotWallet ( policies , User ) ;
2021-02-17 18:43:12 +01:00
}
private async Task < string > ReadAllText ( IFormFile file )
{
using ( var stream = new StreamReader ( file . OpenReadStream ( ) ) )
{
return await stream . ReadToEndAsync ( ) ;
}
}
2021-09-07 04:55:53 +02:00
private string WalletWarning ( bool isHotWallet , string info )
{
var walletType = isHotWallet ? "hot" : "watch-only" ;
var additionalText = isHotWallet
? ""
: " or imported it into an external wallet. If you no longer have access to your private key (recovery seed), immediately replace the wallet" ;
return
$"<p class=\" text - danger fw - bold \ ">Please note that this is a {walletType} wallet!</p>" +
$"<p class=\" text - danger fw - bold \ ">Do not proceed if you have not backed up the wallet{additionalText}.</p>" +
$"<p class=\" text - start mb - 0 \ ">This action will erase the current wallet data from the server. {info}</p>" ;
}
private string WalletReplaceWarning ( bool isHotWallet )
{
return WalletWarning ( isHotWallet ,
"The current wallet will be replaced once you finish the setup of the new wallet. " +
"If you cancel the setup, the current wallet will stay active." ) ;
}
private string WalletRemoveWarning ( bool isHotWallet , string cryptoCode )
{
return WalletWarning ( isHotWallet ,
$"The store won't be able to receive {cryptoCode} onchain payments until a new wallet is set up." ) ;
}
2021-02-11 11:48:54 +01:00
}
}