@ethernity-cloud/sdk-js 1.7.1 → 1.7.7

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.
@@ -1,25 +1,27 @@
1
1
  const Web3 = require('web3');
2
2
  const { ethers } = require('ethers');
3
- const { Account } = require('eth-lib/lib/account');
4
3
  const { config } = require('dotenv');
5
4
  const fs = require('fs');
6
5
  const path = require('path');
6
+ const { isRevert, revertReason } = require('../../revert.js');
7
7
 
8
8
  config();
9
9
 
10
+ // The publishing wallet's key comes from the environment only: a command line
11
+ // is visible to other processes and is printed when the command fails.
10
12
  const PRIVATE_KEY = process.env.PRIVATE_KEY || "";
11
13
  let BLOCKCHAIN_NETWORK = process.env.BLOCKCHAIN_NETWORK || "Bloxberg_Testnet";
12
14
  let NETWORK_RPC = "https://bloxberg.ethernity.cloud";
13
- let IMAGE_REGISTRY_ADDRESS = "0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8"; // bloxberg testnet (ECImageRegistryV2)
15
+ let IMAGE_REGISTRY_ADDRESS = "0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1"; // bloxberg testnet (ECImageRegistryV3)
14
16
  let CHAIN_ID = 8995;
15
17
  let GAS = 9000000;
16
18
  let GAS_PRICE = 1;
17
19
 
