diff --git a/config.js b/config.js index 9343184..ccd3cfd 100644 --- a/config.js +++ b/config.js @@ -9,7 +9,10 @@ const mainnet = { nftSwapMap: 523, curationsPtr: 519, nftRoyaltiesMap: 522, - daoLedger: 515 + daoLedger: 515, + kolibriLedger: 380, + hDaoSwap: "KT1V41fGzkdTJki4d11T1Rp9yPkCmDhB7jph", + kolibriSwap: "KT1CiSKXR68qYSxnbzjwvfeMCRburaSDonT2", } module.exports = { diff --git a/conseilUtil.js b/conseilUtil.js index e054b5f..ef1e4cf 100644 --- a/conseilUtil.js +++ b/conseilUtil.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,8 +9,6 @@ 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 @@ -36,7 +35,7 @@ const hDAOFeed = async () => { */ const getCollectionForAddress = async (address) => { let collectionQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - collectionQuery = conseiljs.ConseilQueryBuilder.addFields(collectionQuery, 'key', 'value'); + collectionQuery = conseiljs.ConseilQueryBuilder.addFields(collectionQuery, 'key', 'value', 'operation_group_id'); 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)}`, @@ -46,10 +45,14 @@ const getCollectionForAddress = async (address) => { 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) } + return { + 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) // NOTE: consider increasing this number somewhat + const queryChunks = chunkArray(collection.map(i => i.piece), 50) // NOTE: consider increasing this number somewhat const makeObjectQuery = (keys) => { let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery(); mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addFields(mintedObjectsQuery, 'key_hash', 'value'); @@ -71,17 +74,84 @@ const getCollectionForAddress = async (address) => { objectIpfsMap[objectId] = ipfsHash })))) - collection = collection.map(i => { return { - ipfsHash: objectIpfsMap[i.piece.toString()], - ...i + 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) => { - let hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addFields(hDaoBalanceQuery, 'value'); + 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` @@ -92,7 +162,7 @@ const gethDaoBalanceForAddress = async (address) => { let balance = 0 try { - const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData({ url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'big_map_contents', hDaoBalanceQuery); + 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}`) @@ -101,6 +171,157 @@ 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. * @@ -108,8 +329,8 @@ const gethDaoBalanceForAddress = async (address) => { * @returns */ const getArtisticOutputForAddress = async (address) => { - let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields(mintOperationQuery, 'operation_group_hash'); + 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']) @@ -123,14 +344,14 @@ const getArtisticOutputForAddress = async (address) => { { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 'mainnet', 'operations', - mintOperationQuery); + 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'); + 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) @@ -153,14 +374,17 @@ 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, '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, 'timestamp', conseiljs.ConseilOperator.AFTER, [d.getTime()]) //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']) - mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit(mintOperationQuery, 30_000) + mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering(mintOperationQuery, 'block_level', conseiljs.ConseilSortDirection.DESC) + mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit(mintOperationQuery, 2500) const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, @@ -181,7 +405,7 @@ const getArtisticUniverse = async (max_time) => { }) let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery(); - swapsQuery = conseiljs.ConseilQueryBuilder.addFields(swapsQuery, 'key', 'value'); + 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, 30_000) // NOTE, limited to 30_000 @@ -239,13 +463,48 @@ 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'); @@ -304,5 +563,8 @@ module.exports = { getArtisticOutputForAddress, getObjectById, getArtisticUniverse, - hDAOFeed + getFeaturedArtisticUniverse, + hDAOFeed, + getRecommendedCurateDefault, + getObjektOwners } diff --git a/index.js b/index.js index 9092b38..5c68d2a 100644 --- a/index.js +++ b/index.js @@ -9,128 +9,26 @@ const { random } = require('lodash') const BURN_ADDRESS = 'tz1burnburnburnburnburnburnburjAYjjX' require('dotenv').config() +const { Semaphore } = require('prex') 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 owners = await conseilUtil.getObjektOwners(obj.token_id) 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; @@ -144,29 +42,30 @@ const randomFeed = async (counter, res) => { 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 getFeed = async (counter, featured) => { /* 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) */ - console.log('feed') - var arr = await conseilUtil.getArtisticUniverse(0) + console.log(`feed, featured: ${featured}`) + var arr + if (featured) { + arr = await conseilUtil.getFeaturedArtisticUniverse(0) + } else { + arr = await conseilUtil.getArtisticUniverse(0) + } var feed = offset(desc(arr), counter) - console.log(feed) + // console.log(feed) feed = await feed.map(async e => { e.token_info = await getIpfsHash(e.ipfsHash) e.token_id = parseInt(e.objectId) @@ -182,18 +81,16 @@ const getFeed = async (counter, res) => { cache_time = (int)(((max_time + ONE_MINUTE_MILLIS) - now_time) / 1000) } */ var promise = Promise.all(feed.map(e => e)) - promise.then(async (results) => { + return 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 }) + // console.log(aux_arr) + return 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() @@ -224,6 +121,8 @@ const getTzLedger = async (tz, res) => { return arr })) + validCreations = validCreations.sort((a, b) => parseInt(b.objectId) - parseInt(a.objectId)) + var arr = [] console.log([...collection, ...validCreations]) var arr = [...collection, ...validCreations] @@ -252,8 +151,6 @@ const getTzLedger = async (tz, res) => { hdao: hdao }) }) - - //return tzLedger } const getObjktById = async (id, res) => { @@ -264,20 +161,6 @@ const getObjktById = async (id, res) => { 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) => { @@ -302,29 +185,86 @@ const hDAOFeed = async (counter, res) => { }) } -//getObjkts() -//testSwaps() -//getFeed(1) -//getTzLedger('tz1UBZUkXpKGhYsP5KtzDNqLLchwF4uHrGjw') -//getObjktById(15306) -//const test2 = async () => console.log(await getObjktLedger()) -//test2() +// list of restricted addresses +const restrictedAdddressesCacheTimeLimit = ONE_MINUTE_MILLIS // the blockchain updates about once a minute +let restrictedAddressesCache = null +const restrictedAddressesLock = new Semaphore(1) +const getRestrictedAddresses = async () => { + await restrictedAddressesLock.wait() + if (restrictedAddressesCache && Date.now() - restrictedAddressesCache.expires < restrictedAdddressesCacheTimeLimit) { + restrictedAddressesLock.release() + // console.log('ADDRESS restrictions from CACHE') + return restrictedAddressesCache.data + } + + const list = await axios.get('https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/w.json').then(res => res.data) + restrictedAddressesCache = { + expires: Date.now(), + data: list + } + restrictedAddressesLock.release() + // console.log('ADDRESS restrictions from NEW') + return list +} + +// list of restricted objkts +const restrictedObjectsCacheTimeLimit = ONE_MINUTE_MILLIS // the blockchain updates about once a minute +let restrictedObjectsCache = null +const restrictedObjectsLock = new Semaphore(1) +const getRestrictedObjkts = async () => { + await restrictedObjectsLock.wait() + if (restrictedObjectsCache && Date.now() - restrictedObjectsCache.expires < restrictedObjectsCacheTimeLimit) { + restrictedObjectsLock.release() + // console.log('OBJKT restrictions from CACHE') + return restrictedObjectsCache.data + } + + const list = await axios.get('https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/o.json').then(res => res.data) + restrictedObjectsCache = { + expires: Date.now(), + data: list + } + restrictedObjectsLock.release() + return list +} 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) +// used for very simple caching of the feed +const feedCacheTimeLimit = ONE_MINUTE_MILLIS // the blockchain updates about once a minute +const feedCache = {} +const feedLocks = {} + +const getFeedLock = (key) => { + if (!feedLocks[key]) { + feedLocks[key] = new Semaphore(1) + } + return feedLocks[key] +} + +app.post('/feed|/featured', async (req, res) => { + const feedOffset = req.body.counter || 0 + const isFeatured = req.path === '/featured' + const lockKey = `${feedOffset}-${isFeatured ? 'featured' : ''}` + + await getFeedLock(lockKey).wait() + if (feedCache[lockKey] && Date.now() - feedCache[lockKey].expires < feedCacheTimeLimit) { + getFeedLock(lockKey).release() + // console.log('Feed from CACHE') + return res.json({ result: feedCache[lockKey].data }) + } + + const aux_arr = await getFeed(feedOffset, isFeatured) + feedCache[lockKey] = { + expires: Date.now(), + data: aux_arr + } + getFeedLock(lockKey).release() + // console.log('Feed from NEW') + return res.json({ result: aux_arr }) }) app.post('/random', async (req, res) => { @@ -334,7 +274,7 @@ app.post('/random', async (req, 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) + var list = await getRestrictedAddresses() list.includes(req.body.tz) ? @@ -347,7 +287,7 @@ app.post('/tz', async (req, 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) + var list = await getRestrictedObjkts() list.includes(parseInt(req.body.objkt_id)) ? @@ -356,13 +296,20 @@ app.post('/objkt', async (req, res) => { res.json({ result: await getObjktById(req.body.objkt_id) }) }) +app.get('/recommend_curate', async (req, res) => { + const amt = await conseilUtil.getRecommendedCurateDefault() + res.set('Cache-Control', `public, max-age=300`) + res.json({ amount: amt }) +}) + app.post('/hdao', async (req, res) => { await hDAOFeed(parseInt(req.body.counter), res) }) -const testhdao = async () => await hDAOFeed(parseInt(0)) +// const testhdao = async () => await hDAOFeed(parseInt(0)) //testhdao() //app.listen(3001) +console.log('SERVER RUNNING ON localhost:3001') module.exports.handler = serverless(app) diff --git a/package.json b/package.json index aebde7f..7a43524 100644 --- a/package.json +++ b/package.json @@ -1,35 +1,39 @@ { - "name": "hicetnunc-apiv2", - "version": "1.0.0", - "description": "", - "main": "index.js", - "scripts": { - "start": "node index.js" - }, - "repository": { - "type": "git", - "url": "git+https://github.com/hicetnunc2000/hicetnunc.git" - }, - "author": "@hicetnunc2000", - "license": "MIT", - "bugs": { - "url": "https://github.com/hicetnunc2000/hicetnunc/issues" - }, - "homepage": "https://github.com/hicetnunc2000/hicetnunc#readme", - "dependencies": { - "axios": "^0.21.1", - "conseiljs": "5.0.7-2", - "cors": "^2.8.5", - "dotenv": "^8.2.0", - "express": "^4.17.1", - "lodash": "^4.17.21", - "loglevel": "1.7.1", - "node-fetch": "2.6.1", - "serverless-dotenv-plugin": "^3.8.1", - "serverless-http": "^2.7.0" - }, - "engines": { - "node": "12.20.1", - "npm": "6.14.10" - } + "name": "hicetnunc-apiv2", + "version": "1.0.0", + "description": "", + "main": "index.js", + "scripts": { + "start": "node index.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/hicetnunc2000/hicetnunc-api.git" + }, + "author": "@hicetnunc2000", + "license": "MIT", + "bugs": { + "url": "https://github.com/hicetnunc2000/hicetnunc-api/issues" + }, + "homepage": "https://github.com/hicetnunc2000/hicetnunc-api#readme", + "dependencies": { + "axios": "^0.21.1", + "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", + "prex": "^0.4.7", + "serverless-dotenv-plugin": "^3.8.1", + "serverless-http": "^2.7.0" + }, + "engines": { + "node": "12.20.1", + "npm": "6.14.10" + } }