mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2025-12-18 14:34:23 +01:00
Added custodian account trade support (#3978)
* Added custodian account trade support * UI updates * Improved UI spacing and field sizes + Fixed input validation * Reset error message when opening trade modal * Better error handing + test + surface error in trade modal in UI * Add delete confirmation modal * Fixed duplicate ID in site nav * Replace jQuery.ajax with fetch for onTradeSubmit * Added support for minimumTradeQty to trading pairs * Fixed LocalBTCPayServerClient after previous refactoring * Handling dust amounts + minor API change * Replaced jQuery with Fetch API + UX improvements + more TODOs * Moved namespace because Rider was unhappy * Major UI improvements when swapping or changing assets, fixed bugs in min trade qty, fixed initial qty after an asset change etc * Commented out code for easier debugging * Fixed missing default values Co-authored-by: Dennis Reimann <mail@dennisreimann.de>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using BTCPayServer.Abstractions.Constants;
|
||||
@@ -7,6 +8,8 @@ using BTCPayServer.Abstractions.Custodians;
|
||||
using BTCPayServer.Abstractions.Extensions;
|
||||
using BTCPayServer.Abstractions.Form;
|
||||
using BTCPayServer.Client;
|
||||
using BTCPayServer.Client.Models;
|
||||
using BTCPayServer.Controllers.Greenfield;
|
||||
using BTCPayServer.Data;
|
||||
using BTCPayServer.Filters;
|
||||
using BTCPayServer.Models.CustodianAccountViewModels;
|
||||
@@ -26,30 +29,32 @@ namespace BTCPayServer.Controllers
|
||||
[ExperimentalRouteAttribute]
|
||||
public class UICustodianAccountsController : Controller
|
||||
{
|
||||
private readonly IEnumerable<ICustodian> _custodianRegistry;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
private readonly BTCPayServerClient _btcPayServerClient;
|
||||
|
||||
public UICustodianAccountsController(
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
IEnumerable<ICustodian> custodianRegistry
|
||||
IEnumerable<ICustodian> custodianRegistry,
|
||||
BTCPayServerClient btcPayServerClient
|
||||
)
|
||||
{
|
||||
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_userManager = userManager;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_custodianRegistry = custodianRegistry;
|
||||
_btcPayServerClient = btcPayServerClient;
|
||||
}
|
||||
|
||||
private readonly IEnumerable<ICustodian> _custodianRegistry;
|
||||
private readonly UserManager<ApplicationUser> _userManager;
|
||||
private readonly CustodianAccountRepository _custodianAccountRepository;
|
||||
private readonly CurrencyNameTable _currencyNameTable;
|
||||
|
||||
public string CreatedCustodianAccountId { get; set; }
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}")]
|
||||
public async Task<IActionResult> ViewCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
var vm = new ViewCustodianAccountViewModel();
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
@@ -62,14 +67,34 @@ namespace BTCPayServer.Controllers
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var vm = new ViewCustodianAccountViewModel();
|
||||
vm.Custodian = custodian;
|
||||
vm.CustodianAccount = custodianAccount;
|
||||
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}.json")]
|
||||
public async Task<IActionResult> ViewCustodianAccountAjax(string storeId, string accountId)
|
||||
{
|
||||
var vm = new ViewCustodianAccountBalancesViewModel();
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var store = GetCurrentStore();
|
||||
var storeBlob = BTCPayServer.Data.StoreDataExtensions.GetStoreBlob(store);
|
||||
var storeBlob = StoreDataExtensions.GetStoreBlob(store);
|
||||
var defaultCurrency = storeBlob.DefaultCurrency;
|
||||
vm.DefaultCurrency = defaultCurrency;
|
||||
|
||||
|
||||
vm.DustThresholdInFiat = 1;
|
||||
vm.StoreDefaultFiat = defaultCurrency;
|
||||
try
|
||||
{
|
||||
var assetBalances = new Dictionary<string, AssetBalanceInfo>();
|
||||
@@ -81,11 +106,15 @@ namespace BTCPayServer.Controllers
|
||||
var asset = pair.Key;
|
||||
|
||||
assetBalances.Add(asset,
|
||||
new AssetBalanceInfo { Asset = asset, Qty = pair.Value }
|
||||
new AssetBalanceInfo
|
||||
{
|
||||
Asset = asset,
|
||||
Qty = pair.Value,
|
||||
FormattedQty = pair.Value.ToString(CultureInfo.InvariantCulture)
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
if (custodian is ICanTrade tradingCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
@@ -95,10 +124,20 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
var asset = pair.Key;
|
||||
var assetBalance = assetBalances[asset];
|
||||
var tradableAssetPairsList =
|
||||
tradableAssetPairs.Where(o => o.AssetBought == asset || o.AssetSold == asset).ToList();
|
||||
var tradableAssetPairsDict = new Dictionary<string, AssetPairData>(tradableAssetPairsList.Count);
|
||||
foreach (var assetPair in tradableAssetPairsList)
|
||||
{
|
||||
tradableAssetPairsDict.Add(assetPair.ToString(), assetPair);
|
||||
}
|
||||
assetBalance.TradableAssetPairs = tradableAssetPairsDict;
|
||||
|
||||
if (asset.Equals(defaultCurrency))
|
||||
{
|
||||
assetBalance.FormattedFiatValue = _currencyNameTable.DisplayFormatCurrency(pair.Value.Qty, defaultCurrency);
|
||||
assetBalance.FormattedFiatValue =
|
||||
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty, defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty;
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -108,11 +147,14 @@ namespace BTCPayServer.Controllers
|
||||
config, default);
|
||||
assetBalance.Bid = quote.Bid;
|
||||
assetBalance.Ask = quote.Ask;
|
||||
assetBalance.FiatAsset = defaultCurrency;
|
||||
assetBalance.FormattedBid = _currencyNameTable.DisplayFormatCurrency(quote.Bid, quote.FromAsset);
|
||||
assetBalance.FormattedAsk = _currencyNameTable.DisplayFormatCurrency(quote.Ask, quote.FromAsset);
|
||||
assetBalance.FormattedFiatValue = _currencyNameTable.DisplayFormatCurrency(pair.Value.Qty * quote.Bid, pair.Value.FiatAsset);
|
||||
assetBalance.TradableAssetPairs = tradableAssetPairs.Where(o => o.AssetBought == asset || o.AssetSold == asset);
|
||||
assetBalance.FormattedBid =
|
||||
_currencyNameTable.DisplayFormatCurrency(quote.Bid, quote.FromAsset);
|
||||
assetBalance.FormattedAsk =
|
||||
_currencyNameTable.DisplayFormatCurrency(quote.Ask, quote.FromAsset);
|
||||
assetBalance.FormattedFiatValue =
|
||||
_currencyNameTable.DisplayFormatCurrency(pair.Value.Qty * quote.Bid,
|
||||
defaultCurrency);
|
||||
assetBalance.FiatValue = pair.Value.Qty * quote.Bid;
|
||||
}
|
||||
catch (WrongTradingPairException)
|
||||
{
|
||||
@@ -136,8 +178,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
vm.CanDeposit = false;
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
vm.CanDeposit = true;
|
||||
var depositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||
foreach (var depositablePaymentMethod in depositablePaymentMethods)
|
||||
{
|
||||
@@ -154,10 +198,10 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
vm.GetAssetBalanceException = e;
|
||||
vm.AssetBalanceExceptionMessage = e.Message;
|
||||
}
|
||||
|
||||
return View(vm);
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/edit")]
|
||||
@@ -173,6 +217,7 @@ namespace BTCPayServer.Controllers
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configForm = await custodian.GetConfigForm(custodianAccount.GetBlob(), "en-US");
|
||||
|
||||
var vm = new EditCustodianAccountViewModel();
|
||||
@@ -198,6 +243,7 @@ namespace BTCPayServer.Controllers
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var configForm = await custodian.GetConfigForm(custodianAccount.GetBlob(), locale);
|
||||
|
||||
var newData = new JObject();
|
||||
@@ -255,12 +301,16 @@ namespace BTCPayServer.Controllers
|
||||
ModelState.AddModelError(nameof(vm.SelectedCustodian), "Invalid Custodian");
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(vm.Name))
|
||||
{
|
||||
vm.Name = custodian.Name;
|
||||
}
|
||||
|
||||
var custodianAccountData = new CustodianAccountData { CustodianCode = vm.SelectedCustodian, StoreId = vm.StoreId, Name = custodian.Name };
|
||||
var custodianAccountData = new CustodianAccountData
|
||||
{
|
||||
CustodianCode = vm.SelectedCustodian, StoreId = vm.StoreId, Name = custodian.Name
|
||||
};
|
||||
|
||||
|
||||
var configData = new JObject();
|
||||
@@ -287,8 +337,7 @@ namespace BTCPayServer.Controllers
|
||||
return View(vm);
|
||||
}
|
||||
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/delete")]
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/delete")]
|
||||
public async Task<IActionResult> DeleteCustodianAccount(string storeId, string accountId)
|
||||
{
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
@@ -301,7 +350,7 @@ namespace BTCPayServer.Controllers
|
||||
if (isDeleted)
|
||||
{
|
||||
TempData[WellKnownTempData.SuccessMessage] = "Custodian account deleted";
|
||||
return RedirectToAction("Dashboard", "UIStores", new { storeId });
|
||||
return RedirectToAction(nameof(UIStoresController.Dashboard), "UIStores", new { storeId });
|
||||
}
|
||||
|
||||
TempData[WellKnownTempData.ErrorMessage] = "Could not delete custodian account";
|
||||
@@ -335,6 +384,98 @@ namespace BTCPayServer.Controllers
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/trade/prepare")]
|
||||
public async Task<IActionResult> GetTradePrepareAjax(string storeId, string accountId,
|
||||
[FromQuery] string assetToTrade, [FromQuery] string assetToTradeInto)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetToTrade) || string.IsNullOrEmpty(assetToTradeInto))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
TradePrepareViewModel vm = new();
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
|
||||
if (custodianAccount == null)
|
||||
return NotFound();
|
||||
|
||||
var custodian = _custodianRegistry.GetCustodianByCode(custodianAccount.CustodianCode);
|
||||
if (custodian == null)
|
||||
{
|
||||
// TODO The custodian account is broken. The custodian is no longer available. Maybe delete the custodian account?
|
||||
return NotFound();
|
||||
}
|
||||
|
||||
var store = GetCurrentStore();
|
||||
var storeBlob = BTCPayServer.Data.StoreDataExtensions.GetStoreBlob(store);
|
||||
var defaultCurrency = storeBlob.DefaultCurrency;
|
||||
|
||||
try
|
||||
{
|
||||
var assetBalancesData =
|
||||
await custodian.GetAssetBalancesAsync(custodianAccount.GetBlob(), cancellationToken: default);
|
||||
|
||||
if (custodian is ICanTrade tradingCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
foreach (var pair in assetBalancesData)
|
||||
{
|
||||
var oneAsset = pair.Key;
|
||||
if (assetToTrade.Equals(oneAsset))
|
||||
{
|
||||
vm.MaxQtyToTrade = pair.Value;
|
||||
//vm.FormattedMaxQtyToTrade = pair.Value;
|
||||
|
||||
if (assetToTrade.Equals(assetToTradeInto))
|
||||
{
|
||||
// We cannot trade the asset for itself
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var quote = await tradingCustodian.GetQuoteForAssetAsync(assetToTrade, assetToTradeInto,
|
||||
config, default);
|
||||
|
||||
// TODO Ask is normally a higher number than Bid!! Let's check this!! Maybe a Unit Test?
|
||||
vm.Ask = quote.Ask;
|
||||
vm.Bid = quote.Bid;
|
||||
vm.FromAsset = quote.FromAsset;
|
||||
vm.ToAsset = quote.ToAsset;
|
||||
}
|
||||
catch (WrongTradingPairException)
|
||||
{
|
||||
// Cannot trade this asset, just ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
[HttpPost("/stores/{storeId}/custodian-accounts/{accountId}/trade")]
|
||||
public async Task<IActionResult> Trade(string storeId, string accountId,
|
||||
[FromBody] TradeRequestData request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _btcPayServerClient.MarketTradeCustodianAccountAsset(storeId, accountId, request);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (GreenfieldAPIException e)
|
||||
{
|
||||
var result = new ObjectResult(e.APIError) { StatusCode = e.HttpCode };
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user