From d6992ab6eeecaa134f39c923ad607717d8d3df1a Mon Sep 17 00:00:00 2001 From: mike sizz Date: Thu, 25 Mar 2021 20:41:42 -0700 Subject: [PATCH 01/13] Move route handlers to their own file. Add prettier support. Add nodemon support for local development. --- .prettierrc | 4 + conseilUtil.js | 308 --------------- index.js | 345 ---------------- package.json | 8 +- src/index.js | 18 + config.js => src/lib/config.js | 12 +- src/lib/conseil.js | 636 ++++++++++++++++++++++++++++++ src/lib/router/index.js | 22 ++ src/lib/router/readFeed.js | 24 ++ src/lib/router/readHdaoFeed.js | 25 ++ src/lib/router/readObjkt.js | 20 + src/lib/router/readRandomFeed.js | 23 ++ src/lib/router/readTezosLedger.js | 41 ++ src/lib/utils.js | 65 +++ 14 files changed, 891 insertions(+), 660 deletions(-) create mode 100644 .prettierrc delete mode 100644 conseilUtil.js delete mode 100644 index.js create mode 100644 src/index.js rename config.js => src/lib/config.js (85%) create mode 100644 src/lib/conseil.js create mode 100644 src/lib/router/index.js create mode 100644 src/lib/router/readFeed.js create mode 100644 src/lib/router/readHdaoFeed.js create mode 100644 src/lib/router/readObjkt.js create mode 100644 src/lib/router/readRandomFeed.js create mode 100644 src/lib/router/readTezosLedger.js create mode 100644 src/lib/utils.js diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..c3481a7 --- /dev/null +++ b/.prettierrc @@ -0,0 +1,4 @@ +{ + "singleQuote": true, + "semi": false +} \ No newline at end of file diff --git a/conseilUtil.js b/conseilUtil.js deleted file mode 100644 index ae696f9..0000000 --- a/conseilUtil.js +++ /dev/null @@ -1,308 +0,0 @@ -const conseiljs = require('conseiljs') -const fetch = require('node-fetch') -const log = require('loglevel') - -const logger = log.getLogger('conseiljs') -logger.setLevel('error', false) -conseiljs.registerLogger(logger) -conseiljs.registerFetch(fetch) -const conseilServer = 'https://conseil-prod.cryptonomic-infra.tech' -const conseilApiKey = 'aa73fa8a-8626-4f43-a605-ff63130f37b1' // signup at nautilus.cloud -const tezosNode = '' - -const mainnet = require('./config').networkConfig - - -const hDAOFeed = async () => { - - let hDAOQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - hDAOQuery = conseiljs.ConseilQueryBuilder.addFields(hDAOQuery, 'key', 'value'); - hDAOQuery = conseiljs.ConseilQueryBuilder.addPredicate(hDAOQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.curationsPtr]) - hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 10_000) - - let hDAOResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', hDAOQuery); - return hDAOResult.map(e => { - return { - token_id : parseInt(e.key), - hDAO_balance : parseInt((e.value).split(' ')[1]) - } - }) -} -/** - * Returns a list of nft token ids and amounts that a given address owns. - * - * @param {string} address - * @returns - */ -const getCollectionForAddress = async (address) => { - let collectionQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - collectionQuery = conseiljs.ConseilQueryBuilder.addFields(collectionQuery, 'key', 'value'); - collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate(collectionQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftLedger]) - collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate(collectionQuery, 'key', conseiljs.ConseilOperator.STARTSWITH, [ - `Pair 0x${conseiljs.TezosMessageUtils.writeAddress(address)}`, - ]) - collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate(collectionQuery, 'value', conseiljs.ConseilOperator.EQ, [0], true) - collectionQuery = conseiljs.ConseilQueryBuilder.setLimit(collectionQuery, 10_000) - - const collectionResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', collectionQuery); - let collection = collectionResult.map((i) => { - return { piece: i.key.toString().replace(/.* ([0-9]{1,}$)/, '$1'), amount: Number(i.value) } - }) - - const queryChunks = chunkArray(collection.map(i => i.piece), 20) // NOTE: consider increasing this number somewhat - const makeObjectQuery = (keys) => { - let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addFields(mintedObjectsQuery, 'key_hash', 'value'); - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintedObjectsQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftMetadataMap]) - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintedObjectsQuery, 'key', (keys.length > 1 ? conseiljs.ConseilOperator.IN : conseiljs.ConseilOperator.EQ), keys) - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.setLimit(mintedObjectsQuery, keys.length) - - return mintedObjectsQuery - } - - const objectQueries = queryChunks.map(c => makeObjectQuery(c)) - const objectIpfsMap = {} - await Promise.all(objectQueries.map(async (q) => await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', q) - .then(result => result.map(row => { - const objectId = row['value'].toString().replace(/^Pair ([0-9]{1,}) .*/, '$1') - const objectUrl = row['value'].toString().replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') - const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7); - - objectIpfsMap[objectId] = ipfsHash - })))) - - collection = collection.map(i => { return { - ipfsHash: objectIpfsMap[i.piece.toString()], - ...i - }}) - - return collection.sort((a, b) => parseInt(b.piece) - parseInt(a.piece)) // sort descending by id – most-recently minted art first -} - -const gethDaoBalanceForAddress = async (address) => { - let hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addFields(hDaoBalanceQuery, 'value'); - hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate(hDaoBalanceQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.daoLedger]) - hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate(hDaoBalanceQuery, 'key', conseiljs.ConseilOperator.EQ, [ - `Pair 0x${conseiljs.TezosMessageUtils.writeAddress(address)} 0` - ]) - hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate(hDaoBalanceQuery, 'value', conseiljs.ConseilOperator.EQ, [0], true) - hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit(hDaoBalanceQuery, 1) - - let balance = 0 - - try { - const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', hDaoBalanceQuery); - balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it - } catch (error) { - console.log(`gethDaoBalanceForAddress failed for ${JSON.stringify(hDaoBalanceQuery)} with ${error}`) - } - - return balance -} - -/** - * Queries Conseil in two steps to get all the objects minted by a specific address. Step 1 is to query for all 'mint_OBJKT' operations performed by the account to get the list of operation group hashes. Then that list is partitioned into chunks and another query (or set of queries) is run to get big_map values. These values are then parsed into an array of 3-tuples containing the hashed big_map key that can be used to query a Tezos node directly, the nft token id and the ipfs item hash. - * - * @param {string} address - * @returns - */ -const getArtisticOutputForAddress = async (address) => { - let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields(mintOperationQuery, 'operation_group_hash'); - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'kind', conseiljs.ConseilOperator.EQ, ['transaction']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'timestamp', conseiljs.ConseilOperator.AFTER, [1612240919000]) // 2021 Feb 1 - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'status', conseiljs.ConseilOperator.EQ, ['applied']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'destination', conseiljs.ConseilOperator.EQ, [mainnet.protocol]) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'parameters_entrypoints', conseiljs.ConseilOperator.EQ, ['mint_OBJKT']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'source', conseiljs.ConseilOperator.EQ, [address]) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering(mintOperationQuery, 'block_level', conseiljs.ConseilSortDirection.DESC) - mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit(mintOperationQuery, 10_000) // TODO: this is hardwired and will not work for highly productive artists - - const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( - { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, - 'mainnet', - 'operations', - mintOperationQuery); - - const operationGroupIds = mintOperationResult.map(r => r['operation_group_hash']) - const queryChunks = chunkArray(operationGroupIds, 30) - - const makeObjectQuery = (opIds) => { - let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addFields(mintedObjectsQuery, 'key_hash', 'value'); - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintedObjectsQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftMetadataMap]) - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintedObjectsQuery, 'operation_group_id', (opIds.length > 1 ? conseiljs.ConseilOperator.IN : conseiljs.ConseilOperator.EQ), opIds) - mintedObjectsQuery = conseiljs.ConseilQueryBuilder.setLimit(mintedObjectsQuery, opIds.length) - - return mintedObjectsQuery - } - - const objectQueries = queryChunks.map(c => makeObjectQuery(c)) - - const objectInfo = await Promise.all(objectQueries.map(async (q) => await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', q) - .then(result => result.map(row => { - const objectId = row['value'].toString().replace(/^Pair ([0-9]{1,}) .*/, '$1') - const objectUrl = row['value'].toString().replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') - const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7); - - return { key: row['key_hash'], objectId, ipfsHash } - })))) - - return objectInfo.flat(1).sort((a, b) => parseInt(b.objectId) - parseInt(a.objectId)) -} - -const getArtisticUniverse = async (max_time) => { - let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields(mintOperationQuery, 'operation_group_hash'); - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'kind', conseiljs.ConseilOperator.EQ, ['transaction']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'timestamp', conseiljs.ConseilOperator.AFTER, [1612240919000]) // 2021 Feb 1 - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'status', conseiljs.ConseilOperator.EQ, ['applied']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'destination', conseiljs.ConseilOperator.EQ, [mainnet.protocol]) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'parameters_entrypoints', conseiljs.ConseilOperator.EQ, ['mint_OBJKT']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit(mintOperationQuery, 14_000) - - const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( - { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, - 'mainnet', - 'operations', - mintOperationQuery); - - const operationGroupIds = mintOperationResult.map(r => r['operation_group_hash']) - - let royaltiesQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - royaltiesQuery = conseiljs.ConseilQueryBuilder.addFields(royaltiesQuery, 'key', 'value'); - royaltiesQuery = conseiljs.ConseilQueryBuilder.addPredicate(royaltiesQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftRoyaltiesMap]) - royaltiesQuery = conseiljs.ConseilQueryBuilder.setLimit(royaltiesQuery, 10_000) - const royaltiesResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', royaltiesQuery) - let artistMap = {} - royaltiesResult.forEach(row => { - artistMap[row['key']] = conseiljs.TezosMessageUtils.readAddress(row['value'].toString().replace(/^Pair 0x([0-9a-z]{1,}) [0-9]+/, '$1')) - }) - - let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - swapsQuery = conseiljs.ConseilQueryBuilder.addFields(swapsQuery, 'key', 'value'); - swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate(swapsQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftSwapMap]) - swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 10_000) // NOTE, limited to 10_000 - - const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', swapsQuery) - - const swapStoragePattern = new RegExp(`Pair [(]Pair 0x([0-9a-z]{44}) ([0-9]+)[)] [(]Pair ([0-9]+) ([0-9]+)[)]`); - let swapMap = {} - - swapsResult.forEach(row => { - swap_id = row['key'] - const match = swapStoragePattern.exec(row['value']) - if (!match) { return; } - const amount = match[2] - const objkt_id = match[3] - const xtz_per_objkt = match[4] - - if (swapMap[row['key']]) { - swapMap[row['key']].push({ }) - } else { - swapMap[row['key']] = [{ swap_id, objkt_id, amount, xtz_per_objkt }] - } - }) - - const queryChunks = chunkArray(operationGroupIds, 50) - - const makeObjectQuery = (opIds) => { - let objectsQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - objectsQuery = conseiljs.ConseilQueryBuilder.addFields(objectsQuery, 'key', 'value', 'operation_group_id'); - objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate(objectsQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftMetadataMap]) - objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate(objectsQuery, 'operation_group_id', (opIds.length > 1 ? conseiljs.ConseilOperator.IN : conseiljs.ConseilOperator.EQ), opIds) - objectsQuery = conseiljs.ConseilQueryBuilder.setLimit(objectsQuery, opIds.length) - - return objectsQuery - } - - const objectQueries = queryChunks.map(c => makeObjectQuery(c)) - - let universe = [] - await Promise.all( - objectQueries.map(async (q) => { - const r = [] - try { - r = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', q) - .then(result => result.map(row => { - const objectId = row['value'].toString().replace(/^Pair ([0-9]{1,}) .*/, '$1') - const objectUrl = row['value'].toString().replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') - const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7); - - universe.push({ objectId, ipfsHash, minter: artistMap[objectId], swaps: swapMap[objectId] !== undefined ? swapMap[objectId] : []}) - })) // NOTE: it's a work in progress, this will drop failed requests and return a smaller set than expected - } finally { - return r - }})) - - return universe -} - -/** - * Returns object ipfs hash and swaps if any - * - * @param {number} objectId - * @returns - */ - -const getObjectById = async (objectId) => { - let objectQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - objectQuery = conseiljs.ConseilQueryBuilder.addFields(objectQuery, 'value'); - objectQuery = conseiljs.ConseilQueryBuilder.addPredicate(objectQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftMetadataMap]) - objectQuery = conseiljs.ConseilQueryBuilder.addPredicate(objectQuery, 'key', conseiljs.ConseilOperator.EQ, [objectId]) - objectQuery = conseiljs.ConseilQueryBuilder.setLimit(objectQuery, 1) - - const objectResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', objectQuery) - - const objectUrl = objectResult[0]['value'].toString().replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') - const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7); - - let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - swapsQuery = conseiljs.ConseilQueryBuilder.addFields(swapsQuery, 'key', 'value'); - swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate(swapsQuery, 'big_map_id', conseiljs.ConseilOperator.EQ, [mainnet.nftSwapMap]) - swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate(swapsQuery, 'value', conseiljs.ConseilOperator.LIKE, [`) (Pair ${objectId} `]) - swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 1000) // NOTE, limited to 1000 swaps for a given object - - const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', swapsQuery) - const swapStoragePattern = new RegExp(`Pair [(]Pair 0x([0-9a-z]{44}) ([0-9]+)[)] [(]Pair ${objectId} ([0-9]+)[)]`); - - let swaps = [] - try { - swapsResult.map(row => { - const match = swapStoragePattern.exec(row['value']) - - swaps.push({ - swap_id: row['key'], - issuer: conseiljs.TezosMessageUtils.readAddress(match[1]), - objkt_amount: match[2], - xtz_per_objkt: match[3] - }) - }) - } catch (error) { - console.log(`failed to process swaps for ${objectId} with ${error}`) - } - - return { objectId, ipfsHash, swaps } -} - -const chunkArray = (arr, len) => { // TODO: move to util.js - let chunks = [], - i = 0, - n = arr.length; - - while (i < n) { - chunks.push(arr.slice(i, i += len)); - } - - return chunks; -} - -module.exports = { - getCollectionForAddress, - gethDaoBalanceForAddress, - getArtisticOutputForAddress, - getObjectById, - getArtisticUniverse, - hDAOFeed -} diff --git a/index.js b/index.js deleted file mode 100644 index d58ad9c..0000000 --- a/index.js +++ /dev/null @@ -1,345 +0,0 @@ -const serverless = require('serverless-http') -const axios = require('axios') -const express = require('express') -const cors = require('cors') -const _ = require('lodash') -const conseilUtil = require('./conseilUtil') -const { random } = require('lodash') -require('dotenv').config() - -const reducer = (accumulator, currentValue) => parseInt(accumulator) + parseInt(currentValue) - -const getIpfsHash = async (ipfsHash) => { - - return await axios.get('https://cloudflare-ipfs.com/ipfs/' + ipfsHash).then(res => res.data) - /* const nftDetailJson = await nftDetails.json(); - - const nftName = nftDetailJson.name; - const nftDescription = nftDetailJson.description; - const nftCreators = nftDetailJson.creators.join(', '); - const nftArtifact = `https://cloudflare-ipfs.com/ipfs/${nftDetailJson.formats[0].uri.toString().slice(7)}`; - const nftArtifactType = nftDetailJson.formats[0].mimeType.toString(); - - return { name: nftName, description: nftDescription, creators: nftCreators, artifactUrl: nftArtifact, artifactType: nftArtifactType }; */ -} -const getObjkts = async () => { - return await axios.get(`https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens`).then(res => res.data) -} - -const getTokenHolders = async (tk_id) => { - return await axios.get('https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + tk_id).then(res => res.data) -} - -const getTokenHoldersArr = async (arr) => { - - return await arr.map(async e => await axios.get('https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + e).then(res => res.data)) - /* await axios.get('https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + arr[0]).then(res => console.log(res.data)) - *//* var result = arr.map(async e => { -return await axios.get('https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + e).then(res => res.data) -}) - -console.log(result) */ -} - -const owners = async (obj) => { - var owners = await axios.get('https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + obj.token_id).then(res => res.data) - var values_arr = (_.values(owners)) - obj.total_amount = (values_arr.map(e => parseInt(e))).length > 0 ? values_arr.filter(e => parseInt(e) > 0).reduce(reducer) : 0 - obj.owners = owners - console.log(obj) - //obj.total_amount = (values_arr.map(e => parseInt(e))).reduce(reducer) - return obj -} - -const totalAmountIntegral = async (obj) => { - var owners = await axios.get('https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + obj.token_id).then(res => res.data) - console.log(owners) - var values_arr = (_.values(owners)) - obj.total_amount = (values_arr.map(e => parseInt(e))).length > 0 ? (values_arr.filter(e => parseInt(e))) : 0 - - obj.owners = owners - return obj -} - -const objktAmount = async (arr) => { - return await arr.map(e => totalAmountIntegral(e)) - //console.log(await getTokenHoldersArr(arr.map(e => _.values(e.token_id)[0]))) -} - -const objktOwners = async (arr) => { - return await arr.map(e => totalAmountIntegral(e)) -} - - -const getObjktLedger = async () => await axios.get('https://better-call.dev/v1/bigmap/mainnet/511/keys?size=6500').then(res => res.data.map(e => ({ amount: parseInt(e.data.value.value), tz: e.data.key.children[0].value, tk_id: parseInt(e.data.key.children[1].value) }))) -const gethDAOLedger = async (counter) => await axios.get('https://api.better-call.dev/v1/bigmap/mainnet/519/keys?size=10&offset=' + counter * 10).then(res => res.data.map(e => { - return { token_id: parseInt(e.data.key.value), hDAO_balance: parseInt(e.data.value.children[0].value) } -})) - -//gethDAOLedger() - - -const getSwaps = async () => { - return await axios.get(`https://api.better-call.dev/v1/bigmap/mainnet/523/keys?size=6000`).then(res => { - return (res.data).map(e => { - var obj = {} - - obj['swap_id'] = e.data.key.value - e.data.value != null ? e.data.value.children.map(e => obj[e.name] = e.value) : null - return obj - }) - }) -} - -const merge = (a, b) => { - a.forEach((e1) => { - b.forEach((e2) => { - if (e1.token_id === e2.tk_id) { - _.assign(e1, e2) - } - }) - }) - return a -} - -const mergeSwaps = (arr, swaps) => { - arr.forEach((e1) => { - - e1.swaps = [] - - swaps.forEach((e2) => { - if (parseInt(e1.token_id) === parseInt(e2.objkt_id)) { - e1.swaps.push(e2) - } - }) - }) - return arr -} - -const desc = arr => _.sortBy(arr, e => parseInt(e.objectId)).reverse() -const offset = (arr, set) => arr.slice(set * 30, set * 30 + 30) - -const filter = (data, tz) => _.filter(data, (e) => { - if (e.token_info != undefined) { - return e.token_info.creators[0] === tz - } -}) - -const filterTz = (data, tz) => _.filter(data, { tz: tz }) - -const test = async () => console.log(desc(await getObjkts())) - -const customFloor = function (value, roundTo) { - return Math.floor(value / roundTo) * roundTo; -} - -const ONE_MINUTE_MILLIS = 60 * 1000 - -const randomFeed = async (counter, res) => { - var feed = await conseilUtil.getArtisticUniverse(0) - feed = offset(_.shuffle(feed), counter) - feed = await feed.map(async e => { - e.token_info = await getIpfsHash(e.ipfsHash) - e.token_id = parseInt(e.objectId) - console.log(e) - return e - }) - var promise = Promise.all(feed.map(e => e)) - promise.then(async (results) => { - var aux_arr = results.map(e => e) - //res.set('Cache-Control', `public, max-age=${cache_time}`) - console.log(aux_arr) - res.json({ result: aux_arr }) - }) -} - -const getFeed = async (counter, res) => { - - /* const now_time = Date.now() - const immutable = (typeof max_time !== 'undefined') && (max_time < now_time) - max_time = (typeof max_time !== 'undefined') ? max_time : customFloor(now_time, ONE_MINUTE_MILLIS) - */ - var arr = await conseilUtil.getArtisticUniverse(0) - - var feed = offset(desc(arr), counter) - console.log(feed) - feed = await feed.map(async e => { - e.token_info = await getIpfsHash(e.ipfsHash) - e.token_id = parseInt(e.objectId) - console.log(e) - return e - }) - //console.log(feed) - /* var cache_time - if (immutable) { - cache_time = 60 * 10 - } - else { - cache_time = (int)(((max_time + ONE_MINUTE_MILLIS) - now_time) / 1000) - } */ - var promise = Promise.all(feed.map(e => e)) - promise.then(async (results) => { - var aux_arr = results.map(e => e) - - //res.set('Cache-Control', `public, max-age=${cache_time}`) - - console.log(aux_arr) - res.json({ result: aux_arr }) - }) -} - -const filterObjkts = (arr, id_arr) => _.filter(arr, { token_id: tk.id }) - -const getTzLedger = async (tz, res) => { - /* var ledger = desc(await getObjktLedger()) - var objkts = await getObjkts() - var tzLedger = _.map(filterTz(ledger, tz), (obj) => _.assign(obj, _.find(objkts, { token_id : obj.tk_id }))) - */ - var collection = await conseilUtil.getCollectionForAddress(tz) - var creations = await conseilUtil.getArtisticOutputForAddress(tz) - var hdao = await conseilUtil.gethDaoBalanceForAddress(tz) - - console.log(hdao) - - var arr = [] - console.log([...collection, ...creations]) - var arr = [...collection, ...creations] - - var result = arr.map(async e => { - e.token_info = await getIpfsHash(e.ipfsHash) - - if (e.piece != undefined) { - e.token_id = parseInt(e.piece) - } else { - e.token_id = parseInt(e.objectId) - - } - console.log(e) - return e - }) - - var promise = Promise.all(result.map(e => e)) - promise.then(async results => { - var result = results.map(e => e) - console.log(result) - res.json({ - result: _.uniqBy(result, (e) => { - return e.token_id - }), - hdao: hdao - }) - }) - - //return tzLedger -} - -const getObjktById = async (id, res) => { - var objkt = await conseilUtil.getObjectById(id) - objkt.token_id = objkt.objectId - objkt = await owners(objkt) - objkt.token_info = await getIpfsHash(objkt.ipfsHash) - console.log(objkt) - - return objkt - //res.json({ result : objkt }) - //var objkts = await getObjkts() - //var swaps = await getSwaps() - //res.json({ result : mergeSwaps([objkt], swaps)[0] }) - //console.log(_.filter(mergeSwaps(objkts, swaps), {token_id : id})) - // var arr = await objktOwners(_.filter(mergeSwaps(objkts, swaps), {token_id : id})) - // var promise = Promise.all(arr.map(e => e)) - - /* promise.then((results) => { - var aux_arr = results.map(e => e) - console.log(aux_arr) - res.json({ result : aux_arr }) - }) */ - //https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=842 -} - -const mergehDAO = async (obj) => { - var obj_aux = await getObjktById(obj.token_id) - obj_aux.hDAO_balance = obj.hDAO_balance - return obj_aux -} - -const hDAOFeed = async (counter, res) => { - - var hDAO = await conseilUtil.hDAOFeed() - var set = _.orderBy(hDAO, ['hDAO_balance'], ['desc']) - var objkts = await (offset(set, 0)).map(async e => await mergehDAO(e)) - - var promise = Promise.all(objkts.map(e => e)) - promise.then(results => { - var result = results.map(e => e) - console.log(result) - res.json({ result: result }) - }).catch(e => { - res.status(500).json({ error: 'downstream API failure' }) - }) -} - -//getObjkts() -//testSwaps() -//getFeed(0) -//getTzLedger('tz1UBZUkXpKGhYsP5KtzDNqLLchwF4uHrGjw') -//getObjktById(5965) -//const test2 = async () => console.log(await getObjktLedger()) -//test2() - -const app = express() - -app.use(express.json()) -app.use(cors({ origin: '*' })) - -app.post('/feed', async (req, res) => { - /* - var counter = req.query.counter - var max_time = req.query.hasOwnProperty('time') ? customFloor(req.query.time, ONE_MINUTE_MILLIS) : null - const now_time_qt = customFloor(Date.now(), ONE_MINUTE_MILLIS) - if (max_time != null & max_time > now_time_qt) { - max_time = null - } - */ - await getFeed(req.body.counter, res) -}) - -app.post('/random', async (req, res) => { - await randomFeed(parseInt(req.body.counter), res) -}) - -app.post('/tz', async (req, res) => { - - // list of restricted addresses - var list = await axios.get('https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/w.json').then(res => res.data) - - list.includes(req.body.tz) - ? - res.json({ result: [] }) - : - await getTzLedger(req.body.tz, res) - -}) - -app.post('/objkt', async (req, res) => { - - // list of restricted objkts - var list = await axios.get('https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/o.json').then(res => res.data) - - list.includes(parseInt(req.body.objkt_id)) - ? - res.json({ result: [] }) - : - res.json({ result: await getObjktById(req.body.objkt_id) }) -}) - -app.post('/hdao', async (req, res) => { - await hDAOFeed(parseInt(req.body.counter), res) -}) - -const testhdao = async () => await hDAOFeed(parseInt(0)) -//testhdao() - -app.listen(3001) -//module.exports.handler = serverless(app) - diff --git a/package.json b/package.json index aebde7f..5796fb2 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,9 @@ "description": "", "main": "index.js", "scripts": { - "start": "node index.js" + "start": "node src/index.js", + "start-dev": "NODE_ENV=development ./node_modules/.bin/nodemon src/index.js", + "pretty": "./node_modules/.bin/prettier --loglevel warn --write src" }, "repository": { "type": "git", @@ -31,5 +33,9 @@ "engines": { "node": "12.20.1", "npm": "6.14.10" + }, + "devDependencies": { + "nodemon": "^2.0.7", + "prettier": "^2.2.1" } } diff --git a/src/index.js b/src/index.js new file mode 100644 index 0000000..abea00c --- /dev/null +++ b/src/index.js @@ -0,0 +1,18 @@ +'use strict' + +process.env.NODE_PATH = 'src/lib' + +require('module').Module._initPaths() +require('dotenv').config() + +const cors = require('cors') +const express = require('express') +const router = require('router') + +const app = express() + +app.use(express.json()) +app.use(cors({ origin: '*' })) +app.use(router) + +app.listen(3001) diff --git a/config.js b/src/lib/config.js similarity index 85% rename from config.js rename to src/lib/config.js index 9343184..bfc8da1 100644 --- a/config.js +++ b/src/lib/config.js @@ -1,4 +1,7 @@ -const mainnet = { +'use strict' + +module.exports = { + networkConfig: { network: 'mainnet', nftContract: 'KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton', hDAOToken: 'KT1AFA2mwNUMNd4SsujE1YYp29vd8BZejyKW', @@ -9,9 +12,6 @@ const mainnet = { nftSwapMap: 523, curationsPtr: 519, nftRoyaltiesMap: 522, - daoLedger: 515 -} - -module.exports = { - networkConfig: mainnet + daoLedger: 515, + }, } diff --git a/src/lib/conseil.js b/src/lib/conseil.js new file mode 100644 index 0000000..d6c6d2b --- /dev/null +++ b/src/lib/conseil.js @@ -0,0 +1,636 @@ +const conseiljs = require('conseiljs') +const fetch = require('node-fetch') +const log = require('loglevel') + +const logger = log.getLogger('conseiljs') +logger.setLevel('error', false) +conseiljs.registerLogger(logger) +conseiljs.registerFetch(fetch) +const conseilServer = 'https://conseil-prod.cryptonomic-infra.tech' +const conseilApiKey = 'aa73fa8a-8626-4f43-a605-ff63130f37b1' // signup at nautilus.cloud +const tezosNode = '' + +const { networkConfig: mainnet } = require('config') + +const hDAOFeed = async () => { + let hDAOQuery = conseiljs.ConseilQueryBuilder.blankQuery() + hDAOQuery = conseiljs.ConseilQueryBuilder.addFields(hDAOQuery, 'key', 'value') + hDAOQuery = conseiljs.ConseilQueryBuilder.addPredicate( + hDAOQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.curationsPtr] + ) + hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 10_000) + + let hDAOResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + hDAOQuery + ) + return hDAOResult.map((e) => { + return { + token_id: parseInt(e.key), + hDAO_balance: parseInt(e.value.split(' ')[1]), + } + }) +} +/** + * Returns a list of nft token ids and amounts that a given address owns. + * + * @param {string} address + * @returns + */ +const getCollectionForAddress = async (address) => { + let collectionQuery = conseiljs.ConseilQueryBuilder.blankQuery() + collectionQuery = conseiljs.ConseilQueryBuilder.addFields( + collectionQuery, + 'key', + 'value' + ) + collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate( + collectionQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftLedger] + ) + collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate( + collectionQuery, + 'key', + conseiljs.ConseilOperator.STARTSWITH, + [`Pair 0x${conseiljs.TezosMessageUtils.writeAddress(address)}`] + ) + collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate( + collectionQuery, + 'value', + conseiljs.ConseilOperator.EQ, + [0], + true + ) + collectionQuery = conseiljs.ConseilQueryBuilder.setLimit( + collectionQuery, + 10_000 + ) + + const collectionResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + collectionQuery + ) + let collection = collectionResult.map((i) => { + return { + piece: i.key.toString().replace(/.* ([0-9]{1,}$)/, '$1'), + amount: Number(i.value), + } + }) + + const queryChunks = chunkArray( + collection.map((i) => i.piece), + 20 + ) // NOTE: consider increasing this number somewhat + const makeObjectQuery = (keys) => { + let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery() + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addFields( + mintedObjectsQuery, + 'key_hash', + 'value' + ) + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintedObjectsQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftMetadataMap] + ) + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintedObjectsQuery, + 'key', + keys.length > 1 + ? conseiljs.ConseilOperator.IN + : conseiljs.ConseilOperator.EQ, + keys + ) + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.setLimit( + mintedObjectsQuery, + keys.length + ) + + return mintedObjectsQuery + } + + const objectQueries = queryChunks.map((c) => makeObjectQuery(c)) + const objectIpfsMap = {} + await Promise.all( + objectQueries.map( + async (q) => + await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + q + ).then((result) => + result.map((row) => { + const objectId = row['value'] + .toString() + .replace(/^Pair ([0-9]{1,}) .*/, '$1') + const objectUrl = row['value'] + .toString() + .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') + const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) + + objectIpfsMap[objectId] = ipfsHash + }) + ) + ) + ) + + collection = collection.map((i) => { + return { + ipfsHash: objectIpfsMap[i.piece.toString()], + ...i, + } + }) + + return collection.sort((a, b) => parseInt(b.piece) - parseInt(a.piece)) // sort descending by id – most-recently minted art first +} + +const gethDaoBalanceForAddress = async (address) => { + let hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( + hDaoBalanceQuery, + 'value' + ) + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + hDaoBalanceQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.daoLedger] + ) + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + hDaoBalanceQuery, + 'key', + conseiljs.ConseilOperator.EQ, + [`Pair 0x${conseiljs.TezosMessageUtils.writeAddress(address)} 0`] + ) + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + hDaoBalanceQuery, + 'value', + conseiljs.ConseilOperator.EQ, + [0], + true + ) + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit(hDaoBalanceQuery, 1) + + let balance = 0 + + try { + const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + hDaoBalanceQuery + ) + balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it + } catch (error) { + console.log( + `gethDaoBalanceForAddress failed for ${JSON.stringify( + hDaoBalanceQuery + )} with ${error}` + ) + } + + return balance +} + +/** + * Queries Conseil in two steps to get all the objects minted by a specific address. Step 1 is to query for all 'mint_OBJKT' operations performed by the account to get the list of operation group hashes. Then that list is partitioned into chunks and another query (or set of queries) is run to get big_map values. These values are then parsed into an array of 3-tuples containing the hashed big_map key that can be used to query a Tezos node directly, the nft token id and the ipfs item hash. + * + * @param {string} address + * @returns + */ +const getArtisticOutputForAddress = async (address) => { + let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() + mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( + mintOperationQuery, + 'operation_group_hash' + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'kind', + conseiljs.ConseilOperator.EQ, + ['transaction'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'timestamp', + conseiljs.ConseilOperator.AFTER, + [1612240919000] + ) // 2021 Feb 1 + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'status', + conseiljs.ConseilOperator.EQ, + ['applied'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'destination', + conseiljs.ConseilOperator.EQ, + [mainnet.protocol] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'parameters_entrypoints', + conseiljs.ConseilOperator.EQ, + ['mint_OBJKT'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'source', + conseiljs.ConseilOperator.EQ, + [address] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering( + mintOperationQuery, + 'block_level', + conseiljs.ConseilSortDirection.DESC + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( + mintOperationQuery, + 10_000 + ) // TODO: this is hardwired and will not work for highly productive artists + + const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'operations', + mintOperationQuery + ) + + const operationGroupIds = mintOperationResult.map( + (r) => r['operation_group_hash'] + ) + const queryChunks = chunkArray(operationGroupIds, 30) + + const makeObjectQuery = (opIds) => { + let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery() + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addFields( + mintedObjectsQuery, + 'key_hash', + 'value' + ) + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintedObjectsQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftMetadataMap] + ) + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintedObjectsQuery, + 'operation_group_id', + opIds.length > 1 + ? conseiljs.ConseilOperator.IN + : conseiljs.ConseilOperator.EQ, + opIds + ) + mintedObjectsQuery = conseiljs.ConseilQueryBuilder.setLimit( + mintedObjectsQuery, + opIds.length + ) + + return mintedObjectsQuery + } + + const objectQueries = queryChunks.map((c) => makeObjectQuery(c)) + + const objectInfo = await Promise.all( + objectQueries.map( + async (q) => + await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + q + ).then((result) => + result.map((row) => { + const objectId = row['value'] + .toString() + .replace(/^Pair ([0-9]{1,}) .*/, '$1') + const objectUrl = row['value'] + .toString() + .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') + const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) + + return { key: row['key_hash'], objectId, ipfsHash } + }) + ) + ) + ) + + return objectInfo + .flat(1) + .sort((a, b) => parseInt(b.objectId) - parseInt(a.objectId)) +} + +const getArtisticUniverse = async (max_time) => { + let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() + mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( + mintOperationQuery, + 'operation_group_hash' + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'kind', + conseiljs.ConseilOperator.EQ, + ['transaction'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'timestamp', + conseiljs.ConseilOperator.AFTER, + [1612240919000] + ) // 2021 Feb 1 + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'status', + conseiljs.ConseilOperator.EQ, + ['applied'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'destination', + conseiljs.ConseilOperator.EQ, + [mainnet.protocol] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'parameters_entrypoints', + conseiljs.ConseilOperator.EQ, + ['mint_OBJKT'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( + mintOperationQuery, + 14_000 + ) + + const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'operations', + mintOperationQuery + ) + + const operationGroupIds = mintOperationResult.map( + (r) => r['operation_group_hash'] + ) + + let royaltiesQuery = conseiljs.ConseilQueryBuilder.blankQuery() + royaltiesQuery = conseiljs.ConseilQueryBuilder.addFields( + royaltiesQuery, + 'key', + 'value' + ) + royaltiesQuery = conseiljs.ConseilQueryBuilder.addPredicate( + royaltiesQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftRoyaltiesMap] + ) + royaltiesQuery = conseiljs.ConseilQueryBuilder.setLimit( + royaltiesQuery, + 10_000 + ) + const royaltiesResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + royaltiesQuery + ) + let artistMap = {} + royaltiesResult.forEach((row) => { + artistMap[row['key']] = conseiljs.TezosMessageUtils.readAddress( + row['value'].toString().replace(/^Pair 0x([0-9a-z]{1,}) [0-9]+/, '$1') + ) + }) + + let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery() + swapsQuery = conseiljs.ConseilQueryBuilder.addFields( + swapsQuery, + 'key', + 'value' + ) + swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + swapsQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftSwapMap] + ) + swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 10_000) // NOTE, limited to 10_000 + + const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + swapsQuery + ) + + const swapStoragePattern = new RegExp( + `Pair [(]Pair 0x([0-9a-z]{44}) ([0-9]+)[)] [(]Pair ([0-9]+) ([0-9]+)[)]` + ) + let swapMap = {} + + swapsResult.forEach((row) => { + swap_id = row['key'] + const match = swapStoragePattern.exec(row['value']) + if (!match) { + return + } + const amount = match[2] + const objkt_id = match[3] + const xtz_per_objkt = match[4] + + if (swapMap[row['key']]) { + swapMap[row['key']].push({}) + } else { + swapMap[row['key']] = [{ swap_id, objkt_id, amount, xtz_per_objkt }] + } + }) + + const queryChunks = chunkArray(operationGroupIds, 50) + + const makeObjectQuery = (opIds) => { + let objectsQuery = conseiljs.ConseilQueryBuilder.blankQuery() + objectsQuery = conseiljs.ConseilQueryBuilder.addFields( + objectsQuery, + 'key', + 'value', + 'operation_group_id' + ) + objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + objectsQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftMetadataMap] + ) + objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + objectsQuery, + 'operation_group_id', + opIds.length > 1 + ? conseiljs.ConseilOperator.IN + : conseiljs.ConseilOperator.EQ, + opIds + ) + objectsQuery = conseiljs.ConseilQueryBuilder.setLimit( + objectsQuery, + opIds.length + ) + + return objectsQuery + } + + const objectQueries = queryChunks.map((c) => makeObjectQuery(c)) + + let universe = [] + await Promise.all( + objectQueries.map(async (q) => { + const r = [] + try { + r = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + q + ).then((result) => + result.map((row) => { + const objectId = row['value'] + .toString() + .replace(/^Pair ([0-9]{1,}) .*/, '$1') + const objectUrl = row['value'] + .toString() + .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') + const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) + + universe.push({ + objectId, + ipfsHash, + minter: artistMap[objectId], + swaps: swapMap[objectId] !== undefined ? swapMap[objectId] : [], + }) + }) + ) // NOTE: it's a work in progress, this will drop failed requests and return a smaller set than expected + } finally { + return r + } + }) + ) + + return universe +} + +/** + * Returns object ipfs hash and swaps if any + * + * @param {number} objectId + * @returns + */ + +const getObjectById = async (objectId) => { + let objectQuery = conseiljs.ConseilQueryBuilder.blankQuery() + objectQuery = conseiljs.ConseilQueryBuilder.addFields(objectQuery, 'value') + objectQuery = conseiljs.ConseilQueryBuilder.addPredicate( + objectQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftMetadataMap] + ) + objectQuery = conseiljs.ConseilQueryBuilder.addPredicate( + objectQuery, + 'key', + conseiljs.ConseilOperator.EQ, + [objectId] + ) + objectQuery = conseiljs.ConseilQueryBuilder.setLimit(objectQuery, 1) + + const objectResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + objectQuery + ) + + const objectUrl = objectResult[0]['value'] + .toString() + .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') + const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) + + let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery() + swapsQuery = conseiljs.ConseilQueryBuilder.addFields( + swapsQuery, + 'key', + 'value' + ) + swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + swapsQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftSwapMap] + ) + swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( + swapsQuery, + 'value', + conseiljs.ConseilOperator.LIKE, + [`) (Pair ${objectId} `] + ) + swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 1000) // NOTE, limited to 1000 swaps for a given object + + const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + swapsQuery + ) + const swapStoragePattern = new RegExp( + `Pair [(]Pair 0x([0-9a-z]{44}) ([0-9]+)[)] [(]Pair ${objectId} ([0-9]+)[)]` + ) + + let swaps = [] + try { + swapsResult.map((row) => { + const match = swapStoragePattern.exec(row['value']) + + swaps.push({ + swap_id: row['key'], + issuer: conseiljs.TezosMessageUtils.readAddress(match[1]), + objkt_amount: match[2], + xtz_per_objkt: match[3], + }) + }) + } catch (error) { + console.log(`failed to process swaps for ${objectId} with ${error}`) + } + + return { objectId, ipfsHash, swaps } +} + +const chunkArray = (arr, len) => { + // TODO: move to util.js + let chunks = [], + i = 0, + n = arr.length + + while (i < n) { + chunks.push(arr.slice(i, (i += len))) + } + + return chunks +} + +module.exports = { + getCollectionForAddress, + gethDaoBalanceForAddress, + getArtisticOutputForAddress, + getObjectById, + getArtisticUniverse, + hDAOFeed, +} diff --git a/src/lib/router/index.js b/src/lib/router/index.js new file mode 100644 index 0000000..fbb8774 --- /dev/null +++ b/src/lib/router/index.js @@ -0,0 +1,22 @@ +'use strict' + +const router = require('express').Router() +const readFeed = require('./readFeed') +const readRandomFeed = require('./readRandomFeed') +const readTezosLedger = require('./readTezosLedger') +const readObjkt = require('./readObjkt') +const readHdaoFeed = require('./readHdaoFeed') + +router.post('/feed', _asyncHandler(readFeed)) +router.post('/random', _asyncHandler(readRandomFeed)) +router.post('/tz', _asyncHandler(readTezosLedger)) +router.post('/objkt', _asyncHandler(readObjkt)) +router.post('/hdao', _asyncHandler(readHdaoFeed)) + +module.exports = router + +function _asyncHandler(cb) { + return function (req, res, next) { + Promise.resolve(cb(req, res, next)).catch(next) + } +} diff --git a/src/lib/router/readFeed.js b/src/lib/router/readFeed.js new file mode 100644 index 0000000..151db83 --- /dev/null +++ b/src/lib/router/readFeed.js @@ -0,0 +1,24 @@ +'use strict' + +const conseil = require('conseil') + +const { getIpfsHash, paginateFeed, sortFeed } = require('utils') + +module.exports = async function readFeed(req, res) { + const rawFeed = await conseil.getArtisticUniverse(0) + const pageCursor = parseInt(req.body.counter) + const feed = paginateFeed(sortFeed(rawFeed), pageCursor) + + res.json({ + result: await Promise.all( + feed.map(async (objkt) => { + objkt.token_info = await getIpfsHash(objkt.ipfsHash) + objkt.token_id = parseInt(objkt.objectId) + + console.log(objkt) + + return objkt + }) + ), + }) +} diff --git a/src/lib/router/readHdaoFeed.js b/src/lib/router/readHdaoFeed.js new file mode 100644 index 0000000..39c6f51 --- /dev/null +++ b/src/lib/router/readHdaoFeed.js @@ -0,0 +1,25 @@ +'use strict' + +const conseil = require('conseil') + +const { getObjktById, paginateFeed } = require('utils') + +module.exports = async function readHdaoFeed(req, res) { + let rawFeed = await conseil.hDAOFeed() + + rawFeed = _.orderBy(rawFeed, ['hDAO_balance'], ['desc']) + + res.json({ + result: await Promise.all( + paginateFeed(rawFeed, 0).map(async (objkt) => await _mergeHdao(objkt)) + ), + }) +} + +async function _mergeHdao(objkt) { + const mergedObjkt = await getObjktById(objkt.token_id) + + mergedObjkt.hDAO_balance = objkt.hDAO_balance + + return mergedObjkt +} diff --git a/src/lib/router/readObjkt.js b/src/lib/router/readObjkt.js new file mode 100644 index 0000000..00932cd --- /dev/null +++ b/src/lib/router/readObjkt.js @@ -0,0 +1,20 @@ +'use strict' + +const axios = require('axios') +const { getObjktById } = require('utils') + +module.exports = async function readObjkt(req, res) { + const list = ( + await axios.get( + 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/o.json' + ) + ).data + const objktId = req.body.objkt_id + const tezosAddr = req.body.tz + + if (list.includes(tezosAddr)) { + return res.json({ result: [] }) + } + + return res.json({ result: await getObjktById(objktId) }) +} diff --git a/src/lib/router/readRandomFeed.js b/src/lib/router/readRandomFeed.js new file mode 100644 index 0000000..aa351bc --- /dev/null +++ b/src/lib/router/readRandomFeed.js @@ -0,0 +1,23 @@ +'use strict' + +const conseil = require('conseil') +const _ = require('lodash') + +const { getIpfsHash, paginateFeed } = require('utils') + +module.exports = async function readRandomFeed(req, res) { + const rawFeed = await conseil.getArtisticUniverse(0) + const pageCursor = parseInt(req.body.cursor) + const feed = paginateFeed(_.shuffle(rawFeed), pageCursor) + + res.json({ + result: await Promise.all( + feed.map(async (objkt) => { + objkt.token_info = await getIpfsHash(objkt.ipfsHash) + objkt.token_id = parseInt(objkt.objectId) + + return objkt + }) + ), + }) +} diff --git a/src/lib/router/readTezosLedger.js b/src/lib/router/readTezosLedger.js new file mode 100644 index 0000000..5d5461b --- /dev/null +++ b/src/lib/router/readTezosLedger.js @@ -0,0 +1,41 @@ +'use strict' + +const axios = require('axios') +const conseil = require('conseil') +const _ = require('lodash') +const { getIpfsHash } = require('utils') + +module.exports = async function readTezosLedger(req, res) { + const list = ( + await axios.get( + 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/w.json' + ) + ).data + const tezosAddr = req.body.tz + + if (list.includes(tezosAddr)) { + return res.json({ result: [] }) + } + + const [collection, creations, hdao] = await Promise.all([ + conseil.getCollectionForAddress(tezosAddr), + conseil.getArtisticOutputForAddress(tezosAddr), + conseil.gethDaoBalanceForAddress(tezosAddr), + ]) + + const result = await Promise.all( + [...collection, ...creations].map(async (objkt) => { + objkt.token_info = await getIpfsHash(objkt.ipfsHash) + objkt.token_id = parseInt(objkt.piece || objkt.objectId) + + return objkt + }) + ) + + res.json({ + result: _.uniqBy(result, (objkt) => { + return objkt.token_id + }), + hdao: hdao, + }) +} diff --git a/src/lib/utils.js b/src/lib/utils.js new file mode 100644 index 0000000..2f8b9eb --- /dev/null +++ b/src/lib/utils.js @@ -0,0 +1,65 @@ +'use strict' + +const FEED_ITEMS_PER_PAGE = process.env.FEED_ITEMS_PER_PAGE || 30 + +const axios = require('axios') +const conseil = require('conseil') +const _ = require('lodash') + +module.exports = { + getIpfsHash, + getObjktById, + getObjktOwners, + paginateFeed, + sortFeed, +} + +async function getIpfsHash(ipfsHash) { + return (await axios.get('https://cloudflare-ipfs.com/ipfs/' + ipfsHash)).data +} + +async function getObjktById(id, res) { + const objkt = await conseil.getObjectById(id) + + objkt.token_id = objkt.objectId + + const [objktOwners, ipfsHash] = await Promise.all([ + getObjktOwners(objkt), + getIpfsHash(objkt.ipfsHash), + ]) + + Object.assign(objkt, objktOwners, { + token_info: ipfsHash, + }) + + return objkt +} + +async function getObjktOwners(objkt) { + const owners = ( + await axios.get( + 'https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + + objkt.token_id + ) + ).data + const ownerAddrs = _.values(owners) + + return { + total_amount: + ownerAddrs.map((e) => parseInt(e)).length > 0 + ? ownerAddrs.filter((e) => parseInt(e) > 0).reduce(paginateFeed) + : 0, + owners, + } +} + +function paginateFeed(feed, cursor) { + return feed.slice( + cursor * FEED_ITEMS_PER_PAGE, + cursor * FEED_ITEMS_PER_PAGE + FEED_ITEMS_PER_PAGE + ) +} + +function sortFeed(feed) { + return _.sortBy(feed, (i) => parseInt(i.objectId)).reverse() +} From 13018830cab8c464555fd1cd4de31b4765ac60bc Mon Sep 17 00:00:00 2001 From: mike sizz Date: Thu, 25 Mar 2021 21:13:54 -0700 Subject: [PATCH 02/13] Add missing dependency. --- src/lib/router/readHdaoFeed.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib/router/readHdaoFeed.js b/src/lib/router/readHdaoFeed.js index 39c6f51..c9fadd4 100644 --- a/src/lib/router/readHdaoFeed.js +++ b/src/lib/router/readHdaoFeed.js @@ -1,5 +1,6 @@ 'use strict' +const _ = require('lodash') const conseil = require('conseil') const { getObjktById, paginateFeed } = require('utils') From 5047b0ae7ed4ff09f3a6eab7c9d8a2baacb570a8 Mon Sep 17 00:00:00 2001 From: mike sizz Date: Thu, 25 Mar 2021 21:36:15 -0700 Subject: [PATCH 03/13] Fix/refactor owner count logic. --- src/lib/utils.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/lib/utils.js b/src/lib/utils.js index 2f8b9eb..65b573f 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -42,14 +42,21 @@ async function getObjktOwners(objkt) { objkt.token_id ) ).data - const ownerAddrs = _.values(owners) + const ownerCountList = _.values(owners) + + let total = 0; + + if (ownerCountList.length) { + total = ownerCountList.reduce((acc, i) => { + const owned = parseInt(i) + + return owned > 0 ? acc + owned : acc + }, 0) + } return { - total_amount: - ownerAddrs.map((e) => parseInt(e)).length > 0 - ? ownerAddrs.filter((e) => parseInt(e) > 0).reduce(paginateFeed) - : 0, - owners, + total_amount: total, + owners } } From 0cfee3323fc90caae349ea7d0b11a4201913b4fd Mon Sep 17 00:00:00 2001 From: mike sizz Date: Thu, 25 Mar 2021 21:43:23 -0700 Subject: [PATCH 04/13] Rename route handler. --- src/lib/router/index.js | 4 ++-- src/lib/router/{readTezosLedger.js => readIssuer.js} | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename src/lib/router/{readTezosLedger.js => readIssuer.js} (94%) diff --git a/src/lib/router/index.js b/src/lib/router/index.js index fbb8774..d88ac33 100644 --- a/src/lib/router/index.js +++ b/src/lib/router/index.js @@ -3,13 +3,13 @@ const router = require('express').Router() const readFeed = require('./readFeed') const readRandomFeed = require('./readRandomFeed') -const readTezosLedger = require('./readTezosLedger') +const readIssuer = require('./readIssuer') const readObjkt = require('./readObjkt') const readHdaoFeed = require('./readHdaoFeed') router.post('/feed', _asyncHandler(readFeed)) router.post('/random', _asyncHandler(readRandomFeed)) -router.post('/tz', _asyncHandler(readTezosLedger)) +router.post('/tz', _asyncHandler(readIssuer)) router.post('/objkt', _asyncHandler(readObjkt)) router.post('/hdao', _asyncHandler(readHdaoFeed)) diff --git a/src/lib/router/readTezosLedger.js b/src/lib/router/readIssuer.js similarity index 94% rename from src/lib/router/readTezosLedger.js rename to src/lib/router/readIssuer.js index 5d5461b..89ed29b 100644 --- a/src/lib/router/readTezosLedger.js +++ b/src/lib/router/readIssuer.js @@ -5,7 +5,7 @@ const conseil = require('conseil') const _ = require('lodash') const { getIpfsHash } = require('utils') -module.exports = async function readTezosLedger(req, res) { +module.exports = async function readIssuer(req, res) { const list = ( await axios.get( 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/w.json' From 44c1e9aaa2ec4369694f994938b938af92d7c0f7 Mon Sep 17 00:00:00 2001 From: mike sizz Date: Thu, 25 Mar 2021 21:52:11 -0700 Subject: [PATCH 05/13] Remove console.log. --- src/lib/router/readFeed.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/lib/router/readFeed.js b/src/lib/router/readFeed.js index 151db83..632eae2 100644 --- a/src/lib/router/readFeed.js +++ b/src/lib/router/readFeed.js @@ -15,8 +15,6 @@ module.exports = async function readFeed(req, res) { objkt.token_info = await getIpfsHash(objkt.ipfsHash) objkt.token_id = parseInt(objkt.objectId) - console.log(objkt) - return objkt }) ), From 6b83daa273d752b0d43588858145e48684a87a10 Mon Sep 17 00:00:00 2001 From: mike sizz Date: Thu, 25 Mar 2021 22:09:41 -0700 Subject: [PATCH 06/13] Move feed item limit to config file. Run prettier. --- src/lib/config.js | 1 + src/lib/utils.js | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/lib/config.js b/src/lib/config.js index bfc8da1..0652b65 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -1,6 +1,7 @@ 'use strict' module.exports = { + feedItemsPerPage: process.env.FEED_ITEMS_PER_PAGE || 30, networkConfig: { network: 'mainnet', nftContract: 'KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton', diff --git a/src/lib/utils.js b/src/lib/utils.js index 65b573f..d0559a4 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -1,11 +1,11 @@ 'use strict' -const FEED_ITEMS_PER_PAGE = process.env.FEED_ITEMS_PER_PAGE || 30 - const axios = require('axios') const conseil = require('conseil') const _ = require('lodash') +const { feedItemsPerPage } = require('config') + module.exports = { getIpfsHash, getObjktById, @@ -44,26 +44,26 @@ async function getObjktOwners(objkt) { ).data const ownerCountList = _.values(owners) - let total = 0; + let total = 0 if (ownerCountList.length) { total = ownerCountList.reduce((acc, i) => { - const owned = parseInt(i) + const owned = parseInt(i) - return owned > 0 ? acc + owned : acc + return owned > 0 ? acc + owned : acc }, 0) } return { total_amount: total, - owners + owners, } } function paginateFeed(feed, cursor) { return feed.slice( - cursor * FEED_ITEMS_PER_PAGE, - cursor * FEED_ITEMS_PER_PAGE + FEED_ITEMS_PER_PAGE + cursor * feedItemsPerPage, + cursor * feedItemsPerPage + feedItemsPerPage ) } From 93b6b590c36eb77f01104f3841fb3be9a248f617 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88ran=20Sandstro=CC=88m?= Date: Sun, 11 Apr 2021 15:34:08 +0200 Subject: [PATCH 07/13] Actually use max time like we should --- conseilUtil.js | 2 +- index.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/conseilUtil.js b/conseilUtil.js index f5c95dd..1e79db8 100644 --- a/conseilUtil.js +++ b/conseilUtil.js @@ -379,7 +379,7 @@ const getArtisticUniverse = async (max_time) => { let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery(); mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields(mintOperationQuery, 'operation_group_hash'); mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'kind', conseiljs.ConseilOperator.EQ, ['transaction']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'timestamp', conseiljs.ConseilOperator.BETWEEN, [max_time, d.getTime()]) //Two weeks ago + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'timestamp', conseiljs.ConseilOperator.BETWEEN, [d.getTime(), max_time]) //Two weeks ago mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'status', conseiljs.ConseilOperator.EQ, ['applied']) mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'destination', conseiljs.ConseilOperator.EQ, [mainnet.protocol]) mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate(mintOperationQuery, 'parameters_entrypoints', conseiljs.ConseilOperator.EQ, ['mint_OBJKT']) diff --git a/index.js b/index.js index 7db1f96..df16492 100644 --- a/index.js +++ b/index.js @@ -58,9 +58,9 @@ const getFeed = async (res, counter, featured, max_time) => { console.log(`feed, featured: ${featured}`) var arr if (featured) { - arr = await conseilUtil.getFeaturedArtisticUniverse(0, max_time) + arr = await conseilUtil.getFeaturedArtisticUniverse(max_time) } else { - arr = await conseilUtil.getArtisticUniverse(0, max_time) + arr = await conseilUtil.getArtisticUniverse(max_time) } var feed = offset(desc(arr), counter) From e3e2f2f2f341bd800803be51d87c0f1fb4cc0fe7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88ran=20Sandstro=CC=88m?= Date: Sun, 11 Apr 2021 22:21:06 +0200 Subject: [PATCH 08/13] Maybe fix AWS lambda --- index.js | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/index.js b/index.js index df16492..dc0c3e8 100644 --- a/index.js +++ b/index.js @@ -198,18 +198,29 @@ const app = express() app.use(express.json()) app.use(cors({ origin: '*' })) -app.post('/feed|/featured', async (req, res) => { +app.post('/feed', async (req, res) => { const feedOffset = req.body.counter || 0 - const isFeatured = req.path === '/featured' const max_time = req.body.max_time - await getFeed(res, feedOffset, isFeatured, max_time) + await getFeed(res, feedOffset, false, max_time) }) -app.get('/feed|/featured', async (req, res) => { +app.post('/featured', async (req, res) => { + const feedOffset = req.body.counter || 0 + const max_time = req.body.max_time + await getFeed(res, feedOffset, true, max_time) +}) + + +app.get('/feed', async (req, res) => { + const feedOffset = req.query.counter || 0 + const max_time = req.query.max_time + await getFeed(res, feedOffset, false, max_time) +}) + +app.get('/featured', async (req, res) => { const feedOffset = req.query.counter || 0 - const isFeatured = req.path === '/featured' const max_time = req.query.max_time - await getFeed(res, feedOffset, isFeatured, max_time) + await getFeed(res, feedOffset, true, max_time) }) From 98a3ecb55e3cc52af9548b8285b3abf229eaafb3 Mon Sep 17 00:00:00 2001 From: mike sizz Date: Sun, 11 Apr 2021 14:40:14 -0700 Subject: [PATCH 09/13] Merge hicetnunc2000/hicetnunc-api master. --- .eslintrc | 10 + .node-version | 1 + .nvmrc | 1 + package.json | 11 +- src/index.js | 12 +- src/lib/config.js | 3 + src/lib/conseil.js | 501 +++++++++++++++++++++++++- src/lib/router/index.js | 150 +++++++- src/lib/router/readFeed.js | 28 +- src/lib/router/readHdaoFeed.js | 8 +- src/lib/router/readIssuer.js | 79 +++- src/lib/router/readObjkt.js | 15 +- src/lib/router/readRandomFeed.js | 2 +- src/lib/router/readRecommendCurate.js | 7 + src/lib/utils.js | 27 +- 15 files changed, 780 insertions(+), 75 deletions(-) create mode 100644 .eslintrc create mode 100644 .node-version create mode 100644 .nvmrc create mode 100644 src/lib/router/readRecommendCurate.js diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..b07d135 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,10 @@ +{ + "extends": "eslint:recommended", + "env": { + "node": true, + "es6": true + }, + "parserOptions": { + "ecmaVersion": 8 + } +} \ No newline at end of file diff --git a/.node-version b/.node-version new file mode 100644 index 0000000..1cd71be --- /dev/null +++ b/.node-version @@ -0,0 +1 @@ +12.20.1 \ No newline at end of file diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..1cd71be --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +12.20.1 \ No newline at end of file diff --git a/package.json b/package.json index 5796fb2..c3a7bd5 100644 --- a/package.json +++ b/package.json @@ -10,20 +10,23 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/hicetnunc2000/hicetnunc.git" + "url": "git+https://github.com/hicetnunc2000/hicetnunc-api.git" }, "author": "@hicetnunc2000", "license": "MIT", "bugs": { - "url": "https://github.com/hicetnunc2000/hicetnunc/issues" + "url": "https://github.com/hicetnunc2000/hicetnunc-api/issues" }, - "homepage": "https://github.com/hicetnunc2000/hicetnunc#readme", + "homepage": "https://github.com/hicetnunc2000/hicetnunc-api#readme", "dependencies": { "axios": "^0.21.1", - "conseiljs": "5.0.7-2", + "bignumber.js": "9.0.1", + "cloud-local-storage": "0.0.11", + "conseiljs": "5.0.8-1", "cors": "^2.8.5", "dotenv": "^8.2.0", "express": "^4.17.1", + "fetch": "^1.1.0", "lodash": "^4.17.21", "loglevel": "1.7.1", "node-fetch": "2.6.1", diff --git a/src/index.js b/src/index.js index abea00c..13aee7d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ 'use strict' +// set node path to import from this directory as if they are node modules process.env.NODE_PATH = 'src/lib' require('module').Module._initPaths() @@ -8,6 +9,9 @@ require('dotenv').config() const cors = require('cors') const express = require('express') const router = require('router') +const serverless = require('serverless-http') + +const { serverPort: PORT } = require('config') const app = express() @@ -15,4 +19,10 @@ app.use(express.json()) app.use(cors({ origin: '*' })) app.use(router) -app.listen(3001) +if (process.env.NODE_ENV === 'development') { + app.listen(PORT, () => { + console.log(`SERVER RUNNING ON localhost:${PORT}`) + }) +} + +module.exports.handler = serverless(app) diff --git a/src/lib/config.js b/src/lib/config.js index 0652b65..71c34e1 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -1,6 +1,8 @@ 'use strict' module.exports = { + burnAddress: + process.env.BURN_ADDRESS || 'tz1burnburnburnburnburnburnburjAYjjX', feedItemsPerPage: process.env.FEED_ITEMS_PER_PAGE || 30, networkConfig: { network: 'mainnet', @@ -15,4 +17,5 @@ module.exports = { nftRoyaltiesMap: 522, daoLedger: 515, }, + serverPort: 3001, } diff --git a/src/lib/conseil.js b/src/lib/conseil.js index d6c6d2b..4fc5fcb 100644 --- a/src/lib/conseil.js +++ b/src/lib/conseil.js @@ -1,6 +1,7 @@ const conseiljs = require('conseiljs') const fetch = require('node-fetch') const log = require('loglevel') +const BigNumber = require('bignumber.js') const logger = log.getLogger('conseiljs') logger.setLevel('error', false) @@ -8,9 +9,7 @@ conseiljs.registerLogger(logger) conseiljs.registerFetch(fetch) const conseilServer = 'https://conseil-prod.cryptonomic-infra.tech' const conseilApiKey = 'aa73fa8a-8626-4f43-a605-ff63130f37b1' // signup at nautilus.cloud -const tezosNode = '' - -const { networkConfig: mainnet } = require('config') +const mainnet = require('./config').networkConfig const hDAOFeed = async () => { let hDAOQuery = conseiljs.ConseilQueryBuilder.blankQuery() @@ -21,7 +20,7 @@ const hDAOFeed = async () => { conseiljs.ConseilOperator.EQ, [mainnet.curationsPtr] ) - hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 10_000) + hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 30_000) let hDAOResult = await conseiljs.TezosConseilClient.getTezosEntityData( { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, @@ -47,7 +46,8 @@ const getCollectionForAddress = async (address) => { collectionQuery = conseiljs.ConseilQueryBuilder.addFields( collectionQuery, 'key', - 'value' + 'value', + 'operation_group_id' ) collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate( collectionQuery, @@ -70,7 +70,7 @@ const getCollectionForAddress = async (address) => { ) collectionQuery = conseiljs.ConseilQueryBuilder.setLimit( collectionQuery, - 10_000 + 30_000 ) const collectionResult = await conseiljs.TezosConseilClient.getTezosEntityData( @@ -81,14 +81,15 @@ const getCollectionForAddress = async (address) => { ) let collection = collectionResult.map((i) => { return { - piece: i.key.toString().replace(/.* ([0-9]{1,}$)/, '$1'), - amount: Number(i.value), + piece: i['key'].toString().replace(/.* ([0-9]{1,}$)/, '$1'), + amount: Number(i['value']), + opId: i['operation_group_id'], } }) const queryChunks = chunkArray( collection.map((i) => i.piece), - 20 + 50 ) // NOTE: consider increasing this number somewhat const makeObjectQuery = (keys) => { let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery() @@ -145,14 +146,123 @@ const getCollectionForAddress = async (address) => { ) ) + const operationGroupIds = collectionResult.map((r) => r.operation_group_id) + const priceQueryChunks = chunkArray(operationGroupIds, 30) + const makeLastPriceQuery = (opIds) => { + let lastPriceQuery = conseiljs.ConseilQueryBuilder.blankQuery() + lastPriceQuery = conseiljs.ConseilQueryBuilder.addFields( + lastPriceQuery, + 'timestamp', + 'amount', + 'operation_group_hash', + 'parameters_entrypoints', + 'parameters' + ) + lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + lastPriceQuery, + 'kind', + conseiljs.ConseilOperator.EQ, + ['transaction'] + ) + lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + lastPriceQuery, + 'status', + conseiljs.ConseilOperator.EQ, + ['applied'] + ) + lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + lastPriceQuery, + 'internal', + conseiljs.ConseilOperator.EQ, + ['false'] + ) + lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + lastPriceQuery, + 'operation_group_hash', + opIds.length > 1 + ? conseiljs.ConseilOperator.IN + : conseiljs.ConseilOperator.EQ, + opIds + ) + lastPriceQuery = conseiljs.ConseilQueryBuilder.setLimit( + lastPriceQuery, + opIds.length + ) + + return lastPriceQuery + } + + const priceQueries = priceQueryChunks.map((c) => makeLastPriceQuery(c)) + const priceMap = {} + await Promise.all( + priceQueries.map( + async (q) => + await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'operations', + q + ).then((result) => + result.map((row) => { + let amount = 0 + const action = row.parameters_entrypoints + + if (action === 'collect') { + amount = Number( + row.parameters.toString().replace(/^Pair ([0-9]+) [0-9]+/, '$1') + ) + } else if (action === 'transfer') { + amount = Number( + row.parameters + .toString() + .replace( + /[{] Pair \"[1-9A-HJ-NP-Za-km-z]{36}\" [{] Pair \"[1-9A-HJ-NP-Za-km-z]{36}\" [(]Pair [0-9]+ [0-9]+[)] [}] [}]/, + '$1' + ) + ) + } + + priceMap[row.operation_group_hash] = { + price: new BigNumber(row.amount), + amount, + timestamp: row.timestamp, + action, + } + }) + ) + ) + ) + collection = collection.map((i) => { + let price = 0 + let receivedOn = new Date() + let action = '' + + try { + const priceRecord = priceMap[i.opId] + price = priceRecord.price + .dividedToIntegerBy(priceRecord.amount) + .toNumber() + receivedOn = new Date(priceRecord.timestamp) + action = priceRecord.action === 'collect' ? 'Purchased' : 'Received' + } catch { + // + } + + delete i.opId + return { + price: isNaN(price) ? 0 : price, + receivedOn, + action, ipfsHash: objectIpfsMap[i.piece.toString()], ...i, } }) - return collection.sort((a, b) => parseInt(b.piece) - parseInt(a.piece)) // sort descending by id – most-recently minted art first + return collection.sort( + (a, b) => b.receivedOn.getTime() - a.receivedOn.getTime() + ) // sort descending by date – most-recently acquired art first } const gethDaoBalanceForAddress = async (address) => { @@ -203,6 +313,313 @@ const gethDaoBalanceForAddress = async (address) => { return balance } +const getTokenBalance = async ( + big_map_id, + address, + fa2 = false, + token_id = 0 +) => { + let tokenBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() + tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( + tokenBalanceQuery, + 'value' + ) + tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + tokenBalanceQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [big_map_id] + ) + if (fa2) { + tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + tokenBalanceQuery, + 'key', + conseiljs.ConseilOperator.EQ, + [ + `Pair 0x${conseiljs.TezosMessageUtils.writeAddress( + address + )} ${token_id}`, + ] + ) + } else { + tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + tokenBalanceQuery, + 'key', + conseiljs.ConseilOperator.EQ, + [`0x${conseiljs.TezosMessageUtils.writeAddress(address)}`] + ) + } + tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + tokenBalanceQuery, + 'value', + conseiljs.ConseilOperator.EQ, + [0], + true + ) + tokenBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit( + tokenBalanceQuery, + 1 + ) + + let balance = 0 + + try { + const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + tokenBalanceQuery + ) + balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it + } catch (error) { + console.log( + `getTokenBalance failed for ${JSON.stringify( + tokenBalanceQuery + )} with ${error}` + ) + } + + return balance +} + +const getTezBalanceForAddress = async (address) => { + let accountQuery = conseiljs.ConseilQueryBuilder.blankQuery() + accountQuery = conseiljs.ConseilQueryBuilder.addFields( + accountQuery, + 'balance' + ) + accountQuery = conseiljs.ConseilQueryBuilder.addPredicate( + accountQuery, + 'account_id', + conseiljs.ConseilOperator.EQ, + [address], + false + ) + accountQuery = conseiljs.ConseilQueryBuilder.setLimit(accountQuery, 1) + + try { + const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'accounts', + accountQuery + ) + balance = balanceResult[0]['balance'] // TODO: consider bigNumber here, for the moment there is no reason for it + } catch (error) { + console.log( + `getTezBalanceForAddress failed for ${JSON.stringify( + accountQuery + )} with ${error}` + ) + } + + return balance +} + +const gethDAOPerTez = async () => { + const tezBalance = await getTezBalanceForAddress(mainnet.hDaoSwap) + const hdaoBalance = await gethDaoBalanceForAddress(mainnet.hDaoSwap) + return hdaoBalance / tezBalance +} + +const getKolibriPerTez = async () => { + const tezBalance = await getTezBalanceForAddress(mainnet.kolibriSwap) + var kolibriBalance = await getTokenBalance( + mainnet.kolibriLedger, + mainnet.kolibriSwap + ) + + // TODO: Find a better way to get the balance, this is FA1.2, mike? + kolibriBalance = + parseInt(kolibriBalance.replace('Pair {} ', '')) / 10 ** (18 - 6) + return kolibriBalance / tezBalance +} + +const gethDaoBalances = async () => { + let hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( + hDaoBalanceQuery, + 'key', + 'value' + ) + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + hDaoBalanceQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.daoLedger] + ) + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + hDaoBalanceQuery, + 'value', + conseiljs.ConseilOperator.EQ, + [0], + true + ) + hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit( + hDaoBalanceQuery, + 500_000 + ) + + let balance = 0 + let hdaoMap = {} + + try { + const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + hDaoBalanceQuery + ) + + balanceResult.forEach((row) => { + hdaoMap[ + conseiljs.TezosMessageUtils.readAddress( + row['key'].toString().replace(/^Pair 0x([0-9a-z]{1,}) [0-9]+/, '$1') + ) + ] = row['value'] + }) + //#balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it + } catch (error) { + console.log( + `gethDaoBalanceForAddress failed for ${JSON.stringify( + hDaoBalanceQuery + )} with ${error}` + ) + } + + return hdaoMap +} + +const getObjektOwners = async (objekt_id) => { + let objektBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() + objektBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( + objektBalanceQuery, + 'key', + 'value' + ) + objektBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + objektBalanceQuery, + 'big_map_id', + conseiljs.ConseilOperator.EQ, + [mainnet.nftLedger] + ) + objektBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + objektBalanceQuery, + 'key', + conseiljs.ConseilOperator.ENDSWITH, + [` ${objekt_id}`], + false + ) + objektBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( + objektBalanceQuery, + 'value', + conseiljs.ConseilOperator.EQ, + [0], + true + ) + objektBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit( + objektBalanceQuery, + 500_000 + ) + + let objektMap = {} + + try { + const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'big_map_contents', + objektBalanceQuery + ) + + balanceResult.forEach((row) => { + objektMap[ + conseiljs.TezosMessageUtils.readAddress( + row['key'].toString().replace(/^Pair 0x([0-9a-z]{1,}) [0-9]+/, '$1') + ) + ] = row['value'] + }) + //#balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it + } catch (error) { + console.log( + `getObjektOwners failed for ${JSON.stringify( + objektBalanceQuery + )} with ${error}` + ) + } + + return objektMap +} + +const getObjektMintingsLastWeek = async () => { + var d = new Date() + d.setDate(d.getDate() - 5) + let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() + mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( + mintOperationQuery, + 'source' + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'kind', + conseiljs.ConseilOperator.EQ, + ['transaction'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'timestamp', + conseiljs.ConseilOperator.AFTER, + [d.getTime()] + ) // 2021 Feb 1 + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'status', + conseiljs.ConseilOperator.EQ, + ['applied'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'destination', + conseiljs.ConseilOperator.EQ, + [mainnet.protocol] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( + mintOperationQuery, + 'parameters_entrypoints', + conseiljs.ConseilOperator.EQ, + ['mint_OBJKT'] + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering( + mintOperationQuery, + 'block_level', + conseiljs.ConseilSortDirection.DESC + ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( + mintOperationQuery, + 900_000 + ) // TODO: this is hardwired and will not work for highly productive artists + + const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( + { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, + 'mainnet', + 'operations', + mintOperationQuery + ) + + const mints = mintOperationResult.map((r) => r['source']) + + var initialValue = {} + var reducer = function (minters, mintOp) { + if (!minters[mintOp]) { + minters[mintOp] = 1 + } else { + minters[mintOp] = minters[mintOp] + 1 + } + return minters + } + return mints.reduce(reducer, initialValue) +} + /** * Queries Conseil in two steps to get all the objects minted by a specific address. Step 1 is to query for all 'mint_OBJKT' operations performed by the account to get the list of operation group hashes. Then that list is partitioned into chunks and another query (or set of queries) is run to get big_map values. These values are then parsed into an array of 3-tuples containing the hashed big_map key that can be used to query a Tezos node directly, the nft token id and the ipfs item hash. * @@ -258,7 +675,7 @@ const getArtisticOutputForAddress = async (address) => { ) mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( mintOperationQuery, - 10_000 + 256 ) // TODO: this is hardwired and will not work for highly productive artists const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( @@ -334,6 +751,8 @@ const getArtisticOutputForAddress = async (address) => { } const getArtisticUniverse = async (max_time) => { + var d = new Date() + d.setDate(d.getDate() - 14) let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( mintOperationQuery, @@ -348,9 +767,9 @@ const getArtisticUniverse = async (max_time) => { mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( mintOperationQuery, 'timestamp', - conseiljs.ConseilOperator.AFTER, - [1612240919000] - ) // 2021 Feb 1 + conseiljs.ConseilOperator.BETWEEN, + [d.getTime(), max_time] + ) //Two weeks ago mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( mintOperationQuery, 'status', @@ -369,9 +788,14 @@ const getArtisticUniverse = async (max_time) => { conseiljs.ConseilOperator.EQ, ['mint_OBJKT'] ) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering( + mintOperationQuery, + 'block_level', + conseiljs.ConseilSortDirection.DESC + ) mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( mintOperationQuery, - 14_000 + 2500 ) const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( @@ -399,7 +823,7 @@ const getArtisticUniverse = async (max_time) => { ) royaltiesQuery = conseiljs.ConseilQueryBuilder.setLimit( royaltiesQuery, - 10_000 + 30_000 ) const royaltiesResult = await conseiljs.TezosConseilClient.getTezosEntityData( { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, @@ -426,7 +850,7 @@ const getArtisticUniverse = async (max_time) => { conseiljs.ConseilOperator.EQ, [mainnet.nftSwapMap] ) - swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 10_000) // NOTE, limited to 10_000 + swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 30_000) // NOTE, limited to 30_000 const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData( { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, @@ -528,13 +952,51 @@ const getArtisticUniverse = async (max_time) => { return universe } +const getFeaturedArtisticUniverse = async (max_time) => { + hdaoMap = await gethDaoBalances() + + mintsPerCreator = await getObjektMintingsLastWeek() + + artisticUniverse = await getArtisticUniverse(max_time) + + hdaoPerTez = await gethDAOPerTez() + + // Cost to be on feed per objekt last 7 days shouldn't be higher than: + // 0.1tez + // 1 hDAO + // But not lower than: + // 0.01 hDAO + // + // We should probably add more thresholds like $, € and yen + // It should be cheap but not too cheap and it shouldn't be + // affected by tez or hDAO volatility + + thresholdHdao = Math.min(1_000_000, Math.max(100_000 * hdaoPerTez, 10_000)) + + return artisticUniverse.filter(function (o) { + return ( + (hdaoMap[o.minter] || 0) / Math.max(mintsPerCreator[o.minter] || 1, 1) > + thresholdHdao + ) + }) +} + +const getRecommendedCurateDefault = async () => { + hdaoPerTez = await gethDAOPerTez() + kolPerTez = await getKolibriPerTez() + hdaoPerKol = hdaoPerTez / kolPerTez + //Minimum of $0.1, 0.1 hDAO, and 0.1tez, in hDAO + return Math.floor( + Math.min(hdaoPerKol * 0.1, 0.1, 0.1 * hdaoPerTez) * 1_000_000 + ) +} + /** * Returns object ipfs hash and swaps if any * * @param {number} objectId * @returns */ - const getObjectById = async (objectId) => { let objectQuery = conseiljs.ConseilQueryBuilder.blankQuery() objectQuery = conseiljs.ConseilQueryBuilder.addFields(objectQuery, 'value') @@ -632,5 +1094,8 @@ module.exports = { getArtisticOutputForAddress, getObjectById, getArtisticUniverse, + getFeaturedArtisticUniverse, hDAOFeed, + getRecommendedCurateDefault, + getObjektOwners, } diff --git a/src/lib/router/index.js b/src/lib/router/index.js index d88ac33..7a0283b 100644 --- a/src/lib/router/index.js +++ b/src/lib/router/index.js @@ -6,17 +6,157 @@ const readRandomFeed = require('./readRandomFeed') const readIssuer = require('./readIssuer') const readObjkt = require('./readObjkt') const readHdaoFeed = require('./readHdaoFeed') +const readRecommendCurate = require('./readRecommendCurate') -router.post('/feed', _asyncHandler(readFeed)) -router.post('/random', _asyncHandler(readRandomFeed)) -router.post('/tz', _asyncHandler(readIssuer)) -router.post('/objkt', _asyncHandler(readObjkt)) -router.post('/hdao', _asyncHandler(readHdaoFeed)) +const MILLISECOND_MODIFIER = 1000 +const ONE_MINUTE_MILLIS = 60 * MILLISECOND_MODIFIER +const DEFAULT_CACHE_TTL = 60 * 10 +const STATUS_CODE_SUCCESS = 200 +const CACHE_MAX_AGE_MAP = { + hdao: 300, + issuer: 120, + objkt: 120, + random: 300, + recommendCurate: 300, +} + +router + .route('/feed|/featured') + .all(_processClientCache) + .get((req, res, next) => { + req.body.counter = req.query.counter + + return next() + }, _asyncHandler(readFeed)) + .post(_asyncHandler(readFeed)) + +router + .route('/random') + .all((req, res, next) => { + _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.random) + + return next() + }) + .get((req, res, next) => { + req.body.counter = req.query.counter + + return next() + }, _asyncHandler(readRandomFeed)) + .post(_asyncHandler(readRandomFeed)) + +router + .route('/tz') + .all((req, res, next) => { + _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.issuer) + + return next() + }) + .get((req, res, next) => { + req.body.tz = req.query.tz + + return next() + }, _asyncHandler(readIssuer)) + .post(_asyncHandler(readIssuer)) + +router + .route('/objkt') + .all((req, res, next) => { + _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.objkt) + + return next() + }) + .get((req, res, next) => { + req.body.objkt_id = req.query.id + + return next() + }, _asyncHandler(readObjkt)) + .post(_asyncHandler(readObjkt)) + +router + .route('/hdao') + .all((req, res, next) => { + _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.hdao) + + return next() + }) + .get((req, res, next) => { + req.body.counter = req.query.counter + + return next() + }, _asyncHandler(readHdaoFeed)) + .post(_asyncHandler(readHdaoFeed)) + +router.get( + '/recommend_curate', + (req, res, next) => { + _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.recommendCurate) + + return next() + }, + _asyncHandler(readRecommendCurate) +) module.exports = router +/** + * Express cannot process promise rejections correctly. We need to encapsulate async + * route handlers with logic to pass promise rejections to express's middleware chain + * as an error. + */ function _asyncHandler(cb) { return function (req, res, next) { Promise.resolve(cb(req, res, next)).catch(next) } } + +/** + * Set the fetch time for conseil requests and set the Cache-Control header + * on successful responses. + */ +function _processClientCache(req, res, next) { + const clientCacheMaxAge = req.body.max_time || req.query.max_time + const isValidClientCacheTime = Number.isInteger(clientCacheMaxAge) + const now = Date.now() + const fetchTime = isValidClientCacheTime + ? clientCacheMaxAge + : _floorTimeToMinute(now) + + // Set fetch time for feed conseil requests + req.feedFetchAt = fetchTime + + const isImmutable = isValidClientCacheTime && clientCacheMaxAge < now + const cacheMaxAge = isImmutable + ? DEFAULT_CACHE_TTL + : _calcCacheTtl(fetchTime, now) + + // Set cache on successful request + _setCacheHeaderOnSuccess(res, cacheMaxAge) + + return next() + + function _floorTimeToMinute(currentTime) { + return Math.floor(currentTime / ONE_MINUTE_MILLIS) * ONE_MINUTE_MILLIS + } + + function _calcCacheTtl(dataGatheredAt, currentTime) { + return Math.floor( + (dataGatheredAt + ONE_MINUTE_MILLIS - currentTime) / MILLISECOND_MODIFIER + ) + } +} + +/** + * Set cache time for cloudfront if request is successful. + * res.end is called by res.send which is called by res.json. + */ +function _setCacheHeaderOnSuccess(res, maxAge) { + const responseEnd = res.end.bind(res) + + res.end = function (...args) { + if (res.statusCode === STATUS_CODE_SUCCESS) { + res.set('Cache-Control', `public, max-age=${maxAge}`) + } + + return responseEnd(...args) + } +} diff --git a/src/lib/router/readFeed.js b/src/lib/router/readFeed.js index 632eae2..f86df1b 100644 --- a/src/lib/router/readFeed.js +++ b/src/lib/router/readFeed.js @@ -5,18 +5,22 @@ const conseil = require('conseil') const { getIpfsHash, paginateFeed, sortFeed } = require('utils') module.exports = async function readFeed(req, res) { - const rawFeed = await conseil.getArtisticUniverse(0) - const pageCursor = parseInt(req.body.counter) - const feed = paginateFeed(sortFeed(rawFeed), pageCursor) + const isFeatured = req.path === '/featured' + const pageCursor = req.body.counter + const fetchTime = req.feedFetchAt + const rawFeed = await (isFeatured + ? conseil.getFeaturedArtisticUniverse(fetchTime) + : conseil.getArtisticUniverse(fetchTime)) - res.json({ - result: await Promise.all( - feed.map(async (objkt) => { - objkt.token_info = await getIpfsHash(objkt.ipfsHash) - objkt.token_id = parseInt(objkt.objectId) + const paginatedFeed = paginateFeed(sortFeed(rawFeed), pageCursor) + const feed = await Promise.all( + paginatedFeed.map(async (objkt) => { + objkt.token_info = await getIpfsHash(objkt.ipfsHash) + objkt.token_id = parseInt(objkt.objectId) - return objkt - }) - ), - }) + return objkt + }) + ) + + return res.json({ result: feed }) } diff --git a/src/lib/router/readHdaoFeed.js b/src/lib/router/readHdaoFeed.js index c9fadd4..9caa4c0 100644 --- a/src/lib/router/readHdaoFeed.js +++ b/src/lib/router/readHdaoFeed.js @@ -6,13 +6,13 @@ const conseil = require('conseil') const { getObjktById, paginateFeed } = require('utils') module.exports = async function readHdaoFeed(req, res) { - let rawFeed = await conseil.hDAOFeed() - - rawFeed = _.orderBy(rawFeed, ['hDAO_balance'], ['desc']) + const rawFeed = await conseil.hDAOFeed() + const sortedFeed = _.orderBy(rawFeed, ['hDAO_balance'], ['desc']) + const paginatedFeed = paginateFeed(sortedFeed, 0) res.json({ result: await Promise.all( - paginateFeed(rawFeed, 0).map(async (objkt) => await _mergeHdao(objkt)) + paginatedFeed.map(async (objkt) => await _mergeHdao(objkt)) ), }) } diff --git a/src/lib/router/readIssuer.js b/src/lib/router/readIssuer.js index 89ed29b..2b6c284 100644 --- a/src/lib/router/readIssuer.js +++ b/src/lib/router/readIssuer.js @@ -1,30 +1,29 @@ 'use strict' -const axios = require('axios') const conseil = require('conseil') const _ = require('lodash') -const { getIpfsHash } = require('utils') + +const { getIpfsHash, getObjktOwners, getRestrictedAddresses } = require('utils') +const { burnAddress } = require('config') module.exports = async function readIssuer(req, res) { - const list = ( - await axios.get( - 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/w.json' - ) - ).data - const tezosAddr = req.body.tz - - if (list.includes(tezosAddr)) { + const issuerAddress = req.body.tz + const restrictedAddresses = await getRestrictedAddresses() + + if (restrictedAddresses.includes(issuerAddress)) { return res.json({ result: [] }) } const [collection, creations, hdao] = await Promise.all([ - conseil.getCollectionForAddress(tezosAddr), - conseil.getArtisticOutputForAddress(tezosAddr), - conseil.gethDaoBalanceForAddress(tezosAddr), + conseil.getCollectionForAddress(issuerAddress), + conseil.getArtisticOutputForAddress(issuerAddress), + conseil.gethDaoBalanceForAddress(issuerAddress), ]) - const result = await Promise.all( - [...collection, ...creations].map(async (objkt) => { + const filteredCreations = await _filteredBurnedCreations(creations) + + const unsortedResults = await Promise.all( + [...collection, ...filteredCreations].map(async (objkt) => { objkt.token_info = await getIpfsHash(objkt.ipfsHash) objkt.token_id = parseInt(objkt.piece || objkt.objectId) @@ -32,10 +31,56 @@ module.exports = async function readIssuer(req, res) { }) ) - res.json({ - result: _.uniqBy(result, (objkt) => { + const sortedResults = _sortResults(unsortedResults) + + return res.json({ + result: _.uniqBy(sortedResults, (objkt) => { return objkt.token_id }), hdao: hdao, }) } + +async function _filteredBurnedCreations(creations) { + const validCreations = [] + + await Promise.all( + creations.map(async (c) => { + c.token_id = c.objectId + + const ownerData = await getObjktOwners(c) + + Object.assign(c, ownerData) + + const burnAddrCount = + c.owners[burnAddress] && parseInt(c.owners[burnAddress]) + const allIssuesBurned = burnAddrCount && burnAddrCount === c.total_amount + + if (!allIssuesBurned) { + delete c.owners + + validCreations.push(c) + } + }) + ) + + return validCreations +} + +function _sortResults(results) { + const unsortedCollection = [] + const unsortedCreations = [] + + results.map((r) => + r.piece ? unsortedCollection.push(r) : unsortedCreations.push(r) + ) + + return [ + ..._sort(unsortedCollection, 'piece'), + ..._sort(unsortedCreations, 'objectId'), + ] + + function _sort(arr, sortKey) { + return arr.sort((a, b) => parseInt(b[sortKey]) - parseInt(a[sortKey])) + } +} diff --git a/src/lib/router/readObjkt.js b/src/lib/router/readObjkt.js index 00932cd..783a369 100644 --- a/src/lib/router/readObjkt.js +++ b/src/lib/router/readObjkt.js @@ -1,18 +1,13 @@ 'use strict' -const axios = require('axios') -const { getObjktById } = require('utils') +const { getObjktById, getRestrictedObjkts } = require('utils') module.exports = async function readObjkt(req, res) { - const list = ( - await axios.get( - 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/o.json' - ) - ).data - const objktId = req.body.objkt_id - const tezosAddr = req.body.tz + const { objkt_id: objktId, tz: tezosAddr } = req.body - if (list.includes(tezosAddr)) { + const restrictedObjkts = await getRestrictedObjkts() + + if (restrictedObjkts.includes(tezosAddr)) { return res.json({ result: [] }) } diff --git a/src/lib/router/readRandomFeed.js b/src/lib/router/readRandomFeed.js index aa351bc..7e02058 100644 --- a/src/lib/router/readRandomFeed.js +++ b/src/lib/router/readRandomFeed.js @@ -7,7 +7,7 @@ const { getIpfsHash, paginateFeed } = require('utils') module.exports = async function readRandomFeed(req, res) { const rawFeed = await conseil.getArtisticUniverse(0) - const pageCursor = parseInt(req.body.cursor) + const pageCursor = req.body.cursor const feed = paginateFeed(_.shuffle(rawFeed), pageCursor) res.json({ diff --git a/src/lib/router/readRecommendCurate.js b/src/lib/router/readRecommendCurate.js new file mode 100644 index 0000000..4b4b692 --- /dev/null +++ b/src/lib/router/readRecommendCurate.js @@ -0,0 +1,7 @@ +'use strict' + +const conseil = require('conseil') + +module.exports = async function readRecommendCurate(req, res) { + return res.json({ amount: await conseil.getRecommendedCurateDefault() }) +} diff --git a/src/lib/utils.js b/src/lib/utils.js index d0559a4..b23941d 100644 --- a/src/lib/utils.js +++ b/src/lib/utils.js @@ -10,6 +10,8 @@ module.exports = { getIpfsHash, getObjktById, getObjktOwners, + getRestrictedAddresses, + getRestrictedObjkts, paginateFeed, sortFeed, } @@ -18,7 +20,7 @@ async function getIpfsHash(ipfsHash) { return (await axios.get('https://cloudflare-ipfs.com/ipfs/' + ipfsHash)).data } -async function getObjktById(id, res) { +async function getObjktById(id) { const objkt = await conseil.getObjectById(id) objkt.token_id = objkt.objectId @@ -42,6 +44,7 @@ async function getObjktOwners(objkt) { objkt.token_id ) ).data + const ownerCountList = _.values(owners) let total = 0 @@ -60,10 +63,28 @@ async function getObjktOwners(objkt) { } } +async function getRestrictedAddresses() { + return ( + await axios.get( + 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/w.json' + ) + ).data +} + +async function getRestrictedObjkts() { + return ( + await axios.get( + 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/o.json' + ) + ).data +} + function paginateFeed(feed, cursor) { + const pageCursor = cursor ? parseInt(cursor) : 0 + return feed.slice( - cursor * feedItemsPerPage, - cursor * feedItemsPerPage + feedItemsPerPage + pageCursor * feedItemsPerPage, + pageCursor * feedItemsPerPage + feedItemsPerPage ) } From 0556abe7954aa46cdfb56f82086f4d14526bbaf5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88ran=20Sandstro=CC=88m?= Date: Mon, 12 Apr 2021 11:04:43 +0200 Subject: [PATCH 10/13] Fix 2 structural refactor bugs --- src/lib/config.js | 3 +++ src/lib/conseil.js | 1 + 2 files changed, 4 insertions(+) diff --git a/src/lib/config.js b/src/lib/config.js index 71c34e1..f3ccadd 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -16,6 +16,9 @@ module.exports = { curationsPtr: 519, nftRoyaltiesMap: 522, daoLedger: 515, + kolibriLedger: 380, + hDaoSwap: "KT1V41fGzkdTJki4d11T1Rp9yPkCmDhB7jph", + kolibriSwap: "KT1CiSKXR68qYSxnbzjwvfeMCRburaSDonT2", }, serverPort: 3001, } diff --git a/src/lib/conseil.js b/src/lib/conseil.js index 4fc5fcb..18d86e7 100644 --- a/src/lib/conseil.js +++ b/src/lib/conseil.js @@ -751,6 +751,7 @@ const getArtisticOutputForAddress = async (address) => { } const getArtisticUniverse = async (max_time) => { + max_time = ((typeof max_time !== 'undefined') && max_time != 0) ? max_time : (new Date()).getTime() var d = new Date() d.setDate(d.getDate() - 14) let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() From b37cfec3a372487fbd407dcb2a9bd52d588d4ee6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88ran=20Sandstro=CC=88m?= Date: Mon, 12 Apr 2021 11:17:17 +0200 Subject: [PATCH 11/13] Need max time from query for get requests to work properly (in new branch in UI) --- src/lib/router/index.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib/router/index.js b/src/lib/router/index.js index 7a0283b..3098fa8 100644 --- a/src/lib/router/index.js +++ b/src/lib/router/index.js @@ -24,6 +24,7 @@ router .route('/feed|/featured') .all(_processClientCache) .get((req, res, next) => { + req.body.max_time = req.query.max_time req.body.counter = req.query.counter return next() @@ -39,7 +40,7 @@ router }) .get((req, res, next) => { req.body.counter = req.query.counter - + req.body.max_time = req.query.max_time return next() }, _asyncHandler(readRandomFeed)) .post(_asyncHandler(readRandomFeed)) From d772e8b6f1ea1750bf80548d515c413d494c2e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88ran=20Sandstro=CC=88m?= Date: Mon, 12 Apr 2021 15:40:24 +0200 Subject: [PATCH 12/13] Cleanup and clarifications, solved some more bugs --- src/lib/conseil.js | 37 +++++++++++++++++++++++-------------- 1 file changed, 23 insertions(+), 14 deletions(-) diff --git a/src/lib/conseil.js b/src/lib/conseil.js index 18d86e7..a0e3e23 100644 --- a/src/lib/conseil.js +++ b/src/lib/conseil.js @@ -20,7 +20,7 @@ const hDAOFeed = async () => { conseiljs.ConseilOperator.EQ, [mainnet.curationsPtr] ) - hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 30_000) + hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 300_000) let hDAOResult = await conseiljs.TezosConseilClient.getTezosEntityData( { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, @@ -70,7 +70,7 @@ const getCollectionForAddress = async (address) => { ) collectionQuery = conseiljs.ConseilQueryBuilder.setLimit( collectionQuery, - 30_000 + 300_000 ) const collectionResult = await conseiljs.TezosConseilClient.getTezosEntityData( @@ -553,7 +553,7 @@ const getObjektOwners = async (objekt_id) => { const getObjektMintingsLastWeek = async () => { var d = new Date() - d.setDate(d.getDate() - 5) + d.setDate(d.getDate() - 7) let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( mintOperationQuery, @@ -596,7 +596,7 @@ const getObjektMintingsLastWeek = async () => { ) mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( mintOperationQuery, - 900_000 + 500_000 ) // TODO: this is hardwired and will not work for highly productive artists const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( @@ -824,7 +824,7 @@ const getArtisticUniverse = async (max_time) => { ) royaltiesQuery = conseiljs.ConseilQueryBuilder.setLimit( royaltiesQuery, - 30_000 + 5_000_000 ) const royaltiesResult = await conseiljs.TezosConseilClient.getTezosEntityData( { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, @@ -851,7 +851,7 @@ const getArtisticUniverse = async (max_time) => { conseiljs.ConseilOperator.EQ, [mainnet.nftSwapMap] ) - swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 30_000) // NOTE, limited to 30_000 + swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 5_000_000) // NOTE, limited to 30_000 const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData( { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, @@ -961,10 +961,14 @@ const getFeaturedArtisticUniverse = async (max_time) => { artisticUniverse = await getArtisticUniverse(max_time) hdaoPerTez = await gethDAOPerTez() + kolPerTez = await getKolibriPerTez() + hdaoPerKol = hdaoPerTez / kolPerTez + // Cost to be on feed per objekt last 7 days shouldn't be higher than: // 0.1tez // 1 hDAO + // $1 // But not lower than: // 0.01 hDAO // @@ -972,14 +976,19 @@ const getFeaturedArtisticUniverse = async (max_time) => { // It should be cheap but not too cheap and it shouldn't be // affected by tez or hDAO volatility - thresholdHdao = Math.min(1_000_000, Math.max(100_000 * hdaoPerTez, 10_000)) - - return artisticUniverse.filter(function (o) { - return ( - (hdaoMap[o.minter] || 0) / Math.max(mintsPerCreator[o.minter] || 1, 1) > - thresholdHdao - ) - }) + let thresholdHdao = Math.floor( + Math.max(10_000, + Math.min(1_000_000, + Math.max( + 100_000 * hdaoPerTez, + 100_000 * hdaoPerKol) + ) + )) + //thresholdHdao = 0 + + return artisticUniverse.filter(o => + (hdaoMap[o.minter] || 100_000) / Math.max(mintsPerCreator[o.minter] || 1, 1) > + thresholdHdao) } const getRecommendedCurateDefault = async () => { From 81a05eaa5b99548d0000870bdd7e3b912d3bce03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Go=CC=88ran=20Sandstro=CC=88m?= Date: Mon, 12 Apr 2021 21:50:56 +0200 Subject: [PATCH 13/13] 2 more bugs --- serverless.yml | 2 +- src/lib/router/readObjkt.js | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/serverless.yml b/serverless.yml index ff6d4b1..315359e 100644 --- a/serverless.yml +++ b/serverless.yml @@ -16,7 +16,7 @@ provider: functions: handler: - handler: index.handler + handler: src/index.handler timeout: 120 events: - http: diff --git a/src/lib/router/readObjkt.js b/src/lib/router/readObjkt.js index 783a369..bfdb02e 100644 --- a/src/lib/router/readObjkt.js +++ b/src/lib/router/readObjkt.js @@ -3,11 +3,10 @@ const { getObjktById, getRestrictedObjkts } = require('utils') module.exports = async function readObjkt(req, res) { - const { objkt_id: objktId, tz: tezosAddr } = req.body - + const objktId = req.body.objkt_id const restrictedObjkts = await getRestrictedObjkts() - if (restrictedObjkts.includes(tezosAddr)) { + if (restrictedObjkts.includes(objktId)) { return res.json({ result: [] }) }