diff --git a/light-client-js/jest.config.js b/light-client-js/jest.config.js new file mode 100644 index 0000000..f5d30d1 --- /dev/null +++ b/light-client-js/jest.config.js @@ -0,0 +1,7 @@ +/** @type {import('ts-jest').JestConfigWithTsJest} **/ +module.exports = { + testEnvironment: "node", + transform: { + "^.+.tsx?$": ["ts-jest",{}], + }, +}; \ No newline at end of file diff --git a/light-client-js/package.json b/light-client-js/package.json index add0e90..3cfdbaf 100644 --- a/light-client-js/package.json +++ b/light-client-js/package.json @@ -1,24 +1,29 @@ - { +{ "name": "light-client-js", "version": "1.0.0", "main": "dist/index.js", "license": "MIT", "devDependencies": { + "@jest/globals": "^29.7.0", + "@types/jest": "^29.5.14", + "jest": "^29.7.0", + "ts-jest": "^29.2.5", "ts-loader": "^9.5.1", "typescript": "^5.7.2", "webpack": "^5.96.1", "webpack-cli": "^5.1.4" }, "dependencies": { + "@ckb-ccc/core": "0.1.0-alpha.6", "light-client-db-worker": "file:../light-client-db-worker", "light-client-wasm": "file:../light-client-wasm", - "stream-browserify": "^3.0.0", - "@ckb-ccc/core": "0.1.0-alpha.6" + "stream-browserify": "^3.0.0" }, "scripts": { "build-debug": "webpack --mode=development --stats-children", "build": "webpack --mode=production", - "serve": "webpack serve" + "serve": "webpack serve", + "test": "jest" }, "exports": { ".": "./dist/index.js" diff --git a/light-client-js/src/index.ts b/light-client-js/src/index.ts index 3ce1ddd..d92d9ba 100644 --- a/light-client-js/src/index.ts +++ b/light-client-js/src/index.ts @@ -1,5 +1,5 @@ import { ClientFindCellsResponse, ClientFindTransactionsGroupedResponse, ClientFindTransactionsResponse, ClientIndexerSearchKeyLike, ClientIndexerSearchKeyTransactionLike, ClientTransactionResponse } from "@ckb-ccc/core"; -import { FetchResponse, JsonRpcLocalNode, JsonRpcRemoteNode, JsonRpcScriptStatus, LocalNode, localNodeTo, NetworkFlag, RemoteNode, remoteNodeTo, ScriptStatus, scriptStatusFrom, scriptStatusTo, LightClientWasmSetScriptsCommand, transformFetchResponse, cccOrderToLightClientWasmOrder, GetTransactionsResponse, TxWithCell, TxWithCells, lightClientGetTransactionsResultTo } from "./types"; +import { FetchResponse, LocalNode, localNodeTo, NetworkFlag, RemoteNode, remoteNodeTo, ScriptStatus, scriptStatusFrom, scriptStatusTo, LightClientSetScriptsCommand, transformFetchResponse, cccOrderToLightClientWasmOrder, GetTransactionsResponse, TxWithCell, TxWithCells, lightClientGetTransactionsResultTo, LightClientLocalNode, LightClientRemoteNode, LightClientScriptStatus } from "./types"; import { ClientBlock, ClientBlockHeader, Hex, hexFrom, HexLike, Num, numFrom, NumLike, numToHex, TransactionLike } from "@ckb-ccc/core/barrel"; import { JsonRpcBlockHeader, JsonRpcTransformers } from "@ckb-ccc/core/advancedBarrel"; const DEFAULT_BUFFER_SIZE = 50 * (1 << 20); @@ -111,28 +111,28 @@ class LightClient { * @returns LocalNode */ async localNodeInfo(): Promise { - return localNodeTo(await this.invokeLightClientCommand("local_node_info") as JsonRpcLocalNode); + return localNodeTo(await this.invokeLightClientCommand("local_node_info") as LightClientLocalNode); } /** * Returns the connected peers' information. * @returns */ async getPeers(): Promise { - return (await this.invokeLightClientCommand("get_peers") as JsonRpcRemoteNode[]).map(x => remoteNodeTo(x)); + return (await this.invokeLightClientCommand("get_peers") as LightClientRemoteNode[]).map(x => remoteNodeTo(x)); } /** * Set some scripts to filter * @param scripts Array of script status * @param command An optional enum parameter to control the behavior of set_scripts */ - async setScripts(scripts: ScriptStatus[], command?: LightClientWasmSetScriptsCommand): Promise { + async setScripts(scripts: ScriptStatus[], command?: LightClientSetScriptsCommand): Promise { await this.invokeLightClientCommand("set_scripts", [scripts.map(x => scriptStatusFrom(x)), command]); } /** * Get filter scripts status */ async getScripts(): Promise { - return (await this.invokeLightClientCommand("get_scripts") as JsonRpcScriptStatus[]).map(x => scriptStatusTo(x)); + return (await this.invokeLightClientCommand("get_scripts") as LightClientScriptStatus[]).map(x => scriptStatusTo(x)); } /** * See https://github.com/nervosnetwork/ckb-indexer#get_cells diff --git a/light-client-js/src/types.test.ts b/light-client-js/src/types.test.ts new file mode 100644 index 0000000..649da3b --- /dev/null +++ b/light-client-js/src/types.test.ts @@ -0,0 +1,205 @@ +import { expect, test } from "@jest/globals"; +import { cccOrderToLightClientWasmOrder, FetchResponse, GetTransactionsResponse, lightClientGetTransactionsResultTo, LightClientLocalNode, LightClientOrder, LightClientRemoteNode, LightClientScriptStatus, LightClientTxWithCell, LightClientTxWithCells, LocalNode, localNodeTo, RemoteNode, remoteNodeTo, scriptStatusFrom, scriptStatusTo, transformFetchResponse, TxWithCell, TxWithCells } from "./types"; +import { Transaction } from "@ckb-ccc/core"; +test("test transform fetch response", () => { + const a: FetchResponse = { status: "fetched", data: 1 }; + const b: FetchResponse = { status: "added", timestamp: 0n }; + const fn = (a: number) => a + 1; + expect(transformFetchResponse(a, fn)).toStrictEqual({ status: "fetched", data: 2 }); + expect(transformFetchResponse(b, fn)).toStrictEqual(b); +}) + +test("test scriptStatusTo/From", () => { + const raw: LightClientScriptStatus = { + script: { + code_hash: "0xabcd1234", + hash_type: "data", + args: "0x0011223344" + }, + script_type: "lock", + block_number: 1234n + }; + const transformed = scriptStatusTo(raw); + + expect(scriptStatusFrom(transformed)).toStrictEqual(raw); +}); + +test("test remoteNodeTo", () => { + const raw: LightClientRemoteNode = { + version: "1", + node_id: "111", + addresses: [ + { address: "test", score: 123n } + ], + connected_duration: 234n, + protocols: [ + { id: 1n, version: "1" } + ], + sync_state: { + proved_best_known_header: { + compact_target: "0x12", + dao: "0x04d0bc8a6028d30c0000c16ff286230066cbed490e00000000a38cdc1afbfe06", + epoch: "0x11112222333333", + extra_hash: "0x45", + hash: "0x56", + nonce: "0x67", + number: "0x78", + parent_hash: "0x89", + proposals_hash: "0x90", + timestamp: "0xab", + transactions_root: "0xbc", + version: "0xcd" + } + } + }; + const newVal: RemoteNode = { + version: "1", + nodeId: "111", + addresses: [ + { address: "test", score: 123n } + ], + connestedDuration: 234n, + protocols: [ + { id: 1n, version: "1" } + ], + syncState: { + provedBestKnownHeader: { + compactTarget: 0x12n, + dao: { + c: 924126743650684932n, + ar: 10000000000000000n, + s: 61369863014n, + u: 504116301100000000n + }, + epoch: [3355443n, 8738n, 4369n], + extraHash: "0x45", + hash: "0x56", + nonce: 0x67n, + number: 0x78n, + parentHash: "0x89", + proposalsHash: "0x90", + timestamp: 0xabn, + transactionsRoot: "0xbc", + version: 0xcdn + }, + requestedBestKnownHeader: undefined + } + }; + expect(remoteNodeTo(raw)).toStrictEqual(newVal); +}); + +test("test localNodeTo", () => { + const raw: LightClientLocalNode = { + version: "aaa", + node_id: "bbb", + active: false, + addresses: [ + { address: "111", score: 123n } + ], + protocols: [ + { id: 1n, name: "test", support_version: ["a", "b", "c"] } + ], + connections: 123n + } + const expectedVal: LocalNode = { + version: "aaa", + nodeId: "bbb", + active: false, + addresses: [ + { address: "111", score: 123n } + ], + protocols: [ + { id: 1n, name: "test", supportVersion: ["a", "b", "c"] } + ], + connections: 123n + }; + expect(localNodeTo(raw)).toStrictEqual(expectedVal); +}); + +test("test cccOrderToLightClientWasmOrder", () => { + expect(cccOrderToLightClientWasmOrder("asc")).toBe(LightClientOrder.Asc); + expect(cccOrderToLightClientWasmOrder("desc")).toBe(LightClientOrder.Desc); +}); + +test("test lightClientGetTransactionsResultTo", () => { + expect(lightClientGetTransactionsResultTo({ last_cursor: "0x1234", objects: [] })).toStrictEqual({ lastCursor: "0x1234", transactions: [] }); + expect(lightClientGetTransactionsResultTo({ + last_cursor: "0x1234", objects: [ + { + block_number: "0x1234", + io_index: 1234, + io_type: "input", + transaction: { + cell_deps: [], + header_deps: [], + inputs: [], + outputs: [], + outputs_data: [], + version: "0x123", + witnesses: ["0x"] + }, + tx_index: 2222 + } as LightClientTxWithCell + ] + })).toStrictEqual({ + lastCursor: "0x1234", + transactions: [ + { + blockNumber: 0x1234n, + ioIndex: 1234n, + ioType: "input", + transaction: Transaction.from({ + cellDeps: [], + headerDeps: [], + inputs: [], + outputs: [], + outputsData: [], + version: 0x123n, + witnesses: ["0x"] + }), + txIndex: 2222n + } + ] + } as GetTransactionsResponse); + + expect(lightClientGetTransactionsResultTo({ + last_cursor: "0x1234", objects: [ + { + block_number: "0x1234", + tx_index: 123, + transaction: { + cell_deps: [], + header_deps: [], + inputs: [], + outputs: [], + outputs_data: [], + version: "0x123", + witnesses: ["0x"] + }, + cells: [ + ["input", 111] + ] + } as LightClientTxWithCells + ] + })).toStrictEqual({ + lastCursor: "0x1234", + transactions: [ + { + blockNumber: 0x1234n, + txIndex: 123n, + transaction: Transaction.from({ + cellDeps: [], + headerDeps: [], + inputs: [], + outputs: [], + outputsData: [], + version: 0x123n, + witnesses: ["0x"] + }), + cells: [ + ["input", 111] + ] + } + ] + } as GetTransactionsResponse); +}); diff --git a/light-client-js/src/types.ts b/light-client-js/src/types.ts index a6f3a13..201bdbe 100644 --- a/light-client-js/src/types.ts +++ b/light-client-js/src/types.ts @@ -1,8 +1,9 @@ import { numFrom } from "@ckb-ccc/core"; import { Hex } from "@ckb-ccc/core"; +import { ClientBlockHeader } from "@ckb-ccc/core"; import { ScriptLike } from "@ckb-ccc/core"; -import { JsonRpcScript, JsonRpcTransaction, JsonRpcTransformers } from "@ckb-ccc/core/advancedBarrel"; -import { Num, Script, Transaction } from "@ckb-ccc/core/barrel"; +import { JsonRpcBlockHeader, JsonRpcScript, JsonRpcTransaction, JsonRpcTransformers } from "@ckb-ccc/core/advancedBarrel"; +import { Num, Transaction } from "@ckb-ccc/core/barrel"; interface WorkerInitializeOptions { inputBuffer: SharedArrayBuffer; @@ -36,7 +37,7 @@ export function transformFetchResponse(input: FetchResponse, fn: (arg: type JsonRpcScriptType = "lock" | "type"; -interface JsonRpcScriptStatus { +interface LightClientScriptStatus { script: JsonRpcScript; script_type: JsonRpcScriptType; block_number: Num; @@ -48,7 +49,7 @@ interface ScriptStatus { blockNumber: Num } -export function scriptStatusTo(input: JsonRpcScriptStatus): ScriptStatus { +export function scriptStatusTo(input: LightClientScriptStatus): ScriptStatus { return ({ blockNumber: input.block_number, script: JsonRpcTransformers.scriptTo(input.script), @@ -56,7 +57,7 @@ export function scriptStatusTo(input: JsonRpcScriptStatus): ScriptStatus { }) } -export function scriptStatusFrom(input: ScriptStatus): JsonRpcScriptStatus { +export function scriptStatusFrom(input: ScriptStatus): LightClientScriptStatus { return ({ block_number: input.blockNumber, script: JsonRpcTransformers.scriptFrom(input.script), @@ -74,12 +75,29 @@ interface RemoteNodeProtocol { version: string; } -interface JsonRpcRemoteNode { +interface LightClientPeerSyncState { + requested_best_known_header?: JsonRpcBlockHeader; + proved_best_known_header?: JsonRpcBlockHeader; +} + +interface PeerSyncState { + requestedBestKnownHeader?: ClientBlockHeader; + provedBestKnownHeader?: ClientBlockHeader; +} + +export function peerSyncStateTo(input: LightClientPeerSyncState): PeerSyncState { + return { + requestedBestKnownHeader: input.requested_best_known_header && JsonRpcTransformers.blockHeaderTo(input.requested_best_known_header), + provedBestKnownHeader: input.proved_best_known_header && JsonRpcTransformers.blockHeaderTo(input.proved_best_known_header), + }; +} + +interface LightClientRemoteNode { version: string; node_id: string; addresses: NodeAddress[]; connected_duration: Num; - sync_state?: any; + sync_state?: LightClientPeerSyncState; protocols: RemoteNodeProtocol[]; } @@ -89,22 +107,22 @@ interface RemoteNode { nodeId: string; addresses: NodeAddress[]; connestedDuration: Num; - syncState?: any; + syncState?: PeerSyncState; protocols: RemoteNodeProtocol[]; } -export function remoteNodeTo(input: JsonRpcRemoteNode): RemoteNode { +export function remoteNodeTo(input: LightClientRemoteNode): RemoteNode { return ({ addresses: input.addresses, connestedDuration: input.connected_duration, nodeId: input.node_id, protocols: input.protocols, version: input.version, - syncState: input.sync_state + syncState: input.sync_state && peerSyncStateTo(input.sync_state) }) } -interface JsonRpcLocalNodeProtocol { +interface LightClientLocalNodeProtocol { id: Num; name: string; support_version: string[]; @@ -117,7 +135,7 @@ interface LocalNodeProtocol { supportVersion: string[]; } -export function localNodeProtocolTo(input: JsonRpcLocalNodeProtocol): LocalNodeProtocol { +export function localNodeProtocolTo(input: LightClientLocalNodeProtocol): LocalNodeProtocol { return ({ id: input.id, name: input.name, @@ -125,13 +143,13 @@ export function localNodeProtocolTo(input: JsonRpcLocalNodeProtocol): LocalNodeP }) } -interface JsonRpcLocalNode { +interface LightClientLocalNode { version: string; node_id: string; active: boolean; addresses: NodeAddress[]; - protocols: JsonRpcLocalNodeProtocol[]; - connections: bigint; + protocols: LightClientLocalNodeProtocol[]; + connections: Num; } interface LocalNode { @@ -143,7 +161,7 @@ interface LocalNode { connections: bigint; } -export function localNodeTo(input: JsonRpcLocalNode): LocalNode { +export function localNodeTo(input: LightClientLocalNode): LocalNode { return ({ nodeId: input.node_id, protocols: input.protocols.map(x => localNodeProtocolTo(x)), @@ -154,19 +172,19 @@ export function localNodeTo(input: JsonRpcLocalNode): LocalNode { }) } type NetworkFlag = { type: "MainNet" } | { type: "TestNet" } | { type: "DevNet"; spec: string; config: string; }; -export enum LightClientWasmSetScriptsCommand { +export enum LightClientSetScriptsCommand { All = 0, Partial = 1, Delete = 2, } -export enum LightClientWasmOrder { +export enum LightClientOrder { Desc = 0, Asc = 1, } -export function cccOrderToLightClientWasmOrder(input: "asc" | "desc"): LightClientWasmOrder { - if (input === "asc") return LightClientWasmOrder.Asc; - else return LightClientWasmOrder.Desc; +export function cccOrderToLightClientWasmOrder(input: "asc" | "desc"): LightClientOrder { + if (input === "asc") return LightClientOrder.Asc; + else return LightClientOrder.Desc; } type LightClientCellType = "input" | "output"; @@ -249,12 +267,12 @@ export type { DbWorkerInitializeOptions, LightClientWorkerInitializeOptions, FetchResponse, - JsonRpcScriptStatus, + LightClientScriptStatus, ScriptStatus, - JsonRpcRemoteNode, + LightClientRemoteNode, RemoteNode, LocalNode, - JsonRpcLocalNode, + LightClientLocalNode, NetworkFlag, LightClientTxWithCell, LightClientTxWithCells, diff --git a/light-client-js/tsconfig.json b/light-client-js/tsconfig.json index c01f681..3f09594 100644 --- a/light-client-js/tsconfig.json +++ b/light-client-js/tsconfig.json @@ -24,6 +24,7 @@ "exclude": [ "webpack.config.js", "dist/**", - "node_modules/**" + "node_modules/**", + "jest.config.js" ] }