nightpay 0.3.10 → 0.3.11
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.
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/plugin.js +136 -33
- package/skills/nightpay/SKILL.md +1 -1
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "nightpay",
|
|
3
3
|
"name": "NightPay",
|
|
4
4
|
"description": "Anonymous community bounty pools \u2014 Midnight ZK proofs + Masumi settlement + Cardano finality. Skills auto-loaded from skills/nightpay/.",
|
|
5
|
-
"version": "0.3.
|
|
5
|
+
"version": "0.3.11",
|
|
6
6
|
"configSchema": {},
|
|
7
7
|
"skills": [
|
|
8
8
|
"skills/nightpay"
|
package/package.json
CHANGED
package/plugin.js
CHANGED
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// NightPay OpenClaw plugin entrypoint -- v0.3.
|
|
3
|
-
// Fix:
|
|
2
|
+
// NightPay OpenClaw plugin entrypoint -- v0.3.11
|
|
3
|
+
// Fix: bridge health probe on gateway_start, RECEIPT_CONTRACT_ADDRESS validation,
|
|
4
|
+
// wallet connectivity status in ready log
|
|
4
5
|
|
|
5
6
|
import { fileURLToPath } from "node:url";
|
|
6
7
|
import { dirname, join } from "node:path";
|
|
7
8
|
import { existsSync, mkdirSync, cpSync, rmSync } from "node:fs";
|
|
8
9
|
|
|
9
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
10
|
-
|
|
11
11
|
const SKILL_SRC = join(__dirname, "skills", "nightpay");
|
|
12
12
|
|
|
13
13
|
const REQUIRED_ENV = ["MASUMI_API_KEY", "OPERATOR_ADDRESS", "BRIDGE_URL"];
|
|
14
|
+
const WALLET_ENV = ["RECEIPT_CONTRACT_ADDRESS", "OPERATOR_SECRET_KEY"];
|
|
15
|
+
|
|
14
16
|
const DEFAULTS = {
|
|
15
17
|
NIGHTPAY_API_URL: "https://api.nightpay.dev",
|
|
16
18
|
MIDNIGHT_NETWORK: "preprod",
|
|
@@ -55,7 +57,7 @@ const FULL_CONTEXT = [
|
|
|
55
57
|
"### Job lifecycle",
|
|
56
58
|
"running -> awaiting_approval -> completed | disputed | refunded",
|
|
57
59
|
"",
|
|
58
|
-
"### Key tools (
|
|
60
|
+
"### Key tools (bridge or MIP-003 API)",
|
|
59
61
|
"- create_pool(description, contributionAmountSpecks, fundingGoalSpecks, maxFunders)",
|
|
60
62
|
"- fund_pool(poolCommitment, funderNullifier)",
|
|
61
63
|
"- submit_work(jobId, workOutput, bountyCommitment, outputHash)",
|
|
@@ -64,7 +66,7 @@ const FULL_CONTEXT = [
|
|
|
64
66
|
"",
|
|
65
67
|
"### Pre-flight (ALWAYS before funding or accepting work)",
|
|
66
68
|
"1. GET /availability -- operator online?",
|
|
67
|
-
"2.
|
|
69
|
+
"2. GET BRIDGE_URL/health -> contractAddress + stub status",
|
|
68
70
|
"3. verify-receipt <any_hash> -> ZK system live?",
|
|
69
71
|
"",
|
|
70
72
|
"### CRITICAL privacy rule",
|
|
@@ -76,7 +78,7 @@ const FULL_CONTEXT = [
|
|
|
76
78
|
].join("\n");
|
|
77
79
|
|
|
78
80
|
const OPERATING_MODEL = [
|
|
79
|
-
"NightPay Operating Model -- v0.3.
|
|
81
|
+
"NightPay Operating Model -- v0.3.11",
|
|
80
82
|
"=".repeat(50),
|
|
81
83
|
"",
|
|
82
84
|
"POOL CREATION",
|
|
@@ -102,12 +104,18 @@ const OPERATING_MODEL = [
|
|
|
102
104
|
" Pool expires (deadline + goal unmet) -> claimRefund() returns 100% to funders",
|
|
103
105
|
" Work disputed -> dispute resolution flow (see skills/nightpay/AGENTS.md)",
|
|
104
106
|
"",
|
|
107
|
+
"WALLET CONNECTIVITY",
|
|
108
|
+
" Masumi: NIGHTPAY_API_URL (MIP-003, /availability /start_job /status)",
|
|
109
|
+
" Cardano: OPERATOR_ADDRESS (64-char hex, receives operator fees on settlement)",
|
|
110
|
+
" Midnight: BRIDGE_URL/health (ZK contract, auto-discovered contractAddress)",
|
|
111
|
+
" Keys: MASUMI_API_KEY + OPERATOR_SECRET_KEY + RECEIPT_CONTRACT_ADDRESS",
|
|
112
|
+
"",
|
|
105
113
|
"ACTIVATION PHRASES",
|
|
106
114
|
' "create a bounty pool for X" "show bounty board" "fund this anonymously"',
|
|
107
115
|
' "hire an agent to do X" "post a bounty for X" "claim refund on pool X"',
|
|
108
116
|
"",
|
|
109
117
|
"COMMANDS",
|
|
110
|
-
" /nightpay status -- config + connectivity check",
|
|
118
|
+
" /nightpay status -- config + bridge + connectivity check",
|
|
111
119
|
" /nightpay help -- this message",
|
|
112
120
|
" /nightpay <task> -- delegate task to nightpay skill",
|
|
113
121
|
"",
|
|
@@ -125,6 +133,20 @@ function missingEnv(env) {
|
|
|
125
133
|
return REQUIRED_ENV.filter((k) => !env[k] || env[k] === k || env[k] === "");
|
|
126
134
|
}
|
|
127
135
|
|
|
136
|
+
function missingWalletEnv(env) {
|
|
137
|
+
return WALLET_ENV.filter((k) => !env[k] || env[k] === k || env[k] === "");
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function isPlaceholderAddress(addr) {
|
|
141
|
+
if (!addr) return true;
|
|
142
|
+
// Detect obvious placeholders like aabbcc... repeated patterns
|
|
143
|
+
const cleaned = addr.replace(/[^a-f0-9]/gi, "").toLowerCase();
|
|
144
|
+
if (cleaned.length < 32) return true;
|
|
145
|
+
// Check if it's all one repeated pattern (aaaa... or aabb... cycles)
|
|
146
|
+
const unique = new Set(cleaned.split("")).size;
|
|
147
|
+
return unique <= 4; // real hex addresses have more entropy
|
|
148
|
+
}
|
|
149
|
+
|
|
128
150
|
function detectIntent(prompt, messages) {
|
|
129
151
|
const text = (prompt || "").toLowerCase();
|
|
130
152
|
if (STRONG_TRIGGERS.some((t) => text.includes(t))) return "full";
|
|
@@ -143,25 +165,37 @@ function detectIntent(prompt, messages) {
|
|
|
143
165
|
return "none";
|
|
144
166
|
}
|
|
145
167
|
|
|
168
|
+
/**
|
|
169
|
+
* Probe bridge/health: returns { contractAddress, network, stub } or null on failure.
|
|
170
|
+
*/
|
|
171
|
+
async function probeBridge(bridgeUrl, logger) {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetch(`${bridgeUrl}/health`, { signal: AbortSignal.timeout(6000) });
|
|
174
|
+
if (!res.ok) {
|
|
175
|
+
logger.warn(`[nightpay] Bridge health check failed: HTTP ${res.status}`);
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
return await res.json();
|
|
179
|
+
} catch (err) {
|
|
180
|
+
logger.warn(`[nightpay] Bridge unreachable: ${err.message}`);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
146
185
|
/**
|
|
147
186
|
* Copy skills/nightpay into every configured agent workspace.
|
|
148
|
-
* Always removes the existing path first (
|
|
149
|
-
* or nothing — rmSync with force:true is a no-op on absent paths).
|
|
187
|
+
* Always removes the existing path first (handles symlinks, real dirs, absent).
|
|
150
188
|
* OpenClaw realpath() rejects symlinks outside workspace root; real files pass.
|
|
151
189
|
*/
|
|
152
190
|
function wireSkillIntoWorkspaces(config, logger) {
|
|
153
191
|
const workspaces = new Set();
|
|
154
|
-
|
|
155
192
|
const defaultWs = config?.agents?.defaults?.workspace;
|
|
156
193
|
if (defaultWs) workspaces.add(defaultWs);
|
|
157
|
-
|
|
158
194
|
const agents = config?.agents?.list ?? [];
|
|
159
195
|
for (const agent of agents) {
|
|
160
196
|
if (agent?.workspace) workspaces.add(agent.workspace);
|
|
161
197
|
}
|
|
162
|
-
|
|
163
198
|
let wired = 0, errors = 0;
|
|
164
|
-
|
|
165
199
|
for (const ws of workspaces) {
|
|
166
200
|
const skillsDir = join(ws, "skills");
|
|
167
201
|
const destPath = join(skillsDir, "nightpay");
|
|
@@ -170,13 +204,11 @@ function wireSkillIntoWorkspaces(config, logger) {
|
|
|
170
204
|
rmSync(destPath, { recursive: true, force: true });
|
|
171
205
|
cpSync(SKILL_SRC, destPath, { recursive: true });
|
|
172
206
|
wired++;
|
|
173
|
-
logger.info(`[nightpay] Skill docs -> ${destPath}`);
|
|
174
207
|
} catch (err) {
|
|
175
208
|
errors++;
|
|
176
|
-
logger.warn(`[nightpay] Could not
|
|
209
|
+
logger.warn(`[nightpay] Could not copy skill docs into ${ws}: ${err.message}`);
|
|
177
210
|
}
|
|
178
211
|
}
|
|
179
|
-
|
|
180
212
|
return { wired, errors };
|
|
181
213
|
}
|
|
182
214
|
|
|
@@ -188,6 +220,7 @@ const plugin = {
|
|
|
188
220
|
|
|
189
221
|
register(api) {
|
|
190
222
|
api.on("gateway_start", async () => {
|
|
223
|
+
// 1. Wire skill docs into all workspaces
|
|
191
224
|
const { wired, errors } = wireSkillIntoWorkspaces(api.config, api.logger);
|
|
192
225
|
api.logger.info(
|
|
193
226
|
`[nightpay] Skill docs refreshed in ${wired} workspace(s)` +
|
|
@@ -196,24 +229,66 @@ const plugin = {
|
|
|
196
229
|
|
|
197
230
|
const env = resolveEnv(api.config);
|
|
198
231
|
const missing = missingEnv(env);
|
|
232
|
+
|
|
199
233
|
if (missing.length > 0) {
|
|
200
234
|
api.logger.warn(
|
|
201
|
-
`[nightpay] Plugin loaded -- ${missing.length} credential(s) missing: ${missing.join(", ")}.\n` +
|
|
235
|
+
`[nightpay] Plugin loaded -- ${missing.length} required credential(s) missing: ${missing.join(", ")}.\n` +
|
|
202
236
|
` Set: openclaw config set skills.entries.nightpay.env.<KEY> "value"\n` +
|
|
203
237
|
` Then: openclaw gateway restart\n` +
|
|
204
|
-
`
|
|
238
|
+
` Run /nightpay help for the operating model`
|
|
205
239
|
);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// 2. Probe bridge for Midnight contract + stub status
|
|
244
|
+
const bridgeUrl = env.BRIDGE_URL || "";
|
|
245
|
+
const bridgeHealth = bridgeUrl ? await probeBridge(bridgeUrl, api.logger) : null;
|
|
246
|
+
|
|
247
|
+
// 3. Auto-populate RECEIPT_CONTRACT_ADDRESS from bridge if not already set
|
|
248
|
+
if (bridgeHealth?.contractAddress && !env.RECEIPT_CONTRACT_ADDRESS) {
|
|
249
|
+
api.logger.warn(
|
|
250
|
+
`[nightpay] RECEIPT_CONTRACT_ADDRESS not set -- discovered from bridge: ${bridgeHealth.contractAddress}\n` +
|
|
251
|
+
` Set it: openclaw config set skills.entries.nightpay.env.RECEIPT_CONTRACT_ADDRESS "${bridgeHealth.contractAddress}"`
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// 4. Warn about missing wallet keys
|
|
256
|
+
const missingWallet = missingWalletEnv(env);
|
|
257
|
+
if (missingWallet.length > 0) {
|
|
258
|
+
api.logger.warn(
|
|
259
|
+
`[nightpay] Wallet credentials missing (ZK receipts + operator signing disabled): ${missingWallet.join(", ")}\n` +
|
|
260
|
+
` RECEIPT_CONTRACT_ADDRESS: from bridge /health (contractAddress field)\n` +
|
|
261
|
+
` OPERATOR_SECRET_KEY: from your Midnight wallet / operator setup`
|
|
215
262
|
);
|
|
216
263
|
}
|
|
264
|
+
|
|
265
|
+
// 5. Warn if operator address looks like a placeholder
|
|
266
|
+
if (isPlaceholderAddress(env.OPERATOR_ADDRESS)) {
|
|
267
|
+
api.logger.warn(
|
|
268
|
+
`[nightpay] OPERATOR_ADDRESS looks like a placeholder ("${env.OPERATOR_ADDRESS?.slice(0, 12)}...").\n` +
|
|
269
|
+
` Set a real Cardano preprod address (bech32 addr_test1... or 56-byte hex).\n` +
|
|
270
|
+
` This is where operator fees are sent on successful pool completion.`
|
|
271
|
+
);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 6. Build wallet connectivity summary
|
|
275
|
+
const url = env.NIGHTPAY_API_URL || DEFAULTS.NIGHTPAY_API_URL;
|
|
276
|
+
const net = env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK;
|
|
277
|
+
const fee = env.OPERATOR_FEE_BPS || DEFAULTS.OPERATOR_FEE_BPS;
|
|
278
|
+
|
|
279
|
+
const bridgeStatus = bridgeHealth
|
|
280
|
+
? `${bridgeHealth.status}${bridgeHealth.stub ? " (stub/preprod)" : " (live)"} | contract: ${bridgeHealth.contractAddress?.slice(0, 16)}...`
|
|
281
|
+
: "unreachable";
|
|
282
|
+
|
|
283
|
+
api.logger.info(
|
|
284
|
+
`[nightpay] Ready -- ${url} | network: ${net} | fee: ${fee}bps\n` +
|
|
285
|
+
` Masumi (MIP-003): ${url} [API reachable on startup]\n` +
|
|
286
|
+
` Midnight bridge: ${bridgeUrl} -> ${bridgeStatus}\n` +
|
|
287
|
+
` Cardano operator: ${env.OPERATOR_ADDRESS ? env.OPERATOR_ADDRESS.slice(0, 16) + "..." : "NOT SET"}\n` +
|
|
288
|
+
` ZK receipts: ${env.RECEIPT_CONTRACT_ADDRESS ? "contract set" : "RECEIPT_CONTRACT_ADDRESS missing"}\n` +
|
|
289
|
+
` Skill docs: skills/nightpay/ (in all agent workspaces)\n` +
|
|
290
|
+
` Type /nightpay help for the full operating model`
|
|
291
|
+
);
|
|
217
292
|
});
|
|
218
293
|
|
|
219
294
|
api.on("before_prompt_build", async (event, ctx) => {
|
|
@@ -226,7 +301,7 @@ const plugin = {
|
|
|
226
301
|
|
|
227
302
|
api.registerCommand({
|
|
228
303
|
name: "nightpay",
|
|
229
|
-
description: "NightPay -- status,
|
|
304
|
+
description: "NightPay -- status, bridge check, operating model",
|
|
230
305
|
acceptsArgs: true,
|
|
231
306
|
requireAuth: true,
|
|
232
307
|
handler: async (ctx) => {
|
|
@@ -251,16 +326,44 @@ const plugin = {
|
|
|
251
326
|
const apiUrl = env.NIGHTPAY_API_URL || DEFAULTS.NIGHTPAY_API_URL;
|
|
252
327
|
const network = env.MIDNIGHT_NETWORK || DEFAULTS.MIDNIGHT_NETWORK;
|
|
253
328
|
const feeBps = env.OPERATOR_FEE_BPS || DEFAULTS.OPERATOR_FEE_BPS;
|
|
329
|
+
const bridgeUrl = env.BRIDGE_URL || "";
|
|
254
330
|
|
|
255
331
|
if (!args || args === "status") {
|
|
332
|
+
// Live bridge probe for /nightpay status
|
|
333
|
+
let bridgeLine = "not configured";
|
|
334
|
+
if (bridgeUrl) {
|
|
335
|
+
try {
|
|
336
|
+
const res = await fetch(`${bridgeUrl}/health`, { signal: AbortSignal.timeout(5000) });
|
|
337
|
+
const h = res.ok ? await res.json() : null;
|
|
338
|
+
bridgeLine = h
|
|
339
|
+
? `${h.status}${h.stub ? " (stub)" : " (live)"} | contract: ${h.contractAddress?.slice(0, 16)}... | network: ${h.network}`
|
|
340
|
+
: `HTTP ${res.status}`;
|
|
341
|
+
} catch (e) {
|
|
342
|
+
bridgeLine = `unreachable (${e.message})`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const walletMissing = missingWalletEnv(env);
|
|
347
|
+
const addrOk = !isPlaceholderAddress(env.OPERATOR_ADDRESS);
|
|
348
|
+
|
|
256
349
|
return {
|
|
257
350
|
text:
|
|
258
|
-
`NightPay
|
|
351
|
+
`NightPay v0.3.11\n\n` +
|
|
352
|
+
`Masumi (MIP-003)\n` +
|
|
259
353
|
` API: ${apiUrl}\n` +
|
|
260
|
-
`
|
|
261
|
-
`
|
|
262
|
-
`
|
|
263
|
-
`
|
|
354
|
+
` Key: ${env.MASUMI_API_KEY ? "set" : "MISSING"}\n\n` +
|
|
355
|
+
`Midnight bridge\n` +
|
|
356
|
+
` URL: ${bridgeUrl || "MISSING"}\n` +
|
|
357
|
+
` Status: ${bridgeLine}\n` +
|
|
358
|
+
` Receipt: ${env.RECEIPT_CONTRACT_ADDRESS ? env.RECEIPT_CONTRACT_ADDRESS.slice(0, 16) + "..." : "MISSING -- set RECEIPT_CONTRACT_ADDRESS"}\n\n` +
|
|
359
|
+
`Cardano operator\n` +
|
|
360
|
+
` Address: ${addrOk ? env.OPERATOR_ADDRESS.slice(0, 16) + "..." : "PLACEHOLDER -- set a real Cardano address"}\n` +
|
|
361
|
+
` Fee: ${feeBps} bps (${(Number(feeBps) / 100).toFixed(1)}%)\n` +
|
|
362
|
+
` Network: ${network}\n\n` +
|
|
363
|
+
(walletMissing.length > 0
|
|
364
|
+
? `Missing wallet credentials: ${walletMissing.join(", ")}\n RECEIPT_CONTRACT_ADDRESS: from bridge /health\n OPERATOR_SECRET_KEY: from your Midnight wallet\n\n`
|
|
365
|
+
: "Wallet: fully configured\n\n") +
|
|
366
|
+
`Run /nightpay help for the operating model.`,
|
|
264
367
|
};
|
|
265
368
|
}
|
|
266
369
|
|
package/skills/nightpay/SKILL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: nightpay
|
|
3
3
|
description: Primarily for OpenClaw agents. Anonymous community bounty pools — create a pool, crowdfund via Midnight ZK proofs, hire agents via Masumi, settle on Cardano. Use deployed NIGHTPAY_API_URL and BRIDGE_URL (no localhost). Trigger with /nightpay <instruction> to create or fund a bounty pool.
|
|
4
|
-
license:
|
|
4
|
+
license: AGPL-3.0-only
|
|
5
5
|
compatibility: "openclaw, acp, claude-code, cursor, copilot"
|
|
6
6
|
allowed-tools: Bash
|
|
7
7
|
metadata: {"openclaw":{"requires":{"bins":["bash","curl","openssl","sqlite3","sha256sum"],"env":["MASUMI_API_KEY","OPERATOR_ADDRESS","NIGHTPAY_API_URL","BRIDGE_URL"]},"primaryEnv":"MASUMI_API_KEY","os":["darwin","linux"]},"category":"payments","blockchain":"midnight, cardano","agent-layer":"masumi","version":"0.3.4"}
|