@ethernity-cloud/sdk-js 1.7.8 → 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 +6 -2
- 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 +6 -2
- package/pynithy/src/preStart.mjs +6 -2
- package/run.js +28 -5
- 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/nodenithy/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
-
const { execSync } = require('child_process');
|
|
2
|
+
const { execSync, execFileSync } = require('child_process');
|
|
3
3
|
require('dotenv').config();
|
|
4
4
|
const readline = require('readline');
|
|
5
5
|
const forge = require('node-forge');
|
|
@@ -8,7 +8,7 @@ const https = require('https');
|
|
|
8
8
|
const casConfig = require('../cas/config.js');
|
|
9
9
|
const casResolver = require('../cas/resolver.js');
|
|
10
10
|
const sessionRegistry = require('../cas/session_registry.js');
|
|
11
|
-
const { LocalKubo, ownEndpoint } = require('../localKubo.js');
|
|
11
|
+
const { LocalKubo, ownEndpoint, PUBLIC_IPFS_HOST } = require('../localKubo.js');
|
|
12
12
|
|
|
13
13
|
if (!fs.existsSync('.env')) {
|
|
14
14
|
console.error("Error: .env file not found");
|
|
@@ -121,13 +121,17 @@ const main = async () => {
|
|
|
121
121
|
const trustedZoneHeap = isMainnet ? '256M' : '1024M';
|
|
122
122
|
|
|
123
123
|
// Where the image goes. An IPFS API of the application's own
|
|
124
|
-
// (IPFS_ENDPOINT,
|
|
125
|
-
// Otherwise the publish runs its own Kubo: the public
|
|
126
|
-
// ipfs.ethernity.cloud
|
|
127
|
-
//
|
|
124
|
+
// (IPFS_ENDPOINT, with IPFS_TOKEN as its Authorization header when set)
|
|
125
|
+
// takes precedence. Otherwise the publish runs its own Kubo: the public
|
|
126
|
+
// API of ipfs.ethernity.cloud takes no adds or pins without a writer
|
|
127
|
+
// token, and the image is held by this Kubo, peered with the bootnode,
|
|
128
|
+
// until its certificate is on chain.
|
|
128
129
|
let localKubo = null;
|
|
129
130
|
let ipfsApi = process.env.IPFS_ENDPOINT || '';
|
|
130
|
-
|
|
131
|
+
let ipfsToken = '';
|
|
132
|
+
if (ownEndpoint(ipfsApi)) {
|
|
133
|
+
ipfsToken = process.env.IPFS_TOKEN || '';
|
|
134
|
+
} else {
|
|
131
135
|
localKubo = new LocalKubo(process.env.PROJECT_NAME);
|
|
132
136
|
try {
|
|
133
137
|
await localKubo.start();
|
|
@@ -139,23 +143,28 @@ const main = async () => {
|
|
|
139
143
|
console.log(`\t✔ IPFS node for this publish: ${ipfsApi}`);
|
|
140
144
|
process.on('exit', () => localKubo.release());
|
|
141
145
|
}
|
|
146
|
+
// node ../ipfs.mjs against ipfsApi, with the token as IPFS_TOKEN in its
|
|
147
|
+
// environment: the command line is in a failure's message and in the
|
|
148
|
+
// process list. A non-zero exit ends the publish.
|
|
149
|
+
const ipfsEnv = { ...process.env, IPFS_TOKEN: ipfsToken };
|
|
150
|
+
const ipfsCli = (args, stdio) => {
|
|
151
|
+
try {
|
|
152
|
+
return execFileSync(process.execPath, ['../ipfs.mjs', '--host', ipfsApi, ...args], { stdio, env: ipfsEnv });
|
|
153
|
+
} catch (e) {
|
|
154
|
+
console.error(`Error: node ../ipfs.mjs ${args.join(' ')} failed (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
|
|
155
|
+
process.exit(1);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
142
158
|
|
|
143
159
|
// The CIDs uploadToIpfs gives, from the same add calls with only-hash: the
|
|
144
160
|
// IPFS node stores and announces nothing. Set by hashForIpfs, after which
|
|
145
161
|
// uploadToIpfs refuses an upload that gives other CIDs.
|
|
146
162
|
let expectedCids = null;
|
|
147
163
|
const hashForIpfs = () => {
|
|
148
|
-
const hashOf = (target) =>
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
} catch (e) {
|
|
153
|
-
console.error(`Error: could not hash ${target} for IPFS: ${e.message}`);
|
|
154
|
-
process.exit(1);
|
|
155
|
-
}
|
|
156
|
-
};
|
|
157
|
-
process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf('--filePath docker-compose-final.yml');
|
|
158
|
-
process.env.IPFS_HASH = hashOf(`--folderPath ${registryPath}`);
|
|
164
|
+
const hashOf = (target) => ipfsCli(['--action', 'hash', ...target], ['ignore', 'pipe', 'inherit'])
|
|
165
|
+
.toString().trim().split('\n').pop().trim();
|
|
166
|
+
process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf(['--filePath', 'docker-compose-final.yml']);
|
|
167
|
+
process.env.IPFS_HASH = hashOf(['--folderPath', registryPath]);
|
|
159
168
|
console.log("IPFS_DOCKER_COMPOSE_HASH (before upload): ", process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
160
169
|
console.log("IPFS_HASH (before upload): ", process.env.IPFS_HASH);
|
|
161
170
|
expectedCids = { image: process.env.IPFS_HASH, compose: process.env.IPFS_DOCKER_COMPOSE_HASH };
|
|
@@ -168,7 +177,7 @@ const main = async () => {
|
|
|
168
177
|
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
169
178
|
}
|
|
170
179
|
console.log('Upload docker-compose-final.yml to IPFS');
|
|
171
|
-
|
|
180
|
+
ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], 'inherit');
|
|
172
181
|
if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
|
|
173
182
|
console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
|
|
174
183
|
process.exit(1);
|
|
@@ -177,7 +186,7 @@ const main = async () => {
|
|
|
177
186
|
console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
178
187
|
writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
179
188
|
console.log('Upload docker registry to IPFS');
|
|
180
|
-
|
|
189
|
+
ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
|
|
181
190
|
if (!fs.existsSync('IPFS_HASH.ipfs')) {
|
|
182
191
|
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
183
192
|
process.exit(1);
|
|
@@ -324,7 +333,7 @@ const main = async () => {
|
|
|
324
333
|
const registered = await sessionRegistry.register(
|
|
325
334
|
casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
|
|
326
335
|
process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
|
|
327
|
-
ipfsApi, '');
|
|
336
|
+
ipfsApi, '', ipfsToken);
|
|
328
337
|
if (localKubo) await localKubo.provide(registered.cid);
|
|
329
338
|
if (registered.registered) {
|
|
330
339
|
console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
|
|
@@ -589,6 +598,29 @@ const main = async () => {
|
|
|
589
598
|
if (registryV2 && localKubo) {
|
|
590
599
|
await localKubo.provide(process.env.IPFS_HASH);
|
|
591
600
|
await localKubo.provide(process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
601
|
+
} else if (!registryV2 && localKubo) {
|
|
602
|
+
// On a V1 registry nothing is recorded before the certificate, so the
|
|
603
|
+
// bootnode's mirror pins nothing from this Kubo, and the certificate
|
|
604
|
+
// extraction service and the nodes, which fetch from the bootnode,
|
|
605
|
+
// would wait on an image it never receives: the compose and the image
|
|
606
|
+
// are pinned on the public API, which fetches them from this Kubo over
|
|
607
|
+
// the connection it holds. The pin takes a token (IPFS_TOKEN); without
|
|
608
|
+
// one the publish stops here, before anything is on chain.
|
|
609
|
+
const publicToken = process.env.IPFS_TOKEN || '';
|
|
610
|
+
if (!publicToken) {
|
|
611
|
+
console.error("Error: this network's image registry records an image only with its certificate, so the image must be pinned on the public IPFS API before the certificate is extracted, and the public API takes a pin only with a token.");
|
|
612
|
+
console.error(` Set IPFS_TOKEN to a writer token of ${PUBLIC_IPFS_HOST} and publish again.`);
|
|
613
|
+
process.exit(1);
|
|
614
|
+
}
|
|
615
|
+
for (const cid of [process.env.IPFS_DOCKER_COMPOSE_HASH, process.env.IPFS_HASH]) {
|
|
616
|
+
console.log(`Pinning ${cid} on the public IPFS API`);
|
|
617
|
+
try {
|
|
618
|
+
execFileSync(process.execPath, ['../ipfs.mjs', '--host', `https://${PUBLIC_IPFS_HOST}`, '--action', 'pin', '--hhash', cid], { stdio: 'inherit', env: { ...process.env, IPFS_TOKEN: publicToken } });
|
|
619
|
+
} catch (e) {
|
|
620
|
+
console.error(`Error: the public IPFS API did not pin ${cid} (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
|
|
621
|
+
process.exit(1);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
592
624
|
}
|
|
593
625
|
if (casTestnet) await registerSessionOnChain();
|
|
594
626
|
|
|
@@ -722,7 +754,7 @@ const main = async () => {
|
|
|
722
754
|
fs.unlinkSync('IPFS_HASH.ipfs');
|
|
723
755
|
}
|
|
724
756
|
console.log('Upload docker registry to IPFS');
|
|
725
|
-
|
|
757
|
+
ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
|
|
726
758
|
if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
|
|
727
759
|
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
728
760
|
process.exit(1);
|
|
@@ -5,7 +5,8 @@ import { ECStatus } from "@ethernity-cloud/runner/enums";
|
|
|
5
5
|
// Filled in from the project's .env by src/preStart.mjs on `npm start`: the
|
|
6
6
|
// securelock this dApp is published as, the trustedzone it was built against,
|
|
7
7
|
// the network's token address and chain id, the -unsafe network when the dApp
|
|
8
|
-
// runs without a CAS (null otherwise), and the IPFS
|
|
8
|
+
// runs without a CAS (null otherwise), and the IPFS API of the dApp's own ('' when
|
|
9
|
+
// the task's artefacts go through the bootnode's payload intake).
|
|
9
10
|
const PROJECT_NAME = "";
|
|
10
11
|
const TRUSTED_ZONE_IMAGE = "";
|
|
11
12
|
const NETWORK_ADDRESS = "";
|
|
@@ -23,7 +24,10 @@ function App() {
|
|
|
23
24
|
if (UNSAFE_NETWORK) {
|
|
24
25
|
await runner.setNetwork(...UNSAFE_NETWORK);
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
// An IPFS API of the dApp's own (IPFS_ENDPOINT) carries the task's
|
|
28
|
+
// artefacts; without one they go through the bootnode's payload intake.
|
|
29
|
+
if (IPFS_ENDPOINT) runner.initializeStorage(IPFS_ENDPOINT);
|
|
30
|
+
else runner.initializePublicIntake();
|
|
27
31
|
|
|
28
32
|
// Events are named by task status; detail is { message, status, progress }.
|
|
29
33
|
runner.addEventListener(ECStatus.DEFAULT, (e) => {
|
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// .env, the way ecld-run reads the same values. PROJECT_NAME and
|
|
4
4
|
// TRUSTED_ZONE_IMAGE take their -unsafe form on an -unsafe network; the
|
|
5
5
|
// network's token address, chain id and -unsafe (network, type) come from the
|
|
6
|
-
// runner's own enums through the SDK's network table
|
|
6
|
+
// runner's own enums through the SDK's network table; IPFS_ENDPOINT is the
|
|
7
|
+
// IPFS API of the dApp's own, or '' (no setting, or the public API, which
|
|
8
|
+
// takes no writes without a token), on which the dApp delivers its task
|
|
9
|
+
// artefacts through the bootnode's payload intake.
|
|
7
10
|
import fs from 'fs';
|
|
8
11
|
import { createRequire } from 'module';
|
|
9
12
|
import dotenv from 'dotenv';
|
|
@@ -13,6 +16,7 @@ dotenv.config();
|
|
|
13
16
|
const require = createRequire(import.meta.url);
|
|
14
17
|
const casConfig = require('@ethernity-cloud/sdk-js/cas/config.js');
|
|
15
18
|
const { networkFor } = require('@ethernity-cloud/sdk-js/network.js');
|
|
19
|
+
const { dappEndpoint } = require('@ethernity-cloud/sdk-js/localKubo.js');
|
|
16
20
|
|
|
17
21
|
const { PROJECT_NAME, TRUSTED_ZONE_IMAGE, BLOCKCHAIN_NETWORK, IPFS_ENDPOINT } = process.env;
|
|
18
22
|
if (!PROJECT_NAME || !BLOCKCHAIN_NETWORK) {
|
|
@@ -31,7 +35,7 @@ const constants = {
|
|
|
31
35
|
NETWORK_ADDRESS: network.address,
|
|
32
36
|
CHAIN_ID: network.chainId || null,
|
|
33
37
|
UNSAFE_NETWORK: network.unsafe || null,
|
|
34
|
-
IPFS_ENDPOINT: IPFS_ENDPOINT
|
|
38
|
+
IPFS_ENDPOINT: dappEndpoint(IPFS_ENDPOINT),
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
const filePath = 'src/ec_helloworld_example.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ethernity-cloud/sdk-js",
|
|
3
|
-
"version": "1.7.
|
|
3
|
+
"version": "1.7.10",
|
|
4
4
|
"description": "A package to run ethernity cloud build scripts",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test \"test/**/*.test.js\"",
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
"!**/*.pyc"
|
|
40
40
|
],
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"@ethernity-cloud/runner": "^0.6.
|
|
42
|
+
"@ethernity-cloud/runner": "^0.6.5",
|
|
43
43
|
"acorn": "^8.11.0",
|
|
44
44
|
"adm-zip": "^0.5.14",
|
|
45
45
|
"axios": "^1.7.2",
|
package/pynithy/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/pynithy/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
|
minio:
|
|
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
|
expose:
|
|
9
12
|
- "9000"
|
|
@@ -3,7 +3,10 @@ version: '3.2'
|
|
|
3
3
|
services:
|
|
4
4
|
minio:
|
|
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
|
expose:
|
|
9
12
|
- "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
|
}
|
package/pynithy/run.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const fs = require('fs');
|
|
2
|
-
const { execSync } = require('child_process');
|
|
2
|
+
const { execSync, execFileSync } = require('child_process');
|
|
3
3
|
require('dotenv').config();
|
|
4
4
|
const readline = require('readline');
|
|
5
5
|
const forge = require('node-forge');
|
|
@@ -8,7 +8,7 @@ const https = require('https');
|
|
|
8
8
|
const casConfig = require('../cas/config.js');
|
|
9
9
|
const casResolver = require('../cas/resolver.js');
|
|
10
10
|
const sessionRegistry = require('../cas/session_registry.js');
|
|
11
|
-
const { LocalKubo, ownEndpoint } = require('../localKubo.js');
|
|
11
|
+
const { LocalKubo, ownEndpoint, PUBLIC_IPFS_HOST } = require('../localKubo.js');
|
|
12
12
|
|
|
13
13
|
if (!fs.existsSync('.env')) {
|
|
14
14
|
console.error("Error: .env file not found");
|
|
@@ -122,13 +122,17 @@ const main = async () => {
|
|
|
122
122
|
}
|
|
123
123
|
|
|
124
124
|
// Where the image goes. An IPFS API of the application's own
|
|
125
|
-
// (IPFS_ENDPOINT,
|
|
126
|
-
// Otherwise the publish runs its own Kubo: the public
|
|
127
|
-
// ipfs.ethernity.cloud
|
|
128
|
-
//
|
|
125
|
+
// (IPFS_ENDPOINT, with IPFS_TOKEN as its Authorization header when set)
|
|
126
|
+
// takes precedence. Otherwise the publish runs its own Kubo: the public
|
|
127
|
+
// API of ipfs.ethernity.cloud takes no adds or pins without a writer
|
|
128
|
+
// token, and the image is held by this Kubo, peered with the bootnode,
|
|
129
|
+
// until its certificate is on chain.
|
|
129
130
|
let localKubo = null;
|
|
130
131
|
let ipfsApi = process.env.IPFS_ENDPOINT || '';
|
|
131
|
-
|
|
132
|
+
let ipfsToken = '';
|
|
133
|
+
if (ownEndpoint(ipfsApi)) {
|
|
134
|
+
ipfsToken = process.env.IPFS_TOKEN || '';
|
|
135
|
+
} else {
|
|
132
136
|
localKubo = new LocalKubo(process.env.PROJECT_NAME);
|
|
133
137
|
try {
|
|
134
138
|
await localKubo.start();
|
|
@@ -140,23 +144,28 @@ const main = async () => {
|
|
|
140
144
|
console.log(`\t✔ IPFS node for this publish: ${ipfsApi}`);
|
|
141
145
|
process.on('exit', () => localKubo.release());
|
|
142
146
|
}
|
|
147
|
+
// node ../ipfs.mjs against ipfsApi, with the token as IPFS_TOKEN in its
|
|
148
|
+
// environment: the command line is in a failure's message and in the
|
|
149
|
+
// process list. A non-zero exit ends the publish.
|
|
150
|
+
const ipfsEnv = { ...process.env, IPFS_TOKEN: ipfsToken };
|
|
151
|
+
const ipfsCli = (args, stdio) => {
|
|
152
|
+
try {
|
|
153
|
+
return execFileSync(process.execPath, ['../ipfs.mjs', '--host', ipfsApi, ...args], { stdio, env: ipfsEnv });
|
|
154
|
+
} catch (e) {
|
|
155
|
+
console.error(`Error: node ../ipfs.mjs ${args.join(' ')} failed (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
|
|
156
|
+
process.exit(1);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
143
159
|
|
|
144
160
|
// The CIDs uploadToIpfs gives, from the same add calls with only-hash: the
|
|
145
161
|
// IPFS node stores and announces nothing. Set by hashForIpfs, after which
|
|
146
162
|
// uploadToIpfs refuses an upload that gives other CIDs.
|
|
147
163
|
let expectedCids = null;
|
|
148
164
|
const hashForIpfs = () => {
|
|
149
|
-
const hashOf = (target) =>
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
} catch (e) {
|
|
154
|
-
console.error(`Error: could not hash ${target} for IPFS: ${e.message}`);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
};
|
|
158
|
-
process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf('--filePath docker-compose-final.yml');
|
|
159
|
-
process.env.IPFS_HASH = hashOf(`--folderPath ${registryPath}`);
|
|
165
|
+
const hashOf = (target) => ipfsCli(['--action', 'hash', ...target], ['ignore', 'pipe', 'inherit'])
|
|
166
|
+
.toString().trim().split('\n').pop().trim();
|
|
167
|
+
process.env.IPFS_DOCKER_COMPOSE_HASH = hashOf(['--filePath', 'docker-compose-final.yml']);
|
|
168
|
+
process.env.IPFS_HASH = hashOf(['--folderPath', registryPath]);
|
|
160
169
|
console.log("IPFS_DOCKER_COMPOSE_HASH (before upload): ", process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
161
170
|
console.log("IPFS_HASH (before upload): ", process.env.IPFS_HASH);
|
|
162
171
|
expectedCids = { image: process.env.IPFS_HASH, compose: process.env.IPFS_DOCKER_COMPOSE_HASH };
|
|
@@ -169,7 +178,7 @@ const main = async () => {
|
|
|
169
178
|
if (fs.existsSync(file)) fs.unlinkSync(file);
|
|
170
179
|
}
|
|
171
180
|
console.log('Upload docker-compose-final.yml to IPFS');
|
|
172
|
-
|
|
181
|
+
ipfsCli(['--action', 'upload', '--filePath', 'docker-compose-final.yml'], 'inherit');
|
|
173
182
|
if (!fs.existsSync('IPFS_DOCKER_COMPOSE_HASH.ipfs')) {
|
|
174
183
|
console.error("Error: Could not upload docker-compose-final.yml to IPFS, please try again!");
|
|
175
184
|
process.exit(1);
|
|
@@ -178,7 +187,7 @@ const main = async () => {
|
|
|
178
187
|
console.log("IPFS_DOCKER_COMPOSE_HASH: ", process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
179
188
|
writeEnv('IPFS_DOCKER_COMPOSE_HASH', process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
180
189
|
console.log('Upload docker registry to IPFS');
|
|
181
|
-
|
|
190
|
+
ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
|
|
182
191
|
if (!fs.existsSync('IPFS_HASH.ipfs')) {
|
|
183
192
|
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
184
193
|
process.exit(1);
|
|
@@ -323,7 +332,7 @@ const main = async () => {
|
|
|
323
332
|
const registered = await sessionRegistry.register(
|
|
324
333
|
casChain.rpcUrl, casChain.chainId, casConfig.SESSION_REGISTRY[templateName],
|
|
325
334
|
process.env.PRIVATE_KEY, fs.readFileSync('etny-securelock-test.yaml'),
|
|
326
|
-
ipfsApi, '');
|
|
335
|
+
ipfsApi, '', ipfsToken);
|
|
327
336
|
if (localKubo) await localKubo.provide(registered.cid);
|
|
328
337
|
if (registered.registered) {
|
|
329
338
|
console.log(`\t✔ Session ${registered.name} registered on-chain (body ${registered.cid})`);
|
|
@@ -588,6 +597,29 @@ const main = async () => {
|
|
|
588
597
|
if (registryV2 && localKubo) {
|
|
589
598
|
await localKubo.provide(process.env.IPFS_HASH);
|
|
590
599
|
await localKubo.provide(process.env.IPFS_DOCKER_COMPOSE_HASH);
|
|
600
|
+
} else if (!registryV2 && localKubo) {
|
|
601
|
+
// On a V1 registry nothing is recorded before the certificate, so the
|
|
602
|
+
// bootnode's mirror pins nothing from this Kubo, and the certificate
|
|
603
|
+
// extraction service and the nodes, which fetch from the bootnode,
|
|
604
|
+
// would wait on an image it never receives: the compose and the image
|
|
605
|
+
// are pinned on the public API, which fetches them from this Kubo over
|
|
606
|
+
// the connection it holds. The pin takes a token (IPFS_TOKEN); without
|
|
607
|
+
// one the publish stops here, before anything is on chain.
|
|
608
|
+
const publicToken = process.env.IPFS_TOKEN || '';
|
|
609
|
+
if (!publicToken) {
|
|
610
|
+
console.error("Error: this network's image registry records an image only with its certificate, so the image must be pinned on the public IPFS API before the certificate is extracted, and the public API takes a pin only with a token.");
|
|
611
|
+
console.error(` Set IPFS_TOKEN to a writer token of ${PUBLIC_IPFS_HOST} and publish again.`);
|
|
612
|
+
process.exit(1);
|
|
613
|
+
}
|
|
614
|
+
for (const cid of [process.env.IPFS_DOCKER_COMPOSE_HASH, process.env.IPFS_HASH]) {
|
|
615
|
+
console.log(`Pinning ${cid} on the public IPFS API`);
|
|
616
|
+
try {
|
|
617
|
+
execFileSync(process.execPath, ['../ipfs.mjs', '--host', `https://${PUBLIC_IPFS_HOST}`, '--action', 'pin', '--hhash', cid], { stdio: 'inherit', env: { ...process.env, IPFS_TOKEN: publicToken } });
|
|
618
|
+
} catch (e) {
|
|
619
|
+
console.error(`Error: the public IPFS API did not pin ${cid} (${e.status != null ? `exit ${e.status}` : (e.signal || e.code)}).`);
|
|
620
|
+
process.exit(1);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
591
623
|
}
|
|
592
624
|
if (casTestnet) await registerSessionOnChain();
|
|
593
625
|
|
|
@@ -699,7 +731,7 @@ const main = async () => {
|
|
|
699
731
|
fs.unlinkSync('IPFS_HASH.ipfs');
|
|
700
732
|
}
|
|
701
733
|
console.log('Upload docker registry to IPFS');
|
|
702
|
-
|
|
734
|
+
ipfsCli(['--action', 'upload', '--folderPath', registryPath], 'inherit');
|
|
703
735
|
if (!fs.existsSync(`./IPFS_HASH.ipfs`)) {
|
|
704
736
|
console.error("Error: Could not upload registry to IPFS, please try again!");
|
|
705
737
|
process.exit(1);
|
|
@@ -5,7 +5,8 @@ import { ECStatus } from "@ethernity-cloud/runner/enums";
|
|
|
5
5
|
// Filled in from the project's .env by src/preStart.mjs on `npm start`: the
|
|
6
6
|
// securelock this dApp is published as, the trustedzone it was built against,
|
|
7
7
|
// the network's token address and chain id, the -unsafe network when the dApp
|
|
8
|
-
// runs without a CAS (null otherwise), and the IPFS
|
|
8
|
+
// runs without a CAS (null otherwise), and the IPFS API of the dApp's own ('' when
|
|
9
|
+
// the task's artefacts go through the bootnode's payload intake).
|
|
9
10
|
const PROJECT_NAME = "";
|
|
10
11
|
const TRUSTED_ZONE_IMAGE = "";
|
|
11
12
|
const NETWORK_ADDRESS = "";
|
|
@@ -23,7 +24,10 @@ function App() {
|
|
|
23
24
|
if (UNSAFE_NETWORK) {
|
|
24
25
|
await runner.setNetwork(...UNSAFE_NETWORK);
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
+
// An IPFS API of the dApp's own (IPFS_ENDPOINT) carries the task's
|
|
28
|
+
// artefacts; without one they go through the bootnode's payload intake.
|
|
29
|
+
if (IPFS_ENDPOINT) runner.initializeStorage(IPFS_ENDPOINT);
|
|
30
|
+
else runner.initializePublicIntake();
|
|
27
31
|
|
|
28
32
|
// Events are named by task status; detail is { message, status, progress }.
|
|
29
33
|
runner.addEventListener(ECStatus.DEFAULT, (e) => {
|
package/pynithy/src/preStart.mjs
CHANGED
|
@@ -3,7 +3,10 @@
|
|
|
3
3
|
// .env, the way ecld-run reads the same values. PROJECT_NAME and
|
|
4
4
|
// TRUSTED_ZONE_IMAGE take their -unsafe form on an -unsafe network; the
|
|
5
5
|
// network's token address, chain id and -unsafe (network, type) come from the
|
|
6
|
-
// runner's own enums through the SDK's network table
|
|
6
|
+
// runner's own enums through the SDK's network table; IPFS_ENDPOINT is the
|
|
7
|
+
// IPFS API of the dApp's own, or '' (no setting, or the public API, which
|
|
8
|
+
// takes no writes without a token), on which the dApp delivers its task
|
|
9
|
+
// artefacts through the bootnode's payload intake.
|
|
7
10
|
import fs from 'fs';
|
|
8
11
|
import { createRequire } from 'module';
|
|
9
12
|
import dotenv from 'dotenv';
|
|
@@ -13,6 +16,7 @@ dotenv.config();
|
|
|
13
16
|
const require = createRequire(import.meta.url);
|
|
14
17
|
const casConfig = require('@ethernity-cloud/sdk-js/cas/config.js');
|
|
15
18
|
const { networkFor } = require('@ethernity-cloud/sdk-js/network.js');
|
|
19
|
+
const { dappEndpoint } = require('@ethernity-cloud/sdk-js/localKubo.js');
|
|
16
20
|
|
|
17
21
|
const { PROJECT_NAME, TRUSTED_ZONE_IMAGE, BLOCKCHAIN_NETWORK, IPFS_ENDPOINT } = process.env;
|
|
18
22
|
if (!PROJECT_NAME || !BLOCKCHAIN_NETWORK) {
|
|
@@ -31,7 +35,7 @@ const constants = {
|
|
|
31
35
|
NETWORK_ADDRESS: network.address,
|
|
32
36
|
CHAIN_ID: network.chainId || null,
|
|
33
37
|
UNSAFE_NETWORK: network.unsafe || null,
|
|
34
|
-
IPFS_ENDPOINT: IPFS_ENDPOINT
|
|
38
|
+
IPFS_ENDPOINT: dappEndpoint(IPFS_ENDPOINT),
|
|
35
39
|
};
|
|
36
40
|
|
|
37
41
|
const filePath = 'src/ec_helloworld_example.js';
|