@ethernity-cloud/sdk-js 1.7.9 → 1.7.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 CHANGED
@@ -56,23 +56,29 @@ 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 `0x8ad24b3F...`, ValidatorRegistry
59
+ validator set (SessionRegistry `0xfA70B5Df...`, ValidatorRegistry
60
60
  `0xbE3759f3...`).
61
61
  - LiteForge Unsafe - the unsafe network beside LiteForge (see below).
62
+ - Ethereum:
63
+ - Sepolia - CAS-attested the same way, by Sepolia's own ethernity-cas
64
+ validator set (SessionRegistry `0xDbB249E1...`, ValidatorRegistry
65
+ `0x8ad24b3F...`), on the PoX family of 2026-10-14.
66
+ - Sepolia Unsafe - the unsafe network beside Sepolia (see below).
62
67
  - Polyhon:
63
68
  - Amoy Testnet - to be provided during the following updates
64
69
  - Mainnet - to be provided during the following updates
65
70
 
66
- The remaining testnets (Amoy, IoTeX, Sepolia) have no ethernity-cas
71
+ The remaining testnets (Amoy, IoTeX) have no ethernity-cas
67
72
  deployment: their enclaves are debug-signed and sign their own certificates
68
73
  from their measurement, and `ecld-publish` registers no CAS session for them.
69
74
 
70
75
  ### The testnets and their unsafe networks
71
- `Bloxberg Testnet` and `LitVM LiteForge` run your securelock in an enclave
76
+ `Bloxberg Testnet`, `LitVM LiteForge` and `Ethereum Sepolia` run your securelock in an enclave
72
77
  attested through that chain's ethernity-cas validator set, on nodes whose
73
78
  platform the CAS can attest (DCAP). Each has an unsafe network beside it,
74
- `Bloxberg Testnet Unsafe` (`Bloxberg_Testnet_Unsafe`) and `LitVM LiteForge
75
- Unsafe` (`LitVM_LiteForge_Unsafe`): the same chain and contracts, for hardware
79
+ `Bloxberg Testnet Unsafe` (`Bloxberg_Testnet_Unsafe`), `LitVM LiteForge
80
+ Unsafe` (`LitVM_LiteForge_Unsafe`) and `Ethereum Sepolia Unsafe`
81
+ (`Ethereum_Sepolia_Unsafe`): the same chain and contracts, for hardware
76
82
  SGX platforms the CAS cannot attest (EPID-only, SGX1). No CAS and no LAS: the
77
83
  enclaves are debug-signed and sign their own certificates from their
78
84
  measurement, so a result proves which image ran but not that an enclave ran
@@ -82,7 +88,8 @@ always says so in the network it names. A dApp publishes one securelock per
82
88
  network from two project directories with the same project name: the unsafe
83
89
  one is registered as `<project>-unsafe` and runs on the
84
90
  `etny-<nodenithy|pynithy>-testnet-unsafe`
85
- (`ecld-<nodenithy|pynithy>-litvm-testnet-unsafe`) trustedzone. Both dApp types
91
+ (`ecld-<nodenithy|pynithy>-litvm-testnet-unsafe`,
92
+ `ecld-<nodenithy|pynithy>-ethereum-sepolia-unsafe`) trustedzone. Both dApp types
86
93
  build for both networks. Mainnet has no unsafe network.
87
94
 
88
95
  ### Pynithy dApps
@@ -120,11 +127,14 @@ sets the enclave heap (default `1024M`).
120
127
  unless `REWARD_ADDRESS` names another one; the image's owner can change it
121
128
  later with the registry's `changeImageRewardAddress`.
122
129
 
123
- How the image reaches the network: unless `.env` names an IPFS API of your
124
- own (`IPFS_ENDPOINT` other than the public `ipfs.ethernity.cloud`), the
125
- publish runs its own IPFS node in docker (`ipfs/kubo`), adds the image and
126
- its compose there and peers it with the network's bootnode, which pins
127
- every registered image. On a network whose image registry records images
130
+ How the image reaches the network: leave `IPFS_ENDPOINT` in `.env` empty
131
+ unless it names an IPFS API of your own (`IPFS_TOKEN` is the Authorization
132
+ header value that API takes, when it takes one); the public API
133
+ `ipfs.ethernity.cloud` takes no adds or pins without a writer token. With
134
+ no API of your own the publish runs its own IPFS node in docker
135
+ (`ipfs/kubo`), adds the image and its compose there and peers it with the
136
+ network's bootnode, which pins every registered image. On a network whose
137
+ image registry records images
128
138
  before their certificate (the bloxberg testnet), the publish registers the
