nightpay 0.1.2 → 0.2.1

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.

Potentially problematic release.


This version of nightpay might be problematic. Click here for more details.

@@ -13,22 +13,34 @@
13
13
  # Usage: ./gateway.sh <command> [args...]
14
14
  #
15
15
  # Commands:
16
- # post-bounty <job_description> <amount_night_specks>
17
- # find-agent <capability_query>
18
- # hire-and-pay <agent_id> <job_description> <commitment_hash>
19
- # check-job <job_id>
20
- # complete <job_id> <commitment_hash>
21
- # refund <job_id> <commitment_hash>
22
- # withdraw-fees [amount_specks] # operator-only: requires OPERATOR_SECRET_KEY
23
- # stats # public contract stats
16
+ # post-bounty <job_description> <amount_night_specks>
17
+ # find-agent <capability_query>
18
+ # hire-and-pay <agent_id> <job_description> <commitment_hash>
19
+ # check-job <job_id>
20
+ # complete <job_id> <commitment_hash> [--approvals sig1:ts1:nonce1,...]
21
+ # refund <job_id> <commitment_hash>
22
+ # withdraw-fees [amount_specks] # operator-only: requires OPERATOR_SECRET_KEY
23
+ # stats # public contract stats
24
+ # approve-multisig <job_id> <output_hash> <approver_key> # per-approver signature
25
+ # optimistic-sweep [--dry-run] # auto-complete expired optimistic windows
24
26
 
25
27
  set -euo pipefail
26
28
 
29
+ # ─── Terminal colors ───────────────────────────────────────────────────────────
30
+ # Gracefully disabled when stderr is not a TTY (CI, logs, pipes)
31
+ if [[ -t 2 ]]; then
32
+ RED=$'\e[31m'; GREEN=$'\e[32m'; YELLOW=$'\e[33m'
33
+ CYAN=$'\e[36m'; BOLD=$'\e[1m'; DIM=$'\e[2m'; RESET=$'\e[0m'
34
+ else
35
+ RED=''; GREEN=''; YELLOW=''; CYAN=''; BOLD=''; DIM=''; RESET=''
36
+ fi
37
+
27
38
  # ─── Required env vars ────────────────────────────────────────────────────────
28
39
  MASUMI_PAYMENT_URL="${MASUMI_PAYMENT_URL:-http://localhost:3001/api/v1}"
29
40
  MASUMI_REGISTRY_URL="${MASUMI_REGISTRY_URL:-http://localhost:3000/api/v1}"
30
41
  MASUMI_API_KEY="${MASUMI_API_KEY:?SECURITY: Set MASUMI_API_KEY}"
31
- MIDNIGHT_NETWORK="${MIDNIGHT_NETWORK:-testnet}"
42
+ # Keep preprod default until Midnight mainnet is live.
43
+ MIDNIGHT_NETWORK="${MIDNIGHT_NETWORK:-preprod}"
32
44
  OPERATOR_ADDRESS="${OPERATOR_ADDRESS:?SECURITY: Set OPERATOR_ADDRESS}"
33
45
  OPERATOR_FEE_BPS="${OPERATOR_FEE_BPS:-200}"
34
46
  MAX_BOUNTY_SPECKS="${MAX_BOUNTY_SPECKS:-500000000}"
@@ -100,7 +112,7 @@ for url in sys.argv[1:]:
100
112
  print(f'ERROR: {url} — must be http/https'); sys.exit(1)
101
113
  print('ok')
102
114
  " "$MASUMI_PAYMENT_URL" "$MASUMI_REGISTRY_URL" 2>&1) || {
103
- echo "SECURITY ERROR: Invalid Masumi URL — $_url_check" >&2; exit 1
115
+ echo -e "${RED}SECURITY ERROR${RESET}: Invalid Masumi URL — $_url_check" >&2; exit 1
104
116
  }
105
117
  fi
106
118
 
@@ -125,10 +137,12 @@ try:
125
137
  print(f'SSRF blocked: {ip}', file=sys.stderr); sys.exit(1)
