nightpay 0.1.0 → 0.4.4

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.
@@ -0,0 +1,398 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ nightpay_sdk.py — Python SDK for NightPay agent integration.
4
+
5
+ Handles setup, env validation, gateway calls, health checks, and self-healing.
6
+ Works with any agent platform or standalone scripts.
7
+
8
+ Usage:
9
+ from nightpay_sdk import NightPay
10
+
11
+ np = NightPay() # auto-discovers skill location
12
+ np.validate() # check env + connectivity
13
+ np.stats() # get contract stats
14
+ np.post_bounty("Review PR", 5000) # post a bounty
15
+
16
+ CLI usage:
17
+ python3 nightpay_sdk.py validate # validate env + health
18
+ python3 nightpay_sdk.py stats # get contract stats
19
+ python3 nightpay_sdk.py setup # run full setup
20
+ python3 nightpay_sdk.py doctor # diagnose and fix issues
21
+ """
22
+
23
+ import os
24
+ import sys
25
+ import json
26
+ import shutil
27
+ import subprocess
28
+ import urllib.request
29
+ import urllib.error
30
+ from pathlib import Path
31
+ from dataclasses import dataclass, field
32
+ from typing import Optional
33
+
34
+
35
+ @dataclass
36
+ class ValidationResult:
37
+ """Result of a validation check."""
38
+ name: str
39
+ passed: bool
40
+ message: str
41
+ severity: str = "error" # error, warning, info
42
+ fix_hint: Optional[str] = None
43
+
44
+ def __str__(self):
45
+ icon = "OK" if self.passed else ("WARN" if self.severity == "warning" else "FAIL")
46
+ s = f" [{icon}] {self.name}: {self.message}"
47
+ if not self.passed and self.fix_hint:
48
+ s += f"\n Fix: {self.fix_hint}"
49
+ return s
50
+
51
+
52
+ @dataclass
53
+ class HealthReport:
54
+ """Complete health check report."""
55
+ checks: list = field(default_factory=list)
56
+
57
+ @property
58
+ def passed(self) -> bool:
59
+ return all(c.passed for c in self.checks if c.severity == "error")
60
+
61
+ @property
62
+ def errors(self) -> int:
63
+ return sum(1 for c in self.checks if not c.passed and c.severity == "error")
64
+
65
+ @property
66
+ def warnings(self) -> int:
67
+ return sum(1 for c in self.checks if not c.passed and c.severity == "warning")
68
+
69
+ def add(self, name, passed, message, severity="error", fix_hint=None):
70
+ self.checks.append(ValidationResult(name, passed, message, severity, fix_hint))
71
+ return self
72
+
73
+ def __str__(self):
74
+ lines = [str(c) for c in self.checks]
75
+ if self.passed:
76
+ lines.append(f"\n NightPay is healthy ({len(self.checks)} checks passed)")
77
+ else:
78
+ lines.append(f"\n Issues: {self.errors} error(s), {self.warnings} warning(s)")
79
+ return "\n".join(lines)
80
+
81
+ def to_dict(self):
82
+ return {
83
+ "healthy": self.passed,
84
+ "errors": self.errors,
85
+ "warnings": self.warnings,
86
+ "checks": [
87
+ {"name": c.name, "passed": c.passed, "message": c.message,
88
+ "severity": c.severity, "fix_hint": c.fix_hint}
89
+ for c in self.checks
90
+ ]
91
+ }
92
+
93
+
94
+ class NightPay:
95
+ """NightPay SDK — setup, validate, and interact with NightPay."""
96
+
97
+ REQUIRED_ENV = {
98
+ "MASUMI_API_KEY": "Masumi payment API key",
99
+ "OPERATOR_ADDRESS": "64-char hex Midnight operator shielded address",
100
+ "NIGHTPAY_API_URL": "Deployed MIP-003 API base URL",
101
+ "BRIDGE_URL": "Midnight bridge URL",
102
+ }
103
+
104
+ REQUIRED_BINS = ["bash", "curl", "openssl", "sqlite3"]
105
+
106
+ def __init__(self, skill_path: Optional[str] = None):
107
+ """Initialize NightPay SDK.
108
+
109
+ Args:
110
+ skill_path: Path to nightpay skill directory.
111
+ Auto-discovers if not provided.
112
+ """
113
+ self.skill_path = Path(skill_path) if skill_path else self._discover_skill()
114
+ self.gateway = self.skill_path / "scripts" / "gateway.sh" if self.skill_path else None
115
+
116
+ def _discover_skill(self) -> Optional[Path]:
117
+ """Auto-discover skill location."""
118
+ candidates = [
119
+ Path.cwd() / "skills" / "nightpay",
120
+ Path.home() / ".openclaw" / "workspace-nightpay" / "skills" / "nightpay",
121
+ Path(__file__).parent,
122
+ Path(__file__).parent.parent / "skills" / "nightpay",
123
+ ]
124
+ for p in candidates:
125
+ if (p / "SKILL.md").exists():
126
+ return p
127
+ # Check for nested structure
128
+ if (p / "skills" / "nightpay" / "SKILL.md").exists():
129
+ return p / "skills" / "nightpay"
130
+ return None
131
+
132
+ # ─── Validation ───────────────────────────────────────────────────────
133
+
134
+ def validate(self, verbose: bool = True) -> HealthReport:
135
+ """Run full validation: prerequisites, env, connectivity, skill files."""
136
+ report = HealthReport()
137
+
138
+ # Prerequisites
139
+ for b in self.REQUIRED_BINS:
140
+ found = shutil.which(b) is not None
141
+ report.add(f"bin:{b}", found,
142
+ f"{b} found" if found else f"{b} not found",
143
+ fix_hint=f"Install {b} via your package manager")
144
+
145
+ # sha256sum or shasum
146
+ has_hash = shutil.which("sha256sum") or shutil.which("shasum")
147
+ report.add("bin:sha256sum", bool(has_hash),
148
+ "sha256sum/shasum found" if has_hash else "neither found",
149
+ fix_hint="Install coreutils (Linux) or use shasum (macOS)")
150
+
151
+ # Env vars
152
+ for var, desc in self.REQUIRED_ENV.items():
153
+ val = os.environ.get(var, "")
154
+ if not val:
155
+ report.add(f"env:{var}", False, f"not set — {desc}",
156
+ fix_hint=f"export {var}='your-value'")
157
+ elif val == var:
158
+ report.add(f"env:{var}", False,
159
+ f"set to placeholder '{var}' — replace with real value",
160
+ fix_hint=f"export {var}='actual-value-here'")
161
+ else:
162
+ # Extra validation
163
+ if var == "OPERATOR_ADDRESS":
164
+ if len(val) != 64 or not all(c in "0123456789abcdefABCDEF" for c in val):
165
+ report.add(f"env:{var}", False,
166
+ f"doesn't look like 64-char hex (got {len(val)} chars)",
167
+ severity="warning")
168
+ else:
169
+ report.add(f"env:{var}", True,
170
+ f"set ({val[:8]}...{val[-4:]})")
171
+ elif var in ("NIGHTPAY_API_URL", "BRIDGE_URL"):
172
+ if "localhost" in val:
173
+ report.add(f"env:{var}", True, f"set ({val})",
174
+ severity="warning",
175
+ fix_hint="localhost only works if stack runs locally")
176
+ else:
177
+ report.add(f"env:{var}", True, f"set ({val})")
178
+ else:
179
+ report.add(f"env:{var}", True, f"set ({val[:8]}...)")
180
+
181
+ # Skill files
182
+ if self.skill_path and (self.skill_path / "SKILL.md").exists():
183
+ report.add("skill:SKILL.md", True, f"found at {self.skill_path}")
184
+ else:
185
+ report.add("skill:SKILL.md", False, "not found",
186
+ fix_hint="Run: npx nightpay init (or bash setup.sh)")
187
+
188
+ if self.gateway and self.gateway.exists():
189
+ is_exec = os.access(str(self.gateway), os.X_OK)
190
+ report.add("skill:gateway.sh", True,
191
+ f"found {'(executable)' if is_exec else '(NOT executable)'}")
192
+ if not is_exec:
193
+ report.add("skill:gateway.sh:exec", False,
194
+ "gateway.sh is not executable",
195
+ fix_hint=f"chmod +x {self.gateway}")
196
+ else:
197
+ report.add("skill:gateway.sh", False, "not found",
198
+ fix_hint="Reinstall skill files")
199
+
200
+ # Connectivity
201
+ api_url = os.environ.get("NIGHTPAY_API_URL", "")
202
+ if api_url and api_url != "NIGHTPAY_API_URL":
203
+ try:
204
+ req = urllib.request.Request(f"{api_url}/availability",
205
+ method="GET")
206
+ with urllib.request.urlopen(req, timeout=10) as resp:
207
+ report.add("connectivity:api", True,
208
+ f"API reachable ({resp.status})")
209
+ except Exception as e:
210
+ report.add("connectivity:api", False,
211
+ f"API unreachable: {e}",
212
+ severity="warning",
213
+ fix_hint="Check NIGHTPAY_API_URL and ensure stack is running")
214
+
215
+ bridge_url = os.environ.get("BRIDGE_URL", "")
216
+ if bridge_url and bridge_url != "BRIDGE_URL":
217
+ try:
218
+ req = urllib.request.Request(f"{bridge_url}/health",
219
+ method="GET")
220
+ with urllib.request.urlopen(req, timeout=10) as resp:
221
+ report.add("connectivity:bridge", True,
222
+ f"Bridge reachable ({resp.status})")
223
+ except Exception as e:
224
+ report.add("connectivity:bridge", False,
225
+ f"Bridge unreachable: {e}",
226
+ severity="warning",
227
+ fix_hint="Check BRIDGE_URL — on-chain mode may not work")
228
+
229
+ if verbose:
230
+ print(report)
231
+
232
+ return report
233
+
234
+ # ─── Self-healing doctor ──────────────────────────────────────────────
235
+
236
+ def doctor(self, auto_fix: bool = False) -> HealthReport:
237
+ """Diagnose issues and optionally auto-fix them."""
238
+ report = self.validate(verbose=False)
239
+
240
+ fixes_applied = 0
241
+
242
+ for check in report.checks:
243
+ if check.passed:
244
+ continue
245
+
246
+ if auto_fix:
247
+ # Auto-fix permissions
248
+ if check.name == "skill:gateway.sh:exec" and self.gateway:
249
+ os.chmod(str(self.gateway), 0o755)
250
+ check.passed = True
251
+ check.message = "fixed: chmod +x applied"
252
+ fixes_applied += 1
253
+
254
+ # Auto-fix nested SKILL.md
255
+ if check.name == "skill:SKILL.md" and self.skill_path:
256
+ nested = self.skill_path / "skills" / "nightpay" / "SKILL.md"
257
+ if nested.exists():
258
+ import shutil as sh
259
+ nested_dir = self.skill_path / "skills" / "nightpay"
260
+ for item in nested_dir.iterdir():
261
+ dest = self.skill_path / item.name
262
+ if item.is_dir():
263
+ sh.copytree(str(item), str(dest),
264
+ dirs_exist_ok=True)
265
+ else:
266
+ sh.copy2(str(item), str(dest))
267
+ check.passed = True
268
+ check.message = "fixed: flattened nested skill directory"
269
+ fixes_applied += 1
270
+
271
+ print(report)
272
+ if fixes_applied:
273
+ print(f"\n Auto-fixed {fixes_applied} issue(s)")
274
+
275
+ return report
276
+
277
+ # ─── Gateway commands ─────────────────────────────────────────────────
278
+
279
+ def _run_gateway(self, *args) -> subprocess.CompletedProcess:
280
+ """Run a gateway.sh command."""
281
+ if not self.gateway or not self.gateway.exists():
282
+ raise FileNotFoundError(
283
+ "gateway.sh not found. Run setup first: bash setup.sh")
284
+ return subprocess.run(
285
+ ["bash", str(self.gateway)] + list(args),
286
+ capture_output=True, text=True, timeout=30
287
+ )
288
+
289
+ def stats(self) -> dict:
290
+ """Get contract statistics."""
291
+ result = self._run_gateway("stats")
292
+ if result.returncode != 0:
293
+ raise RuntimeError(f"stats failed: {result.stderr}")
294
+ try:
295
+ return json.loads(result.stdout)
296
+ except json.JSONDecodeError:
297
+ return {"raw_output": result.stdout.strip()}
298
+
299
+ def post_bounty(self, description: str, amount_specks: int) -> dict:
300
+ """Post a simple bounty."""
301
+ result = self._run_gateway("post-bounty", description,
302
+ str(amount_specks))
303
+ if result.returncode != 0:
304
+ raise RuntimeError(f"post-bounty failed: {result.stderr}")
305
+ try:
306
+ return json.loads(result.stdout)
307
+ except json.JSONDecodeError:
308
+ return {"raw_output": result.stdout.strip()}
309
+
310
+ def create_pool(self, description: str, contribution_specks: int,
311
+ funding_goal_specks: int) -> dict:
312
+ """Create a bounty pool."""
313
+ result = self._run_gateway("create-pool", description,
314
+ str(contribution_specks),
315
+ str(funding_goal_specks))
316
+ if result.returncode != 0:
317
+ raise RuntimeError(f"create-pool failed: {result.stderr}")
318
+ try:
319
+ return json.loads(result.stdout)
320
+ except json.JSONDecodeError:
321
+ return {"raw_output": result.stdout.strip()}
322
+
323
+ def find_agent(self, capability: str) -> dict:
324
+ """Find agents matching a capability query."""
325
+ result = self._run_gateway("find-agent", capability)
326
+ if result.returncode != 0:
327
+ raise RuntimeError(f"find-agent failed: {result.stderr}")
328
+ try:
329
+ return json.loads(result.stdout)
330
+ except json.JSONDecodeError:
331
+ return {"raw_output": result.stdout.strip()}
332
+
333
+ def pool_status(self, pool_commitment: str) -> dict:
334
+ """Check pool status."""
335
+ result = self._run_gateway("pool-status", pool_commitment)
336
+ if result.returncode != 0:
337
+ raise RuntimeError(f"pool-status failed: {result.stderr}")
338
+ try:
339
+ return json.loads(result.stdout)
340
+ except json.JSONDecodeError:
341
+ return {"raw_output": result.stdout.strip()}
342
+
343
+ def health_check(self) -> dict:
344
+ """Quick health check — returns JSON-safe result."""
345
+ report = self.validate(verbose=False)
346
+ return report.to_dict()
347
+
348
+
349
+ # ─── CLI ──────────────────────────────────────────────────────────────────────
350
+ def main():
351
+ if len(sys.argv) < 2:
352
+ print("Usage: python3 nightpay_sdk.py <command>")
353
+ print("Commands: validate, stats, setup, doctor, health")
354
+ sys.exit(1)
355
+
356
+ cmd = sys.argv[1]
357
+ np = NightPay()
358
+
359
+ if cmd == "validate":
360
+ report = np.validate()
361
+ sys.exit(0 if report.passed else 1)
362
+
363
+ elif cmd == "doctor":
364
+ auto = "--auto-fix" in sys.argv
365
+ report = np.doctor(auto_fix=auto)
366
+ sys.exit(0 if report.passed else 1)
367
+
368
+ elif cmd == "health":
369
+ result = np.health_check()
370
+ print(json.dumps(result, indent=2))
371
+ sys.exit(0 if result["healthy"] else 1)
372
+
373
+ elif cmd == "stats":
374
+ try:
375
+ result = np.stats()
376
+ print(json.dumps(result, indent=2))
377
+ except Exception as e:
378
+ print(f"Error: {e}", file=sys.stderr)
379
+ sys.exit(1)
380
+
381
+ elif cmd == "setup":
382
+ # Run bash setup.sh if available
383
+ setup_sh = Path(__file__).parent / "scripts" / "setup.sh"
384
+ if not setup_sh.exists():
385
+ setup_sh = Path(__file__).parent.parent / "scripts" / "setup.sh"
386
+ if setup_sh.exists():
387
+ os.execvp("bash", ["bash", str(setup_sh)] + sys.argv[2:])
388
+ else:
389
+ print("setup.sh not found — run validate instead")
390
+ np.validate()
391
+
392
+ else:
393
+ print(f"Unknown command: {cmd}")
394
+ sys.exit(1)
395
+
396
+
397
+ if __name__ == "__main__":
398
+ main()
@@ -0,0 +1,10 @@
1
+ {
2
+ "id": "nightpay",
3
+ "name": "NightPay",
4
+ "description": "Anonymous community bounty pools \u2014 Midnight ZK proofs + Masumi settlement + Cardano finality. Skills auto-loaded from skills/nightpay/.",
5
+ "version": "0.4.1",
6
+ "configSchema": {},
7
+ "skills": [
8
+ "skills/nightpay"
9
+ ]
10
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nightpay",
3
- "version": "0.1.0",
3
+ "version": "0.4.4",
4
4
  "description": "Anonymous community bounties for AI agents. Midnight ZK proofs + Masumi settlement + Cardano finality.",
5
5
  "keywords": [
6
6
  "bounties",
@@ -16,24 +16,35 @@
16
16
  "agent-skills",
17
17
  "clawhub"
18
18
  ],
19
- "homepage": "https://github.com/nightpay/nightpay",
19
+ "homepage": "https://nightpay.dev",
20
20
  "repository": {
21
21
  "type": "git",
22
22
  "url": "git+https://github.com/nightpay/nightpay.git"
23
23
  },
24
- "license": "MIT",
24
+ "license": "AGPL-3.0-only",
25
25
  "author": "nightpay contributors",
26
26
  "type": "module",
27
27
  "bin": {
28
28
  "nightpay": "bin/cli.js"
29
29
  },
30
30
  "scripts": {
31
- "test": "bash test/smoke.sh"
31
+ "test": "node test/run-quality-gate.mjs",
32
+ "test:quality": "node test/run-quality-gate.mjs",
33
+ "test:smoke": "node test/run-smoke-test.mjs"
32
34
  },
33
35
  "files": [
34
- "bin/",
36
+ "bin/cli.js",
35
37
  "skills/",
36
38
  "README.md",
37
- "LICENSE"
38
- ]
39
+ "LICENSE",
40
+ "nightpay_sdk.py",
41
+ "plugin.js",
42
+ "openclaw.plugin.json"
43
+ ],
44
+ "openclaw": {
45
+ "extensions": [
46
+ "./plugin.js"
47
+ ],
48
+ "type": "skillBundle"
49
+ }
39
50
  }