@elgora/cli 7.2.0 → 7.2.2

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.
Files changed (3) hide show
  1. package/README.md +3 -2
  2. package/dist/cli.js +650 -70
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -35,8 +35,9 @@ npx @elgora/cli --help
35
35
  one exact Solver and Submission pair.
36
36
  - `guardian:verdict` publishes a written Verdict and records the same Verdict
37
37
  on ElgoraHub.
38
- - `guardian:deliver-key` repairs delivery for one exact Solver and Submission
39
- pair when ElgoraHub still permits it.
38
+ - `guardian:deliver-key` wraps one Solver–Submission content key for delivery
39
+ so the funding Poster can retrieve the winning work; idempotent, and accepted
40
+ only for the pair ElgoraHub currently records or the finalized winner.
40
41
  - `claim` prepares or sends the winning Solver's reserved award or the
41
42
  Poster's queued refund, as selected from finalized ElgoraHub state.
42
43
  - `verification-record` is a plain, unauthenticated GET that fetches or lazily
package/dist/cli.js CHANGED
@@ -196,7 +196,7 @@ function canonicalizeJson(value) {
196
196
  // ../../packages/core/src/submission-encryption.ts
197
197
  import { Aes256Gcm, CipherSuite, HkdfSha256 } from "@hpke/core";
198
198
  import { DhkemX25519HkdfSha256 } from "@hpke/dhkem-x25519";
199
- import { keccak256 as keccak2562, toBytes as toBytes2 } from "viem";
199
+ import { concatBytes, keccak256 as keccak2562, toBytes as toBytes2 } from "viem";
200
200
  import { z as z3 } from "zod";
201
201
 
202
202
  // ../../packages/core/src/base64.ts
@@ -474,6 +474,78 @@ function buildWinningSolutionDeliveryInfo(contextCommitment) {
474
474
  `${WINNING_SOLUTION_DELIVERY_HPKE_INFO_PREFIX}:${contextCommitment}`
475
475
  );
476
476
  }
