lib/AlgodexApi.js

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