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:
Wouter Samaey
2022-08-04 04:38:49 +02:00
committed by GitHub
parent 2ea6eb09e6
commit c71e671311
26 changed files with 1058 additions and 251 deletions

View File

@@ -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();
}
}