477
+ var GUARDIAN_ROSTER_KEY_PROOF_INFO = "elgora/guardian-roster-key-proof/hpke-fixture/v0";
478
+ var GUARDIAN_ROSTER_KEY_PROOF_MESSAGE_TYPE = "elgora_guardian_roster_key_proof_v0";
479
+ var GUARDIAN_ROSTER_KEY_PROOF_FIXTURE_BYTES = 32;
480
+ var guardianRosterKeyProofContextSchema = z3.strictObject({
481
+ chain_id: chainIdSchema,
482
+ hub_address: lowerAddressSchema,
483
+ proposed_account: lowerAddressSchema,
484
+ proposed_encryption_public_key: x25519PublicKeySchema
485
+ });
486
+ async function prepareGuardianRosterKeyProof(input) {
487
+ const context = guardianRosterKeyProofContextSchema.parse({
488
+ ...input.context,
489
+ hub_address: input.context.hub_address.toLowerCase(),
490
+ proposed_account: input.context.proposed_account.toLowerCase()
491
+ });
492
+ const fixture = (input.random_bytes ?? secureRandomBytes)(
493
+ GUARDIAN_ROSTER_KEY_PROOF_FIXTURE_BYTES
494
+ );
495
+ const suite = createHpkeSuite();
496
+ const recipientPublicKey = await suite.kem.deserializePublicKey(
497
+ base64UrlToBytes(context.proposed_encryption_public_key)
498
+ );
499
+ const sealed = await suite.seal(
500
+ { recipientPublicKey, info: toBytes2(GUARDIAN_ROSTER_KEY_PROOF_INFO) },
501
+ fixture
502
+ );
503
+ const ciphertext = bytesToBase64Url(toUint8Array(sealed.ct));
504
+ const message = canonicalizeJson({
505
+ type: GUARDIAN_ROSTER_KEY_PROOF_MESSAGE_TYPE,
506
+ chain_id: context.chain_id,
507
+ hub_address: context.hub_address,
508
+ proposed_account: context.proposed_account,
509
+ proposed_encryption_public_key: context.proposed_encryption_public_key,
510
+ ciphertext_sha256: await computeSha256Hex(toUint8Array(sealed.ct))
511
+ });
512
+ return {
513
+ challenge: {
514
+ context,
515
+ message,
516
+ enc: bytesToBase64Url(toUint8Array(sealed.enc)),
517
+ ciphertext
518
+ },
519
+ fixture
520
+ };
521
+ }
522
+ async function openGuardianRosterKeyProofFixture(input) {
523
+ const suite = createHpkeSuite();
524
+ const info = toBytes2(GUARDIAN_ROSTER_KEY_PROOF_INFO);
525
+ const enc = base64UrlToBytes(input.challenge.enc);
526
+ const ciphertext = base64UrlToBytes(input.challenge.ciphertext);
527
+ for (const privateKey of input.recipientPrivateKeys) {
528
+ try {
529
+ const recipientKey = await suite.kem.deserializePrivateKey(
530
+ base64UrlToBytes(x25519PrivateKeySchema.parse(privateKey))
531
+ );
532
+ const fixture = await suite.open({ recipientKey, enc, info }, ciphertext);
533
+ return toUint8Array(fixture);
534
+ } catch {
535
+ }
536
+ }
537
+ throw new Error(
538
+ "none of the supplied private keys could open this guardian roster key proof fixture"
539
+ );
540
+ }
541
+ function buildGuardianRosterKeyProofSignedPayload(message, fixture) {
542
+ if (fixture.length !== GUARDIAN_ROSTER_KEY_PROOF_FIXTURE_BYTES) {
543
+ throw new Error(
544
+ `guardian roster key proof fixture must be ${GUARDIAN_ROSTER_KEY_PROOF_FIXTURE_BYTES} bytes`
545
+ );
546
+ }
547
+ return concatBytes([toBytes2(message), fixture]);
548
+ }
477
549
  async function sealPrivateArtifactEnvelope(input) {
478
550
  const context = privateArtifactContextSchema.parse(
479
551
  normalizeContext(input.context)
@@ -3424,6 +3496,11 @@ var ELGORA_HUB_BOUNTY_READ_ABI = [
3424
3496
  getAbiItem2({ abi: ELGORA_HUB_ABI, name: "activeSubmissionCommitment" }),
3425
3497
  ...ELGORA_HUB_ERRORS
3426
3498
  ];
3499
+ var ELGORA_HUB_OWNERSHIP_READ_ABI = [
3500
+ getAbiItem2({ abi: ELGORA_HUB_ABI, name: "owner" }),
3501
+ getAbiItem2({ abi: ELGORA_HUB_ABI, name: "pendingOwner" }),
3502
+ ...ELGORA_HUB_ERRORS
3503
+ ];
3427
3504
  var ELGORA_HUB_BOUNTY_CREATED_EVENT = getAbiItem2({
3428
3505
  abi: ELGORA_HUB_ABI,
3429
3506
  name: "BountyCreated"
@@ -3453,6 +3530,30 @@ var ELGORA_HUB_CLAIM_ABI = [
3453
3530
  getAbiItem2({ abi: ELGORA_HUB_ABI, name: "claim" }),
3454
3531
  ...ELGORA_HUB_ERRORS
3455
3532
  ];
3533
+ var ELGORA_HUB_ADD_GUARDIAN_ABI = [
3534
+ getAbiItem2({ abi: ELGORA_HUB_ABI, name: "guardianRosterAdmin" }),
3535
+ getAbiItem2({ abi: ELGORA_HUB_ABI, name: "getGuardianRoster" }),
3536
+ getAbiItem2({ abi: ELGORA_HUB_ABI, name: "addGuardian" }),
3537
+ ...ELGORA_HUB_ERRORS
3538
+ ];
3539
+ function encodeAddGuardianTransaction(input) {
3540
+ return {
3541
+ chain_id: input.chainId,
3542
+ to: input.hubAddress,
3543
+ value: "0",
3544
+ data: encodeFunctionData({
3545
+ abi: ELGORA_HUB_ADD_GUARDIAN_ABI,
3546
+ functionName: "addGuardian",
3547
+ args: [input.name, input.account, input.encryptionPublicKey]
3548
+ }),
3549
+ function_name: "add_guardian",
3550
+ args: {
3551
+ name: input.name,
3552
+ account: input.account,
3553
+ encryption_public_key: input.encryptionPublicKey
3554
+ }
3555
+ };
3556
+ }
3456
3557
  function encodeSubmitTransaction(input) {
3457
3558
  return {
3458
3559
  chain_id: input.chainId,
@@ -3496,6 +3597,7 @@ var artifactUploadUrlRequestSchema = z4.object({
3496
3597
  }).strict();
3497
3598
 
3498
3599
  // ../../packages/core/src/api/config.ts
3600
+ import { createHash } from "crypto";
3499
3601
  import { z as z5 } from "zod";
3500
3602
  function withDeploymentEnvDefaults(env) {
3501
3603
  const merged = { ...env };
@@ -3559,7 +3661,6 @@ var envShape = {
3559
3661
  ELGORA_ARTIFACT_STORAGE_BUCKET: z5.string().min(1, "ELGORA_ARTIFACT_STORAGE_BUCKET is required"),
3560
3662
  ELGORA_ARTIFACT_STORAGE_ACCESS_KEY_ID: z5.string().min(1, "ELGORA_ARTIFACT_STORAGE_ACCESS_KEY_ID is required"),
3561
3663
  ELGORA_ARTIFACT_STORAGE_SECRET_ACCESS_KEY: z5.string().min(1, "ELGORA_ARTIFACT_STORAGE_SECRET_ACCESS_KEY is required"),
3562
- ELGORA_DELIVERY_KEY_ID: z5.string().min(1, "ELGORA_DELIVERY_KEY_ID is required").default("delivery_v0"),
3563
3664
  ELGORA_DELIVERY_PUBLIC_KEY: z5.string().min(1, "ELGORA_DELIVERY_PUBLIC_KEY is required"),
3564
3665
  ELGORA_DELIVERY_PRIVATE_KEY: z5.string().min(1, "ELGORA_DELIVERY_PRIVATE_KEY is required")
3565
3666
  };
@@ -3588,23 +3689,25 @@ var ELGORA_HUB_FEE_POLICY_ABI = [
3588
3689
  // ../../packages/core/src/api/orchestrator-readiness.ts
3589
3690
  import { z as z6 } from "zod";
3590
3691
  var MAX_ORCHESTRATOR_ISSUES = 20;
3591
- var boundedText = (maxLength) => z6.string().trim().min(1).transform((value) => value.slice(0, maxLength));
3692
+ var boundedText = (maxLength) => z6.string().trim().min(1).max(maxLength);
3592
3693
  var readinessIssueSchema = z6.object({
3593
3694
  path: boundedText(120),
3594
3695
  message: boundedText(500)
3595
3696
  }).strict();
3596
3697
  var readinessResponseSchema = z6.object({
3597
3698
  judgeable: z6.boolean(),
3598
- issues: z6.array(readinessIssueSchema).transform((issues) => issues.slice(0, MAX_ORCHESTRATOR_ISSUES))
3699
+ issues: z6.array(readinessIssueSchema).max(MAX_ORCHESTRATOR_ISSUES)
3599
3700
  }).strict();
3600
- var anthropicMessageSchema = z6.object({
3601
- content: z6.array(
3701
+ var providerMessageSchema = z6.object({
3702
+ id: z6.string().optional(),
3703
+ choices: z6.array(
3602
3704
  z6.object({
3603
- type: z6.string(),
3604
- text: z6.string().optional()
3705
+ message: z6.object({
3706
+ content: z6.string().nullable().optional()
3707
+ }).passthrough(),
3708
+ finish_reason: z6.string().nullable().optional()
3605
3709
  }).passthrough()
3606
- ),
3607
- stop_reason: z6.string().nullable().optional()
3710
+ ).min(1)
3608
3711
  }).passthrough();
3609
3712
 
3610
3713
  // ../../packages/core/src/api/solver-submission-responses.ts
@@ -4787,6 +4890,16 @@ function readArgs(argv) {
4787
4890
  };
4788
4891
  }
4789
4892
 
4893
+ // ../../packages/subgraph-client/src/errors.ts
4894
+ var SubgraphReadError = class extends Error {
4895
+ reason;
4896
+ constructor(reason, message, options) {
4897
+ super(message, options);
4898
+ this.name = "SubgraphReadError";
4899
+ this.reason = reason;
4900
+ }
4901
+ };
4902
+
4790
4903
  // ../../packages/subgraph-client/src/status.ts
4791
4904
  function mapSubgraphStatus(value) {
4792
4905
  switch (value) {
@@ -4862,6 +4975,7 @@ var SUBMISSION_FIELDS = `
4862
4975
  recordedAt
4863
4976
  recordedBlockNumber
4864
4977
  recordedTxHash
4978
+ submissionCallCount
4865
4979
  `;
4866
4980
  var VERDICT_FIELDS = `
4867
4981
  guardian
@@ -4877,6 +4991,7 @@ var GUARDIAN_ROSTER_SNAPSHOT_FIELDS = `
4877
4991
  id
4878
4992
  memberCount
4879
4993
  members {
4994
+ position
4880
4995
  account
4881
4996
  name
4882
4997
  encryptionPublicKey
@@ -5037,26 +5152,41 @@ function createElgoraSubgraphClient(options) {
5037
5152
  return options.hubAddress;
5038
5153
  }
5039
5154
  async function execute(query, variables) {
5040
- const response = await fetchImpl(
5041
- endpoint,
5042
- JSON.stringify({ query, variables })
5043
- );
5044
- const raw = await response.text();
5155
+ let response;
5156
+ let raw;
5157
+ try {
5158
+ response = await fetchImpl(
5159
+ endpoint,
5160
+ JSON.stringify({ query, variables })
5161
+ );
5162
+ raw = await response.text();
5163
+ } catch (cause) {
5164
+ throw new SubgraphReadError(
5165
+ "transport_failed",
5166
+ "Subgraph request did not complete",
5167
+ { cause }
5168
+ );
5169
+ }
5045
5170
  if (!response.ok) {
5046
- throw new Error(
5171
+ throw new SubgraphReadError(
5172
+ "request_failed",
5047
5173
  `Subgraph request failed with status ${response.status}: ${raw}`
5048
5174
  );
5049
5175
  }
5050
5176
  const parsed = parseJson(raw);
5051
5177
  const errors = readRecord(parsed).errors;
5052
5178
  if (Array.isArray(errors) && errors.length > 0) {
5053
- throw new Error(
5179
+ throw new SubgraphReadError(
5180
+ "query_errors",
5054
5181
  `Subgraph query returned errors: ${JSON.stringify(errors)}`
5055
5182
  );
5056
5183
  }
5057
5184
  const data = readRecord(parsed).data;
5058
5185
  if (data === void 0 || data === null) {
5059
- throw new Error("Subgraph response did not include a data object");
5186
+ throw new SubgraphReadError(
5187
+ "malformed_response",
5188
+ "Subgraph response did not include a data object"
5189
+ );
5060
5190
  }
5061
5191
  return data;
5062
5192
  }
@@ -5246,7 +5376,8 @@ function mapAnalyticsGuardianRoster(row) {
5246
5376
  const members = asArray(record.members, "members");
5247
5377
  const memberCount = asNumber(record.memberCount, "memberCount");
5248
5378
  if (members.length !== memberCount) {
5249
- throw new Error(
5379
+ throw new SubgraphReadError(
5380
+ "unexpected_shape",
5250
5381
  "Historical Guardian roster members do not match memberCount"
5251
5382
  );
5252
5383
  }
@@ -5343,12 +5474,17 @@ function mapSubmissionFields(row) {
5343
5474
  record.recordedBlockNumber,
5344
5475
  "recordedBlockNumber"
5345
5476
  ),
5346
- recordedTxHash: asHex(record.recordedTxHash, "recordedTxHash")
5477
+ recordedTxHash: asHex(record.recordedTxHash, "recordedTxHash"),
5478
+ submissionCallCount: asNumber(
5479
+ record.submissionCallCount,
5480
+ "submissionCallCount"
5481
+ )
5347
5482
  };
5348
5483
  }
5349
5484
  function mapGuardianRosterMember(row) {
5350
5485
  const record = readRecord(row);
5351
5486
  return {
5487
+ position: asSafeInteger(record.position, "members.position"),
5352
5488
  account: asHex(record.account, "account"),
5353
5489
  name: asString(record.name, "name"),
5354
5490
  encryptionPublicKey: asString(
@@ -5359,10 +5495,19 @@ function mapGuardianRosterMember(row) {
5359
5495
  }
5360
5496
  function mapGuardianRosterSnapshot(row) {
5361
5497
  const record = readRecord(row);
5498
+ const members = asArray(record.members, "members").map(mapGuardianRosterMember).sort((left, right) => left.position - right.position);
5499
+ members.forEach((member, index) => {
5500
+ if (member.position !== index) {
5501
+ throw new SubgraphReadError(
5502
+ "unexpected_shape",
5503
+ `Guardian roster snapshot positions are not 0..n-1 for '${asHex(record.id, "id")}'`
5504
+ );
5505
+ }
5506
+ });
5362
5507
  return {
5363
5508
  rosterHash: asHex(record.id, "id"),
5364
5509
  memberCount: asNumber(record.memberCount, "memberCount"),
5365
- members: asArray(record.members, "members").map(mapGuardianRosterMember),
5510
+ members: members.map(({ position: _position, ...member }) => member),
5366
5511
  recordedAt: asString(record.recordedAt, "recordedAt")
5367
5512
  };
5368
5513
  }
@@ -5441,7 +5586,10 @@ function parseJson(raw) {
5441
5586
  try {
5442
5587
  return JSON.parse(raw);
5443
5588
  } catch {
5444
- throw new Error(`Subgraph returned non-JSON response: ${raw}`);
5589
+ throw new SubgraphReadError(
5590
+ "malformed_response",
5591
+ `Subgraph returned non-JSON response: ${raw}`
5592
+ );
5445
5593
  }
5446
5594
  }
5447
5595
  function readRecord(value) {
@@ -5449,12 +5597,16 @@ function readRecord(value) {
5449
5597
  }
5450
5598
  function asString(value, field) {
5451
5599
  if (typeof value === "string") return value;
5452
- throw new Error(`Expected string for subgraph field '${field}'`);
5600
+ throw new SubgraphReadError(
5601
+ "unexpected_shape",
5602
+ `Expected string for subgraph field '${field}'`
5603
+ );
5453
5604
  }
5454
5605
  function asUnsignedBigInt(value, field) {
5455
5606
  const text = asString(value, field);
5456
5607
  if (/^(?:0|[1-9][0-9]*)$/.test(text)) return BigInt(text);
5457
- throw new Error(
5608
+ throw new SubgraphReadError(
5609
+ "unexpected_shape",
5458
5610
  `Expected non-negative integer string for subgraph field '${field}'`
5459
5611
  );
5460
5612
  }
@@ -5464,29 +5616,42 @@ function asNullableString(value, field) {
5464
5616
  }
5465
5617
  function asNumber(value, field) {
5466
5618
  if (typeof value === "number" && Number.isFinite(value)) return value;
5467
- throw new Error(`Expected number for subgraph field '${field}'`);
5619
+ throw new SubgraphReadError(
5620
+ "unexpected_shape",
5621
+ `Expected number for subgraph field '${field}'`
5622
+ );
5468
5623
  }
5469
5624
  function asSafeInteger(value, field) {
5470
5625
  if (typeof value === "number" && Number.isSafeInteger(value) && value >= 0) {
5471
5626
  return value;
5472
5627
  }
5473
- throw new Error(
5628
+ throw new SubgraphReadError(
5629
+ "unexpected_shape",
5474
5630
  `Expected non-negative integer for subgraph field '${field}'`
5475
5631
  );
5476
5632
  }
5477
5633
  function asBoolean(value, field) {
5478
5634
  if (typeof value === "boolean") return value;
5479
- throw new Error(`Expected boolean for subgraph field '${field}'`);
5635
+ throw new SubgraphReadError(
5636
+ "unexpected_shape",
5637
+ `Expected boolean for subgraph field '${field}'`
5638
+ );
5480
5639
  }
5481
5640
  function asArray(value, field) {
5482
5641
  if (Array.isArray(value)) return value;
5483
- throw new Error(`Expected array for subgraph field '${field}'`);
5642
+ throw new SubgraphReadError(
5643
+ "unexpected_shape",
5644
+ `Expected array for subgraph field '${field}'`
5645
+ );
5484
5646
  }
5485
5647
  function asHex(value, field) {
5486
5648
  if (typeof value === "string" && value.startsWith("0x")) {
5487
5649
  return value.toLowerCase();
5488
5650
  }
5489
- throw new Error(`Expected hex string for subgraph field '${field}'`);
5651
+ throw new SubgraphReadError(
5652
+ "unexpected_shape",
5653
+ `Expected hex string for subgraph field '${field}'`
5654
+ );
5490
5655
  }
5491
5656
  function asNullableHex(value, field) {
5492
5657
  if (value === null || value === void 0) return null;
@@ -5548,7 +5713,10 @@ async function main4(env = process.env, argv = process.argv.slice(2)) {
5548
5713
  },
5549
5714
  submissions: submissions.map((submission) => ({
5550
5715
  solver: submission.solver,
5551
- submission_commitment: submission.submissionCommitment
5716
+ submission_commitment: submission.submissionCommitment,
5717
+ submitted_at: submission.recordedAt,
5718
+ submitted_at_block: submission.recordedBlockNumber,
5719
+ submission_call_count: submission.submissionCallCount
5552
5720
  }))
5553
5721
  });
5554
5722
  }
@@ -5596,10 +5764,88 @@ function requiredUrl(env, name) {
5596
5764
  }
5597
5765
  }
5598
5766
 
5599
- // src/guardian-verdict.ts
5767
+ // src/guardian-prove-roster-key.ts
5600
5768
  import { readFile } from "fs/promises";
5601
- import { zeroAddress as zeroAddress4 } from "viem";
5769
+ import { text as readStdinText } from "stream/consumers";
5602
5770
  import { privateKeyToAccount as privateKeyToAccount5 } from "viem/accounts";
5771
+ async function main5(env = process.env, argv = process.argv.slice(2)) {
5772
+ const [path5] = argv;
5773
+ if (!path5) {
5774
+ throw new Error(
5775
+ "Usage: elgora-cli guardian:prove-roster-key <challenge_json_path|->"
5776
+ );
5777
+ }
5778
+ const challenge = parseChallenge(
5779
+ path5 === "-" ? await readStdinText(process.stdin) : await readFile(path5, "utf8")
5780
+ );
5781
+ const envWithDefaults = withDeploymentDefaults(env);
5782
+ const { chainId, hubAddress } = loadPublicDeploymentConfig(envWithDefaults);
5783
+ if (challenge.context.chain_id !== chainId || challenge.context.hub_address.toLowerCase() !== hubAddress.toLowerCase()) {
5784
+ throw new Error(
5785
+ `Challenge names chain ${challenge.context.chain_id} / Hub ${challenge.context.hub_address}, not this deployment's chain ${chainId} / Hub ${hubAddress}`
5786
+ );
5787
+ }
5788
+ const guardianAccountPrivateKey = envWithDefaults.ELGORA_GUARDIAN_ACCOUNT_PRIVATE_KEY?.trim().toLowerCase();
5789
+ if (!guardianAccountPrivateKey || !isHexBytes32(guardianAccountPrivateKey)) {
5790
+ throw new Error(
5791
+ "ELGORA_GUARDIAN_ACCOUNT_PRIVATE_KEY must be a 32-byte hex private key"
5792
+ );
5793
+ }
5794
+ const account = privateKeyToAccount5(guardianAccountPrivateKey);
5795
+ if (account.address.toLowerCase() !== challenge.context.proposed_account) {
5796
+ throw new Error(
5797
+ `ELGORA_GUARDIAN_ACCOUNT_PRIVATE_KEY resolves to ${account.address.toLowerCase()}, but this challenge names proposed_account ${challenge.context.proposed_account}`
5798
+ );
5799
+ }
5800
+ const guardianPrivateKeys = readGuardianPrivateKeys(env).map(
5801
+ (key) => key.private_key
5802
+ );
5803
+ const fixture = await openGuardianRosterKeyProofFixture({
5804
+ challenge,
5805
+ recipientPrivateKeys: guardianPrivateKeys
5806
+ });
5807
+ const signedPayload = buildGuardianRosterKeyProofSignedPayload(
5808
+ challenge.message,
5809
+ fixture
5810
+ );
5811
+ const signature = await account.signMessage({
5812
+ message: { raw: signedPayload }
5813
+ });
5814
+ console.log(
5815
+ JSON.stringify({
5816
+ event: "guardian_prove_roster_key.signed",
5817
+ guardian_account: account.address.toLowerCase(),
5818
+ proposed_encryption_public_key: challenge.context.proposed_encryption_public_key,
5819
+ signature
5820
+ })
5821
+ );
5822
+ }
5823
+ function parseChallenge(text) {
5824
+ let parsed;
5825
+ try {
5826
+ parsed = JSON.parse(text);
5827
+ } catch (error) {
5828
+ throw new Error("Challenge input must be valid JSON", { cause: error });
5829
+ }
5830
+ const challenge = parsed && typeof parsed === "object" && "send_to_guardian" in parsed ? parsed.send_to_guardian : parsed;
5831
+ if (!challenge || typeof challenge !== "object" || typeof challenge.message !== "string" || typeof challenge.enc !== "string" || typeof challenge.ciphertext !== "string") {
5832
+ throw new Error(
5833
+ "Challenge JSON must contain context, message, enc, and ciphertext (the object roster:prepare-guardian-proof printed as send_to_guardian)"
5834
+ );
5835
+ }
5836
+ const context = challenge.context;
5837
+ if (!context || typeof context !== "object" || typeof context.chain_id !== "number" || typeof context.hub_address !== "string" || typeof context.proposed_account !== "string" || typeof context.proposed_encryption_public_key !== "string") {
5838
+ throw new Error(
5839
+ "Challenge context must contain chain_id, hub_address, proposed_account, and proposed_encryption_public_key"
5840
+ );
5841
+ }
5842
+ return challenge;
5843
+ }
5844
+
5845
+ // src/guardian-verdict.ts
5846
+ import { readFile as readFile2 } from "fs/promises";
5847
+ import { zeroAddress as zeroAddress4 } from "viem";
5848
+ import { privateKeyToAccount as privateKeyToAccount6 } from "viem/accounts";
5603
5849
  var OUTCOME = {
5604
5850
  no_valid_submission: 0,
5605
5851
  awarded: 1
@@ -5634,7 +5880,7 @@ function parseVerdictArgs(argv) {
5634
5880
  }
5635
5881
  throw usageError();
5636
5882
  }
5637
- async function main5(env = process.env, argv = process.argv.slice(2)) {
5883
+ async function main6(env = process.env, argv = process.argv.slice(2)) {
5638
5884
  const { apiBaseUrl: apiBaseUrlOverride, argv: positionalArgs } = extractApiBaseUrlFlag(argv);
5639
5885
  const args = parseVerdictArgs(positionalArgs);
5640
5886
  const envWithDefaults = withDeploymentDefaults(env);
@@ -5650,11 +5896,11 @@ async function main5(env = process.env, argv = process.argv.slice(2)) {
5650
5896
  "ELGORA_GUARDIAN_ACCOUNT_PRIVATE_KEY must be a 32-byte hex private key"
5651
5897
  );
5652
5898
  }
5653
- const account = privateKeyToAccount5(guardianPrivateKey.toLowerCase());
5899
+ const account = privateKeyToAccount6(guardianPrivateKey.toLowerCase());
5654
5900
  const publicClient = createChainPublicClient(config.chainId, rpcUrl);
5655
5901
  const walletClient = createChainWalletClient(config.chainId, rpcUrl, account);
5656
5902
  await assertHubDeployment(publicClient, config.chainId, config.hubAddress);
5657
- const verdictBody = await readFile(args.verdictBodyPath, "utf8");
5903
+ const verdictBody = await readFile2(args.verdictBodyPath, "utf8");
5658
5904
  const markdown = args.outcome === "awarded" ? buildWrittenVerdictMarkdown({
5659
5905
  config,
5660
5906
  bountyId: args.bountyId,
@@ -5921,9 +6167,9 @@ function resolveChainId(selector) {
5921
6167
  // src/poster-open-winning-submission.ts
5922
6168
  import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
5923
6169
  import path2 from "path";
5924
- import { privateKeyToAccount as privateKeyToAccount6 } from "viem/accounts";
6170
+ import { privateKeyToAccount as privateKeyToAccount7 } from "viem/accounts";
5925
6171
  var DEFAULT_OUTPUT_ROOT2 = "./winning-submission";
5926
- async function main6(env = process.env, argv = process.argv.slice(2)) {
6172
+ async function main7(env = process.env, argv = process.argv.slice(2)) {
5927
6173
  const { apiBaseUrl: apiBaseUrlOverride, argv: remainingArgv } = extractApiBaseUrlFlag(argv);
5928
6174
  const args = readPosterOpenWinningSubmissionArgs(remainingArgv);
5929
6175
  const envWithDefaults = withDeploymentDefaults(env);
@@ -5938,7 +6184,7 @@ async function main6(env = process.env, argv = process.argv.slice(2)) {
5938
6184
  "ELGORA_POSTER_PRIVATE_KEY must be a 32-byte hex private key"
5939
6185
  );
5940
6186
  }
5941
- const account = privateKeyToAccount6(posterPrivateKey.toLowerCase());
6187
+ const account = privateKeyToAccount7(posterPrivateKey.toLowerCase());
5942
6188
  const publicClient = createChainPublicClient(config.chainId, rpcUrl);
5943
6189
  await assertHubDeployment(publicClient, config.chainId, config.hubAddress);
5944
6190
  const ephemeral = await generateX25519KeyPair();
@@ -6062,26 +6308,26 @@ function required3(env, name) {
6062
6308
  }
6063
6309
 
6064
6310
  // src/publish-fund.ts
6065
- import { readFile as readFile2 } from "fs/promises";
6066
- import { text as readStdinText } from "stream/consumers";
6311
+ import { readFile as readFile3 } from "fs/promises";
6312
+ import { text as readStdinText2 } from "stream/consumers";
6067
6313
  import {
6068
6314
  parseAbi,
6069
6315
  parseEventLogs
6070
6316
  } from "viem";
6071
- import { privateKeyToAccount as privateKeyToAccount7 } from "viem/accounts";
6317
+ import { privateKeyToAccount as privateKeyToAccount8 } from "viem/accounts";
6072
6318
  var PUBLISH_FUND_USAGE = "Usage: elgora-cli poster:publish-fund <bounty_challenge_md_path>\n or: elgora-cli poster:publish-fund -";
6073
6319
  var usdcAbi = parseAbi([
6074
6320
  "function approve(address spender,uint256 value) returns (bool)",
6075
6321
  "function balanceOf(address account) view returns (uint256)"
6076
6322
  ]);
6077
- async function main7(env = process.env, argv = process.argv.slice(2)) {
6323
+ async function main8(env = process.env, argv = process.argv.slice(2)) {
6078
6324
  const { apiBaseUrl: apiBaseUrlOverride, argv: remainingArgv } = extractApiBaseUrlFlag(argv);
6079
6325
  const config = loadPublishFundConfig(
6080
6326
  withDeploymentDefaults(env),
6081
6327
  apiBaseUrlOverride
6082
6328
  );
6083
6329
  const bountyChallengeMarkdown = await readBountyChallengeMarkdown(remainingArgv);
6084
- const account = privateKeyToAccount7(config.posterPrivateKey);
6330
+ const account = privateKeyToAccount8(config.posterPrivateKey);
6085
6331
  const publicClient = createChainPublicClient(config.chainId, config.rpcUrl);
6086
6332
  const walletClient = createChainWalletClient(
6087
6333
  config.chainId,
@@ -6238,9 +6484,9 @@ async function readBountyChallengeMarkdown(argv) {
6238
6484
  throw new Error(PUBLISH_FUND_USAGE);
6239
6485
  }
6240
6486
  if (path5 === "-") {
6241
- return readStdinText(process.stdin);
6487
+ return readStdinText2(process.stdin);
6242
6488
  }
6243
- return readFile2(path5, "utf8");
6489
+ return readFile3(path5, "utf8");
6244
6490
  }
6245
6491
  function loadPublishFundConfig(env, apiBaseUrlOverride) {
6246
6492
  const clientConfig = loadClientDeploymentConfig(env, apiBaseUrlOverride);
@@ -6268,11 +6514,311 @@ function loadPublishFundConfig(env, apiBaseUrlOverride) {
6268
6514
  };
6269
6515
  }
6270
6516
 
6517
+ // src/roster-add-guardian.ts
6518
+ import { isAddress as isAddress4 } from "viem";
6519
+ import { privateKeyToAccount as privateKeyToAccount9 } from "viem/accounts";
6520
+ var ROSTER_ADMIN_ADDRESS_FLAG = "--roster-admin-address";
6521
+ var ENCRYPTION_PUBLIC_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
6522
+ async function main9(env = process.env, argv = process.argv.slice(2)) {
6523
+ const args = parseRosterAddGuardianArgs(argv);
6524
+ const config = loadRosterAddGuardianConfig(env, args.rosterAdminAddress);
6525
+ const account = config.rosterAdminPrivateKey ? privateKeyToAccount9(config.rosterAdminPrivateKey) : null;
6526
+ const rosterAdmin = account?.address ?? args.rosterAdminAddress;
6527
+ if (!rosterAdmin) throw new Error("Roster admin address is required");
6528
+ const normalizedRosterAdmin = rosterAdmin.toLowerCase();
6529
+ const publicClient = createChainPublicClient(config.chainId, config.rpcUrl);
6530
+ await assertHubDeployment(publicClient, config.chainId, config.hubAddress);
6531
+ const liveRosterAdmin = await publicClient.readContract({
6532
+ address: config.hubAddress,
6533
+ abi: ELGORA_HUB_ADD_GUARDIAN_ABI,
6534
+ functionName: "guardianRosterAdmin"
6535
+ });
6536
+ if (liveRosterAdmin.toLowerCase() !== normalizedRosterAdmin) {
6537
+ throw new Error(
6538
+ `${normalizedRosterAdmin} is not the live guardianRosterAdmin (${liveRosterAdmin.toLowerCase()})`
6539
+ );
6540
+ }
6541
+ const roster = await publicClient.readContract({
6542
+ address: config.hubAddress,
6543
+ abi: ELGORA_HUB_ADD_GUARDIAN_ABI,
6544
+ functionName: "getGuardianRoster"
6545
+ });
6546
+ if (roster.some(
6547
+ (guardian) => guardian.account.toLowerCase() === args.guardianAccount
6548
+ )) {
6549
+ throw new Error(
6550
+ `${args.guardianAccount} is already on the Guardian roster`
6551
+ );
6552
+ }
6553
+ const contractArgs = [
6554
+ args.name,
6555
+ args.guardianAccount,
6556
+ args.encryptionPublicKey
6557
+ ];
6558
+ if (!account) {
6559
+ await publicClient.simulateContract({
6560
+ account: normalizedRosterAdmin,
6561
+ address: config.hubAddress,
6562
+ abi: ELGORA_HUB_ADD_GUARDIAN_ABI,
6563
+ functionName: "addGuardian",
6564
+ args: contractArgs
6565
+ });
6566
+ const prepared = encodeAddGuardianTransaction({
6567
+ chainId: config.chainId,
6568
+ hubAddress: config.hubAddress,
6569
+ name: args.name,
6570
+ account: args.guardianAccount,
6571
+ encryptionPublicKey: args.encryptionPublicKey
6572
+ });
6573
+ console.log(
6574
+ JSON.stringify({
6575
+ event: "roster_add_guardian.transaction_prepared",
6576
+ ...prepared
6577
+ })
6578
+ );
6579
+ return;
6580
+ }
6581
+ const walletClient = createChainWalletClient(
6582
+ config.chainId,
6583
+ config.rpcUrl,
6584
+ account
6585
+ );
6586
+ const ethBalance = await publicClient.getBalance({
6587
+ address: account.address
6588
+ });
6589
+ if (ethBalance === 0n) {
6590
+ throw new Error(
6591
+ `Roster admin has no gas token balance: ${account.address}`
6592
+ );
6593
+ }
6594
+ await publicClient.simulateContract({
6595
+ account: account.address,
6596
+ address: config.hubAddress,
6597
+ abi: ELGORA_HUB_ADD_GUARDIAN_ABI,
6598
+ functionName: "addGuardian",
6599
+ args: contractArgs
6600
+ });
6601
+ const transactionHash = await walletClient.writeContract({
6602
+ address: config.hubAddress,
6603
+ abi: ELGORA_HUB_ADD_GUARDIAN_ABI,
6604
+ functionName: "addGuardian",
6605
+ args: contractArgs
6606
+ });
6607
+ const receipt = await publicClient.waitForTransactionReceipt({
6608
+ hash: transactionHash
6609
+ });
6610
+ if (receipt.status !== "success") {
6611
+ throw new Error(`addGuardian transaction reverted: ${transactionHash}`);
6612
+ }
6613
+ console.log(
6614
+ JSON.stringify({
6615
+ event: "roster_add_guardian.recorded",
6616
+ tx_hash: transactionHash,
6617
+ block_number: receipt.blockNumber.toString(),
6618
+ name: args.name,
6619
+ account: args.guardianAccount,
6620
+ encryption_public_key: args.encryptionPublicKey
6621
+ })
6622
+ );
6623
+ }
6624
+ function parseRosterAddGuardianArgs(argv) {
6625
+ const usesRosterAdminAddress = argv[0] === ROSTER_ADMIN_ADDRESS_FLAG;
6626
+ const rosterAdminAddress = usesRosterAdminAddress ? argv[1]?.trim() : void 0;
6627
+ if (usesRosterAdminAddress && (!rosterAdminAddress || !isAddress4(rosterAdminAddress))) {
6628
+ throw usageError2();
6629
+ }
6630
+ const positional = usesRosterAdminAddress ? argv.slice(2) : argv;
6631
+ if (positional.length !== 3) throw usageError2();
6632
+ const name = positional[0]?.trim();
6633
+ const guardianAccount = positional[1]?.trim();
6634
+ const encryptionPublicKey = positional[2]?.trim();
6635
+ if (!name) throw usageError2();
6636
+ if (!guardianAccount || !isAddress4(guardianAccount)) {
6637
+ throw new Error("guardian_account must be a valid EVM address");
6638
+ }
6639
+ if (!encryptionPublicKey || !ENCRYPTION_PUBLIC_KEY_RE.test(encryptionPublicKey)) {
6640
+ throw new Error(
6641
+ "encryption_public_key must be an unpadded base64url X25519 public key (43 characters)"
6642
+ );
6643
+ }
6644
+ return {
6645
+ name,
6646
+ guardianAccount: guardianAccount.toLowerCase(),
6647
+ encryptionPublicKey,
6648
+ ...rosterAdminAddress ? { rosterAdminAddress: rosterAdminAddress.toLowerCase() } : {}
6649
+ };
6650
+ }
6651
+ function loadRosterAddGuardianConfig(env, rosterAdminAddress) {
6652
+ const envWithDefaults = withDeploymentDefaults(env);
6653
+ const publicDeploymentConfig = loadPublicDeploymentConfig(envWithDefaults);
6654
+ const rpcUrl = envWithDefaults.ELGORA_RPC_URL?.trim();
6655
+ if (!rpcUrl) {
6656
+ throw new Error("ELGORA_RPC_URL is required");
6657
+ }
6658
+ const rosterAdminPrivateKey = envWithDefaults.ELGORA_GUARDIAN_ROSTER_ADMIN_PRIVATE_KEY?.trim();
6659
+ if (!rosterAdminAddress && (!rosterAdminPrivateKey || !isHexBytes32(rosterAdminPrivateKey))) {
6660
+ throw new Error(
6661
+ "ELGORA_GUARDIAN_ROSTER_ADMIN_PRIVATE_KEY must be a 32-byte hex private key unless --roster-admin-address is provided"
6662
+ );
6663
+ }
6664
+ return {
6665
+ ...publicDeploymentConfig,
6666
+ rpcUrl,
6667
+ rosterAdminPrivateKey: rosterAdminAddress ? null : rosterAdminPrivateKey?.toLowerCase()
6668
+ };
6669
+ }
6670
+ function usageError2() {
6671
+ return new Error(
6672
+ "Usage: elgora-cli roster:add-guardian [--roster-admin-address <address>] <name> <guardian_account> <encryption_public_key>"
6673
+ );
6674
+ }
6675
+
6676
+ // src/roster-prepare-guardian-proof.ts
6677
+ import { isAddress as isAddress5 } from "viem";
6678
+ var ENCRYPTION_PUBLIC_KEY_RE2 = /^[A-Za-z0-9_-]{43}$/;
6679
+ async function main10(env = process.env, argv = process.argv.slice(2)) {
6680
+ const args = parseArgs(argv);
6681
+ const envWithDefaults = withDeploymentDefaults(env);
6682
+ const { chainId, hubAddress } = loadPublicDeploymentConfig(envWithDefaults);
6683
+ const { challenge, fixture } = await prepareGuardianRosterKeyProof({
6684
+ context: {
6685
+ chain_id: chainId,
6686
+ hub_address: hubAddress,
6687
+ proposed_account: args.guardianAccount,
6688
+ proposed_encryption_public_key: args.encryptionPublicKey
6689
+ }
6690
+ });
6691
+ console.log(
6692
+ JSON.stringify({
6693
+ event: "roster_guardian_proof.challenge_prepared",
6694
+ send_to_guardian: challenge,
6695
+ keep_local_do_not_share: {
6696
+ fixture: bytesToBase64Url(fixture),
6697
+ note: "Needed for roster:verify-guardian-proof once the Guardian replies. Never send this to the Guardian \u2014 sending it defeats the proof."
6698
+ }
6699
+ })
6700
+ );
6701
+ }
6702
+ function parseArgs(argv) {
6703
+ if (argv.length !== 2) throw usageError3();
6704
+ const guardianAccount = argv[0]?.trim();
6705
+ const encryptionPublicKey = argv[1]?.trim();
6706
+ if (!guardianAccount || !isAddress5(guardianAccount)) {
6707
+ throw new Error("guardian_account must be a valid EVM address");
6708
+ }
6709
+ if (!encryptionPublicKey || !ENCRYPTION_PUBLIC_KEY_RE2.test(encryptionPublicKey)) {
6710
+ throw new Error(
6711
+ "encryption_public_key must be an unpadded base64url X25519 public key (43 characters)"
6712
+ );
6713
+ }
6714
+ return {
6715
+ guardianAccount: guardianAccount.toLowerCase(),
6716
+ encryptionPublicKey
6717
+ };
6718
+ }
6719
+ function usageError3() {
6720
+ return new Error(
6721
+ "Usage: elgora-cli roster:prepare-guardian-proof <guardian_account> <encryption_public_key>"
6722
+ );
6723
+ }
6724
+
6725
+ // src/roster-verify-guardian-proof.ts
6726
+ import { readFile as readFile4 } from "fs/promises";
6727
+ import { isAddress as isAddress6, isHex as isHex5, recoverMessageAddress } from "viem";
6728
+ async function main11(_env = process.env, argv = process.argv.slice(2)) {
6729
+ const [preparedPath, responsePath] = argv;
6730
+ if (!preparedPath || !responsePath) {
6731
+ throw new Error(
6732
+ "Usage: elgora-cli roster:verify-guardian-proof <prepared_json_path> <response_json_path>"
6733
+ );
6734
+ }
6735
+ const prepared = parsePrepared(await readFile4(preparedPath, "utf8"));
6736
+ const response = parseResponse(await readFile4(responsePath, "utf8"));
6737
+ const fixture = base64UrlToBytes(prepared.fixture);
6738
+ const signedPayload = buildGuardianRosterKeyProofSignedPayload(
6739
+ prepared.message,
6740
+ fixture
6741
+ );
6742
+ const recoveredAddress = (await recoverMessageAddress({
6743
+ message: { raw: signedPayload },
6744
+ signature: response.signature
6745
+ })).toLowerCase();
6746
+ const proposedAccount = prepared.proposedAccount;
6747
+ const succeeded = recoveredAddress === proposedAccount && response.guardianAccount === proposedAccount;
6748
+ console.log(
6749
+ JSON.stringify({
6750
+ event: succeeded ? "roster_guardian_proof.verified" : "roster_guardian_proof.failed",
6751
+ proposed_account: proposedAccount,
6752
+ recovered_signer: recoveredAddress,
6753
+ response_guardian_account: response.guardianAccount,
6754
+ succeeded
6755
+ })
6756
+ );
6757
+ if (!succeeded) {
6758
+ throw new Error(
6759
+ `Guardian roster key proof FAILED for ${proposedAccount}: recovered signer ${recoveredAddress} does not match`
6760
+ );
6761
+ }
6762
+ }
6763
+ function parsePrepared(text) {
6764
+ let parsed;
6765
+ try {
6766
+ parsed = JSON.parse(text);
6767
+ } catch (error) {
6768
+ throw new Error("Prepared proof JSON must be valid JSON", {
6769
+ cause: error
6770
+ });
6771
+ }
6772
+ const sendToGuardian = get(parsed, "send_to_guardian");
6773
+ const message = get(sendToGuardian, "message");
6774
+ const context = get(sendToGuardian, "context");
6775
+ const proposedAccount = get(context, "proposed_account");
6776
+ const keepLocal = get(parsed, "keep_local_do_not_share");
6777
+ const fixture = get(keepLocal, "fixture");
6778
+ if (typeof message !== "string" || typeof proposedAccount !== "string" || !isAddress6(proposedAccount) || typeof fixture !== "string") {
6779
+ throw new Error(
6780
+ "Prepared proof JSON must be the unmodified stdout of roster:prepare-guardian-proof"
6781
+ );
6782
+ }
6783
+ return {
6784
+ message,
6785
+ fixture,
6786
+ proposedAccount: proposedAccount.toLowerCase()
6787
+ };
6788
+ }
6789
+ function parseResponse(text) {
6790
+ let parsed;
6791
+ try {
6792
+ parsed = JSON.parse(text);
6793
+ } catch (error) {
6794
+ throw new Error("Guardian response JSON must be valid JSON", {
6795
+ cause: error
6796
+ });
6797
+ }
6798
+ const guardianAccount = get(parsed, "guardian_account");
6799
+ const signature = get(parsed, "signature");
6800
+ if (typeof guardianAccount !== "string" || !isAddress6(guardianAccount) || typeof signature !== "string" || !isHex5(signature) || signature.length !== 132) {
6801
+ throw new Error(
6802
+ "Guardian response JSON must be the unmodified stdout of guardian:prove-roster-key"
6803
+ );
6804
+ }
6805
+ return {
6806
+ guardianAccount: guardianAccount.toLowerCase(),
6807
+ signature
6808
+ };
6809
+ }
6810
+ function get(value, key) {
6811
+ if (!value || typeof value !== "object" || !(key in value)) {
6812
+ return void 0;
6813
+ }
6814
+ return value[key];
6815
+ }
6816
+
6271
6817
  // src/spec-commitment.ts
6272
- import { readFile as readFile3 } from "fs/promises";
6273
- import { text as readStdinText2 } from "stream/consumers";
6818
+ import { readFile as readFile5 } from "fs/promises";
6819
+ import { text as readStdinText3 } from "stream/consumers";
6274
6820
  var SPEC_COMMITMENT_USAGE = "Usage: elgora-cli spec-commitment <bounty_challenge_md_path>\n or: elgora-cli spec-commitment -";
6275
- async function main8(_env = process.env, argv = process.argv.slice(2)) {
6821
+ async function main12(_env = process.env, argv = process.argv.slice(2)) {
6276
6822
  const markdown = await readBountyChallengeMarkdown2(argv);
6277
6823
  const specCommitment = deriveBountyChallengeContentCommitment(markdown);
6278
6824
  console.log(
@@ -6289,25 +6835,25 @@ async function readBountyChallengeMarkdown2(argv) {
6289
6835
  throw new Error(SPEC_COMMITMENT_USAGE);
6290
6836
  }
6291
6837
  if (path5 === "-") {
6292
- return readStdinText2(process.stdin);
6838
+ return readStdinText3(process.stdin);
6293
6839
  }
6294
- return readFile3(path5, "utf8");
6840
+ return readFile5(path5, "utf8");
6295
6841
  }
6296
6842
 
6297
6843
  // src/submit.ts
6298
- import { readdir, readFile as readFile4, stat } from "fs/promises";
6844
+ import { readdir, readFile as readFile6, stat } from "fs/promises";
6299
6845
  import path3 from "path";
6300
6846
  import { createInterface } from "readline";
6301
- import { isAddress as isAddress4 } from "viem";
6302
- import { privateKeyToAccount as privateKeyToAccount8 } from "viem/accounts";
6847
+ import { isAddress as isAddress7 } from "viem";
6848
+ import { privateKeyToAccount as privateKeyToAccount10 } from "viem/accounts";
6303
6849
 
6304
6850
  // src/external-api-approval-signer.ts
6305
- import { isHex as isHex5 } from "viem";
6851
+ import { isHex as isHex6 } from "viem";
6306
6852
  function createExternalApiApprovalSigner(input) {
6307
6853
  return async (typedData) => {
6308
6854
  input.writePrompt(input.renderPrompt(typedData));
6309
6855
  const signature = (await input.readSignature())?.trim();
6310
- if (!signature || signature === "0x" || !isHex5(signature)) {
6856
+ if (!signature || signature === "0x" || !isHex6(signature)) {
6311
6857
  throw new Error(
6312
6858
  "External signer must return one non-empty 0x-prefixed hex signature line on stdin"
6313
6859
  );
@@ -6349,13 +6895,13 @@ var MEDIA_TYPE_BY_EXTENSION = {
6349
6895
  diff: "text/x-diff"
6350
6896
  };
6351
6897
  var DEFAULT_MEDIA_TYPE = "application/octet-stream";
6352
- async function main9(env = process.env, argv = process.argv.slice(2)) {
6898
+ async function main13(env = process.env, argv = process.argv.slice(2)) {
6353
6899
  const { solverAddress, argv: argvWithoutSolverAddress } = extractSolverAddressFlag(argv);
6354
6900
  const { apiBaseUrl: apiBaseUrlOverride, argv: remainingArgv } = extractApiBaseUrlFlag(argvWithoutSolverAddress);
6355
6901
  const config = loadSubmitConfig(env, apiBaseUrlOverride, solverAddress);
6356
6902
  const args = readSubmitArgs(remainingArgv);
6357
6903
  const bountyIdText = args.bountyId.toString();
6358
- const account = config.solverPrivateKey ? privateKeyToAccount8(config.solverPrivateKey) : null;
6904
+ const account = config.solverPrivateKey ? privateKeyToAccount10(config.solverPrivateKey) : null;
6359
6905
  const solverAddressFromConfig = account?.address ?? config.solverAddress;
6360
6906
  if (!solverAddressFromConfig) throw new Error("Solver address is required");
6361
6907
  const solver = solverAddressFromConfig.toLowerCase();
@@ -6694,7 +7240,7 @@ function extractSolverAddressFlag(argv) {
6694
7240
  }
6695
7241
  if (arg !== void 0) remaining.push(arg);
6696
7242
  }
6697
- if (solverAddress !== void 0 && !isAddress4(solverAddress)) {
7243
+ if (solverAddress !== void 0 && !isAddress7(solverAddress)) {
6698
7244
  throw new Error("--solver-address must be a valid EVM address");
6699
7245
  }
6700
7246
  return {
@@ -6724,7 +7270,7 @@ async function readArtifactDirectory(artifactDir) {
6724
7270
  }
6725
7271
  return Promise.all(
6726
7272
  entries.map(async (entry) => {
6727
- const bytes = await readFile4(path3.join(artifactDir, entry.name));
7273
+ const bytes = await readFile6(path3.join(artifactDir, entry.name));
6728
7274
  return {
6729
7275
  name: entry.name,
6730
7276
  media_type: guessMediaType(entry.name),
@@ -6834,7 +7380,7 @@ function readTimelockRevealDelaySeconds(env) {
6834
7380
 
6835
7381
  // src/verification-record.ts
6836
7382
  var VERIFICATION_RECORD_USAGE = "Usage: elgora-cli verification-record <bounty_id>";
6837
- async function main10(env = process.env, argv = process.argv.slice(2)) {
7383
+ async function main14(env = process.env, argv = process.argv.slice(2)) {
6838
7384
  const { apiBaseUrl: apiBaseUrlOverride, argv: remainingArgv } = extractApiBaseUrlFlag(argv);
6839
7385
  const args = readVerificationRecordArgs(remainingArgv);
6840
7386
  const envWithDefaults = withDeploymentDefaults(env);
@@ -6891,11 +7437,11 @@ function readVerificationRecordArgs(argv) {
6891
7437
  const [bountyIdText] = argv;
6892
7438
  const bountyId = bountyIdText?.trim();
6893
7439
  if (argv.length !== 1 || !bountyId || !isBountyId(bountyId)) {
6894
- throw usageError2();
7440
+ throw usageError4();
6895
7441
  }
6896
7442
  return { bountyId: BigInt(bountyId) };
6897
7443
  }
6898
- function usageError2() {
7444
+ function usageError4() {
6899
7445
  return new Error(VERIFICATION_RECORD_USAGE);
6900
7446
  }
6901
7447
 
@@ -6912,14 +7458,18 @@ process.stderr.write(
6912
7458
  var COMMANDS = {
6913
7459
  "guardian:judgeable": main4,
6914
7460
  "guardian:open": main2,
6915
- "guardian:verdict": main5,
7461
+ "guardian:prove-roster-key": main5,
7462
+ "guardian:verdict": main6,
6916
7463
  "guardian:deliver-key": main3,
6917
- "poster:open-winning-submission": main6,
6918
- "poster:publish-fund": main7,
6919
- "solver:submit": main9,
7464
+ "poster:open-winning-submission": main7,
7465
+ "poster:publish-fund": main8,
7466
+ "roster:add-guardian": main9,
7467
+ "roster:prepare-guardian-proof": main10,
7468
+ "roster:verify-guardian-proof": main11,
7469
+ "solver:submit": main13,
6920
7470
  claim: main,
6921
- "spec-commitment": main8,
6922
- "verification-record": main10
7471
+ "spec-commitment": main12,
7472
+ "verification-record": main14
6923
7473
  };
6924
7474
  var COMMAND_HELP = {
6925
7475
  "guardian:judgeable": {
@@ -6949,6 +7499,16 @@ var COMMAND_HELP = {
6949
7499
  ],
6950
7500
  apiBaseUrlFlag: true
6951
7501
  },
7502
+ "guardian:prove-roster-key": {
7503
+ usage: "elgora-cli guardian:prove-roster-key <challenge_json_path|->",
7504
+ title: "Prove control of a proposed roster account and HPKE key.",
7505
+ description: "Run asynchronously, independent of anything on chain: open the sealed\n fixture from roster:prepare-guardian-proof's send_to_guardian output\n (pass the whole prepared JSON, or just its send_to_guardian object;\n '-' reads stdin), then EIP-191 personal_sign proof that this account\n recovered it. Refuses to sign if the challenge names a different\n chain, Hub, or proposed_account than this key resolves to. Send the\n printed response back to the roster admin for\n roster:verify-guardian-proof. This is advisory only \u2014 it is never\n submitted to ElgoraHub and enforces nothing there.",
7506
+ required: [
7507
+ "ELGORA_GUARDIAN_ACCOUNT_PRIVATE_KEY",
7508
+ "ELGORA_GUARDIAN_PRIVATE_KEYS_JSON"
7509
+ ],
7510
+ defaulted: ["ELGORA_CHAIN_ID", "ELGORA_HUB_ADDRESS"]
7511
+ },
6952
7512
  "guardian:verdict": {
6953
7513
  usage: "elgora-cli guardian:verdict <bounty_id> awarded <solver> <submission_commitment> <verdict_body_path>\n or: elgora-cli guardian:verdict <bounty_id> no_valid_submission <verdict_body_path>",
6954
7514
  title: "Publish and record this Guardian's Verdict.",
@@ -6964,8 +7524,8 @@ var COMMAND_HELP = {
6964
7524
  },
6965
7525
  "guardian:deliver-key": {
6966
7526
  usage: "elgora-cli guardian:deliver-key <bounty_id> <solver> <submission_commitment>",
6967
- title: "Repair delivery for one exact Submission.",
6968
- description: "Ask the API to wrap the key for the explicit Solver and Submission.\n ElgoraHub decides whether the pair is still active or is the finalized\n winner; written Verdicts and local state do not authorize delivery.",
7527
+ title: "Deliver the finalized winner's content key for Poster retrieval.",
7528
+ description: "Required after a bounty finalizes `awarded`: wrap the winning\n Submission's key so the funding Poster can retrieve the work. Read the\n winning Solver and Submission from ElgoraHub's finalized state (the\n subgraph), not from a local Verdict, and pass exactly that pair. The\n API rechecks against ElgoraHub and accepts only the current active or\n finalized winner; written Verdicts and local state authorize nothing.\n Idempotent \u2014 safe to re-run and safe if another Guardian already did it.",
6969
7529
  required: [
6970
7530
  "ELGORA_GUARDIAN_ACCOUNT_PRIVATE_KEY",
6971
7531
  "ELGORA_GUARDIAN_PRIVATE_KEYS_JSON"
@@ -7005,6 +7565,26 @@ var COMMAND_HELP = {
7005
7565
  ],
7006
7566
  apiBaseUrlFlag: true
7007
7567
  },
7568
+ "roster:add-guardian": {
7569
+ usage: "elgora-cli roster:add-guardian [--roster-admin-address <address>]\n <name> <guardian_account> <encryption_public_key>",
7570
+ title: "Append one Guardian to the live roster.",
7571
+ description: "Call ElgoraHub's guardianRosterAdmin-only addGuardian directly. Only\n future bounties see the new roster; open bounties keep judging\n against the hash pinned when they were created. With\n --roster-admin-address, the transaction is simulated and printed\n unsigned (to, data, value) for an external signer such as a Safe,\n instead of being sent. Run roster:prepare-guardian-proof,\n guardian:prove-roster-key, and roster:verify-guardian-proof against\n the proposed Guardian before ever calling this.",
7572
+ required: [],
7573
+ defaulted: ["ELGORA_CHAIN_ID", "ELGORA_HUB_ADDRESS", "ELGORA_RPC_URL"]
7574
+ },
7575
+ "roster:prepare-guardian-proof": {
7576
+ usage: "elgora-cli roster:prepare-guardian-proof <guardian_account> <encryption_public_key>",
7577
+ title: "Start the key-custody proof for a proposed Guardian.",
7578
+ description: "Picks a fresh random secret, seals it only to the proposed HPKE\n public key, and builds the message the Guardian must sign alongside\n the recovered bytes. Send only the printed send_to_guardian object to\n the Guardian, over whatever channel you already trust them on \u2014 never\n keep_local_do_not_share; that half stays with the roster admin and\n feeds roster:verify-guardian-proof once the Guardian responds with\n guardian:prove-roster-key. Pure local computation: no chain read, no\n wallet, no network call, nothing enforced on ElgoraHub.",
7579
+ required: [],
7580
+ defaulted: ["ELGORA_CHAIN_ID", "ELGORA_HUB_ADDRESS"]
7581
+ },
7582
+ "roster:verify-guardian-proof": {
7583
+ usage: "elgora-cli roster:verify-guardian-proof <prepared_json_path> <response_json_path>",
7584
+ title: "Check a Guardian's key-custody proof response.",
7585
+ description: "Takes the unmodified stdout of roster:prepare-guardian-proof and of\n the Guardian's guardian:prove-roster-key, reconstructs the signed\n payload from the fixture the admin generated (never sent to the\n Guardian), and checks the recovered signer is the exact proposed\n account. Exits non-zero on any mismatch. Advisory only \u2014 convinces\n the admin, proves nothing to ElgoraHub, and does not itself add the\n Guardian; run roster:add-guardian separately once satisfied.",
7586
+ required: []
7587
+ },
7008
7588
  "solver:submit": {
7009
7589
  usage: "elgora-cli solver:submit [--api-base-url <url>] [--solver-address <address>] <bounty_id> <artifact_dir>",
7010
7590
  title: "Encrypt and submit a Solver's private artifacts.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elgora/cli",
3
- "version": "7.2.0",
3
+ "version": "7.2.2",
4
4
  "description": "Command-line client for Elgora, a decentralized bounty marketplace for science. Lets Posters, Solvers, and Guardians interact with Elgora's public API and on-chain contracts.",
5
5
  "type": "module",
6
6
  "license": "MIT",