├── rust-toolchain ├── .github ├── CODEOWNERS ├── workflows │ ├── godwoken-tests.yml │ ├── rust.yml │ ├── node.js.yml │ ├── unit-tests.yml │ ├── docker-publish.yml │ └── docker-publish-indexer.yml └── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md ├── packages ├── api-server │ ├── bin │ │ ├── www │ │ └── cluster │ ├── src │ │ ├── db │ │ │ ├── index.ts │ │ │ └── constant.ts │ │ ├── app │ │ │ ├── www.ts │ │ │ ├── cluster.ts │ │ │ └── app.ts │ │ ├── base │ │ │ ├── index.ts │ │ │ ├── types │ │ │ │ ├── gw-log.ts │ │ │ │ ├── node-info.ts │ │ │ │ └── api.ts │ │ │ ├── filter.ts │ │ │ ├── logger.ts │ │ │ ├── env-config.ts │ │ │ ├── gas-price.ts │ │ │ ├── worker.ts │ │ │ └── address.ts │ │ ├── middlewares │ │ │ ├── header.ts │ │ │ └── jayson.ts │ │ ├── methods │ │ │ ├── modules │ │ │ │ ├── index.ts │ │ │ │ ├── prof.ts │ │ │ │ ├── net.ts │ │ │ │ ├── web3.ts │ │ │ │ ├── debug.ts │ │ │ │ └── poly.ts │ │ │ ├── error-code.ts │ │ │ ├── types.ts │ │ │ ├── constant.ts │ │ │ ├── index.ts │ │ │ └── error.ts │ │ ├── sentry.ts │ │ ├── cache │ │ │ ├── types.ts │ │ │ ├── constant.ts │ │ │ ├── store.ts │ │ │ ├── redis.ts │ │ │ ├── tx-hash.ts │ │ │ ├── guard.ts │ │ │ └── index.ts │ │ ├── gasless │ │ │ ├── utils.ts │ │ │ ├── entrypoint.ts │ │ │ └── payload.ts │ │ ├── decorator.ts │ │ ├── opentelemetry.ts │ │ ├── ws │ │ │ └── wss.ts │ │ ├── rate-limit.ts │ │ ├── util.ts │ │ ├── block-emitter.ts │ │ └── erc20.ts │ ├── .gitignore │ ├── config │ │ └── eth.json │ ├── tests │ │ ├── www.ts │ │ ├── .test.env.example │ │ ├── methods │ │ │ ├── net.test.ts │ │ │ ├── gw.test.ts │ │ │ └── eth.test.ts │ │ ├── base │ │ │ └── types │ │ │ │ ├── uint32.test.ts │ │ │ │ ├── uint64.test.ts │ │ │ │ ├── uint128.test.ts │ │ │ │ └── uint256.test.ts │ │ ├── utils │ │ │ ├── erc20.test.ts │ │ │ ├── gas-price.test.ts │ │ │ ├── gasless.test.ts │ │ │ └── convention.test.ts │ │ ├── db │ │ │ └── helpers.test.ts │ │ └── cache.test.ts │ ├── knexfile.ts │ ├── migrations │ │ ├── 20221102080006_add_chain_id_to_transactions.ts │ │ ├── 20220530035042_fix_transactions_contract_address.ts │ │ ├── 20221018014620_fix_log_index.ts │ │ ├── 20220701114448_remove_foreign_keys.ts │ │ └── 20220512033018_refactor_tables.ts │ ├── tsconfig.json │ ├── cli │ │ ├── README.md │ │ ├── index.ts │ │ └── fix-log-transaction-index.ts │ ├── newrelic.js │ ├── seeds │ │ └── insert_seed_data.ts │ └── package.json └── godwoken │ ├── src │ ├── index.ts │ ├── rpc.ts │ ├── logger.ts │ └── types.ts │ ├── tsconfig.json │ ├── package.json │ └── scripts │ └── regenerate-schemas.sh ├── crates ├── rpc-client │ ├── src │ │ ├── lib.rs │ │ ├── error.rs │ │ ├── convertion.rs │ │ └── godwoken_async_client.rs │ └── Cargo.toml └── indexer │ ├── src │ ├── cpu_count.rs │ ├── lib.rs │ ├── pool.rs │ ├── main.rs │ ├── config.rs │ └── types.rs │ └── Cargo.toml ├── Cargo.toml ├── .dockerignore ├── .gitignore ├── scripts ├── health-check.sh └── generate-indexer-config.js ├── Dockerfile ├── .eslintrc.js ├── docker └── indexer │ └── Dockerfile ├── Makefile ├── docs ├── compatibility.md ├── apis.md └── schema_design.md └── package.json /rust-toolchain: -------------------------------------------------------------------------------- 1 | 1.61.0 2 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @nervosnetwork/protocol-scaling-dev 2 | -------------------------------------------------------------------------------- /packages/api-server/bin/www: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require("../lib/app/www") 4 | -------------------------------------------------------------------------------- /packages/api-server/src/db/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./query"; 2 | export * from "./helpers"; 3 | -------------------------------------------------------------------------------- /packages/api-server/.gitignore: -------------------------------------------------------------------------------- 1 | allowed-addresses.json 2 | newrelic_agent.log 3 | rate-limit-config.json 4 | 5 | *.log 6 | -------------------------------------------------------------------------------- /packages/api-server/bin/cluster: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | require('../newrelic') 4 | require("../lib/app/cluster") 5 | -------------------------------------------------------------------------------- /packages/api-server/config/eth.json: -------------------------------------------------------------------------------- 1 | { 2 | "chain_id" : "1", 3 | "eth_protocolVersion" : "65", 4 | "pEther": 1 5 | } 6 | -------------------------------------------------------------------------------- /crates/rpc-client/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod convertion; 2 | pub mod error; 3 | pub mod godwoken_async_client; 4 | pub mod godwoken_rpc_client; 5 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [workspace] 2 | members = [ 3 | "crates/rpc-client", 4 | "crates/indexer", 5 | ] 6 | 7 | [profile.release] 8 | overflow-checks = true 9 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | node_modules 3 | npm-debug 4 | packages/api-server/.env 5 | packages/api-server/lib 6 | packages/godwoken/lib 7 | target 8 | indexer-config.toml 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | *.env 4 | packages/api-server/allowed-addresses.json 5 | crates/**/target 6 | target 7 | indexer-config.toml 8 | *.sql 9 | *.log 10 | *.cpuprofile 11 | *.heapsnapshot 12 | -------------------------------------------------------------------------------- /packages/api-server/src/db/constant.ts: -------------------------------------------------------------------------------- 1 | export const DEFAULT_MAX_QUERY_NUMBER = 10000; 2 | export const DEFAULT_MAX_QUERY_TIME_MILSECS = 1000 * 10; // 10 seconds 3 | 4 | export const LATEST_MEDIAN_GAS_PRICE = 50; 5 | -------------------------------------------------------------------------------- /crates/indexer/src/cpu_count.rs: -------------------------------------------------------------------------------- 1 | lazy_static::lazy_static! { 2 | pub static ref CPU_COUNT: Option = { 3 | std::env::args() 4 | .nth(4) 5 | .map(|num| num.parse::().unwrap()) 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /crates/indexer/src/lib.rs: -------------------------------------------------------------------------------- 1 | pub mod config; 2 | pub mod cpu_count; 3 | pub mod helper; 4 | pub mod indexer; 5 | pub mod insert_l2_block; 6 | pub mod pool; 7 | pub mod runner; 8 | pub mod types; 9 | 10 | pub use indexer::Web3Indexer; 11 | -------------------------------------------------------------------------------- /packages/godwoken/src/index.ts: -------------------------------------------------------------------------------- 1 | export * as schemas from "../schemas"; 2 | export * as normalizers from "./normalizers"; 3 | export * from "./types"; 4 | export * from "./rpc"; 5 | export { GodwokenClient } from "./client"; 6 | export { logger, winstonLogger } from "./logger"; 7 | -------------------------------------------------------------------------------- /packages/api-server/src/app/www.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies. 3 | */ 4 | 5 | import { startServer } from "./app"; 6 | import { envConfig } from "../base/env-config"; 7 | 8 | /** 9 | * Get port from environment and store in Express. 10 | */ 11 | const port: number = +(envConfig.port || "8024"); 12 | startServer(port); 13 | -------------------------------------------------------------------------------- /packages/api-server/src/base/index.ts: -------------------------------------------------------------------------------- 1 | import { envConfig } from "./env-config"; 2 | import { GwConfig } from "./gw-config"; 3 | import { CKBPriceOracle } from "../price-oracle"; 4 | 5 | export const gwConfig = new GwConfig(envConfig.godwokenJsonRpc); 6 | 7 | export const readonlyPriceOracle = new CKBPriceOracle({ readonly: true }); 8 | -------------------------------------------------------------------------------- /packages/api-server/tests/www.ts: -------------------------------------------------------------------------------- 1 | import jayson from "jayson/promise"; 2 | 3 | export const client = jayson.Client.http({ 4 | port: process.env.PORT || "8024", 5 | }); 6 | 7 | export interface JSONResponse { 8 | jsonrpc: "2.0"; 9 | id: string; 10 | result?: any; 11 | error?: { 12 | code: number; 13 | message: string; 14 | }; 15 | } 16 | -------------------------------------------------------------------------------- /packages/api-server/src/middlewares/header.ts: -------------------------------------------------------------------------------- 1 | import { Response } from "express"; 2 | 3 | export interface ResponseHeader { 4 | instantFinality: boolean; 5 | // add more below if needed 6 | } 7 | 8 | export function setResponseHeader(res: Response, header: ResponseHeader) { 9 | res.setHeader("X-Instant-Finality", header.instantFinality.toString()); 10 | } 11 | -------------------------------------------------------------------------------- /packages/api-server/tests/.test.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL=postgres://username:password@localhost:5432/lumos 2 | GODWOKEN_JSON_RPC=http://localhost:8119 3 | ETH_ACCOUNT_LOCK_HASH=0xbb490fd251be3ea95d695326c2fb4416bd29f87331afa5cbc3f989a75e53de92 4 | ROLLUP_TYPE_HASH=0x349bb0a53e3321f94838c3f26c7d75c03b73811600c9371686fb0622670f5157 5 | CHAIN_ID=0x3 6 | CREATOR_ACCOUNT_ID=0x3 7 | -------------------------------------------------------------------------------- /scripts/health-check.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Query whether web3 api is ready to serve 4 | echo '{ 5 | "id": 42, 6 | "jsonrpc": "2.0", 7 | "method": "poly_getHealthStatus", 8 | "params": [] 9 | }' \ 10 | | tr -d '\n' \ 11 | | curl --silent -H 'content-type: application/json' -d @- \ 12 | http://127.0.0.1:8024 \ 13 | | jq '.result.status' | egrep "true" || exit 1 14 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | 2 | FROM node:14-bullseye 3 | 4 | COPY . /godwoken-web3/. 5 | RUN cd /godwoken-web3 && yarn && yarn build 6 | 7 | RUN npm install pm2 -g 8 | 9 | RUN apt-get update \ 10 | && apt-get dist-upgrade -y \ 11 | && apt-get install curl -y \ 12 | && apt-get install jq -y \ 13 | && apt-get clean \ 14 | && echo "Finished installing dependencies" 15 | 16 | EXPOSE 8024 3000 17 | -------------------------------------------------------------------------------- /crates/rpc-client/src/error.rs: -------------------------------------------------------------------------------- 1 | use thiserror::Error; 2 | 3 | #[derive(Error, Debug)] 4 | pub enum RpcClientError { 5 | #[error("connection failed by: {0}, error: {1}")] 6 | ConnectionError(String, anyhow::Error), 7 | 8 | #[error(transparent)] 9 | SerdeJsonError(#[from] serde_json::Error), 10 | 11 | #[error(transparent)] 12 | Other(#[from] anyhow::Error), 13 | } 14 | -------------------------------------------------------------------------------- /packages/api-server/src/base/types/gw-log.ts: -------------------------------------------------------------------------------- 1 | import { HexString, Hash, HexNumber } from "@ckb-lumos/base"; 2 | 3 | export interface PolyjuiceUserLog { 4 | address: HexString; 5 | data: HexString; 6 | topics: Hash[]; 7 | } 8 | 9 | export interface PolyjuiceSystemLog { 10 | gasUsed: HexNumber; 11 | cumulativeGasUsed: HexNumber; 12 | createdAddress: HexString; 13 | statusCode: HexNumber; 14 | } 15 | -------------------------------------------------------------------------------- /packages/api-server/knexfile.ts: -------------------------------------------------------------------------------- 1 | import dotenv from "dotenv"; 2 | dotenv.config({path: "./.env"}) 3 | 4 | const knexConfig = { 5 | development: { 6 | client: "postgresql", 7 | connection: process.env.DATABASE_URL, 8 | pool: { 9 | min: 2, 10 | max: 10 11 | }, 12 | migrations: { 13 | tableName: "knex_migrations" 14 | } 15 | } 16 | }; 17 | 18 | export default knexConfig; 19 | -------------------------------------------------------------------------------- /packages/godwoken/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2020", 4 | "module": "commonjs", 5 | "declaration": true, 6 | "outDir": "./lib", 7 | "types": ["node"], 8 | "lib": ["es2020"], 9 | "moduleResolution": "node", 10 | "strict": true, 11 | "esModuleInterop": true, 12 | "sourceMap": true, 13 | "resolveJsonModule": true 14 | }, 15 | "exclude": ["**/node_modules", "**/lib"] 16 | } 17 | -------------------------------------------------------------------------------- /packages/api-server/migrations/20221102080006_add_chain_id_to_transactions.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from "knex"; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema.alterTable("transactions", (table) => { 5 | table.decimal("chain_id", null, 0).nullable(); 6 | }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex.schema.alterTable("transactions", (table) => { 11 | table.dropColumn("chain_id"); 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { envConfig } from "../../base/env-config"; 2 | 3 | const enableList = ["Eth", "Web3", "Net", "Gw", "Poly", "Debug"]; 4 | if (envConfig.enableProfRpc === "true") { 5 | enableList.push("Prof"); 6 | } 7 | 8 | export const list = enableList; 9 | 10 | export * from "./eth"; 11 | export * from "./web3"; 12 | export * from "./net"; 13 | export * from "./gw"; 14 | export * from "./poly"; 15 | export * from "./prof"; 16 | export * from "./debug"; 17 | -------------------------------------------------------------------------------- /packages/api-server/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "experimentalDecorators": true, 4 | "target": "es2020", 5 | "module": "commonjs", 6 | "declaration": true, 7 | "outDir": "./lib", 8 | "types": ["node"], 9 | "lib": ["es2020"], 10 | "moduleResolution": "node", 11 | "strict": true, 12 | "esModuleInterop": true, 13 | "sourceMap": true, 14 | "resolveJsonModule": true 15 | }, 16 | "include": ["src"], 17 | "exclude": ["**/node_modules"] 18 | } 19 | -------------------------------------------------------------------------------- /packages/api-server/migrations/20220530035042_fix_transactions_contract_address.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from "knex"; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex("transactions") 5 | .update({ contract_address: null }) 6 | .where({ contract_address: Buffer.from("", "hex") }); 7 | } 8 | 9 | export async function down(knex: Knex): Promise { 10 | await knex("transactions") 11 | .update({ contract_address: Buffer.from("", "hex") }) 12 | .where({ contract_address: null }); 13 | } 14 | -------------------------------------------------------------------------------- /packages/api-server/migrations/20221018014620_fix_log_index.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from "knex"; 2 | 3 | // Fix for wrong logs.log_index 4 | export async function up(knex: Knex): Promise { 5 | await knex.raw( 6 | "with cte as(select block_number, id, log_index, row_number() over (partition by block_number order by id) rn from logs) update logs set log_index=cte.rn - 1 from cte where logs.block_number=cte.block_number and logs.id=cte.id;" 7 | ); 8 | } 9 | 10 | export async function down(knex: Knex): Promise {} 11 | -------------------------------------------------------------------------------- /.github/workflows/godwoken-tests.yml: -------------------------------------------------------------------------------- 1 | name: Godwoken Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | jobs: 12 | godwoken-tests: 13 | uses: godwokenrises/godwoken-tests/.github/workflows/reusable-integration-test-v1.yml@develop 14 | with: 15 | extra_github_env: | 16 | MANUAL_BUILD_WEB3=true 17 | MANUAL_BUILD_WEB3_INDEXER=true 18 | WEB3_GIT_URL=https://github.com/${{ github.repository }} 19 | WEB3_GIT_CHECKOUT=${{ github.ref }} 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | "parser": "@typescript-eslint/parser", 4 | "parserOptions": { 5 | "sourceType": "module" 6 | }, 7 | "plugins": [ 8 | "@typescript-eslint" 9 | ], 10 | "env": { 11 | "browser": true, 12 | "es2020": true 13 | }, 14 | "globals": { 15 | "Atomics": "readonly", 16 | "SharedArrayBuffer": "readonly" 17 | }, 18 | "rules": { 19 | "no-var": "error", 20 | "@typescript-eslint/no-unused-vars": [ 21 | "error", 22 | { 23 | "argsIgnorePattern": "^_|^args$", 24 | "varsIgnorePattern": "^_" 25 | } 26 | ] 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /packages/godwoken/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@godwoken-web3/godwoken", 3 | "version": "1.10.0-rc2", 4 | "private": true, 5 | "main": "lib/index.js", 6 | "scripts": { 7 | "regenerate-schemas": "cd scripts && ./regenerate-schemas.sh && cd -;", 8 | "fmt": "prettier --write \"src/**/*.ts\" package.json", 9 | "lint": "eslint -c ../../.eslintrc.js \"src/**/*.ts\"", 10 | "build": "tsc" 11 | }, 12 | "dependencies": { 13 | "@ckb-lumos/base": "0.18.0-rc6", 14 | "@ckb-lumos/toolkit": "0.18.0-rc6", 15 | "cross-fetch": "^3.1.5", 16 | "jsbi": "^4.2.0", 17 | "winston": "^3.7.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/api-server/src/sentry.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/node"; 2 | import cluster from "cluster"; 3 | import { envConfig } from "./base/env-config"; 4 | import { logger } from "./base/logger"; 5 | 6 | export function initSentry() { 7 | if (envConfig.sentryDns) { 8 | Sentry.init({ 9 | dsn: envConfig.sentryDns, 10 | environment: envConfig.sentryEnvironment || "development", 11 | ignoreErrors: [/^invalid nonce of account/, /^query returned more than/], 12 | }); 13 | const processType = cluster.isMaster ? "master" : "cluster"; 14 | logger.info(`Sentry init in ${processType} !!!`); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/api-server/src/base/filter.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HexString } from "@ckb-lumos/base"; 2 | import { BlockParameter } from "../methods/types"; 3 | 4 | export interface RpcFilterRequest { 5 | fromBlock?: BlockParameter; 6 | toBlock?: BlockParameter; 7 | address?: HexString; 8 | topics?: FilterTopic[]; 9 | blockHash?: HexString; 10 | } 11 | 12 | export enum FilterFlag { 13 | blockFilter = 1, 14 | pendingTransaction = 2, 15 | } 16 | 17 | export type FilterTopic = null | HexString | HexString[]; 18 | 19 | export interface FilterParams { 20 | fromBlock: bigint; 21 | toBlock: bigint; 22 | addresses: HexString[]; 23 | topics: FilterTopic[]; 24 | blockHash?: Hash; 25 | } 26 | -------------------------------------------------------------------------------- /packages/api-server/tests/methods/net.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { client, JSONResponse } from "../www"; 3 | 4 | test("net_version", async (t) => { 5 | let res: JSONResponse = await client.request(t.title, []); 6 | t.falsy(res.error); 7 | }); 8 | 9 | test("net_peerCount", async (t) => { 10 | let res: JSONResponse = await client.request(t.title, []); 11 | t.falsy(res.error); 12 | t.is(res.result, "0x0"); 13 | }); 14 | 15 | // FIXME This case is timeout to run. Uncomment it after fixing the bug. 16 | // test("net_listening", async (t) => { 17 | // let res: JSONResponse = await client.request(t.title, []); 18 | // t.falsy(res.error); 19 | // t.true(res.result); 20 | // }); 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "[feat] " 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | 12 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 13 | 14 | **Describe the solution you'd like** 15 | 16 | A clear and concise description of what you want to happen. 17 | 18 | **Describe alternatives you've considered** 19 | 20 | A clear and concise description of any alternative solutions or features you've considered. 21 | 22 | **Additional context** 23 | 24 | Add any other context or screenshots about the feature request here. 25 | -------------------------------------------------------------------------------- /docker/indexer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM rust:1-bullseye as builder 2 | 3 | RUN apt-get update 4 | RUN apt-get -y install --no-install-recommends llvm-dev clang libclang-dev libssl-dev 5 | 6 | RUN cargo install moleculec --version 0.7.2 7 | 8 | COPY . /godwoken-web3 9 | RUN cd /godwoken-web3 && rustup component add rustfmt && cargo build --release 10 | 11 | FROM ubuntu:focal 12 | 13 | RUN apt-get update \ 14 | && apt-get dist-upgrade -y \ 15 | && apt-get install -y openssl \ 16 | && apt-get install -y libcurl4 \ 17 | && apt-get clean \ 18 | && echo 'Finished installing OS updates' 19 | 20 | # godwoken-web3 indexer 21 | COPY --from=builder /godwoken-web3/target/release/gw-web3-indexer /bin/gw-web3-indexer 22 | 23 | RUN mkdir -p /web3 24 | WORKDIR /web3 25 | 26 | CMD [ "gw-web3-indexer", "--version" ] 27 | -------------------------------------------------------------------------------- /packages/godwoken/src/rpc.ts: -------------------------------------------------------------------------------- 1 | import http from "http"; 2 | import https from "https"; 3 | import { RPC as Rpc } from "@ckb-lumos/toolkit"; 4 | 5 | // don't import from envConfig 6 | const maxSockets: number = process.env.MAX_SOCKETS 7 | ? +process.env.MAX_SOCKETS 8 | : 10; 9 | 10 | const httpAgent = new http.Agent({ 11 | keepAlive: true, 12 | maxSockets, 13 | }); 14 | const httpsAgent = new https.Agent({ 15 | keepAlive: true, 16 | maxSockets, 17 | }); 18 | 19 | export class RPC extends Rpc { 20 | constructor(url: string, options?: object) { 21 | let agent: http.Agent | https.Agent = httpsAgent; 22 | if (url.startsWith("http:")) { 23 | agent = httpAgent; 24 | } 25 | 26 | options = options || {}; 27 | (options as any).agent ||= agent; 28 | super(url, options); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /crates/rpc-client/src/convertion.rs: -------------------------------------------------------------------------------- 1 | use ckb_jsonrpc_types::Script; 2 | use ckb_types::prelude::Entity; 3 | use gw_jsonrpc_types::godwoken::{L2Block, L2BlockView}; 4 | use gw_types::packed; 5 | 6 | pub fn to_l2_block(l2_block_view: L2BlockView) -> packed::L2Block { 7 | let v = l2_block_view; 8 | let l2_block = L2Block { 9 | raw: v.raw, 10 | kv_state: v.kv_state, 11 | kv_state_proof: v.kv_state_proof, 12 | transactions: v.transactions.into_iter().map(|tx| tx.inner).collect(), 13 | block_proof: v.block_proof, 14 | withdrawals: v.withdrawal_requests, 15 | }; 16 | 17 | l2_block.into() 18 | } 19 | 20 | pub fn to_script(script: Script) -> packed::Script { 21 | let ckb_packed_script: ckb_types::packed::Script = script.into(); 22 | packed::Script::new_unchecked(ckb_packed_script.as_bytes()) 23 | } 24 | -------------------------------------------------------------------------------- /packages/api-server/src/cache/types.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from "@ckb-lumos/base"; 2 | import { FilterFlag, RpcFilterRequest } from "../base/filter"; 3 | 4 | export interface FilterCacheInDb { 5 | filter: FilterFlag | RpcFilterRequest; 6 | lastPoll: HexString; 7 | // the filter's last poll record: 8 | // - for eth_newBlockFilter, the last poll record is the block number (bigint) 9 | // - for eth_newPendingTransactionFilter, the last poll record is the pending transaction id (bigint) (currently not support) 10 | // - for normal filter, the last poll record is log_id of log (bigint) 11 | } 12 | 13 | export interface FilterCache { 14 | filter: FilterFlag | RpcFilterRequest; 15 | lastPoll: bigint; 16 | } 17 | 18 | export interface AutoCreateAccountCacheValue { 19 | tx: HexString; 20 | fromAddress: HexString; 21 | } 22 | -------------------------------------------------------------------------------- /packages/api-server/migrations/20220701114448_remove_foreign_keys.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from "knex"; 2 | 3 | export async function up(knex: Knex): Promise { 4 | await knex.schema 5 | .alterTable("transactions", (table) => { 6 | table.dropForeign("block_number"); 7 | }) 8 | .alterTable("logs", (table) => { 9 | table.dropForeign("block_number").dropForeign("transaction_id"); 10 | }); 11 | } 12 | 13 | export async function down(knex: Knex): Promise { 14 | await knex.schema 15 | .alterTable("transactions", (table) => { 16 | table.foreign("block_number").references("blocks.number"); 17 | }) 18 | .alterTable("logs", (table) => { 19 | table.foreign("block_number").references("blocks.number"); 20 | }) 21 | .alterTable("logs", (table) => { 22 | table.foreign("transaction_id").references("transactions.id"); 23 | }); 24 | } 25 | -------------------------------------------------------------------------------- /packages/api-server/src/cache/constant.ts: -------------------------------------------------------------------------------- 1 | // default filter cache setting 2 | export const CACHE_EXPIRED_TIME_MILSECS = 5 * 60 * 1000; // milsec, default 5 minutes 3 | // limit redis store filter size 4 | export const MAX_FILTER_TOPIC_ARRAY_LENGTH = 20; 5 | 6 | // The Cache Key Prfixs 7 | export const GW_RPC_KEY = "gwRPC"; 8 | 9 | export const TX_HASH_MAPPING_PREFIX_KEY = "TxHashMapping"; 10 | export const TX_HASH_MAPPING_CACHE_EXPIRED_TIME_MILSECS = 2 * 60 * 60 * 1000; // 2 hours 11 | export const AUTO_CREATE_ACCOUNT_PREFIX_KEY = "AutoCreateAccount"; 12 | export const AUTO_CREATE_ACCOUNT_CACHE_EXPIRED_TIME_MILSECS = 13 | 2 * 60 * 60 * 1000; // 2 hours 14 | 15 | export const TIP_BLOCK_HASH_CACHE_KEY = "tipBlockHash"; 16 | export const TIP_BLOCK_HASH_CACHE_EXPIRED_TIME_MS = 1000 * 60 * 5; // 5 minutes 17 | 18 | // knex db query cache time 19 | export const QUERY_CACHE_EXPIRED_TIME_MS = 1000 * 45; // 45 seconds ~= block produce interval 20 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | IMAGE_NAME := nervos/godwoken-web3-prebuilds 4 | INDEXER_IMAGE_NAME := nervos/godwoken-web3-indexer-prebuilds 5 | 6 | build-push: 7 | @read -p "Please Enter New Image Tag: " VERSION ; \ 8 | docker build . -t ${IMAGE_NAME}:$$VERSION ; \ 9 | docker push ${IMAGE_NAME}:$$VERSION 10 | 11 | build-test-image: 12 | docker build . -t ${IMAGE_NAME}:latest-test 13 | 14 | build-indexer-image: 15 | @read -p "Please Enter New Indexer Image Tag: " VERSION ; \ 16 | docker build -f ./docker/indexer/Dockerfile . -t ${INDEXER_IMAGE_NAME}:$$VERSION ; \ 17 | 18 | test: 19 | make build-test-image 20 | make test-jq 21 | make test-web3 22 | 23 | test-jq: 24 | docker run --rm ${IMAGE_NAME}:latest-test /bin/bash -c "jq -V" 25 | 26 | test-web3: 27 | docker run --rm -v `pwd`:/app ${IMAGE_NAME}:latest-test /bin/bash -c "cp -r godwoken-web3/node_modules app/node_modules" 28 | yarn check --verify-tree 29 | 30 | migrate: 31 | yarn run migrate:latest 32 | -------------------------------------------------------------------------------- /packages/api-server/tests/base/types/uint32.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { Uint32 } from "../../../src/base/types/uint"; 3 | 4 | const ERROR_MESSAGE = "value to small or too big"; 5 | const value = 100; 6 | const hex = "0x64"; 7 | const littleEndian = "0x64000000"; 8 | 9 | test("Uint32 constructor", (t) => { 10 | t.is(new Uint32(value).getValue(), value); 11 | }); 12 | 13 | test("Uint32 too big", (t) => { 14 | t.throws(() => new Uint32(2 ** 32), undefined, ERROR_MESSAGE); 15 | }); 16 | 17 | test("Uint32 too small", (t) => { 18 | t.throws(() => new Uint32(-1), undefined, ERROR_MESSAGE); 19 | }); 20 | 21 | test("Uint32 toHex", (t) => { 22 | t.is(new Uint32(value).toHex(), hex); 23 | }); 24 | 25 | test("Uint32 fromHex", (t) => { 26 | t.is(Uint32.fromHex(hex).getValue(), value); 27 | }); 28 | 29 | test("Uint32 toLittleEndian", (t) => { 30 | t.is(new Uint32(value).toLittleEndian(), littleEndian); 31 | }); 32 | 33 | test("Uint32 fromLittleEndian", (t) => { 34 | t.is(Uint32.fromLittleEndian(littleEndian).getValue(), value); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/api-server/tests/methods/gw.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { JSONResponse, client } from "../www"; 3 | import * as errcode from "../../src/methods/error-code"; 4 | 5 | test("gw_get_storage_at", async (t) => { 6 | const res: JSONResponse = await client.request(t.title, []); 7 | t.truthy(res.error); 8 | t.is(res.error?.code, errcode.INVALID_PARAMS); 9 | }); 10 | 11 | test("gw_get_nonce", async (t) => { 12 | const res: JSONResponse = await client.request(t.title, ["0xasdf"]); 13 | t.truthy(res.error); 14 | t.is(res.error?.code, errcode.INVALID_PARAMS); 15 | }); 16 | 17 | test("gw_execute_raw_l2transaction", async (t) => { 18 | const res: JSONResponse = await client.request(t.title, [ 19 | "0x5c00000014000000180000001c0000002000000004000000140000001300000038000000ffffff504f4c590000a0724e18090000000000000000000000000000000000000000000000000000000000000000000004000000d504ea1d", 20 | ]); 21 | t.truthy(res.error); 22 | // FIXME https://github.com/nervosnetwork/godwoken-web3/issues/256 23 | // t.is(res.error?.code, errcode.INVALID_PARAMS); 24 | }); 25 | -------------------------------------------------------------------------------- /packages/api-server/tests/base/types/uint64.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { Uint64 } from "../../../src/base/types/uint"; 3 | 4 | const ERROR_MESSAGE = "value to small or too big"; 5 | const value = BigInt("4294967396"); 6 | const hex = "0x100000064"; 7 | const littleEndian = "0x6400000001000000"; 8 | 9 | test("Uint64 constructor", (t) => { 10 | t.is(new Uint64(value).getValue(), value); 11 | }); 12 | 13 | test("Uint64 too big", (t) => { 14 | t.throws(() => new Uint64(2n ** 64n), undefined, ERROR_MESSAGE); 15 | }); 16 | 17 | test("Uint64 too small", (t) => { 18 | t.throws(() => new Uint64(-1n), undefined, ERROR_MESSAGE); 19 | }); 20 | 21 | test("Uint64 toHex", (t) => { 22 | t.is(new Uint64(value).toHex(), hex); 23 | }); 24 | 25 | test("Uint64 fromHex", (t) => { 26 | t.is(Uint64.fromHex(hex).getValue(), value); 27 | }); 28 | 29 | test("Uint64 toLittleEndian", (t) => { 30 | t.is(new Uint64(value).toLittleEndian(), littleEndian); 31 | }); 32 | 33 | test("Uint64 fromLittleEndian", (t) => { 34 | t.is(Uint64.fromLittleEndian(littleEndian).getValue(), value); 35 | }); 36 | -------------------------------------------------------------------------------- /crates/rpc-client/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gw-web3-rpc-client" 3 | version = "0.1.0" 4 | authors = ["Nervos Network"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | serde = { version = "1.0", features = ["derive"] } 11 | serde_json = "1.0" 12 | reqwest = { version = "0.11", features = ["json", "blocking"] } 13 | ckb-jsonrpc-types = "0.100.0" 14 | ckb-types = "0.100.0" 15 | gw-jsonrpc-types = { git = "https://github.com/nervosnetwork/godwoken.git", rev = "6a24f9accd8f4463f122ebe1286412d1f8476247" } 16 | gw-types = { git = "https://github.com/nervosnetwork/godwoken.git", rev = "6a24f9accd8f4463f122ebe1286412d1f8476247" } 17 | gw-common = { git = "https://github.com/nervosnetwork/godwoken.git", rev = "6a24f9accd8f4463f122ebe1286412d1f8476247" } 18 | jsonrpc-core = "17" 19 | rand = "0.8" 20 | anyhow = "1.0" 21 | thiserror = "1.0" 22 | async-jsonrpc-client = { version = "0.3.0", default-features = false, features = ["http-async-std"] } 23 | async-std = "1.12.0" 24 | log = "0.4" 25 | itertools = "0.10.3" 26 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Install Rust components 20 | run: | 21 | cargo fmt --version || rustup component add rustfmt 22 | cargo clippy --version || rustup component add clippy 23 | - uses: actions/cache@v2 24 | with: 25 | path: | 26 | ~/.cargo/registry 27 | ~/.cargo/git 28 | target 29 | key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} 30 | - name: Install moleculec v0.7.2 31 | run: CARGO_TARGET_DIR=target/ cargo install moleculec --locked --version 0.7.2 32 | - name: Build 33 | run: cargo build --verbose 34 | - name: Check format 35 | run: cargo fmt -- --check 36 | - name: Cargo clippy check 37 | env: 38 | RUSTFLAGS: -D warnings 39 | run: cargo clippy 40 | - name: Diff 41 | run: git diff --exit-code 42 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG] " 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## **Version** 11 | - Godwoken v1 or v0? 12 | - (Optional) Get the versions from `poly_version` RPC 13 |
14 | command example 15 | 16 | ```sh 17 | curl https://godwoken-testnet-v1.ckbapp.dev -X POST \ 18 | -H "Content-Type: application/json" \ 19 | -d '{"jsonrpc": "2.0", "method":"poly_version", "params": [], "id": 1}' 20 | ``` 21 |
22 | 23 | ## **Describe the bug** 24 | A clear and concise description of what the bug is. 25 | 26 | **To Reproduce** 27 | 28 | Steps to reproduce the behavior: 29 | 1. Go to '...' 30 | 2. Some actions on '....' 31 | 3. See error 32 | 33 | **Expected behavior** 34 | 35 | A clear and concise description of what you expected to happen. 36 | 37 | **Screenshots or Logs** 38 | 39 | If applicable, add screenshots or logs to help explain your problem. 40 | 41 | ```log 42 | some logs... 43 | ``` 44 | 45 | ## **Additional context** 46 | Add any other context about the problem here. 47 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/modules/prof.ts: -------------------------------------------------------------------------------- 1 | import { cpuProf } from "../../decorator"; 2 | import v8Profiler from "v8-profiler-next"; 3 | import fs from "fs"; 4 | import path from "path"; 5 | 6 | const PROF_TIME_MS = 25000; // 25s 7 | 8 | export class Prof { 9 | constructor() {} 10 | 11 | @cpuProf(PROF_TIME_MS, true) 12 | async cpu(args: any[]): Promise { 13 | const fileName = args[args.length - 1]; 14 | return fileName; 15 | } 16 | 17 | async heap() { 18 | const createHeadDumpFile = async (fileName: string) => { 19 | return new Promise((resolve, reject) => { 20 | const file = fs.createWriteStream(fileName); 21 | const snapshot = v8Profiler.takeSnapshot(); 22 | const transform = snapshot.export(); 23 | transform.pipe(file); 24 | transform.on("finish", () => { 25 | snapshot.delete.bind(snapshot); 26 | resolve(fileName); 27 | }); 28 | transform.on("error", reject); 29 | }); 30 | }; 31 | const fileName = `${Date.now()}.heapsnapshot`; 32 | await createHeadDumpFile(path.join(fileName)); 33 | return fileName; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/api-server/tests/base/types/uint128.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { Uint128 } from "../../../src/base/types/uint"; 3 | 4 | const ERROR_MESSAGE = "value to small or too big"; 5 | const value = BigInt("79228162514264337593543950436"); 6 | const hex = "0x1000000000000000000000064"; 7 | const littleEndian = "0x64000000000000000000000001000000"; 8 | 9 | test("Uint128 constructor", (t) => { 10 | t.is(new Uint128(value).getValue(), value); 11 | }); 12 | 13 | test("Uint128 too big", (t) => { 14 | t.throws(() => new Uint128(2n ** 128n), undefined, ERROR_MESSAGE); 15 | }); 16 | 17 | test("Uint128 too small", (t) => { 18 | t.throws(() => new Uint128(-1n), undefined, ERROR_MESSAGE); 19 | }); 20 | 21 | test("Uint128 toHex", (t) => { 22 | t.is(new Uint128(value).toHex(), hex); 23 | }); 24 | 25 | test("Uint128 fromHex", (t) => { 26 | t.is(Uint128.fromHex(hex).getValue(), value); 27 | }); 28 | 29 | test("Uint128 toLittleEndian", (t) => { 30 | t.is(new Uint128(value).toLittleEndian(), littleEndian); 31 | }); 32 | 33 | test("Uint128 fromLittleEndian", (t) => { 34 | t.is(Uint128.fromLittleEndian(littleEndian).getValue(), value); 35 | }); 36 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/error-code.ts: -------------------------------------------------------------------------------- 1 | // Error code from JSON-RPC 2.0 spec 2 | // reference: http://www.jsonrpc.org/specification#error_object 3 | export const PARSE_ERROR = -32700; 4 | export const INVALID_REQUEST = -32600; 5 | export const METHOD_NOT_FOUND = -32601; 6 | export const INVALID_PARAMS = -32602; 7 | export const INTERNAL_ERROR = -32603; 8 | 9 | // Ethereum Json Rpc compatible error code 10 | // some eth client impl ref: 11 | // - https://github.com/MetaMask/eth-rpc-errors/blob/main/src/error-constants.ts 12 | // - https://infura.io/docs/ethereum#section/Error-codes 13 | export const HEADER_NOT_FOUND_ERROR = -32000; 14 | export const RESOURCE_NOT_FOUND = -32001; 15 | export const RESOURCE_UNAVAILABLE = -32002; 16 | export const TRANSACTION_REJECTED = -32003; 17 | export const METHOD_NOT_SUPPORT = -32004; 18 | export const LIMIT_EXCEEDED = -32005; 19 | export const TRANSACTION_EXECUTION_ERROR = -32000; 20 | 21 | // Polyjuice Chain custom error 22 | // TODO - WEB3_ERROR is pretty generalize error 23 | // later when we have more time, we can split into more detail one 24 | export const WEB3_ERROR = -32099; 25 | export const GW_RPC_REQUEST_ERROR = -32098; 26 | -------------------------------------------------------------------------------- /packages/api-server/src/gasless/utils.ts: -------------------------------------------------------------------------------- 1 | import { HexString, HexNumber } from "@ckb-lumos/base"; 2 | import { logger } from "../base/logger"; 3 | import { EntryPointContract } from "./entrypoint"; 4 | import { decodeGaslessPayload } from "./payload"; 5 | 6 | /** 7 | * Determine whether the eth transaction is a gasless transaction. 8 | */ 9 | export function isGaslessTransaction( 10 | { 11 | to: toAddress, 12 | gasPrice, 13 | data: inputData, 14 | }: { 15 | to: HexString; 16 | gasPrice: HexNumber; 17 | data: HexString; 18 | }, 19 | entrypointContract: EntryPointContract 20 | ): boolean { 21 | // check if gas price is 0 22 | if (BigInt(gasPrice) != 0n) { 23 | return false; 24 | } 25 | 26 | // check if to == entrypoint 27 | if (toAddress != entrypointContract.address) { 28 | return false; 29 | } 30 | 31 | // check input data is GaslessPayload(can be decoded) 32 | try { 33 | decodeGaslessPayload(inputData); 34 | } catch (error: any) { 35 | logger.debug( 36 | "[isGaslessTransaction] try to decode gasless payload failed,", 37 | error.message 38 | ); 39 | return false; 40 | } 41 | 42 | return true; 43 | } 44 | -------------------------------------------------------------------------------- /docs/compatibility.md: -------------------------------------------------------------------------------- 1 | # ETH Compatibility 2 | 3 | ## RPC compatibility 4 | 5 | ### 1. ZERO ADDRESS 6 | 7 | Godwoken does not have the corresponding "zero address"(0x0000000000000000000000000000000000000000) concept, so Polyjuice won't be able to handle zero address as well. 8 | 9 | #### Result 10 | 11 | Transaction with zero address in from/to filed is not supported. 12 | 13 | known issue: #246 14 | 15 | #### Recommend workaround 16 | 17 | - if you are trying to use zero address as a black hole to burn ethers, you can use `transfer function` in `CKB_ERC20_Proxy` to send ethers to zero address. more info can be found in the above section `Transfer Value From EOA To EOA`. 18 | 19 | ### 2. GAS LIMIT 20 | 21 | Godwoken limit the transaction execution resource in CKB-VM with [Cycle Limit](https://docs-xi-two.vercel.app/docs/rfcs/0014-vm-cycle-limits/0014-vm-cycle-limits), we set the `RPC_GAS_LIMIT` to `50000000` for max compatibility with Ethereum toolchain, but the real gas limit you can use depends on such Cycle Limit. 22 | 23 | ## EVM compatibility 24 | 25 | - [Godwoken-Polyjuice](https://github.com/nervosnetwork/godwoken-polyjuice/blob/compatibility-breaking-changes/docs/EVM-compatible.md) 26 | -------------------------------------------------------------------------------- /packages/api-server/tests/base/types/uint256.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { Uint256 } from "../../../src/base/types/uint"; 3 | 4 | const ERROR_MESSAGE = "value to small or too big"; 5 | const value = BigInt( 6 | "26959946667150639794667015087019630673637144422540572481103610249316" 7 | ); 8 | const hex = "0x100000000000000000000000000000000000000000000000000000064"; 9 | const littleEndian = 10 | "0x6400000000000000000000000000000000000000000000000000000001000000"; 11 | 12 | test("Uint256 constructor", (t) => { 13 | t.is(new Uint256(value).getValue(), value); 14 | }); 15 | 16 | test("Uint256 too big", (t) => { 17 | t.throws(() => new Uint256(2n ** 256n), undefined, ERROR_MESSAGE); 18 | }); 19 | 20 | test("Uint256 too small", (t) => { 21 | t.throws(() => new Uint256(-1n), undefined, ERROR_MESSAGE); 22 | }); 23 | 24 | test("Uint256 toHex", (t) => { 25 | t.is(new Uint256(value).toHex(), hex); 26 | }); 27 | 28 | test("Uint256 fromHex", (t) => { 29 | t.is(Uint256.fromHex(hex).getValue(), value); 30 | }); 31 | 32 | test("Uint256 toLittleEndian", (t) => { 33 | t.is(new Uint256(value).toLittleEndian(), littleEndian); 34 | }); 35 | 36 | test("Uint256 fromLittleEndian", (t) => { 37 | t.is(Uint256.fromLittleEndian(littleEndian).getValue(), value); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/api-server/tests/utils/erc20.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { isErc20Transfer } from "../../src/erc20"; 3 | 4 | test("isErc20Transfer", (t) => { 5 | const emptyInput = "0x"; 6 | const illegalInput = "123"; 7 | const uniswapInput = 8 | "0x0dcd7a6c000000000000000000000000f0d9e03b98be925886a5bb9e031920a5c17d333d0000000000000000000000000000000000000000000000000000000013e6c196000000000000000000000000dac17f958d2ee523a2206206994597c13d831ec7000000000000000000000000000000000000000000000000000000006267855000000000000000000000000000000000000000000000000000000000002085cc00000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000004136aff2130612bbad08900110dd05f33609db363ea40b088df3a396ed1da8f4f724c912a2323ca94978d0b6bebc2cae6fe19bb7de7ff19c355394fa4f6900a48e1b00000000000000000000000000000000000000000000000000000000000000"; 9 | const erc20TransferInput = 10 | "0xa9059cbb000000000000000000000000477b8d5ef7c2c42db84deb555419cd817c336b6f000000000000000000000000000000000000000000000000000000001ad27480"; 11 | 12 | t.false(isErc20Transfer(emptyInput)); 13 | t.false(isErc20Transfer(illegalInput)); 14 | t.false(isErc20Transfer(uniswapInput)); 15 | t.true(isErc20Transfer(erc20TransferInput)); 16 | }); 17 | -------------------------------------------------------------------------------- /packages/api-server/tests/utils/gas-price.test.ts: -------------------------------------------------------------------------------- 1 | import { Price } from "../../src/base/gas-price"; 2 | import web3Utils from "web3-utils"; 3 | import test from "ava"; 4 | 5 | test("gas price by ckb price", (t) => { 6 | const ckbPrices = [ 7 | "0.00189", 8 | "0.00199", 9 | "0.002", 10 | "0.0021", 11 | "0.00211", 12 | "0.00221", 13 | "0.00231", 14 | "0.00289", 15 | "0.00299", 16 | "0.003", 17 | "0.0031", 18 | "0.0032", 19 | "0.0033", 20 | "0.0034", 21 | "0.0035", 22 | "0.0036", 23 | "0.0037", 24 | "0.00389", 25 | "0.00489", 26 | "0.00589", 27 | "0.00689", 28 | "0.00789", 29 | "0.00889", 30 | "0.00989", 31 | "0.01", 32 | "0.04", 33 | ]; 34 | for (const p of ckbPrices) { 35 | console.log( 36 | `ckb: $${p}, gasPrice: ${toPCKB( 37 | Price.from(p).toGasPrice() 38 | )} pCKB, minGasPrice: ${toPCKB(Price.from(p).toMinGasPrice())} pCKB` 39 | ); 40 | } 41 | const gasPrices = ckbPrices.map((p) => Price.from(p).toGasPrice()); 42 | const gasPriceSorted = gasPrices.sort((a, b) => +(b - a).toString()); 43 | t.deepEqual(gasPrices, gasPriceSorted); 44 | }); 45 | 46 | function toPCKB(wei: bigint) { 47 | return web3Utils.fromWei(wei.toString(10), "ether"); 48 | } 49 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, cache/restore them, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: 9 | - main 10 | pull_request: 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [14.x, 16.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | cache: 'yarn' 29 | - run: yarn install 30 | - run: yarn run build 31 | - run: yarn workspace @godwoken-web3/api-server run test tests/db/helpers.test.ts 32 | - run: yarn workspace @godwoken-web3/api-server run test tests/utils 33 | - run: yarn workspace @godwoken-web3/api-server run test tests/base/types 34 | - run: yarn run fmt 35 | - run: yarn run lint 36 | - run: git diff --exit-code 37 | -------------------------------------------------------------------------------- /crates/indexer/src/pool.rs: -------------------------------------------------------------------------------- 1 | use std::time::Duration; 2 | 3 | use sqlx::{ 4 | postgres::{PgConnectOptions, PgPoolOptions}, 5 | ConnectOptions, PgPool, 6 | }; 7 | 8 | use crate::config::load_indexer_config; 9 | 10 | lazy_static::lazy_static! { 11 | pub static ref POOL: PgPool = { 12 | let indexer_config = load_indexer_config("./indexer-config.toml").unwrap(); 13 | 14 | let mut opts: PgConnectOptions = indexer_config.pg_url.parse().expect("pg url parse error"); 15 | opts.log_statements(log::LevelFilter::Debug) 16 | .log_slow_statements(log::LevelFilter::Warn, Duration::from_secs(5)); 17 | PgPoolOptions::new() 18 | .max_connections(5) 19 | .connect_lazy_with(opts) 20 | }; 21 | 22 | // Adapt slow query duration to 30s, and enlarge max connections 23 | pub static ref POOL_FOR_UPDATE: PgPool = { 24 | let indexer_config = load_indexer_config("./indexer-config.toml").unwrap(); 25 | 26 | let mut opts: PgConnectOptions = indexer_config.pg_url.parse().expect("pg url parse error"); 27 | opts.log_statements(log::LevelFilter::Debug) 28 | .log_slow_statements(log::LevelFilter::Warn, Duration::from_secs(30)); 29 | PgPoolOptions::new() 30 | .max_connections(20) 31 | .connect_lazy_with(opts) 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/modules/net.ts: -------------------------------------------------------------------------------- 1 | import { HexNumber } from "@ckb-lumos/base"; 2 | import { isListening } from "../../app/app"; 3 | import { gwConfig } from "../../base/index"; 4 | 5 | export class Net { 6 | constructor() {} 7 | 8 | /** 9 | * Returns the current net version 10 | * @param {Array<*>} [params] An empty array 11 | * @param {Function} [cb] A function with an error object as the first argument and the 12 | * net version as the second argument 13 | */ 14 | version(_args: []): string { 15 | return BigInt(gwConfig.web3ChainId).toString(10); 16 | } 17 | 18 | /** 19 | * Returns the current peer nodes number, which is always 0 since godwoken is not implementing p2p network 20 | * @param {Array<*>} [params] An empty array 21 | * @param {Function} [cb] A function with an error object as the first argument and the 22 | * current peer nodes number as the second argument 23 | */ 24 | peerCount(_args: []): HexNumber { 25 | return "0x0"; 26 | } 27 | 28 | /** 29 | * Returns if the client is currently listening 30 | * @param {Array<*>} [params] An empty array 31 | * @param {Function} [cb] A function with an error object as the first argument and the 32 | * boolean as the second argument 33 | */ 34 | listening(_args: []): boolean { 35 | return isListening(); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/api-server/src/base/types/node-info.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HexNumber, HexString, Script } from "@ckb-lumos/base"; 2 | import { 3 | EoaScriptType, 4 | BackendType, 5 | NodeMode, 6 | GwScriptType, 7 | } from "@godwoken-web3/godwoken"; 8 | 9 | export interface EoaScript { 10 | typeHash: Hash; 11 | script: Script; 12 | eoaType: EoaScriptType; 13 | } 14 | 15 | export interface BackendInfo { 16 | validatorCodeHash: Hash; 17 | generatorCodeHash: Hash; 18 | validatorScriptTypeHash: Hash; 19 | backendType: BackendType; 20 | } 21 | 22 | export interface GwScript { 23 | typeHash: Hash; 24 | script: Script; 25 | scriptType: GwScriptType; 26 | } 27 | 28 | export interface RollupCell { 29 | typeHash: Hash; 30 | typeScript: Script; 31 | } 32 | 33 | export interface RollupConfig { 34 | requiredStakingCapacity: HexNumber; 35 | challengeMaturityBlocks: HexNumber; 36 | finalityBlocks: HexNumber; 37 | rewardBurnRate: HexNumber; 38 | chainId: HexNumber; 39 | } 40 | 41 | export interface GaslessTxSupport { 42 | entrypointAddress: HexString; 43 | } 44 | 45 | export interface NodeInfo { 46 | backends: Array; 47 | eoaScripts: Array; 48 | gwScripts: Array; 49 | rollupCell: RollupCell; 50 | rollupConfig: RollupConfig; 51 | version: string; 52 | mode: NodeMode; 53 | gaslessTxSupport?: GaslessTxSupport; 54 | } 55 | -------------------------------------------------------------------------------- /packages/api-server/src/base/logger.ts: -------------------------------------------------------------------------------- 1 | import expressWinston from "express-winston"; 2 | import { logger, winstonLogger } from "@godwoken-web3/godwoken"; 3 | 4 | export { logger }; 5 | 6 | export const expressLogger = expressWinston.logger({ 7 | transports: winstonLogger.transports, 8 | format: winstonLogger.format, 9 | meta: false, // optional: control whether you want to log the meta data about the request (default to true) 10 | msg: "{{req.method}} {{req.url}} {{res.statusCode}} {{res.responseTime}}ms @{{ Array.isArray(req.body) ? req.body.map((o) => o?.method) : req.body?.method }}", // optional: customize the default logging message. E.g. "{{res.statusCode}} {{req.method}} {{res.responseTime}}ms {{req.url}}" 11 | expressFormat: false, // Use the default Express/morgan request formatting. Enabling this will override any msg if true. Will only output colors with colorize set to true 12 | colorize: true, // Color the text and status code, using the Express/morgan color palette (text: gray, status: default green, 3XX cyan, 4XX yellow, 5XX red). 13 | // dynamicMeta: (req, res) => { 14 | // const rpcRequest: any = {} 15 | // const meta: any = {}; 16 | // if (req) { 17 | // meta.rpc = rpcRequest 18 | // rpcRequest.methods = Array.isArray(req.body) ? req.body.map((o) => o?.method) : [req.body?.method] 19 | // } 20 | // return meta 21 | // } 22 | }); 23 | -------------------------------------------------------------------------------- /crates/indexer/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gw-web3-indexer" 3 | version = "0.1.0" 4 | authors = ["Nervos Network"] 5 | edition = "2021" 6 | 7 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 8 | 9 | [dependencies] 10 | gw-types = { git = "https://github.com/nervosnetwork/godwoken.git", rev = "6a24f9accd8f4463f122ebe1286412d1f8476247" } 11 | gw-common = { git = "https://github.com/nervosnetwork/godwoken.git", rev = "6a24f9accd8f4463f122ebe1286412d1f8476247" } 12 | gw-jsonrpc-types = { git = "https://github.com/nervosnetwork/godwoken.git", rev = "6a24f9accd8f4463f122ebe1286412d1f8476247" } 13 | ckb-hash = "0.100.0" 14 | ckb-types = "0.100.0" 15 | anyhow = { version = "1.0", features = ["backtrace"] } 16 | smol = "1.2.5" 17 | thiserror = "1.0" 18 | sqlx = { version = "0.6.0", features = [ "runtime-async-std-native-tls", "postgres", "chrono", "decimal", "bigdecimal" ] } 19 | rust_decimal = "1.10.3" 20 | num-bigint = "0.4" 21 | faster-hex = "0.5.0" 22 | log = "0.4" 23 | rlp = "0.5" 24 | sha3 = "0.9.1" 25 | ethabi = "15.0.0" 26 | toml = "0.5" 27 | serde = { version = "1.0", features = ["derive"] } 28 | serde_json = "1.0" 29 | env_logger = "0.8.3" 30 | jsonrpc-core = "18.0" 31 | gw-web3-rpc-client = { path = "../rpc-client" } 32 | lazy_static = "1.4" 33 | sentry = "0.23.0" 34 | sentry-log = "0.23.0" 35 | dotenv = "0.15.0" 36 | rayon = "1.5.3" 37 | futures = "0.3.21" 38 | itertools = "0.10.3" 39 | num_cpus = "1.0" 40 | -------------------------------------------------------------------------------- /packages/api-server/src/decorator.ts: -------------------------------------------------------------------------------- 1 | import { asyncSleep } from "./util"; 2 | import path from "path"; 3 | import fs from "fs"; 4 | import v8Profiler from "v8-profiler-next"; 5 | 6 | export function cpuProf( 7 | timeMs?: number, 8 | returnFileName: boolean = false, 9 | _fileName?: string 10 | ) { 11 | return function ( 12 | _target: any, 13 | _name: string, 14 | descriptor: TypedPropertyDescriptor<(args: any[]) => Promise> 15 | ) { 16 | const oldFunc = descriptor.value; 17 | descriptor.value = async function (p: any[]) { 18 | // set generateType 1 to generate new format for cpuprofile 19 | // to be compatible with cpuprofile parsing in vscode. 20 | v8Profiler.setGenerateType(1); 21 | v8Profiler.startProfiling("CPU profile"); 22 | 23 | const fileName = _fileName || `${Date.now()}.cpuprofile`; 24 | const cpuprofile = path.join(fileName); 25 | 26 | const params = p; 27 | if (returnFileName === true) { 28 | params.push(fileName); 29 | } 30 | 31 | const result = await oldFunc?.apply(this, [params]); 32 | 33 | // stop profile 34 | if (timeMs != null) { 35 | await asyncSleep(timeMs); 36 | } 37 | const profile = v8Profiler.stopProfiling(); 38 | profile 39 | .export() 40 | .pipe(fs.createWriteStream(cpuprofile)) 41 | .on("finish", () => profile.delete()); 42 | 43 | return result; 44 | }; 45 | return descriptor; 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/modules/web3.ts: -------------------------------------------------------------------------------- 1 | import { getClientVersion } from "../../util"; 2 | import { addHexPrefix, keccak, toBuffer } from "ethereumjs-util"; 3 | import { middleware, validators } from "../validator"; 4 | import { Hash } from "@ckb-lumos/base"; 5 | import { Web3Error } from "../error"; 6 | 7 | export class Web3 { 8 | constructor() { 9 | this.sha3 = middleware(this.sha3.bind(this), 1, [validators.hexString]); 10 | } 11 | 12 | /** 13 | * eg: "godwoken/v1.0/linux-amd64/rust1.47" 14 | * 15 | * Returns the current client version 16 | * @param {Array<*>} [params] An empty array 17 | * @param {Function} [cb] A function with an error object as the first argument and the 18 | * client version as the second argument 19 | */ 20 | clientVersion(args: []): string { 21 | return getClientVersion(); 22 | } 23 | 24 | /** 25 | * Returns Keccak-256 (not the standardized SHA3-256) of the given data 26 | * @param {Array} [params] The data to convert into a SHA3 hash 27 | * @param {Function} [cb] A function with an error object as the first argument and the 28 | * Keccak-256 hash of the given data as the second argument 29 | */ 30 | sha3(args: string[]): Hash { 31 | try { 32 | const rawDigest = keccak(toBuffer(args[0])); 33 | const hexEncodedDigest = addHexPrefix(rawDigest.toString("hex")); 34 | return hexEncodedDigest; 35 | } catch (err: any) { 36 | throw new Web3Error(err.message); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "godwoken-web3", 3 | "main": "index.js", 4 | "license": "MIT", 5 | "private": true, 6 | "workspaces": [ 7 | "packages/godwoken", 8 | "packages/api-server", 9 | "packages/*" 10 | ], 11 | "scripts": { 12 | "build:godwoken": "yarn workspace @godwoken-web3/godwoken run build", 13 | "build:api-server": "yarn workspace @godwoken-web3/api-server run build", 14 | "build": "yarn workspaces run build", 15 | "fmt": "yarn workspaces run fmt", 16 | "lint": "yarn workspaces run lint", 17 | "knex": "yarn workspace @godwoken-web3/api-server run knex", 18 | "migrate:make": "yarn workspace @godwoken-web3/api-server run migrate:make", 19 | "migrate:latest": "yarn workspace @godwoken-web3/api-server run migrate:latest", 20 | "migrate-accounts": "yarn workspace @godwoken-web3/api-server run migrate-accounts", 21 | "start": "yarn workspace @godwoken-web3/api-server run start", 22 | "start:prod": "yarn workspace @godwoken-web3/api-server run start:prod", 23 | "start:pm2": "yarn workspace @godwoken-web3/api-server run start:pm2", 24 | "test": "yarn workspace @godwoken-web3/api-server run test", 25 | "cli": "yarn workspace @godwoken-web3/api-server run cli" 26 | }, 27 | "devDependencies": { 28 | "@typescript-eslint/eslint-plugin": "^5.8.0", 29 | "@typescript-eslint/parser": "^5.8.0", 30 | "eslint": "^8.5.0", 31 | "prettier": "^2.2.1" 32 | }, 33 | "version": "1.10.0-rc2", 34 | "author": "hupeng " 35 | } 36 | -------------------------------------------------------------------------------- /packages/api-server/src/middlewares/jayson.ts: -------------------------------------------------------------------------------- 1 | import jayson from "jayson"; 2 | import { instantFinalityHackMethods, methods } from "../methods/index"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import createServer from "connect"; 5 | import { isInstantFinalityHackMode } from "../util"; 6 | import { ResponseHeader, setResponseHeader } from "./header"; 7 | 8 | const server = new jayson.Server(methods); 9 | const instantFinalityHackServer = new jayson.Server(instantFinalityHackMethods); 10 | 11 | export const jaysonMiddleware = ( 12 | req: Request, 13 | res: Response, 14 | next: NextFunction 15 | ) => { 16 | // we use jayson, and {params: null} will be treated as illegal while undefined will not. 17 | // because this line of code https://github.com/tedeh/jayson/blob/master/lib/utils.js#L331 18 | if (req.body && req.body.params == null) { 19 | req.body.params = [] as any[]; 20 | } 21 | 22 | const enableInstantFinality = isInstantFinalityHackMode(req); 23 | 24 | // manage response header here 25 | const header: ResponseHeader = { 26 | instantFinality: enableInstantFinality, 27 | }; 28 | setResponseHeader(res, header); 29 | 30 | // enable additional feature for special URL 31 | if (enableInstantFinality) { 32 | const middleware = 33 | instantFinalityHackServer.middleware() as createServer.NextHandleFunction; 34 | return middleware(req, res, next); 35 | } 36 | 37 | const middleware = server.middleware() as createServer.NextHandleFunction; 38 | return middleware(req, res, next); 39 | }; 40 | -------------------------------------------------------------------------------- /packages/api-server/cli/README.md: -------------------------------------------------------------------------------- 1 | # CLI 2 | 3 | ### Fix wrong transaction.eth_tx_hash 4 | 5 | This problem fixed in `v1.5.1-rc1`, so data indexed by version `>= v1.5.1-rc1` is ok. 6 | 7 | Run `list-wrong-eth-tx-hashes` to see how many wrong data in database. 8 | 9 | `eth_tx_hash` was wrong before when R or S with leading zeros in database. 10 | 11 | Run once after `web3-indexer` stopped is enough. 12 | 13 | ``` 14 | // List first 20 txs, database-url can also read from env 15 | yarn run cli list-wrong-eth-tx-hashes -d 16 | yarn run cli list-wrong-eth-tx-hashes --help // for more info 17 | 18 | // Fix wrong data 19 | // database-url can also read from env, and chain-id can also read from RPC, using `yarn run cli fix-eth-tx-hash --help` for more infomation. 20 | yarn run cli fix-eth-tx-hash -d -c 21 | yarn run cli fix-eth-tx-hash --help // for more info 22 | ``` 23 | 24 | ### Fix wrong log.transaction_index 25 | 26 | Run `wrong-log-transaction-index-count` to see how many wrong data in database. 27 | 28 | `log.transaction_index` always be zero when indexed after version `v1.6.0-rc1`. 29 | 30 | Run once after `web3-indexer` updated is enough. 31 | 32 | ``` 33 | // Get count, database-url can also read from env 34 | yarn run cli wrong-log-transaction-index-count -d 35 | yarn run cli wrong-log-transaction-index-count --help // for more info 36 | 37 | // Fix wrong data 38 | // database-url can also read from env 39 | yarn run cli fix-log-transaction-index -d 40 | yarn run cli fix-log-transaction-index --help // for more info 41 | ``` 42 | -------------------------------------------------------------------------------- /packages/api-server/src/app/cluster.ts: -------------------------------------------------------------------------------- 1 | import { startOpentelemetry } from "../opentelemetry"; 2 | // Start before logger 3 | startOpentelemetry(); 4 | 5 | import cluster from "cluster"; 6 | import { cpus } from "os"; 7 | import { envConfig } from "../base/env-config"; 8 | import { logger } from "../base/logger"; 9 | import { BlockEmitter } from "../block-emitter"; 10 | import { CKBPriceOracle } from "../price-oracle"; 11 | import { initSentry } from "../sentry"; 12 | 13 | const numCPUs = cpus().length; 14 | const clusterCount = +(envConfig.clusterCount || 0); 15 | const numOfCluster = clusterCount || numCPUs; 16 | 17 | if (cluster.isMaster) { 18 | logger.info(`Master ${process.pid} is running`); 19 | 20 | initSentry(); 21 | 22 | // Fork workers. 23 | for (let i = 0; i < numOfCluster; i++) { 24 | cluster.fork(); 25 | } 26 | 27 | const blockEmitter = new BlockEmitter(); 28 | blockEmitter.startForever(); 29 | 30 | if (envConfig.enablePriceOracle == "true") { 31 | const ckbPriceOracle = new CKBPriceOracle(); 32 | ckbPriceOracle.startForever(); 33 | } 34 | 35 | cluster.on("exit", (worker, _code, _signal) => { 36 | if (worker.process.exitCode === 0) { 37 | logger.warn( 38 | `Worker ${worker.id} (pid: ${worker.process.pid}) died peacefully...` 39 | ); 40 | } else { 41 | logger.error( 42 | `Worker ${worker.id} (pid: ${worker.process.pid}) died with exit code ${worker.process.exitCode}, restarting it` 43 | ); 44 | cluster.fork(); 45 | } 46 | }); 47 | } else { 48 | require("./www"); 49 | 50 | logger.info(`Worker ${process.pid} started`); 51 | } 52 | -------------------------------------------------------------------------------- /packages/godwoken/scripts/regenerate-schemas.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | download(){ 4 | curl -L https://raw.githubusercontent.com/nervosnetwork/godwoken/a5531598ae630990d0b9803642c32015ef04e46e/crates/types/schemas/$1.mol -o tmp/$1.mol 5 | } 6 | 7 | generate(){ 8 | moleculec --language - --schema-file tmp/$1.mol --format json > tmp/$1.json 9 | ./molecule-es/moleculec-es -hasBigInt -inputFile tmp/$1.json -outputFile schemas/$1.esm.js -generateTypeScriptDefinition 10 | rollup -f umd -i schemas/$1.esm.js -o schemas/$1.js --name $2 11 | mv schemas/$1.esm.d.ts schemas/$1.d.ts 12 | mv tmp/$1.json schemas/$1.json 13 | } 14 | 15 | rename_godwoken(){ 16 | for i in ./schemas/godwoken.* ; do mv "$i" "${i/godwoken/index}" ; done 17 | } 18 | 19 | # require moleculec 0.7.2 20 | MOLC=moleculec 21 | MOLC_VERSION=0.7.2 22 | if [ ! -x "$(command -v "${MOLC}")" ] \ 23 | || [ "$(${MOLC} --version | awk '{ print $2 }' | tr -d ' ')" != "${MOLC_VERSION}" ]; then \ 24 | echo "Require moleculec v0.7.2, please run 'cargo install moleculec --locked --version 0.7.2' to install."; \ 25 | fi 26 | 27 | # download molecylec-es, must be v0.3.1 28 | DIR=molecule-es 29 | mkdir -p $DIR 30 | FILENAME=moleculec-es_0.3.1_$(uname -s)_$(uname -m).tar.gz 31 | curl -L https://github.com/nervosnetwork/moleculec-es/releases/download/0.3.1/${FILENAME} -o ${DIR}/${FILENAME} 32 | tar xzvf $DIR/$FILENAME -C $DIR 33 | 34 | mkdir -p tmp 35 | mkdir -p schemas 36 | 37 | download "blockchain" 38 | download "godwoken" 39 | download "store" 40 | 41 | generate "godwoken" "Godwoken" 42 | rename_godwoken 43 | rm -rf ../schemas 44 | mv schemas ../schemas 45 | 46 | rm -rf tmp 47 | rm -rf $DIR 48 | -------------------------------------------------------------------------------- /packages/godwoken/src/logger.ts: -------------------------------------------------------------------------------- 1 | import util from "util"; 2 | import winston, { format } from "winston"; 3 | 4 | // Don't import from `envConfig` 5 | const loggerEnv: { [key: string]: string | undefined } = { 6 | logLevel: process.env.LOG_LEVEL, 7 | logFormat: process.env.LOG_FORMAT, 8 | }; 9 | 10 | const normalFormat = format.printf(({ level, message, timestamp }) => { 11 | return `${timestamp} [${level}]: ${message}`; 12 | }); 13 | 14 | // For development, set default log level to debug 15 | // For production, set default log level to info 16 | let logLevel = loggerEnv.logLevel; 17 | if (logLevel == null && process.env.NODE_ENV === "production") { 18 | logLevel = "info"; 19 | } else if (logLevel == null) { 20 | logLevel = "debug"; 21 | } 22 | 23 | let logFormat: winston.Logform.Format = format.combine( 24 | format.colorize(), 25 | format.timestamp(), 26 | normalFormat 27 | ); 28 | if (loggerEnv.logFormat === "json") { 29 | logFormat = format.combine( 30 | format.uncolorize(), 31 | format.timestamp(), 32 | format.json() 33 | ); 34 | } 35 | 36 | // Export for api-server 37 | export const winstonLogger = winston.createLogger({ 38 | level: logLevel, 39 | format: logFormat, 40 | transports: [new winston.transports.Console()], 41 | }); 42 | 43 | const formatArgs = (args: any[]): string => 44 | args.map((arg) => util.format(arg)).join(" "); 45 | 46 | export const logger = { 47 | debug: (...args: any[]) => winstonLogger.debug(formatArgs(args)), 48 | info: (...args: any[]) => winstonLogger.info(formatArgs(args)), 49 | warn: (...args: any[]) => winstonLogger.warn(formatArgs(args)), 50 | error: (...args: any[]) => winstonLogger.error(formatArgs(args)), 51 | }; 52 | -------------------------------------------------------------------------------- /packages/api-server/src/cache/store.ts: -------------------------------------------------------------------------------- 1 | require("newrelic"); 2 | import { RedisClientType } from "redis"; 3 | import { CACHE_EXPIRED_TIME_MILSECS } from "../cache/constant"; 4 | import { globalClient, SetOptions } from "./redis"; 5 | 6 | export class Store { 7 | private client: RedisClientType; 8 | private setOptions: SetOptions; 9 | 10 | constructor(enableExpired?: boolean, keyExpiredTimeMilSecs?: number) { 11 | this.client = globalClient; 12 | if (enableExpired == null) { 13 | enableExpired = false; 14 | } 15 | 16 | this.setOptions = enableExpired 17 | ? { 18 | PX: keyExpiredTimeMilSecs || CACHE_EXPIRED_TIME_MILSECS, 19 | } 20 | : {}; 21 | } 22 | 23 | async insert( 24 | key: string, 25 | value: string | number, 26 | expiredTimeMilSecs?: number 27 | ) { 28 | let setOptions = this.setOptions; 29 | const PX = expiredTimeMilSecs || this.setOptions.PX; 30 | if (PX) { 31 | setOptions.PX = PX; 32 | } 33 | 34 | return await this.client.set(key, value.toString(), setOptions); 35 | } 36 | 37 | async delete(key: string) { 38 | // use unlink instead of DEL to avoid blocking 39 | return await this.client.unlink(key); 40 | } 41 | 42 | async get(key: string) { 43 | return await this.client.get(key); 44 | } 45 | 46 | async size() { 47 | return await this.client.dbSize(); 48 | } 49 | 50 | async addSet(name: string, members: string | string[]) { 51 | return await this.client.sAdd(name, members); 52 | } 53 | 54 | async incr(key: string) { 55 | return await this.client.incr(key); 56 | } 57 | 58 | async ttl(key: string) { 59 | return await this.client.ttl(key); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /packages/api-server/src/gasless/entrypoint.ts: -------------------------------------------------------------------------------- 1 | import { HexNumber, HexString } from "@ckb-lumos/base"; 2 | import { GodwokenClient } from "@godwoken-web3/godwoken"; 3 | import { Uint32 } from "../base/types/uint"; 4 | 5 | export class EntryPointContract { 6 | public readonly address: HexString; 7 | private iAccountId: number | undefined; 8 | private rpc: GodwokenClient; 9 | private registryAccountId: HexNumber; 10 | 11 | constructor( 12 | rpcOrUrl: GodwokenClient | string, 13 | address: HexString, 14 | registryAccountId: HexNumber 15 | ) { 16 | if (typeof rpcOrUrl === "string") { 17 | this.rpc = new GodwokenClient(rpcOrUrl); 18 | } else { 19 | this.rpc = rpcOrUrl; 20 | } 21 | 22 | this.address = address; 23 | this.registryAccountId = registryAccountId; 24 | } 25 | 26 | async init() { 27 | const registry = 28 | "0x" + 29 | new Uint32(+this.registryAccountId).toLittleEndian().slice(2) + 30 | new Uint32(20).toLittleEndian().slice(2) + 31 | this.address.slice(2); 32 | 33 | const scriptHash = await this.rpc.getScriptHashByRegistryAddress(registry); 34 | if (scriptHash == null) { 35 | throw new Error( 36 | `script hash not found by registry(${registry}) from entrypoint address(${this.address})` 37 | ); 38 | } 39 | 40 | const accountId = await this.rpc.getAccountIdByScriptHash(scriptHash); 41 | if (accountId == null) { 42 | throw new Error( 43 | `account id not found by script hash(${scriptHash}) from entrypoint address(${this.address})` 44 | ); 45 | } 46 | 47 | this.iAccountId = accountId; 48 | } 49 | 50 | public get accountId(): number { 51 | return this.iAccountId!; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/types.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HexNumber, HexString } from "@ckb-lumos/base"; 2 | export type Error = { 3 | code?: number; 4 | message: string; 5 | } | null; 6 | 7 | export type SyningStatus = 8 | | false 9 | | { 10 | startingBlock: number; 11 | currentBlock: number; 12 | highestBlock: number; 13 | }; 14 | 15 | export type Response = number | string | boolean | SyningStatus | Array; 16 | 17 | export type Callback = (err: Error, res?: any | Response) => void; 18 | 19 | export type BlockTag = "latest" | "earliest" | "pending"; 20 | 21 | // Eip1898 support block hash and block number 22 | export interface BlockSpecifier { 23 | blockNumber?: HexNumber; 24 | blockHash?: Hash; 25 | requireCanonical?: boolean; 26 | } 27 | 28 | export type BlockParameter = HexNumber | BlockTag | BlockSpecifier; 29 | 30 | export interface TransactionCallObject { 31 | from?: HexString; 32 | to: HexString; 33 | gas?: HexNumber; 34 | gasPrice?: HexNumber; 35 | value?: HexNumber; 36 | data?: HexNumber; 37 | } 38 | export interface LogItem { 39 | account_id: HexNumber; 40 | service_flag: HexNumber; 41 | data: HexString; 42 | } 43 | export interface SudtOperationLog { 44 | sudtId: number; 45 | fromId: number; 46 | toId: number; 47 | amount: bigint; 48 | } 49 | 50 | export interface SudtPayFeeLog { 51 | sudtId: number; 52 | fromId: number; 53 | blockProducerId: number; 54 | amount: bigint; 55 | } 56 | 57 | export interface PolyjuiceSystemLog { 58 | gasUsed: bigint; 59 | cumulativeGasUsed: bigint; 60 | createdAddress: string; 61 | statusCode: number; 62 | } 63 | 64 | export interface PolyjuiceUserLog { 65 | address: HexString; 66 | data: HexString; 67 | topics: HexString[]; 68 | } 69 | 70 | export type GodwokenLog = 71 | | SudtOperationLog 72 | | SudtPayFeeLog 73 | | PolyjuiceSystemLog 74 | | PolyjuiceUserLog; 75 | -------------------------------------------------------------------------------- /packages/api-server/cli/index.ts: -------------------------------------------------------------------------------- 1 | import { Command } from "commander"; 2 | import { fixEthTxHashRun, listWrongEthTxHashesRun } from "./fix-eth-tx-hash"; 3 | import { version as packageVersion } from "../package.json"; 4 | import { 5 | fixLogTransactionIndexRun, 6 | wrongLogTransactionIndexCountRun, 7 | } from "./fix-log-transaction-index"; 8 | 9 | const program = new Command(); 10 | program.version(packageVersion); 11 | 12 | program 13 | .command("fix-eth-tx-hash") 14 | .description("Fix eth_tx_hash in database where R or S with leading zeros") 15 | .option( 16 | "-d, --database-url ", 17 | "If not provide, will use env `DATABASE_URL`, throw error if not provided too", 18 | undefined 19 | ) 20 | .option( 21 | "-c, --chain-id ", 22 | "Godwoken chain id, if not provoide, will get from RPC", 23 | undefined 24 | ) 25 | .option("-r, --rpc ", "Godwoken / Web3 RPC url", "http://127.0.0.1:8024") 26 | .action(fixEthTxHashRun); 27 | 28 | program 29 | .command("list-wrong-eth-tx-hashes") 30 | .description( 31 | "List transactions which R or S with leading zeros, only list first 20 txs" 32 | ) 33 | .option("-d, --database-url ", "database url", undefined) 34 | .action(listWrongEthTxHashesRun); 35 | 36 | program 37 | .command("fix-log-transaction-index") 38 | .description("Fix wrong log's transaction_index") 39 | .option( 40 | "-d, --database-url ", 41 | "If not provide, will use env `DATABASE_URL`, throw error if not provided too", 42 | undefined 43 | ) 44 | .action(fixLogTransactionIndexRun); 45 | 46 | program 47 | .command("wrong-log-transaction-index-count") 48 | .description("Get log's count which transaction_index is wrong") 49 | .option("-d, --database-url ", "database url", undefined) 50 | .action(wrongLogTransactionIndexCountRun); 51 | 52 | program.parse(); 53 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/constant.ts: -------------------------------------------------------------------------------- 1 | // source: https://github.com/nervosnetwork/godwoken-polyjuice/blob/main/docs/EVM-compatible.md 2 | export const POLY_MAX_BLOCK_GAS_LIMIT = 12500000; 3 | export const POLY_MAX_TRANSACTION_GAS_LIMIT = 12500000; 4 | export const POLY_BLOCK_DIFFICULTY = BigInt("2500000000000000"); 5 | 6 | export const RPC_MAX_GAS_LIMIT = 50000000; 7 | 8 | export const TX_GAS = 21000; 9 | export const TX_GAS_CONTRACT_CREATION = 53000; 10 | export const TX_DATA_ZERO_GAS = 4; 11 | export const TX_DATA_NONE_ZERO_GAS = 16; 12 | 13 | export const ZERO_ETH_ADDRESS = `0x${"0".repeat(40)}`; 14 | export const DEFAULT_LOGS_BLOOM = "0x" + "00".repeat(256); 15 | 16 | export const POLYJUICE_SYSTEM_PREFIX = 255; 17 | export const POLYJUICE_CONTRACT_CODE = 1; 18 | // export const POLYJUICE_DESTRUCTED = 2; 19 | // export const GW_KEY_BYTES = 32; 20 | export const GW_ACCOUNT_KV = 0; 21 | export const CKB_SUDT_ID = "0x1"; 22 | export const META_CONTRACT_ID = "0x0"; 23 | export const SUDT_OPERATION_LOG_FLAG = "0x0"; 24 | export const SUDT_PAY_FEE_LOG_FLAG = "0x1"; 25 | export const POLYJUICE_SYSTEM_LOG_FLAG = "0x2"; 26 | export const POLYJUICE_USER_LOG_FLAG = "0x3"; 27 | 28 | export const HEADER_NOT_FOUND_ERR_MESSAGE = "header not found"; 29 | 30 | export const COMPATIBLE_DOCS_URL = 31 | "https://github.com/nervosnetwork/godwoken-web3/blob/main/docs/compatibility.md"; 32 | 33 | // 128kb 34 | // see also https://github.com/ethereum/go-ethereum/blob/b3b8b268eb585dfd3c1c9e9bbebc55968f3bec4b/core/tx_pool.go#L43-L53 35 | export const MAX_TRANSACTION_SIZE = BigInt("131072"); 36 | 37 | // https://github.com/nervosnetwork/godwoken/blob/develop/crates/eoa-mapping/src/eth_register.rs#L16 38 | export const MAX_ADDRESS_SIZE_PER_REGISTER_BATCH = 50; 39 | 40 | export const AUTO_CREATE_ACCOUNT_FROM_ID = "0x0"; 41 | 42 | // if sync behind 3 blocks, something went wrong 43 | export const MAX_ALLOW_SYNC_BLOCKS_DIFF = 3; 44 | -------------------------------------------------------------------------------- /packages/api-server/src/cache/redis.ts: -------------------------------------------------------------------------------- 1 | import { RedisClientType, createClient } from "redis"; 2 | import { envConfig } from "../base/env-config"; 3 | import { logger } from "../base/logger"; 4 | 5 | // redis SET type 6 | // take from https://github.com/redis/node-redis/blob/2a7a7c1c2e484950ceb57497f786658dacf19127/lib/commands/SET.ts 7 | export type MaximumOneOf = K extends keyof T 8 | ? { [P in K]?: T[K] } & Partial, never>> 9 | : never; 10 | export type SetTTL = MaximumOneOf<{ 11 | EX: number; 12 | PX: number; 13 | EXAT: number; 14 | PXAT: number; 15 | KEEPTTL: true; 16 | }>; 17 | export type SetGuards = MaximumOneOf<{ 18 | NX: true; 19 | XX: true; 20 | }>; 21 | export interface SetCommonOptions { 22 | GET?: true; 23 | } 24 | export type SetOptions = SetTTL & SetGuards & SetCommonOptions; 25 | // endOf Typing 26 | 27 | const maxretries = 100; 28 | 29 | // create global redis client 30 | export const globalClient: RedisClientType = createClient({ 31 | url: envConfig.redisUrl, 32 | socket: { 33 | reconnectStrategy: (attempts) => { 34 | logger.debug(`[RedisGlobalClient] reconnecting attempt ${attempts}..`); 35 | if (attempts > maxretries) { 36 | return new Error( 37 | `[RedisGlobalClient] failed to connect to ${envConfig.redisUrl} in ${maxretries} attempts` 38 | ); 39 | } 40 | // default wait time: https://github.com/redis/node-redis/blob/HEAD/docs/client-configuration.md#reconnect-strategy 41 | return Math.min(attempts * 50, 500); 42 | }, 43 | }, 44 | }); 45 | 46 | globalClient.on("connect", () => { 47 | logger.debug("[RedisGlobalClient] connected."); 48 | }); 49 | globalClient.on("error", (err: any) => 50 | logger.error("[RedisGlobalClient] Error =>", err) 51 | ); 52 | globalClient.on("end", () => 53 | logger.debug("[RedisGlobalClient] connection terminated..") 54 | ); 55 | 56 | globalClient.connect(); 57 | -------------------------------------------------------------------------------- /.github/workflows/unit-tests.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | - develop 8 | pull_request: 9 | workflow_dispatch: 10 | 11 | env: 12 | # Environment variables propagated to godwoken-kicker 13 | MANUAL_BUILD_WEB3: "true" 14 | MANUAL_BUILD_WEB3_INDEXER: "true" 15 | WEB3_GIT_URL: "https://github.com/${{ github.repository }}" 16 | WEB3_GIT_CHECKOUT: "${{ github.sha }}" 17 | 18 | jobs: 19 | unit-tests: 20 | runs-on: ubuntu-latest 21 | steps: 22 | 23 | # Godwoken-Kicker 24 | - uses: actions/checkout@v3 25 | with: 26 | repository: godwokenrises/godwoken-kicker 27 | ref: 'develop' 28 | - name: Kicker init 29 | run: ./kicker init 30 | - name: Kicker start 31 | run: | 32 | # Temporary workaround unreliable web3 health check 33 | (echo " == kicker start attempt: 1 == " && ./kicker start) || \ 34 | (echo " == kicker start attempt: 2 == " && ./kicker stop && ./kicker start) || \ 35 | (echo " == kicker start failed == " && docker-compose --file docker/docker-compose.yml logs --tail 6 && exit 1) 36 | docker-compose --file docker/docker-compose.yml logs --tail 6 37 | # FIXME: Sometimes, Godwoken service is not running 38 | # https://github.com/Flouse/godwoken/runs/3639382192?check_suite_focus=true#step:8:667 39 | - name: Kicker ps 40 | run: sleep 60 && ./kicker ps && ./kicker logs web3 41 | - name: Store kicker network information as environment variables 42 | run: | 43 | cat docker/layer2/config/web3-config.env | grep -v '^#' >> $GITHUB_ENV 44 | echo "DATABASE_URL=postgres://user:password@127.0.0.1:5432/lumos" >> $GITHUB_ENV 45 | echo "REDIS_URL=redis://127.0.0.1:6379" >> $GITHUB_ENV 46 | 47 | # Godwoken-Web3 48 | - uses: actions/checkout@v3 49 | with: 50 | path: godwoken-web3 51 | - name: Yarn run test 52 | working-directory: godwoken-web3 53 | run: yarn && yarn run build && yarn run test 54 | 55 | - name: Kicker logs if failure 56 | if: ${{ failure() }} 57 | run: ./kicker ps && ./kicker logs 58 | -------------------------------------------------------------------------------- /crates/indexer/src/main.rs: -------------------------------------------------------------------------------- 1 | use gw_web3_indexer::{config::load_indexer_config, runner::Runner}; 2 | 3 | use anyhow::Result; 4 | use sentry_log::LogFilter; 5 | 6 | fn main() -> Result<()> { 7 | init_log(); 8 | let indexer_config = load_indexer_config("./indexer-config.toml")?; 9 | 10 | let sentry_environment = indexer_config.sentry_environment.clone().map(|e| e.into()); 11 | let _guard = match &indexer_config.sentry_dsn { 12 | Some(sentry_dsn) => sentry::init(( 13 | sentry_dsn.as_str(), 14 | sentry::ClientOptions { 15 | release: sentry::release_name!(), 16 | environment: sentry_environment, 17 | ..Default::default() 18 | }, 19 | )), 20 | None => sentry::init(()), 21 | }; 22 | 23 | let mut runner = Runner::new(indexer_config)?; 24 | 25 | let command_name = std::env::args().nth(1); 26 | 27 | // `cargo run` -> run sync mode 28 | // `cargo run update ` -> run update mode 29 | if let Some(name) = command_name { 30 | if name == "update" { 31 | let start_block_number = std::env::args() 32 | .nth(2) 33 | .map(|num| num.parse::().unwrap()); 34 | let end_block_number = std::env::args() 35 | .nth(3) 36 | .map(|num| num.parse::().unwrap()); 37 | smol::block_on(runner.run_update(start_block_number, end_block_number))?; 38 | } else { 39 | smol::block_on(runner.run())?; 40 | } 41 | } else { 42 | smol::block_on(runner.run())?; 43 | } 44 | 45 | Ok(()) 46 | } 47 | 48 | fn init_log() { 49 | let logger = env_logger::builder() 50 | .parse_env(env_logger::Env::default().default_filter_or("info")) 51 | .build(); 52 | let level = logger.filter(); 53 | let logger = sentry_log::SentryLogger::with_dest(logger).filter(|md| match md.level() { 54 | log::Level::Error | log::Level::Warn => LogFilter::Event, 55 | _ => LogFilter::Ignore, 56 | }); 57 | log::set_boxed_logger(Box::new(logger)) 58 | .map(|()| log::set_max_level(level)) 59 | .expect("set log"); 60 | } 61 | -------------------------------------------------------------------------------- /packages/api-server/src/base/env-config.ts: -------------------------------------------------------------------------------- 1 | import { env } from "process"; 2 | import dotenv from "dotenv"; 3 | 4 | dotenv.config({ path: "./.env" }); 5 | 6 | export const envConfig = { 7 | get databaseUrl() { 8 | return getRequired("DATABASE_URL"); 9 | }, 10 | get godwokenJsonRpc() { 11 | return getRequired("GODWOKEN_JSON_RPC"); 12 | }, 13 | 14 | _newRelicLicenseKey: getOptional("NEW_RELIC_LICENSE_KEY"), 15 | clusterCount: getOptional("CLUSTER_COUNT"), 16 | redisUrl: getOptional("REDIS_URL"), 17 | pgPoolMax: getOptional("PG_POOL_MAX"), 18 | gasPriceCacheSeconds: getOptional("GAS_PRICE_CACHE_SECONDS"), 19 | extraEstimateGas: getOptional("EXTRA_ESTIMATE_GAS"), 20 | sentryDns: getOptional("SENTRY_DNS"), 21 | sentryEnvironment: getOptional("SENTRY_ENVIRONMENT"), 22 | godwokenReadonlyJsonRpc: getOptional("GODWOKEN_READONLY_JSON_RPC"), 23 | enableCacheEthCall: getOptional("ENABLE_CACHE_ETH_CALL"), 24 | enableCacheEstimateGas: getOptional("ENABLE_CACHE_ESTIMATE_GAS"), 25 | enableCacheExecuteRawL2Tx: getOptional("ENABLE_CACHE_EXECUTE_RAW_L2_TX"), 26 | logLevel: getOptional("LOG_LEVEL"), 27 | logFormat: getOptional("LOG_FORMAT"), 28 | logRequestBody: getOptional("WEB3_LOG_REQUEST_BODY"), 29 | port: getOptional("PORT"), 30 | maxQueryNumber: getOptional("MAX_QUERY_NUMBER"), 31 | maxQueryTimeInMilliseconds: getOptional("MAX_QUERY_TIME_MILSECS"), 32 | enableProfRpc: getOptional("ENABLE_PROF_RPC"), 33 | enablePriceOracle: getOptional("ENABLE_PRICE_ORACLE"), 34 | priceOracleDiffThreshold: getOptional("PRICE_ORACLE_DIFF_THRESHOLD"), 35 | priceOraclePollInterval: getOptional("PRICE_ORACLE_POLL_INTERVAL"), 36 | priceOracleUpdateWindow: getOptional("PRICE_ORACLE_UPDATE_WINDOW"), 37 | gasPriceDivider: getOptional("GAS_PRICE_DIVIDER"), 38 | minGasPriceUpperLimit: getOptional("MIN_GAS_PRICE_UPPER_LIMIT"), 39 | minGasPriceLowerLimit: getOptional("MIN_GAS_PRICE_LOWER_LIMIT"), 40 | blockCongestionGasUsed: getOptional("BLOCK_CONGESTION_GAS_USED"), 41 | }; 42 | 43 | function getRequired(name: string): string { 44 | const value = env[name]; 45 | if (value == null) { 46 | throw new Error(`no env ${name} provided`); 47 | } 48 | 49 | return value; 50 | } 51 | 52 | function getOptional(name: string): string | undefined { 53 | return env[name]; 54 | } 55 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/modules/debug.ts: -------------------------------------------------------------------------------- 1 | import { Hash } from "@ckb-lumos/base"; 2 | import { RPC } from "@ckb-lumos/toolkit"; 3 | import { LogItem } from "@godwoken-web3/godwoken"; 4 | import { envConfig } from "../../base/env-config"; 5 | import { CACHE_EXPIRED_TIME_MILSECS } from "../../cache/constant"; 6 | import { Store } from "../../cache/store"; 7 | import { ethTxHashToGwTxHash } from "../../cache/tx-hash"; 8 | import { Query } from "../../db"; 9 | import { parsePolyjuiceUserLog } from "../../filter-web3-tx"; 10 | import { POLYJUICE_USER_LOG_FLAG } from "../constant"; 11 | import { handleGwError } from "../gw-error"; 12 | import { middleware } from "../validator"; 13 | 14 | export class Debug { 15 | private readonlyRpc: RPC; 16 | private cacheStore: Store; 17 | private query: Query; 18 | 19 | constructor() { 20 | this.readonlyRpc = new RPC( 21 | envConfig.godwokenReadonlyJsonRpc || envConfig.godwokenJsonRpc 22 | ); 23 | this.cacheStore = new Store(true, CACHE_EXPIRED_TIME_MILSECS); 24 | this.query = new Query(); 25 | 26 | this.replayTransaction = middleware(this.replayTransaction.bind(this), 1); 27 | } 28 | 29 | async replayTransaction(args: any[]) { 30 | const ethTxHash: Hash = args[0]; 31 | const gwTxHash: Hash | undefined = await ethTxHashToGwTxHash( 32 | ethTxHash, 33 | this.query, 34 | this.cacheStore 35 | ); 36 | if (gwTxHash == null) { 37 | throw new Error(`gw tx hash not found by eth tx hash ${ethTxHash}`); 38 | } 39 | let result; 40 | try { 41 | result = await this.readonlyRpc.debug_replay_transaction( 42 | gwTxHash, 43 | ...args.slice(1) 44 | ); 45 | } catch (error) { 46 | handleGwError(error); 47 | } 48 | 49 | if (result == null) { 50 | return undefined; 51 | } 52 | 53 | const web3Logs = result.logs 54 | .filter((log: LogItem) => log.service_flag === POLYJUICE_USER_LOG_FLAG) 55 | .map((log: LogItem) => { 56 | const info = parsePolyjuiceUserLog(log.data); 57 | return { 58 | address: info.address, 59 | data: info.data, 60 | topics: info.topics, 61 | }; 62 | }); 63 | 64 | // Replace logs with web3 logs 65 | result.logs = web3Logs; 66 | 67 | return result; 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/api-server/src/base/gas-price.ts: -------------------------------------------------------------------------------- 1 | import web3Utils from "web3-utils"; 2 | import { envConfig } from "./env-config"; 3 | import { Decimal } from "decimal.js"; 4 | import { parseFixed } from "@ethersproject/bignumber"; 5 | 6 | // we enlarger it to be an integer instead of float 7 | const LOWER_CKB_PRICE = enlargeCkbPrice("0.0038"); 8 | const UPPER_GAS_PRICE = web3Utils.toWei("0.00002", "ether"); 9 | const DEFAULT_GAS_PRICE_DIVIDER = 10 | BigInt(UPPER_GAS_PRICE) * BigInt(LOWER_CKB_PRICE); 11 | 12 | // when ckbPrice goes up, the gasPrice goes down (vice versa) 13 | // gasPrice = divider / ckbPrice 14 | const GAS_PRICE_DIVIDER = envConfig.gasPriceDivider 15 | ? BigInt(envConfig.gasPriceDivider) 16 | : DEFAULT_GAS_PRICE_DIVIDER; 17 | 18 | // feeRate = gasPrice * multiplier 19 | export const FEE_RATE_MULTIPLIER = BigInt(100); 20 | 21 | // upper-limit and lower-limit to prevent gas-price goes off-track 22 | export const MIN_GAS_PRICE_LOWER_LIMIT = pCKBToWei( 23 | envConfig.minGasPriceLowerLimit || "0.00001" 24 | ); 25 | export const MIN_GAS_PRICE_UPPER_LIMIT = pCKBToWei( 26 | envConfig.minGasPriceUpperLimit || "0.00004" 27 | ); 28 | 29 | export class Price { 30 | private ckbPrice: string; 31 | 32 | constructor(ckbPrice: string) { 33 | this.ckbPrice = ckbPrice; 34 | } 35 | 36 | toGasPrice(): bigint { 37 | const ckbPrice = enlargeCkbPrice(this.ckbPrice); 38 | const gasPrice = GAS_PRICE_DIVIDER / ckbPrice; 39 | return gasPrice; 40 | } 41 | 42 | toMinGasPrice(): bigint { 43 | const p = this.toGasPrice(); 44 | if (p > MIN_GAS_PRICE_UPPER_LIMIT) return MIN_GAS_PRICE_UPPER_LIMIT; 45 | if (p < MIN_GAS_PRICE_LOWER_LIMIT) return MIN_GAS_PRICE_LOWER_LIMIT; 46 | return p; 47 | } 48 | 49 | toFeeRate(): bigint { 50 | return FEE_RATE_MULTIPLIER * this.toGasPrice(); 51 | } 52 | 53 | toMinFeeRate(): bigint { 54 | return FEE_RATE_MULTIPLIER * this.toMinGasPrice(); 55 | } 56 | 57 | public static from(ckbPrice: string): Price { 58 | return new Price(ckbPrice); 59 | } 60 | } 61 | 62 | //*** helper function ***/ 63 | function enlargeCkbPrice(price: string): bigint { 64 | // 0.000000 => 6 precision 65 | // enlarge it to 10 ** 6 66 | const precision = 6; 67 | const p = new Decimal(price).toFixed(precision); 68 | return parseFixed(p, precision).toBigInt(); 69 | } 70 | 71 | function pCKBToWei(pCKB: string): bigint { 72 | return BigInt(web3Utils.toWei(pCKB, "ether")); 73 | } 74 | -------------------------------------------------------------------------------- /packages/api-server/src/opentelemetry.ts: -------------------------------------------------------------------------------- 1 | import process from "process"; 2 | 3 | import * as opentelemetry from "@opentelemetry/sdk-node"; 4 | 5 | import { ExpressInstrumentation } from "@opentelemetry/instrumentation-express"; 6 | import { HttpInstrumentation } from "@opentelemetry/instrumentation-http"; 7 | import { KnexInstrumentation } from "@opentelemetry/instrumentation-knex"; 8 | import { RedisInstrumentation } from "@opentelemetry/instrumentation-redis-4"; 9 | import { WinstonInstrumentation } from "@opentelemetry/instrumentation-winston"; 10 | 11 | import { JaegerExporter } from "@opentelemetry/exporter-jaeger"; 12 | import { JaegerPropagator } from "@opentelemetry/propagator-jaeger"; 13 | 14 | // TODO: We can remove this after upgrade sdk-node to 0.34 15 | // Reference: https://github.com/open-telemetry/opentelemetry-js/pull/3388 16 | const jaegerExporter = new JaegerExporter(); 17 | const jaegerPropagator = new JaegerPropagator(); 18 | 19 | // Configuration (sdk 0.34 or later): 20 | // Disable: 21 | // OTEL_TRACES_EXPORTER: none 22 | // 23 | // Example(jaeger): 24 | // LOG_FORMAT: json (add trace id to log) 25 | // OTEL_TRACES_EXPORTER: jaeger 26 | // OTEL_PROPAGATORS: jaeger 27 | // OTEL_EXPORTER_OTLP_PROTOCOL: grpc (default is http/protobuf) 28 | // OTEL_EXPORTER_JAEGER_ENDPOINT: http://jaeger:14250 29 | // OTEL_RESOURCE_ATTRIBUTES: service.name=web3-readonly1 30 | // 31 | // Reference: https://github.com/open-telemetry/opentelemetry-js/blob/main/experimental/packages/opentelemetry-sdk-node/README.md 32 | const sdk = new opentelemetry.NodeSDK({ 33 | traceExporter: jaegerExporter, 34 | textMapPropagator: jaegerPropagator, 35 | instrumentations: [ 36 | new HttpInstrumentation(), 37 | new RedisInstrumentation(), 38 | new ExpressInstrumentation(), 39 | new KnexInstrumentation(), 40 | // Add trace_id, span_id and trace_flags to log entry 41 | new WinstonInstrumentation(), 42 | ], 43 | }); 44 | 45 | export function startOpentelemetry() { 46 | sdk 47 | .start() 48 | .then(() => console.log("Opentelemetry tracing initialized")) 49 | .catch((error) => 50 | console.log("Error initializing opentelemetry tracing", error) 51 | ); 52 | 53 | process.on("SIGTERM", () => { 54 | sdk 55 | .shutdown() 56 | .then(() => console.log("Opentelemetry Tracing terminated")) 57 | .catch((error) => 58 | console.log("Error terminating opentelemetry tracing", error) 59 | ) 60 | .finally(() => process.exit(0)); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /packages/api-server/src/cache/tx-hash.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HexString } from "@ckb-lumos/base"; 2 | import { Query } from "../db"; 3 | import { 4 | TX_HASH_MAPPING_CACHE_EXPIRED_TIME_MILSECS, 5 | TX_HASH_MAPPING_PREFIX_KEY, 6 | } from "./constant"; 7 | import { Store } from "./store"; 8 | 9 | function ethTxHashCacheKey(ethTxHash: string) { 10 | return `${TX_HASH_MAPPING_PREFIX_KEY}:eth:${ethTxHash}`; 11 | } 12 | 13 | function gwTxHashCacheKey(gwTxHash: string) { 14 | return `${TX_HASH_MAPPING_PREFIX_KEY}:gw:${gwTxHash}`; 15 | } 16 | 17 | export class TxHashMapping { 18 | private store: Store; 19 | 20 | constructor(store: Store) { 21 | this.store = store; 22 | } 23 | 24 | async save(ethTxHash: Hash, gwTxHash: Hash) { 25 | const ethTxHashKey = ethTxHashCacheKey(ethTxHash); 26 | await this.store.insert( 27 | ethTxHashKey, 28 | gwTxHash, 29 | TX_HASH_MAPPING_CACHE_EXPIRED_TIME_MILSECS 30 | ); 31 | const gwTxHashKey = gwTxHashCacheKey(gwTxHash); 32 | await this.store.insert( 33 | gwTxHashKey, 34 | ethTxHash, 35 | TX_HASH_MAPPING_CACHE_EXPIRED_TIME_MILSECS 36 | ); 37 | } 38 | 39 | async getEthTxHash(gwTxHash: Hash): Promise { 40 | const gwTxHashKey = gwTxHashCacheKey(gwTxHash); 41 | return await this.store.get(gwTxHashKey); 42 | } 43 | 44 | async getGwTxHash(ethTxHash: Hash): Promise { 45 | const ethTxHashKey = ethTxHashCacheKey(ethTxHash); 46 | return await this.store.get(ethTxHashKey); 47 | } 48 | } 49 | 50 | export async function gwTxHashToEthTxHash( 51 | gwTxHash: HexString, 52 | query: Query, 53 | cacheStore: Store 54 | ) { 55 | let ethTxHashInCache = await new TxHashMapping(cacheStore).getEthTxHash( 56 | gwTxHash 57 | ); 58 | if (ethTxHashInCache != null) { 59 | return ethTxHashInCache; 60 | } 61 | 62 | // query from database 63 | const gwTxHashInDb: Hash | undefined = await query.getEthTxHashByGwTxHash( 64 | gwTxHash 65 | ); 66 | return gwTxHashInDb; 67 | } 68 | 69 | export async function ethTxHashToGwTxHash( 70 | ethTxHash: HexString, 71 | query: Query, 72 | cacheStore: Store 73 | ) { 74 | let gwTxHashInCache = await new TxHashMapping(cacheStore).getGwTxHash( 75 | ethTxHash 76 | ); 77 | if (gwTxHashInCache != null) { 78 | return gwTxHashInCache; 79 | } 80 | 81 | // query from database 82 | const ethTxHashInDb: Hash | undefined = await query.getGwTxHashByEthTxHash( 83 | ethTxHash 84 | ); 85 | return ethTxHashInDb; 86 | } 87 | -------------------------------------------------------------------------------- /packages/api-server/newrelic.js: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | require("dotenv").config({ path: "./.env" }); 4 | 5 | /** 6 | * New Relic agent configuration. 7 | * 8 | * See lib/config/default.js in the agent distribution for a more complete 9 | * description of configuration variables and their potential values. 10 | */ 11 | exports.config = { 12 | /** 13 | * Array of application names. 14 | */ 15 | app_name: [ process.env.NEW_RELIC_APP_NAME || 'Godwoken Web3' ], 16 | /** 17 | * Your New Relic license key. 18 | */ 19 | license_key: process.env.NEW_RELIC_LICENSE_KEY || '0000000000000000', 20 | /** 21 | * This setting controls distributed tracing. 22 | * Distributed tracing lets you see the path that a request takes through your 23 | * distributed system. Enabling distributed tracing changes the behavior of some 24 | * New Relic features, so carefully consult the transition guide before you enable 25 | * this feature: https://docs.newrelic.com/docs/transition-guide-distributed-tracing 26 | * Default is false. 27 | */ 28 | distributed_tracing: { 29 | /** 30 | * Enables/disables distributed tracing. 31 | * 32 | * @env NEW_RELIC_DISTRIBUTED_TRACING_ENABLED 33 | */ 34 | enabled: true 35 | }, 36 | error_collector: { 37 | /** 38 | * Enables/disables error collection. 39 | * 40 | * @env NEW_RELIC_ERROR_COLLECTOR_ENABLED 41 | */ 42 | enabled: true, 43 | }, 44 | logging: { 45 | /** 46 | * Level at which to log. 'trace' is most useful to New Relic when diagnosing 47 | * issues with the agent, 'info' and higher will impose the least overhead on 48 | * production applications. 49 | */ 50 | level: 'info' 51 | }, 52 | /** 53 | * When true, all request headers except for those listed in attributes.exclude 54 | * will be captured for all traces, unless otherwise specified in a destination's 55 | * attributes include/exclude lists. 56 | */ 57 | allow_all_headers: true, 58 | attributes: { 59 | /** 60 | * Prefix of attributes to exclude from all destinations. Allows * as wildcard 61 | * at end. 62 | * 63 | * NOTE: If excluding headers, they must be in camelCase form to be filtered. 64 | * 65 | * @env NEW_RELIC_ATTRIBUTES_EXCLUDE 66 | */ 67 | exclude: [ 68 | 'request.headers.cookie', 69 | 'request.headers.authorization', 70 | 'request.headers.proxyAuthorization', 71 | 'request.headers.setCookie*', 72 | 'request.headers.x*', 73 | 'response.headers.cookie', 74 | 'response.headers.authorization', 75 | 'response.headers.proxyAuthorization', 76 | 'response.headers.setCookie*', 77 | 'response.headers.x*' 78 | ] 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /packages/api-server/src/base/types/api.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HexNumber, HexString } from "@ckb-lumos/base"; 2 | 3 | export interface EthTransaction { 4 | hash: Hash; 5 | // when pending, blockNumber & blockHash = null 6 | blockHash: Hash | null; 7 | blockNumber: HexNumber | null; 8 | transactionIndex: HexNumber | null; 9 | from: HexString; 10 | to: HexString | null; 11 | gas: HexNumber; 12 | gasPrice: HexNumber; 13 | input: HexString; 14 | nonce: HexNumber; 15 | value: HexNumber; 16 | v: HexNumber; 17 | r: HexNumber; 18 | s: HexNumber; 19 | } 20 | 21 | export interface EthBlock { 22 | // when pending, number & hash & nonce & logsBloom = pending 23 | number: HexNumber | null; 24 | hash: Hash; 25 | parentHash: Hash; 26 | gasLimit: HexNumber; 27 | gasUsed: HexNumber; 28 | miner: HexString; 29 | size: HexNumber; 30 | logsBloom: HexString; 31 | transactions: (EthTransaction | Hash)[]; 32 | timestamp: HexNumber; 33 | mixHash: Hash; 34 | nonce: HexNumber; 35 | stateRoot: Hash; 36 | sha3Uncles: Hash; 37 | receiptsRoot: Hash; 38 | transactionsRoot: Hash; 39 | uncles: []; 40 | difficulty: HexNumber; 41 | totalDifficulty: HexNumber; 42 | extraData: HexString; 43 | } 44 | 45 | export interface FailedReason { 46 | status_code: HexNumber; 47 | status_type: string; 48 | message: string; 49 | } 50 | 51 | export interface EthTransactionReceipt { 52 | transactionHash: Hash; 53 | transactionIndex: HexNumber; 54 | blockHash: Hash; 55 | blockNumber: HexNumber; 56 | from: HexString; 57 | to: HexString | null; 58 | gasUsed: HexNumber; 59 | cumulativeGasUsed: HexNumber; 60 | logsBloom: HexString; 61 | logs: EthLog[]; 62 | contractAddress: HexString | null; 63 | status: HexNumber; // 0 => failed, 1 => success 64 | failed_reason?: FailedReason; // null if success 65 | } 66 | 67 | export interface EthLog { 68 | // when pending logIndex, transactionIndex, transactionHash, blockHash, blockNumber = null 69 | address: HexString; 70 | blockHash: Hash | null; 71 | blockNumber: HexNumber | null; 72 | transactionIndex: HexNumber | null; 73 | transactionHash: Hash | null; 74 | data: HexString; 75 | logIndex: HexNumber | null; 76 | topics: HexString[]; 77 | removed: boolean; 78 | } 79 | 80 | export interface EthNewHead { 81 | number: HexNumber; 82 | hash: Hash; 83 | parentHash: Hash; 84 | gasLimit: HexNumber; 85 | gasUsed: HexNumber; 86 | miner: HexString; 87 | logsBloom: HexString; 88 | timestamp: HexNumber; 89 | mixHash: Hash; 90 | nonce: HexNumber; 91 | stateRoot: Hash; 92 | sha3Uncles: Hash; 93 | receiptsRoot: Hash; 94 | transactionsRoot: Hash; 95 | difficulty: HexNumber; 96 | extraData: HexString; 97 | baseFeePerGas: HexNumber; 98 | } 99 | -------------------------------------------------------------------------------- /scripts/generate-indexer-config.js: -------------------------------------------------------------------------------- 1 | const dotenv = require("dotenv"); 2 | const path = require("path"); 3 | const fs = require("fs"); 4 | const http = require("http"); 5 | const crypto = require("crypto"); 6 | 7 | const envPath = path.join(__dirname, "../packages/api-server/.env"); 8 | 9 | dotenv.config({ path: envPath }); 10 | 11 | function sendJsonRpc(rpcUrl, method, params) { 12 | const url = new URL(rpcUrl); 13 | 14 | return new Promise((resolve, reject) => { 15 | const data = JSON.stringify({ 16 | id: crypto.randomUUID(), 17 | jsonrpc: "2.0", 18 | method: method, 19 | params: params, 20 | }); 21 | 22 | const options = { 23 | hostname: url.hostname, 24 | port: url.port, 25 | method: "POST", 26 | headers: { 27 | "Content-Type": "application/json", 28 | "Content-Length": data.length, 29 | }, 30 | }; 31 | 32 | const req = http.request(options, (res) => { 33 | res.on("data", (d) => { 34 | const res = JSON.parse(d.toString()); 35 | if (res.error) { 36 | reject(res); 37 | } else { 38 | resolve(res.result); 39 | } 40 | }); 41 | }); 42 | 43 | req.on("error", (error) => { 44 | console.error(error); 45 | }); 46 | 47 | req.write(data); 48 | req.end(); 49 | }); 50 | } 51 | 52 | const run = async () => { 53 | const nodeInfo = await sendJsonRpc( 54 | process.env.GODWOKEN_JSON_RPC, 55 | "gw_get_node_info", 56 | [] 57 | ); 58 | 59 | let config = { 60 | l2_sudt_type_script_hash: nodeInfo.gw_scripts.find( 61 | (s) => s.script_type === "l2_sudt" 62 | ).type_hash, 63 | polyjuice_type_script_hash: nodeInfo.backends.find( 64 | (s) => s.backend_type === "polyjuice" 65 | ).validator_script_type_hash, 66 | rollup_type_hash: nodeInfo.rollup_cell.type_hash, 67 | eth_account_lock_hash: nodeInfo.eoa_scripts.find( 68 | (s) => s.eoa_type === "eth" 69 | ).type_hash, 70 | chain_id: +nodeInfo.rollup_config.chain_id, 71 | 72 | godwoken_rpc_url: process.env.GODWOKEN_JSON_RPC, 73 | pg_url: process.env.DATABASE_URL, 74 | sentry_dsn: process.env.SENTRY_DNS, 75 | sentry_environment: process.env.SENTRY_ENVIRONMENT, 76 | }; 77 | 78 | let tomlStr = ""; 79 | 80 | for (const [key, value] of Object.entries(config)) { 81 | console.log(`[${key}]: ${value}`); 82 | if (value != null && key === "chain_id") { 83 | tomlStr += `${key}=${Number(value)}\n`; 84 | continue; 85 | } 86 | if (value != null) { 87 | tomlStr += `${key}="${value}"\n`; 88 | } 89 | } 90 | 91 | const outputPath = path.join(__dirname, "../indexer-config.toml"); 92 | fs.writeFileSync(outputPath, tomlStr); 93 | }; 94 | 95 | run(); 96 | -------------------------------------------------------------------------------- /packages/api-server/tests/utils/gasless.test.ts: -------------------------------------------------------------------------------- 1 | import test from "ava"; 2 | import { 3 | decodeGaslessPayload, 4 | encodeGaslessPayload, 5 | UserOperation, 6 | } from "../../src/gasless/payload"; 7 | 8 | test("encode gasless payload", (t) => { 9 | const userOperation: UserOperation = { 10 | callContract: "0x1dF923E4F009663B0Fddc1775dac783B85f432fB", 11 | callData: "0xffff", 12 | callGasLimit: "0x61a8", 13 | verificationGasLimit: "0x61a8", 14 | maxFeePerGas: "0x61a8", 15 | maxPriorityFeePerGas: "0x61a8", 16 | paymasterAndData: "0x1df923e4f009663b0fddc1775dac783b85f432fb", 17 | }; 18 | 19 | const payload = encodeGaslessPayload(userOperation); 20 | t.deepEqual( 21 | payload, 22 | "0xfb4350d800000000000000000000000000000000000000000000000000000000000000200000000000000000000000001df923e4f009663b0fddc1775dac783b85f432fb00000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000002ffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141df923e4f009663b0fddc1775dac783b85f432fb000000000000000000000000" 23 | ); 24 | }); 25 | 26 | test("decode gasless payload", (t) => { 27 | const payload = 28 | "0xfb4350d800000000000000000000000000000000000000000000000000000000000000200000000000000000000000001df923e4f009663b0fddc1775dac783b85f432fb00000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000061a800000000000000000000000000000000000000000000000000000000000001200000000000000000000000000000000000000000000000000000000000000002ffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000141df923e4f009663b0fddc1775dac783b85f432fb000000000000000000000000"; 29 | const userOperation = decodeGaslessPayload(payload); 30 | t.deepEqual(userOperation, { 31 | callContract: "0x1dF923E4F009663B0Fddc1775dac783B85f432fB", 32 | callData: "0xffff", 33 | callGasLimit: "0x61a8", 34 | verificationGasLimit: "0x61a8", 35 | maxFeePerGas: "0x61a8", 36 | maxPriorityFeePerGas: "0x61a8", 37 | paymasterAndData: "0x1df923e4f009663b0fddc1775dac783b85f432fb", 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build Push (Web3) 2 | 3 | on: 4 | push: 5 | branches: ["main", '*-rc*'] 6 | tags: ["v*.*.*"] 7 | workflow_dispatch: 8 | 9 | env: 10 | # Use ghcr.io only 11 | REGISTRY: ghcr.io 12 | # github.repository as / 13 | IMAGE_NAME: godwoken-web3-prebuilds 14 | 15 | jobs: 16 | docker-build-push: 17 | runs-on: ubuntu-latest 18 | 19 | # If you specify the access for any of these scopes, all of those that are not specified are set to none. 20 | permissions: 21 | contents: read 22 | packages: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | 28 | # Login against a Docker registry except on PR 29 | # https://github.com/docker/login-action 30 | # GitHub automatically creates a unique GITHUB_TOKEN secret to use in this workflow. 31 | - name: Log into registry ${{ env.REGISTRY }} 32 | if: github.event_name != 'pull_request' 33 | uses: docker/login-action@v1 34 | with: 35 | registry: ${{ env.REGISTRY }} 36 | username: ${{ github.repository_owner }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | # Extract metadata (tags, labels) for Docker 40 | # https://github.com/docker/metadata-action 41 | - name: Extract Docker metadata 42 | id: meta 43 | uses: docker/metadata-action@v3 44 | with: 45 | images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} 46 | 47 | - name: Get Current Commit Id 48 | id: commit 49 | run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 50 | 51 | # Build and push Docker image with Buildx (don't push on PR) 52 | # https://github.com/docker/build-push-action 53 | - name: New Commit Build => Build and push commit image to ${{ env.REGISTRY }} 54 | if: ${{ github.ref_type != 'tag' }} 55 | uses: docker/build-push-action@v2 56 | with: 57 | context: . 58 | push: ${{ github.event_name != 'pull_request' }} 59 | tags: ${{ steps.meta.outputs.tags }}-${{ steps.commit.outputs.sha_short }} 60 | labels: ${{ steps.meta.outputs.labels }} 61 | 62 | # Build and push Docker image with Buildx (don't push on PR) 63 | # only for new tag 64 | - name: Official Release Build => Build and push tag image to ${{ env.REGISTRY }} 65 | if: ${{ startsWith(github.ref, 'refs/tags') }} 66 | uses: docker/build-push-action@v2 67 | with: 68 | context: . 69 | push: ${{ github.event_name != 'pull_request' }} 70 | tags: ${{ steps.meta.outputs.tags }} 71 | labels: ${{ steps.meta.outputs.labels }} 72 | -------------------------------------------------------------------------------- /packages/api-server/src/gasless/payload.ts: -------------------------------------------------------------------------------- 1 | import { HexNumber, HexString } from "@ckb-lumos/base"; 2 | import { AbiCoder } from "web3-eth-abi"; 3 | const abiCoder = require("web3-eth-abi") as AbiCoder; 4 | 5 | export interface UserOperation { 6 | callContract: HexString; 7 | callData: HexString; 8 | callGasLimit: HexNumber; 9 | verificationGasLimit: HexNumber; 10 | maxFeePerGas: HexNumber; 11 | maxPriorityFeePerGas: HexNumber; 12 | paymasterAndData: HexString; 13 | } 14 | 15 | export const USER_OPERATION_ABI_TYPE = { 16 | UserOperation: { 17 | callContract: "address", 18 | callData: "bytes", 19 | callGasLimit: "uint256", 20 | verificationGasLimit: "uint256", 21 | maxFeePerGas: "uint256", 22 | maxPriorityFeePerGas: "uint256", 23 | paymasterAndData: "bytes", 24 | }, 25 | }; 26 | 27 | // first 4 bytes of keccak hash of handleOp((address,bytes,uint256,uint256,uint256,uint256,bytes)) 28 | const ENTRYPOINT_HANDLE_OP_SELECTOR: HexString = "fb4350d8"; 29 | 30 | // Note: according to the godwoken gasless transaction specs: 31 | // a gasless transaction's input data is a serialized Gasless payload. 32 | // gasless payload = ENTRYPOINT_HANDLE_OP_SELECTOR + abiEncode(UserOperation) 33 | // which is also the call data of calling entrypoint contract with handleOp(UserOperation userOp) method 34 | export function decodeGaslessPayload(inputData: HexString): UserOperation { 35 | if (inputData.length < 10) { 36 | throw new Error( 37 | `invalid gasless tx.data length ${inputData.length}, expect at least 10` 38 | ); 39 | } 40 | 41 | // check first 4 bytes 42 | const fnSelector = inputData.slice(2, 10); 43 | if (fnSelector !== ENTRYPOINT_HANDLE_OP_SELECTOR) { 44 | throw new Error( 45 | `invalid gasless tx.data fn selector ${fnSelector}, expect ${ENTRYPOINT_HANDLE_OP_SELECTOR}` 46 | ); 47 | } 48 | 49 | const userOpData = "0x" + inputData.slice(10); 50 | const decoded = abiCoder.decodeParameter(USER_OPERATION_ABI_TYPE, userOpData); 51 | const op: UserOperation = { 52 | callContract: decoded.callContract, 53 | callData: decoded.callData, 54 | callGasLimit: "0x" + BigInt(decoded.callGasLimit).toString(16), 55 | verificationGasLimit: 56 | "0x" + BigInt(decoded.verificationGasLimit).toString(16), 57 | maxFeePerGas: "0x" + BigInt(decoded.maxFeePerGas).toString(16), 58 | maxPriorityFeePerGas: 59 | "0x" + BigInt(decoded.maxPriorityFeePerGas).toString(16), 60 | paymasterAndData: decoded.paymasterAndData, 61 | }; 62 | return op; 63 | } 64 | 65 | export function encodeGaslessPayload(op: UserOperation): HexString { 66 | const data = abiCoder.encodeParameter(USER_OPERATION_ABI_TYPE, op); 67 | // gasless payload = ENTRYPOINT_HANDLE_OP_SELECTOR + abiEncode(UserOperation) 68 | return "0x" + ENTRYPOINT_HANDLE_OP_SELECTOR + data.slice(2); 69 | } 70 | -------------------------------------------------------------------------------- /.github/workflows/docker-publish-indexer.yml: -------------------------------------------------------------------------------- 1 | name: Docker Build Push (Indexer) 2 | 3 | on: 4 | push: 5 | branches: ["main", '*-rc*'] 6 | tags: ["v*.*.*"] 7 | workflow_dispatch: 8 | 9 | env: 10 | # Use ghcr.io only 11 | REGISTRY: ghcr.io 12 | # github.repository as / 13 | IMAGE_NAME: godwoken-web3-indexer-prebuilds 14 | 15 | jobs: 16 | docker-build-push: 17 | runs-on: ubuntu-latest 18 | 19 | # If you specify the access for any of these scopes, all of those that are not specified are set to none. 20 | permissions: 21 | contents: read 22 | packages: write 23 | 24 | steps: 25 | - name: Checkout repository 26 | uses: actions/checkout@v2 27 | 28 | # Login against a Docker registry except on PR 29 | # https://github.com/docker/login-action 30 | # GitHub automatically creates a unique GITHUB_TOKEN secret to use in this workflow. 31 | - name: Log into registry ${{ env.REGISTRY }} 32 | if: github.event_name != 'pull_request' 33 | uses: docker/login-action@v1 34 | with: 35 | registry: ${{ env.REGISTRY }} 36 | username: ${{ github.repository_owner }} 37 | password: ${{ secrets.GITHUB_TOKEN }} 38 | 39 | # Extract metadata (tags, labels) for Docker 40 | # https://github.com/docker/metadata-action 41 | - name: Extract Docker metadata 42 | id: meta 43 | uses: docker/metadata-action@v3 44 | with: 45 | images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ env.IMAGE_NAME }} 46 | 47 | - name: Get Current Commit Id 48 | id: commit 49 | run: echo "::set-output name=sha_short::$(git rev-parse --short HEAD)" 50 | 51 | # Build and push Docker image with Buildx (don't push on PR) 52 | # https://github.com/docker/build-push-action 53 | - name: New Commit Build => Build and push commit image to ${{ env.REGISTRY }} 54 | if: ${{ github.ref_type != 'tag' }} 55 | uses: docker/build-push-action@v2 56 | with: 57 | file: docker/indexer/Dockerfile 58 | context: . 59 | push: ${{ github.event_name != 'pull_request' }} 60 | tags: ${{ steps.meta.outputs.tags }}-${{ steps.commit.outputs.sha_short }} 61 | labels: ${{ steps.meta.outputs.labels }} 62 | 63 | # Build and push Docker image with Buildx (don't push on PR) 64 | # only for new tag 65 | - name: Official Release Build => Build and push tag image to ${{ env.REGISTRY }} 66 | if: ${{ startsWith(github.ref, 'refs/tags') }} 67 | uses: docker/build-push-action@v2 68 | with: 69 | file: docker/indexer/Dockerfile 70 | context: . 71 | push: ${{ github.event_name != 'pull_request' }} 72 | tags: ${{ steps.meta.outputs.tags }} 73 | labels: ${{ steps.meta.outputs.labels }} 74 | 75 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/index.ts: -------------------------------------------------------------------------------- 1 | import * as modules from "./modules"; 2 | import { Callback } from "./types"; 3 | import * as Sentry from "@sentry/node"; 4 | import { INVALID_PARAMS } from "./error-code"; 5 | import { isRpcError, RpcError } from "./error"; 6 | import { envConfig } from "../base/env-config"; 7 | const newrelic = require("newrelic"); 8 | 9 | /** 10 | * get all methods. e.g., getBlockByNumber in eth module 11 | * @private 12 | * @param {Object} mod 13 | * @return {string[]} 14 | */ 15 | function getMethodNames(mod: any): string[] { 16 | return Object.getOwnPropertyNames(mod.prototype); 17 | } 18 | 19 | export interface ModConstructorArgs { 20 | [modName: string]: any[]; 21 | } 22 | 23 | /** 24 | * return all the methods in all module 25 | */ 26 | function getMethods(argsList: ModConstructorArgs = {}) { 27 | const methods: any = {}; 28 | 29 | modules.list.forEach((modName: string) => { 30 | const args = argsList[modName.toLowerCase()] || []; 31 | const mod = new (modules as any)[modName](...args); 32 | getMethodNames((modules as any)[modName]) 33 | .filter((methodName: string) => methodName !== "constructor") 34 | .forEach((methodName: string) => { 35 | const concatedMethodName = `${modName.toLowerCase()}_${methodName}`; 36 | methods[concatedMethodName] = async (args: any[], cb: Callback) => { 37 | try { 38 | const result = await mod[methodName].bind(mod)(args); 39 | return cb(null, result); 40 | } catch (err: any) { 41 | if (envConfig.sentryDns && err.code !== INVALID_PARAMS) { 42 | Sentry.captureException(err, { 43 | extra: { method: concatedMethodName, params: args }, 44 | }); 45 | } 46 | 47 | if (isRpcError(err)) { 48 | const error = { 49 | code: err.code, 50 | message: err.message, 51 | } as RpcError; 52 | 53 | if (err.data) { 54 | error.data = err.data; 55 | } 56 | 57 | if (err.extra) { 58 | error.extra = err.extra; 59 | } 60 | 61 | cb(error); 62 | // NOTE: Our error responses are not automatically collected by NewRelic because we use Jayson instead of 63 | // express' error handler. 64 | // 65 | // Note: In order to link errors to transaction traces, we pass linking metadata. 66 | newrelic.noticeError(err, newrelic.getLinkingMetadata()); 67 | } else { 68 | throw err; 69 | } 70 | } 71 | }; 72 | }); 73 | }); 74 | 75 | // console.log(methods); 76 | return methods; 77 | } 78 | 79 | const instantFinalityHackMode = true; 80 | 81 | export const methods = getMethods(); 82 | export const instantFinalityHackMethods = getMethods({ 83 | eth: [instantFinalityHackMode], 84 | }); 85 | -------------------------------------------------------------------------------- /packages/api-server/src/base/worker.ts: -------------------------------------------------------------------------------- 1 | import * as Sentry from "@sentry/node"; 2 | import { envConfig } from "./env-config"; 3 | import { logger } from "./logger"; 4 | const newrelic = require("newrelic"); 5 | 6 | const POLL_TIME_INTERVAL = 5000; // 5s 7 | const LIVENESS_CHECK_INTERVAL = 5000; // 5s 8 | 9 | // TODO: use the following class to rewrite BlockEmitter 10 | export class BaseWorker { 11 | protected isRunning: boolean; 12 | protected pollTimeInterval: number; 13 | protected livenessCheckInterval: number; 14 | private intervalHandler: NodeJS.Timer | undefined; 15 | 16 | constructor({ 17 | pollTimeInterval = POLL_TIME_INTERVAL, 18 | livenessCheckInterval = LIVENESS_CHECK_INTERVAL, 19 | } = {}) { 20 | this.isRunning = false; 21 | this.pollTimeInterval = pollTimeInterval; 22 | this.livenessCheckInterval = livenessCheckInterval; 23 | } 24 | 25 | // Main worker 26 | async startForever() { 27 | this.start(); 28 | this.intervalHandler = setInterval(async () => { 29 | if (!this.running()) { 30 | logger.error( 31 | `${this.constructor.name} has stopped, maybe check the log?` 32 | ); 33 | this.start(); 34 | } 35 | }, this.livenessCheckInterval); 36 | } 37 | 38 | async stopForever() { 39 | this.stop(); 40 | if (this.intervalHandler != null) { 41 | clearInterval(this.intervalHandler); 42 | logger.debug(`call ${this.constructor.name} to stop forever`); 43 | } 44 | } 45 | 46 | start() { 47 | this.isRunning = true; 48 | this.scheduleLoop(); 49 | } 50 | 51 | stop() { 52 | this.isRunning = false; 53 | } 54 | 55 | running() { 56 | return this.isRunning; 57 | } 58 | 59 | protected scheduleLoop(ms?: number) { 60 | setTimeout(() => { 61 | this.loop(); 62 | }, ms); 63 | } 64 | 65 | protected loop() { 66 | if (!this.running()) { 67 | return; 68 | } 69 | 70 | this.poll() 71 | .then((timeout) => { 72 | this.scheduleLoop(timeout); 73 | }) 74 | .catch((e) => { 75 | logger.error( 76 | `[${this.constructor.name}] Error occurs: ${e} ${e.stack}, stopping polling!` 77 | ); 78 | if (envConfig.sentryDns) { 79 | Sentry.captureException(e); 80 | } 81 | this.stop(); 82 | }); 83 | } 84 | 85 | protected async poll() { 86 | // add new relic background transaction 87 | return newrelic.startBackgroundTransaction( 88 | `${this.constructor.name}#workerPoll`, 89 | async () => { 90 | newrelic.getTransaction(); 91 | try { 92 | return await this.executePoll(); 93 | } catch (error) { 94 | throw error; 95 | } finally { 96 | newrelic.endTransaction(); 97 | } 98 | } 99 | ); 100 | } 101 | 102 | protected async executePoll(): Promise { 103 | return this.pollTimeInterval; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/api-server/seeds/insert_seed_data.ts: -------------------------------------------------------------------------------- 1 | import * as Knex from "knex"; 2 | import block from "./data/block.json"; 3 | import transactions from "./data/transactions.json"; 4 | import transaction_receipts from "./data/transaction_receipts.json"; 5 | export async function seed(knex: Knex): Promise { 6 | // Deletes ALL existing entries 7 | await knex("logs").del(); 8 | await knex("transactions").del(); 9 | await knex("blocks").del(); 10 | 11 | const {number, hash, parentHash, logsBloom, gasLimit, gasUsed, miner, size, timestamp} = block; 12 | // Inserts seed entries 13 | await knex.transaction(async (trx) => { 14 | await trx("blocks").insert( 15 | { 16 | number: BigInt(number), 17 | hash: hash, 18 | parent_hash: parentHash, 19 | logs_bloom: logsBloom, 20 | gas_limit: BigInt(gasLimit), 21 | gas_used: BigInt(gasUsed), 22 | miner: miner, 23 | size: BigInt(size), 24 | timestamp: new Date(timestamp*1000), 25 | } 26 | ); 27 | for(let i = 0; i < transactions.length; i++) { 28 | const tx = transactions[i]; 29 | const tx_receipt = transaction_receipts[i]; 30 | let returnValue = ( 31 | await trx("transactions").insert({ 32 | hash: tx.hash, 33 | block_number: BigInt(block.number), 34 | block_hash: block.hash, 35 | transaction_index: i, 36 | from_address: tx.from, 37 | to_address: tx.to, 38 | value: BigInt(tx.value), 39 | nonce: tx.nonce, 40 | gas_limit: tx.gas, 41 | gas_price: BigInt(tx.gasPrice), 42 | input: tx.input, 43 | v: tx.v, 44 | r: tx.r, 45 | s: tx.s, 46 | cumulative_gas_used: tx_receipt.cumulativeGasUsed, 47 | gas_used: tx_receipt.gasUsed, 48 | logs_bloom: tx_receipt.logsBloom, 49 | contract_address: tx_receipt.contractAddress, 50 | status: tx_receipt.status, 51 | }, ["id"]) 52 | ); 53 | 54 | const logs = tx_receipt.logs; 55 | let newLogs = [] 56 | for (let j = 0; j < logs.length; j ++) { 57 | const newLog = { 58 | transaction_id: returnValue[0].id, 59 | transaction_hash: tx.hash, 60 | transaction_index: i, 61 | block_number: block.number, 62 | block_hash: block.hash, 63 | address: logs[j].address, 64 | data: logs[j].data, 65 | log_index: j, 66 | topics: logs[j].topics, 67 | }; 68 | newLogs.push(newLog); 69 | } 70 | await trx("logs").insert(newLogs); 71 | } 72 | }).then(function(_resp) { 73 | console.log("Init db with seed data complete") 74 | }).catch(function(err) { 75 | console.log(err); 76 | }) 77 | }; 78 | -------------------------------------------------------------------------------- /packages/api-server/src/ws/wss.ts: -------------------------------------------------------------------------------- 1 | import { envConfig } from "../base/env-config"; 2 | import { logger } from "../base/logger"; 3 | import { METHOD_NOT_FOUND } from "../methods/error-code"; 4 | import { methods } from "../methods/index"; 5 | import { JSONRPCVersionTwoRequest } from "jayson"; 6 | 7 | const wsRpcMethods = Object.keys(methods).concat( 8 | "eth_subscribe", 9 | "eth_unsubscribe" 10 | ); 11 | 12 | export function middleware(ws: any) { 13 | ws.on("message", dispatch); 14 | ws.on("data", dispatch); 15 | 16 | function dispatch(msg: string) { 17 | try { 18 | const obj = JSON.parse(msg.toString()); 19 | 20 | logRequest(obj); 21 | 22 | if (Array.isArray(obj)) { 23 | const args = ["@batchRequests" as any].concat(obj, [ 24 | (info: any[]) => batchResponder(obj, info), 25 | ]); 26 | ws.emit.apply(ws, args); 27 | return; 28 | } 29 | 30 | // check if method allow 31 | if (!wsRpcMethods.includes(obj.method)) { 32 | const err = { 33 | code: METHOD_NOT_FOUND, 34 | message: `method ${obj.method} not found!`, 35 | }; 36 | return responder(obj, err, null); 37 | } 38 | 39 | const args = [obj.method].concat(obj.params, [ 40 | (err: any, result: any) => responder(obj, err, result), 41 | ]); 42 | ws.emit.apply(ws, args); 43 | } catch { 44 | ws.close(); 45 | } 46 | } 47 | 48 | function responder(obj: any, err: any, result: any) { 49 | const respObj: JsonRpcRequestResult = { 50 | id: obj.id, 51 | jsonrpc: "2.0", 52 | }; 53 | if (err == null) { 54 | respObj.result = result; 55 | } else { 56 | respObj.error = err; 57 | } 58 | const resp = JSON.stringify(respObj); 59 | ws.send(resp); 60 | } 61 | 62 | function batchResponder(objs: any[], info: any[]) { 63 | const respObjs = objs.map((o, i) => { 64 | const { err, result } = info[i]; 65 | const respObj: JsonRpcRequestResult = { 66 | id: o.id, 67 | jsonrpc: "2.0", 68 | }; 69 | if (err == null) { 70 | respObj.result = result; 71 | } else { 72 | respObj.error = err; 73 | } 74 | return respObj; 75 | }); 76 | 77 | const resp = JSON.stringify(respObjs); 78 | ws.send(resp); 79 | } 80 | 81 | function logRequest( 82 | obj: JSONRPCVersionTwoRequest | Array 83 | ) { 84 | if (envConfig.logRequestBody) { 85 | return logger.info("websocket request.body:", obj); 86 | } 87 | 88 | if (Array.isArray(obj)) { 89 | return logger.info( 90 | "websocket request.method:", 91 | obj.map((o) => o.method) 92 | ); 93 | } 94 | 95 | return logger.info("websocket request.method:", obj.method); 96 | } 97 | } 98 | 99 | interface JsonRpcRequestResult { 100 | id: any; 101 | jsonrpc: "2.0"; 102 | error?: any; 103 | result?: any; 104 | } 105 | -------------------------------------------------------------------------------- /packages/api-server/cli/fix-log-transaction-index.ts: -------------------------------------------------------------------------------- 1 | import Knex, { Knex as KnexType } from "knex"; 2 | import { DBLog } from "../src/db/types"; 3 | import commander from "commander"; 4 | import dotenv from "dotenv"; 5 | 6 | export async function fixLogTransactionIndexRun(program: commander.Command) { 7 | try { 8 | let databaseUrl = (program as any).databaseUrl; 9 | if (databaseUrl == null) { 10 | dotenv.config({ path: "./.env" }); 11 | databaseUrl = process.env.DATABASE_URL; 12 | } 13 | 14 | if (databaseUrl == null) { 15 | throw new Error("Please provide --database-url"); 16 | } 17 | 18 | await fixLogTransactionIndex(databaseUrl); 19 | process.exit(0); 20 | } catch (e) { 21 | console.error(e); 22 | process.exit(1); 23 | } 24 | } 25 | 26 | export async function wrongLogTransactionIndexCountRun( 27 | program: commander.Command 28 | ): Promise { 29 | try { 30 | let databaseUrl = (program as any).databaseUrl; 31 | if (databaseUrl == null) { 32 | dotenv.config({ path: "./.env" }); 33 | databaseUrl = process.env.DATABASE_URL; 34 | } 35 | if (databaseUrl == null) { 36 | throw new Error("Please provide --database-url"); 37 | } 38 | await wrongLogTransactionIndexCount(databaseUrl); 39 | process.exit(0); 40 | } catch (e) { 41 | console.error(e); 42 | process.exit(1); 43 | } 44 | } 45 | 46 | // fix for leading zeros 47 | export async function fixLogTransactionIndex( 48 | databaseUrl: string 49 | ): Promise { 50 | const knex = getKnex(databaseUrl); 51 | 52 | const query = knex("logs") 53 | .whereRaw( 54 | "transaction_index <> (select transaction_index from transactions where hash = logs.transaction_hash)" 55 | ) 56 | .count(); 57 | const sql = query.toSQL(); 58 | console.log("Query SQL:", sql.sql); 59 | const wrongLogsCount = await query; 60 | console.log(`Found ${wrongLogsCount[0].count} wrong logs`); 61 | 62 | const _updateQuery = await knex.raw( 63 | "update logs set transaction_index = subquery.transaction_index from (select transaction_index, hash from transactions) as subquery where logs.transaction_hash = subquery.hash and logs.transaction_index <> subquery.transaction_index;" 64 | ); 65 | 66 | console.log(`All logs updated!`); 67 | } 68 | 69 | async function wrongLogTransactionIndexCount( 70 | databaseUrl: string 71 | ): Promise { 72 | const knex = getKnex(databaseUrl); 73 | 74 | const query = knex("logs") 75 | .whereRaw( 76 | "transaction_index <> (select transaction_index from transactions where hash = logs.transaction_hash)" 77 | ) 78 | .count(); 79 | const sql = query.toSQL(); 80 | console.log("Query SQL:", sql.sql); 81 | const logsCount = await query; 82 | console.log(`Found ${logsCount[0].count} wrong logs`); 83 | } 84 | 85 | function getKnex(databaseUrl: string): KnexType { 86 | const knex = Knex({ 87 | client: "postgresql", 88 | connection: { 89 | connectionString: databaseUrl, 90 | keepAlive: true, 91 | }, 92 | pool: { min: 2, max: 20 }, 93 | }); 94 | return knex; 95 | } 96 | -------------------------------------------------------------------------------- /packages/api-server/src/methods/error.ts: -------------------------------------------------------------------------------- 1 | import { JSONRPCError } from "jayson"; 2 | import { logger } from "../base/logger"; 3 | import { HEADER_NOT_FOUND_ERR_MESSAGE } from "./constant"; 4 | import { 5 | TRANSACTION_EXECUTION_ERROR, 6 | HEADER_NOT_FOUND_ERROR, 7 | INTERNAL_ERROR, 8 | INVALID_PARAMS, 9 | LIMIT_EXCEEDED, 10 | METHOD_NOT_SUPPORT, 11 | WEB3_ERROR, 12 | } from "./error-code"; 13 | import { HexString } from "@ckb-lumos/base"; 14 | 15 | // evmc/polyjuice/godwoken exit code error 16 | export interface Extra { 17 | exit_code: string; 18 | message: string; 19 | stack: ExtraStack; 20 | } 21 | // exit code throw stack 22 | export enum ExtraStack { 23 | evmc = "EVMC", 24 | poly = "POLYJUICE", 25 | gw = "GODWOKEN", 26 | unknown = "UNKNOWN", 27 | } 28 | 29 | export class RpcError extends Error implements JSONRPCError { 30 | code: number; 31 | data?: any; 32 | extra?: Extra; 33 | 34 | constructor(code: number, message: string, data?: any, extra?: Extra) { 35 | super(message); 36 | this.name = "RpcError"; 37 | 38 | this.code = code; 39 | this.data = data; 40 | this.extra = extra; 41 | } 42 | } 43 | 44 | export function isRpcError(err: any): err is RpcError { 45 | return err.name === "RpcError"; 46 | } 47 | 48 | export class TransactionExecutionError extends RpcError { 49 | constructor(message: string, data?: HexString, extra?: Extra) { 50 | super(TRANSACTION_EXECUTION_ERROR, message, data, extra); 51 | } 52 | } 53 | 54 | export class Web3Error extends RpcError { 55 | constructor(message: string, data?: object) { 56 | super(WEB3_ERROR, message, data); 57 | } 58 | } 59 | 60 | export class InvalidParamsError extends RpcError { 61 | constructor(message: string) { 62 | super(INVALID_PARAMS, message); 63 | } 64 | 65 | padContext(context: string): InvalidParamsError { 66 | const msgs = this.message.split(/(invalid argument .: )/); 67 | // [ '', 'invalid argument : ', 'message' ] 68 | if (msgs.length !== 3) { 69 | logger.error( 70 | `[InvalidParamsError] padContext parse message failed: ${ 71 | this.message 72 | }, split array: ${JSON.stringify(msgs)}, will return origin error.` 73 | ); 74 | return this; 75 | } 76 | const newMsg = `${msgs[1]}${context} -> ${msgs[2]}`; 77 | this.message = newMsg; 78 | return this; 79 | } 80 | } 81 | 82 | export class InternalError extends RpcError { 83 | constructor(message: string) { 84 | super(INTERNAL_ERROR, message); 85 | } 86 | } 87 | 88 | export class MethodNotSupportError extends RpcError { 89 | constructor(message: string) { 90 | super(METHOD_NOT_SUPPORT, message); 91 | } 92 | } 93 | 94 | export class HeaderNotFoundError extends RpcError { 95 | constructor(message: string = HEADER_NOT_FOUND_ERR_MESSAGE) { 96 | super(HEADER_NOT_FOUND_ERROR, message); 97 | } 98 | } 99 | 100 | export class LimitExceedError extends RpcError { 101 | constructor(message: string) { 102 | super(LIMIT_EXCEEDED, message); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /packages/api-server/migrations/20220512033018_refactor_tables.ts: -------------------------------------------------------------------------------- 1 | import { Knex } from "knex"; 2 | 3 | // u32: bigint(i64) 4 | // u64: decimal(20, 0) 5 | // u128: decimal(40, 0) 6 | // u256: decimal(80, 0) 7 | export async function up(knex: Knex): Promise { 8 | await knex.schema 9 | .createTable("blocks", function (table: Knex.TableBuilder) { 10 | table.decimal("number", null, 0).primary().notNullable(); 11 | table.binary("hash").notNullable().unique(); 12 | table.binary("parent_hash").notNullable(); 13 | table.decimal("gas_limit", null, 0).notNullable(); 14 | table.decimal("gas_used", null, 0).notNullable(); 15 | table.binary("miner").notNullable(); 16 | table.integer("size").notNullable(); 17 | table.timestamp("timestamp").notNullable(); 18 | }) 19 | .createTable("transactions", function (table: Knex.TableBuilder) { 20 | table.bigIncrements("id"); 21 | table.binary("hash").notNullable().unique(); 22 | table.binary("eth_tx_hash").notNullable().unique(); 23 | table 24 | .decimal("block_number", null, 0) 25 | .notNullable() 26 | .references("blocks.number"); 27 | table.binary("block_hash").notNullable(); 28 | table.integer("transaction_index").notNullable(); 29 | table.binary("from_address").notNullable().index(); 30 | table.binary("to_address").index(); 31 | // value: uint256 32 | table.decimal("value", 80, 0).notNullable(); 33 | table.bigInteger("nonce").notNullable(); 34 | table.decimal("gas_limit", null, 0); 35 | table.decimal("gas_price", null, 0); 36 | table.binary("input"); 37 | table.smallint("v").notNullable(); 38 | table.binary("r").notNullable(); 39 | table.binary("s").notNullable(); 40 | table.decimal("cumulative_gas_used", null, 0); 41 | table.decimal("gas_used", null, 0); 42 | table.binary("contract_address").index(); 43 | table.smallint("exit_code").notNullable(); 44 | table.unique(["block_hash", "transaction_index"], { 45 | indexName: "block_hash_transaction_index_idx", 46 | }); 47 | table.unique(["block_number", "transaction_index"], { 48 | indexName: "block_number_transaction_index_idx", 49 | }); 50 | }) 51 | .createTable("logs", function (table: Knex.TableBuilder) { 52 | table.bigIncrements("id"); 53 | table 54 | .bigInteger("transaction_id") 55 | .notNullable() 56 | .index() 57 | .references("transactions.id"); 58 | table.binary("transaction_hash").notNullable().index(); 59 | table.integer("transaction_index").notNullable(); 60 | table 61 | .decimal("block_number", null, 0) 62 | .notNullable() 63 | .index() 64 | .references("blocks.number"); 65 | table.binary("block_hash").notNullable().index(); 66 | table.binary("address").notNullable().index(); 67 | table.binary("data"); 68 | table.integer("log_index").notNullable(); 69 | table.specificType("topics", "bytea ARRAY").notNullable(); 70 | }); 71 | } 72 | 73 | export async function down(knex: Knex): Promise { 74 | await knex.schema 75 | .dropTable("logs") 76 | .dropTable("transactions") 77 | .dropTable("blocks"); 78 | } 79 | -------------------------------------------------------------------------------- /packages/api-server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@godwoken-web3/api-server", 3 | "version": "1.10.0-rc2", 4 | "private": true, 5 | "scripts": { 6 | "start": "concurrently \"tsc -w\" \"nodemon ./bin/cluster\"", 7 | "start:prod": "NODE_ENV=production node ./bin/cluster", 8 | "start:pm2": "NODE_ENV=production pm2 start ./bin/cluster --no-daemon --name gw-web3 --max-memory-restart 1G", 9 | "test": "ava", 10 | "fmt": "prettier --write \"{migrations,tests,cli}/**/*.{js,ts,tsx}\" src/* package.json tsconfig.json", 11 | "lint": "eslint -c ../../.eslintrc.js \"{src,tests,cli}/**/*.ts\"", 12 | "reset_database": "NODE_ENV=test knex migrate:down && knex migrate:up && knex seed:run", 13 | "knex": "knex", 14 | "migrate:make": "knex migrate:make", 15 | "migrate:latest": "knex migrate:latest", 16 | "migrate-accounts": "ts-node ./src/migrate-accounts.ts", 17 | "build": "tsc", 18 | "cli": "yarn ts-node cli/index.ts" 19 | }, 20 | "ava": { 21 | "extensions": [ 22 | "ts" 23 | ], 24 | "require": [ 25 | "ts-node/register" 26 | ], 27 | "files": [ 28 | "tests/**/*", 29 | "!tests/www.ts" 30 | ] 31 | }, 32 | "dependencies": { 33 | "@ckb-lumos/base": "0.18.0-rc6", 34 | "@ckb-lumos/toolkit": "0.18.0-rc6", 35 | "@godwoken-web3/godwoken": "1.10.0-rc2", 36 | "@ethersproject/bignumber": "^5.7.0", 37 | "@newrelic/native-metrics": "^7.0.1", 38 | "@opentelemetry/api": "^1.3.0", 39 | "@opentelemetry/exporter-jaeger": "^1.8.0", 40 | "@opentelemetry/instrumentation-express": "^0.32.0", 41 | "@opentelemetry/instrumentation-http": "^0.34.0", 42 | "@opentelemetry/instrumentation-knex": "^0.31.0", 43 | "@opentelemetry/instrumentation-redis-4": "^0.34.0", 44 | "@opentelemetry/instrumentation-winston": "^0.31.0", 45 | "@opentelemetry/propagator-jaeger": "^1.8.0", 46 | "@opentelemetry/sdk-node": "^0.34.0", 47 | "@sentry/node": "^6.11.0", 48 | "axios": "^0.27.2", 49 | "blake2b": "2.1.3", 50 | "commander": "^9.4.0", 51 | "cors": "^2.8.5", 52 | "decimal.js": "^10.4.0", 53 | "dotenv": "^8.2.0", 54 | "ethereum-input-data-decoder": "^0.3.5", 55 | "ethereumjs-util": "^7.0.9", 56 | "express": "^4.17.3", 57 | "express-winston": "^4.2.0", 58 | "express-ws": "^5.0.2", 59 | "http-errors": "~1.6.3", 60 | "immutable": "^4.0.0-rc.12", 61 | "jayson": "^3.6.4", 62 | "keccak256": "^1.0.2", 63 | "knex": "2.0.0", 64 | "leveldown": "^6.0.1", 65 | "levelup": "^5.0.1", 66 | "newrelic": "^8.15.0", 67 | "pg": "8.7.3", 68 | "redis": "^4.2.0", 69 | "rlp": "^2.2.6", 70 | "secp256k1": "^4.0.2", 71 | "v8-profiler-next": "^1.9.0", 72 | "web3-eth-abi": "^1.6.0", 73 | "winston": "^3.7.2" 74 | }, 75 | "devDependencies": { 76 | "@types/cors": "^2.8.12", 77 | "@types/express": "^4.17.13", 78 | "@types/express-ws": "^3.0.1", 79 | "@types/http-errors": "^1.8.2", 80 | "@types/leveldown": "^4.0.3", 81 | "@types/levelup": "^4.3.3", 82 | "@types/newrelic": "^7.0.2", 83 | "@types/secp256k1": "^4.0.2", 84 | "ava": "^4.1.0", 85 | "concurrently": "^6.0.0", 86 | "nodemon": "^2.0.19", 87 | "ts-node": "^9.1.1", 88 | "typescript": "^4.2.2" 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /docs/apis.md: -------------------------------------------------------------------------------- 1 | # APIs 2 | 3 | ## Ethereum Compatible Web3 RPC Modules 4 | 5 | ### net 6 | 7 | - net_version 8 | - net_peerCount 9 | - net_listening 10 | 11 | ### web3 12 | 13 | - web3_sha3 14 | - web3_clientVersion 15 | 16 | ### eth 17 | 18 | - eth_chainId 19 | - eth_protocolVersion 20 | - eth_syncing 21 | - eth_coinbase 22 | - eth_mining 23 | - eth_hashrate 24 | - eth_gasPrice 25 | - eth_blockNumber 26 | - eth_getBalance 27 | - eth_getStorageAt 28 | - eth_getTransactionCount 29 | - eth_getCode 30 | - eth_call 31 | - eth_estimateGas 32 | - eth_getBlockByHash 33 | - eth_getBlockByNumber 34 | - eth_getBlockTransactionCountByHash 35 | - eth_getBlockTransactionCountByNumber 36 | - eth_getUncleByBlockHashAndIndex 37 | - eth_getUncleByBlockNumberAndIndex 38 | - eth_getUncleCountByBlockHash 39 | - eth_getCompilers 40 | - eth_getTransactionByHash 41 | - eth_getTransactionByBlockHashAndIndex 42 | - eth_getTransactionByBlockNumberAndIndex 43 | - eth_getTransactionReceipt 44 | - eth_newFilter 45 | - eth_newBlockFilter 46 | - eth_newPendingTransactionFilter 47 | - eth_uninstallFilter 48 | - eth_getFilterLogs 49 | - eth_getFilterChanges 50 | - eth_getLogs 51 | - eth_getTipNumber 52 | - eth_subscribe (only for WebSocket) 53 | - eth_unsubscribe (only for WebSocket) 54 | 55 | ### Usage 56 | 57 | You can find most usage guidelines from Ethereum RPC docs like 58 | 59 | ### Unsupported Methods 60 | 61 | - eth_accounts (only supported by wallet client) 62 | - eth_sign (only supported by wallet client) 63 | - eth_signTransaction (only supported by wallet client) 64 | - eth_sendTransaction (only supported by wallet client) 65 | 66 | ## Additional Modules 67 | 68 | ### gw (Godwoken RPCs) 69 | 70 | #### Methods 71 | 72 | - gw_ping 73 | - gw_get_tip_block_hash 74 | - gw_get_block_hash 75 | - gw_get_block 76 | - gw_get_block_by_number 77 | - gw_get_balance 78 | - gw_get_storage_at 79 | - gw_get_account_id_by_script_hash 80 | - gw_get_nonce 81 | - gw_get_script 82 | - gw_get_script_hash 83 | - gw_get_data 84 | - gw_get_transaction_receipt 85 | - gw_get_transaction 86 | - gw_execute_l2transaction 87 | - gw_execute_raw_l2transaction 88 | - gw_submit_l2transaction 89 | - gw_submit_withdrawal_request 90 | - gw_get_registry_address_by_script_hash 91 | - gw_get_script_hash_by_registry_address 92 | - gw_get_fee_config 93 | - gw_get_withdrawal 94 | - gw_get_last_submitted_info 95 | - gw_get_node_info 96 | - gw_is_request_in_queue 97 | - gw_get_pending_tx_hashes 98 | - gw_debug_replay_transaction (should enable `Debug` RPC module in Godwoken) 99 | 100 | #### Usage 101 | 102 | Get details at [Godwoken Docs](https://github.com/godwokenrises/godwoken/blob/develop/docs/RPC.md) 103 | 104 | ### poly (Polyjuice RPCs) 105 | 106 | #### Methods 107 | 108 | - poly_getCreatorId 109 | - poly_getDefaultFromId 110 | - poly_getContractValidatorTypeHash 111 | - poly_getRollupTypeHash 112 | - poly_getEthAccountLockHash 113 | - poly_version 114 | - poly_getEthTxHashByGwTxHash 115 | - poly_getGwTxHashByEthTxHash 116 | - poly_getHealthStatus 117 | 118 | #### Usage 119 | 120 | Get details at [Poly APIs doc](poly-apis.md) 121 | 122 | ### debug (Debug RPCs) 123 | 124 | #### Methods 125 | - debug_replayTransaction (should enable `Debug` RPC module in Godwoken) 126 | -------------------------------------------------------------------------------- /packages/api-server/tests/utils/convention.test.ts: -------------------------------------------------------------------------------- 1 | import { snakeToCamel, camelToSnake } from "../../src/util"; 2 | import test from "ava"; 3 | 4 | test("snakeToCamel", (t) => { 5 | const obj = { 6 | hello_world: "hello world", 7 | hello_earth: { 8 | hello_human: { 9 | bob_person: { 10 | alias_name: "bob", 11 | age_now: 34, 12 | }, 13 | }, 14 | hello_cat: { 15 | bob_cat: { 16 | alias_name: "bob", 17 | age_now: 2, 18 | }, 19 | }, 20 | hello_array: [{ first_array: 1 }], 21 | hello_nullable: { 22 | null_val: null, 23 | nan_val: NaN, 24 | undefined_val: undefined, 25 | }, 26 | }, 27 | }; 28 | const expectObj = { 29 | helloWorld: "hello world", 30 | helloEarth: { 31 | helloHuman: { 32 | bobPerson: { 33 | aliasName: "bob", 34 | ageNow: 34, 35 | }, 36 | }, 37 | helloCat: { 38 | bobCat: { 39 | aliasName: "bob", 40 | ageNow: 2, 41 | }, 42 | }, 43 | helloArray: [{ firstArray: 1 }], 44 | helloNullable: { 45 | nullVal: null, 46 | nanVal: NaN, 47 | undefinedVal: undefined, 48 | }, 49 | }, 50 | }; 51 | t.deepEqual(snakeToCamel(obj), expectObj); 52 | }); 53 | 54 | test("camelToSnake", (t) => { 55 | const expectObj = { 56 | hello_world: "hello world", 57 | hello_earth: { 58 | hello_human: { 59 | bob_person: { 60 | alias_name: "bob", 61 | age_now: 34, 62 | }, 63 | }, 64 | hello_cat: { 65 | bob_cat: { 66 | alias_name: "bob", 67 | age_now: 2, 68 | }, 69 | }, 70 | hello_array: [{ first_array: 1 }], 71 | hello_nullable: { 72 | null_val: null, 73 | nan_val: NaN, 74 | undefined_val: undefined, 75 | }, 76 | }, 77 | }; 78 | const obj = { 79 | helloWorld: "hello world", 80 | helloEarth: { 81 | helloHuman: { 82 | bobPerson: { 83 | aliasName: "bob", 84 | ageNow: 34, 85 | }, 86 | }, 87 | helloCat: { 88 | bobCat: { 89 | aliasName: "bob", 90 | ageNow: 2, 91 | }, 92 | }, 93 | helloArray: [{ firstArray: 1 }], 94 | helloNullable: { 95 | nullVal: null, 96 | nanVal: NaN, 97 | undefinedVal: undefined, 98 | }, 99 | }, 100 | }; 101 | t.deepEqual(camelToSnake(obj), expectObj); 102 | }); 103 | 104 | test("overflow", (t) => { 105 | const overflowDepthObj = { 106 | hello_world: { 107 | hello_world: { 108 | hello_world: { 109 | hello_world: { 110 | hello_world: { 111 | hello_world: { 112 | hello_world: { 113 | hello_world: { 114 | hello_world: { 115 | hello_world: ["what a small world!"], 116 | }, 117 | }, 118 | }, 119 | }, 120 | }, 121 | }, 122 | }, 123 | }, 124 | }, 125 | }; 126 | 127 | const depthObj = { 128 | hello_world: { 129 | hello_world: { 130 | hello_world: { 131 | hello_world: { 132 | hello_world: { 133 | hello_world: { 134 | hello_world: { 135 | hello_world: ["what a small world!"], 136 | }, 137 | }, 138 | }, 139 | }, 140 | }, 141 | }, 142 | }, 143 | }; 144 | 145 | t.throws(() => snakeToCamel(overflowDepthObj), { 146 | instanceOf: Error, 147 | message: "[snakeToCamel] recursive depth reached max limit.", 148 | }); 149 | t.notThrows(() => snakeToCamel(depthObj)); 150 | }); 151 | -------------------------------------------------------------------------------- /docs/schema_design.md: -------------------------------------------------------------------------------- 1 | # Schema Design 2 | 3 | ## Sql Schema(postgresql) 4 | ```sql 5 | CREATE TABLE blocks ( 6 | number numeric PRIMARY KEY, 7 | hash bytea NOT NULL, 8 | parent_hash bytea NOT NULL, 9 | gas_limit numeric NOT NULL, 10 | gas_used numeric NOT NULL, 11 | miner bytea NOT NULL, 12 | size integer NOT NULL, 13 | "timestamp" timestamp with time zone NOT NULL 14 | ); 15 | 16 | create unique index on blocks(hash); 17 | 18 | CREATE TABLE transactions ( 19 | id BIGSERIAL PRIMARY KEY, 20 | hash bytea NOT NULL, 21 | eth_tx_hash bytea NOT NULL, 22 | block_number numeric REFERENCES blocks(number) NOT NULL,, 23 | block_hash bytea NOT NULL, 24 | transaction_index integer NOT NULL, 25 | from_address bytea NOT NULL, 26 | to_address bytea, 27 | value numeric(80,0) NOT NULL, 28 | nonce bigint NOT NULL, 29 | gas_limit numeric, 30 | gas_price numeric, 31 | input bytea, 32 | v smallint NOT NULL, 33 | r bytea NOT NULL, 34 | s bytea NOT NULL, 35 | cumulative_gas_used numeric, 36 | gas_used numeric, 37 | contract_address bytea, 38 | exit_code smallint NOT NULL 39 | ); 40 | 41 | CREATE INDEX ON transactions (from_address); 42 | CREATE INDEX ON transactions (to_address); 43 | CREATE INDEX ON transactions (contract_address); 44 | CREATE UNIQUE INDEX block_number_transaction_index_idx ON transactions (block_number, transaction_index); 45 | CREATE UNIQUE INDEX block_hash_transaction_index_idx ON transactions (block_hash, transaction_index); 46 | 47 | CREATE TABLE logs ( 48 | id BIGSERIAL PRIMARY KEY, 49 | transaction_id bigint REFERENCES transactions(id) NOT NULL, 50 | transaction_hash bytea NOT NULL, 51 | transaction_index integer NOT NULL, 52 | block_number numeric REFERENCES blocks(number) NOT NULL, 53 | block_hash bytea NOT NULL, 54 | address bytea NOT NULL, 55 | data bytea, 56 | log_index integer NOT NULL, 57 | topics bytea[] NOT NULL 58 | ); 59 | 60 | CREATE INDEX ON logs (transaction_id); 61 | CREATE INDEX ON logs (transaction_hash); 62 | CREATE INDEX ON logs (block_hash); 63 | CREATE INDEX ON logs (address); 64 | CREATE INDEX ON logs (block_number); 65 | ``` 66 | 67 | ## 字段含义 68 | 69 | ### block 70 | 71 | - number: 区块高度,由于godwoken只会revert不会分叉,在同一个高度只存在一个区块,故可以作为主键 72 | - hash: 区块哈希 73 | - parent_hash: 上一个区块hash 74 | - gas_limit: 该区块最多能花的gas_limit, 等于各个交易的gas_limit之和,这个值在eth里面现在不超过12.5million 75 | - gas_used: 该区块内所有交易花费的gas之和 76 | - timestamp: 区块的时间戳 77 | - miner: godwoken里指的是block producer,这里miner字段与web3接口保持一致 78 | - size: 区块大小,bytes 79 | 80 | 81 | ### transaction 82 | - hash: 交易哈希 83 | - eth_tx_hash: 交易的以太坊格式交易哈希 84 | - block_number:区块高度 85 | - block_hash:区块哈希 86 | - transaction_index:交易在区块里的位置,这个和L2Transaction在L2Block的位置存在差异 87 | - from_address:交易发出方,对应godwoken里面L2Transaction的from_id 88 | - to_address: 交易接受方,在eth中如果是合约创建交易则为null;在godwoken中需要解析L2Transaction的args(不同于to_id概念),提取出sudt转账交易的接受账户,或者是polyjuice交易的接受账户(合约) 89 | - value: 转账额度(是sudt的转账还是polyjuice的转账?) 90 | - nonce: 地址发出过的交易数量,单调递增(polyjuice交易是否有单独的nonce?) 91 | - gas_limit: polyjuice交易的gas_limit,非polyjuice交易设置为0 92 | - gas_price: polyjuice交易的gas_price,非polyjuice交易设置为0 93 | - input: solidity合约调用的input,非polyjuice交易设置为null 94 | - v: ECDSA recovery ID 95 | - r: ECDSA signature 96 | - s: ECDSA signature 97 | - cumulative_gas_used: 该区块里当前交易和之前的交易花费的gas之和 98 | - gas_used:交易实际花费的gas 99 | - log_bloom:该交易中logs的bloom filter 100 | - contract_address: 如果是合约创建交易,这个则为创建的合约的地址;否则为null 101 | - exit_code: 表示交易是否成功,0成功,其它为失败 102 | 103 | ### log 104 | - transaction_id: 交易id,transaction表主键 105 | - transaction_hash:交易哈希 106 | - transaction_index:交易在区块中位置 107 | - block_number:区块高度 108 | - block_hash:区块哈希 109 | - address:产生这条log的地址,一般是某个合约地址 110 | - log_index:log在交易receipt中的位置 111 | - topics: 112 | - topic[0]: Event的签名,`keccak(EVENT_NAME+"("+EVENT_ARGS.map(canonical_type_of).join(",")+")")` ,对于anonymous event不生成该topic 113 | - topic[1] ~ topic[3]: 被indexed字段修饰的Event参数 114 | - data:non-indexed的Event参数 115 | -------------------------------------------------------------------------------- /packages/api-server/src/base/address.ts: -------------------------------------------------------------------------------- 1 | import { Hash, HexString, Script, utils } from "@ckb-lumos/base"; 2 | import { GodwokenClient } from "@godwoken-web3/godwoken"; 3 | import { Store } from "../cache/store"; 4 | import { COMPATIBLE_DOCS_URL } from "../methods/constant"; 5 | import { gwConfig } from "./index"; 6 | import { logger } from "./logger"; 7 | import { Uint32 } from "./types/uint"; 8 | 9 | const ZERO_ETH_ADDRESS = "0x" + "00".repeat(20); 10 | 11 | // the eth address vs script hash is not changeable, so we set no expire for cache 12 | const scriptHashCache = new Store(false); 13 | 14 | // Only support eth address now! 15 | export class EthRegistryAddress { 16 | private registryId: number = +gwConfig.accounts.ethAddrReg.id; 17 | private addressByteSize: number = 20; 18 | public readonly address: HexString; 19 | 20 | constructor(address: HexString) { 21 | if (!address.startsWith("0x") || address.length != 42) { 22 | throw new Error(`Eth address format error: ${address}`); 23 | } 24 | this.address = address.toLowerCase(); 25 | } 26 | 27 | public serialize(): HexString { 28 | return ( 29 | "0x" + 30 | new Uint32(this.registryId).toLittleEndian().slice(2) + 31 | new Uint32(this.addressByteSize).toLittleEndian().slice(2) + 32 | this.address.slice(2) 33 | ); 34 | } 35 | 36 | public static Deserialize(hex: HexString): EthRegistryAddress { 37 | const hexWithoutPrefix = hex.slice(2); 38 | // const registryId: number = Uint32.fromLittleEndian(hexWithoutPrefix.slice(0, 8)).getValue(); 39 | const addressByteSize: number = Uint32.fromLittleEndian( 40 | hexWithoutPrefix.slice(8, 16) 41 | ).getValue(); 42 | const address: HexString = hexWithoutPrefix.slice(16); 43 | if (addressByteSize !== 20 || address.length !== 40) { 44 | throw new Error(`Eth address deserialize error: ${hex}`); 45 | } 46 | return new EthRegistryAddress("0x" + address); 47 | } 48 | } 49 | 50 | export async function ethAddressToScriptHash( 51 | ethAddress: HexString, 52 | godwokenClient: GodwokenClient 53 | ): Promise { 54 | // try get result from redis cache 55 | const CACHE_KEY_PREFIX = "ethAddressToScriptHash"; 56 | let result = await scriptHashCache.get(`${CACHE_KEY_PREFIX}:${ethAddress}`); 57 | if (result != null) { 58 | logger.debug( 59 | `[ethAddressToScriptHash] using cache: ${ethAddress} -> ${result}` 60 | ); 61 | return result; 62 | } 63 | 64 | const registryAddress: EthRegistryAddress = new EthRegistryAddress( 65 | ethAddress 66 | ); 67 | const scriptHash: Hash | undefined = 68 | await godwokenClient.getScriptHashByRegistryAddress( 69 | registryAddress.serialize() 70 | ); 71 | 72 | // add cache 73 | if (scriptHash != null) { 74 | logger.debug( 75 | `[ethAddressToScriptHash] update cache: ${ethAddress} -> ${scriptHash}` 76 | ); 77 | scriptHashCache.insert(`${CACHE_KEY_PREFIX}:${ethAddress}`, scriptHash); 78 | } 79 | 80 | return scriptHash; 81 | } 82 | 83 | export async function ethAddressToAccountId( 84 | ethAddress: HexString, 85 | godwokenClient: GodwokenClient 86 | ): Promise { 87 | if (ethAddress === "0x") { 88 | return +gwConfig.accounts.polyjuiceCreator.id; 89 | } 90 | 91 | if (ethAddress === ZERO_ETH_ADDRESS) { 92 | throw new Error( 93 | `zero address ${ZERO_ETH_ADDRESS} has no valid account_id! more info: ${COMPATIBLE_DOCS_URL}` 94 | ); 95 | } 96 | 97 | const scriptHash: Hash | undefined = await ethAddressToScriptHash( 98 | ethAddress, 99 | godwokenClient 100 | ); 101 | if (scriptHash == null) { 102 | return undefined; 103 | } 104 | 105 | const id: number | undefined = await godwokenClient.getAccountIdByScriptHash( 106 | scriptHash 107 | ); 108 | return id; 109 | } 110 | 111 | export function ethEoaAddressToScriptHash(address: string) { 112 | const script: Script = { 113 | code_hash: gwConfig.eoaScripts.eth.typeHash, 114 | hash_type: "type", 115 | args: gwConfig.rollupCell.typeHash + address.slice(2), 116 | }; 117 | const scriptHash = utils.computeScriptHash(script); 118 | return scriptHash; 119 | } 120 | -------------------------------------------------------------------------------- /packages/api-server/src/cache/guard.ts: -------------------------------------------------------------------------------- 1 | require("newrelic"); 2 | import { Store } from "./store"; 3 | import { HexString } from "@ckb-lumos/base"; 4 | import fs from "fs"; 5 | import path from "path"; 6 | import { CACHE_EXPIRED_TIME_MILSECS } from "./constant"; 7 | import { logger } from "../base/logger"; 8 | 9 | const RedisPrefixName = "access"; 10 | const configPath = path.resolve(__dirname, "../../rate-limit-config.json"); 11 | 12 | export const EXPIRED_TIME_MILSECS = 1 * 60 * 1000; // milsec, default 1 minutes 13 | export const MAX_REQUEST_COUNT = 30; 14 | 15 | export interface RateLimitConfig { 16 | expired_time_milsec: number; 17 | methods: RpcMethodLimit; 18 | } 19 | 20 | export interface RpcMethodLimit { 21 | [reqMethod: string]: number; // max rpc method request counts in expired_time 22 | } 23 | 24 | export function getRateLimitConfig() { 25 | if (fs.existsSync(configPath)) { 26 | // todo: validate config 27 | const config: RateLimitConfig = require(configPath); 28 | return config; 29 | } 30 | 31 | // default config, no rpc method apply rate limit 32 | return { 33 | expired_time_milsec: EXPIRED_TIME_MILSECS, 34 | methods: {}, 35 | } as RateLimitConfig; 36 | } 37 | 38 | export class AccessGuard { 39 | public store: Store; 40 | public rpcMethods: RpcMethodLimit; 41 | public expiredTimeMilsecs: number; 42 | 43 | constructor( 44 | enableExpired = true, 45 | expiredTimeMilsecs?: number, // milsec, default 1 minutes 46 | store?: Store 47 | ) { 48 | const config = getRateLimitConfig(); 49 | logger.debug("rate-limit-config:", config); 50 | expiredTimeMilsecs = expiredTimeMilsecs || config.expired_time_milsec; 51 | this.store = store || new Store(enableExpired, expiredTimeMilsecs); 52 | this.rpcMethods = config.methods; 53 | this.expiredTimeMilsecs = expiredTimeMilsecs || CACHE_EXPIRED_TIME_MILSECS; 54 | } 55 | 56 | async setMaxReqLimit(rpcMethod: string, maxReqCount: number) { 57 | this.rpcMethods[rpcMethod] = maxReqCount; 58 | } 59 | 60 | async getCount(rpcMethod: string, reqId: string) { 61 | const id = getId(rpcMethod, reqId); 62 | const count = await this.store.get(id); 63 | if (count == null) { 64 | return null; 65 | } 66 | return +count; 67 | } 68 | 69 | async add(rpcMethod: string, reqId: string): Promise { 70 | const isExist = await this.isExist(rpcMethod, reqId); 71 | if (!isExist) { 72 | const id = getId(rpcMethod, reqId); 73 | await this.store.insert(id, 0); 74 | return id; 75 | } 76 | } 77 | 78 | async updateCount(rpcMethod: string, reqId: string) { 79 | const isExist = await this.isExist(rpcMethod, reqId); 80 | if (isExist === true) { 81 | const id = getId(rpcMethod, reqId); 82 | await this.store.incr(id); 83 | } 84 | } 85 | 86 | async isExist(rpcMethod: string, reqId: string) { 87 | const id = getId(rpcMethod, reqId); 88 | const data = await this.store.get(id); 89 | if (data == null) return false; 90 | return true; 91 | } 92 | 93 | async isOverRate(rpcMethod: string, reqId: string): Promise { 94 | const id = getId(rpcMethod, reqId); 95 | const data = await this.store.get(id); 96 | if (data == null) return false; 97 | if (this.rpcMethods[rpcMethod] == null) return false; 98 | 99 | const count = +data; 100 | const maxNumber = this.rpcMethods[rpcMethod]; 101 | if (count > maxNumber) { 102 | return true; 103 | } 104 | return false; 105 | } 106 | 107 | async getKeyTTL(rpcMethod: string, reqId: string) { 108 | const id = getId(rpcMethod, reqId); 109 | const remainSecs = await this.store.ttl(id); 110 | if (remainSecs === -1) { 111 | const value = (await this.store.get(id)) || "0"; 112 | logger.info( 113 | `key ${id} with no ttl, reset: ${this.expiredTimeMilsecs}ms, ${value}` 114 | ); 115 | await this.store.insert(id, value, this.expiredTimeMilsecs / 1000); 116 | return await this.store.ttl(id); 117 | } 118 | return remainSecs; 119 | } 120 | } 121 | 122 | export function getId(rpcMethod: string, reqUniqueId: string): HexString { 123 | return `${RedisPrefixName}.${rpcMethod}.${reqUniqueId}`; 124 | } 125 | -------------------------------------------------------------------------------- /packages/api-server/src/app/app.ts: -------------------------------------------------------------------------------- 1 | import createError from "http-errors"; 2 | import express from "express"; 3 | import { jaysonMiddleware } from "../middlewares/jayson"; 4 | import cors from "cors"; 5 | import { wrapper } from "../ws/methods"; 6 | import expressWs from "express-ws"; 7 | import Sentry from "@sentry/node"; 8 | import { applyRateLimitByIp } from "../rate-limit"; 9 | import { initSentry } from "../sentry"; 10 | import { envConfig } from "../base/env-config"; 11 | import { gwConfig } from "../base/index"; 12 | import { expressLogger, logger } from "../base/logger"; 13 | import { Server } from "http"; 14 | 15 | const newrelic = require("newrelic"); 16 | 17 | const app: express.Express = express(); 18 | 19 | const BODY_PARSER_LIMIT = "100mb"; 20 | 21 | app.use(express.json({ limit: BODY_PARSER_LIMIT })); 22 | 23 | const sentryOptionRequest = [ 24 | "cookies", 25 | "data", 26 | "headers", 27 | "method", 28 | "query_string", 29 | "url", 30 | "body", 31 | ]; 32 | if (envConfig.sentryDns) { 33 | initSentry(); 34 | 35 | // The request handler must be the first middleware on the app 36 | app.use( 37 | Sentry.Handlers.requestHandler({ 38 | request: sentryOptionRequest, 39 | }) 40 | ); 41 | } 42 | 43 | expressWs(app); 44 | 45 | const corsOptions: cors.CorsOptions = { 46 | origin: "*", 47 | optionsSuccessStatus: 200, // some legacy browsers (IE11, various SmartTVs) choke on 204 48 | credentials: true, 49 | }; 50 | 51 | app.use(expressLogger); 52 | app.use(cors(corsOptions)); 53 | app.use(express.urlencoded({ extended: false, limit: BODY_PARSER_LIMIT })); 54 | 55 | app.use( 56 | ( 57 | req: express.Request, 58 | _res: express.Response, 59 | next: express.NextFunction 60 | ) => { 61 | const transactionName = `${req.method} ${req.url}#${req.body.method}`; 62 | logger.debug("#transactionName:", transactionName); 63 | newrelic.setTransactionName(transactionName); 64 | 65 | // log request method / body 66 | if (envConfig.logRequestBody) { 67 | logger.debug("request.body:", req.body); 68 | } 69 | 70 | next(); 71 | } 72 | ); 73 | 74 | app.use( 75 | async ( 76 | req: express.Request, 77 | res: express.Response, 78 | next: express.NextFunction 79 | ) => { 80 | // restrict access rate limit via ip 81 | await applyRateLimitByIp(req, res, next); 82 | } 83 | ); 84 | 85 | (app as any).ws("/ws", wrapper); 86 | app.use("/", jaysonMiddleware); 87 | 88 | if (envConfig.sentryDns) { 89 | // The error handler must be before any other error middleware and after all controllers 90 | app.use( 91 | Sentry.Handlers.errorHandler({ 92 | // request: sentryOptionRequest, 93 | }) 94 | ); 95 | } 96 | 97 | // catch 404 and forward to error handler 98 | app.use( 99 | ( 100 | _req: express.Request, 101 | _res: express.Response, 102 | next: express.NextFunction 103 | ) => { 104 | next(createError(404)); 105 | } 106 | ); 107 | 108 | // error handler 109 | app.use(function ( 110 | err: any, 111 | req: express.Request, 112 | res: express.Response, 113 | next: express.NextFunction 114 | ) { 115 | logger.error(err.stack); 116 | 117 | // set locals, only providing error in development 118 | res.locals.message = err.message; 119 | res.locals.error = req.app.get("env") === "development" ? err : {}; 120 | 121 | // render the error page 122 | logger.error("err.status:", err.status); 123 | if (res.headersSent) { 124 | return next(err); 125 | } 126 | res.status(err.status || 500); 127 | res.render("error"); 128 | }); 129 | 130 | let server: Server | undefined; 131 | 132 | async function startServer(port: number): Promise { 133 | try { 134 | await gwConfig.init(); 135 | logger.info("godwoken config initialized!"); 136 | } catch (err) { 137 | logger.error("godwoken config initialize failed:", err); 138 | process.exit(1); 139 | } 140 | 141 | server = app.listen(port, () => { 142 | const addr = (server as Server).address(); 143 | const bind = 144 | typeof addr === "string" ? "pipe " + addr : "port " + addr!.port; 145 | logger.info("godwoken-web3-api:server Listening on " + bind); 146 | }); 147 | } 148 | 149 | function isListening() { 150 | if (server == null) { 151 | return false; 152 | } 153 | return server.listening; 154 | } 155 | 156 | export { startServer, isListening }; 157 | -------------------------------------------------------------------------------- /packages/api-server/src/rate-limit.ts: -------------------------------------------------------------------------------- 1 | import { AccessGuard } from "./cache/guard"; 2 | import { LIMIT_EXCEEDED } from "./methods/error-code"; 3 | import { Request, Response, NextFunction } from "express"; 4 | import { logger } from "./base/logger"; 5 | import { JSONRPCError } from "jayson"; 6 | 7 | export const accessGuard = new AccessGuard(); 8 | 9 | export async function wsApplyRateLimitByIp(req: Request, method: string) { 10 | const ip = getIp(req); 11 | const methods = Object.keys(accessGuard.rpcMethods); 12 | if (methods.includes(method) && ip != null) { 13 | const res = await wsRateLimit(method, ip); 14 | if (res != null) { 15 | return res.error; 16 | } 17 | } 18 | return undefined; 19 | } 20 | 21 | export async function applyRateLimitByIp( 22 | req: Request, 23 | res: Response, 24 | next: NextFunction 25 | ) { 26 | const methods = Object.keys(accessGuard.rpcMethods); 27 | if (methods.length === 0) { 28 | return next(); 29 | } 30 | 31 | let isResSent = false; 32 | for (const method of methods) { 33 | const ip = getIp(req); 34 | const isBan = await rateLimit(req, res, method, ip); 35 | 36 | if (isBan) { 37 | // if one method is ban, we refuse all 38 | isResSent = true; 39 | break; 40 | } 41 | } 42 | 43 | if (!isResSent) { 44 | next(); 45 | } 46 | } 47 | 48 | export async function rateLimit( 49 | req: Request, 50 | res: Response, 51 | rpcMethod: string, 52 | reqId: string | undefined 53 | ) { 54 | let isBan = false; 55 | if (hasMethod(req.body, rpcMethod) && reqId != null) { 56 | const isExist = await accessGuard.isExist(rpcMethod, reqId); 57 | if (!isExist) { 58 | await accessGuard.add(rpcMethod, reqId); 59 | } 60 | 61 | const isOverRate = await accessGuard.isOverRate(rpcMethod, reqId); 62 | if (isOverRate) { 63 | isBan = true; 64 | 65 | const remainSecs = await accessGuard.getKeyTTL(rpcMethod, reqId); 66 | const remainMilsecs = remainSecs * 1000; 67 | const httpRateLimitCode = 429; 68 | const httpRateLimitHeader = { 69 | "Retry-After": remainMilsecs.toString(), 70 | }; 71 | 72 | const message = `Too Many Requests, IP: ${reqId}, please wait ${remainSecs}s and retry. RPC method: ${rpcMethod}.`; 73 | const error = { 74 | code: LIMIT_EXCEEDED, 75 | message: message, 76 | }; 77 | 78 | logger.debug( 79 | `Rate Limit Exceed, ip: ${reqId}, method: ${rpcMethod}, ttl: ${remainSecs}s` 80 | ); 81 | 82 | const content = Array.isArray(req.body) 83 | ? req.body.map((b) => { 84 | return { 85 | jsonrpc: "2.0", 86 | id: b.id, 87 | error: error, 88 | }; 89 | }) 90 | : { 91 | jsonrpc: "2.0", 92 | id: req.body.id, 93 | error: error, 94 | }; 95 | res.status(httpRateLimitCode).header(httpRateLimitHeader).send(content); 96 | } else { 97 | await accessGuard.updateCount(rpcMethod, reqId); 98 | } 99 | } 100 | return isBan; 101 | } 102 | 103 | export async function wsRateLimit( 104 | rpcMethod: string, 105 | reqId: string 106 | ): Promise<{ error: JSONRPCError; remainSecs: number } | undefined> { 107 | const isExist = await accessGuard.isExist(rpcMethod, reqId); 108 | if (!isExist) { 109 | await accessGuard.add(rpcMethod, reqId); 110 | } 111 | 112 | const isOverRate = await accessGuard.isOverRate(rpcMethod, reqId); 113 | if (isOverRate) { 114 | const remainSecs = await accessGuard.getKeyTTL(rpcMethod, reqId); 115 | 116 | const message = `Too Many Requests, IP: ${reqId}, please wait ${remainSecs}s and retry. RPC method: ${rpcMethod}.`; 117 | const error: JSONRPCError = { 118 | code: LIMIT_EXCEEDED, 119 | message: message, 120 | }; 121 | 122 | logger.debug( 123 | `Rate Limit Exceed, ip: ${reqId}, method: ${rpcMethod}, ttl: ${remainSecs}s` 124 | ); 125 | return { error, remainSecs }; 126 | } else { 127 | await accessGuard.updateCount(rpcMethod, reqId); 128 | } 129 | return undefined; 130 | } 131 | 132 | export function hasMethod(body: any, name: string) { 133 | if (Array.isArray(body)) { 134 | return body.map((b) => b.method).includes(name); 135 | } 136 | 137 | return body.method === name; 138 | } 139 | 140 | export function getIp(req: Request) { 141 | let ip; 142 | if (req.headers["x-forwarded-for"] != null) { 143 | ip = (req.headers["x-forwarded-for"] as string).split(",")[0].trim(); 144 | } 145 | 146 | return ip || req.socket.remoteAddress; 147 | } 148 | -------------------------------------------------------------------------------- /packages/api-server/src/util.ts: -------------------------------------------------------------------------------- 1 | import { HexString } from "@ckb-lumos/base"; 2 | import { Request } from "express"; 3 | import { 4 | TX_DATA_NONE_ZERO_GAS, 5 | TX_DATA_ZERO_GAS, 6 | TX_GAS, 7 | TX_GAS_CONTRACT_CREATION, 8 | } from "./methods/constant"; 9 | 10 | const { platform } = require("os"); 11 | const { version: packageVersion } = require("../../../package.json"); 12 | 13 | export function getClientVersion() { 14 | //todo: change to rust process version 15 | const { version } = process; 16 | return `Godwoken/v${packageVersion}/${platform()}/node${version.substring( 17 | 1 18 | )}`; 19 | } 20 | 21 | export function toCamel(s: string) { 22 | return s.replace(/([-_][a-z])/gi, ($1) => { 23 | return $1.toUpperCase().replace("-", "").replace("_", ""); 24 | }); 25 | } 26 | 27 | export function toSnake(s: string) { 28 | return s.replace(/[A-Z]/g, (letter) => `_${letter.toLowerCase()}`); 29 | } 30 | 31 | export function isObjectOrArray(obj: any) { 32 | if ( 33 | Object.prototype.toString.call(obj) === "[object Object]" || 34 | Object.prototype.toString.call(obj) === "[object Array]" 35 | ) { 36 | return true; 37 | } 38 | return false; 39 | } 40 | 41 | // convert object/array key snake_name => camelName 42 | export function snakeToCamel( 43 | t: object, 44 | excludeKeys: string[] = [], 45 | depthLimit: number = 10 // prevent memory leak for recursive 46 | ) { 47 | if (depthLimit === 0) { 48 | throw new Error("[snakeToCamel] recursive depth reached max limit."); 49 | } 50 | 51 | if (!isObjectOrArray(t)) { 52 | return t; 53 | } 54 | 55 | let camel: any = {}; 56 | Object.entries(t).map(([key, value]) => { 57 | let newValue = snakeToCamel(value, excludeKeys, depthLimit - 1); 58 | const newKey = excludeKeys.includes(key) ? key : toCamel(key); 59 | camel[newKey] = Array.isArray(value) ? Object.values(newValue) : newValue; 60 | }); 61 | return camel; 62 | } 63 | 64 | // convert object key camelName => snake_name 65 | export function camelToSnake( 66 | t: object, 67 | excludeKeys: string[] = [], 68 | depthLimit: number = 10 // prevent memory leak for recursive 69 | ) { 70 | if (depthLimit === 0) { 71 | throw new Error("[camelToSnake] recursive depth reached max limit."); 72 | } 73 | 74 | if (!isObjectOrArray(t)) { 75 | return t; 76 | } 77 | 78 | let snake: any = {}; 79 | Object.entries(t).map(([key, value]) => { 80 | let newValue = camelToSnake(value, excludeKeys, depthLimit - 1); 81 | const newKey = excludeKeys.includes(key) ? key : toSnake(key); 82 | snake[newKey] = Array.isArray(value) ? Object.values(newValue) : newValue; 83 | }); 84 | return snake; 85 | } 86 | 87 | export function toHex(i: number | string) { 88 | if (typeof i !== "number" && typeof i !== "string") return i; 89 | 90 | return "0x" + BigInt(i).toString(16); 91 | } 92 | 93 | export function validateHexString(hex: string): boolean { 94 | return /^0x([0-9a-fA-F][0-9a-fA-F])*$/.test(hex); 95 | } 96 | 97 | export function validateHexNumber(hex: string): boolean { 98 | return /^0x(0|[0-9a-fA-F]+)$/.test(hex); 99 | } 100 | 101 | export function calcIntrinsicGas( 102 | to: HexString | undefined, 103 | input: HexString | undefined 104 | ) { 105 | to = to === "0x" ? undefined : to; 106 | const isCreate = to == null; 107 | let gas: bigint; 108 | if (isCreate) { 109 | gas = BigInt(TX_GAS_CONTRACT_CREATION); 110 | } else { 111 | gas = BigInt(TX_GAS); 112 | } 113 | 114 | if (input && input.length > 0) { 115 | const buf = Buffer.from(input.slice(2), "hex"); 116 | const byteLen = buf.byteLength; 117 | let nonZeroLen = 0; 118 | for (const b of buf) { 119 | if (b !== 0) { 120 | nonZeroLen++; 121 | } 122 | } 123 | const zeroLen = byteLen - nonZeroLen; 124 | gas = 125 | gas + 126 | BigInt(zeroLen) * BigInt(TX_DATA_ZERO_GAS) + 127 | BigInt(nonZeroLen) * BigInt(TX_DATA_NONE_ZERO_GAS); 128 | } 129 | return gas; 130 | } 131 | 132 | export function calcFee(serializedL2Tx: HexString, feeRate: bigint) { 133 | const byteLen = BigInt(serializedL2Tx.slice(2).length / 2); 134 | return byteLen * feeRate; 135 | } 136 | 137 | // WEB3_RPC_URL/instant-finality-hack or WEB3_RPC_URL?instant-finality-hack=true 138 | export function isInstantFinalityHackMode(req: Request): boolean { 139 | return ( 140 | req.url.includes("/instant-finality-hack") || 141 | (req.query && req.query["instant-finality-hack"] == "true") 142 | ); 143 | } 144 | 145 | export async function asyncSleep(ms = 0) { 146 | return new Promise((r) => setTimeout(() => r("ok"), ms)); 147 | } 148 | -------------------------------------------------------------------------------- /packages/api-server/src/cache/index.ts: -------------------------------------------------------------------------------- 1 | require("newrelic"); 2 | import { FilterCacheInDb, FilterCache } from "./types"; 3 | import { Store } from "./store"; 4 | import crypto from "crypto"; 5 | import { HexString } from "@ckb-lumos/base"; 6 | import { 7 | CACHE_EXPIRED_TIME_MILSECS, 8 | MAX_FILTER_TOPIC_ARRAY_LENGTH, 9 | } from "./constant"; 10 | import { validators } from "../methods/validator"; 11 | import { FilterFlag, FilterTopic, RpcFilterRequest } from "../base/filter"; 12 | 13 | export class FilterManager { 14 | public store: Store; 15 | 16 | constructor( 17 | enableExpired = false, 18 | expiredTimeMilsecs = CACHE_EXPIRED_TIME_MILSECS, // milsec, default 5 minutes 19 | store?: Store 20 | ) { 21 | this.store = store || new Store(enableExpired, expiredTimeMilsecs); 22 | } 23 | 24 | async install( 25 | filter: FilterFlag | RpcFilterRequest, 26 | initialPollIdx: bigint 27 | ): Promise { 28 | verifyFilterType(filter); 29 | const id = newId(); 30 | const filterCache: FilterCache = { 31 | filter: filter, 32 | lastPoll: initialPollIdx, 33 | }; 34 | await this.store.insert(id, serializeFilterCache(filterCache)); 35 | return id; 36 | } 37 | 38 | async get(id: string): Promise { 39 | const data = await this.store.get(id); 40 | if (data == null) return undefined; 41 | 42 | const filterCache = deserializeFilterCache(data); 43 | return filterCache.filter; 44 | } 45 | 46 | async uninstall(id: string): Promise { 47 | const filter = await this.get(id); 48 | if (!filter) return false; // or maybe throw `filter not exits by id: ${id}`; 49 | 50 | await this.store.delete(id); 51 | return true; 52 | } 53 | 54 | async getFilterCache(id: string): Promise { 55 | const data = await this.store.get(id); 56 | if (data == null) 57 | throw new Error(`filter ${id} not exits, might be out of dated.`); 58 | 59 | return deserializeFilterCache(data); 60 | } 61 | 62 | async updateLastPoll(id: string, lastPoll: bigint) { 63 | let filterCache = await this.getFilterCache(id); 64 | filterCache.lastPoll = lastPoll; 65 | this.store.insert(id, serializeFilterCache(filterCache)); 66 | } 67 | 68 | async getLastPoll(id: string) { 69 | const filterCache = await this.getFilterCache(id); 70 | return filterCache.lastPoll; 71 | } 72 | 73 | async size() { 74 | return await this.store.size(); 75 | } 76 | } 77 | 78 | export function newId(): HexString { 79 | return "0x" + crypto.randomBytes(16).toString("hex"); 80 | } 81 | 82 | export function verifyLimitSizeForTopics(topics?: FilterTopic[]) { 83 | if (topics == null) { 84 | return; 85 | } 86 | 87 | if (topics.length > MAX_FILTER_TOPIC_ARRAY_LENGTH) { 88 | throw new Error( 89 | `got FilterTopics.length ${topics.length}, expect limit: ${MAX_FILTER_TOPIC_ARRAY_LENGTH}` 90 | ); 91 | } 92 | 93 | for (const topic of topics) { 94 | if (Array.isArray(topic)) { 95 | if (topic.length > MAX_FILTER_TOPIC_ARRAY_LENGTH) { 96 | throw new Error( 97 | `got one or more topic item's length ${topic.length}, expect limit: ${MAX_FILTER_TOPIC_ARRAY_LENGTH}` 98 | ); 99 | } 100 | } 101 | } 102 | } 103 | 104 | export function verifyFilterType(filter: any) { 105 | if (typeof filter === "number") { 106 | return verifyFilterFlag(filter); 107 | } 108 | 109 | verifyFilterObject(filter); 110 | verifyLimitSizeForTopics(filter.topics); 111 | } 112 | 113 | export function verifyFilterFlag(target: any) { 114 | if ( 115 | target !== FilterFlag.blockFilter && 116 | target !== FilterFlag.pendingTransaction 117 | ) { 118 | throw new Error(`invalid value for filterFlag`); 119 | } 120 | return; 121 | } 122 | 123 | export function verifyFilterObject(target: any) { 124 | return validators.newFilterParams([target], 0); 125 | } 126 | 127 | export function serializeFilterCache(data: FilterCache) { 128 | const filterDb: FilterCacheInDb = { 129 | filter: data.filter, 130 | lastPoll: "0x" + data.lastPoll.toString(16), 131 | }; 132 | return JSON.stringify(filterDb); 133 | } 134 | 135 | export function deserializeFilterCache(data: string): FilterCache { 136 | const filterCacheInDb: FilterCacheInDb = JSON.parse(data); 137 | validators.hexNumber([filterCacheInDb.lastPoll], 0); 138 | 139 | const filterCache: FilterCache = { 140 | filter: filterCacheInDb.filter, 141 | lastPoll: BigInt(filterCacheInDb.lastPoll), 142 | }; 143 | 144 | verifyFilterType(filterCache.filter); 145 | return filterCache; 146 | } 147 | -------------------------------------------------------------------------------- /crates/rpc-client/src/godwoken_async_client.rs: -------------------------------------------------------------------------------- 1 | use anyhow::Result; 2 | use async_jsonrpc_client::{BatchTransport, HttpClient, Output, Params as ClientParams, Transport}; 3 | use ckb_jsonrpc_types::Script; 4 | use ckb_types::H256; 5 | use gw_jsonrpc_types::ckb_jsonrpc_types::Uint32; 6 | use itertools::Itertools; 7 | use serde::de::DeserializeOwned; 8 | use serde_json::{from_value, json}; 9 | 10 | type AccountID = Uint32; 11 | 12 | pub struct GodwokenAsyncClient { 13 | client: HttpClient, 14 | } 15 | 16 | impl GodwokenAsyncClient { 17 | pub fn new(client: HttpClient) -> Self { 18 | Self { client } 19 | } 20 | 21 | pub fn with_url(url: &str) -> Result { 22 | let client = HttpClient::builder().build(url)?; 23 | Ok(Self::new(client)) 24 | } 25 | } 26 | 27 | impl GodwokenAsyncClient { 28 | pub async fn get_script_hash(&self, account_id: u32) -> Result { 29 | let script_hash: H256 = self 30 | .request( 31 | "gw_get_script_hash", 32 | Some(ClientParams::Array(vec![json!(AccountID::from( 33 | account_id 34 | ))])), 35 | ) 36 | .await?; 37 | 38 | Ok(script_hash) 39 | } 40 | 41 | pub async fn get_script(&self, script_hash: H256) -> Result> { 42 | let script: Option