@effectstream/runtime 0.100.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.
- package/package.json +34 -0
- package/src/api/http-server.ts +942 -0
- package/src/api/pagination.ts +456 -0
- package/src/api/rpc-evm/eip1193.ts +920 -0
- package/src/api/rpc-evm/rpc-payloads.ts +297 -0
- package/src/api/rpc-evm/rpc-types.ts +87 -0
- package/src/api/rpc-evm/rpc-utils.ts +50 -0
- package/src/config-snapshot.ts +192 -0
- package/src/main.ts +317 -0
- package/src/mod.ts +3 -0
- package/src/process-blocks.ts +302 -0
- package/src/telemetry.ts +37 -0
- package/src/types.ts +76 -0
- package/src/version-migrations.ts +163 -0
|
@@ -0,0 +1,920 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createAsyncMiddleware,
|
|
3
|
+
JsonRpcEngine,
|
|
4
|
+
} from "@metamask/json-rpc-engine";
|
|
5
|
+
import {
|
|
6
|
+
createInvalidParamsError,
|
|
7
|
+
createMethodNotFoundRpcError,
|
|
8
|
+
createMethodNotSupportedRpcError,
|
|
9
|
+
type EvmRpcReturn,
|
|
10
|
+
type PaimaEvmRpcSchema,
|
|
11
|
+
} from "./rpc-types.ts";
|
|
12
|
+
import {
|
|
13
|
+
// getBlockByHash,
|
|
14
|
+
getLatestProcessedBlockHeight,
|
|
15
|
+
} from "@effectstream/db";
|
|
16
|
+
import type { Pool } from "pg";
|
|
17
|
+
import {
|
|
18
|
+
type EIP1193Parameters,
|
|
19
|
+
isAddress,
|
|
20
|
+
type RpcBlock,
|
|
21
|
+
type RpcError,
|
|
22
|
+
type RpcTransaction,
|
|
23
|
+
toHex,
|
|
24
|
+
} from "viem";
|
|
25
|
+
import { Buffer } from "node:buffer";
|
|
26
|
+
import sha3 from "js-sha3";
|
|
27
|
+
import {
|
|
28
|
+
toRpcBlock,
|
|
29
|
+
toRpcLog,
|
|
30
|
+
toRpcTransaction,
|
|
31
|
+
toRpcTransactionReceipt,
|
|
32
|
+
} from "./rpc-payloads.ts";
|
|
33
|
+
import {
|
|
34
|
+
isValidBlockHash,
|
|
35
|
+
isValidHexadecimal,
|
|
36
|
+
isValidTxHash,
|
|
37
|
+
strip0x,
|
|
38
|
+
} from "./rpc-utils.ts";
|
|
39
|
+
const { keccak_256 } = sha3;
|
|
40
|
+
import { runPreparedQuery } from "@effectstream/db";
|
|
41
|
+
import { ENV } from "@effectstream/utils/node-env";
|
|
42
|
+
|
|
43
|
+
export function evmRpcEngine(dbConn: Pool): JsonRpcEngine {
|
|
44
|
+
const engine = new JsonRpcEngine();
|
|
45
|
+
// TODO: add cache middleware
|
|
46
|
+
// registerCacheMiddleware(evmRpcEngine);
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* ===============================
|
|
50
|
+
* JSON-RPC Middleware definitions
|
|
51
|
+
* ===============================
|
|
52
|
+
*/
|
|
53
|
+
|
|
54
|
+
// TODO: Most of these methods are not implemented, or partially implemented.
|
|
55
|
+
// Check effectstream-v1 eip1193.ts for the full implementation details.
|
|
56
|
+
engine.push(
|
|
57
|
+
createAsyncMiddleware(async (req, res) => {
|
|
58
|
+
const evmRpc: typeof req & EIP1193Parameters<PaimaEvmRpcSchema> =
|
|
59
|
+
req as any;
|
|
60
|
+
|
|
61
|
+
const setResult = <Method extends string>(
|
|
62
|
+
result: EvmRpcReturn<Method>,
|
|
63
|
+
): void => {
|
|
64
|
+
res.result = result as any;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// TODO: missing these validity checks:
|
|
68
|
+
// - InvalidInputRpcError / InvalidParamsRpcError
|
|
69
|
+
switch (evmRpc.method) {
|
|
70
|
+
/*
|
|
71
|
+
* ==============
|
|
72
|
+
* Gossip methods
|
|
73
|
+
* ==============
|
|
74
|
+
*/
|
|
75
|
+
case "eth_blockNumber": {
|
|
76
|
+
const [lastBlockHeight] = await runPreparedQuery(
|
|
77
|
+
getLatestProcessedBlockHeight.run(
|
|
78
|
+
undefined,
|
|
79
|
+
dbConn,
|
|
80
|
+
),
|
|
81
|
+
"eth_blockNumber",
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
setResult<typeof evmRpc.method>(
|
|
85
|
+
`0x${lastBlockHeight.block_height.toString(16)}`,
|
|
86
|
+
);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
case "eth_sendRawTransaction": {
|
|
90
|
+
// TODO: we can probably support this
|
|
91
|
+
// but supporting this conversion would have to be a Effectstream-level feature and not a EVM-RPC feature
|
|
92
|
+
res.error = createMethodNotSupportedRpcError(
|
|
93
|
+
evmRpc.method,
|
|
94
|
+
`${evmRpc.method} currently unsupported`,
|
|
95
|
+
);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* =============
|
|
101
|
+
* State methods
|
|
102
|
+
* =============
|
|
103
|
+
*/
|
|
104
|
+
|
|
105
|
+
case "eth_getBalance": {
|
|
106
|
+
setResult<typeof evmRpc.method>(
|
|
107
|
+
`${toHex(BigInt(0))}`,
|
|
108
|
+
);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
case "eth_getStorageAt": {
|
|
113
|
+
// not possible in Paima Engine at the moment, but maybe in the future related to precompiles
|
|
114
|
+
res.error = createMethodNotSupportedRpcError(
|
|
115
|
+
evmRpc.method,
|
|
116
|
+
`${evmRpc.method} currently unsupported`,
|
|
117
|
+
);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
case "eth_getTransactionCount": {
|
|
121
|
+
const address = evmRpc.params[0];
|
|
122
|
+
if (!isAddress(address)) {
|
|
123
|
+
res.error = createInvalidParamsError(
|
|
124
|
+
{ address },
|
|
125
|
+
`Address is not a valid Ethereum address`,
|
|
126
|
+
);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let blockHeight: number;
|
|
130
|
+
// TODO
|
|
131
|
+
// const blockTag = simplifyBlockTag(evmRpc.params[1] ?? "finalized");
|
|
132
|
+
|
|
133
|
+
// if (blockParam == "earliest") return FIRST_BLOCK_HEIGHT;
|
|
134
|
+
// if (blockParam == "latest") {
|
|
135
|
+
// const [lastBlockHeight] = await getLatestProcessedBlockHeight.run(
|
|
136
|
+
// undefined,
|
|
137
|
+
// dbConn,
|
|
138
|
+
// );
|
|
139
|
+
// blockHeight = lastBlockHeight.block_height;
|
|
140
|
+
// }
|
|
141
|
+
|
|
142
|
+
// try {
|
|
143
|
+
// blockHeight = await toBlockNumber(evmRpc.params[1] ?? "finalized");
|
|
144
|
+
// } catch (err) {
|
|
145
|
+
// res.error = err as RpcError;
|
|
146
|
+
// return;
|
|
147
|
+
// }
|
|
148
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
149
|
+
// "/transaction_count/address",
|
|
150
|
+
// {
|
|
151
|
+
// params: { query: { blockHeight, address } },
|
|
152
|
+
// },
|
|
153
|
+
// );
|
|
154
|
+
// if (error != null) {
|
|
155
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
156
|
+
// return;
|
|
157
|
+
// }
|
|
158
|
+
// if (data.success === false) {
|
|
159
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
160
|
+
// return;
|
|
161
|
+
// }
|
|
162
|
+
setResult<typeof evmRpc.method>(
|
|
163
|
+
`0x${
|
|
164
|
+
Number(0)
|
|
165
|
+
// (data.result.scheduledData + data.result.submittedInputs)
|
|
166
|
+
.toString(
|
|
167
|
+
16,
|
|
168
|
+
)
|
|
169
|
+
}`,
|
|
170
|
+
);
|
|
171
|
+
return;
|
|
172
|
+
}
|
|
173
|
+
case "eth_getCode": {
|
|
174
|
+
// not possible in Paima Engine at the moment, but maybe in the future related to precompiles
|
|
175
|
+
res.error = createMethodNotSupportedRpcError(
|
|
176
|
+
evmRpc.method,
|
|
177
|
+
`${evmRpc.method} currently unsupported`,
|
|
178
|
+
);
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
case "eth_call": {
|
|
182
|
+
// not possible in Paima Engine at the moment, but maybe in the future related to precompiles
|
|
183
|
+
res.error = createMethodNotSupportedRpcError(
|
|
184
|
+
evmRpc.method,
|
|
185
|
+
`${evmRpc.method} currently unsupported`,
|
|
186
|
+
);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
case "eth_estimateGas": {
|
|
190
|
+
// no concept of gas in Paima (gas is from the underlying chain)
|
|
191
|
+
setResult<typeof evmRpc.method>(`0x0`);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* ===============
|
|
197
|
+
* History methods
|
|
198
|
+
* ===============
|
|
199
|
+
*/
|
|
200
|
+
|
|
201
|
+
case "eth_getBlockTransactionCountByHash": {
|
|
202
|
+
const blockHash = evmRpc.params[0];
|
|
203
|
+
if (typeof blockHash !== "string" || !isValidBlockHash(blockHash)) {
|
|
204
|
+
res.error = createInvalidParamsError(
|
|
205
|
+
{ blockHash },
|
|
206
|
+
`Needs to be a valid block hash (0x...)`,
|
|
207
|
+
);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
211
|
+
// "/transaction_count/blockHash",
|
|
212
|
+
// {
|
|
213
|
+
// params: { query: { blockHash: blockHash } },
|
|
214
|
+
// },
|
|
215
|
+
// );
|
|
216
|
+
// if (error != null) {
|
|
217
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
218
|
+
// return;
|
|
219
|
+
// }
|
|
220
|
+
// if (data.success === false) {
|
|
221
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
222
|
+
// return;
|
|
223
|
+
// }
|
|
224
|
+
setResult<typeof evmRpc.method>(
|
|
225
|
+
`0x${
|
|
226
|
+
Number(0)
|
|
227
|
+
// (data.result.scheduledData + data.result.submittedInputs)
|
|
228
|
+
.toString(
|
|
229
|
+
16,
|
|
230
|
+
)
|
|
231
|
+
}`,
|
|
232
|
+
);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
case "eth_getBlockTransactionCountByNumber": {
|
|
236
|
+
let blockHeight: number;
|
|
237
|
+
// try {
|
|
238
|
+
// blockHeight = await toBlockNumber(evmRpc.params[0] ?? "finalized");
|
|
239
|
+
// } catch (err) {
|
|
240
|
+
// res.error = err as RpcError;
|
|
241
|
+
// return;
|
|
242
|
+
// }
|
|
243
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
244
|
+
// "/transaction_count/blockHeight",
|
|
245
|
+
// {
|
|
246
|
+
// params: { query: { blockHeight } },
|
|
247
|
+
// },
|
|
248
|
+
// );
|
|
249
|
+
// if (error != null) {
|
|
250
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
251
|
+
// return;
|
|
252
|
+
// }
|
|
253
|
+
// if (data.success === false) {
|
|
254
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
255
|
+
// return;
|
|
256
|
+
// }
|
|
257
|
+
setResult<typeof evmRpc.method>(
|
|
258
|
+
`0x${
|
|
259
|
+
Number(0)
|
|
260
|
+
// (data.result.scheduledData + data.result.submittedInputs)
|
|
261
|
+
.toString(
|
|
262
|
+
16,
|
|
263
|
+
)
|
|
264
|
+
}`,
|
|
265
|
+
);
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
case "eth_getUncleCountByBlockHash": {
|
|
269
|
+
setResult<typeof evmRpc.method>(`0x0`); // no uncles in Paima
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
case "eth_getUncleCountByBlockNumber": {
|
|
273
|
+
setResult<typeof evmRpc.method>(`0x0`); // no uncles in Paima
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
case "eth_getBlockByHash": {
|
|
277
|
+
const blockHash = evmRpc.params[0];
|
|
278
|
+
if (typeof blockHash !== "string" || !isValidBlockHash(blockHash)) {
|
|
279
|
+
res.error = createInvalidParamsError(
|
|
280
|
+
{ blockHash },
|
|
281
|
+
`Needs to be a valid block hash (0x...)`,
|
|
282
|
+
);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const transactionDetails = evmRpc.params[1];
|
|
287
|
+
if (typeof transactionDetails !== "boolean") {
|
|
288
|
+
res.error = createInvalidParamsError(
|
|
289
|
+
{ transactionDetails },
|
|
290
|
+
`Transaction details must be specified as a boolean`,
|
|
291
|
+
);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const [block] = await runPreparedQuery(
|
|
296
|
+
getLatestProcessedBlockHeight.run(
|
|
297
|
+
undefined,
|
|
298
|
+
// { block_hash: Buffer.from(blockHash.slice(2), "hex") },
|
|
299
|
+
dbConn,
|
|
300
|
+
),
|
|
301
|
+
"eth_getBlockByHash",
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
if (!block) {
|
|
305
|
+
res.error = createInvalidParamsError(
|
|
306
|
+
{ blockHash },
|
|
307
|
+
`Block not found`,
|
|
308
|
+
);
|
|
309
|
+
return;
|
|
310
|
+
}
|
|
311
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
312
|
+
// "/block_content/blockHash",
|
|
313
|
+
// {
|
|
314
|
+
// params: {
|
|
315
|
+
// query: {
|
|
316
|
+
// blockHash,
|
|
317
|
+
// txDetails: transactionDetails ? "full" : "hash",
|
|
318
|
+
// },
|
|
319
|
+
// },
|
|
320
|
+
// },
|
|
321
|
+
// );
|
|
322
|
+
// if (error != null) {
|
|
323
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
324
|
+
// return;
|
|
325
|
+
// }
|
|
326
|
+
// if (data.success === false) {
|
|
327
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
328
|
+
// return;
|
|
329
|
+
// }
|
|
330
|
+
const randomBlockHash = (): string =>
|
|
331
|
+
"0x" +
|
|
332
|
+
Array(64).fill(0).map(() =>
|
|
333
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
334
|
+
).join("");
|
|
335
|
+
const mockInput = {
|
|
336
|
+
blockHeight: 0, // block.block_height,
|
|
337
|
+
blockHash: randomBlockHash(),
|
|
338
|
+
prevBlockHash: randomBlockHash(),
|
|
339
|
+
msTimestamp: Date.now(),
|
|
340
|
+
txs: [],
|
|
341
|
+
};
|
|
342
|
+
const mock: RpcBlock = toRpcBlock(mockInput, transactionDetails);
|
|
343
|
+
setResult<typeof evmRpc.method>(mock);
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
case "eth_getBlockByNumber": {
|
|
347
|
+
let blockHeight: number;
|
|
348
|
+
let block;
|
|
349
|
+
try {
|
|
350
|
+
// TODO this should be dynamic
|
|
351
|
+
// TOOD for now just passing the block
|
|
352
|
+
const [block_] = await runPreparedQuery(
|
|
353
|
+
getLatestProcessedBlockHeight.run(
|
|
354
|
+
undefined,
|
|
355
|
+
dbConn,
|
|
356
|
+
),
|
|
357
|
+
"eth_getBlockByNumber",
|
|
358
|
+
);
|
|
359
|
+
block = block_;
|
|
360
|
+
blockHeight = block.block_height;
|
|
361
|
+
// blockHeight = await toBlockNumber(evmRpc.params[0] ?? "finalized");
|
|
362
|
+
} catch (err) {
|
|
363
|
+
res.error = err as RpcError;
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const transactionDetails = evmRpc.params[1];
|
|
368
|
+
if (typeof transactionDetails !== "boolean") {
|
|
369
|
+
res.error = createInvalidParamsError(
|
|
370
|
+
{ transactionDetails },
|
|
371
|
+
`Transaction details must be specified as a boolean`,
|
|
372
|
+
);
|
|
373
|
+
return;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
377
|
+
// "/block_content/blockHeight",
|
|
378
|
+
// {
|
|
379
|
+
// params: {
|
|
380
|
+
// query: {
|
|
381
|
+
// blockHeight,
|
|
382
|
+
// txDetails: transactionDetails ? "full" : "hash",
|
|
383
|
+
// },
|
|
384
|
+
// },
|
|
385
|
+
// },
|
|
386
|
+
// );
|
|
387
|
+
// if (error != null) {
|
|
388
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
389
|
+
// return;
|
|
390
|
+
// }
|
|
391
|
+
// if (data.success === false) {
|
|
392
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
393
|
+
// return;
|
|
394
|
+
// }
|
|
395
|
+
|
|
396
|
+
// example: 0x1e888a0eadd622fe55b53692139e64f23852b247060fcb31322c3a76b265e760
|
|
397
|
+
const randomBlockHash = (): string =>
|
|
398
|
+
"0x" +
|
|
399
|
+
Array(64).fill(0).map(() =>
|
|
400
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
401
|
+
).join("");
|
|
402
|
+
const mockInput = {
|
|
403
|
+
blockHeight: block.block_height,
|
|
404
|
+
blockHash: randomBlockHash(),
|
|
405
|
+
prevBlockHash: randomBlockHash(),
|
|
406
|
+
msTimestamp: Date.now(),
|
|
407
|
+
txs: [],
|
|
408
|
+
};
|
|
409
|
+
const mock: RpcBlock = toRpcBlock(mockInput, transactionDetails);
|
|
410
|
+
setResult<typeof evmRpc.method>(mock);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
case "eth_getTransactionByHash": {
|
|
414
|
+
const txHash = evmRpc.params[0];
|
|
415
|
+
if (!isValidTxHash(txHash)) {
|
|
416
|
+
res.error = createInvalidParamsError(
|
|
417
|
+
{ txHash },
|
|
418
|
+
`Transaction hash did not match EVM format: ${txHash}`,
|
|
419
|
+
);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
// TODO
|
|
423
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
424
|
+
// "/transaction_content/txHash",
|
|
425
|
+
// {
|
|
426
|
+
// params: { query: { txHash } },
|
|
427
|
+
// },
|
|
428
|
+
// );
|
|
429
|
+
// if (error != null) {
|
|
430
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
431
|
+
// return;
|
|
432
|
+
// }
|
|
433
|
+
// if (data.success === false) {
|
|
434
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
435
|
+
// return;
|
|
436
|
+
// }
|
|
437
|
+
const [block] = await runPreparedQuery(
|
|
438
|
+
getLatestProcessedBlockHeight.run(
|
|
439
|
+
undefined,
|
|
440
|
+
dbConn,
|
|
441
|
+
),
|
|
442
|
+
"eth_getTransactionByHash",
|
|
443
|
+
);
|
|
444
|
+
if (!block) {
|
|
445
|
+
res.error = createInvalidParamsError(
|
|
446
|
+
{ txHash },
|
|
447
|
+
`Block not found`,
|
|
448
|
+
);
|
|
449
|
+
return;
|
|
450
|
+
}
|
|
451
|
+
const randomBlockHash = (): string =>
|
|
452
|
+
"0x" +
|
|
453
|
+
Array(64).fill(0).map(() =>
|
|
454
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
455
|
+
).join("");
|
|
456
|
+
const mockInput = {
|
|
457
|
+
blockHash: randomBlockHash(),
|
|
458
|
+
blockNumber: block.block_height,
|
|
459
|
+
from: randomBlockHash(),
|
|
460
|
+
inputData: "hello",
|
|
461
|
+
txHash: txHash,
|
|
462
|
+
txIndex: 0,
|
|
463
|
+
};
|
|
464
|
+
const mock: RpcTransaction = toRpcTransaction(mockInput);
|
|
465
|
+
setResult<typeof evmRpc.method>(mock);
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
case "eth_getTransactionByBlockHashAndIndex": {
|
|
469
|
+
const blockHash = evmRpc.params[0];
|
|
470
|
+
if (typeof blockHash !== "string" || !isValidBlockHash(blockHash)) {
|
|
471
|
+
res.error = createInvalidParamsError(
|
|
472
|
+
{ blockHash },
|
|
473
|
+
`Needs to be a valid block hash (0x...)`,
|
|
474
|
+
);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// const txIndex = Number.parseInt(evmRpc.params[1], 16);
|
|
478
|
+
// if (typeof txIndex !== "number") {
|
|
479
|
+
// res.error = createInvalidParamsError(
|
|
480
|
+
// { txIndex },
|
|
481
|
+
// `Transaction index must be specified as a number`,
|
|
482
|
+
// );
|
|
483
|
+
// return;
|
|
484
|
+
// }
|
|
485
|
+
|
|
486
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
487
|
+
// "/transaction_content/blockHashAndIndex",
|
|
488
|
+
// { params: { query: { blockHash, txIndex } } },
|
|
489
|
+
// );
|
|
490
|
+
// if (error != null) {
|
|
491
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
492
|
+
// return;
|
|
493
|
+
// }
|
|
494
|
+
// if (data.success === false) {
|
|
495
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
496
|
+
// return;
|
|
497
|
+
// }
|
|
498
|
+
const [block] = await runPreparedQuery(
|
|
499
|
+
getLatestProcessedBlockHeight.run(
|
|
500
|
+
undefined,
|
|
501
|
+
dbConn,
|
|
502
|
+
),
|
|
503
|
+
"eth_getTransactionByBlockHashAndIndex",
|
|
504
|
+
);
|
|
505
|
+
const randomBlockHash = (): string =>
|
|
506
|
+
"0x" +
|
|
507
|
+
Array(64).fill(0).map(() =>
|
|
508
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
509
|
+
).join("");
|
|
510
|
+
const mockInput = {
|
|
511
|
+
blockHash: randomBlockHash(),
|
|
512
|
+
blockNumber: block.block_height,
|
|
513
|
+
from: randomBlockHash(),
|
|
514
|
+
inputData: randomBlockHash(),
|
|
515
|
+
txHash: randomBlockHash(),
|
|
516
|
+
txIndex: 0,
|
|
517
|
+
};
|
|
518
|
+
const mock = toRpcTransaction(mockInput);
|
|
519
|
+
setResult<typeof evmRpc.method>(mock);
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
case "eth_getTransactionByBlockNumberAndIndex": {
|
|
523
|
+
let blockHeight: number;
|
|
524
|
+
try {
|
|
525
|
+
const [block] = await runPreparedQuery(
|
|
526
|
+
getLatestProcessedBlockHeight.run(
|
|
527
|
+
undefined,
|
|
528
|
+
dbConn,
|
|
529
|
+
),
|
|
530
|
+
"eth_getTransactionByBlockNumberAndIndex",
|
|
531
|
+
);
|
|
532
|
+
blockHeight = block.block_height;
|
|
533
|
+
// blockHeight = await toBlockNumber(evmRpc.params[0]);
|
|
534
|
+
} catch (err) {
|
|
535
|
+
res.error = err as RpcError;
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
const txIndex = Number.parseInt(evmRpc.params[1], 16);
|
|
539
|
+
if (typeof txIndex !== "number") {
|
|
540
|
+
res.error = createInvalidParamsError(
|
|
541
|
+
{ txIndex },
|
|
542
|
+
`Transaction index must be specified as a number`,
|
|
543
|
+
);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
548
|
+
// "/transaction_content/blockNumberAndIndex",
|
|
549
|
+
// { params: { query: { blockHeight, txIndex } } },
|
|
550
|
+
// );
|
|
551
|
+
// if (error != null) {
|
|
552
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
553
|
+
// return;
|
|
554
|
+
// }
|
|
555
|
+
// if (data.success === false) {
|
|
556
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
557
|
+
// return;
|
|
558
|
+
// }
|
|
559
|
+
const [block] = await runPreparedQuery(
|
|
560
|
+
getLatestProcessedBlockHeight.run(
|
|
561
|
+
undefined,
|
|
562
|
+
dbConn,
|
|
563
|
+
),
|
|
564
|
+
"eth_getTransactionByBlockNumberAndIndex",
|
|
565
|
+
);
|
|
566
|
+
const randomBlockHash = (): string =>
|
|
567
|
+
"0x" +
|
|
568
|
+
Array(64).fill(0).map(() =>
|
|
569
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
570
|
+
).join("");
|
|
571
|
+
const mockInput = {
|
|
572
|
+
blockHash: randomBlockHash(),
|
|
573
|
+
blockNumber: block.block_height,
|
|
574
|
+
from: randomBlockHash(),
|
|
575
|
+
inputData: randomBlockHash(),
|
|
576
|
+
txHash: randomBlockHash(),
|
|
577
|
+
txIndex: 0,
|
|
578
|
+
};
|
|
579
|
+
const mock = toRpcTransaction(mockInput);
|
|
580
|
+
setResult<typeof evmRpc.method>(mock);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
case "eth_getLogs": {
|
|
584
|
+
const request = evmRpc.params[0];
|
|
585
|
+
|
|
586
|
+
let fromBlock: number;
|
|
587
|
+
let toBlock: number;
|
|
588
|
+
// if (request.blockHash) {
|
|
589
|
+
// if (
|
|
590
|
+
// typeof request.blockHash !== "string" ||
|
|
591
|
+
// !isValidBlockHash(request.blockHash)
|
|
592
|
+
// ) {
|
|
593
|
+
// res.error = createInvalidParamsError(
|
|
594
|
+
// { blockHash: request.blockHash },
|
|
595
|
+
// `Needs to be a valid block hash (0x...)`,
|
|
596
|
+
// );
|
|
597
|
+
// return;
|
|
598
|
+
// }
|
|
599
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
600
|
+
// "/block_content/blockHash",
|
|
601
|
+
// {
|
|
602
|
+
// params: {
|
|
603
|
+
// query: { blockHash: request.blockHash, txDetails: "none" },
|
|
604
|
+
// },
|
|
605
|
+
// },
|
|
606
|
+
// );
|
|
607
|
+
// if (error != null) {
|
|
608
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
609
|
+
// return;
|
|
610
|
+
// }
|
|
611
|
+
// if (data.success === false) {
|
|
612
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
613
|
+
// return;
|
|
614
|
+
// }
|
|
615
|
+
|
|
616
|
+
// fromBlock = data.result.blockHeight;
|
|
617
|
+
// toBlock = data.result.blockHeight;
|
|
618
|
+
// } else {
|
|
619
|
+
// try {
|
|
620
|
+
// fromBlock = await toBlockNumber(request.fromBlock ?? "latest");
|
|
621
|
+
// } catch (err) {
|
|
622
|
+
// res.error = err as RpcError;
|
|
623
|
+
// return;
|
|
624
|
+
// }
|
|
625
|
+
// try {
|
|
626
|
+
// toBlock = await toBlockNumber(request.toBlock ?? "latest");
|
|
627
|
+
// } catch (err) {
|
|
628
|
+
// res.error = err as RpcError;
|
|
629
|
+
// return;
|
|
630
|
+
// }
|
|
631
|
+
// }
|
|
632
|
+
|
|
633
|
+
// const filterFields: Partial<
|
|
634
|
+
// Pick<
|
|
635
|
+
// PaimaNodeRestComponents["schemas"]["GetLogsParams"],
|
|
636
|
+
// "filters" | "topic"
|
|
637
|
+
// >
|
|
638
|
+
// > = {};
|
|
639
|
+
// if (request.topics != null && request.topics.length > 0) {
|
|
640
|
+
// // https://ethereum.stackexchange.com/a/107326/140076
|
|
641
|
+
// // problem: how do we know if something is an address, or a string that happens to be the same length?
|
|
642
|
+
// const [topicHash, ...data] = request.topics;
|
|
643
|
+
// filterFields.topic = strip0x(topicHash as string);
|
|
644
|
+
|
|
645
|
+
// // we need to map any other data provided from EVM format to Paima format
|
|
646
|
+
// if (data.length > 0) {
|
|
647
|
+
// const eventDefinition = (():
|
|
648
|
+
// | undefined
|
|
649
|
+
// | RegisteredEvent<LogEvent<LogEventFields<TSchema>[]>> => {
|
|
650
|
+
// const appEvents = EngineService.INSTANCE.getSM().getAppEvents();
|
|
651
|
+
|
|
652
|
+
// for (const defs of Object.values(appEvents)) {
|
|
653
|
+
// for (const def of defs) {
|
|
654
|
+
// if (def.topicHash === filterFields.topic) {
|
|
655
|
+
// return def;
|
|
656
|
+
// }
|
|
657
|
+
// }
|
|
658
|
+
// }
|
|
659
|
+
|
|
660
|
+
// return undefined;
|
|
661
|
+
// })();
|
|
662
|
+
|
|
663
|
+
// // if no event is found for this topic, we can just return right away
|
|
664
|
+
// // note: EVM RPCs also do not return an error for not-found topics, as they may be registered later
|
|
665
|
+
// if (eventDefinition == null) {
|
|
666
|
+
// setResult<typeof evmRpc.method>([]);
|
|
667
|
+
// return;
|
|
668
|
+
// }
|
|
669
|
+
|
|
670
|
+
// const eventPaths = eventDefinition.path as EventPath;
|
|
671
|
+
// const evmAbi = toEvmAbi(eventDefinition.definition);
|
|
672
|
+
// if (evmAbi == null) {
|
|
673
|
+
// res.error = createInternalRpcError(
|
|
674
|
+
// {},
|
|
675
|
+
// `EVM ABI conversion for this event type is not supported`,
|
|
676
|
+
// );
|
|
677
|
+
// return;
|
|
678
|
+
// }
|
|
679
|
+
// for (let i = 0, j = 0; i < data.length; i++) {
|
|
680
|
+
// const topic = data[i];
|
|
681
|
+
// if (topic == null) continue;
|
|
682
|
+
// if (Array.isArray(topic)) {
|
|
683
|
+
// res.error = createInvalidParamsError(
|
|
684
|
+
// { topic },
|
|
685
|
+
// `Array-style OR topics unsupported at the moment`,
|
|
686
|
+
// );
|
|
687
|
+
// return;
|
|
688
|
+
// }
|
|
689
|
+
// let nextFilter: undefined | ArgPath = undefined;
|
|
690
|
+
// // increase `j` to the index of the next filter for this event
|
|
691
|
+
// for (; j < eventPaths.length; j++) {
|
|
692
|
+
// let path = eventPaths[j];
|
|
693
|
+
// if (typeof path !== "string") {
|
|
694
|
+
// nextFilter = path;
|
|
695
|
+
// break;
|
|
696
|
+
// }
|
|
697
|
+
// }
|
|
698
|
+
// // no fields left (there are more event RPC filters specified than fields for this event)
|
|
699
|
+
// if (nextFilter == null) {
|
|
700
|
+
// break;
|
|
701
|
+
// }
|
|
702
|
+
// const filter = eventPaths[j] as ArgPath;
|
|
703
|
+
|
|
704
|
+
// if (filterFields.filters == null) {
|
|
705
|
+
// filterFields.filters = {};
|
|
706
|
+
// }
|
|
707
|
+
// filterFields.filters[filter.name] = decodeAbiParameters(
|
|
708
|
+
// [evmAbi[i]],
|
|
709
|
+
// topic,
|
|
710
|
+
// )[0] as string;
|
|
711
|
+
// }
|
|
712
|
+
// }
|
|
713
|
+
// }
|
|
714
|
+
|
|
715
|
+
// const { data, error } = await getPaimaNodeRestClient().POST(
|
|
716
|
+
// "/get_logs",
|
|
717
|
+
// {
|
|
718
|
+
// body: {
|
|
719
|
+
// fromBlock,
|
|
720
|
+
// toBlock,
|
|
721
|
+
// ...filterFields,
|
|
722
|
+
// address: request.address,
|
|
723
|
+
// },
|
|
724
|
+
// },
|
|
725
|
+
// );
|
|
726
|
+
// if (error != null) {
|
|
727
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
728
|
+
// return;
|
|
729
|
+
// }
|
|
730
|
+
// if (data.success === false) {
|
|
731
|
+
// res.error = createInternalRpcError({}, data.errorMessage);
|
|
732
|
+
// return;
|
|
733
|
+
// }
|
|
734
|
+
|
|
735
|
+
setResult<typeof evmRpc.method>([].map(toRpcLog));
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
case "eth_getTransactionReceipt": {
|
|
739
|
+
const txHash = evmRpc.params[0];
|
|
740
|
+
if (!isValidTxHash(txHash)) {
|
|
741
|
+
res.error = createInvalidParamsError(
|
|
742
|
+
{ txHash },
|
|
743
|
+
`Transaction hash did not match EVM format: ${txHash}`,
|
|
744
|
+
);
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
// const { data: txData, error: txError } = await getPaimaNodeRestClient()
|
|
749
|
+
// .GET(
|
|
750
|
+
// "/transaction_content/txHash",
|
|
751
|
+
// {
|
|
752
|
+
// params: { query: { txHash } },
|
|
753
|
+
// },
|
|
754
|
+
// );
|
|
755
|
+
// if (txError != null) {
|
|
756
|
+
// res.error = createInternalRpcError({}, txError?.errorMessage);
|
|
757
|
+
// return;
|
|
758
|
+
// }
|
|
759
|
+
// if (txData.success === false) {
|
|
760
|
+
// res.error = createInternalRpcError({}, txData.errorMessage);
|
|
761
|
+
// return;
|
|
762
|
+
// }
|
|
763
|
+
|
|
764
|
+
// // TODO: create a version of get_logs for specific transactions
|
|
765
|
+
|
|
766
|
+
const [block] = await runPreparedQuery(
|
|
767
|
+
getLatestProcessedBlockHeight.run(
|
|
768
|
+
undefined,
|
|
769
|
+
dbConn,
|
|
770
|
+
),
|
|
771
|
+
"eth_getTransactionReceipt",
|
|
772
|
+
);
|
|
773
|
+
|
|
774
|
+
if (!block) {
|
|
775
|
+
res.error = createInvalidParamsError(
|
|
776
|
+
{ txHash },
|
|
777
|
+
`Block not found`,
|
|
778
|
+
);
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
const randomBlockHash = (): string =>
|
|
783
|
+
"0x" +
|
|
784
|
+
Array(64).fill(0).map(() =>
|
|
785
|
+
Math.floor(Math.random() * 16).toString(16)
|
|
786
|
+
).join("");
|
|
787
|
+
|
|
788
|
+
const mockInput = {
|
|
789
|
+
result: {
|
|
790
|
+
txHash: randomBlockHash(),
|
|
791
|
+
blockHash: randomBlockHash(),
|
|
792
|
+
blockNumber: block.block_height,
|
|
793
|
+
txIndex: 0,
|
|
794
|
+
from: randomBlockHash(),
|
|
795
|
+
success: true,
|
|
796
|
+
},
|
|
797
|
+
};
|
|
798
|
+
setResult<typeof evmRpc.method>(toRpcTransactionReceipt(mockInput));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
case "eth_getUncleByBlockHashAndIndex": {
|
|
802
|
+
setResult<typeof evmRpc.method>(null); // no uncles in Paima
|
|
803
|
+
return;
|
|
804
|
+
}
|
|
805
|
+
case "eth_getUncleByBlockNumberAndIndex": {
|
|
806
|
+
setResult<typeof evmRpc.method>(null); // no uncles in Paima
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// /**
|
|
811
|
+
// * ===============
|
|
812
|
+
// * Other methods
|
|
813
|
+
// * ===============
|
|
814
|
+
// */
|
|
815
|
+
|
|
816
|
+
case "web3_clientVersion": {
|
|
817
|
+
// TODO: should include the following
|
|
818
|
+
// 1: commit hash somehow
|
|
819
|
+
// 2: platform
|
|
820
|
+
// 3: node version
|
|
821
|
+
// example of Geth: Geth/v1.9.15-stable-0f77f34b/linux-amd64/go1.14.4"
|
|
822
|
+
// we just keep it simple for now
|
|
823
|
+
// TODO: UPDATE THIS
|
|
824
|
+
setResult<typeof evmRpc.method>(
|
|
825
|
+
`PaimaEngine/v0.0.1`,
|
|
826
|
+
);
|
|
827
|
+
return;
|
|
828
|
+
}
|
|
829
|
+
case "web3_sha3": {
|
|
830
|
+
const data = evmRpc.params[0];
|
|
831
|
+
if (typeof data !== "string" || !isValidHexadecimal(strip0x(data))) {
|
|
832
|
+
res.error = createInvalidParamsError(
|
|
833
|
+
{ data },
|
|
834
|
+
`Data must be a hex-encoded string 0x...`,
|
|
835
|
+
);
|
|
836
|
+
return;
|
|
837
|
+
}
|
|
838
|
+
// web3_sha3 passes the data as a hex-string, so we need to convert it
|
|
839
|
+
const buffer = Buffer.from(strip0x(data), "hex");
|
|
840
|
+
|
|
841
|
+
setResult<typeof evmRpc.method>(`0x${keccak_256(buffer as any)}`);
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
case "net_version": {
|
|
845
|
+
// net_version is an older version of eth_chainId that predates the ETC hardfork
|
|
846
|
+
// eth_chainId should always be used instead, but net_version===eth_chainId in almost all networks
|
|
847
|
+
// the only real notable exception being ETC where these values are not equal
|
|
848
|
+
// learn more: https://medium.com/@pedrouid/chainid-vs-networkid-how-do-they-differ-on-ethereum-eec2ed41635b
|
|
849
|
+
setResult<typeof evmRpc.method>(
|
|
850
|
+
`0x${ENV.EFFECTSTREAM_CHAIN_ID.toString(16)}`,
|
|
851
|
+
);
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
case "net_listening": {
|
|
855
|
+
// note: depending on your interpretation,
|
|
856
|
+
// this should one of
|
|
857
|
+
// A) always return true (is the server online at all)
|
|
858
|
+
// B) Only return true if this RPC should be used by others (is one RPC better than another)
|
|
859
|
+
// C) Can you sync blocks from connecting to this node
|
|
860
|
+
// ex: in wagmi, it's used to rank multiple RPCs and it prioritizes RPCs that return true
|
|
861
|
+
setResult<typeof evmRpc.method>(true); // maybe this should be ENV.SERVER_ONLY_MODE
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
case "net_peerCount": {
|
|
865
|
+
// todo: theoretically we could support this based on MQTT connections or something similar, but it's not worth it
|
|
866
|
+
setResult<typeof evmRpc.method>(`0x0`);
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
case "eth_gasPrice": {
|
|
870
|
+
// no concept of gas in Paima (gas is from the underlying chain)
|
|
871
|
+
setResult<typeof evmRpc.method>(`0x0`);
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
case "eth_chainId": {
|
|
875
|
+
// TODO: this is not a great thing to return, since every game is its own chain. Not sure what to do about this
|
|
876
|
+
// maybe the hash of the game name or something?
|
|
877
|
+
setResult<typeof evmRpc.method>(
|
|
878
|
+
`0x${ENV.EFFECTSTREAM_CHAIN_ID.toString(16)}`,
|
|
879
|
+
);
|
|
880
|
+
return;
|
|
881
|
+
}
|
|
882
|
+
|
|
883
|
+
case "eth_syncing": {
|
|
884
|
+
// if (ENV.SERVER_ONLY_MODE) {
|
|
885
|
+
// setResult<typeof evmRpc.method>(false);
|
|
886
|
+
// }
|
|
887
|
+
// const { data, error } = await getPaimaNodeRestClient().GET(
|
|
888
|
+
// "/latest_processed_blockheight",
|
|
889
|
+
// );
|
|
890
|
+
// if (error != null) {
|
|
891
|
+
// res.error = createInternalRpcError({}, error?.errorMessage);
|
|
892
|
+
// return;
|
|
893
|
+
// }
|
|
894
|
+
|
|
895
|
+
const [block] = await runPreparedQuery(
|
|
896
|
+
getLatestProcessedBlockHeight.run(
|
|
897
|
+
undefined,
|
|
898
|
+
dbConn,
|
|
899
|
+
),
|
|
900
|
+
"eth_syncing",
|
|
901
|
+
);
|
|
902
|
+
|
|
903
|
+
// TODO Updaste this
|
|
904
|
+
const ENV_START_BLOCKHEIGHT = 0;
|
|
905
|
+
setResult<typeof evmRpc.method>({
|
|
906
|
+
startingBlock: `0x${ENV_START_BLOCKHEIGHT.toString(16)}`,
|
|
907
|
+
currentBlock: `0x${block.block_height.toString(16)}`,
|
|
908
|
+
});
|
|
909
|
+
return;
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
default: {
|
|
913
|
+
res.error = createMethodNotFoundRpcError(evmRpc.method);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
}),
|
|
917
|
+
);
|
|
918
|
+
|
|
919
|
+
return engine;
|
|
920
|
+
}
|