@aztec/foundation 0.62.0 → 0.63.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 (140) hide show
  1. package/dest/abi/abi.d.ts +708 -228
  2. package/dest/abi/abi.d.ts.map +1 -1
  3. package/dest/abi/abi.js +92 -8
  4. package/dest/abi/encoder.d.ts.map +1 -1
  5. package/dest/abi/encoder.js +4 -1
  6. package/dest/abi/event_selector.d.ts +4 -0
  7. package/dest/abi/event_selector.d.ts.map +1 -1
  8. package/dest/abi/event_selector.js +7 -1
  9. package/dest/aztec-address/index.d.ts +19 -3
  10. package/dest/aztec-address/index.d.ts.map +1 -1
  11. package/dest/aztec-address/index.js +43 -14
  12. package/dest/buffer/buffer32.d.ts +1 -0
  13. package/dest/buffer/buffer32.d.ts.map +1 -1
  14. package/dest/buffer/buffer32.js +4 -1
  15. package/dest/config/env_var.d.ts +1 -1
  16. package/dest/config/env_var.d.ts.map +1 -1
  17. package/dest/crypto/index.d.ts +1 -0
  18. package/dest/crypto/index.d.ts.map +1 -1
  19. package/dest/crypto/index.js +2 -1
  20. package/dest/crypto/keys/index.d.ts +5 -0
  21. package/dest/crypto/keys/index.d.ts.map +1 -0
  22. package/dest/crypto/keys/index.js +8 -0
  23. package/dest/eth-address/index.d.ts +2 -6
  24. package/dest/eth-address/index.d.ts.map +1 -1
  25. package/dest/eth-address/index.js +3 -7
  26. package/dest/eth-signature/eth_signature.d.ts +2 -0
  27. package/dest/eth-signature/eth_signature.d.ts.map +1 -1
  28. package/dest/eth-signature/eth_signature.js +7 -1
  29. package/dest/fields/fields.d.ts +1 -3
  30. package/dest/fields/fields.d.ts.map +1 -1
  31. package/dest/fields/fields.js +2 -1
  32. package/dest/json-rpc/client/fetch.d.ts +21 -0
  33. package/dest/json-rpc/client/fetch.d.ts.map +1 -0
  34. package/dest/json-rpc/client/fetch.js +66 -0
  35. package/dest/json-rpc/client/index.d.ts +2 -1
  36. package/dest/json-rpc/client/index.d.ts.map +1 -1
  37. package/dest/json-rpc/client/index.js +3 -2
  38. package/dest/json-rpc/client/safe_json_rpc_client.d.ts +13 -0
  39. package/dest/json-rpc/client/safe_json_rpc_client.d.ts.map +1 -0
  40. package/dest/json-rpc/client/safe_json_rpc_client.js +45 -0
  41. package/dest/json-rpc/convert.d.ts +11 -19
  42. package/dest/json-rpc/convert.d.ts.map +1 -1
  43. package/dest/json-rpc/convert.js +30 -123
  44. package/dest/json-rpc/fixtures/test_state.d.ts +45 -3
  45. package/dest/json-rpc/fixtures/test_state.d.ts.map +1 -1
  46. package/dest/json-rpc/fixtures/test_state.js +58 -2
  47. package/dest/json-rpc/index.d.ts +1 -2
  48. package/dest/json-rpc/index.d.ts.map +1 -1
  49. package/dest/json-rpc/index.js +2 -3
  50. package/dest/json-rpc/js_utils.d.ts.map +1 -1
  51. package/dest/json-rpc/js_utils.js +2 -1
  52. package/dest/json-rpc/server/index.d.ts +1 -2
  53. package/dest/json-rpc/server/index.d.ts.map +1 -1
  54. package/dest/json-rpc/server/index.js +2 -3
  55. package/dest/json-rpc/server/safe_json_rpc_server.d.ts +112 -0
  56. package/dest/json-rpc/server/safe_json_rpc_server.d.ts.map +1 -0
  57. package/dest/json-rpc/server/safe_json_rpc_server.js +275 -0
  58. package/dest/json-rpc/test/index.d.ts +2 -0
  59. package/dest/json-rpc/test/index.d.ts.map +1 -0
  60. package/dest/json-rpc/test/index.js +2 -0
  61. package/dest/json-rpc/test/integration.d.ts +13 -0
  62. package/dest/json-rpc/test/integration.d.ts.map +1 -0
  63. package/dest/json-rpc/test/integration.js +12 -0
  64. package/dest/log/logger.d.ts.map +1 -1
  65. package/dest/log/logger.js +2 -2
  66. package/dest/schemas/api.d.ts +21 -0
  67. package/dest/schemas/api.d.ts.map +1 -0
  68. package/dest/schemas/api.js +8 -0
  69. package/dest/schemas/index.d.ts +6 -0
  70. package/dest/schemas/index.d.ts.map +1 -0
  71. package/dest/schemas/index.js +6 -0
  72. package/dest/schemas/parse.d.ts +9 -0
  73. package/dest/schemas/parse.d.ts.map +1 -0
  74. package/dest/schemas/parse.js +26 -0
  75. package/dest/schemas/schemas.d.ts +79 -0
  76. package/dest/schemas/schemas.d.ts.map +1 -0
  77. package/dest/schemas/schemas.js +87 -0
  78. package/dest/schemas/types.d.ts +3 -0
  79. package/dest/schemas/types.d.ts.map +1 -0
  80. package/dest/schemas/types.js +2 -0
  81. package/dest/schemas/utils.d.ts +40 -0
  82. package/dest/schemas/utils.d.ts.map +1 -0
  83. package/dest/schemas/utils.js +56 -0
  84. package/dest/string/index.d.ts +7 -0
  85. package/dest/string/index.d.ts.map +1 -0
  86. package/dest/string/index.js +13 -0
  87. package/dest/types/index.d.ts +2 -0
  88. package/dest/types/index.d.ts.map +1 -1
  89. package/dest/validation/index.d.ts +6 -0
  90. package/dest/validation/index.d.ts.map +1 -1
  91. package/dest/validation/index.js +11 -1
  92. package/package.json +7 -4
  93. package/src/abi/abi.ts +203 -233
  94. package/src/abi/encoder.ts +2 -0
  95. package/src/abi/event_selector.ts +7 -0
  96. package/src/aztec-address/index.ts +64 -18
  97. package/src/buffer/buffer32.ts +5 -0
  98. package/src/config/env_var.ts +18 -8
  99. package/src/crypto/index.ts +1 -0
  100. package/src/crypto/keys/index.ts +9 -0
  101. package/src/eth-address/index.ts +2 -6
  102. package/src/eth-signature/eth_signature.ts +8 -0
  103. package/src/fields/fields.ts +2 -3
  104. package/src/json-rpc/client/fetch.ts +81 -0
  105. package/src/json-rpc/client/index.ts +2 -1
  106. package/src/json-rpc/client/safe_json_rpc_client.ts +61 -0
  107. package/src/json-rpc/convert.ts +29 -142
  108. package/src/json-rpc/fixtures/test_state.ts +87 -3
  109. package/src/json-rpc/index.ts +1 -8
  110. package/src/json-rpc/js_utils.ts +1 -0
  111. package/src/json-rpc/server/index.ts +1 -2
  112. package/src/json-rpc/server/safe_json_rpc_server.ts +336 -0
  113. package/src/json-rpc/test/index.ts +1 -0
  114. package/src/json-rpc/test/integration.ts +24 -0
  115. package/src/log/logger.ts +2 -1
  116. package/src/schemas/api.ts +47 -0
  117. package/src/schemas/index.ts +5 -0
  118. package/src/schemas/parse.ts +29 -0
  119. package/src/schemas/schemas.ts +111 -0
  120. package/src/schemas/types.ts +3 -0
  121. package/src/schemas/utils.ts +85 -0
  122. package/src/string/index.ts +15 -0
  123. package/src/types/index.ts +3 -0
  124. package/src/validation/index.ts +11 -0
  125. package/dest/json-rpc/class_converter.d.ts +0 -144
  126. package/dest/json-rpc/class_converter.d.ts.map +0 -1
  127. package/dest/json-rpc/class_converter.js +0 -102
  128. package/dest/json-rpc/client/json_rpc_client.d.ts +0 -35
  129. package/dest/json-rpc/client/json_rpc_client.d.ts.map +0 -1
  130. package/dest/json-rpc/client/json_rpc_client.js +0 -117
  131. package/dest/json-rpc/server/json_proxy.d.ts +0 -30
  132. package/dest/json-rpc/server/json_proxy.d.ts.map +0 -1
  133. package/dest/json-rpc/server/json_proxy.js +0 -46
  134. package/dest/json-rpc/server/json_rpc_server.d.ts +0 -102
  135. package/dest/json-rpc/server/json_rpc_server.d.ts.map +0 -1
  136. package/dest/json-rpc/server/json_rpc_server.js +0 -265
  137. package/src/json-rpc/class_converter.ts +0 -213
  138. package/src/json-rpc/client/json_rpc_client.ts +0 -148
  139. package/src/json-rpc/server/json_proxy.ts +0 -60
  140. package/src/json-rpc/server/json_rpc_server.ts +0 -332