18
20
  function setVars(network = "") {
19
21
  if (BLOCKCHAIN_NETWORK.includes("Bloxberg")) {
20
- // The bloxberg testnet reads ECImageRegistryV2; mainnet the original registry.
22
+ // The bloxberg testnet reads ECImageRegistryV3; mainnet the original registry.
21
23
  IMAGE_REGISTRY_ADDRESS = BLOCKCHAIN_NETWORK.includes("Testnet")
22
- ? "0x99A84C624C028bdf0a855A1E9E3f2fcf7275B3D8"
24
+ ? "0xa372a6e1Eb7Fcf343AF6b91E809b900C55001CD1"
23
25
  : "0x15D73a742529C3fb11f3FA32EF7f0CC3870ACA31";
24
26
  } else if (BLOCKCHAIN_NETWORK.includes("Polygon")) {
25
27
  if (BLOCKCHAIN_NETWORK.includes("Mainnet")) {
@@ -58,6 +60,26 @@ function isStringPrivateKey(privateKey) {
58
60
  }
59
61
  }
60
62
 
63
+ // The address of a private key given with or without its 0x prefix.
64
+ function walletAddress(privateKey) {
65
+ return new ethers.Wallet(privateKey.startsWith("0x") ? privateKey : `0x${privateKey}`).address;
66
+ }
67
+
68
+ // REWARD_ADDRESS from the environment: where the image's developer fee is
69
+ // paid when not the publishing wallet, or "" when unset. ECImageRegistryV3
70
+ // refuses the zero address, so it is refused here before any transaction.
71
+ function rewardAddressFromEnv() {
72
+ const rewardAddress = (process.env.REWARD_ADDRESS || "").trim();
73
+ if (!rewardAddress) return "";
74
+ if (!ethers.utils.isAddress(rewardAddress)) {
75
+ throw new Error(`REWARD_ADDRESS ${rewardAddress} is not an address`);
76
+ }
77
+ if (rewardAddress.toLowerCase() === ethers.constants.AddressZero) {
78
+ throw new Error("REWARD_ADDRESS is the zero address; leave it empty to be paid at the publishing wallet");
79
+ }
80
+ return rewardAddress;
81
+ }
82
+
61
83
  async function checkAccountBalance() {
62
84
  try {
63
85
  const web3 = new ethers.providers.JsonRpcProvider(NETWORK_RPC);
@@ -124,61 +146,12 @@ class ImageRegistry {
124
146
  const receipt = await this.provider.sendTransaction(signedTxn.rawTransaction);
125
147
  }
126
148
 
149
+ // Record the securelock with its certificate on a V1 registry, in one
150
+ // addImage call. A failure, including the registry's refusal, throws.
127
151
  async addSecureLockImageCert(certContent, ipfsHash, imageName, version, dockerComposeHash, enclaveNameSecureLock, fee) {
128
- try {
129
- console.log("Adding secure lock image cert to image registry");
130
- // Fetch current nonce
131
- if (BLOCKCHAIN_NETWORK.includes("Polygon")) {
132
- console.log("Polygon Mainnet");
133
- let nonce = await this.provider.getTransactionCount(this.acct.address, 'pending');
134
- // Fetch current gas price and increase it
135
- let gasPrice = await this.provider.getGasPrice();
136
- gasPrice = gasPrice.mul(ethers.BigNumber.from(110)).div(ethers.BigNumber.from(100));
137
- const unicornTxn = await this.imageRegistryContract.addImage(
138
- ipfsHash, certContent, version, imageName, dockerComposeHash, enclaveNameSecureLock, fee,
139
- {
140
- nonce: nonce,
141
- gasPrice: gasPrice,
142
- });
143
- // console.log("Transaction: ", unicornTxn);
144
- const receipt = await this.imageRegistryContract.provider.waitForTransaction(unicornTxn.hash);
145
-
146
- // console.log("transaction status: ", receipt.status);
147
- console.log("transaction receipt: ", unicornTxn.hash);
148
- if (receipt.status === 1) {
149
- console.log("Adding secure lock image cert transaction was successful!");
150
- } else {
151
- // console.log("receipt.status", receipt.status)
152
- console.log("Image certificates already exist for this image!");
153
- }
154
- } else {
155
- const unicornTxn = await this.imageRegistryContract.addImage(
156
- ipfsHash, certContent, version, imageName, dockerComposeHash, enclaveNameSecureLock, fee);
157
- // console.log("Transaction: ", unicornTxn);
158
- const receipt = await this.imageRegistryContract.provider.waitForTransaction(unicornTxn.hash);
159
-
160
- // console.log("transaction status: ", receipt.status);
161
- console.log("transaction receipt: ", unicornTxn.hash);
162
- if (receipt.status === 1) {
163
- console.log("Adding secure lock image cert transaction was successful!");
164
- } else {
165
- // console.log("receipt.status", receipt.status)
166
- console.log("Image certificates already exist for this image!");
167
- }
168
- }
169
-
170
- // console.log("this.acct.address: ", this.acct.address);
171
- // console.log("private key", PRIVATE_KEY);
172
- // console.log('Getting nonce');
173
- // const nonce = await this.imageRegistryContract.provider.getTransactionCount(this.acct.address);
174
-
175
-
176
- // const signedTxn = await this.acct.signTransaction(unicornTxn);
177
- // const receipt = await this.provider.sendTransaction(signedTxn.rawTransaction);
178
- } catch (e) {
179
- // console.error(e);
180
- console.log("Image certificates already exist for this image!");
181
- }
152
+ console.log("Adding secure lock image cert to image registry");
153
+ await this.write('addImage', ipfsHash, certContent, version, imageName, dockerComposeHash, enclaveNameSecureLock, fee);
154
+ console.log("Adding secure lock image cert transaction was successful!");
182
155
  }
183
156
 
184
157
  async getImagePublicKeyCert(ipfsHash) {
@@ -253,6 +226,18 @@ class ImageRegistry {
253
226
  const receipt = await this.provider.sendTransaction(signedTxn.rawTransaction);
254
227
  }
255
228
 
229
+ // Where the image's developer fee is paid. The registry records the
230
+ // publishing wallet at registration; only the image's owner changes it.
231
+ async setRewardAddress(ipfsHash, rewardAddress) {
232
+ const current = await this.imageRegistryContract.getRewardAddress(ipfsHash);
233
+ if (current.toLowerCase() === rewardAddress.toLowerCase()) {
234
+ console.log(`Reward address is already ${rewardAddress}`);
235
+ return;
236
+ }
237
+ console.log(`Setting the reward address to ${rewardAddress}`);
238
+ await this.write('changeImageRewardAddress', ipfsHash, rewardAddress);
239
+ }
240
+
256
241
  async getTrustedZoneCert(ipfsHash) {
257
242
  const cert = await this.imageRegistryContract.getTrustedZoneCert(ipfsHash);
258
243
  return cert;
@@ -283,14 +268,142 @@ class ImageRegistry {
283
268
  return ['', '', ''];
284
269
  }
285
270
  }
271
+
272
+ // Whether the registry is an ECImageRegistryV2 or later, which records an
273
+ // image before its certificate exists (registerImage, then setImageCert).
274
+ // A V1 registry has no pendingImages() and reverts; a node that does not
275
+ // answer is an error, not a V1 registry.
276
+ async isV2() {
277
+ try {
278
+ await this.imageRegistryContract.pendingImages();
279
+ return true;
280
+ } catch (e) {
281
+ if (isRevert(e)) return false;
282
+ throw e;
283
+ }
284
+ }
285
+
286
+ // The wallet that owns the securelock name on an ECImageRegistryV3 or
287
+ // later (the zero address while nobody does), or null on a registry
288
+ // without name owners. Only the owner publishes under the name, and a
289
+ // name and its -unsafe twin have one owner.
290
+ async imageNameOwner(imageName) {
291
+ try {
292
+ return await this.imageRegistryContract.imageNameOwner(imageName);
293
+ } catch (e) {
294
+ if (isRevert(e)) return null;
295
+ throw e;
296
+ }
297
+ }
298
+
299
+ // Send `method(...args)` from the publishing wallet. It is simulated
300
+ // first, so a call the registry refuses stops here with the registry's
301
+ // reason, then sent with its gas estimate plus 30%: what a registry write
302
+ // costs depends on the pending list, which other publishers change
303
+ // between the estimate and the block.
304
+ async write(method, ...args) {
305
+ try {
306
+ await this.imageRegistryContract.callStatic[method](...args);
307
+ } catch (e) {
308
+ throw new Error(`the image registry refuses ${method}: ${revertReason(e, this.imageRegistryContract.interface)}`);
309
+ }
310
+ const gas = await this.imageRegistryContract.estimateGas[method](...args);
311
+ const overrides = { gasLimit: gas.mul(13).div(10) };
312
+ if (BLOCKCHAIN_NETWORK.includes("Polygon")) {
313
+ overrides.nonce = await this.provider.getTransactionCount(this.acct.address, 'pending');
314
+ overrides.gasPrice = (await this.provider.getGasPrice()).mul(110).div(100);
315
+ }
316
+ const tx = await this.imageRegistryContract[method](...args, overrides);
317
+ const receipt = await this.imageRegistryContract.provider.waitForTransaction(tx.hash);
318
+ console.log("transaction receipt: ", tx.hash);
319
+ if (receipt.status !== 1) {
320
+ throw new Error(`${method} reverted (${tx.hash})`);
321
+ }
322
+ }
323
+
324
+ // Record the securelock on a V2 registry before its certificate exists:
325
+ // name, protocol version v3, compose, session, the publisher's fee and the
326
+ // IPFS node that holds the image (a multiaddr, or ""). A hash this wallet
327
+ // already registered with the same name, compose and session is left as
328
+ // it is; a hash another wallet registered is refused, and so is a name
329
+ // another wallet owns. The record cannot be changed, so a hash registered
330
+ // with another compose or session is refused too: the nodes would fetch
331
+ // the compose the record names.
332
+ async registerImage(ipfsHash, imageName, dockerComposeHash, session, fee, ipfsPeer) {
333
+ const details = await this.imageRegistryContract.imageDetails(ipfsHash);
334
+ if (details.owner !== ethers.constants.AddressZero) {
335
+ if (details.owner.toLowerCase() !== this.acct.address.toLowerCase()) {
336
+ throw new Error(`${ipfsHash} is registered by ${details.owner}, not by this wallet`);
337
+ }
338
+ const differing = [
339
+ ['name', details.name, imageName],
340
+ ['compose', details.dockerComposeHash, dockerComposeHash],
341
+ ['session', details.session, session],
342
+ ].filter(([, recorded, wanted]) => recorded !== wanted);
343
+ if (differing.length) {
344
+ const recorded = differing.map(([field, value]) => `${field} ${value}`).join(', ');
345
+ const wanted = differing.map(([field, , value]) => `${field} ${value}`).join(', ');
346
+ throw new Error(`${ipfsHash} is registered with ${recorded}, and this publish has ${wanted}; run ecld-build to publish a new version`);
347
+ }
348
+ console.log(`${ipfsHash} is already registered`);
349
+ return false;
350
+ }
351
+ const nameOwner = await this.imageNameOwner(imageName);
352
+ if (nameOwner && nameOwner !== ethers.constants.AddressZero
353
+ && nameOwner.toLowerCase() !== this.acct.address.toLowerCase()) {
354
+ throw new Error(`the image name ${imageName} belongs to ${nameOwner}; publish under another PROJECT_NAME`);
355
+ }
356
+ console.log(`Registering ${imageName} v3 as ${ipfsHash} (compose ${dockerComposeHash}, peer ${ipfsPeer || 'none'})`);
357
+ await this.write('registerImage',
358
+ ipfsHash, "v3", imageName, dockerComposeHash, session, Number(fee), ipfsPeer || "");
359
+ return true;
360
+ }
361
+
362
+ // Write the certificate of a registered image, once, from the wallet that
363
+ // registered it. A certificate already there is left as it is. The RPC
364
+ // endpoint balances requests over nodes that can lag the chain by several
365
+ // blocks, and a read can fail outright, so an image this publish
366
+ // registered minutes earlier can read as missing, or be refused as "Image
367
+ // not found" by the simulation: both are tried again every 15 seconds for
368
+ // `waitMs` before the image is called missing.
369
+ async setImageCert(ipfsHash, cert, waitMs = 600000) {
370
+ const deadline = Date.now() + waitMs;
371
+ for (;;) {
372
+ let details = null;
373
+ try {
374
+ details = await this.imageRegistryContract.imageDetails(ipfsHash);
375
+ } catch (e) {
376
+ if (Date.now() >= deadline) throw e;
377
+ }
378
+ if (details && details.owner !== ethers.constants.AddressZero) {
379
+ if (details.certPublicKey) {
380
+ if (details.certPublicKey.trim() === cert.trim()) {
381
+ console.log(`The certificate of ${ipfsHash} is already registered`);
382
+ return false;
383
+ }
384
+ throw new Error(`${ipfsHash} already has a certificate, and it differs from the extracted one`);
385
+ }
386
+ try {
387
+ console.log(`Registering the certificate of ${ipfsHash}`);
388
+ await this.write('setImageCert', ipfsHash, cert);
389
+ return true;
390
+ } catch (e) {
391
+ if (!(e.message || '').includes('Image not found') || Date.now() >= deadline) throw e;
392
+ }
393
+ } else if (details && Date.now() >= deadline) {
394
+ throw new Error(`${ipfsHash} is not registered`);
395
+ }
396
+ await new Promise((resolve) => setTimeout(resolve, 15000));
397
+ }
398
+ }
286
399
  }
287
400
 
288
401
  (async () => {
289
402
  try {
290
- const [networkName, projectName, version, privateKey, action] = process.argv.slice(2);
403
+ const [networkName, projectName, version, action] = process.argv.slice(2);
291
404
  if (action === "validateAddress") {
292
- if (privateKey) {
293
- console.log(isStringPrivateKey(privateKey));
405
+ if (PRIVATE_KEY) {
406
+ console.log(isStringPrivateKey(PRIVATE_KEY));
294
407
  }
295
408
  process.exit(0);
296
409
  }
@@ -304,8 +417,8 @@ class ImageRegistry {
304
417
  process.exit(0);
305
418
  }
306
419
  // The certificate of the trustedzone's own record. getLatestImageVersionPublicKey
307
- // reads the securelock records instead: ECImageRegistryV2 reverts it for
308
- // a trustedzone, and the bloxberg mainnet and LitVM registries return a
420
+ // reads the securelock records instead: ECImageRegistryV2 and V3 revert it
421
+ // for a trustedzone, and the bloxberg mainnet and LitVM registries return a
309
422
  // different certificate for the same name.
310
423
  if (action === 'getTrustedZoneCert') {
311
424
  const imageRegistry = new ImageRegistry();
@@ -328,6 +441,47 @@ class ImageRegistry {
328
441
  }
329
442
 
330
443
  const imageRegistry = new ImageRegistry();
444
+ // Whether the registry records images before their certificate.
445
+ if (action === 'isV2') {
446
+ try {
447
+ console.log((await imageRegistry.isV2()) ? 'true' : 'false');
448
+ } catch (e) {
449
+ console.error(`Could not read the image registry ${IMAGE_REGISTRY_ADDRESS}: ${e.reason || e.message}`);
450
+ process.exit(1);
451
+ }
452
+ process.exit(0);
453
+ }
454
+ // Record the image before its certificate (V2): IPFS_HASH,
455
+ // IPFS_DOCKER_COMPOSE_HASH, PROJECT_NAME (the name registered),
456
+ // ENCLAVE_NAME_SECURELOCK (the session), DEVELOPER_FEE and IPFS_PEER
457
+ // (the publish's node) come from the environment.
458
+ if (action === 'registerImage') {
459
+ try {
460
+ const rewardAddress = rewardAddressFromEnv();
461
+ await imageRegistry.registerImage(
462
+ process.env.IPFS_HASH || "", process.env.PROJECT_NAME || "",
463
+ process.env.IPFS_DOCKER_COMPOSE_HASH || "", process.env.ENCLAVE_NAME_SECURELOCK || "",
464
+ process.env.DEVELOPER_FEE || "0", process.env.IPFS_PEER || "");
465
+ if (rewardAddress) {
466
+ await imageRegistry.setRewardAddress(process.env.IPFS_HASH || "", rewardAddress);
467
+ }
468
+ } catch (e) {
469
+ console.error(`Could not register the image: ${e.reason || e.message}`);
470
+ process.exit(1);
471
+ }
472
+ process.exit(0);
473
+ }
474
+ // Write the extracted certificate of a registered image (V2).
475
+ if (action === 'setImageCert') {
476
+ try {
477
+ const cert = fs.readFileSync("./registry/certificate.securelock.crt", 'utf8');
478
+ await imageRegistry.setImageCert(process.env.IPFS_HASH || "", cert);
479
+ } catch (e) {
480
+ console.error(`Could not register the certificate: ${e.reason || e.message}`);
481
+ process.exit(1);
482
+ }
483
+ process.exit(0);
484
+ }
331
485
  if (action === 'registerSecureLockImage') {
332
486
  const secureLock = fs.readFileSync("./registry/certificate.securelock.crt", 'utf8');
333
487
  // console.log("SECURELOCK:", secureLock);
@@ -357,7 +511,28 @@ class ImageRegistry {
357
511
  if (enclaveVersion && enclaveVersion !== "v3") versionKeys.push(enclaveVersion);
358
512
  for (const versionKey of versionKeys) {
359
513
  console.log(`Registering securelock under version '${versionKey}'`);
360
- await imageRegistry.addSecureLockImageCert(secureLock, ipfsHash, imageName, versionKey, ipfsDockerComposeHash, enclaveNameSecureLock, fee);
514
+ try {
515
+ await imageRegistry.addSecureLockImageCert(secureLock, ipfsHash, imageName, versionKey, ipfsDockerComposeHash, enclaveNameSecureLock, fee);
516
+ } catch (e) {
517
+ // The registry refuses a hash the (name, version) channel already holds.
518
+ if ((e.message || '').includes('Image hash is already in registry')) {
519
+ console.log(`The securelock is already registered under version '${versionKey}'`);
520
+ continue;
521
+ }
522
+ console.error(`Could not register the securelock under version '${versionKey}': ${e.reason || e.message}`);
523
+ process.exit(1);
524
+ }
525
+ }
526
+ // REWARD_ADDRESS: where the image's developer fee is paid, when not
527
+ // the publishing wallet.
528
+ try {
529
+ const rewardAddress = rewardAddressFromEnv();
530
+ if (rewardAddress) {
531
+ await imageRegistry.setRewardAddress(ipfsHash, rewardAddress);
532
+ }
533
+ } catch (e) {
534
+ console.error(`Could not set the reward address: ${e.reason || e.message}`);
535
+ process.exit(1);
361
536
  }
362
537
  process.exit(0);
363
538
  }
@@ -367,6 +542,22 @@ class ImageRegistry {
367
542
  process.exit(0);
368
543
  }
369
544
  console.log(`Checking image: '${projectName}' on the ${networkName} blockchain...`);
545
+ // On a registry with name owners the name's owner answers, whether or
546
+ // not an image is certified under the name yet.
547
+ const nameOwner = await imageRegistry.imageNameOwner(projectName);
548
+ if (nameOwner !== null) {
549
+ if (nameOwner === ethers.constants.AddressZero) {
550
+ console.log(`Image: '${projectName}' is available on the ${networkName} blockchain.`);
551
+ process.exit(0);
552
+ }
553
+ if (PRIVATE_KEY && isStringPrivateKey(PRIVATE_KEY) === "OK"
554
+ && walletAddress(PRIVATE_KEY).toLowerCase() !== nameOwner.toLowerCase()) {
555
+ console.log(`!!! Image: '${projectName}' is owned by '${nameOwner}'.\nYou are not the account holder of the image.\nPlease change the project name and try again.\n`);
556
+ process.exit(1);
557
+ }
558
+ console.log(`Image: '${projectName}' is owned by '${nameOwner}'.\nIf you are not the account holder, you will not be able to publish your project with the current name. Please change the project name and try again.\n`);
559
+ process.exit(0);
560
+ }
370
561
  const imageHash = (await imageRegistry._getLatestImageVersionPublicKey(projectName, version))[0];
371
562
  console.log(`Image hash: ${imageHash}`);
372
563
  if (!imageHash) {
@@ -375,10 +566,9 @@ class ImageRegistry {
375
566
  }
376
567
  const imageOwner = (await imageRegistry.getImageDetails(imageHash))[0];
377
568
 
378
- if (privateKey) {
379
- if (isStringPrivateKey(privateKey) === "OK") {
380
- const account = Account.fromPrivate(privateKey);
381
- if (imageOwner !== account.address) {
569
+ if (PRIVATE_KEY) {
570
+ if (isStringPrivateKey(PRIVATE_KEY) === "OK") {
571
+ if (imageOwner.toLowerCase() !== walletAddress(PRIVATE_KEY).toLowerCase()) {
382
572
  console.log(`!!! Image: '${projectName}' is owned by '${imageOwner}'.\nYou are not the account holder of the image.\nPlease change the project name and try again.\n`);
383
573
  process.exit(1);
384
574
  }
@@ -65,14 +65,21 @@ console.log("Docker Composer Hash:", dockerComposerHash);
65
65
  const submitResponse = await submitIpfsHash(hhash, enclaveName, protocolVersion, network, templateVersion, dockerComposerHash);
66
66
  console.log("Submit IPFS Hash Response:", submitResponse);
67
67
 
68
- // Check IPFS Hash Status
68
+ // The service answers publicKey 0 while the image is queued or running
69
+ // (with status, percent and queuePosition), -1 when the extraction failed
70
+ // (with reason) and the certificate when done.
69
71
  while (true) {
70
72
  const checkResponse = await checkIpfsHashStatus(hhash);
71
73
  if ("publicKey" in checkResponse) {
72
74
  if (checkResponse.publicKey === 0) {
73
- console.log(`Public key not available yet. Queue position: ${checkResponse.queuePosition || 'Unknown'}`);
75
+ if (checkResponse.status === "running") {
76
+ console.log(`Public key extraction running, ${checkResponse.percent || 0}% done`);
77
+ } else {
78
+ console.log(`Public key not available yet. Queue position: ${checkResponse.queuePosition ?? 'Unknown'}`);
79
+ }
74
80
  } else if (checkResponse.publicKey === -1) {
75
- console.log("Hash is not derived from Eternity Cloud SDK.");
81
+ console.log(`Public key extraction failed: ${checkResponse.reason || "reason not reported by the service"}`);
82
+ console.log("Fix the cause (keep the image pinned, build with the current SDK) and publish again: a new submission of the same hash runs the extraction again.");
76
83
  process.exit(1);
77
84
  } else {
78
85
  console.log("Public Key:", checkResponse.publicKey);
@@ -83,16 +83,22 @@ if __name__ == "__main__":
83
83
  )
84
84
  print("Submit IPFS Hash Response:", submit_response)
85
85
 
86
- # Check IPFS Hash Status
86
+ # The service answers publicKey 0 while the image is queued or running
87
+ # (with status, percent and queuePosition), -1 when the extraction failed
88
+ # (with reason) and the certificate when done.
87
89
  while True:
88
90
  check_response = check_ipfs_hash_status(hhash)
89
91
  if "publicKey" in check_response:
90
92
  if check_response["publicKey"] == 0:
91
- print(
92
- f"Public key not available yet. Queue position: {check_response.get('queuePosition', 'Unknown')}"
93
- )
94
- elif check_response["publicKey"] == -1:
95
- print("Hash is not derived from Eternity Cloud SDK.")
93
+ if check_response.get("status") == "running":
94
+ print(f"Public key extraction running, {check_response.get('percent', 0)}% done")
95
+ else:
96
+ print(
97
+ f"Public key not available yet. Queue position: {check_response.get('queuePosition', 'Unknown')}"
98
+ )
99
+ elif check_response["publicKey"] in (-1, "-1"):
100
+ print(f"Public key extraction failed: {check_response.get('reason', 'reason not reported by the service')}")
101
+ print("Fix the cause (keep the image pinned, build with the current SDK) and publish again: a new submission of the same hash runs the extraction again.")
96
102
  exit(1)
97
103
  else:
98
104
  print("Public Key:", check_response["publicKey"])