openclaw-overlay-plugin 0.7.22

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 (221) hide show
  1. package/README.md +406 -0
  2. package/SKILL.md +78 -0
  3. package/clawdbot.plugin.json +106 -0
  4. package/dist/cli-main.d.ts +7 -0
  5. package/dist/cli-main.js +192 -0
  6. package/dist/cli.d.ts +8 -0
  7. package/dist/cli.js +14 -0
  8. package/dist/core/config.d.ts +11 -0
  9. package/dist/core/config.js +13 -0
  10. package/dist/core/index.d.ts +25 -0
  11. package/dist/core/index.js +26 -0
  12. package/dist/core/payment.d.ts +16 -0
  13. package/dist/core/payment.js +94 -0
  14. package/dist/core/types.d.ts +94 -0
  15. package/dist/core/types.js +4 -0
  16. package/dist/core/verify.d.ts +28 -0
  17. package/dist/core/verify.js +104 -0
  18. package/dist/core/wallet.d.ts +99 -0
  19. package/dist/core/wallet.js +219 -0
  20. package/dist/scripts/baemail/commands.d.ts +64 -0
  21. package/dist/scripts/baemail/commands.js +258 -0
  22. package/dist/scripts/baemail/handler.d.ts +36 -0
  23. package/dist/scripts/baemail/handler.js +284 -0
  24. package/dist/scripts/baemail/index.d.ts +5 -0
  25. package/dist/scripts/baemail/index.js +5 -0
  26. package/dist/scripts/config.d.ts +48 -0
  27. package/dist/scripts/config.js +68 -0
  28. package/dist/scripts/index.d.ts +7 -0
  29. package/dist/scripts/index.js +7 -0
  30. package/dist/scripts/messaging/connect.d.ts +8 -0
  31. package/dist/scripts/messaging/connect.js +114 -0
  32. package/dist/scripts/messaging/handlers.d.ts +21 -0
  33. package/dist/scripts/messaging/handlers.js +334 -0
  34. package/dist/scripts/messaging/inbox.d.ts +11 -0
  35. package/dist/scripts/messaging/inbox.js +51 -0
  36. package/dist/scripts/messaging/index.d.ts +8 -0
  37. package/dist/scripts/messaging/index.js +8 -0
  38. package/dist/scripts/messaging/poll.d.ts +7 -0
  39. package/dist/scripts/messaging/poll.js +52 -0
  40. package/dist/scripts/messaging/send.d.ts +7 -0
  41. package/dist/scripts/messaging/send.js +43 -0
  42. package/dist/scripts/output.d.ts +12 -0
  43. package/dist/scripts/output.js +19 -0
  44. package/dist/scripts/overlay/discover.d.ts +7 -0
  45. package/dist/scripts/overlay/discover.js +72 -0
  46. package/dist/scripts/overlay/index.d.ts +7 -0
  47. package/dist/scripts/overlay/index.js +7 -0
  48. package/dist/scripts/overlay/registration.d.ts +19 -0
  49. package/dist/scripts/overlay/registration.js +176 -0
  50. package/dist/scripts/overlay/services.d.ts +29 -0
  51. package/dist/scripts/overlay/services.js +167 -0
  52. package/dist/scripts/overlay/transaction.d.ts +42 -0
  53. package/dist/scripts/overlay/transaction.js +103 -0
  54. package/dist/scripts/payment/build.d.ts +24 -0
  55. package/dist/scripts/payment/build.js +54 -0
  56. package/dist/scripts/payment/commands.d.ts +15 -0
  57. package/dist/scripts/payment/commands.js +73 -0
  58. package/dist/scripts/payment/index.d.ts +6 -0
  59. package/dist/scripts/payment/index.js +6 -0
  60. package/dist/scripts/payment/types.d.ts +56 -0
  61. package/dist/scripts/payment/types.js +4 -0
  62. package/dist/scripts/services/index.d.ts +6 -0
  63. package/dist/scripts/services/index.js +6 -0
  64. package/dist/scripts/services/queue.d.ts +11 -0
  65. package/dist/scripts/services/queue.js +28 -0
  66. package/dist/scripts/services/request.d.ts +7 -0
  67. package/dist/scripts/services/request.js +82 -0
  68. package/dist/scripts/services/respond.d.ts +11 -0
  69. package/dist/scripts/services/respond.js +132 -0
  70. package/dist/scripts/types.d.ts +107 -0
  71. package/dist/scripts/types.js +4 -0
  72. package/dist/scripts/utils/index.d.ts +6 -0
  73. package/dist/scripts/utils/index.js +6 -0
  74. package/dist/scripts/utils/merkle.d.ts +12 -0
  75. package/dist/scripts/utils/merkle.js +47 -0
  76. package/dist/scripts/utils/storage.d.ts +66 -0
  77. package/dist/scripts/utils/storage.js +211 -0
  78. package/dist/scripts/utils/woc.d.ts +26 -0
  79. package/dist/scripts/utils/woc.js +91 -0
  80. package/dist/scripts/wallet/balance.d.ts +22 -0
  81. package/dist/scripts/wallet/balance.js +240 -0
  82. package/dist/scripts/wallet/identity.d.ts +70 -0
  83. package/dist/scripts/wallet/identity.js +151 -0
  84. package/dist/scripts/wallet/index.d.ts +6 -0
  85. package/dist/scripts/wallet/index.js +6 -0
  86. package/dist/scripts/wallet/setup.d.ts +15 -0
  87. package/dist/scripts/wallet/setup.js +105 -0
  88. package/dist/scripts/x-verification/commands.d.ts +27 -0
  89. package/dist/scripts/x-verification/commands.js +222 -0
  90. package/dist/scripts/x-verification/index.d.ts +4 -0
  91. package/dist/scripts/x-verification/index.js +4 -0
  92. package/dist/services/built-in/api-proxy/index.d.ts +6 -0
  93. package/dist/services/built-in/api-proxy/index.js +23 -0
  94. package/dist/services/built-in/code-develop/index.d.ts +6 -0
  95. package/dist/services/built-in/code-develop/index.js +23 -0
  96. package/dist/services/built-in/code-review/index.d.ts +10 -0
  97. package/dist/services/built-in/code-review/index.js +51 -0
  98. package/dist/services/built-in/image-analysis/index.d.ts +6 -0
  99. package/dist/services/built-in/image-analysis/index.js +33 -0
  100. package/dist/services/built-in/memory-store/index.d.ts +6 -0
  101. package/dist/services/built-in/memory-store/index.js +22 -0
  102. package/dist/services/built-in/roulette/index.d.ts +6 -0
  103. package/dist/services/built-in/roulette/index.js +27 -0
  104. package/dist/services/built-in/summarize/index.d.ts +6 -0
  105. package/dist/services/built-in/summarize/index.js +21 -0
  106. package/dist/services/built-in/tell-joke/handler.d.ts +7 -0
  107. package/dist/services/built-in/tell-joke/handler.js +122 -0
  108. package/dist/services/built-in/tell-joke/index.d.ts +9 -0
  109. package/dist/services/built-in/tell-joke/index.js +31 -0
  110. package/dist/services/built-in/translate/index.d.ts +6 -0
  111. package/dist/services/built-in/translate/index.js +21 -0
  112. package/dist/services/built-in/web-research/index.d.ts +9 -0
  113. package/dist/services/built-in/web-research/index.js +51 -0
  114. package/dist/services/index.d.ts +13 -0
  115. package/dist/services/index.js +14 -0
  116. package/dist/services/loader.d.ts +77 -0
  117. package/dist/services/loader.js +292 -0
  118. package/dist/services/manager.d.ts +86 -0
  119. package/dist/services/manager.js +255 -0
  120. package/dist/services/registry.d.ts +98 -0
  121. package/dist/services/registry.js +204 -0
  122. package/dist/services/types.d.ts +230 -0
  123. package/dist/services/types.js +30 -0
  124. package/dist/test/cli.test.d.ts +7 -0
  125. package/dist/test/cli.test.js +329 -0
  126. package/dist/test/comprehensive-overlay.test.d.ts +13 -0
  127. package/dist/test/comprehensive-overlay.test.js +593 -0
  128. package/dist/test/key-derivation.test.d.ts +12 -0
  129. package/dist/test/key-derivation.test.js +86 -0
  130. package/dist/test/overlay-submit.test.d.ts +10 -0
  131. package/dist/test/overlay-submit.test.js +460 -0
  132. package/dist/test/request-response-flow.test.d.ts +5 -0
  133. package/dist/test/request-response-flow.test.js +209 -0
  134. package/dist/test/service-system.test.d.ts +5 -0
  135. package/dist/test/service-system.test.js +190 -0
  136. package/dist/test/utils/server-logic.d.ts +98 -0
  137. package/dist/test/utils/server-logic.js +286 -0
  138. package/dist/test/wallet.test.d.ts +7 -0
  139. package/dist/test/wallet.test.js +146 -0
  140. package/index.ts +1965 -0
  141. package/openclaw.plugin.json +106 -0
  142. package/package.json +73 -0
  143. package/src/cli-main.ts +230 -0
  144. package/src/cli.ts +16 -0
  145. package/src/core/README.md +246 -0
  146. package/src/core/config.ts +21 -0
  147. package/src/core/index.ts +42 -0
  148. package/src/core/payment.ts +111 -0
  149. package/src/core/types.ts +102 -0
  150. package/src/core/verify.ts +119 -0
  151. package/src/core/wallet.ts +282 -0
  152. package/src/scripts/baemail/commands.ts +326 -0
  153. package/src/scripts/baemail/handler.ts +338 -0
  154. package/src/scripts/baemail/index.ts +6 -0
  155. package/src/scripts/config.ts +81 -0
  156. package/src/scripts/index.ts +8 -0
  157. package/src/scripts/messaging/connect.ts +121 -0
  158. package/src/scripts/messaging/handlers.ts +394 -0
  159. package/src/scripts/messaging/inbox.ts +64 -0
  160. package/src/scripts/messaging/index.ts +9 -0
  161. package/src/scripts/messaging/poll.ts +59 -0
  162. package/src/scripts/messaging/send.ts +54 -0
  163. package/src/scripts/output.ts +21 -0
  164. package/src/scripts/overlay/discover.ts +81 -0
  165. package/src/scripts/overlay/index.ts +8 -0
  166. package/src/scripts/overlay/registration.ts +199 -0
  167. package/src/scripts/overlay/services.ts +199 -0
  168. package/src/scripts/overlay/transaction.ts +124 -0
  169. package/src/scripts/payment/build.ts +65 -0
  170. package/src/scripts/payment/commands.ts +92 -0
  171. package/src/scripts/payment/index.ts +7 -0
  172. package/src/scripts/payment/types.ts +62 -0
  173. package/src/scripts/services/index.ts +7 -0
  174. package/src/scripts/services/queue.ts +35 -0
  175. package/src/scripts/services/request.ts +98 -0
  176. package/src/scripts/services/respond.ts +149 -0
  177. package/src/scripts/types.ts +121 -0
  178. package/src/scripts/utils/index.ts +7 -0
  179. package/src/scripts/utils/merkle.ts +57 -0
  180. package/src/scripts/utils/storage.ts +231 -0
  181. package/src/scripts/utils/woc.ts +106 -0
  182. package/src/scripts/wallet/balance.ts +277 -0
  183. package/src/scripts/wallet/identity.ts +203 -0
  184. package/src/scripts/wallet/index.ts +7 -0
  185. package/src/scripts/wallet/setup.ts +121 -0
  186. package/src/scripts/x-verification/commands.ts +256 -0
  187. package/src/scripts/x-verification/index.ts +5 -0
  188. package/src/services/built-in/api-proxy/index.ts +26 -0
  189. package/src/services/built-in/api-proxy/prompt.md +26 -0
  190. package/src/services/built-in/code-develop/index.ts +26 -0
  191. package/src/services/built-in/code-develop/prompt.md +35 -0
  192. package/src/services/built-in/code-review/index.ts +54 -0
  193. package/src/services/built-in/code-review/prompt.md +105 -0
  194. package/src/services/built-in/image-analysis/index.ts +36 -0
  195. package/src/services/built-in/image-analysis/prompt.md +42 -0
  196. package/src/services/built-in/memory-store/index.ts +25 -0
  197. package/src/services/built-in/memory-store/prompt.md +45 -0
  198. package/src/services/built-in/roulette/index.ts +30 -0
  199. package/src/services/built-in/roulette/prompt.md +35 -0
  200. package/src/services/built-in/summarize/index.ts +24 -0
  201. package/src/services/built-in/summarize/prompt.md +27 -0
  202. package/src/services/built-in/tell-joke/handler.ts +134 -0
  203. package/src/services/built-in/tell-joke/index.ts +34 -0
  204. package/src/services/built-in/tell-joke/prompt.md +59 -0
  205. package/src/services/built-in/translate/index.ts +24 -0
  206. package/src/services/built-in/translate/prompt.md +23 -0
  207. package/src/services/built-in/web-research/index.ts +54 -0
  208. package/src/services/built-in/web-research/prompt.md +110 -0
  209. package/src/services/index.ts +16 -0
  210. package/src/services/loader.ts +344 -0
  211. package/src/services/manager.ts +304 -0
  212. package/src/services/registry.ts +246 -0
  213. package/src/services/types.ts +259 -0
  214. package/src/test/cli.test.ts +352 -0
  215. package/src/test/comprehensive-overlay.test.ts +729 -0
  216. package/src/test/key-derivation.test.ts +102 -0
  217. package/src/test/overlay-submit.test.ts +570 -0
  218. package/src/test/request-response-flow.test.ts +252 -0
  219. package/src/test/service-system.test.ts +241 -0
  220. package/src/test/utils/server-logic.ts +368 -0
  221. package/src/test/wallet.test.ts +166 -0
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Unit tests for overlay /submit endpoint compatibility.
3
+ *
4
+ * These tests validate that the client constructs BEEF and payloads
5
+ * in the exact format expected by the clawdbot-overlay server's
6
+ * topic managers using PushDrop tokens.
7
+ *
8
+ * Run with: npx tsx src/test/overlay-submit.test.ts
9
+ */
10
+ export {};
@@ -0,0 +1,460 @@
1
+ /**
2
+ * Unit tests for overlay /submit endpoint compatibility.
3
+ *
4
+ * These tests validate that the client constructs BEEF and payloads
5
+ * in the exact format expected by the clawdbot-overlay server's
6
+ * topic managers using PushDrop tokens.
7
+ *
8
+ * Run with: npx tsx src/test/overlay-submit.test.ts
9
+ */
10
+ import { Beef, Transaction, PrivateKey, P2PKH, LockingScript, OP, PushDrop } from '@bsv/sdk';
11
+ const PROTOCOL_ID = 'clawdbot-overlay-v1';
12
+ /**
13
+ * Extract data fields from a PushDrop script using the SDK's decode method.
14
+ */
15
+ function extractPushDropFields(script) {
16
+ try {
17
+ const decoded = PushDrop.decode(script);
18
+ return decoded.fields;
19
+ }
20
+ catch {
21
+ return null;
22
+ }
23
+ }
24
+ /**
25
+ * Parse identity output using PushDrop decode and server's validation logic.
26
+ */
27
+ function parseIdentityOutput(script) {
28
+ const fields = extractPushDropFields(script);
29
+ if (!fields || fields.length < 1)
30
+ return null;
31
+ try {
32
+ const payload = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
33
+ // Server validation rules
34
+ if (payload.protocol !== PROTOCOL_ID)
35
+ return null;
36
+ if (payload.type !== 'identity')
37
+ return null;
38
+ if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey))
39
+ return null;
40
+ if (typeof payload.name !== 'string' || payload.name.length === 0)
41
+ return null;
42
+ if (!Array.isArray(payload.capabilities))
43
+ return null;
44
+ if (typeof payload.timestamp !== 'string')
45
+ return null;
46
+ return payload;
47
+ }
48
+ catch {
49
+ return null;
50
+ }
51
+ }
52
+ /**
53
+ * Parse service output using PushDrop decode and server's validation logic.
54
+ */
55
+ function parseServiceOutput(script) {
56
+ const fields = extractPushDropFields(script);
57
+ if (!fields || fields.length < 1)
58
+ return null;
59
+ try {
60
+ const payload = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
61
+ // Server validation rules
62
+ if (payload.protocol !== PROTOCOL_ID)
63
+ return null;
64
+ if (payload.type !== 'service')
65
+ return null;
66
+ if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey))
67
+ return null;
68
+ if (typeof payload.serviceId !== 'string' || payload.serviceId.length === 0)
69
+ return null;
70
+ if (typeof payload.name !== 'string' || payload.name.length === 0)
71
+ return null;
72
+ if (!payload.pricing || typeof payload.pricing.amountSats !== 'number')
73
+ return null;
74
+ if (typeof payload.timestamp !== 'string')
75
+ return null;
76
+ return payload;
77
+ }
78
+ catch {
79
+ return null;
80
+ }
81
+ }
82
+ /**
83
+ * Create a minimally encoded push chunk for script building.
84
+ */
85
+ function createPushChunk(data) {
86
+ if (data.length === 0) {
87
+ return { op: 0 };
88
+ }
89
+ if (data.length === 1 && data[0] === 0) {
90
+ return { op: 0 };
91
+ }
92
+ if (data.length === 1 && data[0] > 0 && data[0] <= 16) {
93
+ return { op: 0x50 + data[0] };
94
+ }
95
+ if (data.length <= 75) {
96
+ return { op: data.length, data };
97
+ }
98
+ if (data.length <= 255) {
99
+ return { op: 0x4c, data };
100
+ }
101
+ if (data.length <= 65535) {
102
+ return { op: 0x4d, data };
103
+ }
104
+ return { op: 0x4e, data };
105
+ }
106
+ /**
107
+ * Build a PushDrop-style locking script with JSON payload.
108
+ * Format: <pubkey> OP_CHECKSIG <jsonBytes> OP_DROP
109
+ */
110
+ function buildPushDropScript(privKey, payload) {
111
+ const pubKey = privKey.toPublicKey();
112
+ const pubKeyBytes = pubKey.toDER();
113
+ const jsonBytes = Array.from(new TextEncoder().encode(JSON.stringify(payload)));
114
+ const chunks = [];
115
+ // P2PK lock: <pubkey> OP_CHECKSIG
116
+ chunks.push({ op: pubKeyBytes.length, data: pubKeyBytes });
117
+ chunks.push({ op: OP.OP_CHECKSIG });
118
+ // Data field: <jsonBytes>
119
+ chunks.push(createPushChunk(jsonBytes));
120
+ // OP_DROP to clean stack
121
+ chunks.push({ op: OP.OP_DROP });
122
+ return new LockingScript(chunks);
123
+ }
124
+ /**
125
+ * Simulate the server's identifyAdmissibleOutputs logic.
126
+ */
127
+ function identifyAdmissibleOutputs(beef, type) {
128
+ // Parse BEEF and get the newest (subject) transaction
129
+ const parsedBeef = Beef.fromBinary(beef);
130
+ const subjectTx = parsedBeef.txs[0]._tx;
131
+ if (!subjectTx) {
132
+ return { outputsToAdmit: [], coinsToRetain: [] };
133
+ }
134
+ const outputsToAdmit = [];
135
+ for (let i = 0; i < subjectTx.outputs.length; i++) {
136
+ const output = subjectTx.outputs[i];
137
+ if (output.lockingScript) {
138
+ const parsed = type === 'identity'
139
+ ? parseIdentityOutput(output.lockingScript)
140
+ : parseServiceOutput(output.lockingScript);
141
+ if (parsed !== null) {
142
+ outputsToAdmit.push(i);
143
+ }
144
+ }
145
+ }
146
+ return { outputsToAdmit, coinsToRetain: [] };
147
+ }
148
+ // ============================================================================
149
+ // Test utilities
150
+ // ============================================================================
151
+ let testsPassed = 0;
152
+ let testsFailed = 0;
153
+ function assert(condition, message) {
154
+ if (!condition) {
155
+ console.error(`❌ FAIL: ${message}`);
156
+ testsFailed++;
157
+ throw new Error(message);
158
+ }
159
+ console.log(`✅ PASS: ${message}`);
160
+ testsPassed++;
161
+ }
162
+ function assertThrows(fn, message) {
163
+ try {
164
+ fn();
165
+ console.error(`❌ FAIL: ${message} (expected to throw)`);
166
+ testsFailed++;
167
+ }
168
+ catch {
169
+ console.log(`✅ PASS: ${message}`);
170
+ testsPassed++;
171
+ }
172
+ }
173
+ // ============================================================================
174
+ // Test: BEEF format validation
175
+ // ============================================================================
176
+ async function testBeefFormat() {
177
+ console.log('\n=== Test: BEEF Format Validation ===');
178
+ // Create a minimal transaction chain
179
+ const privKey = PrivateKey.fromRandom();
180
+ const pubKeyHash = privKey.toPublicKey().toHash();
181
+ // Source transaction (simulating a mined tx with merkle proof)
182
+ const sourceTx = new Transaction();
183
+ sourceTx.addOutput({
184
+ lockingScript: new P2PKH().lock(pubKeyHash),
185
+ satoshis: 10000,
186
+ });
187
+ // Spending transaction with PushDrop output
188
+ const tx = new Transaction();
189
+ tx.addInput({
190
+ sourceTransaction: sourceTx,
191
+ sourceOutputIndex: 0,
192
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
193
+ });
194
+ tx.addOutput({
195
+ lockingScript: buildPushDropScript(privKey, { protocol: PROTOCOL_ID, type: 'identity', test: true }),
196
+ satoshis: 1,
197
+ });
198
+ await tx.sign();
199
+ // Build BEEF
200
+ const beef = new Beef();
201
+ beef.mergeTransaction(tx);
202
+ const binary = beef.toBinary();
203
+ // Validate BEEF magic bytes
204
+ const magic = binary.slice(0, 4);
205
+ const magicHex = magic.map(b => b.toString(16).padStart(2, '0')).join('');
206
+ assert(magicHex === '0100beef' || magicHex === '0200beef', `BEEF magic bytes should be 0100beef or 0200beef, got ${magicHex}`);
207
+ // Validate BEEF can be parsed
208
+ const parsed = Beef.fromBinary(binary);
209
+ assert(parsed.txs.length >= 1, `BEEF should contain at least 1 transaction, got ${parsed.txs.length}`);
210
+ // Validate the newest transaction can be found in BEEF
211
+ const beefTx = parsed.txs[0];
212
+ const newestTxid = beefTx.txid || beefTx._tx?.id('hex');
213
+ assert(newestTxid === tx.id('hex'), `Newest transaction in BEEF should match original, got ${newestTxid?.slice(0, 16)}`);
214
+ }
215
+ // ============================================================================
216
+ // Test: Identity payload validation
217
+ // ============================================================================
218
+ async function testIdentityPayload() {
219
+ console.log('\n=== Test: Identity Payload Validation ===');
220
+ const privKey = PrivateKey.fromRandom();
221
+ const identityKey = privKey.toPublicKey().toString();
222
+ // Valid identity payload
223
+ const validPayload = {
224
+ protocol: PROTOCOL_ID,
225
+ type: 'identity',
226
+ identityKey,
227
+ name: 'test-agent',
228
+ description: 'A test agent',
229
+ channels: { overlay: 'https://example.com' },
230
+ capabilities: ['test'],
231
+ timestamp: new Date().toISOString(),
232
+ };
233
+ const script = buildPushDropScript(privKey, validPayload);
234
+ const parsed = parseIdentityOutput(script);
235
+ assert(parsed !== null, 'Valid identity payload should be parsed');
236
+ assert(parsed.identityKey === identityKey, 'Identity key should match');
237
+ assert(parsed.name === 'test-agent', 'Name should match');
238
+ assert(parsed.type === 'identity', 'Type should be identity');
239
+ // Invalid: wrong protocol
240
+ const wrongProtocol = { ...validPayload, protocol: 'wrong-protocol' };
241
+ const script2 = buildPushDropScript(privKey, wrongProtocol);
242
+ assert(parseIdentityOutput(script2) === null, 'Wrong protocol should be rejected');
243
+ // Invalid: wrong type
244
+ const wrongType = { ...validPayload, type: 'service' };
245
+ const script3 = buildPushDropScript(privKey, wrongType);
246
+ assert(parseIdentityOutput(script3) === null, 'Wrong type should be rejected');
247
+ // Invalid: bad identity key
248
+ const badKey = { ...validPayload, identityKey: 'not-a-valid-key' };
249
+ const script4 = buildPushDropScript(privKey, badKey);
250
+ assert(parseIdentityOutput(script4) === null, 'Invalid identity key should be rejected');
251
+ // Invalid: empty name
252
+ const emptyName = { ...validPayload, name: '' };
253
+ const script5 = buildPushDropScript(privKey, emptyName);
254
+ assert(parseIdentityOutput(script5) === null, 'Empty name should be rejected');
255
+ // Invalid: capabilities not array
256
+ const badCaps = { ...validPayload, capabilities: 'not-array' };
257
+ const script6 = buildPushDropScript(privKey, badCaps);
258
+ assert(parseIdentityOutput(script6) === null, 'Non-array capabilities should be rejected');
259
+ }
260
+ // ============================================================================
261
+ // Test: Service payload validation
262
+ // ============================================================================
263
+ async function testServicePayload() {
264
+ console.log('\n=== Test: Service Payload Validation ===');
265
+ const privKey = PrivateKey.fromRandom();
266
+ const identityKey = privKey.toPublicKey().toString();
267
+ // Valid service payload
268
+ const validPayload = {
269
+ protocol: PROTOCOL_ID,
270
+ type: 'service',
271
+ identityKey,
272
+ serviceId: 'test-service',
273
+ name: 'Test Service',
274
+ description: 'A test service',
275
+ pricing: { model: 'per-task', amountSats: 100 },
276
+ timestamp: new Date().toISOString(),
277
+ };
278
+ const script = buildPushDropScript(privKey, validPayload);
279
+ const parsed = parseServiceOutput(script);
280
+ assert(parsed !== null, 'Valid service payload should be parsed');
281
+ assert(parsed.serviceId === 'test-service', 'Service ID should match');
282
+ assert(parsed.pricing.amountSats === 100, 'Price should match');
283
+ // Invalid: missing pricing
284
+ const noPricing = { ...validPayload, pricing: undefined };
285
+ const script2 = buildPushDropScript(privKey, noPricing);
286
+ assert(parseServiceOutput(script2) === null, 'Missing pricing should be rejected');
287
+ // Invalid: empty serviceId
288
+ const emptyId = { ...validPayload, serviceId: '' };
289
+ const script3 = buildPushDropScript(privKey, emptyId);
290
+ assert(parseServiceOutput(script3) === null, 'Empty serviceId should be rejected');
291
+ }
292
+ // ============================================================================
293
+ // Test: Full BEEF submission simulation
294
+ // ============================================================================
295
+ async function testBeefSubmission() {
296
+ console.log('\n=== Test: BEEF Submission Simulation ===');
297
+ const privKey = PrivateKey.fromRandom();
298
+ const pubKeyHash = privKey.toPublicKey().toHash();
299
+ const identityKey = privKey.toPublicKey().toString();
300
+ // Create source transaction (simulating confirmed tx)
301
+ const sourceTx = new Transaction();
302
+ sourceTx.addOutput({
303
+ lockingScript: new P2PKH().lock(pubKeyHash),
304
+ satoshis: 10000,
305
+ });
306
+ // Valid identity registration
307
+ const identityPayload = {
308
+ protocol: PROTOCOL_ID,
309
+ type: 'identity',
310
+ identityKey,
311
+ name: 'test-agent',
312
+ description: 'Test agent for unit tests',
313
+ channels: { overlay: 'https://clawoverlay.com' },
314
+ capabilities: ['testing'],
315
+ timestamp: new Date().toISOString(),
316
+ };
317
+ const tx = new Transaction();
318
+ tx.addInput({
319
+ sourceTransaction: sourceTx,
320
+ sourceOutputIndex: 0,
321
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
322
+ });
323
+ tx.addOutput({
324
+ lockingScript: buildPushDropScript(privKey, identityPayload),
325
+ satoshis: 1,
326
+ });
327
+ tx.addOutput({
328
+ lockingScript: new P2PKH().lock(pubKeyHash),
329
+ satoshis: 9900,
330
+ });
331
+ await tx.sign();
332
+ // Build BEEF with ancestry
333
+ const beef = new Beef();
334
+ beef.mergeTransaction(tx);
335
+ const beefBinary = beef.toBinary();
336
+ // Simulate server's topic manager
337
+ const result = identifyAdmissibleOutputs(beefBinary, 'identity');
338
+ assert(result.outputsToAdmit.length === 1, `Should admit 1 output, got ${result.outputsToAdmit.length}`);
339
+ assert(result.outputsToAdmit[0] === 0, 'Should admit output index 0 (PushDrop)');
340
+ }
341
+ // ============================================================================
342
+ // Test: Chained transactions (stored BEEF)
343
+ // ============================================================================
344
+ async function testChainedBeef() {
345
+ console.log('\n=== Test: Chained BEEF (multiple unconfirmed txs) ===');
346
+ const privKey = PrivateKey.fromRandom();
347
+ const pubKeyHash = privKey.toPublicKey().toHash();
348
+ const identityKey = privKey.toPublicKey().toString();
349
+ // Grandparent tx (simulating mined tx - would have merkle proof)
350
+ const grandparentTx = new Transaction();
351
+ grandparentTx.addOutput({
352
+ lockingScript: new P2PKH().lock(pubKeyHash),
353
+ satoshis: 100000,
354
+ });
355
+ // Parent tx (first overlay submission - unconfirmed)
356
+ const parentTx = new Transaction();
357
+ parentTx.addInput({
358
+ sourceTransaction: grandparentTx,
359
+ sourceOutputIndex: 0,
360
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
361
+ });
362
+ parentTx.addOutput({
363
+ lockingScript: buildPushDropScript(privKey, {
364
+ protocol: PROTOCOL_ID,
365
+ type: 'identity',
366
+ identityKey,
367
+ name: 'parent-tx',
368
+ description: 'First registration',
369
+ channels: {},
370
+ capabilities: [],
371
+ timestamp: new Date().toISOString(),
372
+ }),
373
+ satoshis: 1,
374
+ });
375
+ parentTx.addOutput({
376
+ lockingScript: new P2PKH().lock(pubKeyHash),
377
+ satoshis: 99900,
378
+ });
379
+ await parentTx.sign();
380
+ // Child tx (second overlay submission - spending parent's change)
381
+ const childTx = new Transaction();
382
+ childTx.addInput({
383
+ sourceTransaction: parentTx,
384
+ sourceOutputIndex: 1, // Spend the change output
385
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
386
+ });
387
+ childTx.addOutput({
388
+ lockingScript: buildPushDropScript(privKey, {
389
+ protocol: PROTOCOL_ID,
390
+ type: 'service',
391
+ identityKey,
392
+ serviceId: 'test-svc',
393
+ name: 'Test Service',
394
+ description: 'Service from child tx',
395
+ pricing: { model: 'per-task', amountSats: 50 },
396
+ timestamp: new Date().toISOString(),
397
+ }),
398
+ satoshis: 1,
399
+ });
400
+ childTx.addOutput({
401
+ lockingScript: new P2PKH().lock(pubKeyHash),
402
+ satoshis: 99800,
403
+ });
404
+ await childTx.sign();
405
+ // Build BEEF - should include full chain
406
+ const beef = new Beef();
407
+ beef.mergeTransaction(childTx);
408
+ const beefBinary = beef.toBinary();
409
+ // Verify BEEF contains all transactions
410
+ const parsedBeef = Beef.fromBinary(beefBinary);
411
+ assert(parsedBeef.txs.length >= 2, `BEEF should contain at least 2 txs for chain, got ${parsedBeef.txs.length}`);
412
+ // Verify child tx is the newest in BEEF
413
+ const beefTx = parsedBeef.txs[0];
414
+ const newestTxid = beefTx.txid || beefTx._tx?.id('hex');
415
+ assert(newestTxid === childTx.id('hex'), 'Newest tx in BEEF should be the child transaction');
416
+ // Simulate server validation
417
+ const result = identifyAdmissibleOutputs(beefBinary, 'service');
418
+ assert(result.outputsToAdmit.length === 1, 'Should admit the service output');
419
+ }
420
+ // ============================================================================
421
+ // Test: Invalid BEEF handling
422
+ // ============================================================================
423
+ async function testInvalidBeef() {
424
+ console.log('\n=== Test: Invalid BEEF Handling ===');
425
+ // Empty BEEF
426
+ const emptyBeef = new Beef();
427
+ const emptyBinary = emptyBeef.toBinary();
428
+ assertThrows(() => Transaction.fromBEEF(emptyBinary), 'Empty BEEF should throw when extracting transaction');
429
+ // Malformed BEEF (random bytes)
430
+ const garbage = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
431
+ assertThrows(() => Beef.fromBinary(Array.from(garbage)), 'Garbage bytes should throw when parsing BEEF');
432
+ // BEEF with wrong magic
433
+ const wrongMagic = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF, 0, 0]);
434
+ assertThrows(() => Beef.fromBinary(Array.from(wrongMagic)), 'Wrong magic bytes should throw when parsing BEEF');
435
+ }
436
+ // ============================================================================
437
+ // Main test runner
438
+ // ============================================================================
439
+ async function runTests() {
440
+ console.log('Starting overlay submit tests (PushDrop format)...\n');
441
+ try {
442
+ await testBeefFormat();
443
+ await testIdentityPayload();
444
+ await testServicePayload();
445
+ await testBeefSubmission();
446
+ await testChainedBeef();
447
+ await testInvalidBeef();
448
+ console.log(`\n========================================`);
449
+ console.log(`Tests completed: ${testsPassed} passed, ${testsFailed} failed`);
450
+ console.log(`========================================`);
451
+ if (testsFailed > 0) {
452
+ process.exit(1);
453
+ }
454
+ }
455
+ catch (e) {
456
+ console.error('\nTest suite failed:', e);
457
+ process.exit(1);
458
+ }
459
+ }
460
+ runTests();
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Tests for request/response flow and duplicate prevention.
3
+ */
4
+ declare function run(): Promise<void>;
5
+ export { run };
@@ -0,0 +1,209 @@
1
+ /**
2
+ * Tests for request/response flow and duplicate prevention.
3
+ */
4
+ import fs from 'node:fs';
5
+ import path from 'node:path';
6
+ // Simple test runner (matching existing pattern)
7
+ let passed = 0;
8
+ let failed = 0;
9
+ function test(name, fn) {
10
+ return (async () => {
11
+ try {
12
+ await fn();
13
+ console.log(` ✓ ${name}`);
14
+ passed++;
15
+ }
16
+ catch (err) {
17
+ const msg = err instanceof Error ? err.message : String(err);
18
+ console.log(` ✗ ${name}`);
19
+ console.log(` ${msg}`);
20
+ failed++;
21
+ }
22
+ })();
23
+ }
24
+ function assert(condition, message) {
25
+ if (!condition)
26
+ throw new Error(`Assertion failed: ${message}`);
27
+ }
28
+ // Mock paths for testing
29
+ const TEST_DIR = path.join(process.cwd(), 'test-data');
30
+ const TEST_PATHS = {
31
+ serviceQueue: path.join(TEST_DIR, 'service-queue.jsonl'),
32
+ walletDir: path.join(TEST_DIR, 'wallet'),
33
+ registration: path.join(TEST_DIR, 'registration.json'),
34
+ services: path.join(TEST_DIR, 'services.json'),
35
+ };
36
+ function setupTestEnv() {
37
+ // Setup test directory
38
+ if (fs.existsSync(TEST_DIR)) {
39
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
40
+ }
41
+ fs.mkdirSync(TEST_DIR, { recursive: true });
42
+ }
43
+ function cleanupTestEnv() {
44
+ if (fs.existsSync(TEST_DIR)) {
45
+ fs.rmSync(TEST_DIR, { recursive: true, force: true });
46
+ }
47
+ }
48
+ async function run() {
49
+ console.log('\nRequest/Response Flow Tests\n');
50
+ // ── Queue Cleanup Tests ──────────────────────────────────────────────
51
+ await test('cleanupServiceQueue removes old fulfilled entries', async () => {
52
+ setupTestEnv();
53
+ const now = Date.now();
54
+ const oldTime = now - (3 * 60 * 60 * 1000); // 3 hours ago
55
+ const recentTime = now - (30 * 60 * 1000); // 30 minutes ago
56
+ // Add test entries
57
+ const entries = [
58
+ { status: 'pending', requestId: 'pending-1', _ts: recentTime },
59
+ { status: 'fulfilled', requestId: 'old-fulfilled', _ts: oldTime },
60
+ { status: 'fulfilled', requestId: 'recent-fulfilled', _ts: recentTime },
61
+ { status: 'rejected', requestId: 'old-rejected', _ts: oldTime }
62
+ ];
63
+ const content = entries.map(e => JSON.stringify(e)).join('\n') + '\n';
64
+ fs.writeFileSync(TEST_PATHS.serviceQueue, content);
65
+ // Mock PATHS for cleanupServiceQueue
66
+ const originalPaths = await import('../scripts/config.js');
67
+ const mockPaths = { ...originalPaths.PATHS, serviceQueue: TEST_PATHS.serviceQueue };
68
+ // Temporarily replace PATHS
69
+ globalThis.mockPaths = mockPaths;
70
+ // Redefine cleanupServiceQueue with mocked paths
71
+ function mockCleanupServiceQueue(maxAgeMs = 24 * 60 * 60 * 1000, finalStatusMaxAgeMs = 2 * 60 * 60 * 1000) {
72
+ if (!fs.existsSync(TEST_PATHS.serviceQueue))
73
+ return;
74
+ const currentTime = Date.now();
75
+ const finalStatuses = ['fulfilled', 'rejected', 'delivery_failed', 'failed', 'error'];
76
+ const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
77
+ const keptLines = [];
78
+ let removedCount = 0;
79
+ for (const line of lines) {
80
+ try {
81
+ const entry = JSON.parse(line);
82
+ const entryAge = currentTime - (entry._ts || 0);
83
+ // Always keep pending entries that aren't too old
84
+ if (entry.status === 'pending' && entryAge < maxAgeMs) {
85
+ keptLines.push(line);
86
+ continue;
87
+ }
88
+ // Keep final status entries only if they're recent
89
+ if (finalStatuses.includes(entry.status) && entryAge < finalStatusMaxAgeMs) {
90
+ keptLines.push(line);
91
+ continue;
92
+ }
93
+ // Remove this entry
94
+ removedCount++;
95
+ }
96
+ catch {
97
+ // Keep malformed entries to avoid data loss
98
+ keptLines.push(line);
99
+ }
100
+ }
101
+ if (removedCount > 0) {
102
+ fs.writeFileSync(TEST_PATHS.serviceQueue, keptLines.join('\n') + (keptLines.length ? '\n' : ''));
103
+ }
104
+ }
105
+ // Run cleanup with 2 hour limit for final statuses
106
+ mockCleanupServiceQueue(24 * 60 * 60 * 1000, 2 * 60 * 60 * 1000);
107
+ // Check remaining entries
108
+ const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
109
+ const remaining = lines.map(line => JSON.parse(line));
110
+ assert(remaining.length === 2, `Expected 2 remaining entries, got ${remaining.length}`);
111
+ assert(remaining.find(e => e.requestId === 'pending-1') !== undefined, 'Should keep recent pending');
112
+ assert(remaining.find(e => e.requestId === 'recent-fulfilled') !== undefined, 'Should keep recent fulfilled');
113
+ assert(remaining.find(e => e.requestId === 'old-fulfilled') === undefined, 'Should remove old fulfilled');
114
+ assert(remaining.find(e => e.requestId === 'old-rejected') === undefined, 'Should remove old rejected');
115
+ cleanupTestEnv();
116
+ });
117
+ await test('updateServiceQueueStatus updates request status atomically', async () => {
118
+ setupTestEnv();
119
+ // Add a test entry
120
+ const entry = {
121
+ status: 'pending',
122
+ requestId: 'test-request-456',
123
+ serviceId: 'test-service',
124
+ from: 'sender-key',
125
+ _ts: Date.now()
126
+ };
127
+ fs.writeFileSync(TEST_PATHS.serviceQueue, JSON.stringify(entry) + '\n');
128
+ // Mock updateServiceQueueStatus with test paths
129
+ function mockUpdateServiceQueueStatus(requestId, newStatus, additionalFields = {}) {
130
+ if (!fs.existsSync(TEST_PATHS.serviceQueue))
131
+ return false;
132
+ const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
133
+ let updated = false;
134
+ const updatedLines = lines.map(line => {
135
+ try {
136
+ const entryData = JSON.parse(line);
137
+ if (entryData.requestId === requestId) {
138
+ updated = true;
139
+ return JSON.stringify({
140
+ ...entryData,
141
+ status: newStatus,
142
+ ...additionalFields,
143
+ updatedAt: Date.now()
144
+ });
145
+ }
146
+ return line;
147
+ }
148
+ catch {
149
+ return line;
150
+ }
151
+ });
152
+ if (updated) {
153
+ fs.writeFileSync(TEST_PATHS.serviceQueue, updatedLines.join('\n') + '\n');
154
+ }
155
+ return updated;
156
+ }
157
+ // Update status
158
+ const updated = mockUpdateServiceQueueStatus('test-request-456', 'fulfilled', {
159
+ fulfilledAt: Date.now(),
160
+ result: { message: 'success' }
161
+ });
162
+ assert(updated === true, 'Should return true for successful update');
163
+ // Verify update
164
+ const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
165
+ const updatedEntry = JSON.parse(lines[0]);
166
+ assert(updatedEntry.status === 'fulfilled', 'Status should be updated to fulfilled');
167
+ assert(updatedEntry.requestId === 'test-request-456', 'Request ID should remain the same');
168
+ assert(updatedEntry.fulfilledAt !== undefined, 'Should have fulfilledAt timestamp');
169
+ assert(updatedEntry.updatedAt !== undefined, 'Should have updatedAt timestamp');
170
+ assert(updatedEntry.result?.message === 'success', 'Should have result data');
171
+ cleanupTestEnv();
172
+ });
173
+ await test('updateServiceQueueStatus returns false for non-existent request', async () => {
174
+ setupTestEnv();
175
+ // Mock updateServiceQueueStatus with test paths
176
+ function mockUpdateServiceQueueStatus(requestId, _newStatus) {
177
+ if (!fs.existsSync(TEST_PATHS.serviceQueue))
178
+ return false;
179
+ const lines = fs.readFileSync(TEST_PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
180
+ let updated = false;
181
+ lines.map(line => {
182
+ try {
183
+ const entry = JSON.parse(line);
184
+ if (entry.requestId === requestId) {
185
+ updated = true;
186
+ }
187
+ return line;
188
+ }
189
+ catch {
190
+ return line;
191
+ }
192
+ });
193
+ return updated;
194
+ }
195
+ const updated = mockUpdateServiceQueueStatus('non-existent-request', 'fulfilled');
196
+ assert(updated === false, 'Should return false for non-existent request');
197
+ cleanupTestEnv();
198
+ });
199
+ // ── Summary ──────────────────────────────────────────────────────────
200
+ console.log(`\n${passed} passed, ${failed} failed\n`);
201
+ if (failed > 0) {
202
+ process.exit(1);
203
+ }
204
+ }
205
+ // Run tests if this file is executed directly
206
+ if (import.meta.url === `file://${process.argv[1]}`) {
207
+ run().catch(console.error);
208
+ }
209
+ export { run };
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Service system comprehensive tests.
3
+ * Tests service registration, loading, validation, and execution.
4
+ */
5
+ export {};