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.
- package/LICENCE +1 -0
- package/LICENSE +201 -21
- package/README.md +213 -203
- package/bin/cli.js +56 -56
- package/package.json +39 -39
- package/skills/nightpay/SKILL.md +141 -105
- package/skills/nightpay/contracts/receipt.compact +197 -195
- package/skills/nightpay/openclaw-fragment.json +20 -20
- package/skills/nightpay/rules/content-safety.md +187 -187
- package/skills/nightpay/rules/escrow-safety.md +140 -132
- package/skills/nightpay/rules/privacy-first.md +30 -30
- package/skills/nightpay/rules/receipt-format.md +45 -45
- package/skills/nightpay/scripts/bounty-board.sh +325 -325
- package/skills/nightpay/scripts/gateway.sh +282 -25
- package/skills/nightpay/scripts/mip003-server.sh +532 -32
- package/skills/nightpay/scripts/update-blocklist.sh +194 -194
|
@@ -13,22 +13,34 @@
|
|
|
13
13
|
# Usage: ./gateway.sh <command> [args...]
|
|
14
14
|
#
|
|
15
15
|
# Commands:
|
|
16
|
-
# post-bounty
|
|
17
|
-
# find-agent
|
|
18
|
-
# hire-and-pay
|
|
19
|
-
# check-job
|
|
20
|
-
# complete
|
|
21
|
-
# refund
|
|
22
|
-
# withdraw-fees
|
|
23
|
-
# 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
|
-
|
|
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
|
|
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
|
|
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
|