diff --git a/.env.example b/.env.example index c594254..906e34d 100644 --- a/.env.example +++ b/.env.example @@ -9,3 +9,4 @@ TESTNET_HTTP_RPC_URL = "https://testnet.ckb.dev" API_HTTP_PORT = 3000 API_WS_PORT = 3001 +ALLOW_ORIGIN = "*" # for multiple allow origins, separate with , eg: "domain1.com,domain2.com" diff --git a/README.md b/README.md index 0c508ff..36aff03 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,23 @@ pnpm dev cd frontend && pnpm dev ``` +## .env config + +The following environment variables need to be configured: + +- `MAINNET_DATABASE_FILE`: Path to SQLite database file for mainnet +- `TESTNET_DATABASE_FILE`: Path to SQLite database file for testnet +- `MAINNET_WS_RPC_URL`: CKB Node WebSocket RPC URL for mainnet +- `TESTNET_WS_RPC_URL`: CKB Node WebSocket RPC URL for testnet +- `MAINNET_HTTP_RPC_URL`: CKB Node HTTP RPC URL for mainnet +- `TESTNET_HTTP_RPC_URL`: CKB Node HTTP RPC URL for testnet +- `API_HTTP_PORT`: Port for Backend HTTP API server +- `API_WS_PORT`: Port for Backend WebSocket server +- `ALLOW_ORIGIN`: CORS allowed origins (default: "*") for Backend server + - for multiple allow origins, separate with , eg: "domain1.com,domain2.com" + +See `.env.example` for default values. + ## License This project is licensed under the MIT License - see the LICENSE file for details. diff --git a/src/api/sever.ts b/src/api/sever.ts index 4360917..acbc163 100644 --- a/src/api/sever.ts +++ b/src/api/sever.ts @@ -1,5 +1,6 @@ import type { Hex } from "@ckb-ccc/core"; import express, { type Request, type Response } from "express"; +import { Config } from "../core/config"; import type { DB } from "../db"; import { logger } from "../util/logger"; @@ -7,9 +8,17 @@ export function createServer(db: DB) { const app = express(); app.use((_req, res, next) => { - res.header("Access-Control-Allow-Origin", "*"); - res.header("Access-Control-Allow-Methods", "GET"); - res.header("Access-Control-Allow-Headers", "Content-Type"); + for (const origin of Config.allowOrigin) { + res.header("Access-Control-Allow-Origin", origin); + } + if (Config.allowOrigin.length > 0) { + res.header( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE", + ); + res.header("Access-Control-Allow-Headers", "Content-Type"); + } + next(); }); diff --git a/src/core/config.ts b/src/core/config.ts index 1895f8c..580fb1c 100644 --- a/src/core/config.ts +++ b/src/core/config.ts @@ -1,5 +1,6 @@ import "dotenv/config"; import process from "node:process"; +import { URL } from "node:url"; export const Config = { mainnetDatabaseFile: @@ -19,4 +20,24 @@ export const Config = { apiHttpPort: Number(process.env.API_HTTP_PORT ?? 3000), apiWsPort: Number(process.env.API_WS_PORT ?? 3001), + allowOrigin: extractAllowOriginList(process.env.ALLOW_ORIGIN ?? "*"), }; + +export function extractAllowOriginList(value: string) { + const list = value + .split(",") + // test valid url + .filter((v) => { + if (v === "*") { + return true; + } + + try { + new URL(v.trim()); + return true; + } catch { + return false; + } + }); + return list; +} diff --git a/src/core/type.ts b/src/core/type.ts index 019e3d9..f295c98 100644 --- a/src/core/type.ts +++ b/src/core/type.ts @@ -13,6 +13,26 @@ export interface JsonRpcPoolTransactionEntry { timestamp: Hex; } +export interface PoolTransactionReject { + type: PoolTransactionRejectType; + description: string; +} + +export enum PoolTransactionRejectType { + LowFeeRate = "LowFeeRate", // transaction fee lower than config + ExceededMaximumAncestorsCount = "ExceededMaximumAncestorsCount", // Transaction exceeded maximum ancestors count limit + ExceededTransactionSizeLimit = "ExceededTransactionSizeLimit", // Transaction exceeded maximum size limit + Full = "Full", // Transaction are replaced because the pool is full + Duplicated = "Duplicated", // Transaction already exists in transaction_pool + Malformed = "Malformed", // Malformed transaction + DeclaredWrongCycles = "DeclaredWrongCycles", // Declared wrong cycles + Resolve = "Resolve", // Resolve failed + Verification = "Verification", // Verification failed + Expiry = "Expiry", // Transaction expired + RBFRejected = "RBFRejected", // RBF rejected + Invalidated = "Invalidated", // Invalidated rejected +} + export type DBBlockHeader = { id: number; compact_target: Hex; diff --git a/src/core/ws.ts b/src/core/ws.ts index f1ada17..f2dbd3e 100644 --- a/src/core/ws.ts +++ b/src/core/ws.ts @@ -1,6 +1,5 @@ import { type JsonRpcBlock, - type JsonRpcTransaction, JsonRpcTransformers, } from "@ckb-ccc/core/advancedBarrel"; import { WebSocket } from "ws"; @@ -8,7 +7,7 @@ import type { DB } from "../db"; import { logger } from "../util/logger"; import type { JsonRpcPoolTransactionEntry, - JsonRpcTransactionView, + PoolTransactionReject, } from "./type"; export interface WebsocketTopicSubscriber { @@ -100,10 +99,10 @@ export class Subscriber { sub_id: undefined, handler: ([tx, reason]: [ JsonRpcPoolTransactionEntry, - string, + PoolTransactionReject, ]) => { logger.debug( - `new rejected tx: ${tx.transaction.hash.slice(0, 22)}, reason: ${reason}`, + `new rejected tx: ${tx.transaction.hash.slice(0, 22)}, reason: ${JSON.stringify(reason)}`, ); this.db.updateMempoolRejectedTransaction(tx, reason); }, diff --git a/src/db/index.ts b/src/db/index.ts index 760b0d9..3eb8b85 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -12,6 +12,7 @@ import type { DBBlockHeader, JsonRpcPoolTransactionEntry, JsonRpcTransactionView, + PoolTransactionReject, } from "../core/type"; import { getNowTimestamp } from "../util/time"; import { DepType, HashType, TransactionStatus } from "./type"; @@ -371,8 +372,9 @@ export class DB { updateMempoolRejectedTransaction( tx: JsonRpcPoolTransactionEntry, - reason: string, + txReject: PoolTransactionReject, ) { + const reason = `${txReject.type}: ${txReject.description}`; const getStmt = this.db.prepare<[Hex], { id: DBId }>( "SELECT id From transactions WHERE tx_hash = ?", ); diff --git a/src/index.ts b/src/index.ts index 01a06e2..029500e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,6 +5,8 @@ import { testnetSubscriber } from "./core/sub"; import { logger } from "./util/logger"; async function main() { + logger.info(`Config: ${JSON.stringify(Config, null, 2)}`); + testnetSubscriber.run(); const testnetSever = createServer(testnetDB);