@@ -1,3 +1,6 @@
1
+ import { z } from 'zod';
2
+
3
+ import { type ApiSchemaFor, optional, schemas } from '../../schemas/index.js';
1
4
  import { sleep } from '../../sleep/index.js';
2
5
 
3
6
  /**
@@ -5,6 +8,15 @@ import { sleep } from '../../sleep/index.js';
5
8
  */
6
9
  export class TestNote {
7
10
  constructor(private data: string) {}
11
+
12
+ static get schema() {
13
+ return z.object({ data: z.string() }).transform(({ data }) => new TestNote(data));
14
+ }
15
+
16
+ toJSON() {
17
+ return { data: this.data };
18
+ }
19
+
8
20
  /**
9
21
  * Create a string representation of this class.
10
22
  * @returns The string representation.
@@ -22,13 +34,26 @@ export class TestNote {
22
34
  }
23
35
  }
24
36
 
37
+ export interface TestStateApi {
38
+ getNote: (index: number) => Promise<TestNote | undefined>;
39
+ getNotes: (limit?: number) => Promise<TestNote[]>;
40
+ getNotes2: (limit: bigint | undefined) => Promise<TestNote[]>;
41
+ getNotes3: (limit?: number) => Promise<TestNote[]>;
42
+ clear: () => Promise<void>;
43
+ addNotes: (notes: TestNote[]) => Promise<TestNote[]>;
44
+ fail: () => Promise<void>;
45
+ count: () => Promise<number>;
46
+ getStatus: () => Promise<{ status: string; count: bigint }>;
47
+ getTuple(): Promise<[string, string | undefined, number]>;
48
+ }
49
+
25
50
  /**
26
51
  * Represents a simple state management for TestNote instances.
27
52
  * Provides functionality to get a note by index and add notes asynchronously.
28
53
  * Primarily used for testing JSON RPC-related functionalities.
29
54
  */
30
- export class TestState {
31
- constructor(private notes: TestNote[]) {}
55
+ export class TestState implements TestStateApi {
56
+ constructor(public notes: TestNote[]) {}
32
57
  /**
33
58
  * Retrieve the TestNote instance at the specified index from the notes array.
34
59
  * This method allows getting a desired TestNote from the collection of notes
@@ -37,9 +62,40 @@ export class TestState {
37
62
  * @param index - The index of the TestNote to be retrieved from the notes array.
38
63
  * @returns The TestNote instance corresponding to the given index.
39
64
  */
40
- getNote(index: number): TestNote {
65
+ async getNote(index: number): Promise<TestNote> {
66
+ await sleep(0.1);
41
67
  return this.notes[index];
42
68
  }
69
+
70
+ fail(): Promise<void> {
71
+ throw new Error('Test state failed');
72
+ }
73
+
74
+ async count(): Promise<number> {
75
+ await sleep(0.1);
76
+ return this.notes.length;
77
+ }
78
+
79
+ async getNotes(limit?: number): Promise<TestNote[]> {
80
+ await sleep(0.1);
81
+ return limit ? this.notes.slice(0, limit) : this.notes;
82
+ }
83
+
84
+ async getNotes2(limit: bigint | undefined): Promise<TestNote[]> {
85
+ await sleep(0.1);
86
+ return limit ? this.notes.slice(0, Number(limit)) : this.notes;
87
+ }
88
+
89
+ async getNotes3(limit = 1): Promise<TestNote[]> {
90
+ await sleep(0.1);
91
+ return limit ? this.notes.slice(0, Number(limit)) : this.notes;
92
+ }
93
+
94
+ async clear(): Promise<void> {
95
+ await sleep(0.1);
96
+ this.notes = [];
97
+ }
98
+
43
99
  /**
44
100
  * Add an array of TestNote instances to the current TestState's notes.
45
101
  * This function simulates asynchronous behavior by waiting for a duration
@@ -56,4 +112,32 @@ export class TestState {
56
112
  await sleep(notes.length);
57
113
  return this.notes;
58
114
  }
115
+
116
+ async forceClear() {
117
+ await sleep(0.1);
118
+ this.notes = [];
119
+ }
120
+
121
+ async getStatus(): Promise<{ status: string; count: bigint }> {
122
+ await sleep(0.1);
123
+ return { status: 'ok', count: BigInt(this.notes.length) };
124
+ }
125
+
126
+ async getTuple(): Promise<[string, string | undefined, number]> {
127
+ await sleep(0.1);
128
+ return ['a', undefined, 1];
129
+ }
59
130
  }
131
+
132
+ export const TestStateSchema: ApiSchemaFor<TestStateApi> = {
133
+ getNote: z.function().args(z.number()).returns(TestNote.schema.optional()),
134
+ getNotes: z.function().args(optional(schemas.Integer)).returns(z.array(TestNote.schema)),
135
+ getNotes2: z.function().args(optional(schemas.BigInt)).returns(z.array(TestNote.schema)),
136
+ getNotes3: z.function().args(optional(schemas.Integer)).returns(z.array(TestNote.schema)),
137
+ clear: z.function().returns(z.void()),
138
+ addNotes: z.function().args(z.array(TestNote.schema)).returns(z.array(TestNote.schema)),
139
+ fail: z.function().returns(z.void()),
140
+ count: z.function().returns(z.number()),
141
+ getStatus: z.function().returns(z.object({ status: z.string(), count: schemas.BigInt })),
142
+ getTuple: z.function().returns(z.tuple([z.string(), optional(z.string()), z.number()])),
143
+ };
@@ -1,8 +1 @@
1
- export {
2
- StringClassConverterInput,
3
- JsonClassConverterInput as ObjClassConverterInput,
4
- JsonEncodedClass,
5
- ClassConverter,
6
- } from './class_converter.js';
7
-
8
- export { JsonStringify } from './convert.js';
1
+ export { jsonStringify } from './convert.js';
@@ -1,4 +1,5 @@
1
1
  // Make sure this property was not inherited
2
+ // TODO(palla): Delete this file
2
3
 
3
4
  /**
4
5
  * Does this own the property?
@@ -1,2 +1 @@
1
- export * from './json_rpc_server.js';
2
- export { JsonProxy } from './json_proxy.js';
1
+ export * from './safe_json_rpc_server.js';
@@ -0,0 +1,336 @@
1
+ import cors from '@koa/cors';
2
+ import http from 'http';
3
+ import Koa from 'koa';
4
+ import bodyParser from 'koa-bodyparser';
5
+ import compress from 'koa-compress';
6
+ import Router from 'koa-router';
7
+ import { type AddressInfo } from 'net';
8
+ import { format, inspect } from 'util';
9
+ import { ZodError } from 'zod';
10
+
11
+ import { type DebugLogger, createDebugLogger } from '../../log/index.js';
12
+ import { promiseWithResolvers } from '../../promise/utils.js';
13
+ import { type ApiSchema, type ApiSchemaFor, parseWithOptionals, schemaHasMethod } from '../../schemas/index.js';
14
+ import { jsonStringify } from '../convert.js';
15
+ import { assert } from '../js_utils.js';
16
+
17
+ export class SafeJsonRpcServer {
18
+ /**
19
+ * The HTTP server accepting remote requests.
20
+ * This member field is initialized when the server is started.
21
+ */
22
+ private httpServer?: http.Server;
23
+
24
+ constructor(
25
+ /** The proxy object to delegate requests to. */
26
+ private readonly proxy: Proxy,
27
+ /** Health check function */
28
+ private readonly healthCheck: StatusCheckFn = () => true,
29
+ /** Logger */
30
+ private log = createDebugLogger('json-rpc:server'),
31
+ ) {}
32
+
33
+ public isHealthy(): boolean | Promise<boolean> {
34
+ return this.healthCheck();
35
+ }
36
+
37
+ /**
38
+ * Get an express app object.
39
+ * @param prefix - Our server prefix.
40
+ * @returns The app object.
41
+ */
42
+ public getApp(prefix = '') {
43
+ const router = this.getRouter(prefix);
44
+
45
+ const exceptionHandler = async (ctx: Koa.Context, next: () => Promise<void>) => {
46
+ try {
47
+ await next();
48
+ } catch (err: any) {
49
+ const method = (ctx.request.body as any)?.method ?? 'unknown';
50
+ this.log.warn(`Error in JSON RPC server call ${method}: ${inspect(err)}`);
51
+ if (err instanceof SyntaxError) {
52
+ ctx.status = 400;
53
+ ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32700, message: `Parse error: ${err.message}` } };
54
+ } else if (err instanceof ZodError) {
55
+ const message = err.issues.map(e => `${e.message} (${e.path.join('.')})`).join('. ') || 'Validation error';
56
+ ctx.status = 400;
57
+ ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32701, message } };
58
+ } else {
59
+ ctx.status = 500;
60
+ ctx.body = { jsonrpc: '2.0', id: null, error: { code: -32600, message: err.message ?? 'Internal error' } };
61
+ }
62
+ }
63
+ };
64
+
65
+ const jsonResponse = async (ctx: Koa.Context, next: () => Promise<void>) => {
66
+ try {
67
+ await next();
68
+ if (ctx.body && typeof ctx.body === 'object') {
69
+ ctx.body = jsonStringify(ctx.body);
70
+ }
71
+ } catch (err: any) {
72
+ ctx.status = 500;
73
+ ctx.body = { jsonrpc: '2.0', error: { code: -32700, message: `Unable to serialize response: ${err.message}` } };
74
+ }
75
+ };
76
+
77
+ const app = new Koa();
78
+ app.on('error', error => {
79
+ this.log.error(`Error on API handler: ${error}`);
80
+ });
81
+
82
+ app.use(compress({ br: false } as any));
83
+ app.use(jsonResponse);
84
+ app.use(exceptionHandler);
85
+ app.use(bodyParser({ jsonLimit: '50mb', enableTypes: ['json'], detectJSON: () => true }));
86
+ app.use(cors());
87
+ app.use(router.routes());
88
+ app.use(router.allowedMethods());
89
+
90
+ return app;
91
+ }
92
+
93
+ /**
94
+ * Get a router object wrapping our RPC class.
95
+ * @param prefix - The server prefix.
96
+ * @returns The router object.
97
+ */
98
+ private getRouter(prefix: string) {
99
+ const router = new Router({ prefix });
100
+ // "JSON RPC mode" where a single endpoint is used and the method is given in the request body
101
+ router.post('/', async (ctx: Koa.Context) => {
102
+ const { params = [], jsonrpc, id, method } = ctx.request.body as any;
103
+ // Fail if not a registered function in the proxy
104
+ if (typeof method !== 'string' || method === 'constructor' || !this.proxy.hasMethod(method)) {
105
+ ctx.status = 400;
106
+ ctx.body = { jsonrpc, id, error: { code: -32601, message: `Method not found: ${method}` } };
107
+ } else {
108
+ const result = await this.proxy.call(method, params);
109
+ ctx.body = { jsonrpc, id, result };
110
+ ctx.status = 200;
111
+ }
112
+ });
113
+
114
+ return router;
115
+ }
116
+
117
+ /**
118
+ * Start this server with koa.
119
+ * @param port - Port number.
120
+ * @param prefix - Prefix string.
121
+ */
122
+ public start(port: number, prefix = ''): void {
123
+ if (this.httpServer) {
124
+ throw new Error('Server is already listening');
125
+ }
126
+
127
+ this.httpServer = http.createServer(this.getApp(prefix).callback());
128
+ this.httpServer.listen(port);
129
+ }
130
+
131
+ /**
132
+ * Stops the HTTP server
133
+ */
134
+ public stop(): Promise<void> {
135
+ if (!this.httpServer) {
136
+ return Promise.resolve();
137
+ }
138
+
139
+ const { promise, resolve, reject } = promiseWithResolvers<void>();
140
+ this.httpServer.close(err => {
141
+ if (err) {
142
+ reject(err);
143
+ } else {
144
+ resolve();
145
+ }
146
+ });
147
+ return promise;
148
+ }
149
+
150
+ /**
151
+ * Explicitly calls an RPC method.
152
+ * @param methodName - The RPC method.
153
+ * @param jsonParams - The RPC parameters.
154
+ * @returns The remote result.
155
+ */
156
+ public async call(methodName: string, jsonParams: any[] = []) {
157
+ return await this.proxy.call(methodName, jsonParams);
158
+ }
159
+ }
160
+
161
+ export type StatusCheckFn = () => boolean | Promise<boolean>;
162
+
163
+ interface Proxy {
164
+ hasMethod(methodName: string): boolean;
165
+ call(methodName: string, jsonParams?: any[]): Promise<any>;
166
+ }
167
+
168
+ /**
169
+ * Forwards calls to a handler. Relies on a schema definition to validate and convert inputs
170
+ * before forwarding calls, and then converts outputs into JSON using default conversions.
171
+ */
172
+ export class SafeJsonProxy<T extends object = any> implements Proxy {
173
+ private log = createDebugLogger('json-rpc:proxy');
174
+ private schema: ApiSchema;
175
+
176
+ constructor(private handler: T, schema: ApiSchemaFor<T>) {
177
+ this.schema = schema;
178
+ }
179
+
180
+ /**
181
+ * Call an RPC method.
182
+ * @param methodName - The RPC method.
183
+ * @param jsonParams - The RPC parameters.
184
+ * @returns The remote result.
185
+ */
186
+ public async call(methodName: string, jsonParams: any[] = []) {
187
+ this.log.debug(format(`request`, methodName, jsonParams));
188
+
189
+ assert(Array.isArray(jsonParams), `Params to ${methodName} is not an array: ${jsonParams}`);
190
+ assert(schemaHasMethod(this.schema, methodName), `Method ${methodName} not found in schema`);
191
+ const method = this.handler[methodName as keyof T];
192
+ assert(typeof method === 'function', `Method ${methodName} is not a function`);
193
+ const args = parseWithOptionals(jsonParams, this.schema[methodName].parameters());
194
+ const ret = await method.apply(this.handler, args);
195
+ this.log.debug(format('response', methodName, ret));
196
+ return ret;
197
+ }
198
+
199
+ public hasMethod(methodName: string): boolean {
200
+ return schemaHasMethod(this.schema, methodName) && typeof this.handler[methodName as keyof T] === 'function';
201
+ }
202
+ }
203
+
204
+ class NamespacedSafeJsonProxy implements Proxy {
205
+ private readonly proxies: Record<string, Proxy> = {};
206
+
207
+ constructor(handlers: NamespacedApiHandlers) {
208
+ for (const [namespace, [handler, schema]] of Object.entries(handlers)) {
209
+ this.proxies[namespace] = new SafeJsonProxy(handler, schema);
210
+ }
211
+ }
212
+
213
+ public call(namespacedMethodName: string, jsonParams: any[] = []) {
214
+ const [namespace, methodName] = namespacedMethodName.split('_', 2);
215
+ assert(namespace && methodName, `Invalid namespaced method name: ${namespacedMethodName}`);
216
+ const handler = this.proxies[namespace];
217
+ assert(handler, `Namespace not found: ${namespace}`);
218
+ return handler.call(methodName, jsonParams);
219
+ }
220
+
221
+ public hasMethod(namespacedMethodName: string): boolean {
222
+ const [namespace, methodName] = namespacedMethodName.split('_', 2);
223
+ const handler = this.proxies[namespace];
224
+ return handler?.hasMethod(methodName);
225
+ }
226
+ }
227
+
228
+ export type NamespacedApiHandlers = Record<string, ApiHandler>;
229
+
230
+ export type ApiHandler<T extends object = any> = [T, ApiSchemaFor<T>, StatusCheckFn?];
231
+
232
+ export function makeHandler<T extends object>(handler: T, schema: ApiSchemaFor<T>): ApiHandler<T> {
233
+ return [handler, schema];
234
+ }
235
+
236
+ function makeAggregateHealthcheck(namedHandlers: NamespacedApiHandlers, log?: DebugLogger): StatusCheckFn {
237
+ return async () => {
238
+ try {
239
+ const results = await Promise.all(
240
+ Object.entries(namedHandlers).map(([name, [, , healthCheck]]) => [name, healthCheck ? healthCheck() : true]),
241
+ );
242
+ const failed = results.filter(([_, result]) => !result);
243
+ if (failed.length > 0) {
244
+ log?.warn(`Health check failed for ${failed.map(([name]) => name).join(', ')}`);
245
+ return false;
246
+ }
247
+ return true;
248
+ } catch (err) {
249
+ log?.error(`Error during health check`, err);
250
+ return false;
251
+ }
252
+ };
253
+ }
254
+
255
+ /**
256
+ * Creates a single SafeJsonRpcServer from multiple handlers.
257
+ * @param servers - List of handlers to be combined.
258
+ * @returns A single JsonRpcServer with namespaced methods.
259
+ */
260
+ export function createNamespacedSafeJsonRpcServer(
261
+ handlers: NamespacedApiHandlers,
262
+ log = createDebugLogger('json-rpc:server'),
263
+ ): SafeJsonRpcServer {
264
+ const proxy = new NamespacedSafeJsonProxy(handlers);
265
+ const healthCheck = makeAggregateHealthcheck(handlers, log);
266
+ return new SafeJsonRpcServer(proxy, healthCheck, log);
267
+ }
268
+
269
+ export function createSafeJsonRpcServer<T extends object = any>(
270
+ handler: T,
271
+ schema: ApiSchemaFor<T>,
272
+ healthCheck?: StatusCheckFn,
273
+ ) {
274
+ const proxy = new SafeJsonProxy(handler, schema);
275
+ return new SafeJsonRpcServer(proxy, healthCheck);
276
+ }
277
+
278
+ /**
279
+ * Creates a router for handling a plain status request that will return 200 status when running.
280
+ * @param getCurrentStatus - List of health check functions to run.
281
+ * @param apiPrefix - The prefix to use for all api requests
282
+ * @returns - The router for handling status requests.
283
+ */
284
+ export function createStatusRouter(getCurrentStatus: StatusCheckFn, apiPrefix = '') {
285
+ const router = new Router({ prefix: `${apiPrefix}` });
286
+ router.get('/status', async (ctx: Koa.Context) => {
287
+ let ok: boolean;
288
+ try {
289
+ ok = (await getCurrentStatus()) === true;
290
+ } catch (err) {
291
+ ok = false;
292
+ }
293
+
294
+ ctx.status = ok ? 200 : 500;
295
+ });
296
+ return router;
297
+ }
298
+
299
+ /**
300
+ * Wraps a JsonRpcServer in a nodejs http server and starts it.
301
+ * Installs a status router that calls to the isHealthy method to the server.
302
+ * Returns once starts listening unless noWait is set.
303
+ * @returns A running http server.
304
+ */
305
+ export async function startHttpRpcServer(
306
+ rpcServer: Pick<SafeJsonRpcServer, 'getApp' | 'isHealthy'>,
307
+ options: {
308
+ host?: string;
309
+ port?: number | string;
310
+ apiPrefix?: string;
311
+ timeoutMs?: number;
312
+ noWait?: boolean;
313
+ } = {},
314
+ ): Promise<http.Server & { port: number }> {
315
+ const app = rpcServer.getApp(options.apiPrefix);
316
+
317
+ const statusRouter = createStatusRouter(rpcServer.isHealthy.bind(rpcServer), options.apiPrefix);
318
+ app.use(statusRouter.routes()).use(statusRouter.allowedMethods());
319
+
320
+ const httpServer = http.createServer(app.callback());
321
+ if (options.timeoutMs) {
322
+ httpServer.timeout = options.timeoutMs;
323
+ }
324
+
325
+ const { promise, resolve } = promiseWithResolvers<void>();
326
+ const listenPort = options.port ? (typeof options.port === 'string' ? parseInt(options.port) : options.port) : 0;
327
+ httpServer.listen(listenPort, options.host, () => resolve());
328
+
329
+ // Wait until listen callback is called
330
+ if (!options.noWait) {
331
+ await promise;
332
+ }
333
+
334
+ const port = (httpServer.address() as AddressInfo).port;
335
+ return Object.assign(httpServer, { port });
336
+ }
@@ -0,0 +1 @@
1
+ export { createJsonRpcTestSetup, type JsonRpcTestContext } from './integration.js';
@@ -0,0 +1,24 @@
1
+ import type http from 'http';
2
+
3
+ import { type ApiSchemaFor } from '../../schemas/api.js';
4
+ import { makeFetch } from '../client/fetch.js';
5
+ import { createSafeJsonRpcClient } from '../client/safe_json_rpc_client.js';
6
+ import { startHttpRpcServer } from '../server/safe_json_rpc_server.js';
7
+ import { type SafeJsonRpcServer, createSafeJsonRpcServer } from '../server/safe_json_rpc_server.js';
8
+
9
+ export type JsonRpcTestContext<T extends object> = {
10
+ server: SafeJsonRpcServer;
11
+ client: T;
12
+ httpServer: http.Server & { port: number };
13
+ };
14
+
15
+ export async function createJsonRpcTestSetup<T extends object>(
16
+ handler: T,
17
+ schema: ApiSchemaFor<T>,
18
+ ): Promise<JsonRpcTestContext<T>> {
19
+ const server = createSafeJsonRpcServer<T>(handler, schema);
20
+ const httpServer = await startHttpRpcServer(server, { host: '127.0.0.1' });
21
+ const noRetryFetch = makeFetch([], true);
22
+ const client = createSafeJsonRpcClient<T>(`http://127.0.0.1:${httpServer.port}`, schema, false, false, noRetryFetch);
23
+ return { server, client, httpServer };
24
+ }
package/src/log/logger.ts CHANGED
@@ -12,7 +12,8 @@ export type LogLevel = (typeof LogLevels)[number];
12
12
 
