@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.
- package/c4w.js +1 -1
- package/connectionState.d.ts +23 -0
- package/connectionState.js +109 -0
- package/eventEmitter.d.ts +6 -0
- package/eventEmitter.js +28 -0
- package/index.d.ts +13 -44
- package/index.js +95 -32
- package/package.json +1 -1
- package/subscriptionManager.d.ts +35 -0
- package/subscriptionManager.js +289 -0
- package/types.d.ts +50 -0
- package/types.js +45 -0
- package/server.d.ts +0 -8
- package/server.js +0 -177
|
@@ -0,0 +1,289 @@
|
|
|
1
|
+
import { ProviderRpcError } from './types.js'; // Removed unused Config, DataRequest, RequestArguments
|
|
2
|
+
const DEFAULT_POLLING_INTERVAL = 12000; // 12 seconds
|
|
3
|
+
export class SubscriptionManager {
|
|
4
|
+
activeSubscriptions = new Map();
|
|
5
|
+
eventEmitter;
|
|
6
|
+
rpcCall;
|
|
7
|
+
config;
|
|
8
|
+
nextSubscriptionId = 0;
|
|
9
|
+
constructor(rpcCallCallback, eventEmitter, config = {}) {
|
|
10
|
+
this.rpcCall = rpcCallCallback;
|
|
11
|
+
this.eventEmitter = eventEmitter;
|
|
12
|
+
this.config = {
|
|
13
|
+
pollingInterval: DEFAULT_POLLING_INTERVAL,
|
|
14
|
+
...config,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
// New method to handle requests related to subscriptions and filters
|
|
18
|
+
handleRequest(method, paramsArray) {
|
|
19
|
+
switch (method) {
|
|
20
|
+
case 'eth_subscribe':
|
|
21
|
+
if (paramsArray.length < 1 || typeof paramsArray[0] !== 'string') {
|
|
22
|
+
return Promise.reject(new ProviderRpcError(-32602, 'Invalid params for eth_subscribe: missing subscription type.'));
|
|
23
|
+
}
|
|
24
|
+
return this.subscribe(paramsArray[0], paramsArray.slice(1));
|
|
25
|
+
case 'eth_unsubscribe':
|
|
26
|
+
if (paramsArray.length < 1 || typeof paramsArray[0] !== 'string') {
|
|
27
|
+
return Promise.reject(new ProviderRpcError(-32602, 'Invalid params for eth_unsubscribe: missing or invalid subscription ID.'));
|
|
28
|
+
}
|
|
29
|
+
return this.unsubscribe(paramsArray[0]);
|
|
30
|
+
case 'eth_newBlockFilter':
|
|
31
|
+
return this.newFilter('block');
|
|
32
|
+
case 'eth_newPendingTransactionFilter':
|
|
33
|
+
// This will internally throw, but we return the promise from newFilter
|
|
34
|
+
return this.newFilter('pendingTransaction');
|
|
35
|
+
case 'eth_newFilter': // For log filters
|
|
36
|
+
return this.newFilter('log', paramsArray);
|
|
37
|
+
case 'eth_getFilterChanges':
|
|
38
|
+
if (paramsArray.length < 1 || typeof paramsArray[0] !== 'string') {
|
|
39
|
+
return Promise.reject(new ProviderRpcError(-32602, 'Invalid params for eth_getFilterChanges: missing or invalid filter ID.'));
|
|
40
|
+
}
|
|
41
|
+
return this.getFilterChanges(paramsArray[0]);
|
|
42
|
+
case 'eth_uninstallFilter':
|
|
43
|
+
if (paramsArray.length < 1 || typeof paramsArray[0] !== 'string') {
|
|
44
|
+
return Promise.reject(new ProviderRpcError(-32602, 'Invalid params for eth_uninstallFilter: missing or invalid filter ID.'));
|
|
45
|
+
}
|
|
46
|
+
return this.uninstallFilter(paramsArray[0]);
|
|
47
|
+
default:
|
|
48
|
+
// Not a method handled by SubscriptionManager
|
|
49
|
+
return null;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
generateSubscriptionId() {
|
|
53
|
+
this.nextSubscriptionId++;
|
|
54
|
+
// Simple hex string, similar to what nodes return
|
|
55
|
+
return `0x${this.nextSubscriptionId.toString(16)}`;
|
|
56
|
+
}
|
|
57
|
+
// For eth_subscribe
|
|
58
|
+
async subscribe(kind, params = []) {
|
|
59
|
+
if (this.config.debug) {
|
|
60
|
+
console.log(`[SubMan] eth_subscribe: kind=${kind}, params=`, params);
|
|
61
|
+
}
|
|
62
|
+
const subId = this.generateSubscriptionId();
|
|
63
|
+
let subscriptionDetails = {
|
|
64
|
+
id: subId,
|
|
65
|
+
subscriptionMethod: 'eth_subscribe',
|
|
66
|
+
kind: kind,
|
|
67
|
+
};
|
|
68
|
+
switch (kind) {
|
|
69
|
+
case 'newHeads':
|
|
70
|
+
// No specific params needed from client for newHeads
|
|
71
|
+
break;
|
|
72
|
+
case 'logs':
|
|
73
|
+
subscriptionDetails.params = params[0] || {};
|
|
74
|
+
break;
|
|
75
|
+
default:
|
|
76
|
+
// Should not happen if kind is correctly typed
|
|
77
|
+
throw new ProviderRpcError(4200, `Subscription kind "${kind}" is not supported by eth_subscribe.`);
|
|
78
|
+
}
|
|
79
|
+
const finalSubscription = subscriptionDetails;
|
|
80
|
+
this.activeSubscriptions.set(subId, finalSubscription);
|
|
81
|
+
this.schedulePoll(finalSubscription, 0); // Start active polling
|
|
82
|
+
if (this.config.debug) {
|
|
83
|
+
console.log(`[SubMan] Created eth_subscribe subscription: ${subId}`, finalSubscription);
|
|
84
|
+
}
|
|
85
|
+
return subId;
|
|
86
|
+
}
|
|
87
|
+
// For eth_newFilter, eth_newBlockFilter, eth_newPendingTransactionFilter
|
|
88
|
+
async newFilter(kind, params = []) {
|
|
89
|
+
if (this.config.debug) {
|
|
90
|
+
console.log(`[SubMan] eth_newFilter: kind=${kind}, params=`, params);
|
|
91
|
+
}
|
|
92
|
+
if (kind === 'pendingTransaction') {
|
|
93
|
+
if (this.config.debug)
|
|
94
|
+
console.warn('[SubMan] eth_newPendingTransactionFilter is not supported.');
|
|
95
|
+
throw new ProviderRpcError(4200, 'eth_newPendingTransactionFilter is not supported.');
|
|
96
|
+
}
|
|
97
|
+
const filterId = this.generateSubscriptionId();
|
|
98
|
+
const filterDetails = {
|
|
99
|
+
id: filterId,
|
|
100
|
+
subscriptionMethod: 'eth_newFilter',
|
|
101
|
+
kind: kind,
|
|
102
|
+
params: kind === 'log' ? (params[0] || {}) : undefined,
|
|
103
|
+
// No timeoutId as polling is manual via getFilterChanges
|
|
104
|
+
};
|
|
105
|
+
this.activeSubscriptions.set(filterId, filterDetails);
|
|
106
|
+
if (this.config.debug) {
|
|
107
|
+
console.log(`[SubMan] Created eth_newFilter filter: ${filterId}`, filterDetails);
|
|
108
|
+
}
|
|
109
|
+
return filterId;
|
|
110
|
+
}
|
|
111
|
+
// For eth_getFilterChanges
|
|
112
|
+
async getFilterChanges(filterId) {
|
|
113
|
+
const sub = this.activeSubscriptions.get(filterId);
|
|
114
|
+
if (!sub || sub.subscriptionMethod !== 'eth_newFilter') {
|
|
115
|
+
if (this.config.debug)
|
|
116
|
+
console.warn(`[SubMan] getFilterChanges: Filter not found or not a filter type for ID ${filterId}`);
|
|
117
|
+
throw new ProviderRpcError(-32000, 'Filter not found.'); // Standard Ethereum error for missing filter
|
|
118
|
+
}
|
|
119
|
+
if (this.config.debug) {
|
|
120
|
+
console.log(`[SubMan] getFilterChanges for filter: ${filterId}, kind: ${sub.kind}`);
|
|
121
|
+
}
|
|
122
|
+
// Perform a one-time poll based on the filter kind
|
|
123
|
+
return this.executePollLogic(sub, true); // Pass true to indicate results should be returned, not emitted
|
|
124
|
+
}
|
|
125
|
+
// For eth_uninstallFilter
|
|
126
|
+
async uninstallFilter(filterId) {
|
|
127
|
+
const sub = this.activeSubscriptions.get(filterId);
|
|
128
|
+
if (sub && sub.subscriptionMethod === 'eth_newFilter') {
|
|
129
|
+
this.activeSubscriptions.delete(filterId);
|
|
130
|
+
// No timeout to clear for filters
|
|
131
|
+
if (this.config.debug) {
|
|
132
|
+
console.log(`[SubMan] Uninstalled filter: ${filterId}`);
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
if (this.config.debug) {
|
|
137
|
+
console.warn(`[SubMan] UninstallFilter failed: Filter not found or not a filter type for ID ${filterId}`);
|
|
138
|
+
}
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
// Common polling logic, can return results or emit them via eventEmitter
|
|
142
|
+
async executePollLogic(sub, returnResults = false) {
|
|
143
|
+
let results = [];
|
|
144
|
+
let currentVerifiableBlockNumber = null;
|
|
145
|
+
try {
|
|
146
|
+
const blockNumberHex = await this.rpcCall('eth_blockNumber', []);
|
|
147
|
+
if (blockNumberHex)
|
|
148
|
+
currentVerifiableBlockNumber = BigInt(blockNumberHex);
|
|
149
|
+
}
|
|
150
|
+
catch (e) {
|
|
151
|
+
if (this.config.debug)
|
|
152
|
+
console.warn('[SubMan] Could not fetch eth_blockNumber for polling context:', e);
|
|
153
|
+
}
|
|
154
|
+
if (sub.kind === 'newHeads' || sub.kind === 'block') { // block is for eth_newBlockFilter
|
|
155
|
+
if (currentVerifiableBlockNumber) {
|
|
156
|
+
let fromBlock = sub.lastPolledBlockNumber ? sub.lastPolledBlockNumber + 1n : currentVerifiableBlockNumber;
|
|
157
|
+
if (sub.kind === 'newHeads') { // For newHeads, we only care if the latest is newer
|
|
158
|
+
if (!sub.lastPolledBlockNumber || currentVerifiableBlockNumber <= sub.lastPolledBlockNumber) {
|
|
159
|
+
return []; // No new head yet for eth_subscribe('newHeads')
|
|
160
|
+
}
|
|
161
|
+
// If new, target currentVerifiableBlockNumber for eth_getBlockByNumber
|
|
162
|
+
fromBlock = currentVerifiableBlockNumber;
|
|
163
|
+
}
|
|
164
|
+
for (let blockNum = fromBlock; blockNum <= currentVerifiableBlockNumber; blockNum++) {
|
|
165
|
+
try {
|
|
166
|
+
const block = await this.rpcCall('eth_getBlockByNumber', ['0x' + blockNum.toString(16), sub.kind === 'newHeads' /* full block for newHeads */]);
|
|
167
|
+
if (block && this.activeSubscriptions.has(sub.id)) {
|
|
168
|
+
if (sub.kind === 'newHeads')
|
|
169
|
+
results.push(block);
|
|
170
|
+
else if (sub.kind === 'block')
|
|
171
|
+
results.push(block.hash); // eth_newBlockFilter returns block hashes
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
catch (e) {
|
|
175
|
+
if (this.config.debug)
|
|
176
|
+
console.error(`[SubMan] Error fetching block ${blockNum} for sub/filter ${sub.id}:`, e);
|
|
177
|
+
if (sub.kind === 'block')
|
|
178
|
+
break; // For block filters, stop if a block in sequence fails
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (results.length > 0 && sub.kind === 'newHeads' && !returnResults && this.activeSubscriptions.has(sub.id)) {
|
|
182
|
+
// For newHeads subscription, emit each block individually or as an array?
|
|
183
|
+
// EIP-1193 usually implies one result per message for newHeads
|
|
184
|
+
results.forEach(blockResult => {
|
|
185
|
+
this.eventEmitter.emit('message', {
|
|
186
|
+
type: 'eth_subscription',
|
|
187
|
+
data: { subscription: sub.id, result: blockResult }
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
if (this.config.debug)
|
|
191
|
+
console.log(`[SubMan] Emitted ${results.length} newHead(s) for sub ${sub.id}`);
|
|
192
|
+
}
|
|
193
|
+
sub.lastPolledBlockNumber = currentVerifiableBlockNumber;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
else if (sub.kind === 'logs' || sub.kind === 'log') { // 'log' is for eth_newFilter(logs)
|
|
197
|
+
const filterParams = { ...(sub.params || {}) };
|
|
198
|
+
if (sub.lastPolledBlockNumber) {
|
|
199
|
+
filterParams.fromBlock = '0x' + (sub.lastPolledBlockNumber + 1n).toString(16);
|
|
200
|
+
}
|
|
201
|
+
else if (!filterParams.fromBlock && currentVerifiableBlockNumber) {
|
|
202
|
+
filterParams.fromBlock = '0x' + currentVerifiableBlockNumber.toString(16);
|
|
203
|
+
}
|
|
204
|
+
if (currentVerifiableBlockNumber) {
|
|
205
|
+
filterParams.toBlock = '0x' + currentVerifiableBlockNumber.toString(16);
|
|
206
|
+
}
|
|
207
|
+
else if (!filterParams.toBlock) {
|
|
208
|
+
filterParams.toBlock = 'latest';
|
|
209
|
+
}
|
|
210
|
+
if (filterParams.fromBlock && filterParams.toBlock && filterParams.toBlock !== 'latest' && BigInt(filterParams.fromBlock) > BigInt(filterParams.toBlock)) {
|
|
211
|
+
// Skip if fromBlock is ahead
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
try {
|
|
215
|
+
const logs = await this.rpcCall('eth_getLogs', [filterParams]);
|
|
216
|
+
if (logs && logs.length > 0 && this.activeSubscriptions.has(sub.id)) {
|
|
217
|
+
results = logs;
|
|
218
|
+
if (!returnResults) { // For eth_subscribe('logs')
|
|
219
|
+
this.eventEmitter.emit('message', {
|
|
220
|
+
type: 'eth_subscription',
|
|
221
|
+
data: { subscription: sub.id, result: logs } // Usually an array of logs
|
|
222
|
+
});
|
|
223
|
+
if (this.config.debug)
|
|
224
|
+
console.log(`[SubMan] Emitted ${logs.length} logs for sub ${sub.id}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
if (filterParams.toBlock !== 'latest')
|
|
228
|
+
sub.lastPolledBlockNumber = BigInt(filterParams.toBlock);
|
|
229
|
+
else if (currentVerifiableBlockNumber)
|
|
230
|
+
sub.lastPolledBlockNumber = currentVerifiableBlockNumber;
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
if (this.config.debug)
|
|
234
|
+
console.error(`[SubMan] Error fetching logs for sub/filter ${sub.id}:`, e);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return results;
|
|
239
|
+
}
|
|
240
|
+
schedulePoll(sub, delay) {
|
|
241
|
+
if (!this.activeSubscriptions.has(sub.id) || sub.subscriptionMethod === 'eth_newFilter')
|
|
242
|
+
return;
|
|
243
|
+
sub.timeoutId = setTimeout(async () => {
|
|
244
|
+
try {
|
|
245
|
+
if (!this.activeSubscriptions.has(sub.id))
|
|
246
|
+
return;
|
|
247
|
+
await this.pollSubscriptionAndEmit(sub); // Renamed from pollSubscription for clarity
|
|
248
|
+
}
|
|
249
|
+
catch (error) {
|
|
250
|
+
console.error(`[SubMan] Unexpected error in poll scheduling for subscription ${sub.id}:`, error);
|
|
251
|
+
}
|
|
252
|
+
}, delay);
|
|
253
|
+
}
|
|
254
|
+
// Wrapper for eth_subscribe polling that emits results and reschedules
|
|
255
|
+
async pollSubscriptionAndEmit(sub) {
|
|
256
|
+
if (!this.activeSubscriptions.has(sub.id)) {
|
|
257
|
+
if (this.config.debug)
|
|
258
|
+
console.log(`[SubMan] Subscription ${sub.id} was unsubscribed. Skipping poll.`);
|
|
259
|
+
if (sub.timeoutId)
|
|
260
|
+
clearTimeout(sub.timeoutId);
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
if (this.config.debug) {
|
|
264
|
+
console.log(`[SubMan] Polling for eth_subscribe: ${sub.id}, kind: ${sub.kind}`);
|
|
265
|
+
}
|
|
266
|
+
await this.executePollLogic(sub, false); // false means emit results, don't return them
|
|
267
|
+
if (this.activeSubscriptions.has(sub.id)) {
|
|
268
|
+
this.schedulePoll(sub, this.config.pollingInterval || DEFAULT_POLLING_INTERVAL);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
// For eth_unsubscribe (specific to eth_subscribe subscriptions)
|
|
272
|
+
async unsubscribe(subscriptionId) {
|
|
273
|
+
const sub = this.activeSubscriptions.get(subscriptionId);
|
|
274
|
+
if (sub && sub.subscriptionMethod === 'eth_subscribe') {
|
|
275
|
+
if (sub.timeoutId) {
|
|
276
|
+
clearTimeout(sub.timeoutId);
|
|
277
|
+
}
|
|
278
|
+
this.activeSubscriptions.delete(subscriptionId);
|
|
279
|
+
if (this.config.debug) {
|
|
280
|
+
console.log(`[SubMan] Unsubscribed (eth_subscribe): ${subscriptionId}`);
|
|
281
|
+
}
|
|
282
|
+
return true;
|
|
283
|
+
}
|
|
284
|
+
if (this.config.debug) {
|
|
285
|
+
console.warn(`[SubMan] Unsubscribe failed: Subscription not found or not an eth_subscribe type for ID ${subscriptionId}`);
|
|
286
|
+
}
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
package/types.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export declare class ProviderRpcError extends Error {
|
|
2
|
+
code: number;
|
|
3
|
+
data?: unknown;
|
|
4
|
+
constructor(code: number, message: string, data?: unknown);
|
|
5
|
+
static createError(error: any): ProviderRpcError;
|
|
6
|
+
}
|
|
7
|
+
export interface RequestArguments {
|
|
8
|
+
readonly method: string;
|
|
9
|
+
readonly params?: readonly unknown[] | object;
|
|
10
|
+
}
|
|
11
|
+
export interface ProviderConnectInfo {
|
|
12
|
+
readonly chainId: string;
|
|
13
|
+
}
|
|
14
|
+
export interface ProviderMessage {
|
|
15
|
+
readonly type: string;
|
|
16
|
+
readonly data: unknown;
|
|
17
|
+
}
|
|
18
|
+
export interface Cache {
|
|
19
|
+
cacheable(req: DataRequest): boolean;
|
|
20
|
+
get(req: DataRequest): Uint8Array | undefined;
|
|
21
|
+
set(req: DataRequest, data: Uint8Array): void;
|
|
22
|
+
}
|
|
23
|
+
export interface Config {
|
|
24
|
+
chainId: number;
|
|
25
|
+
beacon_apis: string[];
|
|
26
|
+
rpcs: string[];
|
|
27
|
+
proofer?: string[];
|
|
28
|
+
trusted_block_hashes: string[];
|
|
29
|
+
cache?: Cache;
|
|
30
|
+
debug?: boolean;
|
|
31
|
+
include_code?: boolean;
|
|
32
|
+
verify?: (method: string, args: any[]) => boolean;
|
|
33
|
+
pollingInterval?: number;
|
|
34
|
+
}
|
|
35
|
+
export interface DataRequest {
|
|
36
|
+
method: string;
|
|
37
|
+
chain_id: number;
|
|
38
|
+
encoding: string;
|
|
39
|
+
type: string;
|
|
40
|
+
exclude_mask: number;
|
|
41
|
+
url: string;
|
|
42
|
+
payload: any;
|
|
43
|
+
req_ptr: number;
|
|
44
|
+
}
|
|
45
|
+
export declare enum MethodType {
|
|
46
|
+
PROOFABLE = 1,
|
|
47
|
+
UNPROOFABLE = 2,
|
|
48
|
+
NOT_SUPPORTED = 3,
|
|
49
|
+
LOCAL = 4
|
|
50
|
+
}
|
package/types.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// --- EIP-1193 & C4Client Specific Types ---
|
|
2
|
+
// From EIP-1193: Error structure
|
|
3
|
+
export class ProviderRpcError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
data;
|
|
6
|
+
constructor(code, message, data) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'ProviderRpcError';
|
|
9
|
+
this.code = code;
|
|
10
|
+
if (data !== undefined) {
|
|
11
|
+
this.data = data;
|
|
12
|
+
}
|
|
13
|
+
Object.setPrototypeOf(this, ProviderRpcError.prototype);
|
|
14
|
+
}
|
|
15
|
+
static createError(error) {
|
|
16
|
+
let providerError;
|
|
17
|
+
if (error instanceof ProviderRpcError) {
|
|
18
|
+
providerError = error;
|
|
19
|
+
}
|
|
20
|
+
else {
|
|
21
|
+
let code;
|
|
22
|
+
let message = error?.message || 'An unknown error occurred';
|
|
23
|
+
const originalData = error?.data;
|
|
24
|
+
if (typeof message === 'string' && message.startsWith('Method ') && message.endsWith(' is not supported')) {
|
|
25
|
+
code = 4200;
|
|
26
|
+
}
|
|
27
|
+
else if (typeof message === 'string' && (message.includes('HTTP error!') || message.includes('All nodes failed') || message.includes('Failed to fetch'))) {
|
|
28
|
+
code = 4900;
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
code = (typeof error?.code === 'number') ? error.code : -32603;
|
|
32
|
+
}
|
|
33
|
+
providerError = new ProviderRpcError(code, message, originalData);
|
|
34
|
+
}
|
|
35
|
+
return providerError;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
// Enum for RPC method types
|
|
39
|
+
export var MethodType;
|
|
40
|
+
(function (MethodType) {
|
|
41
|
+
MethodType[MethodType["PROOFABLE"] = 1] = "PROOFABLE";
|
|
42
|
+
MethodType[MethodType["UNPROOFABLE"] = 2] = "UNPROOFABLE";
|
|
43
|
+
MethodType[MethodType["NOT_SUPPORTED"] = 3] = "NOT_SUPPORTED";
|
|
44
|
+
MethodType[MethodType["LOCAL"] = 4] = "LOCAL";
|
|
45
|
+
})(MethodType || (MethodType = {}));
|
package/server.d.ts
DELETED
package/server.js
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
import Client from "./index.js";
|
|
2
|
-
import * as http from 'http';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
// ensure cache_dir
|
|
5
|
-
let cache_memory_limit = parseInt(process.env.CACHE_RAM_LIMIT || '100'); // in MB
|
|
6
|
-
let cache_dir = process.env.CACHE_DIR || '.cache';
|
|
7
|
-
if (!fs.existsSync(cache_dir))
|
|
8
|
-
fs.mkdirSync(cache_dir, { recursive: true });
|
|
9
|
-
function cache_key(req) {
|
|
10
|
-
if (req.type == 'eth_rpc' && req.payload)
|
|
11
|
-
return `rpc_${req.payload?.method}_${req.payload?.params?.join('_')}`;
|
|
12
|
-
return req.url;
|
|
13
|
-
}
|
|
14
|
-
const cached_methods = ["eth_getBlockByNumber", "eth_getBlockByHash", "eth_getBlockReceipts"];
|
|
15
|
-
const memory_cache = new Map();
|
|
16
|
-
function check_limit() {
|
|
17
|
-
let total_size = 0;
|
|
18
|
-
for (const [k, v] of memory_cache.entries()) {
|
|
19
|
-
total_size += v.data.length;
|
|
20
|
-
}
|
|
21
|
-
while (total_size > cache_memory_limit * 1024 * 1024) {
|
|
22
|
-
// find the oldest entry
|
|
23
|
-
let oldest_k = null;
|
|
24
|
-
let oldest_v = null;
|
|
25
|
-
for (const [k, v] of memory_cache.entries()) {
|
|
26
|
-
if (oldest_v == null || v.timestamp < oldest_v.timestamp) {
|
|
27
|
-
oldest_k = k;
|
|
28
|
-
oldest_v = v;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
if (oldest_k)
|
|
32
|
-
memory_cache.delete(oldest_k);
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
function is_timeout(key, entry) {
|
|
36
|
-
if (key.endsWith('latest') || key.endsWith('head')) {
|
|
37
|
-
if (!entry)
|
|
38
|
-
return true;
|
|
39
|
-
return entry.timestamp < Date.now() - 1000 * 6;
|
|
40
|
-
}
|
|
41
|
-
return false;
|
|
42
|
-
}
|
|
43
|
-
const cache = {
|
|
44
|
-
cacheable: (req) => !!(req.encoding == "ssz" || (req.type == 'eth_rpc' && req.payload && cached_methods.includes(req.payload?.method))),
|
|
45
|
-
get: (req) => {
|
|
46
|
-
const key = cache_key(req);
|
|
47
|
-
let entry = memory_cache.get(key);
|
|
48
|
-
if (is_timeout(key, entry))
|
|
49
|
-
entry = undefined;
|
|
50
|
-
if (!entry && !is_timeout(key)) {
|
|
51
|
-
const file_path = cache_dir + '/' + key;
|
|
52
|
-
if (fs.existsSync(file_path)) {
|
|
53
|
-
let data = fs.readFileSync(file_path);
|
|
54
|
-
memory_cache.set(key, { data, timestamp: fs.statSync(file_path).mtime.getTime() });
|
|
55
|
-
setTimeout(check_limit, 0);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
return entry?.data;
|
|
59
|
-
},
|
|
60
|
-
set: (req, data) => {
|
|
61
|
-
const key = cache_key(req);
|
|
62
|
-
memory_cache.set(key, { data, timestamp: Date.now() });
|
|
63
|
-
if (!is_timeout(key))
|
|
64
|
-
fs.writeFile(cache_dir + '/' + key, Buffer.from(data), () => { });
|
|
65
|
-
check_limit();
|
|
66
|
-
}
|
|
67
|
-
};
|
|
68
|
-
export default class Server {
|
|
69
|
-
client;
|
|
70
|
-
constructor(conf) {
|
|
71
|
-
conf = conf || {};
|
|
72
|
-
if (process.env.CHAIN_ID)
|
|
73
|
-
conf.chainId = parseInt(process.env.CHAIN_ID);
|
|
74
|
-
if (process.env.BEACON_API)
|
|
75
|
-
conf.beacon_apis = process.env.BEACON_API.split(",");
|
|
76
|
-
if (process.env.RPC)
|
|
77
|
-
conf.rpcs = process.env.RPC.split(",");
|
|
78
|
-
conf.cache = cache;
|
|
79
|
-
conf.debug = true;
|
|
80
|
-
this.client = new Client(conf);
|
|
81
|
-
}
|
|
82
|
-
toHexString(bytes) {
|
|
83
|
-
return '0x' + Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('');
|
|
84
|
-
}
|
|
85
|
-
async handleRequest(req, res) {
|
|
86
|
-
// Only accept POST requests
|
|
87
|
-
if (req.method !== 'POST') {
|
|
88
|
-
const error = 'Method not allowed';
|
|
89
|
-
console.error(`[${new Date().toISOString()}] Error: ${error}`);
|
|
90
|
-
res.writeHead(405, { 'Content-Type': 'application/json' });
|
|
91
|
-
res.end(JSON.stringify({ error }));
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
// Read request body
|
|
95
|
-
let body = '';
|
|
96
|
-
req.on('data', (chunk) => {
|
|
97
|
-
body += chunk.toString();
|
|
98
|
-
});
|
|
99
|
-
req.on('end', async () => {
|
|
100
|
-
try {
|
|
101
|
-
const jsonRpc = JSON.parse(body);
|
|
102
|
-
if (!jsonRpc.method || !jsonRpc.params || !Array.isArray(jsonRpc.params)) {
|
|
103
|
-
throw new Error('Invalid JSON-RPC request');
|
|
104
|
-
}
|
|
105
|
-
let start = Date.now();
|
|
106
|
-
console.log(` ==> ${jsonRpc.method} (${jsonRpc.params.join(',')}) ...`);
|
|
107
|
-
// Create proof using the client
|
|
108
|
-
const proof = await this.client.createProof(jsonRpc.method, jsonRpc.params);
|
|
109
|
-
let end = Date.now();
|
|
110
|
-
console.log(` <== ${jsonRpc.method} (${jsonRpc.params.join(',')}) ... ${end - start}ms (${proof.length} bytes)`);
|
|
111
|
-
// Check Accept header to determine response format
|
|
112
|
-
const acceptHeader = req.headers.accept;
|
|
113
|
-
if (acceptHeader === 'application/octet-stream') {
|
|
114
|
-
res.writeHead(200, {
|
|
115
|
-
'Content-Type': 'application/octet-stream',
|
|
116
|
-
'Content-Length': proof.length
|
|
117
|
-
});
|
|
118
|
-
res.end(Buffer.from(proof));
|
|
119
|
-
}
|
|
120
|
-
else {
|
|
121
|
-
const response = {
|
|
122
|
-
jsonrpc: '2.0',
|
|
123
|
-
id: jsonRpc.id,
|
|
124
|
-
result: this.toHexString(proof)
|
|
125
|
-
};
|
|
126
|
-
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
127
|
-
res.end(JSON.stringify(response));
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
catch (error) {
|
|
131
|
-
const errorMessage = error instanceof Error ? error.message : 'Internal error';
|
|
132
|
-
console.error(`[${new Date().toISOString()}] Error processing request:`, errorMessage);
|
|
133
|
-
if (error instanceof Error && error.stack) {
|
|
134
|
-
console.error(`Stack trace:`, error.stack);
|
|
135
|
-
}
|
|
136
|
-
res.writeHead(400, { 'Content-Type': 'application/json' });
|
|
137
|
-
const errorResponse = {
|
|
138
|
-
jsonrpc: '2.0',
|
|
139
|
-
id: JSON.parse(body)?.id,
|
|
140
|
-
error: {
|
|
141
|
-
code: -32603,
|
|
142
|
-
message: errorMessage
|
|
143
|
-
}
|
|
144
|
-
};
|
|
145
|
-
res.end(JSON.stringify(errorResponse));
|
|
146
|
-
console.log(`[${new Date().toISOString()}] Sent error response:`, errorResponse);
|
|
147
|
-
}
|
|
148
|
-
});
|
|
149
|
-
req.on('error', (error) => {
|
|
150
|
-
console.error(`[${new Date().toISOString()}] Request error:`, error);
|
|
151
|
-
res.writeHead(500, { 'Content-Type': 'application/json' });
|
|
152
|
-
res.end(JSON.stringify({
|
|
153
|
-
jsonrpc: '2.0',
|
|
154
|
-
error: {
|
|
155
|
-
code: -32603,
|
|
156
|
-
message: 'Internal server error'
|
|
157
|
-
}
|
|
158
|
-
}));
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
async start(port = 8545) {
|
|
162
|
-
const server = http.createServer((req, res) => this.handleRequest(req, res));
|
|
163
|
-
return new Promise((resolve, reject) => {
|
|
164
|
-
server.listen(port, () => {
|
|
165
|
-
console.log(`[${new Date().toISOString()}] Server running at http://localhost:${port}/`);
|
|
166
|
-
resolve();
|
|
167
|
-
});
|
|
168
|
-
server.on('error', (error) => {
|
|
169
|
-
console.error(`[${new Date().toISOString()}] Server error:`, error);
|
|
170
|
-
reject(error);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
// Start the server directly
|
|
176
|
-
const server = new Server({});
|
|
177
|
-
server.start(parseInt(process.env.PORT || '8545'));
|