@appliedblockchain/silentdatarollup-custom-rpc 1.0.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.
Files changed (3) hide show
  1. package/README.md +70 -0
  2. package/dist/index.js +462 -0
  3. package/package.json +47 -0
package/README.md ADDED
@@ -0,0 +1,70 @@
1
+ # Silent Data [Rollup] Providers - Custom RPC Package
2
+
3
+ ## Table of Contents
4
+
5
+ - [Introduction](#introduction)
6
+ - [Prerequisites](#prerequisites)
7
+ - [Integration](#integration)
8
+ - [Custom RPC Integration](#custom-rpc-integration)
9
+ - [Installing Custom RPC Dependencies](#installing-custom-rpc-dependencies)
10
+ - [Custom RPC Integration Example](#custom-rpc-integration-example)
11
+ - [Troubleshooting](#troubleshooting)
12
+ - [License](#license)
13
+ - [Additional Resources](#additional-resources)
14
+
15
+ ## Introduction
16
+
17
+ Custom RPC provider for Silent Data [Rollup], providing a local development environment with Silent Data [Rollup] integration.
18
+
19
+ ## Prerequisites
20
+
21
+ - Node.js (version 18 or higher)
22
+ - npm
23
+ - Basic knowledge of Ethereum and smart contracts
24
+ - Hardhat (for local development)
25
+
26
+ ## Integration
27
+
28
+ ### Custom RPC Integration
29
+
30
+ #### Installing Custom RPC Dependencies
31
+
32
+ ```bash
33
+ npm install @appliedblockchain/silentdatarollup-custom-rpc
34
+ ```
35
+
36
+ #### Custom RPC Integration Example
37
+
38
+ The Custom RPC package provides a local development environment that integrates Silent Data [Rollup] with a local Hardhat node. To start the development environment:
39
+
40
+ ```bash
41
+ npm run dev
42
+ ```
43
+
44
+ This command will start both a Hardhat node and the Custom RPC server concurrently. The Custom RPC server will proxy requests to the Hardhat node while adding Silent Data [Rollup] functionality.
45
+
46
+ You can configure the Custom RPC server by creating a `.env` file in your project root:
47
+
48
+ ```env
49
+ PORT=3000
50
+ HARDHAT_RPC_URL=http://localhost:8545
51
+ ```
52
+
53
+ ## Troubleshooting
54
+
55
+ If you encounter any issues, please check the following:
56
+
57
+ 1. Ensure Hardhat is properly installed and configured
58
+ 2. Verify that the port specified in your `.env` file is available
59
+ 3. Check that the Hardhat node is running and accessible
60
+ 4. Ensure all required environment variables are set
61
+
62
+ ## License
63
+
64
+ This project is licensed under the [MIT License](LICENSE).
65
+
66
+ ## Additional Resources
67
+
68
+ - [Silent Data [Rollup] Documentation](https://docs.silentdata.com)
69
+ - [Hardhat Documentation](https://hardhat.org/docs)
70
+ - [Ethers.js Documentation](https://docs.ethers.org/v6/)
package/dist/index.js ADDED
@@ -0,0 +1,462 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/index.ts
27
+ var import_config = require("dotenv/config");
28
+ var import_express = __toESM(require("express"));
29
+ var import_body_parser = __toESM(require("body-parser"));
30
+ var import_http_proxy_middleware = require("http-proxy-middleware");
31
+ var import_ethers2 = require("ethers");
32
+
33
+ // src/middleware.ts
34
+ var import_silentdatarollup_core2 = require("@appliedblockchain/silentdatarollup-core");
35
+
36
+ // src/signatures.ts
37
+ var import_ethers = require("ethers");
38
+ var import_silentdatarollup_core = require("@appliedblockchain/silentdatarollup-core");
39
+ var TIMESTAMP_MIN_OFFSET = -12;
40
+ var TIMESTAMP_MAX_OFFSET = 4;
41
+ function validateRFC3339Timestamp(timestamp) {
42
+ const rfc3339Pattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})$/;
43
+ if (!rfc3339Pattern.test(timestamp)) {
44
+ return false;
45
+ }
46
+ const timestampDate = new Date(timestamp);
47
+ if (Number.isNaN(timestampDate.getTime())) {
48
+ return false;
49
+ }
50
+ const now = /* @__PURE__ */ new Date();
51
+ const minAllowedTime = new Date(
52
+ now.getTime() + TIMESTAMP_MIN_OFFSET * 60 * 1e3
53
+ );
54
+ const maxAllowedTime = new Date(
55
+ now.getTime() + TIMESTAMP_MAX_OFFSET * 60 * 1e3
56
+ );
57
+ return timestampDate >= minAllowedTime && timestampDate <= maxAllowedTime;
58
+ }
59
+ function recoverSerialSigner(message, signature) {
60
+ try {
61
+ const messageHash = import_ethers.ethers.hashMessage(message);
62
+ const recoveredAddress = import_ethers.ethers.recoverAddress(messageHash, signature);
63
+ return recoveredAddress;
64
+ } catch {
65
+ return null;
66
+ }
67
+ }
68
+ function recoverTypedSigner(data, signature) {
69
+ try {
70
+ const domain = import_silentdatarollup_core.eip721Domain;
71
+ const types = {
72
+ Request: [
73
+ { name: "method", type: "string" },
74
+ { name: "params", type: "string" },
75
+ { name: "timestamp", type: "string" }
76
+ ]
77
+ };
78
+ const recoveredAddress = import_ethers.ethers.verifyTypedData(
79
+ domain,
80
+ types,
81
+ data,
82
+ signature
83
+ );
84
+ return recoveredAddress;
85
+ } catch {
86
+ return null;
87
+ }
88
+ }
89
+ function recoverSignerWithDelegate(delegateTicket, delegateSignature, message, isEIP712) {
90
+ try {
91
+ const ticket = JSON.parse(delegateTicket);
92
+ if (!ticket.expires || !ticket.ephemeralAddress) {
93
+ return null;
94
+ }
95
+ const expiryDate = new Date(ticket.expires);
96
+ if (Number.isNaN(expiryDate.getTime()) || expiryDate <= /* @__PURE__ */ new Date()) {
97
+ return null;
98
+ }
99
+ let delegateAddress;
100
+ if (isEIP712) {
101
+ const data = {
102
+ method: JSON.parse(message).method,
103
+ params: JSON.stringify(JSON.parse(message).params),
104
+ timestamp: ticket.timestamp
105
+ };
106
+ delegateAddress = recoverTypedSigner(data, delegateSignature);
107
+ } else {
108
+ delegateAddress = recoverSerialSigner(message, delegateSignature);
109
+ }
110
+ if (!delegateAddress || delegateAddress.toLowerCase() !== ticket.ephemeralAddress.toLowerCase()) {
111
+ return null;
112
+ }
113
+ const ticketData = {
114
+ expires: ticket.expires,
115
+ ephemeralAddress: ticket.ephemeralAddress
116
+ };
117
+ const delegatorAddress = import_ethers.ethers.verifyTypedData(
118
+ import_silentdatarollup_core.eip721Domain,
119
+ import_silentdatarollup_core.delegateEIP721Types,
120
+ ticketData,
121
+ ticket.signature
122
+ );
123
+ return {
124
+ delegate: delegateAddress,
125
+ delegator: delegatorAddress
126
+ };
127
+ } catch {
128
+ return null;
129
+ }
130
+ }
131
+ function recoverSigner(headers, body) {
132
+ const timestamp = headers[import_silentdatarollup_core.HEADER_TIMESTAMP];
133
+ if (!timestamp) {
134
+ return null;
135
+ }
136
+ if (!validateRFC3339Timestamp(timestamp)) {
137
+ return null;
138
+ }
139
+ const delegateTicket = headers[import_silentdatarollup_core.HEADER_DELEGATE];
140
+ const delegateSignature = headers[import_silentdatarollup_core.HEADER_DELEGATE_SIGNATURE];
141
+ const eip712DelegateSignature = headers[import_silentdatarollup_core.HEADER_EIP712_DELEGATE_SIGNATURE];
142
+ const signature = headers[import_silentdatarollup_core.HEADER_SIGNATURE];
143
+ const eip712Signature = headers[import_silentdatarollup_core.HEADER_EIP712_SIGNATURE];
144
+ if (signature && eip712Signature || delegateSignature && eip712DelegateSignature) {
145
+ return null;
146
+ }
147
+ if (delegateTicket && (delegateSignature || eip712DelegateSignature)) {
148
+ const result = recoverSignerWithDelegate(
149
+ delegateTicket,
150
+ delegateSignature || eip712DelegateSignature || "",
151
+ body,
152
+ !!eip712DelegateSignature
153
+ );
154
+ return result ? result.delegator : null;
155
+ }
156
+ if (signature) {
157
+ const message = `${body}${timestamp}`;
158
+ return recoverSerialSigner(message, signature);
159
+ }
160
+ if (eip712Signature) {
161
+ try {
162
+ const parsedBody = JSON.parse(body);
163
+ const data = {
164
+ method: parsedBody.method,
165
+ params: JSON.stringify(parsedBody.params),
166
+ timestamp
167
+ };
168
+ return recoverTypedSigner(data, eip712Signature);
169
+ } catch {
170
+ return null;
171
+ }
172
+ }
173
+ return null;
174
+ }
175
+ function generateRandomKeyPair() {
176
+ const wallet = import_ethers.ethers.Wallet.createRandom();
177
+ return {
178
+ privateKey: wallet.privateKey,
179
+ address: wallet.address
180
+ };
181
+ }
182
+ function validateAddress(signer, address) {
183
+ if (!signer || !address) {
184
+ return new Error("Missing signer or address");
185
+ }
186
+ if (signer.toLowerCase() !== address.toLowerCase()) {
187
+ return new Error(`Signer ${signer} does not match address ${address}`);
188
+ }
189
+ return null;
190
+ }
191
+ function validateMethodAccess(method, params, signer, headers) {
192
+ switch (method) {
193
+ case "eth_getTransactionCount":
194
+ case "eth_getProof":
195
+ if (!signer) {
196
+ return true;
197
+ }
198
+ if (!params || !params[0]) {
199
+ return false;
200
+ }
201
+ return params[0].toLowerCase() === signer.toLowerCase();
202
+ case "eth_call":
203
+ return sanitiseEthCallFrom(params, signer ?? "", headers);
204
+ default:
205
+ return false;
206
+ }
207
+ }
208
+ function sanitiseEthCallFrom(params, signer, headers) {
209
+ if (!params || params.length === 0) {
210
+ return false;
211
+ }
212
+ const validationError = validateSignerForEthCall(params, signer, headers);
213
+ if (validationError) {
214
+ try {
215
+ let param0;
216
+ if (typeof params[0] === "object" && params[0] !== null) {
217
+ param0 = params[0];
218
+ } else {
219
+ return false;
220
+ }
221
+ const { address } = generateRandomKeyPair();
222
+ param0.from = address;
223
+ params[0] = param0;
224
+ return true;
225
+ } catch {
226
+ return false;
227
+ }
228
+ }
229
+ return true;
230
+ }
231
+ function validateSignerForEthCall(params, signer, headers) {
232
+ if (params.length === 0) {
233
+ const err = new Error("empty params for eth_call");
234
+ return err;
235
+ }
236
+ let param0;
237
+ if (typeof params[0] === "object" && params[0] !== null) {
238
+ param0 = params[0];
239
+ } else {
240
+ const err = new Error("eth_call param[0] is not a json object");
241
+ return err;
242
+ }
243
+ const hasSignature = headers && (headers[import_silentdatarollup_core.HEADER_SIGNATURE] || headers[import_silentdatarollup_core.HEADER_EIP712_SIGNATURE]);
244
+ if (!hasSignature) {
245
+ const err = new Error("no signature provided");
246
+ return err;
247
+ }
248
+ if (param0.from === void 0 || param0.from === null) {
249
+ param0.from = signer;
250
+ return null;
251
+ } else if (typeof param0.from === "string") {
252
+ const validationError = validateAddress(signer, param0.from);
253
+ if (validationError) {
254
+ return validationError;
255
+ }
256
+ return null;
257
+ } else {
258
+ const err = new Error(
259
+ "'from' field in eth_call must be a valid address or null"
260
+ );
261
+ return err;
262
+ }
263
+ }
264
+
265
+ // src/transactions.ts
266
+ function sanitizeTransaction(transaction) {
267
+ if (!transaction) {
268
+ return null;
269
+ }
270
+ return {
271
+ blockHash: transaction.blockHash ?? null,
272
+ blockNumber: transaction.blockNumber?.toString() ?? null,
273
+ transactionHash: transaction.hash ?? null,
274
+ transactionIndex: transaction.index?.toString() ?? null
275
+ };
276
+ }
277
+ function sanitizeTransactionReceipt(transactionReceipt) {
278
+ if (!transactionReceipt) {
279
+ return null;
280
+ }
281
+ return {
282
+ status: transactionReceipt.status?.toString() ?? null,
283
+ blockHash: transactionReceipt.blockHash ?? null,
284
+ blockNumber: transactionReceipt.blockNumber?.toString() ?? null,
285
+ transactionHash: transactionReceipt.hash ?? null,
286
+ transactionIndex: transactionReceipt.index?.toString() ?? null
287
+ };
288
+ }
289
+ function validateTransactionAccess(transaction, signer) {
290
+ if (!signer || !transaction) {
291
+ return false;
292
+ }
293
+ const signerLower = signer.toLowerCase();
294
+ if (transaction.from && transaction.from.toLowerCase() === signerLower) {
295
+ return true;
296
+ }
297
+ if (transaction.to && transaction.to.toLowerCase() === signerLower) {
298
+ return true;
299
+ }
300
+ return false;
301
+ }
302
+ function validateSignerAgainstResponse(transaction, signer) {
303
+ return validateTransactionAccess(transaction, signer);
304
+ }
305
+ async function handleGetTransactionByHash(provider2, txHash, signer) {
306
+ const transaction = await provider2.getTransaction(txHash);
307
+ if (!transaction) {
308
+ return null;
309
+ }
310
+ if (validateSignerAgainstResponse(transaction, signer)) {
311
+ return transaction;
312
+ } else {
313
+ const sanitizedData = sanitizeTransaction(transaction);
314
+ return sanitizedData;
315
+ }
316
+ }
317
+ async function handleGetTransactionReceipt(provider2, txHash, signer) {
318
+ const transaction = await provider2.getTransactionReceipt(txHash);
319
+ if (!transaction) {
320
+ return null;
321
+ }
322
+ if (validateSignerAgainstResponse(transaction, signer)) {
323
+ return transaction;
324
+ } else {
325
+ const sanitizedData = sanitizeTransactionReceipt(transaction);
326
+ return sanitizedData;
327
+ }
328
+ }
329
+ async function validateTransactionMethod(provider2, method, params, signer) {
330
+ if (!params || !params[0]) {
331
+ return null;
332
+ }
333
+ const txHash = params[0];
334
+ try {
335
+ if (method === "eth_getTransactionByHash") {
336
+ return await handleGetTransactionByHash(provider2, txHash, signer);
337
+ } else if (method === "eth_getTransactionReceipt") {
338
+ return await handleGetTransactionReceipt(provider2, txHash, signer);
339
+ }
340
+ return null;
341
+ } catch {
342
+ return null;
343
+ }
344
+ }
345
+
346
+ // src/middleware.ts
347
+ function errorResponse(id, jsonrpc) {
348
+ return {
349
+ id,
350
+ jsonrpc,
351
+ error: {
352
+ code: -1,
353
+ message: "An error occurred",
354
+ data: "An error occurred"
355
+ }
356
+ };
357
+ }
358
+ function parseRpcRequest(req) {
359
+ let rpcRequest = null;
360
+ let bodyString = "";
361
+ if (req.method !== "POST") {
362
+ return { rpcRequest: null, bodyString };
363
+ }
364
+ try {
365
+ bodyString = req.body.toString();
366
+ rpcRequest = JSON.parse(bodyString);
367
+ req.body = rpcRequest;
368
+ } catch {
369
+ return { rpcRequest: null, bodyString };
370
+ }
371
+ if (!rpcRequest || typeof rpcRequest !== "object" || !("method" in rpcRequest) || !("id" in rpcRequest) || !("jsonrpc" in rpcRequest)) {
372
+ return { rpcRequest: null, bodyString };
373
+ }
374
+ return {
375
+ rpcRequest,
376
+ bodyString
377
+ };
378
+ }
379
+ function createRpcValidationMiddleware(provider2) {
380
+ return async (req, res, next) => {
381
+ const { rpcRequest, bodyString } = parseRpcRequest(req);
382
+ if (!rpcRequest) {
383
+ next();
384
+ return;
385
+ }
386
+ const { method, id, jsonrpc, params } = rpcRequest;
387
+ console.log(`Received request with method: ${method}`);
388
+ if (!import_silentdatarollup_core2.WHITELISTED_METHODS.includes(method)) {
389
+ console.log(`Method ${method} is not whitelisted`);
390
+ return res.status(403).json(errorResponse(id, jsonrpc));
391
+ }
392
+ if (!import_silentdatarollup_core2.SIGN_RPC_METHODS.includes(method)) {
393
+ next();
394
+ return;
395
+ }
396
+ const signer = recoverSigner(
397
+ req.headers,
398
+ bodyString
399
+ );
400
+ if (["eth_getTransactionByHash", "eth_getTransactionReceipt"].includes(method)) {
401
+ const transaction = await validateTransactionMethod(
402
+ provider2,
403
+ method,
404
+ params,
405
+ signer
406
+ );
407
+ console.log(`Sending transaction response for method: ${method}`);
408
+ return res.status(200).json({
409
+ id,
410
+ jsonrpc,
411
+ result: transaction
412
+ });
413
+ }
414
+ if (!validateMethodAccess(method, params, signer)) {
415
+ console.log(`Failed to validate method access for method: ${method}`);
416
+ return res.status(403).json(errorResponse(id, jsonrpc));
417
+ }
418
+ next();
419
+ };
420
+ }
421
+
422
+ // src/index.ts
423
+ var import_silentdatarollup_core3 = require("@appliedblockchain/silentdatarollup-core");
424
+ var CUSTOM_RPC_PORT = process.env.CUSTOM_RPC_PORT || 54321;
425
+ var CUSTOM_RPC_PROXY_URL = process.env.CUSTOM_RPC_PROXY_URL || "http://localhost:8545";
426
+ var provider = new import_ethers2.JsonRpcProvider(CUSTOM_RPC_PROXY_URL);
427
+ var app = (0, import_express.default)();
428
+ app.use((req, res, next) => {
429
+ res.header("Access-Control-Allow-Origin", "*");
430
+ res.header("Access-Control-Allow-Credentials", "true");
431
+ res.header(
432
+ "Access-Control-Allow-Headers",
433
+ `Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, ${import_silentdatarollup_core3.HEADER_TIMESTAMP}, ${import_silentdatarollup_core3.HEADER_SIGNATURE}, ${import_silentdatarollup_core3.HEADER_DELEGATE}, ${import_silentdatarollup_core3.HEADER_DELEGATE_SIGNATURE}, ${import_silentdatarollup_core3.HEADER_EIP712_DELEGATE_SIGNATURE}, ${import_silentdatarollup_core3.HEADER_EIP712_SIGNATURE}`
434
+ );
435
+ res.header("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT");
436
+ if (req.method === "OPTIONS") {
437
+ return res.sendStatus(204);
438
+ }
439
+ next();
440
+ });
441
+ app.use(import_body_parser.default.raw({ type: "*/*" }));
442
+ app.use(createRpcValidationMiddleware(provider));
443
+ app.use(
444
+ (0, import_http_proxy_middleware.createProxyMiddleware)({
445
+ target: CUSTOM_RPC_PROXY_URL,
446
+ changeOrigin: true,
447
+ on: {
448
+ proxyReq: (proxyReq, req) => {
449
+ if (req.body) {
450
+ const bodyData = JSON.stringify(req.body);
451
+ console.log(`Proxying request with body: ${bodyData}`);
452
+ proxyReq.setHeader("Content-Length", Buffer.byteLength(bodyData));
453
+ proxyReq.write(bodyData);
454
+ }
455
+ }
456
+ }
457
+ })
458
+ );
459
+ app.disable("x-powered-by");
460
+ app.listen(CUSTOM_RPC_PORT, () => {
461
+ console.log(`Custom RPC listening at http://localhost:${CUSTOM_RPC_PORT}`);
462
+ });
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@appliedblockchain/silentdatarollup-custom-rpc",
3
+ "version": "1.0.0",
4
+ "description": "Custom RPC for Silent Data [Rollup]",
5
+ "author": "Applied Blockchain",
6
+ "homepage": "https://github.com/appliedblockchain/silent-data-rollup-providers#readme",
7
+ "keywords": [
8
+ "ethereum",
9
+ "provider",
10
+ "silentdata",
11
+ "rollup",
12
+ "custom-rpc"
13
+ ],
14
+ "license": "MIT",
15
+ "repository": "https://github.com/appliedblockchain/silent-data-rollup-providers",
16
+ "main": "dist/index.js",
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "bin": "dist/index.js",
21
+ "scripts": {
22
+ "build": "tsup src/index.ts --format cjs --clean",
23
+ "check-exports": "attw --pack . --profile node16",
24
+ "dev": "concurrently --kill-others-on-fail -s all -p '[{name}]' -n 'HARDHAT,CUSTOM RPC' -c 'bgGreen.bold,bgBlue.bold' 'hardhat node' 'nodemon'",
25
+ "prepack": "npm run build"
26
+ },
27
+ "dependencies": {
28
+ "@appliedblockchain/silentdatarollup-core": "1.0.0",
29
+ "body-parser": "1.20.3",
30
+ "dotenv": "16.4.7",
31
+ "ethers": "6.13.2",
32
+ "express": "4.21.2",
33
+ "http-proxy-middleware": "3.0.3"
34
+ },
35
+ "devDependencies": {
36
+ "@types/express": "4.17.21",
37
+ "@types/node": "22.5.4",
38
+ "concurrently": "9.1.2",
39
+ "hardhat": "^0.0.7",
40
+ "nodemon": "3.0.1",
41
+ "ts-node": "10.9.2",
42
+ "typescript": "5.6.2"
43
+ },
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ }
47
+ }