13
13
  function getLogLevel() {
14
14
  const envLogLevel = process.env.LOG_LEVEL?.toLowerCase() as LogLevel;
15
- const defaultNonTestLogLevel = process.env.DEBUG === undefined ? ('info' as const) : ('debug' as const);
15
+ const defaultNonTestLogLevel =
16
+ process.env.DEBUG === undefined || process.env.DEBUG === '' ? ('info' as const) : ('debug' as const);
16
17
  const defaultLogLevel = process.env.NODE_ENV === 'test' ? ('silent' as const) : defaultNonTestLogLevel;
17
18
  return LogLevels.includes(envLogLevel) ? envLogLevel : defaultLogLevel;
18
19
  }
@@ -0,0 +1,47 @@
1
+ import { type z } from 'zod';
2
+
3
+ import { type ZodNullableOptional } from './utils.js';
4
+
5
+ // Forces usage of ZodNullableOptional in parameters schemas so we properly accept nulls for optional parameters.
6
+ type ZodParameterTypeFor<T> = undefined extends T
7
+ ? ZodNullableOptional<z.ZodType<T, z.ZodTypeDef, any>>
8
+ : z.ZodType<T, z.ZodTypeDef, any>;
9
+
10
+ type ZodReturnTypeFor<T> = z.ZodType<T, z.ZodTypeDef, any>;
11
+
12
+ // This monstruosity is used for mapping function arguments to their schema representation.
13
+ // The complexity is required to satisfy ZodTuple which requires a fixed length tuple and
14
+ // has a very annoying type of [] | [ZodTypeAny, ...ZodTypeAny], and most types fail to match
15
+ // the second option. While a purely recursive approach works, it fails when trying to deal
16
+ // with optional arguments (ie optional items in the tuple), and ts does not really like them
17
+ // during a recursion and fails with infinite stack depth.
18
+ // This type appears to satisfy everyone. Everyone but me.
19
+ type ZodMapParameterTypes<T> = T extends []
20
+ ? []
21
+ : T extends [item: infer Head, ...infer Rest]
22
+ ? [ZodParameterTypeFor<Head>, ...{ [K in keyof Rest]: ZodParameterTypeFor<Rest[K]> }]
23
+ : T extends [item?: infer Head, ...infer Rest]
24
+ ? [ZodNullableOptional<ZodParameterTypeFor<Head>>, ...{ [K in keyof Rest]: ZodParameterTypeFor<Rest[K]> }]
25
+ : never;
26
+
27
+ /** Maps all functions in an interface to their schema representation. */
28
+ export type ApiSchemaFor<T> = {
29
+ [K in keyof T]: T[K] extends (...args: infer Args) => Promise<infer Ret>
30
+ ? z.ZodFunction<z.ZodTuple<ZodMapParameterTypes<Args>, z.ZodUnknown>, ZodReturnTypeFor<Ret>>
31
+ : never;
32
+ };
33
+
34
+ /** Generic Api schema not bounded to a specific implementation. */
35
+ export type ApiSchema = {
36
+ [key: string]: z.ZodFunction<z.ZodTuple<any, any>, z.ZodTypeAny>;
37
+ };
38
+
39
+ /** Return whether an API schema defines a valid function schema for a given method name. */
40
+ export function schemaHasMethod(schema: ApiSchema, methodName: string) {
41
+ return (
42
+ typeof methodName === 'string' &&
43
+ Object.hasOwn(schema, methodName) &&
44
+ typeof schema[methodName].parameters === 'function' &&
45
+ typeof schema[methodName].returnType === 'function'
46
+ );
47
+ }
@@ -0,0 +1,5 @@
1
+ export * from './api.js';
2
+ export * from './parse.js';
3
+ export * from './schemas.js';
4
+ export * from './utils.js';
5
+ export * from './types.js';
@@ -0,0 +1,29 @@
1
+ import { z } from 'zod';
2
+
3
+ import { times } from '../collection/array.js';
4
+
5
+ /** Parses the given arguments using a tuple from the provided schemas. */
6
+ export function parse<T extends [] | [z.ZodTypeAny, ...z.ZodTypeAny[]]>(args: IArguments, ...schemas: T) {
7
+ return z.tuple(schemas).parse(args);
8
+ }
9
+
10
+ /**
11
+ * Parses the given arguments against a tuple, allowing empty for optional items.
12
+ * @dev Zod doesn't like tuplues with optional items. See https://github.com/colinhacks/zod/discussions/949.
13
+ */
14
+ export function parseWithOptionals<T extends z.AnyZodTuple>(args: any[], schema: T): T['_output'] {
15
+ const missingCount = schema.items.length - args.length;
16
+ const optionalCount = schema.items.filter(isOptional).length;
17
+ const toParse =
18
+ missingCount > 0 && missingCount <= optionalCount ? args.concat(times(missingCount, () => undefined)) : args;
19
+ return schema.parse(toParse);
20
+ }
21
+
22
+ function isOptional(schema: z.ZodTypeAny) {
23
+ try {
24
+ return schema.isOptional();
25
+ } catch (err) {
26
+ // See https://github.com/colinhacks/zod/issues/1911
27
+ return schema._def.typeName === 'ZodOptional';
28
+ }
29
+ }