├── .gitignore ├── .prettierrc ├── .profile ├── Dockerfile ├── Procfile ├── Procfile.dev ├── README.md ├── _webpack.config.js ├── app.yaml ├── jest.config.js ├── package.json ├── prisma ├── manual-relationships.txt └── schema.prisma ├── sql-scratchpad.sql ├── src ├── abis │ ├── ERC20.json │ ├── ERC721.json │ ├── IZeroEx.json │ ├── TraitRouter.json │ ├── TraitValidator.json │ └── Trustus.json ├── addresses.ts ├── api │ ├── nft-metadata.ts │ └── orderbook.ts ├── contracts │ ├── ERC20.ts │ ├── ERC721.ts │ ├── IZeroEx.ts │ ├── TraitRouter.ts │ ├── TraitValidator.ts │ ├── Trustus.ts │ ├── common.ts │ ├── factories │ │ ├── ERC20__factory.ts │ │ ├── ERC721__factory.ts │ │ ├── IZeroEx__factory.ts │ │ ├── TraitRouter__factory.ts │ │ ├── TraitValidator__factory.ts │ │ ├── Trustus__factory.ts │ │ └── index.ts │ └── index.ts ├── default-config.ts ├── errors │ └── api-error.ts ├── logger.ts ├── prisma-client.ts ├── redis-client.ts ├── repositories │ └── .gitkeep ├── scripts │ ├── create-order.ts │ ├── nft-lookup.ts │ ├── publish-os-scrape-collection-event.ts │ ├── query.ts │ └── test-validate.ts ├── services │ ├── api-web │ │ ├── app.ts │ │ ├── index.ts │ │ └── utils │ │ │ └── order-parsing.ts │ ├── consumers │ │ ├── get-exchange-events-by-block-number.ts │ │ ├── get-nft-events-by-block-number.ts │ │ ├── nft-metadata.ts │ │ ├── save-block-to-db.ts │ │ ├── sync-opensea-collection-metadata-by-address.ts │ │ └── utils │ │ │ └── exchange-events-parser.ts │ ├── cron │ │ └── sync-opensea-collections.ts │ ├── publishers │ │ ├── block-number.ts │ │ ├── fix-block-gaps-by-job.ts │ │ └── nft-metadata.ts │ └── utils │ │ ├── covalent.ts │ │ ├── jobs.ts │ │ ├── messaging-types.ts │ │ ├── nfts.ts │ │ ├── opensea.ts │ │ ├── pubsub.ts │ │ └── sleep.ts ├── types-complex.ts ├── types.ts └── validations.ts ├── test └── foo.test.ts ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | /client/build 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | 25 | 26 | node_modules 27 | dist 28 | gcp-creds.json 29 | gcp_creds.json 30 | gcp_key.json 31 | gcp-key.json 32 | 33 | 34 | .env 35 | .env.local 36 | .env.development.local 37 | .env.test.local 38 | .env.production.local 39 | 40 | .DS_Store 41 | npm-debug.log* 42 | yarn-debug.log* 43 | yarn-error.log* 44 | 45 | .creds -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "endOfLine": "auto", 4 | "singleQuote": true, 5 | "semi": false, 6 | "printWidth": 120 7 | } -------------------------------------------------------------------------------- /.profile: -------------------------------------------------------------------------------- 1 | # https://devcenter.heroku.com/articles/dynos#startup 2 | # https://stackoverflow.com/a/63447691 3 | 4 | # heroku config:set GOOGLE_APPLICATION_CREDENTIALS=gcp_key.json 5 | # heroku config:set GOOGLE_CREDENTIALS= 6 | 7 | 8 | 9 | echo ${GOOGLE_CREDENTIALS} > /app/gcp_key.json -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | 3 | # Stage 1 4 | FROM node:12-alpine as yarn-install 5 | WORKDIR /usr/src/app 6 | # Install app dependencies 7 | COPY package.json yarn.lock ./ 8 | RUN apk update && \ 9 | apk upgrade && \ 10 | apk add --no-cache --virtual build-dependencies bash git openssh python make g++ && \ 11 | yarn --frozen-lockfile --no-cache && \ 12 | apk del build-dependencies && \ 13 | yarn cache clean 14 | 15 | # Runtime container with minimal dependencies 16 | FROM node:12-alpine 17 | WORKDIR /usr/src/app 18 | COPY --from=yarn-install /usr/src/app/node_modules /usr/src/app/node_modules 19 | # Bundle app source 20 | COPY . . 21 | 22 | RUN yarn build 23 | 24 | EXPOSE 3000 25 | CMD [ "node", "lib/server.js" ] -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: yarn start:api 2 | svc-pub-block: yarn start:service:producer:block-number 3 | svc-pub-fix-gaps-cron: yarn start:service:producer:fix-block-gaps-by-job 4 | svc-con-exch-events: yarn start:service:consumer:exchange-events-by-block-number 5 | svc-con-block-events: yarn start:service:consumer:save-block-to-db 6 | svc-con-nft-metadata: yarn start:service:consumer:nft-metadata 7 | svc-cron-os-collections: yarn start:service:cron:os-collections-sync -------------------------------------------------------------------------------- /Procfile.dev: -------------------------------------------------------------------------------- 1 | web: yarn dev:api 2 | svc-pub-block: yarn dev:service:producer:block-number 3 | svc-pub-fix-gaps-cron: yarn dev:service:producer:fix-block-gaps-by-job 4 | svc-con-exch-events: yarn dev:service:consumer:exchange-events-by-block-number 5 | svc-con-block-events: yarn dev:service:consumer:save-block-to-db 6 | svc-con-nft-metadata: yarn dev:service:consumer:nft-metadata 7 | svc-cron-os-collections: yarn dev:service:cron:os-collections-sync -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Trader.xyz Orderbook 2 | 3 | Some notes: 4 | 5 | ### Heroku config 6 | 7 | #### Preboot 8 | 9 | Make sure preboot is enabled for zero-downtime deploys 10 | `https://devcenter.heroku.com/articles/preboot#enabling-and-disabling-preboot` -------------------------------------------------------------------------------- /_webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | 3 | module.exports = { 4 | entry: './src/index.ts', 5 | output: { 6 | filename: 'worker.js', 7 | path: path.join(__dirname, 'dist'), 8 | }, 9 | // Cloudflare Worker environment is similar to a webworker 10 | target: 'webworker', 11 | resolve: { 12 | extensions: ['.ts', '.tsx', '.js'], 13 | // Alias for resolving the Prisma Client properly 14 | alias: { 15 | '@prisma/client$': require.resolve('@prisma/client'), 16 | }, 17 | }, 18 | mode: 'development', 19 | // Wrangler doesn't like eval which devtools use in development. 20 | devtool: 'none', 21 | module: { 22 | rules: [ 23 | { 24 | // Compile Typescript code 25 | test: /\.tsx?$/, 26 | loader: 'ts-loader', 27 | options: { 28 | transpileOnly: true, 29 | }, 30 | }, 31 | ], 32 | }, 33 | } -------------------------------------------------------------------------------- /app.yaml: -------------------------------------------------------------------------------- 1 | runtime: nodejs 2 | env: flex 3 | 4 | network: 5 | name: default # redis network name 6 | session_affinity: true # websocket https://cloud.google.com/appengine/docs/flexible/nodejs/using-websockets-and-session-affinity 7 | 8 | # While testing... 9 | manual_scaling: 10 | instances: 1 -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '^.+\\.(ts|tsx)$': 'ts-jest', 4 | }, 5 | }; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "trader-orderbook-api", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "author": "johnrjj", 6 | "private": true, 7 | "license": "MIT", 8 | "devDependencies": { 9 | "@cloudflare/workers-types": "^3.8.0", 10 | "@cloudflare/wrangler": "^1.19.12", 11 | "@typechain/ethers-v5": "^10.1.0", 12 | "@types/express": "^4.17.13", 13 | "@types/ioredis": "^4.28.10", 14 | "@types/jest": "^27.4.1", 15 | "@types/lodash": "^4.14.182", 16 | "@types/node": "^17.0.31", 17 | "@types/node-cron": "^3.0.1", 18 | "@types/node-fetch": "2", 19 | "@types/uuid": "^8.3.4", 20 | "foreman": "^3.0.1", 21 | "jest": "^27.5.1", 22 | "prisma": "^4.0.0", 23 | "supertest": "^6.2.3", 24 | "ts-jest": "^27.1.4", 25 | "ts-loader": "9.3.0", 26 | "ts-node": "^10.7.0", 27 | "ts-node-dev": "^1.1.8", 28 | "ts-to-zod": "^1.10.0", 29 | "typechain": "^8.1.0", 30 | "typescript": "^4.6.4", 31 | "webpack": "^5.72.0" 32 | }, 33 | "dependencies": { 34 | "@alch/alchemy-web3": "^1.4.6", 35 | "@ethersproject/abi": "^5.7.0", 36 | "@ethersproject/address": "^5.7.0", 37 | "@ethersproject/bignumber": "^5.7.0", 38 | "@ethersproject/bytes": "^5.7.0", 39 | "@ethersproject/constants": "^5.7.0", 40 | "@ethersproject/contracts": "^5.7.0", 41 | "@ethersproject/providers": "^5.7.0", 42 | "@google-cloud/logging": "^9.8.3", 43 | "@google-cloud/logging-winston": "^4.2.3", 44 | "@google-cloud/pubsub": "^2.19.3", 45 | "@prisma/client": "4.0.0", 46 | "@sentry/node": "^6.19.7", 47 | "@sentry/tracing": "^6.19.7", 48 | "@traderxyz/nft-swap-sdk": "^0.32.0", 49 | "@vercel/fetch": "^6.2.0", 50 | "@zoralabs/nft-metadata": "^1.2.6", 51 | "base-64": "^1.0.0", 52 | "body-parser": "^1.20.0", 53 | "cabin": "^9.1.2", 54 | "compression": "^1.7.4", 55 | "cors": "^2.8.5", 56 | "date-fns": "^2.29.2", 57 | "dotenv": "^16.0.0", 58 | "ethers": "^5.7.0", 59 | "express": "^4.18.1", 60 | "express-rate-limit": "^6.4.0", 61 | "helmet": "^5.0.2", 62 | "ioredis": "^5.0.4", 63 | "lodash": "^4.17.21", 64 | "logdna-winston": "^4.0.1", 65 | "node-cron": "^3.0.0", 66 | "node-fetch": "2", 67 | "ohmyfetch": "^0.4.16", 68 | "pg-template-tag": "^0.1.2", 69 | "prettier": "^2.6.2", 70 | "rate-limit-redis": "^3.0.1", 71 | "sql-template-tag": "^5.0.1", 72 | "uuid": "^8.3.2", 73 | "winston": "^3.7.2", 74 | "zod": "^3.14.4" 75 | }, 76 | "scripts": { 77 | "build": "tsc", 78 | "prettier": "prettier --write 'src/**/*.ts' --config .prettierrc", 79 | "lint": "tslint --project . --format stylish", 80 | "lint:fix": "tslint --project . --format stylish --fix", 81 | "lint:prettier": "prettier --check \"src/**/*.{ts,js,json,yml,md}\"", 82 | "start": "node dist/index.js", 83 | "clean": "rimraf dist", 84 | "start:api": "node dist/services/api-web/index.js", 85 | "start:service:producer:block-number": "node ./dist/services/publishers/block-number.js", 86 | "start:service:producer:fix-block-gaps-by-job": "node ./dist/services/publishers/fix-block-gaps-by-job.js", 87 | "start:service:consumer:exchange-events-by-block-number": "node ./dist/services/consumers/get-exchange-events-by-block-number.js", 88 | "start:service:consumer:save-block-to-db": "node ./dist/services/consumers/save-block-to-db.js", 89 | "start:service:consumer:nft-metadata": "node ./dist/services/consumers/nft-metadata.js", 90 | "start:service:cron:os-collections-sync": "node ./dist/services/cron/sync-opensea-collections.js", 91 | "dev": "nf start --procfile=Procfile.dev web=1,svc-pub-block=1,svc-pub-fix-gaps-cron=1,src-con-exch-events=1,src-con-block-events=1", 92 | "dev:api": "ts-node-dev --transpile-only --files ./src/services/api-web/index.ts", 93 | "dev:service:producer:block-number": "ts-node-dev --transpile-only --files ./src/services/publishers/block-number.ts", 94 | "dev:service:producer:fix-block-gaps-by-job": "ts-node-dev --transpile-only --files ./src/services/publishers/fix-block-gaps-by-job.ts", 95 | "dev:service:consumer:exchange-events-by-block-number": "ts-node-dev --transpile-only --files ./src/services/consumers/get-exchange-events-by-block-number.ts", 96 | "dev:service:consumer:save-block-to-db": "ts-node-dev --transpile-only --files ./src/services/consumers/save-block-to-db.ts", 97 | "dev:service:consumer:nft-metadata": "ts-node-dev --transpile-only --files ./src/services/consumers/nft-metadata.ts", 98 | "dev:service:consumer:os-collection-by-address": "ts-node-dev --transpile-only --files ./src/services/consumers/sync-opensea-collection-metadata-by-address.ts", 99 | "dev:service:cron:os-collections-sync": "ts-node-dev --transpile-only --files ./src/services/cron/sync-opensea-collections.ts", 100 | "scriptthing": "TS_NODE=true NODE_OPTIONS=\"-r ts-node/register\" node ./src/scripts/test.ts", 101 | "test": "jest --passWithNoTests", 102 | "test:watch": "jest --watch", 103 | "typecheck": "tsc --noEmit", 104 | "prisma:pull": "npx prisma db pull", 105 | "prisma:generate": "npx prisma generate", 106 | "zod:generate": "yarn ts-to-zod src/types.ts src/validations.ts", 107 | "prisma:sync": "yarn prisma:pull && yarn prisma:generate", 108 | "generate-types": "typechain --out-dir 'src/contracts' --target=ethers-v5 'src/abis/**/*.json'" 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /prisma/manual-relationships.txt: -------------------------------------------------------------------------------- 1 | https://www.prisma.io/docs/guides/database/using-prisma-with-planetscale#how-to-add-in-missing-relations-after-introspection 2 | 3 | Add this to the order: 4 | order_status order_status_v4_nfts[] 5 | 6 | Add this to the order_status: 7 | order orders_v4_nfts @relation(fields: [nonce], references: [nonce]) -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | previewFeatures = ["fullTextSearch"] 4 | } 5 | 6 | datasource db { 7 | provider = "postgresql" 8 | url = env("DATABASE_URL") 9 | } 10 | 11 | model orders_v4_nfts { 12 | id String @id @unique(map: "id") @db.Text 13 | maker String @db.Text 14 | taker String @default("0x0000000000000000000000000000000000000000") @db.Text 15 | expiry String @db.Text 16 | expiry_datetime DateTime 17 | nonce String @unique @db.Text 18 | erc20_token String @db.Text 19 | erc20_token_amount String @db.Text 20 | fees Json? 21 | nft_token String @db.Text 22 | nft_token_id String @db.Text 23 | nft_token_amount String @db.Text 24 | nft_token_properties Json? 25 | system_metadata Json? 26 | app_metadata Json? 27 | chain_id String @db.Text 28 | verifying_contract String @db.Text 29 | direction String @db.Text 30 | signature Json 31 | nft_type String @db.Text 32 | app_id String? @db.Text 33 | date_created DateTime? @default(now()) 34 | date_last_updated DateTime? @default(now()) 35 | 36 | order_valid Boolean? 37 | date_last_validated DateTime? 38 | 39 | @@unique([chain_id, nonce, erc20_token, erc20_token_amount, nft_token, nft_token_amount], map: "uniq_order_index") 40 | } 41 | 42 | model order_status_v4_nfts { 43 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Text 44 | block_number BigInt? 45 | date_posted_to_db DateTime? @default(now()) 46 | order_status String @db.Text 47 | nonce String @db.Text 48 | block_hash String? @db.Text 49 | address String? @db.Text 50 | data String? @db.Text 51 | transaction_hash String? @db.Text 52 | signature String? @db.Text 53 | topic String? @db.Text 54 | name String? @db.Text 55 | parsed_args Json? 56 | chain_id String @db.Text 57 | verifying_contract String @db.Text 58 | 59 | @@unique([block_hash, order_status, chain_id, nonce], map: "comp") 60 | @@index([block_number], map: "order_status_block_num_idx") 61 | @@index([nonce], map: "order_status_v4_nfts_nonce_index") 62 | @@index([transaction_hash], map: "order_status_v4_nfts_tx_hash") 63 | } 64 | 65 | model job_records { 66 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Text 67 | job_name String @db.Text 68 | chain_id String @db.Text 69 | block_number BigInt 70 | hash String? @db.Text 71 | parent_hash String? @db.Text 72 | job_data Json? 73 | date_processed DateTime? @default(now()) 74 | verifying_contract String @db.Text 75 | 76 | @@unique([job_name, chain_id, block_number, hash, parent_hash], map: "job_record_idx") 77 | } 78 | 79 | model blocks { 80 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Text 81 | number BigInt 82 | hash String @db.Text 83 | parent_hash String @db.Text 84 | nonce String @db.Text 85 | timestamp BigInt 86 | date_mined DateTime 87 | chain_id String @db.Text 88 | date_processed_by_api DateTime? @default(now()) 89 | 90 | @@unique([chain_id, number, hash, parent_hash, nonce], map: "unique_block_idx") 91 | } 92 | 93 | model nft_metadata { 94 | id String @id @default(dbgenerated("gen_random_uuid()")) @db.Text 95 | 96 | token_id String 97 | token_address String 98 | chain_id String 99 | token_uri String 100 | metadata Json? 101 | token_url String? 102 | token_url_mime_type String? 103 | name String? 104 | description String? 105 | content_url String? 106 | content_url_mime_type String? 107 | image_url String? 108 | external_link String? 109 | attributes Json? 110 | date_updated DateTime? @default(now()) 111 | } 112 | 113 | 114 | model opensea_collection_metadata_by_contract_address_v1 { 115 | address String @id 116 | 117 | collection Json 118 | 119 | asset_contract_type String? 120 | created_date String? 121 | name String? 122 | nft_version String? 123 | opensea_version String? 124 | owner String? 125 | schema_name String? 126 | symbol String? 127 | total_supply String? 128 | description String? 129 | external_link String? 130 | image_url String? 131 | default_to_fiat Boolean? 132 | dev_buyer_fee_basis_points String? 133 | dev_seller_fee_basis_points String? 134 | only_proxied_transfers Boolean? 135 | opensea_buyer_fee_basis_points String? 136 | opensea_seller_fee_basis_points String? 137 | buyer_fee_basis_points String? 138 | seller_fee_basis_points String? 139 | payout_address String? 140 | 141 | date_scrape_updated DateTime? @default(now()) 142 | } 143 | 144 | model opensea_collection_metadata_by_slug_v1 { 145 | slug String @id 146 | 147 | name String? 148 | external_link String? 149 | description String? 150 | image_url String? 151 | 152 | primary_asset_contracts Json? 153 | editors Json? 154 | traits Json? 155 | stats Json? 156 | banner_image_url String? 157 | chat_url String? 158 | created_date DateTime? 159 | dev_buyer_fee_basis_points String? 160 | dev_seller_fee_basis_points String? 161 | discord_url String? 162 | display_data Json? 163 | external_url String? 164 | featured Boolean? 165 | featured_image_url String? 166 | hidden Boolean? 167 | safelist_request_status String? 168 | is_subject_to_whitelist Boolean? 169 | 170 | large_image_url String? 171 | medium_username String? 172 | only_proxied_transfers Boolean? 173 | opensea_buyer_fee_basis_points String? 174 | payout_address String? 175 | opensea_seller_fee_basis_points String? 176 | require_email Boolean? 177 | short_description String? 178 | telegram_url String? 179 | twitter_username String? 180 | instagram_username String? 181 | wiki_url String? 182 | is_nsfw Boolean? 183 | 184 | date_updated DateTime? @default(now()) 185 | } 186 | 187 | // model covalent_nft_history_by_contract_v1 { 188 | // chain_id String 189 | // collection_name String 190 | // collection_address String 191 | // collection_ticker_symbol String 192 | // opening_date String 193 | // volume_wei_day String 194 | // volume_quote_day number 195 | // average_volume_wei_day String 196 | // average_volume_quote_day number 197 | // unique_token_ids_sold_count_day number 198 | // floor_price_wei_7d String 199 | // floor_price_quote_7d number 200 | // gas_quote_rate_day number 201 | // quote_currency String 202 | // } 203 | 204 | // This should always be in sync with orders_v4_nfts + a few columns from order_status_v4_nfts 205 | model orders_with_latest_status { 206 | // order properties 207 | maker String 208 | taker String 209 | expiry String 210 | expiry_datetime DateTime 211 | nonce String @unique 212 | erc20_token String 213 | erc20_token_amount String 214 | fees Json? 215 | nft_token String 216 | nft_token_id String 217 | nft_token_amount String 218 | nft_token_properties Json? 219 | system_metadata Json? 220 | app_metadata Json? 221 | chain_id String 222 | verifying_contract String 223 | direction String 224 | signature Json 225 | nft_type String 226 | app_id String? 227 | date_created DateTime? 228 | date_last_updated DateTime? 229 | order_valid Boolean? 230 | date_last_validated DateTime? 231 | // left join order status properties 232 | transaction_hash String? 233 | block_number BigInt? 234 | order_status String? 235 | } 236 | -------------------------------------------------------------------------------- /sql-scratchpad.sql: -------------------------------------------------------------------------------- 1 | -- select * from orders_v4_nfts as orders 2 | -- left outer join order_status_v4_nfts order_status on orders.nonce = order_status.nonce 3 | -- where orders.nonce is not null 4 | -- AND orders.nonce = '0x0ec20fd860da445aabbeccb880c38d44' 5 | 6 | 7 | SELECT erc20_token, 8 | chain_id, 9 | nonce, 10 | app_id, 11 | app_metadata, 12 | date_created, 13 | date_last_updated, 14 | direction, 15 | * 16 | from ( 17 | select 18 | -- Only one status update per nonce (there can be many status updates to a single order) 19 | -- We're only interested in the latest status update (as we assume that's it's final resting state, e.g. filled or cancelled 20 | DISTINCT ON (nonce) * 21 | from ( 22 | orders_v4_nfts as orders 23 | left outer join ( 24 | select block_number, 25 | transaction_hash, 26 | order_status, 27 | nonce as order_nonce_from_update, 28 | date_posted_to_db 29 | from order_status_v4_nfts 30 | order by nonce, 31 | block_number desc, 32 | date_posted_to_db desc 33 | ) as order_status on orders.nonce = order_status.order_nonce_from_update 34 | ) 35 | ) as orders_with_latest_status 36 | where orders_with_latest_status.nonce is not null 37 | order by orders_with_latest_status.date_posted_to_db desc; 38 | 39 | -- AND orders.nonce = '0x0ec20fd860da445aabbeccb880c38d44' 40 | -- 0x7845622560b14d79a7dc07c85bbdf086 41 | 42 | drop 43 | view if exists orders_with_latest_status; 44 | create view orders_with_latest_status as 45 | select 46 | -- Only one status update per nonce, per maker (there can be many status updates to a single order) 47 | -- We're only interested in the latest status update (as we assume that's it's final resting state, e.g. filled or cancelled 48 | DISTINCT ON (nonce, maker) * 49 | from 50 | ( 51 | orders_v4_nfts as orders 52 | left outer join ( 53 | select 54 | block_number, 55 | transaction_hash, 56 | order_status, 57 | nonce as order_nonce_from_update, 58 | date_posted_to_db 59 | from 60 | order_status_v4_nfts 61 | order by 62 | block_number desc, 63 | date_posted_to_db desc 64 | ) as order_status on orders.nonce = order_status.order_nonce_from_update 65 | ); 66 | -------------------------------------------------------------------------------- /src/abis/ERC20.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "constant": true, 4 | "inputs": [], 5 | "name": "name", 6 | "outputs": [ 7 | { 8 | "name": "", 9 | "type": "string" 10 | } 11 | ], 12 | "payable": false, 13 | "stateMutability": "view", 14 | "type": "function" 15 | }, 16 | { 17 | "constant": false, 18 | "inputs": [ 19 | { 20 | "name": "_spender", 21 | "type": "address" 22 | }, 23 | { 24 | "name": "_value", 25 | "type": "uint256" 26 | } 27 | ], 28 | "name": "approve", 29 | "outputs": [ 30 | { 31 | "name": "", 32 | "type": "bool" 33 | } 34 | ], 35 | "payable": false, 36 | "stateMutability": "nonpayable", 37 | "type": "function" 38 | }, 39 | { 40 | "constant": true, 41 | "inputs": [], 42 | "name": "totalSupply", 43 | "outputs": [ 44 | { 45 | "name": "", 46 | "type": "uint256" 47 | } 48 | ], 49 | "payable": false, 50 | "stateMutability": "view", 51 | "type": "function" 52 | }, 53 | { 54 | "constant": false, 55 | "inputs": [ 56 | { 57 | "name": "_from", 58 | "type": "address" 59 | }, 60 | { 61 | "name": "_to", 62 | "type": "address" 63 | }, 64 | { 65 | "name": "_value", 66 | "type": "uint256" 67 | } 68 | ], 69 | "name": "transferFrom", 70 | "outputs": [ 71 | { 72 | "name": "", 73 | "type": "bool" 74 | } 75 | ], 76 | "payable": false, 77 | "stateMutability": "nonpayable", 78 | "type": "function" 79 | }, 80 | { 81 | "constant": true, 82 | "inputs": [], 83 | "name": "decimals", 84 | "outputs": [ 85 | { 86 | "name": "", 87 | "type": "uint8" 88 | } 89 | ], 90 | "payable": false, 91 | "stateMutability": "view", 92 | "type": "function" 93 | }, 94 | { 95 | "constant": true, 96 | "inputs": [ 97 | { 98 | "name": "_owner", 99 | "type": "address" 100 | } 101 | ], 102 | "name": "balanceOf", 103 | "outputs": [ 104 | { 105 | "name": "balance", 106 | "type": "uint256" 107 | } 108 | ], 109 | "payable": false, 110 | "stateMutability": "view", 111 | "type": "function" 112 | }, 113 | { 114 | "constant": true, 115 | "inputs": [], 116 | "name": "symbol", 117 | "outputs": [ 118 | { 119 | "name": "", 120 | "type": "string" 121 | } 122 | ], 123 | "payable": false, 124 | "stateMutability": "view", 125 | "type": "function" 126 | }, 127 | { 128 | "constant": false, 129 | "inputs": [ 130 | { 131 | "name": "_to", 132 | "type": "address" 133 | }, 134 | { 135 | "name": "_value", 136 | "type": "uint256" 137 | } 138 | ], 139 | "name": "transfer", 140 | "outputs": [ 141 | { 142 | "name": "", 143 | "type": "bool" 144 | } 145 | ], 146 | "payable": false, 147 | "stateMutability": "nonpayable", 148 | "type": "function" 149 | }, 150 | { 151 | "constant": true, 152 | "inputs": [ 153 | { 154 | "name": "_owner", 155 | "type": "address" 156 | }, 157 | { 158 | "name": "_spender", 159 | "type": "address" 160 | } 161 | ], 162 | "name": "allowance", 163 | "outputs": [ 164 | { 165 | "name": "", 166 | "type": "uint256" 167 | } 168 | ], 169 | "payable": false, 170 | "stateMutability": "view", 171 | "type": "function" 172 | }, 173 | { 174 | "payable": true, 175 | "stateMutability": "payable", 176 | "type": "fallback" 177 | }, 178 | { 179 | "anonymous": false, 180 | "inputs": [ 181 | { 182 | "indexed": true, 183 | "name": "owner", 184 | "type": "address" 185 | }, 186 | { 187 | "indexed": true, 188 | "name": "spender", 189 | "type": "address" 190 | }, 191 | { 192 | "indexed": false, 193 | "name": "value", 194 | "type": "uint256" 195 | } 196 | ], 197 | "name": "Approval", 198 | "type": "event" 199 | }, 200 | { 201 | "anonymous": false, 202 | "inputs": [ 203 | { 204 | "indexed": true, 205 | "name": "from", 206 | "type": "address" 207 | }, 208 | { 209 | "indexed": true, 210 | "name": "to", 211 | "type": "address" 212 | }, 213 | { 214 | "indexed": false, 215 | "name": "value", 216 | "type": "uint256" 217 | } 218 | ], 219 | "name": "Transfer", 220 | "type": "event" 221 | } 222 | ] 223 | -------------------------------------------------------------------------------- /src/addresses.ts: -------------------------------------------------------------------------------- 1 | export interface AddressesForChain { 2 | exchange: string 3 | wrappedNativeToken: string 4 | } 5 | 6 | const addresses: { [key: string]: AddressesForChain | undefined } = { 7 | '1': { 8 | exchange: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', 9 | wrappedNativeToken: '0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2', 10 | }, 11 | '3': { 12 | exchange: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', 13 | wrappedNativeToken: '0xc778417e063141139fce010982780140aa0cd5ab', 14 | }, 15 | '4': { 16 | exchange: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', 17 | wrappedNativeToken: '0xc778417e063141139fce010982780140aa0cd5ab', 18 | }, 19 | '5': { 20 | exchange: '0xf91bb752490473b8342a3e964e855b9f9a2a668e', 21 | wrappedNativeToken: '0xb4fbf271143f4fbf7b91a5ded31805e42b2208d6', 22 | }, 23 | '10': { 24 | exchange: '0xdef1abe32c034e558cdd535791643c58a13acc10', 25 | wrappedNativeToken: '', 26 | }, 27 | '42': { 28 | exchange: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', 29 | wrappedNativeToken: '0xd0a1e359811322d97991e03f863a0c30c2cf029c', 30 | }, 31 | '56': { 32 | exchange: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', 33 | wrappedNativeToken: '0xbb4cdb9cbd36b01bd1cbaebf2de08d9173bc095c', 34 | }, 35 | '1337': { 36 | exchange: '0x5315e44798395d4a952530d131249fe00f554565', 37 | wrappedNativeToken: '0x0b1ba0af832d7c05fd64161e0db78e85978e8082', 38 | }, 39 | '137': { 40 | exchange: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', 41 | wrappedNativeToken: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', 42 | }, 43 | '80001': { 44 | exchange: '0x4fb72262344034e034fce3d9c701fd9213a55260', 45 | wrappedNativeToken: '0x9c3c9283d3e44854697cd22d3faa240cfb032889', 46 | }, 47 | '43114': { 48 | exchange: '0xdef1c0ded9bec7f1a1670819833240f027b25eff', 49 | wrappedNativeToken: '0xb31f66aa3c1e785363f0875a1b74e27b85fd66c7', 50 | }, 51 | '42161': { 52 | exchange: '0xdef1abe32c034e558cdd535791643c58a13acc10', 53 | wrappedNativeToken: '', 54 | }, 55 | } 56 | 57 | export { addresses } 58 | -------------------------------------------------------------------------------- /src/api/nft-metadata.ts: -------------------------------------------------------------------------------- 1 | import { Router } from 'express' 2 | import { getLoggerForService, ServiceNamesLogLabel } from '../logger' 3 | import { getPrismaClient } from '../prisma-client' 4 | import { createApiError } from '../errors/api-error' 5 | import { publishNftMetadataForNftRequest } from '../services/publishers/nft-metadata' 6 | import { 7 | fetchNftMetadataFromAlchemy, 8 | fetchNftsForWallet, 9 | getNftMetadataForContractOnAnyChain, 10 | getNftMetadataOnAnyChain, 11 | } from '../services/utils/nfts' 12 | import { fetchOpenseaCollectionByContractAddress } from '../services/utils/opensea' 13 | import { upsertOpenSeaCollectionScrapedData } from '../services/consumers/sync-opensea-collection-metadata-by-address' 14 | 15 | const prisma = getPrismaClient() 16 | 17 | const logger = getLoggerForService(ServiceNamesLogLabel['api-web']) 18 | 19 | const createNftMetadataRequestRouter = () => { 20 | const nftMetadataRouter = Router() 21 | 22 | nftMetadataRouter.get('/', (_, res) => res.sendStatus(200)) 23 | nftMetadataRouter.get('/healthcheck', (_, res) => res.sendStatus(200)) 24 | 25 | // Fetch collection metadata (from opensea (or cached opensea)) 26 | nftMetadataRouter.get('/:chainId/:contractAddress', async (req, res, next) => { 27 | const sync = req.query.sync 28 | 29 | const chainId = req.params.chainId 30 | const contractAddress = req.params.contractAddress 31 | if (!chainId) { 32 | return res.status(400).json(createApiError('CHAIN_ID_MISSING', 'chainId missing from GET')) 33 | } 34 | if (!contractAddress) { 35 | return res.status(400).json(createApiError('CONTRACT_ADDRESS_MISSING', 'contractAddress missing from GET')) 36 | } 37 | 38 | const maybeExistingOpenSeaContractMetadata = 39 | await prisma.opensea_collection_metadata_by_contract_address_v1.findFirst({ 40 | where: { 41 | address: contractAddress.toLowerCase(), 42 | }, 43 | }) 44 | 45 | if (maybeExistingOpenSeaContractMetadata) { 46 | logger.info(`API:NFTCollectionMetadata: Found existing NFT collection metadata ${contractAddress}.`, { 47 | chainId, 48 | contractAddress, 49 | }) 50 | return res.json({ 51 | osData: maybeExistingOpenSeaContractMetadata, 52 | contractAddress, 53 | chainId, 54 | }) 55 | } 56 | 57 | if (sync) { 58 | logger.info( 59 | `API:NFTCollectionMetadata: Syncing new NFT Collection metadata ${contractAddress}. Looking up metadata.`, 60 | { chainId, contractAddress } 61 | ) 62 | try { 63 | const openseaCollectionByContractAddress = await fetchOpenseaCollectionByContractAddress( 64 | contractAddress, 65 | chainId 66 | ) 67 | if (openseaCollectionByContractAddress === null) { 68 | logger.info(`API:NFTCollectionMetadata: 404 looking for asset`, { 69 | chainId, 70 | contractAddress, 71 | openseaCollectionByContractAddress, 72 | }) 73 | return res.json({ 74 | osDataError: 'Collection not found', 75 | osData: null, 76 | contractAddress, 77 | chainId, 78 | }) 79 | } 80 | const upsertedRes = await upsertOpenSeaCollectionScrapedData(openseaCollectionByContractAddress) 81 | logger.info( 82 | `API:NFTCollectionMetadata: Inserted new NFT Collection metadata into db ${contractAddress}. Looking up metadata.`, 83 | { chainId, contractAddress, upsertedRes, openseaCollectionByContractAddress } 84 | ) 85 | return res.json({ 86 | osData: upsertedRes, 87 | contractAddress, 88 | chainId, 89 | }) 90 | } catch (e) { 91 | logger.error(`API:NFTCollectionMetadata: Error!`, { chainId, contractAddress, e }) 92 | return res.json({ 93 | osDataError: 'Error syncing OpenSea data', 94 | osData: null, 95 | contractAddress, 96 | chainId, 97 | }) 98 | } 99 | } 100 | 101 | return res.json({ 102 | osData: null, 103 | contractAddress, 104 | chainId, 105 | }) 106 | }) 107 | 108 | // Get nfts for wallet 109 | nftMetadataRouter.get('/wallet/:walletAddress/:chainId/', async (req, res, next) => { 110 | const chainId = req.params.chainId 111 | const walletAddress = req.params.walletAddress 112 | 113 | const pageKey = req.query.pageKey?.toString() 114 | let maybeAllowlist: string[] | undefined = undefined 115 | if (req.query.allowlist) { 116 | if (Array.isArray(req.query.allowlist)) { 117 | maybeAllowlist = [...(req.query.allowlist as string[])] 118 | } else { 119 | maybeAllowlist = [req.query.allowlist as string] 120 | } 121 | } 122 | 123 | const sync = req.query.sync 124 | 125 | if (!chainId) { 126 | return res.status(400).json(createApiError('CHAIN_ID_MISSING', 'chainId missing from GET')) 127 | } 128 | if (!walletAddress) { 129 | return res.status(400).json(createApiError('WALLET_ADDRESS_MISSING', 'walletAddress missing from GET')) 130 | } 131 | 132 | let parsedChainId: number | null = null 133 | try { 134 | parsedChainId = parseInt(chainId, 10) 135 | } catch (e) { 136 | return res.status(400).json(createApiError('INVALID_CHAIN_ID', 'Problem parsing chainId')) 137 | } 138 | try { 139 | const metadata = await fetchNftsForWallet( 140 | walletAddress, 141 | parsedChainId.toString(), 142 | maybeAllowlist, 143 | pageKey ?? undefined 144 | ) 145 | return res.status(200).json(metadata) 146 | } catch (e: any) { 147 | console.log(e) 148 | return res.status(400).json({ error: e.error ?? e.message ?? e.code ?? e }) 149 | } 150 | }) 151 | 152 | // NFT metadata from alchemy 153 | nftMetadataRouter.get('/:chainId/:contractAddress/:tokenId/alchemy', async (req, res, next) => { 154 | const chainId = req.params.chainId 155 | const contractAddress = req.params.contractAddress 156 | const tokenId = req.params.tokenId 157 | 158 | const sync = req.query.sync 159 | 160 | if (!chainId) { 161 | return res.status(400).json(createApiError('CHAIN_ID_MISSING', 'chainId missing from GET')) 162 | } 163 | if (!contractAddress) { 164 | return res.status(400).json(createApiError('CONTRACT_ADDRESS_MISSING', 'contractAddress missing from GET')) 165 | } 166 | if (!tokenId) { 167 | return res.status(400).json(createApiError('TOKEN_ID_MIDDING', 'tokenId missing from GET')) 168 | } 169 | 170 | let parsedChainId: number | null = null 171 | try { 172 | parsedChainId = parseInt(chainId, 10) 173 | } catch (e) { 174 | return res.status(400).json(createApiError('INVALID_CHAIN_ID', 'Problem parsing chainId')) 175 | } 176 | try { 177 | const metadata = await fetchNftMetadataFromAlchemy(contractAddress, tokenId, parsedChainId.toString()) 178 | return res.status(200).json(metadata) 179 | } catch (e: any) { 180 | return res.status(400).json({ error: e.error ?? e.message ?? e.code ?? e }) 181 | } 182 | }) 183 | 184 | // Collection address on chain 185 | nftMetadataRouter.get('/:chainId/:contractAddress/onchain', async (req, res, next) => { 186 | const chainId = req.params.chainId 187 | const contractAddress = req.params.contractAddress 188 | 189 | const sync = req.query.sync 190 | 191 | if (!chainId) { 192 | return res.status(400).json(createApiError('CHAIN_ID_MISSING', 'chainId missing from GET')) 193 | } 194 | if (!contractAddress) { 195 | return res.status(400).json(createApiError('CONTRACT_ADDRESS_MISSING', 'contractAddress missing from GET')) 196 | } 197 | 198 | let parsedChainId: number | null = null 199 | try { 200 | parsedChainId = parseInt(chainId, 10) 201 | } catch (e) { 202 | return res.status(400).json(createApiError('INVALID_CHAIN_ID', 'Problem parsing chainId')) 203 | } 204 | try { 205 | const metadata = await getNftMetadataForContractOnAnyChain(contractAddress, parsedChainId.toString()) 206 | return res.status(200).json(metadata) 207 | } catch (e: any) { 208 | return res.status(400).json({ error: e.error ?? e.message ?? e.code ?? e }) 209 | } 210 | }) 211 | 212 | // nftMetadataRouter.get('/:chainId/:contractAddress/:tokenId/opensea', async (req, res, next) => { 213 | // const chainId = req.params.chainId 214 | // const contractAddress = req.params.contractAddress 215 | // const tokenId = req.params.tokenId 216 | 217 | // const sync = req.query.sync 218 | 219 | // if (!chainId) { 220 | // return res.status(400).json(createApiError('CHAIN_ID_MISSING', 'chainId missing from GET')) 221 | // } 222 | // if (!contractAddress) { 223 | // return res.status(400).json(createApiError('CONTRACT_ADDRESS_MISSING', 'contractAddress missing from GET')) 224 | // } 225 | // if (!tokenId) { 226 | // return res.status(400).json(createApiError('TOKEN_ID_MIDDING', 'tokenId missing from GET')) 227 | // } 228 | 229 | // let parsedChainId: number | null = null 230 | // try { 231 | // parsedChainId = parseInt(chainId, 10) 232 | // } catch (e) { 233 | // return res.status(400).json(createApiError('INVALID_CHAIN_ID', 'Problem parsing chainId')) 234 | // } 235 | // try { 236 | // const metadata = await fetchNftMetadataFromAlchemy(contractAddress, tokenId, parsedChainId) 237 | // return res.status(200).json(metadata) 238 | // } catch (e) { 239 | // return res.status(400).json({ error: e }) 240 | // } 241 | // }) 242 | 243 | nftMetadataRouter.get('/:chainId/:contractAddress/:tokenId', async (req, res, next) => { 244 | const chainId = req.params.chainId 245 | const contractAddress = req.params.contractAddress 246 | const tokenId = req.params.tokenId 247 | 248 | const sync = req.query.sync 249 | 250 | if (!chainId) { 251 | return res.status(400).json(createApiError('CHAIN_ID_MISSING', 'chainId missing from GET')) 252 | } 253 | if (!contractAddress) { 254 | return res.status(400).json(createApiError('CONTRACT_ADDRESS_MISSING', 'contractAddress missing from GET')) 255 | } 256 | if (!tokenId) { 257 | return res.status(400).json(createApiError('TOKEN_ID_MIDDING', 'tokenId missing from GET')) 258 | } 259 | logger.info(`Publishing nft metadata request message`, { chainId, contractAddress, tokenId }) 260 | 261 | if (sync?.toString() === 'true') { 262 | const nftMetadata = await getNftMetadataOnAnyChain(contractAddress, tokenId, chainId) 263 | return res.status(200).json({ 264 | nftMetadata, 265 | }) 266 | } 267 | 268 | const pubRequestMsgId = await publishNftMetadataForNftRequest(contractAddress, tokenId, chainId) 269 | return res.status(200).json({ id: pubRequestMsgId }) 270 | }) 271 | 272 | return nftMetadataRouter 273 | } 274 | 275 | export { createNftMetadataRequestRouter } 276 | -------------------------------------------------------------------------------- /src/contracts/ERC20.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { 5 | BaseContract, 6 | BigNumber, 7 | BigNumberish, 8 | BytesLike, 9 | CallOverrides, 10 | ContractTransaction, 11 | Overrides, 12 | PopulatedTransaction, 13 | Signer, 14 | utils, 15 | } from 'ethers' 16 | import type { FunctionFragment, Result, EventFragment } from '@ethersproject/abi' 17 | import type { Listener, Provider } from '@ethersproject/providers' 18 | import type { TypedEventFilter, TypedEvent, TypedListener, OnEvent, PromiseOrValue } from './common' 19 | 20 | export interface ERC20Interface extends utils.Interface { 21 | functions: { 22 | 'name()': FunctionFragment 23 | 'approve(address,uint256)': FunctionFragment 24 | 'totalSupply()': FunctionFragment 25 | 'transferFrom(address,address,uint256)': FunctionFragment 26 | 'decimals()': FunctionFragment 27 | 'balanceOf(address)': FunctionFragment 28 | 'symbol()': FunctionFragment 29 | 'transfer(address,uint256)': FunctionFragment 30 | 'allowance(address,address)': FunctionFragment 31 | } 32 | 33 | getFunction( 34 | nameOrSignatureOrTopic: 35 | | 'name' 36 | | 'approve' 37 | | 'totalSupply' 38 | | 'transferFrom' 39 | | 'decimals' 40 | | 'balanceOf' 41 | | 'symbol' 42 | | 'transfer' 43 | | 'allowance' 44 | ): FunctionFragment 45 | 46 | encodeFunctionData(functionFragment: 'name', values?: undefined): string 47 | encodeFunctionData( 48 | functionFragment: 'approve', 49 | values: [PromiseOrValue, PromiseOrValue] 50 | ): string 51 | encodeFunctionData(functionFragment: 'totalSupply', values?: undefined): string 52 | encodeFunctionData( 53 | functionFragment: 'transferFrom', 54 | values: [PromiseOrValue, PromiseOrValue, PromiseOrValue] 55 | ): string 56 | encodeFunctionData(functionFragment: 'decimals', values?: undefined): string 57 | encodeFunctionData(functionFragment: 'balanceOf', values: [PromiseOrValue]): string 58 | encodeFunctionData(functionFragment: 'symbol', values?: undefined): string 59 | encodeFunctionData( 60 | functionFragment: 'transfer', 61 | values: [PromiseOrValue, PromiseOrValue] 62 | ): string 63 | encodeFunctionData(functionFragment: 'allowance', values: [PromiseOrValue, PromiseOrValue]): string 64 | 65 | decodeFunctionResult(functionFragment: 'name', data: BytesLike): Result 66 | decodeFunctionResult(functionFragment: 'approve', data: BytesLike): Result 67 | decodeFunctionResult(functionFragment: 'totalSupply', data: BytesLike): Result 68 | decodeFunctionResult(functionFragment: 'transferFrom', data: BytesLike): Result 69 | decodeFunctionResult(functionFragment: 'decimals', data: BytesLike): Result 70 | decodeFunctionResult(functionFragment: 'balanceOf', data: BytesLike): Result 71 | decodeFunctionResult(functionFragment: 'symbol', data: BytesLike): Result 72 | decodeFunctionResult(functionFragment: 'transfer', data: BytesLike): Result 73 | decodeFunctionResult(functionFragment: 'allowance', data: BytesLike): Result 74 | 75 | events: { 76 | 'Approval(address,address,uint256)': EventFragment 77 | 'Transfer(address,address,uint256)': EventFragment 78 | } 79 | 80 | getEvent(nameOrSignatureOrTopic: 'Approval'): EventFragment 81 | getEvent(nameOrSignatureOrTopic: 'Transfer'): EventFragment 82 | } 83 | 84 | export interface ApprovalEventObject { 85 | owner: string 86 | spender: string 87 | value: BigNumber 88 | } 89 | export type ApprovalEvent = TypedEvent<[string, string, BigNumber], ApprovalEventObject> 90 | 91 | export type ApprovalEventFilter = TypedEventFilter 92 | 93 | export interface TransferEventObject { 94 | from: string 95 | to: string 96 | value: BigNumber 97 | } 98 | export type TransferEvent = TypedEvent<[string, string, BigNumber], TransferEventObject> 99 | 100 | export type TransferEventFilter = TypedEventFilter 101 | 102 | export interface ERC20 extends BaseContract { 103 | connect(signerOrProvider: Signer | Provider | string): this 104 | attach(addressOrName: string): this 105 | deployed(): Promise 106 | 107 | interface: ERC20Interface 108 | 109 | queryFilter( 110 | event: TypedEventFilter, 111 | fromBlockOrBlockhash?: string | number | undefined, 112 | toBlock?: string | number | undefined 113 | ): Promise> 114 | 115 | listeners(eventFilter?: TypedEventFilter): Array> 116 | listeners(eventName?: string): Array 117 | removeAllListeners(eventFilter: TypedEventFilter): this 118 | removeAllListeners(eventName?: string): this 119 | off: OnEvent 120 | on: OnEvent 121 | once: OnEvent 122 | removeListener: OnEvent 123 | 124 | functions: { 125 | name(overrides?: CallOverrides): Promise<[string]> 126 | 127 | approve( 128 | _spender: PromiseOrValue, 129 | _value: PromiseOrValue, 130 | overrides?: Overrides & { from?: PromiseOrValue } 131 | ): Promise 132 | 133 | totalSupply(overrides?: CallOverrides): Promise<[BigNumber]> 134 | 135 | transferFrom( 136 | _from: PromiseOrValue, 137 | _to: PromiseOrValue, 138 | _value: PromiseOrValue, 139 | overrides?: Overrides & { from?: PromiseOrValue } 140 | ): Promise 141 | 142 | decimals(overrides?: CallOverrides): Promise<[number]> 143 | 144 | balanceOf(_owner: PromiseOrValue, overrides?: CallOverrides): Promise<[BigNumber] & { balance: BigNumber }> 145 | 146 | symbol(overrides?: CallOverrides): Promise<[string]> 147 | 148 | transfer( 149 | _to: PromiseOrValue, 150 | _value: PromiseOrValue, 151 | overrides?: Overrides & { from?: PromiseOrValue } 152 | ): Promise 153 | 154 | allowance( 155 | _owner: PromiseOrValue, 156 | _spender: PromiseOrValue, 157 | overrides?: CallOverrides 158 | ): Promise<[BigNumber]> 159 | } 160 | 161 | name(overrides?: CallOverrides): Promise 162 | 163 | approve( 164 | _spender: PromiseOrValue, 165 | _value: PromiseOrValue, 166 | overrides?: Overrides & { from?: PromiseOrValue } 167 | ): Promise 168 | 169 | totalSupply(overrides?: CallOverrides): Promise 170 | 171 | transferFrom( 172 | _from: PromiseOrValue, 173 | _to: PromiseOrValue, 174 | _value: PromiseOrValue, 175 | overrides?: Overrides & { from?: PromiseOrValue } 176 | ): Promise 177 | 178 | decimals(overrides?: CallOverrides): Promise 179 | 180 | balanceOf(_owner: PromiseOrValue, overrides?: CallOverrides): Promise 181 | 182 | symbol(overrides?: CallOverrides): Promise 183 | 184 | transfer( 185 | _to: PromiseOrValue, 186 | _value: PromiseOrValue, 187 | overrides?: Overrides & { from?: PromiseOrValue } 188 | ): Promise 189 | 190 | allowance( 191 | _owner: PromiseOrValue, 192 | _spender: PromiseOrValue, 193 | overrides?: CallOverrides 194 | ): Promise 195 | 196 | callStatic: { 197 | name(overrides?: CallOverrides): Promise 198 | 199 | approve( 200 | _spender: PromiseOrValue, 201 | _value: PromiseOrValue, 202 | overrides?: CallOverrides 203 | ): Promise 204 | 205 | totalSupply(overrides?: CallOverrides): Promise 206 | 207 | transferFrom( 208 | _from: PromiseOrValue, 209 | _to: PromiseOrValue, 210 | _value: PromiseOrValue, 211 | overrides?: CallOverrides 212 | ): Promise 213 | 214 | decimals(overrides?: CallOverrides): Promise 215 | 216 | balanceOf(_owner: PromiseOrValue, overrides?: CallOverrides): Promise 217 | 218 | symbol(overrides?: CallOverrides): Promise 219 | 220 | transfer( 221 | _to: PromiseOrValue, 222 | _value: PromiseOrValue, 223 | overrides?: CallOverrides 224 | ): Promise 225 | 226 | allowance( 227 | _owner: PromiseOrValue, 228 | _spender: PromiseOrValue, 229 | overrides?: CallOverrides 230 | ): Promise 231 | } 232 | 233 | filters: { 234 | 'Approval(address,address,uint256)'( 235 | owner?: PromiseOrValue | null, 236 | spender?: PromiseOrValue | null, 237 | value?: null 238 | ): ApprovalEventFilter 239 | Approval( 240 | owner?: PromiseOrValue | null, 241 | spender?: PromiseOrValue | null, 242 | value?: null 243 | ): ApprovalEventFilter 244 | 245 | 'Transfer(address,address,uint256)'( 246 | from?: PromiseOrValue | null, 247 | to?: PromiseOrValue | null, 248 | value?: null 249 | ): TransferEventFilter 250 | Transfer( 251 | from?: PromiseOrValue | null, 252 | to?: PromiseOrValue | null, 253 | value?: null 254 | ): TransferEventFilter 255 | } 256 | 257 | estimateGas: { 258 | name(overrides?: CallOverrides): Promise 259 | 260 | approve( 261 | _spender: PromiseOrValue, 262 | _value: PromiseOrValue, 263 | overrides?: Overrides & { from?: PromiseOrValue } 264 | ): Promise 265 | 266 | totalSupply(overrides?: CallOverrides): Promise 267 | 268 | transferFrom( 269 | _from: PromiseOrValue, 270 | _to: PromiseOrValue, 271 | _value: PromiseOrValue, 272 | overrides?: Overrides & { from?: PromiseOrValue } 273 | ): Promise 274 | 275 | decimals(overrides?: CallOverrides): Promise 276 | 277 | balanceOf(_owner: PromiseOrValue, overrides?: CallOverrides): Promise 278 | 279 | symbol(overrides?: CallOverrides): Promise 280 | 281 | transfer( 282 | _to: PromiseOrValue, 283 | _value: PromiseOrValue, 284 | overrides?: Overrides & { from?: PromiseOrValue } 285 | ): Promise 286 | 287 | allowance( 288 | _owner: PromiseOrValue, 289 | _spender: PromiseOrValue, 290 | overrides?: CallOverrides 291 | ): Promise 292 | } 293 | 294 | populateTransaction: { 295 | name(overrides?: CallOverrides): Promise 296 | 297 | approve( 298 | _spender: PromiseOrValue, 299 | _value: PromiseOrValue, 300 | overrides?: Overrides & { from?: PromiseOrValue } 301 | ): Promise 302 | 303 | totalSupply(overrides?: CallOverrides): Promise 304 | 305 | transferFrom( 306 | _from: PromiseOrValue, 307 | _to: PromiseOrValue, 308 | _value: PromiseOrValue, 309 | overrides?: Overrides & { from?: PromiseOrValue } 310 | ): Promise 311 | 312 | decimals(overrides?: CallOverrides): Promise 313 | 314 | balanceOf(_owner: PromiseOrValue, overrides?: CallOverrides): Promise 315 | 316 | symbol(overrides?: CallOverrides): Promise 317 | 318 | transfer( 319 | _to: PromiseOrValue, 320 | _value: PromiseOrValue, 321 | overrides?: Overrides & { from?: PromiseOrValue } 322 | ): Promise 323 | 324 | allowance( 325 | _owner: PromiseOrValue, 326 | _spender: PromiseOrValue, 327 | overrides?: CallOverrides 328 | ): Promise 329 | } 330 | } 331 | -------------------------------------------------------------------------------- /src/contracts/TraitRouter.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { 5 | BaseContract, 6 | BigNumber, 7 | BigNumberish, 8 | BytesLike, 9 | CallOverrides, 10 | ContractTransaction, 11 | Overrides, 12 | PopulatedTransaction, 13 | Signer, 14 | utils, 15 | } from 'ethers' 16 | import type { FunctionFragment, Result } from '@ethersproject/abi' 17 | import type { Listener, Provider } from '@ethersproject/providers' 18 | import type { TypedEventFilter, TypedEvent, TypedListener, OnEvent, PromiseOrValue } from './common' 19 | 20 | export interface TraitRouterInterface extends utils.Interface { 21 | functions: { 22 | 'onERC721Received(address,address,uint256,bytes)': FunctionFragment 23 | 'traitValidator()': FunctionFragment 24 | 'zeroExV4()': FunctionFragment 25 | } 26 | 27 | getFunction(nameOrSignatureOrTopic: 'onERC721Received' | 'traitValidator' | 'zeroExV4'): FunctionFragment 28 | 29 | encodeFunctionData( 30 | functionFragment: 'onERC721Received', 31 | values: [PromiseOrValue, PromiseOrValue, PromiseOrValue, PromiseOrValue] 32 | ): string 33 | encodeFunctionData(functionFragment: 'traitValidator', values?: undefined): string 34 | encodeFunctionData(functionFragment: 'zeroExV4', values?: undefined): string 35 | 36 | decodeFunctionResult(functionFragment: 'onERC721Received', data: BytesLike): Result 37 | decodeFunctionResult(functionFragment: 'traitValidator', data: BytesLike): Result 38 | decodeFunctionResult(functionFragment: 'zeroExV4', data: BytesLike): Result 39 | 40 | events: {} 41 | } 42 | 43 | export interface TraitRouter extends BaseContract { 44 | connect(signerOrProvider: Signer | Provider | string): this 45 | attach(addressOrName: string): this 46 | deployed(): Promise 47 | 48 | interface: TraitRouterInterface 49 | 50 | queryFilter( 51 | event: TypedEventFilter, 52 | fromBlockOrBlockhash?: string | number | undefined, 53 | toBlock?: string | number | undefined 54 | ): Promise> 55 | 56 | listeners(eventFilter?: TypedEventFilter): Array> 57 | listeners(eventName?: string): Array 58 | removeAllListeners(eventFilter: TypedEventFilter): this 59 | removeAllListeners(eventName?: string): this 60 | off: OnEvent 61 | on: OnEvent 62 | once: OnEvent 63 | removeListener: OnEvent 64 | 65 | functions: { 66 | onERC721Received( 67 | arg0: PromiseOrValue, 68 | from: PromiseOrValue, 69 | tokenId: PromiseOrValue, 70 | data: PromiseOrValue, 71 | overrides?: Overrides & { from?: PromiseOrValue } 72 | ): Promise 73 | 74 | traitValidator(overrides?: CallOverrides): Promise<[string]> 75 | 76 | zeroExV4(overrides?: CallOverrides): Promise<[string]> 77 | } 78 | 79 | onERC721Received( 80 | arg0: PromiseOrValue, 81 | from: PromiseOrValue, 82 | tokenId: PromiseOrValue, 83 | data: PromiseOrValue, 84 | overrides?: Overrides & { from?: PromiseOrValue } 85 | ): Promise 86 | 87 | traitValidator(overrides?: CallOverrides): Promise 88 | 89 | zeroExV4(overrides?: CallOverrides): Promise 90 | 91 | callStatic: { 92 | onERC721Received( 93 | arg0: PromiseOrValue, 94 | from: PromiseOrValue, 95 | tokenId: PromiseOrValue, 96 | data: PromiseOrValue, 97 | overrides?: CallOverrides 98 | ): Promise 99 | 100 | traitValidator(overrides?: CallOverrides): Promise 101 | 102 | zeroExV4(overrides?: CallOverrides): Promise 103 | } 104 | 105 | filters: {} 106 | 107 | estimateGas: { 108 | onERC721Received( 109 | arg0: PromiseOrValue, 110 | from: PromiseOrValue, 111 | tokenId: PromiseOrValue, 112 | data: PromiseOrValue, 113 | overrides?: Overrides & { from?: PromiseOrValue } 114 | ): Promise 115 | 116 | traitValidator(overrides?: CallOverrides): Promise 117 | 118 | zeroExV4(overrides?: CallOverrides): Promise 119 | } 120 | 121 | populateTransaction: { 122 | onERC721Received( 123 | arg0: PromiseOrValue, 124 | from: PromiseOrValue, 125 | tokenId: PromiseOrValue, 126 | data: PromiseOrValue, 127 | overrides?: Overrides & { from?: PromiseOrValue } 128 | ): Promise 129 | 130 | traitValidator(overrides?: CallOverrides): Promise 131 | 132 | zeroExV4(overrides?: CallOverrides): Promise 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /src/contracts/TraitValidator.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { 5 | BaseContract, 6 | BigNumber, 7 | BigNumberish, 8 | BytesLike, 9 | CallOverrides, 10 | ContractTransaction, 11 | Overrides, 12 | PopulatedTransaction, 13 | Signer, 14 | utils, 15 | } from 'ethers' 16 | import type { FunctionFragment, Result, EventFragment } from '@ethersproject/abi' 17 | import type { Listener, Provider } from '@ethersproject/providers' 18 | import type { TypedEventFilter, TypedEvent, TypedListener, OnEvent, PromiseOrValue } from './common' 19 | 20 | export declare namespace Trustus { 21 | export type TrustusPacketStruct = { 22 | v: PromiseOrValue 23 | r: PromiseOrValue 24 | s: PromiseOrValue 25 | request: PromiseOrValue 26 | deadline: PromiseOrValue 27 | payload: PromiseOrValue 28 | } 29 | 30 | export type TrustusPacketStructOutput = [number, string, string, string, BigNumber, string] & { 31 | v: number 32 | r: string 33 | s: string 34 | request: string 35 | deadline: BigNumber 36 | payload: string 37 | } 38 | } 39 | 40 | export interface TraitValidatorInterface extends utils.Interface { 41 | functions: { 42 | 'DOMAIN_SEPARATOR()': FunctionFragment 43 | 'propertyDeadline(bytes32)': FunctionFragment 44 | 'setProperty(bytes32,(uint8,bytes32,bytes32,bytes32,uint256,bytes))': FunctionFragment 45 | 'validateProperty(address,uint256,bytes)': FunctionFragment 46 | } 47 | 48 | getFunction( 49 | nameOrSignatureOrTopic: 'DOMAIN_SEPARATOR' | 'propertyDeadline' | 'setProperty' | 'validateProperty' 50 | ): FunctionFragment 51 | 52 | encodeFunctionData(functionFragment: 'DOMAIN_SEPARATOR', values?: undefined): string 53 | encodeFunctionData(functionFragment: 'propertyDeadline', values: [PromiseOrValue]): string 54 | encodeFunctionData( 55 | functionFragment: 'setProperty', 56 | values: [PromiseOrValue, Trustus.TrustusPacketStruct] 57 | ): string 58 | encodeFunctionData( 59 | functionFragment: 'validateProperty', 60 | values: [PromiseOrValue, PromiseOrValue, PromiseOrValue] 61 | ): string 62 | 63 | decodeFunctionResult(functionFragment: 'DOMAIN_SEPARATOR', data: BytesLike): Result 64 | decodeFunctionResult(functionFragment: 'propertyDeadline', data: BytesLike): Result 65 | decodeFunctionResult(functionFragment: 'setProperty', data: BytesLike): Result 66 | decodeFunctionResult(functionFragment: 'validateProperty', data: BytesLike): Result 67 | 68 | events: { 69 | 'PropertySet(bytes32,uint256)': EventFragment 70 | } 71 | 72 | getEvent(nameOrSignatureOrTopic: 'PropertySet'): EventFragment 73 | } 74 | 75 | export interface PropertySetEventObject { 76 | request: string 77 | deadline: BigNumber 78 | } 79 | export type PropertySetEvent = TypedEvent<[string, BigNumber], PropertySetEventObject> 80 | 81 | export type PropertySetEventFilter = TypedEventFilter 82 | 83 | export interface TraitValidator extends BaseContract { 84 | connect(signerOrProvider: Signer | Provider | string): this 85 | attach(addressOrName: string): this 86 | deployed(): Promise 87 | 88 | interface: TraitValidatorInterface 89 | 90 | queryFilter( 91 | event: TypedEventFilter, 92 | fromBlockOrBlockhash?: string | number | undefined, 93 | toBlock?: string | number | undefined 94 | ): Promise> 95 | 96 | listeners(eventFilter?: TypedEventFilter): Array> 97 | listeners(eventName?: string): Array 98 | removeAllListeners(eventFilter: TypedEventFilter): this 99 | removeAllListeners(eventName?: string): this 100 | off: OnEvent 101 | on: OnEvent 102 | once: OnEvent 103 | removeListener: OnEvent 104 | 105 | functions: { 106 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise<[string]> 107 | 108 | propertyDeadline(arg0: PromiseOrValue, overrides?: CallOverrides): Promise<[BigNumber]> 109 | 110 | setProperty( 111 | request: PromiseOrValue, 112 | packet: Trustus.TrustusPacketStruct, 113 | overrides?: Overrides & { from?: PromiseOrValue } 114 | ): Promise 115 | 116 | validateProperty( 117 | tokenAddress: PromiseOrValue, 118 | tokenId: PromiseOrValue, 119 | propertyData: PromiseOrValue, 120 | overrides?: CallOverrides 121 | ): Promise<[void]> 122 | } 123 | 124 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 125 | 126 | propertyDeadline(arg0: PromiseOrValue, overrides?: CallOverrides): Promise 127 | 128 | setProperty( 129 | request: PromiseOrValue, 130 | packet: Trustus.TrustusPacketStruct, 131 | overrides?: Overrides & { from?: PromiseOrValue } 132 | ): Promise 133 | 134 | validateProperty( 135 | tokenAddress: PromiseOrValue, 136 | tokenId: PromiseOrValue, 137 | propertyData: PromiseOrValue, 138 | overrides?: CallOverrides 139 | ): Promise 140 | 141 | callStatic: { 142 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 143 | 144 | propertyDeadline(arg0: PromiseOrValue, overrides?: CallOverrides): Promise 145 | 146 | setProperty( 147 | request: PromiseOrValue, 148 | packet: Trustus.TrustusPacketStruct, 149 | overrides?: CallOverrides 150 | ): Promise 151 | 152 | validateProperty( 153 | tokenAddress: PromiseOrValue, 154 | tokenId: PromiseOrValue, 155 | propertyData: PromiseOrValue, 156 | overrides?: CallOverrides 157 | ): Promise 158 | } 159 | 160 | filters: { 161 | 'PropertySet(bytes32,uint256)'(request?: PromiseOrValue | null, deadline?: null): PropertySetEventFilter 162 | PropertySet(request?: PromiseOrValue | null, deadline?: null): PropertySetEventFilter 163 | } 164 | 165 | estimateGas: { 166 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 167 | 168 | propertyDeadline(arg0: PromiseOrValue, overrides?: CallOverrides): Promise 169 | 170 | setProperty( 171 | request: PromiseOrValue, 172 | packet: Trustus.TrustusPacketStruct, 173 | overrides?: Overrides & { from?: PromiseOrValue } 174 | ): Promise 175 | 176 | validateProperty( 177 | tokenAddress: PromiseOrValue, 178 | tokenId: PromiseOrValue, 179 | propertyData: PromiseOrValue, 180 | overrides?: CallOverrides 181 | ): Promise 182 | } 183 | 184 | populateTransaction: { 185 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 186 | 187 | propertyDeadline(arg0: PromiseOrValue, overrides?: CallOverrides): Promise 188 | 189 | setProperty( 190 | request: PromiseOrValue, 191 | packet: Trustus.TrustusPacketStruct, 192 | overrides?: Overrides & { from?: PromiseOrValue } 193 | ): Promise 194 | 195 | validateProperty( 196 | tokenAddress: PromiseOrValue, 197 | tokenId: PromiseOrValue, 198 | propertyData: PromiseOrValue, 199 | overrides?: CallOverrides 200 | ): Promise 201 | } 202 | } 203 | -------------------------------------------------------------------------------- /src/contracts/Trustus.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { BaseContract, BigNumber, BytesLike, CallOverrides, PopulatedTransaction, Signer, utils } from 'ethers' 5 | import type { FunctionFragment, Result } from '@ethersproject/abi' 6 | import type { Listener, Provider } from '@ethersproject/providers' 7 | import type { TypedEventFilter, TypedEvent, TypedListener, OnEvent, PromiseOrValue } from './common' 8 | 9 | export interface TrustusInterface extends utils.Interface { 10 | functions: { 11 | 'DOMAIN_SEPARATOR()': FunctionFragment 12 | } 13 | 14 | getFunction(nameOrSignatureOrTopic: 'DOMAIN_SEPARATOR'): FunctionFragment 15 | 16 | encodeFunctionData(functionFragment: 'DOMAIN_SEPARATOR', values?: undefined): string 17 | 18 | decodeFunctionResult(functionFragment: 'DOMAIN_SEPARATOR', data: BytesLike): Result 19 | 20 | events: {} 21 | } 22 | 23 | export interface Trustus extends BaseContract { 24 | connect(signerOrProvider: Signer | Provider | string): this 25 | attach(addressOrName: string): this 26 | deployed(): Promise 27 | 28 | interface: TrustusInterface 29 | 30 | queryFilter( 31 | event: TypedEventFilter, 32 | fromBlockOrBlockhash?: string | number | undefined, 33 | toBlock?: string | number | undefined 34 | ): Promise> 35 | 36 | listeners(eventFilter?: TypedEventFilter): Array> 37 | listeners(eventName?: string): Array 38 | removeAllListeners(eventFilter: TypedEventFilter): this 39 | removeAllListeners(eventName?: string): this 40 | off: OnEvent 41 | on: OnEvent 42 | once: OnEvent 43 | removeListener: OnEvent 44 | 45 | functions: { 46 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise<[string]> 47 | } 48 | 49 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 50 | 51 | callStatic: { 52 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 53 | } 54 | 55 | filters: {} 56 | 57 | estimateGas: { 58 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 59 | } 60 | 61 | populateTransaction: { 62 | DOMAIN_SEPARATOR(overrides?: CallOverrides): Promise 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/contracts/common.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import type { Listener } from '@ethersproject/providers' 5 | import type { Event, EventFilter } from 'ethers' 6 | 7 | export interface TypedEvent = any, TArgsObject = any> extends Event { 8 | args: TArgsArray & TArgsObject 9 | } 10 | 11 | export interface TypedEventFilter<_TEvent extends TypedEvent> extends EventFilter {} 12 | 13 | export interface TypedListener { 14 | (...listenerArg: [...__TypechainArgsArray, TEvent]): void 15 | } 16 | 17 | type __TypechainArgsArray = T extends TypedEvent ? U : never 18 | 19 | export interface OnEvent { 20 | (eventFilter: TypedEventFilter, listener: TypedListener): TRes 21 | (eventName: string, listener: Listener): TRes 22 | } 23 | 24 | export type MinEthersFactory = { 25 | deploy(...a: ARGS[]): Promise 26 | } 27 | 28 | export type GetContractTypeFromFactory = F extends MinEthersFactory ? C : never 29 | 30 | export type GetARGsTypeFromFactory = F extends MinEthersFactory ? Parameters : never 31 | 32 | export type PromiseOrValue = T | Promise 33 | -------------------------------------------------------------------------------- /src/contracts/factories/ERC20__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Signer, utils } from 'ethers' 6 | import type { Provider } from '@ethersproject/providers' 7 | import type { ERC20, ERC20Interface } from '../ERC20' 8 | 9 | const _abi = [ 10 | { 11 | constant: true, 12 | inputs: [], 13 | name: 'name', 14 | outputs: [ 15 | { 16 | name: '', 17 | type: 'string', 18 | }, 19 | ], 20 | payable: false, 21 | stateMutability: 'view', 22 | type: 'function', 23 | }, 24 | { 25 | constant: false, 26 | inputs: [ 27 | { 28 | name: '_spender', 29 | type: 'address', 30 | }, 31 | { 32 | name: '_value', 33 | type: 'uint256', 34 | }, 35 | ], 36 | name: 'approve', 37 | outputs: [ 38 | { 39 | name: '', 40 | type: 'bool', 41 | }, 42 | ], 43 | payable: false, 44 | stateMutability: 'nonpayable', 45 | type: 'function', 46 | }, 47 | { 48 | constant: true, 49 | inputs: [], 50 | name: 'totalSupply', 51 | outputs: [ 52 | { 53 | name: '', 54 | type: 'uint256', 55 | }, 56 | ], 57 | payable: false, 58 | stateMutability: 'view', 59 | type: 'function', 60 | }, 61 | { 62 | constant: false, 63 | inputs: [ 64 | { 65 | name: '_from', 66 | type: 'address', 67 | }, 68 | { 69 | name: '_to', 70 | type: 'address', 71 | }, 72 | { 73 | name: '_value', 74 | type: 'uint256', 75 | }, 76 | ], 77 | name: 'transferFrom', 78 | outputs: [ 79 | { 80 | name: '', 81 | type: 'bool', 82 | }, 83 | ], 84 | payable: false, 85 | stateMutability: 'nonpayable', 86 | type: 'function', 87 | }, 88 | { 89 | constant: true, 90 | inputs: [], 91 | name: 'decimals', 92 | outputs: [ 93 | { 94 | name: '', 95 | type: 'uint8', 96 | }, 97 | ], 98 | payable: false, 99 | stateMutability: 'view', 100 | type: 'function', 101 | }, 102 | { 103 | constant: true, 104 | inputs: [ 105 | { 106 | name: '_owner', 107 | type: 'address', 108 | }, 109 | ], 110 | name: 'balanceOf', 111 | outputs: [ 112 | { 113 | name: 'balance', 114 | type: 'uint256', 115 | }, 116 | ], 117 | payable: false, 118 | stateMutability: 'view', 119 | type: 'function', 120 | }, 121 | { 122 | constant: true, 123 | inputs: [], 124 | name: 'symbol', 125 | outputs: [ 126 | { 127 | name: '', 128 | type: 'string', 129 | }, 130 | ], 131 | payable: false, 132 | stateMutability: 'view', 133 | type: 'function', 134 | }, 135 | { 136 | constant: false, 137 | inputs: [ 138 | { 139 | name: '_to', 140 | type: 'address', 141 | }, 142 | { 143 | name: '_value', 144 | type: 'uint256', 145 | }, 146 | ], 147 | name: 'transfer', 148 | outputs: [ 149 | { 150 | name: '', 151 | type: 'bool', 152 | }, 153 | ], 154 | payable: false, 155 | stateMutability: 'nonpayable', 156 | type: 'function', 157 | }, 158 | { 159 | constant: true, 160 | inputs: [ 161 | { 162 | name: '_owner', 163 | type: 'address', 164 | }, 165 | { 166 | name: '_spender', 167 | type: 'address', 168 | }, 169 | ], 170 | name: 'allowance', 171 | outputs: [ 172 | { 173 | name: '', 174 | type: 'uint256', 175 | }, 176 | ], 177 | payable: false, 178 | stateMutability: 'view', 179 | type: 'function', 180 | }, 181 | { 182 | payable: true, 183 | stateMutability: 'payable', 184 | type: 'fallback', 185 | }, 186 | { 187 | anonymous: false, 188 | inputs: [ 189 | { 190 | indexed: true, 191 | name: 'owner', 192 | type: 'address', 193 | }, 194 | { 195 | indexed: true, 196 | name: 'spender', 197 | type: 'address', 198 | }, 199 | { 200 | indexed: false, 201 | name: 'value', 202 | type: 'uint256', 203 | }, 204 | ], 205 | name: 'Approval', 206 | type: 'event', 207 | }, 208 | { 209 | anonymous: false, 210 | inputs: [ 211 | { 212 | indexed: true, 213 | name: 'from', 214 | type: 'address', 215 | }, 216 | { 217 | indexed: true, 218 | name: 'to', 219 | type: 'address', 220 | }, 221 | { 222 | indexed: false, 223 | name: 'value', 224 | type: 'uint256', 225 | }, 226 | ], 227 | name: 'Transfer', 228 | type: 'event', 229 | }, 230 | ] 231 | 232 | export class ERC20__factory { 233 | static readonly abi = _abi 234 | static createInterface(): ERC20Interface { 235 | return new utils.Interface(_abi) as ERC20Interface 236 | } 237 | static connect(address: string, signerOrProvider: Signer | Provider): ERC20 { 238 | return new Contract(address, _abi, signerOrProvider) as ERC20 239 | } 240 | } 241 | -------------------------------------------------------------------------------- /src/contracts/factories/ERC721__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Signer, utils } from 'ethers' 6 | import type { Provider } from '@ethersproject/providers' 7 | import type { ERC721, ERC721Interface } from '../ERC721' 8 | 9 | const _abi = [ 10 | { 11 | anonymous: false, 12 | inputs: [ 13 | { 14 | indexed: true, 15 | internalType: 'address', 16 | name: 'owner', 17 | type: 'address', 18 | }, 19 | { 20 | indexed: true, 21 | internalType: 'address', 22 | name: 'spender', 23 | type: 'address', 24 | }, 25 | { 26 | indexed: true, 27 | internalType: 'uint256', 28 | name: 'id', 29 | type: 'uint256', 30 | }, 31 | ], 32 | name: 'Approval', 33 | type: 'event', 34 | }, 35 | { 36 | anonymous: false, 37 | inputs: [ 38 | { 39 | indexed: true, 40 | internalType: 'address', 41 | name: 'owner', 42 | type: 'address', 43 | }, 44 | { 45 | indexed: true, 46 | internalType: 'address', 47 | name: 'operator', 48 | type: 'address', 49 | }, 50 | { 51 | indexed: false, 52 | internalType: 'bool', 53 | name: 'approved', 54 | type: 'bool', 55 | }, 56 | ], 57 | name: 'ApprovalForAll', 58 | type: 'event', 59 | }, 60 | { 61 | anonymous: false, 62 | inputs: [ 63 | { 64 | indexed: true, 65 | internalType: 'address', 66 | name: 'from', 67 | type: 'address', 68 | }, 69 | { 70 | indexed: true, 71 | internalType: 'address', 72 | name: 'to', 73 | type: 'address', 74 | }, 75 | { 76 | indexed: true, 77 | internalType: 'uint256', 78 | name: 'id', 79 | type: 'uint256', 80 | }, 81 | ], 82 | name: 'Transfer', 83 | type: 'event', 84 | }, 85 | { 86 | inputs: [ 87 | { 88 | internalType: 'address', 89 | name: 'spender', 90 | type: 'address', 91 | }, 92 | { 93 | internalType: 'uint256', 94 | name: 'id', 95 | type: 'uint256', 96 | }, 97 | ], 98 | name: 'approve', 99 | outputs: [], 100 | stateMutability: 'nonpayable', 101 | type: 'function', 102 | }, 103 | { 104 | inputs: [ 105 | { 106 | internalType: 'address', 107 | name: '', 108 | type: 'address', 109 | }, 110 | ], 111 | name: 'balanceOf', 112 | outputs: [ 113 | { 114 | internalType: 'uint256', 115 | name: '', 116 | type: 'uint256', 117 | }, 118 | ], 119 | stateMutability: 'view', 120 | type: 'function', 121 | }, 122 | { 123 | inputs: [ 124 | { 125 | internalType: 'uint256', 126 | name: '', 127 | type: 'uint256', 128 | }, 129 | ], 130 | name: 'getApproved', 131 | outputs: [ 132 | { 133 | internalType: 'address', 134 | name: '', 135 | type: 'address', 136 | }, 137 | ], 138 | stateMutability: 'view', 139 | type: 'function', 140 | }, 141 | { 142 | inputs: [ 143 | { 144 | internalType: 'address', 145 | name: '', 146 | type: 'address', 147 | }, 148 | { 149 | internalType: 'address', 150 | name: '', 151 | type: 'address', 152 | }, 153 | ], 154 | name: 'isApprovedForAll', 155 | outputs: [ 156 | { 157 | internalType: 'bool', 158 | name: '', 159 | type: 'bool', 160 | }, 161 | ], 162 | stateMutability: 'view', 163 | type: 'function', 164 | }, 165 | { 166 | inputs: [], 167 | name: 'name', 168 | outputs: [ 169 | { 170 | internalType: 'string', 171 | name: '', 172 | type: 'string', 173 | }, 174 | ], 175 | stateMutability: 'view', 176 | type: 'function', 177 | }, 178 | { 179 | inputs: [ 180 | { 181 | internalType: 'uint256', 182 | name: '', 183 | type: 'uint256', 184 | }, 185 | ], 186 | name: 'ownerOf', 187 | outputs: [ 188 | { 189 | internalType: 'address', 190 | name: '', 191 | type: 'address', 192 | }, 193 | ], 194 | stateMutability: 'view', 195 | type: 'function', 196 | }, 197 | { 198 | inputs: [ 199 | { 200 | internalType: 'address', 201 | name: 'from', 202 | type: 'address', 203 | }, 204 | { 205 | internalType: 'address', 206 | name: 'to', 207 | type: 'address', 208 | }, 209 | { 210 | internalType: 'uint256', 211 | name: 'id', 212 | type: 'uint256', 213 | }, 214 | ], 215 | name: 'safeTransferFrom', 216 | outputs: [], 217 | stateMutability: 'nonpayable', 218 | type: 'function', 219 | }, 220 | { 221 | inputs: [ 222 | { 223 | internalType: 'address', 224 | name: 'from', 225 | type: 'address', 226 | }, 227 | { 228 | internalType: 'address', 229 | name: 'to', 230 | type: 'address', 231 | }, 232 | { 233 | internalType: 'uint256', 234 | name: 'id', 235 | type: 'uint256', 236 | }, 237 | { 238 | internalType: 'bytes', 239 | name: 'data', 240 | type: 'bytes', 241 | }, 242 | ], 243 | name: 'safeTransferFrom', 244 | outputs: [], 245 | stateMutability: 'nonpayable', 246 | type: 'function', 247 | }, 248 | { 249 | inputs: [ 250 | { 251 | internalType: 'address', 252 | name: 'operator', 253 | type: 'address', 254 | }, 255 | { 256 | internalType: 'bool', 257 | name: 'approved', 258 | type: 'bool', 259 | }, 260 | ], 261 | name: 'setApprovalForAll', 262 | outputs: [], 263 | stateMutability: 'nonpayable', 264 | type: 'function', 265 | }, 266 | { 267 | inputs: [ 268 | { 269 | internalType: 'bytes4', 270 | name: 'interfaceId', 271 | type: 'bytes4', 272 | }, 273 | ], 274 | name: 'supportsInterface', 275 | outputs: [ 276 | { 277 | internalType: 'bool', 278 | name: '', 279 | type: 'bool', 280 | }, 281 | ], 282 | stateMutability: 'pure', 283 | type: 'function', 284 | }, 285 | { 286 | inputs: [], 287 | name: 'symbol', 288 | outputs: [ 289 | { 290 | internalType: 'string', 291 | name: '', 292 | type: 'string', 293 | }, 294 | ], 295 | stateMutability: 'view', 296 | type: 'function', 297 | }, 298 | { 299 | inputs: [ 300 | { 301 | internalType: 'uint256', 302 | name: 'id', 303 | type: 'uint256', 304 | }, 305 | ], 306 | name: 'tokenURI', 307 | outputs: [ 308 | { 309 | internalType: 'string', 310 | name: '', 311 | type: 'string', 312 | }, 313 | ], 314 | stateMutability: 'view', 315 | type: 'function', 316 | }, 317 | { 318 | inputs: [ 319 | { 320 | internalType: 'address', 321 | name: 'from', 322 | type: 'address', 323 | }, 324 | { 325 | internalType: 'address', 326 | name: 'to', 327 | type: 'address', 328 | }, 329 | { 330 | internalType: 'uint256', 331 | name: 'id', 332 | type: 'uint256', 333 | }, 334 | ], 335 | name: 'transferFrom', 336 | outputs: [], 337 | stateMutability: 'nonpayable', 338 | type: 'function', 339 | }, 340 | ] 341 | 342 | export class ERC721__factory { 343 | static readonly abi = _abi 344 | static createInterface(): ERC721Interface { 345 | return new utils.Interface(_abi) as ERC721Interface 346 | } 347 | static connect(address: string, signerOrProvider: Signer | Provider): ERC721 { 348 | return new Contract(address, _abi, signerOrProvider) as ERC721 349 | } 350 | } 351 | -------------------------------------------------------------------------------- /src/contracts/factories/TraitRouter__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import { Signer, utils, Contract, ContractFactory, Overrides } from 'ethers' 5 | import type { Provider, TransactionRequest } from '@ethersproject/providers' 6 | import type { PromiseOrValue } from '../common' 7 | import type { TraitRouter, TraitRouterInterface } from '../TraitRouter' 8 | 9 | const _abi = [ 10 | { 11 | inputs: [ 12 | { 13 | internalType: 'address', 14 | name: 'traitValidatorAddress', 15 | type: 'address', 16 | }, 17 | ], 18 | stateMutability: 'nonpayable', 19 | type: 'constructor', 20 | }, 21 | { 22 | inputs: [ 23 | { 24 | internalType: 'address', 25 | name: '', 26 | type: 'address', 27 | }, 28 | { 29 | internalType: 'address', 30 | name: 'from', 31 | type: 'address', 32 | }, 33 | { 34 | internalType: 'uint256', 35 | name: 'tokenId', 36 | type: 'uint256', 37 | }, 38 | { 39 | internalType: 'bytes', 40 | name: 'data', 41 | type: 'bytes', 42 | }, 43 | ], 44 | name: 'onERC721Received', 45 | outputs: [ 46 | { 47 | internalType: 'bytes4', 48 | name: '', 49 | type: 'bytes4', 50 | }, 51 | ], 52 | stateMutability: 'nonpayable', 53 | type: 'function', 54 | }, 55 | { 56 | inputs: [], 57 | name: 'traitValidator', 58 | outputs: [ 59 | { 60 | internalType: 'contract TraitValidator', 61 | name: '', 62 | type: 'address', 63 | }, 64 | ], 65 | stateMutability: 'view', 66 | type: 'function', 67 | }, 68 | { 69 | inputs: [], 70 | name: 'zeroExV4', 71 | outputs: [ 72 | { 73 | internalType: 'address', 74 | name: '', 75 | type: 'address', 76 | }, 77 | ], 78 | stateMutability: 'view', 79 | type: 'function', 80 | }, 81 | { 82 | stateMutability: 'payable', 83 | type: 'receive', 84 | }, 85 | ] 86 | 87 | const _bytecode = 88 | '0x60a060405273def1c0ded9bec7f1a1670819833240f027b25eff60805234801561002857600080fd5b50604051610e77380380610e778339810160408190526100479161006c565b600080546001600160a01b0319166001600160a01b039290921691909117905561009c565b60006020828403121561007e57600080fd5b81516001600160a01b038116811461009557600080fd5b9392505050565b608051610dba6100bd60003960008181609401526101dd0152610dba6000f3fe6080604052600436106100385760003560e01c8063150b7a021461004457806319d5643f14610082578063d6839196146100ce57600080fd5b3661003f57005b600080fd5b34801561005057600080fd5b5061006461005f3660046103da565b6100ee565b6040516001600160e01b031990911681526020015b60405180910390f35b34801561008e57600080fd5b506100b67f000000000000000000000000000000000000000000000000000000000000000081565b6040516001600160a01b039091168152602001610079565b3480156100da57600080fd5b506000546100b6906001600160a01b031681565b6000808080808061010187890189610931565b945094509450945094508261010001516001600160a01b0316336001600160a01b0316146101675760405162461bcd60e51b815260206004820152600e60248201526d24b73b30b634b21039b2b73232b960911b60448201526064015b60405180910390fd5b60005460405163efff44f360e01b81526001600160a01b039091169063efff44f3906101999088908890600401610a75565b600060405180830381600087803b1580156101b357600080fd5b505af11580156101c7573d6000803e3d6000fd5b50505050336001600160a01b031663b88d4fde307f00000000000000000000000000000000000000000000000000000000000000008c87878760405160200161021293929190610c19565b6040516020818303038152906040526040518563ffffffff1660e01b81526004016102409493929190610d23565b600060405180830381600087803b15801561025a57600080fd5b505af115801561026e573d6000803e3d6000fd5b50505050801561031b5760c08301516040516000916001600160a01b038d16918381818185875af1925050503d80600081146102c6576040519150601f19603f3d011682016040523d82523d6000602084013e6102cb565b606091505b50509050806103155760405162461bcd60e51b815260206004820152601660248201527510dbdd5b19081b9bdd081cd95b99081c185e5b595b9d60521b604482015260640161015e565b5061039b565b60a083015160c084015160405163a9059cbb60e01b81526001600160a01b038d81166004830152602482019290925291169063a9059cbb906044016020604051808303816000875af1158015610375573d6000803e3d6000fd5b505050506040513d601f19601f820116820180604052508101906103999190610d60565b505b50630a85bd0160e11b9a9950505050505050505050565b6001600160a01b03811681146103c757600080fd5b50565b80356103d5816103b2565b919050565b6000806000806000608086880312156103f257600080fd5b85356103fd816103b2565b9450602086013561040d816103b2565b93506040860135925060608601356001600160401b038082111561043057600080fd5b818801915088601f83011261044457600080fd5b81358181111561045357600080fd5b89602082850101111561046557600080fd5b9699959850939650602001949392505050565b634e487b7160e01b600052604160045260246000fd5b604051606081016001600160401b03811182821017156104b0576104b0610478565b60405290565b604080519081016001600160401b03811182821017156104b0576104b0610478565b60405161016081016001600160401b03811182821017156104b0576104b0610478565b60405160c081016001600160401b03811182821017156104b0576104b0610478565b604051601f8201601f191681016001600160401b038111828210171561054557610545610478565b604052919050565b803560ff811681146103d557600080fd5b600082601f83011261056f57600080fd5b81356001600160401b0381111561058857610588610478565b61059b601f8201601f191660200161051d565b8181528460208386010111156105b057600080fd5b816020850160208301376000918101602001919091529392505050565b8035600281106103d557600080fd5b60006001600160401b038211156105f5576105f5610478565b5060051b60200190565b600082601f83011261061057600080fd5b81356020610625610620836105dc565b61051d565b82815260059290921b8401810191818101908684111561064457600080fd5b8286015b848110156106d85780356001600160401b03808211156106685760008081fd5b908801906060828b03601f19018113156106825760008081fd5b61068a61048e565b87840135610697816103b2565b8152604084810135898301529184013591838311156106b65760008081fd5b6106c48d8a8588010161055e565b908201528652505050918301918301610648565b509695505050505050565b600082601f8301126106f457600080fd5b81356020610704610620836105dc565b82815260059290921b8401810191818101908684111561072357600080fd5b8286015b848110156106d85780356001600160401b03808211156107475760008081fd5b908801906040828b03601f19018113156107615760008081fd5b6107696104b6565b87840135610776816103b2565b815290830135908282111561078b5760008081fd5b6107998c898487010161055e565b818901528652505050918301918301610727565b600061016082840312156107c057600080fd5b6107c86104d8565b90506107d3826105cd565b81526107e1602083016103ca565b60208201526107f2604083016103ca565b6040820152606082013560608201526080820135608082015261081760a083016103ca565b60a082015260c082013560c082015260e08201356001600160401b038082111561084057600080fd5b61084c858386016105ff565b60e084015261010091506108618285016103ca565b82840152610120915081840135828401526101409150818401358181111561088857600080fd5b610894868287016106e3565b8385015250505092915050565b6000608082840312156108b357600080fd5b604051608081018181106001600160401b03821117156108d5576108d5610478565b6040529050808235600581106108ea57600080fd5b81526108f86020840161054d565b602082015260408301356040820152606083013560608201525092915050565b80151581146103c757600080fd5b80356103d581610918565b6000806000806000610100868803121561094a57600080fd5b8535945060208601356001600160401b038082111561096857600080fd5b9087019060c0828a03121561097c57600080fd5b6109846104fb565b61098d8361054d565b81526020830135602082015260408301356040820152606083013560608201526080830135608082015260a0830135828111156109c957600080fd5b6109d58b82860161055e565b60a083015250955060408801359150808211156109f157600080fd5b506109fe888289016107ad565b935050610a0e87606088016108a1565b9150610a1c60e08701610926565b90509295509295909350565b6000815180845260005b81811015610a4e57602081850181015186830182015201610a32565b81811115610a60576000602083870101525b50601f01601f19169290920160200192915050565b8281526040602082015260ff82511660408201526020820151606082015260408201516080820152606082015160a0820152608082015160c0820152600060a083015160c060e0840152610acd610100840182610a28565b95945050505050565b634e487b7160e01b600052602160045260246000fd5b60028110610afc57610afc610ad6565b9052565b600081518084526020808501808196508360051b8101915082860160005b85811015610b71578284038952815180516001600160a01b031685528581015186860152604090810151606091860182905290610b5d81870183610a28565b9a87019a9550505090840190600101610b1e565b5091979650505050505050565b600081518084526020808501808196508360051b8101915082860160005b85811015610b71578284038952815180516001600160a01b031685528501516040868601819052610bcf81870183610a28565b9a87019a9550505090840190600101610b9c565b805160058110610bf557610bf5610ad6565b825260208181015160ff169083015260408082015190830152606090810151910152565b60c08152610c2b60c082018551610aec565b60006020850151610c4760e08401826001600160a01b03169052565b506040850151610100610c64818501836001600160a01b03169052565b60608701519150610120828186015260808801519250610140838187015260a08901519350610160610ca0818801866001600160a01b03169052565b60c08a015161018088015260e08a01519450806101a088015250610cc8610220870185610b00565b928901516001600160a01b03166101c0870152908801516101e086015287015160bf1985830301610200860152909150610d028282610b7e565b92505050610d136020830185610be3565b82151560a0830152949350505050565b6001600160a01b0385811682528416602082015260408101839052608060608201819052600090610d5690830184610a28565b9695505050505050565b600060208284031215610d7257600080fd5b8151610d7d81610918565b939250505056fea264697066735822122059894ce2d5722ce8115c704f89accd83c8c8d17b675a6f26470eadade9176f1f64736f6c634300080d0033' 89 | 90 | type TraitRouterConstructorParams = [signer?: Signer] | ConstructorParameters 91 | 92 | const isSuperArgs = (xs: TraitRouterConstructorParams): xs is ConstructorParameters => 93 | xs.length > 1 94 | 95 | export class TraitRouter__factory extends ContractFactory { 96 | constructor(...args: TraitRouterConstructorParams) { 97 | if (isSuperArgs(args)) { 98 | super(...args) 99 | } else { 100 | super(_abi, _bytecode, args[0]) 101 | } 102 | } 103 | 104 | override deploy( 105 | traitValidatorAddress: PromiseOrValue, 106 | overrides?: Overrides & { from?: PromiseOrValue } 107 | ): Promise { 108 | return super.deploy(traitValidatorAddress, overrides || {}) as Promise 109 | } 110 | override getDeployTransaction( 111 | traitValidatorAddress: PromiseOrValue, 112 | overrides?: Overrides & { from?: PromiseOrValue } 113 | ): TransactionRequest { 114 | return super.getDeployTransaction(traitValidatorAddress, overrides || {}) 115 | } 116 | override attach(address: string): TraitRouter { 117 | return super.attach(address) as TraitRouter 118 | } 119 | override connect(signer: Signer): TraitRouter__factory { 120 | return super.connect(signer) as TraitRouter__factory 121 | } 122 | 123 | static readonly bytecode = _bytecode 124 | static readonly abi = _abi 125 | static createInterface(): TraitRouterInterface { 126 | return new utils.Interface(_abi) as TraitRouterInterface 127 | } 128 | static connect(address: string, signerOrProvider: Signer | Provider): TraitRouter { 129 | return new Contract(address, _abi, signerOrProvider) as TraitRouter 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /src/contracts/factories/TraitValidator__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | import { Signer, utils, Contract, ContractFactory, Overrides } from 'ethers' 5 | import type { Provider, TransactionRequest } from '@ethersproject/providers' 6 | import type { PromiseOrValue } from '../common' 7 | import type { TraitValidator, TraitValidatorInterface } from '../TraitValidator' 8 | 9 | const _abi = [ 10 | { 11 | inputs: [ 12 | { 13 | internalType: 'address', 14 | name: 'oracleAddress', 15 | type: 'address', 16 | }, 17 | ], 18 | stateMutability: 'nonpayable', 19 | type: 'constructor', 20 | }, 21 | { 22 | inputs: [], 23 | name: 'Trustus__InvalidPacket', 24 | type: 'error', 25 | }, 26 | { 27 | anonymous: false, 28 | inputs: [ 29 | { 30 | indexed: true, 31 | internalType: 'bytes32', 32 | name: 'request', 33 | type: 'bytes32', 34 | }, 35 | { 36 | indexed: false, 37 | internalType: 'uint256', 38 | name: 'deadline', 39 | type: 'uint256', 40 | }, 41 | ], 42 | name: 'PropertySet', 43 | type: 'event', 44 | }, 45 | { 46 | inputs: [], 47 | name: 'DOMAIN_SEPARATOR', 48 | outputs: [ 49 | { 50 | internalType: 'bytes32', 51 | name: '', 52 | type: 'bytes32', 53 | }, 54 | ], 55 | stateMutability: 'view', 56 | type: 'function', 57 | }, 58 | { 59 | inputs: [ 60 | { 61 | internalType: 'bytes32', 62 | name: '', 63 | type: 'bytes32', 64 | }, 65 | ], 66 | name: 'propertyDeadline', 67 | outputs: [ 68 | { 69 | internalType: 'uint256', 70 | name: '', 71 | type: 'uint256', 72 | }, 73 | ], 74 | stateMutability: 'view', 75 | type: 'function', 76 | }, 77 | { 78 | inputs: [ 79 | { 80 | internalType: 'bytes32', 81 | name: 'request', 82 | type: 'bytes32', 83 | }, 84 | { 85 | components: [ 86 | { 87 | internalType: 'uint8', 88 | name: 'v', 89 | type: 'uint8', 90 | }, 91 | { 92 | internalType: 'bytes32', 93 | name: 'r', 94 | type: 'bytes32', 95 | }, 96 | { 97 | internalType: 'bytes32', 98 | name: 's', 99 | type: 'bytes32', 100 | }, 101 | { 102 | internalType: 'bytes32', 103 | name: 'request', 104 | type: 'bytes32', 105 | }, 106 | { 107 | internalType: 'uint256', 108 | name: 'deadline', 109 | type: 'uint256', 110 | }, 111 | { 112 | internalType: 'bytes', 113 | name: 'payload', 114 | type: 'bytes', 115 | }, 116 | ], 117 | internalType: 'struct Trustus.TrustusPacket', 118 | name: 'packet', 119 | type: 'tuple', 120 | }, 121 | ], 122 | name: 'setProperty', 123 | outputs: [], 124 | stateMutability: 'nonpayable', 125 | type: 'function', 126 | }, 127 | { 128 | inputs: [ 129 | { 130 | internalType: 'address', 131 | name: 'tokenAddress', 132 | type: 'address', 133 | }, 134 | { 135 | internalType: 'uint256', 136 | name: 'tokenId', 137 | type: 'uint256', 138 | }, 139 | { 140 | internalType: 'bytes', 141 | name: 'propertyData', 142 | type: 'bytes', 143 | }, 144 | ], 145 | name: 'validateProperty', 146 | outputs: [], 147 | stateMutability: 'view', 148 | type: 'function', 149 | }, 150 | ] 151 | 152 | const _bytecode = 153 | '0x60c060405234801561001057600080fd5b506040516107b13803806107b183398101604081905261002f91610105565b466080526100db604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f4fddeac0301e587e124c270ef73baebbfedaa619fd4476a3c9ed806f79df5a6e918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b60a0526001600160a01b03166000908152602081905260409020805460ff19166001179055610135565b60006020828403121561011757600080fd5b81516001600160a01b038116811461012e57600080fd5b9392505050565b60805160a05161065761015a60003960006102240152600061014f01526106576000f3fe608060405234801561001057600080fd5b506004361061004c5760003560e01c80631395c0f3146100515780633644e515146100665780635ea777ac14610080578063efff44f3146100a0575b600080fd5b61006461005f36600461046a565b6100b3565b005b61006e61014b565b60405190815260200160405180910390f35b61006e61008e3660046104ff565b60016020526000908152604090205481565b6100646100ae366004610518565b610246565b4260016000868686866040516020016100cf9493929190610566565b60405160208183030381529060405280519060200120815260200190815260200160002054116101455760405162461bcd60e51b815260206004820152601e60248201527f45787069726564206f7220696e6578697374656e742070726f70657274790000604482015260640160405180910390fd5b50505050565b60007f000000000000000000000000000000000000000000000000000000000000000046146102215761021c604080517f8b73c3c69bb8fe3d512ecc4cf759cc79239f7b179b0ffacaa9a75d522b39400f60208201527f4fddeac0301e587e124c270ef73baebbfedaa619fd4476a3c9ed806f79df5a6e918101919091527fc89efdaa54c0f20c7adf612882df0950f5a951637e0307cdcb4c672f298b8bc660608201524660808201523060a082015260009060c00160405160208183030381529060405280519060200120905090565b905090565b507f000000000000000000000000000000000000000000000000000000000000000090565b61025082826102c4565b61026d57604051630e06dc0560e41b815260040160405180910390fd5b60008281526001602052604090819020608083013590819055905183917f02840d79a5b9f2afd4c7bec02bc927692c8e36368e763bc1c434feff7c07887a916102b891815260200190565b60405180910390a25050565b600081608001354211156102da57506000610464565b816060013583146102ed57506000610464565b600060016102f961014b565b7ffc7ecbf4f091085173dad8d1d3c2dfd218c018596a572201cd849763d1114e7a6060860135608087013561033160a0890189610599565b60405161033f9291906105e7565b60405190819003812061036d9493929160200193845260208401929092526040830152606082015260800190565b604051602081830303815290604052805190602001206040516020016103aa92919061190160f01b81526002810192909252602282015260420190565b60408051601f198184030181529190528051602091820120906103cf908601866105f7565b604080516000815260208181018084529490945260ff9092168282015291860135606082015290850135608082015260a0016020604051602081039080840390855afa158015610423573d6000803e3d6000fd5b5050604051601f1901519150506001600160a01b0381161580159061046057506001600160a01b03811660009081526020819052604090205460ff165b9150505b92915050565b6000806000806060858703121561048057600080fd5b84356001600160a01b038116811461049757600080fd5b935060208501359250604085013567ffffffffffffffff808211156104bb57600080fd5b818701915087601f8301126104cf57600080fd5b8135818111156104de57600080fd5b8860208285010111156104f057600080fd5b95989497505060200194505050565b60006020828403121561051157600080fd5b5035919050565b6000806040838503121561052b57600080fd5b82359150602083013567ffffffffffffffff81111561054957600080fd5b830160c0818603121561055b57600080fd5b809150509250929050565b6bffffffffffffffffffffffff198560601b16815283601482015281836034830137600091016034019081529392505050565b6000808335601e198436030181126105b057600080fd5b83018035915067ffffffffffffffff8211156105cb57600080fd5b6020019150368190038213156105e057600080fd5b9250929050565b8183823760009101908152919050565b60006020828403121561060957600080fd5b813560ff8116811461061a57600080fd5b939250505056fea26469706673582212207dfcd57f77154d265cb0a90f0bbd3e14d34b892fd4102bdfdb937312c8534ad964736f6c634300080d0033' 154 | 155 | type TraitValidatorConstructorParams = [signer?: Signer] | ConstructorParameters 156 | 157 | const isSuperArgs = (xs: TraitValidatorConstructorParams): xs is ConstructorParameters => 158 | xs.length > 1 159 | 160 | export class TraitValidator__factory extends ContractFactory { 161 | constructor(...args: TraitValidatorConstructorParams) { 162 | if (isSuperArgs(args)) { 163 | super(...args) 164 | } else { 165 | super(_abi, _bytecode, args[0]) 166 | } 167 | } 168 | 169 | override deploy( 170 | oracleAddress: PromiseOrValue, 171 | overrides?: Overrides & { from?: PromiseOrValue } 172 | ): Promise { 173 | return super.deploy(oracleAddress, overrides || {}) as Promise 174 | } 175 | override getDeployTransaction( 176 | oracleAddress: PromiseOrValue, 177 | overrides?: Overrides & { from?: PromiseOrValue } 178 | ): TransactionRequest { 179 | return super.getDeployTransaction(oracleAddress, overrides || {}) 180 | } 181 | override attach(address: string): TraitValidator { 182 | return super.attach(address) as TraitValidator 183 | } 184 | override connect(signer: Signer): TraitValidator__factory { 185 | return super.connect(signer) as TraitValidator__factory 186 | } 187 | 188 | static readonly bytecode = _bytecode 189 | static readonly abi = _abi 190 | static createInterface(): TraitValidatorInterface { 191 | return new utils.Interface(_abi) as TraitValidatorInterface 192 | } 193 | static connect(address: string, signerOrProvider: Signer | Provider): TraitValidator { 194 | return new Contract(address, _abi, signerOrProvider) as TraitValidator 195 | } 196 | } 197 | -------------------------------------------------------------------------------- /src/contracts/factories/Trustus__factory.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | 5 | import { Contract, Signer, utils } from 'ethers' 6 | import type { Provider } from '@ethersproject/providers' 7 | import type { Trustus, TrustusInterface } from '../Trustus' 8 | 9 | const _abi = [ 10 | { 11 | inputs: [], 12 | name: 'Trustus__InvalidPacket', 13 | type: 'error', 14 | }, 15 | { 16 | inputs: [], 17 | name: 'DOMAIN_SEPARATOR', 18 | outputs: [ 19 | { 20 | internalType: 'bytes32', 21 | name: '', 22 | type: 'bytes32', 23 | }, 24 | ], 25 | stateMutability: 'view', 26 | type: 'function', 27 | }, 28 | ] 29 | 30 | export class Trustus__factory { 31 | static readonly abi = _abi 32 | static createInterface(): TrustusInterface { 33 | return new utils.Interface(_abi) as TrustusInterface 34 | } 35 | static connect(address: string, signerOrProvider: Signer | Provider): Trustus { 36 | return new Contract(address, _abi, signerOrProvider) as Trustus 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/contracts/factories/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export { ERC20__factory } from './ERC20__factory' 5 | export { ERC721__factory } from './ERC721__factory' 6 | export { IZeroEx__factory } from './IZeroEx__factory' 7 | export { TraitRouter__factory } from './TraitRouter__factory' 8 | export { TraitValidator__factory } from './TraitValidator__factory' 9 | export { Trustus__factory } from './Trustus__factory' 10 | -------------------------------------------------------------------------------- /src/contracts/index.ts: -------------------------------------------------------------------------------- 1 | /* Autogenerated file. Do not edit manually. */ 2 | /* tslint:disable */ 3 | /* eslint-disable */ 4 | export type { ERC20 } from './ERC20' 5 | export type { ERC721 } from './ERC721' 6 | export type { IZeroEx } from './IZeroEx' 7 | export type { TraitRouter } from './TraitRouter' 8 | export type { TraitValidator } from './TraitValidator' 9 | export type { Trustus } from './Trustus' 10 | export * as factories from './factories' 11 | export { ERC20__factory } from './factories/ERC20__factory' 12 | export { ERC721__factory } from './factories/ERC721__factory' 13 | export { IZeroEx__factory } from './factories/IZeroEx__factory' 14 | export { TraitRouter__factory } from './factories/TraitRouter__factory' 15 | export { TraitValidator__factory } from './factories/TraitValidator__factory' 16 | export { Trustus__factory } from './factories/Trustus__factory' 17 | -------------------------------------------------------------------------------- /src/default-config.ts: -------------------------------------------------------------------------------- 1 | import { addresses } from './addresses' 2 | 3 | const DEFAULT_SENTRY_DSN = process.env.SENTRY_DSN 4 | const DEFAULT_SENTRY_SAMPLE_RATE = 1.0 5 | 6 | export const CHAIN_IDS = { 7 | MAINNET: '1', 8 | ROPSTEN: '3', 9 | RINKEBY: '4', 10 | POLYGON: '137', 11 | POLYGON_MUMBAI: '80001', 12 | ARBITRUM: '42161', 13 | OPTIMISM: '10', 14 | GOERLI: '5', 15 | } 16 | 17 | export const CHAIN_IDS_NAMES = { 18 | [CHAIN_IDS.MAINNET]: 'Mainnet', 19 | [CHAIN_IDS.ROPSTEN]: 'Ropsten', 20 | [CHAIN_IDS.POLYGON]: 'Polygon', 21 | [CHAIN_IDS.POLYGON_MUMBAI]: 'Polygon Mumbai Testnet', 22 | [CHAIN_IDS.ARBITRUM]: 'Arbitrum', 23 | [CHAIN_IDS.OPTIMISM]: 'Optimism', 24 | [CHAIN_IDS.GOERLI]: 'Goerli', 25 | } 26 | 27 | const WS_RPC = { 28 | // mainnet 29 | [CHAIN_IDS.MAINNET]: process.env.RPC_MAINNET, 30 | // ropsten 31 | [CHAIN_IDS.ROPSTEN]: process.env.RPC_ROPSTEN, 32 | // polygon 33 | [CHAIN_IDS.POLYGON]: process.env.RPC_POLYGON, 34 | // polygon mumbai 35 | [CHAIN_IDS.POLYGON_MUMBAI]: process.env.RPC_POLYGON_MUMBAI, 36 | // arbitrum 37 | [CHAIN_IDS.ARBITRUM]: process.env.RPC_MAINNET_ARBITRUM, 38 | // optimism 39 | [CHAIN_IDS.OPTIMISM]: process.env.RPC_OPTIMISM, 40 | // goerli 41 | [CHAIN_IDS.GOERLI]: process.env.RPC_GOERLI, 42 | } 43 | 44 | const JSON_RPC = { 45 | // mainnet 46 | [CHAIN_IDS.MAINNET]: process.env.RPC_MAINNET, 47 | // ropsten 48 | [CHAIN_IDS.ROPSTEN]: process.env.RPC_ROPSTEN, 49 | // polygon 50 | [CHAIN_IDS.POLYGON]: process.env.RPC_POLYGON, 51 | // polygon mumbai 52 | [CHAIN_IDS.POLYGON_MUMBAI]: process.env.RPC_POLYGON_MUMBAI, 53 | // arbitrum 54 | [CHAIN_IDS.ARBITRUM]: process.env.RPC_MAINNET_ARBITRUM, 55 | // optimism 56 | [CHAIN_IDS.OPTIMISM]: process.env.RPC_OPTIMISM, 57 | // goerli 58 | [CHAIN_IDS.GOERLI]: process.env.RPC_GOERLI, 59 | } 60 | 61 | const getZeroExContract = (chainId: string): string => { 62 | const addressesForChain = addresses[chainId] 63 | if (!addressesForChain) { 64 | throw new Error(`Unknown addresses for chain ${chainId} (chain not supported)`) 65 | } 66 | const zeroExContractAddress = addressesForChain.exchange 67 | return zeroExContractAddress 68 | } 69 | 70 | const getWsRpcUrlByChainId = (chainId: string) => { 71 | const wsRpc = WS_RPC[chainId.trim().toString()] 72 | if (!wsRpc) { 73 | throw new Error(`Unknown WS RPC URL for chain ${chainId} (chain not supported)`) 74 | } 75 | return wsRpc 76 | } 77 | 78 | const getJsonRpcUrlByChainId = (chainId: string) => { 79 | const jsonRpc = JSON_RPC[chainId.trim().toString()] 80 | if (!jsonRpc) { 81 | throw new Error(`Unknown Json RPC URL for chain ${chainId} (chain not supported)`) 82 | } 83 | return jsonRpc 84 | } 85 | 86 | const GCP_PROJECT_ID = process.env.GCP_PROJECT_ID ?? 'traderxyz' 87 | 88 | const DEFAULT_OPENSEA_API_KEY = process.env.OPENSEA_API_KEY 89 | const DEFAULT_COVALENT_API_KEY = process.env.COVALENT_API_KEY 90 | 91 | export { 92 | DEFAULT_SENTRY_DSN, 93 | DEFAULT_SENTRY_SAMPLE_RATE, 94 | GCP_PROJECT_ID, 95 | DEFAULT_OPENSEA_API_KEY, 96 | DEFAULT_COVALENT_API_KEY, 97 | getWsRpcUrlByChainId, 98 | getZeroExContract, 99 | getJsonRpcUrlByChainId, 100 | WS_RPC, 101 | JSON_RPC, 102 | } 103 | -------------------------------------------------------------------------------- /src/errors/api-error.ts: -------------------------------------------------------------------------------- 1 | export interface IAPIError { 2 | errorCode: string 3 | errorMessage: string 4 | } 5 | 6 | const createApiError = (code: unknown, message: string): IAPIError => ({ 7 | errorCode: code as string, 8 | errorMessage: message, 9 | }) 10 | 11 | export { createApiError } 12 | -------------------------------------------------------------------------------- /src/logger.ts: -------------------------------------------------------------------------------- 1 | import winston from 'winston' 2 | 3 | import { LoggingWinston } from '@google-cloud/logging-winston' 4 | 5 | const isProduction = () => process.env.NODE_ENV === 'production' 6 | 7 | const LOG_LEVEL = process.env.LOG_LEVEL || 'debug' 8 | 9 | const gcpLoggingWinston = new LoggingWinston() 10 | 11 | const transports: winston.transport[] = [ 12 | // stdout default 13 | new winston.transports.Console(), 14 | ] 15 | 16 | // Add cloud logging if in production 17 | if (isProduction()) { 18 | console.log('Enabling cloud logging (reason: in production)') 19 | transports.push(gcpLoggingWinston) 20 | } 21 | 22 | const logger = winston.createLogger({ 23 | level: LOG_LEVEL, 24 | transports, 25 | }) 26 | 27 | export enum ServiceNamesLogLabel { 28 | 'api-web' = 'api-web', 29 | 'producer:block-number' = 'producer:block-number', 30 | 'producer:fix-block-gaps-by-job' = 'producer:fix-block-gaps-by-job', 31 | 'producer:nft-metadata-request' = 'producer:nft-metadata-request', 32 | 'consumer:exchange-events-by-block-number' = 'consumer:exchange-events-by-block-number', 33 | 'consumer:save-block-to-db' = 'consumer:save-block-to-db', 34 | 'consumer:nft-metadata-request' = 'consumer:nft-metadata-request', 35 | 'consumer:nft-opensea-collection-by-address-sync' = 'consumer:nft-opensea-collection-by-address-sync', 36 | 'cron:sync-opensea-recent-collections' = 'cron:sync-opensea-recent-collections', 37 | } 38 | 39 | const getLoggerForService = (serviceName: ServiceNamesLogLabel) => { 40 | const childLogger = logger.child({ labels: { serviceName }, name: serviceName }) 41 | return childLogger 42 | } 43 | 44 | export { logger, getLoggerForService } 45 | -------------------------------------------------------------------------------- /src/prisma-client.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | 3 | let prismaSingleton: PrismaClient | null 4 | 5 | const getPrismaClient = (): PrismaClient => { 6 | if (!prismaSingleton) { 7 | prismaSingleton = new PrismaClient({ 8 | // log: ['query', 'info', 'warn', 'error'], 9 | }) 10 | } 11 | return prismaSingleton 12 | } 13 | 14 | export { getPrismaClient } 15 | -------------------------------------------------------------------------------- /src/redis-client.ts: -------------------------------------------------------------------------------- 1 | import Redis from 'ioredis' 2 | 3 | const redisUrl = process.env.REDIS_URL 4 | 5 | if (!redisUrl) { 6 | throw new Error('redisUrl not set in environment variable') 7 | } 8 | 9 | const createClient = () => new Redis(redisUrl) 10 | 11 | const redisClientSingleton = createClient() 12 | 13 | const getRedisClient = () => redisClientSingleton 14 | 15 | export { getRedisClient } 16 | -------------------------------------------------------------------------------- /src/repositories/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/trader-xyz/trader-orderbook/09ed7d223e4e148458b34a8230dd7c2227876b55/src/repositories/.gitkeep -------------------------------------------------------------------------------- /src/scripts/create-order.ts: -------------------------------------------------------------------------------- 1 | import { $fetch } from 'ohmyfetch' 2 | import { NftSwapV4, SwappableAsset } from '@traderxyz/nft-swap-sdk' 3 | import { ethers } from 'ethers' 4 | 5 | const MAKER_WALLET_ADDRESS = '0xabc23F70Df4F45dD3Df4EC6DA6827CB05853eC9b' 6 | const MAKER_PRIVATE_KEY = 'fc5db508b0a52da8fbcac3ab698088715595f8de9cccf2467d51952eec564ec9' 7 | // NOTE(johnrjj) - NEVER use these private keys for anything of value, testnets only! 8 | 9 | const WETH_TOKEN_ADDRESS_TESTNET = '0xc778417e063141139fce010982780140aa0cd5ab' 10 | const DAI_TOKEN_ADDRESS_TESTNET = '0x31f42841c2db5173425b5223809cf3a38fede360' 11 | const TEST_NFT_CONTRACT_ADDRESS = '0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b' // https://ropsten.etherscan.io/token/0xf5de760f2e916647fd766b4ad9e85ff943ce3a2b?a=0xabc23F70Df4F45dD3Df4EC6DA6827CB05853eC9b 12 | 13 | const RPC_TESTNET = 'https://eth-ropsten.alchemyapi.io/v2/is1WqyAFM1nNFFx2aCozhTep7IxHVNGo' 14 | 15 | const ROPSTEN_CHAIN_ID = 3 16 | 17 | const MAKER_WALLET = new ethers.Wallet(MAKER_PRIVATE_KEY) 18 | // const TAKER_WALLET = new ethers.Wallet(TAKER_PRIVATE_KEY); 19 | 20 | const PROVIDER = new ethers.providers.StaticJsonRpcProvider(RPC_TESTNET) 21 | 22 | const MAKER_SIGNER = MAKER_WALLET.connect(PROVIDER) 23 | 24 | const nftSwap = new NftSwapV4(MAKER_SIGNER as any, MAKER_SIGNER, ROPSTEN_CHAIN_ID) 25 | 26 | const NFT_ASSET: SwappableAsset = { 27 | type: 'ERC721', 28 | tokenAddress: TEST_NFT_CONTRACT_ADDRESS, 29 | tokenId: '11045', 30 | } 31 | 32 | const ERC20_ASSET: SwappableAsset = { 33 | type: 'ERC20', 34 | tokenAddress: DAI_TOKEN_ADDRESS_TESTNET, 35 | amount: '420000000000000', // 1 USDC 36 | } 37 | 38 | const doThing = async () => { 39 | // const offers = await nftSwap.getOrders({ 40 | // nftToken: '0x64CeE6c2beFeB3077F97b74c7027B7247d42e563', 41 | // nftTokenId: '1', 42 | // // sellOrBuyNft: "buy", 43 | // }) 44 | 45 | // console.log('offers', offers) 46 | 47 | try { 48 | const order = nftSwap.buildOrder(NFT_ASSET, ERC20_ASSET, MAKER_WALLET_ADDRESS) 49 | 50 | const invalidOrder = nftSwap.buildOrder( 51 | { ...NFT_ASSET, tokenAddress: '0x5Af0D9827E0c53E4799BB226655A1de152A425a5' }, 52 | ERC20_ASSET, 53 | MAKER_WALLET_ADDRESS 54 | ) 55 | 56 | const signedOrder = await nftSwap.signOrder(order) 57 | 58 | // delete (signedOrder as any).expiry 59 | 60 | const localEndpoint = 'http://localhost:5000/orderbook/order' 61 | const prodEndpoint = 'https://api.trader.xyz/orderbook/order' 62 | // const endpoint = '' 63 | const postOrderResult = await $fetch(localEndpoint, { 64 | method: 'POST', 65 | body: { order: signedOrder, chainId: ROPSTEN_CHAIN_ID.toString(), metadata: { keykeyyee: '12341234' } }, 66 | }) 67 | console.log('posted order', postOrderResult) 68 | } catch (e: any) { 69 | console.log('error creating order') 70 | console.log(e) 71 | console.log(e.data) 72 | throw e 73 | } 74 | } 75 | 76 | doThing().catch((e) => console.log('error with script', e)) 77 | -------------------------------------------------------------------------------- /src/scripts/nft-lookup.ts: -------------------------------------------------------------------------------- 1 | // const foo = createAlchemyWeb3('', { maxRetries: 10 }) 2 | 3 | import { CHAIN_IDS } from '../default-config' 4 | import { fetchCollection, fetchNftsForWallet, getNftMetadataOnAnyChain } from '../services/utils/nfts' 5 | 6 | const doAsync = async () => { 7 | // const nftsForUser = foo.alchemy.getNfts({ owner: '', contractAddresses: undefined /** or [array of contract addresses] */, pageKey: undefined}, ) 8 | 9 | // const nftMetadataForToken = foo.alchemy.getNftMetadata({ contractAddress: '', tokenId: '', tokenType: 'erc721' }) 10 | const items = await getNftMetadataOnAnyChain('0x5763F564E0B5D8233Da0aCcf2585f2dbeF0f0dfa', '394', CHAIN_IDS.OPTIMISM) 11 | // const items = await fetchNftsForWallet('0xC33881b8FD07d71098b440fA8A3797886D831061') 12 | // const items = await fetchCollection('0x61fce80d72363b731425c3a2a46a1a5fed9814b2') 13 | console.log('items', items) 14 | console.log('length', items) 15 | } 16 | 17 | doAsync() 18 | -------------------------------------------------------------------------------- /src/scripts/publish-os-scrape-collection-event.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | import { PubSub } from '@google-cloud/pubsub' 3 | import { GCP_PROJECT_ID } from '../default-config' 4 | import { NftOpenseaScrapeCollectionByAddressRequestEvent } from '../services/utils/messaging-types' 5 | import { PUBSUB_TOPICS } from '../services/utils/pubsub' 6 | 7 | const createEvent = (contractAddress: string, chainId: string) => { 8 | const scrapeCollectionRequestMessage: NftOpenseaScrapeCollectionByAddressRequestEvent = { 9 | data: { 10 | contractAddress: contractAddress.toLowerCase(), 11 | chainId, 12 | }, 13 | eventName: 'nft.opensea.scrape.collection-by-address', 14 | topic: PUBSUB_TOPICS.NftOpenSeaCollectionScrape, 15 | } 16 | 17 | return scrapeCollectionRequestMessage 18 | } 19 | 20 | const COLLECTIONS_TO_INDEX_MAINNET_ONLY: Array<[string, string]> = [['0xed5af388653567af2f388e6224dc7c4b3241c544', '1']] 21 | 22 | const doAsync = async () => { 23 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 24 | 25 | // Create publisher options 26 | const options = { 27 | flowControlOptions: { 28 | maxOutstandingMessages: 50, 29 | maxOutstandingBytes: 10 * 1024 * 1024, // 10 MB 30 | }, 31 | } 32 | 33 | const collectionScrapeTopic = pubsub.topic(PUBSUB_TOPICS.NftOpenSeaCollectionScrape, options) 34 | 35 | // For flow controlled publishing, we'll use a publisher flow controller 36 | // instead of `topic.publish()`. 37 | const flow = collectionScrapeTopic.flowControlled() 38 | 39 | // Publish messages in a fast loop. 40 | for (let i = 0; i < COLLECTIONS_TO_INDEX_MAINNET_ONLY.length; i++) { 41 | const [contractAddress, chainId] = COLLECTIONS_TO_INDEX_MAINNET_ONLY[i] 42 | // You can also just `await` on `publish()` unconditionally, but if 43 | // you want to avoid pausing to the event loop on each iteration, 44 | // you can manually check the return value before doing so. 45 | const wait = flow.publish({ 46 | data: Buffer.from(JSON.stringify(createEvent(contractAddress, chainId))), 47 | orderingKey: chainId, 48 | attributes: { 49 | chainId: chainId, 50 | }, 51 | }) 52 | if (wait) { 53 | await wait 54 | } 55 | } 56 | 57 | console.log('Published all messages') 58 | } 59 | 60 | doAsync() 61 | -------------------------------------------------------------------------------- /src/scripts/query.ts: -------------------------------------------------------------------------------- 1 | import SQL, { join } from 'pg-template-tag' 2 | import { CHAIN_IDS } from '../default-config' 3 | import { getPrismaClient } from '../prisma-client' 4 | 5 | interface QueryConfig { 6 | limit: number 7 | offset: number 8 | } 9 | 10 | interface GetOrdersWithOrderStatusQueryOptions { 11 | chainId: string 12 | } 13 | 14 | const query = (options: GetOrdersWithOrderStatusQueryOptions, queryConfig: Partial = {}) => { 15 | // var conditions = []; 16 | // if (filter.email) conditions.push(SQL`email like ${filter.email}`); 17 | // if (filter.minAge) conditions.push(SQL`age > ${filter.minAge}`); 18 | // if (filter.maxAge) conditions.push(SQL`age < ${filter.maxAge}`); 19 | // return connection.query(SQL`select * from users where ${SQL.join(conditions, ' and ')}`); 20 | 21 | const finalConfig: QueryConfig = { 22 | limit: 1000, 23 | offset: 0, 24 | ...queryConfig, 25 | } 26 | 27 | let conditions: any[] = [SQL`orders_with_latest_status.nonce is not null`] 28 | if (options.chainId) { 29 | conditions.push(SQL`chain_id = ${options.chainId}`) 30 | } 31 | const conditionsSql = join(conditions, ' and ') 32 | 33 | return SQL` 34 | select 35 | -- pls fix 36 | * 37 | from 38 | ( 39 | select 40 | -- Only one status update per nonce (there can be many status updates to a single order) 41 | -- We're only interested in the latest status update (as we assume that's it's final resting state, e.g. filled or cancelled 42 | DISTINCT ON (nonce) * 43 | from 44 | ( 45 | orders_v4_nfts as orders 46 | left outer join ( 47 | select 48 | block_number, 49 | transaction_hash, 50 | order_status, 51 | nonce as order_nonce_from_update, 52 | date_posted_to_db 53 | from 54 | order_status_v4_nfts 55 | order by 56 | nonce, 57 | block_number desc, 58 | date_posted_to_db desc 59 | ) as order_status on orders.nonce = order_status.order_nonce_from_update 60 | ) 61 | ) as orders_with_latest_status 62 | where 63 | ${conditionsSql} 64 | order by 65 | orders_with_latest_status.date_posted_to_db desc 66 | limit 67 | ${finalConfig.limit} 68 | offset 69 | ${finalConfig.offset} 70 | ` 71 | } 72 | 73 | const doAsync = async () => { 74 | const prisma = getPrismaClient() 75 | 76 | const foo = await prisma.orders_with_latest_status.findFirst({ 77 | where: { 78 | nonce: '0xc240339744dd481dbc5b50992442d553', 79 | chain_id: CHAIN_IDS.ROPSTEN, 80 | }, 81 | }) 82 | 83 | console.log('foo', foo) 84 | } 85 | 86 | doAsync() 87 | -------------------------------------------------------------------------------- /src/scripts/test-validate.ts: -------------------------------------------------------------------------------- 1 | import { AlchemyProvider, JsonRpcProvider, StaticJsonRpcProvider } from '@ethersproject/providers' 2 | import { NftSwapV4 } from '@traderxyz/nft-swap-sdk' 3 | import { getJsonRpcUrlByChainId } from '../default-config' 4 | 5 | const chainId = '1' 6 | const alchemyRpc = new StaticJsonRpcProvider(getJsonRpcUrlByChainId(chainId), parseInt(chainId)) 7 | 8 | const foo = new NftSwapV4(alchemyRpc as any, alchemyRpc as any, 1) 9 | 10 | const doAsync = async () => { 11 | const orders = await foo.getOrders({ 12 | nftToken: '0x5af0d9827e0c53e4799bb226655a1de152a425a5'.toLowerCase(), 13 | }) 14 | 15 | // console.log(orders); 16 | 17 | const order = orders.orders[0] 18 | 19 | // console.log('order', order); 20 | 21 | const isValidSig = await foo.validateSignature(order.order) 22 | 23 | const fillableStatus = await foo.checkOrderCanBeFilledMakerSide(order.order) 24 | 25 | const orderStatus = await foo.getOrderStatus(order.order) 26 | 27 | console.log('isValidSig', isValidSig) 28 | console.log('fillableStatus', fillableStatus) 29 | console.log('orderStatus', orderStatus) 30 | } 31 | 32 | doAsync() 33 | -------------------------------------------------------------------------------- /src/services/api-web/app.ts: -------------------------------------------------------------------------------- 1 | console.log('App bootstrapping...') 2 | import express from 'express' 3 | import helmet from 'helmet' 4 | import cors from 'cors' 5 | import compression from 'compression' 6 | import bodyParser from 'body-parser' 7 | import { express as expressLogging } from '@google-cloud/logging-winston' 8 | import rateLimit from 'express-rate-limit' 9 | import RedisStore from 'rate-limit-redis' 10 | import * as Sentry from '@sentry/node' 11 | import * as Tracing from '@sentry/tracing' 12 | 13 | import { createOrderbookRouter } from '../../api/orderbook' 14 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 15 | import { createNftMetadataRequestRouter } from '../../api/nft-metadata' 16 | // import { getRedisClient } from '../../redis-client' 17 | 18 | const logger = getLoggerForService(ServiceNamesLogLabel['api-web']) 19 | 20 | const bootstrapApp = async () => { 21 | const isProduction = process.env.NODE_ENV === 'production' 22 | logger.debug('Initializing API web service express app...', { isProduction }) 23 | 24 | // Express 25 | const app = express() 26 | 27 | // const redisClient = getRedisClient() 28 | 29 | if (isProduction) { 30 | Sentry.init({ 31 | dsn: process.SENTRY_DSN, 32 | integrations: [ 33 | // enable HTTP calls tracing 34 | new Sentry.Integrations.Http({ tracing: true }), 35 | // enable Express.js middleware tracing 36 | new Tracing.Integrations.Express({ app }), 37 | ], 38 | 39 | // Set tracesSampleRate to 1.0 to capture 100% 40 | // of transactions for performance monitoring. 41 | // We recommend adjusting this value in production 42 | tracesSampleRate: 1.0, 43 | }) 44 | 45 | // RequestHandler creates a separate execution context using domains, so that every 46 | // transaction/span/breadcrumb is attached to its own Hub instance 47 | app.use(Sentry.Handlers.requestHandler()) 48 | // TracingHandler creates a trace for every incoming request 49 | app.use(Sentry.Handlers.tracingHandler()) 50 | } 51 | 52 | // // Create and use the rate limiter 53 | // const readOnlyRateLimiter = rateLimit({ 54 | // // Rate limiter configuration 55 | // windowMs: 1 * 60 * 1000, // 1 minute 56 | // max: async (_request, _response) => { 57 | // return 600 58 | // }, 59 | // standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 60 | // legacyHeaders: false, // Disable the `X-RateLimit-*` headers 61 | // // Redis store configuration 62 | // store: new RedisStore({ 63 | // prefix: 'rl-orderbook-view', 64 | // sendCommand: (...args: string[]) => (redisClient as any).call(...args), 65 | // }), 66 | // }) 67 | 68 | // // Create and use the rate limiter 69 | // const readWriteRateLimiter = rateLimit({ 70 | // // Rate limiter configuration 71 | // windowMs: 1 * 60 * 1000, // 1 minute 72 | // // Limit each IP to 100 requests per `window` 73 | // max: async (_request, _response) => { 74 | // return 60 75 | // }, 76 | // standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers 77 | // legacyHeaders: false, // Disable the `X-RateLimit-*` headers 78 | // // Redis store configuration 79 | // store: new RedisStore({ 80 | // prefix: 'rl-orderbook-write', 81 | // sendCommand: (...args: string[]) => (redisClient as any).call(...args), 82 | // }), 83 | // }) 84 | 85 | const logMiddleware = await expressLogging.makeMiddleware(logger) 86 | 87 | app.use(logMiddleware) 88 | 89 | app.use(helmet()) 90 | app.enable('trust proxy') 91 | app.use(compression()) 92 | app.use(express.json()) 93 | app.use(cors()) 94 | app.use(bodyParser.urlencoded({ extended: true })) 95 | app.use(bodyParser.json()) 96 | 97 | // Set up routes and middlewares 98 | // Basic Healthchecks 99 | app.get('/', (_, res) => res.sendStatus(200)) 100 | app.get('/v2', (_, res) => res.sendStatus(200)) 101 | app.get('/healthcheck', (_, res) => res.sendStatus(200)) 102 | app.get('/status', (_, res) => res.sendStatus(200)) 103 | 104 | app.use('/orderbook', createOrderbookRouter()) 105 | app.use('/nft/metadata', createNftMetadataRequestRouter()) 106 | 107 | // The error handler must be before any other error middleware and after all controllers 108 | app.use(Sentry.Handlers.errorHandler()) 109 | 110 | // Error middlewares 111 | app.use((_req, _res, next) => { 112 | const err = new Error('Not Found') as any 113 | err.status = 404 114 | next(err) 115 | }) 116 | 117 | app.use((error, _req, res, _next) => { 118 | res.status(error.status || 500) 119 | res.json({ ...error }) 120 | }) 121 | 122 | // Config done! Ready to go. 123 | logger.log('debug', 'App configured.') 124 | 125 | return app 126 | } 127 | 128 | export { bootstrapApp } 129 | -------------------------------------------------------------------------------- /src/services/api-web/index.ts: -------------------------------------------------------------------------------- 1 | require('dotenv').config() 2 | 3 | import { bootstrapApp } from './app' 4 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 5 | 6 | const PORT = parseInt(process.env.PORT || '5000', 10) 7 | const logger = getLoggerForService(ServiceNamesLogLabel['api-web']) 8 | 9 | bootstrapApp().then((app) => { 10 | app.listen(PORT, () => { 11 | logger.info( 12 | `🚀 Trader.xyz Orderbook API service instance started. Listening on port ${PORT} ( http://localhost:${PORT}/healthcheck ) 🚀` 13 | ) 14 | }) 15 | }) 16 | -------------------------------------------------------------------------------- /src/services/api-web/utils/order-parsing.ts: -------------------------------------------------------------------------------- 1 | import { orders_with_latest_status } from '@prisma/client' 2 | import { 3 | SignedERC721OrderStructSerialized, 4 | SignedERC1155OrderStructSerialized, 5 | NftOrderV4Serialized, 6 | SignedNftOrderV4Serialized, 7 | } from '@traderxyz/nft-swap-sdk/dist/sdk/v4/types' 8 | import { randomUUID } from 'crypto' 9 | import fromUnixTime from 'date-fns/fromUnixTime' 10 | import { getZeroExContract } from '../../../default-config' 11 | import { PropertyStructSerialized } from '../../../types' 12 | import { NftOrderV4DatabaseModel } from '../../../types-complex' 13 | 14 | const modelDbOrderToSdkOrder = (orderFromDb: orders_with_latest_status): NftOrderV4Serialized => { 15 | switch (orderFromDb.nft_type.toUpperCase()) { 16 | case 'ERC721': 17 | const erc721Order: SignedERC721OrderStructSerialized = { 18 | direction: parseInt(orderFromDb.direction), 19 | erc20Token: orderFromDb.erc20_token, 20 | erc20TokenAmount: orderFromDb.erc20_token_amount, 21 | erc721Token: orderFromDb.nft_token, 22 | erc721TokenId: orderFromDb.nft_token_id, 23 | erc721TokenProperties: (orderFromDb.nft_token_properties as any[]) ?? [], 24 | expiry: orderFromDb.expiry, 25 | fees: (orderFromDb.fees as any[]) ?? [], 26 | maker: orderFromDb.maker, 27 | nonce: orderFromDb.nonce, 28 | signature: orderFromDb.signature as any, 29 | taker: orderFromDb.taker, 30 | } 31 | return erc721Order 32 | case 'ERC1155': 33 | const erc1155Order: SignedERC1155OrderStructSerialized = { 34 | direction: parseInt(orderFromDb.direction), 35 | erc20Token: orderFromDb.erc20_token, 36 | erc20TokenAmount: orderFromDb.erc20_token_amount, 37 | erc1155Token: orderFromDb.nft_token, 38 | erc1155TokenId: orderFromDb.nft_token_id, 39 | erc1155TokenAmount: orderFromDb.nft_token_amount, 40 | erc1155TokenProperties: (orderFromDb.nft_token_properties as any[]) ?? [], 41 | expiry: orderFromDb.expiry, 42 | fees: (orderFromDb.fees as any[]) ?? [], 43 | maker: orderFromDb.maker, 44 | nonce: orderFromDb.nonce, 45 | signature: orderFromDb.signature as any, 46 | taker: orderFromDb.taker, 47 | } 48 | return erc1155Order 49 | default: 50 | throw new Error(`Unknown nft type ${orderFromDb.nft_type}`) 51 | } 52 | } 53 | 54 | const nftOrderToDbModel = ( 55 | signedOrder: SignedNftOrderV4Serialized, 56 | chainId: string, 57 | orderMetadataFromApp: Record 58 | ): NftOrderV4DatabaseModel => { 59 | const expiryDateTime = fromUnixTime(parseInt(signedOrder.expiry)) 60 | 61 | let nftToken: string 62 | let nftTokenAmount: string 63 | let nftTokenId: string 64 | let nftType: string 65 | let nftTokenProperties: Array = [] 66 | 67 | if ('erc1155Token' in signedOrder) { 68 | nftToken = signedOrder.erc1155Token 69 | nftTokenAmount = signedOrder.erc1155TokenAmount 70 | nftTokenId = signedOrder.erc1155TokenId 71 | nftType = 'ERC1155' 72 | nftTokenProperties = signedOrder.erc1155TokenProperties ?? [] 73 | } else if ('erc721Token' in signedOrder) { 74 | nftToken = signedOrder.erc721Token 75 | nftTokenAmount = '1' 76 | nftTokenId = signedOrder.erc721TokenId 77 | nftType = 'ERC721' 78 | nftTokenProperties = signedOrder.erc721TokenProperties ?? [] 79 | } else { 80 | throw new Error('typesafe') 81 | } 82 | 83 | const zeroExContractAddress = getZeroExContract(chainId) 84 | 85 | const dbResult: NftOrderV4DatabaseModel = { 86 | id: randomUUID(), 87 | chain_id: chainId, 88 | direction: signedOrder.direction.toString(), 89 | erc20_token: signedOrder.erc20Token, 90 | erc20_token_amount: signedOrder.erc20TokenAmount, 91 | expiry: signedOrder.expiry, 92 | expiry_datetime: expiryDateTime, 93 | maker: signedOrder.maker, 94 | taker: signedOrder.taker, 95 | nft_token: nftToken, 96 | nft_token_amount: nftTokenAmount, 97 | nft_token_id: nftTokenId, 98 | nft_type: nftType, 99 | nft_token_properties: nftTokenProperties, 100 | fees: signedOrder.fees, 101 | nonce: signedOrder.nonce, 102 | signature: signedOrder.signature, 103 | verifying_contract: zeroExContractAddress, 104 | app_metadata: { 105 | ...orderMetadataFromApp, 106 | }, 107 | system_metadata: { 108 | foo: 'bazbot', 109 | }, 110 | } 111 | return dbResult 112 | } 113 | 114 | export { nftOrderToDbModel, modelDbOrderToSdkOrder } 115 | -------------------------------------------------------------------------------- /src/services/consumers/get-exchange-events-by-block-number.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import { PubSub } from '@google-cloud/pubsub' 3 | import { v4 as uuid } from 'uuid' 4 | import type { Prisma } from '@prisma/client' 5 | import { getPrismaClient } from '../../prisma-client' 6 | import { BlockNumberUpdateEvent } from '../utils/messaging-types' 7 | import { PubSubMessage, PUBSUB_SUBSCRIPTIONS } from '../utils/pubsub' 8 | import { JOBS } from '../utils/jobs' 9 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 10 | import { 11 | getZeroExContract, 12 | DEFAULT_SENTRY_DSN, 13 | DEFAULT_SENTRY_SAMPLE_RATE, 14 | GCP_PROJECT_ID, 15 | getJsonRpcUrlByChainId, 16 | } from '../../default-config' 17 | import { getOrderStatusLogsForBlocks } from './utils/exchange-events-parser' 18 | 19 | Sentry.init({ 20 | dsn: process.env.SENTRY_DSN ?? DEFAULT_SENTRY_DSN, 21 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 22 | }) 23 | 24 | const subscriptionId = PUBSUB_SUBSCRIPTIONS.ProcessExchangeOrderUpdatesByBlockNumber 25 | const jobName = JOBS.OrderUpdateByBlock 26 | 27 | const prisma = getPrismaClient() 28 | 29 | const logger = getLoggerForService(ServiceNamesLogLabel['consumer:exchange-events-by-block-number']) 30 | 31 | const fetchAndSaveOrderEvents = async ( 32 | blockNumber: number, 33 | chainId: string, 34 | verifyingContract: string, 35 | blockHash: string, 36 | parentHash: string 37 | ) => { 38 | const blockData = { blockNumber, blockHash, chainId, verifyingContract, parentHash } 39 | logger.debug(`ExchangeEvents: Processing block ${blockNumber} on chain ${chainId}`, blockData) 40 | 41 | const maybeExistingStatus = await prisma.job_records.findFirst({ 42 | select: { 43 | id: true, 44 | }, 45 | where: { 46 | block_number: blockNumber, 47 | chain_id: chainId, 48 | job_name: jobName, 49 | verifying_contract: verifyingContract, 50 | hash: blockHash, 51 | parent_hash: parentHash, 52 | }, 53 | }) 54 | 55 | if (maybeExistingStatus?.id) { 56 | logger.silly(`ExchangeEvents: Already processed this block ${blockNumber} on chain ${chainId}. Exiting.`, blockData) 57 | return 58 | } 59 | 60 | const jsonRpcUrl = getJsonRpcUrlByChainId(chainId) 61 | 62 | const orderUpdateEvents = await getOrderStatusLogsForBlocks(jsonRpcUrl, verifyingContract, chainId, blockNumber) 63 | 64 | logger.silly( 65 | `ExchangeEvents: Found ${orderUpdateEvents.length} update events for block ${blockNumber} on chain ${chainId} `, 66 | { orderUpdateEvents, ...blockData } 67 | ) 68 | 69 | try { 70 | const [jobRecordEntry, orderUpdatesCreated] = await prisma.$transaction([ 71 | prisma.job_records.create({ 72 | data: { 73 | chain_id: chainId.toString(), 74 | job_name: jobName, 75 | block_number: blockNumber, 76 | job_data: orderUpdateEvents as any, 77 | verifying_contract: verifyingContract, 78 | hash: blockHash, 79 | parent_hash: parentHash, 80 | id: uuid(), 81 | }, 82 | }), 83 | prisma.order_status_v4_nfts.createMany({ 84 | data: orderUpdateEvents.map((event) => { 85 | let orderStatus: string 86 | if (event.eventType === 'fill') { 87 | orderStatus = 'filled' 88 | } else if (event.eventType === 'cancel') { 89 | orderStatus = 'cancelled' 90 | } else { 91 | orderStatus = 'unknown' 92 | } 93 | 94 | const orderInput: Prisma.order_status_v4_nftsCreateManyInput = { 95 | order_status: orderStatus, 96 | address: event.address.toLowerCase(), 97 | block_hash: event.blockHash.toLowerCase(), 98 | block_number: event.blockNumber, 99 | data: event.data, 100 | name: event.name, 101 | parsed_args: event.parsedData as any, 102 | signature: event.signature, 103 | topic: event.topic, 104 | transaction_hash: event.transactionHash.toLowerCase(), 105 | nonce: event.parsedData.nonce.toLowerCase(), 106 | chain_id: chainId, 107 | verifying_contract: verifyingContract, 108 | } 109 | return orderInput 110 | }), 111 | skipDuplicates: true, 112 | }), 113 | ]) 114 | 115 | logger.debug( 116 | `ExchangeEvents: Inserted ${orderUpdatesCreated.count} exchange events for block ${blockNumber} on chain ${chainId}`, 117 | { orderUpdatesCreated, jobRecordEntry, orderUpdateEvents, ...blockData } 118 | ) 119 | return true 120 | } catch (e: any) { 121 | // "Unique constraint failed on the {constraint}" 122 | // https://www.prisma.io/docs/reference/api-reference/error-reference#p2002 123 | const PRISMA_CLIENT_QUERY_ENGINE_ERROR_DUPLICATE_KEY = 'P2002' 124 | 125 | if (e.code === PRISMA_CLIENT_QUERY_ENGINE_ERROR_DUPLICATE_KEY) { 126 | logger.log('silly', `ExchangeEvents: Already have seen this block (duplicate key)`, { 127 | ...blockData, 128 | code: e.code, 129 | meta: e.meta, 130 | message: e.message, 131 | }) 132 | return false 133 | } 134 | logger.log('error', 'Unknown error', { ...blockData, error: e }) 135 | throw e 136 | } 137 | } 138 | 139 | const startAsync = async () => { 140 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 141 | 142 | const newBlockSub = pubsub.subscription(subscriptionId, { 143 | flowControl: { 144 | maxMessages: 5, 145 | }, 146 | }) 147 | 148 | const handleMessage = async (message: PubSubMessage) => { 149 | const msgJson: BlockNumberUpdateEvent = JSON.parse(message.data) 150 | 151 | if (msgJson.eventName !== 'block-number.update') { 152 | logger.log('debug', `Unknown event, not sure how to handle`, msgJson) 153 | throw new Error(`Unknown message eventName: ${msgJson.eventName}`) 154 | } 155 | 156 | // TODO(johnrjj) - validate messages with zod 157 | 158 | const blockNumber = msgJson.data.blockNumber 159 | const chainId = msgJson.data.chainId 160 | const parentHash = msgJson.data.parentHash 161 | const hash = msgJson.data.hash 162 | 163 | try { 164 | await fetchAndSaveOrderEvents(blockNumber, chainId, getZeroExContract(chainId), hash, parentHash) 165 | await message.ack() 166 | logger.info(`ExchangeEvents: Acked ${blockNumber}`, { message }) 167 | } catch (e) { 168 | logger.error(`ExchangeEvents:error`, { e, blockNumber, chainId, hash, parentHash }) 169 | message.nack() 170 | // swallow error 171 | } 172 | } 173 | 174 | newBlockSub.on('message', handleMessage) 175 | } 176 | 177 | startAsync() 178 | -------------------------------------------------------------------------------- /src/services/consumers/get-nft-events-by-block-number.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import { PubSub } from '@google-cloud/pubsub' 3 | import { v4 as uuid } from 'uuid' 4 | import type { Prisma } from '@prisma/client' 5 | import { getPrismaClient } from '../../prisma-client' 6 | import { BlockNumberUpdateEvent } from '../utils/messaging-types' 7 | import { PubSubMessage, PUBSUB_SUBSCRIPTIONS } from '../utils/pubsub' 8 | import { JOBS } from '../utils/jobs' 9 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 10 | import { 11 | getZeroExContract, 12 | DEFAULT_SENTRY_DSN, 13 | DEFAULT_SENTRY_SAMPLE_RATE, 14 | GCP_PROJECT_ID, 15 | getJsonRpcUrlByChainId, 16 | } from '../../default-config' 17 | import { getOrderStatusLogsForBlocks } from './utils/exchange-events-parser' 18 | 19 | Sentry.init({ 20 | dsn: process.env.SENTRY_DSN ?? DEFAULT_SENTRY_DSN, 21 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 22 | }) 23 | 24 | const subscriptionId = PUBSUB_SUBSCRIPTIONS.ProcessExchangeOrderUpdatesByBlockNumber 25 | const jobName = JOBS.OrderUpdateByBlock 26 | 27 | const prisma = getPrismaClient() 28 | 29 | const logger = getLoggerForService(ServiceNamesLogLabel['consumer:exchange-events-by-block-number']) 30 | 31 | const fetchAndSaveOrderEvents = async ( 32 | blockNumber: number, 33 | chainId: string, 34 | verifyingContract: string, 35 | blockHash: string, 36 | parentHash: string 37 | ) => { 38 | const blockData = { blockNumber, blockHash, chainId, verifyingContract, parentHash } 39 | logger.debug(`ExchangeEvents: Processing block ${blockNumber} on chain ${chainId}`, blockData) 40 | 41 | const maybeExistingStatus = await prisma.job_records.findFirst({ 42 | select: { 43 | id: true, 44 | }, 45 | where: { 46 | block_number: blockNumber, 47 | chain_id: chainId, 48 | job_name: jobName, 49 | verifying_contract: verifyingContract, 50 | hash: blockHash, 51 | parent_hash: parentHash, 52 | }, 53 | }) 54 | 55 | if (maybeExistingStatus?.id) { 56 | logger.silly(`ExchangeEvents: Already processed this block ${blockNumber} on chain ${chainId}. Exiting.`, blockData) 57 | return 58 | } 59 | 60 | const jsonRpcUrl = getJsonRpcUrlByChainId(chainId) 61 | 62 | const orderUpdateEvents = await getOrderStatusLogsForBlocks(jsonRpcUrl, verifyingContract, chainId, blockNumber) 63 | 64 | logger.silly( 65 | `ExchangeEvents: Found ${orderUpdateEvents.length} update events for block ${blockNumber} on chain ${chainId} `, 66 | { orderUpdateEvents, ...blockData } 67 | ) 68 | 69 | try { 70 | const [jobRecordEntry, orderUpdatesCreated] = await prisma.$transaction([ 71 | prisma.job_records.create({ 72 | data: { 73 | chain_id: chainId.toString(), 74 | job_name: jobName, 75 | block_number: blockNumber, 76 | job_data: orderUpdateEvents as any, 77 | verifying_contract: verifyingContract, 78 | hash: blockHash, 79 | parent_hash: parentHash, 80 | id: uuid(), 81 | }, 82 | }), 83 | prisma.order_status_v4_nfts.createMany({ 84 | data: orderUpdateEvents.map((event) => { 85 | let orderStatus: string 86 | if (event.eventType === 'fill') { 87 | orderStatus = 'filled' 88 | } else if (event.eventType === 'cancel') { 89 | orderStatus = 'cancelled' 90 | } else { 91 | orderStatus = 'unknown' 92 | } 93 | 94 | const orderInput: Prisma.order_status_v4_nftsCreateManyInput = { 95 | order_status: orderStatus, 96 | address: event.address.toLowerCase(), 97 | block_hash: event.blockHash.toLowerCase(), 98 | block_number: event.blockNumber, 99 | data: event.data, 100 | name: event.name, 101 | parsed_args: event.parsedData as any, 102 | signature: event.signature, 103 | topic: event.topic, 104 | transaction_hash: event.transactionHash.toLowerCase(), 105 | nonce: event.parsedData.nonce.toLowerCase(), 106 | chain_id: chainId, 107 | verifying_contract: verifyingContract, 108 | } 109 | return orderInput 110 | }), 111 | skipDuplicates: true, 112 | }), 113 | ]) 114 | 115 | logger.debug( 116 | `ExchangeEvents: Inserted ${orderUpdatesCreated.count} exchange events for block ${blockNumber} on chain ${chainId}`, 117 | { orderUpdatesCreated, jobRecordEntry, orderUpdateEvents, ...blockData } 118 | ) 119 | return true 120 | } catch (e: any) { 121 | // "Unique constraint failed on the {constraint}" 122 | // https://www.prisma.io/docs/reference/api-reference/error-reference#p2002 123 | const PRISMA_CLIENT_QUERY_ENGINE_ERROR_DUPLICATE_KEY = 'P2002' 124 | 125 | if (e.code === PRISMA_CLIENT_QUERY_ENGINE_ERROR_DUPLICATE_KEY) { 126 | logger.log('silly', `ExchangeEvents: Already have seen this block (duplicate key)`, { 127 | ...blockData, 128 | code: e.code, 129 | meta: e.meta, 130 | message: e.message, 131 | }) 132 | return false 133 | } 134 | logger.log('error', 'Unknown error', { ...blockData, error: e }) 135 | throw e 136 | } 137 | } 138 | 139 | const startAsync = async () => { 140 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 141 | 142 | const newBlockSub = pubsub.subscription(subscriptionId, { 143 | flowControl: { 144 | maxMessages: 5, 145 | }, 146 | }) 147 | 148 | const handleMessage = async (message: PubSubMessage) => { 149 | const msgJson: BlockNumberUpdateEvent = JSON.parse(message.data) 150 | 151 | if (msgJson.eventName !== 'block-number.update') { 152 | logger.log('debug', `Unknown event, not sure how to handle`, msgJson) 153 | throw new Error(`Unknown message eventName: ${msgJson.eventName}`) 154 | } 155 | 156 | // TODO(johnrjj) - validate messages with zod 157 | 158 | const blockNumber = msgJson.data.blockNumber 159 | const chainId = msgJson.data.chainId 160 | const parentHash = msgJson.data.parentHash 161 | const hash = msgJson.data.hash 162 | 163 | try { 164 | await fetchAndSaveOrderEvents(blockNumber, chainId, getZeroExContract(chainId), hash, parentHash) 165 | await message.ack() 166 | logger.info(`ExchangeEvents: Acked ${blockNumber}`, { message }) 167 | } catch (e) { 168 | throw e 169 | } 170 | } 171 | 172 | newBlockSub.on('message', handleMessage) 173 | } 174 | 175 | startAsync() 176 | -------------------------------------------------------------------------------- /src/services/consumers/nft-metadata.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import { PubSub } from '@google-cloud/pubsub' 3 | import { v4 as uuid } from 'uuid' 4 | // import type { Prisma } from '@prisma/client' 5 | import { getPrismaClient } from '../../prisma-client' 6 | import { 7 | BlockNumberUpdateEvent, 8 | NftMetadataCollectionRequestEvent, 9 | NftMetadataRequestEvent, 10 | } from '../utils/messaging-types' 11 | import { PubSubMessage, PUBSUB_SUBSCRIPTIONS } from '../utils/pubsub' 12 | import { JOBS } from '../utils/jobs' 13 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 14 | import { 15 | getZeroExContract, 16 | DEFAULT_SENTRY_DSN, 17 | DEFAULT_SENTRY_SAMPLE_RATE, 18 | GCP_PROJECT_ID, 19 | getJsonRpcUrlByChainId, 20 | } from '../../default-config' 21 | import { getOrderStatusLogsForBlocks } from './utils/exchange-events-parser' 22 | import { fetchCollection, fetchNftMetadataFromAlchemy, getNftMetadataOnAnyChain } from '../utils/nfts' 23 | 24 | Sentry.init({ 25 | dsn: process.env.SENTRY_DSN, 26 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 27 | }) 28 | 29 | const subscriptionId = PUBSUB_SUBSCRIPTIONS.NftMetadataUpdateHandlerSub 30 | 31 | const prisma = getPrismaClient() 32 | 33 | const logger = getLoggerForService(ServiceNamesLogLabel['consumer:nft-metadata-request']) 34 | const startAsync = async () => { 35 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 36 | 37 | const nftMetadataUpdateRequest = pubsub.subscription(subscriptionId, { 38 | flowControl: { 39 | maxMessages: 5, 40 | }, 41 | }) 42 | 43 | const handleMessage = async (message: PubSubMessage) => { 44 | const msgJson: NftMetadataRequestEvent | NftMetadataCollectionRequestEvent = JSON.parse(message.data) 45 | 46 | if (msgJson.eventName === 'nft.metadata.update.individual') { 47 | const individualNft: NftMetadataRequestEvent = msgJson 48 | 49 | const chainId = individualNft.data.chainId 50 | const contractAddress = individualNft.data.contractAddress 51 | const tokenId = individualNft.data.tokenId 52 | logger.info( 53 | `NftMetadata: Received NFT Metadata update request for token id ${tokenId} on contract ${contractAddress}. Looking up data...`, 54 | { 55 | chainId, 56 | contractAddress, 57 | tokenId, 58 | } 59 | ) 60 | 61 | const nftData = await getNftMetadataOnAnyChain(contractAddress, tokenId, chainId) 62 | 63 | if (!nftData.tokenURI) { 64 | logger.error(`NftMetadata: Metadata lookup failed for ${contractAddress}, tokenId ${tokenId}`, { 65 | nftData, 66 | chainId, 67 | contractAddress, 68 | tokenId, 69 | }) 70 | throw new Error('Zora Metadata method failed') 71 | } 72 | 73 | const createdNftData = await prisma.nft_metadata.create({ 74 | data: { 75 | chain_id: chainId, 76 | token_address: nftData.tokenAddress, 77 | token_id: nftData.tokenId, 78 | token_uri: nftData.tokenURI, 79 | attributes: nftData.attributes, 80 | content_url: nftData.contentURL, 81 | content_url_mime_type: nftData.contentURLMimeType, 82 | description: nftData.description, 83 | external_link: nftData.externalLink, 84 | image_url: nftData.imageURL, 85 | metadata: nftData.metadata, 86 | name: nftData.name, 87 | token_url: nftData.tokenURL, 88 | token_url_mime_type: nftData.tokenURLMimeType, 89 | }, 90 | }) 91 | 92 | logger.info(`NftMetadata: Inserted NFT metadata for tokenId ${tokenId} on contract ${contractAddress}`, { 93 | nftData, 94 | createdNftData, 95 | chainId, 96 | contractAddress, 97 | tokenId, 98 | }) 99 | 100 | await message.ack() 101 | 102 | // const nftDataFromAlchemy = await fetchNftMetadataFromAlchemy(contractAddress, tokenId) 103 | return nftData 104 | } 105 | 106 | if (msgJson.eventName === 'nft.metadata.update.collection') { 107 | const collectionMsg: NftMetadataCollectionRequestEvent = msgJson 108 | 109 | const chainId = collectionMsg.data.chainId 110 | const contractAddress = collectionMsg.data.contractAddress 111 | const startToken = collectionMsg.data.startToken 112 | 113 | logger.error(`NftMetadata: Collection Sync not yet implemented`) 114 | throw new Error(`Collection Sync not yet implemented`) 115 | 116 | // const collectionData = await fetchCollection(contractAddress) 117 | // await message.ack() 118 | } 119 | 120 | logger.debug(`NftMetadata: Unknown event, not sure how to handle`, msgJson) 121 | throw new Error(`Unknown message eventName: ${(msgJson as any).eventName}`) 122 | 123 | // TODO(johnrjj) - validate messages with zod 124 | } 125 | 126 | nftMetadataUpdateRequest.on('message', handleMessage) 127 | 128 | logger.info(`NFTMetadata: Service started`) 129 | } 130 | 131 | startAsync() 132 | -------------------------------------------------------------------------------- /src/services/consumers/save-block-to-db.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import { PubSub } from '@google-cloud/pubsub' 3 | import { StaticJsonRpcProvider, WebSocketProvider } from '@ethersproject/providers' 4 | import fromUnixTime from 'date-fns/fromUnixTime' 5 | import { getPrismaClient } from '../../prisma-client' 6 | import type { BlockNumberUpdateEvent } from '../utils/messaging-types' 7 | import { PubSubMessage, PUBSUB_SUBSCRIPTIONS } from '../utils/pubsub' 8 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 9 | import { 10 | getJsonRpcUrlByChainId, 11 | DEFAULT_SENTRY_DSN, 12 | DEFAULT_SENTRY_SAMPLE_RATE, 13 | GCP_PROJECT_ID, 14 | } from '../../default-config' 15 | 16 | Sentry.init({ 17 | dsn: process.env.SENTRY_DSN, 18 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 19 | }) 20 | 21 | const subscriptionId = PUBSUB_SUBSCRIPTIONS.SaveNewBlockToTable 22 | 23 | const prisma = getPrismaClient() 24 | 25 | const logger = getLoggerForService(ServiceNamesLogLabel['consumer:save-block-to-db']) 26 | 27 | const fetchAndSaveBlock = async (blockNumber: number, chainId: string, hash: string, parentHash: string) => { 28 | const logInfo = { blockNumber, chainId, hash, parentHash } 29 | 30 | logger.log('debug', `BlockHandler: Pulled event to process block ${blockNumber} on chain ${chainId}`, { ...logInfo }) 31 | 32 | const blockRes = await prisma.blocks.findFirst({ 33 | select: { 34 | number: true, 35 | }, 36 | where: { 37 | number: blockNumber, 38 | chain_id: chainId, 39 | hash: hash, 40 | parent_hash: parentHash, 41 | }, 42 | }) 43 | 44 | if (blockRes?.number) { 45 | logger.log('silly', 'BlockHandler: Already processed this block, exiting as success', { ...logInfo }) 46 | return 47 | } 48 | 49 | const jsonRpcUrl = getJsonRpcUrlByChainId(chainId) 50 | if (!jsonRpcUrl) { 51 | logger.error(`BlockHandler: RPC unknown for chain ${chainId}`, { ...logInfo }) 52 | throw new Error(`RPC unknown for chain ${chainId}`) 53 | } 54 | const jsonProvider = new StaticJsonRpcProvider(jsonRpcUrl) 55 | 56 | const block = await jsonProvider.getBlock(blockNumber) 57 | 58 | try { 59 | const insertedBlock = await prisma.blocks.create({ 60 | data: { 61 | chain_id: chainId, 62 | date_mined: fromUnixTime(block.timestamp), 63 | hash: block.hash, 64 | parent_hash: block.parentHash, 65 | nonce: block.nonce, 66 | number: block.number, 67 | timestamp: block.timestamp, 68 | }, 69 | }) 70 | 71 | logger.log('debug', `BlockHandler: Processed block: ${insertedBlock.number} on chain ${insertedBlock.chain_id}`, { 72 | block, 73 | ...logInfo, 74 | }) 75 | 76 | return insertedBlock 77 | } catch (e: any) { 78 | // "Unique constraint failed on the {constraint}" 79 | // https://www.prisma.io/docs/reference/api-reference/error-reference#p2002 80 | const PRISMA_CLIENT_QUERY_ENGINE_ERROR_DUPLICATE_KEY = 'P2002' 81 | 82 | if (e.code === PRISMA_CLIENT_QUERY_ENGINE_ERROR_DUPLICATE_KEY) { 83 | logger.log('silly', `BlockHandler: Already have seen this block`, { 84 | code: e.code, 85 | meta: e.meta, 86 | message: e.message, 87 | ...logInfo, 88 | }) 89 | return null 90 | } 91 | logger.log('error', 'BlockHandler: Unknown error', { error: e, ...logInfo }) 92 | throw e 93 | } 94 | } 95 | 96 | const startAsync = async () => { 97 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 98 | 99 | const newBlockSub = pubsub.subscription(subscriptionId, { 100 | flowControl: { 101 | maxMessages: 10, 102 | }, 103 | }) 104 | 105 | const handleMessage = async (message: PubSubMessage) => { 106 | const msgJson: BlockNumberUpdateEvent = JSON.parse(message.data) 107 | 108 | if (msgJson.eventName !== 'block-number.update') { 109 | logger.log('debug', `Unknown event, not sure how to handle`, { msgJson, message }) 110 | throw new Error(`Unknown message eventName: ${msgJson.eventName}`) 111 | } 112 | 113 | // TODO(johnrjj) - validate messages with zod 114 | 115 | const blockNumber = msgJson.data.blockNumber 116 | const chainId = msgJson.data.chainId 117 | const hash = msgJson.data.hash 118 | const parentHash = msgJson.data.parentHash 119 | 120 | try { 121 | const insertedRows = await fetchAndSaveBlock(blockNumber, chainId, hash, parentHash) 122 | if (insertedRows === null) { 123 | logger.log('info', `BlockHandler: Skipped block ${blockNumber} on chain ${chainId}`, { message }) 124 | } 125 | await message.ack() 126 | logger.log('debug', `BlockHandler: Acked block ${blockNumber} on chain ${chainId}`, { message }) 127 | } catch (e) { 128 | logger.error(`BlockHandler:error`, { e, blockNumber, chainId, hash, parentHash }) 129 | message.nack() 130 | } 131 | } 132 | 133 | newBlockSub.on('message', handleMessage) 134 | } 135 | 136 | startAsync() 137 | -------------------------------------------------------------------------------- /src/services/consumers/sync-opensea-collection-metadata-by-address.ts: -------------------------------------------------------------------------------- 1 | import { PubSub } from '@google-cloud/pubsub' 2 | import * as Sentry from '@sentry/node' 3 | import { getPrismaClient } from '../../prisma-client' 4 | import type { NftOpenseaScrapeCollectionByAddressRequestEvent } from '../utils/messaging-types' 5 | import { PubSubMessage, PUBSUB_SUBSCRIPTIONS } from '../utils/pubsub' 6 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 7 | import { DEFAULT_SENTRY_DSN, DEFAULT_SENTRY_SAMPLE_RATE, GCP_PROJECT_ID } from '../../default-config' 8 | import { 9 | fetchOpenseaCollectionByContractAddress, 10 | OpenSeaV1CollectionByContractAddressResponsePayload, 11 | } from '../utils/opensea' 12 | 13 | Sentry.init({ 14 | dsn: process.env.SENTRY_DSN, 15 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 16 | }) 17 | 18 | const subscriptionId = PUBSUB_SUBSCRIPTIONS.NftOpenSeaCollectionScrape 19 | 20 | const prisma = getPrismaClient() 21 | 22 | const logger = getLoggerForService(ServiceNamesLogLabel['consumer:nft-opensea-collection-by-address-sync']) 23 | 24 | export const upsertOpenSeaCollectionScrapedData = async ( 25 | openseaCollectionByContractAddress: OpenSeaV1CollectionByContractAddressResponsePayload 26 | ) => { 27 | const upsertRes = await prisma.opensea_collection_metadata_by_contract_address_v1.upsert({ 28 | where: { 29 | address: openseaCollectionByContractAddress.address?.toLowerCase(), 30 | }, 31 | create: { 32 | ...openseaCollectionByContractAddress, 33 | address: openseaCollectionByContractAddress.address?.toLowerCase(), 34 | owner: openseaCollectionByContractAddress.owner?.toString(), 35 | dev_buyer_fee_basis_points: openseaCollectionByContractAddress.dev_buyer_fee_basis_points?.toString(), 36 | dev_seller_fee_basis_points: openseaCollectionByContractAddress.dev_seller_fee_basis_points?.toString(), 37 | opensea_buyer_fee_basis_points: openseaCollectionByContractAddress.opensea_buyer_fee_basis_points?.toString(), 38 | opensea_seller_fee_basis_points: openseaCollectionByContractAddress.opensea_seller_fee_basis_points?.toString(), 39 | buyer_fee_basis_points: openseaCollectionByContractAddress.buyer_fee_basis_points?.toString(), 40 | seller_fee_basis_points: openseaCollectionByContractAddress.seller_fee_basis_points?.toString(), 41 | collection: { 42 | ...openseaCollectionByContractAddress.collection, 43 | } as any, 44 | }, 45 | update: { 46 | ...openseaCollectionByContractAddress, 47 | address: openseaCollectionByContractAddress.address?.toLowerCase(), 48 | owner: openseaCollectionByContractAddress.owner?.toString(), 49 | dev_buyer_fee_basis_points: openseaCollectionByContractAddress.dev_buyer_fee_basis_points?.toString(), 50 | dev_seller_fee_basis_points: openseaCollectionByContractAddress.dev_seller_fee_basis_points?.toString(), 51 | opensea_buyer_fee_basis_points: openseaCollectionByContractAddress.opensea_buyer_fee_basis_points?.toString(), 52 | opensea_seller_fee_basis_points: openseaCollectionByContractAddress.opensea_seller_fee_basis_points?.toString(), 53 | buyer_fee_basis_points: openseaCollectionByContractAddress.buyer_fee_basis_points?.toString(), 54 | seller_fee_basis_points: openseaCollectionByContractAddress.seller_fee_basis_points?.toString(), 55 | collection: { 56 | ...openseaCollectionByContractAddress.collection, 57 | } as any, 58 | date_scrape_updated: new Date(), 59 | }, 60 | }) 61 | return upsertRes 62 | } 63 | 64 | const startAsync = async () => { 65 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 66 | 67 | const newBlockSub = pubsub.subscription(subscriptionId, { 68 | flowControl: { 69 | maxMessages: 3, 70 | }, 71 | }) 72 | 73 | const handleMessage = async (message: PubSubMessage) => { 74 | const msgJson: NftOpenseaScrapeCollectionByAddressRequestEvent = JSON.parse(message.data) 75 | 76 | if (msgJson.eventName !== 'nft.opensea.scrape.collection-by-address') { 77 | logger.log('debug', `Unknown event, not sure how to handle`, { msgJson, message }) 78 | throw new Error(`Unknown message eventName: ${msgJson.eventName}`) 79 | } 80 | 81 | console.log(message) 82 | 83 | // TODO(johnrjj) - validate messages with zod 84 | const contractAddress = msgJson.data.contractAddress.toLowerCase() 85 | const chainId = msgJson.data.chainId 86 | 87 | try { 88 | const openseaCollectionByContractAddress = await fetchOpenseaCollectionByContractAddress(contractAddress, chainId) 89 | 90 | if (!openseaCollectionByContractAddress) { 91 | logger.error(`OpenSeaCollectionByAddressSync: Was unable to index ${contractAddress}`, { 92 | contractAddress, 93 | openseaCollectionByContractAddress, 94 | }) 95 | return 96 | // Should we throw?? 97 | // throw new Error('DLQ:Unable to fetch'); 98 | } 99 | const upsertedRes = await upsertOpenSeaCollectionScrapedData(openseaCollectionByContractAddress) 100 | await message.ack() 101 | logger.log( 102 | 'debug', 103 | `OpenSeaCollectionByAddressSync: Upserted ${upsertedRes.address} collection (${upsertedRes.name})`, 104 | { message, upsertedRes } 105 | ) 106 | } catch (e) { 107 | logger.log('error', `OpenSeaCollectionByAddressSync: Something went wrong!`, { message, e }) 108 | message.nack() 109 | throw e 110 | } 111 | } 112 | 113 | newBlockSub.on('message', handleMessage) 114 | } 115 | 116 | startAsync() 117 | -------------------------------------------------------------------------------- /src/services/consumers/utils/exchange-events-parser.ts: -------------------------------------------------------------------------------- 1 | import { Log, StaticJsonRpcProvider } from '@ethersproject/providers' 2 | import type { LogDescription } from 'ethers/lib/utils' 3 | import { IZeroEx__factory } from '../../../contracts' 4 | import { 5 | ERC1155OrderCancelledEvent, 6 | ERC1155OrderFilledEvent, 7 | ERC721OrderCancelledEvent, 8 | ERC721OrderFilledEvent, 9 | } from '../../../contracts/IZeroEx' 10 | 11 | export const getOrderStatusLogsForBlocks = async ( 12 | rpcUrl: string, 13 | zeroExContractAddress: string, 14 | _chainId: string, 15 | fromBlock: number, 16 | toBlock: number = fromBlock 17 | ) => { 18 | const wsProvider = new StaticJsonRpcProvider(rpcUrl) 19 | 20 | const zeroExContract = IZeroEx__factory.connect(zeroExContractAddress, wsProvider) 21 | 22 | const erc721FilledEvent = 23 | zeroExContract.interface.events[ 24 | 'ERC721OrderFilled(uint8,address,address,uint256,address,uint256,address,uint256,address)' 25 | ] 26 | const erc1155FilledEvent = 27 | zeroExContract.interface.events[ 28 | 'ERC1155OrderFilled(uint8,address,address,uint256,address,uint256,address,uint256,uint128,address)' 29 | ] 30 | const erc721CancelledEvent = zeroExContract.interface.events['ERC721OrderCancelled(address,uint256)'] 31 | const erc1155CancelledEvent = zeroExContract.interface.events['ERC1155OrderCancelled(address,uint256)'] 32 | 33 | const zeroExNftOrderTopics = [ 34 | zeroExContract.interface.getEventTopic(erc721FilledEvent), 35 | zeroExContract.interface.getEventTopic(erc1155FilledEvent), 36 | zeroExContract.interface.getEventTopic(erc721CancelledEvent), 37 | zeroExContract.interface.getEventTopic(erc1155CancelledEvent), 38 | ] 39 | 40 | const pulledLogs = await wsProvider.getLogs({ 41 | fromBlock: fromBlock, 42 | toBlock: toBlock, 43 | address: zeroExContractAddress, 44 | // nested arr to OR (otherwise its an AND if you flatten -- https://docs.ethers.io/v5/concepts/events/) 45 | topics: [zeroExNftOrderTopics], 46 | }) 47 | 48 | const parsedLogsTuple: Array = pulledLogs.map((originalLog) => { 49 | const parsedLogDescription = zeroExContract.interface.parseLog(originalLog) 50 | return [parsedLogDescription, originalLog] as const 51 | }) 52 | 53 | const parsedEvents: Array = parsedLogsTuple.map(([parsedLog, originalLog]) => { 54 | const logName = parsedLog.name 55 | // const args = parsedLog.args 56 | const signature = parsedLog.signature 57 | const topic = parsedLog.topic 58 | 59 | const blockNumber = originalLog.blockNumber 60 | const blockHash = originalLog.blockHash 61 | const address = originalLog.address.toLowerCase() 62 | const data = originalLog.data 63 | const transactionHash = originalLog.transactionHash 64 | 65 | const common: EventCommon = { 66 | blockNumber, 67 | blockHash, 68 | address, 69 | data, 70 | transactionHash, 71 | signature, 72 | topic, 73 | } 74 | 75 | switch (logName) { 76 | case erc721FilledEvent.name: 77 | // KLUDGE(johnrjj) - Kinda sorta the right types, the 'args' property is correct but some of the other fields aren't. 78 | const erc721FilledLog = parsedLog as unknown as ERC721OrderFilledEvent 79 | const parseErc721FillEventArgs: FillEventParsedArgs = parseFillEvent(erc721FilledLog) 80 | const erc721FillEventPayload: Erc721FillEventPayload = { 81 | ...common, 82 | name: 'ERC721OrderFilled', 83 | eventType: 'fill', 84 | parsedData: parseErc721FillEventArgs, 85 | } 86 | return erc721FillEventPayload 87 | case erc1155FilledEvent.name: 88 | const erc1155FilledLog = parsedLog as unknown as ERC1155OrderFilledEvent 89 | const parseErc1155FillEventArgs: FillEventParsedArgs = parseFillEvent(erc1155FilledLog) 90 | const erc1155FillEventPayload: Erc1155FillEventPayload = { 91 | ...common, 92 | name: 'ERC1155OrderFilled', 93 | eventType: 'fill', 94 | parsedData: parseErc1155FillEventArgs, 95 | } 96 | return erc1155FillEventPayload 97 | case erc721CancelledEvent.name: 98 | const erc721CancelledLog = parsedLog as unknown as ERC721OrderCancelledEvent 99 | const parseErc721CancelEventArgs: CancelEventParsedArgs = parseCancelEvent(erc721CancelledLog) 100 | const erc721CancelEvent: Erc721CancelEventPayload = { 101 | ...common, 102 | name: 'ERC721OrderCancelled', 103 | eventType: 'cancel', 104 | parsedData: parseErc721CancelEventArgs, 105 | } 106 | return erc721CancelEvent 107 | case erc1155CancelledEvent.name: 108 | const erc1155CancelledLog = parsedLog as unknown as ERC1155OrderCancelledEvent 109 | const parseErc1155CancelEventArgs: CancelEventParsedArgs = parseCancelEvent(erc1155CancelledLog) 110 | const erc1155CancelEvent: Erc1155CancelEventPayload = { 111 | ...common, 112 | name: 'ERC1155OrderCancelled', 113 | eventType: 'cancel', 114 | parsedData: parseErc1155CancelEventArgs, 115 | } 116 | return erc1155CancelEvent 117 | default: 118 | throw new Error(`Unknown log name ${logName}`) 119 | } 120 | }) 121 | 122 | return parsedEvents 123 | } 124 | 125 | export interface FillEventParsedArgs { 126 | direction: string 127 | erc20Token: string 128 | erc20TokenAmount: string 129 | nftToken: string 130 | nftTokenId: string 131 | nftTokenAmount: string 132 | maker: string 133 | taker: string 134 | nonce: string 135 | matcher: string 136 | } 137 | 138 | export interface CancelEventParsedArgs { 139 | maker: string 140 | nonce: string 141 | } 142 | 143 | export interface EventCommon { 144 | blockNumber: number 145 | blockHash: string 146 | address: string 147 | data: string 148 | transactionHash: string 149 | signature: string 150 | topic: string 151 | } 152 | 153 | export interface Erc721FillEventPayload extends EventCommon { 154 | name: 'ERC721OrderFilled' 155 | eventType: 'fill' 156 | parsedData: FillEventParsedArgs 157 | } 158 | 159 | export interface Erc1155FillEventPayload extends EventCommon { 160 | name: 'ERC1155OrderFilled' 161 | eventType: 'fill' 162 | parsedData: FillEventParsedArgs 163 | } 164 | 165 | export interface Erc721CancelEventPayload extends EventCommon { 166 | name: 'ERC721OrderCancelled' 167 | eventType: 'cancel' 168 | parsedData: CancelEventParsedArgs 169 | } 170 | 171 | export interface Erc1155CancelEventPayload extends EventCommon { 172 | name: 'ERC1155OrderCancelled' 173 | eventType: 'cancel' 174 | parsedData: CancelEventParsedArgs 175 | } 176 | 177 | export type FillEventPayload = Erc721FillEventPayload | Erc1155FillEventPayload 178 | 179 | export type CancelEventPayload = Erc721CancelEventPayload | Erc1155CancelEventPayload 180 | 181 | const parseCancelEvent = (e: ERC721OrderCancelledEvent | ERC1155OrderCancelledEvent): CancelEventParsedArgs => { 182 | // KLUDGE(johnrjj) -- need to get this type safe 183 | switch ((e as any).name) { 184 | case 'ERC721OrderCancelled': 185 | const erc721CancelEvent = e as ERC721OrderCancelledEvent 186 | const erc721CancelLogArgs = erc721CancelEvent.args 187 | const erc721ParsedArgs: CancelEventParsedArgs = { 188 | maker: erc721CancelLogArgs.maker, 189 | nonce: erc721CancelLogArgs.nonce.toString(), // NOTE(johnrjj) - There's a footgun here if nonces come in the form of hex numbers 190 | } 191 | return erc721ParsedArgs 192 | case 'ERC1155OrderCancelled': 193 | const erc1155CancelEvent = e as ERC721OrderCancelledEvent 194 | const erc1155CancelLogArgs = erc1155CancelEvent.args 195 | const erc1155ParsedArgs: CancelEventParsedArgs = { 196 | maker: erc1155CancelLogArgs.maker, 197 | nonce: erc1155CancelLogArgs.nonce.toString(), 198 | } 199 | return erc1155ParsedArgs 200 | default: 201 | throw new Error(`unknown cancel order event ${(e as any).name ?? e.event}`) 202 | } 203 | } 204 | 205 | const parseFillEvent = (e: ERC721OrderFilledEvent | ERC1155OrderFilledEvent): FillEventParsedArgs => { 206 | // KLUDGE(johnrjj) -- need to get this type safe 207 | switch ((e as any).name) { 208 | case 'ERC721OrderFilled': 209 | const erc721FillEvent = e as ERC721OrderFilledEvent 210 | const erc721FilledLogArgs = erc721FillEvent.args 211 | 212 | const erc721ParsedArgs: FillEventParsedArgs = { 213 | direction: erc721FilledLogArgs.direction.toString(), 214 | erc20Token: erc721FilledLogArgs.erc20Token, 215 | erc20TokenAmount: erc721FilledLogArgs.erc20TokenAmount.toString(), 216 | nftToken: erc721FilledLogArgs.erc721Token.toString(), 217 | nftTokenId: erc721FilledLogArgs.erc721TokenId.toString(), 218 | nftTokenAmount: '1', 219 | maker: erc721FilledLogArgs.maker, 220 | taker: erc721FilledLogArgs.taker, 221 | nonce: erc721FilledLogArgs.nonce.toString(), 222 | matcher: erc721FilledLogArgs.matcher, 223 | } 224 | return erc721ParsedArgs 225 | case 'ERC1155OrderFilled': 226 | const erc1155FillEvent = e as ERC1155OrderFilledEvent 227 | const erc1155FilledLogArgs = erc1155FillEvent.args 228 | const erc1155ParsedArgs: FillEventParsedArgs = { 229 | direction: erc1155FilledLogArgs.direction.toString(), 230 | erc20Token: erc1155FilledLogArgs.erc20Token, 231 | erc20TokenAmount: erc1155FilledLogArgs.erc20FillAmount.toString(), 232 | nftToken: erc1155FilledLogArgs.erc1155Token.toString(), 233 | nftTokenId: erc1155FilledLogArgs.erc1155TokenId.toString(), 234 | nftTokenAmount: erc1155FilledLogArgs.erc1155FillAmount.toString(), 235 | maker: erc1155FilledLogArgs.maker, 236 | taker: erc1155FilledLogArgs.taker, 237 | nonce: erc1155FilledLogArgs.nonce.toString(), 238 | matcher: erc1155FilledLogArgs.matcher, 239 | } 240 | return erc1155ParsedArgs 241 | default: 242 | throw new Error(`unknown fill event ${(e as any).name ?? e.event}`) 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /src/services/cron/sync-opensea-collections.ts: -------------------------------------------------------------------------------- 1 | import createFetch from '@vercel/fetch' 2 | import { getPrismaClient } from '../../prisma-client' 3 | import { sleep } from '../utils/sleep' 4 | 5 | import * as Sentry from '@sentry/node' 6 | import cron from 'node-cron' 7 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 8 | import { 9 | CHAIN_IDS, 10 | DEFAULT_OPENSEA_API_KEY, 11 | DEFAULT_SENTRY_DSN, 12 | DEFAULT_SENTRY_SAMPLE_RATE, 13 | } from '../../default-config' 14 | import { JOBS } from '../utils/jobs' 15 | import { OpenSeaCollectionData } from '../utils/opensea' 16 | 17 | Sentry.init({ 18 | dsn: process.env.SENTRY_DSN ?? DEFAULT_SENTRY_DSN, 19 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 20 | }) 21 | 22 | const logger = getLoggerForService(ServiceNamesLogLabel['cron:sync-opensea-recent-collections']) 23 | 24 | const fetch = createFetch() 25 | 26 | const collectionsUrl = (offset: number) => `https://api.opensea.io/api/v1/collections?offset=${offset}&limit=300` 27 | 28 | interface ResponseThing { 29 | collections: OpenSeaCollectionData[] 30 | } 31 | 32 | const prisma = getPrismaClient() 33 | 34 | const MAX_LIMIT = 300 35 | const SLEEP_OFFSET_IN_MS = 500 36 | const MAX_OFFSET_ON_OPENSEA = 50000 37 | const MAX_INSERT_MISSES = 3 38 | const OPENSEA_API_KEY = process.env.OPENSEA_API_KEY 39 | 40 | const doOpenSeaCollectionSync = async () => { 41 | let offset = 0 42 | let insertMisses = 0 43 | 44 | let insertCount = 0 45 | 46 | while (offset <= MAX_OFFSET_ON_OPENSEA) { 47 | console.log(`Looking up offset ${offset}`) 48 | 49 | const collectionsResult: ResponseThing = await fetch(collectionsUrl(offset), { 50 | headers: { 51 | 'X-API-KEY': OPENSEA_API_KEY!, 52 | Accept: 'application/json', 53 | }, 54 | }).then((x) => x.json()) 55 | 56 | const collections = collectionsResult.collections 57 | 58 | console.log(`Offset: ${offset} - Found ${collections.length}`) 59 | 60 | const collectionsWithTimestamp = collections.map((c) => { 61 | delete (c as any).default_to_fiat 62 | return { 63 | ...c, 64 | created_date: new Date(c.created_date), 65 | } 66 | }) 67 | 68 | console.log(`Offset: ${offset} - Inserting ${collections.length} columns`) 69 | 70 | const dbRes = await prisma.opensea_collection_metadata_by_slug_v1.createMany({ 71 | data: [...collectionsWithTimestamp] as any, 72 | skipDuplicates: true, 73 | }) 74 | 75 | console.log(`Offset: ${offset} - Inserted ${dbRes.count} columns`) 76 | 77 | insertCount += dbRes.count 78 | 79 | if (dbRes.count === 0) { 80 | insertMisses += 1 81 | } else { 82 | insertMisses = 0 83 | } 84 | 85 | offset += MAX_LIMIT 86 | 87 | sleep(SLEEP_OFFSET_IN_MS) 88 | 89 | if (insertMisses >= MAX_INSERT_MISSES) { 90 | logger.silly(`doOpenSeaCollectionSync: No more new collections detected.`, { insertCount, insertMisses, offset }) 91 | return insertCount 92 | } 93 | } 94 | 95 | return insertCount 96 | } 97 | 98 | const jobsToMonitorGaps = [[JOBS.OpenSeaRecentCollectionsSync, CHAIN_IDS.MAINNET]] 99 | 100 | const CRON_FREQUENCY_IN_MINUTES = 10 101 | const cronTasks = jobsToMonitorGaps.map(([jobName, chainId]) => { 102 | // ('*/30 * * * *') <-- cron for every 30 min 103 | /** 104 | * Run every 10 minutes 105 | */ 106 | const cronTask = cron.schedule(`*/${CRON_FREQUENCY_IN_MINUTES} * * * *`, async (now) => { 107 | logger.info(`Running ${jobName} job (chain ${chainId})`, { jobName, now }) 108 | const insertedRows = await doOpenSeaCollectionSync() 109 | logger.info(`Job ${jobName} completed successfully. ${insertedRows} new Collections found/inserted.`, { 110 | jobName, 111 | now, 112 | insertedRows, 113 | }) 114 | }) 115 | 116 | logger.info(`Initialized ${jobName} cron job`, { jobName }) 117 | return cronTask 118 | }) 119 | 120 | logger.info( 121 | `Started ${JOBS.OpenSeaRecentCollectionsSync} Cron job for ${cronTasks.length} jobs). Will run every ${CRON_FREQUENCY_IN_MINUTES} minutes for each job`, 122 | { jobsToMonitorGaps } 123 | ) 124 | -------------------------------------------------------------------------------- /src/services/publishers/block-number.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import { PubSub } from '@google-cloud/pubsub' 3 | import { createAlchemyWeb3 } from '@alch/alchemy-web3' 4 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 5 | import { PUBSUB_TOPICS } from '../utils/pubsub' 6 | import type { BlockNumberUpdateEvent } from '../utils/messaging-types' 7 | import { 8 | CHAIN_IDS, 9 | DEFAULT_SENTRY_DSN, 10 | DEFAULT_SENTRY_SAMPLE_RATE, 11 | GCP_PROJECT_ID, 12 | getWsRpcUrlByChainId, 13 | } from '../../default-config' 14 | 15 | Sentry.init({ 16 | dsn: process.env.SENTRY_DSN ?? DEFAULT_SENTRY_DSN, 17 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 18 | }) 19 | 20 | interface AlchemyBlockHeader { 21 | number: number 22 | hash: string 23 | parentHash: string 24 | nonce: string 25 | sha3Uncles: string 26 | logsBloom: string 27 | transactionRoot: string 28 | stateRoot: string 29 | receiptsRoot: string 30 | miner: string 31 | extraData: string 32 | gasLimit: number 33 | gasUsed: number 34 | timestamp: number | string 35 | baseFeePerGas?: number 36 | } 37 | 38 | const logger = getLoggerForService(ServiceNamesLogLabel['producer:block-number']) 39 | 40 | const runBlockWatcherAsync = async (chainId: string) => { 41 | const alchemyRpcUrl = getWsRpcUrlByChainId(chainId) 42 | if (!alchemyRpcUrl) { 43 | logger.error('Alchemy RPC Url null', { alchemyRpcUrl, chainId }) 44 | throw new Error('Alchemy RPC Url null') 45 | } 46 | const web3 = createAlchemyWeb3(alchemyRpcUrl) 47 | 48 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 49 | 50 | const blockNumberTopic = pubsub.topic(PUBSUB_TOPICS.BlockNumberUpdate) 51 | 52 | const handleNewBlock = async (error: Error, blockHeader: AlchemyBlockHeader) => { 53 | if (error) { 54 | console.log(error) 55 | throw new Error('Error connecting') 56 | } 57 | const blockNumber = blockHeader.number 58 | const timestamp = blockHeader.timestamp 59 | 60 | logger.info(`BlockPublisher: New block for chain ${chainId} (${blockNumber}) detected. Publishing event...`, { 61 | timestamp, 62 | blockNumber, 63 | chainId, 64 | }) 65 | 66 | const blockNumberMessage: BlockNumberUpdateEvent = { 67 | data: { 68 | blockNumber: blockNumber, 69 | chainId: chainId, 70 | timestamp: timestamp.toString(), 71 | hash: blockHeader.hash, 72 | parentHash: blockHeader.parentHash, 73 | nonce: blockHeader.nonce, 74 | }, 75 | eventName: 'block-number.update', 76 | topic: PUBSUB_TOPICS.BlockNumberUpdate, 77 | } 78 | 79 | const messageId = await blockNumberTopic.publishMessage({ 80 | json: blockNumberMessage, 81 | orderingKey: chainId, 82 | attributes: { 83 | chainId: chainId, 84 | blockNumber: blockNumber.toString(10), 85 | }, 86 | }) 87 | 88 | logger.silly(`BlockPublisher: Posted new block for chain ${chainId} (${blockNumber}) to 'new block' topic.`, { 89 | timestamp, 90 | blockNumber, 91 | chainId, 92 | blockNumberMessage, 93 | messageId, 94 | }) 95 | } 96 | 97 | web3.eth.subscribe('newBlockHeaders', handleNewBlock) 98 | } 99 | 100 | interface Web3JsBlockHeader { 101 | number: number 102 | hash: string 103 | parentHash: string 104 | nonce: string 105 | sha3Uncles: string 106 | logsBloom: string 107 | transactionRoot: string 108 | stateRoot: string 109 | receiptRoot: string 110 | miner: string 111 | extraData: string 112 | gasLimit: number 113 | gasUsed: number 114 | timestamp: number | string 115 | } 116 | 117 | const CHAINS: string[] = [ 118 | CHAIN_IDS.MAINNET, 119 | CHAIN_IDS.POLYGON, 120 | CHAIN_IDS.POLYGON_MUMBAI, 121 | CHAIN_IDS.OPTIMISM, 122 | CHAIN_IDS.ARBITRUM, 123 | CHAIN_IDS.GOERLI, 124 | ] 125 | 126 | CHAINS.forEach((chain) => { 127 | runBlockWatcherAsync(chain) 128 | }) 129 | -------------------------------------------------------------------------------- /src/services/publishers/fix-block-gaps-by-job.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import cron from 'node-cron' 3 | import { Prisma, PrismaClient } from '@prisma/client' 4 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 5 | import { getPrismaClient } from '../../prisma-client' 6 | import { CHAIN_IDS, DEFAULT_SENTRY_DSN, DEFAULT_SENTRY_SAMPLE_RATE } from '../../default-config' 7 | import { JOBS } from '../utils/jobs' 8 | 9 | Sentry.init({ 10 | dsn: process.env.SENTRY_DSN ?? DEFAULT_SENTRY_DSN, 11 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 12 | }) 13 | 14 | const logger = getLoggerForService(ServiceNamesLogLabel['producer:fix-block-gaps-by-job']) 15 | 16 | // https://stackoverflow.com/questions/4340793/how-to-find-gaps-in-sequential-numbering-in-mysql/29736658#29736658 17 | const fetchBlockGapsByJobName = async (jobName: string, chainId: string, prisma: PrismaClient) => { 18 | if (!jobName) { 19 | throw new Error('jobName canot be null') 20 | } 21 | if (!chainId) { 22 | throw new Error('chainId canot be null') 23 | } 24 | const gaps = await prisma.$queryRaw>` 25 | select 26 | a .block_number + 1 gapStart, 27 | ( 28 | select 29 | x.block_number -1 30 | from 31 | job_records x 32 | where 33 | x.block_number > a .block_number + 1 34 | AND a .job_name = '${Prisma.sql([jobName])}' 35 | AND a .chain_id = '${Prisma.sql([chainId.toString()])}' 36 | limit 37 | 1 38 | ) gapEnd 39 | from 40 | job_records a 41 | left join job_records b on b.block_number = a .block_number + 1 42 | where 43 | b.block_number is null 44 | AND a .job_name = '${Prisma.sql([jobName])}' 45 | AND a .chain_id = '${Prisma.sql([chainId.toString()])}' 46 | 47 | order by 48 | gapStart; 49 | ` 50 | return gaps ?? [] 51 | } 52 | 53 | const jobsToMonitorGaps = [[JOBS.OrderUpdateByBlock, CHAIN_IDS.ROPSTEN]] 54 | 55 | const cronTasks = jobsToMonitorGaps.map(([jobName, chainId]) => { 56 | // ('*/30 * * * *') <-- cron for every 30 min 57 | /** 58 | * Run every 30 minutes 59 | */ 60 | const cronTask = cron.schedule('*/1 * * * *', async (now) => { 61 | const prisma = getPrismaClient() 62 | 63 | const jobName = JOBS.OrderUpdateByBlock 64 | 65 | logger.info(`Running fix block gap cron job for job ${jobName}`, { jobName, now }) 66 | 67 | const blockGapsForJob = await fetchBlockGapsByJobName(jobName, chainId, prisma) 68 | 69 | console.log('blockGaps', blockGapsForJob) 70 | logger.info(`${blockGapsForJob.length} block gaps detected for job ${jobName}`, { jobName, now }) 71 | 72 | return 73 | }) 74 | logger.info(`Initialized FixBlockGapsByJob cron service for job ${jobName}`) 75 | return cronTask 76 | }) 77 | 78 | logger.info( 79 | `Started FixBlockGapsByJob Cron Service for ${cronTasks.length} jobs. Will run every 30 minutes for each job`, 80 | { jobsToMonitorGaps } 81 | ) 82 | 83 | /** 84 | * 85 | * 86 | * Other solutions: 87 | * 88 | * -- SELECT (t1.block_number + 1) as gap_starts_at, 89 | -- (SELECT MIN(t3.block_number) -1 FROM job_records t3 WHERE t3.block_number > t1.block_number) as gap_ends_at 90 | -- FROM job_records t1 91 | -- WHERE NOT EXISTS (SELECT t2.block_number FROM job_records t2 WHERE t2.block_number = t1.block_number + 1) 92 | -- HAVING gap_ends_at IS NOT NULL 93 | 94 | // fastest below: 95 | SELECT 96 | CONCAT( 97 | z.expected, 98 | IF(z.got -1 > z.expected, CONCAT(' thru ', z.got -1), '') 99 | ) AS missing 100 | FROM 101 | ( 102 | SELECT 103 | @rownum: = @rownum + 1 AS expected, 104 | IF(@rownum = block_number, 0, @rownum: = block_number) AS got 105 | FROM 106 | ( 107 | SELECT 108 | @rownum: = ( 109 | SELECT 110 | MIN(block_number) -1 111 | FROM 112 | job_records 113 | ) 114 | ) AS a 115 | JOIN job_records 116 | ORDER BY 117 | block_number 118 | ) AS z 119 | WHERE 120 | z.got ! = 0; 121 | * 122 | * 123 | */ 124 | -------------------------------------------------------------------------------- /src/services/publishers/nft-metadata.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from '@sentry/node' 2 | import { PubSub } from '@google-cloud/pubsub' 3 | import { createAlchemyWeb3 } from '@alch/alchemy-web3' 4 | import { getLoggerForService, ServiceNamesLogLabel } from '../../logger' 5 | import { PUBSUB_TOPICS } from '../utils/pubsub' 6 | import type { NftMetadataCollectionRequestEvent, NftMetadataRequestEvent } from '../utils/messaging-types' 7 | import { 8 | DEFAULT_SENTRY_DSN, 9 | DEFAULT_SENTRY_SAMPLE_RATE, 10 | GCP_PROJECT_ID, 11 | getWsRpcUrlByChainId, 12 | } from '../../default-config' 13 | 14 | Sentry.init({ 15 | dsn: process.env.SENTRY_DSN ?? DEFAULT_SENTRY_DSN, 16 | tracesSampleRate: DEFAULT_SENTRY_SAMPLE_RATE, 17 | }) 18 | 19 | const logger = getLoggerForService(ServiceNamesLogLabel['producer:nft-metadata-request']) 20 | 21 | /** 22 | * 23 | */ 24 | interface PublishNftMetadataUpdateRequestOptions { 25 | tokenId: string | null 26 | collection: boolean | null 27 | startToken: string | null 28 | } 29 | 30 | const publishMessageToTopic = async ( 31 | nftMetadataMessage: NftMetadataRequestEvent | NftMetadataCollectionRequestEvent, 32 | chainId: string 33 | ) => { 34 | const pubsub = new PubSub({ projectId: GCP_PROJECT_ID }) 35 | const nftUpdateTopic = pubsub.topic(PUBSUB_TOPICS.NftMetadataUpdateRequest) 36 | const messageId = await nftUpdateTopic.publishMessage({ 37 | json: nftMetadataMessage, 38 | orderingKey: chainId, 39 | attributes: { 40 | chainId: chainId, 41 | }, 42 | }) 43 | 44 | return messageId 45 | } 46 | 47 | const publishNftMetadataUpdateRequest = async ( 48 | contractAddress: string, 49 | chainId: string, 50 | options: PublishNftMetadataUpdateRequestOptions 51 | ): Promise => { 52 | const alchemyRpcUrl = getWsRpcUrlByChainId(chainId) 53 | if (!alchemyRpcUrl) { 54 | logger.error('Alchemy RPC Url null', { alchemyRpcUrl, chainId }) 55 | throw new Error('Alchemy RPC Url null') 56 | } 57 | 58 | if (options.tokenId) { 59 | const nftMetadataUpdateRequestMessage: NftMetadataRequestEvent = { 60 | data: { 61 | chainId: chainId, 62 | contractAddress: contractAddress, 63 | tokenId: options.tokenId, 64 | }, 65 | eventName: 'nft.metadata.update.individual', 66 | topic: PUBSUB_TOPICS.NftMetadataUpdateRequest, 67 | } 68 | 69 | const messageId = await publishMessageToTopic(nftMetadataUpdateRequestMessage, chainId) 70 | 71 | logger.log('info', `NftMetadataPublisher: Posted message ${messageId}`, nftMetadataUpdateRequestMessage) 72 | return messageId 73 | } 74 | 75 | if (options.collection) { 76 | const nftMetadataUpdateCollectionRequestMessage: NftMetadataCollectionRequestEvent = { 77 | data: { 78 | chainId: chainId, 79 | contractAddress: contractAddress, 80 | startToken: options.startToken ?? undefined, 81 | }, 82 | eventName: 'nft.metadata.update.collection', 83 | topic: PUBSUB_TOPICS.NftMetadataUpdateRequest, 84 | } 85 | 86 | const messageId = await publishMessageToTopic(nftMetadataUpdateCollectionRequestMessage, chainId) 87 | 88 | logger.log('info', `NftMetadataPublisher: Posted message ${messageId}`, nftMetadataUpdateCollectionRequestMessage) 89 | return messageId 90 | } 91 | 92 | logger.error(`NftMetadataPublisher: Unknown nft metadata configuration`, { options, contractAddress, chainId }) 93 | throw new Error('Unknown configuration') 94 | } 95 | 96 | const publishNftMetadataForNftRequest = (contractAddress: string, tokenId: string, chainId: string) => { 97 | return publishNftMetadataUpdateRequest(contractAddress, chainId, { tokenId, collection: null, startToken: null }) 98 | } 99 | 100 | const publishNftMetadataForNftCollectionRequest = ( 101 | contractAddress: string, 102 | chainId: string, 103 | startToken: string | null 104 | ) => { 105 | return publishNftMetadataUpdateRequest(contractAddress, chainId, { 106 | tokenId: null, 107 | collection: true, 108 | startToken: startToken, 109 | }) 110 | } 111 | 112 | export { publishNftMetadataForNftRequest, publishNftMetadataForNftCollectionRequest } 113 | -------------------------------------------------------------------------------- /src/services/utils/covalent.ts: -------------------------------------------------------------------------------- 1 | import { encode } from 'base-64' 2 | 3 | // https://api.covalenthq.com/v1/1/nft_market/collection/0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d/?quote-currency=USD&format=JSON&from=2022-01-01 4 | 5 | import createFetch from '@vercel/fetch' 6 | import { DEFAULT_COVALENT_API_KEY, DEFAULT_OPENSEA_API_KEY } from '../../default-config' 7 | import format from 'date-fns/format' 8 | import subDays from 'date-fns/subDays' 9 | 10 | const fetch = createFetch() 11 | 12 | export interface NftHistoryByDay { 13 | chain_id: number 14 | collection_name: string 15 | collection_address: string 16 | collection_ticker_symbol: string 17 | opening_date: string 18 | volume_wei_day: string 19 | volume_quote_day: number 20 | average_volume_wei_day: string 21 | average_volume_quote_day: number 22 | unique_token_ids_sold_count_day: number 23 | floor_price_wei_7d: string 24 | floor_price_quote_7d: number 25 | gas_quote_rate_day: number 26 | quote_currency: string 27 | } 28 | 29 | export interface NftHistoricalDataPayload { 30 | updated_at: string 31 | items: NftHistoryByDay[] 32 | pagination?: any 33 | } 34 | 35 | export interface CovalentResponse { 36 | data: T 37 | error: boolean 38 | error_message?: any 39 | error_code?: any 40 | } 41 | 42 | const getNftHistory = async (collectionAddress: string, start?: Date, end?: Date) => { 43 | const defaultEnd = new Date() 44 | const defaultStart = subDays(defaultEnd, 30) 45 | 46 | const startFormatted = format(start ?? defaultStart, 'yyyy-MM-dd') 47 | const endFormatted = format(end ?? defaultEnd, 'yyyy-MM-dd') 48 | 49 | const nftHistoryResponse: CovalentResponse = await fetch( 50 | `https://api.covalenthq.com/v1/1/nft_market/collection/${collectionAddress}/?quote-currency=USD&format=JSON&from=${startFormatted}`, 51 | { 52 | headers: { 53 | // Colon after just tells auth 'no password, only username' 54 | Authorization: 'Basic ' + encode(`${DEFAULT_COVALENT_API_KEY}:`), 55 | }, 56 | } 57 | ).then((r) => r.json()) 58 | 59 | return nftHistoryResponse 60 | } 61 | 62 | const bayc = '0xbc4ca0eda7647a8ab7c2061c2e118a18a936f13d' 63 | getNftHistory(bayc) 64 | .then((x) => console.log(x)) 65 | .catch((e) => console.log('err', e)) 66 | -------------------------------------------------------------------------------- /src/services/utils/jobs.ts: -------------------------------------------------------------------------------- 1 | const JOBS = { 2 | OrderUpdateByBlock: 'exchange_order_update_events_by_block', 3 | SaveBlock: 'save_block', 4 | OpenSeaRecentCollectionsSync: 'opensea_recent_collections_sync', 5 | } 6 | 7 | export { JOBS } 8 | -------------------------------------------------------------------------------- /src/services/utils/messaging-types.ts: -------------------------------------------------------------------------------- 1 | type AvailableEventName = 2 | | 'block-number.update' 3 | | 'order.validate-status' 4 | | 'nft.metadata.update.individual' 5 | | 'nft.metadata.update.collection' 6 | | 'nft.opensea.scrape.collection-by-address' 7 | 8 | export interface BaseEvent { 9 | topic: string 10 | eventName: AvailableEventName 11 | data: Record 12 | } 13 | 14 | export interface BlockNumberUpdateEvent extends BaseEvent { 15 | eventName: 'block-number.update' 16 | data: { 17 | blockNumber: number 18 | chainId: string 19 | timestamp: string 20 | hash: string 21 | parentHash: string 22 | nonce: string 23 | } 24 | } 25 | 26 | export interface OrderStatusUpdateRequestEvent extends BaseEvent { 27 | eventName: 'order.validate-status' 28 | data: { 29 | orderNonce: string 30 | chainId: string 31 | } 32 | } 33 | 34 | export interface NftMetadataRequestEvent extends BaseEvent { 35 | eventName: 'nft.metadata.update.individual' 36 | data: { 37 | contractAddress: string 38 | chainId: string 39 | tokenId: string 40 | } 41 | } 42 | 43 | export interface NftMetadataCollectionRequestEvent extends BaseEvent { 44 | eventName: 'nft.metadata.update.collection' 45 | data: { 46 | contractAddress: string 47 | chainId: string 48 | startToken?: string 49 | } 50 | } 51 | 52 | export interface NftOpenseaScrapeCollectionByAddressRequestEvent extends BaseEvent { 53 | eventName: 'nft.opensea.scrape.collection-by-address' 54 | data: { 55 | contractAddress: string 56 | chainId: string 57 | } 58 | } 59 | 60 | export type AvailableEvents = 61 | | BlockNumberUpdateEvent 62 | | OrderStatusUpdateRequestEvent 63 | | NftMetadataRequestEvent 64 | | NftMetadataCollectionRequestEvent 65 | | NftOpenseaScrapeCollectionByAddressRequestEvent 66 | -------------------------------------------------------------------------------- /src/services/utils/nfts.ts: -------------------------------------------------------------------------------- 1 | import { createAlchemyWeb3 } from '@alch/alchemy-web3' 2 | import queryString from 'query-string' 3 | import { Agent } from '@zoralabs/nft-metadata' 4 | import createFetch from '@vercel/fetch' 5 | import { StaticJsonRpcProvider } from '@ethersproject/providers' 6 | import { getJsonRpcUrlByChainId, DEFAULT_OPENSEA_API_KEY } from '../../default-config' 7 | import { ERC721__factory } from '../../contracts' 8 | 9 | interface CanonicalNftWithMetadata { 10 | tokenId: string 11 | tokenAddress: string 12 | metadata: any 13 | tokenURI: string 14 | tokenURL?: string 15 | tokenURLMimeType?: string 16 | name?: string 17 | description?: string 18 | contentURL?: string 19 | contentURLMimeType?: string 20 | imageURL?: string 21 | imageURLMimeType?: string 22 | externalLink?: string 23 | attributes?: Record[] 24 | } 25 | 26 | const fetch = createFetch() 27 | 28 | interface NftMetadata { 29 | image?: string 30 | attributes?: Array> 31 | } 32 | interface TokenUri { 33 | raw: string 34 | gateway: string 35 | } 36 | interface NftMedia { 37 | uri?: TokenUri 38 | } 39 | interface NftContract { 40 | address: string 41 | } 42 | interface NftId { 43 | tokenId: string 44 | tokenMetadata?: NftTokenMetadata 45 | } 46 | interface NftTokenMetadata { 47 | tokenType: 'erc721' | 'erc1155' 48 | } 49 | 50 | export interface NftWithMetadata { 51 | contract: NftContract 52 | id: NftId 53 | title: string 54 | description: string 55 | tokenUri?: TokenUri 56 | media?: NftMedia[] 57 | metadata?: NftMetadata 58 | timeLastUpdated: string 59 | } 60 | 61 | export interface GetCollectionResponse { 62 | nfts: Array 63 | nextToken: string | undefined 64 | } 65 | 66 | export interface AccountNftsResponse { 67 | ownedNfts: Array 68 | totalCount: number 69 | blockHash: string 70 | } 71 | 72 | const fetchCollection = async ( 73 | contractAdddress: string, 74 | chainId: string, 75 | startTokenId?: string 76 | ): Promise => { 77 | const requestOptions = { 78 | method: 'GET', 79 | redirect: 'follow', 80 | } as const 81 | 82 | // const contractAddr = "0x61fce80d72363b731425c3a2a46a1a5fed9814b2"; 83 | // const startTokenId = undefined //"0x1ea2"; 84 | const withMetadata = 'true' 85 | 86 | const queryParams = queryString.stringify({ 87 | contractAddress: contractAdddress, 88 | startToken: startTokenId, 89 | withMetadata: withMetadata, 90 | }) 91 | 92 | const baseURL = getJsonRpcUrlByChainId(chainId) 93 | const fetchURL = `${baseURL}?${queryParams}` 94 | 95 | const collectionResponse: GetCollectionResponse = await fetch(fetchURL, requestOptions).then((response) => 96 | response.json() 97 | ) 98 | 99 | return collectionResponse 100 | } 101 | 102 | const fetchNftMetadataFromAlchemy = async ( 103 | contractAddress: string, 104 | tokenId: string, 105 | chainId: string, 106 | tokenType?: 'erc721' | 'erc1155' 107 | ): Promise => { 108 | const alchemy = createAlchemyWeb3(getJsonRpcUrlByChainId(chainId)) 109 | const alchemyNftMetadata = await alchemy.alchemy.getNftMetadata({ contractAddress, tokenId, tokenType }) 110 | const nftMetadata = alchemyNftMetadata as NftWithMetadata // KLUDGE(johnrjj) 111 | return nftMetadata 112 | } 113 | 114 | const fetchNftsForWallet = async ( 115 | owner: string, 116 | chainId: string, 117 | contractAddresssFilter?: string[] | undefined, 118 | pageKey?: string | undefined 119 | ) => { 120 | const alchemy = createAlchemyWeb3(getJsonRpcUrlByChainId(chainId)) 121 | const nftsOwned = await alchemy.alchemy.getNfts({ 122 | owner: owner, 123 | contractAddresses: contractAddresssFilter, 124 | pageKey, 125 | // filters: ['SPAM'], 126 | // filters: ['SPAM'], 127 | }) 128 | // KLUDGE(johnrjj) - Returns wrong types when `withMetadata` is true (which it is by default) 129 | return nftsOwned 130 | } 131 | 132 | const fetchNftMetadataFromOpenSea = async (contractAddress: string, tokenId: string, _chainId: string) => { 133 | if (_chainId !== '1') { 134 | throw new Error('only mainnet') 135 | } 136 | const metadataResponseFromOpenSea = await fetch( 137 | `https://api.opensea.io/api/v1/asset/${contractAddress}/${tokenId}/?include_orders=false`, 138 | { 139 | headers: { 140 | 'X-API-KEY': DEFAULT_OPENSEA_API_KEY, 141 | Accept: 'application/json', 142 | }, 143 | } 144 | ).then((x) => x.json()) 145 | 146 | return metadataResponseFromOpenSea 147 | } 148 | 149 | interface NftWithMetadataZoraLibraryVersion { 150 | tokenId: string 151 | tokenAddress: string 152 | metadata: any 153 | tokenURI: string 154 | tokenURL: string 155 | tokenURLMimeType: string 156 | name?: string 157 | description?: string 158 | contentURL?: string 159 | contentURLMimeType?: string 160 | imageURL?: string 161 | imageURLMimeType?: string 162 | externalLink?: string 163 | attributes?: Record[] 164 | } 165 | 166 | const getNftMetadataOnAnyChain = async ( 167 | contractAddress: string, 168 | tokenId: string, 169 | chainId: string 170 | ): Promise => { 171 | const parser = new Agent({ 172 | network: chainId, 173 | timeout: 30 * 1000, 174 | provider: new StaticJsonRpcProvider(getJsonRpcUrlByChainId(chainId), parseInt(chainId)), 175 | }) 176 | 177 | const metadata: NftWithMetadataZoraLibraryVersion = await parser.fetchMetadata(contractAddress, tokenId) 178 | 179 | return metadata 180 | } 181 | 182 | const getNftMetadataForContractOnAnyChain = async (contractAddress: string, chainId: string) => { 183 | const provider = new StaticJsonRpcProvider(getJsonRpcUrlByChainId(chainId), parseInt(chainId)) 184 | 185 | const erc721 = ERC721__factory.connect(contractAddress, provider) 186 | 187 | let name: string | undefined = undefined 188 | try { 189 | name = await erc721.name() 190 | } catch (e) {} 191 | 192 | return { 193 | name, 194 | } 195 | } 196 | 197 | export { 198 | fetchCollection, 199 | fetchNftMetadataFromAlchemy, 200 | fetchNftMetadataFromOpenSea, 201 | fetchNftsForWallet, 202 | getNftMetadataForContractOnAnyChain, 203 | getNftMetadataOnAnyChain, 204 | } 205 | -------------------------------------------------------------------------------- /src/services/utils/opensea.ts: -------------------------------------------------------------------------------- 1 | import createFetch from '@vercel/fetch' 2 | import { CHAIN_IDS, DEFAULT_OPENSEA_API_KEY } from '../../default-config' 3 | 4 | const fetch = createFetch() 5 | 6 | export interface OpenSeaV1CollectionByContractAddressResponsePayload { 7 | collection: OpenSeaCollectionData 8 | address: string 9 | asset_contract_type: string 10 | created_date: string 11 | name: string 12 | nft_version: string 13 | opensea_version?: null 14 | owner: number 15 | schema_name: string 16 | symbol: string 17 | total_supply: string 18 | description: string 19 | external_link: string 20 | image_url: string 21 | default_to_fiat: boolean 22 | dev_buyer_fee_basis_points: number 23 | dev_seller_fee_basis_points: number 24 | only_proxied_transfers: boolean 25 | opensea_buyer_fee_basis_points: number 26 | opensea_seller_fee_basis_points: number 27 | buyer_fee_basis_points: number 28 | seller_fee_basis_points: number 29 | payout_address: string 30 | } 31 | 32 | export interface OpenSeaCollectionData { 33 | banner_image_url: string 34 | chat_url?: null 35 | created_date: string 36 | default_to_fiat: boolean 37 | description: string 38 | dev_buyer_fee_basis_points: string 39 | dev_seller_fee_basis_points: string 40 | discord_url: string 41 | display_data: DisplayData 42 | external_url: string 43 | featured: boolean 44 | featured_image_url?: null 45 | hidden: boolean 46 | safelist_request_status: string 47 | image_url: string 48 | is_subject_to_whitelist: boolean 49 | large_image_url?: null 50 | medium_username?: null 51 | name: string 52 | only_proxied_transfers: boolean 53 | opensea_buyer_fee_basis_points: string 54 | opensea_seller_fee_basis_points: string 55 | payout_address: string 56 | require_email: boolean 57 | short_description?: null 58 | slug: string 59 | telegram_url?: null 60 | twitter_username?: null 61 | instagram_username: string 62 | wiki_url?: null 63 | is_nsfw: boolean 64 | } 65 | 66 | export interface DisplayData { 67 | card_display_style: string 68 | } 69 | 70 | // https://api.opensea.io/api/v1/asset_contract/0xED5AF388653567Af2F388E6224dC7C4b3241C544 71 | // NOTE(johnrjj) - This endpoint REQUIRES X-API-KEY header 72 | const fetchOpenseaCollectionByContractAddress = async ( 73 | contractAddress: string, 74 | chainId: string 75 | ): Promise => { 76 | let fetchUrl = `https://api.opensea.io/api/v1/asset_contract/${contractAddress}` 77 | if (chainId === CHAIN_IDS.ROPSTEN) { 78 | fetchUrl = `https://testnets-api.opensea.io/api/v1/asset_contract/${contractAddress}` 79 | } 80 | 81 | let headers: Record = { 82 | 'X-API-KEY': DEFAULT_OPENSEA_API_KEY, 83 | } 84 | if (chainId === CHAIN_IDS.ROPSTEN) { 85 | headers = {} 86 | } 87 | 88 | const fetchRes: OpenSeaV1CollectionByContractAddressResponsePayload | null = await fetch(fetchUrl, { 89 | headers, 90 | }).then((preJsonRes) => { 91 | console.log('preJsonRes', preJsonRes) 92 | if (preJsonRes.status === 404) { 93 | return null 94 | } 95 | return preJsonRes.json() 96 | }) 97 | 98 | return fetchRes 99 | } 100 | 101 | /** 102 | * Fetching by collection slug yields much more data (e.g. stats, etc) than by address 103 | * However, we have to fetch by address first to figure out the slug id 104 | * @param collectionSlug 105 | * @returns Collection data by slug (includes stats and other data) 106 | */ 107 | const fetchOpenseaCollectionBySlug = async (collectionSlug: string) => { 108 | const collectionSlugData: CollectionDataBySlugResponse = await fetch( 109 | `https://api.opensea.io/api/v1/collection/${collectionSlug}` 110 | ).then((x) => x.json()) 111 | return collectionSlugData 112 | } 113 | 114 | export interface CollectionDataBySlugResponse { 115 | collection: CollectionBySlugData 116 | } 117 | 118 | export interface CollectionBySlugData { 119 | editors: string[] 120 | payment_tokens: PaymentToken[] 121 | primary_asset_contracts: PrimaryAssetContract[] 122 | traits: Record> 123 | stats: Stats 124 | banner_image_url: string 125 | chat_url: any 126 | created_date: string 127 | default_to_fiat: boolean 128 | description: string 129 | dev_buyer_fee_basis_points: string 130 | dev_seller_fee_basis_points: string 131 | discord_url: string 132 | display_data: DisplayData 133 | external_url: string 134 | featured: boolean 135 | featured_image_url: any 136 | hidden: boolean 137 | safelist_request_status: string 138 | image_url: string 139 | is_subject_to_whitelist: boolean 140 | large_image_url: any 141 | medium_username: any 142 | name: string 143 | only_proxied_transfers: boolean 144 | opensea_buyer_fee_basis_points: string 145 | opensea_seller_fee_basis_points: string 146 | payout_address: string 147 | require_email: boolean 148 | short_description: any 149 | slug: string 150 | telegram_url: any 151 | twitter_username: any 152 | instagram_username: string 153 | wiki_url: any 154 | is_nsfw: boolean 155 | } 156 | 157 | export interface PaymentToken { 158 | id: number 159 | symbol: string 160 | address: string 161 | image_url: string 162 | name: string 163 | decimals: number 164 | eth_price: number 165 | usd_price: number 166 | } 167 | 168 | export interface PrimaryAssetContract { 169 | address: string 170 | asset_contract_type: string 171 | created_date: string 172 | name: string 173 | nft_version: string 174 | opensea_version: any 175 | owner: number 176 | schema_name: string 177 | symbol: string 178 | total_supply: string 179 | description: string 180 | external_link: string 181 | image_url: string 182 | default_to_fiat: boolean 183 | dev_buyer_fee_basis_points: number 184 | dev_seller_fee_basis_points: number 185 | only_proxied_transfers: boolean 186 | opensea_buyer_fee_basis_points: number 187 | opensea_seller_fee_basis_points: number 188 | buyer_fee_basis_points: number 189 | seller_fee_basis_points: number 190 | payout_address: string 191 | } 192 | 193 | export interface Stats { 194 | one_day_volume: number 195 | one_day_change: number 196 | one_day_sales: number 197 | one_day_average_price: number 198 | seven_day_volume: number 199 | seven_day_change: number 200 | seven_day_sales: number 201 | seven_day_average_price: number 202 | thirty_day_volume: number 203 | thirty_day_change: number 204 | thirty_day_sales: number 205 | thirty_day_average_price: number 206 | total_volume: number 207 | total_sales: number 208 | total_supply: number 209 | count: number 210 | num_owners: number 211 | average_price: number 212 | num_reports: number 213 | market_cap: number 214 | floor_price: number 215 | } 216 | 217 | export interface DisplayData { 218 | card_display_style: string 219 | } 220 | 221 | export { fetchOpenseaCollectionByContractAddress } 222 | -------------------------------------------------------------------------------- /src/services/utils/pubsub.ts: -------------------------------------------------------------------------------- 1 | // Message interface on the PubSub nodejs client 2 | export interface PubSubMessage { 3 | // message.id = ID of the message. 4 | id: string 5 | // message.data = Contents of the message. 6 | data: string 7 | // message.attributes = Attributes of the message. 8 | attributes: any 9 | // Ack the message: 10 | ack: () => Promise 11 | // Nack: This doesn't ack the message, but allows more messages to be retrieved 12 | // if your limit was hit or if you don't want to ack the message. 13 | nack: () => void 14 | // message.publishTime = Date when Pub/Sub received the message. 15 | publishTime: string 16 | // message.ackId = ID used to acknowledge the message receival. 17 | } 18 | 19 | const PUBSUB_TOPICS = { 20 | BlockNumberUpdate: 'projects/traderxyz/topics/api.block-number', 21 | ValidateOrderStatus: 'projects/traderxyz/topics/api.order.validate-status', 22 | NftMetadataUpdateRequest: 'projects/traderxyz/topics/api.nft-metadata-request', 23 | NftOpenSeaCollectionScrape: 'projects/traderxyz/topics/api.nft.opensea.scrape.collection-by-address', 24 | } 25 | 26 | const PUBSUB_SUBSCRIPTIONS = { 27 | ProcessExchangeOrderUpdatesByBlockNumber: 'projects/traderxyz/subscriptions/api.order-status.by-block-number.sub', 28 | SaveNewBlockToTable: 'projects/traderxyz/subscriptions/api.block-table.sub', 29 | NftMetadataUpdateHandlerSub: 'projects/traderxyz/subscriptions/api.nft-metadata-request.sub', 30 | NftOpenSeaCollectionScrape: 'projects/traderxyz/subscriptions/api.nft.opensea.scrape.collection-by-address.sub', 31 | } 32 | 33 | export { PUBSUB_TOPICS, PUBSUB_SUBSCRIPTIONS } 34 | -------------------------------------------------------------------------------- /src/services/utils/sleep.ts: -------------------------------------------------------------------------------- 1 | export const sleep = (t: number) => { 2 | return new Promise((resolve) => { 3 | const timeout = setTimeout(() => { 4 | clearTimeout(timeout) 5 | resolve() 6 | }, t) 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /src/types-complex.ts: -------------------------------------------------------------------------------- 1 | import type { Prisma } from '@prisma/client' 2 | import type { FeeStructSerialized, PropertyStructSerialized, SignatureStruct } from './types' 3 | 4 | // Comment out when running zod generate 5 | export interface NftOrderV4DatabaseModel { 6 | id: string 7 | maker: string 8 | taker: string 9 | expiry: string 10 | expiry_datetime: Date 11 | nonce: string 12 | erc20_token: string 13 | erc20_token_amount: string 14 | fees: Array | null 15 | nft_token: string 16 | nft_token_id: string 17 | nft_token_amount: string 18 | nft_token_properties: Array 19 | system_metadata: Record 20 | app_metadata: Record 21 | chain_id: string 22 | verifying_contract: string 23 | direction: string 24 | signature: SignatureStruct 25 | nft_type: string 26 | } 27 | 28 | export type orders_v4_nfts = { 29 | id: string 30 | maker: string 31 | taker: string 32 | expiry: string 33 | expiry_datetime: Date 34 | nonce: string 35 | erc20_token: string 36 | erc20_token_amount: string 37 | fees: Prisma.JsonValue | null 38 | nft_token: string 39 | nft_token_id: string 40 | nft_token_amount: string 41 | nft_token_properties: Prisma.JsonValue | null 42 | system_metadata: Prisma.JsonValue | null 43 | app_metadata: Prisma.JsonValue | null 44 | chain_id: string 45 | verifying_contract: string 46 | direction: string 47 | signature: Prisma.JsonValue 48 | nft_type: string 49 | date_added: Date | null 50 | date_last_updated: Date | null 51 | } 52 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | // -------------------------- 2 | /// Order types from 0x protocol 3 | // -------------------------- 4 | import type { BigNumber, BigNumberish } from '@ethersproject/bignumber' 5 | import type { BytesLike } from '@ethersproject/bytes' 6 | import type { Prisma } from '@prisma/client' 7 | 8 | // export type FeeStruct = { 9 | // recipient: string; 10 | // amount: BigNumberish; 11 | // feeData: string | Array; 12 | // }; 13 | 14 | export type FeeStructSerialized = { 15 | recipient: string 16 | amount: string 17 | feeData: string 18 | } 19 | 20 | // export type PropertyStruct = { 21 | // propertyValidator: string; 22 | // propertyData: string | Array; 23 | // }; 24 | 25 | export type PropertyStructSerialized = { 26 | propertyValidator: string 27 | propertyData: string | Array 28 | } 29 | 30 | // export type ERC1155OrderStruct = { 31 | // direction: BigNumberish; 32 | // maker: string; 33 | // taker: string; 34 | // expiry: BigNumberish; 35 | // nonce: BigNumberish; 36 | // erc20Token: string; 37 | // erc20TokenAmount: BigNumberish; 38 | // fees: FeeStruct[]; 39 | // erc1155Token: string; 40 | // erc1155TokenId: BigNumberish; 41 | // erc1155TokenProperties: PropertyStruct[]; 42 | // erc1155TokenAmount: BigNumberish; 43 | // }; 44 | 45 | export type ERC1155OrderStructSerialized = { 46 | direction: number 47 | maker: string 48 | taker: string 49 | expiry: string 50 | nonce: string 51 | erc20Token: string 52 | erc20TokenAmount: string 53 | fees: FeeStructSerialized[] 54 | erc1155Token: string 55 | erc1155TokenId: string 56 | erc1155TokenProperties: PropertyStructSerialized[] 57 | erc1155TokenAmount: string 58 | } 59 | 60 | // export type ERC721OrderStruct = { 61 | // direction: BigNumberish; 62 | // maker: string; 63 | // taker: string; 64 | // expiry: BigNumberish; 65 | // nonce: BigNumberish; 66 | // erc20Token: string; 67 | // erc20TokenAmount: BigNumberish; 68 | // fees: FeeStruct[]; 69 | // erc721Token: string; 70 | // erc721TokenId: BigNumberish; 71 | // erc721TokenProperties: PropertyStruct[]; 72 | // }; 73 | 74 | export type ERC721OrderStructSerialized = { 75 | direction: number 76 | maker: string 77 | taker: string 78 | expiry: string 79 | nonce: string 80 | erc20Token: string 81 | erc20TokenAmount: string 82 | fees: FeeStructSerialized[] 83 | erc721Token: string 84 | erc721TokenId: string 85 | erc721TokenProperties: PropertyStructSerialized[] 86 | } 87 | 88 | // export type UserFacingFeeStruct = { 89 | // recipient: string; 90 | // amount: BigNumberish; 91 | // // Make fee data optional for devx (most folks don't use the feeData arg and it _needs_ to be '0x' if not being used). 92 | // // automatically defaults to '0x' 93 | // feeData?: BytesLike; 94 | // }; 95 | 96 | // export interface OrderStructOptionsCommon { 97 | // direction: BigNumberish; 98 | // maker: string; 99 | // taker: string; 100 | // expiry: Date | number; 101 | // nonce: BigNumberish; 102 | // // erc20Token: string; 103 | // // erc20TokenAmount: BigNumberish; 104 | // fees: UserFacingFeeStruct[]; 105 | // tokenProperties: PropertyStruct[]; 106 | // } 107 | 108 | // export interface OrderStructOptionsCommonStrict { 109 | // direction: BigNumberish; 110 | // // erc20Token: string; 111 | // // erc20TokenAmount: BigNumberish; 112 | // maker: string; 113 | // taker?: string; 114 | // expiry?: Date | number; 115 | // nonce?: BigNumberish; 116 | // fees?: UserFacingFeeStruct[]; 117 | // tokenProperties?: PropertyStruct[]; 118 | // } 119 | 120 | // export interface Fee { 121 | // recipient: string; 122 | // amount: BigNumber; 123 | // feeData: string; 124 | // } 125 | 126 | // export interface Property { 127 | // propertyValidator: string; 128 | // propertyData: string; 129 | // } 130 | 131 | // export type NftOrderV4 = ERC1155OrderStruct | ERC721OrderStruct; 132 | 133 | export type NftOrderV4Serialized = ERC1155OrderStructSerialized | ERC721OrderStructSerialized 134 | 135 | // export interface SignedERC721OrderStruct extends ERC721OrderStruct { 136 | // signature: SignatureStruct; 137 | // } 138 | 139 | // export interface SignedERC1155OrderStruct extends ERC1155OrderStruct { 140 | // signature: SignatureStruct; 141 | // } 142 | 143 | export interface SignedERC721OrderStructSerialized extends ERC721OrderStructSerialized { 144 | signature: SignatureStructSerialized 145 | } 146 | 147 | export interface SignedERC1155OrderStructSerialized extends ERC1155OrderStructSerialized { 148 | signature: SignatureStructSerialized 149 | } 150 | 151 | // export type SignedNftOrderV4 = 152 | // | SignedERC721OrderStruct 153 | // | SignedERC1155OrderStruct; 154 | 155 | export type SignedNftOrderV4Serialized = SignedERC721OrderStructSerialized | SignedERC1155OrderStructSerialized 156 | 157 | export type ECSignature = { 158 | v: number 159 | r: string 160 | s: string 161 | } 162 | 163 | export type SignatureStruct = { 164 | signatureType: number // 2 for EIP-712 165 | v: number 166 | r: string 167 | s: string 168 | } 169 | 170 | export type SignatureStructSerialized = { 171 | signatureType: number // 2 for EIP-712 172 | v: number 173 | r: string 174 | s: string 175 | } 176 | 177 | // -------------------------- 178 | // Order types from database/prisma 179 | // -------------------------- 180 | 181 | // /** 182 | // * From https://github.com/sindresorhus/type-fest/ 183 | // * Matches a JSON object. 184 | // * This type can be useful to enforce some input to be JSON-compatible or as a super-type to be extended from. 185 | // */ 186 | // export type JsonObject = { [Key in string]?: JsonValue } 187 | 188 | // /** 189 | // * From https://github.com/sindresorhus/type-fest/ 190 | // * Matches a JSON array. 191 | // */ 192 | // export interface JsonArray extends Array {} 193 | 194 | // /** 195 | // * From https://github.com/sindresorhus/type-fest/ 196 | // * Matches any valid JSON value. 197 | // */ 198 | // export type JsonValue = string | number | boolean | JsonObject | JsonArray 199 | 200 | export interface NftOrderV4DatabaseModelZod { 201 | id: string 202 | maker: string 203 | taker: string 204 | expiry: string 205 | expiry_datetime: Date 206 | nonce: string 207 | erc20_token: string 208 | erc20_token_amount: string 209 | fees: Array 210 | nft_token: string 211 | nft_token_id: string 212 | nft_token_amount: string 213 | nft_token_properties: Array 214 | system_metadata?: any //Record 215 | app_metadata?: any // Record 216 | chain_id: string 217 | verifying_contract: string 218 | direction: string 219 | signature: SignatureStruct 220 | nft_type: string 221 | date_added: Date 222 | date_last_updated: Date 223 | } 224 | -------------------------------------------------------------------------------- /src/validations.ts: -------------------------------------------------------------------------------- 1 | // Generated by ts-to-zod 2 | import { z } from 'zod' 3 | 4 | export const feeStructSerializedSchema = z.object({ 5 | recipient: z.string(), 6 | amount: z.string(), 7 | feeData: z.string(), 8 | }) 9 | 10 | export const propertyStructSerializedSchema = z.object({ 11 | propertyValidator: z.string(), 12 | propertyData: z.union([z.string(), z.array(z.number())]), 13 | }) 14 | 15 | export const eRC1155OrderStructSerializedSchema = z.object({ 16 | direction: z.number(), 17 | maker: z.string(), 18 | taker: z.string(), 19 | expiry: z.string(), 20 | nonce: z.string(), 21 | erc20Token: z.string(), 22 | erc20TokenAmount: z.string(), 23 | fees: z.array(feeStructSerializedSchema), 24 | erc1155Token: z.string(), 25 | erc1155TokenId: z.string(), 26 | erc1155TokenProperties: z.array(propertyStructSerializedSchema), 27 | erc1155TokenAmount: z.string(), 28 | }) 29 | 30 | export const eRC721OrderStructSerializedSchema = z.object({ 31 | direction: z.number(), 32 | maker: z.string(), 33 | taker: z.string(), 34 | expiry: z.string(), 35 | nonce: z.string(), 36 | erc20Token: z.string(), 37 | erc20TokenAmount: z.string(), 38 | fees: z.array(feeStructSerializedSchema), 39 | erc721Token: z.string(), 40 | erc721TokenId: z.string(), 41 | erc721TokenProperties: z.array(propertyStructSerializedSchema), 42 | }) 43 | 44 | export const nftOrderV4SerializedSchema = z.union([ 45 | eRC1155OrderStructSerializedSchema, 46 | eRC721OrderStructSerializedSchema, 47 | ]) 48 | 49 | export const eCSignatureSchema = z.object({ 50 | v: z.number(), 51 | r: z.string(), 52 | s: z.string(), 53 | }) 54 | 55 | export const signatureStructSchema = z.object({ 56 | signatureType: z.number(), 57 | v: z.number(), 58 | r: z.string(), 59 | s: z.string(), 60 | }) 61 | 62 | export const signatureStructSerializedSchema = z.object({ 63 | signatureType: z.number(), 64 | v: z.number(), 65 | r: z.string(), 66 | s: z.string(), 67 | }) 68 | 69 | export const nftOrderV4DatabaseModelZodSchema = z.object({ 70 | id: z.string(), 71 | maker: z.string(), 72 | taker: z.string(), 73 | expiry: z.string(), 74 | expiry_datetime: z.date(), 75 | nonce: z.string(), 76 | erc20_token: z.string(), 77 | erc20_token_amount: z.string(), 78 | fees: z.array(feeStructSerializedSchema), 79 | nft_token: z.string(), 80 | nft_token_id: z.string(), 81 | nft_token_amount: z.string(), 82 | nft_token_properties: z.array(propertyStructSerializedSchema), 83 | system_metadata: z.any().optional(), 84 | app_metadata: z.any().optional(), 85 | chain_id: z.string(), 86 | verifying_contract: z.string(), 87 | direction: z.string(), 88 | signature: signatureStructSchema, 89 | nft_type: z.string(), 90 | date_added: z.date(), 91 | date_last_updated: z.date(), 92 | }) 93 | 94 | export const signedERC721OrderStructSerializedSchema = eRC721OrderStructSerializedSchema.extend({ 95 | signature: signatureStructSerializedSchema, 96 | }) 97 | 98 | export const signedERC1155OrderStructSerializedSchema = eRC1155OrderStructSerializedSchema.extend({ 99 | signature: signatureStructSerializedSchema, 100 | }) 101 | 102 | export const signedNftOrderV4SerializedSchema = z.union([ 103 | signedERC721OrderStructSerializedSchema, 104 | signedERC1155OrderStructSerializedSchema, 105 | ]) 106 | -------------------------------------------------------------------------------- /test/foo.test.ts: -------------------------------------------------------------------------------- 1 | describe('testsuite', () => { 2 | it('should foobar', async () => { 3 | expect(1).toBe(1) 4 | }) 5 | }) -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "commonjs", 5 | "outDir": "dist", 6 | "rootDir": "src", 7 | "strict": true, 8 | "noImplicitAny": false, 9 | "moduleResolution": "node", 10 | "allowSyntheticDefaultImports": true, 11 | "esModuleInterop": true, 12 | "lib": [ 13 | "esnext", 14 | "es2020" 15 | ], 16 | "allowJs": true, 17 | "skipLibCheck": true, 18 | "forceConsistentCasingInFileNames": true, 19 | "resolveJsonModule": true, 20 | "noUnusedParameters": false, 21 | }, 22 | "exclude": [ 23 | "client", 24 | "migrations", 25 | "dist" 26 | ], 27 | "include": ["src/**/*.ts"] 28 | } --------------------------------------------------------------------------------