btcpayserver/BTCPayServer/wwwroot/js/custodian-account.js

684 lines
25 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,
simulationAbortController: null,
priceRefresherInterval: null,
fromAsset: null,
toAsset: null,
qty: null,
maxQty: null,
price: null,
priceForPair: {}
},
withdraw: {
asset: null,
paymentMethod: null,
errorMsg: null,
qty: null,
minQty: null,
maxQty: null,
badConfigFields: null,
results: null,
isUpdating: null,
isExecuting: false,
simulationAbortController: null,
ledgerEntries: null
},
},
computed: {
tradeQtyToReceive: function () {
return this.trade.qty / this.trade.price;
},
canExecuteTrade: function () {
return this.trade.qty >= this.getMinQtyToTrade() && this.trade.price !== null && this.trade.fromAsset !== null && this.trade.toAsset !== 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.fromAsset]?.tradableAssetPairs;
if (pairs) {
for (let i in pairs) {
let pair = pairs[i];
if (pair.assetBought === this.trade.fromAsset) {
r.push(pair.assetSold);
} else if (pair.assetSold === this.trade.fromAsset) {
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 === null || row.fiatValue > t.account.dustThresholdInFiat;
});
}
rows = rows.sort(function (a, b) {
if(b.fiatValue !== null && a.fiatValue !== null){
return b.fiatValue - a.fiatValue;
}else if(b.fiatValue !== null && a.fiatValue === null){
return 1;
}else if(b.fiatValue === null && a.fiatValue !== null) {
return -1;
}else{
return b.asset.localeCompare(a.asset);
}
});
return rows;
}
},
canExecuteWithdrawal: function () {
return (this.withdraw.minQty != null && this.withdraw.qty >= this.withdraw.minQty)
&& (this.withdraw.maxQty != null && this.withdraw.qty <= this.withdraw.maxQty)
&& this.withdraw.badConfigFields?.length === 0
&& this.withdraw.paymentMethod
&& !this.withdraw.isExecuting
&& !this.withdraw.isUpdating
&& this.withdraw.results === null;
},
availableAssetsToWithdraw: function () {
let r = [];
const balances = this?.account?.assetBalances;
if (balances) {
for (let asset in balances) {
const balance = balances[asset];
if (balance?.withdrawablePaymentMethods?.length) {
r.push(asset);
}
}
}
;
return r.sort();
},
availablePaymentMethodsToWithdraw: function () {
if (this.withdraw.asset) {
let paymentMethods = this?.account?.assetBalances?.[this.withdraw.asset]?.withdrawablePaymentMethods;
if (paymentMethods) {
return paymentMethods.sort();
}
}
return [];
},
withdrawFees: function(){
let r = [];
if(this.withdraw.ledgerEntries){
for (let i = 0; i< this.withdraw.ledgerEntries.length; i++){
let entry = this.withdraw.ledgerEntries[i];
if(entry.type === 'Fee'){
r.push(entry);
}
}
}
return r;
}
},
methods: {
getMaxQty: function (fromAsset) {
let row = this.account?.assetBalances?.[fromAsset];
if (row) {
return row.qty;
}
return null;
},
getMinQtyToTrade: function (fromAsset = this.trade.fromAsset, toAsset = this.trade.toAsset) {
if (fromAsset && toAsset && this.account?.assetBalances) {
for (let asset in this.account.assetBalances) {
let row = this.account.assetBalances[asset];
let pairCode = fromAsset + "/" + toAsset;
let pairCodeReverse = toAsset + "/" + fromAsset;
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) {
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.maxQty;
},
setWithdrawQtyPercent: function (percent) {
this.withdraw.qty = percent / 100 * this.withdraw.maxQty;
},
openTradeModal: function (row) {
let _this = this;
this.trade.row = row;
this.trade.results = null;
this.trade.errorMsg = null;
this.trade.fromAsset = row.asset;
if (row.asset === this.account.storeDefaultFiat) {
this.trade.toAsset = "BTC";
} else {
this.trade.toAsset = this.account.storeDefaultFiat;
}
this.trade.qty = row.qty;
this.trade.maxQty = 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) {
this.withdraw.asset = row.asset;
this.withdraw.qty = row.qty;
this.withdraw.paymentMethod = null;
this.withdraw.minQty = 0;
this.withdraw.maxQty = row.qty;
this.withdraw.results = null;
this.withdraw.errorMsg = null;
this.withdraw.isUpdating = null;
this.withdraw.isExecuting = false;
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.setModalCanBeClosed(this.modals.trade, false);
const _this = this;
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': this.getRequestVerificationToken()
},
body: JSON.stringify({
fromAsset: _this.trade.fromAsset,
toAsset: _this.trade.toAsset,
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.setModalCanBeClosed(_this.modals.trade, true);
_this.trade.isExecuting = false;
},
onWithdrawSubmit: async function (e) {
e.preventDefault();
const form = e.currentTarget;
const url = form.getAttribute('action');
const method = form.getAttribute('method');
this.withdraw.isExecuting = true;
this.setModalCanBeClosed(this.modals.withdraw, false);
let dataToSubmit = {
paymentMethod: this.withdraw.paymentMethod,
qty: this.withdraw.qty
};
const _this = this;
const response = await fetch(url, {
method: method,
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': this.getRequestVerificationToken()
},
body: JSON.stringify(dataToSubmit)
});
let data = null;
try {
data = await response.json();
} catch (e) {
}
if (response.ok) {
_this.withdraw.results = data;
_this.withdraw.errorMsg = null;
_this.refreshAccountBalances();
} else {
_this.withdraw.errorMsg = data && data.message || "Error";
}
_this.setModalCanBeClosed(_this.modals.withdraw, true);
_this.withdraw.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.fromAsset || !this.trade.toAsset) {
// We need to know the 2 assets or we cannot do anything...
return;
}
if (this.trade.fromAsset === this.trade.toAsset) {
// 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 dataToSubmit = {
fromAsset: this.trade.fromAsset,
toAsset: this.trade.toAsset,
qty: this.trade.qty
};
let url = window.ajaxTradeSimulateUrl;
this.trade.simulationAbortController = new AbortController();
let _this = this;
fetch(url, {
method: "POST",
body: JSON.stringify(dataToSubmit),
signal: this.trade.simulationAbortController.signal,
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': this.getRequestVerificationToken()
}
}
).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.maxQty = data.maxQty;
// By default trade everything
if (_this.trade.qty === null) {
_this.trade.qty = _this.trade.maxQty;
}
// Cannot trade more than what we have
if (data.maxQty < _this.trade.qty) {
_this.trade.qty = _this.trade.maxQty;
}
let pair = data.toAsset + "/" + data.fromAsset;
let pairReverse = data.fromAsset + "/" + data.toAsset;
// 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.toAsset, this.trade.fromAsset);
let assetToTradeIntoHoldings = this.account?.assetBalances?.[this.trade.toAsset];
if (assetToTradeIntoHoldings) {
return assetToTradeIntoHoldings.qty >= minQtyToTrade;
}
},
swapTradeAssets: function () {
// Swap the 2 assets
let tmp = this.trade.fromAsset;
this.trade.fromAsset = this.trade.toAsset;
this.trade.toAsset = tmp;
this.trade.price = 1 / this.trade.price;
this.refreshTradeSimulation();
},
refreshTradeSimulation: function () {
let maxQty = this.getMaxQty(this.trade.fromAsset);
this.trade.qty = maxQty
this.trade.maxQty = maxQty;
if(this.trade.simulationAbortController) {
this.trade.simulationAbortController.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;
for(let asset in _this.account.assetBalances){
let assetInfo = _this.account.assetBalances[asset];
if(asset !== _this.account.storeDefaultFiat) {
let pair1 = asset + '/' + _this.account.storeDefaultFiat;
_this.trade.priceForPair[pair1] = assetInfo.bid;
let pair2 = _this.account.storeDefaultFiat + '/' + asset;
_this.trade.priceForPair[pair2] = 1 / assetInfo.bid;
}
}
});
},
getRequestVerificationToken: function () {
return document.querySelector("input[name='__RequestVerificationToken']").value;
},
setModalCanBeClosed: function (modal, flag) {
modal._config.keyboard = flag;
if (flag) {
modal._config.backdrop = true;
} else {
modal._config.backdrop = 'static';
}
},
refreshWithdrawalSimulation: function () {
if(!this.withdraw.paymentMethod || !this.withdraw.qty){
// We are missing required data, stop now.
return;
}
if(this.withdraw.simulationAbortController) {
this.withdraw.simulationAbortController.abort();
}
let data = {
paymentMethod: this.withdraw.paymentMethod,
qty: this.withdraw.qty
};
const _this = this;
const token = this.getRequestVerificationToken();
this.withdraw.isUpdating = true;
this.withdraw.simulationAbortController = new AbortController();
fetch(window.ajaxWithdrawSimulateUrl, {
method: "POST",
body: JSON.stringify(data),
signal: this.withdraw.simulationAbortController.signal,
headers: {
'Content-Type': 'application/json',
'RequestVerificationToken': token
}
}).then(function (response) {
_this.withdraw.isUpdating = false;
return response.json();
}).then(function (data) {
if (data.minQty === null) {
_this.withdraw.minQty = 0;
} else {
_this.withdraw.minQty = data.minQty;
}
if (data.maxQty === null) {
_this.withdraw.maxQty = _this.account.assetBalances?.[_this.withdraw.asset]?.qty;
} else {
_this.withdraw.maxQty = data.maxQty;
}
if (_this.withdraw.qty === null || _this.withdraw.qty > _this.withdraw.maxQty) {
_this.withdraw.qty = _this.withdraw.maxQty;
}
_this.withdraw.badConfigFields = data.badConfigFields;
_this.withdraw.errorMsg = data.errorMessage;
_this.withdraw.ledgerEntries = data.ledgerEntries;
});
},
getStoreDefaultFiatValueForAsset: function(asset){
// TODO
}
},
watch: {
'trade.fromAsset': function (newValue, oldValue) {
if (newValue === this.trade.toAsset) {
// This is the same as swapping the 2 assets
this.trade.toAsset = oldValue;
this.trade.price = 1 / this.trade.price;
this.refreshTradeSimulation();
}
if (newValue !== oldValue) {
// The qty is going to be wrong, so set to 100%
this.trade.qty = this.getMaxQty(this.trade.fromAsset);
}
},
'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;
});
},
'withdraw.asset': function (newValue, oldValue) {
if (this.availablePaymentMethodsToWithdraw.length > 0) {
this.withdraw.paymentMethod = this.availablePaymentMethodsToWithdraw[0];
} else {
this.withdraw.paymentMethod = null;
}
},
'withdraw.paymentMethod': function (newValue, oldValue) {
if (this.withdraw.paymentMethod && this.withdraw.qty) {
this.withdraw.minQty = 0;
this.withdraw.maxQty = null;
this.withdraw.errorMsg = null;
this.withdraw.badConfigFields = null;
this.refreshWithdrawalSimulation();
}
},
'withdraw.qty': function (newValue, oldValue) {
if (newValue > this.withdraw.maxQty) {
this.withdraw.qty = this.withdraw.maxQty;
}
this.refreshWithdrawalSimulation();
}
},
created: function () {
this.refreshAccountBalances();
},
mounted: function () {
// Runs when the app is ready
}
});