mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-17 22:14:26 +01:00
Add a "Wallet" menu
This commit is contained in:
@@ -125,6 +125,18 @@
|
|||||||
<Content Update="Views\Server\Services.cshtml">
|
<Content Update="Views\Server\Services.cshtml">
|
||||||
<Pack>$(IncludeRazorContentInPack)</Pack>
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
</Content>
|
</Content>
|
||||||
|
<Content Update="Views\Wallets\ListWallets.cshtml">
|
||||||
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Views\Wallets\_Nav.cshtml">
|
||||||
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Views\Wallets\_ViewImports.cshtml">
|
||||||
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
|
</Content>
|
||||||
|
<Content Update="Views\Wallets\_ViewStart.cshtml">
|
||||||
|
<Pack>$(IncludeRazorContentInPack)</Pack>
|
||||||
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ namespace BTCPayServer.Controllers
|
|||||||
}
|
}
|
||||||
|
|
||||||
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
DerivationSchemeViewModel vm = new DerivationSchemeViewModel();
|
||||||
vm.ServerUrl = GetStoreUrl(storeId);
|
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||||
vm.CryptoCode = cryptoCode;
|
vm.CryptoCode = cryptoCode;
|
||||||
vm.RootKeyPath = network.GetRootKeyPath();
|
vm.RootKeyPath = network.GetRootKeyPath();
|
||||||
SetExistingValues(store, vm);
|
SetExistingValues(store, vm);
|
||||||
@@ -59,7 +59,7 @@ namespace BTCPayServer.Controllers
|
|||||||
[Route("{storeId}/derivations/{cryptoCode}")]
|
[Route("{storeId}/derivations/{cryptoCode}")]
|
||||||
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
public async Task<IActionResult> AddDerivationScheme(string storeId, DerivationSchemeViewModel vm, string cryptoCode)
|
||||||
{
|
{
|
||||||
vm.ServerUrl = GetStoreUrl(storeId);
|
vm.ServerUrl = WalletsController.GetLedgerWebsocketUrl(this.HttpContext, cryptoCode, null);
|
||||||
vm.CryptoCode = cryptoCode;
|
vm.CryptoCode = cryptoCode;
|
||||||
var store = HttpContext.GetStoreData();
|
var store = HttpContext.GetStoreData();
|
||||||
if (store == null)
|
if (store == null)
|
||||||
@@ -161,279 +161,5 @@ namespace BTCPayServer.Controllers
|
|||||||
vm.Confirmation = true;
|
vm.Confirmation = true;
|
||||||
return View(vm);
|
return View(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public class GetInfoResult
|
|
||||||
{
|
|
||||||
public int RecommendedSatoshiPerByte { get; set; }
|
|
||||||
public double Balance { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
public class SendToAddressResult
|
|
||||||
{
|
|
||||||
public string TransactionId { get; set; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("{storeId}/ws/ledger")]
|
|
||||||
public async Task<IActionResult> LedgerConnection(
|
|
||||||
string storeId,
|
|
||||||
string command,
|
|
||||||
// getinfo
|
|
||||||
string cryptoCode = null,
|
|
||||||
// getxpub
|
|
||||||
int account = 0,
|
|
||||||
// sendtoaddress
|
|
||||||
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
|
||||||
)
|
|
||||||
{
|
|
||||||
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
|
||||||
return NotFound();
|
|
||||||
var store = HttpContext.GetStoreData();
|
|
||||||
if (store == null)
|
|
||||||
return NotFound();
|
|
||||||
|
|
||||||
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
|
||||||
|
|
||||||
using (var normalOperationTimeout = new CancellationTokenSource())
|
|
||||||
using (var signTimeout = new CancellationTokenSource())
|
|
||||||
{
|
|
||||||
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
|
|
||||||
var hw = new HardwareWalletService(webSocket);
|
|
||||||
object result = null;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
BTCPayNetwork network = null;
|
|
||||||
if (cryptoCode != null)
|
|
||||||
{
|
|
||||||
network = _NetworkProvider.GetNetwork(cryptoCode);
|
|
||||||
if (network == null)
|
|
||||||
throw new FormatException("Invalid value for crypto code");
|
|
||||||
}
|
|
||||||
|
|
||||||
BitcoinAddress destinationAddress = null;
|
|
||||||
if (destination != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (destinationAddress == null)
|
|
||||||
throw new FormatException("Invalid value for destination");
|
|
||||||
}
|
|
||||||
|
|
||||||
FeeRate feeRateValue = null;
|
|
||||||
if (feeRate != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
|
||||||
throw new FormatException("Invalid value for fee rate");
|
|
||||||
}
|
|
||||||
|
|
||||||
Money amountBTC = null;
|
|
||||||
if (amount != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
amountBTC = Money.Parse(amount);
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
if (amountBTC == null || amountBTC <= Money.Zero)
|
|
||||||
throw new FormatException("Invalid value for amount");
|
|
||||||
}
|
|
||||||
|
|
||||||
bool subsctractFeesValue = false;
|
|
||||||
if (substractFees != null)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
subsctractFeesValue = bool.Parse(substractFees);
|
|
||||||
}
|
|
||||||
catch { throw new FormatException("Invalid value for subtract fees"); }
|
|
||||||
}
|
|
||||||
if (command == "test")
|
|
||||||
{
|
|
||||||
result = await hw.Test(normalOperationTimeout.Token);
|
|
||||||
}
|
|
||||||
if (command == "getxpub")
|
|
||||||
{
|
|
||||||
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
|
|
||||||
result = getxpubResult;
|
|
||||||
}
|
|
||||||
if (command == "getinfo")
|
|
||||||
{
|
|
||||||
var strategy = GetDirectDerivationStrategy(store, network);
|
|
||||||
var strategyBase = GetDerivationStrategy(store, network);
|
|
||||||
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
|
||||||
{
|
|
||||||
throw new Exception($"This store is not configured to use this ledger");
|
|
||||||
}
|
|
||||||
|
|
||||||
var feeProvider = _FeeRateProvider.CreateFeeProvider(network);
|
|
||||||
var recommendedFees = feeProvider.GetFeeRateAsync();
|
|
||||||
var balance = _WalletProvider.GetWallet(network).GetBalance(strategyBase);
|
|
||||||
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
|
||||||
}
|
|
||||||
|
|
||||||
if (command == "sendtoaddress")
|
|
||||||
{
|
|
||||||
if (!_Dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
|
||||||
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
|
||||||
var strategy = GetDirectDerivationStrategy(store, network);
|
|
||||||
var strategyBase = GetDerivationStrategy(store, network);
|
|
||||||
var wallet = _WalletProvider.GetWallet(network);
|
|
||||||
var change = wallet.GetChangeAddressAsync(strategyBase);
|
|
||||||
|
|
||||||
var unspentCoins = await wallet.GetUnspentCoins(strategyBase);
|
|
||||||
var changeAddress = await change;
|
|
||||||
var send = new[] { (
|
|
||||||
destination: destinationAddress as IDestination,
|
|
||||||
amount: amountBTC,
|
|
||||||
substractFees: subsctractFeesValue) };
|
|
||||||
|
|
||||||
foreach (var element in send)
|
|
||||||
{
|
|
||||||
if (element.destination == null)
|
|
||||||
throw new ArgumentNullException(nameof(element.destination));
|
|
||||||
if (element.amount == null)
|
|
||||||
throw new ArgumentNullException(nameof(element.amount));
|
|
||||||
if (element.amount <= Money.Zero)
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
|
||||||
}
|
|
||||||
|
|
||||||
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
|
|
||||||
if (foundKeyPath == null)
|
|
||||||
{
|
|
||||||
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
|
||||||
}
|
|
||||||
|
|
||||||
TransactionBuilder builder = new TransactionBuilder();
|
|
||||||
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
|
||||||
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
|
||||||
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
|
||||||
|
|
||||||
foreach (var element in send)
|
|
||||||
{
|
|
||||||
builder.Send(element.destination, element.amount);
|
|
||||||
if (element.substractFees)
|
|
||||||
builder.SubtractFees();
|
|
||||||
}
|
|
||||||
builder.SetChange(changeAddress.Item1);
|
|
||||||
|
|
||||||
if (network.MinFee == null)
|
|
||||||
{
|
|
||||||
builder.SendEstimatedFees(feeRateValue);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var estimatedFee = builder.EstimateFees(feeRateValue);
|
|
||||||
if (network.MinFee > estimatedFee)
|
|
||||||
builder.SendFees(network.MinFee);
|
|
||||||
else
|
|
||||||
builder.SendEstimatedFees(feeRateValue);
|
|
||||||
}
|
|
||||||
builder.Shuffle();
|
|
||||||
var unsigned = builder.BuildTransaction(false);
|
|
||||||
|
|
||||||
var keypaths = new Dictionary<Script, KeyPath>();
|
|
||||||
foreach (var c in unspentCoins)
|
|
||||||
{
|
|
||||||
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
var hasChange = unsigned.Outputs.Count == 2;
|
|
||||||
var usedCoins = builder.FindSpentCoins(unsigned);
|
|
||||||
|
|
||||||
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
|
||||||
|
|
||||||
if (!strategy.Segwit)
|
|
||||||
{
|
|
||||||
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
|
||||||
var explorer = _ExplorerProvider.GetExplorerClient(network);
|
|
||||||
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
|
||||||
foreach (var getTransactionAsync in getTransactionAsyncs)
|
|
||||||
{
|
|
||||||
var tx = (await getTransactionAsync.Op);
|
|
||||||
if (tx == null)
|
|
||||||
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
|
||||||
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
|
||||||
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
|
||||||
{
|
|
||||||
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
|
||||||
InputCoin = c,
|
|
||||||
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
|
||||||
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
|
||||||
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
|
||||||
if (!broadcastResult[0].Success)
|
|
||||||
{
|
|
||||||
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
throw new Exception("Error while broadcasting: " + ex.Message);
|
|
||||||
}
|
|
||||||
wallet.InvalidateCache(strategyBase);
|
|
||||||
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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.SerializerSettings));
|
|
||||||
await webSocket.SendAsync(new ArraySegment<byte>(bytes), WebSocketMessageType.Text, true, new CancellationTokenSource(2000).Token);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
await webSocket.CloseSocket();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return new EmptyResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private DirectDerivationStrategy GetDirectDerivationStrategy(StoreData store, BTCPayNetwork network)
|
|
||||||
{
|
|
||||||
var strategy = GetDerivationStrategy(store, network);
|
|
||||||
var directStrategy = strategy as DirectDerivationStrategy;
|
|
||||||
if (directStrategy == null)
|
|
||||||
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
|
||||||
return directStrategy;
|
|
||||||
}
|
|
||||||
|
|
||||||
private DerivationStrategyBase GetDerivationStrategy(StoreData store, BTCPayNetwork network)
|
|
||||||
{
|
|
||||||
var strategy = store
|
|
||||||
.GetSupportedPaymentMethods(_NetworkProvider)
|
|
||||||
.OfType<DerivationStrategy>()
|
|
||||||
.FirstOrDefault(s => s.Network.NBitcoinNetwork == network.NBitcoinNetwork);
|
|
||||||
if (strategy == null)
|
|
||||||
{
|
|
||||||
throw new Exception($"Derivation strategy for {network.CryptoCode} is not set");
|
|
||||||
}
|
|
||||||
|
|
||||||
return strategy.DerivationStrategyBase;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,11 +38,9 @@ namespace BTCPayServer.Controllers
|
|||||||
BTCPayRateProviderFactory _RateFactory;
|
BTCPayRateProviderFactory _RateFactory;
|
||||||
public string CreatedStoreId { get; set; }
|
public string CreatedStoreId { get; set; }
|
||||||
public StoresController(
|
public StoresController(
|
||||||
NBXplorerDashboard dashboard,
|
|
||||||
IServiceProvider serviceProvider,
|
IServiceProvider serviceProvider,
|
||||||
BTCPayServerOptions btcpayServerOptions,
|
BTCPayServerOptions btcpayServerOptions,
|
||||||
BTCPayServerEnvironment btcpayEnv,
|
BTCPayServerEnvironment btcpayEnv,
|
||||||
IOptions<MvcJsonOptions> mvcJsonOptions,
|
|
||||||
StoreRepository repo,
|
StoreRepository repo,
|
||||||
TokenRepository tokenRepo,
|
TokenRepository tokenRepo,
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
@@ -56,7 +54,6 @@ namespace BTCPayServer.Controllers
|
|||||||
IHostingEnvironment env)
|
IHostingEnvironment env)
|
||||||
{
|
{
|
||||||
_RateFactory = rateFactory;
|
_RateFactory = rateFactory;
|
||||||
_Dashboard = dashboard;
|
|
||||||
_Repo = repo;
|
_Repo = repo;
|
||||||
_TokenRepository = tokenRepo;
|
_TokenRepository = tokenRepo;
|
||||||
_UserManager = userManager;
|
_UserManager = userManager;
|
||||||
@@ -66,19 +63,16 @@ namespace BTCPayServer.Controllers
|
|||||||
_Env = env;
|
_Env = env;
|
||||||
_NetworkProvider = networkProvider;
|
_NetworkProvider = networkProvider;
|
||||||
_ExplorerProvider = explorerProvider;
|
_ExplorerProvider = explorerProvider;
|
||||||
_MvcJsonOptions = mvcJsonOptions.Value;
|
|
||||||
_FeeRateProvider = feeRateProvider;
|
_FeeRateProvider = feeRateProvider;
|
||||||
_ServiceProvider = serviceProvider;
|
_ServiceProvider = serviceProvider;
|
||||||
_BtcpayServerOptions = btcpayServerOptions;
|
_BtcpayServerOptions = btcpayServerOptions;
|
||||||
_BTCPayEnv = btcpayEnv;
|
_BTCPayEnv = btcpayEnv;
|
||||||
}
|
}
|
||||||
NBXplorerDashboard _Dashboard;
|
|
||||||
BTCPayServerOptions _BtcpayServerOptions;
|
BTCPayServerOptions _BtcpayServerOptions;
|
||||||
BTCPayServerEnvironment _BTCPayEnv;
|
BTCPayServerEnvironment _BTCPayEnv;
|
||||||
IServiceProvider _ServiceProvider;
|
IServiceProvider _ServiceProvider;
|
||||||
BTCPayNetworkProvider _NetworkProvider;
|
BTCPayNetworkProvider _NetworkProvider;
|
||||||
private ExplorerClientProvider _ExplorerProvider;
|
private ExplorerClientProvider _ExplorerProvider;
|
||||||
private MvcJsonOptions _MvcJsonOptions;
|
|
||||||
private IFeeProviderFactory _FeeRateProvider;
|
private IFeeProviderFactory _FeeRateProvider;
|
||||||
BTCPayWalletProvider _WalletProvider;
|
BTCPayWalletProvider _WalletProvider;
|
||||||
AccessTokenController _TokenController;
|
AccessTokenController _TokenController;
|
||||||
@@ -94,21 +88,6 @@ namespace BTCPayServer.Controllers
|
|||||||
get; set;
|
get; set;
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
|
||||||
[Route("{storeId}/wallet/{cryptoCode}")]
|
|
||||||
public IActionResult Wallet(string cryptoCode)
|
|
||||||
{
|
|
||||||
WalletModel model = new WalletModel();
|
|
||||||
model.ServerUrl = GetStoreUrl(StoreData.Id);
|
|
||||||
model.CryptoCurrency = cryptoCode;
|
|
||||||
return View(model);
|
|
||||||
}
|
|
||||||
|
|
||||||
private string GetStoreUrl(string storeId)
|
|
||||||
{
|
|
||||||
return HttpContext.Request.GetAbsoluteRoot() + "/stores/" + storeId + "/";
|
|
||||||
}
|
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
[Route("{storeId}/users")]
|
[Route("{storeId}/users")]
|
||||||
public async Task<IActionResult> StoreUsers()
|
public async Task<IActionResult> StoreUsers()
|
||||||
@@ -447,7 +426,8 @@ namespace BTCPayServer.Controllers
|
|||||||
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
vm.DerivationSchemes.Add(new StoreViewModel.DerivationScheme()
|
||||||
{
|
{
|
||||||
Crypto = network.CryptoCode,
|
Crypto = network.CryptoCode,
|
||||||
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty
|
Value = strategy?.DerivationStrategyBase?.ToString() ?? string.Empty,
|
||||||
|
WalletId = new WalletId(store.Id, network.CryptoCode),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -23,18 +23,15 @@ namespace BTCPayServer.Controllers
|
|||||||
private StoreRepository _Repo;
|
private StoreRepository _Repo;
|
||||||
private BTCPayNetworkProvider _NetworkProvider;
|
private BTCPayNetworkProvider _NetworkProvider;
|
||||||
private UserManager<ApplicationUser> _UserManager;
|
private UserManager<ApplicationUser> _UserManager;
|
||||||
private BTCPayWalletProvider _WalletProvider;
|
|
||||||
|
|
||||||
public UserStoresController(
|
public UserStoresController(
|
||||||
UserManager<ApplicationUser> userManager,
|
UserManager<ApplicationUser> userManager,
|
||||||
BTCPayNetworkProvider networkProvider,
|
BTCPayNetworkProvider networkProvider,
|
||||||
BTCPayWalletProvider walletProvider,
|
|
||||||
StoreRepository storeRepository)
|
StoreRepository storeRepository)
|
||||||
{
|
{
|
||||||
_Repo = storeRepository;
|
_Repo = storeRepository;
|
||||||
_NetworkProvider = networkProvider;
|
_NetworkProvider = networkProvider;
|
||||||
_UserManager = userManager;
|
_UserManager = userManager;
|
||||||
_WalletProvider = walletProvider;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
@@ -85,17 +82,6 @@ namespace BTCPayServer.Controllers
|
|||||||
{
|
{
|
||||||
StoresViewModel result = new StoresViewModel();
|
StoresViewModel result = new StoresViewModel();
|
||||||
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||||
|
|
||||||
var balances = stores
|
|
||||||
.Select(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
|
||||||
.OfType<DerivationStrategy>()
|
|
||||||
.Select(d => ((Wallet: _WalletProvider.GetWallet(d.Network),
|
|
||||||
DerivationStrategy: d.DerivationStrategyBase)))
|
|
||||||
.Where(_ => _.Wallet != null)
|
|
||||||
.Select(async _ => (await GetBalanceString(_)) + " " + _.Wallet.Network.CryptoCode))
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
await Task.WhenAll(balances.SelectMany(_ => _));
|
|
||||||
for (int i = 0; i < stores.Length; i++)
|
for (int i = 0; i < stores.Length; i++)
|
||||||
{
|
{
|
||||||
var store = stores[i];
|
var store = stores[i];
|
||||||
@@ -104,8 +90,7 @@ namespace BTCPayServer.Controllers
|
|||||||
Id = store.Id,
|
Id = store.Id,
|
||||||
Name = store.StoreName,
|
Name = store.StoreName,
|
||||||
WebSite = store.StoreWebsite,
|
WebSite = store.StoreWebsite,
|
||||||
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key),
|
IsOwner = store.HasClaim(Policies.CanModifyStoreSettings.Key)
|
||||||
Balances = store.HasClaim(Policies.CanModifyStoreSettings.Key) ? balances[i].Select(t => t.Result).ToArray() : Array.Empty<string>()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return View(result);
|
return View(result);
|
||||||
@@ -128,22 +113,6 @@ namespace BTCPayServer.Controllers
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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()
|
private string GetUserId()
|
||||||
{
|
{
|
||||||
return _UserManager.GetUserId(User);
|
return _UserManager.GetUserId(User);
|
||||||
|
|||||||
408
BTCPayServer/Controllers/WalletsController.cs
Normal file
408
BTCPayServer/Controllers/WalletsController.cs
Normal file
@@ -0,0 +1,408 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
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;
|
||||||
|
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;
|
||||||
|
using Microsoft.Extensions.Options;
|
||||||
|
using NBitcoin;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using static BTCPayServer.Controllers.StoresController;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Controllers
|
||||||
|
{
|
||||||
|
[Route("wallets")]
|
||||||
|
[Authorize(AuthenticationSchemes = Policies.CookieAuthentication)]
|
||||||
|
[AutoValidateAntiforgeryToken]
|
||||||
|
public class WalletsController : Controller
|
||||||
|
{
|
||||||
|
private StoreRepository _Repo;
|
||||||
|
private BTCPayNetworkProvider _NetworkProvider;
|
||||||
|
private readonly UserManager<ApplicationUser> _userManager;
|
||||||
|
private readonly IOptions<MvcJsonOptions> _mvcJsonOptions;
|
||||||
|
private readonly NBXplorerDashboard _dashboard;
|
||||||
|
private readonly ExplorerClientProvider _explorerProvider;
|
||||||
|
private readonly IFeeProviderFactory _feeRateProvider;
|
||||||
|
private readonly BTCPayWalletProvider _walletProvider;
|
||||||
|
|
||||||
|
public WalletsController(StoreRepository repo,
|
||||||
|
BTCPayNetworkProvider networkProvider,
|
||||||
|
UserManager<ApplicationUser> userManager,
|
||||||
|
IOptions<MvcJsonOptions> mvcJsonOptions,
|
||||||
|
NBXplorerDashboard dashboard,
|
||||||
|
ExplorerClientProvider explorerProvider,
|
||||||
|
IFeeProviderFactory feeRateProvider,
|
||||||
|
BTCPayWalletProvider walletProvider)
|
||||||
|
{
|
||||||
|
_Repo = repo;
|
||||||
|
_NetworkProvider = networkProvider;
|
||||||
|
_userManager = userManager;
|
||||||
|
_mvcJsonOptions = mvcJsonOptions;
|
||||||
|
_dashboard = dashboard;
|
||||||
|
_explorerProvider = explorerProvider;
|
||||||
|
_feeRateProvider = feeRateProvider;
|
||||||
|
_walletProvider = walletProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IActionResult> ListWallets()
|
||||||
|
{
|
||||||
|
var wallets = new ListWalletsViewModel();
|
||||||
|
var stores = await _Repo.GetStoresByUserId(GetUserId());
|
||||||
|
|
||||||
|
var onChainWallets = stores
|
||||||
|
.SelectMany(s => s.GetSupportedPaymentMethods(_NetworkProvider)
|
||||||
|
.OfType<DerivationStrategy>()
|
||||||
|
.Select(d => ((Wallet: _walletProvider.GetWallet(d.Network),
|
||||||
|
DerivationStrategy: d.DerivationStrategyBase,
|
||||||
|
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}")]
|
||||||
|
public async Task<IActionResult> WalletSend(
|
||||||
|
[ModelBinder(typeof(WalletIdModelBinder))]
|
||||||
|
WalletId walletId)
|
||||||
|
{
|
||||||
|
if (walletId?.StoreId == null)
|
||||||
|
return NotFound();
|
||||||
|
var store = await _Repo.FindStore(walletId.StoreId, GetUserId());
|
||||||
|
if (store == null || !store.HasClaim(Policies.CanModifyStoreSettings.Key))
|
||||||
|
return NotFound();
|
||||||
|
|
||||||
|
var paymentMethod = store
|
||||||
|
.GetSupportedPaymentMethods(_NetworkProvider)
|
||||||
|
.OfType<DerivationStrategy>()
|
||||||
|
.FirstOrDefault(p => p.PaymentId.PaymentType == Payments.PaymentTypes.BTCLike && p.PaymentId.CryptoCode == walletId.CryptoCode);
|
||||||
|
|
||||||
|
if (paymentMethod == null)
|
||||||
|
return NotFound();
|
||||||
|
WalletModel model = new WalletModel();
|
||||||
|
model.ServerUrl = GetLedgerWebsocketUrl(this.HttpContext, walletId.CryptoCode, paymentMethod.DerivationStrategyBase);
|
||||||
|
model.CryptoCurrency = walletId.CryptoCode;
|
||||||
|
return View(model);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string GetLedgerWebsocketUrl(HttpContext httpContext, string cryptoCode, DerivationStrategyBase derivationStrategy)
|
||||||
|
{
|
||||||
|
return $"{httpContext.Request.GetAbsoluteRoot().WithTrailingSlash()}ws/ledger/{cryptoCode}/{derivationStrategy?.ToString() ?? string.Empty}";
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet]
|
||||||
|
[Route("/ws/ledger/{cryptoCode}/{derivationScheme?}")]
|
||||||
|
public async Task<IActionResult> LedgerConnection(
|
||||||
|
string command,
|
||||||
|
// getinfo
|
||||||
|
string cryptoCode = null,
|
||||||
|
// getxpub
|
||||||
|
[ModelBinder(typeof(ModelBinders.DerivationSchemeModelBinder))]
|
||||||
|
DerivationStrategyBase derivationScheme = null,
|
||||||
|
int account = 0,
|
||||||
|
// sendtoaddress
|
||||||
|
string destination = null, string amount = null, string feeRate = null, string substractFees = null
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!HttpContext.WebSockets.IsWebSocketRequest)
|
||||||
|
return NotFound();
|
||||||
|
var webSocket = await HttpContext.WebSockets.AcceptWebSocketAsync();
|
||||||
|
|
||||||
|
using (var normalOperationTimeout = new CancellationTokenSource())
|
||||||
|
using (var signTimeout = new CancellationTokenSource())
|
||||||
|
{
|
||||||
|
normalOperationTimeout.CancelAfter(TimeSpan.FromMinutes(30));
|
||||||
|
var hw = new HardwareWalletService(webSocket);
|
||||||
|
object result = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
BTCPayNetwork network = null;
|
||||||
|
if (cryptoCode != null)
|
||||||
|
{
|
||||||
|
network = _NetworkProvider.GetNetwork(cryptoCode);
|
||||||
|
if (network == null)
|
||||||
|
throw new FormatException("Invalid value for crypto code");
|
||||||
|
}
|
||||||
|
|
||||||
|
BitcoinAddress destinationAddress = null;
|
||||||
|
if (destination != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
destinationAddress = BitcoinAddress.Create(destination, network.NBitcoinNetwork);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (destinationAddress == null)
|
||||||
|
throw new FormatException("Invalid value for destination");
|
||||||
|
}
|
||||||
|
|
||||||
|
FeeRate feeRateValue = null;
|
||||||
|
if (feeRate != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
feeRateValue = new FeeRate(Money.Satoshis(int.Parse(feeRate, CultureInfo.InvariantCulture)), 1);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (feeRateValue == null || feeRateValue.FeePerK <= Money.Zero)
|
||||||
|
throw new FormatException("Invalid value for fee rate");
|
||||||
|
}
|
||||||
|
|
||||||
|
Money amountBTC = null;
|
||||||
|
if (amount != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
amountBTC = Money.Parse(amount);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
if (amountBTC == null || amountBTC <= Money.Zero)
|
||||||
|
throw new FormatException("Invalid value for amount");
|
||||||
|
}
|
||||||
|
|
||||||
|
bool subsctractFeesValue = false;
|
||||||
|
if (substractFees != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
subsctractFeesValue = bool.Parse(substractFees);
|
||||||
|
}
|
||||||
|
catch { throw new FormatException("Invalid value for subtract fees"); }
|
||||||
|
}
|
||||||
|
if (command == "test")
|
||||||
|
{
|
||||||
|
result = await hw.Test(normalOperationTimeout.Token);
|
||||||
|
}
|
||||||
|
if (command == "getxpub")
|
||||||
|
{
|
||||||
|
var getxpubResult = await hw.GetExtPubKey(network, account, normalOperationTimeout.Token);
|
||||||
|
result = getxpubResult;
|
||||||
|
}
|
||||||
|
if (command == "getinfo")
|
||||||
|
{
|
||||||
|
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||||
|
if (strategy == null || await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token) == null)
|
||||||
|
{
|
||||||
|
throw new Exception($"This store is not configured to use this ledger");
|
||||||
|
}
|
||||||
|
|
||||||
|
var feeProvider = _feeRateProvider.CreateFeeProvider(network);
|
||||||
|
var recommendedFees = feeProvider.GetFeeRateAsync();
|
||||||
|
var balance = _walletProvider.GetWallet(network).GetBalance(derivationScheme);
|
||||||
|
result = new GetInfoResult() { Balance = (double)(await balance).ToDecimal(MoneyUnit.BTC), RecommendedSatoshiPerByte = (int)(await recommendedFees).GetFee(1).Satoshi };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (command == "sendtoaddress")
|
||||||
|
{
|
||||||
|
if (!_dashboard.IsFullySynched(network.CryptoCode, out var summary))
|
||||||
|
throw new Exception($"{network.CryptoCode}: not started or fully synched");
|
||||||
|
var strategy = GetDirectDerivationStrategy(derivationScheme);
|
||||||
|
var wallet = _walletProvider.GetWallet(network);
|
||||||
|
var change = wallet.GetChangeAddressAsync(derivationScheme);
|
||||||
|
|
||||||
|
var unspentCoins = await wallet.GetUnspentCoins(derivationScheme);
|
||||||
|
var changeAddress = await change;
|
||||||
|
var send = new[] { (
|
||||||
|
destination: destinationAddress as IDestination,
|
||||||
|
amount: amountBTC,
|
||||||
|
substractFees: subsctractFeesValue) };
|
||||||
|
|
||||||
|
foreach (var element in send)
|
||||||
|
{
|
||||||
|
if (element.destination == null)
|
||||||
|
throw new ArgumentNullException(nameof(element.destination));
|
||||||
|
if (element.amount == null)
|
||||||
|
throw new ArgumentNullException(nameof(element.amount));
|
||||||
|
if (element.amount <= Money.Zero)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(element.amount), "The amount should be above zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
var foundKeyPath = await hw.GetKeyPath(network, strategy, normalOperationTimeout.Token);
|
||||||
|
if (foundKeyPath == null)
|
||||||
|
{
|
||||||
|
throw new HardwareWalletException($"This store is not configured to use this ledger");
|
||||||
|
}
|
||||||
|
|
||||||
|
TransactionBuilder builder = new TransactionBuilder();
|
||||||
|
builder.StandardTransactionPolicy.MinRelayTxFee = summary.Status.BitcoinStatus.MinRelayTxFee;
|
||||||
|
builder.SetConsensusFactory(network.NBitcoinNetwork);
|
||||||
|
builder.AddCoins(unspentCoins.Select(c => c.Coin).ToArray());
|
||||||
|
|
||||||
|
foreach (var element in send)
|
||||||
|
{
|
||||||
|
builder.Send(element.destination, element.amount);
|
||||||
|
if (element.substractFees)
|
||||||
|
builder.SubtractFees();
|
||||||
|
}
|
||||||
|
builder.SetChange(changeAddress.Item1);
|
||||||
|
|
||||||
|
if (network.MinFee == null)
|
||||||
|
{
|
||||||
|
builder.SendEstimatedFees(feeRateValue);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var estimatedFee = builder.EstimateFees(feeRateValue);
|
||||||
|
if (network.MinFee > estimatedFee)
|
||||||
|
builder.SendFees(network.MinFee);
|
||||||
|
else
|
||||||
|
builder.SendEstimatedFees(feeRateValue);
|
||||||
|
}
|
||||||
|
builder.Shuffle();
|
||||||
|
var unsigned = builder.BuildTransaction(false);
|
||||||
|
|
||||||
|
var keypaths = new Dictionary<Script, KeyPath>();
|
||||||
|
foreach (var c in unspentCoins)
|
||||||
|
{
|
||||||
|
keypaths.TryAdd(c.Coin.ScriptPubKey, c.KeyPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasChange = unsigned.Outputs.Count == 2;
|
||||||
|
var usedCoins = builder.FindSpentCoins(unsigned);
|
||||||
|
|
||||||
|
Dictionary<uint256, Transaction> parentTransactions = new Dictionary<uint256, Transaction>();
|
||||||
|
|
||||||
|
if (!strategy.Segwit)
|
||||||
|
{
|
||||||
|
var parentHashes = usedCoins.Select(c => c.Outpoint.Hash).ToHashSet();
|
||||||
|
var explorer = _explorerProvider.GetExplorerClient(network);
|
||||||
|
var getTransactionAsyncs = parentHashes.Select(h => (Op: explorer.GetTransactionAsync(h), Hash: h)).ToList();
|
||||||
|
foreach (var getTransactionAsync in getTransactionAsyncs)
|
||||||
|
{
|
||||||
|
var tx = (await getTransactionAsync.Op);
|
||||||
|
if (tx == null)
|
||||||
|
throw new Exception($"Parent transaction {getTransactionAsync.Hash} not found");
|
||||||
|
parentTransactions.Add(tx.Transaction.GetHash(), tx.Transaction);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
signTimeout.CancelAfter(TimeSpan.FromMinutes(5));
|
||||||
|
var transaction = await hw.SignTransactionAsync(usedCoins.Select(c => new SignatureRequest
|
||||||
|
{
|
||||||
|
InputTransaction = parentTransactions.TryGet(c.Outpoint.Hash),
|
||||||
|
InputCoin = c,
|
||||||
|
KeyPath = foundKeyPath.Derive(keypaths[c.TxOut.ScriptPubKey]),
|
||||||
|
PubKey = strategy.Root.Derive(keypaths[c.TxOut.ScriptPubKey]).PubKey
|
||||||
|
}).ToArray(), unsigned, hasChange ? foundKeyPath.Derive(changeAddress.Item2) : null, signTimeout.Token);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var broadcastResult = await wallet.BroadcastTransactionsAsync(new List<Transaction>() { transaction });
|
||||||
|
if (!broadcastResult[0].Success)
|
||||||
|
{
|
||||||
|
throw new Exception($"RPC Error while broadcasting: {broadcastResult[0].RPCCode} {broadcastResult[0].RPCCodeMessage} {broadcastResult[0].RPCMessage}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
throw new Exception("Error while broadcasting: " + ex.Message);
|
||||||
|
}
|
||||||
|
wallet.InvalidateCache(derivationScheme);
|
||||||
|
result = new SendToAddressResult() { TransactionId = transaction.GetHash().ToString() };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
private DirectDerivationStrategy GetDirectDerivationStrategy(DerivationStrategyBase strategy)
|
||||||
|
{
|
||||||
|
if (strategy == null)
|
||||||
|
throw new Exception("The derivation scheme is not provided");
|
||||||
|
var directStrategy = strategy as DirectDerivationStrategy;
|
||||||
|
if (directStrategy == null)
|
||||||
|
directStrategy = (strategy as P2SHDerivationStrategy).Inner as DirectDerivationStrategy;
|
||||||
|
return directStrategy;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public class GetInfoResult
|
||||||
|
{
|
||||||
|
public int RecommendedSatoshiPerByte { get; set; }
|
||||||
|
public double Balance { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class SendToAddressResult
|
||||||
|
{
|
||||||
|
public string TransactionId { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -39,6 +39,8 @@ using BTCPayServer.HostedServices;
|
|||||||
using Meziantou.AspNetCore.BundleTagHelpers;
|
using Meziantou.AspNetCore.BundleTagHelpers;
|
||||||
using System.Security.Claims;
|
using System.Security.Claims;
|
||||||
using BTCPayServer.Security;
|
using BTCPayServer.Security;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
|
|
||||||
namespace BTCPayServer.Hosting
|
namespace BTCPayServer.Hosting
|
||||||
{
|
{
|
||||||
@@ -105,7 +107,11 @@ namespace BTCPayServer.Hosting
|
|||||||
});
|
});
|
||||||
|
|
||||||
services.AddSingleton<CssThemeManager>();
|
services.AddSingleton<CssThemeManager>();
|
||||||
services.Configure<MvcOptions>((o) => { o.Filters.Add(new ContentSecurityPolicyCssThemeManager()); });
|
services.Configure<MvcOptions>((o) => {
|
||||||
|
o.Filters.Add(new ContentSecurityPolicyCssThemeManager());
|
||||||
|
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(WalletId)));
|
||||||
|
o.ModelMetadataDetailsProviders.Add(new SuppressChildValidationMetadataProvider(typeof(DerivationStrategyBase)));
|
||||||
|
});
|
||||||
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
services.AddSingleton<IHostedService, CssThemeManagerHostedService>();
|
||||||
services.AddSingleton<IHostedService, MigratorHostedService>();
|
services.AddSingleton<IHostedService, MigratorHostedService>();
|
||||||
|
|
||||||
|
|||||||
@@ -117,13 +117,13 @@ namespace BTCPayServer.Hosting
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Needed to debug U2F for ledger support
|
// Needed to debug U2F for ledger support
|
||||||
//services.Configure<KestrelServerOptions>(kestrel =>
|
services.Configure<KestrelServerOptions>(kestrel =>
|
||||||
//{
|
{
|
||||||
// kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
kestrel.Listen(IPAddress.Loopback, 5012, l =>
|
||||||
// {
|
{
|
||||||
// l.UseHttps("devtest.pfx", "toto");
|
l.UseHttps("devtest.pfx", "toto");
|
||||||
// });
|
});
|
||||||
//});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(
|
public void Configure(
|
||||||
|
|||||||
58
BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs
Normal file
58
BTCPayServer/ModelBinders/DerivationSchemeModelBinder.cs
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
using NBitcoin;
|
||||||
|
using System.Reflection;
|
||||||
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc.Internal;
|
||||||
|
using NBXplorer.DerivationStrategy;
|
||||||
|
|
||||||
|
namespace BTCPayServer.ModelBinders
|
||||||
|
{
|
||||||
|
public class DerivationSchemeModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
public DerivationSchemeModelBinder()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
#region IModelBinder Members
|
||||||
|
|
||||||
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (!typeof(DerivationStrategyBase).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
|
||||||
|
bindingContext.ModelName);
|
||||||
|
if (val == null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = val.FirstValue as string;
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
var networkProvider = (BTCPayNetworkProvider)bindingContext.HttpContext.RequestServices.GetService(typeof(BTCPayNetworkProvider));
|
||||||
|
var cryptoCode = bindingContext.ValueProvider.GetValue("cryptoCode").FirstValue;
|
||||||
|
var network = networkProvider.GetNetwork(cryptoCode ?? "BTC");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var data = new DerivationStrategyFactory(network.NBitcoinNetwork).Parse(key);
|
||||||
|
if (!bindingContext.ModelType.IsInstanceOfType(data))
|
||||||
|
{
|
||||||
|
throw new FormatException("Invalid destination type");
|
||||||
|
}
|
||||||
|
bindingContext.Result = ModelBindingResult.Success(data);
|
||||||
|
}
|
||||||
|
catch { throw new FormatException("Invalid derivation scheme"); }
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
|
}
|
||||||
|
}
|
||||||
39
BTCPayServer/ModelBinders/WalletIdModelBinder.cs
Normal file
39
BTCPayServer/ModelBinders/WalletIdModelBinder.cs
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.AspNetCore.Mvc.ModelBinding;
|
||||||
|
|
||||||
|
namespace BTCPayServer.ModelBinders
|
||||||
|
{
|
||||||
|
public class WalletIdModelBinder : IModelBinder
|
||||||
|
{
|
||||||
|
public Task BindModelAsync(ModelBindingContext bindingContext)
|
||||||
|
{
|
||||||
|
if (!typeof(WalletId).GetTypeInfo().IsAssignableFrom(bindingContext.ModelType))
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
ValueProviderResult val = bindingContext.ValueProvider.GetValue(
|
||||||
|
bindingContext.ModelName);
|
||||||
|
if (val == null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
string key = val.FirstValue as string;
|
||||||
|
if (key == null)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(WalletId.TryParse(key, out var walletId))
|
||||||
|
{
|
||||||
|
bindingContext.Result = ModelBindingResult.Success(walletId);
|
||||||
|
}
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
{
|
{
|
||||||
public string Crypto { get; set; }
|
public string Crypto { get; set; }
|
||||||
public string Value { get; set; }
|
public string Value { get; set; }
|
||||||
|
public WalletId WalletId { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public StoreViewModel()
|
public StoreViewModel()
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ namespace BTCPayServer.Models.StoreViewModels
|
|||||||
get;
|
get;
|
||||||
set;
|
set;
|
||||||
}
|
}
|
||||||
public string[] Balances
|
|
||||||
{
|
|
||||||
get; set;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
22
BTCPayServer/Models/WalletViewModels/ListWalletsViewModel.cs
Normal file
22
BTCPayServer/Models/WalletViewModels/ListWalletsViewModel.cs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Models.WalletViewModels
|
||||||
|
{
|
||||||
|
public class ListWalletsViewModel
|
||||||
|
{
|
||||||
|
public class WalletViewModel
|
||||||
|
{
|
||||||
|
public string StoreName { get; set; }
|
||||||
|
public string StoreId { get; set; }
|
||||||
|
public string CryptoCode { get; set; }
|
||||||
|
public string Balance { get; set; }
|
||||||
|
public bool IsOwner { get; set; }
|
||||||
|
public WalletId Id { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<WalletViewModel> Wallets { get; set; } = new List<WalletViewModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -5,7 +5,7 @@ using System.Linq;
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||||
|
|
||||||
namespace BTCPayServer.Models.StoreViewModels
|
namespace BTCPayServer.Models.WalletViewModels
|
||||||
{
|
{
|
||||||
public class WalletModel
|
public class WalletModel
|
||||||
{
|
{
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
}
|
}
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="UserStores" asp-action="ListStores" class="nav-link js-scroll-trigger">Stores</a></li>
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="Apps" asp-action="ListApps" class="nav-link js-scroll-trigger">Apps</a></li>
|
||||||
|
<li class="nav-item"><a asp-area="" asp-controller="Wallets" asp-action="ListWallets" class="nav-link js-scroll-trigger">Wallets</a></li>
|
||||||
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
<li class="nav-item"><a asp-area="" asp-controller="Invoice" asp-action="ListInvoices" class="nav-link js-scroll-trigger">Invoices</a></li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
<a asp-area="" asp-controller="Manage" asp-action="Index" title="Manage" class="nav-link js-scroll-trigger">My settings</a>
|
||||||
|
|||||||
@@ -82,7 +82,7 @@
|
|||||||
<td style="text-align:right">
|
<td style="text-align:right">
|
||||||
@if(!string.IsNullOrWhiteSpace(scheme.Value))
|
@if(!string.IsNullOrWhiteSpace(scheme.Value))
|
||||||
{
|
{
|
||||||
<a asp-action="Wallet" asp-route-cryptoCode="@scheme.Crypto">Wallet</a><span> - </span>
|
<a asp-action="WalletSend" asp-controller="Wallets" asp-route-walletId="@scheme.WalletId">Wallet</a><span> - </span>
|
||||||
}
|
}
|
||||||
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
|
<a asp-action="AddDerivationScheme" asp-route-cryptoCode="@scheme.Crypto">Modify</a>
|
||||||
</td>
|
</td>
|
||||||
|
|||||||
@@ -27,7 +27,6 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>Name</th>
|
<th>Name</th>
|
||||||
<th>Website</th>
|
<th>Website</th>
|
||||||
<th>On-Chain balances</th>
|
|
||||||
<th style="text-align:right">Actions</th>
|
<th style="text-align:right">Actions</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@@ -42,16 +41,6 @@
|
|||||||
<a href="@store.WebSite">@store.WebSite</a>
|
<a href="@store.WebSite">@store.WebSite</a>
|
||||||
}
|
}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
|
||||||
@for (int i = 0; i < store.Balances.Length; i++)
|
|
||||||
{
|
|
||||||
<span>@store.Balances[i]</span>
|
|
||||||
if (i != store.Balances.Length - 1)
|
|
||||||
{
|
|
||||||
<br />
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</td>
|
|
||||||
<td style="text-align:right">
|
<td style="text-align:right">
|
||||||
@if (store.IsOwner)
|
@if (store.IsOwner)
|
||||||
{
|
{
|
||||||
|
|||||||
56
BTCPayServer/Views/Wallets/ListWallets.cshtml
Normal file
56
BTCPayServer/Views/Wallets/ListWallets.cshtml
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
@model BTCPayServer.Models.WalletViewModels.ListWalletsViewModel
|
||||||
|
@{
|
||||||
|
ViewData["Title"] = "Wallets";
|
||||||
|
}
|
||||||
|
|
||||||
|
<section>
|
||||||
|
<div class="container">
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<partial name="_StatusMessage" for="@TempData["TempDataProperty-StatusMessage"]" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-lg-12 text-center">
|
||||||
|
<h2 class="section-heading">@ViewData["Title"]</h2>
|
||||||
|
<hr class="primary">
|
||||||
|
<p>Create and manage wallets.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<table class="table table-sm table-responsive-md">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Store Name</th>
|
||||||
|
<th>Crypto Code</th>
|
||||||
|
<th>Balance</th>
|
||||||
|
<th style="text-align:right">Actions</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
@foreach(var wallet in Model.Wallets)
|
||||||
|
{
|
||||||
|
<tr>
|
||||||
|
@if(wallet.IsOwner)
|
||||||
|
{
|
||||||
|
<td><a asp-action="UpdateStore" asp-controller="Stores" asp-route-storeId="@wallet.StoreId">@wallet.StoreName</a></td>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<td>@wallet.StoreName</td>
|
||||||
|
}
|
||||||
|
<td>@wallet.CryptoCode</td>
|
||||||
|
<td>@wallet.Balance</td>
|
||||||
|
<td style="text-align:right">
|
||||||
|
<a asp-action="WalletSend" asp-route-walletId="@wallet.Id">Manage</a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
@{
|
@{
|
||||||
Layout = "../Shared/_NavLayout.cshtml";
|
Layout = "../Shared/_NavLayout.cshtml";
|
||||||
ViewData["Title"] = "Manage wallet";
|
ViewData["Title"] = "Manage wallet";
|
||||||
|
ViewData.SetActivePageAndTitle(WalletsNavPages.Send);
|
||||||
}
|
}
|
||||||
|
|
||||||
<h4>@ViewData["Title"]</h4>
|
<h4>@ViewData["Title"]</h4>
|
||||||
12
BTCPayServer/Views/Wallets/WalletsNavPages.cs
Normal file
12
BTCPayServer/Views/Wallets/WalletsNavPages.cs
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer.Views.Wallets
|
||||||
|
{
|
||||||
|
public enum WalletsNavPages
|
||||||
|
{
|
||||||
|
Send
|
||||||
|
}
|
||||||
|
}
|
||||||
6
BTCPayServer/Views/Wallets/_Nav.cshtml
Normal file
6
BTCPayServer/Views/Wallets/_Nav.cshtml
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
@inject SignInManager<ApplicationUser> SignInManager
|
||||||
|
|
||||||
|
<div class="nav flex-column nav-pills">
|
||||||
|
<a class="nav-link @ViewData.IsActivePage(WalletsNavPages.Send)" asp-action="WalletSend">Send</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
2
BTCPayServer/Views/Wallets/_ViewImports.cshtml
Normal file
2
BTCPayServer/Views/Wallets/_ViewImports.cshtml
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
@using BTCPayServer.Views.Wallets
|
||||||
|
@using BTCPayServer.Models.WalletViewModels
|
||||||
3
BTCPayServer/Views/Wallets/_ViewStart.cshtml
Normal file
3
BTCPayServer/Views/Wallets/_ViewStart.cshtml
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
@{
|
||||||
|
ViewBag.MainTitle = "Manage wallet";
|
||||||
|
}
|
||||||
40
BTCPayServer/WalletId.cs
Normal file
40
BTCPayServer/WalletId.cs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace BTCPayServer
|
||||||
|
{
|
||||||
|
public class WalletId
|
||||||
|
{
|
||||||
|
static readonly Regex _WalletStoreRegex = new Regex("^S-([a-zA-Z0-9]{30,60})-([a-zA-Z]{2,5})$");
|
||||||
|
public static bool TryParse(string str, out WalletId walletId)
|
||||||
|
{
|
||||||
|
walletId = null;
|
||||||
|
WalletId w = new WalletId();
|
||||||
|
var match = _WalletStoreRegex.Match(str);
|
||||||
|
if (!match.Success)
|
||||||
|
return false;
|
||||||
|
w.StoreId = match.Groups[1].Value;
|
||||||
|
w.CryptoCode = match.Groups[2].Value.ToUpperInvariant();
|
||||||
|
walletId = w;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
public WalletId()
|
||||||
|
{
|
||||||
|
|
||||||
|
}
|
||||||
|
public WalletId(string storeId, string cryptoCode)
|
||||||
|
{
|
||||||
|
StoreId = storeId;
|
||||||
|
CryptoCode = cryptoCode;
|
||||||
|
}
|
||||||
|
public string StoreId { get; set; }
|
||||||
|
public string CryptoCode { get; set; }
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"S-{StoreId}-{CryptoCode.ToUpperInvariant()}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
var ledgerDetected = false;
|
var ledgerDetected = false;
|
||||||
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel + "ws/ledger");
|
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel);
|
||||||
|
|
||||||
var cryptoSelector = $("#CryptoCurrency");
|
var cryptoSelector = $("#CryptoCurrency");
|
||||||
function GetSelectedCryptoCode() {
|
function GetSelectedCryptoCode() {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
$(function () {
|
$(function () {
|
||||||
var ledgerDetected = false;
|
var ledgerDetected = false;
|
||||||
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl + "ws/ledger");
|
var bridge = new ledgerwebsocket.LedgerWebSocketBridge(srvModel.serverUrl);
|
||||||
var recommendedFees = "";
|
var recommendedFees = "";
|
||||||
var recommendedBalance = "";
|
var recommendedBalance = "";
|
||||||
var cryptoCode = $("#cryptoCode").val();
|
var cryptoCode = $("#cryptoCode").val();
|
||||||
|
|||||||
Reference in New Issue
Block a user