├── .env ├── .env.prod ├── .eslintrc ├── .gitignore ├── .node-version ├── .nvmrc ├── .prettierrc ├── .serverless ├── cloudformation-template-create-stack.json ├── cloudformation-template-update-stack.json ├── hicetnunc-apiv2.zip └── serverless-state.json ├── README.md ├── index.js ├── lib ├── config.js ├── conseil.js ├── router │ ├── readFeed.js │ ├── readHdaoFeed.js │ ├── readIssuer.js │ ├── readObjkt.js │ ├── readRandomFeed.js │ ├── readRecommendCurate.js │ └── router.js ├── swagger.js └── utils.js ├── package.json ├── serverless.yml └── swagger-output.json /.env: -------------------------------------------------------------------------------- 1 | TIMESTAMP='' 2 | BUFFER='' 3 | -------------------------------------------------------------------------------- /.env.prod: -------------------------------------------------------------------------------- 1 | REACT_APP_FEED='http://localhost:3001/feed' 2 | REACT_APP_TZ='http://localhost:3001/tz' 3 | REACT_APP_OBJKT='http://localhost:3001/objkt' 4 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "eslint:recommended", 3 | "env": { 4 | "node": true, 5 | "es6": true 6 | }, 7 | "parserOptions": { 8 | "ecmaVersion": 8 9 | } 10 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | package-lock.json 3 | -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 12.20.1 -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 12.20.1 -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "semi": false 4 | } -------------------------------------------------------------------------------- /.serverless/cloudformation-template-create-stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "ServerlessDeploymentBucket": { 6 | "Type": "AWS::S3::Bucket", 7 | "Properties": { 8 | "BucketEncryption": { 9 | "ServerSideEncryptionConfiguration": [ 10 | { 11 | "ServerSideEncryptionByDefault": { 12 | "SSEAlgorithm": "AES256" 13 | } 14 | } 15 | ] 16 | } 17 | } 18 | }, 19 | "ServerlessDeploymentBucketPolicy": { 20 | "Type": "AWS::S3::BucketPolicy", 21 | "Properties": { 22 | "Bucket": { 23 | "Ref": "ServerlessDeploymentBucket" 24 | }, 25 | "PolicyDocument": { 26 | "Statement": [ 27 | { 28 | "Action": "s3:*", 29 | "Effect": "Deny", 30 | "Principal": "*", 31 | "Resource": [ 32 | { 33 | "Fn::Join": [ 34 | "", 35 | [ 36 | "arn:", 37 | { 38 | "Ref": "AWS::Partition" 39 | }, 40 | ":s3:::", 41 | { 42 | "Ref": "ServerlessDeploymentBucket" 43 | }, 44 | "/*" 45 | ] 46 | ] 47 | } 48 | ], 49 | "Condition": { 50 | "Bool": { 51 | "aws:SecureTransport": false 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | } 59 | }, 60 | "Outputs": { 61 | "ServerlessDeploymentBucketName": { 62 | "Value": { 63 | "Ref": "ServerlessDeploymentBucket" 64 | } 65 | } 66 | } 67 | } -------------------------------------------------------------------------------- /.serverless/cloudformation-template-update-stack.json: -------------------------------------------------------------------------------- 1 | { 2 | "AWSTemplateFormatVersion": "2010-09-09", 3 | "Description": "The AWS CloudFormation template for this Serverless application", 4 | "Resources": { 5 | "ServerlessDeploymentBucket": { 6 | "Type": "AWS::S3::Bucket", 7 | "Properties": { 8 | "BucketEncryption": { 9 | "ServerSideEncryptionConfiguration": [ 10 | { 11 | "ServerSideEncryptionByDefault": { 12 | "SSEAlgorithm": "AES256" 13 | } 14 | } 15 | ] 16 | } 17 | } 18 | }, 19 | "ServerlessDeploymentBucketPolicy": { 20 | "Type": "AWS::S3::BucketPolicy", 21 | "Properties": { 22 | "Bucket": { 23 | "Ref": "ServerlessDeploymentBucket" 24 | }, 25 | "PolicyDocument": { 26 | "Statement": [ 27 | { 28 | "Action": "s3:*", 29 | "Effect": "Deny", 30 | "Principal": "*", 31 | "Resource": [ 32 | { 33 | "Fn::Join": [ 34 | "", 35 | [ 36 | "arn:", 37 | { 38 | "Ref": "AWS::Partition" 39 | }, 40 | ":s3:::", 41 | { 42 | "Ref": "ServerlessDeploymentBucket" 43 | }, 44 | "/*" 45 | ] 46 | ] 47 | } 48 | ], 49 | "Condition": { 50 | "Bool": { 51 | "aws:SecureTransport": false 52 | } 53 | } 54 | } 55 | ] 56 | } 57 | } 58 | }, 59 | "HandlerLogGroup": { 60 | "Type": "AWS::Logs::LogGroup", 61 | "Properties": { 62 | "LogGroupName": "/aws/lambda/hicetnunc-apiv2-dev-handler" 63 | } 64 | }, 65 | "IamRoleLambdaExecution": { 66 | "Type": "AWS::IAM::Role", 67 | "Properties": { 68 | "AssumeRolePolicyDocument": { 69 | "Version": "2012-10-17", 70 | "Statement": [ 71 | { 72 | "Effect": "Allow", 73 | "Principal": { 74 | "Service": [ 75 | "lambda.amazonaws.com" 76 | ] 77 | }, 78 | "Action": [ 79 | "sts:AssumeRole" 80 | ] 81 | } 82 | ] 83 | }, 84 | "Policies": [ 85 | { 86 | "PolicyName": { 87 | "Fn::Join": [ 88 | "-", 89 | [ 90 | "hicetnunc-apiv2", 91 | "dev", 92 | "lambda" 93 | ] 94 | ] 95 | }, 96 | "PolicyDocument": { 97 | "Version": "2012-10-17", 98 | "Statement": [ 99 | { 100 | "Effect": "Allow", 101 | "Action": [ 102 | "logs:CreateLogStream", 103 | "logs:CreateLogGroup" 104 | ], 105 | "Resource": [ 106 | { 107 | "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/hicetnunc-apiv2-dev*:*" 108 | } 109 | ] 110 | }, 111 | { 112 | "Effect": "Allow", 113 | "Action": [ 114 | "logs:PutLogEvents" 115 | ], 116 | "Resource": [ 117 | { 118 | "Fn::Sub": "arn:${AWS::Partition}:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/hicetnunc-apiv2-dev*:*:*" 119 | } 120 | ] 121 | } 122 | ] 123 | } 124 | } 125 | ], 126 | "Path": "/", 127 | "RoleName": { 128 | "Fn::Join": [ 129 | "-", 130 | [ 131 | "hicetnunc-apiv2", 132 | "dev", 133 | { 134 | "Ref": "AWS::Region" 135 | }, 136 | "lambdaRole" 137 | ] 138 | ] 139 | } 140 | } 141 | }, 142 | "HandlerLambdaFunction": { 143 | "Type": "AWS::Lambda::Function", 144 | "Properties": { 145 | "Code": { 146 | "S3Bucket": { 147 | "Ref": "ServerlessDeploymentBucket" 148 | }, 149 | "S3Key": "serverless/hicetnunc-apiv2/dev/1618393133304-2021-04-14T09:38:53.304Z/hicetnunc-apiv2.zip" 150 | }, 151 | "FunctionName": "hicetnunc-apiv2-dev-handler", 152 | "Handler": "index.handler", 153 | "MemorySize": 1024, 154 | "Role": { 155 | "Fn::GetAtt": [ 156 | "IamRoleLambdaExecution", 157 | "Arn" 158 | ] 159 | }, 160 | "Runtime": "nodejs12.x", 161 | "Timeout": 120, 162 | "Environment": { 163 | "Variables": { 164 | "TIMESTAMP": "", 165 | "BUFFER": "" 166 | } 167 | } 168 | }, 169 | "DependsOn": [ 170 | "HandlerLogGroup" 171 | ] 172 | }, 173 | "HandlerLambdaVersionGCmXsMwAYUClY3fyWghMQxBCM2bIsW7KlcyxYdPbQ": { 174 | "Type": "AWS::Lambda::Version", 175 | "DeletionPolicy": "Retain", 176 | "Properties": { 177 | "FunctionName": { 178 | "Ref": "HandlerLambdaFunction" 179 | }, 180 | "CodeSha256": "ZOi7Fhm1n1KnXKePGawp2Qh0OfzV08fAdVyBW2wiSTI=" 181 | } 182 | }, 183 | "ApiGatewayRestApi": { 184 | "Type": "AWS::ApiGateway::RestApi", 185 | "Properties": { 186 | "Name": "dev-hicetnunc-apiv2", 187 | "EndpointConfiguration": { 188 | "Types": [ 189 | "EDGE" 190 | ] 191 | }, 192 | "Policy": "" 193 | } 194 | }, 195 | "ApiGatewayResourceProxyVar": { 196 | "Type": "AWS::ApiGateway::Resource", 197 | "Properties": { 198 | "ParentId": { 199 | "Fn::GetAtt": [ 200 | "ApiGatewayRestApi", 201 | "RootResourceId" 202 | ] 203 | }, 204 | "PathPart": "{proxy+}", 205 | "RestApiId": { 206 | "Ref": "ApiGatewayRestApi" 207 | } 208 | } 209 | }, 210 | "ApiGatewayMethodOptions": { 211 | "Type": "AWS::ApiGateway::Method", 212 | "Properties": { 213 | "AuthorizationType": "NONE", 214 | "HttpMethod": "OPTIONS", 215 | "MethodResponses": [ 216 | { 217 | "StatusCode": "200", 218 | "ResponseParameters": { 219 | "method.response.header.Access-Control-Allow-Origin": true, 220 | "method.response.header.Access-Control-Allow-Headers": true, 221 | "method.response.header.Access-Control-Allow-Methods": true 222 | }, 223 | "ResponseModels": {} 224 | } 225 | ], 226 | "RequestParameters": {}, 227 | "Integration": { 228 | "Type": "MOCK", 229 | "RequestTemplates": { 230 | "application/json": "{statusCode:200}" 231 | }, 232 | "ContentHandling": "CONVERT_TO_TEXT", 233 | "IntegrationResponses": [ 234 | { 235 | "StatusCode": "200", 236 | "ResponseParameters": { 237 | "method.response.header.Access-Control-Allow-Origin": "'*'", 238 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", 239 | "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT'" 240 | }, 241 | "ResponseTemplates": { 242 | "application/json": "" 243 | } 244 | } 245 | ] 246 | }, 247 | "ResourceId": { 248 | "Fn::GetAtt": [ 249 | "ApiGatewayRestApi", 250 | "RootResourceId" 251 | ] 252 | }, 253 | "RestApiId": { 254 | "Ref": "ApiGatewayRestApi" 255 | } 256 | } 257 | }, 258 | "ApiGatewayMethodProxyVarOptions": { 259 | "Type": "AWS::ApiGateway::Method", 260 | "Properties": { 261 | "AuthorizationType": "NONE", 262 | "HttpMethod": "OPTIONS", 263 | "MethodResponses": [ 264 | { 265 | "StatusCode": "200", 266 | "ResponseParameters": { 267 | "method.response.header.Access-Control-Allow-Origin": true, 268 | "method.response.header.Access-Control-Allow-Headers": true, 269 | "method.response.header.Access-Control-Allow-Methods": true 270 | }, 271 | "ResponseModels": {} 272 | } 273 | ], 274 | "RequestParameters": {}, 275 | "Integration": { 276 | "Type": "MOCK", 277 | "RequestTemplates": { 278 | "application/json": "{statusCode:200}" 279 | }, 280 | "ContentHandling": "CONVERT_TO_TEXT", 281 | "IntegrationResponses": [ 282 | { 283 | "StatusCode": "200", 284 | "ResponseParameters": { 285 | "method.response.header.Access-Control-Allow-Origin": "'*'", 286 | "method.response.header.Access-Control-Allow-Headers": "'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token,X-Amz-User-Agent'", 287 | "method.response.header.Access-Control-Allow-Methods": "'OPTIONS,DELETE,GET,HEAD,PATCH,POST,PUT'" 288 | }, 289 | "ResponseTemplates": { 290 | "application/json": "" 291 | } 292 | } 293 | ] 294 | }, 295 | "ResourceId": { 296 | "Ref": "ApiGatewayResourceProxyVar" 297 | }, 298 | "RestApiId": { 299 | "Ref": "ApiGatewayRestApi" 300 | } 301 | } 302 | }, 303 | "ApiGatewayMethodAny": { 304 | "Type": "AWS::ApiGateway::Method", 305 | "Properties": { 306 | "HttpMethod": "ANY", 307 | "RequestParameters": {}, 308 | "ResourceId": { 309 | "Fn::GetAtt": [ 310 | "ApiGatewayRestApi", 311 | "RootResourceId" 312 | ] 313 | }, 314 | "RestApiId": { 315 | "Ref": "ApiGatewayRestApi" 316 | }, 317 | "ApiKeyRequired": false, 318 | "AuthorizationType": "NONE", 319 | "Integration": { 320 | "IntegrationHttpMethod": "POST", 321 | "Type": "AWS_PROXY", 322 | "Uri": { 323 | "Fn::Join": [ 324 | "", 325 | [ 326 | "arn:", 327 | { 328 | "Ref": "AWS::Partition" 329 | }, 330 | ":apigateway:", 331 | { 332 | "Ref": "AWS::Region" 333 | }, 334 | ":lambda:path/2015-03-31/functions/", 335 | { 336 | "Fn::GetAtt": [ 337 | "HandlerLambdaFunction", 338 | "Arn" 339 | ] 340 | }, 341 | "/invocations" 342 | ] 343 | ] 344 | } 345 | }, 346 | "MethodResponses": [] 347 | } 348 | }, 349 | "ApiGatewayMethodProxyVarAny": { 350 | "Type": "AWS::ApiGateway::Method", 351 | "Properties": { 352 | "HttpMethod": "ANY", 353 | "RequestParameters": {}, 354 | "ResourceId": { 355 | "Ref": "ApiGatewayResourceProxyVar" 356 | }, 357 | "RestApiId": { 358 | "Ref": "ApiGatewayRestApi" 359 | }, 360 | "ApiKeyRequired": false, 361 | "AuthorizationType": "NONE", 362 | "Integration": { 363 | "IntegrationHttpMethod": "POST", 364 | "Type": "AWS_PROXY", 365 | "Uri": { 366 | "Fn::Join": [ 367 | "", 368 | [ 369 | "arn:", 370 | { 371 | "Ref": "AWS::Partition" 372 | }, 373 | ":apigateway:", 374 | { 375 | "Ref": "AWS::Region" 376 | }, 377 | ":lambda:path/2015-03-31/functions/", 378 | { 379 | "Fn::GetAtt": [ 380 | "HandlerLambdaFunction", 381 | "Arn" 382 | ] 383 | }, 384 | "/invocations" 385 | ] 386 | ] 387 | } 388 | }, 389 | "MethodResponses": [] 390 | } 391 | }, 392 | "ApiGatewayDeployment1618393127899": { 393 | "Type": "AWS::ApiGateway::Deployment", 394 | "Properties": { 395 | "RestApiId": { 396 | "Ref": "ApiGatewayRestApi" 397 | }, 398 | "StageName": "dev" 399 | }, 400 | "DependsOn": [ 401 | "ApiGatewayMethodOptions", 402 | "ApiGatewayMethodProxyVarOptions", 403 | "ApiGatewayMethodAny", 404 | "ApiGatewayMethodProxyVarAny" 405 | ] 406 | }, 407 | "HandlerLambdaPermissionApiGateway": { 408 | "Type": "AWS::Lambda::Permission", 409 | "Properties": { 410 | "FunctionName": { 411 | "Fn::GetAtt": [ 412 | "HandlerLambdaFunction", 413 | "Arn" 414 | ] 415 | }, 416 | "Action": "lambda:InvokeFunction", 417 | "Principal": "apigateway.amazonaws.com", 418 | "SourceArn": { 419 | "Fn::Join": [ 420 | "", 421 | [ 422 | "arn:", 423 | { 424 | "Ref": "AWS::Partition" 425 | }, 426 | ":execute-api:", 427 | { 428 | "Ref": "AWS::Region" 429 | }, 430 | ":", 431 | { 432 | "Ref": "AWS::AccountId" 433 | }, 434 | ":", 435 | { 436 | "Ref": "ApiGatewayRestApi" 437 | }, 438 | "/*/*" 439 | ] 440 | ] 441 | } 442 | } 443 | } 444 | }, 445 | "Outputs": { 446 | "ServerlessDeploymentBucketName": { 447 | "Value": { 448 | "Ref": "ServerlessDeploymentBucket" 449 | } 450 | }, 451 | "HandlerLambdaFunctionQualifiedArn": { 452 | "Description": "Current Lambda function version", 453 | "Value": { 454 | "Ref": "HandlerLambdaVersionGCmXsMwAYUClY3fyWghMQxBCM2bIsW7KlcyxYdPbQ" 455 | } 456 | }, 457 | "ServiceEndpoint": { 458 | "Description": "URL of the service endpoint", 459 | "Value": { 460 | "Fn::Join": [ 461 | "", 462 | [ 463 | "https://", 464 | { 465 | "Ref": "ApiGatewayRestApi" 466 | }, 467 | ".execute-api.", 468 | { 469 | "Ref": "AWS::Region" 470 | }, 471 | ".", 472 | { 473 | "Ref": "AWS::URLSuffix" 474 | }, 475 | "/dev" 476 | ] 477 | ] 478 | } 479 | } 480 | } 481 | } -------------------------------------------------------------------------------- /.serverless/hicetnunc-apiv2.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hicetnunc2000/hicetnunc-api/6a9b34cdfc9af4ab09b30ae196ee89b068be255c/.serverless/hicetnunc-apiv2.zip -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hicetnunc API 2 | 3 | API backend for the [hicetnunc.xyz](https://hicetnunc.xyz) NFT Art House deployed on the Tezos blockchain. Used in conjunction with [hicetnunc-ui](https://github.com/hicetnunc2000/hicetnunc). This code has been tested with @nodejs 12.20.1 and @npm 6.14.10. 4 | 5 | Please take a look at the [issues list](https://github.com/hicetnunc2000/hicetnunc-api/issues) for contributions. 6 | 7 | Licensed under MIT license. 8 | 9 | Built with APIs from @Cryptonomic and @baking-bad. 10 | 11 | ## Running 12 | 13 | ```bash 14 | npm install -g serverless@2.8.0 15 | npm i 16 | npm start 17 | ``` 18 | 19 | ``` 20 | POST /feed :counter 21 | POST /tz :tz 22 | POST /objkt :objkt_id 23 | POST /hdao :counter 24 | ``` 25 | 26 | ## API documentation 27 | 28 | Swagger docs generated using [swagger-autogen](https://github.com/davibaltar/swagger-autogen). These allow you to test and view the API responses. 29 | 30 | ``` 31 | GET /docs 32 | ``` 33 | 34 | If you add or amend the API endpoints, please also update the swagger definitions, and regenerate the docs: 35 | 36 | ``` 37 | npm run swagger-autogen 38 | ``` -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require('module').Module._initPaths() 4 | require('dotenv').config() 5 | 6 | const cors = require('cors') 7 | const express = require('express') 8 | const compression = require('compression') 9 | const router = require('./lib/router/router') 10 | const serverless = require('serverless-http') 11 | 12 | const { serverPort: PORT } = require('./lib/config') 13 | 14 | const app = express() 15 | 16 | app.use(compression()) 17 | app.use(express.json()) 18 | app.use(cors({ origin: '*' })) 19 | app.use(router) 20 | 21 | if (process.env.NODE_ENV === 'development') { 22 | app.listen(PORT, () => { 23 | console.log(`SERVER RUNNING ON localhost:${PORT}`) 24 | }) 25 | } 26 | module.exports.handler = serverless(app) 27 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | module.exports = { 4 | burnAddress: 5 | process.env.BURN_ADDRESS || 'tz1burnburnburnburnburnburnburjAYjjX', 6 | feedItemsPerPage: process.env.FEED_ITEMS_PER_PAGE || 30, 7 | networkConfig: { 8 | network: 'mainnet', 9 | nftContract: 'KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton', 10 | hDAOToken: 'KT1AFA2mwNUMNd4SsujE1YYp29vd8BZejyKW', 11 | curations: 'KT1TybhR7XraG75JFYKSrh7KnxukMBT5dor6', 12 | protocol: 'KT1Hkg5qeNhfwpKW4fXvq7HGZB9z2EnmCCA9', 13 | nftLedger: 511, 14 | nftMetadataMap: 514, 15 | nftSwapMap: 523, 16 | curationsPtr: 519, 17 | nftRoyaltiesMap: 522, 18 | daoLedger: 515, 19 | kolibriLedger: 380, 20 | hDaoSwap: "KT1Qm3urGqkRsWsovGzNb2R81c3dSfxpteHG", 21 | kolibriSwap: "KT1K4EwTpbvYN9agJdjpyJm4ZZdhpUNKB3F6", 22 | }, 23 | serverPort: 3001, 24 | } 25 | -------------------------------------------------------------------------------- /lib/conseil.js: -------------------------------------------------------------------------------- 1 | const conseiljs = require('conseiljs') 2 | const fetch = require('node-fetch') 3 | const log = require('loglevel') 4 | const BigNumber = require('bignumber.js') 5 | const axios = require('axios') 6 | const _ = require('lodash') 7 | const Bottleneck = require("bottleneck/es5"); 8 | 9 | const logger = log.getLogger('conseiljs') 10 | logger.setLevel('error', false) 11 | conseiljs.registerLogger(logger) 12 | conseiljs.registerFetch(fetch) 13 | const conseilServer = 'https://conseil-prod.cryptonomic-infra.tech' 14 | const conseilApiKey = 'aa73fa8a-8626-4f43-a605-ff63130f37b1' // signup at nautilus.cloud 15 | const mainnet = require('./config').networkConfig 16 | // In order to speed up queries we need to limit some feed results to only new 17 | // items and transactions :( 18 | const LATEST_EPOCH = 1424923 19 | const MILLISECOND_MODIFIER = 1000 20 | const ONE_MINUTE_MILLIS = 60 * MILLISECOND_MODIFIER 21 | const ONE_HOUR_MILLIS = 60 * ONE_MINUTE_MILLIS 22 | const ONE_DAY_MILLIS = 24 * ONE_HOUR_MILLIS 23 | const ONE_WEEK_MILLIS = 7 * ONE_DAY_MILLIS 24 | 25 | Array.prototype.sum = Array.prototype.sum || function (){ 26 | return this.reduce(function(p,c){return p+c},0); 27 | }; 28 | 29 | Array.prototype.avg = Array.prototype.avg || function () { 30 | return this.sum()/this.length; 31 | }; 32 | 33 | const _getMinTime = () => { 34 | var d = new Date() 35 | d.setDate(d.getDate() - 14) 36 | return d.getTime() 37 | } 38 | 39 | const _subtractWeek = (timestamp) => { 40 | return timestamp - ONE_WEEK_MILLIS 41 | } 42 | 43 | const _subtractDays = (timestamp, days) => { 44 | return timestamp - (ONE_DAY_MILLIS * days) 45 | } 46 | 47 | const _subtractHours = (timestamp, hours) => { 48 | return timestamp - (ONE_HOUR_MILLIS * hours) 49 | } 50 | 51 | const _floorTimeToMinute = (currentTime) => { 52 | return Math.floor(currentTime / ONE_MINUTE_MILLIS) * ONE_MINUTE_MILLIS 53 | } 54 | 55 | const _getTimeQuantizedToMinute = () => { 56 | return _floorTimeToMinute((new Date()).getTime()) 57 | } 58 | 59 | 60 | async function getBackendConfig() { 61 | return ( 62 | await axios.get( 63 | 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/config/backend_vars.json' 64 | ) 65 | ).data 66 | } 67 | 68 | 69 | const hDAOFeed = async () => { 70 | let hDAOQuery = conseiljs.ConseilQueryBuilder.blankQuery() 71 | hDAOQuery = conseiljs.ConseilQueryBuilder.addFields(hDAOQuery, 'key', 'value') 72 | hDAOQuery = conseiljs.ConseilQueryBuilder.addPredicate( 73 | hDAOQuery, 74 | 'big_map_id', 75 | conseiljs.ConseilOperator.EQ, 76 | [mainnet.curationsPtr] 77 | ) 78 | hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 300_000) 79 | 80 | let hDAOResult = await conseiljs.TezosConseilClient.getTezosEntityData( 81 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 82 | 'mainnet', 83 | 'big_map_contents', 84 | hDAOQuery 85 | ) 86 | return hDAOResult.map((e) => { 87 | return { 88 | token_id: parseInt(e.key), 89 | hDAO_balance: parseInt(e.value.split(' ')[1]), 90 | } 91 | }) 92 | } 93 | 94 | const getObjkthDAOBalance = async (token_id) => { 95 | token_id = (typeof token_id === 'string') ? token_id : token_id.toString() 96 | let hDAOQuery = conseiljs.ConseilQueryBuilder.blankQuery() 97 | hDAOQuery = conseiljs.ConseilQueryBuilder.addFields(hDAOQuery, 'key', 'value') 98 | hDAOQuery = conseiljs.ConseilQueryBuilder.addPredicate( 99 | hDAOQuery, 100 | 'big_map_id', 101 | conseiljs.ConseilOperator.EQ, 102 | [mainnet.curationsPtr] 103 | ) 104 | hDAOQuery = conseiljs.ConseilQueryBuilder.addPredicate(hDAOQuery, 'key', conseiljs.ConseilOperator.EQ, [token_id]) 105 | hDAOQuery = conseiljs.ConseilQueryBuilder.setLimit(hDAOQuery, 1) 106 | 107 | let hDAOResult = await conseiljs.TezosConseilClient.getTezosEntityData( 108 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 109 | 'mainnet', 110 | 'big_map_contents', 111 | hDAOQuery 112 | ) 113 | if (hDAOResult.length == 1) { 114 | return hDAOResult.map((e) => { 115 | return parseInt(e.value.split(' ')[1]) 116 | })[0] 117 | } 118 | return 0 119 | } 120 | 121 | /** 122 | * Returns a list of nft token ids and amounts that a given address owns. 123 | * 124 | * @param {string} address 125 | * @returns 126 | */ 127 | const getCollectionForAddress = async (address) => { 128 | let collectionQuery = conseiljs.ConseilQueryBuilder.blankQuery() 129 | collectionQuery = conseiljs.ConseilQueryBuilder.addFields( 130 | collectionQuery, 131 | 'key', 132 | 'value', 133 | 'operation_group_id' 134 | ) 135 | collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate( 136 | collectionQuery, 137 | 'big_map_id', 138 | conseiljs.ConseilOperator.EQ, 139 | [mainnet.nftLedger] 140 | ) 141 | collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate( 142 | collectionQuery, 143 | 'key', 144 | conseiljs.ConseilOperator.STARTSWITH, 145 | [`Pair 0x${conseiljs.TezosMessageUtils.writeAddress(address)}`] 146 | ) 147 | collectionQuery = conseiljs.ConseilQueryBuilder.addPredicate( 148 | collectionQuery, 149 | 'value', 150 | conseiljs.ConseilOperator.EQ, 151 | [0], 152 | true 153 | ) 154 | collectionQuery = conseiljs.ConseilQueryBuilder.setLimit( 155 | collectionQuery, 156 | 300_000 157 | ) 158 | 159 | const collectionResult = await conseiljs.TezosConseilClient.getTezosEntityData( 160 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 161 | 'mainnet', 162 | 'big_map_contents', 163 | collectionQuery 164 | ) 165 | let collection = collectionResult.map((i) => { 166 | return { 167 | piece: i['key'].toString().replace(/.* ([0-9]{1,}$)/, '$1'), 168 | amount: Number(i['value']), 169 | opId: i['operation_group_id'], 170 | } 171 | }) 172 | 173 | const queryChunks = chunkArray( 174 | collection.map((i) => i.piece), 175 | 50 176 | ) // NOTE: consider increasing this number somewhat 177 | const makeObjectQuery = (keys) => { 178 | let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery() 179 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addFields( 180 | mintedObjectsQuery, 181 | 'key_hash', 182 | 'value' 183 | ) 184 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 185 | mintedObjectsQuery, 186 | 'big_map_id', 187 | conseiljs.ConseilOperator.EQ, 188 | [mainnet.nftMetadataMap] 189 | ) 190 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 191 | mintedObjectsQuery, 192 | 'key', 193 | keys.length > 1 194 | ? conseiljs.ConseilOperator.IN 195 | : conseiljs.ConseilOperator.EQ, 196 | keys 197 | ) 198 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.setLimit( 199 | mintedObjectsQuery, 200 | keys.length 201 | ) 202 | 203 | return mintedObjectsQuery 204 | } 205 | 206 | const objectQueries = queryChunks.map((c) => makeObjectQuery(c)) 207 | const objectIpfsMap = {} 208 | await Promise.all( 209 | objectQueries.map( 210 | async (q) => 211 | await conseiljs.TezosConseilClient.getTezosEntityData( 212 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 213 | 'mainnet', 214 | 'big_map_contents', 215 | q 216 | ).then((result) => 217 | result.map((row) => { 218 | const objectId = row['value'] 219 | .toString() 220 | .replace(/^Pair ([0-9]{1,}) .*/, '$1') 221 | const objectUrl = row['value'] 222 | .toString() 223 | .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') 224 | const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) 225 | 226 | objectIpfsMap[objectId] = ipfsHash 227 | }) 228 | ) 229 | ) 230 | ) 231 | 232 | const operationGroupIds = collectionResult.map((r) => r.operation_group_id) 233 | const priceQueryChunks = chunkArray(operationGroupIds, 30) 234 | const makeLastPriceQuery = (opIds) => { 235 | let lastPriceQuery = conseiljs.ConseilQueryBuilder.blankQuery() 236 | lastPriceQuery = conseiljs.ConseilQueryBuilder.addFields( 237 | lastPriceQuery, 238 | 'timestamp', 239 | 'amount', 240 | 'operation_group_hash', 241 | 'parameters_entrypoints', 242 | 'parameters' 243 | ) 244 | lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 245 | lastPriceQuery, 246 | 'kind', 247 | conseiljs.ConseilOperator.EQ, 248 | ['transaction'] 249 | ) 250 | lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 251 | lastPriceQuery, 252 | 'status', 253 | conseiljs.ConseilOperator.EQ, 254 | ['applied'] 255 | ) 256 | lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 257 | lastPriceQuery, 258 | 'internal', 259 | conseiljs.ConseilOperator.EQ, 260 | ['false'] 261 | ) 262 | lastPriceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 263 | lastPriceQuery, 264 | 'operation_group_hash', 265 | opIds.length > 1 266 | ? conseiljs.ConseilOperator.IN 267 | : conseiljs.ConseilOperator.EQ, 268 | opIds 269 | ) 270 | lastPriceQuery = conseiljs.ConseilQueryBuilder.setLimit( 271 | lastPriceQuery, 272 | opIds.length 273 | ) 274 | 275 | return lastPriceQuery 276 | } 277 | 278 | const priceQueries = priceQueryChunks.map((c) => makeLastPriceQuery(c)) 279 | const priceMap = {} 280 | await Promise.all( 281 | priceQueries.map( 282 | async (q) => 283 | await conseiljs.TezosConseilClient.getTezosEntityData( 284 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 285 | 'mainnet', 286 | 'operations', 287 | q 288 | ).then((result) => 289 | result.map((row) => { 290 | let amount = 0 291 | const action = row.parameters_entrypoints 292 | 293 | if (action === 'collect') { 294 | amount = Number( 295 | row.parameters.toString().replace(/^Pair ([0-9]+) [0-9]+/, '$1') 296 | ) 297 | } else if (action === 'transfer') { 298 | amount = Number( 299 | row.parameters 300 | .toString() 301 | .replace( 302 | /[{] Pair \"[1-9A-HJ-NP-Za-km-z]{36}\" [{] Pair \"[1-9A-HJ-NP-Za-km-z]{36}\" [(]Pair [0-9]+ [0-9]+[)] [}] [}]/, 303 | '$1' 304 | ) 305 | ) 306 | } 307 | 308 | priceMap[row.operation_group_hash] = { 309 | price: new BigNumber(row.amount), 310 | amount, 311 | timestamp: row.timestamp, 312 | action, 313 | } 314 | }) 315 | ) 316 | ) 317 | ) 318 | 319 | collection = collection.map((i) => { 320 | let price = 0 321 | let receivedOn = new Date() 322 | let action = '' 323 | 324 | try { 325 | const priceRecord = priceMap[i.opId] 326 | price = priceRecord.price 327 | .dividedToIntegerBy(priceRecord.amount) 328 | .toNumber() 329 | receivedOn = new Date(priceRecord.timestamp) 330 | action = priceRecord.action === 'collect' ? 'Purchased' : 'Received' 331 | } catch { 332 | // 333 | } 334 | 335 | delete i.opId 336 | 337 | return { 338 | price: isNaN(price) ? 0 : price, 339 | receivedOn, 340 | action, 341 | ipfsHash: objectIpfsMap[i.piece.toString()], 342 | ...i, 343 | } 344 | }) 345 | 346 | return collection.sort( 347 | (a, b) => b.receivedOn.getTime() - a.receivedOn.getTime() 348 | ) // sort descending by date – most-recently acquired art first 349 | } 350 | 351 | const gethDaoBalanceForAddress = async (address) => { 352 | console.log('gethDaoBalanceForAddress', address) 353 | let hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() 354 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( 355 | hDaoBalanceQuery, 356 | 'value' 357 | ) 358 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 359 | hDaoBalanceQuery, 360 | 'big_map_id', 361 | conseiljs.ConseilOperator.EQ, 362 | [mainnet.daoLedger] 363 | ) 364 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 365 | hDaoBalanceQuery, 366 | 'key', 367 | conseiljs.ConseilOperator.EQ, 368 | [`Pair 0x${conseiljs.TezosMessageUtils.writeAddress(address)} 0`] 369 | ) 370 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 371 | hDaoBalanceQuery, 372 | 'value', 373 | conseiljs.ConseilOperator.EQ, 374 | [0], 375 | true 376 | ) 377 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit(hDaoBalanceQuery, 1) 378 | 379 | let balance = 0 380 | 381 | try { 382 | const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( 383 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 384 | 'mainnet', 385 | 'big_map_contents', 386 | hDaoBalanceQuery 387 | ) 388 | balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it 389 | } catch (error) { 390 | console.log( 391 | `gethDaoBalanceForAddress failed for ${JSON.stringify( 392 | hDaoBalanceQuery 393 | )} with ${error}` 394 | ) 395 | } 396 | 397 | return balance 398 | } 399 | 400 | const getTokenBalance = async ( 401 | big_map_id, 402 | address, 403 | fa2 = false, 404 | token_id = 0 405 | ) => { 406 | let tokenBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() 407 | tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( 408 | tokenBalanceQuery, 409 | 'value' 410 | ) 411 | tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 412 | tokenBalanceQuery, 413 | 'big_map_id', 414 | conseiljs.ConseilOperator.EQ, 415 | [big_map_id] 416 | ) 417 | if (fa2) { 418 | tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 419 | tokenBalanceQuery, 420 | 'key', 421 | conseiljs.ConseilOperator.EQ, 422 | [ 423 | `Pair 0x${conseiljs.TezosMessageUtils.writeAddress( 424 | address 425 | )} ${token_id}`, 426 | ] 427 | ) 428 | } else { 429 | tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 430 | tokenBalanceQuery, 431 | 'key', 432 | conseiljs.ConseilOperator.EQ, 433 | [`0x${conseiljs.TezosMessageUtils.writeAddress(address)}`] 434 | ) 435 | } 436 | tokenBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 437 | tokenBalanceQuery, 438 | 'value', 439 | conseiljs.ConseilOperator.EQ, 440 | [0], 441 | true 442 | ) 443 | tokenBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit( 444 | tokenBalanceQuery, 445 | 1 446 | ) 447 | 448 | let balance = 0 449 | 450 | try { 451 | const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( 452 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 453 | 'mainnet', 454 | 'big_map_contents', 455 | tokenBalanceQuery 456 | ) 457 | balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it 458 | } catch (error) { 459 | console.log( 460 | `getTokenBalance failed for ${JSON.stringify( 461 | tokenBalanceQuery 462 | )} with ${error}` 463 | ) 464 | } 465 | 466 | return balance 467 | } 468 | 469 | const getTezBalanceForAddress = async (address) => { 470 | let accountQuery = conseiljs.ConseilQueryBuilder.blankQuery() 471 | accountQuery = conseiljs.ConseilQueryBuilder.addFields( 472 | accountQuery, 473 | 'balance' 474 | ) 475 | accountQuery = conseiljs.ConseilQueryBuilder.addPredicate( 476 | accountQuery, 477 | 'account_id', 478 | conseiljs.ConseilOperator.EQ, 479 | [address], 480 | false 481 | ) 482 | accountQuery = conseiljs.ConseilQueryBuilder.setLimit(accountQuery, 1) 483 | 484 | try { 485 | const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( 486 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 487 | 'mainnet', 488 | 'accounts', 489 | accountQuery 490 | ) 491 | balance = balanceResult[0]['balance'] // TODO: consider bigNumber here, for the moment there is no reason for it 492 | } catch (error) { 493 | console.log( 494 | `getTezBalanceForAddress failed for ${JSON.stringify( 495 | accountQuery 496 | )} with ${error}` 497 | ) 498 | } 499 | 500 | return balance 501 | } 502 | 503 | const gethDAOPerTez = async (address) => { 504 | const tezBalance = await getTezBalanceForAddress(address) 505 | const hdaoBalance = await gethDaoBalanceForAddress(address) 506 | return hdaoBalance / Math.max(tezBalance,1) 507 | } 508 | 509 | const getKolibriPerTez = async (address) => { 510 | const tezBalance = await getTezBalanceForAddress(address) 511 | var kolibriBalance = await getTokenBalance( 512 | mainnet.kolibriLedger, 513 | address 514 | ) 515 | 516 | // TODO: Find a better way to get the balance, this is FA1.2, mike? 517 | kolibriBalance = 518 | parseInt(kolibriBalance.replace('Pair {} ', '')) / 10 ** (18 - 6) 519 | return kolibriBalance / Math.max(tezBalance,1) 520 | } 521 | 522 | const gethDaoBalances = async () => { 523 | let hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() 524 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( 525 | hDaoBalanceQuery, 526 | 'key', 527 | 'value' 528 | ) 529 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 530 | hDaoBalanceQuery, 531 | 'big_map_id', 532 | conseiljs.ConseilOperator.EQ, 533 | [mainnet.daoLedger] 534 | ) 535 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 536 | hDaoBalanceQuery, 537 | 'value', 538 | conseiljs.ConseilOperator.EQ, 539 | [0], 540 | true 541 | ) 542 | hDaoBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit( 543 | hDaoBalanceQuery, 544 | 500_000 545 | ) 546 | 547 | let balance = 0 548 | let hdaoMap = {} 549 | 550 | try { 551 | const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( 552 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 553 | 'mainnet', 554 | 'big_map_contents', 555 | hDaoBalanceQuery 556 | ) 557 | 558 | balanceResult.forEach((row) => { 559 | hdaoMap[ 560 | conseiljs.TezosMessageUtils.readAddress( 561 | row['key'].toString().replace(/^Pair 0x([0-9a-z]{1,}) [0-9]+/, '$1') 562 | ) 563 | ] = parseInt(row['value']) 564 | }) 565 | //#balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it 566 | } catch (error) { 567 | console.log( 568 | `gethDaoBalanceForAddress failed for ${JSON.stringify( 569 | hDaoBalanceQuery 570 | )} with ${error}` 571 | ) 572 | } 573 | 574 | return hdaoMap 575 | } 576 | 577 | const getObjektOwners = async (objekt_id) => { 578 | let objektBalanceQuery = conseiljs.ConseilQueryBuilder.blankQuery() 579 | objektBalanceQuery = conseiljs.ConseilQueryBuilder.addFields( 580 | objektBalanceQuery, 581 | 'key', 582 | 'value' 583 | ) 584 | objektBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 585 | objektBalanceQuery, 586 | 'big_map_id', 587 | conseiljs.ConseilOperator.EQ, 588 | [mainnet.nftLedger] 589 | ) 590 | objektBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 591 | objektBalanceQuery, 592 | 'key', 593 | conseiljs.ConseilOperator.ENDSWITH, 594 | [` ${objekt_id}`], 595 | false 596 | ) 597 | objektBalanceQuery = conseiljs.ConseilQueryBuilder.addPredicate( 598 | objektBalanceQuery, 599 | 'value', 600 | conseiljs.ConseilOperator.EQ, 601 | [0], 602 | true 603 | ) 604 | objektBalanceQuery = conseiljs.ConseilQueryBuilder.setLimit( 605 | objektBalanceQuery, 606 | 500_000 607 | ) 608 | 609 | let objektMap = {} 610 | 611 | try { 612 | const balanceResult = await conseiljs.TezosConseilClient.getTezosEntityData( 613 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 614 | 'mainnet', 615 | 'big_map_contents', 616 | objektBalanceQuery 617 | ) 618 | 619 | balanceResult.forEach((row) => { 620 | objektMap[ 621 | conseiljs.TezosMessageUtils.readAddress( 622 | row['key'].toString().replace(/^Pair 0x([0-9a-z]{1,}) [0-9]+/, '$1') 623 | ) 624 | ] = row['value'] 625 | }) 626 | //#balance = balanceResult[0]['value'] // TODO: consider bigNumber here, for the moment there is no reason for it 627 | } catch (error) { 628 | console.log( 629 | `getObjektOwners failed for ${JSON.stringify( 630 | objektBalanceQuery 631 | )} with ${error}` 632 | ) 633 | } 634 | 635 | return objektMap 636 | } 637 | 638 | const getObjektMintingsDuringPeriod = async (min_time, max_time) => { 639 | let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() 640 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( 641 | mintOperationQuery, 642 | 'source' 643 | ) 644 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 645 | mintOperationQuery, 646 | 'kind', 647 | conseiljs.ConseilOperator.EQ, 648 | ['transaction'] 649 | ) 650 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 651 | mintOperationQuery, 652 | 'timestamp', 653 | conseiljs.ConseilOperator.BETWEEN, 654 | [min_time, max_time] 655 | ) // 2021 Feb 1 656 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 657 | mintOperationQuery, 658 | 'status', 659 | conseiljs.ConseilOperator.EQ, 660 | ['applied'] 661 | ) 662 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 663 | mintOperationQuery, 664 | 'destination', 665 | conseiljs.ConseilOperator.EQ, 666 | [mainnet.protocol] 667 | ) 668 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 669 | mintOperationQuery, 670 | 'parameters_entrypoints', 671 | conseiljs.ConseilOperator.EQ, 672 | ['mint_OBJKT'] 673 | ) 674 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering( 675 | mintOperationQuery, 676 | 'block_level', 677 | conseiljs.ConseilSortDirection.DESC 678 | ) 679 | mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( 680 | mintOperationQuery, 681 | 500_000 682 | ) // TODO: this is hardwired and will not work for highly productive artists 683 | 684 | const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( 685 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 686 | 'mainnet', 687 | 'operations', 688 | mintOperationQuery 689 | ) 690 | 691 | const mints = mintOperationResult.map((r) => r['source']) 692 | 693 | var initialValue = new Map() 694 | var reducer = function (minters, mintOp) { 695 | if (!minters[mintOp]) { 696 | minters[mintOp] = 1 697 | } else { 698 | minters[mintOp] = minters[mintOp] + 1 699 | } 700 | return minters 701 | } 702 | return mints.reduce(reducer, initialValue) 703 | } 704 | 705 | /** 706 | * 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. 707 | * 708 | * @param {string} address 709 | * @returns 710 | */ 711 | const getArtisticOutputForAddress = async (address) => { 712 | let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() 713 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( 714 | mintOperationQuery, 715 | 'operation_group_hash' 716 | ) 717 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 718 | mintOperationQuery, 719 | 'kind', 720 | conseiljs.ConseilOperator.EQ, 721 | ['transaction'] 722 | ) 723 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 724 | mintOperationQuery, 725 | 'timestamp', 726 | conseiljs.ConseilOperator.AFTER, 727 | [1612240919000] 728 | ) // 2021 Feb 1 729 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 730 | mintOperationQuery, 731 | 'status', 732 | conseiljs.ConseilOperator.EQ, 733 | ['applied'] 734 | ) 735 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 736 | mintOperationQuery, 737 | 'destination', 738 | conseiljs.ConseilOperator.EQ, 739 | [mainnet.protocol] 740 | ) 741 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 742 | mintOperationQuery, 743 | 'parameters_entrypoints', 744 | conseiljs.ConseilOperator.EQ, 745 | ['mint_OBJKT'] 746 | ) 747 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 748 | mintOperationQuery, 749 | 'source', 750 | conseiljs.ConseilOperator.EQ, 751 | [address] 752 | ) 753 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering( 754 | mintOperationQuery, 755 | 'block_level', 756 | conseiljs.ConseilSortDirection.DESC 757 | ) 758 | mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( 759 | mintOperationQuery, 760 | 256 761 | ) // TODO: this is hardwired and will not work for highly productive artists 762 | 763 | const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( 764 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 765 | 'mainnet', 766 | 'operations', 767 | mintOperationQuery 768 | ) 769 | 770 | const operationGroupIds = mintOperationResult.map( 771 | (r) => r['operation_group_hash'] 772 | ) 773 | const queryChunks = chunkArray(operationGroupIds, 30) 774 | 775 | const makeObjectQuery = (opIds) => { 776 | let mintedObjectsQuery = conseiljs.ConseilQueryBuilder.blankQuery() 777 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addFields( 778 | mintedObjectsQuery, 779 | 'key_hash', 780 | 'value' 781 | ) 782 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 783 | mintedObjectsQuery, 784 | 'big_map_id', 785 | conseiljs.ConseilOperator.EQ, 786 | [mainnet.nftMetadataMap] 787 | ) 788 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 789 | mintedObjectsQuery, 790 | 'operation_group_id', 791 | opIds.length > 1 792 | ? conseiljs.ConseilOperator.IN 793 | : conseiljs.ConseilOperator.EQ, 794 | opIds 795 | ) 796 | mintedObjectsQuery = conseiljs.ConseilQueryBuilder.setLimit( 797 | mintedObjectsQuery, 798 | opIds.length 799 | ) 800 | 801 | return mintedObjectsQuery 802 | } 803 | 804 | const objectQueries = queryChunks.map((c) => makeObjectQuery(c)) 805 | 806 | const objectInfo = await Promise.all( 807 | objectQueries.map( 808 | async (q) => 809 | await conseiljs.TezosConseilClient.getTezosEntityData( 810 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 811 | 'mainnet', 812 | 'big_map_contents', 813 | q 814 | ).then((result) => 815 | result.map((row) => { 816 | const objectId = row['value'] 817 | .toString() 818 | .replace(/^Pair ([0-9]{1,}) .*/, '$1') 819 | const objectUrl = row['value'] 820 | .toString() 821 | .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') 822 | const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) 823 | 824 | return { key: row['key_hash'], objectId, ipfsHash } 825 | }) 826 | ) 827 | ) 828 | ) 829 | 830 | return objectInfo 831 | .flat(1) 832 | .sort((a, b) => parseInt(b.objectId) - parseInt(a.objectId)) 833 | } 834 | 835 | 836 | 837 | const getArtisticUniverse = async (max_time, min_time, limit, skip_swaps=true) => { 838 | max_time = ((typeof max_time !== 'undefined') && max_time != 0) ? max_time : _getTimeQuantizedToMinute() 839 | min_time = ((typeof min_time !== 'undefined') && min_time != 0) ? min_time : _subtractDays(max_time, 3) 840 | limit = ((typeof limit !== 'undefined') && limit != 0) ? limit : 2500 841 | let min_block_level = LATEST_EPOCH 842 | let mintOperationQuery = conseiljs.ConseilQueryBuilder.blankQuery() 843 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addFields( 844 | mintOperationQuery, 845 | 'operation_group_hash' 846 | ) 847 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 848 | mintOperationQuery, 849 | 'kind', 850 | conseiljs.ConseilOperator.EQ, 851 | ['transaction'] 852 | ) 853 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 854 | mintOperationQuery, 855 | 'timestamp', 856 | conseiljs.ConseilOperator.BETWEEN, 857 | [min_time, max_time] 858 | ) //Two weeks ago 859 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 860 | mintOperationQuery, 861 | 'block_level', 862 | conseiljs.ConseilOperator.GT, 863 | [min_block_level] 864 | ) 865 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 866 | mintOperationQuery, 867 | 'status', 868 | conseiljs.ConseilOperator.EQ, 869 | ['applied'] 870 | ) 871 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 872 | mintOperationQuery, 873 | 'destination', 874 | conseiljs.ConseilOperator.EQ, 875 | [mainnet.protocol] 876 | ) 877 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addPredicate( 878 | mintOperationQuery, 879 | 'parameters_entrypoints', 880 | conseiljs.ConseilOperator.EQ, 881 | ['mint_OBJKT'] 882 | ) 883 | mintOperationQuery = conseiljs.ConseilQueryBuilder.addOrdering( 884 | mintOperationQuery, 885 | 'block_level', 886 | conseiljs.ConseilSortDirection.DESC 887 | ) 888 | mintOperationQuery = conseiljs.ConseilQueryBuilder.setLimit( 889 | mintOperationQuery, 890 | limit 891 | ) 892 | 893 | const mintOperationResult = await conseiljs.TezosConseilClient.getTezosEntityData( 894 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 895 | 'mainnet', 896 | 'operations', 897 | mintOperationQuery 898 | ) 899 | 900 | const operationGroupIds = mintOperationResult.map( 901 | (r) => r['operation_group_hash'] 902 | ) 903 | 904 | let royaltiesQuery = conseiljs.ConseilQueryBuilder.blankQuery() 905 | royaltiesQuery = conseiljs.ConseilQueryBuilder.addFields( 906 | royaltiesQuery, 907 | 'key', 908 | 'value' 909 | ) 910 | royaltiesQuery = conseiljs.ConseilQueryBuilder.addPredicate( 911 | royaltiesQuery, 912 | 'big_map_id', 913 | conseiljs.ConseilOperator.EQ, 914 | [mainnet.nftRoyaltiesMap] 915 | ) 916 | royaltiesQuery = conseiljs.ConseilQueryBuilder.addPredicate( 917 | royaltiesQuery, 918 | 'block_level', 919 | conseiljs.ConseilOperator.GT, 920 | [min_block_level] 921 | ) 922 | royaltiesQuery = conseiljs.ConseilQueryBuilder.addPredicate( 923 | royaltiesQuery, 924 | 'timestamp', 925 | conseiljs.ConseilOperator.BETWEEN, 926 | [min_time, max_time] 927 | ) //Two weeks ago 928 | royaltiesQuery = conseiljs.ConseilQueryBuilder.setLimit( 929 | royaltiesQuery, 930 | 100_000 931 | ) 932 | const royaltiesResult = await conseiljs.TezosConseilClient.getTezosEntityData( 933 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 934 | 'mainnet', 935 | 'big_map_contents', 936 | royaltiesQuery 937 | ) 938 | let artistMap = {} 939 | royaltiesResult.forEach((row) => { 940 | artistMap[row['key']] = conseiljs.TezosMessageUtils.readAddress( 941 | row['value'].toString().replace(/^Pair 0x([0-9a-z]{1,}) [0-9]+/, '$1') 942 | ) 943 | }) 944 | 945 | let swapMap = {} 946 | 947 | if (!skip_swaps) { 948 | let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery() 949 | swapsQuery = conseiljs.ConseilQueryBuilder.addFields( 950 | swapsQuery, 951 | 'key', 952 | 'value' 953 | ) 954 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 955 | swapsQuery, 956 | 'block_level', 957 | conseiljs.ConseilOperator.GT, 958 | [min_block_level] 959 | ) 960 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 961 | swapsQuery, 962 | 'timestamp', 963 | conseiljs.ConseilOperator.BETWEEN, 964 | [min_time, max_time] 965 | ) //Two weeks ago 966 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 967 | swapsQuery, 968 | 'big_map_id', 969 | conseiljs.ConseilOperator.EQ, 970 | [mainnet.nftSwapMap] 971 | ) 972 | swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 30_000) // NOTE, limited to 30_000 973 | 974 | const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData( 975 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 976 | 'mainnet', 977 | 'big_map_contents', 978 | swapsQuery 979 | ) 980 | 981 | const swapStoragePattern = new RegExp( 982 | `Pair [(]Pair 0x([0-9a-z]{44}) ([0-9]+)[)] [(]Pair ([0-9]+) ([0-9]+)[)]` 983 | ) 984 | 985 | 986 | swapsResult.forEach((row) => { 987 | swap_id = row['key'] 988 | const match = swapStoragePattern.exec(row['value']) 989 | if (!match) { 990 | return 991 | } 992 | const amount = match[2] 993 | const objkt_id = match[3] 994 | const xtz_per_objkt = match[4] 995 | 996 | if (swapMap[row['key']]) { 997 | swapMap[row['key']].push({}) 998 | } else { 999 | swapMap[row['key']] = [{ swap_id, objkt_id, amount, xtz_per_objkt }] 1000 | } 1001 | }) 1002 | } 1003 | 1004 | const queryChunks = chunkArray(operationGroupIds, 50) 1005 | 1006 | const makeObjectQuery = (opIds) => { 1007 | let objectsQuery = conseiljs.ConseilQueryBuilder.blankQuery() 1008 | objectsQuery = conseiljs.ConseilQueryBuilder.addFields( 1009 | objectsQuery, 1010 | 'key', 1011 | 'value', 1012 | 'operation_group_id' 1013 | ) 1014 | objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1015 | objectsQuery, 1016 | 'big_map_id', 1017 | conseiljs.ConseilOperator.EQ, 1018 | [mainnet.nftMetadataMap] 1019 | ) 1020 | objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1021 | objectsQuery, 1022 | 'timestamp', 1023 | conseiljs.ConseilOperator.BETWEEN, 1024 | [min_time, max_time] 1025 | ) 1026 | objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1027 | objectsQuery, 1028 | 'block_level', 1029 | conseiljs.ConseilOperator.GT, 1030 | [min_block_level] 1031 | ) 1032 | objectsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1033 | objectsQuery, 1034 | 'operation_group_id', 1035 | opIds.length > 1 1036 | ? conseiljs.ConseilOperator.IN 1037 | : conseiljs.ConseilOperator.EQ, 1038 | opIds 1039 | ) 1040 | objectsQuery = conseiljs.ConseilQueryBuilder.setLimit( 1041 | objectsQuery, 1042 | opIds.length 1043 | ) 1044 | 1045 | return objectsQuery 1046 | } 1047 | 1048 | const objectQueries = queryChunks.map((c) => makeObjectQuery(c)) 1049 | 1050 | let universe = [] 1051 | await Promise.all( 1052 | objectQueries.map(async (q) => { 1053 | const r = [] 1054 | try { 1055 | r = await conseiljs.TezosConseilClient.getTezosEntityData( 1056 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 1057 | 'mainnet', 1058 | 'big_map_contents', 1059 | q 1060 | ).then((result) => 1061 | result.map((row) => { 1062 | const objectId = row['value'] 1063 | .toString() 1064 | .replace(/^Pair ([0-9]{1,}) .*/, '$1') 1065 | const objectUrl = row['value'] 1066 | .toString() 1067 | .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') 1068 | const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) 1069 | 1070 | universe.push({ 1071 | objectId, 1072 | ipfsHash, 1073 | minter: artistMap[objectId], 1074 | swaps: swapMap[objectId] !== undefined ? swapMap[objectId] : [], 1075 | }) 1076 | }) 1077 | ) // NOTE: it's a work in progress, this will drop failed requests and return a smaller set than expected 1078 | } finally { 1079 | return r 1080 | } 1081 | }) 1082 | ) 1083 | 1084 | return universe 1085 | } 1086 | 1087 | const getFeaturedArtisticUniverse = async (max_time) => { 1088 | max_time = ((typeof max_time !== 'undefined') && max_time != 0) ? max_time : _getTimeQuantizedToMinute() 1089 | console.log('max time', max_time) 1090 | const min_time = _subtractWeek(max_time) 1091 | console.log('min time', min_time) 1092 | hdaoMap = await gethDaoBalances() 1093 | const twoh_ago = _subtractHours(max_time, 3) 1094 | 1095 | const backendConf = await getBackendConfig() 1096 | if(backendConf.disableFilter) { 1097 | return await getArtisticUniverse(max_time, _subtractDays(max_time, 3), 2000) 1098 | } 1099 | // Weekly and recent mints can be used to identify two misbehaving users: 1100 | // 1. bot-minters who mint huge amounts of works, they should gain followers early or via 3rd party sites 1101 | // 2. copyminters who mint many works in short timespan before they get banned 1102 | mintsPerCreatorWeek = await getObjektMintingsDuringPeriod(min_time, max_time) 1103 | 1104 | //We now create the scoring task 1105 | let minter_arr = [ ...Object.keys(mintsPerCreatorWeek) ] 1106 | let minters = _.take(_.uniq(minter_arr), 30) 1107 | const limiter = new Bottleneck({ 1108 | maxConcurrent: 1, 1109 | minTime: 20 1110 | }); 1111 | let tzktscores = new Map() 1112 | let scoreTask = Promise.all( 1113 | Array.from(minters).map(async (minter) => { 1114 | const mscore = await limiter.schedule(() => { 1115 | return axios.get( 1116 | `https://api.tzkt.io/v1/accounts/${minter}/metadata` 1117 | ).then((data) => { 1118 | 1119 | let score = 0 1120 | if(data) { 1121 | if(data.data.alias) score += 0.2 1122 | if(data.data.twitter) score += 0.25 1123 | if(data.data.instagram) score += 0.25 1124 | if(data.data.github) score += 0.2 1125 | if(data.data.site) score += 0.25 1126 | if(data.data.reddit) score += 0.25 1127 | if(data.data.email) score += 0.2 1128 | if(data.data.logo) score += 0.2 1129 | if(data.data.description) score += 0.2 1130 | } 1131 | return Math.min(score, 1) 1132 | }).catch((error) => { 1133 | if (error.response) { 1134 | if(error.response.status == 429) { 1135 | console.log('TOO FAST') 1136 | return -1 1137 | } 1138 | } 1139 | return 0 1140 | }) 1141 | }) 1142 | tzktscores[minter] = mscore 1143 | }) 1144 | ) 1145 | 1146 | //We run scoring task along with the "real" tasks for performance 1147 | 1148 | var tasks = [ 1149 | scoreTask, 1150 | getObjektMintingsDuringPeriod(twoh_ago, max_time), 1151 | getArtisticUniverse(max_time, _subtractDays(max_time, 3), 2000) 1152 | ] 1153 | if(!backendConf.disableSwapPrices) { 1154 | //We setup thresholds using price data so that people aren't affected by huge swings in hdao value 1155 | tasks = tasks.concat([ 1156 | gethDAOPerTez(backendConf.hDaoSwap), 1157 | getKolibriPerTez(backendConf.kolibriSwap)]) 1158 | var [bs, mintsPerCreator3h, artisticUniverse, hdaoPerTez, kolPerTez] = await Promise.all(tasks) 1159 | hdaoPerKol = hdaoPerTez / Math.max(kolPerTez, 0.0000000001) 1160 | } 1161 | else { 1162 | var [bs, mintsPerCreator3h, artisticUniverse] = await Promise.all(tasks) 1163 | } 1164 | 1165 | 1166 | let replacementScore = Object.values(tzktscores).filter(v => v !== -1).avg() / 2 1167 | tzktscores = _.mapValues(tzktscores, function(value) { 1168 | if (value === -1) { 1169 | return replacementScore 1170 | } else 1171 | { 1172 | return value 1173 | } 1174 | }) 1175 | // Cost to be on feed per objekt last 7 days shouldn't be higher than any of: 1176 | // 0.5tez 1177 | // 10 hDAO 1178 | // $1 1179 | // But not lower than: 1180 | // 0.1 hDAO 1181 | // 1182 | // We should probably add more thresholds like $, € and yen 1183 | // It should be cheap but not too cheap and it shouldn't be 1184 | // affected by tez or hDAO volatility 1185 | let thresholdHdao = backendConf.filterMinhDAO 1186 | if (!backendConf.disableSwapPrices) { 1187 | console.log(hdaoPerTez, hdaoPerKol) 1188 | thresholdHdao = Math.floor( 1189 | Math.max(backendConf.filterMinhDAO, 1190 | Math.min( 1191 | backendConf.filterMaxhDAO, 1192 | backendConf.filterMaxTez * hdaoPerTez, 1193 | backendConf.filterMaxKol * hdaoPerKol) 1194 | ) 1195 | ) 1196 | } 1197 | console.log('thresholdHdao', thresholdHdao) 1198 | // We now filter based on if user has a tzkt profile or not and if they spam 1199 | // the feed or have a "normal human" behavior to avoid bots 1200 | return artisticUniverse.filter(o => { 1201 | //We look at hdao and tzkt status to set as a base-score 1202 | let minterhDAO = (hdaoMap[o.minter] || 0) 1203 | let minterScore = (tzktscores[o.minter] || replacementScore) 1204 | let minterBaseScore = minterhDAO + (minterScore * thresholdHdao) + (minterhDAO * minterScore * backendConf.tzktScorehDAOMultiplier) 1205 | 1206 | let minterMintsWeek = Math.max(mintsPerCreatorWeek[o.minter] || 1, 1) 1207 | let minterMints3hExp = (2 ** Math.max((mintsPerCreator3h[o.minter] || 1) - 1, 0)) 1208 | let minterFreeObjkts = minterScore * backendConf.filterWeeklyFreeObjkts 1209 | 1210 | let divisor = Math.max(minterMintsWeek + minterMints3hExp - minterFreeObjkts, 0) 1211 | 1212 | return (minterBaseScore / divisor) > thresholdHdao 1213 | }) 1214 | } 1215 | 1216 | const getRecommendedCurateDefault = async () => { 1217 | const backendConf = await getBackendConfig() 1218 | if(!backendConf.disableSwapPrices) { 1219 | hdaoPerTez = await gethDAOPerTez(backendConf.hDaoSwap) 1220 | kolPerTez = await getKolibriPerTez(backendConf.kolibriSwap) 1221 | hdaoPerKol = hdaoPerTez / kolPerTez 1222 | //Minimum of $0.1, 0.1 hDAO, and 0.1tez, in hDAO 1223 | return Math.floor( 1224 | Math.min(hdaoPerKol * 0.1, 0.1, 0.1 * hdaoPerTez) * 1_000_000 1225 | ) 1226 | } 1227 | return backendConf.curateDefault 1228 | } 1229 | 1230 | /** 1231 | * Returns object ipfs hash and swaps if any 1232 | * 1233 | * @param {number} objectId 1234 | * @returns 1235 | */ 1236 | const getObjectById = async (objectId, with_swaps=true, min_creation_level, max_creation_level) => { 1237 | let objectQuery = conseiljs.ConseilQueryBuilder.blankQuery() 1238 | objectQuery = conseiljs.ConseilQueryBuilder.addFields(objectQuery, 1239 | 'key', 1240 | 'value', 1241 | 'block_level') 1242 | objectQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1243 | objectQuery, 1244 | 'big_map_id', 1245 | conseiljs.ConseilOperator.EQ, 1246 | [mainnet.nftMetadataMap] 1247 | ) 1248 | objectQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1249 | objectQuery, 1250 | 'key', 1251 | conseiljs.ConseilOperator.EQ, 1252 | [objectId] 1253 | ) 1254 | if ((typeof min_creation_level !== 'undefined') && min_creation_level !== null) { 1255 | if ((typeof max_creation_level !== 'undefined' ) && max_creation_level !== null ) { 1256 | objectQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1257 | objectQuery, 1258 | 'block_level', 1259 | conseiljs.ConseilOperator.BETWEEN, 1260 | [min_creation_level, max_creation_level] 1261 | ) 1262 | } else { 1263 | objectQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1264 | objectQuery, 1265 | 'block_level', 1266 | conseiljs.ConseilOperator.GT, 1267 | [min_creation_level] 1268 | ) 1269 | } 1270 | 1271 | } 1272 | 1273 | 1274 | objectQuery = conseiljs.ConseilQueryBuilder.addOrdering( 1275 | objectQuery, 1276 | 'block_level', 1277 | conseiljs.ConseilSortDirection.DESC 1278 | ) 1279 | 1280 | objectQuery = conseiljs.ConseilQueryBuilder.setLimit(objectQuery, 1) 1281 | 1282 | const objectResult = await conseiljs.TezosConseilClient.getTezosEntityData( 1283 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 1284 | 'mainnet', 1285 | 'big_map_contents', 1286 | objectQuery 1287 | ) 1288 | 1289 | const objectUrl = objectResult[0]['value'] 1290 | .toString() 1291 | .replace(/.* 0x([0-9a-z]{1,}) \}$/, '$1') 1292 | const ipfsHash = Buffer.from(objectUrl, 'hex').toString().slice(7) 1293 | 1294 | let swaps = [] 1295 | 1296 | 1297 | 1298 | if (!with_swaps) { 1299 | return { objectId, ipfsHash, swaps } 1300 | } 1301 | let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery() 1302 | swapsQuery = conseiljs.ConseilQueryBuilder.addFields( 1303 | swapsQuery, 1304 | 'key', 1305 | 'value' 1306 | ) 1307 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1308 | swapsQuery, 1309 | 'big_map_id', 1310 | conseiljs.ConseilOperator.EQ, 1311 | [mainnet.nftSwapMap] 1312 | ) 1313 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1314 | swapsQuery, 1315 | 'value', 1316 | conseiljs.ConseilOperator.LIKE, 1317 | [`) (Pair ${objectId} `] 1318 | ) 1319 | if ((typeof min_creation_level !== 'undefined') && min_creation_level !== null) { 1320 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1321 | swapsQuery, 1322 | 'block_level', 1323 | conseiljs.ConseilOperator.GT, 1324 | [min_creation_level] 1325 | ) 1326 | } 1327 | 1328 | swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 1000) // NOTE, limited to 1000 swaps for a given object 1329 | 1330 | const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData( 1331 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 1332 | 'mainnet', 1333 | 'big_map_contents', 1334 | swapsQuery 1335 | ) 1336 | const swapStoragePattern = new RegExp( 1337 | `Pair [(]Pair 0x([0-9a-z]{44}) ([0-9]+)[)] [(]Pair ${objectId} ([0-9]+)[)]` 1338 | ) 1339 | 1340 | try { 1341 | swapsResult.map((row) => { 1342 | const match = swapStoragePattern.exec(row['value']) 1343 | 1344 | swaps.push({ 1345 | swap_id: row['key'], 1346 | issuer: conseiljs.TezosMessageUtils.readAddress(match[1]), 1347 | objkt_amount: match[2], 1348 | xtz_per_objkt: match[3], 1349 | }) 1350 | }) 1351 | } catch (error) { 1352 | console.log(`failed to process swaps for ${objectId} with ${error}`) 1353 | } 1354 | 1355 | return { objectId, ipfsHash, swaps } 1356 | } 1357 | 1358 | 1359 | const getSwapsForAddress = async(address) => { 1360 | let swaps = [] 1361 | let swapsQuery = conseiljs.ConseilQueryBuilder.blankQuery() 1362 | swapsQuery = conseiljs.ConseilQueryBuilder.addFields( 1363 | swapsQuery, 1364 | 'key', 1365 | 'value' 1366 | ) 1367 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1368 | swapsQuery, 1369 | 'big_map_id', 1370 | conseiljs.ConseilOperator.EQ, 1371 | [mainnet.nftSwapMap] 1372 | ) 1373 | 1374 | let addrHash = `0x${conseiljs.TezosMessageUtils.writeAddress(address)}` 1375 | swapsQuery = conseiljs.ConseilQueryBuilder.addPredicate( 1376 | swapsQuery, 1377 | 'value', 1378 | conseiljs.ConseilOperator.LIKE, 1379 | [`Pair (Pair ${addrHash} `] 1380 | ) 1381 | swapsQuery = conseiljs.ConseilQueryBuilder.setLimit(swapsQuery, 1000) // NOTE, limited to 1000 swaps for a given object 1382 | 1383 | const swapsResult = await conseiljs.TezosConseilClient.getTezosEntityData( 1384 | { url: conseilServer, apiKey: conseilApiKey, network: 'mainnet' }, 1385 | 'mainnet', 1386 | 'big_map_contents', 1387 | swapsQuery 1388 | ) 1389 | const swapStoragePattern = new RegExp( 1390 | `Pair [(]Pair ${addrHash} ([0-9]+)[)] [(]Pair ([0-9]+) ([0-9]+)[)]` 1391 | ) 1392 | 1393 | try { 1394 | swapsResult.map((row) => { 1395 | const match = swapStoragePattern.exec(row['value']) 1396 | 1397 | swaps.push({ 1398 | swap_id: row['key'], 1399 | token_id: match[2], 1400 | objkt_amount: match[1], 1401 | xtz_per_objkt: match[3], 1402 | }) 1403 | }) 1404 | } catch (error) { 1405 | console.log(`failed to process swaps for ${address} with ${error}`) 1406 | } 1407 | 1408 | return swaps 1409 | } 1410 | 1411 | const chunkArray = (arr, len) => { 1412 | // TODO: move to util.js 1413 | let chunks = [], 1414 | i = 0, 1415 | n = arr.length 1416 | 1417 | while (i < n) { 1418 | chunks.push(arr.slice(i, (i += len))) 1419 | } 1420 | 1421 | return chunks 1422 | } 1423 | 1424 | module.exports = { 1425 | getCollectionForAddress, 1426 | gethDaoBalanceForAddress, 1427 | getObjkthDAOBalance, 1428 | getArtisticOutputForAddress, 1429 | getObjectById, 1430 | getArtisticUniverse, 1431 | getFeaturedArtisticUniverse, 1432 | hDAOFeed, 1433 | getRecommendedCurateDefault, 1434 | getObjektOwners, 1435 | getSwapsForAddress, 1436 | } 1437 | -------------------------------------------------------------------------------- /lib/router/readFeed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const conseil = require('./../conseil') 4 | 5 | const { getIpfsHash, paginateFeed, sortFeed } = require('./../utils') 6 | 7 | module.exports = async function readFeed(req, res) { 8 | const isFeatured = req.path === '/featured' 9 | const pageCursor = req.body.counter 10 | const max_time = req.body.max_time 11 | const rawFeed = await (isFeatured 12 | ? conseil.getFeaturedArtisticUniverse(max_time) 13 | : conseil.getArtisticUniverse(max_time)) 14 | 15 | const paginatedFeed = paginateFeed(sortFeed(rawFeed), pageCursor) 16 | const feed = await Promise.all( 17 | paginatedFeed.map(async (objkt) => { 18 | objkt.token_info = await getIpfsHash(objkt.ipfsHash) 19 | objkt.token_id = parseInt(objkt.objectId) 20 | 21 | return objkt 22 | }) 23 | ) 24 | 25 | return res.json({ result: feed }) 26 | } 27 | -------------------------------------------------------------------------------- /lib/router/readHdaoFeed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const _ = require('lodash') 4 | const conseil = require('./../conseil') 5 | 6 | const { getObjktById, paginateFeed, getRestrictedAddresses, getRestrictedObjkts } = require('./../utils') 7 | 8 | module.exports = async function readHdaoFeed(req, res) { 9 | const rawFeed = await conseil.hDAOFeed() 10 | const sortedFeed = _.orderBy(rawFeed, ['hDAO_balance'], ['desc']).slice(0, 500) 11 | const mergedFeed = await Promise.all( 12 | sortedFeed.map(async (objkt) => await _mergeHdao(objkt)) 13 | ) 14 | 15 | const restrictedObjekts = await getRestrictedObjkts().catch(() => []) 16 | const restrictedAddresses = await getRestrictedAddresses().catch(() => []) 17 | const filteredFeed = mergedFeed.filter(o => (typeof o !== 'undefined')) 18 | .filter(o => o.hasOwnProperty('token_info')) 19 | .filter(o => !restrictedObjekts.includes(o.token_id)) 20 | .filter(o => !restrictedAddresses.includes(o.token_info.creators[0])) 21 | const counter = req.body.counter || 0 22 | const paginatedFeed = paginateFeed(filteredFeed, counter) 23 | 24 | res.json({ 25 | result: paginatedFeed, 26 | }) 27 | } 28 | 29 | async function _mergeHdao(objkt) { 30 | const mergedObjkt = await getObjktById(objkt.token_id, false).catch(() => {}) 31 | 32 | mergedObjkt.hDAO_balance = objkt.hDAO_balance 33 | 34 | return mergedObjkt 35 | } 36 | -------------------------------------------------------------------------------- /lib/router/readIssuer.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const conseil = require('./../conseil') 4 | const _ = require('lodash') 5 | 6 | const { getIpfsHash, getObjktOwners, getRestrictedAddresses, getRestrictedObjkts } = require('./../utils') 7 | const { burnAddress } = require('./../config') 8 | 9 | module.exports = async function readIssuer(req, res) { 10 | const issuerAddress = req.body.tz 11 | const restrictedAddresses = await getRestrictedAddresses().catch(() => []) 12 | const restrictedObjkts = await getRestrictedObjkts().catch(() => []) 13 | 14 | if (restrictedAddresses.includes(issuerAddress)) { 15 | return res.status(410).send('Wallet/User is restricted and/or a copyminter'); 16 | } 17 | 18 | const [collection, creations, hdao, swaps] = await Promise.all([ 19 | conseil.getCollectionForAddress(issuerAddress), 20 | conseil.getArtisticOutputForAddress(issuerAddress), 21 | conseil.gethDaoBalanceForAddress(issuerAddress), 22 | conseil.getSwapsForAddress(issuerAddress), 23 | ]) 24 | 25 | const filteredCreations = await _filteredBurnedCreations(creations) 26 | 27 | const unsortedResults = await Promise.all( 28 | [...collection, ...filteredCreations] 29 | .filter(objkt => !restrictedObjkts.includes(parseInt(objkt.piece || objkt.objectId))) 30 | .map(async (objkt) => { 31 | objkt.token_info = await getIpfsHash(objkt.ipfsHash) 32 | objkt.token_id = parseInt(objkt.piece || objkt.objectId) 33 | 34 | return objkt 35 | }) 36 | ) 37 | 38 | const swap_groups = swaps.reduce((groups, item) => { 39 | const group = (groups[item.token_id] || []); 40 | group.push(_.omit(item, 'token_id')); 41 | groups[item.token_id] = group; 42 | return groups; 43 | }, {}); 44 | 45 | const sortedResults = _sortResults(unsortedResults) 46 | 47 | return res.json({ 48 | result: _.uniqBy(sortedResults, (objkt) => { 49 | return objkt.token_id 50 | }), 51 | hdao: hdao, 52 | swaps: swap_groups, 53 | }) 54 | } 55 | 56 | async function _filteredBurnedCreations(creations) { 57 | const validCreations = [] 58 | 59 | await Promise.all( 60 | creations.map(async (c) => { 61 | c.token_id = c.objectId 62 | 63 | const ownerData = await getObjktOwners(c) 64 | 65 | Object.assign(c, ownerData) 66 | 67 | const burnAddrCount = 68 | c.owners[burnAddress] && parseInt(c.owners[burnAddress]) 69 | const allIssuesBurned = burnAddrCount && burnAddrCount === c.total_amount 70 | 71 | if (!allIssuesBurned) { 72 | delete c.owners 73 | 74 | validCreations.push(c) 75 | } 76 | }) 77 | ) 78 | 79 | return validCreations 80 | } 81 | 82 | function _sortResults(results) { 83 | const unsortedCollection = [] 84 | const unsortedCreations = [] 85 | 86 | results.map((r) => 87 | r.piece ? unsortedCollection.push(r) : unsortedCreations.push(r) 88 | ) 89 | 90 | return [ 91 | ..._sort(unsortedCollection, 'piece'), 92 | ..._sort(unsortedCreations, 'objectId'), 93 | ] 94 | 95 | function _sort(arr, sortKey) { 96 | return arr.sort((a, b) => parseInt(b[sortKey]) - parseInt(a[sortKey])) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /lib/router/readObjkt.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const { getObjktById, getRestrictedObjkts, getRestrictedAddresses } = require('./../utils') 4 | 5 | module.exports = async function readObjkt(req, res) { 6 | const objktId = req.body.objkt_id 7 | const restrictedObjkts = await getRestrictedObjkts().catch(() => []) 8 | 9 | if (restrictedObjkts.includes(objktId)) { 10 | return res.status(410).send('Object is restricted and/or from a copyminter'); 11 | } 12 | const objekt = await getObjktById(objktId) 13 | const restrictedAddresses = await getRestrictedAddresses().catch(() => []) 14 | if (restrictedAddresses.includes(objekt.token_info.creators[0])) { 15 | return res.status(410).send('Object is restricted and/or from a copyminter'); 16 | } 17 | 18 | return res.json({ result: objekt }) 19 | } 20 | -------------------------------------------------------------------------------- /lib/router/readRandomFeed.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const conseil = require('./../conseil') 4 | const _ = require('lodash') 5 | 6 | const { getIpfsHash, paginateFeed, getObjektByIdBCD, getRestrictedObjkts, getRestrictedAddresses } = require('./../utils') 7 | 8 | module.exports = async function readRandomFeed(req, res) { 9 | const latestObjekt = await conseil.getArtisticUniverse(0, 0, 2) 10 | const latestId = parseInt(latestObjekt[0].objectId) 11 | console.log(latestId, typeof latestId) 12 | const restrictedObjekts = await getRestrictedObjkts().catch(() => []) 13 | const ids = _.shuffle(_.range(latestId - 152)).map(id => id + 152) 14 | const filteredIds = ids.filter(id => !restrictedObjekts.includes(id)).slice(0, 20); 15 | const mergedFeed = await Promise.all( 16 | filteredIds.map(async (id) => await getObjektByIdBCD(id, false).catch(() => {})) 17 | ) 18 | const restrictedAddresses = await getRestrictedAddresses().catch(() => []) 19 | const filteredFeed = mergedFeed.filter(o => (typeof o !== 'undefined')) 20 | .filter(o => o.hasOwnProperty('token_info')) 21 | .filter(o => !restrictedAddresses.includes(o.token_info.creators[0])) 22 | 23 | 24 | const pageCursor = req.body.cursor || 0 25 | const feed = paginateFeed(filteredFeed, pageCursor) 26 | 27 | res.json({ 28 | result: await Promise.all( 29 | feed.map(async (objkt) => { 30 | objkt.token_info = await getIpfsHash(objkt.ipfsHash) 31 | 32 | return objkt 33 | }) 34 | ), 35 | }) 36 | 37 | } 38 | -------------------------------------------------------------------------------- /lib/router/readRecommendCurate.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const conseil = require('./../conseil') 4 | 5 | module.exports = async function readRecommendCurate(req, res) { 6 | return res.json({ amount: await conseil.getRecommendedCurateDefault() }) 7 | } 8 | -------------------------------------------------------------------------------- /lib/router/router.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const router = require('express').Router() 4 | const swaggerUi = require('swagger-ui-express'); 5 | const swaggerDocument = require('../../swagger-output.json'); 6 | const readFeed = require('./readFeed') 7 | const readRandomFeed = require('./readRandomFeed') 8 | const readIssuer = require('./readIssuer') 9 | const readObjkt = require('./readObjkt') 10 | const readHdaoFeed = require('./readHdaoFeed') 11 | const readRecommendCurate = require('./readRecommendCurate') 12 | 13 | const MILLISECOND_MODIFIER = 1000 14 | const ONE_MINUTE_MILLIS = 60 * MILLISECOND_MODIFIER 15 | const DEFAULT_CACHE_TTL = 60 * 10 16 | const STATUS_CODE_SUCCESS = 200 17 | const CACHE_MAX_AGE_MAP = { 18 | hdao: 300, 19 | issuer: 120, 20 | objkt: 120, 21 | random: 300, 22 | recommendCurate: 300, 23 | } 24 | 25 | router 26 | .route('/feed|/featured') 27 | .all(_processClientCache) 28 | .get((req, res, next) => { 29 | /* 30 | #swagger.start 31 | #swagger.path = '/feed/{featured}' 32 | #swagger.method = 'get' 33 | #swagger.summary = 'Main feed' 34 | #swagger.description = 'Endpoint used to return the most recently minted OBJKTs. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500. Use of the optional `featured` path parameter will apply a different filter to the feed.' 35 | #swagger.parameters['featured'] = { 36 | in: 'path', 37 | type: 'string', 38 | description: 'Applies a filter to the results - returning no more than 1 item per minter, 39 | including only those swapped for less than 0.1 tez and that haven\'t been updated with 40 | lots of hDAO.', 41 | required: false, 42 | schema: 'featured' 43 | } 44 | #swagger.parameters['counter'] = { 45 | in: 'querystring', 46 | description: 'Pagination number. Default is 0', 47 | required: false, 48 | type: 'number', 49 | schema: { "counter": 0 } 50 | } 51 | #swagger.parameters['max_time'] = { 52 | in: 'querystring', 53 | description: 'Unix timestamp. Used to limit the maximum blockchain operation by date/time', 54 | required: false, 55 | type: 'number', 56 | schema: { "max_time": 1618585403788 } 57 | } 58 | #swagger.responses[200] = { 59 | schema: { 60 | "result": [ 61 | { $ref: "#/definitions/objkt" } 62 | ] 63 | } 64 | } 65 | #swagger.end 66 | */ 67 | 68 | req.body.max_time = req.query.max_time 69 | req.body.counter = req.query.counter 70 | 71 | return next() 72 | }, _asyncHandler(readFeed)) 73 | .post(_asyncHandler(readFeed), () => { 74 | /* 75 | #swagger.start 76 | #swagger.path = '/feed/{featured}' 77 | #swagger.method = 'post' 78 | #swagger.summary = 'Main feed' 79 | #swagger.description = 'Endpoint used to return the most recently minted OBJKTs. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500. Use of the optional `featured` path parameter will apply a different filter to the feed.' 80 | #swagger.parameters['featured'] = { 81 | in: 'path', 82 | type: 'string', 83 | description: 'Applies a filter to the results - returning no more than 1 item per minter, 84 | including only those swapped for less than 0.1 tez and that haven\'t been updated with 85 | lots of hDAO.', 86 | required: false, 87 | schema: 'featured' 88 | } 89 | #swagger.parameters['counter'] = { 90 | in: 'body', 91 | description: 'Pagination number. Default is 0', 92 | required: false, 93 | type: 'number', 94 | schema: { "counter": 0 } 95 | } 96 | #swagger.parameters['max_time'] = { 97 | in: 'body', 98 | description: 'Unix epoch timestamp. Used to limit the maximum blockchain operation by date/time', 99 | required: false, 100 | type: 'number', 101 | schema: { "max_time": 1618585403788 } 102 | } 103 | #swagger.responses[200] = { 104 | schema: { 105 | "result": [ 106 | { $ref: "#/definitions/objkt" } 107 | ] 108 | } 109 | } 110 | #swagger.end */ 111 | }) 112 | 113 | router 114 | .route('/random') 115 | .all((req, res, next) => { 116 | _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.random) 117 | 118 | return next() 119 | }) 120 | .get((req, res, next) => { 121 | /* #swagger.summary = 'Random OBJKTs' 122 | #swagger.description = 'Endpoint used to return an array of a random set of OBJKTs.' 123 | */ 124 | 125 | req.body.counter = req.query.counter 126 | return next() 127 | }, _asyncHandler(readRandomFeed)) 128 | .post(_asyncHandler(readRandomFeed), () => { 129 | /* #swagger.summary = 'Random OBJKTs' 130 | #swagger.description = 'Endpoint used to return an array of a random set of OBJKTs.' 131 | */ 132 | }) 133 | 134 | router 135 | .route('/tz') 136 | .all((req, res, next) => { 137 | _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.issuer) 138 | 139 | return next() 140 | }) 141 | .get((req, res, next) => { 142 | /* #swagger.summary = 'Account information' 143 | #swagger.description = 'Endpoint used to return information about a wallet address. This 144 | includes the OBJKTs that wallet created, those that it holds, and the amount of hDAO it 145 | holds. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500.' 146 | */ 147 | req.body.tz = req.query.tz 148 | 149 | return next() 150 | }, _asyncHandler(readIssuer)) 151 | .post(_asyncHandler(readIssuer), () => { 152 | /* #swagger.summary = 'Account information' 153 | #swagger.description = 'Endpoint used to return information about a wallet address. This 154 | includes the OBJKTs that wallet created, those that it holds, and the amount of hDAO it 155 | holds. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500.' 156 | */ 157 | }) 158 | 159 | router 160 | .route('/objkt') 161 | .all((req, res, next) => { 162 | _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.objkt) 163 | 164 | return next() 165 | }) 166 | .get((req, res, next) => { 167 | /* #swagger.summary = 'OBJKT details' 168 | #swagger.description = 'Endpoint used to return detailed information about an OBJKT.' 169 | */ 170 | req.body.objkt_id = req.query.id 171 | 172 | return next() 173 | }, _asyncHandler(readObjkt)) 174 | .post(_asyncHandler(readObjkt), () => { 175 | /* #swagger.summary = 'OBJKT details' 176 | #swagger.description = 'Endpoint used to return detailed information about an OBJKT.' 177 | */ 178 | }) 179 | 180 | router 181 | .route('/hdao') 182 | .all((req, res, next) => { 183 | _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.hdao) 184 | 185 | return next() 186 | }) 187 | .get((req, res, next) => { 188 | /* #swagger.summary = 'hDAO feed' 189 | #swagger.description = 'Endpoint used to return the list of OBJKTs with hDAO in descending 190 | order of how many hDAO have been spend on them. Data is returned 30 at a time, and can be 191 | paginated. Total results are limited to 30000.' 192 | #swagger.parameters['counter'] = { 193 | in: 'querystring', 194 | description: 'Results page count (default 0)', 195 | type: 'number', 196 | required: false 197 | } 198 | */ 199 | req.body.counter = req.query.counter 200 | 201 | return next() 202 | }, _asyncHandler(readHdaoFeed)) 203 | .post((req, res, next) => { 204 | /* #swagger.summary = 'hDAO feed' 205 | #swagger.description = 'Endpoint used to return the list of OBJKTs with hDAO in descending 206 | order of how many hDAO have been spend on them. Data is returned 30 at a time, and can be 207 | paginated. Total results are limited to 30000.' 208 | #swagger.parameters['counter'] = { 209 | in: 'body', 210 | description: 'Results page count (default 0)', 211 | type: 'number', 212 | required: false, 213 | schema: { "counter": 0 } 214 | } 215 | */ 216 | 217 | return next() 218 | }, _asyncHandler(readHdaoFeed)) 219 | 220 | router.get( 221 | '/recommend_curate', 222 | (req, res, next) => { 223 | /* #swagger.summary = 'hDAO minimum spend recommendation' 224 | #swagger.description = 'Endpoint determines the current swap prices between these currency pairs 225 | (USD:hDAO, XTZ:hDAO), and it calculates the hDAO value of them. This value is returned if larger 226 | than 0.1 hDAO, else 0.1 is returned. This data is cached for 300s. 227 | */ 228 | _setCacheHeaderOnSuccess(res, CACHE_MAX_AGE_MAP.recommendCurate) 229 | 230 | return next() 231 | }, 232 | _asyncHandler(readRecommendCurate) 233 | ) 234 | 235 | // Swagger docs router 236 | var swaggerOptions = { 237 | explorer: true, 238 | defaultModelsExpandDepth: -1, 239 | customCssUrl: 'https://cdn.jsdelivr.net/npm/swagger-ui-themes@3.0.0/themes/3.x/theme-newspaper.css', 240 | customCss: '.swagger-ui .topbar { display: none } ' 241 | }; 242 | router.use('/docs', swaggerUi.serve) 243 | router.get('/docs', swaggerUi.setup(swaggerDocument, swaggerOptions) /* #swagger.ignore = true */ ) 244 | 245 | module.exports = router 246 | 247 | /** 248 | * Express cannot process promise rejections correctly. We need to encapsulate async 249 | * route handlers with logic to pass promise rejections to express's middleware chain 250 | * as an error. 251 | */ 252 | function _asyncHandler(cb) { 253 | return function (req, res, next) { 254 | Promise.resolve(cb(req, res, next)).catch(next) 255 | } 256 | } 257 | 258 | /** 259 | * Set the fetch time for conseil requests and set the Cache-Control header 260 | * on successful responses. 261 | */ 262 | function _processClientCache(req, res, next) { 263 | const clientCacheMaxAge = req.body.max_time || req.query.max_time 264 | const isValidClientCacheTime = Number.isInteger(clientCacheMaxAge) 265 | const now = Date.now() 266 | const fetchTime = isValidClientCacheTime 267 | ? clientCacheMaxAge 268 | : _floorTimeToMinute(now) 269 | 270 | // Set fetch time for feed conseil requests 271 | req.feedFetchAt = fetchTime 272 | 273 | const isImmutable = isValidClientCacheTime && clientCacheMaxAge < now 274 | const cacheMaxAge = isImmutable 275 | ? DEFAULT_CACHE_TTL 276 | : _calcCacheTtl(fetchTime, now) 277 | 278 | // Set cache on successful request 279 | _setCacheHeaderOnSuccess(res, cacheMaxAge) 280 | 281 | return next() 282 | 283 | function _floorTimeToMinute(currentTime) { 284 | return Math.floor(currentTime / ONE_MINUTE_MILLIS) * ONE_MINUTE_MILLIS 285 | } 286 | 287 | function _calcCacheTtl(dataGatheredAt, currentTime) { 288 | return Math.floor( 289 | (dataGatheredAt + ONE_MINUTE_MILLIS - currentTime) / MILLISECOND_MODIFIER 290 | ) 291 | } 292 | } 293 | 294 | /** 295 | * Set cache time for cloudfront if request is successful. 296 | * res.end is called by res.send which is called by res.json. 297 | */ 298 | function _setCacheHeaderOnSuccess(res, maxAge) { 299 | const responseEnd = res.end.bind(res) 300 | 301 | res.end = function (...args) { 302 | if (res.statusCode === STATUS_CODE_SUCCESS) { 303 | res.set('Cache-Control', `public, max-age=${maxAge}`) 304 | } 305 | 306 | return responseEnd(...args) 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /lib/swagger.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const swaggerAutogen = require('swagger-autogen')() 4 | 5 | const doc = { 6 | info: { 7 | title: "Hic et Nunc API", 8 | description: "This API is used as the backend for https://hicetnunc.xyz. It consumes information from a number of Tezos blockchain information providers, including https://cryptonomic.github.io/ConseilJS/ and https://better-call.dev/docs, along with objkt metadata sourced from IPFS" 9 | }, 10 | host: "localhost:3001", 11 | schemes: ['https','http'], 12 | definitions: { 13 | objkt: { 14 | "objectId": "24043", 15 | "ipfsHash": "QmXx8hY7nh41bFzEfXW1iiiKcEBJgsdkjvtMvP2Xj3o2Z7", 16 | "minter": "tz1eT35U51k1FxduLgFFTqrcYV4b6LRtTvUr", 17 | "swaps": [{ $ref: "#/definitions/swap" }], 18 | "token_info": { $ref: "#/definitions/token_info"}, 19 | "token_id": "24043" 20 | }, 21 | swap: { 22 | "swap_id": "24043", 23 | "objkt_id": "20987", 24 | "amount": "14", 25 | "xtz_per_objkt": "1000000" 26 | }, 27 | token_info: { 28 | "name": "Neonland II", 29 | "description": "Circle", 30 | "tags": [ 31 | "neon", 32 | "circle" 33 | ], 34 | "symbol": "OBJKT", 35 | "artifactUri": "ipfs://QmewqUfDNPf8ZV51Awwhs62nkEVHk6fBcivtEVZvpLn5Vi", 36 | "displayUri": "", 37 | "creators": [ 38 | "tz1eT35U51k1FxduLgFFTqrcYV4b6LRtTvUr" 39 | ], 40 | "formats": [ 41 | { 42 | "uri": "ipfs://QmewqUfDNPf8ZV51Awwhs62nkEVHk6fBcivtEVZvpLn5Vi", 43 | "mimeType": "image/png" 44 | } 45 | ], 46 | "thumbnailUri": "ipfs://QmNrhZHUaEqxhyLfqoq1mtHSipkWHeT31LNHb1QEbDHgnc", 47 | "decimals": 0, 48 | "isBooleanAmount": false, 49 | "shouldPreferSymbol": false 50 | } 51 | } 52 | } 53 | 54 | const outputFile = 'swagger-output.json' 55 | const endpointsFiles = ['lib/router/router.js'] 56 | 57 | /* NOTE: if you use the express Router, you must pass in the 58 | 'endpointsFiles' only the root file where the route starts, 59 | such as: index.js, app.js, routes.js, ... */ 60 | 61 | swaggerAutogen(outputFile, endpointsFiles, doc) -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | const axios = require('axios') 4 | const conseil = require('./conseil') 5 | const _ = require('lodash') 6 | 7 | const { feedItemsPerPage } = require('./config') 8 | 9 | module.exports = { 10 | getIpfsHash, 11 | getObjktById, 12 | getObjktOwners, 13 | getRestrictedAddresses, 14 | getRestrictedObjkts, 15 | paginateFeed, 16 | sortFeed, 17 | getObjektByIdBCD, 18 | } 19 | 20 | async function getIpfsHash(ipfsHash) { 21 | return (await axios.get('https://cloudflare-ipfs.com/ipfs/' + ipfsHash)).data 22 | } 23 | 24 | 25 | //Maintain this map if you don't want objekt queries to take a shit-ton of time 26 | const _block_levels = { 27 | "0": 1365242, 28 | "1000": 1372553, 29 | "2000": 1375633, 30 | "3000": 1377717, 31 | "4000": 1379648, 32 | "5000": 1382041, 33 | "6000": 1384721, 34 | "7000": 1388547, 35 | "8000": 1390594, 36 | "9000": 1393010, 37 | "10000": 1394970, 38 | "11000": 1396760, 39 | "12000": 1398667, 40 | "13000": 1400470, 41 | "14000": 1401962, 42 | "15000": 1403406, 43 | "16000": 1404892, 44 | "17000": 1406495, 45 | "18000": 1407853, 46 | "19000": 1409213, 47 | "20000": 1410465, 48 | "21000": 1411763, 49 | "22000": 1413317, 50 | "23000": 1414740, 51 | "24000": 1416229, 52 | "25000": 1417775, 53 | "26000": 1419345, 54 | "27000": 1420800, 55 | "28000": 1422176, 56 | "29000": 1423576, 57 | "30000": 1424923, 58 | "31000": 1426276, 59 | "32000": 1427562, 60 | "33000": 1428886, 61 | "34000": 1430094, 62 | "35000": 1431211, 63 | "36000": 1432197, 64 | "37000": 1433459, 65 | "38000": 1434792, 66 | "39000": 1436072, 67 | "40000": 1437412, 68 | "41000": 1438318, 69 | "42000": 1439212, 70 | "43000": 1440202, 71 | "44000": 1440814, 72 | "45000": 1441702, 73 | "46000": 1442582, 74 | "47000": 1443245, 75 | "48000": 1444101, 76 | "49000": 1444784, 77 | "50000": 1445717, 78 | "51000": 1446437, 79 | "52000": 1447444, 80 | "53000": 1448401, 81 | "54000": 1449172, 82 | "55000": 1450216, 83 | "56000": 1451043, 84 | "57000": 1451899, 85 | "58000": 1453002 86 | } 87 | 88 | 89 | async function getObjektByIdBCD(id) { 90 | let url = `https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens?token_id=${id}` 91 | const bcdData = await axios.get(url) 92 | let objData = bcdData.data[0] 93 | let _ipfsHash = Object.values(objData.extras)[0] 94 | let objkt = { 95 | token_id: id, 96 | ipfsHash: _ipfsHash.replace('ipfs://', '') 97 | } 98 | const [objktOwners, ipfsMetadata] = await Promise.all([ 99 | getObjktOwners(objkt), 100 | getIpfsHash(objkt.ipfsHash), 101 | ]) 102 | 103 | let creators = await getRealIssuer(objkt.token_id); 104 | 105 | Object.assign(objkt, objktOwners, { 106 | token_info: Object.assign({}, ipfsMetadata, { 107 | creators: creators, 108 | }), 109 | swaps: [] 110 | }) 111 | return objkt 112 | 113 | } 114 | 115 | async function getObjktById(id, with_swaps=true) { 116 | const id_int = (typeof id == 'string') ? parseInt(id) : id 117 | let block_range = Math.floor(id_int / 1000) 118 | let block_start = _block_levels[Math.max(Math.min(block_range * 1000, 38_000), 0)] 119 | let block_end = (block_range <= 38) ? _block_levels[Math.min((block_range + 1) * 1000, 39000)] : null 120 | 121 | try { 122 | const objkt = await conseil.getObjectById(id, with_swaps=with_swaps, block_start, block_end).catch(() => {}) 123 | objkt.token_id = objkt.objectId 124 | const [objktOwners, ipfsMetadata, hdBalance] = await Promise.all([ 125 | getObjktOwners(objkt), 126 | getIpfsHash(objkt.ipfsHash), 127 | conseil.getObjkthDAOBalance(objkt.token_id).catch(() => -1) 128 | ]) 129 | 130 | let creators = await getRealIssuer(objkt.token_id); 131 | 132 | Object.assign(objkt, objktOwners, { 133 | token_info: Object.assign({}, ipfsMetadata, { 134 | creators: creators, 135 | }), 136 | hDAO_balance: hdBalance 137 | }) 138 | 139 | return objkt 140 | } 141 | catch { 142 | return {} 143 | } 144 | 145 | } 146 | 147 | async function getRealIssuer (id) { 148 | try { 149 | const apiUrl = `https://api.tzstats.com/explorer/bigmap/522/${id}` 150 | const result = await axios.get(apiUrl) 151 | if (result.data) { 152 | const issuer = result.data.value.issuer 153 | return [ issuer ]; 154 | } else { 155 | throw new Error('No creator found for ' + id) 156 | } 157 | } catch (err) { 158 | throw err; 159 | } 160 | } 161 | 162 | async function getObjktOwners(objkt) { 163 | const owners = ( 164 | await axios.get( 165 | 'https://api.better-call.dev/v1/contract/mainnet/KT1RJ6PbjHpwc3M5rw5s2Nbmefwbuwbdxton/tokens/holders?token_id=' + 166 | objkt.token_id 167 | ) 168 | ).data 169 | 170 | const ownerCountList = _.values(owners) 171 | 172 | let total = 0 173 | 174 | if (ownerCountList.length) { 175 | total = ownerCountList.reduce((acc, i) => { 176 | const owned = parseInt(i) 177 | 178 | return owned > 0 ? acc + owned : acc 179 | }, 0) 180 | } 181 | 182 | return { 183 | total_amount: total, 184 | owners, 185 | } 186 | } 187 | 188 | async function getRestrictedAddresses() { 189 | return ( 190 | await axios.get( 191 | 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/w.json' 192 | ) 193 | ).data 194 | } 195 | 196 | async function getRestrictedObjkts() { 197 | return ( 198 | await axios.get( 199 | 'https://raw.githubusercontent.com/hicetnunc2000/hicetnunc/main/filters/o.json' 200 | ) 201 | ).data 202 | } 203 | 204 | 205 | function paginateFeed(feed, cursor) { 206 | const pageCursor = cursor ? parseInt(cursor) : 0 207 | 208 | return feed.slice( 209 | pageCursor * feedItemsPerPage, 210 | pageCursor * feedItemsPerPage + feedItemsPerPage 211 | ) 212 | } 213 | 214 | function sortFeed(feed) { 215 | return _.sortBy(feed, (i) => parseInt(i.objectId)).reverse() 216 | } 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hicetnunc-apiv2", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "start": "serverless offline start --printOutput", 8 | "start-express": "NODE_ENV=development ./node_modules/.bin/nodemon index.js", 9 | "pretty": "./node_modules/.bin/prettier --loglevel warn --write lib", 10 | "swagger-autogen": "node lib/swagger.js" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "git+https://github.com/hicetnunc2000/hicetnunc-api.git" 15 | }, 16 | "author": "@hicetnunc2000", 17 | "license": "MIT", 18 | "bugs": { 19 | "url": "https://github.com/hicetnunc2000/hicetnunc-api/issues" 20 | }, 21 | "homepage": "https://github.com/hicetnunc2000/hicetnunc-api#readme", 22 | "dependencies": { 23 | "axios": "^0.21.1", 24 | "bignumber.js": "9.0.1", 25 | "bottleneck": "^2.19.5", 26 | "cloud-local-storage": "0.0.11", 27 | "compression": "^1.7.4", 28 | "conseiljs": "5.0.8-1", 29 | "cors": "^2.8.5", 30 | "dotenv": "^8.2.0", 31 | "express": "^4.17.1", 32 | "fetch": "^1.1.0", 33 | "lodash": "^4.17.21", 34 | "loglevel": "1.7.1", 35 | "node-fetch": "2.6.1", 36 | "serverless-dotenv-plugin": "^3.8.1", 37 | "serverless-http": "^2.7.0", 38 | "swagger-autogen": "^2.7.8", 39 | "swagger-ui-express": "^4.1.6" 40 | }, 41 | "engines": { 42 | "node": "12.20.1", 43 | "npm": "6.14.10" 44 | }, 45 | "devDependencies": { 46 | "nodemon": "^2.0.7", 47 | "prettier": "^2.2.1", 48 | "serverless-offline": "^6.9.0", 49 | "serverless-plugin-include-dependencies": "^4.1.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /serverless.yml: -------------------------------------------------------------------------------- 1 | 2 | service: hicetnunc-apiv2 3 | 4 | frameworkVersion: '2.8.0' 5 | 6 | package: 7 | exclude: 8 | - node_modules/** 9 | 10 | plugins: 11 | - serverless-plugin-include-dependencies 12 | - serverless-dotenv-plugin 13 | - serverless-offline 14 | provider: 15 | name: aws 16 | runtime: nodejs12.x 17 | 18 | functions: 19 | handler: 20 | handler: index.handler 21 | timeout: 120 22 | events: 23 | - http: 24 | path: / 25 | method: ANY 26 | cors: 27 | origin: '*' 28 | - http: 29 | path: /{proxy+} 30 | method: ANY 31 | cors: 32 | origin: '*' 33 | 34 | custom: 35 | serverless-offline: 36 | httpPort: 3001 37 | -------------------------------------------------------------------------------- /swagger-output.json: -------------------------------------------------------------------------------- 1 | { 2 | "swagger": "2.0", 3 | "info": { 4 | "title": "Hic et Nunc API", 5 | "description": "This API is used as the backend for https://hicetnunc.xyz. It consumes information from a number of Tezos blockchain information providers, including https://cryptonomic.github.io/ConseilJS/ and https://better-call.dev/docs, along with objkt metadata sourced from IPFS", 6 | "version": "1.0.0" 7 | }, 8 | "host": "localhost:3001", 9 | "basePath": "/", 10 | "tags": [], 11 | "schemes": [ 12 | "https", 13 | "http" 14 | ], 15 | "consumes": [], 16 | "produces": [], 17 | "paths": { 18 | "/feed/{featured}": { 19 | "get": { 20 | "tags": [], 21 | "summary": "Main feed", 22 | "description": "Endpoint used to return the most recently minted OBJKTs. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500. Use of the optional `featured` path parameter will apply a different filter to the feed.", 23 | "parameters": [ 24 | { 25 | "name": "featured", 26 | "in": "path", 27 | "required": false, 28 | "type": "string", 29 | "description": "Applies a filter to the results - returning no more than 1 item per minter, including only those swapped for less than 0.1 tez and that haven't been updated with lots of hDAO.", 30 | "schema": { 31 | "type": "string", 32 | "example": "featured" 33 | } 34 | }, 35 | { 36 | "name": "counter", 37 | "in": "querystring", 38 | "description": "Pagination number. Default is 0", 39 | "required": false, 40 | "type": "number", 41 | "schema": { 42 | "type": "object", 43 | "properties": { 44 | "counter": { 45 | "type": "number", 46 | "example": 0 47 | } 48 | } 49 | } 50 | }, 51 | { 52 | "name": "max_time", 53 | "in": "querystring", 54 | "description": "Unix timestamp. Used to limit the maximum blockchain operation by date/time", 55 | "required": false, 56 | "type": "number", 57 | "schema": { 58 | "type": "object", 59 | "properties": { 60 | "max_time": { 61 | "type": "number", 62 | "example": 1618585403788 63 | } 64 | } 65 | } 66 | } 67 | ], 68 | "responses": { 69 | "200": { 70 | "schema": { 71 | "type": "object", 72 | "properties": { 73 | "result": { 74 | "type": "array", 75 | "items": { 76 | "$ref": "#/definitions/objkt" 77 | } 78 | } 79 | }, 80 | "xml": { 81 | "name": "main" 82 | } 83 | }, 84 | "description": "OK" 85 | } 86 | } 87 | }, 88 | "post": { 89 | "tags": [], 90 | "summary": "Main feed", 91 | "description": "Endpoint used to return the most recently minted OBJKTs. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500. Use of the optional `featured` path parameter will apply a different filter to the feed.", 92 | "parameters": [ 93 | { 94 | "name": "featured", 95 | "in": "path", 96 | "required": false, 97 | "type": "string", 98 | "description": "Applies a filter to the results - returning no more than 1 item per minter, including only those swapped for less than 0.1 tez and that haven't been updated with lots of hDAO.", 99 | "schema": { 100 | "type": "string", 101 | "example": "featured" 102 | } 103 | }, 104 | { 105 | "name": "counter", 106 | "in": "body", 107 | "description": "Pagination number. Default is 0", 108 | "required": false, 109 | "type": "number", 110 | "schema": { 111 | "type": "object", 112 | "properties": { 113 | "counter": { 114 | "type": "number", 115 | "example": 0 116 | } 117 | } 118 | } 119 | }, 120 | { 121 | "name": "max_time", 122 | "in": "body", 123 | "description": "Unix epoch timestamp. Used to limit the maximum blockchain operation by date/time", 124 | "required": false, 125 | "type": "number", 126 | "schema": { 127 | "type": "object", 128 | "properties": { 129 | "max_time": { 130 | "type": "number", 131 | "example": 1618585403788 132 | } 133 | } 134 | } 135 | } 136 | ], 137 | "responses": { 138 | "200": { 139 | "schema": { 140 | "type": "object", 141 | "properties": { 142 | "result": { 143 | "type": "array", 144 | "items": { 145 | "$ref": "#/definitions/objkt" 146 | } 147 | } 148 | }, 149 | "xml": { 150 | "name": "main" 151 | } 152 | }, 153 | "description": "OK" 154 | } 155 | } 156 | } 157 | }, 158 | "/random": { 159 | "get": { 160 | "tags": [], 161 | "summary": "Random OBJKTs", 162 | "description": "Endpoint used to return an array of a random set of OBJKTs.", 163 | "parameters": [ 164 | { 165 | "name": "counter", 166 | "in": "query", 167 | "type": "string" 168 | }, 169 | { 170 | "name": "obj", 171 | "in": "body", 172 | "schema": { 173 | "type": "object", 174 | "properties": { 175 | "counter": { 176 | "type": "string", 177 | "example": "any" 178 | } 179 | } 180 | } 181 | } 182 | ], 183 | "responses": {} 184 | }, 185 | "post": { 186 | "tags": [], 187 | "summary": "Random OBJKTs", 188 | "description": "Endpoint used to return an array of a random set of OBJKTs.", 189 | "parameters": [], 190 | "responses": {} 191 | } 192 | }, 193 | "/tz": { 194 | "get": { 195 | "tags": [], 196 | "summary": "Account information", 197 | "description": "Endpoint used to return information about a wallet address. This includes the OBJKTs that wallet created, those that it holds, and the amount of hDAO it holds. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500.", 198 | "parameters": [ 199 | { 200 | "name": "tz", 201 | "in": "query", 202 | "type": "string" 203 | }, 204 | { 205 | "name": "obj", 206 | "in": "body", 207 | "schema": { 208 | "type": "object", 209 | "properties": { 210 | "tz": { 211 | "type": "string", 212 | "example": "any" 213 | } 214 | } 215 | } 216 | } 217 | ], 218 | "responses": {} 219 | }, 220 | "post": { 221 | "tags": [], 222 | "summary": "Account information", 223 | "description": "Endpoint used to return information about a wallet address. This includes the OBJKTs that wallet created, those that it holds, and the amount of hDAO it holds. Data is returned 30 at a time, and can be paginated. Total results are limited to 2500.", 224 | "parameters": [], 225 | "responses": {} 226 | } 227 | }, 228 | "/objkt": { 229 | "get": { 230 | "tags": [], 231 | "summary": "OBJKT details", 232 | "description": "Endpoint used to return detailed information about an OBJKT.", 233 | "parameters": [ 234 | { 235 | "name": "id", 236 | "in": "query", 237 | "type": "string" 238 | }, 239 | { 240 | "name": "obj", 241 | "in": "body", 242 | "schema": { 243 | "type": "object", 244 | "properties": { 245 | "objkt_id": { 246 | "type": "string", 247 | "example": "any" 248 | } 249 | } 250 | } 251 | } 252 | ], 253 | "responses": {} 254 | }, 255 | "post": { 256 | "tags": [], 257 | "summary": "OBJKT details", 258 | "description": "Endpoint used to return detailed information about an OBJKT.", 259 | "parameters": [], 260 | "responses": {} 261 | } 262 | }, 263 | "/hdao": { 264 | "get": { 265 | "tags": [], 266 | "summary": "hDAO feed", 267 | "description": "Endpoint used to return the list of OBJKTs with hDAO in descending order of how many hDAO have been spend on them. Data is returned 30 at a time, and can be paginated. Total results are limited to 30000.", 268 | "parameters": [ 269 | { 270 | "name": "counter", 271 | "in": "querystring", 272 | "type": "number", 273 | "description": "Results page count (default 0)", 274 | "required": false 275 | }, 276 | { 277 | "name": "obj", 278 | "in": "body", 279 | "schema": { 280 | "type": "object", 281 | "properties": { 282 | "counter": { 283 | "type": "string", 284 | "example": "any" 285 | } 286 | } 287 | } 288 | } 289 | ], 290 | "responses": {} 291 | }, 292 | "post": { 293 | "tags": [], 294 | "summary": "hDAO feed", 295 | "description": "Endpoint used to return the list of OBJKTs with hDAO in descending order of how many hDAO have been spend on them. Data is returned 30 at a time, and can be paginated. Total results are limited to 30000.", 296 | "parameters": [ 297 | { 298 | "name": "counter", 299 | "in": "body", 300 | "description": "Results page count (default 0)", 301 | "type": "number", 302 | "required": false, 303 | "schema": { 304 | "type": "object", 305 | "properties": { 306 | "counter": { 307 | "type": "number", 308 | "example": 0 309 | } 310 | } 311 | } 312 | } 313 | ], 314 | "responses": {} 315 | } 316 | }, 317 | "/recommend_curate": { 318 | "get": { 319 | "tags": [], 320 | "summary": "hDAO minimum spend recommendation", 321 | "description": "", 322 | "parameters": [], 323 | "responses": {} 324 | } 325 | } 326 | }, 327 | "definitions": { 328 | "objkt": { 329 | "type": "object", 330 | "properties": { 331 | "objectId": { 332 | "type": "string", 333 | "example": "24043" 334 | }, 335 | "ipfsHash": { 336 | "type": "string", 337 | "example": "QmXx8hY7nh41bFzEfXW1iiiKcEBJgsdkjvtMvP2Xj3o2Z7" 338 | }, 339 | "minter": { 340 | "type": "string", 341 | "example": "tz1eT35U51k1FxduLgFFTqrcYV4b6LRtTvUr" 342 | }, 343 | "swaps": { 344 | "type": "array", 345 | "items": { 346 | "$ref": "#/definitions/swap" 347 | } 348 | }, 349 | "token_info": { 350 | "$ref": "#/definitions/token_info" 351 | }, 352 | "token_id": { 353 | "type": "string", 354 | "example": "24043" 355 | } 356 | } 357 | }, 358 | "swap": { 359 | "type": "object", 360 | "properties": { 361 | "swap_id": { 362 | "type": "string", 363 | "example": "24043" 364 | }, 365 | "objkt_id": { 366 | "type": "string", 367 | "example": "20987" 368 | }, 369 | "amount": { 370 | "type": "string", 371 | "example": "14" 372 | }, 373 | "xtz_per_objkt": { 374 | "type": "string", 375 | "example": "1000000" 376 | } 377 | } 378 | }, 379 | "token_info": { 380 | "type": "object", 381 | "properties": { 382 | "name": { 383 | "type": "string", 384 | "example": "Neonland II" 385 | }, 386 | "description": { 387 | "type": "string", 388 | "example": "Circle" 389 | }, 390 | "tags": { 391 | "type": "array", 392 | "example": [ 393 | "neon", 394 | "circle" 395 | ], 396 | "items": { 397 | "type": "string" 398 | } 399 | }, 400 | "symbol": { 401 | "type": "string", 402 | "example": "OBJKT" 403 | }, 404 | "artifactUri": { 405 | "type": "string", 406 | "example": "ipfs://QmewqUfDNPf8ZV51Awwhs62nkEVHk6fBcivtEVZvpLn5Vi" 407 | }, 408 | "displayUri": { 409 | "type": "string", 410 | "example": "" 411 | }, 412 | "creators": { 413 | "type": "array", 414 | "example": [ 415 | "tz1eT35U51k1FxduLgFFTqrcYV4b6LRtTvUr" 416 | ], 417 | "items": { 418 | "type": "string" 419 | } 420 | }, 421 | "formats": { 422 | "type": "array", 423 | "items": { 424 | "type": "object", 425 | "properties": { 426 | "uri": { 427 | "type": "string", 428 | "example": "ipfs://QmewqUfDNPf8ZV51Awwhs62nkEVHk6fBcivtEVZvpLn5Vi" 429 | }, 430 | "mimeType": { 431 | "type": "string", 432 | "example": "image/png" 433 | } 434 | } 435 | } 436 | }, 437 | "thumbnailUri": { 438 | "type": "string", 439 | "example": "ipfs://QmNrhZHUaEqxhyLfqoq1mtHSipkWHeT31LNHb1QEbDHgnc" 440 | }, 441 | "decimals": { 442 | "type": "number", 443 | "example": 0 444 | }, 445 | "isBooleanAmount": { 446 | "type": "boolean", 447 | "example": false 448 | }, 449 | "shouldPreferSymbol": { 450 | "type": "boolean", 451 | "example": false 452 | } 453 | } 454 | } 455 | } 456 | } --------------------------------------------------------------------------------