@ethernity-cloud/sdk-js 1.7.1 → 1.7.8

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.
@@ -325,6 +325,19 @@ class EtnySecureLock {
325
325
  const attributes = quote.slice(96, 112);
326
326
  const quoteMrenclave = quote.slice(112, 144).toString('hex');
327
327
  const reportData = quote.slice(368, 432);
328
+ // TWO report_data layouts, distinguished by which one names a
329
+ // REGISTERED validator (checked below):
330
+ // fresh (per-session): [0:32] freshness commitment, [32:52] address
331
+ // legacy (boot quote): [0:20] address, [20:52] cert hash
332
+ // The fresh layout is what a current CAS injects; legacy remains only
333
+ // for a CAS that could not read this enclave's nonce. Reading the wrong
334
+ // one would parse commitment bytes as an address and fail every task,
335
+ // so both are resolved and the registry decides which is real.
336
+ const casCandidates = [
337
+ [ethers.utils.getAddress('0x' + reportData.slice(32, 52).toString('hex')), null],
338
+ [ethers.utils.getAddress('0x' + reportData.slice(0, 20).toString('hex')),
339
+ reportData.slice(20, 52).toString('hex')],
340
+ ];
328
341
 
329
342
  const vrAbi = [
330
343
  'function isValidator(address) view returns (bool)',
@@ -430,8 +443,11 @@ class EtnySecureLock {
430
443
  console.log('CAS environment binding OK: quote commits to ' +
431
444
  `attestation session ${sessionId.slice(0, 16)}...`);
432
445
  }
433
- // SGX ATTRIBUTES.FLAGS bit 1 = DEBUG. Tolerated only when the session
434
- // explicitly says so (testnet debug CAS).
446
+ // SGX ATTRIBUTES.FLAGS bit 1 = DEBUG. A debug CAS protects nothing: its
447
+ // memory is readable from outside the enclave, so its keys and the
448
+ // session it serves are not secret. Both CAS modes (mainnet and
449
+ // testnet_cas) REFUSE one. Only plain 'testnet' tolerates it, and there
450
+ // the enclave never contacts a CAS at all so the check never runs.
435
451
  if ((attributes[0] & 0x02) && NETWORK_TYPE !== 'testnet') {
436
452
  return fault('CAS enclave runs in SGX DEBUG mode');
437
453
  }
@@ -451,26 +467,12 @@ class EtnySecureLock {
451
467
  }
452
468
 
453
469
  loadEnv() {
454
- if (process.env.ETNY_CHAIN_ID) {
455
- this.chain_id = parseInt(process.env.ETNY_CHAIN_ID);
456
- }
457
-
458
- if (process.env.ETNY_SMART_CONTRACT_ADDRESS) {
459
- this.smart_contract_address = process.env.ETNY_SMART_CONTRACT_ADDRESS.trim();
460
- }
461
-
462
- if (process.env.ETNY_WEB3_PROVIDER) {
463
- this.web3_provider = process.env.ETNY_WEB3_PROVIDER.trim();
464
- }
465
-
466
- if (process.env.IMAGE_REGISTRY_ADDRESS) {
467
- this.image_registry_address = process.env.IMAGE_REGISTRY_ADDRESS.trim();
468
- }
469
-
470
- if (process.env.ETNY_BUCKET) {
471
- this.etny_bucket = process.env.ETNY_BUCKET.trim();
472
- }
473
-
470
+ // The chain id, PoX smart contract, Image Registry, Web3 provider and
471
+ // the SwiftStream bucket are build constants (setInitializers), never
472
+ // read from process.env: the node supplies the environment, and an
473
+ // override would let it choose the registry the trustedzone's
474
+ // certificate comes from. Only the per-task values below are taken
475
+ // from the environment.
474
476
  if (process.env.ETNY_CLIENT_CHALLENGE) {
475
477
  this.client_challenge = process.env.ETNY_CLIENT_CHALLENGE.trim();
476
478
  }
@@ -496,14 +498,35 @@ class EtnySecureLock {
496
498
  // this.iamge_registry_signer = new ethers.Contract(this.image_registry_address, image_registry_abi.abi, this.walletSigner);
497
499
  }
498
500
 
501
+ // The node-supplied bucket .env may only contribute these per-task values.
502
+ // Everything else the securelock trusts is a build constant (chain id, PoX
503
+ // contract, Image Registry, Web3 provider, ValidatorRegistry, bucket) or a
504
+ // variable the CAS injects into the enclave process environment (ECAS_*,
505
+ // SCONE_*). Any key outside this set, and any key already present in
506
+ // process.env, is ignored.
507
+ static NODE_ENV_ALLOWLIST = new Set([
508
+ 'ETNY_MODE',
509
+ 'ETNY_ORDER_ID',
510
+ 'ETNY_CLIENT_CHALLENGE',
511
+ ]);
512
+
499
513
  async readEnv() {
500
514
  try {
501
515
  console.log('Getting .env if exists...');
502
516
  const envFile = await this.swiftStreamService.getFileContent(this.etny_bucket, ".env");
503
517
  console.log("Env:", envFile);
504
- console.log("Saving .env file...");
505
- await fs.promises.writeFile('.env', envFile);
506
- require('dotenv').config();
518
+ // Parsed in memory: dotenv.parse does not touch process.env.
519
+ const parsed = require('dotenv').parse(Buffer.from(envFile));
520
+ for (const [key, value] of Object.entries(parsed)) {
521
+ if (!EtnySecureLock.NODE_ENV_ALLOWLIST.has(key)) {
522
+ console.log(`Ignoring non-allowlisted .env key from the node bucket: ${key}`);
523
+ continue;
524
+ }
525
+ if (Object.prototype.hasOwnProperty.call(process.env, key)) {
526
+ continue;
527
+ }
528
+ process.env[key] = value;
529
+ }
507
530
 
508
531
  this.version = 'v3';
509
532
  this.etny_mode = process.env.ETNY_MODE;
@@ -516,30 +539,49 @@ class EtnySecureLock {
516
539
  }
517
540
  }
518
541
 
519
- // v4 checksum validation: the DO-request is submitted on-chain by the owner,
520
- // so the metadata checksum is already authenticated by the transaction sender;
521
- // we only confirm the file content hashes to that recorded checksum. Empty
522
- // input carries no checksum (has_checksum === false) and is skipped. Kept
523
- // identical across securelock / trustedzone / validator so all three agree.
524
- async validateClientPayload(payload_data, input_data) {
525
- // The order metadata comes from the chain; a transient RPC failure
526
- // must end in a signed failure result the trustedzone can hand over,
527
- // not in a task that produces nothing until the trustedzone times out.
542
+ // The order metadata, from the chain, retried over transient RPC failures.
543
+ // It names the trustedzone whose certificate checks the handover and
544
+ // encrypts the result, so without it no result reaches the trustedzone.
545
+ // Returns whether it was loaded; a persistent failure sets SYSTEM_ERROR.
546
+ async loadOrderMetadata() {
528
547
  const maxRetries = 5;
529
548
  for (let attempt = 1; ; attempt++) {
530
549
  try {
531
550
  await this.getDORequestMetadata();
532
- break;
551
+ return true;
533
552
  } catch (e) {
534
553
  if (attempt === maxRetries) {
535
554
  this.task_code = TaskStatus.SYSTEM_ERROR;
536
555
  this.task_result = `Failed to fetch metadata after ${maxRetries} attempts: ${e && e.message ? e.message : e}`;
537
556
  this.is_valid_client_data = false;
538
- return;
557
+ return false;
539
558
  }
540
559
  }
541
560
  }
561
+ }
542
562
 
563
+ // v4 checksum validation: the DO-request is submitted on-chain by the owner,
564
+ // so the metadata checksum is already authenticated by the transaction sender;
565
+ // we only confirm the file content hashes to that recorded checksum. An
566
+ // empty input (no CID) is checked against its recorded checksum when there
567
+ // is one (the runners record the checksum of the empty string) and is the
568
+ // empty string when there is none; an input that names a CID must carry a
569
+ // checksum. Data that could not be read or decrypted (false) is reported,
570
+ // not hashed. Kept identical across securelock / trustedzone / validator so
571
+ // all three agree.
572
+ async validateClientPayload(payload_data, input_data) {
573
+ if (typeof payload_data !== 'string') {
574
+ this.task_code = TaskStatus.PAYLOAD_CHECKSUM_ERROR;
575
+ this.task_result = 'PAYLOAD NOT AVAILABLE';
576
+ this.is_valid_client_data = false;
577
+ return;
578
+ }
579
+ if (typeof input_data !== 'string') {
580
+ this.task_code = TaskStatus.INPUT_CHECKSUM_ERROR;
581
+ this.task_result = 'INPUT NOT AVAILABLE';
582
+ this.is_valid_client_data = false;
583
+ return;
584
+ }
543
585
  const pmeta = this._metadata.payload_metadata_obj;
544
586
  if (pmeta.has_checksum) {
545
587
  const expected = pmeta.checksum.toLowerCase().replace(/^0x/, '');
@@ -565,6 +607,14 @@ class EtnySecureLock {
565
607
  this.task_result = 'INPUT CHECKSUM DOESN\'T MATCH';
566
608
  this.is_valid_client_data = false;
567
609
  }
610
+ } else if (imeta.ipfs_hash) {
611
+ this.task_code = TaskStatus.INPUT_CHECKSUM_ERROR;
612
+ this.task_result = 'INPUT CHECKSUM DOESN\'T EXIST';
613
+ this.is_valid_client_data = false;
614
+ } else if (input_data !== '') {
615
+ this.task_code = TaskStatus.INPUT_CHECKSUM_ERROR;
616
+ this.task_result = 'INPUT CHECKSUM DOESN\'T MATCH';
617
+ this.is_valid_client_data = false;
568
618
  }
569
619
  }
570
620
 
@@ -606,7 +656,8 @@ class EtnySecureLock {
606
656
 
607
657
  const encryptedTuple = etny_crypto.encryptedDataFromBase64Json(encryptedBase64);
608
658
  const decryptedResult = etny_crypto.decrypt(this.privateKeyMaterial(), encryptedTuple);
609
- return decryptedResult.toString();
659
+ // decrypt() returns false for an object that does not decrypt.
660
+ return decryptedResult === false ? false : decryptedResult.toString();
610
661
  } catch (e) {
611
662
  console.log(`Failed to get ${objectName} file`);
612
663
  return false;
@@ -615,13 +666,15 @@ class EtnySecureLock {
615
666
 
616
667
  async getTrustedZonePublicKey() {
617
668
  try {
669
+ // The certificate of the trustedzone the order runs with: the latest
670
+ // v3 image registered under the trustedzone name in metadata1 field 2,
671
+ // as the Python securelock reads it. Field 1 is this securelock's own
672
+ // hash, which is not a trustedzone entry.
618
673
  console.log('Getting the public key of the trusted zone enclave');
619
- let imageHash = this._metadata.image_hash;
620
- console.log('imageHash:', imageHash);
621
- if (EtnySecureLock.debug) {
622
- imageHash = 'v3-hash-1';
623
- }
624
- this.trustedZonePublicKey = await this.image_registry.getTrustedZoneImageCertPublicKey(imageHash);
674
+ const trustedZoneName = this._metadata.image_metadata.split(':')[2];
675
+ console.log('trustedZoneName:', trustedZoneName);
676
+ const latest = await this.image_registry.getLatestTrustedZoneImageCertPublicKey(trustedZoneName, 'v3');
677
+ this.trustedZonePublicKey = latest[1];
625
678
  console.log('Public key of the TrustedZone enclave from smart contract:', this.trustedZonePublicKey);
626
679
  } catch (e) {
627
680
  console.log('getTrustedZonePublicKey', e);
@@ -781,6 +834,12 @@ class EtnySecureLock {
781
834
 
782
835
  async saveResult() {
783
836
  try {
837
+ // A failure reported before execute() loaded the order metadata
838
+ // (one thrown in create(), see reportFailure) loads it here.
839
+ if (!this._metadata && !(await this.loadOrderMetadata())) {
840
+ console.log('Save result: no order metadata, so no trustedzone certificate to encrypt the result to');
841
+ return;
842
+ }
784
843
  await this.getTrustedZonePublicKey();
785
844
  // THE HANDOVER IS ALWAYS SIGNED: the trustedzone refuses an
786
845
  // unverifiable result/result-code, so the operator's storage
@@ -843,6 +902,40 @@ class EtnySecureLock {
843
902
  }
844
903
 
845
904
  async execute() {
905
+ // The order metadata names the trustedzone whose certificate checks
906
+ // the handover and encrypts the result, and the task binding is signed
907
+ // into the result; both are read before any check that can end the
908
+ // task, so every outcome below reaches the trustedzone as a result it
909
+ // can verify.
910
+ if (!(await this.loadOrderMetadata())) {
911
+ return false;
912
+ }
913
+ await this.getTrustedZonePublicKey();
914
+
915
+ // Task binding: the order id and enclave challenge the trustedzone
916
+ // generated for this task, forwarded as task.securelock + .sig. When
917
+ // present (and only with a valid trustedzone signature) the result and
918
+ // the code are signed over "<challenge>:<order_id>:" and the envelope
919
+ // carries both, so the trustedzone can refuse a result captured from
920
+ // any other run. Absent with an older trustedzone, which then verifies
921
+ // the bare result as before.
922
+ this.task_binding = null;
923
+ try {
924
+ const taskValue = await this.getFileContentAndDecrypt(this.etny_bucket, 'task.securelock');
925
+ const taskSig = await this.getFileContentAndDecrypt(this.etny_bucket, 'task.securelock.sig');
926
+ if (etny_crypto.verifySignature(this.trustedZonePublicKey, taskValue, taskSig)) {
927
+ const task = JSON.parse(String(taskValue));
928
+ if (Number.isInteger(Number(task.order_id)) && typeof task.challenge === 'string' && task.challenge) {
929
+ this.task_binding = { order_id: Number(task.order_id), challenge: task.challenge };
930
+ console.log(`Task binding attested by trustedzone: order ${this.task_binding.order_id}`);
931
+ }
932
+ } else {
933
+ console.error('task.securelock signature validation FAILED; result will not be bound');
934
+ }
935
+ } catch (e) {
936
+ console.log('No task binding supplied; result will not be bound');
937
+ }
938
+
846
939
  // ADJUDICATE the CAS attestation verdict recorded at startup. An
847
940
  // INVALID CAS quote terminates the task before the payload is
848
941
  // handled; the failure result (CAS_ATTESTATION_FAULT, 40) reaches
@@ -862,7 +955,6 @@ class EtnySecureLock {
862
955
  // handover -- the operator's storage layer between the enclaves must
863
956
  // not be able to substitute either. Mirrors the pynithy
864
957
  // validate_trustedzone_data behavior.
865
- await this.getTrustedZonePublicKey();
866
958
  const payloadSig = await this.getFileContentAndDecrypt(this.etny_bucket, this.payload + '.sig');
867
959
  const inputSig = await this.getFileContentAndDecrypt(this.etny_bucket, this.input + '.sig');
868
960
  if (payloadSig === false ||
@@ -901,7 +993,6 @@ class EtnySecureLock {
901
993
  const callerValue = await this.getFileContentAndDecrypt(this.etny_bucket, 'caller.securelock');
902
994
  const callerSig = await this.getFileContentAndDecrypt(this.etny_bucket, 'caller.securelock.sig');
903
995
  if (typeof etny_crypto.verifySignature === 'function') {
904
- await this.getTrustedZonePublicKey();
905
996
  if (etny_crypto.verifySignature(this.trustedZonePublicKey, callerValue, callerSig)) {
906
997
  this.task_caller = String(callerValue).trim();
907
998
  console.log(`Task caller attested by trustedzone: ${this.task_caller}`);
@@ -915,31 +1006,6 @@ class EtnySecureLock {
915
1006
  console.log('No attested task caller supplied; treating task as anonymous');
916
1007
  }
917
1008
 
918
- // Task binding: the order id and enclave challenge the trustedzone
919
- // generated for this task, forwarded as task.securelock + .sig. When
920
- // present (and only with a valid trustedzone signature) the result and
921
- // the code are signed over "<challenge>:<order_id>:" and the envelope
922
- // carries both, so the trustedzone can refuse a result captured from
923
- // any other run. Absent with an older trustedzone, which then verifies
924
- // the bare result as before.
925
- this.task_binding = null;
926
- try {
927
- const taskValue = await this.getFileContentAndDecrypt(this.etny_bucket, 'task.securelock');
928
- const taskSig = await this.getFileContentAndDecrypt(this.etny_bucket, 'task.securelock.sig');
929
- await this.getTrustedZonePublicKey();
930
- if (etny_crypto.verifySignature(this.trustedZonePublicKey, taskValue, taskSig)) {
931
- const task = JSON.parse(String(taskValue));
932
- if (Number.isInteger(Number(task.order_id)) && typeof task.challenge === 'string' && task.challenge) {
933
- this.task_binding = { order_id: Number(task.order_id), challenge: task.challenge };
934
- console.log(`Task binding attested by trustedzone: order ${this.task_binding.order_id}`);
935
- }
936
- } else {
937
- console.error('task.securelock signature validation FAILED; result will not be bound');
938
- }
939
- } catch (e) {
940
- console.log('No task binding supplied; result will not be bound');
941
- }
942
-
943
1009
  this.configureStateRegistry();
944
1010
 
945
1011
  // Interactive session: the trustedzone stages session.config when the
@@ -31,9 +31,9 @@ const writeEnv = (key, value) => {
31
31
  // Kept in sync with etny-{pynithy,nodenithy}/v3/networks.yaml (the authoritative
32
32
  // on-chain constants). Covers all supported networks for both dApp types.
33
33
  export const ECRunner = {
34
- // --- bloxberg (the testnet reads ECImageRegistryV2, mainnet the original) ---
35
- 'etny-pynithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
36
- 'etny-nodenithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
34
+ // --- bloxberg (the testnet reads ECImageRegistryV3, mainnet the original) ---
35
+ 'etny-pynithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
36
+ 'etny-nodenithy-testnet': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
37
37
  'etny-pynithy': ['0x549A6E06BB2084100148D50F51CF77a3436C3Ae7', '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31', 'https://bloxberg.ethernity.cloud', 8995],
38
38
  'etny-nodenithy': ['0x549A6E06BB2084100148D50F51CF77a3436C3Ae7', '0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31', 'https://bloxberg.ethernity.cloud', 8995],
39
39
  // --- polygon mainnet ---
@@ -52,8 +52,8 @@ export const ECRunner = {
52
52
  'ecld-pynithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
53
53
  'ecld-nodenithy-litvm-testnet': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
54
54
  // --- the -unsafe networks: the contracts of the network each is named after ---
55
- 'etny-pynithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
56
- 'etny-nodenithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8', 'https://bloxberg.ethernity.cloud', 8995],
55
+ 'etny-pynithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
56
+ 'etny-nodenithy-testnet-unsafe': ['0x02882F03097fE8cD31afbdFbB5D72a498B41112c', '0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1', 'https://bloxberg.ethernity.cloud', 8995],
57
57
  'ecld-pynithy-litvm-testnet-unsafe': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441],
58
58
  'ecld-nodenithy-litvm-testnet-unsafe': ['0x29D3eC870565B6A1510232bd950A8Bc8336f0EB2', '0x55e0ad455Be85162b71a790f00Fc305680E3CE53', 'https://liteforge.rpc.caldera.xyz/infra-partner-http', 4441]
59
59
  };
@@ -90,11 +90,12 @@ const runCommand = (command, canPass = false) => {
90
90
 
91
91
  // Removes the containers this build and publish create, matched by exact name:
92
92
  // `--filter name=` matches a substring, so name=las would also match the LAS of
93
- // a CAS validator set on the same host.
93
+ // a CAS validator set on the same host. `-v` removes their anonymous volumes
94
+ // too: registry:2 keeps its pushed images in one, about 1.3 GB per build.
94
95
  const removeContainer = (name) => {
95
96
  const ids = shell.exec(`docker ps -a -q --filter "name=^/${name}$"`, { silent: true }).stdout.trim();
96
97
  if (ids) {
97
- runCommand(`docker rm -f ${ids.split('\n').join(' ')}`);
98
+ runCommand(`docker rm -f -v ${ids.split('\n').join(' ')}`);
98
99
  }
99
100
  };
100
101
 
@@ -223,12 +223,13 @@ const retryOnFailure = async (fn, label) => {
223
223
  return null;
224
224
  };
225
225
 
226
- // Upload a single file: POST it to /api/v0/add and return its hash.
227
- const uploadFileToIPFSOnce = async (filePath) => {
226
+ // Upload a single file: POST it to /api/v0/add and return its hash. With
227
+ // onlyHash the node computes the same hash and stores nothing.
228
+ const uploadFileToIPFSOnce = async (filePath, onlyHash = false) => {
228
229
  const files = [filePath];
229
230
  const dir = path.dirname(filePath);
230
231
  const { body, boundary, length } = buildMultipartBody(files, dir);
231
- const { statusCode, text } = await postAdd('', body, boundary, length, null);
232
+ const { statusCode, text } = await postAdd(onlyHash ? '?only-hash=true' : '', body, boundary, length, null);
232
233
  if (statusCode !== 200) {
233
234
  console.log(`Failed to upload to IPFS. Status code: ${statusCode}`);
234
235
  return null;
@@ -260,13 +261,15 @@ const uploadFileToIPFS = async (filePath) => {
260
261
  };
261
262
 
262
263
  // Upload a whole directory with wrap-with-directory=true and return the root
263
- // directory hash -- the exact behaviour of Python's upload_dir.
264
- const uploadDirOnce = async (dirPath) => {
264
+ // directory hash -- the exact behaviour of Python's upload_dir. With onlyHash
265
+ // the node computes the same root hash and stores nothing.
266
+ const uploadDirOnce = async (dirPath, onlyHash = false) => {
265
267
  const abs = path.resolve(dirPath);
266
268
  const files = gatherFiles(abs);
267
269
  const totalSize = files.reduce((acc, f) => acc + fs.statSync(f).size, 0);
268
270
  const totalMb = Math.floor(totalSize / (1024 * 1024));
269
271
  const { body, boundary, length } = buildMultipartBody(files, abs);
272
+ const step = onlyHash ? 'Hashing enclave for IPFS' : 'Uploading and pinning enclave to IPFS';
270
273
 
271
274
  let lastShownMb = -1;
272
275
  let frame = 0;
@@ -276,27 +279,28 @@ const uploadDirOnce = async (dirPath) => {
276
279
  frame = (frame + 1) % SPINNER_FRAMES.length;
277
280
  lastShownMb = mb;
278
281
  process.stdout.write(
279
- `\r\t${SPINNER_FRAMES[frame]} Uploading and pinning enclave to IPFS... ${mb}MB/${totalMb}MB`
282
+ `\r\t${SPINNER_FRAMES[frame]} ${step}... ${mb}MB/${totalMb}MB`
280
283
  );
281
284
  }
282
285
  };
283
286
 
284
287
  const query =
285
- '?quieter=true&stream-channels=true&wrap-with-directory=true&progress=false&timeout=5m';
288
+ '?quieter=true&stream-channels=true&wrap-with-directory=true&progress=false&timeout=5m' +
289
+ (onlyHash ? '&only-hash=true' : '');
286
290
  const { statusCode, text } = await postAdd(query, body, boundary, length, onProgress);
287
291
 
288
292
  if (statusCode !== 200) {
289
- process.stdout.write(`\r\t${FAIL}Uploading and pinning enclave to IPFS\n`);
293
+ process.stdout.write(`\r\t${FAIL}${step}\n`);
290
294
  console.log(`Failed to upload to IPFS. Status code: ${statusCode}`);
291
295
  return false;
292
296
  }
293
297
  const rootHash = parseRootHash(text);
294
298
  if (!rootHash) {
295
- process.stdout.write(`\r\t${FAIL}Uploading and pinning enclave to IPFS\n`);
299
+ process.stdout.write(`\r\t${FAIL}${step}\n`);
296
300
  console.log('Failed to upload to IPFS. Could not determine root hash.');
297
301
  return false;
298
302
  }
299
- process.stdout.write(`\r\t${CHECK}Uploading and pinning enclave to IPFS\n`);
303
+ process.stdout.write(`\r\t${CHECK}${step}\n`);
300
304
  return rootHash;
301
305
  };
302
306
 
@@ -322,6 +326,16 @@ const upload = async (p) => {
322
326
  return null;
323
327
  };
324
328
 
329
+ // The hash `upload(p)` gives, from the same add call with only-hash: the IPFS
330
+ // node neither stores nor announces anything.
331
+ const hashForIPFS = async (p) => {
332
+ const stat = fs.statSync(p);
333
+ if (stat.isFile()) return retryOnFailure(() => uploadFileToIPFSOnce(p, true), 'hash_file');
334
+ if (stat.isDirectory()) return retryOnFailure(() => uploadDirOnce(p, true), 'hash_dir');
335
+ console.log(`Path ${p} is neither a file nor a directory.`);
336
+ return null;
337
+ };
338
+
325
339
  // ---------------------------------------------------------------------------
326
340
  // Download helpers (kubo-rpc-client based; unchanged behaviour)
327
341
  // ---------------------------------------------------------------------------
@@ -397,6 +411,7 @@ const getContentFromIPFS = async (hash, maxRetries = process.env.REACT_APP_IPFS_
397
411
  export {
398
412
  initialize,
399
413
  upload,
414
+ hashForIPFS,
400
415
  uploadFileToIPFS,
401
416
  uploadFolderToIPFS,
402
417
  getFromIPFS,
@@ -405,7 +420,7 @@ export {
405
420
  };
406
421
 
407
422
  // ---------------------------------------------------------------------------
408
- // CLI (unchanged interface: --host --action upload/download --filePath/--folderPath)
423
+ // CLI: --host --action upload/hash/download --filePath/--folderPath
409
424
  // ---------------------------------------------------------------------------
410
425
 
411
426
  program
@@ -413,7 +428,7 @@ program
413
428
  .option('--hhash <hhash>', 'IPFS hash')
414
429
  .option('--filePath <path>', 'Path to the file')
415
430
  .option('--folderPath <path>', 'Path to the folder')
416
- .option('--action <action>', 'Action to perform (upload, download)')
431
+ .option('--action <action>', 'Action to perform (upload, hash, download)')
417
432
  .option('--token <token>', 'IPFS authorization token')
418
433
  .option('--output <path>', 'Output path for download');
419
434
 
@@ -445,6 +460,15 @@ if (isMain) {
445
460
  console.error('Please provide a filePath or folderPath for upload.');
446
461
  process.exit(1);
447
462
  }
463
+ } else if (options.action === 'hash') {
464
+ const target = options.filePath || options.folderPath;
465
+ if (!target) {
466
+ console.error('Please provide a filePath or folderPath to hash.');
467
+ process.exit(1);
468
+ }
469
+ const hhash = await hashForIPFS(target);
470
+ console.log(`${hhash}`);
471
+ process.exit(hhash ? 0 : 1);
448
472
  } else if (options.action === 'download') {
449
473
  if (options.filePath) {
450
474
  await getFromIPFS(options.hhash, options.filePath);
@@ -456,7 +480,7 @@ if (isMain) {
456
480
  console.error('Please provide a filePath or folderPath for download.');
457
481
  }
458
482
  } else {
459
- console.error('Please provide a valid action (upload, download).');
483
+ console.error('Please provide a valid action (upload, hash, download).');
460
484
  process.exit(1);
461
485
  }
462
486
  };
@@ -1 +1 @@
1
- [{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"wallet","type":"address"}],"name":"AllowedWalletAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"wallet","type":"address"}],"name":"AllowedWalletRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"},{"indexed":false,"internalType":"string","name":"publicKey","type":"string"},{"indexed":false,"internalType":"string","name":"version","type":"string"}],"name":"ImageAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"ImagePublished","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"ImageRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"ImageValidated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"TrustedZoneImageAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"TrustedZoneImageRemoved","type":"event"},{"inputs":[{"internalType":"address","name":"wallet","type":"address"}],"name":"addAllowedWallet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"}],"name":"addImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"}],"name":"addTrustedZoneImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"address","name":"newRewardAddress","type":"address"}],"name":"changeImageRewardAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"changeOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"address","name":"newRewardAddress","type":"address"}],"name":"changeTrustedZoneImageRewardAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"publishImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"wallet","type":"address"}],"name":"removeAllowedWallet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"validateImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"allowedWallets","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getImageCertPublicKey","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getImageSession","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"version","type":"string"}],"name":"getLatestImageVersionPublicKey","outputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"version","type":"string"}],"name":"getLatestTrustedZoneImageCertPublicKey","outputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getRewardAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getTrustedZoneImageCertPublicKey","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getTrustedZoneImageSession","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getTrustedZoneRewardAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"imageDetails","outputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"},{"internalType":"address","name":"rewardAddress","type":"address"},{"internalType":"bool","name":"validated","type":"bool"},{"internalType":"bool","name":"published","type":"bool"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"name","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"images","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"imageVersions","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"imageVersionsHash","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"trustedZoneImageDetails","outputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"},{"internalType":"address","name":"rewardAddress","type":"address"},{"internalType":"bool","name":"validated","type":"bool"},{"internalType":"bool","name":"published","type":"bool"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"name","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"trustedZoneImageVersions","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"trustedZoneVersionsHash","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"}]
1
+ [{"inputs":[{"internalType":"string","name":"imageName","type":"string"}],"name":"imageNameOwner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"wallet","type":"address"}],"name":"AllowedWalletAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"wallet","type":"address"}],"name":"AllowedWalletRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"},{"indexed":false,"internalType":"string","name":"publicKey","type":"string"},{"indexed":false,"internalType":"string","name":"version","type":"string"}],"name":"ImageAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"ImagePublished","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"ImageRemoved","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"ImageValidated","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"internalType":"address","name":"previousOwner","type":"address"},{"indexed":true,"internalType":"address","name":"newOwner","type":"address"}],"name":"OwnerChanged","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"TrustedZoneImageAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"}],"name":"TrustedZoneImageRemoved","type":"event"},{"inputs":[{"internalType":"address","name":"wallet","type":"address"}],"name":"addAllowedWallet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"}],"name":"addImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"}],"name":"addTrustedZoneImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"address","name":"newRewardAddress","type":"address"}],"name":"changeImageRewardAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"newOwner","type":"address"}],"name":"changeOwner","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"address","name":"newRewardAddress","type":"address"}],"name":"changeTrustedZoneImageRewardAddress","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"publishImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"address","name":"wallet","type":"address"}],"name":"removeAllowedWallet","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"validateImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[],"stateMutability":"nonpayable","type":"constructor"},{"inputs":[{"internalType":"address","name":"","type":"address"}],"name":"allowedWallets","outputs":[{"internalType":"bool","name":"","type":"bool"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getImageCertPublicKey","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getImageSession","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"version","type":"string"}],"name":"getLatestImageVersionPublicKey","outputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"version","type":"string"}],"name":"getLatestTrustedZoneImageCertPublicKey","outputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getRewardAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getTrustedZoneImageCertPublicKey","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getTrustedZoneImageSession","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"getTrustedZoneRewardAddress","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"imageDetails","outputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"},{"internalType":"address","name":"rewardAddress","type":"address"},{"internalType":"bool","name":"validated","type":"bool"},{"internalType":"bool","name":"published","type":"bool"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"name","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"uint256","name":"","type":"uint256"}],"name":"images","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"imageVersions","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"imageVersionsHash","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[],"name":"owner","outputs":[{"internalType":"address","name":"","type":"address"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"}],"name":"trustedZoneImageDetails","outputs":[{"internalType":"address","name":"owner","type":"address"},{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"},{"internalType":"address","name":"rewardAddress","type":"address"},{"internalType":"bool","name":"validated","type":"bool"},{"internalType":"bool","name":"published","type":"bool"},{"internalType":"string","name":"certPublicKey","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"name","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"trustedZoneImageVersions","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"","type":"string"},{"internalType":"string","name":"","type":"string"},{"internalType":"uint256","name":"","type":"uint256"}],"name":"trustedZoneVersionsHash","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"},{"indexed":false,"internalType":"string","name":"imageName","type":"string"},{"indexed":false,"internalType":"string","name":"version","type":"string"}],"name":"ImageCertSet","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"internalType":"string","name":"ipfsHash","type":"string"},{"indexed":false,"internalType":"string","name":"imageName","type":"string"},{"indexed":false,"internalType":"string","name":"version","type":"string"},{"indexed":true,"internalType":"address","name":"owner","type":"address"},{"indexed":false,"internalType":"string","name":"ipfsPeer","type":"string"}],"name":"ImageRegistered","type":"event"},{"inputs":[],"name":"pendingImages","outputs":[{"internalType":"string[]","name":"","type":"string[]"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"version","type":"string"},{"internalType":"string","name":"imageName","type":"string"},{"internalType":"string","name":"dockerComposeHash","type":"string"},{"internalType":"string","name":"session","type":"string"},{"internalType":"uint8","name":"fee","type":"uint8"},{"internalType":"string","name":"ipfsPeer","type":"string"}],"name":"registerImage","outputs":[],"stateMutability":"nonpayable","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"}],"name":"registrationPeer","outputs":[{"internalType":"string","name":"","type":"string"}],"stateMutability":"view","type":"function"},{"inputs":[{"internalType":"string","name":"ipfsHash","type":"string"},{"internalType":"string","name":"certPublicKey","type":"string"}],"name":"setImageCert","outputs":[],"stateMutability":"nonpayable","type":"function"}]