@corpus-core/colibri-stateless 0.2.1 → 0.2.3

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.
@@ -0,0 +1,23 @@
1
+ import { EventEmitter } from './eventEmitter.js';
2
+ import { ProviderRpcError } from './types.js';
3
+ interface ConnectionStateConfig {
4
+ chainId: number;
5
+ debug?: boolean;
6
+ }
7
+ export declare class ConnectionState {
8
+ private _isConnected;
9
+ private _currentChainId;
10
+ private _initialConnectionAttempted;
11
+ private config;
12
+ private fetchRpcChainIdCallback;
13
+ private eventEmitter;
14
+ private formatChainIdFunc;
15
+ constructor(config: ConnectionStateConfig, fetchRpcChainIdCallback: () => Promise<any>, eventEmitter: EventEmitter, formatChainIdFunc: (value: any, debug?: boolean) => string | null);
16
+ get isConnected(): boolean;
17
+ get currentChainId(): string | null;
18
+ get initialConnectionAttempted(): boolean;
19
+ attemptInitialConnection(): Promise<void>;
20
+ processSuccessfulRequest(requestMethod: string, requestResult: any): void;
21
+ processFailedRequest(error: ProviderRpcError): void;
22
+ }
23
+ export {};
@@ -0,0 +1,109 @@
1
+ import { ProviderRpcError } from './types.js';
2
+ export class ConnectionState {
3
+ _isConnected = false;
4
+ _currentChainId = null;
5
+ _initialConnectionAttempted = false;
6
+ config;
7
+ fetchRpcChainIdCallback;
8
+ eventEmitter;
9
+ formatChainIdFunc;
10
+ constructor(config, fetchRpcChainIdCallback, eventEmitter, formatChainIdFunc) {
11
+ this.config = config;
12
+ this.fetchRpcChainIdCallback = fetchRpcChainIdCallback;
13
+ this.eventEmitter = eventEmitter;
14
+ this.formatChainIdFunc = formatChainIdFunc;
15
+ this._currentChainId = this.formatChainIdFunc(this.config.chainId, this.config.debug);
16
+ }
17
+ get isConnected() {
18
+ return this._isConnected;
19
+ }
20
+ get currentChainId() {
21
+ return this._currentChainId;
22
+ }
23
+ get initialConnectionAttempted() {
24
+ return this._initialConnectionAttempted;
25
+ }
26
+ async attemptInitialConnection() {
27
+ if (this._initialConnectionAttempted)
28
+ return;
29
+ this._initialConnectionAttempted = true;
30
+ if (this.config.debug) {
31
+ console.log('[CS] Attempting initial connection...');
32
+ }
33
+ try {
34
+ const result = await this.fetchRpcChainIdCallback();
35
+ const formattedChainId = this.formatChainIdFunc(result, this.config.debug);
36
+ if (formattedChainId) {
37
+ this._isConnected = true;
38
+ this._currentChainId = formattedChainId;
39
+ if (this.config.debug) {
40
+ console.log('[CS] Initial connection successful. ChainId:', this._currentChainId);
41
+ }
42
+ this.eventEmitter.emit('connect', { chainId: this._currentChainId });
43
+ }
44
+ else {
45
+ this._isConnected = false;
46
+ if (this.config.debug) {
47
+ console.warn('[CS] Initial eth_chainId did not return a valid format or was null:', result, ". Client remains disconnected but uses configured chainId.");
48
+ }
49
+ }
50
+ }
51
+ catch (error) {
52
+ this._isConnected = false;
53
+ if (this.config.debug) {
54
+ console.error('[CS] Initial connection check failed:', error);
55
+ if (error instanceof ProviderRpcError) {
56
+ console.error('[CS] ProviderRpcError during initial connection check:', error.code, error.message);
57
+ }
58
+ }
59
+ }
60
+ }
61
+ processSuccessfulRequest(requestMethod, requestResult) {
62
+ if (!this._isConnected) {
63
+ this._isConnected = true;
64
+ const updateAndEmitConnect = async () => {
65
+ let determinedChainId = null;
66
+ if (requestMethod === 'eth_chainId') {
67
+ determinedChainId = this.formatChainIdFunc(requestResult, this.config.debug);
68
+ }
69
+ else {
70
+ try {
71
+ const chainIdResult = await this.fetchRpcChainIdCallback();
72
+ determinedChainId = this.formatChainIdFunc(chainIdResult, this.config.debug);
73
+ }
74
+ catch (e) {
75
+ if (this.config.debug)
76
+ console.warn('[CS] Failed to fetch chainId for connect event, using current _currentChainId as fallback.', e);
77
+ determinedChainId = this._currentChainId;
78
+ }
79
+ }
80
+ this._currentChainId = determinedChainId || this.formatChainIdFunc(this.config.chainId, this.config.debug);
81
+ this.eventEmitter.emit('connect', { chainId: this._currentChainId });
82
+ };
83
+ updateAndEmitConnect().catch(err => {
84
+ if (this.config.debug)
85
+ console.error("[CS] Error during background processing for connect event:", err);
86
+ });
87
+ }
88
+ else if (requestMethod === 'eth_chainId') {
89
+ const newChainId = this.formatChainIdFunc(requestResult, this.config.debug);
90
+ if (newChainId && this._currentChainId !== newChainId) {
91
+ const oldChainId = this._currentChainId;
92
+ this._currentChainId = newChainId;
93
+ if (this.config.debug) {
94
+ console.log('[CS] ChainId changed from', oldChainId, 'to', this._currentChainId);
95
+ }
96
+ this.eventEmitter.emit('chainChanged', this._currentChainId);
97
+ }
98
+ }
99
+ }
100
+ processFailedRequest(error) {
101
+ if (error.code === 4900 && this._isConnected) {
102
+ this._isConnected = false;
103
+ if (this.config.debug) {
104
+ console.log('[CS] Disconnected due to error 4900.');
105
+ }
106
+ this.eventEmitter.emit('disconnect', error);
107
+ }
108
+ }
109
+ }
@@ -0,0 +1,6 @@
1
+ export declare class EventEmitter {
2
+ private listeners;
3
+ on(event: string, callback: (data: any) => void): this;
4
+ removeListener(event: string, callback: (data: any) => void): this;
5
+ emit(event: string, data?: any): void;
6
+ }
@@ -0,0 +1,28 @@
1
+ export class EventEmitter {
2
+ listeners = {};
3
+ on(event, callback) {
4
+ if (!this.listeners[event]) {
5
+ this.listeners[event] = [];
6
+ }
7
+ this.listeners[event].push(callback);
8
+ return this;
9
+ }
10
+ removeListener(event, callback) {
11
+ if (this.listeners[event]) {
12
+ this.listeners[event] = this.listeners[event].filter(listener => listener !== callback);
13
+ }
14
+ return this;
15
+ }
16
+ emit(event, data) {
17
+ if (this.listeners[event]) {
18
+ this.listeners[event].forEach(listener => {
19
+ try {
20
+ listener(data);
21
+ }
22
+ catch (e) {
23
+ console.error(`Error in event listener for ${event}:`, e);
24
+ }
25
+ });
26
+ }
27
+ }
28
+ }
package/index.d.ts CHANGED
@@ -1,44 +1,14 @@
1
1
  import { Storage as C4Storage } from "./wasm.js";
