lib/AlgodexApi.js

  1. // ///////////////////////////
  2. // Alexander Trefonas //
  3. // 7/9/2021 //
  4. // Copyright Algodev Inc //
  5. // All Rights Reserved. //
  6. // ///////////////////////////
  7. const logger = require('./logger');
  8. const ValidationError = require('./error/ValidationError');
  9. const ajv = require('./schema');
  10. const algosdk = require('algosdk');
  11. const _ = require('lodash');
  12. const ee = require('./events');
  13. const AlgodexClient = require('./order/http/AlgodexClient');
  14. const ExplorerClient = require('./http/clients/ExplorerClient');
  15. const IndexerClient = require('./http/clients/IndexerClient');
  16. const structure = require('./order/structure');
  17. const constants = require('./constants');
  18. const compile = require('./order/compile/compile');
  19. /**
  20. * ## [⚡ Wallet Event](#event:WalletEvent)
  21. * Fires during wallet operations
  22. *
  23. * @event AlgodexApi#WalletEvent
  24. * @type {Object}
  25. * @property {string} type Type of Event
  26. * @property {Wallet} wallet Wallet Data
  27. */
  28. /**
  29. * ## [⚡ Order Event](#event:OrderEvent)
  30. * Fires during order operations
  31. *
  32. * @event AlgodexApi#OrderEvent
  33. * @type {Object}
  34. * @property {string} type Type of Event
  35. * @property {Order} asset Order Data
  36. */
  37. /**
  38. * Setter Options
  39. * @typedef {Object} SetterOptions
  40. * @property {boolean} validate Enable validation
  41. * @ignore
  42. */
  43. /**
  44. * Composed Promises
  45. * @ignore
  46. * @param {function} functions
  47. * @return {function(*): *}
  48. */
  49. function composePromise(...functions) {
  50. return (initialValue) =>
  51. functions.reduceRight(
  52. (sum, fn) => Promise.resolve(sum).then(fn),
  53. initialValue,
  54. );
  55. }
  56. /**
  57. * Check for a validation flag in options
  58. *
  59. * @param {SetterOptions} options Setter Options
  60. * @return {boolean}
  61. * @private
  62. */
  63. function _hasValidateOption(options) {
  64. return (
  65. typeof options !== 'undefined' &&
  66. typeof options.validate !== 'undefined' &&
  67. options.validate
  68. );
  69. }
  70. /**
  71. *
  72. * @param {*} data
  73. * @param {*} key
  74. * @param {*} options
  75. * @return {ValidationError}
  76. * @private
  77. */
  78. function _getValidationError(data, key, options) {
  79. if (_hasValidateOption(options)) {
  80. const validate = ajv.getSchema(key);
  81. // Validate basic type errors
  82. if (!validate(data)) {
  83. // algodex.setAsset Validation Error
  84. return new ValidationError(validate.errors);
  85. }
  86. }
  87. }
  88. /**
  89. *
  90. * @param {Object} data Data to validate
  91. * @param {*} key
  92. * @param {*} initialized
  93. * @param {*} options
  94. * @return {ValidationError|Error}
  95. * @private
  96. */
  97. function _getSetterError(data, {key, initialized=false, options}={}) {
  98. if (!initialized) {
  99. return new Error('Algodex not initialized, run setConfig');
  100. }
  101. return _getValidationError(data, key, options);
  102. }
  103. /**
  104. *
  105. * @param {Array<Wallet>} current Current Addresses
  106. * @param {Array<Wallet>} addresses New Addresses
  107. * @return {*}
  108. * @private
  109. */
  110. function _filterExistingWallets(current, addresses) {
  111. if (!Array.isArray(current)) {
  112. throw new TypeError('Must be an array of current addresses');
  113. }
  114. if (!Array.isArray(addresses)) {
  115. throw new TypeError('Must be an array of addresses');
  116. }
  117. const lookup = current.map((addr)=>addr.address);
  118. return addresses.filter((w)=>!lookup.includes(w.address));
  119. }
  120. const _sendTransactions = async (client, signedTransactions) => {
  121. const sentRawTxnIdArr = [];
  122. for (const group of signedTransactions) {
  123. logger.debug(`Sending ${group.length} Group Transactions`);
  124. const rawTxns = group.map((txn) => txn.blob);
  125. try {
  126. const {txId} = await client.sendRawTransaction(rawTxns).do();
  127. sentRawTxnIdArr.push(txId);
  128. } catch (e) {
  129. console.log(e);
  130. }
  131. }
  132. await Promise.all(sentRawTxnIdArr.map((txId) => algosdk.waitForConfirmation(client, txId, 10 )));
  133. };
  134. /**
  135. * # 📦 AlgodexAPI
  136. *
  137. * The API requires several services to operate. These include but are not
  138. * limited to {@link ExplorerClient}, {@link Algodv2}, {@link Indexer}, and
  139. * {@link AlgodexClient}. This constructor allows for easy use of the underlying
  140. * smart contract {@link teal}.
  141. *
  142. * See [setWallet](#setWallet) and [placeOrder](#placeOrder) for more details
  143. *
  144. *
  145. * @example
  146. * // Create the API
  147. * const config = require('./config.js')
  148. * const api = new AlgodexAPI({config})
  149. *
  150. * // Configure Wallet
  151. * await api.setWallet({
  152. * address: "TESTWALLET",
  153. * type: "my-algo-wallet",
  154. * connector: MyAlgoWallet
  155. * })
  156. *
  157. * // Placing an Order
  158. * await api.placeOrder({
  159. * type: "buy",
  160. * amount: 100,
  161. * asset: {id: 123456}
  162. * })
  163. *
  164. * @param {APIProperties} props API Constructor Properties
  165. * @throws {Errors.ValidationError}
  166. * @constructor
  167. */
  168. function AlgodexApi(props = {}) {
  169. const {config, asset, wallet} = props;
  170. this.emit = ee.emit;
  171. this.on = ee.on;
  172. this.type = 'API';
  173. const validate = ajv.getSchema('APIProperties');
  174. // Validate Parameters
  175. if (!validate({type: this.type, wallet, asset, config})) {
  176. // Failed to Construct Algodex()
  177. throw new ValidationError(validate.errors);
  178. }
  179. // Initialization Flag
  180. this.isInitialized = false;
  181. this.addresses = [];
  182. // Initialize the instance, skip validation
  183. const options = {validate: false};
  184. if (typeof config !== 'undefined') this.setConfig(config, options);
  185. if (typeof wallet !== 'undefined') this.setWallet(wallet, options);
  186. if (typeof asset !== 'undefined') this.setAsset(asset, options);
  187. }
  188. // Prototypes
  189. AlgodexApi.prototype = {
  190. /**
  191. * getAppId
  192. *
  193. * Fetches the application ID for the current order
  194. *
  195. * @param {Order} order The User Order
  196. * @return {number}
  197. */
  198. async getAppId(order) {
  199. const result = await this.algod.versionsCheck().do();
  200. const isTestnet = result['genesis_id'].includes('testnet');
  201. const isBuyOrder = order.type === 'buy';
  202. let appId;
  203. if (isTestnet && isBuyOrder) {
  204. appId = constants.TEST_ALGO_ORDERBOOK_APPID;
  205. } else if (isTestnet) {
  206. appId = constants.TEST_ASA_ORDERBOOK_APPID;
  207. }
  208. if (!isTestnet && isBuyOrder) {
  209. appId = constants.ALGO_ORDERBOOK_APPID;
  210. } else if (!isTestnet) {
  211. appId = constants.ASA_ORDERBOOK_APPID;
  212. }
  213. return appId;
  214. },
  215. /**
  216. * ## [⚙ Set Config](#setConfig)
  217. *
  218. * Override the current configuration. This is a manditory operation for the
  219. * use of {@link AlgodexApi}. It is run when a {@link Config} is passed to the
  220. * constructor of {@link AlgodexApi} or when a consumer updates an instance of
  221. * the {@link AlgodexApi}
  222. *
  223. * @example
  224. * let algodex = new Algodex({wallet, asset, config})
  225. * algodex.setConfig(newConfig)
  226. *
  227. * @todo Add Application IDs
  228. * @method
  229. * @param {Config} config Configuration Object
  230. * @param {SetterOptions} [options] Options for setting
  231. * @throws ValidationError
  232. * @fires AlgodexApi#InitEvent
  233. */
  234. setConfig(config, options = {throws: true, validate: true}) {
  235. const err = _getValidationError(config, 'Config', options);
  236. if (err) throw err;
  237. // TODO: Get Params
  238. // this.params = await algosdk.getParams()
  239. if (!_.isEqual(config, this.config)) {
  240. this.isInitialized = false;
  241. // TODO: add params
  242. // Set instance
  243. /**
  244. * @type {Algodv2}
  245. */
  246. this.algod =
  247. config.algod instanceof algosdk.Algodv2 ?
  248. config.algod :
  249. config.algod.uri.endsWith('ps2') ?
  250. new algosdk.Algodv2(
  251. config.algod.token,
  252. config.algod.uri,
  253. config.algod.port || '',
  254. {
  255. 'x-api-key': config.algod.token, // For Purestake
  256. },
  257. ) :
  258. new algosdk.Algodv2(
  259. config.algod.token,
  260. config.algod.uri,
  261. config.algod.port || '',
  262. )
  263. ;
  264. this.indexer =
  265. config.indexer instanceof algosdk.Indexer ?
  266. config.indexer :
  267. config.indexer.uri.endsWith('idx2') ?
  268. new algosdk.Indexer(
  269. config.indexer.token,
  270. config.indexer.uri,
  271. config.indexer.port || '',
  272. {
  273. 'x-api-key': config.indexer.token, // For Purestake
  274. },
  275. ) :
  276. new algosdk.Indexer(
  277. config.indexer.token,
  278. config.indexer.uri,
  279. config.indexer.port || '',
  280. );
  281. // this.dexd = new AlgodexClient(config.dexd.uri);
  282. this.http = {
  283. explorer: new ExplorerClient(config.explorer.uri),
  284. dexd: new AlgodexClient(config.dexd.uri),
  285. indexer: new IndexerClient(
  286. config.indexer instanceof algosdk.Indexer ?
  287. this.indexer.c.bc.baseURL.origin :
  288. config.indexer.uri,
  289. false,
  290. config,
  291. this.indexer,
  292. ),
  293. };
  294. this.config = config;
  295. /**
  296. * ### ✔ isInitialized
  297. * Flag for having a valid configuration
  298. * @type {boolean}
  299. */
  300. this.isInitialized = true;
  301. this.emit('initialized', this.isInitialized);
  302. }
  303. },
  304. /**
  305. * ## [⚙ Set Asset](#setAsset)
  306. *
  307. * Changes the current asset. This method is also run when an {@link Asset} is
  308. * passed to the constructor of {@link AlgodexApi}.
  309. *
  310. * @example
  311. * // Assign during construction
  312. * const api = new AlgodexAPI({config, asset: {id: 123456}})
  313. *
  314. * @example
  315. * // Dynamically configure Asset
  316. * api.setAsset({
  317. * id: 123456
  318. * })
  319. *
  320. * @param {Asset} asset Algorand Asset
  321. * @param {SetterOptions} [options] Options for setting
  322. * @throws ValidationError
  323. * @fires AlgodexApi#AssetEvent
  324. */
  325. setAsset(asset, options = {validate: true}) {
  326. const err = _getSetterError(
  327. asset,
  328. {
  329. key: 'Asset',
  330. initialized: this.isInitialized,
  331. options,
  332. },
  333. );
  334. if (err) throw err;
  335. // Set the asset
  336. Object.freeze(asset);
  337. /**
  338. * ### ⚙ asset
  339. *
  340. * Current asset. Use {@link AlgodexApi#setAsset} to update
  341. *
  342. * @type {Asset}
  343. */
  344. this.asset = asset;
  345. this.emit('asset-change', this.asset);
  346. },
  347. /**
  348. * ## [⚙ Set Wallet](#setWallet)
  349. *
  350. * Configure the current wallet.
  351. *
  352. * @param {Wallet} _wallet
  353. * @param {SetterOptions} [options] Options for setting
  354. * @throws ValidationError
  355. * @fires AlgodexApi#WalletEvent
  356. */
  357. setWallet(_wallet, options = {validate: true, merge: false}) {
  358. const wallet = options.merge ? {
  359. ...this.wallet,
  360. ..._wallet,
  361. }: _wallet;
  362. if (_.isUndefined(wallet)) {
  363. throw new TypeError('Must have valid wallet');
  364. }
  365. if (_hasValidateOption(options)) {
  366. const validate = ajv.getSchema('Wallet');
  367. // Validate basic type errors
  368. if (!validate(wallet)) {
  369. const err = new ValidationError(validate.errors);
  370. this.emit('error', err);
  371. throw err;
  372. }
  373. }
  374. if (wallet.type === 'sdk' &&
  375. typeof wallet.mnemonic !== 'undefined' &&
  376. typeof wallet.connector.sk === 'undefined'
  377. ) {
  378. wallet.connector.sk = algosdk.mnemonicToSecretKey(wallet.mnemonic).sk;
  379. wallet.connector.connected = true;
  380. }
  381. // Object.freeze(wallet);
  382. // TODO: Get Account Info
  383. // this.wallet do update = await getAccountInfo()
  384. Object.freeze(wallet);
  385. /**
  386. * ### 👛 wallet
  387. *
  388. * Current wallet. Use {@link AlgodexApi#setWallet} to update
  389. * @type {Wallet}
  390. */
  391. this.wallet = wallet;
  392. this.emit('wallet', {type: 'change', wallet});
  393. },
  394. /**
  395. *
  396. * @param {Order} order Order to check
  397. * @param {Array<Order>} [orderbook] The Orderbook
  398. * @return {Promise<boolean>}
  399. */
  400. async getIsExistingEscrow(order, orderbook) {
  401. // Fetch the orderbook when it's not passed in
  402. const res = typeof orderbook === 'undefined' ?
  403. await this.http.dexd.fetchOrders('wallet', order.address):
  404. orderbook;
  405. // Filter orders by current order
  406. return res.filter((o)=>{
  407. // Check for order types
  408. return o.type === order.type &&
  409. // If the creator is the orders address
  410. o.contract.creator === order.address &&
  411. // If the entries match
  412. (order.contract.entry.slice(59) === o.contract.entry || order.contract.entry === o.contract.entry);
  413. }).length > 0;
  414. },
  415. /**
  416. * ## [💱 Place Order](#placeOrder)
  417. *
  418. * Execute an Order in Algodex. See {@link Order} for details of what
  419. * order types are supported
  420. *
  421. * @todo Add Order Logic
  422. * @param {Order} _order
  423. * @param {Object} [options]
  424. * @param {Wallet} [options.wallet]
  425. * @param {Array} [options.orderbook]
  426. * @throws Errors.ValidationError
  427. * @fires AlgodexApi#OrderEvent
  428. */
  429. async placeOrder( _order, {wallet: _wallet, orderbook}={}) {
  430. // Massage Wallet
  431. let wallet = typeof _wallet !== 'undefined' ? _wallet : this.wallet;
  432. if (typeof wallet === 'undefined') {
  433. throw new Error('No wallet found!');
  434. }
  435. if (
  436. typeof wallet.connector === 'undefined' ||
  437. !wallet.connector.connected
  438. ) {
  439. throw new Error('Must connect wallet!');
  440. }
  441. if (typeof wallet?.assets === 'undefined') {
  442. wallet = {
  443. ...wallet,
  444. ...await this.http.indexer.fetchAccountInfo(wallet),
  445. };
  446. }
  447. // Massage Order
  448. const order = typeof _order !== 'undefined' ? _order : this.order;
  449. // TODO: move to introspection method
  450. if (
  451. typeof order?.asset === 'undefined' ||
  452. typeof order?.asset?.id === 'undefined' ||
  453. typeof order?.asset?.decimals === 'undefined'
  454. ) {
  455. throw new TypeError('Invalid Asset');
  456. }
  457. // Fetch orders
  458. if (typeof orderbook === 'undefined') {
  459. const res = await this.http.dexd.fetchAssetOrders(order.asset.id);
  460. // TODO add clean api endpoint
  461. orderbook = this.http.dexd.mapToAllEscrowOrders({
  462. buy: res.buyASAOrdersInEscrow,
  463. sell: res.sellASAOrdersInEscrow,
  464. });
  465. }
  466. let resOrders;
  467. const execute = composePromise(
  468. ()=>resOrders,
  469. (txns)=>_sendTransactions(this.algod, txns),
  470. (orders)=>{
  471. resOrders = orders;
  472. return wallet.connector.sign(orders, wallet?.connector?.sk);
  473. },
  474. (o)=>structure(this, o),
  475. );
  476. return await execute({
  477. ...order,
  478. total: typeof order.total === 'undefined' ? order.amount * order.price : order.total,
  479. version: typeof order.version === 'undefined' ? constants.ESCROW_CONTRACT_VERSION : order.version,
  480. appId: typeof order.appId === 'undefined' ? await this.getAppId(order) : order.appId,
  481. client: this.algod,
  482. indexer: this.indexer,
  483. asset: {
  484. ...order.asset,
  485. orderbook,
  486. },
  487. wallet: {
  488. ...wallet,
  489. orderbook: orderbook.filter(
  490. ({orderCreatorAddr})=>orderCreatorAddr===wallet.address,
  491. ),
  492. },
  493. });
  494. },
  495. /**
  496. * Close An Existing Order
  497. * @param {Order} order
  498. * @return {Promise<function(*): *>}
  499. */
  500. async closeOrder(order) {
  501. let resOrders;
  502. const execute = composePromise(
  503. () => resOrders,
  504. (txns) => _sendTransactions(this.algod, txns),
  505. (orders) => {
  506. resOrders = orders;
  507. return order.wallet.connector.sign(orders, order.wallet?.connector?.sk);
  508. },
  509. (o) => structure(this, o),
  510. );
  511. let _order = {
  512. ...order,
  513. version: typeof order.version === 'undefined' ? constants.ESCROW_CONTRACT_VERSION : order.version,
  514. appId: typeof order.appId === 'undefined' ? await this.getAppId(order) : order.appId,
  515. client: typeof order.algod !== 'undefined' ? order.algod : this.algod,
  516. indexer: typeof order.indexer !== 'undefined' ? order.indexer : this.indexer,
  517. execution: 'close',
  518. };
  519. if (!(_order?.contract?.lsig instanceof algosdk.LogicSigAccount)) {
  520. console.log('doesnt have lsig');
  521. _order = await compile(_order);
  522. }
  523. return await execute(_order);
  524. },
  525. };
  526. module.exports = AlgodexApi;
  527. if (process.env.NODE_ENV === 'test') {
  528. module.exports._hasValidateOption = _hasValidateOption;
  529. module.exports._getSetterError = _getSetterError;
  530. module.exports._getValidationError = _getValidationError;
  531. module.exports._filterExistingWallets = _filterExistingWallets;
  532. }
  533. JAVASCRIPT
    Copied!