126
138
  except socket.gaierror as e:
127
139
  print(f'DNS error: {e}', file=sys.stderr); sys.exit(1)
128
- " "$url" || { echo "SECURITY ERROR: SSRF guard blocked request to $url" >&2; exit 1; }
140
+ " "$url" || { echo -e "${RED}SECURITY ERROR${RESET}: SSRF guard blocked request to $url" >&2; exit 1; }
129
141
  curl -sf --max-time 30 "$@" "$url"
130
142
  }
131
143
 
144
+ # ─── SSRF error (colored) — used by _ssrf_safe_curl ───────────────────────────
145
+
132
146
  masumi_get() {
133
147
  _ssrf_safe_curl "${MASUMI_REGISTRY_URL}${1}" \
134
148
  -H "Authorization: Bearer $MASUMI_API_KEY"
@@ -162,7 +176,7 @@ rate_limit() {
162
176
  local now; now=$(date +%s)
163
177
  local diff=$(( now - last_ts ))
164
178
  if (( diff < RATE_LIMIT_SECONDS )); then
165
- echo "ERROR: Rate limit — wait $(( RATE_LIMIT_SECONDS - diff ))s before calling $cmd again" >&2
179
+ echo -e "${RED}ERROR${RESET}: Rate limit — wait ${BOLD}$(( RATE_LIMIT_SECONDS - diff ))s${RESET} before calling ${CYAN}$cmd${RESET} again" >&2
166
180
  exit 1
167
181
  fi
168
182
  fi
@@ -244,8 +258,8 @@ validate_job_id() {
244
258
  # The same HMAC can never be reused because the nonce is different every call.
245
259
  require_operator_auth() {
246
260
  if [[ -z "${OPERATOR_SECRET_KEY:-}" ]]; then
247
- echo "SECURITY ERROR: withdraw-fees requires OPERATOR_SECRET_KEY env var." >&2
248
- echo "This prevents unauthorized parties from draining accumulated fees." >&2
261
+ echo -e "${RED}SECURITY ERROR${RESET}: withdraw-fees requires ${BOLD}OPERATOR_SECRET_KEY${RESET} env var." >&2
262
+ echo -e "${DIM}This prevents unauthorized parties from draining accumulated fees.${RESET}" >&2
249
263
  exit 1
250
264
  fi
251
265
  local payload="$1"
@@ -358,6 +372,17 @@ print(json.dumps({
358
372
  fi
359
373
  }
360
374
 
375
+ # ─── Optimistic delivery & multisig env vars ──────────────────────────────────
376
+ OPTIMISTIC_WINDOW_HOURS="${OPTIMISTIC_WINDOW_HOURS:-48}"
377
+ MULTISIG_THRESHOLD_SPECKS="${MULTISIG_THRESHOLD_SPECKS:-1000000}"
378
+ MULTISIG_M="${MULTISIG_M:-2}"
379
+ MULTISIG_N="${MULTISIG_N:-3}"
380
+ OPTIMISTIC_SWEEP_PAGE_SIZE="${OPTIMISTIC_SWEEP_PAGE_SIZE:-200}" # capped to <= 500
381
+ # APPROVER_KEYS: comma-separated HMAC secrets, one per approver
382
+ # e.g. APPROVER_KEYS="key1secret,key2secret,key3secret"
383
+ APPROVER_KEYS="${APPROVER_KEYS:-}"
384
+ MIP003_PORT="${MIP003_PORT:-8090}"
385
+
361
386
  # ─── Commands ─────────────────────────────────────────────────────────────────
362
387
 
363
388
  case "$COMMAND" in
@@ -387,8 +412,8 @@ print(json.dumps({'jobHash': sys.argv[1], 'amount': int(sys.argv[2]), 'nonce': s
387
412
  BRIDGE_RESULT=$(bridge_post "/postBounty" "$BRIDGE_PAYLOAD" 2>/dev/null) && {
388
413
  TX_ID=$(echo "$BRIDGE_RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('txId',''))" 2>/dev/null)
389
414
  ON_CHAIN=$(echo "$BRIDGE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print('false' if d.get('stub') else 'true')" 2>/dev/null)
390
- echo " Midnight TX: $TX_ID (on-chain: $ON_CHAIN)" >&2
391
- } || echo " WARNING: Bridge unavailable — commitment computed locally only" >&2
415
+ echo -e " ${GREEN}Midnight TX${RESET}: ${DIM}$TX_ID${RESET} ${CYAN}(on-chain: $ON_CHAIN)${RESET}" >&2
416
+ } || echo -e " ${YELLOW}WARNING${RESET}: Bridge unavailable — commitment computed locally only" >&2
392
417
  fi
393
418
 
394
419
  # SECURITY: nonce printed once for the caller to store securely.
@@ -447,13 +472,129 @@ print(json.dumps({
447
472
  masumi_get "/purchases/$JOB_ID/status"
448
473
  ;;
449
474
 
475
+ approve-multisig)
476
+ # Each approver runs this with their own key.
477
+ # Collect M blobs and pass all sigs to: gateway.sh complete <id> <commit> --approvals sig1:ts1:nonce1,...
478
+ JOB_ID="${1:?Usage: approve-multisig <job_id> <output_hash> <approver_key>}"
479
+ OUTPUT_HASH="${2:?Usage: approve-multisig <job_id> <output_hash> <approver_key>}"
480
+ APPROVER_KEY="${3:?Usage: approve-multisig <job_id> <output_hash> <approver_key>}"
481
+
482
+ validate_job_id "$JOB_ID"
483
+ validate_commitment "$OUTPUT_HASH" # reuse 64-hex validator
484
+
485
+ TS=$(date +%s)
486
+ NONCE=$(generate_nonce)
487
+ # SECURITY: timestamp + nonce in payload — each approval is unique, stale replays rejected by complete
488
+ SIG_PAYLOAD="${JOB_ID}:${OUTPUT_HASH}:${TS}:${NONCE}"
489
+ SIG=$(echo -n "$SIG_PAYLOAD" | openssl dgst -sha256 -hmac "$APPROVER_KEY" | awk '{print $2}')
490
+
491
+ python3 -c "
492
+ import sys, json
493
+ print(json.dumps({
494
+ 'job_id': sys.argv[1],
495
+ 'output_hash': sys.argv[2],
496
+ 'sig': sys.argv[3],
497
+ 'ts': int(sys.argv[4]),
498
+ 'nonce': sys.argv[5],
499
+ 'approval_blob': f'{sys.argv[3]}:{sys.argv[4]}:{sys.argv[5]}',
500
+ 'note': 'Pass all collected approval_blobs to: gateway.sh complete <job_id> <commitment> --approvals blob1,blob2'
501
+ }, indent=2))
502
+ " "$JOB_ID" "$OUTPUT_HASH" "$SIG" "$TS" "$NONCE"
503
+ ;;
504
+
450
505
  complete)
451
- JOB_ID="${1:?Usage: complete <job_id> <commitment_hash>}"
506
+ JOB_ID="${1:?Usage: complete <job_id> <commitment_hash> [--approvals sig1:ts1:nonce1,...]}"
452
507
  COMMITMENT="${2:?Usage: complete <job_id> <commitment_hash>}"
508
+ # Optional: $3 = "--approvals", $4 = comma-separated sig:ts:nonce blobs
509
+ APPROVALS_FLAG="${3:-}"
510
+ APPROVALS_RAW="${4:-}"
453
511
 
454
512
  validate_job_id "$JOB_ID" # SECURITY: prevent path traversal
455
513
  validate_commitment "$COMMITMENT"
456
514
 
515
+ # ── Multisig verification (for high-value bounties) ────────────────────
516
+ # Query the MIP-003 server for job amount to decide if multisig required
517
+ JOB_AMOUNT=0
518
+ if command -v curl >/dev/null 2>&1; then
519
+ _JOB_INFO=$(curl -sf --max-time 5 "http://localhost:${MIP003_PORT}/status/${JOB_ID}" 2>/dev/null || echo '{}')
520
+ JOB_AMOUNT=$(echo "$_JOB_INFO" | python3 -c "
521
+ import sys, json
522
+ try:
523
+ d = json.load(sys.stdin)
524
+ print(d.get('amount_specks') or 0)
525
+ except: print(0)
526
+ " 2>/dev/null || echo 0)
527
+ fi
528
+
529
+ if (( JOB_AMOUNT >= MULTISIG_THRESHOLD_SPECKS )); then
530
+ if [[ -z "$APPROVALS_RAW" || "$APPROVALS_FLAG" != "--approvals" ]]; then
531
+ echo -e "${RED}ERROR${RESET}: job amount ${BOLD}${JOB_AMOUNT} specks${RESET} >= threshold ${MULTISIG_THRESHOLD_SPECKS}" >&2
532
+ echo -e "${YELLOW}Multisig required.${RESET} Each approver runs:" >&2
533
+ echo -e " ${CYAN}gateway.sh approve-multisig${RESET} $JOB_ID <output_hash> <approver_key>" >&2
534
+ echo -e "Then collect M approval_blobs and run:" >&2
535
+ echo -e " ${CYAN}gateway.sh complete${RESET} $JOB_ID $COMMITMENT --approvals blob1,blob2" >&2
536
+ exit 1
537
+ fi
538
+
539
+ # Verify M-of-N approvals using Python stdlib only (no new deps)
540
+ VERIFY_OK=$(python3 -c "
541
+ import sys, json, hmac, hashlib, time
542
+
543
+ job_id = sys.argv[1]
544
+ output_hash = sys.argv[2]
545
+ approvals_raw = sys.argv[3]
546
+ approver_keys = [k for k in sys.argv[4].split(',') if k] if sys.argv[4] else []
547
+ required_m = int(sys.argv[5])
548
+ max_age_secs = 86400 # approvals expire after 24h — prevents replay attacks
549
+
550
+ # Parse: each entry is sig:ts:nonce
551
+ approvals = []
552
+ for entry in approvals_raw.split(','):
553
+ parts = entry.split(':')
554
+ if len(parts) != 3:
555
+ print(f'ERROR: malformed approval blob (expected sig:ts:nonce): {entry}', file=sys.stderr)
556
+ sys.exit(1)
557
+ try:
558
+ approvals.append({'sig': parts[0], 'ts': int(parts[1]), 'nonce': parts[2]})
559
+ except ValueError:
560
+ print(f'ERROR: non-integer timestamp in approval: {entry}', file=sys.stderr)
561
+ sys.exit(1)
562
+
563
+ now = int(time.time())
564
+ valid_count = 0
565
+ used_keys = set() # SECURITY: each key index counts once — no double-counting
566
+
567
+ for approval in approvals:
568
+ age = now - approval['ts']
569
+ if age > max_age_secs:
570
+ print(f'WARN: approval ts={approval[\"ts\"]} is too old (age={age}s > {max_age_secs}s)', file=sys.stderr)
571
+ continue
572
+ if age < -300: # 5-min future clock skew tolerance
573
+ print(f'WARN: approval ts={approval[\"ts\"]} is too far in future (age={age}s)', file=sys.stderr)
574
+ continue
575
+
576
+ payload = f'{job_id}:{output_hash}:{approval[\"ts\"]}:{approval[\"nonce\"]}'
577
+ for i, key in enumerate(approver_keys):
578
+ if i in used_keys:
579
+ continue
580
+ expected = hmac.new(key.encode(), payload.encode(), hashlib.sha256).hexdigest()
581
+ if hmac.compare_digest(expected, approval['sig']):
582
+ used_keys.add(i)
583
+ valid_count += 1
584
+ break
585
+
586
+ if valid_count >= required_m:
587
+ print(f'ok:{valid_count}')
588
+ else:
589
+ print(f'ERROR: only {valid_count} valid approvals, need {required_m}', file=sys.stderr)
590
+ sys.exit(1)
591
+ " "$JOB_ID" "$COMMITMENT" "$APPROVALS_RAW" "$APPROVER_KEYS" "$MULTISIG_M" 2>&1) || {
592
+ echo -e "${RED}SECURITY ERROR${RESET}: multisig verification failed — $VERIFY_OK" >&2
593
+ exit 1
594
+ }
595
+ echo -e " ${GREEN}Multisig${RESET}: $VERIFY_OK approvals verified" >&2
596
+ fi
597
+
457
598
  RESULT_DATA=$(masumi_get "/purchases/$JOB_ID/result")
458
599
 
459
600
  # SECURITY: canonical JSON (sorted keys, no whitespace) prevents hash
@@ -481,8 +622,27 @@ print(json.dumps({'bountyCommitment': sys.argv[1], 'outputHash': sys.argv[2]}))
481
622
  BRIDGE_RESULT=$(bridge_post "/completeAndReceipt" "$BRIDGE_PAYLOAD" 2>/dev/null) && {
482
623
  BRIDGE_TX_ID=$(echo "$BRIDGE_RESULT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('txId',''))" 2>/dev/null)
483
624
  BRIDGE_ON_CHAIN=$(echo "$BRIDGE_RESULT" | python3 -c "import sys,json; d=json.load(sys.stdin); print('false' if d.get('stub') else 'true')" 2>/dev/null)
484
- echo " Midnight TX: $BRIDGE_TX_ID (on-chain: $BRIDGE_ON_CHAIN)" >&2
485
- } || echo " WARNING: Bridge unavailable — receipt computed locally only" >&2
625
+ echo -e " ${GREEN}Midnight TX${RESET}: ${DIM}$BRIDGE_TX_ID${RESET} ${CYAN}(on-chain: $BRIDGE_ON_CHAIN)${RESET}" >&2
626
+ } || echo -e " ${YELLOW}WARNING${RESET}: Bridge unavailable — receipt computed locally only" >&2
627
+ fi
628
+
629
+ # Fetch economics from MIP-003 for the cost footer (ClawWork pattern)
630
+ _ECON_AMOUNT=0
631
+ _ECON_FEE=0
632
+ _ECON_NET=0
633
+ if command -v curl >/dev/null 2>&1; then
634
+ _ECON_INFO=$(curl -sf --max-time 3 "http://localhost:${MIP003_PORT}/status/${JOB_ID}" 2>/dev/null || echo '{}')
635
+ read -r _ECON_AMOUNT _ECON_FEE _ECON_NET <<< "$(python3 -c "
636
+ import sys, json
637
+ try:
638
+ d = json.load(sys.stdin)
639
+ amount = int(d.get('amount_specks') or 0)
640
+ fee_bps = int('${OPERATOR_FEE_BPS}')
641
+ fee = amount * fee_bps // 10000
642
+ net = amount - fee
643
+ print(amount, fee, net)
644
+ except: print(0, 0, 0)
645
+ " <<< "$_ECON_INFO" 2>/dev/null || echo "0 0 0")"
486
646
  fi
487
647
 
488
648
  python3 -c "
@@ -496,9 +656,16 @@ print(json.dumps({
496
656
  'midnightNetwork': sys.argv[5],
497
657
  'receiptContract': sys.argv[6],
498
658
  'midnightTxId': sys.argv[7] or None,
499
- 'onChain': sys.argv[8] == 'true'
659
+ 'onChain': sys.argv[8] == 'true',
660
+ # Economics footer — ClawWork-compatible cost accounting shape
661
+ 'economics': {
662
+ 'amountSpecks': int(sys.argv[9]),
663
+ 'fee': int(sys.argv[10]),
664
+ 'netToAgent': int(sys.argv[11]),
665
+ 'feeBps': int(sys.argv[12]),
666
+ },
500
667
  }, indent=2))
501
- " "$RECEIPT_HASH" "$OUTPUT_HASH" "$COMMITMENT" "$COMPLETION_NONCE" "$MIDNIGHT_NETWORK" "$RECEIPT_CONTRACT" "$BRIDGE_TX_ID" "$BRIDGE_ON_CHAIN"
668
+ " "$RECEIPT_HASH" "$OUTPUT_HASH" "$COMMITMENT" "$COMPLETION_NONCE" "$MIDNIGHT_NETWORK" "$RECEIPT_CONTRACT" "$BRIDGE_TX_ID" "$BRIDGE_ON_CHAIN" "$_ECON_AMOUNT" "$_ECON_FEE" "$_ECON_NET" "$OPERATOR_FEE_BPS"
502
669
  ;;
503
670
 
504
671
  refund)
@@ -509,7 +676,7 @@ print(json.dumps({
509
676
  validate_commitment "$COMMITMENT"
510
677
 
511
678
  # Step 1: Cancel Masumi escrow on Cardano
512
- echo "Cancelling Masumi escrow for job $JOB_ID..." >&2
679
+ echo -e "${CYAN}Cancelling Masumi escrow${RESET} for job ${BOLD}$JOB_ID${RESET}..." >&2
513
680
  masumi_post "/purchases/$JOB_ID/cancel" "{}"
514
681
 
515
682
  # Step 2: Emit on-chain NIGHT refund intent for the Midnight contract.
@@ -555,10 +722,10 @@ print(json.dumps({
555
722
  ;;
556
723
 
557
724
  stats)
558
- echo "Querying nightpay stats from $RECEIPT_CONTRACT on $MIDNIGHT_NETWORK..." >&2
725
+ echo -e "${CYAN}Querying nightpay stats${RESET} from ${DIM}$RECEIPT_CONTRACT${RESET} on ${BOLD}$MIDNIGHT_NETWORK${RESET}..." >&2
559
726
  if [[ -n "$BRIDGE_URL" ]]; then
560
727
  bridge_get "/stats" && exit 0
561
- echo " WARNING: Bridge unavailable — showing placeholder" >&2
728
+ echo -e " ${YELLOW}WARNING${RESET}: Bridge unavailable — showing placeholder" >&2
562
729
  fi
563
730
  python3 -c "
564
731
  import sys, json
@@ -571,8 +738,98 @@ print(json.dumps({
571
738
  " "$RECEIPT_CONTRACT" "$MIDNIGHT_NETWORK"
572
739
  ;;
573
740
 
741
+ optimistic-sweep)
742
+ # Scan for jobs whose optimistic window has expired and auto-complete them.
743
+ # Run on a cron: */30 * * * * bash gateway.sh optimistic-sweep
744
+ # Dry-run: gateway.sh optimistic-sweep --dry-run
745
+ DRY_RUN=0
746
+ [[ "${1:-}" == "--dry-run" ]] && DRY_RUN=1
747
+
748
+ MIP003_URL="http://localhost:${MIP003_PORT}"
749
+ echo -e "${CYAN}Scanning for auto-approvable jobs${RESET} ${DIM}(window=${OPTIMISTIC_WINDOW_HOURS}h, url=${MIP003_URL}, pageSize=${OPTIMISTIC_SWEEP_PAGE_SIZE})${RESET}..." >&2
750
+
751
+ # Fetch one paginated slice of jobs ready for optimistic completion.
752
+ NOW_ISO=$(python3 -c "
753
+ from datetime import datetime, timezone
754
+ print(datetime.now(timezone.utc).isoformat())
755
+ ")
756
+ JOBS_JSON=$(curl -sf --max-time 10 "${MIP003_URL}/jobs?status=awaiting_approval&approved_before=${NOW_ISO}&limit=${OPTIMISTIC_SWEEP_PAGE_SIZE}&offset=0" 2>/dev/null || echo '{"jobs":[]}')
757
+
758
+ # Filter for expired windows and auto-complete each
759
+ python3 -c "
760
+ import sys, json, subprocess, os
761
+ from datetime import datetime, timezone
762
+
763
+ jobs_json = sys.argv[1]
764
+ gateway = sys.argv[2]
765
+ dry_run = sys.argv[3] == '1'
766
+ env = os.environ.copy()
767
+
768
+ try:
769
+ data = json.loads(jobs_json)
770
+ except Exception as e:
771
+ print(f'ERROR: could not parse /jobs response: {e}', file=sys.stderr)
772
+ sys.exit(1)
773
+
774
+ now = datetime.now(timezone.utc).isoformat()
775
+ jobs = data.get('jobs', [])
776
+ done = 0
777
+ errors = 0
778
+
779
+ for job in jobs:
780
+ jid = job.get('job_id', '')
781
+ approved_at = job.get('approved_at')
782
+ input_data = job.get('input_data') or {}
783
+
784
+ if not approved_at or approved_at > now:
785
+ continue # window not yet expired
786
+
787
+ # Extract commitmentHash from input_data (set by hire-and-pay)
788
+ if isinstance(input_data, str):
789
+ try: input_data = json.loads(input_data)
790
+ except: input_data = {}
791
+ commit = input_data.get('commitmentHash', '')
792
+
793
+ if not commit:
794
+ print(f'SKIP {jid}: no commitmentHash in input_data')
795
+ errors += 1
796
+ continue
797
+
798
+ if dry_run:
799
+ print(f'DRY-RUN: would complete job_id={jid} commitment={commit[:16]}...')
800
+ done += 1
801
+ else:
802
+ result = subprocess.run(
803
+ ['/usr/bin/env', 'bash', gateway, 'complete', jid, commit],
804
+ env=env, capture_output=True, text=True
805
+ )
806
+ if result.returncode == 0:
807
+ print(f'AUTO-COMPLETE OK: {jid}')
808
+ done += 1
809
+ else:
810
+ print(f'AUTO-COMPLETE FAILED: {jid} — {result.stderr.strip()}')
811
+ errors += 1
812
+
813
+ print(f'Sweep complete: {done} completed, {errors} errors.', file=sys.stderr)
814
+ " "$JOBS_JSON" "$0" "$DRY_RUN"
815
+ ;;
816
+
574
817
  *)
575
- echo "Commands: post-bounty, find-agent, hire-and-pay, check-job, complete, refund, withdraw-fees, stats"
818
+ echo -e "${BOLD}nightpay gateway${RESET} anonymous bounty lifecycle CLI" >&2
819
+ echo "" >&2
820
+ echo -e "${BOLD}Commands:${RESET}" >&2
821
+ echo -e " ${CYAN}post-bounty${RESET} <desc> <amount> Fund a bounty anonymously" >&2
822
+ echo -e " ${CYAN}find-agent${RESET} <query> Search Masumi for agents" >&2
823
+ echo -e " ${CYAN}hire-and-pay${RESET} <agent> <desc> <hash> Create escrow, start job" >&2
824
+ echo -e " ${CYAN}check-job${RESET} <job_id> Poll job status" >&2
825
+ echo -e " ${CYAN}complete${RESET} <job_id> <hash> Mint receipt, release payment" >&2
826
+ echo -e " ${CYAN}refund${RESET} <job_id> <hash> Cancel escrow, refund NIGHT" >&2
827
+ echo -e " ${CYAN}approve-multisig${RESET} <id> <hash> <key> Sign high-value approval" >&2
828
+ echo -e " ${CYAN}optimistic-sweep${RESET} [--dry-run] Auto-complete expired windows" >&2
829
+ echo -e " ${CYAN}withdraw-fees${RESET} [amount] Operator fee withdrawal" >&2
830
+ echo -e " ${CYAN}stats${RESET} On-chain contract stats" >&2
831
+ echo "" >&2
832
+ echo -e "${DIM}Required: MASUMI_API_KEY MIDNIGHT_NETWORK OPERATOR_ADDRESS RECEIPT_CONTRACT_ADDRESS${RESET}" >&2
576
833
  exit 1
577
834
  ;;
578
835
  esac