2
- export interface Cache {
3
- cacheable(req: DataRequest): boolean;
4
- get(req: DataRequest): Uint8Array | undefined;
5
- set(req: DataRequest, data: Uint8Array): void;
6
- }
7
- interface RequestArguments {
8
- readonly method: string;
9
- readonly params?: readonly unknown[] | object;
10
- }
11
- export interface Config {
12
- chainId: number;
13
- beacon_apis: string[];
14
- rpcs: string[];
15
- proofer?: string[];
16
- trusted_block_hashes: string[];
17
- cache?: Cache;
18
- debug?: boolean;
19
- include_code?: boolean;
20
- verify?: (method: string, args: any[]) => boolean;
21
- }
22
- export interface DataRequest {
23
- method: string;
24
- chain_id: number;
25
- encoding: string;
26
- type: string;
27
- exclude_mask: number;
28
- url: string;
29
- payload: any;
30
- req_ptr: number;
31
- }
32
- export declare enum MethodType {
33
- PROOFABLE = 1,
34
- UNPROOFABLE = 2,
35
- NOT_SUPPORTED = 3,
36
- LOCAL = 4
37
- }
38
- export declare function handle_request(req: DataRequest, conf: Config): Promise<void>;
2
+ import { ProviderRpcError, RequestArguments, Cache, Config as C4Config, DataRequest, MethodType as C4MethodType, ProviderMessage } from './types.js';
3
+ import { EthSubscribeSubscriptionType, EthNewFilterType } from './subscriptionManager.js';
4
+ export { ProviderRpcError, RequestArguments, Cache, C4Config, DataRequest, C4MethodType as MethodType, ProviderMessage, EthSubscribeSubscriptionType, EthNewFilterType };
5
+ export declare function handle_request(req: DataRequest, conf: C4Config): Promise<void>;
39
6
  export default class C4Client {
40
- config: Config;
41
- constructor(config?: Partial<Config>);
7
+ config: C4Config;
8
+ private eventEmitter;
9
+ private connectionState;
10
+ private subscriptionManager;
11
+ constructor(config?: Partial<C4Config>);
42
12
  private getProoferConfig;
43
13
  private get flags();
44
14
  /**
@@ -46,7 +16,7 @@ export default class C4Client {
46
16
  * @param method - The method to check
47
17
  * @returns The method type
48
18
  */
49
- getMethodSupport(method: string): Promise<MethodType>;
19
+ getMethodSupport(method: string): Promise<C4MethodType>;
50
20
  /**
51
21
  * creates a proof for the given method and arguments
52
22
  * @param method - The method to create a proof for
@@ -73,8 +43,7 @@ export default class C4Client {
73
43
  */
74
44
  rpc(method: string, args: any[]): Promise<any>;
75
45
  static register_storage(storage: C4Storage): Promise<void>;
76
- request(args: RequestArguments): Promise<any>;
77
- on(event: string, callback: (data: any) => void): void;
78
- removeListener(event: string, callback: (data: any) => void): void;
46
+ request(args: RequestArguments): Promise<unknown>;
47
+ on(event: string, callback: (data: any) => void): this;
48
+ removeListener(event: string, callback: (data: any) => void): this;
79
49
  }
