├── query-gateway ├── keys │ └── .keep ├── config │ └── gateway-config.yml └── scripts │ └── checkKey.js ├── src ├── model │ ├── index.ts │ └── generated │ │ ├── index.ts │ │ ├── transfer.model.ts │ │ └── marshal.ts ├── bsc │ ├── main.ts │ └── processor.ts ├── eth │ ├── main.ts │ └── processor.ts └── abi │ ├── erc20.ts │ ├── abi.support.ts │ ├── erc20.abi.ts │ └── multicall.ts ├── .gitignore ├── .dockerignore ├── .env ├── assets └── README.MD ├── schema.graphql ├── abi ├── README.md └── erc20.json ├── tsconfig.json ├── renovate.json ├── squid.yaml ├── package.json ├── docker-compose.yml ├── LICENSE ├── .gitpod.yml ├── db └── migrations │ └── 1687967108702-Data.js ├── commands.json └── README.md /query-gateway/keys/.keep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/model/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./generated" 2 | -------------------------------------------------------------------------------- /src/model/generated/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./transfer.model" 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /lib 3 | /builds 4 | 5 | /**Versions.json 6 | 7 | # IDE files 8 | /.idea 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | /.git 2 | /node_modules 3 | /lib 4 | /*Versions.json 5 | npm-debug.log 6 | 7 | # OS Files 8 | .DS_Store 9 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | DB_NAME=squid 2 | DB_PORT=23798 3 | GQL_PORT=4350 4 | # JSON-RPC node endpoint, both wss and https endpoints are accepted 5 | RPC_ENDPOINT= 6 | -------------------------------------------------------------------------------- /assets/README.MD: -------------------------------------------------------------------------------- 1 | # Assets 2 | 3 | `assets` is the designated folder for any additional files to be used by the squid, for example a static data file. The folder is added by default to `Dockerfile` and is kept when the squid is deployed to the Aquairum. -------------------------------------------------------------------------------- /query-gateway/config/gateway-config.yml: -------------------------------------------------------------------------------- 1 | scheduler_id: "12D3KooWQER7HEpwsvqSzqzaiV36d3Bn6DZrnwEunnzS76pgZkMU" 2 | send_metrics: true 3 | available_datasets: 4 | ethereum-mainnet: "czM6Ly9ldGhlcmV1bS1tYWlubmV0LTE" 5 | binance-mainnet: "czM6Ly9ic2MtbWFpbm5ldC0x" 6 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | type Transfer @entity { 2 | id: ID! 3 | network: String! @index 4 | block: Int! @index 5 | timestamp: DateTime! @index 6 | from: String! @index 7 | to: String! @index 8 | value: BigInt! 9 | txHash: String! @index 10 | } 11 | -------------------------------------------------------------------------------- /query-gateway/scripts/checkKey.js: -------------------------------------------------------------------------------- 1 | const fs = require('fs') 2 | const keyPath = process.argv[2] 3 | if (!keyPath || !fs.existsSync(keyPath) || !fs.lstatSync(keyPath).isFile()) { 4 | console.error(`ERROR: Key file not found at ${keyPath}`) 5 | process.exit(1) 6 | } 7 | -------------------------------------------------------------------------------- /abi/README.md: -------------------------------------------------------------------------------- 1 | # ABI folder 2 | 3 | This is a dedicated folder for ABI files. Place you contract ABI here and generate facade classes for type-safe decoding of the event, function data and contract state queries with 4 | 5 | ```sh 6 | sqd typegen 7 | ``` 8 | 9 | This `typegen` command is defined in `commands.json`. 10 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "target": "es2020", 5 | "outDir": "lib", 6 | "rootDir": "src", 7 | "strict": true, 8 | "declaration": false, 9 | "sourceMap": true, 10 | "esModuleInterop": true, 11 | "experimentalDecorators": true, 12 | "emitDecoratorMetadata": true, 13 | "skipLibCheck": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": ["src"], 17 | "exclude": [ 18 | "node_modules" 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ], 6 | "packageRules": [ 7 | { 8 | "groupName": "@subsquid", 9 | "matchPackagePatterns": [ 10 | "^@subsquid/" 11 | ], 12 | "matchUpdateTypes": [ 13 | "minor", 14 | "patch", 15 | "pin", 16 | "digest" 17 | ] 18 | }, 19 | { 20 | "matchPackagePatterns": ["*"], 21 | "excludePackagePatterns": ["^@subsquid/"], 22 | "enabled": false 23 | } 24 | ], 25 | "automerge": true, 26 | "automergeType": "pr", 27 | "automergeStrategy": "squash", 28 | "ignoreTests": true 29 | } -------------------------------------------------------------------------------- /squid.yaml: -------------------------------------------------------------------------------- 1 | manifestVersion: subsquid.io/v0.1 2 | name: my-awesome-mc-squid 3 | version: 1 4 | description: A squid that indexes USDC transfers on ETH and BSC 5 | build: 6 | deploy: 7 | addons: 8 | postgres: 9 | processor: 10 | - name: eth-processor 11 | cmd: 12 | - node 13 | - lib/eth/main 14 | - name: bsc-processor 15 | cmd: 16 | - node 17 | - lib/bsc/main 18 | api: 19 | cmd: 20 | - npx 21 | - squid-graphql-server 22 | - '--dumb-cache' 23 | - in-memory 24 | - '--dumb-cache-ttl' 25 | - '1000' 26 | - '--dumb-cache-size' 27 | - '100' 28 | - '--dumb-cache-max-age' 29 | - '1000' 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "squid-evm-template", 3 | "private": true, 4 | "scripts": { 5 | "build": "rm -rf lib && tsc" 6 | }, 7 | "dependencies": { 8 | "@subsquid/archive-registry": "^3.2.0", 9 | "@subsquid/evm-processor": "^1.8.2", 10 | "@subsquid/graphql-server": "^4.3.0", 11 | "@subsquid/typeorm-migration": "^1.2.1", 12 | "@subsquid/typeorm-store": "^1.2.2", 13 | "dotenv": "^16.1.4", 14 | "ethers": "^6.5.1", 15 | "pg": "^8.11.0", 16 | "typeorm": "^0.3.16" 17 | }, 18 | "devDependencies": { 19 | "@subsquid/evm-typegen": "^3.2.2", 20 | "@subsquid/typeorm-codegen": "^1.3.1", 21 | "@types/node": "^18.16.17", 22 | "typescript": "~5.1.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/model/generated/transfer.model.ts: -------------------------------------------------------------------------------- 1 | import {Entity as Entity_, Column as Column_, PrimaryColumn as PrimaryColumn_, Index as Index_} from "typeorm" 2 | import * as marshal from "./marshal" 3 | 4 | @Entity_() 5 | export class Transfer { 6 | constructor(props?: Partial) { 7 | Object.assign(this, props) 8 | } 9 | 10 | @PrimaryColumn_() 11 | id!: string 12 | 13 | @Index_() 14 | @Column_("text", {nullable: false}) 15 | network!: string 16 | 17 | @Index_() 18 | @Column_("int4", {nullable: false}) 19 | block!: number 20 | 21 | @Index_() 22 | @Column_("timestamp with time zone", {nullable: false}) 23 | timestamp!: Date 24 | 25 | @Index_() 26 | @Column_("text", {nullable: false}) 27 | from!: string 28 | 29 | @Index_() 30 | @Column_("text", {nullable: false}) 31 | to!: string 32 | 33 | @Column_("numeric", {transformer: marshal.bigintTransformer, nullable: false}) 34 | value!: bigint 35 | 36 | @Index_() 37 | @Column_("text", {nullable: false}) 38 | txHash!: string 39 | } 40 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3" 2 | 3 | services: 4 | db: 5 | image: postgres:15 6 | environment: 7 | POSTGRES_DB: squid 8 | POSTGRES_PASSWORD: postgres 9 | ports: 10 | - "${DB_PORT}:5432" 11 | # command: ["postgres", "-c", "log_statement=all"] 12 | shm_size: 1gb 13 | query-gateway: 14 | image: subsquid/query-gateway:0.1.9 15 | environment: 16 | BOOTSTRAP: "true" 17 | HTTP_LISTEN_ADDR: 0.0.0.0:8000 18 | BOOT_NODES: > 19 | 12D3KooWSRvKpvNbsrGbLXGFZV7GYdcrYNh4W2nipwHHMYikzV58 /dns4/testnet.subsquid.io/tcp/22345, 20 | 12D3KooWQC9tPzj2ShLn39RFHS5SGbvbP2pEd7bJ61kSW2LwxGSB /dns4/testnet.subsquid.io/tcp/22346, 21 | 12D3KooWQER7HEpwsvqSzqzaiV36d3Bn6DZrnwEunnzS76pgZkMU /dns4/testnet.subsquid.io/tcp/32345 22 | RPC_URL: https://arbitrum-goerli.public.blastapi.io 23 | KEY_PATH: /query-gateway/keys/doubleProc.key 24 | CONFIG_PATH: /query-gateway/config/gateway-config.yml 25 | RUST_LOG: info,query_gateway=debug,ethers_providers=warn 26 | volumes: 27 | - ./query-gateway:/query-gateway 28 | ports: 29 | - "8000:8000" 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Subsquid Labs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/bsc/main.ts: -------------------------------------------------------------------------------- 1 | import {TypeormDatabase} from '@subsquid/typeorm-store' 2 | import {Transfer} from '../model' 3 | import * as erc20abi from '../abi/erc20' 4 | import {processor, BSC_USDC_ADDRESS} from './processor' 5 | 6 | processor.run(new TypeormDatabase({supportHotBlocks: true, stateSchema: 'bsc_processor'}), async (ctx) => { 7 | const transfers: Transfer[] = [] 8 | for (let c of ctx.blocks) { 9 | for (let log of c.logs) { 10 | if (log.address !== BSC_USDC_ADDRESS || log.topics[0] !== erc20abi.events.Transfer.topic) continue 11 | let {from, to, value} = erc20abi.events.Transfer.decode(log) 12 | transfers.push( 13 | new Transfer({ 14 | id: log.id, 15 | network: 'bsc', 16 | block: c.header.height, 17 | timestamp: new Date(c.header.timestamp), 18 | from, 19 | to, 20 | value, 21 | txHash: log.transactionHash 22 | }) 23 | ) 24 | } 25 | } 26 | await ctx.store.upsert(transfers) 27 | }) 28 | -------------------------------------------------------------------------------- /src/eth/main.ts: -------------------------------------------------------------------------------- 1 | import {TypeormDatabase} from '@subsquid/typeorm-store' 2 | import {Transfer} from '../model' 3 | import * as erc20abi from '../abi/erc20' 4 | import {processor, ETH_USDC_ADDRESS} from './processor' 5 | 6 | processor.run(new TypeormDatabase({supportHotBlocks: true, stateSchema: 'eth_processor'}), async (ctx) => { 7 | const transfers: Transfer[] = [] 8 | for (let c of ctx.blocks) { 9 | for (let log of c.logs) { 10 | if (log.address !== ETH_USDC_ADDRESS || log.topics[0] !== erc20abi.events.Transfer.topic) continue 11 | let {from, to, value} = erc20abi.events.Transfer.decode(log) 12 | transfers.push( 13 | new Transfer({ 14 | id: log.id, 15 | network: 'eth', 16 | block: c.header.height, 17 | timestamp: new Date(c.header.timestamp), 18 | from, 19 | to, 20 | value, 21 | txHash: log.transactionHash 22 | }) 23 | ) 24 | } 25 | } 26 | await ctx.store.upsert(transfers) 27 | }) 28 | -------------------------------------------------------------------------------- /src/bsc/processor.ts: -------------------------------------------------------------------------------- 1 | import {lookupArchive} from '@subsquid/archive-registry' 2 | import { 3 | BlockHeader, 4 | DataHandlerContext, 5 | EvmBatchProcessor, 6 | EvmBatchProcessorFields, 7 | Log as _Log, 8 | Transaction as _Transaction, 9 | } from '@subsquid/evm-processor' 10 | import {Store} from '@subsquid/typeorm-store' 11 | import * as erc20abi from '../abi/erc20' 12 | 13 | export const BSC_USDC_ADDRESS = '0x8965349fb649A33a30cbFDa057D8eC2C48AbE2A2'.toLowerCase() 14 | 15 | export const processor = new EvmBatchProcessor() 16 | .setDataSource({ 17 | archive: 'http://localhost:8000/network/binance-mainnet', 18 | // Disabled for quests to avoid DDoSing Ankr :) 19 | //chain: 'https://rpc.ankr.com/bsc', 20 | }) 21 | .setFinalityConfirmation(75) 22 | .setFields({ 23 | log: { 24 | transactionHash: true 25 | } 26 | }) 27 | .setBlockRange({ 28 | from: 28_000_000, 29 | }) 30 | .addLog({ 31 | address: [BSC_USDC_ADDRESS], 32 | topic0: [erc20abi.events.Transfer.topic] 33 | }) 34 | 35 | export type Fields = EvmBatchProcessorFields 36 | export type Context = DataHandlerContext 37 | export type Block = BlockHeader 38 | export type Log = _Log 39 | export type Transaction = _Transaction 40 | -------------------------------------------------------------------------------- /src/eth/processor.ts: -------------------------------------------------------------------------------- 1 | import {lookupArchive} from '@subsquid/archive-registry' 2 | import { 3 | BlockHeader, 4 | DataHandlerContext, 5 | EvmBatchProcessor, 6 | EvmBatchProcessorFields, 7 | Log as _Log, 8 | Transaction as _Transaction, 9 | } from '@subsquid/evm-processor' 10 | import {Store} from '@subsquid/typeorm-store' 11 | import * as erc20abi from '../abi/erc20' 12 | 13 | export const ETH_USDC_ADDRESS = '0x7EA2be2df7BA6E54B1A9C70676f668455E329d29'.toLowerCase() 14 | 15 | export const processor = new EvmBatchProcessor() 16 | .setDataSource({ 17 | archive: 'http://localhost:8000/network/ethereum-mainnet', 18 | // Disabled for quests to avoid DDoSing Ankr :) 19 | //chain: 'https://rpc.ankr.com/eth', 20 | }) 21 | .setFinalityConfirmation(75) 22 | .setFields({ 23 | log: { 24 | transactionHash: true 25 | } 26 | }) 27 | .setBlockRange({ 28 | from: 16_000_000, 29 | }) 30 | .addLog({ 31 | address: [ETH_USDC_ADDRESS], 32 | topic0: [erc20abi.events.Transfer.topic] 33 | }) 34 | 35 | export type Fields = EvmBatchProcessorFields 36 | export type Context = DataHandlerContext 37 | export type Block = BlockHeader 38 | export type Log = _Log 39 | export type Transaction = _Transaction 40 | -------------------------------------------------------------------------------- /.gitpod.yml: -------------------------------------------------------------------------------- 1 | # This configuration file was automatically generated by Gitpod. 2 | # Please adjust to your needs (see https://www.gitpod.io/docs/introduction/learn-gitpod/gitpod-yaml) 3 | # and commit this file to your remote git repository to share the goodness with others. 4 | 5 | # Learn more from ready-to-use templates: https://www.gitpod.io/docs/introduction/getting-started/quickstart 6 | github: 7 | prebuilds: 8 | # enable for the master/default branch (defaults to true) 9 | master: true 10 | # enable for all branches in this repo (defaults to false) 11 | branches: false 12 | # enable for pull requests coming from this repo (defaults to true) 13 | pullRequests: true 14 | # add a check to pull requests (defaults to true) 15 | addCheck: true 16 | # add a "Review in Gitpod" button as a comment to pull requests (defaults to false) 17 | addComment: false 18 | 19 | tasks: 20 | - init: | 21 | npm i 22 | npm i -g @subsquid/cli 23 | docker compose pull 24 | gp sync-done setup 25 | - name: DB 26 | command: | 27 | gp sync-await setup 28 | sqd up 29 | - name: GraphQL API 30 | command: | 31 | gp sync-await setup 32 | sqd serve 33 | - command: | 34 | gp ports await 4350 35 | gp preview $(gp url 4350)/graphql 36 | - name: Squid procesor 37 | command: | 38 | gp open src/processor.ts 39 | gp sync-await setup 40 | sqd build 41 | gp ports await 23798 42 | sqd process 43 | -------------------------------------------------------------------------------- /db/migrations/1687967108702-Data.js: -------------------------------------------------------------------------------- 1 | module.exports = class Data1687967108702 { 2 | name = 'Data1687967108702' 3 | 4 | async up(db) { 5 | await db.query(`CREATE TABLE "transfer" ("id" character varying NOT NULL, "network" text NOT NULL, "block" integer NOT NULL, "timestamp" TIMESTAMP WITH TIME ZONE NOT NULL, "from" text NOT NULL, "to" text NOT NULL, "value" numeric NOT NULL, "tx_hash" text NOT NULL, CONSTRAINT "PK_fd9ddbdd49a17afcbe014401295" PRIMARY KEY ("id"))`) 6 | await db.query(`CREATE INDEX "IDX_024eb30e5fd99a5bea7befe60e" ON "transfer" ("network") `) 7 | await db.query(`CREATE INDEX "IDX_c116ab40c3b32ca2d9c1d17d8b" ON "transfer" ("block") `) 8 | await db.query(`CREATE INDEX "IDX_70ff8b624c3118ac3a4862d22c" ON "transfer" ("timestamp") `) 9 | await db.query(`CREATE INDEX "IDX_be54ea276e0f665ffc38630fc0" ON "transfer" ("from") `) 10 | await db.query(`CREATE INDEX "IDX_4cbc37e8c3b47ded161f44c24f" ON "transfer" ("to") `) 11 | await db.query(`CREATE INDEX "IDX_f605a03972b4f28db27a0ee70d" ON "transfer" ("tx_hash") `) 12 | } 13 | 14 | async down(db) { 15 | await db.query(`DROP TABLE "transfer"`) 16 | await db.query(`DROP INDEX "public"."IDX_024eb30e5fd99a5bea7befe60e"`) 17 | await db.query(`DROP INDEX "public"."IDX_c116ab40c3b32ca2d9c1d17d8b"`) 18 | await db.query(`DROP INDEX "public"."IDX_70ff8b624c3118ac3a4862d22c"`) 19 | await db.query(`DROP INDEX "public"."IDX_be54ea276e0f665ffc38630fc0"`) 20 | await db.query(`DROP INDEX "public"."IDX_4cbc37e8c3b47ded161f44c24f"`) 21 | await db.query(`DROP INDEX "public"."IDX_f605a03972b4f28db27a0ee70d"`) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/abi/erc20.ts: -------------------------------------------------------------------------------- 1 | import * as ethers from 'ethers' 2 | import {LogEvent, Func, ContractBase} from './abi.support' 3 | import {ABI_JSON} from './erc20.abi' 4 | 5 | export const abi = new ethers.Interface(ABI_JSON); 6 | 7 | export const events = { 8 | Approval: new LogEvent<([owner: string, spender: string, value: bigint] & {owner: string, spender: string, value: bigint})>( 9 | abi, '0x8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925' 10 | ), 11 | Transfer: new LogEvent<([from: string, to: string, value: bigint] & {from: string, to: string, value: bigint})>( 12 | abi, '0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef' 13 | ), 14 | } 15 | 16 | export const functions = { 17 | name: new Func<[], {}, string>( 18 | abi, '0x06fdde03' 19 | ), 20 | approve: new Func<[_spender: string, _value: bigint], {_spender: string, _value: bigint}, boolean>( 21 | abi, '0x095ea7b3' 22 | ), 23 | totalSupply: new Func<[], {}, bigint>( 24 | abi, '0x18160ddd' 25 | ), 26 | transferFrom: new Func<[_from: string, _to: string, _value: bigint], {_from: string, _to: string, _value: bigint}, boolean>( 27 | abi, '0x23b872dd' 28 | ), 29 | decimals: new Func<[], {}, number>( 30 | abi, '0x313ce567' 31 | ), 32 | balanceOf: new Func<[_owner: string], {_owner: string}, bigint>( 33 | abi, '0x70a08231' 34 | ), 35 | symbol: new Func<[], {}, string>( 36 | abi, '0x95d89b41' 37 | ), 38 | transfer: new Func<[_to: string, _value: bigint], {_to: string, _value: bigint}, boolean>( 39 | abi, '0xa9059cbb' 40 | ), 41 | allowance: new Func<[_owner: string, _spender: string], {_owner: string, _spender: string}, bigint>( 42 | abi, '0xdd62ed3e' 43 | ), 44 | } 45 | 46 | export class Contract extends ContractBase { 47 | 48 | name(): Promise { 49 | return this.eth_call(functions.name, []) 50 | } 51 | 52 | totalSupply(): Promise { 53 | return this.eth_call(functions.totalSupply, []) 54 | } 55 | 56 | decimals(): Promise { 57 | return this.eth_call(functions.decimals, []) 58 | } 59 | 60 | balanceOf(_owner: string): Promise { 61 | return this.eth_call(functions.balanceOf, [_owner]) 62 | } 63 | 64 | symbol(): Promise { 65 | return this.eth_call(functions.symbol, []) 66 | } 67 | 68 | allowance(_owner: string, _spender: string): Promise { 69 | return this.eth_call(functions.allowance, [_owner, _spender]) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/abi/abi.support.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | import * as ethers from 'ethers' 3 | 4 | 5 | export interface LogRecord { 6 | topics: string[] 7 | data: string 8 | } 9 | 10 | 11 | export class LogEvent { 12 | private fragment: ethers.EventFragment 13 | 14 | constructor(private abi: ethers.Interface, public readonly topic: string) { 15 | let fragment = abi.getEvent(topic) 16 | assert(fragment != null, 'Missing fragment') 17 | this.fragment = fragment 18 | } 19 | 20 | decode(rec: LogRecord): Args { 21 | return this.abi.decodeEventLog(this.fragment, rec.data, rec.topics) as any as Args 22 | } 23 | } 24 | 25 | 26 | export class Func { 27 | private fragment: ethers.FunctionFragment 28 | 29 | constructor(private abi: ethers.Interface, public readonly sighash: string) { 30 | let fragment = abi.getFunction(sighash) 31 | assert(fragment != null, 'Missing fragment') 32 | this.fragment = fragment 33 | } 34 | 35 | decode(input: ethers.BytesLike): Args & FieldArgs { 36 | return this.abi.decodeFunctionData(this.fragment, input) as any as Args & FieldArgs 37 | } 38 | 39 | encode(args: Args): string { 40 | return this.abi.encodeFunctionData(this.fragment, args) 41 | } 42 | 43 | decodeResult(output: ethers.BytesLike): Result { 44 | const decoded = this.abi.decodeFunctionResult(this.fragment, output) 45 | return decoded.length > 1 ? decoded : decoded[0] 46 | } 47 | 48 | tryDecodeResult(output: ethers.BytesLike): Result | undefined { 49 | try { 50 | return this.decodeResult(output) 51 | } catch(err: any) { 52 | return undefined 53 | } 54 | } 55 | } 56 | 57 | 58 | export function isFunctionResultDecodingError(val: unknown): val is Error & {data: string} { 59 | if (!(val instanceof Error)) return false 60 | let err = val as any 61 | return err.code == 'CALL_EXCEPTION' 62 | && typeof err.data == 'string' 63 | && !err.errorArgs 64 | && !err.errorName 65 | } 66 | 67 | 68 | export interface ChainContext { 69 | _chain: Chain 70 | } 71 | 72 | 73 | export interface BlockContext { 74 | _chain: Chain 75 | block: Block 76 | } 77 | 78 | 79 | export interface Block { 80 | height: number 81 | } 82 | 83 | 84 | export interface Chain { 85 | client: { 86 | call: (method: string, params?: unknown[]) => Promise 87 | } 88 | } 89 | 90 | 91 | export class ContractBase { 92 | private readonly _chain: Chain 93 | private readonly blockHeight: number 94 | readonly address: string 95 | 96 | constructor(ctx: BlockContext, address: string) 97 | constructor(ctx: ChainContext, block: Block, address: string) 98 | constructor(ctx: BlockContext, blockOrAddress: Block | string, address?: string) { 99 | this._chain = ctx._chain 100 | if (typeof blockOrAddress === 'string') { 101 | this.blockHeight = ctx.block.height 102 | this.address = ethers.getAddress(blockOrAddress) 103 | } else { 104 | if (address == null) { 105 | throw new Error('missing contract address') 106 | } 107 | this.blockHeight = blockOrAddress.height 108 | this.address = ethers.getAddress(address) 109 | } 110 | } 111 | 112 | async eth_call(func: Func, args: Args): Promise { 113 | let data = func.encode(args) 114 | let result = await this._chain.client.call('eth_call', [ 115 | {to: this.address, data}, 116 | '0x'+this.blockHeight.toString(16) 117 | ]) 118 | return func.decodeResult(result) 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /commands.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://cdn.subsquid.io/schemas/commands.json", 3 | "commands": { 4 | "clean": { 5 | "description": "delete all build artifacts", 6 | "cmd": ["npx", "--yes", "rimraf", "lib"] 7 | }, 8 | "build": { 9 | "description": "Build the squid project", 10 | "deps": ["clean"], 11 | "cmd": ["tsc"] 12 | }, 13 | "check-key": { 14 | "cmd": ["node", "query-gateway/scripts/checkKey.js", "query-gateway/keys/doubleProc.key"], 15 | "hidden": true 16 | }, 17 | "up": { 18 | "deps": ["check-key"], 19 | "description": "Start a PG database", 20 | "cmd": ["docker", "compose", "up", "-d"] 21 | }, 22 | "down": { 23 | "description": "Drop a PG database", 24 | "cmd": ["docker", "compose", "down"] 25 | }, 26 | "migration:apply": { 27 | "description": "Apply the DB migrations", 28 | "cmd": ["squid-typeorm-migration", "apply"] 29 | }, 30 | "migration:generate": { 31 | "description": "Generate a DB migration matching the TypeORM entities", 32 | "deps": ["build", "migration:clean"], 33 | "cmd": ["squid-typeorm-migration", "generate"] 34 | }, 35 | "migration:clean": { 36 | "description": "Clean the migrations folder", 37 | "cmd": ["npx", "--yes", "rimraf", "./db/migrations"] 38 | }, 39 | "migration": { 40 | "deps": ["build"], 41 | "cmd": ["squid-typeorm-migration", "generate"], 42 | "hidden": true 43 | }, 44 | "codegen": { 45 | "description": "Generate TypeORM entities from the schema file", 46 | "cmd": ["squid-typeorm-codegen"] 47 | }, 48 | "typegen": { 49 | "description": "Generate data access classes for an ABI file(s) in the ./abi folder", 50 | "cmd": ["squid-evm-typegen", "./src/abi", {"glob": "./abi/*.json"}, "--multicall"] 51 | }, 52 | "process:eth": { 53 | "description": "Load .env and start the ETH squid processor", 54 | "deps": ["build", "migration:apply"], 55 | "cmd": ["node", "--require=dotenv/config", "lib/eth/main.js"] 56 | }, 57 | "process:bsc": { 58 | "description": "Load .env and start the BSC squid processor", 59 | "deps": ["build", "migration:apply"], 60 | "cmd": ["node", "--require=dotenv/config", "lib/bsc/main.js"] 61 | }, 62 | "process:prod:eth": { 63 | "description": "Start the squid processor", 64 | "deps": ["migration:apply"], 65 | "cmd": ["node", "lib/eth/main.js"], 66 | "hidden": true 67 | }, 68 | "process:prod:bsc": { 69 | "description": "Start the squid processor", 70 | "deps": ["migration:apply"], 71 | "cmd": ["node", "lib/bsc/main.js"], 72 | "hidden": true 73 | }, 74 | "serve": { 75 | "description": "Start the GraphQL API server", 76 | "cmd": ["squid-graphql-server"] 77 | }, 78 | "serve:prod": { 79 | "description": "Start the GraphQL API server with caching and limits", 80 | "cmd": ["squid-graphql-server", 81 | "--dumb-cache", "in-memory", 82 | "--dumb-cache-ttl", "1000", 83 | "--dumb-cache-size", "100", 84 | "--dumb-cache-max-age", "1000" ] 85 | }, 86 | "check-updates": { 87 | "cmd": ["npx", "--yes", "npm-check-updates", "--filter=/subsquid/", "--upgrade"], 88 | "hidden": true 89 | }, 90 | "bump": { 91 | "description": "Bump @subsquid packages to the latest versions", 92 | "deps": ["check-updates"], 93 | "cmd": ["npm", "i", "-f"] 94 | }, 95 | "open": { 96 | "description": "Open a local browser window", 97 | "cmd": ["npx", "--yes", "opener"] 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/model/generated/marshal.ts: -------------------------------------------------------------------------------- 1 | import assert from 'assert' 2 | 3 | 4 | export interface Marshal { 5 | fromJSON(value: unknown): T 6 | toJSON(value: T): S 7 | } 8 | 9 | 10 | export const string: Marshal = { 11 | fromJSON(value: unknown): string { 12 | assert(typeof value === 'string', 'invalid String') 13 | return value 14 | }, 15 | toJSON(value) { 16 | return value 17 | } 18 | } 19 | 20 | 21 | export const id = string 22 | 23 | 24 | export const int: Marshal = { 25 | fromJSON(value: unknown): number { 26 | assert(Number.isInteger(value), 'invalid Int') 27 | return value as number 28 | }, 29 | toJSON(value) { 30 | return value 31 | } 32 | } 33 | 34 | 35 | export const float: Marshal = { 36 | fromJSON(value: unknown): number { 37 | assert(typeof value === 'number', 'invalid Float') 38 | return value as number 39 | }, 40 | toJSON(value) { 41 | return value 42 | } 43 | } 44 | 45 | 46 | export const boolean: Marshal = { 47 | fromJSON(value: unknown): boolean { 48 | assert(typeof value === 'boolean', 'invalid Boolean') 49 | return value 50 | }, 51 | toJSON(value: boolean): boolean { 52 | return value 53 | } 54 | } 55 | 56 | 57 | export const bigint: Marshal = { 58 | fromJSON(value: unknown): bigint { 59 | assert(typeof value === 'string', 'invalid BigInt') 60 | return BigInt(value) 61 | }, 62 | toJSON(value: bigint): string { 63 | return value.toString() 64 | } 65 | } 66 | 67 | 68 | export const bigdecimal: Marshal = { 69 | fromJSON(value: unknown): bigint { 70 | assert(typeof value === 'string', 'invalid BigDecimal') 71 | return decimal.BigDecimal(value) 72 | }, 73 | toJSON(value: any): string { 74 | return value.toString() 75 | } 76 | } 77 | 78 | 79 | // credit - https://github.com/Urigo/graphql-scalars/blob/91b4ea8df891be8af7904cf84751930cc0c6613d/src/scalars/iso-date/validator.ts#L122 80 | const RFC_3339_REGEX = 81 | /^(\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])T([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60))(\.\d{1,})?([Z])$/ 82 | 83 | 84 | function isIsoDateTimeString(s: string): boolean { 85 | return RFC_3339_REGEX.test(s) 86 | } 87 | 88 | 89 | export const datetime: Marshal = { 90 | fromJSON(value: unknown): Date { 91 | assert(typeof value === 'string', 'invalid DateTime') 92 | assert(isIsoDateTimeString(value), 'invalid DateTime') 93 | return new Date(value) 94 | }, 95 | toJSON(value: Date): string { 96 | return value.toISOString() 97 | } 98 | } 99 | 100 | 101 | export const bytes: Marshal = { 102 | fromJSON(value: unknown): Buffer { 103 | assert(typeof value === 'string', 'invalid Bytes') 104 | assert(value.length % 2 === 0, 'invalid Bytes') 105 | assert(/^0x[0-9a-f]+$/i.test(value), 'invalid Bytes') 106 | return Buffer.from(value.slice(2), 'hex') 107 | }, 108 | toJSON(value: Uint8Array): string { 109 | if (Buffer.isBuffer(value)) { 110 | return '0x' + value.toString('hex') 111 | } else { 112 | return '0x' + Buffer.from(value.buffer, value.byteOffset, value.byteLength).toString('hex') 113 | } 114 | } 115 | } 116 | 117 | 118 | export function fromList(list: unknown, f: (val: unknown) => T): T[] { 119 | assert(Array.isArray(list)) 120 | return list.map((val) => f(val)) 121 | } 122 | 123 | 124 | export function nonNull(val: T | undefined | null): T { 125 | assert(val != null, 'non-nullable value is null') 126 | return val 127 | } 128 | 129 | 130 | export const bigintTransformer = { 131 | to(x?: bigint) { 132 | return x?.toString() 133 | }, 134 | from(s?: string): bigint | undefined { 135 | return s == null ? undefined : BigInt(s) 136 | } 137 | } 138 | 139 | 140 | export const floatTransformer = { 141 | to(x?: number) { 142 | return x?.toString() 143 | }, 144 | from(s?: string): number | undefined { 145 | return s == null ? undefined : Number(s) 146 | } 147 | } 148 | 149 | 150 | export const bigdecimalTransformer = { 151 | to(x?: any) { 152 | return x?.toString() 153 | }, 154 | from(s?: any): any | undefined { 155 | return s == null ? undefined : decimal.BigDecimal(s) 156 | } 157 | } 158 | 159 | 160 | export function enumFromJson(json: unknown, enumObject: E): E[keyof E] { 161 | assert(typeof json == 'string', 'invalid enum value') 162 | let val = (enumObject as any)[json] 163 | assert(typeof val == 'string', `invalid enum value`) 164 | return val as any 165 | } 166 | 167 | 168 | const decimal = { 169 | get BigDecimal(): any { 170 | throw new Error('Package `@subsquid/big-decimal` is not installed') 171 | } 172 | } 173 | 174 | 175 | try { 176 | Object.defineProperty(decimal, "BigDecimal", { 177 | value: require('@subsquid/big-decimal').BigDecimal 178 | }) 179 | } catch (e) {} 180 | -------------------------------------------------------------------------------- /abi/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 | ] -------------------------------------------------------------------------------- /src/abi/erc20.abi.ts: -------------------------------------------------------------------------------- 1 | export const ABI_JSON = [ 2 | { 3 | "type": "function", 4 | "name": "name", 5 | "constant": true, 6 | "stateMutability": "view", 7 | "payable": false, 8 | "inputs": [], 9 | "outputs": [ 10 | { 11 | "type": "string" 12 | } 13 | ] 14 | }, 15 | { 16 | "type": "function", 17 | "name": "approve", 18 | "constant": false, 19 | "payable": false, 20 | "inputs": [ 21 | { 22 | "type": "address", 23 | "name": "_spender" 24 | }, 25 | { 26 | "type": "uint256", 27 | "name": "_value" 28 | } 29 | ], 30 | "outputs": [ 31 | { 32 | "type": "bool" 33 | } 34 | ] 35 | }, 36 | { 37 | "type": "function", 38 | "name": "totalSupply", 39 | "constant": true, 40 | "stateMutability": "view", 41 | "payable": false, 42 | "inputs": [], 43 | "outputs": [ 44 | { 45 | "type": "uint256" 46 | } 47 | ] 48 | }, 49 | { 50 | "type": "function", 51 | "name": "transferFrom", 52 | "constant": false, 53 | "payable": false, 54 | "inputs": [ 55 | { 56 | "type": "address", 57 | "name": "_from" 58 | }, 59 | { 60 | "type": "address", 61 | "name": "_to" 62 | }, 63 | { 64 | "type": "uint256", 65 | "name": "_value" 66 | } 67 | ], 68 | "outputs": [ 69 | { 70 | "type": "bool" 71 | } 72 | ] 73 | }, 74 | { 75 | "type": "function", 76 | "name": "decimals", 77 | "constant": true, 78 | "stateMutability": "view", 79 | "payable": false, 80 | "inputs": [], 81 | "outputs": [ 82 | { 83 | "type": "uint8" 84 | } 85 | ] 86 | }, 87 | { 88 | "type": "function", 89 | "name": "balanceOf", 90 | "constant": true, 91 | "stateMutability": "view", 92 | "payable": false, 93 | "inputs": [ 94 | { 95 | "type": "address", 96 | "name": "_owner" 97 | } 98 | ], 99 | "outputs": [ 100 | { 101 | "type": "uint256", 102 | "name": "balance" 103 | } 104 | ] 105 | }, 106 | { 107 | "type": "function", 108 | "name": "symbol", 109 | "constant": true, 110 | "stateMutability": "view", 111 | "payable": false, 112 | "inputs": [], 113 | "outputs": [ 114 | { 115 | "type": "string" 116 | } 117 | ] 118 | }, 119 | { 120 | "type": "function", 121 | "name": "transfer", 122 | "constant": false, 123 | "payable": false, 124 | "inputs": [ 125 | { 126 | "type": "address", 127 | "name": "_to" 128 | }, 129 | { 130 | "type": "uint256", 131 | "name": "_value" 132 | } 133 | ], 134 | "outputs": [ 135 | { 136 | "type": "bool" 137 | } 138 | ] 139 | }, 140 | { 141 | "type": "function", 142 | "name": "allowance", 143 | "constant": true, 144 | "stateMutability": "view", 145 | "payable": false, 146 | "inputs": [ 147 | { 148 | "type": "address", 149 | "name": "_owner" 150 | }, 151 | { 152 | "type": "address", 153 | "name": "_spender" 154 | } 155 | ], 156 | "outputs": [ 157 | { 158 | "type": "uint256" 159 | } 160 | ] 161 | }, 162 | { 163 | "type": "fallback", 164 | "stateMutability": "payable" 165 | }, 166 | { 167 | "type": "event", 168 | "anonymous": false, 169 | "name": "Approval", 170 | "inputs": [ 171 | { 172 | "type": "address", 173 | "name": "owner", 174 | "indexed": true 175 | }, 176 | { 177 | "type": "address", 178 | "name": "spender", 179 | "indexed": true 180 | }, 181 | { 182 | "type": "uint256", 183 | "name": "value", 184 | "indexed": false 185 | } 186 | ] 187 | }, 188 | { 189 | "type": "event", 190 | "anonymous": false, 191 | "name": "Transfer", 192 | "inputs": [ 193 | { 194 | "type": "address", 195 | "name": "from", 196 | "indexed": true 197 | }, 198 | { 199 | "type": "address", 200 | "name": "to", 201 | "indexed": true 202 | }, 203 | { 204 | "type": "uint256", 205 | "name": "value", 206 | "indexed": false 207 | } 208 | ] 209 | } 210 | ] 211 | -------------------------------------------------------------------------------- /src/abi/multicall.ts: -------------------------------------------------------------------------------- 1 | import * as ethers from 'ethers' 2 | import {ContractBase, Func} from './abi.support' 3 | 4 | 5 | const abi = new ethers.Interface([ 6 | { 7 | type: 'function', 8 | name: 'aggregate', 9 | stateMutability: 'nonpayable', 10 | inputs: [ 11 | { 12 | name: 'calls', 13 | type: 'tuple[]', 14 | components: [ 15 | {name: 'target', type: 'address'}, 16 | {name: 'callData', type: 'bytes'}, 17 | ] 18 | } 19 | ], 20 | outputs: [ 21 | {name: 'blockNumber', type: 'uint256'}, 22 | {name: 'returnData', type: 'bytes[]'}, 23 | ] 24 | }, 25 | { 26 | name: 'tryAggregate', 27 | type: 'function', 28 | stateMutability: 'nonpayable', 29 | inputs: [ 30 | {name: 'requireSuccess', type: 'bool'}, 31 | { 32 | name: 'calls', 33 | type: 'tuple[]', 34 | components: [ 35 | {name: 'target', type: 'address'}, 36 | {name: 'callData', type: 'bytes'}, 37 | ] 38 | } 39 | ], 40 | outputs: [ 41 | { 42 | name: 'returnData', 43 | type: 'tuple[]', 44 | components: [ 45 | {name: 'success', type: 'bool'}, 46 | {name: 'returnData', type: 'bytes'}, 47 | ] 48 | }, 49 | ] 50 | } 51 | ]) 52 | 53 | 54 | type AnyFunc = Func 55 | type Call = [address: string, bytes: string] 56 | 57 | 58 | const aggregate = new Func<[calls: Call[]], {}, {blockNumber: bigint, returnData: string[]}>( 59 | abi, abi.getFunction('aggregate')!.selector 60 | ) 61 | 62 | 63 | const try_aggregate = new Func<[requireSuccess: boolean, calls: Array<[target: string, callData: string]>], {}, Array<{success: boolean, returnData: string}>>( 64 | abi, abi.getFunction('tryAggregate')!.selector 65 | ) 66 | 67 | 68 | export type MulticallResult = { 69 | success: true 70 | value: T 71 | } | { 72 | success: false 73 | returnData?: string 74 | value?: undefined 75 | } 76 | 77 | 78 | export class Multicall extends ContractBase { 79 | static aggregate = aggregate 80 | static try_aggregate = try_aggregate 81 | 82 | aggregate( 83 | func: Func, 84 | address: string, 85 | calls: Args[], 86 | paging?: number 87 | ): Promise 88 | 89 | aggregate( 90 | func: Func, 91 | calls: [address: string, args: Args][], 92 | paging?: number 93 | ): Promise 94 | 95 | aggregate( 96 | calls: [func: AnyFunc, address: string, args: any[]][], 97 | paging?: number 98 | ): Promise 99 | 100 | async aggregate(...args: any[]): Promise { 101 | let [calls, funcs, page] = this.makeCalls(args) 102 | let size = calls.length 103 | let results = new Array(size) 104 | for (let [from, to] of splitIntoPages(size, page)) { 105 | let {returnData} = await this.eth_call(aggregate, [calls.slice(from, to)]) 106 | for (let i = from; i < to; i++) { 107 | let data = returnData[i - from] 108 | results[i] = funcs[i].decodeResult(data) 109 | } 110 | } 111 | return results 112 | } 113 | 114 | tryAggregate( 115 | func: Func, 116 | address: string, 117 | calls: Args[], 118 | paging?: number 119 | ): Promise[]> 120 | 121 | tryAggregate( 122 | func: Func, 123 | calls: [address: string, args: Args][], 124 | paging?: number 125 | ): Promise[]> 126 | 127 | tryAggregate( 128 | calls: [func: AnyFunc, address: string, args: any[]][], 129 | paging?: number 130 | ): Promise[]> 131 | 132 | async tryAggregate(...args: any[]): Promise { 133 | let [calls, funcs, page] = this.makeCalls(args) 134 | let size = calls.length 135 | let results = new Array(size) 136 | for (let [from, to] of splitIntoPages(size, page)) { 137 | let response = await this.eth_call(try_aggregate, [false, calls.slice(from, to)]) 138 | for (let i = from; i < to; i++) { 139 | let res = response[i - from] 140 | if (res.success) { 141 | try { 142 | results[i] = { 143 | success: true, 144 | value: funcs[i].decodeResult(res.returnData) 145 | } 146 | } catch(err: any) { 147 | results[i] = {success: false, returnData: res.returnData} 148 | } 149 | } else { 150 | results[i] = {success: false} 151 | } 152 | } 153 | } 154 | return results 155 | } 156 | 157 | private makeCalls(args: any[]): [calls: Call[], funcs: AnyFunc[], page: number] { 158 | let page = typeof args[args.length-1] == 'number' ? args.pop()! : Number.MAX_SAFE_INTEGER 159 | switch(args.length) { 160 | case 1: { 161 | let list: [func: AnyFunc, address: string, args: any[]][] = args[0] 162 | let calls = new Array(list.length) 163 | let funcs = new Array(list.length) 164 | for (let i = 0; i < list.length; i++) { 165 | let [func, address, args] = list[i] 166 | calls[i] = [address, func.encode(args)] 167 | funcs[i] = func 168 | } 169 | return [calls, funcs, page] 170 | } 171 | case 2: { 172 | let func: AnyFunc = args[0] 173 | let list: [address: string, args: any[]][] = args[1] 174 | let calls = new Array(list.length) 175 | let funcs = new Array(list.length) 176 | for (let i = 0; i < list.length; i++) { 177 | let [address, args] = list[i] 178 | calls[i] = [address, func.encode(args)] 179 | funcs[i] = func 180 | } 181 | return [calls, funcs, page] 182 | } 183 | case 3: { 184 | let func: AnyFunc = args[0] 185 | let address: string = args[1] 186 | let list: any[][] = args[2] 187 | let calls = new Array(list.length) 188 | let funcs = new Array(list.length) 189 | for (let i = 0; i < list.length; i++) { 190 | let args = list[i] 191 | calls[i] = [address, func.encode(args)] 192 | funcs[i] = func 193 | } 194 | return [calls, funcs, page] 195 | } 196 | default: 197 | throw new Error('unexpected number of arguments') 198 | } 199 | } 200 | } 201 | 202 | 203 | function* splitIntoPages(size: number, page: number): Iterable<[from: number, to: number]> { 204 | let from = 0 205 | while (size) { 206 | let step = Math.min(page, size) 207 | let to = from + step 208 | yield [from, to] 209 | size -= step 210 | from = to 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | Subsquid Logo 5 | 6 |

7 | 8 | [![docs.rs](https://docs.rs/leptos/badge.svg)](https://docs.subsquid.io/) 9 | [![Discord](https://img.shields.io/discord/1031524867910148188?color=%237289DA&label=discord)](https://discord.gg/subsquid) 10 | 11 | [Website](https://subsquid.io) | [Docs](https://docs.subsquid.io/) | [Discord](https://discord.gg/subsquid) 12 | 13 | [Subsquid Network FAQ](https://docs.subsquid.io/subsquid-network/) 14 | 15 | # Deploy a double processor squid 16 | 17 | This is a quest to run a squid with two processors. Here is how to run it: 18 | 19 | ### I. Install dependencies: Node.js, Docker, Git. 20 | 21 |
22 | On Windows 23 | 24 | 1. Enable [Hyper-V](https://learn.microsoft.com/en-us/virtualization/hyper-v-on-windows/quick-start/enable-hyper-v). 25 | 2. Install [Docker for Windows](https://docs.docker.com/desktop/install/windows-install/). 26 | 3. Install NodeJS LTS using the [official installer](https://nodejs.org/en/download). 27 | 4. Install [Git for Windows](https://git-scm.com/download/win). 28 | 29 | In all installs it is OK to leave all the options at their default values. You will need a terminal to complete this tutorial - [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) bash is the preferred option. 30 | 31 |
32 |
33 | On Mac 34 | 35 | 1. Install [Docker for Mac](https://docs.docker.com/desktop/install/mac-install/). 36 | 2. Install Git using the [installer](https://sourceforge.net/projects/git-osx-installer/) or by [other means](https://git-scm.com/download/mac). 37 | 3. Install NodeJS LTS using the [official installer](https://nodejs.org/en/download). 38 | 39 | We recommend configuring NodeJS to install global packages to a folder owned by an unprivileged account. Create the folder by running 40 | ```bash 41 | mkdir ~/global-node-packages 42 | ``` 43 | then configure NodeJS to use it 44 | ```bash 45 | npm config set prefix ~/global-node-packages 46 | ``` 47 | Make sure that the folder `~/global-node-packages/bin` is in `PATH`. That allows running globally installed NodeJS executables from any terminal. Here is a one-liner that detects your shell and takes care of setting `PATH`: 48 | ``` 49 | CURSHELL=`ps -hp $$ | awk '{print $5}'`; case `basename $CURSHELL` in 'bash') DEST="$HOME/.bash_profile";; 'zsh') DEST="$HOME/.zshenv";; esac; echo 'export PATH="${HOME}/global-node-packages/bin:$PATH"' >> "$DEST" 50 | ``` 51 | Alternatively you can add the following line to `~/.zshenv` (if you are using zsh) or `~/.bash_profile` (if you are using bash) manually: 52 | ``` 53 | export PATH="${HOME}/global-node-packages/bin:$PATH" 54 | ``` 55 | 56 | Re-open the terminal to apply the changes. 57 | 58 |
59 |
60 | On Linux 61 | 62 | Install [NodeJS (v16 or newer)](https://nodejs.org/en/download/package-manager), Git and Docker using your distro's package manager. 63 | 64 | We recommend configuring NodeJS to install global packages to a folder owned by an unprivileged account. Create the folder by running 65 | ```bash 66 | mkdir ~/global-node-packages 67 | ``` 68 | then configure NodeJS to use it 69 | ```bash 70 | npm config set prefix ~/global-node-packages 71 | ``` 72 | Make sure that any executables globally installed by NodeJS are in `PATH`. That allows running them from any terminal. Open the `~/.bashrc` file in a text editor and add the following line at the end: 73 | ``` 74 | export PATH="${HOME}/global-node-packages/bin:$PATH" 75 | ``` 76 | Re-open the terminal to apply the changes. 77 | 78 |
79 | 80 | ### II. Install Subsquid CLI 81 | 82 | Open a terminal and run 83 | ```bash 84 | npm install --global @subsquid/cli@latest 85 | ``` 86 | This adds the [`sqd` command](/squid-cli). Verify that the installation was successful by running 87 | ```bash 88 | sqd --version 89 | ``` 90 | A healthy response should look similar to 91 | ``` 92 | @subsquid/cli/2.5.0 linux-x64 node-v20.5.1 93 | ``` 94 | 95 | ### III. Run the squid 96 | 97 | 1. Open a terminal and run the following commands to create the squid and enter its folder: 98 | ```bash 99 | sqd init my-double-proc-squid -t https://github.com/subsquid-quests/double-chain-squid 100 | ``` 101 | ```bash 102 | cd my-double-proc-squid 103 | ``` 104 | You can replace `my-double-proc-squid` with any name you choose for your squid. If a squid with that name already exists in [Aquarium](https://docs.subsquid.io/deploy-squid/), the first command will throw an error; if that happens simply think of another name and repeat the commands. 105 | 106 | 2. Press "Get Key" button in the quest card to obtain the `doubleProc.key` key file. Save it to the `./query-gateway/keys` subfolder of the squid folder. The file will be used by the query gateway container. 107 | 108 | 3. The template squid uses a PostgreSQL database and a query gateway. Start Docker containers that run these with 109 | ```bash 110 | sqd up 111 | ``` 112 | Wait for about a minute before proceeding to the next step. 113 | 114 | If you get an error message about `unknown shorthand flag: 'd' in -d`, that means that you're using an old version of `docker` that does not support the `compose` command yet. Update Docker or edit the `commands.json` file as follows: 115 | ```diff 116 | "up": { 117 | "deps": ["check-key"], 118 | "description": "Start a PG database", 119 | - "cmd": ["docker", "compose", "up", "-d"] 120 | + "cmd": ["docker-compose", "up", "-d"] 121 | }, 122 | "down": { 123 | "description": "Drop a PG database", 124 | - "cmd": ["docker", "compose", "down"] 125 | + "cmd": ["docker-compose", "down"] 126 | }, 127 | ``` 128 | 129 | 4. Prepare the squid for running by installing dependencies, building the source code and creating all the necessary database tables: 130 | ```bash 131 | npm ci 132 | sqd build 133 | sqd migration:apply 134 | ``` 135 | 5. Start your squid with 136 | ```bash 137 | sqd run . 138 | ``` 139 | The command should output lines like these: 140 | ``` 141 | [api] 22:00:36 WARN sqd:graphql-server enabling dumb in-memory cache (size: 100mb, ttl: 1000ms, max-age: 1000ms) 142 | [api] 22:00:36 INFO sqd:graphql-server listening on port 4350 143 | [eth-processor] 22:00:36 INFO sqd:processor processing blocks from 16000000 144 | [eth-processor] 22:00:36 INFO sqd:processor using archive data source 145 | [eth-processor] 22:00:36 INFO sqd:processor prometheus metrics are served at port 40163 146 | [bsc-processor] 22:00:36 INFO sqd:processor processing blocks from 28000000 147 | [bsc-processor] 22:00:36 INFO sqd:processor using archive data source 148 | [bsc-processor] 22:00:36 INFO sqd:processor prometheus metrics are served at port 39533 149 | [bsc-processor] 22:00:39 INFO sqd:processor 28004339 / 32107455, rate: 1537 blocks/sec, mapping: 603 blocks/sec, 1157 items/sec, eta: 45m 150 | [eth-processor] 22:00:40 INFO sqd:processor 16005819 / 18226899, rate: 1686 blocks/sec, mapping: 644 blocks/sec, 1224 items/sec, eta: 22m 151 | [bsc-processor] 22:00:44 INFO sqd:processor 28011319 / 32107455, rate: 1503 blocks/sec, mapping: 648 blocks/sec, 1250 items/sec, eta: 46m 152 | ``` 153 | The squid should sync in 25-30 minutes. When it's done, stop it with Ctrl-C, then stop and remove the auxiliary containers with 154 | ```bash 155 | sqd down 156 | ``` 157 | 158 | # Quest Info 159 | 160 | | Category | Skill Level | Time required (minutes) | Max Participants | Reward | Status | 161 | | ---------------- | ------------------------------------ | ----------------------- | ---------------- | ----------------------------------- | ------ | 162 | | Squid Deployment | $\textcolor{green}{\textsf{Simple}}$ | ~40 | - | $\textcolor{red}{\textsf{500tSQD}}$ | open | 163 | 164 | # Acceptance critera 165 | 166 | Sync this squid using the key from the quest card. The syncing progress is tracked by the amount of data the squid has retrieved from [Subsquid Network](https://docs.subsquid.io/subsquid-network). 167 | 168 | # About this squid 169 | 170 | This [squid](https://docs.subsquid.io/) captures USDC Transfer events on ETH and BSC, stores them in the same database and serves the data over a common GraphQL API. 171 | 172 | The Ethereum data ingester ("processor") is located in `src/eth` and similarly the Binance Chain processor can be found in `src/bsc`. The scripts file `commands.json` was updated with the commands `process:eth` and `process:bsc` that run the processors. GraphQL server runs as a separate process started by `sqd serve`. You can also use `sqd run` to run all the services at once. 173 | 174 | The squid uses [Subsquid Network](https://docs.subsquid.io/subsquid-network) as its primary data source. 175 | --------------------------------------------------------------------------------