@ethernity-cloud/sdk-js 1.5.9 → 1.5.11
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/README.md +38 -3
- package/nodenithy/build/securelock/src/ecld_state.js +28 -1
- package/nodenithy/build/securelock/src/esr_local.js +161 -0
- package/nodenithy/build/securelock/src/etny_exec.js +8 -3
- package/nodenithy/run.js +0 -0
- package/package.json +16 -12
- package/pynithy/run.js +0 -0
- package/run.js +245 -0
- package/test.js +92 -14
package/README.md
CHANGED
|
@@ -65,11 +65,46 @@ The sdk has been tested on the following operating systems:
|
|
|
65
65
|
```
|
|
66
66
|
Required after build, to build and integrate the secure certificates that will be used during executions and to register the project to the Ethernity Cloud Image Register.
|
|
67
67
|
|
|
68
|
-
- **
|
|
68
|
+
- **Test (local, no chain)**: To run your backend locally with the enclave's own
|
|
69
|
+
executor — no SGX, no gas, instant — run:
|
|
69
70
|
```sh
|
|
70
|
-
|
|
71
|
+
npx ecld-test 'hello("World")'
|
|
72
|
+
npx ecld-test --file payload.js
|
|
73
|
+
npx ecld-test --input data.json 'processData(___etny_data_set___)'
|
|
74
|
+
```
|
|
75
|
+
This validates the *call* (function names, arguments, result wiring) before a
|
|
76
|
+
single on-chain request is paid for. If your dApp uses ESR (Enclave State
|
|
77
|
+
Registry), state is emulated locally and **on by default**: `require('../ecld_state').StateRegistry`
|
|
78
|
+
get/commit, ownership/ACL, and `taskCaller()` all work in-process against an
|
|
79
|
+
in-memory registry, and persist between runs in `.ecld-esr-local.*.json`. The
|
|
80
|
+
task caller defaults to your developer address; override it with `--caller`.
|
|
81
|
+
`npx ecld-test serve` starts a local API so the runner's LOCAL mode drives your
|
|
82
|
+
real integration end to end. Exit code 0 on `SUCCESS`, 1 otherwise.
|
|
83
|
+
|
|
84
|
+
- **Run (on the network)**: To submit a payload to the network and print the
|
|
85
|
+
decrypted result, run:
|
|
86
|
+
```sh
|
|
87
|
+
npx ecld-run 'hello("World")'
|
|
88
|
+
npx ecld-run --file payload.js
|
|
89
|
+
npx ecld-run --input data.json 'processData(___etny_data_set___)'
|
|
90
|
+
npx ecld-run --json 'esrIncrement()'
|
|
91
|
+
```
|
|
92
|
+
This is the network-side sibling of `ecld-test`: instead of executing locally
|
|
93
|
+
it drives the runner end to end — encrypt → IPFS → on-chain request → wait for
|
|
94
|
+
a node → download and decrypt the result. It **costs gas and needs a funded
|
|
95
|
+
key**. Network and enclaves come from `.env` (`BLOCKCHAIN_NETWORK`,
|
|
96
|
+
`PROJECT_NAME`, `TRUSTED_ZONE_IMAGE`), overridable with
|
|
97
|
+
`--network`/`--securelock`/`--trustedzone`; the signing key is `PRIVATE_KEY`
|
|
98
|
+
(or `ECLD_PRIVATE_KEY`) — a funded `0x` key that pays for the order. Resources
|
|
99
|
+
are tunable with `--task-price`/`--cpu`/`--memory`/`--storage`/`--bandwidth`/`--duration`/`--validators`.
|
|
100
|
+
Exit code 0 on a `SUCCESS` task result, 1 otherwise.
|
|
101
|
+
|
|
102
|
+
- **Inspect (read-only)**: To read enclave and on-chain diagnostics — network,
|
|
103
|
+
trustedzone/securelock registration, and ESR state — without spending gas, run:
|
|
104
|
+
```sh
|
|
105
|
+
npx ecld-info
|
|
106
|
+
npx ecld-info esr state <key>
|
|
71
107
|
```
|
|
72
|
-
command to start the demo application and test the integration.
|
|
73
108
|
|
|
74
109
|
## Project Structure
|
|
75
110
|
|
|
@@ -50,6 +50,16 @@ let _bucket = null;
|
|
|
50
50
|
let _contractAddress = null;
|
|
51
51
|
let _provider = null;
|
|
52
52
|
|
|
53
|
+
// LOCAL TESTING ONLY (esr_local). In the real enclave these stay null: the node
|
|
54
|
+
// applies commitFor on-chain and _contract() builds a real ethers.Contract. The
|
|
55
|
+
// local ESR emulator overrides the "contract" with an in-memory object and
|
|
56
|
+
// applies commits itself, so ecld-test runs state-using backends with no chain.
|
|
57
|
+
let _contractOverride = null;
|
|
58
|
+
let _localCommitApply = null;
|
|
59
|
+
|
|
60
|
+
function setContractOverride(obj) { _contractOverride = obj; }
|
|
61
|
+
function setLocalCommitApply(fn) { _localCommitApply = fn; }
|
|
62
|
+
|
|
53
63
|
/**
|
|
54
64
|
* Wire the registry to the enclave's identity, storage and chain access.
|
|
55
65
|
* Called by securelock at task start; payload code never calls this.
|
|
@@ -329,6 +339,8 @@ class StateRegistry {
|
|
|
329
339
|
}
|
|
330
340
|
|
|
331
341
|
_contract(signerOrProvider) {
|
|
342
|
+
// Local emulator: use the in-memory contract object instead of a real one.
|
|
343
|
+
if (_contractOverride) return _contractOverride;
|
|
332
344
|
return new ethers.Contract(_contractAddress, ESR_ABI, signerOrProvider || _provider);
|
|
333
345
|
}
|
|
334
346
|
|
|
@@ -515,6 +527,14 @@ class StateRegistry {
|
|
|
515
527
|
await _swift.putFileContent(
|
|
516
528
|
_bucket, `esr.commit.${Number(relayNonce)}.json`, '',
|
|
517
529
|
Buffer.from(JSON.stringify(auth), 'utf8'));
|
|
530
|
+
|
|
531
|
+
// LOCAL TESTING: with no node to relay commitFor, apply the commit to the
|
|
532
|
+
// in-memory registry now so getState/getVersion reflect it. No-op in the
|
|
533
|
+
// real enclave (hook is null) -- there the node applies it on-chain.
|
|
534
|
+
if (_localCommitApply) {
|
|
535
|
+
await _localCommitApply(enclave, keyHashHex, cid, Number(expectedVersion));
|
|
536
|
+
}
|
|
537
|
+
|
|
518
538
|
return { relayed: true, relayNonce: Number(relayNonce) };
|
|
519
539
|
}
|
|
520
540
|
}
|
|
@@ -600,6 +620,8 @@ async function esrAcl(key) {
|
|
|
600
620
|
return { ...acl };
|
|
601
621
|
}
|
|
602
622
|
|
|
623
|
+
function setTaskCaller(caller) { _taskCaller = normAddr(caller); }
|
|
624
|
+
|
|
603
625
|
module.exports = {
|
|
604
626
|
StateRegistry,
|
|
605
627
|
configure,
|
|
@@ -607,6 +629,7 @@ module.exports = {
|
|
|
607
629
|
looksLikeCID,
|
|
608
630
|
ledgerSnapshot,
|
|
609
631
|
taskCaller,
|
|
632
|
+
setTaskCaller,
|
|
610
633
|
restampLedgerCaller,
|
|
611
634
|
StatePermissionError,
|
|
612
635
|
esrGrant,
|
|
@@ -614,5 +637,9 @@ module.exports = {
|
|
|
614
637
|
esrSetPublicRead,
|
|
615
638
|
esrTransfer,
|
|
616
639
|
esrOwner,
|
|
617
|
-
esrAcl
|
|
640
|
+
esrAcl,
|
|
641
|
+
// local-testing hooks (esr_local only)
|
|
642
|
+
setContractOverride,
|
|
643
|
+
setLocalCommitApply,
|
|
644
|
+
normAddr
|
|
618
645
|
};
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local ESR emulator for `ecld-test` -- run state-using backends with no chain.
|
|
3
|
+
*
|
|
4
|
+
* In-memory stand-ins for the two external things StateRegistry talks to, so
|
|
5
|
+
* get/commit/esrGrant/... behave EXACTLY as on-chain but in-process, instantly,
|
|
6
|
+
* with zero orders and zero gas:
|
|
7
|
+
*
|
|
8
|
+
* MemSwift -- state-blob store (replaces SwiftStream): putFileContent /
|
|
9
|
+
* getFileContentBytes over a Map, optionally mirrored to a JSON
|
|
10
|
+
* file so state survives across `ecld-test` runs.
|
|
11
|
+
* MemContract -- the ESR contract methods StateRegistry calls
|
|
12
|
+
* (getState / getVersion / relayNonce / commitDigest) plus the
|
|
13
|
+
* commit APPLY the node would do on-chain, keyed by
|
|
14
|
+
* (enclave, keyHash) -> { cid, version }.
|
|
15
|
+
*
|
|
16
|
+
* install(ecldState, { caller, filePrefix }) wires these into ecld_state via its
|
|
17
|
+
* local-testing hooks. Used only by the local test harness; the real enclave
|
|
18
|
+
* never requires this file.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
'use strict';
|
|
22
|
+
|
|
23
|
+
const fs = require('fs');
|
|
24
|
+
const { ethers } = require('ethers');
|
|
25
|
+
|
|
26
|
+
function key(enclave, keyHash) {
|
|
27
|
+
return `${String(enclave).toLowerCase()}|${String(keyHash).toLowerCase()}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
class MemSwift {
|
|
31
|
+
constructor(file) {
|
|
32
|
+
this._file = file || null;
|
|
33
|
+
this._store = new Map();
|
|
34
|
+
if (this._file && fs.existsSync(this._file)) {
|
|
35
|
+
try {
|
|
36
|
+
const raw = JSON.parse(fs.readFileSync(this._file, 'utf8'));
|
|
37
|
+
for (const [k, v] of Object.entries(raw)) this._store.set(k, Buffer.from(v, 'base64'));
|
|
38
|
+
} catch (e) { /* start empty */ }
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
_persist() {
|
|
43
|
+
if (!this._file) return;
|
|
44
|
+
try {
|
|
45
|
+
const obj = {};
|
|
46
|
+
for (const [k, v] of this._store) obj[k] = Buffer.from(v).toString('base64');
|
|
47
|
+
fs.writeFileSync(this._file + '.tmp', JSON.stringify(obj));
|
|
48
|
+
fs.renameSync(this._file + '.tmp', this._file);
|
|
49
|
+
} catch (e) { /* best-effort */ }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async putFileContent(bucket, name, _prefix, data) {
|
|
53
|
+
const buf = Buffer.isBuffer(data) ? data
|
|
54
|
+
: (typeof data === 'string' ? Buffer.from(data, 'utf8') : Buffer.from(data));
|
|
55
|
+
this._store.set(`${bucket}/${name}`, buf);
|
|
56
|
+
this._persist();
|
|
57
|
+
return true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async getFileContentBytes(bucket, name) {
|
|
61
|
+
const v = this._store.get(`${bucket}/${name}`);
|
|
62
|
+
return v === undefined ? null : v;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async getFileContent(bucket, name) {
|
|
66
|
+
const v = await this.getFileContentBytes(bucket, name);
|
|
67
|
+
return v === null ? null : v.toString('utf8');
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async createBucket() { return true; }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class MemContract {
|
|
74
|
+
constructor(file) {
|
|
75
|
+
this._file = file || null;
|
|
76
|
+
this._entries = {}; // key -> { cid, version, updatedAt }
|
|
77
|
+
this._nonce = {}; // enclave -> n
|
|
78
|
+
if (this._file && fs.existsSync(this._file)) {
|
|
79
|
+
try {
|
|
80
|
+
const d = JSON.parse(fs.readFileSync(this._file, 'utf8'));
|
|
81
|
+
this._entries = d.entries || {};
|
|
82
|
+
this._nonce = d.nonce || {};
|
|
83
|
+
} catch (e) { /* start empty */ }
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
_persist() {
|
|
88
|
+
if (!this._file) return;
|
|
89
|
+
try {
|
|
90
|
+
fs.writeFileSync(this._file + '.tmp', JSON.stringify({ entries: this._entries, nonce: this._nonce }));
|
|
91
|
+
fs.renameSync(this._file + '.tmp', this._file);
|
|
92
|
+
} catch (e) { /* best-effort */ }
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ---- the ethers.Contract methods StateRegistry calls ----
|
|
96
|
+
async getState(enclave, keyHash) {
|
|
97
|
+
const e = this._entries[key(enclave, keyHash)];
|
|
98
|
+
if (!e) return ['', ethers.BigNumber.from(0), ethers.BigNumber.from(0)];
|
|
99
|
+
return [e.cid, ethers.BigNumber.from(e.version), ethers.BigNumber.from(e.updatedAt || 0)];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async getVersion(enclave, keyHash) {
|
|
103
|
+
const e = this._entries[key(enclave, keyHash)];
|
|
104
|
+
return ethers.BigNumber.from(e ? e.version : 0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async relayNonce(enclave) {
|
|
108
|
+
return ethers.BigNumber.from(this._nonce[String(enclave).toLowerCase()] || 0);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async commitDigest(enclave, keyHash, cid, expectedVersion, relayNonce) {
|
|
112
|
+
// Deterministic 32-byte digest (no real signing needed locally; the
|
|
113
|
+
// securelock still signs it with the local test key).
|
|
114
|
+
return ethers.utils.id(JSON.stringify([String(enclave).toLowerCase(), keyHash, cid,
|
|
115
|
+
Number(expectedVersion), Number(relayNonce)]));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async entryCount() {
|
|
119
|
+
return ethers.BigNumber.from(Object.keys(this._entries).length);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// ---- the on-chain commitFor, applied locally ----
|
|
123
|
+
applyCommit(enclave, keyHash, cid, expectedVersion) {
|
|
124
|
+
const k = key(enclave, keyHash);
|
|
125
|
+
const cur = this._entries[k] ? Number(this._entries[k].version) : 0;
|
|
126
|
+
if (Number(expectedVersion) !== cur) {
|
|
127
|
+
throw new Error(`VersionMismatch: expected ${expectedVersion} but current is ${cur}`);
|
|
128
|
+
}
|
|
129
|
+
this._entries[k] = { cid, version: cur + 1, updatedAt: 0 };
|
|
130
|
+
const e = String(enclave).toLowerCase();
|
|
131
|
+
this._nonce[e] = (this._nonce[e] || 0) + 1;
|
|
132
|
+
this._persist();
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// A fixed local test identity so walletAddress is stable across runs.
|
|
137
|
+
const LOCAL_IDENTITY = Buffer.from('ecld-local-test-identity-key-0001');
|
|
138
|
+
|
|
139
|
+
function install(ecldState, { caller = null, filePrefix = '.ecld-esr-local' } = {}) {
|
|
140
|
+
const swiftFile = filePrefix ? `${filePrefix}.swift.json` : null;
|
|
141
|
+
const regFile = filePrefix ? `${filePrefix}.registry.json` : null;
|
|
142
|
+
const swift = new MemSwift(swiftFile);
|
|
143
|
+
const contract = new MemContract(regFile);
|
|
144
|
+
|
|
145
|
+
ecldState.configure({
|
|
146
|
+
identityPriv: LOCAL_IDENTITY,
|
|
147
|
+
swiftStreamService: swift,
|
|
148
|
+
bucket: 'ecld-local',
|
|
149
|
+
contractAddress: '0xLOCAL',
|
|
150
|
+
provider: null,
|
|
151
|
+
caller,
|
|
152
|
+
});
|
|
153
|
+
ecldState.setContractOverride(contract);
|
|
154
|
+
ecldState.setLocalCommitApply(
|
|
155
|
+
(enclave, keyHash, cid, expectedVersion) =>
|
|
156
|
+
contract.applyCommit(enclave, keyHash, cid, expectedVersion));
|
|
157
|
+
|
|
158
|
+
return { swift, contract };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { install, MemSwift, MemContract };
|
|
@@ -121,7 +121,7 @@ function emptyRequiredConfig() {
|
|
|
121
121
|
);
|
|
122
122
|
}
|
|
123
123
|
|
|
124
|
-
function executeTask(payload, input) {
|
|
124
|
+
async function executeTask(payload, input) {
|
|
125
125
|
const missing = emptyRequiredConfig();
|
|
126
126
|
if (missing.length) {
|
|
127
127
|
return [
|
|
@@ -161,7 +161,7 @@ function executeTask(payload, input) {
|
|
|
161
161
|
return exec(payload, input, scope);
|
|
162
162
|
}
|
|
163
163
|
|
|
164
|
-
function exec(payload, input, globals = null) {
|
|
164
|
+
async function exec(payload, input, globals = null) {
|
|
165
165
|
try {
|
|
166
166
|
if (payload && payload !== "") {
|
|
167
167
|
const scope = globals || {};
|
|
@@ -172,7 +172,12 @@ function exec(payload, input, globals = null) {
|
|
|
172
172
|
// the payload by bare name — matching how payloads are written
|
|
173
173
|
// (e.g. `processData(___etny_data_set___)`).
|
|
174
174
|
with (scope) {
|
|
175
|
-
|
|
175
|
+
// ESR reads/commits are async, so a state-using payload
|
|
176
|
+
// (`esrIncrement()`, `await reg.commit(...)`) evaluates to a
|
|
177
|
+
// Promise. Await it so the finished value -- not the pending
|
|
178
|
+
// Promise -- is what gets wrapped. Non-async payloads await to
|
|
179
|
+
// themselves, so this is transparent for plain results.
|
|
180
|
+
const value = await eval(payload);
|
|
176
181
|
// A payload that called ecldResult/___etny_result___ itself
|
|
177
182
|
// already holds the finished [0, envelopeJson] tuple -- pass
|
|
178
183
|
// it through instead of wrapping the result a second time.
|
package/nodenithy/run.js
CHANGED
|
File without changes
|
package/package.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethernity-cloud/sdk-js",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.11",
|
|
4
4
|
"description": "A package to run ethernity cloud build scripts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"ecld-build": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/build.js",
|
|
7
7
|
"ecld-publish": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/publish.js",
|
|
8
8
|
"ecld-init": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/init.js",
|
|
9
|
-
"ecld-run": "cross-env
|
|
9
|
+
"ecld-run": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/run.js",
|
|
10
10
|
"postinstall": "node postinstall.js",
|
|
11
11
|
"ecld-test": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/test.js",
|
|
12
12
|
"ecld-info": "cross-env node ./node_modules/@ethernity-cloud/sdk-js/info.js"
|
|
@@ -17,7 +17,8 @@
|
|
|
17
17
|
"ecld-init": "./init.js",
|
|
18
18
|
"postinstall": "./postinstall.js",
|
|
19
19
|
"ecld-test": "./test.js",
|
|
20
|
-
"ecld-info": "./info.js"
|
|
20
|
+
"ecld-info": "./info.js",
|
|
21
|
+
"ecld-run": "./run.js"
|
|
21
22
|
},
|
|
22
23
|
"files": [
|
|
23
24
|
"nodenithy/*",
|
|
@@ -25,28 +26,31 @@
|
|
|
25
26
|
"build.js",
|
|
26
27
|
"init.js",
|
|
27
28
|
"publish.js",
|
|
28
|
-
"postinstall.js"
|
|
29
|
+
"postinstall.js",
|
|
30
|
+
"test.js",
|
|
31
|
+
"info.js",
|
|
32
|
+
"run.js"
|
|
29
33
|
],
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"@ethernity-cloud/runner": "^0.
|
|
35
|
+
"@ethernity-cloud/runner": "^0.4.4",
|
|
36
|
+
"acorn": "^8.11.0",
|
|
37
|
+
"adm-zip": "^0.5.14",
|
|
32
38
|
"axios": "^1.7.2",
|
|
39
|
+
"cli-progress": "^3.12.0",
|
|
33
40
|
"commander": "^12.1.0",
|
|
41
|
+
"cross-env": "^7.0.3",
|
|
34
42
|
"dotenv": "^16.4.5",
|
|
35
43
|
"eth-lib": "^0.1.29",
|
|
36
44
|
"ethers": "^5.5.4",
|
|
37
45
|
"kubo-rpc-client": "^4.1.1",
|
|
38
|
-
"web3": "^4.9.0",
|
|
39
|
-
"cross-env": "^7.0.3",
|
|
40
|
-
"shelljs": "^0.8.5",
|
|
41
|
-
"shx": "^0.3.4",
|
|
42
46
|
"node-forge": "^1.3.1",
|
|
43
|
-
"cli-progress": "^3.12.0",
|
|
44
47
|
"react": "^18.3.1",
|
|
45
48
|
"react-dom": "^18.3.1",
|
|
46
49
|
"react-scripts": "^5.0.1",
|
|
50
|
+
"shelljs": "^0.8.5",
|
|
51
|
+
"shx": "^0.3.4",
|
|
47
52
|
"web-vitals": "^2.1.4",
|
|
48
|
-
"
|
|
49
|
-
"acorn": "^8.11.0"
|
|
53
|
+
"web3": "^4.9.0"
|
|
50
54
|
},
|
|
51
55
|
"author": "Alexandru Luca",
|
|
52
56
|
"license": "AGPL-3.0"
|
package/pynithy/run.js
CHANGED
|
File without changes
|
package/run.js
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/* ecld-run — submit a payload to the Ethernity Cloud network and print the result.
|
|
3
|
+
*
|
|
4
|
+
* The network-side sibling of `ecld-test`: same payload flags (a code string,
|
|
5
|
+
* --file, --input/--input-text), but instead of executing locally it drives the
|
|
6
|
+
* real runner (@ethernity-cloud/runner's EthernityCloudRunner) end to end —
|
|
7
|
+
* encrypt the payload, push to IPFS, place the on-chain DO request, wait for a
|
|
8
|
+
* node to process it, download and decrypt the result.
|
|
9
|
+
*
|
|
10
|
+
* Where ecld-test answers "does my CALL work?", ecld-run answers "does it work
|
|
11
|
+
* on the network, on the enclave I published?" — it costs gas and needs a
|
|
12
|
+
* funded key.
|
|
13
|
+
*
|
|
14
|
+
* Network and enclave come from the project's .env (written by ecld-init /
|
|
15
|
+
* ecld-publish), matching how ethernity_task drives a run:
|
|
16
|
+
*
|
|
17
|
+
* BLOCKCHAIN_NETWORK e.g. "Bloxberg_Testnet" -> the network token address
|
|
18
|
+
* PROJECT_NAME the securelock enclave to run
|
|
19
|
+
* TRUSTED_ZONE_IMAGE the trustedzone enclave
|
|
20
|
+
* PRIVATE_KEY a funded 0x key — it pays for the order
|
|
21
|
+
*
|
|
22
|
+
* Usage, from the project root:
|
|
23
|
+
*
|
|
24
|
+
* npx ecld-run 'hello("World")'
|
|
25
|
+
* npx ecld-run --file payload.js
|
|
26
|
+
* npx ecld-run --input data.json 'processData(___etny_data_set___)'
|
|
27
|
+
* npx ecld-run --network BLOXBERG_TESTNET --task-price 3 'esrIncrement()'
|
|
28
|
+
* npx ecld-run --json 'hello("World")' # machine-readable result
|
|
29
|
+
*
|
|
30
|
+
* The runner is ESM; this CJS entry loads it with dynamic import().
|
|
31
|
+
*
|
|
32
|
+
* Exit code 0 on a SUCCESS task result, 1 otherwise.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
'use strict';
|
|
36
|
+
|
|
37
|
+
const fs = require('fs');
|
|
38
|
+
try { require('dotenv').config(); } catch (e) { /* dotenv optional */ }
|
|
39
|
+
|
|
40
|
+
const DEFAULT_IPFS = 'https://ipfs.ethernity.cloud/api/v0';
|
|
41
|
+
|
|
42
|
+
/* Map a BLOCKCHAIN_NETWORK token (as ecld-init writes it, spaces -> underscores)
|
|
43
|
+
* to the runner's network token ADDRESS. Built from the runner's own ECAddress
|
|
44
|
+
* so it can't drift. Accepts a few spellings per network. */
|
|
45
|
+
function networkAddress(ECAddress, raw) {
|
|
46
|
+
const key = String(raw || '').trim().toUpperCase().replace(/\s+/g, '_');
|
|
47
|
+
const table = {
|
|
48
|
+
BLOXBERG_TESTNET: ECAddress.BLOXBERG.TESTNET_ADDRESS,
|
|
49
|
+
BLOXBERG_MAINNET: ECAddress.BLOXBERG.MAINNET_ADDRESS,
|
|
50
|
+
POLYGON_MAINNET: ECAddress.POLYGON.MAINNET_ADDRESS,
|
|
51
|
+
POLYGON_AMOY: ECAddress.POLYGON.TESTNET_ADDRESS,
|
|
52
|
+
POLYGON_AMOY_TESTNET: ECAddress.POLYGON.TESTNET_ADDRESS,
|
|
53
|
+
IOTEX_TESTNET: ECAddress.IOTEX && ECAddress.IOTEX.TESTNET_ADDRESS,
|
|
54
|
+
ETHEREUM_SEPOLIA: ECAddress.SEPOLIA && ECAddress.SEPOLIA.TESTNET_ADDRESS,
|
|
55
|
+
SEPOLIA: ECAddress.SEPOLIA && ECAddress.SEPOLIA.TESTNET_ADDRESS,
|
|
56
|
+
LITVM_LITEFORGE: ECAddress.LITVM && ECAddress.LITVM.TESTNET_ADDRESS,
|
|
57
|
+
LITVM: ECAddress.LITVM && ECAddress.LITVM.TESTNET_ADDRESS,
|
|
58
|
+
};
|
|
59
|
+
return { key, address: table[key] || null, known: Object.keys(table) };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseArgs(argv) {
|
|
63
|
+
const opts = { ipfs: DEFAULT_IPFS, timeout: 600, taskPrice: 3, cpu: 1, memory: 1, storage: 10, bandwidth: 1, duration: 1, validators: 1, node: '' };
|
|
64
|
+
const positional = [];
|
|
65
|
+
for (let i = 0; i < argv.length; i++) {
|
|
66
|
+
const a = argv[i];
|
|
67
|
+
if (a === '--file' || a === '-f') opts.file = argv[++i];
|
|
68
|
+
else if (a === '--input' || a === '-i') opts.inputFile = argv[++i];
|
|
69
|
+
else if (a === '--input-text') opts.inputText = argv[++i];
|
|
70
|
+
else if (a === '--network') opts.network = argv[++i];
|
|
71
|
+
else if (a === '--securelock') opts.securelock = argv[++i];
|
|
72
|
+
else if (a === '--trustedzone') opts.trustedzone = argv[++i];
|
|
73
|
+
else if (a === '--node') opts.node = argv[++i];
|
|
74
|
+
else if (a === '--ipfs') opts.ipfs = argv[++i];
|
|
75
|
+
else if (a === '--timeout') opts.timeout = parseInt(argv[++i], 10);
|
|
76
|
+
else if (a === '--task-price') opts.taskPrice = parseInt(argv[++i], 10);
|
|
77
|
+
else if (a === '--cpu') opts.cpu = parseInt(argv[++i], 10);
|
|
78
|
+
else if (a === '--memory') opts.memory = parseInt(argv[++i], 10);
|
|
79
|
+
else if (a === '--storage') opts.storage = parseInt(argv[++i], 10);
|
|
80
|
+
else if (a === '--bandwidth') opts.bandwidth = parseInt(argv[++i], 10);
|
|
81
|
+
else if (a === '--duration') opts.duration = parseInt(argv[++i], 10);
|
|
82
|
+
else if (a === '--validators') opts.validators = parseInt(argv[++i], 10);
|
|
83
|
+
else if (a === '--json') opts.json = true;
|
|
84
|
+
else if (a === '--help' || a === '-h') opts.help = true;
|
|
85
|
+
else positional.push(a);
|
|
86
|
+
}
|
|
87
|
+
return { opts, positional };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const HELP = `ecld-run — submit a payload to the Ethernity Cloud network (costs gas; needs a funded key).
|
|
91
|
+
Local sibling: ecld-test.
|
|
92
|
+
|
|
93
|
+
usage: ecld-run 'hello("World")' | ecld-run --file payload.js | ecld-run --input data.json 'fn(___etny_data_set___)'
|
|
94
|
+
|
|
95
|
+
payload: a code string, or --file <path>; input via --input <file> / --input-text <str>
|
|
96
|
+
network: --network BLOXBERG_TESTNET (default: BLOCKCHAIN_NETWORK from .env)
|
|
97
|
+
enclaves: --securelock <name> (default PROJECT_NAME), --trustedzone <name> (default TRUSTED_ZONE_IMAGE)
|
|
98
|
+
key: PRIVATE_KEY (a funded 0x key) from .env / env, or ECLD_PRIVATE_KEY
|
|
99
|
+
resources: --task-price 3 --cpu 1 --memory 1 --storage 10 --bandwidth 1 --duration 1 --validators 1
|
|
100
|
+
other: --node <addr> --ipfs <url> --timeout <sec> --json`;
|
|
101
|
+
|
|
102
|
+
async function main() {
|
|
103
|
+
const { opts, positional } = parseArgs(process.argv.slice(2));
|
|
104
|
+
if (opts.help) { console.log(HELP); process.exit(0); }
|
|
105
|
+
|
|
106
|
+
// ---- payload ----
|
|
107
|
+
let code;
|
|
108
|
+
if (opts.file) code = fs.readFileSync(opts.file, 'utf8');
|
|
109
|
+
else if (positional.length) code = positional.join(' ');
|
|
110
|
+
else if ((process.env.ECLD_TEST_CODE || '').trim()) code = process.env.ECLD_TEST_CODE.trim();
|
|
111
|
+
else { console.error("ecld-run: provide a payload string, --file, or ECLD_TEST_CODE\n\n" + HELP); process.exit(2); }
|
|
112
|
+
|
|
113
|
+
if (opts.inputFile !== undefined && opts.inputText !== undefined) {
|
|
114
|
+
console.error('ecld-run: --input and --input-text are mutually exclusive'); process.exit(2);
|
|
115
|
+
}
|
|
116
|
+
let input = null;
|
|
117
|
+
if (opts.inputFile !== undefined) input = fs.readFileSync(opts.inputFile, 'utf8');
|
|
118
|
+
else if (opts.inputText !== undefined) input = opts.inputText;
|
|
119
|
+
// The enclave executor exposes the input as ___etny_data_set___; the runner
|
|
120
|
+
// passes it as part of the code's scope, so a payload references it by name.
|
|
121
|
+
// ecld-run mirrors ecld-test: input is delivered via the payload call itself.
|
|
122
|
+
|
|
123
|
+
// ---- runner (ESM) ----
|
|
124
|
+
let EthernityCloudRunner, ECStatus, ECEvent, ECAddress, ECOrderTaskStatus;
|
|
125
|
+
try {
|
|
126
|
+
const runnerMod = await import('@ethernity-cloud/runner');
|
|
127
|
+
EthernityCloudRunner = runnerMod.default || runnerMod.EthernityCloudRunner;
|
|
128
|
+
const enums = await import('@ethernity-cloud/runner/enums.js');
|
|
129
|
+
({ ECStatus, ECEvent, ECAddress, ECOrderTaskStatus } = enums);
|
|
130
|
+
} catch (e) {
|
|
131
|
+
console.error('ecld-run: the runner package is not installed or is too old '
|
|
132
|
+
+ '(need @ethernity-cloud/runner >= 0.4.4): ' + e.message);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ---- network / enclaves / key ----
|
|
137
|
+
const rawNetwork = opts.network || process.env.BLOCKCHAIN_NETWORK || 'BLOXBERG_TESTNET';
|
|
138
|
+
const { key: netKey, address, known } = networkAddress(ECAddress, rawNetwork);
|
|
139
|
+
if (!address) {
|
|
140
|
+
console.error(`ecld-run: unknown network '${rawNetwork}'. Known: ${known.join(', ')}`);
|
|
141
|
+
process.exit(1);
|
|
142
|
+
}
|
|
143
|
+
const securelock = opts.securelock || process.env.PROJECT_NAME;
|
|
144
|
+
const trustedzone = opts.trustedzone || process.env.TRUSTED_ZONE_IMAGE; // may be undefined -> run() default
|
|
145
|
+
if (!securelock) {
|
|
146
|
+
console.error('ecld-run: no securelock enclave: set PROJECT_NAME in .env '
|
|
147
|
+
+ '(run ecld-init/ecld-publish) or pass --securelock.');
|
|
148
|
+
process.exit(1);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
let privateKey = (process.env.PRIVATE_KEY || process.env.ECLD_PRIVATE_KEY || '').trim();
|
|
152
|
+
if (!privateKey) {
|
|
153
|
+
console.error('ecld-run: no signing key. Set PRIVATE_KEY (a funded 0x key) in .env '
|
|
154
|
+
+ '(ecld-publish stores it) or ECLD_PRIVATE_KEY.');
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
if (!privateKey.startsWith('0x')) privateKey = '0x' + privateKey;
|
|
158
|
+
|
|
159
|
+
console.log(`network : ${netKey} (${address})`);
|
|
160
|
+
console.log(`securelock : ${securelock}`);
|
|
161
|
+
console.log(`trustedzone: ${trustedzone || '(runner default)'}`);
|
|
162
|
+
console.log(`payload : ${JSON.stringify(code)}`);
|
|
163
|
+
console.log(`input : ${input === null ? '<empty>' : JSON.stringify(input.slice(0, 80))}`);
|
|
164
|
+
|
|
165
|
+
// ---- drive the runner ----
|
|
166
|
+
let runner;
|
|
167
|
+
try {
|
|
168
|
+
runner = new EthernityCloudRunner(address, { privateKey });
|
|
169
|
+
} catch (e) {
|
|
170
|
+
console.error('ecld-run: could not initialise the runner: ' + e.message);
|
|
171
|
+
process.exit(1);
|
|
172
|
+
}
|
|
173
|
+
runner.initializeStorage(opts.ipfs);
|
|
174
|
+
|
|
175
|
+
let lastPhase = null;
|
|
176
|
+
const onProgress = (e) => {
|
|
177
|
+
const d = (e && e.detail) || {};
|
|
178
|
+
if (d.progress !== lastPhase) { lastPhase = d.progress; console.log(`[${d.progress}] ${d.message}`); }
|
|
179
|
+
else if (d.message) console.log(` ${d.message}`);
|
|
180
|
+
};
|
|
181
|
+
runner.addEventListener(ECStatus.DEFAULT, onProgress);
|
|
182
|
+
|
|
183
|
+
const resources = {
|
|
184
|
+
taskPrice: opts.taskPrice, cpu: opts.cpu, memory: opts.memory,
|
|
185
|
+
storage: opts.storage, bandwidth: opts.bandwidth, duration: opts.duration, validators: opts.validators,
|
|
186
|
+
};
|
|
187
|
+
console.log(`resources : ${JSON.stringify(resources)}`);
|
|
188
|
+
console.log('submitting order... (this places an on-chain request and costs gas)');
|
|
189
|
+
|
|
190
|
+
const done = new Promise((resolve) => {
|
|
191
|
+
runner.addEventListener(ECStatus.SUCCESS, () => resolve('SUCCESS'));
|
|
192
|
+
runner.addEventListener(ECStatus.ERROR, (e) => {
|
|
193
|
+
const msg = (e && e.detail && e.detail.message) || 'unknown error';
|
|
194
|
+
resolve({ error: msg });
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// run() resolves when the task terminates; the events resolve `done`. Race a
|
|
199
|
+
// timeout so a stuck order doesn't hang the CLI forever.
|
|
200
|
+
const timeoutMs = (opts.timeout || 600) * 1000;
|
|
201
|
+
let timer;
|
|
202
|
+
const timeout = new Promise((resolve) => { timer = setTimeout(() => resolve('TIMEOUT'), timeoutMs); });
|
|
203
|
+
|
|
204
|
+
try {
|
|
205
|
+
const runArgs = [resources, securelock, code, opts.node];
|
|
206
|
+
if (trustedzone) runArgs.push(trustedzone);
|
|
207
|
+
// Fire the run; its own promise also settles on completion.
|
|
208
|
+
const runPromise = runner.run(...runArgs).catch((e) => ({ error: e.message }));
|
|
209
|
+
const outcome = await Promise.race([done, timeout, runPromise]);
|
|
210
|
+
clearTimeout(timer);
|
|
211
|
+
|
|
212
|
+
if (outcome === 'TIMEOUT') {
|
|
213
|
+
console.error(`\necld-run: no result after ${opts.timeout}s; the order may still process on-chain.`);
|
|
214
|
+
process.exit(1);
|
|
215
|
+
}
|
|
216
|
+
if (outcome && outcome.error) {
|
|
217
|
+
console.error(`\nstatus : ERROR`);
|
|
218
|
+
console.error(`error : ${outcome.error}`);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// SUCCESS
|
|
223
|
+
const result = await runner.getResult();
|
|
224
|
+
const typed = (runner.getStructuredResult && runner.getStructuredResult()) || null;
|
|
225
|
+
const taskCode = runner.resultTaskCode;
|
|
226
|
+
const taskName = runner.resultTaskCodeName
|
|
227
|
+
|| (ECOrderTaskStatus && taskCode !== undefined ? ECOrderTaskStatus[taskCode] : undefined);
|
|
228
|
+
|
|
229
|
+
if (opts.json) {
|
|
230
|
+
console.log(JSON.stringify({ task_code: taskCode, task_code_name: taskName, value: result, structured: typed }, null, 2));
|
|
231
|
+
} else {
|
|
232
|
+
if (taskName && taskName !== 'SUCCESS') console.log(`task code : ${taskCode} (${taskName})`);
|
|
233
|
+
console.log('result :');
|
|
234
|
+
console.log(result);
|
|
235
|
+
}
|
|
236
|
+
// A non-zero enclave task code is a failed run even though the order landed.
|
|
237
|
+
process.exit(taskCode === undefined || taskCode === 0 ? 0 : 1);
|
|
238
|
+
} catch (e) {
|
|
239
|
+
clearTimeout(timer);
|
|
240
|
+
console.error('ecld-run: run failed: ' + (e && e.stack ? e.stack : e));
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
main().catch((e) => { console.error(e && e.stack ? e.stack : String(e)); process.exit(1); });
|
package/test.js
CHANGED
|
@@ -24,6 +24,14 @@
|
|
|
24
24
|
* @ethernity-cloud/runner >= 0.3.6 executes every run() against this API
|
|
25
25
|
* instead of the blockchain.
|
|
26
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
|
+
*
|
|
27
35
|
* Exit code 0 on TaskStatus SUCCESS, 1 otherwise.
|
|
28
36
|
*/
|
|
29
37
|
|
|
@@ -32,6 +40,7 @@ const os = require('os');
|
|
|
32
40
|
const path = require('path');
|
|
33
41
|
const http = require('http');
|
|
34
42
|
const crypto = require('crypto');
|
|
43
|
+
const { ethers } = require('ethers');
|
|
35
44
|
|
|
36
45
|
const TASK_STATUS_NAMES = {
|
|
37
46
|
0: 'SUCCESS',
|
|
@@ -62,10 +71,21 @@ function copyDir(src, dest) {
|
|
|
62
71
|
/* Stage the executor + the project's backend the way ecld-build does inside
|
|
63
72
|
* the enclave image, then load the REAL executor from the staged copy. */
|
|
64
73
|
function loadExecutor(projectSrc) {
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
74
|
+
// Stage INSIDE the SDK tree (not the OS temp dir) so the executor's
|
|
75
|
+
// `require('ethers')` and other runtime deps resolve by walking up to the
|
|
76
|
+
// SDK's own node_modules -- exactly what binary-fs gives the enclave.
|
|
77
|
+
const stageRoot = path.join(VENDORED_SRC, '..', '.ecld-test-stage');
|
|
78
|
+
fs.mkdirSync(stageRoot, { recursive: true });
|
|
79
|
+
const stage = fs.mkdtempSync(path.join(stageRoot, 'run-'));
|
|
80
|
+
const cleanup = () => { try { fs.rmSync(stage, { recursive: true, force: true }); } catch (e) {} };
|
|
81
|
+
process.on('exit', cleanup);
|
|
82
|
+
// `serve` runs until interrupted -- clean the stage on Ctrl-C / kill too.
|
|
83
|
+
for (const sig of ['SIGINT', 'SIGTERM', 'SIGHUP']) {
|
|
84
|
+
process.on(sig, () => { cleanup(); process.exit(130); });
|
|
68
85
|
}
|
|
86
|
+
// Stage the ENTIRE vendored securelock src so `require('./ecld_state')`,
|
|
87
|
+
// esr_local, and their deps all resolve, exactly as inside the enclave image.
|
|
88
|
+
copyDir(VENDORED_SRC, stage);
|
|
69
89
|
const backendDir = path.join(projectSrc, 'serverless');
|
|
70
90
|
const backendJs = path.join(backendDir, 'backend.js');
|
|
71
91
|
if (fs.existsSync(backendJs)) {
|
|
@@ -78,11 +98,56 @@ function loadExecutor(projectSrc) {
|
|
|
78
98
|
} else {
|
|
79
99
|
console.log('backend : none found — running with bare globals, like a stock enclave');
|
|
80
100
|
}
|
|
81
|
-
|
|
101
|
+
const executor = require(path.join(stage, 'etny_exec.js'));
|
|
102
|
+
executor.__stageDir = stage;
|
|
103
|
+
return executor;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/* The task caller for local runs -- the DEVELOPER'S address, mirroring the
|
|
107
|
+
* trustedzone-attested DO owner the real securelock receives. Resolution:
|
|
108
|
+
* 1. --caller / ECLD_TEST_CALLER
|
|
109
|
+
* (a) WALLET_ADDRESS in .env (written at publish time)
|
|
110
|
+
* (b) derive from ECLD_PRIVATE_KEY
|
|
111
|
+
* else DEFAULT_TEST_CALLER (a well-known dev address). */
|
|
112
|
+
const DEFAULT_TEST_CALLER = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';
|
|
113
|
+
|
|
114
|
+
function resolveCaller(explicit) {
|
|
115
|
+
const chosen = explicit || process.env.ECLD_TEST_CALLER;
|
|
116
|
+
if (chosen) return chosen.trim();
|
|
117
|
+
if (process.env.WALLET_ADDRESS) return process.env.WALLET_ADDRESS.trim(); // (a)
|
|
118
|
+
const pk = process.env.ECLD_PRIVATE_KEY; // (b)
|
|
119
|
+
if (pk) {
|
|
120
|
+
try { return new ethers.Wallet(pk.startsWith('0x') ? pk : '0x' + pk).address; }
|
|
121
|
+
catch (e) { /* fall through */ }
|
|
122
|
+
}
|
|
123
|
+
console.log(`[ecld-test] no developer address configured; using default test caller `
|
|
124
|
+
+ `${DEFAULT_TEST_CALLER} (set ECLD_TEST_CALLER or WALLET_ADDRESS)`);
|
|
125
|
+
return DEFAULT_TEST_CALLER;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/* Install the local ESR emulator + set the task caller (ON by default). */
|
|
129
|
+
function installEsr(executor, caller) {
|
|
130
|
+
try {
|
|
131
|
+
const stage = executor.__stageDir;
|
|
132
|
+
const ecldState = require(path.join(stage, 'ecld_state.js'));
|
|
133
|
+
const esrLocal = require(path.join(stage, 'esr_local.js'));
|
|
134
|
+
esrLocal.install(ecldState, { caller, filePrefix: '.ecld-esr-local' });
|
|
135
|
+
return { ecldState };
|
|
136
|
+
} catch (e) {
|
|
137
|
+
console.log(`esr : emulation unavailable (${e.message})`);
|
|
138
|
+
return null;
|
|
139
|
+
}
|
|
82
140
|
}
|
|
83
141
|
|
|
84
|
-
function runTask(executor, payload, input) {
|
|
85
|
-
|
|
142
|
+
async function runTask(executor, payload, input, esr, caller) {
|
|
143
|
+
// Set the task caller EXACTLY as the real securelock does after the
|
|
144
|
+
// trustedzone hands it the DO owner -- for every task, ESR or not.
|
|
145
|
+
if (esr && esr.ecldState) {
|
|
146
|
+
try { esr.ecldState.setTaskCaller(caller || null); } catch (e) { /* ignore */ }
|
|
147
|
+
}
|
|
148
|
+
// executeTask is async (ESR reads/commits await); the real securelock awaits
|
|
149
|
+
// it too (securelock.js: `await executeTask(...)`).
|
|
150
|
+
const [code, resultRaw] = await executor.executeTask(payload, input || '');
|
|
86
151
|
const result = typeof resultRaw === 'string' ? resultRaw : JSON.stringify(resultRaw);
|
|
87
152
|
const checksum = crypto.createHash('sha256').update(result, 'utf8').digest('hex');
|
|
88
153
|
const challenge = Array.from({ length: 20 }, () =>
|
|
@@ -98,7 +163,7 @@ function runTask(executor, payload, input) {
|
|
|
98
163
|
};
|
|
99
164
|
}
|
|
100
165
|
|
|
101
|
-
function serve(executor, host, port) {
|
|
166
|
+
function serve(executor, host, port, esr, defaultCaller) {
|
|
102
167
|
const server = http.createServer((req, res) => {
|
|
103
168
|
const send = (status, obj) => {
|
|
104
169
|
const body = JSON.stringify(obj);
|
|
@@ -128,9 +193,13 @@ function serve(executor, host, port) {
|
|
|
128
193
|
if (input !== undefined && input !== null && typeof input !== 'string') {
|
|
129
194
|
input = JSON.stringify(input);
|
|
130
195
|
}
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
const caller = body.caller !== undefined ? body.caller : defaultCaller;
|
|
197
|
+
runTask(executor, body.payload, input, esr, caller)
|
|
198
|
+
.then((out) => {
|
|
199
|
+
console.log(`[local-api] task -> ${out.task_code} (${out.task_code_name})`);
|
|
200
|
+
send(200, out);
|
|
201
|
+
})
|
|
202
|
+
.catch((e) => send(500, { error: e.message }));
|
|
134
203
|
} catch (e) {
|
|
135
204
|
if (e instanceof SyntaxError) return send(400, { error: 'invalid JSON body' });
|
|
136
205
|
return send(500, { error: e.message });
|
|
@@ -145,7 +214,7 @@ function serve(executor, host, port) {
|
|
|
145
214
|
});
|
|
146
215
|
}
|
|
147
216
|
|
|
148
|
-
function main() {
|
|
217
|
+
async function main() {
|
|
149
218
|
const argv = process.argv.slice(2);
|
|
150
219
|
const opts = { src: 'src', host: '127.0.0.1', port: 8745 };
|
|
151
220
|
const positional = [];
|
|
@@ -157,6 +226,7 @@ function main() {
|
|
|
157
226
|
else if (a === '--src') opts.src = argv[++i];
|
|
158
227
|
else if (a === '--host') opts.host = argv[++i];
|
|
159
228
|
else if (a === '--port') opts.port = parseInt(argv[++i], 10);
|
|
229
|
+
else if (a === '--caller') opts.caller = argv[++i];
|
|
160
230
|
else positional.push(a);
|
|
161
231
|
}
|
|
162
232
|
|
|
@@ -169,8 +239,16 @@ function main() {
|
|
|
169
239
|
serve.backendFunctions = [];
|
|
170
240
|
}
|
|
171
241
|
|
|
242
|
+
// ESR emulation ON by default so a state-using backend runs locally exactly
|
|
243
|
+
// as in an ESR-enabled enclave (no chain/node/SGX). Caller (default: your dev
|
|
244
|
+
// address) is set for every task, so taskCaller() and the ACL behave as
|
|
245
|
+
// on-chain.
|
|
246
|
+
const caller = resolveCaller(opts.caller);
|
|
247
|
+
const esr = installEsr(executor, caller);
|
|
248
|
+
if (esr) console.log(`esr : local emulation ON caller ${caller}`);
|
|
249
|
+
|
|
172
250
|
if (positional[0] === 'serve') {
|
|
173
|
-
return serve(executor, opts.host, opts.port);
|
|
251
|
+
return serve(executor, opts.host, opts.port, esr, caller);
|
|
174
252
|
}
|
|
175
253
|
|
|
176
254
|
let payload;
|
|
@@ -191,7 +269,7 @@ function main() {
|
|
|
191
269
|
|
|
192
270
|
console.log(`payload : ${JSON.stringify(payload)}`);
|
|
193
271
|
console.log(`input : ${input === null ? '<empty>' : JSON.stringify(input.slice(0, 80))}`);
|
|
194
|
-
const out = runTask(executor, payload, input);
|
|
272
|
+
const out = await runTask(executor, payload, input, esr, caller);
|
|
195
273
|
console.log(`task code: ${out.task_code} (${out.task_code_name})`);
|
|
196
274
|
console.log(`result : ${JSON.stringify(out.result)}`);
|
|
197
275
|
if (out.task_code !== 0) {
|
|
@@ -201,4 +279,4 @@ function main() {
|
|
|
201
279
|
process.exit(out.task_code === 0 ? 0 : 1);
|
|
202
280
|
}
|
|
203
281
|
|
|
204
|
-
main();
|
|
282
|
+
main().catch((e) => { console.error(e && e.stack ? e.stack : String(e)); process.exit(1); });
|