80
- export {};
package/index.js CHANGED
@@ -1,19 +1,34 @@
1
1
  // Import the Emscripten-generated module
2
2
  import { as_bytes, as_char_ptr, as_json, copy_to_c, getC4w } from "./wasm.js";
3
- export var MethodType;
4
- (function (MethodType) {
5
- MethodType[MethodType["PROOFABLE"] = 1] = "PROOFABLE";
6
- MethodType[MethodType["UNPROOFABLE"] = 2] = "UNPROOFABLE";
7
- MethodType[MethodType["NOT_SUPPORTED"] = 3] = "NOT_SUPPORTED";
8
- MethodType[MethodType["LOCAL"] = 4] = "LOCAL";
9
- })(MethodType || (MethodType = {}));
10
- async function initialize_storage(conf) {
11
- // TODO handle trusted_block_hashes
12
- const c4w = await getC4w();
3
+ import { EventEmitter } from './eventEmitter.js';
4
+ import { ConnectionState } from './connectionState.js';
5
+ import { ProviderRpcError, MethodType as C4MethodType } from './types.js';
6
+ import { SubscriptionManager } from './subscriptionManager.js';
7
+ // Re-export types needed by consumers of the C4Client module
8
+ 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;
13
27
  }
14
28
  async function fetch_rpc(urls, payload, as_proof = false) {
15
29
  let last_error = "All nodes failed";
16
30
  for (const url of urls) {
31
+ console.log({ url, payload });
17
32
  const response = await fetch(url, {
18
33
  method: 'POST',
19
34
  body: JSON.stringify({ id: 1, jsonrpc: "2.0", ...payload }),
@@ -111,23 +126,38 @@ function check_trusted_blockhashes(trusted_block_hashes, c4w, chainId) {
111
126
  }
112
127
  export default class C4Client {
113
128
  config;
129
+ eventEmitter;
130
+ connectionState;
131
+ subscriptionManager;
114
132
  constructor(config) {
115
133
  this.config = {
116
- ...{
117
- chainId: 1, // Default chainId
118
- beacon_apis: ["https://lodestar-mainnet.chainsafe.io"], // Default beacon API
119
- rpcs: ["https://rpc.ankr.com/eth"], // Default RPC
120
- trusted_block_hashes: [],
121
- proofer: ["https://mainnet.colibri-proof.tech"],
122
- },
123
- ...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
+ trusted_block_hashes: [],
139
+ proofer: ["https://mainnet.colibri-proof.tech"],
140
+ 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
124
143
  };
144
+ 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 });
125
155
  }
126
156
  async getProoferConfig() {
127
157
  const c4w = await getC4w();
128
158
  if (!c4w.storage)
129
159
  return '0x';
130
- const state = c4w.storage.get('state_' + this.config.chainId);
160
+ const state = c4w.storage.get('states_' + this.config.chainId);
131
161
  return '0x' + (state ? Array.from(state).map(_ => _.toString(16).padStart(2, '0')).join('') : '');
132
162
  }
133
163
  get flags() {
@@ -224,36 +254,69 @@ export default class C4Client {
224
254
  * @returns The result
225
255
  */
226
256
  async rpc(method, args) {
257
+ // eth_subscribe and eth_unsubscribe are handled by C4Client.request before this method is called.
258
+ // This rpc method is for the underlying data fetching/proving.
227
259
  const method_type = await this.getMethodSupport(method);
228
- let proof = new Uint8Array();
229
260
  switch (method_type) {
230
- case MethodType.PROOFABLE: {
231
- if (this.config.verify && !this.config.verify(method, args))
261
+ case C4MethodType.PROOFABLE: {
262
+ if (this.config.verify && !this.config.verify(method, args)) {
232
263
  return await fetch_rpc(this.config.rpcs, { method, params: args }, false);
233
- proof = this.config.proofer && this.config.proofer.length
264
+ }
265
+ const proof = this.config.proofer && this.config.proofer.length
234
266
  ? await fetch_rpc(this.config.proofer, { method, params: args, c4: await this.getProoferConfig() }, true)
235
267
  : await this.createProof(method, args);
236
- break;
268
+ return this.verifyProof(method, args, proof);
237
269
  }
238
- case MethodType.UNPROOFABLE:
270
+ case C4MethodType.UNPROOFABLE:
239
271
  return await fetch_rpc(this.config.rpcs, { method, params: args }, false);
240
- case MethodType.NOT_SUPPORTED:
241
- throw new Error(`Method ${method} is not supported`);
242
- case MethodType.LOCAL:
243
- break;
272
+ case C4MethodType.NOT_SUPPORTED:
273
+ throw new ProviderRpcError(4200, `Method ${method} is not supported by C4Client.rpc core`);
274
+ case C4MethodType.LOCAL:
275
+ return this.verifyProof(method, args, new Uint8Array());
244
276
  }
245
- return this.verifyProof(method, args, proof);
277
+ // Should be unreachable if MethodType enum is comprehensive and handled
278
+ throw new ProviderRpcError(-32603, `Internal error: Unhandled method type for ${method} in C4Client.rpc core`);
246
279
  }
247
280
  static async register_storage(storage) {
248
281
  const c4w = await getC4w();
249
282
  c4w.storage = storage;
250
283
  }
251
284
  async request(args) {
252
- const res = await this.rpc(args.method, args.params || []);
253
- return res;
285
+ if (!this.connectionState.initialConnectionAttempted) {
286
+ await this.connectionState.attemptInitialConnection();
287
+ }
288
+ const { method, params } = args;
289
+ const paramsArray = Array.isArray(params) ? params : (params ? [params] : []);
290
+ // Attempt to handle with SubscriptionManager first
291
+ const subscriptionResult = this.subscriptionManager.handleRequest(method, paramsArray);
292
+ if (subscriptionResult !== null) {
293
+ // If handleRequest returns a Promise, it means it's handling the request
294
+ return subscriptionResult;
295
+ }
296
+ // If not handled by SubscriptionManager, proceed with standard RPC call logic
297
+ try {
298
+ const result = await this.rpc(method, paramsArray);
299
+ this.connectionState.processSuccessfulRequest(method, result);
300
+ return result;
301
+ }
302
+ catch (error) {
303
+ const providerError = ProviderRpcError.createError(error);
304
+ this.connectionState.processFailedRequest(providerError);
305
+ throw providerError;
306
+ }
254
307
  }
255
308
  on(event, callback) {
309
+ this.eventEmitter.on(event, callback);
310
+ if ((event === 'connect' || event === 'disconnect') && !this.connectionState.initialConnectionAttempted) {
311
+ this.connectionState.attemptInitialConnection().catch(err => {
312
+ if (this.config.debug)
313
+ console.error("[C4Client] Error during lazy initial connection attempt triggered by 'on':", err);
314
+ });
315
+ }
316
+ return this;
256
317
  }
257
318
  removeListener(event, callback) {
319
+ this.eventEmitter.removeListener(event, callback);
320
+ return this;
258
321
  }
259
322
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corpus-core/colibri-stateless",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",
@@ -0,0 +1,35 @@
1
+ import { EventEmitter } from './eventEmitter.js';
2
+ export type EthSubscribeSubscriptionType = 'newHeads' | 'logs';
3
+ export type EthNewFilterType = 'block' | 'pendingTransaction' | 'log';
4
+ export interface Subscription {
5
+ id: string;
6
+ subscriptionMethod: 'eth_subscribe' | 'eth_newFilter';
7
+ kind: EthSubscribeSubscriptionType | EthNewFilterType;
8
+ params?: any;
9
+ lastPolledBlockNumber?: bigint;
10
+ timeoutId?: number;
11
+ }
12
+ interface SubscriptionManagerConfig {
13
+ debug?: boolean;
14
+ pollingInterval?: number;
15
+ }
16
+ export type RpcCaller = (method: string, params: any[]) => Promise<any>;
17
+ export declare class SubscriptionManager {
18
+ private activeSubscriptions;
19
+ private eventEmitter;
20
+ private rpcCall;
21
+ private config;
22
+ private nextSubscriptionId;
23
+ constructor(rpcCallCallback: RpcCaller, eventEmitter: EventEmitter, config?: SubscriptionManagerConfig);
24
+ handleRequest(method: string, paramsArray: any[]): Promise<any> | null;
25
+ private generateSubscriptionId;
26
+ subscribe(kind: EthSubscribeSubscriptionType, params?: any[]): Promise<string>;
27
+ newFilter(kind: EthNewFilterType, params?: any[]): Promise<string>;
28
+ getFilterChanges(filterId: string): Promise<any[]>;
29
+ uninstallFilter(filterId: string): Promise<boolean>;
30
+ private executePollLogic;
31
+ private schedulePoll;
32
+ private pollSubscriptionAndEmit;
33
+ unsubscribe(subscriptionId: string): Promise<boolean>;
34
+ }
35
+ export {};