mirror of
https://github.com/aljazceru/btcpayserver.git
synced 2026-02-23 15:14:49 +01:00
Custodian Account Deposit UI (#4024)
Co-authored-by: d11n <mail@dennisreimann.de>
This commit is contained in:
@@ -9,15 +9,16 @@ 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;
|
||||
using BTCPayServer.Payments;
|
||||
using BTCPayServer.Services.Custodian.Client;
|
||||
using BTCPayServer.Services.Rates;
|
||||
using Microsoft.AspNetCore.Authorization;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
using Microsoft.AspNetCore.Mvc;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using CustodianAccountData = BTCPayServer.Data.CustodianAccountData;
|
||||
using StoreData = BTCPayServer.Data.StoreData;
|
||||
@@ -30,24 +31,28 @@ namespace BTCPayServer.Controllers
|
||||
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;
|
||||
private readonly BTCPayNetworkProvider _networkProvider;
|
||||
private readonly LinkGenerator _linkGenerator;
|
||||
|
||||
public UICustodianAccountsController(
|
||||
CurrencyNameTable currencyNameTable,
|
||||
UserManager<ApplicationUser> userManager,
|
||||
CustodianAccountRepository custodianAccountRepository,
|
||||
IEnumerable<ICustodian> custodianRegistry,
|
||||
BTCPayServerClient btcPayServerClient
|
||||
BTCPayServerClient btcPayServerClient,
|
||||
BTCPayNetworkProvider networkProvider,
|
||||
LinkGenerator linkGenerator
|
||||
)
|
||||
{
|
||||
_currencyNameTable = currencyNameTable ?? throw new ArgumentNullException(nameof(currencyNameTable));
|
||||
_userManager = userManager;
|
||||
_custodianAccountRepository = custodianAccountRepository;
|
||||
_custodianRegistry = custodianRegistry;
|
||||
_btcPayServerClient = btcPayServerClient;
|
||||
_networkProvider = networkProvider;
|
||||
_linkGenerator = linkGenerator;
|
||||
}
|
||||
|
||||
public string CreatedCustodianAccountId { get; set; }
|
||||
@@ -75,7 +80,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}.json")]
|
||||
public async Task<IActionResult> ViewCustodianAccountAjax(string storeId, string accountId)
|
||||
public async Task<IActionResult> ViewCustodianAccountJson(string storeId, string accountId)
|
||||
{
|
||||
var vm = new ViewCustodianAccountBalancesViewModel();
|
||||
var custodianAccount = await _custodianAccountRepository.FindById(storeId, accountId);
|
||||
@@ -93,6 +98,7 @@ namespace BTCPayServer.Controllers
|
||||
var store = GetCurrentStore();
|
||||
var storeBlob = StoreDataExtensions.GetStoreBlob(store);
|
||||
var defaultCurrency = storeBlob.DefaultCurrency;
|
||||
vm.StoreId = store.Id;
|
||||
vm.DustThresholdInFiat = 1;
|
||||
vm.StoreDefaultFiat = defaultCurrency;
|
||||
try
|
||||
@@ -126,11 +132,13 @@ namespace BTCPayServer.Controllers
|
||||
var assetBalance = assetBalances[asset];
|
||||
var tradableAssetPairsList =
|
||||
tradableAssetPairs.Where(o => o.AssetBought == asset || o.AssetSold == asset).ToList();
|
||||
var tradableAssetPairsDict = new Dictionary<string, AssetPairData>(tradableAssetPairsList.Count);
|
||||
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))
|
||||
@@ -178,20 +186,9 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
vm.CanDeposit = false;
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
vm.CanDeposit = true;
|
||||
var depositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||
foreach (var depositablePaymentMethod in depositablePaymentMethods)
|
||||
{
|
||||
var depositableAsset = depositablePaymentMethod.Split("-")[0];
|
||||
if (assetBalances.ContainsKey(depositableAsset))
|
||||
{
|
||||
var assetBalance = assetBalances[depositableAsset];
|
||||
assetBalance.CanDeposit = true;
|
||||
}
|
||||
}
|
||||
vm.DepositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||
}
|
||||
|
||||
vm.AssetBalances = assetBalances;
|
||||
@@ -385,7 +382,7 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/trade/prepare")]
|
||||
public async Task<IActionResult> GetTradePrepareAjax(string storeId, string accountId,
|
||||
public async Task<IActionResult> GetTradePrepareJson(string storeId, string accountId,
|
||||
[FromQuery] string assetToTrade, [FromQuery] string assetToTradeInto)
|
||||
{
|
||||
if (string.IsNullOrEmpty(assetToTrade) || string.IsNullOrEmpty(assetToTradeInto))
|
||||
@@ -437,7 +434,7 @@ namespace BTCPayServer.Controllers
|
||||
{
|
||||
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;
|
||||
@@ -476,6 +473,105 @@ namespace BTCPayServer.Controllers
|
||||
}
|
||||
}
|
||||
|
||||
[HttpGet("/stores/{storeId}/custodian-accounts/{accountId}/deposit/prepare")]
|
||||
public async Task<IActionResult> GetDepositPrepareJson(string storeId, string accountId,
|
||||
[FromQuery] string paymentMethod)
|
||||
{
|
||||
if (string.IsNullOrEmpty(paymentMethod))
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
DepositPrepareViewModel 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();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (custodian is ICanDeposit depositableCustodian)
|
||||
{
|
||||
var config = custodianAccount.GetBlob();
|
||||
|
||||
vm.PaymentMethod = paymentMethod;
|
||||
var depositablePaymentMethods = depositableCustodian.GetDepositablePaymentMethods();
|
||||
if (!depositablePaymentMethods.Contains(paymentMethod))
|
||||
{
|
||||
vm.ErrorMessage = $"Payment method \"{paymentMethod}\" is not supported by {custodian.Name}";
|
||||
return BadRequest(vm);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var depositAddressResult =
|
||||
await depositableCustodian.GetDepositAddressAsync(paymentMethod, config, default);
|
||||
vm.Address = depositAddressResult.Address;
|
||||
|
||||
var paymentMethodObj = PaymentMethodId.Parse(paymentMethod);
|
||||
if (paymentMethodObj.IsBTCOnChain)
|
||||
{
|
||||
var network = _networkProvider.GetNetwork<BTCPayNetwork>("BTC");
|
||||
var bip21 = network.GenerateBIP21(depositAddressResult.Address, null);
|
||||
vm.Link = bip21.ToString();
|
||||
var paymentMethodId = PaymentMethodId.TryParse(paymentMethod);
|
||||
if (paymentMethodId != null)
|
||||
{
|
||||
var walletId = new WalletId(storeId, paymentMethodId.CryptoCode);
|
||||
var returnUrl = _linkGenerator.GetUriByAction(
|
||||
nameof(ViewCustodianAccount),
|
||||
"UICustodianAccounts",
|
||||
new { storeId = custodianAccount.StoreId, accountId = custodianAccount.Id },
|
||||
Request.Scheme,
|
||||
Request.Host,
|
||||
Request.PathBase);
|
||||
|
||||
vm.CryptoImageUrl = GetImage(paymentMethodId, network);
|
||||
vm.CreateTransactionUrl = _linkGenerator.GetUriByAction(
|
||||
nameof(UIWalletsController.WalletSend),
|
||||
"UIWallets",
|
||||
new { walletId, defaultDestination = vm.Address, returnUrl },
|
||||
Request.Scheme,
|
||||
Request.Host,
|
||||
Request.PathBase);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// TODO support LN + shitcoins
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
vm.ErrorMessage = e.Message;
|
||||
return new ObjectResult(vm) { StatusCode = 500 };
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return BadRequest();
|
||||
}
|
||||
|
||||
return Ok(vm);
|
||||
}
|
||||
|
||||
private string GetImage(PaymentMethodId paymentMethodId, BTCPayNetwork network)
|
||||
{
|
||||
// TODO this method was copy-pasted from BTCPayServer.Controllers.UIWalletsController.GetImage(). Maybe refactor this?
|
||||
var res = paymentMethodId.PaymentType == PaymentTypes.BTCLike
|
||||
? Url.Content(network.CryptoImagePath)
|
||||
: Url.Content(network.LightningImagePath);
|
||||
return Request.GetRelativePathOrAbsolute(res);
|
||||
}
|
||||
|
||||
private StoreData GetCurrentStore() => HttpContext.GetStoreData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,10 +130,7 @@ namespace BTCPayServer.Hosting
|
||||
services.TryAddSingleton<PaymentRequestService>();
|
||||
services.TryAddSingleton<UserService>();
|
||||
services.AddSingleton<CustodianAccountRepository>();
|
||||
|
||||
|
||||
services.TryAddSingleton<WalletHistogramService>();
|
||||
services.TryAddSingleton<CustodianAccountRepository>();
|
||||
services.AddSingleton<ApplicationDbContextFactory>();
|
||||
services.AddOptions<BTCPayServerOptions>().Configure(
|
||||
(options) =>
|
||||
|
||||
@@ -15,7 +15,6 @@ public class AssetBalanceInfo
|
||||
public decimal FiatValue { get; set; }
|
||||
public Dictionary<string, AssetPairData> TradableAssetPairs { get; set; }
|
||||
public bool CanWithdraw { get; set; }
|
||||
public bool CanDeposit { get; set; }
|
||||
public string FormattedBid { get; set; }
|
||||
public string FormattedAsk { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels;
|
||||
|
||||
public class DepositPrepareViewModel
|
||||
{
|
||||
public string PaymentMethod { get; set; }
|
||||
public string Address { get; set; }
|
||||
public string Link { get; set; }
|
||||
public string CryptoImageUrl { get; set; }
|
||||
public string ErrorMessage { get; set; }
|
||||
public string CreateTransactionUrl { get; set; }
|
||||
}
|
||||
@@ -1,4 +1,3 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace BTCPayServer.Models.CustodianAccountViewModels
|
||||
@@ -8,8 +7,9 @@ namespace BTCPayServer.Models.CustodianAccountViewModels
|
||||
public Dictionary<string,AssetBalanceInfo> AssetBalances { get; set; }
|
||||
public string AssetBalanceExceptionMessage { get; set; }
|
||||
|
||||
public string StoreId { get; set; }
|
||||
public string StoreDefaultFiat { get; set; }
|
||||
public decimal DustThresholdInFiat { get; set; }
|
||||
public bool CanDeposit { get; set; }
|
||||
public string[] DepositablePaymentMethods { get; set; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Views.Apps
|
||||
@using BTCPayServer.Abstractions.Extensions
|
||||
@using BTCPayServer.Abstractions.Custodians
|
||||
@model BTCPayServer.Models.CustodianAccountViewModels.ViewCustodianAccountViewModel
|
||||
@@ -6,6 +6,11 @@
|
||||
ViewData.SetActivePage(AppsNavPages.Create, "Custodian account: " + @Model?.CustodianAccount.Name);
|
||||
}
|
||||
|
||||
@section PageHeadContent
|
||||
{
|
||||
<link href="~/main/qrcode.css" rel="stylesheet" asp-append-version="true"/>
|
||||
}
|
||||
|
||||
@section PageFootContent {
|
||||
<partial name="_ValidationScriptsPartial"/>
|
||||
}
|
||||
@@ -15,333 +20,398 @@
|
||||
</style>
|
||||
|
||||
<div id="custodianAccountView" v-cloak>
|
||||
<div class="sticky-header-setup"></div>
|
||||
<div class="sticky-header d-flex flex-wrap gap-3 align-items-center justify-content-between">
|
||||
<h2 class="mb-0">
|
||||
@ViewData["Title"]
|
||||
</h2>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a class="btn btn-primary" role="button" v-if="account && account.canDeposit" v-on:click="openDepositModal()" href="#">
|
||||
<span class="fa fa-download"></span> Deposit
|
||||
</a>
|
||||
<a asp-action="EditCustodianAccount" asp-route-storeId="@Model.CustodianAccount.StoreId" asp-route-accountId="@Model.CustodianAccount.Id" class="btn btn-primary" role="button" id="EditCustodianAccountConfig">
|
||||
<span class="fa fa-gear"></span> Configure
|
||||
</a>
|
||||
<!--
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
|
||||
<a class="btn btn-secondary" id="ViewApp" target="app_" href="/apps/MQ2sCVsmQ95JBZ4aZDtoSwMAnBY/pos">View</a>
|
||||
-->
|
||||
</div>
|
||||
<div class="sticky-header-setup"></div>
|
||||
<div class="sticky-header d-flex flex-wrap gap-3 align-items-center justify-content-between">
|
||||
<h2 class="mb-0">
|
||||
@ViewData["Title"]
|
||||
</h2>
|
||||
<div class="d-flex flex-wrap gap-3">
|
||||
<a class="btn btn-primary" role="button" v-if="account?.depositablePaymentMethods?.length > 0" v-on:click="openDepositModal()" href="#">
|
||||
<span class="fa fa-download"></span> Deposit
|
||||
</a>
|
||||
<a asp-action="EditCustodianAccount" asp-route-storeId="@Model.CustodianAccount.StoreId" asp-route-accountId="@Model.CustodianAccount.Id" class="btn btn-primary" role="button" id="EditCustodianAccountConfig">
|
||||
<span class="fa fa-gear"></span> Configure
|
||||
</a>
|
||||
<!--
|
||||
<button type="submit" class="btn btn-primary order-sm-1" id="SaveSettings">Save</button>
|
||||
<a class="btn btn-secondary" id="ViewApp" target="app_" href="/apps/MQ2sCVsmQ95JBZ4aZDtoSwMAnBY/pos">View</a>
|
||||
-->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<partial name="_StatusMessage"/>
|
||||
<partial name="_StatusMessage"/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div v-if="!account" class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-xl-12">
|
||||
<div v-if="!account" class="loading d-flex justify-content-center p-3">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="account">
|
||||
<p class="alert alert-danger" v-if="account.assetBalanceExceptionMessage">
|
||||
{{ account.assetBalanceExceptionMessage }}
|
||||
</p>
|
||||
|
||||
<h2>Balances</h2>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="hideDustAmounts" id="flexCheckDefault">
|
||||
<label class="form-check-label" for="flexCheckDefault">
|
||||
Hide holdings worth less than {{ account.dustThresholdInFiat }} {{ account.storeDefaultFiat }}.
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div v-if="account">
|
||||
<p class="alert alert-danger" v-if="account.assetBalanceExceptionMessage">
|
||||
{{ account.assetBalanceExceptionMessage }}
|
||||
</p>
|
||||
|
||||
<h2>Balances</h2>
|
||||
|
||||
<div class="form-check">
|
||||
<input class="form-check-input" type="checkbox" v-model="hideDustAmounts" id="flexCheckDefault">
|
||||
<label class="form-check-label" for="flexCheckDefault" >
|
||||
Hide holdings worth less than {{ account.dustThresholdInFiat }} {{ account.storeDefaultFiat }}.
|
||||
</label>
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th class="text-end">Balance</th>
|
||||
<th class="text-end">Unit Price (Bid)</th>
|
||||
<th class="text-end">Unit Price (Ask)</th>
|
||||
<th class="text-end">Fiat Value</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in sortedAssetRows" :key="row.asset">
|
||||
<td>{{ row.asset }}</td>
|
||||
<!-- TODO format as number? How? -->
|
||||
<th class="text-end">{{ row.formattedQty }}</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedBid }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedAsk }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedFiatValue }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
<a v-if="row.tradableAssetPairs" v-on:click="openTradeModal(row)" href="#">Trade</a>
|
||||
<a v-if="canDepositAsset(row.asset)" v-on:click="openDepositModal(row)" href="#">Deposit</a>
|
||||
<a v-if="row.canWithdraw" v-on:click="openWithdrawModal(row)" href="#">Withdraw</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="account.assetBalances.length === 0">
|
||||
<td colspan="999" class="text-center">No assets are stored with this custodian (yet).</td>
|
||||
</tr>
|
||||
<tr v-if="account.assetBalanceExceptionMessage !== null">
|
||||
<td colspan="999" class="text-center">An error occured while loading assets and balances.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Features</h2>
|
||||
<p>The @Model?.Custodian.Name custodian supports:</p>
|
||||
<ul>
|
||||
<li>Viewing asset account</li>
|
||||
@if (Model?.Custodian is ICanTrade)
|
||||
{
|
||||
<li>Trading</li>
|
||||
}
|
||||
@if (Model?.Custodian is ICanDeposit)
|
||||
{
|
||||
<li>Depositing</li>
|
||||
}
|
||||
@if (Model?.Custodian is ICanWithdraw)
|
||||
{
|
||||
<li>Withdrawing (Greenfield API only, for now)</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="withdrawModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Withdraw</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Withdrawals are coming soon, but if you need this today, you can use our <a rel="noopener noreferrer" href="https://docs.btcpayserver.org/API/Greenfield/v1/" target="_blank">Greenfield API "Withdraw to store wallet" endpoint</a> to execute a withdrawal.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="depositModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Deposit</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p v-if="deposit.errorMsg" class="alert alert-danger">{{ deposit.errorMsg }}</p>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="DepositAsset">Asset to Deposit</label>
|
||||
<select class="form-select" v-model="deposit.asset" name="DepositAsset">
|
||||
<option v-for="option in availableAssetsToDeposit" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label" for="DepositPaymentNetwork">Payment Method</label>
|
||||
<select class="form-select" v-model="deposit.paymentMethod" name="DepositPaymentNetwork">
|
||||
<option v-for="option in availablePaymentMethodsToDeposit" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="loading d-flex justify-content-center p-3" v-if="deposit.isLoading">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-responsive">
|
||||
<table class="table table-hover table-responsive-md">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Asset</th>
|
||||
<th class="text-end">Balance</th>
|
||||
<th class="text-end">Unit Price (Bid)</th>
|
||||
<th class="text-end">Unit Price (Ask)</th>
|
||||
<th class="text-end">Fiat Value</th>
|
||||
<th class="text-end">Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="row in sortedAssetRows" :key="row.asset">
|
||||
<td>{{ row.asset }}</td>
|
||||
<!-- TODO format as number? How? -->
|
||||
<th class="text-end">{{ row.formattedQty }}</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedBid }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedAsk }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
{{ row.formattedFiatValue }}
|
||||
</th>
|
||||
<th class="text-end">
|
||||
<a v-if="row.tradableAssetPairs" v-on:click="openTradeModal(row)" href="#">Trade</a>
|
||||
<a v-if="row.canDeposit" v-on:click="openDepositModal(row)" href="#">Deposit</a>
|
||||
<a v-if="row.canWithdraw" v-on:click="openWithdrawModal(row)" href="#">Withdraw</a>
|
||||
</th>
|
||||
</tr>
|
||||
<tr v-if="account.assetBalances.length === 0">
|
||||
<td colspan="999" class="text-center">No assets are stored with this custodian (yet).</td>
|
||||
</tr>
|
||||
<tr v-if="account.assetBalanceExceptionMessage !== null">
|
||||
<td colspan="999" class="text-center">An error occured while loading assets and balances.</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
|
||||
<h2>Features</h2>
|
||||
<p>The @Model?.Custodian.Name custodian supports:</p>
|
||||
<ul>
|
||||
<li>Viewing asset account</li>
|
||||
@if (Model?.Custodian is ICanTrade)
|
||||
{
|
||||
<li>Trading</li>
|
||||
}
|
||||
@if (Model?.Custodian is ICanDeposit)
|
||||
{
|
||||
<li>Depositing (Greenfield API only, for now)</li>
|
||||
}
|
||||
@if (Model?.Custodian is ICanWithdraw)
|
||||
{
|
||||
<li>Withdrawing (Greenfield API only, for now)</li>
|
||||
}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="withdrawModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Withdraw</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Withdrawals are coming soon, but if you need this today, you can use our <a rel="noopener noreferrer" href="https://docs.btcpayserver.org/API/Greenfield/v1/" target="_blank">Greenfield API "Withdraw to store wallet" endpoint</a> to execute a withdrawal.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="depositModal">
|
||||
<div class="modal-dialog" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Deposit</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>Deposits are coming soon, but if you need this today, you can use our <a rel="noopener noreferrer" href="https://docs.btcpayserver.org/API/Greenfield/v1/" target="_blank">Greenfield API "Get a deposit address for custodian" endpoint</a> to get a deposit address to send your assets to.</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal" tabindex="-1" role="dialog" id="tradeModal" :data-bs-keyboard="!trade.isExecuting">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" v-on:submit="onTradeSubmit" method="post" asp-action="Trade" asp-route-accountId="@Model?.CustodianAccount?.Id" asp-route-storeId="@Model?.CustodianAccount?.StoreId">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Trade {{ trade.qty }} {{ trade.assetToTrade }} into {{ trade.assetToTradeInto }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" v-if="!trade.isExecuting">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="loading d-flex justify-content-center p-3" v-if="trade.isExecuting">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
<div v-if="!deposit.isLoading && (deposit.link || deposit.address)">
|
||||
<div class="tab-content text-center">
|
||||
<div v-if="deposit.link" class="tab-pane" id="link-tab" role="tabpanel">
|
||||
<div class="qr-container mb-3">
|
||||
<img :src="deposit.cryptoImageUrl" class="qr-icon" :alt="deposit.asset"/>
|
||||
<qrcode v-bind:value="deposit.link" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"></qrcode>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group" :data-clipboard="deposit.link">
|
||||
<input type="text" class="form-control" style="cursor:copy" readonly="readonly" :value="deposit.link" id="payment-link"/>
|
||||
<button type="button" class="btn btn-outline-secondary p-2" style="width:7em;" data-clipboard-confirm>
|
||||
<vc:icon symbol="copy"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="deposit.address" class="tab-pane show active" id="address-tab" role="tabpanel">
|
||||
<div class="qr-container mb-3">
|
||||
<img v-bind:src="deposit.cryptoImageUrl" class="qr-icon" :alt="deposit.asset"/>
|
||||
<qrcode v-bind:value="deposit.address" :options="{ width: 256, margin: 1, color: {dark:'#000', light:'#f5f5f7'} }" tag="svg"></qrcode>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="input-group" :data-clipboard="deposit.address">
|
||||
<input type="text" class="form-control" style="cursor:copy" readonly="readonly" :value="deposit.address" id="address"/>
|
||||
<button type="button" class="input-group-text btn btn-outline-secondary p-2" style="width:7em;" data-clipboard-confirm>
|
||||
<vc:icon symbol="copy"/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="nav justify-content-center">
|
||||
<a v-if="deposit.address" :class="{active: deposit.tab === 'address' }" class="btcpay-pill" data-bs-toggle="tab" href="#address-tab">Address</a>
|
||||
<a v-if="deposit.link" :class="{active: deposit.tab === 'link' }" class="btcpay-pill" data-bs-toggle="tab" href="#link-tab">Link</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!trade.isExecuting && trade.results === null">
|
||||
<p v-if="trade.errorMsg" class="alert alert-danger">{{ trade.errorMsg }}</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
|
||||
<a v-if="deposit.createTransactionUrl" class="btn btn-primary" :href="deposit.createTransactionUrl">
|
||||
Create Transaction
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row mb-2 trade-qty">
|
||||
<div class="col-side">
|
||||
<label class="form-label">
|
||||
Convert
|
||||
<div class="input-group has-validation">
|
||||
<div class="modal" tabindex="-1" role="dialog" id="tradeModal" :data-bs-keyboard="!trade.isExecuting">
|
||||
<div class="modal-dialog" role="document">
|
||||
<form class="modal-content" v-on:submit="onTradeSubmit" method="post" asp-action="Trade" asp-route-accountId="@Model?.CustodianAccount?.Id" asp-route-storeId="@Model?.CustodianAccount?.StoreId">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">Trade {{ trade.qty }} {{ trade.assetToTrade }} into {{ trade.assetToTradeInto }}</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close" v-if="!trade.isExecuting">
|
||||
<vc:icon symbol="close"/>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="loading d-flex justify-content-center p-3" v-if="trade.isExecuting">
|
||||
<div class="spinner-border text-light" role="status">
|
||||
<span class="visually-hidden">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!trade.isExecuting && trade.results === null">
|
||||
<p v-if="trade.errorMsg" class="alert alert-danger">{{ trade.errorMsg }}</p>
|
||||
|
||||
<div class="row mb-2 trade-qty">
|
||||
<div class="col-side">
|
||||
<label class="form-label">
|
||||
Convert
|
||||
<div class="input-group has-validation">
|
||||
<!--
|
||||
getMinQtyToTrade() = {{ getMinQtyToTrade(this.trade.assetToTradeInto, this.trade.assetToTrade) }}
|
||||
<br/>
|
||||
Max Qty to Trade = {{ trade.maxQtyToTrade }}
|
||||
-->
|
||||
<input name="Qty" type="number" min="0" step="any" :max="trade.maxQtyToTrade" :min="getMinQtyToTrade()" class="form-control qty" v-bind:class="{ 'is-invalid': trade.qty < getMinQtyToTrade() || trade.qty > trade.maxQtyToTrade }" v-model="trade.qty"/>
|
||||
<select name="FromAsset" v-model="trade.assetToTrade" class="form-control">
|
||||
<option v-for="option in availableAssetsToTrade" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-center text-center">
|
||||
|
||||
<br/>
|
||||
<button v-if="canSwapTradeAssets()" type="button" class="btn btn-secondary btn-square" v-on:click="swapTradeAssets()" aria-label="Swap assets">
|
||||
<i class="fa fa-arrows-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-side">
|
||||
<label class="form-label">
|
||||
Into
|
||||
<div class="input-group">
|
||||
<input disabled="disabled" type="number" class="form-control qty" v-model="tradeQtyToReceive"/>
|
||||
<select name="ToAsset" v-model="trade.assetToTradeInto" class="form-control">
|
||||
<option v-for="option in availableAssetsToTradeInto" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<input name="Qty" type="number" min="0" step="any" :max="trade.maxQtyToTrade" :min="getMinQtyToTrade()" class="form-control qty" v-bind:class="{ 'is-invalid': trade.qty < getMinQtyToTrade() || trade.qty > trade.maxQtyToTrade }" v-model="trade.qty"/>
|
||||
<select name="FromAsset" v-model="trade.assetToTrade" class="form-select">
|
||||
<option v-for="option in availableAssetsToTrade" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Set qty to a percentage of holdings">
|
||||
<button v-on:click="setTradeQtyPercent(10)" class="btn btn-secondary" type="button">10%</button>
|
||||
<button v-on:click="setTradeQtyPercent(25)" class="btn btn-secondary" type="button">25%</button>
|
||||
<button v-on:click="setTradeQtyPercent(50)" class="btn btn-secondary" type="button">50%</button>
|
||||
<button v-on:click="setTradeQtyPercent(75)" class="btn btn-secondary" type="button">75%</button>
|
||||
<button v-on:click="setTradeQtyPercent(90)" class="btn btn-secondary" type="button">90%</button>
|
||||
<button v-on:click="setTradeQtyPercent(100)" class="btn btn-secondary" type="button">100%</button>
|
||||
</div>
|
||||
<div class="col-center text-center">
|
||||
|
||||
<br/>
|
||||
<button v-if="canSwapTradeAssets()" type="button" class="btn btn-secondary btn-square" v-on:click="swapTradeAssets()" aria-label="Swap assets">
|
||||
<i class="fa fa-arrows-h" aria-hidden="true"></i>
|
||||
</button>
|
||||
</div>
|
||||
<div class="col-side">
|
||||
<label class="form-label">
|
||||
Into
|
||||
<div class="input-group">
|
||||
<input disabled="disabled" type="number" class="form-control qty" v-model="tradeQtyToReceive"/>
|
||||
<select name="ToAsset" v-model="trade.assetToTradeInto" class="form-select">
|
||||
<option v-for="option in availableAssetsToTradeInto" v-bind:value="option">
|
||||
{{ option }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-group" role="group" aria-label="Set qty to a percentage of holdings">
|
||||
<button v-on:click="setTradeQtyPercent(10)" class="btn btn-secondary" type="button">10%</button>
|
||||
<button v-on:click="setTradeQtyPercent(25)" class="btn btn-secondary" type="button">25%</button>
|
||||
<button v-on:click="setTradeQtyPercent(50)" class="btn btn-secondary" type="button">50%</button>
|
||||
<button v-on:click="setTradeQtyPercent(75)" class="btn btn-secondary" type="button">75%</button>
|
||||
<button v-on:click="setTradeQtyPercent(90)" class="btn btn-secondary" type="button">90%</button>
|
||||
<button v-on:click="setTradeQtyPercent(100)" class="btn btn-secondary" type="button">100%</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<p v-if="trade.price">
|
||||
<br/>
|
||||
1 {{ trade.assetToTradeInto }} = {{ trade.price }} {{ trade.assetToTrade }}
|
||||
<br/>
|
||||
1 {{ trade.assetToTrade }} = {{ 1 / trade.price }} {{ trade.assetToTradeInto }}
|
||||
</p>
|
||||
<p v-if="canExecuteTrade">
|
||||
After the trade
|
||||
{{ trade.maxQtyToTrade - trade.qty }} {{ trade.assetToTrade }} will remain in your account.
|
||||
</p>
|
||||
|
||||
<!--
|
||||
<p>
|
||||
trade.priceForPair = {{ trade.priceForPair }}
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Min Qty to Trade</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>EUR</td>
|
||||
<td>BTC</td>
|
||||
<td>{{getMinQtyToTrade('EUR', 'BTC')}}</td>
|
||||
<td>{{trade.priceForPair['EUR/BTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BTC</td>
|
||||
<td>EUR</td>
|
||||
<td>{{getMinQtyToTrade('BTC', 'EUR')}}</td>
|
||||
<td>{{trade.priceForPair['BTC/EUR']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EUR</td>
|
||||
<td>LTC</td>
|
||||
<td>{{getMinQtyToTrade('EUR', 'LTC')}}</td>
|
||||
<td>{{trade.priceForPair['EUR/LTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LTC</td>
|
||||
<td>EUR</td>
|
||||
<td>{{getMinQtyToTrade('LTC', 'EUR')}}</td>
|
||||
<td>{{trade.priceForPair['LTC/EUR']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BTC</td>
|
||||
<td>LTC</td>
|
||||
<td>{{getMinQtyToTrade('BTC', 'LTC')}}</td>
|
||||
<td>{{trade.priceForPair['BTC/LTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LTC</td>
|
||||
<td>BTC</td>
|
||||
<td>{{getMinQtyToTrade('LTC', 'BTC')}}</td>
|
||||
<td>{{trade.priceForPair['LTC/BTC']}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
-->
|
||||
<small class="form-text text-muted">Final results may vary due to trading fees and slippage.</small>
|
||||
</div>
|
||||
<div v-if="trade.results !== null">
|
||||
<p class="alert alert-success">Successfully traded {{ trade.results.fromAsset}} into {{ trade.results.toAsset}}.</p>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Asset</th>
|
||||
<th>Comment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="entry in trade.results.ledgerEntries">
|
||||
<td class="text-end" v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }"><span v-if="entry.qty > 0">+</span>{{ entry.qty }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">{{ entry.asset }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">
|
||||
<span v-if="entry.type !== 'Trade'">{{ entry.type}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Trade ID: {{ trade.results.tradeId }}</p>
|
||||
</div>
|
||||
<p v-if="trade.price">
|
||||
<br/>
|
||||
1 {{ trade.assetToTradeInto }} = {{ trade.price }} {{ trade.assetToTrade }}
|
||||
<br/>
|
||||
1 {{ trade.assetToTrade }} = {{ 1 / trade.price }} {{ trade.assetToTradeInto }}
|
||||
</p>
|
||||
<p v-if="canExecuteTrade">
|
||||
After the trade
|
||||
{{ trade.maxQtyToTrade - trade.qty }} {{ trade.assetToTrade }} will remain in your account.
|
||||
</p>
|
||||
|
||||
<!--
|
||||
<p>
|
||||
trade.priceForPair = {{ trade.priceForPair }}
|
||||
</p>
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>From</th>
|
||||
<th>To</th>
|
||||
<th>Min Qty to Trade</th>
|
||||
<th>Price</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>EUR</td>
|
||||
<td>BTC</td>
|
||||
<td>{{getMinQtyToTrade('EUR', 'BTC')}}</td>
|
||||
<td>{{trade.priceForPair['EUR/BTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BTC</td>
|
||||
<td>EUR</td>
|
||||
<td>{{getMinQtyToTrade('BTC', 'EUR')}}</td>
|
||||
<td>{{trade.priceForPair['BTC/EUR']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EUR</td>
|
||||
<td>LTC</td>
|
||||
<td>{{getMinQtyToTrade('EUR', 'LTC')}}</td>
|
||||
<td>{{trade.priceForPair['EUR/LTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LTC</td>
|
||||
<td>EUR</td>
|
||||
<td>{{getMinQtyToTrade('LTC', 'EUR')}}</td>
|
||||
<td>{{trade.priceForPair['LTC/EUR']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>BTC</td>
|
||||
<td>LTC</td>
|
||||
<td>{{getMinQtyToTrade('BTC', 'LTC')}}</td>
|
||||
<td>{{trade.priceForPair['BTC/LTC']}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>LTC</td>
|
||||
<td>BTC</td>
|
||||
<td>{{getMinQtyToTrade('LTC', 'BTC')}}</td>
|
||||
<td>{{trade.priceForPair['LTC/BTC']}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
-->
|
||||
<small class="form-text text-muted">Final results may vary due to trading fees and slippage.</small>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="!trade.isExecuting">
|
||||
|
||||
<div class="modal-footer-left">
|
||||
<span v-if="trade.isUpdating">
|
||||
Updating quote...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<span v-if="trade.results">Close</span>
|
||||
<span v-if="!trade.results">Cancel</span>
|
||||
</button>
|
||||
<button v-if="canExecuteTrade" type="submit" class="btn btn-primary">Execute</button>
|
||||
<div v-if="trade.results !== null">
|
||||
<p class="alert alert-success">Successfully traded {{ trade.results.fromAsset}} into {{ trade.results.toAsset}}.</p>
|
||||
<table class="table table-striped">
|
||||
<thead>
|
||||
<tr>
|
||||
<th colspan="2">Asset</th>
|
||||
<th>Comment</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr v-for="entry in trade.results.ledgerEntries">
|
||||
<td class="text-end" v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }"><span v-if="entry.qty > 0">+</span>{{ entry.qty }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">{{ entry.asset }}</td>
|
||||
<td v-bind:class="{ 'text-success': entry.qty > 0, 'text-danger': entry.qty < 0 }">
|
||||
<span v-if="entry.type !== 'Trade'">{{ entry.type}}</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<p>Trade ID: {{ trade.results.tradeId }}</p>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" v-if="!trade.isExecuting">
|
||||
|
||||
<div class="modal-footer-left">
|
||||
<span v-if="trade.isUpdating">
|
||||
Updating quote...
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
<span v-if="trade.results">Close</span>
|
||||
<span v-if="!trade.results">Cancel</span>
|
||||
</button>
|
||||
<button v-if="canExecuteTrade" type="submit" class="btn btn-primary">Execute</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="~/vendor/vuejs/vue.min.js" asp-append-version="true"></script>
|
||||
<script src="~/vendor/vue-qrcode/vue-qrcode.min.js" asp-append-version="true"></script>
|
||||
<script type="text/javascript">
|
||||
var ajaxBalanceUrl = "@Url.Action("ViewCustodianAccountAjax", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxTradePrepareUrl = "@Url.Action("GetTradePrepareAjax", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxBalanceUrl = "@Url.Action("ViewCustodianAccountJson", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxTradePrepareUrl = "@Url.Action("GetTradePrepareJson", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
var ajaxDepositUrl = "@Url.Action("GetDepositPrepareJson", "UICustodianAccounts", new { storeId = Model?.CustodianAccount.StoreId, accountId = Model?.CustodianAccount.Id })";
|
||||
</script>
|
||||
<script src="~/js/custodian-account.js" asp-append-version="true"></script>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
new Vue({
|
||||
el: '#custodianAccountView',
|
||||
components: {
|
||||
qrcode: VueQrcode
|
||||
},
|
||||
data: {
|
||||
account: null,
|
||||
hideDustAmounts: true,
|
||||
@@ -8,6 +11,16 @@ new Vue({
|
||||
withdraw: null,
|
||||
deposit: null
|
||||
},
|
||||
deposit: {
|
||||
asset: null,
|
||||
paymentMethod: null,
|
||||
address: null,
|
||||
link: null,
|
||||
errorMsg: null,
|
||||
cryptoImageUrl: null,
|
||||
tab: null,
|
||||
isLoading: false
|
||||
},
|
||||
trade: {
|
||||
row: null,
|
||||
results: null,
|
||||
@@ -62,6 +75,33 @@ new Vue({
|
||||
}
|
||||
return r.sort();
|
||||
},
|
||||
availableAssetsToDeposit: function () {
|
||||
let paymentMethods = this?.account?.depositablePaymentMethods;
|
||||
let r = [];
|
||||
if (paymentMethods && paymentMethods.length > 0) {
|
||||
for (let i = 0; i < paymentMethods.length; i++) {
|
||||
let asset = paymentMethods[i].split("-")[0];
|
||||
if (r.indexOf(asset) === -1) {
|
||||
r.push(asset);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.sort();
|
||||
},
|
||||
availablePaymentMethodsToDeposit: function () {
|
||||
let paymentMethods = this?.account?.depositablePaymentMethods;
|
||||
let r = [];
|
||||
if (Array.isArray(paymentMethods)) {
|
||||
for (let i = 0; i < paymentMethods.length; i++) {
|
||||
let pm = paymentMethods[i];
|
||||
let asset = pm.split("-")[0];
|
||||
if (asset === this.deposit.asset) {
|
||||
r.push(pm);
|
||||
}
|
||||
}
|
||||
}
|
||||
return r.sort();
|
||||
},
|
||||
sortedAssetRows: function () {
|
||||
if (this.account?.assetBalances) {
|
||||
let rows = Object.values(this.account.assetBalances);
|
||||
@@ -100,19 +140,19 @@ new Vue({
|
||||
let pair = row.tradableAssetPairs?.[pairCode];
|
||||
let pairReverse = row.tradableAssetPairs?.[pairCodeReverse];
|
||||
|
||||
if(pair !== null || pairReverse !== null){
|
||||
if (pair !== null || pairReverse !== null) {
|
||||
if (pair && !pairReverse) {
|
||||
return pair.minimumTradeQty;
|
||||
} else if (!pair && pairReverse) {
|
||||
// TODO price here could not be what we expect it to be...
|
||||
let price = this.trade.priceForPair?.[pairCode];
|
||||
if(!price){
|
||||
if (!price) {
|
||||
return null;
|
||||
}
|
||||
// if (reverse) {
|
||||
// return price / pairReverse.minimumTradeQty;
|
||||
// }else {
|
||||
return price * pairReverse.minimumTradeQty;
|
||||
return price * pairReverse.minimumTradeQty;
|
||||
// }
|
||||
}
|
||||
}
|
||||
@@ -135,8 +175,6 @@ new Vue({
|
||||
this.trade.assetToTradeInto = this.account.storeDefaultFiat;
|
||||
}
|
||||
|
||||
// TODO watch "this.trade.assetToTrade" for changes and if so, set "qty" to max + fill "maxQtyToTrade" and "price"
|
||||
|
||||
this.trade.qty = row.qty;
|
||||
this.trade.maxQtyToTrade = row.qty;
|
||||
this.trade.price = row.bid;
|
||||
@@ -164,6 +202,12 @@ new Vue({
|
||||
if (this.modals.deposit === null) {
|
||||
this.modals.deposit = new window.bootstrap.Modal('#depositModal');
|
||||
}
|
||||
if (row) {
|
||||
this.deposit.asset = row.asset;
|
||||
}else if(!this.deposit.asset && this.availableAssetsToDeposit.length > 0){
|
||||
this.deposit.asset = this.availableAssetsToDeposit[0];
|
||||
}
|
||||
|
||||
this.modals.deposit.show();
|
||||
},
|
||||
onTradeSubmit: async function (e) {
|
||||
@@ -180,7 +224,7 @@ new Vue({
|
||||
this.modals.trade._config.keyboard = false;
|
||||
|
||||
const _this = this;
|
||||
const token = document.querySelector("input[name='__RequestVerificationToken']").value;
|
||||
const token = this.getRequestVerificationToken();
|
||||
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
@@ -244,7 +288,7 @@ new Vue({
|
||||
}
|
||||
|
||||
if (this.trade.isUpdating) {
|
||||
console.log("Previous request is still running. No need to hammer the server.");
|
||||
// Previous request is still running. No need to hammer the server
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -290,23 +334,35 @@ new Vue({
|
||||
if (data.maxQtyToTrade < _this.trade.qty) {
|
||||
_this.trade.qty = _this.trade.maxQtyToTrade;
|
||||
}
|
||||
let pair = data.fromAsset+"/"+data.toAsset;
|
||||
let pairReverse = data.toAsset+"/"+data.fromAsset;
|
||||
|
||||
let pair = data.fromAsset + "/" + data.toAsset;
|
||||
let pairReverse = data.toAsset + "/" + data.fromAsset;
|
||||
|
||||
// TODO Should we use "bid" in some cases? The spread can be huge with some shitcoins.
|
||||
_this.trade.price = data.ask;
|
||||
_this.trade.priceForPair[pair] = data.ask;
|
||||
_this.trade.priceForPair[pairReverse] = 1 / data.ask;
|
||||
|
||||
|
||||
}).catch(function (e) {
|
||||
_this.trade.isUpdating = false;
|
||||
if (e instanceof DOMException && e.code === DOMException.ABORT_ERR) {
|
||||
console.log("User aborted fetch request");
|
||||
// User aborted fetch request
|
||||
} else {
|
||||
throw e;
|
||||
}
|
||||
});
|
||||
},
|
||||
canDepositAsset: function (asset) {
|
||||
let paymentMethods = this?.account?.depositablePaymentMethods;
|
||||
if (paymentMethods && paymentMethods.length > 0) {
|
||||
for (let i = 0; i < paymentMethods.length; i++) {
|
||||
let pmParts = paymentMethods[i].split("-");
|
||||
if (asset === pmParts[0]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
},
|
||||
canSwapTradeAssets: function () {
|
||||
let minQtyToTrade = this.getMinQtyToTrade(this.trade.assetToTradeInto, this.trade.assetToTrade);
|
||||
let assetToTradeIntoHoldings = this.account?.assetBalances?.[this.trade.assetToTradeInto];
|
||||
@@ -320,15 +376,15 @@ new Vue({
|
||||
this.trade.assetToTrade = this.trade.assetToTradeInto;
|
||||
this.trade.assetToTradeInto = tmp;
|
||||
this.trade.price = 1 / this.trade.price;
|
||||
|
||||
|
||||
this._refreshTradeDataAfterAssetChange();
|
||||
},
|
||||
_refreshTradeDataAfterAssetChange: function(){
|
||||
_refreshTradeDataAfterAssetChange: function () {
|
||||
let maxQtyToTrade = this.getMaxQtyToTrade(this.trade.assetToTrade);
|
||||
this.trade.qty = maxQtyToTrade
|
||||
this.trade.maxQtyToTrade = maxQtyToTrade;
|
||||
|
||||
this.killAjaxIfRunning(this.trade.updateTradePriceAbortController);
|
||||
this.trade.updateTradePriceAbortController.abort();
|
||||
|
||||
// Update the price asap, so we can continue
|
||||
let _this = this;
|
||||
@@ -336,9 +392,6 @@ new Vue({
|
||||
_this.updateTradePrice();
|
||||
}, 100);
|
||||
},
|
||||
killAjaxIfRunning: function (abortController) {
|
||||
abortController.abort();
|
||||
},
|
||||
refreshAccountBalances: function () {
|
||||
let _this = this;
|
||||
fetch(window.ajaxBalanceUrl).then(function (response) {
|
||||
@@ -346,21 +399,63 @@ new Vue({
|
||||
}).then(function (result) {
|
||||
_this.account = result;
|
||||
});
|
||||
},
|
||||
getRequestVerificationToken: function () {
|
||||
return document.querySelector("input[name='__RequestVerificationToken']").value;
|
||||
}
|
||||
},
|
||||
watch: {
|
||||
'trade.assetToTrade': function(newValue, oldValue){
|
||||
if(newValue === this.trade.assetToTradeInto){
|
||||
'trade.assetToTrade': function (newValue, oldValue) {
|
||||
if (newValue === this.trade.assetToTradeInto) {
|
||||
// This is the same as swapping the 2 assets
|
||||
this.trade.assetToTradeInto = oldValue;
|
||||
this.trade.price = 1 / this.trade.price;
|
||||
|
||||
|
||||
this._refreshTradeDataAfterAssetChange();
|
||||
}
|
||||
if(newValue !== oldValue){
|
||||
if (newValue !== oldValue) {
|
||||
// The qty is going to be wrong, so set to 100%
|
||||
this.trade.qty = this.getMaxQtyToTrade(this.trade.assetToTrade);
|
||||
}
|
||||
},
|
||||
'deposit.asset': function (newValue, oldValue) {
|
||||
if (this.availablePaymentMethodsToDeposit.length > 0) {
|
||||
this.deposit.paymentMethod = this.availablePaymentMethodsToDeposit[0];
|
||||
} else {
|
||||
this.deposit.paymentMethod = null;
|
||||
}
|
||||
},
|
||||
'deposit.paymentMethod': function (newValue, oldValue) {
|
||||
let _this = this;
|
||||
const token = this.getRequestVerificationToken();
|
||||
this.deposit.isLoading = true;
|
||||
fetch(window.ajaxDepositUrl + "?paymentMethod=" + encodeURI(this.deposit.paymentMethod), {
|
||||
method: "GET",
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'RequestVerificationToken': token
|
||||
}
|
||||
}).then(function (response) {
|
||||
_this.deposit.isLoading = false;
|
||||
return response.json();
|
||||
}).then(function (data) {
|
||||
_this.deposit.address = data.address;
|
||||
_this.deposit.link = data.link;
|
||||
_this.deposit.createTransactionUrl = data.createTransactionUrl;
|
||||
_this.deposit.cryptoImageUrl = data.cryptoImageUrl;
|
||||
|
||||
if(!_this.deposit.tab){
|
||||
_this.deposit.tab = 'address';
|
||||
}
|
||||
if(_this.deposit.tab === 'address' && !_this.deposit.address && _this.deposit.link){
|
||||
// Tab "address" is not available, but tab "link" is.
|
||||
_this.deposit.tab = 'link';
|
||||
}
|
||||
|
||||
_this.deposit.errorMsg = data.errorMessage;
|
||||
});
|
||||
|
||||
|
||||
}
|
||||
},
|
||||
created: function () {
|
||||
|
||||
@@ -529,7 +529,7 @@ svg.icon-note {
|
||||
}
|
||||
}
|
||||
|
||||
#tradeModal .qty{ width: 53%; }
|
||||
#tradeModal .qty{ width: 41%; }
|
||||
#tradeModal .btn-square{ padding: 0; width: 2.5rem; height: 2.5rem; }
|
||||
|
||||
#tradeModal .trade-qty {
|
||||
|
||||
Reference in New Issue
Block a user