@corpus-core/colibri-stateless 0.7.5 → 0.7.7

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.
package/index.js CHANGED
@@ -21,160 +21,22 @@
21
21
  * SPDX-License-Identifier: MIT
22
22
  */
23
23
  // Import the Emscripten-generated module
24
- import { as_bytes, as_char_ptr, as_json, copy_to_c, getC4w } from "./wasm.js";
24
+ import { as_bytes, as_char_ptr, as_json, copy_to_c, getC4w, get_prover_config_hex, set_trusted_checkpoint } from "./wasm.js";
25
25
  import { EventEmitter } from './eventEmitter.js';
26
26
  import { ConnectionState } from './connectionState.js';
27
27
  import { ProviderRpcError, MethodType as C4MethodType } from './types.js';
28
+ import { default_config, get_chain_id, chain_conf } from './chains.js';
28
29
  import { SubscriptionManager } from './subscriptionManager.js';
29
30
  import Strategy from './strategy.js';
30
31
  import { TransactionVerifier, PrototypeProtection } from './transactionVerifier.js';
32
+ import { fetch_rpc, handle_request } from './http.js';
31
33
  export { Strategy };
32
34
  // Re-export types needed by consumers of the C4Client module
33
35
  export { ProviderRpcError, C4MethodType as MethodType };
34
36
  // Re-export transaction verification utilities
35
37
  export { TransactionVerifier, PrototypeProtection } from './transactionVerifier.js';