129
139
  image from your wallet first (`registerImage`, with your node's address),
130
140
  has the certificate extracted (on your SGX host, or by
@@ -160,13 +170,22 @@ sets the enclave heap (default `1024M`).
160
170
  npx ecld-run --unsafe 'hello("World")'
161
171
  ```
162
172
  This is the network-side sibling of `ecld-test`: instead of executing locally
163
- it drives the runner end to end — encrypt → IPFS → on-chain request → wait for
164
- a node → download and decrypt the result. It **costs gas and needs a funded
165
- key**. Network and enclaves come from `.env` (`BLOCKCHAIN_NETWORK`,
166
- `PROJECT_NAME`, `TRUSTED_ZONE_IMAGE`), overridable with
173
+ it drives the runner end to end — encrypt → on-chain request → payload intake
174
+ (or the IPFS API named with `--ipfs <url>`) → wait for a node → download and
175
+ decrypt the result. It **costs gas and needs a funded key**. Network and
176
+ enclaves come from `.env` (`BLOCKCHAIN_NETWORK`, `PROJECT_NAME`,
177
+ `TRUSTED_ZONE_IMAGE`), overridable with
167
178
  `--network`/`--securelock`/`--trustedzone`; the signing key is `PRIVATE_KEY`
168
179
  (or `ECLD_PRIVATE_KEY`) — a funded `0x` key that pays for the order. Resources
169
180
  are tunable with `--task-price`/`--cpu`/`--memory`/`--storage`/`--bandwidth`/`--duration`/`--validators`.
181
+ `--ipfs intake` (the default) delivers the payload to the bootnode's payload
182
+ intake once the request is on chain; `--ipfs <url>` writes and reads through
183
+ that IPFS RPC API instead, with `ECLD_IPFS_TOKEN` (or `IPFS_TOKEN`) as its
184
+ Authorization header, and a URL naming the public API is refused without one.
185
+ The intake serves the networks the bootnode's agent runs, today the bloxberg
186
+ testnet, LitVM LiteForge and Ethereum Sepolia, each with its unsafe twin: a dApp on any other
187
+ network (Bloxberg mainnet, Polygon, IoTeX) needs an IPFS API of its
188
+ own (`IPFS_ENDPOINT` for the Hello World template, `--ipfs <url>` for `ecld-run`).
170
189
  Exit code 0 on a `SUCCESS` task result, 1 otherwise. `--unsafe` runs the
171
190
  network's unsafe variant; on an unsafe network the securelock and
172
191
  trustedzone default to their `-unsafe` names.
@@ -214,13 +233,17 @@ function hello(msg='World') {
214
233
 
215
234
  module.exports = { hello };
216
235
  ```
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 endpoint.
236
+ 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
237
  ```js
219
238
  import './App.css';
220
239
  import EthernityCloudRunner from "@ethernity-cloud/runner";
221
240
  import { ECStatus } from "@ethernity-cloud/runner/enums";
222
241
 
223
- // Filled in from the project's .env by src/preStart.mjs on `npm start`.
242
+ // Filled in from the project's .env by src/preStart.mjs on `npm start`: the
243
+ // securelock this dApp is published as, the trustedzone it was built against,
244
+ // the network's token address and chain id, the -unsafe network when the dApp
245
+ // runs without a CAS (null otherwise), and the IPFS API of the dApp's own ('' when
246
+ // the task's artefacts go through the bootnode's payload intake).
224
247
  const PROJECT_NAME = "";
225
248
  const TRUSTED_ZONE_IMAGE = "";
226
249
  const NETWORK_ADDRESS = "";
@@ -238,7 +261,10 @@ function App() {
238
261
  if (UNSAFE_NETWORK) {
239
262
  await runner.setNetwork(...UNSAFE_NETWORK);
240
263
  }
241
- runner.initializeStorage(IPFS_ENDPOINT);
264
+ // An IPFS API of the dApp's own (IPFS_ENDPOINT) carries the task's
265
+ // artefacts; without one they go through the bootnode's payload intake.
266
+ if (IPFS_ENDPOINT) runner.initializeStorage(IPFS_ENDPOINT);
267
+ else runner.initializePublicIntake();
242
268
 
243
269
  // Events are named by task status; detail is { message, status, progress }.
244
270
  runner.addEventListener(ECStatus.DEFAULT, (e) => {
@@ -250,6 +276,7 @@ function App() {
250
276
  runner.addEventListener(ECStatus.SUCCESS, async () => {
251
277
  const result = await runner.getResult();
252
278
  console.log(`Task Result: ${result}`);
279
+ // display the result in page below the buttons
253
280
  const line = document.createElement("p");
254
281
  line.textContent = `Task Result: ${result}`;
255
282
  document.body.appendChild(line);
package/cas/config.js CHANGED
@@ -18,21 +18,29 @@ const VALIDATOR_REGISTRY = {
18
18
  'etny-nodenithy-testnet': '0x0014aB102FEa6b02e3D793FF4b9a0Af309fB4E14',
19
19
  'ecld-pynithy-litvm-testnet': '0xbE3759f327e8643fC5A1717fb2072A0dAcBAD29E',
20
20
  'ecld-nodenithy-litvm-testnet': '0xbE3759f327e8643fC5A1717fb2072A0dAcBAD29E',
21
+ 'ecld-pynithy-ethereum-sepolia': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
22
+ 'ecld-nodenithy-ethereum-sepolia': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
21
23
  };
22
24
 
23
25
  const SESSION_REGISTRY = {
24
26
  'etny-pynithy-testnet': '0x444429681fCb26470Cb12B5436E48402cF7478A3',
25
27
  'etny-nodenithy-testnet': '0x444429681fCb26470Cb12B5436E48402cF7478A3',
26
- 'ecld-pynithy-litvm-testnet': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
27
- 'ecld-nodenithy-litvm-testnet': '0x8ad24b3F406A41a0F8D3440021792EB203957F43',
28
+ 'ecld-pynithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
29
+ 'ecld-nodenithy-litvm-testnet': '0xfA70B5Df7C773A21164cAc5413704117e4dEaB76',
30
+ // Bound to Sepolia's ECImageRegistryV3 of 2026-10-14.
31
+ 'ecld-pynithy-ethereum-sepolia': '0xDbB249E108d19fC1691483Bf897bA8BA0dA99c06',
32
+ 'ecld-nodenithy-ethereum-sepolia': '0xDbB249E108d19fC1691483Bf897bA8BA0dA99c06',
28
33
  };
29
34
 
30
- // The chain the registries above live on.
35
+ // The chain the registries above live on. Sepolia's RPC is Tenderly's gateway:
36
+ // publicnode caps eth_getLogs at 50,000 blocks and prunes older ranges.
31
37
  const CHAIN = {
32
38
  'etny-pynithy-testnet': { rpcUrl: 'https://bloxberg.ethernity.cloud', chainId: 8995 },
33
39
  'etny-nodenithy-testnet': { rpcUrl: 'https://bloxberg.ethernity.cloud', chainId: 8995 },
34
40
  'ecld-pynithy-litvm-testnet': { rpcUrl: 'https://liteforge.rpc.caldera.xyz/infra-partner-http', chainId: 4441 },
35
41
  'ecld-nodenithy-litvm-testnet': { rpcUrl: 'https://liteforge.rpc.caldera.xyz/infra-partner-http', chainId: 4441 },
42
+ 'ecld-pynithy-ethereum-sepolia': { rpcUrl: 'https://sepolia.gateway.tenderly.co', chainId: 11155111 },
43
+ 'ecld-nodenithy-ethereum-sepolia': { rpcUrl: 'https://sepolia.gateway.tenderly.co', chainId: 11155111 },
36
44
  };
37
45
 
38
46
  // Whether the securelock takes its certificate from a CAS session: mainnet
@@ -55,6 +63,7 @@ const isMainnetNetwork = (network) =>
55
63
  const UNSAFE_NETWORKS = {
56
64
  Bloxberg_Testnet_Unsafe: 'Bloxberg_Testnet',
57
65
  LitVM_LiteForge_Unsafe: 'LitVM_LiteForge',
66
+ Ethereum_Sepolia_Unsafe: 'Ethereum_Sepolia',
58
67
  };
59
68
 
60
69
  // Case-insensitive: ecld-init writes Bloxberg_Testnet_Unsafe, a --network
@@ -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
- async function pinBody(ipfsApiUrl, body) {
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 r = await axios.post(`${ipfsApiUrl}/api/v0/add?cid-version=1&raw-leaves=true&pin=true`, form, { timeout: 60000 });
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` and register it under its own `name:`. Returns
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,10 +51,18 @@ 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: '0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2',
55
- imageRegistry: '0x55e0ad455Be85162b71a790f00Fc305680E3CE53',
54
+ protocol: '0xE35991de1f2A435e29E70406FA2781b1AffCb168',
55
+ imageRegistry: '0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D',
56
56
  esr: '0x709052Fe77Af543d3d9FE2Ac06a15c635c8D4Be5',
57
57
  },
58
+ // The family of 2026-10-14, owned by 0xA247...; the previous PoX 0x29D3eC87...
59
+ // and V1 registry 0x55e0ad45... are abandoned.
60
+ ETHEREUM_SEPOLIA: {
61
+ type: 'testnet', chainId: 11155111, rpc: 'https://sepolia.gateway.tenderly.co',
62
+ protocol: '0x798975F9FdD90A21Cb11Ac26F1377e1542c58f9c',
63
+ imageRegistry: '0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F',
64
+ esr: '0x9927809B61122B2af3f3b3A3303875e0687b8eE3',
65
+ },
58
66
  };
