├── README.md ├── docker-compose.yml ├── package.json ├── project.yaml ├── schema.graphql ├── src ├── index.ts ├── mappings │ └── mappingHandlers.ts └── types │ ├── index.ts │ └── models │ ├── Account.ts │ ├── Transfer.ts │ └── index.ts └── tsconfig.json /README.md: -------------------------------------------------------------------------------- 1 | # What is SubQuery? 2 | 3 | SubQuery powers the next generation of Polkadot dApps by allowing developers to extract, transform and query blockchain data in real time using GraphQL. In addition to this, SubQuery provides production quality hosting infrastructure to run these projects in. 4 | 5 | # SubQuery Example - Account transfers Reverse Lookup 6 | 7 | This subquery example adds a reverse lookup field on Account.myToAddress meaning that it is accessible from the Account entity but is just a reference or pointer back to Transfer.to. 8 | 9 | # Getting Started 10 | 11 | ### 1. Clone the entire subql-example repository 12 | 13 | ```shell 14 | git clone https://github.com/subquery/tutorials-account-transfer-reverse-lookups.git 15 | 16 | ``` 17 | 18 | ### 2. Install dependencies 19 | 20 | ```shell 21 | cd 22 | yarn 23 | ``` 24 | 25 | ### 3. Generate types 26 | 27 | ```shell 28 | yarn codegen 29 | ``` 30 | 31 | ### 4. Build the project 32 | 33 | ```shell 34 | yarn build 35 | ``` 36 | 37 | ### 5. Start Docker 38 | 39 | ```shell 40 | docker-compose pull & docker-compose up 41 | ``` 42 | 43 | ### 6. Run locally 44 | 45 | Open http://localhost:3000/ on your browser 46 | 47 | ### 7. Example query to run 48 | 49 | ```shell 50 | query{ 51 | accounts(first:5){ 52 | nodes{ 53 | id 54 | myToAddress{ 55 | nodes{ 56 | id 57 | amount 58 | } 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | postgres: 5 | image: postgres:12-alpine 6 | ports: 7 | - 5432:5432 8 | volumes: 9 | - .data/postgres:/var/lib/postgresql/data 10 | environment: 11 | POSTGRES_PASSWORD: postgres 12 | 13 | subquery-node: 14 | image: onfinality/subql-node:latest 15 | depends_on: 16 | - "postgres" 17 | restart: always 18 | environment: 19 | DB_USER: postgres 20 | DB_PASS: postgres 21 | DB_DATABASE: postgres 22 | DB_HOST: postgres 23 | DB_PORT: 5432 24 | volumes: 25 | - ./:/app 26 | command: 27 | - -f=/app 28 | - --local 29 | 30 | graphql-engine: 31 | image: onfinality/subql-query:latest 32 | ports: 33 | - 3000:3000 34 | depends_on: 35 | - "postgres" 36 | - "subquery-node" 37 | restart: always 38 | environment: 39 | DB_USER: postgres 40 | DB_PASS: postgres 41 | DB_DATABASE: postgres 42 | DB_HOST: postgres 43 | DB_PORT: 5432 44 | command: 45 | - --name=app 46 | - --playground 47 | - --indexer=http://subquery-node:3000 48 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "account-transfers", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "dist/index.js", 6 | "scripts": { 7 | "build": "tsc -b", 8 | "prepack": "rm -rf dist && npm build", 9 | "test": "jest", 10 | "codegen": "./node_modules/.bin/subql codegen" 11 | }, 12 | "homepage": "https://github.com/subquery/subql-starter", 13 | "repository": "github:subquery/subql-starter", 14 | "files": [ 15 | "dist", 16 | "schema.graphql", 17 | "project.yaml" 18 | ], 19 | "author": "sa", 20 | "license": "Apache-2.0", 21 | "devDependencies": { 22 | "@polkadot/api": "^6", 23 | "@subql/types": "latest", 24 | "typescript": "^4.1.3", 25 | "@subql/cli": "latest" 26 | } 27 | } -------------------------------------------------------------------------------- /project.yaml: -------------------------------------------------------------------------------- 1 | specVersion: 0.0.1 2 | description: '' 3 | repository: '' 4 | schema: ./schema.graphql 5 | network: 6 | endpoint: 'wss://polkadot.api.onfinality.io/public-ws' 7 | dictionary: 'https://api.subquery.network/sq/subquery/dictionary-polkadot' 8 | dataSources: 9 | - name: main 10 | kind: substrate/Runtime 11 | startBlock: 1 12 | mapping: 13 | handlers: 14 | - handler: handleEvent 15 | kind: substrate/EventHandler 16 | filter: 17 | module: balances 18 | method: Transfer 19 | -------------------------------------------------------------------------------- /schema.graphql: -------------------------------------------------------------------------------- 1 | # This is an example of a one-many entity relation. One account can have many receiving addresses. 2 | # The Account entity has 1 column storing the toAddress. 3 | # The Transfer entity has 4 columns where "to" refers to the Account.id 4 | 5 | type Account @entity { 6 | id: ID! #this primary key is set as the toAddress 7 | myToAddress: [Transfer] @derivedFrom(field:"to") 8 | } 9 | 10 | type Transfer @entity { 11 | id: ID! #this primary key is the block number + the event id 12 | amount: BigInt 13 | blockNumber: BigInt 14 | to: Account! #receiving address 15 | } 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | //Exports all handler functions 2 | export * from './mappings/mappingHandlers' 3 | -------------------------------------------------------------------------------- /src/mappings/mappingHandlers.ts: -------------------------------------------------------------------------------- 1 | import {SubstrateEvent} from "@subql/types"; 2 | import {Account, Transfer} from "../types"; 3 | import {Balance} from "@polkadot/types/interfaces"; 4 | 5 | export async function handleEvent(event: SubstrateEvent): Promise { 6 | // The balances.transfer event has the following payload \[from, to, value\] that we can access 7 | 8 | // const fromAddress = event.event.data[0]; 9 | const toAddress = event.event.data[1]; 10 | const amount = event.event.data[2]; 11 | 12 | // query for toAddress from DB 13 | const toAccount = await Account.get(toAddress.toString()); 14 | // if not in DB, instantiate a new Account object using the toAddress as a unique ID 15 | if (!toAccount) { 16 | await new Account(toAddress.toString()).save(); 17 | } 18 | 19 | // instantiate a new Transfer object using the block number and event.idx as a unique ID 20 | const transfer = new Transfer(`${event.block.block.header.number.toNumber()}-${event.idx}`, ); 21 | transfer.blockNumber = event.block.block.header.number.toBigInt(); 22 | transfer.toId = toAddress.toString(); 23 | transfer.amount = (amount as Balance).toBigInt(); 24 | await transfer.save(); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/types/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | // Auto-generated , DO NOT EDIT 4 | export * from "./models"; 5 | 6 | 7 | -------------------------------------------------------------------------------- /src/types/models/Account.ts: -------------------------------------------------------------------------------- 1 | // Auto-generated , DO NOT EDIT 2 | import {Entity, FunctionPropertyNames} from "@subql/types"; 3 | import assert from 'assert'; 4 | 5 | 6 | 7 | 8 | export class Account implements Entity { 9 | 10 | constructor(id: string) { 11 | this.id = id; 12 | } 13 | 14 | 15 | public id: string; 16 | 17 | 18 | async save(): Promise{ 19 | let id = this.id; 20 | assert(id !== null, "Cannot save Account entity without an ID"); 21 | await store.set('Account', id.toString(), this); 22 | } 23 | static async remove(id:string): Promise{ 24 | assert(id !== null, "Cannot remove Account entity without an ID"); 25 | await store.remove('Account', id.toString()); 26 | } 27 | 28 | static async get(id:string): Promise{ 29 | assert((id !== null && id !== undefined), "Cannot get Account entity without an ID"); 30 | const record = await store.get('Account', id.toString()); 31 | if (record){ 32 | return Account.create(record); 33 | }else{ 34 | return; 35 | } 36 | } 37 | 38 | 39 | 40 | static create(record: Partial>> & Entity): Account { 41 | assert(typeof record.id === 'string', "id must be provided"); 42 | let entity = new Account(record.id); 43 | Object.assign(entity,record); 44 | return entity; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/types/models/Transfer.ts: -------------------------------------------------------------------------------- 1 | // Auto-generated , DO NOT EDIT 2 | import {Entity, FunctionPropertyNames} from "@subql/types"; 3 | import assert from 'assert'; 4 | 5 | 6 | 7 | 8 | export class Transfer implements Entity { 9 | 10 | constructor(id: string) { 11 | this.id = id; 12 | } 13 | 14 | 15 | public id: string; 16 | 17 | public amount?: bigint; 18 | 19 | public blockNumber?: bigint; 20 | 21 | public toId: string; 22 | 23 | 24 | async save(): Promise{ 25 | let id = this.id; 26 | assert(id !== null, "Cannot save Transfer entity without an ID"); 27 | await store.set('Transfer', id.toString(), this); 28 | } 29 | static async remove(id:string): Promise{ 30 | assert(id !== null, "Cannot remove Transfer entity without an ID"); 31 | await store.remove('Transfer', id.toString()); 32 | } 33 | 34 | static async get(id:string): Promise{ 35 | assert((id !== null && id !== undefined), "Cannot get Transfer entity without an ID"); 36 | const record = await store.get('Transfer', id.toString()); 37 | if (record){ 38 | return Transfer.create(record); 39 | }else{ 40 | return; 41 | } 42 | } 43 | 44 | 45 | static async getByToId(toId: string): Promise{ 46 | 47 | const records = await store.getByField('Transfer', 'toId', toId); 48 | return records.map(record => Transfer.create(record)); 49 | 50 | } 51 | 52 | 53 | static create(record: Partial>> & Entity): Transfer { 54 | assert(typeof record.id === 'string', "id must be provided"); 55 | let entity = new Transfer(record.id); 56 | Object.assign(entity,record); 57 | return entity; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/types/models/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 2 | 3 | // Auto-generated , DO NOT EDIT 4 | 5 | export {Account} from "./Account" 6 | 7 | export {Transfer} from "./Transfer" 8 | 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "esModuleInterop": true, 6 | "declaration": true, 7 | "importHelpers": true, 8 | "resolveJsonModule": true, 9 | "module": "commonjs", 10 | "outDir": "dist", 11 | "rootDir": "src", 12 | "target": "es2017" 13 | }, 14 | "include": [ 15 | "src/**/*", 16 | "node_modules/@subql/types/dist/global.d.ts" 17 | ] 18 | } 19 | --------------------------------------------------------------------------------