UNPKG

@algodex/algodex-sdk

Version:

API calls for interacting with the Algorand blockchain

305 lines (277 loc) 10.6 kB
/* * 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/. */ // const withExecuteTxns = require('./taker/withExecuteTxns'); const getTakerOrderInformation = require('./taker/getTakerOrderInformation'); const getCutTakerOrders = require('./taker/getCutTakerOrders'); // const fromBaseUnits = require('../../utils/units/fromBaseUnits'); const logger = require('../../logger'); /** * * @param {string} takerWalletAddr * @param {boolean} isSellingAssetAsTakerOrder * @param {Object[]} allOrderBookOrders * @return {Array} * @ignore */ function getQueuedTakerOrders( takerWalletAddr, isSellingAssetAsTakerOrder, allOrderBookOrders, ) { const queuedOrders = []; // getAllOrderBookEscrowOrders is UI dependant and needs to be customized for the React version if (allOrderBookOrders == null || allOrderBookOrders.length === 0) { return []; } // FIXME: don't allow executions against own orders! check wallet address doesn't match // takerWalletAddr for (let i = 0; i < allOrderBookOrders.length; i++) { const orderBookEntry = allOrderBookOrders[i]; if (orderBookEntry['escrowOrderType'] === 'buy' && !isSellingAssetAsTakerOrder) { // only look for sell orders in this case continue; } if (orderBookEntry['escrowOrderType'] === 'sell' && isSellingAssetAsTakerOrder) { // only look for buy orders in this case continue; } orderBookEntry.price = parseFloat(orderBookEntry.price); queuedOrders.push(orderBookEntry); } if (isSellingAssetAsTakerOrder) { // sort highest first (index 0) to lowest (last index) // these are buy orders, so we want to sell to the highest first queuedOrders.sort((a, b) => (a.price < b.price) ? 1 : (a.price === b.price) ? ((a.price < b.price) ? 1 : -1) : -1); } else { // sort lowest first (index 0) to highest (last index) // these are sell orders, so we want to buy the lowest first queuedOrders.sort((a, b) => (a.price > b.price) ? 1 : (a.price === b.price) ? ((a.price > b.price) ? 1 : -1) : -1); } return queuedOrders; } /** * @deprecated * @param {number} assetId * @param {AlgodexApi} api * @return {Promise<*>} * @ignore */ async function getOrderbook(assetId, api) { logger.warn(`Fetching Orderbook for ${assetId}`); const res = await api.http.dexd.fetchAssetOrders(assetId); return api.http.dexd.mapToAllEscrowOrders({ buy: res.buyASAOrdersInEscrow, sell: res.sellASAOrdersInEscrow, }); } /** * * * # 🏃 getTakerOrders * * Accepts an {@link Order} with execution of [Taker]{@tutorial Taker} and matches the criteria with [Executable]{@tutorial Executable} orders in the [Orderbook]{@tutorial Orderbook}. * * If executable orders exist then the relevant transactions are generated. * * The generated transactions fall into one of the two categories below: * ### SingleOrderExecution * **Condition:** For when the desired user "total" amount is less than the available escrow amount * * Example: There is an order in the orderbook at the user's desired price and desired amount * * **Return value:** An array of length=1 containing {@link Order} object with an txnArr attached to the contract * * * ### MultiOrderExececution * **Condition:** For when the desired user total amount is greater than the available escrow amount * * Example: There are multiple orders in the orderbook at the user's desired price, but no entry contains the user's desired amount. * * **Return value:** An array of variable length. Each item represents a group of transactions. * * ### When is it used? * This method and the corresponding factories are used anytime a user is executing upon an existing [Algodex Orderbook]{@tutorial Orderbook} [Order]{@link Order}. * * This method is used to generate the taker transactions in [getMakerTakerTxns]{@link module/structure.getMakerTakerTxns} * * This method would be ideal for use in algorithmic trading strategies. * * @example * const [AlgodexAPI]{@link AlgodexApi} = require(@algodex/algodex-sdk) * const api = new [AlgodexAPI]{@link AlgodexApi}(require('../config.json')) * const order = { * "client": api.algod, * "indexer": api.indexer, * "asset": { * "id": 15322902, * "decimals": 6, * }, * "address": "TJFFNUYWHPPIYDE4DGGYPGHWKGAPJEWP3DGE5THZS3B2M2XIAPQ2WY3X4I", * "price": 2.22, * "amount": 1, * "total": 2, * "execution": "taker", * "type": "buy", * "appId": 22045503, * "version": 6 * } * * // Scenario: singleOrder * //order.execution === 'taker' * let res = await getTakerOrders(api, order) * console.log(res.contract.txns) * //Outputs an array with structure of: * [makeExecuteAssetTxns]{@link module:txns/sell.makeExecuteAssetTxns} || [makeExecuteAlgoTxns]{@link module:txns/buy.makeExecuteAlgoTxns} * * * * @example * const [AlgodexAPI]{@link AlgodexApi} = require(@algodex/algodex-sdk) * const api = new [AlgodexAPI]{@link AlgodexApi}(require('../config.json')) * const order = { * "client": api.algod, * "indexer": api.indexer, * "asset": { * "id": 15322902, * "decimals": 6, * }, * "address": "TJFFNUYWHPPIYDE4DGGYPGHWKGAPJEWP3DGE5THZS3B2M2XIAPQ2WY3X4I", * "price": 2.22, * "amount": 1, * "total": 2, * "execution": "taker", * "type": "buy", * "appId": 22045503, * "version": 6 * } * // Scenario: multiOrder * //order.execution === 'taker' * let res = await getTakerOrders(api, order) * console.log(res) * //Outputs an array with each item being: * [withExecuteAssetTxns]{@link module:txns/sell.withExecuteAssetTxns} || [withExecuteAlgoTxns]{@link module:txns/buy.withExecuteAlgoTxns} * * @param {AlgodexApi} api The Algodex API * @param {Order} order The User's Order * @return {Promise<Structure[]>} * @throws ValidationError * @see [makeExecuteAssetTxns]{@link module:txns/sell.makeExecuteAssetTxns} || [makeExecuteAlgoTxns]{@link module:txns/buy.makeExecuteAlgoTxns} || [withExecuteAssetTxns]{@link module:txns/sell.withExecuteAssetTxns} || [withExecuteAlgoTxns]{@link module:txns/buy.withExecuteAlgoTxns} * @memberOf module:order/structure */ async function getTakerOrders(api, order) { if (order.execution !== 'taker' && order.execution !== 'market' && order.execution !== 'both') { throw new TypeError(`Unsupported execution of ${order.execution}, use [taker, market, both] for automated orderbook matching`); } // Fetch Orderbook if it doesn't exist const _orderbook = !order.asset?.orderbook ? // TODO: Move to new Orderbook Shape await getOrderbook(order.asset.id, api) : order.asset.orderbook; // Clone Object for mutations const _order = {...order, asset: {...order.asset, orderbook: _orderbook}}; /** * @todo Move to new Orderbook Shape, send User's Order instance as the first parameter * @type {Array} */ const _queuedOrders = getQueuedTakerOrders(_order.address, _order.type === 'sell', _orderbook); // Exit if no taker orders if (_queuedOrders.length === 0) { logger.warn({address: order.address, type: _order.type}, 'No orders exist for user to execute', ); // Exit early return []; } /** * First Order * @type {Order} */ const _firstOrder = _queuedOrders[0]; // rough implementation will change name/ placement later /** * Balance of the First Order * @type {Number} */ // const firstOrderBalance = fromBaseUnits( // _order.type === 'buy' ? // _firstOrder.asaBalance : // _firstOrder.algoBalance/ _firstOrder.price, // to get assetAmount // ); // We want to see if the escrow amount is larger than order amount because comparing by order total can lead to unexpected results // If user is selling below market price we want to make sure they get the best deal possible for their "amount" sold so we should ignore their total. /** * Check to see if the order fits * @type {boolean} */ // const isMultiOrderExecution = _order.amount > firstOrderBalance; // We should always check by amounts, totals can be misleading when users input prices that over/under /** * Flag for if the User's order has Executable Orders * @type {boolean} */ const isExecutable = _order. type === 'buy' ? _order.price >= _firstOrder.price : _order.price <= _firstOrder.price; // No Taker Orders Found if (!isExecutable) { logger.warn({ userPrice: _order.price, totalOrders: _queuedOrders.length, spreadPrice: _firstOrder.price, }, 'No orders exist at the price.'); // Exit early return []; } // User's order "fits" into the top order. Execute against that order // if (!isMultiOrderExecution) { // // Closeout if the order matches the balance // const withCloseout = firstOrderBalance === _order.total; // // const _price = parseFloat(_firstOrder.price); // /** // * Mapped Order from API // * // * @todo This should come from the API and we should only need to set amount/total // * @type {Order} // * @private // */ // const _mappedOrder = { // execution: 'execute', // client: _order.client, // indexer: api.indexer, // address: _firstOrder.orderCreatorAddr, // type: _firstOrder.escrowOrderType, // price: _price, // amount: _order.amount, // total: _price * _order.amount, // appId: parseInt(_firstOrder.appId), // asset: { // id: _firstOrder.assetId, // }, // contract: { // N: _firstOrder.n, // D: _firstOrder.d, // min: _firstOrder.min, // entry: _firstOrder.orderEntry, // escrow: _firstOrder.escrowAddr, // creator: _firstOrder.orderCreatorAddr, // }, // version: _firstOrder.version, // wallet: _order.wallet, // }; // // // Return an Array with the compiled order // return [await withExecuteTxns(_mappedOrder, withCloseout)]; // } // // // Order is overflowing, split it and generate TakerTxns // if (isMultiOrderExecution) { // TODO: Handle Market Orders return await getCutTakerOrders( api, {..._order, indexer: api.indexer}, _queuedOrders, await getTakerOrderInformation({..._order, indexer: api.indexer}, _queuedOrders)); // } } module.exports = getTakerOrders;