@ethernity-cloud/sdk-js 1.7.9 → 1.7.10
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 +33 -13
- package/cas/config.js +2 -2
- package/cas/session_registry.js +9 -6
- package/info.js +2 -2
- package/init.js +37 -18
- package/localKubo.js +18 -10
- package/nodenithy/build.mjs +5 -5
- package/nodenithy/ipfs.mjs +74 -18
- package/nodenithy/run/docker-compose-swift-stream.yml +4 -1
- package/nodenithy/run/image_registry.js +1 -1
- package/nodenithy/run.js +54 -22
- package/nodenithy/src/ec_helloworld_example.js +5 -5
- package/nodenithy/src/preStart.mjs +6 -2
- package/package.json +2 -2
- package/pynithy/build.mjs +5 -5
- package/pynithy/ipfs.mjs +74 -18
- package/pynithy/run/docker-compose-swift-stream.yml +4 -1
- package/pynithy/run/docker-compose-swift-stream.yml.tmpl +4 -1
- package/pynithy/run/image_registry.js +1 -1
- package/pynithy/run.js +54 -22
- package/pynithy/src/ec_helloworld_example.js +5 -5
- package/pynithy/src/preStart.mjs +6 -2
- package/run.js +23 -7
- package/nodenithy/ipfs.js +0 -135
- package/nodenithy/run/git.sh +0 -22
- package/nodenithy/run/ipfs.sh +0 -58
- package/nodenithy/run/ipfs_pre.sh +0 -75
- package/nodenithy/run.sh +0 -277
- package/nodenithy/scripts/build-ipfs-upload.sh +0 -23
- package/nodenithy/scripts/build.sh +0 -40
- package/nodenithy/scripts/ipfs-daemon.sh +0 -7
- package/nodenithy/scripts/run.sh +0 -50
- package/pynithy/run/git.sh +0 -22
- package/pynithy/run/ipfs.sh +0 -89
package/README.md
CHANGED
|
@@ -56,7 +56,7 @@ The sdk has been tested on the following operating systems:
|
|
|
56
56
|
- Mainnet - to be provided during the following updates
|
|
57
57
|
- LitVM:
|
|
58
58
|
- LiteForge - CAS-attested the same way, by LitVM's own ethernity-cas
|
|
59
|
-
validator set (SessionRegistry `
|
|
59
|
+
validator set (SessionRegistry `0xfA70B5Df...`, ValidatorRegistry
|
|
60
60
|
`0xbE3759f3...`).
|
|
61
61
|
- LiteForge Unsafe - the unsafe network beside LiteForge (see below).
|
|
62
62
|
- Polyhon:
|
|
@@ -120,11 +120,14 @@ sets the enclave heap (default `1024M`).
|
|
|
120
120
|
unless `REWARD_ADDRESS` names another one; the image's owner can change it
|
|
121
121
|
later with the registry's `changeImageRewardAddress`.
|
|
122
122
|
|
|
123
|
-
How the image reaches the network:
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
123
|
+
How the image reaches the network: leave `IPFS_ENDPOINT` in `.env` empty
|
|
124
|
+
unless it names an IPFS API of your own (`IPFS_TOKEN` is the Authorization
|
|
125
|
+
header value that API takes, when it takes one); the public API
|
|
126
|
+
`ipfs.ethernity.cloud` takes no adds or pins without a writer token. With
|
|
127
|
+
no API of your own the publish runs its own IPFS node in docker
|
|
128
|
+
(`ipfs/kubo`), adds the image and its compose there and peers it with the
|
|
129
|
+
network's bootnode, which pins every registered image. On a network whose
|
|
130
|
+
image registry records images
|
|
128
131
|
before their certificate (the bloxberg testnet), the publish registers the
|
|
129
132
|
image from your wallet first (`registerImage`, with your node's address),
|
|
130
133
|
has the certificate extracted (on your SGX host, or by
|
|
@@ -160,13 +163,22 @@ sets the enclave heap (default `1024M`).
|
|
|
160
163
|
npx ecld-run --unsafe 'hello("World")'
|
|
161
164
|
```
|
|
162
165
|
This is the network-side sibling of `ecld-test`: instead of executing locally
|
|
163
|
-
it drives the runner end to end — encrypt →
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
`
|
|
166
|
+
it drives the runner end to end — encrypt → on-chain request → payload intake
|
|
167
|
+
(or the IPFS API named with `--ipfs <url>`) → wait for a node → download and
|
|
168
|
+
decrypt the result. It **costs gas and needs a funded key**. Network and
|
|
169
|
+
enclaves come from `.env` (`BLOCKCHAIN_NETWORK`, `PROJECT_NAME`,
|
|
170
|
+
`TRUSTED_ZONE_IMAGE`), overridable with
|
|
167
171
|
`--network`/`--securelock`/`--trustedzone`; the signing key is `PRIVATE_KEY`
|
|
168
172
|
(or `ECLD_PRIVATE_KEY`) — a funded `0x` key that pays for the order. Resources
|
|
169
173
|
are tunable with `--task-price`/`--cpu`/`--memory`/`--storage`/`--bandwidth`/`--duration`/`--validators`.
|
|
174
|
+
`--ipfs intake` (the default) delivers the payload to the bootnode's payload
|
|
175
|
+
intake once the request is on chain; `--ipfs <url>` writes and reads through
|
|
176
|
+
that IPFS RPC API instead, with `ECLD_IPFS_TOKEN` (or `IPFS_TOKEN`) as its
|
|
177
|
+
Authorization header, and a URL naming the public API is refused without one.
|
|
178
|
+
The intake serves the networks the bootnode's agent runs, today the bloxberg
|
|
179
|
+
testnet and LitVM LiteForge, each with its unsafe twin: a dApp on any other
|
|
180
|
+
network (Bloxberg mainnet, Polygon, IoTeX, Sepolia) needs an IPFS API of its
|
|
181
|
+
own (`IPFS_ENDPOINT` for the Hello World template, `--ipfs <url>` for `ecld-run`).
|
|
170
182
|
Exit code 0 on a `SUCCESS` task result, 1 otherwise. `--unsafe` runs the
|
|
171
183
|
network's unsafe variant; on an unsafe network the securelock and
|
|
172
184
|
trustedzone default to their `-unsafe` names.
|
|
@@ -214,13 +226,17 @@ function hello(msg='World') {
|
|
|
214
226
|
|
|
215
227
|
module.exports = { hello };
|
|
216
228
|
```
|
|
217
|
-
From your frontend application, using the ethernity cloud runner library (`@ethernity-cloud/runner`, installed by `ecld-init` at the version this SDK depends on), you will be calling the function as seen in the below example, where we pass `hello("World");` to be executed on the backend which will run in the Blockchain. `npm start` runs `src/preStart.mjs` first, which fills the constants at the top from the project's `.env`: the securelock name the dApp is published as, the trustedzone it was built against, the network's token address and chain id, the `-unsafe` network when the dApp runs without a CAS, and the IPFS
|
|
229
|
+
From your frontend application, using the ethernity cloud runner library (`@ethernity-cloud/runner`, installed by `ecld-init` at the version this SDK depends on), you will be calling the function as seen in the below example, where we pass `hello("World");` to be executed on the backend which will run in the Blockchain. `npm start` runs `src/preStart.mjs` first, which fills the constants at the top from the project's `.env`: the securelock name the dApp is published as, the trustedzone it was built against, the network's token address and chain id, the `-unsafe` network when the dApp runs without a CAS, and the IPFS API of the dApp's own (empty when the task's artefacts go through the bootnode's payload intake).
|
|
218
230
|
```js
|
|
219
231
|
import './App.css';
|
|
220
232
|
import EthernityCloudRunner from "@ethernity-cloud/runner";
|
|
221
233
|
import { ECStatus } from "@ethernity-cloud/runner/enums";
|
|
222
234
|
|
|
223
|
-
// Filled in from the project's .env by src/preStart.mjs on `npm start
|
|
235
|
+
// Filled in from the project's .env by src/preStart.mjs on `npm start`: the
|
|
236
|
+
// securelock this dApp is published as, the trustedzone it was built against,
|
|
237
|
+
// the network's token address and chain id, the -unsafe network when the dApp
|
|
238
|
+
// runs without a CAS (null otherwise), and the IPFS API of the dApp's own ('' when
|
|
239
|
+
// the task's artefacts go through the bootnode's payload intake).
|
|
224
240
|
const PROJECT_NAME = "";
|
|
225
241
|
const TRUSTED_ZONE_IMAGE = "";
|
|
226
242
|
const NETWORK_ADDRESS = "";
|
|
@@ -238,7 +254,10 @@ function App() {
|
|
|
238
254
|
if (UNSAFE_NETWORK) {
|
|
239
255
|
await runner.setNetwork(...UNSAFE_NETWORK);
|
|
240
256
|
}
|
|
241
|
-
|
|
257
|
+
// An IPFS API of the dApp's own (IPFS_ENDPOINT) carries the task's
|
|
258
|
+
// artefacts; without one they go through the bootnode's payload intake.
|
|
259
|
+
if (IPFS_ENDPOINT) runner.initializeStorage(IPFS_ENDPOINT);
|
|
260
|
+
else runner.initializePublicIntake();
|
|
242
261
|
|
|
243
262
|
// Events are named by task status; detail is { message, status, progress }.
|
|
244
263
|
runner.addEventListener(ECStatus.DEFAULT, (e) => {
|
|
@@ -250,6 +269,7 @@ function App() {
|
|
|
250
269
|
runner.addEventListener(ECStatus.SUCCESS, async () => {
|
|
251
270
|
const result = await runner.getResult();
|
|
252
271
|
console.log(`Task Result: ${result}`);
|
|
272
|
+
// display the result in page below the buttons
|
|
253
273
|
const line = document.createElement("p");
|
|
254
274
|
line.textContent = `Task Result: ${result}`;
|
|
255
275
|
document.body.appendChild(line);
|
package/cas/config.js
CHANGED
|
@@ -23,8 +23,8 @@ const VALIDATOR_REGISTRY = {
|
|
|
23
23
|
const SESSION_REGISTRY = {
|
|
24
24
|
'etny-pynithy-testnet': '0x444429681fCb26470Cb12B5436E48402cF7478A3',
|
|
25
25
|
'etny-nodenithy-testnet': '0x444429681fCb26470Cb12B5436E48402cF7478A3',
|
|
26
|
-
'ecld-pynithy-litvm-testnet': '
|
|
27
|
-
'ecld-nodenithy-litvm-testnet': '
|
|
26
|
+
'ecld-pynithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
|
|
27
|
+
'ecld-nodenithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
|
|
28
28
|
};
|
|
29
29
|
|
|
30
30
|
// The chain the registries above live on.
|
package/cas/session_registry.js
CHANGED
|
@@ -74,11 +74,13 @@ function sessionHash(body) {
|
|
|
74
74
|
return '0x' + crypto.createHash('sha256').update(body).digest('hex');
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
-
// Pin the exact bytes as CIDv1/raw/sha2-256 and return the CID.
|
|
78
|
-
|
|
77
|
+
// Pin the exact bytes as CIDv1/raw/sha2-256 and return the CID. `token` is
|
|
78
|
+
// the API's Authorization header value (IPFS_TOKEN), sent when set.
|
|
79
|
+
async function pinBody(ipfsApiUrl, body, token = '') {
|
|
79
80
|
const form = new FormData();
|
|
80
81
|
form.append('file', new Blob([body]), 'session');
|
|
81
|
-
const
|
|
82
|
+
const headers = token ? { Authorization: token } : {};
|
|
83
|
+
const r = await axios.post(`${ipfsApiUrl}/api/v0/add?cid-version=1&raw-leaves=true&pin=true`, form, { headers, timeout: 60000 });
|
|
82
84
|
return r.data.Hash;
|
|
83
85
|
}
|
|
84
86
|
|
|
@@ -120,16 +122,17 @@ async function send(reg, chainId, refused, method, ...args) {
|
|
|
120
122
|
if (rcpt.status !== 1) throw new Error(`SessionRegistry.${method} reverted for ${refused} (tx ${tx.hash})`);
|
|
121
123
|
}
|
|
122
124
|
|
|
123
|
-
// Pin `body`
|
|
125
|
+
// Pin `body` through `ipfsApiUrl` (with `ipfsToken` as its Authorization
|
|
126
|
+
// header when set) and register it under its own `name:`. Returns
|
|
124
127
|
// {name, hash, cid, registered}: `registered` is false when the chain already
|
|
125
128
|
// held these exact bytes as the name's latest version, registered by this
|
|
126
129
|
// wallet.
|
|
127
|
-
async function register(rpcUrl, chainId, registryAddress, key, body, ipfsApiUrl, imageCid = '') {
|
|
130
|
+
async function register(rpcUrl, chainId, registryAddress, key, body, ipfsApiUrl, imageCid = '', ipfsToken = '') {
|
|
128
131
|
const { name, rules } = parseNameAndRules(body.toString('utf8'));
|
|
129
132
|
if (!name) throw new Error('the session body has no `name:`');
|
|
130
133
|
const hash = sessionHash(body);
|
|
131
134
|
const reg = contract(rpcUrl, registryAddress, key);
|
|
132
|
-
const cid = await pinBody(ipfsApiUrl, body);
|
|
135
|
+
const cid = await pinBody(ipfsApiUrl, body, ipfsToken);
|
|
133
136
|
const latest = await latestRecord(reg, name);
|
|
134
137
|
if (latest && latest.hash === hash && latest.creator.toLowerCase() === (await reg.signer.getAddress()).toLowerCase()) {
|
|
135
138
|
return { name, hash, cid, registered: false };
|
package/info.js
CHANGED
|
@@ -51,8 +51,8 @@ const NETWORKS = {
|
|
|
51
51
|
},
|
|
52
52
|
LITVM_LITEFORGE: {
|
|
53
53
|
type: 'testnet', chainId: 4441, rpc: 'https://liteforge.rpc.caldera.xyz/infra-partner-http',
|
|
54
|
-
protocol: '
|
|
55
|
-
imageRegistry: '
|
|
54
|
+
protocol: '0xE35991de1f2A435e29E70406FA2781b1AffCb168',
|
|
55
|
+
imageRegistry: '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D',
|
|
56
56
|
esr: '0x709052Fe77Af543d3d9FE2Ac06a15c635c8D4Be5',
|
|
57
57
|
},
|
|
58
58
|
};
|
package/init.js
CHANGED
|
@@ -4,10 +4,8 @@ const { spawn, execSync } = require("child_process");
|
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const casConfig = require("./cas/config.js");
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
output: process.stdout,
|
|
10
|
-
});
|
|
7
|
+
// The prompt, opened by main.
|
|
8
|
+
let rl = null;
|
|
11
9
|
|
|
12
10
|
const writeEnv = (key, value) => {
|
|
13
11
|
const envFile = ".env";
|
|
@@ -36,6 +34,23 @@ const writeEnv = (key, value) => {
|
|
|
36
34
|
// name does not contain "SECURELOCK".
|
|
37
35
|
const PROJECT_NAME_RULE = /^(?!.*SECURELOCK)[0-9A-Za-z._-]{1,121}$/;
|
|
38
36
|
|
|
37
|
+
// Where the publish adds the image and the dApp delivers its task artefacts.
|
|
38
|
+
// The first choice has the publish run its own IPFS node and the dApp use the
|
|
39
|
+
// bootnode's payload intake (IPFS_ENDPOINT empty); the second names an IPFS
|
|
40
|
+
// (Kubo) RPC API of the dApp's own, with the Authorization header value it
|
|
41
|
+
// takes. The public API, ipfs.ethernity.cloud, takes no adds or pins without
|
|
42
|
+
// a writer token.
|
|
43
|
+
const IPFS_OPTIONS = [
|
|
44
|
+
"the publish's own IPFS node in docker (default)",
|
|
45
|
+
"an IPFS (Kubo) RPC API of your own (URL, optional Authorization token)",
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// The .env values (IPFS_ENDPOINT, IPFS_TOKEN) of an IPFS_OPTIONS choice.
|
|
49
|
+
const ipfsSettings = (choice, url, token) =>
|
|
50
|
+
choice === IPFS_OPTIONS[1]
|
|
51
|
+
? { IPFS_ENDPOINT: String(url || "").trim(), IPFS_TOKEN: String(token || "").trim() }
|
|
52
|
+
: { IPFS_ENDPOINT: "", IPFS_TOKEN: "" };
|
|
53
|
+
|
|
39
54
|
const getProjectName = () => {
|
|
40
55
|
return new Promise((resolve) => {
|
|
41
56
|
const askProjectName = () => {
|
|
@@ -159,6 +174,10 @@ const printIntro = () => {
|
|
|
159
174
|
};
|
|
160
175
|
|
|
161
176
|
const main = async () => {
|
|
177
|
+
rl = readline.createInterface({
|
|
178
|
+
input: process.stdin,
|
|
179
|
+
output: process.stdout,
|
|
180
|
+
});
|
|
162
181
|
printIntro();
|
|
163
182
|
const projectName = await getProjectName();
|
|
164
183
|
console.log();
|
|
@@ -256,30 +275,28 @@ const main = async () => {
|
|
|
256
275
|
// });
|
|
257
276
|
|
|
258
277
|
console.log();
|
|
259
|
-
const ipfsServiceOptions = ["Ethernity (best effort)", "Custom IPFS"];
|
|
260
278
|
const ipfsService = await promptOptions(
|
|
261
|
-
"
|
|
262
|
-
|
|
263
|
-
|
|
279
|
+
"Which IPFS API do the publish and the dApp write through? (default is 1): ",
|
|
280
|
+
IPFS_OPTIONS,
|
|
281
|
+
IPFS_OPTIONS[0],
|
|
264
282
|
);
|
|
265
283
|
|
|
266
|
-
let
|
|
267
|
-
if (ipfsService ===
|
|
268
|
-
|
|
284
|
+
let ipfsUrl, ipfsToken;
|
|
285
|
+
if (ipfsService === IPFS_OPTIONS[1]) {
|
|
286
|
+
ipfsUrl = await new Promise((resolve) =>
|
|
269
287
|
rl.question(
|
|
270
|
-
"Enter the
|
|
288
|
+
"Enter the URL of the IPFS RPC API (e.g. http://127.0.0.1:5001): ",
|
|
271
289
|
resolve,
|
|
272
290
|
),
|
|
273
291
|
);
|
|
274
292
|
ipfsToken = await new Promise((resolve) =>
|
|
275
293
|
rl.question(
|
|
276
|
-
"Enter
|
|
294
|
+
"Enter its Authorization header value (empty for none): ",
|
|
277
295
|
resolve,
|
|
278
296
|
),
|
|
279
297
|
);
|
|
280
|
-
} else {
|
|
281
|
-
customUrl = "https://ipfs.ethernity.cloud";
|
|
282
298
|
}
|
|
299
|
+
const ipfs = ipfsSettings(ipfsService, ipfsUrl, ipfsToken);
|
|
283
300
|
|
|
284
301
|
fs.mkdirSync("src/serverless", { recursive: true });
|
|
285
302
|
|
|
@@ -379,10 +396,10 @@ const main = async () => {
|
|
|
379
396
|
|
|
380
397
|
|
|
381
398
|
writeEnv("BLOCKCHAIN_NETWORK", network);
|
|
382
|
-
writeEnv("IPFS_ENDPOINT",
|
|
399
|
+
writeEnv("IPFS_ENDPOINT", ipfs.IPFS_ENDPOINT);
|
|
400
|
+
writeEnv("IPFS_TOKEN", ipfs.IPFS_TOKEN);
|
|
383
401
|
console.log();
|
|
384
402
|
await configureEsr();
|
|
385
|
-
writeEnv("IPFS_TOKEN", ipfsToken || "");
|
|
386
403
|
writeEnv("VERSION", "v1");
|
|
387
404
|
console.log();
|
|
388
405
|
console.log(
|
|
@@ -391,4 +408,6 @@ const main = async () => {
|
|
|
391
408
|
rl.close();
|
|
392
409
|
};
|
|
393
410
|
|
|
394
|
-
main();
|
|
411
|
+
if (require.main === module) main();
|
|
412
|
+
|
|
413
|
+
module.exports = { IPFS_OPTIONS, ipfsSettings, writeEnv };
|
package/localKubo.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
// The Kubo a publish runs for its own upload.
|
|
2
2
|
//
|
|
3
3
|
// An image is published by adding it to IPFS and registering its CID. The
|
|
4
|
-
// public
|
|
5
|
-
// IPFS endpoint of its own runs a Kubo in
|
|
6
|
-
// docker), adds the image through that Kubo's
|
|
7
|
-
//
|
|
8
|
-
// Kubo with the bootnode, whose mirror pins every registered
|
|
9
|
-
// which the certificate extraction service fetches, and stops
|
|
10
|
-
// certificate is on chain. The Kubo listens for the API on the
|
|
11
|
-
// interface only; its swarm port is published when free, so a
|
|
12
|
-
// a reachable address is also dialed directly.
|
|
4
|
+
// public API of ipfs.ethernity.cloud takes no adds or pins without a writer
|
|
5
|
+
// token, so a publish without an IPFS endpoint of its own runs a Kubo in
|
|
6
|
+
// docker (the SDK already requires docker), adds the image through that Kubo's
|
|
7
|
+
// HTTP API with the same add call (so a build gives the same CIDs on every
|
|
8
|
+
// node), peers the Kubo with the bootnode, whose mirror pins every registered
|
|
9
|
+
// image and from which the certificate extraction service fetches, and stops
|
|
10
|
+
// it once the certificate is on chain. The Kubo listens for the API on the
|
|
11
|
+
// loopback interface only; its swarm port is published when free, so a
|
|
12
|
+
// publisher with a reachable address is also dialed directly.
|
|
13
13
|
const { execFileSync } = require('child_process');
|
|
14
14
|
const net = require('net');
|
|
15
15
|
const axios = require('axios');
|
|
@@ -65,6 +65,14 @@ function ownEndpoint(endpoint) {
|
|
|
65
65
|
}
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
// The IPFS_ENDPOINT a dApp's template is built with (src/preStart.mjs): the
|
|
69
|
+
// setting when it is an endpoint of the application's own, '' otherwise, on
|
|
70
|
+
// which the dApp delivers its task artefacts through the bootnode's payload
|
|
71
|
+
// intake.
|
|
72
|
+
function dappEndpoint(endpoint) {
|
|
73
|
+
return ownEndpoint(endpoint) ? endpoint : '';
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
class LocalKubo {
|
|
69
77
|
constructor(name) {
|
|
70
78
|
this.container = `ecld-kubo-${String(name || 'publish').replace(/[^A-Za-z0-9_.-]/g, '-')}`;
|
|
@@ -202,4 +210,4 @@ class LocalKubo {
|
|
|
202
210
|
}
|
|
203
211
|
}
|
|
204
212
|
|
|
205
|
-
module.exports = { LocalKubo, ownEndpoint, apiPortIn, PUBLIC_IPFS_HOST };
|
|
213
|
+
module.exports = { LocalKubo, ownEndpoint, dappEndpoint, apiPortIn, PUBLIC_IPFS_HOST };
|
package/nodenithy/build.mjs
CHANGED
|
@@ -48,14 +48,14 @@ export const ECRunner = {
|
|
|
48
48
|
// --- ethereum sepolia ---
|
|
49
49
|
'ecld-pynithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
|
|
50
50
|
'ecld-nodenithy-ethereum-sepolia': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://ethereum-sepolia-rpc.publicnode.com', 11155111],
|
|
51
|
-
// --- litvm liteforge testnet
|
|
52
|
-
'ecld-pynithy-litvm-testnet': ['
|
|
53
|
-
'ecld-nodenithy-litvm-testnet': ['
|
|
51
|
+
// --- litvm liteforge testnet: the PoX is the EthernityImplementationV2 1.1 of 2026-10-11; the image registry is the ECImageRegistryV3 of 2026-10-12 ---
|
|
52
|
+
'ecld-pynithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
|
|
53
|
+
'ecld-nodenithy-litvm-testnet': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
|
|
54
54
|
// --- the -unsafe networks: the contracts of the network each is named after ---
|
|
55
55
|
'etny-pynithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
|
|
56
56
|
'etny-nodenithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
|
|
57
|
-
'ecld-pynithy-litvm-testnet-unsafe': ['
|
|
58
|
-
'ecld-nodenithy-litvm-testnet-unsafe': ['
|
|
57
|
+
'ecld-pynithy-litvm-testnet-unsafe': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
|
|
58
|
+
'ecld-nodenithy-litvm-testnet-unsafe': ['0xE35991de1f2A435e29E70406FA2781b1AffCb168', '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441]
|
|
59
59
|
};
|
|
60
60
|
|
|
61
61
|
// ethernity-cas ValidatorRegistry per template, baked into the enclave so it
|
package/nodenithy/ipfs.mjs
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
// ipfs.mjs
|
|
2
2
|
//
|
|
3
|
-
// IPFS client
|
|
3
|
+
// IPFS client of the publish (nodenithy/run.js, pynithy/run.js): adds, hashes
|
|
4
|
+
// and reads through the IPFS API named with --host, the publish's own Kubo
|
|
5
|
+
// (http://127.0.0.1:<port>) or an IPFS API of the dApp's own; --token, or
|
|
6
|
+
// IPFS_TOKEN in the environment when the flag is absent, is the Authorization
|
|
7
|
+
// header value that API takes, when it takes one.
|
|
4
8
|
//
|
|
5
9
|
// The UPLOAD path is a faithful port of the Python SDK's IPFSClient
|
|
6
10
|
// (ethernity_cloud_sdk_py/commands/pynithy/ipfs_client.py): it posts directly
|
|
@@ -12,7 +16,7 @@
|
|
|
12
16
|
// the Python pipeline step-for-step so both SDKs upload identically (e.g. for
|
|
13
17
|
// the certex / public-key extraction flow).
|
|
14
18
|
//
|
|
15
|
-
// The DOWNLOAD path
|
|
19
|
+
// The DOWNLOAD path uses kubo-rpc-client, with the same Authorization header.
|
|
16
20
|
import http from 'http';
|
|
17
21
|
import https from 'https';
|
|
18
22
|
import { URL } from 'url';
|
|
@@ -31,29 +35,33 @@ const getRetryDelay = (retryCount, baseDelay = 1) => baseDelay * 2 ** retryCount
|
|
|
31
35
|
const RETRY_COUNT = 10; // mirrors Python ipfs_client.RETRY_COUNT
|
|
32
36
|
|
|
33
37
|
// Endpoint + auth, mirroring the Python IPFSClient constructor.
|
|
34
|
-
// apiUrl
|
|
35
|
-
//
|
|
38
|
+
// apiUrl : the API's base URL, e.g. "http://127.0.0.1:5001"; adds go to
|
|
39
|
+
// `${apiUrl}/api/v0/add`
|
|
40
|
+
// authHeader : the Authorization header value (--token), or null
|
|
36
41
|
let apiUrl = null;
|
|
37
|
-
let authHeader = null;
|
|
42
|
+
let authHeader = null;
|
|
38
43
|
|
|
39
44
|
// kubo-rpc-client instance, used only for the download helpers below.
|
|
40
45
|
let ipfs = null;
|
|
41
46
|
|
|
42
47
|
const initialize = (host, protocol, port, token) => {
|
|
48
|
+
authHeader = token && token !== '' ? token : null;
|
|
43
49
|
// Normalise the endpoint to a base URL string, matching how the Python client
|
|
44
50
|
// treats `ipfs_endpoint` (it just prepends it to `/api/v0/add`).
|
|
51
|
+
let opts;
|
|
45
52
|
if (host && host.search('http') !== -1) {
|
|
46
|
-
|
|
47
|
-
|
|
53
|
+
// The API's base URL: a trailing /api/v0 is dropped, since addUrl appends it.
|
|
54
|
+
apiUrl = host.replace(/\/+$/, '').replace(/\/api\/v0$/, '');
|
|
55
|
+
opts = { url: apiUrl };
|
|
48
56
|
} else {
|
|
49
57
|
const proto = protocol || 'http';
|
|
50
58
|
const p = port ? `:${port}` : '';
|
|
51
59
|
apiUrl = `${proto}://${host}${p}`.replace(/\/+$/, '');
|
|
52
|
-
|
|
53
|
-
if (token) opts.headers = { authorization: token };
|
|
54
|
-
ipfs = create(opts);
|
|
60
|
+
opts = { host, protocol: proto, port };
|
|
55
61
|
}
|
|
56
|
-
|
|
62
|
+
// The reads send the same Authorization header as the adds.
|
|
63
|
+
if (authHeader) opts.headers = { authorization: authHeader };
|
|
64
|
+
ipfs = create(opts);
|
|
57
65
|
};
|
|
58
66
|
|
|
59
67
|
// ---------------------------------------------------------------------------
|
|
@@ -336,8 +344,48 @@ const hashForIPFS = async (p) => {
|
|
|
336
344
|
return null;
|
|
337
345
|
};
|
|
338
346
|
|
|
347
|
+
// Pin `cid` on the API (`pin/add`), which fetches the blocks it does not hold
|
|
348
|
+
// from the peers that provide them and answers once the pin is complete.
|
|
349
|
+
// Resolves true when the API names the CID among its pins.
|
|
350
|
+
const pinOnIPFS = (cid, timeoutMs = 3600000) =>
|
|
351
|
+
new Promise((resolve, reject) => {
|
|
352
|
+
const url = new URL(`${apiUrl}/api/v0/pin/add?arg=${encodeURIComponent(cid)}&recursive=true`);
|
|
353
|
+
const mod = url.protocol === 'https:' ? https : http;
|
|
354
|
+
const headers = { 'Content-Length': '0' };
|
|
355
|
+
if (authHeader) headers.Authorization = authHeader;
|
|
356
|
+
const req = mod.request(
|
|
357
|
+
{
|
|
358
|
+
method: 'POST',
|
|
359
|
+
hostname: url.hostname,
|
|
360
|
+
port: url.port || (url.protocol === 'https:' ? 443 : 80),
|
|
361
|
+
path: `${url.pathname}${url.search}`,
|
|
362
|
+
headers,
|
|
363
|
+
timeout: timeoutMs
|
|
364
|
+
},
|
|
365
|
+
(res) => {
|
|
366
|
+
let text = '';
|
|
367
|
+
res.setEncoding('utf8');
|
|
368
|
+
res.on('data', (chunk) => {
|
|
369
|
+
text += chunk;
|
|
370
|
+
});
|
|
371
|
+
res.on('end', () => {
|
|
372
|
+
if (res.statusCode !== 200) {
|
|
373
|
+
console.log(`Failed to pin ${cid} on ${apiUrl}: status ${res.statusCode}`);
|
|
374
|
+
console.log(text.slice(0, 300));
|
|
375
|
+
resolve(false);
|
|
376
|
+
return;
|
|
377
|
+
}
|
|
378
|
+
resolve(text.includes(cid));
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
);
|
|
382
|
+
req.on('timeout', () => req.destroy(new Error(`pin/add of ${cid} took longer than ${timeoutMs} ms`)));
|
|
383
|
+
req.on('error', reject);
|
|
384
|
+
req.end();
|
|
385
|
+
});
|
|
386
|
+
|
|
339
387
|
// ---------------------------------------------------------------------------
|
|
340
|
-
// Download helpers (kubo-rpc-client
|
|
388
|
+
// Download helpers (kubo-rpc-client, with the Authorization header of initialize())
|
|
341
389
|
// ---------------------------------------------------------------------------
|
|
342
390
|
|
|
343
391
|
const getFromIPFS = async (hhash, filePath, maxRetries = process.env.REACT_APP_IPFS_RETRIES || 5) => {
|
|
@@ -412,6 +460,7 @@ export {
|
|
|
412
460
|
initialize,
|
|
413
461
|
upload,
|
|
414
462
|
hashForIPFS,
|
|
463
|
+
pinOnIPFS,
|
|
415
464
|
uploadFileToIPFS,
|
|
416
465
|
uploadFolderToIPFS,
|
|
417
466
|
getFromIPFS,
|
|
@@ -424,12 +473,12 @@ export {
|
|
|
424
473
|
// ---------------------------------------------------------------------------
|
|
425
474
|
|
|
426
475
|
program
|
|
427
|
-
.
|
|
476
|
+
.requiredOption('--host <host>', "IPFS API URL: the publish's own Kubo (http://127.0.0.1:<port>) or an IPFS API of the dApp's own")
|
|
428
477
|
.option('--hhash <hhash>', 'IPFS hash')
|
|
429
478
|
.option('--filePath <path>', 'Path to the file')
|
|
430
479
|
.option('--folderPath <path>', 'Path to the folder')
|
|
431
|
-
.option('--action <action>', 'Action to perform (upload, hash, download)')
|
|
432
|
-
.option('--token <token>', '
|
|
480
|
+
.option('--action <action>', 'Action to perform (upload, hash, pin, download)')
|
|
481
|
+
.option('--token <token>', 'Authorization header value for the API (IPFS_TOKEN in the environment when absent)')
|
|
433
482
|
.option('--output <path>', 'Output path for download');
|
|
434
483
|
|
|
435
484
|
const isMain = process.argv[1] && process.argv[1].endsWith('ipfs.mjs');
|
|
@@ -439,8 +488,7 @@ if (isMain) {
|
|
|
439
488
|
const options = program.opts();
|
|
440
489
|
|
|
441
490
|
const main = async () => {
|
|
442
|
-
|
|
443
|
-
initialize(host, undefined, undefined, options.token || '');
|
|
491
|
+
initialize(options.host, undefined, undefined, options.token || process.env.IPFS_TOKEN || '');
|
|
444
492
|
|
|
445
493
|
if (options.action === 'upload') {
|
|
446
494
|
if (options.filePath) {
|
|
@@ -469,6 +517,14 @@ if (isMain) {
|
|
|
469
517
|
const hhash = await hashForIPFS(target);
|
|
470
518
|
console.log(`${hhash}`);
|
|
471
519
|
process.exit(hhash ? 0 : 1);
|
|
520
|
+
} else if (options.action === 'pin') {
|
|
521
|
+
if (!options.hhash) {
|
|
522
|
+
console.error('Please provide --hhash, the CID to pin.');
|
|
523
|
+
process.exit(1);
|
|
524
|
+
}
|
|
525
|
+
const pinned = await pinOnIPFS(options.hhash);
|
|
526
|
+
if (pinned) console.log(`${options.hhash}`);
|
|
527
|
+
process.exit(pinned ? 0 : 1);
|
|
472
528
|
} else if (options.action === 'download') {
|
|
473
529
|
if (options.filePath) {
|
|
474
530
|
await getFromIPFS(options.hhash, options.filePath);
|
|
@@ -480,7 +536,7 @@ if (isMain) {
|
|
|
480
536
|
console.error('Please provide a filePath or folderPath for download.');
|
|
481
537
|
}
|
|
482
538
|
} else {
|
|
483
|
-
console.error('Please provide a valid action (upload, hash, download).');
|
|
539
|
+
console.error('Please provide a valid action (upload, hash, pin, download).');
|
|
484
540
|
process.exit(1);
|
|
485
541
|
}
|
|
486
542
|
};
|
|
@@ -3,7 +3,10 @@ version: '3.2'
|
|
|
3
3
|
services:
|
|
4
4
|
etny-swift-stream:
|
|
5
5
|
container_name: etny-swift-stream
|
|
6
|
-
|
|
6
|
+
# quay.io/minio/minio:RELEASE.2023-03-13T19-46-17Z, mirrored on 2026-10-13
|
|
7
|
+
# (image id c69ff79d5831): quay.io answers 401 to an anonymous pull of
|
|
8
|
+
# that tag, and the mirror is pulled without credentials.
|
|
9
|
+
image: registry.ethernity.cloud:443/debuggingdelight/ethernity-cloud-sdk-registry/minio/minio:RELEASE.2023-03-13T19-46-17Z
|
|
7
10
|
command: server /data
|
|
8
11
|
ports:
|
|
9
12
|
- "9000:9000"
|
|
@@ -39,7 +39,7 @@ function setVars(network = "") {
|
|
|
39
39
|
}
|
|
40
40
|
} else if (BLOCKCHAIN_NETWORK.includes("LitVM")) {
|
|
41
41
|
NETWORK_RPC = "https://liteforge.rpc.caldera.xyz/infra-partner-http";
|
|
42
|
-
IMAGE_REGISTRY_ADDRESS = "
|
|
42
|
+
IMAGE_REGISTRY_ADDRESS = "0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D";
|
|
43
43
|
CHAIN_ID = 4441;
|
|
44
44
|
}
|
|
45
45
|
}
|