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,102 @@
1
+ /**
2
+ * Unit tests for key derivation consistency.
3
+ *
4
+ * CRITICAL: These tests ensure that transaction signing uses the correct
5
+ * child private key that matches the derived address.
6
+ *
7
+ * Bug history: Initially, code was deriving a child address using BRC-29
8
+ * but signing with the root private key, causing OP_EQUALVERIFY failures.
9
+ *
10
+ * Run: node dist/test/key-derivation.test.js
11
+ */
12
+
13
+ import { PrivateKey, Transaction, P2PKH, CachedKeyDeriver, Utils } from '@bsv/sdk';
14
+ import { brc29ProtocolID } from '@bsv/wallet-toolbox';
15
+
16
+ async function assert(condition: boolean, message: string) {
17
+ if (!condition) {
18
+ throw new Error(`Assertion failed: ${message}`);
19
+ }
20
+ }
21
+
22
+ async function runTests() {
23
+ console.log('🧪 Running Key Derivation Tests...\n');
24
+
25
+ // Test setup
26
+ const rootPrivKey = PrivateKey.fromRandom();
27
+ const keyDeriver = new CachedKeyDeriver(rootPrivKey);
28
+ const derivationPrefix = Utils.toBase64(Array.from(Utils.toArray('import', 'utf8')));
29
+ const derivationSuffix = Utils.toBase64(Array.from(Utils.toArray('now', 'utf8')));
30
+ const keyString = `${derivationPrefix} ${derivationSuffix}`;
31
+
32
+ const childPrivKey = keyDeriver.derivePrivateKey(brc29ProtocolID, keyString, 'self');
33
+ const pubKey = keyDeriver.derivePublicKey(brc29ProtocolID, keyString, 'self', true);
34
+ const derivedAddress = pubKey.toAddress();
35
+ const hashResult = pubKey.toHash();
36
+ const derivedHash160 = typeof hashResult === 'string'
37
+ ? new Uint8Array(hashResult.match(/.{2}/g)!.map(h => parseInt(h, 16)))
38
+ : new Uint8Array(hashResult);
39
+
40
+ // Test 1: Consistency
41
+ console.log('✓ Test 1: Derived keys are consistent');
42
+ const keyDeriver2 = new CachedKeyDeriver(rootPrivKey);
43
+ const childPrivKey2 = keyDeriver2.derivePrivateKey(brc29ProtocolID, keyString, 'self');
44
+ await assert(childPrivKey.toHex() === childPrivKey2.toHex(), 'Child keys should be identical');
45
+
46
+ // Test 2: Child key matches derived address
47
+ console.log('✓ Test 2: Child private key matches derived address');
48
+ const childPubKey = childPrivKey.toPublicKey();
49
+ const childAddress = childPubKey.toAddress();
50
+ await assert(childAddress === derivedAddress, 'Child key address should match derived address');
51
+
52
+ // Test 3: Root key does NOT match (critical!)
53
+ console.log('✓ Test 3: CRITICAL - Root key does NOT match derived address');
54
+ const rootAddress = rootPrivKey.toPublicKey().toAddress();
55
+ await assert(rootAddress !== derivedAddress, 'Root address must differ from derived address');
56
+
57
+ // Test 4: Transaction with child key succeeds
58
+ console.log('✓ Test 4: CRITICAL - Transaction signed with child key validates');
59
+ const fundingTx = new Transaction();
60
+ fundingTx.addOutput({
61
+ lockingScript: new P2PKH().lock(Array.from(derivedHash160)),
62
+ satoshis: 1000,
63
+ });
64
+
65
+ const spendingTx = new Transaction();
66
+ spendingTx.addInput({
67
+ sourceTransaction: fundingTx,
68
+ sourceOutputIndex: 0,
69
+ unlockingScriptTemplate: new P2PKH().unlock(childPrivKey),
70
+ });
71
+ spendingTx.addOutput({
72
+ lockingScript: new P2PKH().lock(Array.from(derivedHash160)),
73
+ satoshis: 900,
74
+ });
75
+
76
+ await spendingTx.sign();
77
+ const inputScript = spendingTx.inputs[0].unlockingScript;
78
+ await assert(!!inputScript, 'Unlocking script should be present');
79
+ await assert(Array.from(inputScript!.toBinary()).length > 0, 'Script should have content');
80
+
81
+ // Test 5: Different paths produce different addresses
82
+ console.log('✓ Test 5: Different derivation paths produce different addresses');
83
+ const path1 = Utils.toBase64(Array.from(Utils.toArray('import', 'utf8'))) + ' ' + Utils.toBase64(Array.from(Utils.toArray('now', 'utf8')));
84
+ const path2 = Utils.toBase64(Array.from(Utils.toArray('import', 'utf8'))) + ' ' + Utils.toBase64(Array.from(Utils.toArray('later', 'utf8')));
85
+
86
+ const pubKey1 = keyDeriver.derivePublicKey(brc29ProtocolID, path1, 'self', true);
87
+ const pubKey2 = keyDeriver.derivePublicKey(brc29ProtocolID, path2, 'self', true);
88
+
89
+ await assert(pubKey1.toAddress() !== pubKey2.toAddress(), 'Different paths should produce different addresses');
90
+
91
+ console.log('\n✅ All tests passed!\n');
92
+ console.log('Key derivation is working correctly:');
93
+ console.log(` Root address: ${rootAddress}`);
94
+ console.log(` Derived address: ${derivedAddress}`);
95
+ console.log(` Child key works: YES`);
96
+ console.log(` Root key works: NO (correctly rejected)\n`);
97
+ }
98
+
99
+ runTests().catch((err) => {
100
+ console.error('\n❌ Tests failed:', err.message);
101
+ process.exit(1);
102
+ });
@@ -0,0 +1,570 @@
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
+
11
+ import { Beef, Transaction, PrivateKey, P2PKH, LockingScript, OP, PushDrop } from '@bsv/sdk';
12
+
13
+ const PROTOCOL_ID = 'clawdbot-overlay-v1';
14
+
15
+ // ============================================================================
16
+ // Server-side logic (using PushDrop for validation)
17
+ // ============================================================================
18
+
19
+ interface ClawdbotIdentityData {
20
+ protocol: string;
21
+ type: 'identity';
22
+ identityKey: string;
23
+ name: string;
24
+ description: string;
25
+ channels: Record<string, string>;
26
+ capabilities: string[];
27
+ timestamp: string;
28
+ }
29
+
30
+ interface ClawdbotServiceData {
31
+ protocol: string;
32
+ type: 'service';
33
+ identityKey: string;
34
+ serviceId: string;
35
+ name: string;
36
+ description: string;
37
+ pricing: { model: string; amountSats: number };
38
+ timestamp: string;
39
+ }
40
+
41
+ /**
42
+ * Extract data fields from a PushDrop script using the SDK's decode method.
43
+ */
44
+ function extractPushDropFields(script: LockingScript): number[][] | null {
45
+ try {
46
+ const decoded = PushDrop.decode(script);
47
+ return decoded.fields;
48
+ } catch {
49
+ return null;
50
+ }
51
+ }
52
+
53
+ /**
54
+ * Parse identity output using PushDrop decode and server's validation logic.
55
+ */
56
+ function parseIdentityOutput(script: LockingScript): ClawdbotIdentityData | null {
57
+ const fields = extractPushDropFields(script);
58
+ if (!fields || fields.length < 1) return null;
59
+
60
+ try {
61
+ const payload = JSON.parse(
62
+ new TextDecoder().decode(new Uint8Array(fields[0]))
63
+ ) as ClawdbotIdentityData;
64
+
65
+ // Server validation rules
66
+ if (payload.protocol !== PROTOCOL_ID) return null;
67
+ if (payload.type !== 'identity') return null;
68
+ if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey)) return null;
69
+ if (typeof payload.name !== 'string' || payload.name.length === 0) return null;
70
+ if (!Array.isArray(payload.capabilities)) return null;
71
+ if (typeof payload.timestamp !== 'string') return null;
72
+
73
+ return payload;
74
+ } catch {
75
+ return null;
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Parse service output using PushDrop decode and server's validation logic.
81
+ */
82
+ function parseServiceOutput(script: LockingScript): ClawdbotServiceData | null {
83
+ const fields = extractPushDropFields(script);
84
+ if (!fields || fields.length < 1) return null;
85
+
86
+ try {
87
+ const payload = JSON.parse(
88
+ new TextDecoder().decode(new Uint8Array(fields[0]))
89
+ ) as ClawdbotServiceData;
90
+
91
+ // Server validation rules
92
+ if (payload.protocol !== PROTOCOL_ID) return null;
93
+ if (payload.type !== 'service') return null;
94
+ if (typeof payload.identityKey !== 'string' || !/^[0-9a-fA-F]{66}$/.test(payload.identityKey)) return null;
95
+ if (typeof payload.serviceId !== 'string' || payload.serviceId.length === 0) return null;
96
+ if (typeof payload.name !== 'string' || payload.name.length === 0) return null;
97
+ if (!payload.pricing || typeof payload.pricing.amountSats !== 'number') return null;
98
+ if (typeof payload.timestamp !== 'string') return null;
99
+
100
+ return payload;
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Create a minimally encoded push chunk for script building.
108
+ */
109
+ function createPushChunk(data: number[]): { op: number; data?: number[] } {
110
+ if (data.length === 0) {
111
+ return { op: 0 };
112
+ }
113
+ if (data.length === 1 && data[0] === 0) {
114
+ return { op: 0 };
115
+ }
116
+ if (data.length === 1 && data[0] > 0 && data[0] <= 16) {
117
+ return { op: 0x50 + data[0] };
118
+ }
119
+ if (data.length <= 75) {
120
+ return { op: data.length, data };
121
+ }
122
+ if (data.length <= 255) {
123
+ return { op: 0x4c, data };
124
+ }
125
+ if (data.length <= 65535) {
126
+ return { op: 0x4d, data };
127
+ }
128
+ return { op: 0x4e, data };
129
+ }
130
+
131
+ /**
132
+ * Build a PushDrop-style locking script with JSON payload.
133
+ * Format: <pubkey> OP_CHECKSIG <jsonBytes> OP_DROP
134
+ */
135
+ function buildPushDropScript(privKey: PrivateKey, payload: object): LockingScript {
136
+ const pubKey = privKey.toPublicKey();
137
+ const pubKeyBytes = pubKey.toDER() as number[];
138
+ const jsonBytes = Array.from(new TextEncoder().encode(JSON.stringify(payload)));
139
+
140
+ const chunks: Array<{ op: number; data?: number[] }> = [];
141
+
142
+ // P2PK lock: <pubkey> OP_CHECKSIG
143
+ chunks.push({ op: pubKeyBytes.length, data: pubKeyBytes });
144
+ chunks.push({ op: OP.OP_CHECKSIG });
145
+
146
+ // Data field: <jsonBytes>
147
+ chunks.push(createPushChunk(jsonBytes));
148
+
149
+ // OP_DROP to clean stack
150
+ chunks.push({ op: OP.OP_DROP });
151
+
152
+ return new LockingScript(chunks);
153
+ }
154
+
155
+ /**
156
+ * Simulate the server's identifyAdmissibleOutputs logic.
157
+ */
158
+ function identifyAdmissibleOutputs(
159
+ beef: number[],
160
+ type: 'identity' | 'service'
161
+ ): { outputsToAdmit: number[]; coinsToRetain: number[] } {
162
+ // Parse BEEF and get the newest (subject) transaction
163
+ const parsedBeef = Beef.fromBinary(beef);
164
+ const subjectTx = parsedBeef.txs[0]._tx;
165
+ if (!subjectTx) {
166
+ return { outputsToAdmit: [], coinsToRetain: [] };
167
+ }
168
+
169
+ const outputsToAdmit: number[] = [];
170
+
171
+ for (let i = 0; i < subjectTx.outputs.length; i++) {
172
+ const output = subjectTx.outputs[i];
173
+ if (output.lockingScript) {
174
+ const parsed = type === 'identity'
175
+ ? parseIdentityOutput(output.lockingScript)
176
+ : parseServiceOutput(output.lockingScript);
177
+ if (parsed !== null) {
178
+ outputsToAdmit.push(i);
179
+ }
180
+ }
181
+ }
182
+
183
+ return { outputsToAdmit, coinsToRetain: [] };
184
+ }
185
+
186
+ // ============================================================================
187
+ // Test utilities
188
+ // ============================================================================
189
+
190
+ let testsPassed = 0;
191
+ let testsFailed = 0;
192
+
193
+ function assert(condition: boolean, message: string): void {
194
+ if (!condition) {
195
+ console.error(`❌ FAIL: ${message}`);
196
+ testsFailed++;
197
+ throw new Error(message);
198
+ }
199
+ console.log(`✅ PASS: ${message}`);
200
+ testsPassed++;
201
+ }
202
+
203
+ function assertThrows(fn: () => void, message: string): void {
204
+ try {
205
+ fn();
206
+ console.error(`❌ FAIL: ${message} (expected to throw)`);
207
+ testsFailed++;
208
+ } catch {
209
+ console.log(`✅ PASS: ${message}`);
210
+ testsPassed++;
211
+ }
212
+ }
213
+
214
+ // ============================================================================
215
+ // Test: BEEF format validation
216
+ // ============================================================================
217
+
218
+ async function testBeefFormat(): Promise<void> {
219
+ console.log('\n=== Test: BEEF Format Validation ===');
220
+
221
+ // Create a minimal transaction chain
222
+ const privKey = PrivateKey.fromRandom();
223
+ const pubKeyHash = privKey.toPublicKey().toHash();
224
+
225
+ // Source transaction (simulating a mined tx with merkle proof)
226
+ const sourceTx = new Transaction();
227
+ sourceTx.addOutput({
228
+ lockingScript: new P2PKH().lock(pubKeyHash),
229
+ satoshis: 10000,
230
+ });
231
+
232
+ // Spending transaction with PushDrop output
233
+ const tx = new Transaction();
234
+ tx.addInput({
235
+ sourceTransaction: sourceTx,
236
+ sourceOutputIndex: 0,
237
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
238
+ });
239
+ tx.addOutput({
240
+ lockingScript: buildPushDropScript(privKey, { protocol: PROTOCOL_ID, type: 'identity', test: true }),
241
+ satoshis: 1,
242
+ });
243
+ await tx.sign();
244
+
245
+ // Build BEEF
246
+ const beef = new Beef();
247
+ beef.mergeTransaction(tx);
248
+ const binary = beef.toBinary();
249
+
250
+ // Validate BEEF magic bytes
251
+ const magic = binary.slice(0, 4);
252
+ const magicHex = magic.map(b => b.toString(16).padStart(2, '0')).join('');
253
+ assert(
254
+ magicHex === '0100beef' || magicHex === '0200beef',
255
+ `BEEF magic bytes should be 0100beef or 0200beef, got ${magicHex}`
256
+ );
257
+
258
+ // Validate BEEF can be parsed
259
+ const parsed = Beef.fromBinary(binary);
260
+ assert(parsed.txs.length >= 1, `BEEF should contain at least 1 transaction, got ${parsed.txs.length}`);
261
+
262
+ // Validate the newest transaction can be found in BEEF
263
+ const beefTx = parsed.txs[0] as { txid?: string; _tx?: Transaction };
264
+ const newestTxid = beefTx.txid || beefTx._tx?.id('hex');
265
+ assert(newestTxid === tx.id('hex'), `Newest transaction in BEEF should match original, got ${newestTxid?.slice(0, 16)}`);
266
+ }
267
+
268
+ // ============================================================================
269
+ // Test: Identity payload validation
270
+ // ============================================================================
271
+
272
+ async function testIdentityPayload(): Promise<void> {
273
+ console.log('\n=== Test: Identity Payload Validation ===');
274
+
275
+ const privKey = PrivateKey.fromRandom();
276
+ const identityKey = privKey.toPublicKey().toString();
277
+
278
+ // Valid identity payload
279
+ const validPayload: ClawdbotIdentityData = {
280
+ protocol: PROTOCOL_ID,
281
+ type: 'identity',
282
+ identityKey,
283
+ name: 'test-agent',
284
+ description: 'A test agent',
285
+ channels: { overlay: 'https://example.com' },
286
+ capabilities: ['test'],
287
+ timestamp: new Date().toISOString(),
288
+ };
289
+
290
+ const script = buildPushDropScript(privKey, validPayload);
291
+ const parsed = parseIdentityOutput(script);
292
+
293
+ assert(parsed !== null, 'Valid identity payload should be parsed');
294
+ assert(parsed!.identityKey === identityKey, 'Identity key should match');
295
+ assert(parsed!.name === 'test-agent', 'Name should match');
296
+ assert(parsed!.type === 'identity', 'Type should be identity');
297
+
298
+ // Invalid: wrong protocol
299
+ const wrongProtocol = { ...validPayload, protocol: 'wrong-protocol' };
300
+ const script2 = buildPushDropScript(privKey, wrongProtocol);
301
+ assert(parseIdentityOutput(script2) === null, 'Wrong protocol should be rejected');
302
+
303
+ // Invalid: wrong type
304
+ const wrongType = { ...validPayload, type: 'service' as const };
305
+ const script3 = buildPushDropScript(privKey, wrongType);
306
+ assert(parseIdentityOutput(script3) === null, 'Wrong type should be rejected');
307
+
308
+ // Invalid: bad identity key
309
+ const badKey = { ...validPayload, identityKey: 'not-a-valid-key' };
310
+ const script4 = buildPushDropScript(privKey, badKey);
311
+ assert(parseIdentityOutput(script4) === null, 'Invalid identity key should be rejected');
312
+
313
+ // Invalid: empty name
314
+ const emptyName = { ...validPayload, name: '' };
315
+ const script5 = buildPushDropScript(privKey, emptyName);
316
+ assert(parseIdentityOutput(script5) === null, 'Empty name should be rejected');
317
+
318
+ // Invalid: capabilities not array
319
+ const badCaps = { ...validPayload, capabilities: 'not-array' as unknown as string[] };
320
+ const script6 = buildPushDropScript(privKey, badCaps);
321
+ assert(parseIdentityOutput(script6) === null, 'Non-array capabilities should be rejected');
322
+ }
323
+
324
+ // ============================================================================
325
+ // Test: Service payload validation
326
+ // ============================================================================
327
+
328
+ async function testServicePayload(): Promise<void> {
329
+ console.log('\n=== Test: Service Payload Validation ===');
330
+
331
+ const privKey = PrivateKey.fromRandom();
332
+ const identityKey = privKey.toPublicKey().toString();
333
+
334
+ // Valid service payload
335
+ const validPayload: ClawdbotServiceData = {
336
+ protocol: PROTOCOL_ID,
337
+ type: 'service',
338
+ identityKey,
339
+ serviceId: 'test-service',
340
+ name: 'Test Service',
341
+ description: 'A test service',
342
+ pricing: { model: 'per-task', amountSats: 100 },
343
+ timestamp: new Date().toISOString(),
344
+ };
345
+
346
+ const script = buildPushDropScript(privKey, validPayload);
347
+ const parsed = parseServiceOutput(script);
348
+
349
+ assert(parsed !== null, 'Valid service payload should be parsed');
350
+ assert(parsed!.serviceId === 'test-service', 'Service ID should match');
351
+ assert(parsed!.pricing.amountSats === 100, 'Price should match');
352
+
353
+ // Invalid: missing pricing
354
+ const noPricing = { ...validPayload, pricing: undefined as unknown as { model: string; amountSats: number } };
355
+ const script2 = buildPushDropScript(privKey, noPricing);
356
+ assert(parseServiceOutput(script2) === null, 'Missing pricing should be rejected');
357
+
358
+ // Invalid: empty serviceId
359
+ const emptyId = { ...validPayload, serviceId: '' };
360
+ const script3 = buildPushDropScript(privKey, emptyId);
361
+ assert(parseServiceOutput(script3) === null, 'Empty serviceId should be rejected');
362
+ }
363
+
364
+ // ============================================================================
365
+ // Test: Full BEEF submission simulation
366
+ // ============================================================================
367
+
368
+ async function testBeefSubmission(): Promise<void> {
369
+ console.log('\n=== Test: BEEF Submission Simulation ===');
370
+
371
+ const privKey = PrivateKey.fromRandom();
372
+ const pubKeyHash = privKey.toPublicKey().toHash();
373
+ const identityKey = privKey.toPublicKey().toString();
374
+
375
+ // Create source transaction (simulating confirmed tx)
376
+ const sourceTx = new Transaction();
377
+ sourceTx.addOutput({
378
+ lockingScript: new P2PKH().lock(pubKeyHash),
379
+ satoshis: 10000,
380
+ });
381
+
382
+ // Valid identity registration
383
+ const identityPayload: ClawdbotIdentityData = {
384
+ protocol: PROTOCOL_ID,
385
+ type: 'identity',
386
+ identityKey,
387
+ name: 'test-agent',
388
+ description: 'Test agent for unit tests',
389
+ channels: { overlay: 'https://clawoverlay.com' },
390
+ capabilities: ['testing'],
391
+ timestamp: new Date().toISOString(),
392
+ };
393
+
394
+ const tx = new Transaction();
395
+ tx.addInput({
396
+ sourceTransaction: sourceTx,
397
+ sourceOutputIndex: 0,
398
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
399
+ });
400
+ tx.addOutput({
401
+ lockingScript: buildPushDropScript(privKey, identityPayload),
402
+ satoshis: 1,
403
+ });
404
+ tx.addOutput({
405
+ lockingScript: new P2PKH().lock(pubKeyHash),
406
+ satoshis: 9900,
407
+ });
408
+ await tx.sign();
409
+
410
+ // Build BEEF with ancestry
411
+ const beef = new Beef();
412
+ beef.mergeTransaction(tx);
413
+ const beefBinary = beef.toBinary();
414
+
415
+ // Simulate server's topic manager
416
+ const result = identifyAdmissibleOutputs(beefBinary, 'identity');
417
+
418
+ assert(result.outputsToAdmit.length === 1, `Should admit 1 output, got ${result.outputsToAdmit.length}`);
419
+ assert(result.outputsToAdmit[0] === 0, 'Should admit output index 0 (PushDrop)');
420
+ }
421
+
422
+ // ============================================================================
423
+ // Test: Chained transactions (stored BEEF)
424
+ // ============================================================================
425
+
426
+ async function testChainedBeef(): Promise<void> {
427
+ console.log('\n=== Test: Chained BEEF (multiple unconfirmed txs) ===');
428
+
429
+ const privKey = PrivateKey.fromRandom();
430
+ const pubKeyHash = privKey.toPublicKey().toHash();
431
+ const identityKey = privKey.toPublicKey().toString();
432
+
433
+ // Grandparent tx (simulating mined tx - would have merkle proof)
434
+ const grandparentTx = new Transaction();
435
+ grandparentTx.addOutput({
436
+ lockingScript: new P2PKH().lock(pubKeyHash),
437
+ satoshis: 100000,
438
+ });
439
+
440
+ // Parent tx (first overlay submission - unconfirmed)
441
+ const parentTx = new Transaction();
442
+ parentTx.addInput({
443
+ sourceTransaction: grandparentTx,
444
+ sourceOutputIndex: 0,
445
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
446
+ });
447
+ parentTx.addOutput({
448
+ lockingScript: buildPushDropScript(privKey, {
449
+ protocol: PROTOCOL_ID,
450
+ type: 'identity',
451
+ identityKey,
452
+ name: 'parent-tx',
453
+ description: 'First registration',
454
+ channels: {},
455
+ capabilities: [],
456
+ timestamp: new Date().toISOString(),
457
+ }),
458
+ satoshis: 1,
459
+ });
460
+ parentTx.addOutput({
461
+ lockingScript: new P2PKH().lock(pubKeyHash),
462
+ satoshis: 99900,
463
+ });
464
+ await parentTx.sign();
465
+
466
+ // Child tx (second overlay submission - spending parent's change)
467
+ const childTx = new Transaction();
468
+ childTx.addInput({
469
+ sourceTransaction: parentTx,
470
+ sourceOutputIndex: 1, // Spend the change output
471
+ unlockingScriptTemplate: new P2PKH().unlock(privKey),
472
+ });
473
+ childTx.addOutput({
474
+ lockingScript: buildPushDropScript(privKey, {
475
+ protocol: PROTOCOL_ID,
476
+ type: 'service',
477
+ identityKey,
478
+ serviceId: 'test-svc',
479
+ name: 'Test Service',
480
+ description: 'Service from child tx',
481
+ pricing: { model: 'per-task', amountSats: 50 },
482
+ timestamp: new Date().toISOString(),
483
+ }),
484
+ satoshis: 1,
485
+ });
486
+ childTx.addOutput({
487
+ lockingScript: new P2PKH().lock(pubKeyHash),
488
+ satoshis: 99800,
489
+ });
490
+ await childTx.sign();
491
+
492
+ // Build BEEF - should include full chain
493
+ const beef = new Beef();
494
+ beef.mergeTransaction(childTx);
495
+ const beefBinary = beef.toBinary();
496
+
497
+ // Verify BEEF contains all transactions
498
+ const parsedBeef = Beef.fromBinary(beefBinary);
499
+ assert(parsedBeef.txs.length >= 2, `BEEF should contain at least 2 txs for chain, got ${parsedBeef.txs.length}`);
500
+
501
+ // Verify child tx is the newest in BEEF
502
+ const beefTx = parsedBeef.txs[0] as { txid?: string; _tx?: Transaction };
503
+ const newestTxid = beefTx.txid || beefTx._tx?.id('hex');
504
+ assert(newestTxid === childTx.id('hex'), 'Newest tx in BEEF should be the child transaction');
505
+
506
+ // Simulate server validation
507
+ const result = identifyAdmissibleOutputs(beefBinary, 'service');
508
+ assert(result.outputsToAdmit.length === 1, 'Should admit the service output');
509
+ }
510
+
511
+ // ============================================================================
512
+ // Test: Invalid BEEF handling
513
+ // ============================================================================
514
+
515
+ async function testInvalidBeef(): Promise<void> {
516
+ console.log('\n=== Test: Invalid BEEF Handling ===');
517
+
518
+ // Empty BEEF
519
+ const emptyBeef = new Beef();
520
+ const emptyBinary = emptyBeef.toBinary();
521
+
522
+ assertThrows(
523
+ () => Transaction.fromBEEF(emptyBinary),
524
+ 'Empty BEEF should throw when extracting transaction'
525
+ );
526
+
527
+ // Malformed BEEF (random bytes)
528
+ const garbage = new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8]);
529
+ assertThrows(
530
+ () => Beef.fromBinary(Array.from(garbage)),
531
+ 'Garbage bytes should throw when parsing BEEF'
532
+ );
533
+
534
+ // BEEF with wrong magic
535
+ const wrongMagic = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF, 0, 0]);
536
+ assertThrows(
537
+ () => Beef.fromBinary(Array.from(wrongMagic)),
538
+ 'Wrong magic bytes should throw when parsing BEEF'
539
+ );
540
+ }
541
+
542
+ // ============================================================================
543
+ // Main test runner
544
+ // ============================================================================
545
+
546
+ async function runTests(): Promise<void> {
547
+ console.log('Starting overlay submit tests (PushDrop format)...\n');
548
+
549
+ try {
550
+ await testBeefFormat();
551
+ await testIdentityPayload();
552
+ await testServicePayload();
553
+ await testBeefSubmission();
554
+ await testChainedBeef();
555
+ await testInvalidBeef();
556
+
557
+ console.log(`\n========================================`);
558
+ console.log(`Tests completed: ${testsPassed} passed, ${testsFailed} failed`);
559
+ console.log(`========================================`);
560
+
561
+ if (testsFailed > 0) {
562
+ process.exit(1);
563
+ }
564
+ } catch (e) {
565
+ console.error('\nTest suite failed:', e);
566
+ process.exit(1);
567
+ }
568
+ }
569
+
570
+ runTests();