@algodex/algodex-sdk
Version:
API calls for interacting with the Algorand blockchain
523 lines (481 loc) • 15.6 kB
JavaScript
/*
* Copyright (C) 2021-2022 Algodex VASP (BVI) Corp.
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/.
*/
/*
* Algodex REST API Interface
*
* Includes all responses from the publicly exposed routes
*
* @author Alexander Trefonas
* @author Michael Feher
* Algodex VASP (BVI) Corp.
*/
const Big = require('big.js');
const HTTPClient = require('../../http/HTTPClient.js');
const floatToFixed = require('../../utils/format/floatToFixed');
const calculateAsaBuyAmount = require('../../utils/calc/toAlgoAmount');
const convertFromAsaUnits = require('../../utils/units/fromAsaUnits');
const dayjs = require('dayjs');
const logger = require('pino')({
level: process.env.LOG_LEVEL || 'error',
});
/**
* # 🕸 Algodex HTTP Client
*
* Used to connect to the Algodex REST endpoints
*
* @TODO: Generate from OAS/Swagger
* @param {string} baseUrl Base URL to use for API requests
* @param {boolean} [etags] Flag to enable Etag on GET Requests (optional)
* @param {integer} [apiVersion] 1 or 2 (optional)
* @param {string} [v1Url] Set this for any APIs that need to use v1 (optional)
* @extends HTTPClient
* @ignore
* @constructor
*/
function AlgodexClient(baseUrl, etags=true, apiVersion=1, v1Url=undefined) {
// Apply client
const client = new HTTPClient(baseUrl, etags);
Object.keys(client).forEach((key)=>{
this[key] = client[key];
});
this.apiVersion = apiVersion;
if (v1Url) {
this.v1Url = v1Url;
}
}
// Extend from HTTPClient
AlgodexClient.prototype = Object.create(HTTPClient.prototype);
// Override/Assign prototypes
Object.assign(AlgodexClient.prototype, {
/**
*
* @param {object} ob Orderbook Object
* @return {Order} A Order Object
**/
mapOrderbookToOrders(ob) {
const price =parseFloat(ob.price);
const amount = parseFloat(ob.formattedASAAmount);
return {
execution: 'execute',
client: ob.client,
indexer: ob.indexer,
address: ob.orderCreatorAddr,
type: ob.escrowOrderType,
price,
amount,
total: price * amount,
appId: parseInt(ob.appId),
asset: {
id: ob.assetId,
},
contract: {
N: ob.n,
D: ob.d,
min: ob.min,
entry: ob.orderEntry,
escrow: ob.escrowAddr,
creator: ob.orderCreatorAddr,
},
version: ob.version,
};
},
/**
*
* @param {Object} orderBook
* @return {*}
*/
mapToAllEscrowOrders(orderBook) {
if (!Array.isArray(orderBook.buy)) {
throw new TypeError('Buy orders must be an array');
}
if (!Array.isArray(orderBook.sell)) {
throw new TypeError('Sell orders must be an array');
}
const mapOrders = (orders, type) => {
return orders.map((order) => {
return {
// TODO: Move to standard Orders
// type,
// asset: {
// id: order.assetId,
// },
// price: fromBaseUnits(order.assetLimitPriceInAlgos),
// amount: type === 'buy', order.algoAmount || order.asaAmount
// contract: {
// entry: `${order.assetLimitPriceN}-${order.assetLimitPriceD}-${order.minimumExecutionSizeInAlgo}-${order.assetId}`,
// N: order.assetLimitPriceN,
// D: order.assetLimitPriceD,
// min: order.minimumExecutionSizeInAlgo,
// escrow: order.escrowAddress,
// algoBalance: order.algoAmount,
// asaBalance: order.asaAmount,
// },
client: order.client,
orderEntry: `${order.assetLimitPriceN}-${order.assetLimitPriceD}-${order.minimumExecutionSizeInAlgo}-${order.assetId}`,
price: order.formattedPrice,
formattedASAAmount: order.formattedASAAmount,
n: parseInt(order.assetLimitPriceN),
d: parseInt(order.assetLimitPriceD),
min: order.minimumExecutionSizeInAlgo,
version: order.version,
escrowAddr: order.escrowAddress,
algoBalance: order.algoAmount,
asaBalance: order.asaAmount,
escrowOrderType: type,
isASAEscrow: type === 'sell',
orderCreatorAddr: order.ownerAddress,
assetId: order.assetId,
appId: order.appId,
};
});
};
return mapOrders(orderBook.buy, 'buy').concat(mapOrders(orderBook.sell, 'sell'));
},
/**
* Convert to standard orders list
* @param {Order[]} orders
* @param {number} asaDecimals
* @param {"buy"|"sell"} type
* @return {*}
*/
aggregateOrders(orders, asaDecimals, type) {
if (!Array.isArray(orders)) {
throw new TypeError('Orders must be an array!');
}
const isBuyOrder = type === 'buy';
let total = 0;
const leftPriceDecimalsLength = orders.map((order) => {
const price = new Big(convertFromAsaUnits(order.asaPrice, asaDecimals));
const left = Math.floor(price);
const right = price.sub(left);
return right !== 0 && right.toString().length > 2 ?
right.toString().length - 2 :
0;
});
const decimalLength =
leftPriceDecimalsLength.length === 0 ?
0 :
Math.max(...leftPriceDecimalsLength);
const sortOrdersToAggregate = (a, b) => {
if (isBuyOrder) {
return b.asaPrice - a.asaPrice;
}
return a.asaPrice - b.asaPrice;
};
const reduceAggregateData = (result, order) => {
const _price = convertFromAsaUnits(order.asaPrice, asaDecimals);
const price = floatToFixed(_price, 6, decimalLength);
const orderAmount = isBuyOrder ? order.algoAmount : order.asaAmount;
const amount = isBuyOrder ?
calculateAsaBuyAmount(price, orderAmount) :
parseFloat(order.formattedASAAmount);
total += amount;
const index = result.findIndex((obj) => obj.price === price);
if (index !== -1) {
result[index].amount += amount;
result[index].total += amount;
return result;
}
result.push({
price,
amount,
total,
});
return result;
};
const sortRowsByPrice = (a, b) => {
return b.price - a.price;
};
return orders.sort(sortOrdersToAggregate)
.reduce(reduceAggregateData, [])
.sort(sortRowsByPrice);
},
/**
*
* @param {object} data
* @param {object} assetSearchData // Used as backup for matching asset names
* @return {null|*}
*/
mapOpenOrdersData(data, assetSearchData=null) {
if (
!data ||
!data.buyASAOrdersInEscrow ||
!data.sellASAOrdersInEscrow
// FIXME - removed null check for allAssets due to serverside issue
) {
return null;
}
const getAssetMap = (assetSearchData) => {
if (assetSearchData?.assets) {
return assetSearchData.assets.reduce((map, asset) => {
const unitName = asset.unitName;
map.set(asset.assetId, unitName);
return map;
}, new Map());
}
return null;
};
const assetToNameMap = getAssetMap(assetSearchData);
const {
buyASAOrdersInEscrow: buyOrdersData,
sellASAOrdersInEscrow: sellOrdersData,
allAssets: assetsData,
} = data;
const assetsInfo = (assetsData || []).reduce((allAssetsInfo, currentAssetInfo) => {
allAssetsInfo[currentAssetInfo.index] = currentAssetInfo;
return allAssetsInfo;
}, {});
const buyOrders = buyOrdersData.map((order) => {
const {assetId, formattedPrice, formattedASAAmount, unix_time} = order;
return {
asset: {id: assetId},
date: dayjs.unix(unix_time).format('YYYY-MM-DD HH:mm:ss'),
// date: moment(unix_time, 'YYYY-MM-DD HH:mm').format(),
unix_time: unix_time,
price: floatToFixed(formattedPrice),
pair: assetsInfo[assetId] ? `${assetsInfo[assetId]?.params['unit-name']}/ALGO` :
assetToNameMap?.get(assetId)+'/ALGO',
type: 'BUY',
status: 'OPEN',
amount: formattedASAAmount,
metadata: order,
};
});
const sellOrders = sellOrdersData.map((order) => {
const {assetId, formattedPrice, formattedASAAmount, unix_time} = order;
return {
asset: {id: assetId},
date: dayjs.unix(unix_time).format('YYYY-MM-DD HH:mm:ss'),
unix_time: unix_time,
price: floatToFixed(formattedPrice),
pair: assetsInfo[assetId] ? `${assetsInfo[assetId]?.params['unit-name']}/ALGO` :
assetToNameMap?.get(assetId)+'/ALGO',
type: 'SELL',
status: 'OPEN',
amount: formattedASAAmount,
metadata: order,
};
});
const allOrders = [...buyOrders, ...sellOrders];
allOrders.sort((a, b) => (a.unix_time < b.unix_time ? 1 : -1));
return allOrders;
},
/**
*
* @param {object} data
* @return {null|*}
*/
mapTradeHistoryData(data) {
const buySide = 'BUY';
const sellSide = 'SELL';
if (!data || !data.transactions || !data.allAssets) {
return null;
}
const {transactions: tradeHistoryData, allAssets: assetsData} = data;
const assetsInfo = assetsData.reduce((allAssetsInfo, currentAssetInfo) => {
allAssetsInfo[currentAssetInfo.index] = currentAssetInfo;
return allAssetsInfo;
}, {});
return tradeHistoryData.map(
({
unix_time,
asset_id,
tradeType,
formattedPrice,
formattedASAAmount,
group_id,
}) => {
const side = tradeType === 'buyASA' ? buySide : sellSide;
return {
id: asset_id,
date: dayjs(unix_time * 1000).format('YYYY-MM-DD HH:mm:ss'),
price: floatToFixed(formattedPrice),
pair: `${assetsInfo[asset_id].params['unit-name']}/ALGO`,
side,
amount: formattedASAAmount,
groupId: encodeURIComponent(group_id),
};
},
);
},
/**
* Fetch Orders by Type
*
* Reduces the response to the standard Orders Object
*
* @param {'wallet'|'asset'} type Type of fetch, wallet or asset
* @param {string} id Index of the resource
* @return {Promise<Array<Order>>}
*/
async fetchOrders(type, id) {
let res;
if (type === 'wallet') {
res = await this.fetchWalletOrders(id);
} else if (type === 'asset') {
res = await this.fetchAssetOrders(id);
}
return this.mapToAllEscrowOrders({
buy: res.buyASAOrdersInEscrow,
sell: res.sellASAOrdersInEscrow,
}).map((o)=> this.mapOrderbookToOrders(o));
},
/**
* Fetch Asset's Chart
*
* Returns OHLCV(Open, High, Low, Close, Volume) response from the API.
* Used in lightweight-charts to display the Candlestick and Area views.
*
* @param {number|string} id Unique Asset Identifier
* @param {string} interval Time interval to aggregate data on
* @return {Promise<Object>}
*/
async fetchAssetChart(id, interval) {
logger.debug(`fetchAssetChart(${id}, ${interval})`);
const res = await this.get(
this.apiVersion === 2 ? `${this.baseUrl}/trades/charts/asset/${id}/period/${interval}` :
`${this.baseUrl}/charts2.php?assetId=${id}&chartTime=${interval}`,
);
return res.data;
},
/**
* Fetch Asset's Orders
* @param {number|string} id Unique Asset Identifier
* @return {Promise<Object>}
*/
async fetchAssetOrders(id) {
logger.debug(`fetchAssetOrders(${id})`);
// const baseUrl = this.v1Url ? this.v1Url : this.baseUrl;
// const url = `${baseUrl}/orders.php?assetId=${id}`; // temporarily do not use v2
const url = this.apiVersion === 2 ? `${this.baseUrl}/orders/asset/${id}` :
`${this.baseUrl}/orders.php?assetId=${id}`;
const res = await this.get(url);
return res.data;
},
/**
*
* Fetch Wallet's Orders
*
* @TODO: Deprecate includeAssetInfo
* @param {string} address Account Address
* @param {boolean} includeAssetInfo Should fetch asset info
* @return {Promise<Object>}
*/
async fetchWalletOrders(address, includeAssetInfo = true) {
logger.debug(
`fetchOpenOrdersByAddress(${address}, ${includeAssetInfo})`,
);
// const res = await this.get(
// `${this.baseUrl}/orders.php?ownerAddr=${address}` +
// `&getAssetInfo=${includeAssetInfo}`,
// );
console.log('in fetchWalletOrders');
const res = await this.get(
this.apiVersion === 2 ? `${this.baseUrl}/orders/wallet/${address}` :
`${this.baseUrl}/orders.php?ownerAddr=${address}&getAssetInfo=${includeAssetInfo}`,
);
return res.data;
},
/**
* Fetch Asset's Trade History
*
* @param {string | number} id
* @return {Promise<Object>}
*/
async fetchAssetTradeHistory(id) {
logger.debug(`fetchAssetTradeHistory(${id})`);
const res = await this.get(
this.apiVersion === 2 ? `${this.baseUrl}/trades/history/asset/${id}` :
`${this.baseUrl}/trade_history.php?assetId=${id}`,
);
return res.data;
},
/**
* Fetch Wallet's Trade History
* @param {string} address
* @param {boolean} includeAssetInfo
* @return {Promise<Object>}
*/
async fetchWalletTradeHistory(address, includeAssetInfo = true) {
logger.debug(`fetchWalletTradeHistory(${address}, ${includeAssetInfo})`);
const res = await this.get(
this.apiVersion === 2 ? `${this.baseUrl}/trades/history/wallet/${address}` :
`${this.baseUrl}/trade_history.php?ownerAddr=${address}` +
`&getAssetInfo=${includeAssetInfo}`,
);
return res.data;
},
/**
* Fetch Wallet's Assets
* @param {string} address
* @return {Promise<Object>}
*/
async fetchWalletAssets(address) {
logger.debug(`fetchWalletAssets(${address})`);
const res = await this.get(
this.apiVersion === 2 ? `${this.baseUrl}/wallet/assets/${address}` :
`${this.baseUrl}/wallet_assets.php?ownerAddr=${address}`,
);
return res.data;
},
/**
* Search for Assets
* @param {string} query
* @return {Promise<{assets: any}>}
*/
async searchAssets(query) {
logger.debug(`searchAssets(${query})`);
// FIXME
// const url = this.v1Url ? this.v1Url : this.baseUrl;
const v2Url = query && query.length > 0 ? `${this.baseUrl}/assets/search/${query}` :
`${this.baseUrl}/assets/searchall`;
const res = await this.get(
this.apiVersion === 2 ? v2Url :
`${this.baseUrl}/asset_search.php?query=${query}`,
// `${url}/asset_search.php?query=${query}`,
);
return {assets: res.data};
},
/**
* Fetch Assets
*
* Return all available Assets in Algodex.
*
* @return {Promise<*>}
*/
async fetchAssets() {
logger.debug(`fetchAssets() api version: ${this.apiVersion}`);
const {
data: {data}, // FIXME for API below since it is used for prefetching
} = await this.get(this.apiVersion === 2 ? `${this.baseUrl}/assets/all` :
`${this.baseUrl}/assets.php`);
return data;
},
/**
* Fetch Asset's Price
*
* Get Asset state from Algodex
*
* @param {string|number} id
* @return {Promise<*>}
*/
async fetchAssetPrice(id) {
logger.debug(`fetchAssetPrice(${id})`);
let res = {};
try {
res = await this.get(this.apiVersion === 2 ? `${this.baseUrl}/assets/${id}` :
`${this.baseUrl}/assets.php?id=${id}`);
} catch (error) {
return {};
}
const {
data: {data},
} = res;
return typeof data[0] !== 'undefined' ? data[0] : {};
},
});
module.exports = AlgodexClient;