36
- async function fetch_rpc(urls, payload, as_proof = false) {
37
- let last_error = "All nodes failed";
38
- for (const url of urls) {
39
- const response = await fetch(url, {
40
- method: 'POST',
41
- body: JSON.stringify({ id: 1, jsonrpc: "2.0", ...payload }),
42
- headers: {
43
- "Content-Type": "application/json",
44
- "Accept": as_proof ? "application/octet-stream" : "application/json"
45
- }
46
- });
47
- if (response.ok) {
48
- if (!as_proof) {
49
- const res = await response.json();
50
- if (res.error) {
51
- last_error = res.error?.message || res.error;
52
- continue;
53
- }
54
- return res.result;
55
- }
56
- const bytes = await response.blob().then(blob => blob.arrayBuffer());
57
- return new Uint8Array(bytes);
58
- }
59
- else
60
- last_error = `HTTP error! Status: ${response.status}, Details: ${await response.text()}`;
61
- }
62
- throw new Error(last_error);
63
- }
64
- function log(msg) {
65
- console.error(msg);
66
- }
67
- export async function handle_request(req, conf) {
68
- const free_buffers = [];
69
- const servers = req.type == "checkpointz"
70
- ? (conf.checkpointz || [])
71
- : (req.type == "beacon_api"
72
- ? ((conf.prover && conf.prover.length) ? conf.prover : conf.beacon_apis)
73
- : conf.rpcs);
74
- const c4w = await getC4w();
75
- let path = (req.type == 'eth_rpc' && req.payload)
76
- ? `rpc: ${req.payload?.method}(${req.payload?.params.join(',')})`
77
- : req.url;
78
- let cacheable = conf.cache && conf.cache.cacheable(req);
79
- if (cacheable && conf.cache) {
80
- const data = conf.cache.get(req);
81
- if (data) {
82
- if (conf.debug)
83
- log(`::: ${path} (len=${data.length} bytes) CACHED`);
84
- c4w._c4w_req_set_response(req.req_ptr, copy_to_c(data, c4w), data.length, 0);
85
- return;
86
- }
87
- }
88
- let node_index = 0;
89
- let last_error = "All nodes failed";
90
- for (const server of servers) {
91
- if (req.exclude_mask & (1 << node_index)) {
92
- node_index++;
93
- continue;
94
- }
95
- try {
96
- const response = await fetch(server + (req.url ? ('/' + req.url) : ''), {
97
- method: req.method,
98
- body: req.payload ? JSON.stringify(req.payload) : undefined,
99
- headers: {
100
- "Content-Type": "application/json",
101
- "Accept": req.encoding == "json" ? "application/json" : "application/octet-stream"
102
- }
103
- });
104
- if (!response.ok)
105
- throw new Error(`HTTP error! Status: ${response.status}, Details: ${await response.text()}`);
106
- const bytes = await response.blob().then(blob => blob.arrayBuffer());
107
- const data = new Uint8Array(bytes);
108
- c4w._c4w_req_set_response(req.req_ptr, copy_to_c(data, c4w), data.length, node_index);
109
- if (conf.debug)
110
- log(`::: ${path} (len=${data.length} bytes) FETCHED`);
111
- if (conf.cache && cacheable)
112
- conf.cache.set(req, data);
113
- return;
114
- }
115
- catch (e) {
116
- last_error = (e instanceof Error) ? e.message : String(e);
117
- }
118
- node_index++;
119
- }
120
- c4w._c4w_req_set_error(req.req_ptr, as_char_ptr(last_error, c4w, free_buffers), 0);
121
- free_buffers.forEach(ptr => c4w._free(ptr));
122
- if (conf.debug)
123
- log(`::: ${path} (Error: ${last_error})`);
124
- }
125
- const default_config = {
126
- '1': {
127
- alias: ["mainnet", "eth", "0x1"],
128
- beacon_apis: ["https://lodestar-mainnet.chainsafe.io"],
129
- rpcs: ["https://rpc.ankr.com/eth"],
130
- prover: ["https://mainnet1.colibri-proof.tech"],
131
- checkpointz: ["https://sync-mainnet.beaconcha.in", "https://beaconstate.info", "https://sync.invis.tools", "https://beaconstate.ethstaker.cc"],
132
- pollingInterval: 12000,
133
- },
134
- '11155111': {
135
- alias: ["sepolia", "0xaa36a7"],
136
- beacon_apis: ["https://ethereum-sepolia-beacon-api.publicnode.com"],
137
- rpcs: ["https://ethereum-sepolia-rpc.publicnode.com"],
138
- prover: ["https://sepolia.colibri-proof.tech"],
139
- checkpointz: [], // No public checkpointz for Sepolia yet
140
- pollingInterval: 12000,
141
- },
142
- '100': {
143
- alias: ["gnosis", "xdai", "0x64"],
144
- beacon_apis: ["https://gnosis.colibri-proof.tech"],
145
- rpcs: ["https://rpc.ankr.com/gnosis"],
146
- prover: ["https://gnosis.colibri-proof.tech"],
147
- checkpointz: [], // TODO: Add Gnosis checkpointz servers
148
- pollingInterval: 5000,
149
- },
150
- '10200': {
151
- alias: ["chiado", "0x27d8"],
152
- beacon_apis: ["https://gnosis-chiado-beacon-api.publicnode.com"],
153
- rpcs: ["https://gnosis-chiado-rpc.publicnode.com"],
154
- prover: ["https://chiado.colibri-proof.tech"],
155
- checkpointz: [], // No public checkpointz for Chiado yet
156
- pollingInterval: 5000,
157
- },
158
- };
159
- function get_chain_id(chain_id) {
160
- const chain_id_num = parseInt(chain_id);
161
- if (!isNaN(chain_id_num))
162
- return chain_id_num;
163
- for (const chain in default_config) {
164
- if (default_config[chain].alias.includes(chain_id))
165
- return parseInt(chain);
166
- }
167
- throw new Error("Invalid chain id: " + chain_id);
168
- }
169
- function chain_conf(config, chainId) {
170
- const k = config?.chains?.[chainId];
171
- if (k)
172
- return k;
173
- const k2 = default_config[chainId + ''];
174
- if (k2)
175
- return k2;
176
- return undefined;
177
- }
38
+ // fetch_rpc und handle_request sind nach http.ts verschoben
39
+ // default_config, get_chain_id, chain_conf ausgelagert nach ./chains.ts
178
40
  function cleanup_args(method, args) {
179
41
  if (method == "eth_verifyLogs")
180
42
  return args.map(arg => ({
@@ -188,16 +50,30 @@ export default class C4Client {
188
50
  eventEmitter;
189
51
  connectionState;
190
52
  subscriptionManager;
191
- // Schutz vor Prototype Pollution durch Einfrieren kritischer Methoden
53
+ initMap = new Map();
54
+ flags = 0;
55
+ // Protect against prototype pollution by freezing critical methods
192
56
  static CRITICAL_METHODS = ['rpc', 'request', 'verifyProof', 'createProof'];
193
57
  static {
194
- // Schutz vor Prototype Pollution anwenden
58
+ // Apply prototype pollution protection
195
59
  PrototypeProtection.protectClass(C4Client, C4Client.CRITICAL_METHODS);
196
60
  }
61
+ /**
62
+ * Creates a new Colibri client instance.
63
+ *
64
+ * Example:
65
+ * ```ts
66
+ * import Colibri from '@corpus-core/colibri-stateless';
67
+ * const client = new Colibri({ chainId: 1 });
68
+ * const blockNumber = await client.request({ method: 'eth_blockNumber' });
69
+ * ```
70
+ *
71
+ * @param config Optional partial configuration; user config overrides defaults.
72
+ */
197
73
  constructor(config) {
198
74
  const chainId = config?.chainId ? get_chain_id(config?.chainId + '') : 1;
199
75
  const chain_config = { ...default_config[chainId + ''] };
200
- // Schutz vor Config-Manipulation durch Deep-Freeze
76
+ // Protect against config manipulation (deep-freeze at sensitive boundaries)
201
77
  const baseConfig = {
202
78
  chains: {},
203
79
  trusted_checkpoint: undefined,
@@ -208,7 +84,7 @@ export default class C4Client {
208
84
  ...config, // User config overrides defaults
209
85
  chainId,
210
86
  };
211
- // Schutz vor Config-Manipulation anwenden
87
+ // Apply config immutability/protection
212
88
  PrototypeProtection.protectConfig(baseConfig, ['rpcs', 'beacon_apis', 'prover', 'checkpointz']);
213
89
  this.config = baseConfig;
214
90
  if (!this.config.warningHandler)
@@ -216,25 +92,38 @@ export default class C4Client {
216
92
  if (!this.config.proofStrategy)
217
93
  this.config.proofStrategy = Strategy.VerifyIfPossible;
218
94
  this.eventEmitter = new EventEmitter();
219
- this.connectionState = new ConnectionState({ chainId: parseInt(this.config.chainId + ''), debug: this.config.debug }, async () => this.rpc('eth_chainId', [], C4MethodType.LOCAL), // Use the specific callback
95
+ this.connectionState = new ConnectionState({ chainId: parseInt(this.config.chainId + ''), debug: this.config.debug }, async () => this.rpc('eth_chainId', [], C4MethodType.LOCAL), // specific callback to detect chainId
220
96
  this.eventEmitter);
221
97
  this.subscriptionManager = new SubscriptionManager(async (method, params) => this.rpc(method, params), this.eventEmitter, {
222
98
  debug: this.config.debug,
223
99
  pollingInterval: this.config.pollingInterval || chain_conf(this.config, this.config.chainId)?.pollingInterval || 12000
224
100
  });
225
101
  }
226
- async getProverConfig() {
227
- const c4w = await getC4w();
228
- if (!c4w.storage)
229
- return '0x';
230
- const state = c4w.storage.get('states_' + this.config.chainId);
231
- return '0x' + (state ? Array.from(state).map(_ => _.toString(16).padStart(2, '0')).join('') : '');
232
- }
233
- get flags() {
234
- return this.config.include_code ? 1 : 0;
102
+ // Prover config helpers are moved to wasm.ts (get_prover_config_hex)
103
+ async fetch_checkpointz() {
104
+ let checkpoint = undefined;
105
+ for (const url of [...(this.config.checkpointz || []), ...(this.config.beacon_apis || []), ...(this.config.prover || [])]) {
106
+ const response = await fetch(url + (url.endsWith('/') ? '' : '/') + 'eth/v1/beacon/states/head/finality_checkpoints', {
107
+ method: 'GET',
108
+ headers: {
109
+ "Content-Type": "application/json"
110
+ }
111
+ });
112
+ if (response.ok) {
113
+ const res = await response.json();
114
+ checkpoint = res?.data?.finalized?.root;
115
+ if (checkpoint)
116
+ break;
117
+ }
118
+ }
119
+ if (!checkpoint)
120
+ throw new Error('No checkpoint found');
121
+ this.config.trusted_checkpoint = checkpoint;
122
+ // set trusted checkpoint in C state so we can use it in proof calls immediately
123
+ await set_trusted_checkpoint(this.config.chainId, checkpoint);
235
124
  }
236
125
  /**
237
- * checks, whether the rpc-method is supported or proofable.
126
+ * Checks whether the RPC method is supported or proofable.
238
127
  * @param method - The method to check
239
128
  * @returns The method type
240
129
  */
@@ -246,7 +135,7 @@ export default class C4Client {
246
135
  return method_type;
247
136
  }
248
137
  /**
249
- * creates a proof for the given method and arguments
138
+ * Creates a proof for the given method and arguments.
250
139
  * @param method - The method to create a proof for
251
140
  * @param args - The arguments to create a proof for
252
141
  * @returns The proof
@@ -279,7 +168,7 @@ export default class C4Client {
279
168
  }
280
169
  }
281
170
  /**
282
- * verifies a proof for the given method and arguments
171
+ * Verifies a proof for the given method and arguments.
283
172
  * @param method - The method to verify the proof for
284
173
  * @param args - The arguments to verify the proof for
285
174
  * @param proof - The proof to verify
@@ -316,10 +205,10 @@ export default class C4Client {
316
205
  }
317
206
  }
318
207
  /**
319
- * executes a rpc-method, which includes
320
- * -creating or fetching the proof
321
- * -verifying the proof
322
- * -returning the result
208
+ * Executes an RPC method. This includes:
209
+ * - Creating or fetching the proof
210
+ * - Verifying the proof
211
+ * - Returning the result
323
212
  * @param method - The method to execute
324
213
  * @param args - The arguments to execute the method with
325
214
  * @returns The result
@@ -327,7 +216,12 @@ export default class C4Client {
327
216
  async rpc(method, args, method_type) {
328
217
  // eth_subscribe and eth_unsubscribe are handled by C4Client.request before this method is called.
329
218
  // This rpc method is for the underlying data fetching/proving.
330
- // Spezielle Behandlung für eth_sendTransaction mit Verifikation
219
+ if (!this.initMap.get(this.config.chainId)) {
220
+ this.initMap.set(this.config.chainId, true);
221
+ if (this.config.checkpointz && this.config.checkpointz.length > 0 && !this.config.trusted_checkpoint && (await get_prover_config_hex(this.config.chainId)).length == 2)
222
+ await this.fetch_checkpointz();
223
+ }
224
+ // Special handling for eth_sendTransaction with verification
331
225
  if (method === 'eth_sendTransaction' && this.config.verifyTransactions) {
332
226
  return await TransactionVerifier.verifyAndSendTransaction(args[0], this.config, (method, args, methodType) => this.rpc(method, args, methodType), fetch_rpc);
333
227
  }
@@ -339,7 +233,7 @@ export default class C4Client {
339
233
  return await fetch_rpc(this.config.rpcs, { method, params: args }, false);
340
234
  }
341
235
  const proof = this.config.prover && this.config.prover.length
342
- ? await fetch_rpc(this.config.prover, { method, params: cleanup_args(method, args), c4: await this.getProverConfig() }, true)
236
+ ? await fetch_rpc(this.config.prover, { method, params: cleanup_args(method, args), c4: await get_prover_config_hex(this.config.chainId) }, true)
343
237
  : await this.createProof(method, args);
344
238
  return this.verifyProof(method, args, proof);
345
239
  }
@@ -385,6 +279,12 @@ export default class C4Client {
385
279
  throw providerError;
386
280
  }
387
281
  }
282
+ /**
283
+ * Registers an event listener on the Colibri client.
284
+ * @param event Event name (e.g. 'connect', 'disconnect', 'message')
285
+ * @param callback Callback invoked with event data
286
+ * @return This client for chaining
287
+ */
388
288
  on(event, callback) {
389
289
  this.eventEmitter.on(event, callback);
390
290
  if ((event === 'connect' || event === 'disconnect') && !this.connectionState.initialConnectionAttempted) {
@@ -395,6 +295,12 @@ export default class C4Client {
395
295
  }
396
296
  return this;
397
297
  }
298
+ /**
299
+ * Removes a previously registered event listener from the Colibri client.
300
+ * @param event Event name
301
+ * @param callback Same function reference passed to on()
302
+ * @return This client for chaining
303
+ */
398
304
  removeListener(event, callback) {
399
305
  this.eventEmitter.removeListener(event, callback);
400
306
  return this;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@corpus-core/colibri-stateless",
3
- "version": "0.7.5",
3
+ "version": "0.7.7",
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",
@@ -10,7 +10,8 @@
10
10
  "test:webpack": "npm run webpack:install && npm run webpack:build",
11
11
  "webpack:install": "cd webpack && npm install --legacy-peer-deps",
12
12
  "webpack:build": "cd webpack && npm run build",
13
- "webpack:start": "cd webpack && npm run start"
13
+ "webpack:start": "cd webpack && npm run start",
14
+ "check:cors": "node scripts/check_cors.mjs"
14
15
  },
15
16
  "repository": {
16
17
  "type": "git",
package/wasm.d.ts CHANGED
@@ -48,6 +48,11 @@ export interface Storage {
48
48
  export type C4WModule = {
49
49
  then: (cb: (mod: C4W) => void) => void;
50
50
  };
51
+ /**
52
+ * Optionally override the URL/path used to load the WASM binary when not embedded.
53
+ * @param url Absolute/relative URL in browsers, or filesystem path in Node.
54
+ */
55
+ export declare function set_wasm_url(url: string): void;
51
56
  export declare function loadC4WModule(): Promise<C4W>;
52
57
  export declare function getC4w(): Promise<C4W>;
53
58
  export declare function get_default_storage(): Promise<Storage>;
@@ -55,3 +60,15 @@ export declare function as_char_ptr(str: string, c4w: C4W, free_ptrs?: number[])
55
60
  export declare function as_json(ptr: number, c4w: C4W, free_ptrs?: number[] | boolean): any;
56
61
  export declare function as_bytes(ptr: number, len: number, c4w: C4W, free_ptrs?: number[] | boolean): Uint8Array;
57
62
  export declare function copy_to_c(data: Uint8Array, c4w: C4W, free_ptrs?: number[]): number;
63
+ /**
64
+ * Returns the prover config state for a given chain as hex string with 0x prefix.
65
+ * @param chainId Chain identifier
66
+ * @return Hex string (e.g. "0x...") or "0x" if no state present
67
+ */
68
+ export declare function get_prover_config_hex(chainId: number): Promise<string>;
69
+ /**
70
+ * Sets the trusted checkpoint inside the C context to initialize state.
71
+ * @param chainId Chain identifier
72
+ * @param checkpoint Trusted checkpoint root hex string
73
+ */
74
+ export declare function set_trusted_checkpoint(chainId: number, checkpoint: string): Promise<void>;
package/wasm.js CHANGED
@@ -20,9 +20,25 @@
20
20
  *
21
21
  * SPDX-License-Identifier: MIT
22
22
  */
23
+ let wasmUrlOverride = null;
24
+ /**
25
+ * Optionally override the URL/path used to load the WASM binary when not embedded.
26
+ * @param url Absolute/relative URL in browsers, or filesystem path in Node.
27
+ */
28
+ export function set_wasm_url(url) {
29
+ wasmUrlOverride = url;
30
+ }
23
31
  export async function loadC4WModule() {
24
32
  const module = (await import("./c4w.js"));
25
- return module.default(); // Emscripten initializes the module
33
+ const args = {};
34
+ if (wasmUrlOverride) {
35
+ args.locateFile = (path) => path.endsWith('.wasm') ? wasmUrlOverride : path;
36
+ }
37
+ else if (isBrowserEnvironment()) {
38
+ // Default browser-friendly resolution so bundlers copy the asset without extra config
39
+ args.locateFile = (path) => path.endsWith('.wasm') ? new URL('./c4w.wasm', import.meta.url).toString() : path;
40
+ }
41
+ return module.default(args); // Emscripten initializes the module
26
42
  }
27
43
  let module = null;
28
44
  let modulePromise = null;
@@ -134,3 +150,26 @@ export function copy_to_c(data, c4w, free_ptrs) {
134
150
  free_ptrs.push(ptr);
135
151
  return ptr;
136
152
  }
153
+ /**
154
+ * Returns the prover config state for a given chain as hex string with 0x prefix.
155
+ * @param chainId Chain identifier
156
+ * @return Hex string (e.g. "0x...") or "0x" if no state present
157
+ */
158
+ export async function get_prover_config_hex(chainId) {
159
+ const c4w = await getC4w();
160
+ if (!c4w.storage)
161
+ return '0x';
162
+ const state = c4w.storage.get('states_' + chainId);
163
+ return '0x' + (state ? Array.from(state).map(_ => _.toString(16).padStart(2, '0')).join('') : '');
164
+ }
165
+ /**
166
+ * Sets the trusted checkpoint inside the C context to initialize state.
167
+ * @param chainId Chain identifier
168
+ * @param checkpoint Trusted checkpoint root hex string
169
+ */
170
+ export async function set_trusted_checkpoint(chainId, checkpoint) {
171
+ const c4w = await getC4w();
172
+ const free_buffers = [];
173
+ c4w._c4w_create_verify_ctx(0, 0, 0, 0, BigInt(chainId), as_char_ptr(checkpoint, c4w, free_buffers));
174
+ free_buffers.forEach(ptr => c4w._free(ptr));
175
+ }