@corpus-core/colibri-stateless 0.2.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,8 +11,7 @@ export declare class ConnectionState {
11
11
  private config;
12
12
  private fetchRpcChainIdCallback;
13
13
  private eventEmitter;
14
- private formatChainIdFunc;
15
- constructor(config: ConnectionStateConfig, fetchRpcChainIdCallback: () => Promise<any>, eventEmitter: EventEmitter, formatChainIdFunc: (value: any, debug?: boolean) => string | null);
14
+ constructor(config: ConnectionStateConfig, fetchRpcChainIdCallback: () => Promise<any>, eventEmitter: EventEmitter);
16
15
  get isConnected(): boolean;
17
16
  get currentChainId(): string | null;
18
17
  get initialConnectionAttempted(): boolean;
@@ -1,4 +1,27 @@
1
1
  import { ProviderRpcError } from './types.js';
2
+ // Helper function for chain ID formatting - intentionally kept separate for now
3
+ // If this function is *only* used by ConnectionState, it could be moved inside or made local.
4
+ // However, C4Client.rpc also uses it for its LOCAL eth_chainId case, so it might need broader access.
5
+ // For this refactoring, we assume it's passed in or globally accessible if needed elsewhere.
6
+ // Helper function for chain ID formatting (can be used by ConnectionManager and C4Client)
7
+ function formatChainId(value, debug) {
8
+ if (typeof value === 'string') {
9
+ if (value.startsWith('0x')) {
10
+ return value.toLowerCase();
11
+ }
12
+ const parsed = parseInt(value, 10);
13
+ if (!isNaN(parsed)) {
14
+ return '0x' + parsed.toString(16);
15
+ }
16
+ }
17
+ else if (typeof value === 'number') {
18
+ return '0x' + value.toString(16);
19
+ }
20
+ if (debug) {
21
+ console.warn('Could not format chainId:', value);
22
+ }
23
+ return null;
24
+ }
2
25
  export class ConnectionState {
3
26
  _isConnected = false;
4
27
  _currentChainId = null;
@@ -6,13 +29,11 @@ export class ConnectionState {
6
29
  config;
7
30
  fetchRpcChainIdCallback;
8
31
  eventEmitter;
9
- formatChainIdFunc;
10
- constructor(config, fetchRpcChainIdCallback, eventEmitter, formatChainIdFunc) {
32
+ constructor(config, fetchRpcChainIdCallback, eventEmitter) {
11
33
  this.config = config;
12
34
  this.fetchRpcChainIdCallback = fetchRpcChainIdCallback;
13
35
  this.eventEmitter = eventEmitter;
14
- this.formatChainIdFunc = formatChainIdFunc;
15
- this._currentChainId = this.formatChainIdFunc(this.config.chainId, this.config.debug);
36
+ this._currentChainId = formatChainId(this.config.chainId, this.config.debug);
16
37
  }
17
38
  get isConnected() {
18
39
  return this._isConnected;
@@ -32,7 +53,7 @@ export class ConnectionState {
32
53
  }
33
54
  try {
34
55
  const result = await this.fetchRpcChainIdCallback();
35
- const formattedChainId = this.formatChainIdFunc(result, this.config.debug);
56
+ const formattedChainId = formatChainId(result, this.config.debug);
36
57
  if (formattedChainId) {
37
58
  this._isConnected = true;
38
59
  this._currentChainId = formattedChainId;
@@ -64,12 +85,12 @@ export class ConnectionState {
64
85
  const updateAndEmitConnect = async () => {
65
86
  let determinedChainId = null;
66
87
  if (requestMethod === 'eth_chainId') {
67
- determinedChainId = this.formatChainIdFunc(requestResult, this.config.debug);
88
+ determinedChainId = formatChainId(requestResult, this.config.debug);
68
89
  }
69
90
  else {
70
91
  try {
71
92
  const chainIdResult = await this.fetchRpcChainIdCallback();
72
- determinedChainId = this.formatChainIdFunc(chainIdResult, this.config.debug);
93
+ determinedChainId = formatChainId(chainIdResult, this.config.debug);
73
94
  }
74
95
  catch (e) {
75
96
  if (this.config.debug)
@@ -77,7 +98,7 @@ export class ConnectionState {
77
98
  determinedChainId = this._currentChainId;
78
99
  }
79
100
  }
80
- this._currentChainId = determinedChainId || this.formatChainIdFunc(this.config.chainId, this.config.debug);
101
+ this._currentChainId = determinedChainId || formatChainId(this.config.chainId, this.config.debug);
81
102
  this.eventEmitter.emit('connect', { chainId: this._currentChainId });
82
103
  };
83
104
  updateAndEmitConnect().catch(err => {
@@ -86,7 +107,7 @@ export class ConnectionState {
86
107
  });
87
108
  }
88
109
  else if (requestMethod === 'eth_chainId') {
89
- const newChainId = this.formatChainIdFunc(requestResult, this.config.debug);
110
+ const newChainId = formatChainId(requestResult, this.config.debug);
90
111
  if (newChainId && this._currentChainId !== newChainId) {
91
112
  const oldChainId = this._currentChainId;
92
113
  this._currentChainId = newChainId;
package/index.d.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Storage as C4Storage } from "./wasm.js";
2
2
  import { ProviderRpcError, RequestArguments, Cache, Config as C4Config, DataRequest, MethodType as C4MethodType, ProviderMessage } from './types.js';
3
3
  import { EthSubscribeSubscriptionType, EthNewFilterType } from './subscriptionManager.js';
4
+ import Strategy from './strategy.js';
5
+ export { Strategy };
4
6
  export { ProviderRpcError, RequestArguments, Cache, C4Config, DataRequest, C4MethodType as MethodType, ProviderMessage, EthSubscribeSubscriptionType, EthNewFilterType };
5
7
  export declare function handle_request(req: DataRequest, conf: C4Config): Promise<void>;
6
8
  export default class C4Client {
@@ -41,7 +43,7 @@ export default class C4Client {
41
43
  * @param args - The arguments to execute the method with
42
44
  * @returns The result
43
45
  */
44
- rpc(method: string, args: any[]): Promise<any>;
46
+ rpc(method: string, args: any[], method_type?: C4MethodType): Promise<any>;
45
47
  static register_storage(storage: C4Storage): Promise<void>;
46
48
  request(args: RequestArguments): Promise<unknown>;
47
49
  on(event: string, callback: (data: any) => void): this;
package/index.js CHANGED
@@ -4,31 +4,13 @@ import { EventEmitter } from './eventEmitter.js';
4
4
  import { ConnectionState } from './connectionState.js';
5
5
  import { ProviderRpcError, MethodType as C4MethodType } from './types.js';
6
6
  import { SubscriptionManager } from './subscriptionManager.js';
7
+ import Strategy from './strategy.js';
8
+ export { Strategy };
7
9
  // Re-export types needed by consumers of the C4Client module
8
10
  export { ProviderRpcError, C4MethodType as MethodType };
9
- // Helper function for chain ID formatting (can be used by ConnectionManager and C4Client)
10
- function formatChainId(value, debug) {
11
- if (typeof value === 'string') {
12
- if (value.startsWith('0x')) {
13
- return value.toLowerCase();
14
- }
15
- const parsed = parseInt(value, 10);
16
- if (!isNaN(parsed)) {
17
- return '0x' + parsed.toString(16);
18
- }
19
- }
20
- else if (typeof value === 'number') {
21
- return '0x' + value.toString(16);
22
- }
23
- if (debug) {
24
- console.warn('Could not format chainId:', value);
25
- }
26
- return null;
27
- }
28
11
  async function fetch_rpc(urls, payload, as_proof = false) {
29
12
  let last_error = "All nodes failed";
30
13
  for (const url of urls) {
31
- console.log({ url, payload });
32
14
  const response = await fetch(url, {
33
15
  method: 'POST',
34
16
  body: JSON.stringify({ id: 1, jsonrpc: "2.0", ...payload }),
@@ -124,34 +106,55 @@ function check_trusted_blockhashes(trusted_block_hashes, c4w, chainId) {
124
106
  c4w._free(ptr);
125
107
  trusted_block_hashes.length = 0;
126
108
  }
109
+ const default_config = {
110
+ '1': {
111
+ alias: ["mainnet", "eth", "0x1"],
112
+ beacon_apis: ["https://lodestar-mainnet.chainsafe.io"],
113
+ rpcs: ["https://rpc.ankr.com/eth"],
114
+ proofer: ["https://mainnet.colibri-proof.tech"],
115
+ },
116
+ '100': {
117
+ alias: ["gnosis", "xdai", "0x64"],
118
+ beacon_apis: ["https://gnosis.colibri-proof.tech"],
119
+ rpcs: ["https://rpc.ankr.com/gnosis"],
120
+ proofer: ["https://gnosis.colibri-proof.tech"],
121
+ },
122
+ };
123
+ function get_chain_id(chain_id) {
124
+ const chain_id_num = parseInt(chain_id);
125
+ if (!isNaN(chain_id_num))
126
+ return chain_id_num;
127
+ for (const chain in default_config) {
128
+ if (default_config[chain].alias.includes(chain_id))
129
+ return parseInt(chain);
130
+ }
131
+ throw new Error("Invalid chain id: " + chain_id);
132
+ }
127
133
  export default class C4Client {
128
134
  config;
129
135
  eventEmitter;
130
136
  connectionState;
131
137
  subscriptionManager;
132
138
  constructor(config) {
139
+ const chainId = config?.chainId ? get_chain_id(config?.chainId + '') : 1;
140
+ const chain_config = { ...default_config[chainId + ''] };
133
141
  this.config = {
134
- // Defaults including pollingInterval if not provided
135
- chainId: 1,
136
- beacon_apis: ["https://lodestar-mainnet.chainsafe.io"],
137
- rpcs: ["https://rpc.ankr.com/eth"],
138
142
  trusted_block_hashes: [],
139
- proofer: ["https://mainnet.colibri-proof.tech"],
140
143
  pollingInterval: 12000, // Default here, can be overridden by user config
141
- // ... other defaults like debug: false etc. might be useful
142
- ...config // User config overrides defaults
144
+ rpcs: chain_config.rpcs || [],
145
+ beacon_apis: chain_config.beacon_apis || [],
146
+ proofer: chain_config.proofer || [],
147
+ ...config, // User config overrides defaults
148
+ chainId,
143
149
  };
150
+ if (!this.config.warningHandler)
151
+ this.config.warningHandler = async (req, message) => console.warn(message);
152
+ if (!this.config.proofStrategy)
153
+ this.config.proofStrategy = Strategy.VerifyIfPossible;
144
154
  this.eventEmitter = new EventEmitter();
145
- // Specific callback for ConnectionState for eth_chainId
146
- const fetchChainIdForConnectionState = async () => this.rpc('eth_chainId', []);
147
- // General RpcCaller for SubscriptionManager
148
- const generalRpcCaller = async (method, params) => {
149
- return this.rpc(method, params);
150
- };
151
- this.connectionState = new ConnectionState({ chainId: this.config.chainId, debug: this.config.debug }, fetchChainIdForConnectionState, // Use the specific callback
152
- this.eventEmitter, formatChainId);
153
- this.subscriptionManager = new SubscriptionManager(generalRpcCaller, // Use the general rpc caller
154
- this.eventEmitter, { debug: this.config.debug, pollingInterval: this.config.pollingInterval });
155
+ this.connectionState = new ConnectionState({ chainId: parseInt(this.config.chainId + ''), debug: this.config.debug }, async () => this.rpc('eth_chainId', [], C4MethodType.LOCAL), // Use the specific callback
156
+ this.eventEmitter);
157
+ this.subscriptionManager = new SubscriptionManager(async (method, params) => this.rpc(method, params), this.eventEmitter, { debug: this.config.debug, pollingInterval: this.config.pollingInterval });
155
158
  }
156
159
  async getProoferConfig() {
157
160
  const c4w = await getC4w();
@@ -221,7 +224,7 @@ export default class C4Client {
221
224
  let ctx = 0;
222
225
  try {
223
226
  if (this.config.trusted_block_hashes && this.config.trusted_block_hashes.length > 0)
224
- check_trusted_blockhashes(this.config.trusted_block_hashes, c4w, this.config.chainId);
227
+ check_trusted_blockhashes(this.config.trusted_block_hashes, c4w, parseInt(this.config.chainId));
225
228
  // Call the C function
226
229
  ctx = c4w._c4w_create_verify_ctx(copy_to_c(proof, c4w, free_buffers), proof.length, as_char_ptr(method, c4w, free_buffers), as_char_ptr(JSON.stringify(args), c4w, free_buffers), BigInt(this.config.chainId));
227
230
  while (true) {
@@ -253,10 +256,11 @@ export default class C4Client {
253
256
  * @param args - The arguments to execute the method with
254
257
  * @returns The result
255
258
  */
256
- async rpc(method, args) {
259
+ async rpc(method, args, method_type) {
257
260
  // eth_subscribe and eth_unsubscribe are handled by C4Client.request before this method is called.
258
261
  // This rpc method is for the underlying data fetching/proving.
259
- const method_type = await this.getMethodSupport(method);
262
+ if (method_type === undefined)
263
+ method_type = await this.getMethodSupport(method);
260
264
  switch (method_type) {
261
265
  case C4MethodType.PROOFABLE: {
262
266
  if (this.config.verify && !this.config.verify(method, args)) {
@@ -293,9 +297,13 @@ export default class C4Client {
293
297
  // If handleRequest returns a Promise, it means it's handling the request
294
298
  return subscriptionResult;
295
299
  }
300
+ let conf = this.config.chains[this.config.chainId];
301
+ let strategy = conf?.proofStrategy || this.config.proofStrategy;
296
302
  // If not handled by SubscriptionManager, proceed with standard RPC call logic
297
303
  try {
298
- const result = await this.rpc(method, paramsArray);
304
+ const result = strategy
305
+ ? await strategy(this, args, this.config, fetch_rpc)
306
+ : await this.rpc(method, paramsArray);
299
307
  this.connectionState.processSuccessfulRequest(method, result);
300
308
  return result;
301
309
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corpus-core/colibri-stateless",
3
- "version": "0.2.5",
3
+ "version": "0.3.0",
4
4
  "description": "Colibri is a stateless and trustless ethereum client, which is optimized for the mobile apps or embedded devices, because it does not hols the state, but verifies on demand.",
5
5
  "main": "index.js",
6
6
  "type": "module",
package/strategy.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { ColibriClient, RequestArguments, Config, FetchRpc } from './types.js';
2
+ declare function OnlyProofStrategy(client: ColibriClient, req: RequestArguments, config: Config, fetch_rpc: FetchRpc): Promise<any>;
3
+ declare function ProofIfPossibleStrategy(client: ColibriClient, req: RequestArguments, config: Config, fetch_rpc: FetchRpc): Promise<any>;
4
+ declare function WarningStrategy(client: ColibriClient, req: RequestArguments, config: Config, fetch_rpc: FetchRpc): Promise<any>;
5
+ declare const _default: {
6
+ VerifiedOnly: typeof OnlyProofStrategy;
7
+ VerifyIfPossible: typeof ProofIfPossibleStrategy;
8
+ WarningWithFallback: typeof WarningStrategy;
9
+ };
10
+ export default _default;
package/strategy.js ADDED
@@ -0,0 +1,90 @@
1
+ import { MethodType } from './types.js';
2
+ function argsToArray(args) {
3
+ return Array.isArray(args) ? args : (args ? [args] : []);
4
+ }
5
+ function deepEqual(a, b) {
6
+ if (a === b)
7
+ return true;
8
+ if (a === null || b === null)
9
+ return false;
10
+ if (a.constructor !== b.constructor)
11
+ return false;
12
+ if (a instanceof Date)
13
+ return a.getTime() === b.getTime();
14
+ if (a instanceof RegExp)
15
+ return a.source === b.source && a.flags === b.flags;
16
+ if (a instanceof Uint8Array)
17
+ return a.length === b.length && a.every((value, index) => value === b[index]);
18
+ if (a instanceof Array)
19
+ return a.length === b.length && a.every((value, index) => deepEqual(value, b[index]));
20
+ if (a instanceof Object)
21
+ return Object.keys(a).every(key => b[key] === undefined || deepEqual(a[key], b[key]));
22
+ if (a instanceof String)
23
+ return a.toLowerCase() === b.toLowerCase();
24
+ if (a instanceof Number)
25
+ return a.toFixed(10) === b.toFixed(10);
26
+ if (a instanceof Boolean)
27
+ return a === b;
28
+ if (a instanceof BigInt)
29
+ return a.toString() === b.toString();
30
+ if (a instanceof Buffer)
31
+ return a.equals(b);
32
+ if (a instanceof Map)
33
+ return a.size === b.size && Array.from(a.entries()).every(([key, value]) => deepEqual(value, b.get(key)));
34
+ if (a instanceof Set)
35
+ return a.size === b.size && Array.from(a).every(value => b.has(value));
36
+ if (a instanceof Date)
37
+ return a.getTime() === b.getTime();
38
+ return a == b;
39
+ }
40
+ function fetch_unverified_rpc(config, req, fetch_rpc) {
41
+ const fallback_provider = config.fallback_provider;
42
+ if (fallback_provider)
43
+ return fallback_provider.request(req);
44
+ let conf = config.chains[config.chainId];
45
+ let rpcs = conf?.rpcs || config.rpcs;
46
+ if (!rpcs || !Array.isArray(rpcs) || rpcs.length === 0)
47
+ throw new Error("No RPC- Endpoint configured");
48
+ return fetch_rpc(rpcs, req, false);
49
+ }
50
+ function OnlyProofStrategy(client, req, config, fetch_rpc) {
51
+ return client.rpc(req.method, argsToArray(req.params));
52
+ }
53
+ async function ProofIfPossibleStrategy(client, req, config, fetch_rpc) {
54
+ const method_type = await client.getMethodSupport(req.method);
55
+ switch (method_type) {
56
+ case MethodType.PROOFABLE:
57
+ case MethodType.LOCAL:
58
+ return client.rpc(req.method, argsToArray(req.params), method_type);
59
+ default:
60
+ return fetch_unverified_rpc(config, req, fetch_rpc);
61
+ }
62
+ }
63
+ async function WarningStrategy(client, req, config, fetch_rpc) {
64
+ const method_type = await client.getMethodSupport(req.method);
65
+ switch (method_type) {
66
+ case MethodType.LOCAL:
67
+ return client.rpc(req.method, argsToArray(req.params), method_type);
68
+ case MethodType.UNPROOFABLE:
69
+ case MethodType.NOT_SUPPORTED:
70
+ return fetch_unverified_rpc(config, req, fetch_rpc);
71
+ case MethodType.PROOFABLE: {
72
+ const [verified_result, unverified_result] = await Promise.all([
73
+ client.rpc(req.method, argsToArray(req.params), MethodType.PROOFABLE)
74
+ .catch(async (err) => {
75
+ await config.warningHandler(req, `[Warning] ${req.method} failed to be verfiy: ${err.message}, falling back to Default`);
76
+ return undefined;
77
+ }),
78
+ fetch_unverified_rpc(config, req, fetch_rpc)
79
+ ]);
80
+ if (verified_result !== undefined && !deepEqual(verified_result, unverified_result))
81
+ await config.warningHandler(req, `[Warning] ${req.method} does not match the rpc-result`);
82
+ return unverified_result;
83
+ }
84
+ }
85
+ }
86
+ export default {
87
+ VerifiedOnly: OnlyProofStrategy,
88
+ VerifyIfPossible: ProofIfPossibleStrategy,
89
+ WarningWithFallback: WarningStrategy
90
+ };
package/types.d.ts CHANGED
@@ -4,6 +4,13 @@ export declare class ProviderRpcError extends Error {
4
4
  constructor(code: number, message: string, data?: unknown);
5
5
  static createError(error: any): ProviderRpcError;
6
6
  }
7
+ export interface ColibriClient {
8
+ rpc(method: string, params: any[], method_type?: MethodType): Promise<any>;
9
+ getMethodSupport(method: string): Promise<MethodType>;
10
+ }
11
+ export type FetchRpc = (urls: string[], payload: any, as_proof: boolean) => Promise<any>;
12
+ export type ProofStrategy = (client: ColibriClient, req: RequestArguments, config: Config, fetch_rpc: FetchRpc) => Promise<any>;
13
+ export type WarningHandler = (req: RequestArguments, message: string) => Promise<any>;
7
14
  export interface RequestArguments {
8
15
  readonly method: string;
9
16
  readonly params?: readonly unknown[] | object;
@@ -20,17 +27,30 @@ export interface Cache {
20
27
  get(req: DataRequest): Uint8Array | undefined;
21
28
  set(req: DataRequest, data: Uint8Array): void;
22
29
  }
23
- export interface Config {
24
- chainId: number;
30
+ export interface ChainConfig {
25
31
  beacon_apis: string[];
26
32
  rpcs: string[];
27
33
  proofer?: string[];
28
34
  trusted_block_hashes: string[];
35
+ verify?: (method: string, args: any[]) => boolean;
36
+ pollingInterval?: number;
37
+ proofStrategy?: ProofStrategy;
38
+ }
39
+ export interface EIP1193Client {
40
+ request(args: RequestArguments): Promise<unknown>;
41
+ on(event: string, callback: (data: any) => void): this;
42
+ removeListener(event: string, callback: (data: any) => void): this;
43
+ }
44
+ export interface Config extends ChainConfig {
45
+ chainId: number | string;
29
46
  cache?: Cache;
30
47
  debug?: boolean;
31
48
  include_code?: boolean;
32
- verify?: (method: string, args: any[]) => boolean;
33
- pollingInterval?: number;
49
+ chains: {
50
+ [chainId: number]: ChainConfig;
51
+ };
52
+ fallback_provider?: EIP1193Client;
53
+ warningHandler: WarningHandler;
34
54
  }
35
55
  export interface DataRequest {
36
56
  method: string;