59
67
  // The -unsafe networks: the chain and contracts of the network each is the
60
68
  // unsafe variant of, without a CAS.
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
- const rl = readline.createInterface({
8
- input: process.stdin,
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();
@@ -195,6 +214,7 @@ const main = async () => {
195
214
  "LitVM LiteForge",
196
215
  "Bloxberg Testnet Unsafe",
197
216
  "LitVM LiteForge Unsafe",
217
+ "Ethereum Sepolia Unsafe",
198
218
  ];
199
219
  const blockchainNetwork = await promptOptions(
200
220
  "On which Blockchain network do you want to have the app set up, as a starting point? (default is Bloxberg Testnet): ",
@@ -256,30 +276,28 @@ const main = async () => {
256
276
  // });
257
277
 
258
278
  console.log();
259
- const ipfsServiceOptions = ["Ethernity (best effort)", "Custom IPFS"];
260
279
  const ipfsService = await promptOptions(
261
- "Select the IPFS pinning service you want to use (default is Ethernity): ",
262
- ipfsServiceOptions,
263
- "Ethernity (best effort)",
280
+ "Which IPFS API do the publish and the dApp write through? (default is 1): ",
281
+ IPFS_OPTIONS,
282
+ IPFS_OPTIONS[0],
264
283
  );
265
284
 
266
- let customUrl, ipfsToken;
267
- if (ipfsService === "Custom IPFS") {
268
- customUrl = await new Promise((resolve) =>
285
+ let ipfsUrl, ipfsToken;
286
+ if (ipfsService === IPFS_OPTIONS[1]) {
287
+ ipfsUrl = await new Promise((resolve) =>
269
288
  rl.question(
270
- "Enter the endpoint URL for the IPFS pinning service you want to use: ",
289
+ "Enter the URL of the IPFS RPC API (e.g. http://127.0.0.1:5001): ",
271
290
  resolve,
272
291
  ),
273
292
  );
274
293
  ipfsToken = await new Promise((resolve) =>
275
294
  rl.question(
276
- "Enter the access token to be used when calling the IPFS pinning service: ",
295
+ "Enter its Authorization header value (empty for none): ",
277
296
  resolve,
278
297
  ),
279
298
  );
280
- } else {
281
- customUrl = "https://ipfs.ethernity.cloud";
282
299
  }
300
+ const ipfs = ipfsSettings(ipfsService, ipfsUrl, ipfsToken);
283
301
 
284
302
  fs.mkdirSync("src/serverless", { recursive: true });
285
303
 
@@ -373,16 +391,17 @@ const main = async () => {
373
391
  "LitVM LiteForge": `ecld-${svc}-litvm-testnet`,
374
392
  "Bloxberg Testnet Unsafe": `etny-${svc}-testnet-unsafe`,
375
393
  "LitVM LiteForge Unsafe": `ecld-${svc}-litvm-testnet-unsafe`,
394
+ "Ethereum Sepolia Unsafe": `ecld-${svc}-ethereum-sepolia-unsafe`,
376
395
  };
377
396
  const trustedZoneImage = imageByNetwork[blockchainNetwork] || `etny-${svc}-testnet`;
378
397
  writeEnv("TRUSTED_ZONE_IMAGE", trustedZoneImage);
379
398
 
380
399
 
381
400
  writeEnv("BLOCKCHAIN_NETWORK", network);
382
- writeEnv("IPFS_ENDPOINT", customUrl);
401
+ writeEnv("IPFS_ENDPOINT", ipfs.IPFS_ENDPOINT);
402
+ writeEnv("IPFS_TOKEN", ipfs.IPFS_TOKEN);
383
403
  console.log();
384
404
  await configureEsr();
385
- writeEnv("IPFS_TOKEN", ipfsToken || "");
386
405
  writeEnv("VERSION", "v1");
387
406
  console.log();
388
407
  console.log(
@@ -391,4 +410,6 @@ const main = async () => {
391
410
  rl.close();
392
411
  };
393
412
 
394
- main();
413
+ if (require.main === module) main();
414
+
415
+ 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 write API of ipfs.ethernity.cloud is closing, so a publish without an
5
- // IPFS endpoint of its own runs a Kubo in docker (the SDK already requires
6
- // docker), adds the image through that Kubo's HTTP API with the same call the
7
- // public API received (so a build gives the same CIDs as before), peers the
8
- // Kubo with the bootnode, whose mirror pins every registered image and from
9
- // which the certificate extraction service fetches, and stops it once the
10
- // certificate is on chain. The Kubo listens for the API on the loopback
11
- // interface only; its swarm port is published when free, so a publisher with
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/network.js CHANGED
@@ -31,6 +31,7 @@ function networkFor({ ECAddress, ECNetworkByChainId }, raw) {
31
31
  IOTEX_TESTNET: { address: ECAddress.IOTEX.TESTNET_ADDRESS, chainId: ECNetworkByChainId.IOTEX.TESTNET },
32
32
  ETHEREUM_SEPOLIA: sepolia,
33
33
  SEPOLIA: sepolia,
34
+ ETHEREUM_SEPOLIA_UNSAFE: { ...sepolia, unsafe: ['SEPOLIA', 'TESTNET_UNSAFE'] },
34
35
  LITVM_LITEFORGE: litvm,
35
36
  LITVM: litvm,
36
37
  LITVM_LITEFORGE_UNSAFE: { ...litvm, unsafe: ['LITVM', 'TESTNET_UNSAFE'] },
@@ -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 (shares sepolia contracts; distinct chain/RPC) ---
52
- 'ecld-pynithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
53
- 'ecld-nodenithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
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': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
58
- 'ecld-nodenithy-litvm-testnet-unsafe': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441]
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
@@ -1,6 +1,10 @@
1
1
  // ipfs.mjs
2
2
  //
3
- // IPFS client for the Ethernity Cloud JS SDK.
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 keeps the existing kubo-rpc-client based helpers.
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 : e.g. "https://ipfs.ethernity.cloud"
35
- // addUrl : `${apiUrl}/api/v0/add`
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; // Authorization header value, or 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
- apiUrl = host.replace(/\/+$/, '');
47
- ipfs = create(host);
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
- const opts = { host, protocol: proto, port };
53
- if (token) opts.headers = { authorization: token };
54
- ipfs = create(opts);
60
+ opts = { host, protocol: proto, port };
55
61
  }
56
- authHeader = token && token !== '' ? token : null;
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 based; unchanged behaviour)
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
- .option('--host <host>', 'IPFS host')
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>', 'IPFS authorization 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
- const host = options.host || 'localhost';
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
- image: quay.io/minio/minio:RELEASE.2023-03-13T19-46-17Z
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,8 +39,19 @@ 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 = "0x55e0ad455Be85162b71a790f00Fc305680E3CE53";
42
+ IMAGE_REGISTRY_ADDRESS = "0xAf8Ce29103E1f59feDD49688ff070Ffc2346AF1D";
43
43
  CHAIN_ID = 4441;
44
+ } else if (BLOCKCHAIN_NETWORK.includes("Sepolia")) {
45
+ // Sepolia's ECImageRegistryV3 of 2026-10-14; Tenderly's gateway serves
46
+ // the log ranges publicnode prunes.
47
+ NETWORK_RPC = "https://sepolia.gateway.tenderly.co";
48
+ IMAGE_REGISTRY_ADDRESS = "0xa7E43a269E3307cf9d246b07df2B89CAA1b4C42F";
49
+ CHAIN_ID = 11155111;
50
+ // Sepolia's EVM charges storage-heavy calls several times the local
51
+ // EVM's gas (a contract creation about seven times); the base fee is
52
+ // under 0.0001 gwei, so 0.1 gwei is paid in full as the tip.
53
+ GAS = 20000000;
54
+ GAS_PRICE = 100000000;
44
55
  }
45
56
  }
46
57