btcpayserver/BTCPayServer/wwwroot/js/custodian-account.js
Wouter Samaey de92677b69
Custodian Account Deposit UI (#4024)
Co-authored-by: d11n <mail@dennisreimann.de>
2022-08-09 20:03:55 +02:00

467 lines
17 KiB
JavaScript

new Vue({
el: '#custodianAccountView',
components: {
qrcode: VueQrcode
},
data: {
account: null,
hideDustAmounts: true,
modals: {
trade: null,
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,
errorMsg: null,
isExecuting: false,
isUpdating: false,
updateTradePriceAbortController: new AbortController(),
priceRefresherInterval: null,
assetToTrade: null,
assetToTradeInto: null,
qty: null,
maxQtyToTrade: null,
price: null,
priceForPair: {}
}
},
computed: {
tradeQtyToReceive: function () {
return this.trade.qty / this.trade.price;
},
canExecuteTrade: function () {
return this.trade.qty >= this.getMinQtyToTrade() && this.trade.price !== null && this.trade.assetToTrade !== null && this.trade.assetToTradeInto !== null && !this.trade.isExecuting && this.trade.results === null;
},
availableAssetsToTrade: function () {
let r = [];
let balances = this?.account?.assetBalances;
if (balances) {
let t = this;
let rows = Object.values(balances);
rows = rows.filter(function (row) {
return row.fiatValue > t.account.dustThresholdInFiat;
});
for (let i in rows) {
r.push(rows[i].asset);
}
}
return r.sort();
},
availableAssetsToTradeInto: function () {
let r = [];
let pairs = this.account?.assetBalances?.[this.trade.assetToTrade]?.tradableAssetPairs;
if (pairs) {
for (let i in pairs) {
let pair = pairs[i];
if (pair.assetBought === this.trade.assetToTrade) {
r.push(pair.assetSold);
} else if (pair.assetSold === this.trade.assetToTrade) {
r.push(pair.assetBought);
}
}
}
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);
let t = this;
if (this.hideDustAmounts) {
rows = rows.filter(function (row) {
return row.fiatValue > t.account.dustThresholdInFiat;
});
}
rows = rows.sort(function (a, b) {
return b.fiatValue - a.fiatValue;
});
return rows;
}
}
},
methods: {
getMaxQtyToTrade: function (assetToTrade) {
let row = this.account?.assetBalances?.[assetToTrade];
if (row) {
return row.qty;
}
return null;
},
getMinQtyToTrade: function (assetToTrade = this.trade.assetToTrade, assetToTradeInto = this.trade.assetToTradeInto) {
if (assetToTrade && assetToTradeInto && this.account?.assetBalances) {
for (let asset in this.account.assetBalances) {
let row = this.account.assetBalances[asset];
let pairCode = assetToTrade + "/" + assetToTradeInto;
let pairCodeReverse = assetToTradeInto + "/" + assetToTrade;
let pair = row.tradableAssetPairs?.[pairCode];
let pairReverse = row.tradableAssetPairs?.[pairCodeReverse];
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) {
return null;
}
// if (reverse) {
// return price / pairReverse.minimumTradeQty;
// }else {
return price * pairReverse.minimumTradeQty;
// }
}
}
}
}
return 0;
},
setTradeQtyPercent: function (percent) {
this.trade.qty = percent / 100 * this.trade.maxQtyToTrade;
},
openTradeModal: function (row) {
let _this = this;
this.trade.row = row;
this.trade.results = null;
this.trade.errorMsg = null;
this.trade.assetToTrade = row.asset;
if (row.asset === this.account.storeDefaultFiat) {
this.trade.assetToTradeInto = "BTC";
} else {
this.trade.assetToTradeInto = this.account.storeDefaultFiat;
}
this.trade.qty = row.qty;
this.trade.maxQtyToTrade = row.qty;
this.trade.price = row.bid;
if (this.modals.trade === null) {
this.modals.trade = new window.bootstrap.Modal('#tradeModal');
// Disable price refreshing when modal closes...
const tradeModelElement = document.getElementById('tradeModal')
tradeModelElement.addEventListener('hide.bs.modal', event => {
_this.setTradePriceRefresher(false);
});
}
this.setTradePriceRefresher(true);
this.modals.trade.show();
},
openWithdrawModal: function (row) {
if (this.modals.withdraw === null) {
this.modals.withdraw = new window.bootstrap.Modal('#withdrawModal');
}
this.modals.withdraw.show();
},
openDepositModal: function (row) {
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) {
e.preventDefault();
const form = e.currentTarget;
const url = form.getAttribute('action');
const method = form.getAttribute('method');
this.trade.isExecuting = true;
// Prevent the modal from closing by clicking outside or via the keyboard
this.modals.trade._config.backdrop = 'static';
this.modals.trade._config.keyboard = false;
const _this = this;
const token = this.getRequestVerificationToken();
const response = await fetch(url, {
method,
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
},
body: JSON.stringify({
fromAsset: _this.trade.assetToTrade,
toAsset: _this.trade.assetToTradeInto,
qty: _this.trade.qty
})
});
let data = null;
try {
data = await response.json();
} catch (e) {
}
if (response.ok) {
_this.trade.results = data;
_this.trade.errorMsg = null;
_this.setTradePriceRefresher(false);
_this.refreshAccountBalances();
} else {
_this.trade.errorMsg = data && data.message || "Error";
}
_this.modals.trade._config.backdrop = true;
_this.modals.trade._config.keyboard = true;
_this.trade.isExecuting = false;
},
setTradePriceRefresher: function (enabled) {
if (enabled) {
// Update immediately...
this.updateTradePrice();
// And keep updating every few seconds...
let _this = this;
this.trade.priceRefresherInterval = setInterval(function () {
_this.updateTradePrice();
}, 5000);
} else {
clearInterval(this.trade.priceRefresherInterval);
}
},
updateTradePrice: function () {
if (!this.trade.assetToTrade || !this.trade.assetToTradeInto) {
// We need to know the 2 assets or we cannot do anything...
return;
}
if (this.trade.assetToTrade === this.trade.assetToTradeInto) {
// The 2 assets must be different
this.trade.price = null;
return;
}
if (this.trade.isUpdating) {
// Previous request is still running. No need to hammer the server
return;
}
this.trade.isUpdating = true;
let _this = this;
var searchParams = new URLSearchParams(window.location.search);
if (this.trade.assetToTrade) {
searchParams.set("assetToTrade", this.trade.assetToTrade);
}
if (this.trade.assetToTradeInto) {
searchParams.set("assetToTradeInto", this.trade.assetToTradeInto);
}
let url = window.ajaxTradePrepareUrl + "?" + searchParams.toString();
this.trade.updateTradePriceAbortController = new AbortController();
fetch(url, {
signal: this.trade.updateTradePriceAbortController.signal,
headers: {
'Content-Type': 'application/json'
}
}
).then(function (response) {
_this.trade.isUpdating = false;
if (response.ok) {
return response.json();
}
// _this.trade.results = data;
// _this.trade.errorMsg = null; }
// Do nothing on error
}
).then(function (data) {
_this.trade.maxQtyToTrade = data.maxQtyToTrade;
// By default trade everything
if (_this.trade.qty === null) {
_this.trade.qty = _this.trade.maxQtyToTrade;
}
// Cannot trade more than what we have
if (data.maxQtyToTrade < _this.trade.qty) {
_this.trade.qty = _this.trade.maxQtyToTrade;
}
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) {
// 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];
if (assetToTradeIntoHoldings) {
return assetToTradeIntoHoldings.qty >= minQtyToTrade;
}
},
swapTradeAssets: function () {
// Swap the 2 assets
let tmp = this.trade.assetToTrade;
this.trade.assetToTrade = this.trade.assetToTradeInto;
this.trade.assetToTradeInto = tmp;
this.trade.price = 1 / this.trade.price;
this._refreshTradeDataAfterAssetChange();
},
_refreshTradeDataAfterAssetChange: function () {
let maxQtyToTrade = this.getMaxQtyToTrade(this.trade.assetToTrade);
this.trade.qty = maxQtyToTrade
this.trade.maxQtyToTrade = maxQtyToTrade;
this.trade.updateTradePriceAbortController.abort();
// Update the price asap, so we can continue
let _this = this;
setTimeout(function () {
_this.updateTradePrice();
}, 100);
},
refreshAccountBalances: function () {
let _this = this;
fetch(window.ajaxBalanceUrl).then(function (response) {
return response.json();
}).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) {
// 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) {
// 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 () {
this.refreshAccountBalances();
},
mounted: function () {
// Runs when the app is ready
}
});