@ethernity-cloud/sdk-js 1.5.9 → 1.5.12

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/test.js CHANGED
@@ -1,204 +1,286 @@
1
- #!/usr/bin/env node
2
- /* ecld-test — run a Nodenithy payload locally, without SGX, exactly as the
3
- * enclave would.
4
- *
5
- * Stages the project's src/serverless next to the vendored securelock
6
- * executor (the same layout ecld-build produces inside the enclave image) and
7
- * executes the payload with that executor: same backend reflection, same
8
- * ___etny_result___ / ___etny_data_set___ scope, same TaskStatus codes.
9
- * Validates the CALL — names, arguments, result wiring — before anything is
10
- * paid for on-chain. It does not validate attestation, checksums or
11
- * encryption; those need a real (testnet) run.
12
- *
13
- * Usage, from the project root:
14
- *
15
- * npx ecld-test 'hello("World")' one-shot, in-process
16
- * npx ecld-test --file payload.js
17
- * npx ecld-test --input data.json 'processData(___etny_data_set___)'
18
- * npx ecld-test --input-text '{"x":1}' 'processData(___etny_data_set___)'
19
- *
20
- * npx ecld-test serve [--port 8745] local API for runner LOCAL mode
21
- *
22
- * `serve` starts the local test API so a dApp can exercise its real runner
23
- * integration end to end: new EthernityCloudRunner('LOCAL') in
24
- * @ethernity-cloud/runner >= 0.3.6 executes every run() against this API
25
- * instead of the blockchain.
26
- *
27
- * Exit code 0 on TaskStatus SUCCESS, 1 otherwise.
28
- */
29
-
30
- const fs = require('fs');
31
- const os = require('os');
32
- const path = require('path');
33
- const http = require('http');
34
- const crypto = require('crypto');
35
-
36
- const TASK_STATUS_NAMES = {
37
- 0: 'SUCCESS',
38
- 1: 'SYSTEM_ERROR',
39
- 2: 'KEY_ERROR',
40
- 3: 'SYNTAX_WARNING',
41
- 4: 'BASE_EXCEPTION',
42
- 5: 'PAYLOAD_NOT_DEFINED',
43
- 6: 'PAYLOAD_CHECKSUM_ERROR',
44
- 7: 'INPUT_CHECKSUM_ERROR',
45
- 8: 'EVAL_ERROR',
46
- 28: 'IMPORT_ERROR',
47
- 32: 'CONFIG_ERROR',
48
- };
49
-
50
- const VENDORED_SRC = path.join(__dirname, 'nodenithy', 'build', 'securelock', 'src');
51
-
52
- function copyDir(src, dest) {
53
- fs.mkdirSync(dest, { recursive: true });
54
- for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
55
- const from = path.join(src, entry.name);
56
- const to = path.join(dest, entry.name);
57
- if (entry.isDirectory()) copyDir(from, to);
58
- else fs.copyFileSync(from, to);
59
- }
60
- }
61
-
62
- /* Stage the executor + the project's backend the way ecld-build does inside
63
- * the enclave image, then load the REAL executor from the staged copy. */
64
- function loadExecutor(projectSrc) {
65
- const stage = fs.mkdtempSync(path.join(os.tmpdir(), 'ecld-test-'));
66
- for (const f of ['etny_exec.js', 'task_status.js']) {
67
- fs.copyFileSync(path.join(VENDORED_SRC, f), path.join(stage, f));
68
- }
69
- const backendDir = path.join(projectSrc, 'serverless');
70
- const backendJs = path.join(backendDir, 'backend.js');
71
- if (fs.existsSync(backendJs)) {
72
- copyDir(backendDir, path.join(stage, 'serverless'));
73
- console.log(`backend : ${backendJs}`);
74
- } else if (fs.existsSync(path.join(backendDir, 'backend.py'))) {
75
- console.log('backend : backend.py found — this is a Pynithy project; use');
76
- console.log(' ecld-test from ethernity-cloud-sdk-py instead.');
77
- console.log(' Continuing with bare globals (stock-enclave mode).');
78
- } else {
79
- console.log('backend : none found — running with bare globals, like a stock enclave');
80
- }
81
- return require(path.join(stage, 'etny_exec.js'));
82
- }
83
-
84
- function runTask(executor, payload, input) {
85
- const [code, resultRaw] = executor.executeTask(payload, input || '');
86
- const result = typeof resultRaw === 'string' ? resultRaw : JSON.stringify(resultRaw);
87
- const checksum = crypto.createHash('sha256').update(result, 'utf8').digest('hex');
88
- const challenge = Array.from({ length: 20 }, () =>
89
- 'abcdefghijklmnopqrstuvwxyz0123456789'[Math.floor(Math.random() * 36)]
90
- ).join('');
91
- return {
92
- task_code: code,
93
- task_code_name: TASK_STATUS_NAMES[code] || `UNKNOWN_${code}`,
94
- result,
95
- checksum,
96
- enclave_challenge: challenge,
97
- result_string: `v3:${code}:${checksum}:${challenge}:`,
98
- };
99
- }
100
-
101
- function serve(executor, host, port) {
102
- const server = http.createServer((req, res) => {
103
- const send = (status, obj) => {
104
- const body = JSON.stringify(obj);
105
- res.writeHead(status, {
106
- 'Content-Type': 'application/json',
107
- 'Content-Length': Buffer.byteLength(body),
108
- 'Access-Control-Allow-Origin': '*',
109
- 'Access-Control-Allow-Headers': 'Content-Type',
110
- });
111
- res.end(body);
112
- };
113
- if (req.method === 'OPTIONS') return send(204, {});
114
- if (req.method === 'GET' && req.url === '/v1/health') {
115
- // Probe with an expression that lists scope? Keep it simple: report ok.
116
- return send(200, { status: 'ok', backend: serve.backendFunctions || [] });
117
- }
118
- if (req.method === 'POST' && req.url === '/v1/task') {
119
- let raw = '';
120
- req.on('data', (c) => { raw += c; });
121
- req.on('end', () => {
122
- try {
123
- const body = JSON.parse(raw || '{}');
124
- if (typeof body.payload !== 'string' || !body.payload.trim()) {
125
- return send(400, { error: 'payload (string) is required' });
126
- }
127
- let input = body.input;
128
- if (input !== undefined && input !== null && typeof input !== 'string') {
129
- input = JSON.stringify(input);
130
- }
131
- const out = runTask(executor, body.payload, input);
132
- console.log(`[local-api] task -> ${out.task_code} (${out.task_code_name})`);
133
- return send(200, out);
134
- } catch (e) {
135
- if (e instanceof SyntaxError) return send(400, { error: 'invalid JSON body' });
136
- return send(500, { error: e.message });
137
- }
138
- });
139
- return;
140
- }
141
- send(404, { error: 'not found' });
142
- });
143
- server.listen(port, host, () => {
144
- console.log(`[local-api] listening on http://${host}:${port} (POST /v1/task, GET /v1/health)`);
145
- });
146
- }
147
-
148
- function main() {
149
- const argv = process.argv.slice(2);
150
- const opts = { src: 'src', host: '127.0.0.1', port: 8745 };
151
- const positional = [];
152
- for (let i = 0; i < argv.length; i++) {
153
- const a = argv[i];
154
- if (a === '--file' || a === '-f') opts.file = argv[++i];
155
- else if (a === '--input' || a === '-i') opts.inputFile = argv[++i];
156
- else if (a === '--input-text') opts.inputText = argv[++i];
157
- else if (a === '--src') opts.src = argv[++i];
158
- else if (a === '--host') opts.host = argv[++i];
159
- else if (a === '--port') opts.port = parseInt(argv[++i], 10);
160
- else positional.push(a);
161
- }
162
-
163
- const projectSrc = path.resolve(opts.src);
164
- const executor = loadExecutor(projectSrc);
165
- // Record the backend's exported names once for /v1/health.
166
- try {
167
- serve.backendFunctions = Object.keys(require(path.join(projectSrc, 'serverless', 'backend.js')));
168
- } catch (e) {
169
- serve.backendFunctions = [];
170
- }
171
-
172
- if (positional[0] === 'serve') {
173
- return serve(executor, opts.host, opts.port);
174
- }
175
-
176
- let payload;
177
- if (opts.file) payload = fs.readFileSync(opts.file, 'utf8');
178
- else if (positional.length) payload = positional.join(' ');
179
- else {
180
- console.error("usage: ecld-test 'hello(\"World\")' | ecld-test --file payload.js | ecld-test serve");
181
- process.exit(2);
182
- }
183
-
184
- let input = null;
185
- if (opts.inputFile && opts.inputText !== undefined) {
186
- console.error('--input and --input-text are mutually exclusive');
187
- process.exit(2);
188
- }
189
- if (opts.inputFile) input = fs.readFileSync(opts.inputFile, 'utf8');
190
- else if (opts.inputText !== undefined) input = opts.inputText;
191
-
192
- console.log(`payload : ${JSON.stringify(payload)}`);
193
- console.log(`input : ${input === null ? '<empty>' : JSON.stringify(input.slice(0, 80))}`);
194
- const out = runTask(executor, payload, input);
195
- console.log(`task code: ${out.task_code} (${out.task_code_name})`);
196
- console.log(`result : ${JSON.stringify(out.result)}`);
197
- if (out.task_code !== 0) {
198
- console.log('\nThis is exactly what the network would write on-chain — the failed');
199
- console.log('attempt would still cost gas. Fix locally, then submit.');
200
- }
201
- process.exit(out.task_code === 0 ? 0 : 1);
202
- }
203
-
204
- main();
1
+ #!/usr/bin/env node
2
+ /* ecld-test — run a Nodenithy payload locally, without SGX, exactly as the
3
+ * enclave would.
4
+ *
5
+ * Stages the project's src/serverless next to the vendored securelock
6
+ * executor (the same layout ecld-build produces inside the enclave image) and
7
+ * executes the payload with that executor: same backend reflection, same
8
+ * ___etny_result___ / ___etny_data_set___ scope, same TaskStatus codes.
9
+ * Validates the CALL — names, arguments, result wiring — before anything is
10
+ * paid for on-chain. It does not validate attestation, checksums or
11
+ * encryption; those need a real (testnet) run.
12
+ *
13
+ * Usage, from the project root:
14
+ *
15
+ * npx ecld-test 'hello("World")' one-shot, in-process
16
+ * npx ecld-test --file payload.js
17
+ * npx ecld-test --input data.json 'processData(___etny_data_set___)'
18
+ * npx ecld-test --input-text '{"x":1}' 'processData(___etny_data_set___)'
19
+ *
20
+ * npx ecld-test serve [--port 8745] local API for runner LOCAL mode
21
+ *
22
+ * `serve` starts the local test API so a dApp can exercise its real runner
23
+ * integration end to end: new EthernityCloudRunner('LOCAL') in
24
+ * @ethernity-cloud/runner >= 0.3.6 executes every run() against this API
25
+ * instead of the blockchain.
26
+ *
27
+ * ESR (Enclave State Registry) is emulated locally and ON by default: a
28
+ * state-using backend -- `require('../ecld_state').StateRegistry`, get/commit,
29
+ * the ownership/ACL API -- runs fully in-process against an in-memory registry
30
+ * + store, with no chain, node, or SGX. State persists between runs in
31
+ * .ecld-esr-local.*.json so a counter advances exactly as it would on-chain.
32
+ * The task caller (the DO owner the trustedzone attests) defaults to your
33
+ * developer address and is set for every task; override with --caller.
34
+ *
35
+ * Exit code 0 on TaskStatus SUCCESS, 1 otherwise.
36
+ */
37
+
38
+ const fs = require('fs');
39
+ const os = require('os');
40
+ const path = require('path');
41
+ const http = require('http');
42
+ const crypto = require('crypto');
43
+ const { ethers } = require('ethers');
44
+
45
+ const TASK_STATUS_NAMES = {
46
+ 0: 'SUCCESS',
47
+ 1: 'SYSTEM_ERROR',
48
+ 2: 'KEY_ERROR',
49
+ 3: 'SYNTAX_WARNING',
50
+ 4: 'BASE_EXCEPTION',
51
+ 5: 'PAYLOAD_NOT_DEFINED',
52
+ 6: 'PAYLOAD_CHECKSUM_ERROR',
53
+ 7: 'INPUT_CHECKSUM_ERROR',
54
+ 8: 'EVAL_ERROR',
55
+ 28: 'IMPORT_ERROR',
56
+ 32: 'CONFIG_ERROR',
57
+ 33: 'EXECUTION_TIMEOUT',
58
+ 34: 'ESR_GAS_LIMIT_EXCEEDED',
59
+ 35: 'SECURITY_VIOLATION',
60
+ 36: 'ESR_NONCE_VIOLATION',
61
+ };
62
+
63
+ const VENDORED_SRC = path.join(__dirname, 'nodenithy', 'build', 'securelock', 'src');
64
+
65
+ function copyDir(src, dest) {
66
+ fs.mkdirSync(dest, { recursive: true });
67
+ for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
68
+ const from = path.join(src, entry.name);
69
+ const to = path.join(dest, entry.name);
70
+ if (entry.isDirectory()) copyDir(from, to);
71
+ else fs.copyFileSync(from, to);
72
+ }
73
+ }
74
+
75
+ /* Stage the executor + the project's backend the way ecld-build does inside
76
+ * the enclave image, then load the REAL executor from the staged copy. */
77
+ function loadExecutor(projectSrc) {
78
+ // Stage INSIDE the SDK tree (not the OS temp dir) so the executor's
79
+ // `require('ethers')` and other runtime deps resolve by walking up to the
80
+ // SDK's own node_modules -- exactly what binary-fs gives the enclave.
81
+ const stageRoot = path.join(VENDORED_SRC, '..', '.ecld-test-stage');
82
+ fs.mkdirSync(stageRoot, { recursive: true });
83
+ const stage = fs.mkdtempSync(path.join(stageRoot, 'run-'));
84
+ const cleanup = () => { try { fs.rmSync(stage, { recursive: true, force: true }); } catch (e) {} };
85
+ process.on('exit', cleanup);
86
+ // `serve` runs until interrupted -- clean the stage on Ctrl-C / kill too.
87
+ for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
88
+ process.on(sig, () => { cleanup(); process.exit(130); });
89
+ }
90
+ // Stage the ENTIRE vendored securelock src so `require('./ecld_state')`,
91
+ // esr_local, and their deps all resolve, exactly as inside the enclave image.
92
+ copyDir(VENDORED_SRC, stage);
93
+ const backendDir = path.join(projectSrc, 'serverless');
94
+ const backendJs = path.join(backendDir, 'backend.js');
95
+ if (fs.existsSync(backendJs)) {
96
+ copyDir(backendDir, path.join(stage, 'serverless'));
97
+ console.log(`backend : ${backendJs}`);
98
+ } else if (fs.existsSync(path.join(backendDir, 'backend.py'))) {
99
+ console.log('backend : backend.py found — this is a Pynithy project; use');
100
+ console.log(' ecld-test from ethernity-cloud-sdk-py instead.');
101
+ console.log(' Continuing with bare globals (stock-enclave mode).');
102
+ } else {
103
+ console.log('backend : none found — running with bare globals, like a stock enclave');
104
+ }
105
+ const executor = require(path.join(stage, 'etny_exec.js'));
106
+ executor.__stageDir = stage;
107
+ return executor;
108
+ }
109
+
110
+ /* The task caller for local runs -- the DEVELOPER'S address, mirroring the
111
+ * trustedzone-attested DO owner the real securelock receives. Resolution:
112
+ * 1. --caller / ECLD_TEST_CALLER
113
+ * (a) WALLET_ADDRESS in .env (written at publish time)
114
+ * (b) derive from ECLD_PRIVATE_KEY
115
+ * else DEFAULT_TEST_CALLER (a well-known dev address). */
116
+ const DEFAULT_TEST_CALLER = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
117
+
118
+ function resolveCaller(explicit) {
119
+ const chosen = explicit || process.env.ECLD_TEST_CALLER;
120
+ if (chosen) return chosen.trim();
121
+ if (process.env.WALLET_ADDRESS) return process.env.WALLET_ADDRESS.trim(); // (a)
122
+ const pk = process.env.ECLD_PRIVATE_KEY; // (b)
123
+ if (pk) {
124
+ try { return new ethers.Wallet(pk.startsWith('0x') ? pk : '0x' + pk).address; }
125
+ catch (e) { /* fall through */ }
126
+ }
127
+ console.log(`[ecld-test] no developer address configured; using default test caller `
128
+ + `${DEFAULT_TEST_CALLER} (set ECLD_TEST_CALLER or WALLET_ADDRESS)`);
129
+ return DEFAULT_TEST_CALLER;
130
+ }
131
+
132
+ /* Install the local ESR emulator + set the task caller (ON by default). */
133
+ function installEsr(executor, caller) {
134
+ try {
135
+ const stage = executor.__stageDir;
136
+ const ecldState = require(path.join(stage, 'ecld_state.js'));
137
+ const esrLocal = require(path.join(stage, 'esr_local.js'));
138
+ esrLocal.install(ecldState, { caller, filePrefix: '.ecld-esr-local' });
139
+ return { ecldState };
140
+ } catch (e) {
141
+ console.log(`esr : emulation unavailable (${e.message})`);
142
+ return null;
143
+ }
144
+ }
145
+
146
+ async function runTask(executor, payload, input, esr, caller) {
147
+ // Set the task caller EXACTLY as the real securelock does after the
148
+ // trustedzone hands it the DO owner -- for every task, ESR or not.
149
+ if (esr && esr.ecldState) {
150
+ try { esr.ecldState.setTaskCaller(caller || null); } catch (e) { /* ignore */ }
151
+ }
152
+ // executeTask is async (ESR reads/commits await); the real securelock awaits
153
+ // it too (securelock.js: `await executeTask(...)`).
154
+ const [code, resultRaw] = await executor.executeTask(payload, input || '');
155
+ const result = typeof resultRaw === 'string' ? resultRaw : JSON.stringify(resultRaw);
156
+ const checksum = crypto.createHash('sha256').update(result, 'utf8').digest('hex');
157
+ const challenge = Array.from({ length: 20 }, () =>
158
+ 'abcdefghijklmnopqrstuvwxyz0123456789'[Math.floor(Math.random() * 36)]
159
+ ).join('');
160
+ return {
161
+ task_code: code,
162
+ task_code_name: TASK_STATUS_NAMES[code] || `UNKNOWN_${code}`,
163
+ result,
164
+ checksum,
165
+ enclave_challenge: challenge,
166
+ result_string: `v3:${code}:${checksum}:${challenge}:`,
167
+ };
168
+ }
169
+
170
+ function serve(executor, host, port, esr, defaultCaller) {
171
+ const server = http.createServer((req, res) => {
172
+ const send = (status, obj) => {
173
+ const body = JSON.stringify(obj);
174
+ res.writeHead(status, {
175
+ 'Content-Type': 'application/json',
176
+ 'Content-Length': Buffer.byteLength(body),
177
+ 'Access-Control-Allow-Origin': '*',
178
+ 'Access-Control-Allow-Headers': 'Content-Type',
179
+ });
180
+ res.end(body);
181
+ };
182
+ if (req.method === 'OPTIONS') return send(204, {});
183
+ if (req.method === 'GET' && req.url === '/v1/health') {
184
+ // Probe with an expression that lists scope? Keep it simple: report ok.
185
+ return send(200, { status: 'ok', backend: serve.backendFunctions || [] });
186
+ }
187
+ if (req.method === 'POST' && req.url === '/v1/task') {
188
+ let raw = '';
189
+ req.on('data', (c) => { raw += c; });
190
+ req.on('end', () => {
191
+ try {
192
+ const body = JSON.parse(raw || '{}');
193
+ if (typeof body.payload !== 'string' || !body.payload.trim()) {
194
+ return send(400, { error: 'payload (string) is required' });
195
+ }
196
+ let input = body.input;
197
+ if (input !== undefined && input !== null && typeof input !== 'string') {
198
+ input = JSON.stringify(input);
199
+ }
200
+ const caller = body.caller !== undefined ? body.caller : defaultCaller;
201
+ runTask(executor, body.payload, input, esr, caller)
202
+ .then((out) => {
203
+ console.log(`[local-api] task -> ${out.task_code} (${out.task_code_name})`);
204
+ send(200, out);
205
+ })
206
+ .catch((e) => send(500, { error: e.message }));
207
+ } catch (e) {
208
+ if (e instanceof SyntaxError) return send(400, { error: 'invalid JSON body' });
209
+ return send(500, { error: e.message });
210
+ }
211
+ });
212
+ return;
213
+ }
214
+ send(404, { error: 'not found' });
215
+ });
216
+ server.listen(port, host, () => {
217
+ console.log(`[local-api] listening on http://${host}:${port} (POST /v1/task, GET /v1/health)`);
218
+ });
219
+ }
220
+
221
+ async function main() {
222
+ const argv = process.argv.slice(2);
223
+ const opts = { src: 'src', host: '127.0.0.1', port: 8745 };
224
+ const positional = [];
225
+ for (let i = 0; i < argv.length; i++) {
226
+ const a = argv[i];
227
+ if (a === '--file' || a === '-f') opts.file = argv[++i];
228
+ else if (a === '--input' || a === '-i') opts.inputFile = argv[++i];
229
+ else if (a === '--input-text') opts.inputText = argv[++i];
230
+ else if (a === '--src') opts.src = argv[++i];
231
+ else if (a === '--host') opts.host = argv[++i];
232
+ else if (a === '--port') opts.port = parseInt(argv[++i], 10);
233
+ else if (a === '--caller') opts.caller = argv[++i];
234
+ else positional.push(a);
235
+ }
236
+
237
+ const projectSrc = path.resolve(opts.src);
238
+ const executor = loadExecutor(projectSrc);
239
+ // Record the backend's exported names once for /v1/health.
240
+ try {
241
+ serve.backendFunctions = Object.keys(require(path.join(projectSrc, 'serverless', 'backend.js')));
242
+ } catch (e) {
243
+ serve.backendFunctions = [];
244
+ }
245
+
246
+ // ESR emulation ON by default so a state-using backend runs locally exactly
247
+ // as in an ESR-enabled enclave (no chain/node/SGX). Caller (default: your dev
248
+ // address) is set for every task, so taskCaller() and the ACL behave as
249
+ // on-chain.
250
+ const caller = resolveCaller(opts.caller);
251
+ const esr = installEsr(executor, caller);
252
+ if (esr) console.log(`esr : local emulation ON caller ${caller}`);
253
+
254
+ if (positional[0] === 'serve') {
255
+ return serve(executor, opts.host, opts.port, esr, caller);
256
+ }
257
+
258
+ let payload;
259
+ if (opts.file) payload = fs.readFileSync(opts.file, 'utf8');
260
+ else if (positional.length) payload = positional.join(' ');
261
+ else {
262
+ console.error("usage: ecld-test 'hello(\"World\")' | ecld-test --file payload.js | ecld-test serve");
263
+ process.exit(2);
264
+ }
265
+
266
+ let input = null;
267
+ if (opts.inputFile && opts.inputText !== undefined) {
268
+ console.error('--input and --input-text are mutually exclusive');
269
+ process.exit(2);
270
+ }
271
+ if (opts.inputFile) input = fs.readFileSync(opts.inputFile, 'utf8');
272
+ else if (opts.inputText !== undefined) input = opts.inputText;
273
+
274
+ console.log(`payload : ${JSON.stringify(payload)}`);
275
+ console.log(`input : ${input === null ? '<empty>' : JSON.stringify(input.slice(0, 80))}`);
276
+ const out = await runTask(executor, payload, input, esr, caller);
277
+ console.log(`task code: ${out.task_code} (${out.task_code_name})`);
278
+ console.log(`result : ${JSON.stringify(out.result)}`);
279
+ if (out.task_code !== 0) {
280
+ console.log('\nThis is exactly what the network would write on-chain — the failed');
281
+ console.log('attempt would still cost gas. Fix locally, then submit.');
282
+ }
283
+ process.exit(out.task_code === 0 ? 0 : 1);
284
+ }
285
+
286
+ main().catch((e) => { console.error(e && e.stack ? e.stack : String(e)); process.exit(1); });