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.
- package/README.md +406 -0
- package/SKILL.md +78 -0
- package/clawdbot.plugin.json +106 -0
- package/dist/cli-main.d.ts +7 -0
- package/dist/cli-main.js +192 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +14 -0
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.js +13 -0
- package/dist/core/index.d.ts +25 -0
- package/dist/core/index.js +26 -0
- package/dist/core/payment.d.ts +16 -0
- package/dist/core/payment.js +94 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/core/types.js +4 -0
- package/dist/core/verify.d.ts +28 -0
- package/dist/core/verify.js +104 -0
- package/dist/core/wallet.d.ts +99 -0
- package/dist/core/wallet.js +219 -0
- package/dist/scripts/baemail/commands.d.ts +64 -0
- package/dist/scripts/baemail/commands.js +258 -0
- package/dist/scripts/baemail/handler.d.ts +36 -0
- package/dist/scripts/baemail/handler.js +284 -0
- package/dist/scripts/baemail/index.d.ts +5 -0
- package/dist/scripts/baemail/index.js +5 -0
- package/dist/scripts/config.d.ts +48 -0
- package/dist/scripts/config.js +68 -0
- package/dist/scripts/index.d.ts +7 -0
- package/dist/scripts/index.js +7 -0
- package/dist/scripts/messaging/connect.d.ts +8 -0
- package/dist/scripts/messaging/connect.js +114 -0
- package/dist/scripts/messaging/handlers.d.ts +21 -0
- package/dist/scripts/messaging/handlers.js +334 -0
- package/dist/scripts/messaging/inbox.d.ts +11 -0
- package/dist/scripts/messaging/inbox.js +51 -0
- package/dist/scripts/messaging/index.d.ts +8 -0
- package/dist/scripts/messaging/index.js +8 -0
- package/dist/scripts/messaging/poll.d.ts +7 -0
- package/dist/scripts/messaging/poll.js +52 -0
- package/dist/scripts/messaging/send.d.ts +7 -0
- package/dist/scripts/messaging/send.js +43 -0
- package/dist/scripts/output.d.ts +12 -0
- package/dist/scripts/output.js +19 -0
- package/dist/scripts/overlay/discover.d.ts +7 -0
- package/dist/scripts/overlay/discover.js +72 -0
- package/dist/scripts/overlay/index.d.ts +7 -0
- package/dist/scripts/overlay/index.js +7 -0
- package/dist/scripts/overlay/registration.d.ts +19 -0
- package/dist/scripts/overlay/registration.js +176 -0
- package/dist/scripts/overlay/services.d.ts +29 -0
- package/dist/scripts/overlay/services.js +167 -0
- package/dist/scripts/overlay/transaction.d.ts +42 -0
- package/dist/scripts/overlay/transaction.js +103 -0
- package/dist/scripts/payment/build.d.ts +24 -0
- package/dist/scripts/payment/build.js +54 -0
- package/dist/scripts/payment/commands.d.ts +15 -0
- package/dist/scripts/payment/commands.js +73 -0
- package/dist/scripts/payment/index.d.ts +6 -0
- package/dist/scripts/payment/index.js +6 -0
- package/dist/scripts/payment/types.d.ts +56 -0
- package/dist/scripts/payment/types.js +4 -0
- package/dist/scripts/services/index.d.ts +6 -0
- package/dist/scripts/services/index.js +6 -0
- package/dist/scripts/services/queue.d.ts +11 -0
- package/dist/scripts/services/queue.js +28 -0
- package/dist/scripts/services/request.d.ts +7 -0
- package/dist/scripts/services/request.js +82 -0
- package/dist/scripts/services/respond.d.ts +11 -0
- package/dist/scripts/services/respond.js +132 -0
- package/dist/scripts/types.d.ts +107 -0
- package/dist/scripts/types.js +4 -0
- package/dist/scripts/utils/index.d.ts +6 -0
- package/dist/scripts/utils/index.js +6 -0
- package/dist/scripts/utils/merkle.d.ts +12 -0
- package/dist/scripts/utils/merkle.js +47 -0
- package/dist/scripts/utils/storage.d.ts +66 -0
- package/dist/scripts/utils/storage.js +211 -0
- package/dist/scripts/utils/woc.d.ts +26 -0
- package/dist/scripts/utils/woc.js +91 -0
- package/dist/scripts/wallet/balance.d.ts +22 -0
- package/dist/scripts/wallet/balance.js +240 -0
- package/dist/scripts/wallet/identity.d.ts +70 -0
- package/dist/scripts/wallet/identity.js +151 -0
- package/dist/scripts/wallet/index.d.ts +6 -0
- package/dist/scripts/wallet/index.js +6 -0
- package/dist/scripts/wallet/setup.d.ts +15 -0
- package/dist/scripts/wallet/setup.js +105 -0
- package/dist/scripts/x-verification/commands.d.ts +27 -0
- package/dist/scripts/x-verification/commands.js +222 -0
- package/dist/scripts/x-verification/index.d.ts +4 -0
- package/dist/scripts/x-verification/index.js +4 -0
- package/dist/services/built-in/api-proxy/index.d.ts +6 -0
- package/dist/services/built-in/api-proxy/index.js +23 -0
- package/dist/services/built-in/code-develop/index.d.ts +6 -0
- package/dist/services/built-in/code-develop/index.js +23 -0
- package/dist/services/built-in/code-review/index.d.ts +10 -0
- package/dist/services/built-in/code-review/index.js +51 -0
- package/dist/services/built-in/image-analysis/index.d.ts +6 -0
- package/dist/services/built-in/image-analysis/index.js +33 -0
- package/dist/services/built-in/memory-store/index.d.ts +6 -0
- package/dist/services/built-in/memory-store/index.js +22 -0
- package/dist/services/built-in/roulette/index.d.ts +6 -0
- package/dist/services/built-in/roulette/index.js +27 -0
- package/dist/services/built-in/summarize/index.d.ts +6 -0
- package/dist/services/built-in/summarize/index.js +21 -0
- package/dist/services/built-in/tell-joke/handler.d.ts +7 -0
- package/dist/services/built-in/tell-joke/handler.js +122 -0
- package/dist/services/built-in/tell-joke/index.d.ts +9 -0
- package/dist/services/built-in/tell-joke/index.js +31 -0
- package/dist/services/built-in/translate/index.d.ts +6 -0
- package/dist/services/built-in/translate/index.js +21 -0
- package/dist/services/built-in/web-research/index.d.ts +9 -0
- package/dist/services/built-in/web-research/index.js +51 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.js +14 -0
- package/dist/services/loader.d.ts +77 -0
- package/dist/services/loader.js +292 -0
- package/dist/services/manager.d.ts +86 -0
- package/dist/services/manager.js +255 -0
- package/dist/services/registry.d.ts +98 -0
- package/dist/services/registry.js +204 -0
- package/dist/services/types.d.ts +230 -0
- package/dist/services/types.js +30 -0
- package/dist/test/cli.test.d.ts +7 -0
- package/dist/test/cli.test.js +329 -0
- package/dist/test/comprehensive-overlay.test.d.ts +13 -0
- package/dist/test/comprehensive-overlay.test.js +593 -0
- package/dist/test/key-derivation.test.d.ts +12 -0
- package/dist/test/key-derivation.test.js +86 -0
- package/dist/test/overlay-submit.test.d.ts +10 -0
- package/dist/test/overlay-submit.test.js +460 -0
- package/dist/test/request-response-flow.test.d.ts +5 -0
- package/dist/test/request-response-flow.test.js +209 -0
- package/dist/test/service-system.test.d.ts +5 -0
- package/dist/test/service-system.test.js +190 -0
- package/dist/test/utils/server-logic.d.ts +98 -0
- package/dist/test/utils/server-logic.js +286 -0
- package/dist/test/wallet.test.d.ts +7 -0
- package/dist/test/wallet.test.js +146 -0
- package/index.ts +1965 -0
- package/openclaw.plugin.json +106 -0
- package/package.json +73 -0
- package/src/cli-main.ts +230 -0
- package/src/cli.ts +16 -0
- package/src/core/README.md +246 -0
- package/src/core/config.ts +21 -0
- package/src/core/index.ts +42 -0
- package/src/core/payment.ts +111 -0
- package/src/core/types.ts +102 -0
- package/src/core/verify.ts +119 -0
- package/src/core/wallet.ts +282 -0
- package/src/scripts/baemail/commands.ts +326 -0
- package/src/scripts/baemail/handler.ts +338 -0
- package/src/scripts/baemail/index.ts +6 -0
- package/src/scripts/config.ts +81 -0
- package/src/scripts/index.ts +8 -0
- package/src/scripts/messaging/connect.ts +121 -0
- package/src/scripts/messaging/handlers.ts +394 -0
- package/src/scripts/messaging/inbox.ts +64 -0
- package/src/scripts/messaging/index.ts +9 -0
- package/src/scripts/messaging/poll.ts +59 -0
- package/src/scripts/messaging/send.ts +54 -0
- package/src/scripts/output.ts +21 -0
- package/src/scripts/overlay/discover.ts +81 -0
- package/src/scripts/overlay/index.ts +8 -0
- package/src/scripts/overlay/registration.ts +199 -0
- package/src/scripts/overlay/services.ts +199 -0
- package/src/scripts/overlay/transaction.ts +124 -0
- package/src/scripts/payment/build.ts +65 -0
- package/src/scripts/payment/commands.ts +92 -0
- package/src/scripts/payment/index.ts +7 -0
- package/src/scripts/payment/types.ts +62 -0
- package/src/scripts/services/index.ts +7 -0
- package/src/scripts/services/queue.ts +35 -0
- package/src/scripts/services/request.ts +98 -0
- package/src/scripts/services/respond.ts +149 -0
- package/src/scripts/types.ts +121 -0
- package/src/scripts/utils/index.ts +7 -0
- package/src/scripts/utils/merkle.ts +57 -0
- package/src/scripts/utils/storage.ts +231 -0
- package/src/scripts/utils/woc.ts +106 -0
- package/src/scripts/wallet/balance.ts +277 -0
- package/src/scripts/wallet/identity.ts +203 -0
- package/src/scripts/wallet/index.ts +7 -0
- package/src/scripts/wallet/setup.ts +121 -0
- package/src/scripts/x-verification/commands.ts +256 -0
- package/src/scripts/x-verification/index.ts +5 -0
- package/src/services/built-in/api-proxy/index.ts +26 -0
- package/src/services/built-in/api-proxy/prompt.md +26 -0
- package/src/services/built-in/code-develop/index.ts +26 -0
- package/src/services/built-in/code-develop/prompt.md +35 -0
- package/src/services/built-in/code-review/index.ts +54 -0
- package/src/services/built-in/code-review/prompt.md +105 -0
- package/src/services/built-in/image-analysis/index.ts +36 -0
- package/src/services/built-in/image-analysis/prompt.md +42 -0
- package/src/services/built-in/memory-store/index.ts +25 -0
- package/src/services/built-in/memory-store/prompt.md +45 -0
- package/src/services/built-in/roulette/index.ts +30 -0
- package/src/services/built-in/roulette/prompt.md +35 -0
- package/src/services/built-in/summarize/index.ts +24 -0
- package/src/services/built-in/summarize/prompt.md +27 -0
- package/src/services/built-in/tell-joke/handler.ts +134 -0
- package/src/services/built-in/tell-joke/index.ts +34 -0
- package/src/services/built-in/tell-joke/prompt.md +59 -0
- package/src/services/built-in/translate/index.ts +24 -0
- package/src/services/built-in/translate/prompt.md +23 -0
- package/src/services/built-in/web-research/index.ts +54 -0
- package/src/services/built-in/web-research/prompt.md +110 -0
- package/src/services/index.ts +16 -0
- package/src/services/loader.ts +344 -0
- package/src/services/manager.ts +304 -0
- package/src/services/registry.ts +246 -0
- package/src/services/types.ts +259 -0
- package/src/test/cli.test.ts +352 -0
- package/src/test/comprehensive-overlay.test.ts +729 -0
- package/src/test/key-derivation.test.ts +102 -0
- package/src/test/overlay-submit.test.ts +570 -0
- package/src/test/request-response-flow.test.ts +252 -0
- package/src/test/service-system.test.ts +241 -0
- package/src/test/utils/server-logic.ts +368 -0
- package/src/test/wallet.test.ts +166 -0
|
@@ -0,0 +1,593 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Comprehensive Overlay Submission Tests
|
|
3
|
+
*
|
|
4
|
+
* Tests all aspects of overlay submission:
|
|
5
|
+
* - BEEF format and construction
|
|
6
|
+
* - Payload validation (identity, service, revocation)
|
|
7
|
+
* - PushDrop script format
|
|
8
|
+
* - Transaction chain validation
|
|
9
|
+
* - Server response handling
|
|
10
|
+
*
|
|
11
|
+
* Run with: npx tsx src/test/comprehensive-overlay.test.ts
|
|
12
|
+
*/
|
|
13
|
+
import { Beef, Transaction, PrivateKey, P2PKH, LockingScript, OP } from '@bsv/sdk';
|
|
14
|
+
import { PROTOCOL_ID, extractPushDropFields, parseIdentityOutput, parseRevocationOutput, parseServiceOutput, identifyIdentityOutputs, identifyServiceOutputs, validateBeef, validateBeefAncestry, } from './utils/server-logic.js';
|
|
15
|
+
const results = [];
|
|
16
|
+
function test(name, fn) {
|
|
17
|
+
try {
|
|
18
|
+
const result = fn();
|
|
19
|
+
if (result instanceof Promise) {
|
|
20
|
+
result
|
|
21
|
+
.then(() => {
|
|
22
|
+
results.push({ name, passed: true });
|
|
23
|
+
console.log(`โ
${name}`);
|
|
24
|
+
})
|
|
25
|
+
.catch((e) => {
|
|
26
|
+
results.push({ name, passed: false, error: e.message });
|
|
27
|
+
console.log(`โ ${name}: ${e.message}`);
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
results.push({ name, passed: true });
|
|
32
|
+
console.log(`โ
${name}`);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
37
|
+
results.push({ name, passed: false, error: errorMessage });
|
|
38
|
+
console.log(`โ ${name}: ${errorMessage}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function assert(condition, message) {
|
|
42
|
+
if (!condition)
|
|
43
|
+
throw new Error(message);
|
|
44
|
+
}
|
|
45
|
+
function assertEqual(actual, expected, message) {
|
|
46
|
+
if (actual !== expected) {
|
|
47
|
+
throw new Error(`${message}: expected ${expected}, got ${actual}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// Helper Functions - PushDrop Script Building
|
|
52
|
+
// ============================================================================
|
|
53
|
+
/**
|
|
54
|
+
* Create a minimally encoded push chunk for script building.
|
|
55
|
+
*/
|
|
56
|
+
function createPushChunk(data) {
|
|
57
|
+
if (data.length === 0) {
|
|
58
|
+
return { op: 0 };
|
|
59
|
+
}
|
|
60
|
+
if (data.length === 1 && data[0] === 0) {
|
|
61
|
+
return { op: 0 };
|
|
62
|
+
}
|
|
63
|
+
if (data.length === 1 && data[0] > 0 && data[0] <= 16) {
|
|
64
|
+
return { op: 0x50 + data[0] };
|
|
65
|
+
}
|
|
66
|
+
if (data.length <= 75) {
|
|
67
|
+
return { op: data.length, data };
|
|
68
|
+
}
|
|
69
|
+
if (data.length <= 255) {
|
|
70
|
+
return { op: 0x4c, data };
|
|
71
|
+
}
|
|
72
|
+
if (data.length <= 65535) {
|
|
73
|
+
return { op: 0x4d, data };
|
|
74
|
+
}
|
|
75
|
+
return { op: 0x4e, data };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Build a PushDrop-style locking script with JSON payload.
|
|
79
|
+
* Format: <pubkey> OP_CHECKSIG <jsonBytes> OP_DROP
|
|
80
|
+
*
|
|
81
|
+
* This mimics what PushDrop.lock() produces for testing purposes.
|
|
82
|
+
*/
|
|
83
|
+
function buildPushDropScript(privKey, payload) {
|
|
84
|
+
const pubKey = privKey.toPublicKey();
|
|
85
|
+
const pubKeyBytes = pubKey.toDER();
|
|
86
|
+
const jsonBytes = Array.from(new TextEncoder().encode(JSON.stringify(payload)));
|
|
87
|
+
const chunks = [];
|
|
88
|
+
// P2PK lock: <pubkey> OP_CHECKSIG
|
|
89
|
+
chunks.push({ op: pubKeyBytes.length, data: pubKeyBytes });
|
|
90
|
+
chunks.push({ op: OP.OP_CHECKSIG });
|
|
91
|
+
// Data field: <jsonBytes>
|
|
92
|
+
chunks.push(createPushChunk(jsonBytes));
|
|
93
|
+
// OP_DROP to clean stack
|
|
94
|
+
chunks.push({ op: OP.OP_DROP });
|
|
95
|
+
return new LockingScript(chunks);
|
|
96
|
+
}
|
|
97
|
+
async function createSignedTransaction(privKey, sourceTx, sourceVout, pushDropPayload, changeSats = 9900) {
|
|
98
|
+
const tx = new Transaction();
|
|
99
|
+
const pubKeyHash = privKey.toPublicKey().toHash();
|
|
100
|
+
tx.addInput({
|
|
101
|
+
sourceTransaction: sourceTx,
|
|
102
|
+
sourceOutputIndex: sourceVout,
|
|
103
|
+
unlockingScriptTemplate: new P2PKH().unlock(privKey),
|
|
104
|
+
});
|
|
105
|
+
tx.addOutput({
|
|
106
|
+
lockingScript: buildPushDropScript(privKey, pushDropPayload),
|
|
107
|
+
satoshis: 1, // PushDrop outputs need at least 1 sat
|
|
108
|
+
});
|
|
109
|
+
if (changeSats > 0) {
|
|
110
|
+
tx.addOutput({
|
|
111
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
112
|
+
satoshis: changeSats,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
await tx.sign();
|
|
116
|
+
return tx;
|
|
117
|
+
}
|
|
118
|
+
function createSourceTransaction(privKey, satoshis = 10000) {
|
|
119
|
+
const pubKeyHash = privKey.toPublicKey().toHash();
|
|
120
|
+
const tx = new Transaction();
|
|
121
|
+
tx.addOutput({
|
|
122
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
123
|
+
satoshis,
|
|
124
|
+
});
|
|
125
|
+
return tx;
|
|
126
|
+
}
|
|
127
|
+
// ============================================================================
|
|
128
|
+
// BEEF Format Tests
|
|
129
|
+
// ============================================================================
|
|
130
|
+
console.log('\n=== BEEF Format Tests ===\n');
|
|
131
|
+
test('BEEF: valid v2 magic bytes', async () => {
|
|
132
|
+
const privKey = PrivateKey.fromRandom();
|
|
133
|
+
const sourceTx = createSourceTransaction(privKey);
|
|
134
|
+
const tx = await createSignedTransaction(privKey, sourceTx, 0, { test: true });
|
|
135
|
+
const beef = new Beef();
|
|
136
|
+
beef.mergeTransaction(tx);
|
|
137
|
+
const binary = beef.toBinary();
|
|
138
|
+
const result = validateBeef(binary);
|
|
139
|
+
assert(result.valid, result.error || 'BEEF should be valid');
|
|
140
|
+
assertEqual(result.version, 2, 'Should be BEEF v2');
|
|
141
|
+
});
|
|
142
|
+
test('BEEF: contains multiple transactions', async () => {
|
|
143
|
+
const privKey = PrivateKey.fromRandom();
|
|
144
|
+
const sourceTx = createSourceTransaction(privKey);
|
|
145
|
+
const tx = await createSignedTransaction(privKey, sourceTx, 0, { test: true });
|
|
146
|
+
const beef = new Beef();
|
|
147
|
+
beef.mergeTransaction(tx);
|
|
148
|
+
const binary = beef.toBinary();
|
|
149
|
+
const result = validateBeef(binary);
|
|
150
|
+
assert(result.txCount >= 2, `Should have at least 2 txs, got ${result.txCount}`);
|
|
151
|
+
});
|
|
152
|
+
test('BEEF: invalid magic bytes rejected', () => {
|
|
153
|
+
const garbage = [0xDE, 0xAD, 0xBE, 0xEF, 0, 0, 0, 0];
|
|
154
|
+
const result = validateBeef(garbage);
|
|
155
|
+
assert(!result.valid, 'Should reject invalid magic');
|
|
156
|
+
});
|
|
157
|
+
test('BEEF: empty BEEF rejected', () => {
|
|
158
|
+
const emptyBeef = new Beef();
|
|
159
|
+
const binary = emptyBeef.toBinary();
|
|
160
|
+
const result = validateBeef(binary);
|
|
161
|
+
assert(!result.valid, 'Should reject empty BEEF');
|
|
162
|
+
});
|
|
163
|
+
// ============================================================================
|
|
164
|
+
// Identity Payload Tests
|
|
165
|
+
// ============================================================================
|
|
166
|
+
console.log('\n=== Identity Payload Tests ===\n');
|
|
167
|
+
test('Identity: valid payload accepted', () => {
|
|
168
|
+
const privKey = PrivateKey.fromRandom();
|
|
169
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
170
|
+
const payload = {
|
|
171
|
+
protocol: PROTOCOL_ID,
|
|
172
|
+
type: 'identity',
|
|
173
|
+
identityKey,
|
|
174
|
+
name: 'test-agent',
|
|
175
|
+
description: 'A test agent',
|
|
176
|
+
channels: { overlay: 'https://example.com' },
|
|
177
|
+
capabilities: ['testing'],
|
|
178
|
+
timestamp: new Date().toISOString(),
|
|
179
|
+
};
|
|
180
|
+
const script = buildPushDropScript(privKey, payload);
|
|
181
|
+
const parsed = parseIdentityOutput(script);
|
|
182
|
+
assert(parsed !== null, 'Should parse valid identity');
|
|
183
|
+
assertEqual(parsed.identityKey, identityKey, 'Identity key should match');
|
|
184
|
+
assertEqual(parsed.name, 'test-agent', 'Name should match');
|
|
185
|
+
});
|
|
186
|
+
test('Identity: wrong protocol rejected', () => {
|
|
187
|
+
const privKey = PrivateKey.fromRandom();
|
|
188
|
+
const payload = {
|
|
189
|
+
protocol: 'wrong-protocol',
|
|
190
|
+
type: 'identity',
|
|
191
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
192
|
+
name: 'test',
|
|
193
|
+
description: '',
|
|
194
|
+
channels: {},
|
|
195
|
+
capabilities: [],
|
|
196
|
+
timestamp: new Date().toISOString(),
|
|
197
|
+
};
|
|
198
|
+
const script = buildPushDropScript(privKey, payload);
|
|
199
|
+
assert(parseIdentityOutput(script) === null, 'Should reject wrong protocol');
|
|
200
|
+
});
|
|
201
|
+
test('Identity: wrong type rejected', () => {
|
|
202
|
+
const privKey = PrivateKey.fromRandom();
|
|
203
|
+
const payload = {
|
|
204
|
+
protocol: PROTOCOL_ID,
|
|
205
|
+
type: 'service', // Wrong type
|
|
206
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
207
|
+
name: 'test',
|
|
208
|
+
description: '',
|
|
209
|
+
channels: {},
|
|
210
|
+
capabilities: [],
|
|
211
|
+
timestamp: new Date().toISOString(),
|
|
212
|
+
};
|
|
213
|
+
const script = buildPushDropScript(privKey, payload);
|
|
214
|
+
assert(parseIdentityOutput(script) === null, 'Should reject wrong type');
|
|
215
|
+
});
|
|
216
|
+
test('Identity: invalid identity key rejected', () => {
|
|
217
|
+
const privKey = PrivateKey.fromRandom();
|
|
218
|
+
const payload = {
|
|
219
|
+
protocol: PROTOCOL_ID,
|
|
220
|
+
type: 'identity',
|
|
221
|
+
identityKey: 'not-a-valid-key',
|
|
222
|
+
name: 'test',
|
|
223
|
+
description: '',
|
|
224
|
+
channels: {},
|
|
225
|
+
capabilities: [],
|
|
226
|
+
timestamp: new Date().toISOString(),
|
|
227
|
+
};
|
|
228
|
+
const script = buildPushDropScript(privKey, payload);
|
|
229
|
+
assert(parseIdentityOutput(script) === null, 'Should reject invalid identity key');
|
|
230
|
+
});
|
|
231
|
+
test('Identity: short identity key rejected', () => {
|
|
232
|
+
const privKey = PrivateKey.fromRandom();
|
|
233
|
+
const payload = {
|
|
234
|
+
protocol: PROTOCOL_ID,
|
|
235
|
+
type: 'identity',
|
|
236
|
+
identityKey: '02abcd', // Too short
|
|
237
|
+
name: 'test',
|
|
238
|
+
description: '',
|
|
239
|
+
channels: {},
|
|
240
|
+
capabilities: [],
|
|
241
|
+
timestamp: new Date().toISOString(),
|
|
242
|
+
};
|
|
243
|
+
const script = buildPushDropScript(privKey, payload);
|
|
244
|
+
assert(parseIdentityOutput(script) === null, 'Should reject short identity key');
|
|
245
|
+
});
|
|
246
|
+
test('Identity: empty name rejected', () => {
|
|
247
|
+
const privKey = PrivateKey.fromRandom();
|
|
248
|
+
const payload = {
|
|
249
|
+
protocol: PROTOCOL_ID,
|
|
250
|
+
type: 'identity',
|
|
251
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
252
|
+
name: '',
|
|
253
|
+
description: '',
|
|
254
|
+
channels: {},
|
|
255
|
+
capabilities: [],
|
|
256
|
+
timestamp: new Date().toISOString(),
|
|
257
|
+
};
|
|
258
|
+
const script = buildPushDropScript(privKey, payload);
|
|
259
|
+
assert(parseIdentityOutput(script) === null, 'Should reject empty name');
|
|
260
|
+
});
|
|
261
|
+
test('Identity: non-array capabilities rejected', () => {
|
|
262
|
+
const privKey = PrivateKey.fromRandom();
|
|
263
|
+
const payload = {
|
|
264
|
+
protocol: PROTOCOL_ID,
|
|
265
|
+
type: 'identity',
|
|
266
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
267
|
+
name: 'test',
|
|
268
|
+
description: '',
|
|
269
|
+
channels: {},
|
|
270
|
+
capabilities: 'not-an-array',
|
|
271
|
+
timestamp: new Date().toISOString(),
|
|
272
|
+
};
|
|
273
|
+
const script = buildPushDropScript(privKey, payload);
|
|
274
|
+
assert(parseIdentityOutput(script) === null, 'Should reject non-array capabilities');
|
|
275
|
+
});
|
|
276
|
+
// ============================================================================
|
|
277
|
+
// Service Payload Tests
|
|
278
|
+
// ============================================================================
|
|
279
|
+
console.log('\n=== Service Payload Tests ===\n');
|
|
280
|
+
test('Service: valid payload accepted', () => {
|
|
281
|
+
const privKey = PrivateKey.fromRandom();
|
|
282
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
283
|
+
const payload = {
|
|
284
|
+
protocol: PROTOCOL_ID,
|
|
285
|
+
type: 'service',
|
|
286
|
+
identityKey,
|
|
287
|
+
serviceId: 'test-service',
|
|
288
|
+
name: 'Test Service',
|
|
289
|
+
description: 'A test service',
|
|
290
|
+
pricing: { model: 'per-task', amountSats: 100 },
|
|
291
|
+
timestamp: new Date().toISOString(),
|
|
292
|
+
};
|
|
293
|
+
const script = buildPushDropScript(privKey, payload);
|
|
294
|
+
const parsed = parseServiceOutput(script);
|
|
295
|
+
assert(parsed !== null, 'Should parse valid service');
|
|
296
|
+
assertEqual(parsed.serviceId, 'test-service', 'Service ID should match');
|
|
297
|
+
assertEqual(parsed.pricing.amountSats, 100, 'Price should match');
|
|
298
|
+
});
|
|
299
|
+
test('Service: empty serviceId rejected', () => {
|
|
300
|
+
const privKey = PrivateKey.fromRandom();
|
|
301
|
+
const payload = {
|
|
302
|
+
protocol: PROTOCOL_ID,
|
|
303
|
+
type: 'service',
|
|
304
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
305
|
+
serviceId: '',
|
|
306
|
+
name: 'Test',
|
|
307
|
+
description: '',
|
|
308
|
+
pricing: { model: 'per-task', amountSats: 100 },
|
|
309
|
+
timestamp: new Date().toISOString(),
|
|
310
|
+
};
|
|
311
|
+
const script = buildPushDropScript(privKey, payload);
|
|
312
|
+
assert(parseServiceOutput(script) === null, 'Should reject empty serviceId');
|
|
313
|
+
});
|
|
314
|
+
test('Service: missing pricing rejected', () => {
|
|
315
|
+
const privKey = PrivateKey.fromRandom();
|
|
316
|
+
const payload = {
|
|
317
|
+
protocol: PROTOCOL_ID,
|
|
318
|
+
type: 'service',
|
|
319
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
320
|
+
serviceId: 'test',
|
|
321
|
+
name: 'Test',
|
|
322
|
+
description: '',
|
|
323
|
+
timestamp: new Date().toISOString(),
|
|
324
|
+
};
|
|
325
|
+
const script = buildPushDropScript(privKey, payload);
|
|
326
|
+
assert(parseServiceOutput(script) === null, 'Should reject missing pricing');
|
|
327
|
+
});
|
|
328
|
+
test('Service: invalid pricing rejected', () => {
|
|
329
|
+
const privKey = PrivateKey.fromRandom();
|
|
330
|
+
const payload = {
|
|
331
|
+
protocol: PROTOCOL_ID,
|
|
332
|
+
type: 'service',
|
|
333
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
334
|
+
serviceId: 'test',
|
|
335
|
+
name: 'Test',
|
|
336
|
+
description: '',
|
|
337
|
+
pricing: { model: 'per-task', amountSats: 'not-a-number' },
|
|
338
|
+
timestamp: new Date().toISOString(),
|
|
339
|
+
};
|
|
340
|
+
const script = buildPushDropScript(privKey, payload);
|
|
341
|
+
assert(parseServiceOutput(script) === null, 'Should reject invalid pricing');
|
|
342
|
+
});
|
|
343
|
+
// ============================================================================
|
|
344
|
+
// Revocation Payload Tests
|
|
345
|
+
// ============================================================================
|
|
346
|
+
console.log('\n=== Revocation Payload Tests ===\n');
|
|
347
|
+
test('Revocation: valid payload accepted', () => {
|
|
348
|
+
const privKey = PrivateKey.fromRandom();
|
|
349
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
350
|
+
const payload = {
|
|
351
|
+
protocol: PROTOCOL_ID,
|
|
352
|
+
type: 'identity-revocation',
|
|
353
|
+
identityKey,
|
|
354
|
+
reason: 'Test revocation',
|
|
355
|
+
timestamp: new Date().toISOString(),
|
|
356
|
+
};
|
|
357
|
+
const script = buildPushDropScript(privKey, payload);
|
|
358
|
+
const parsed = parseRevocationOutput(script);
|
|
359
|
+
assert(parsed !== null, 'Should parse valid revocation');
|
|
360
|
+
assertEqual(parsed.identityKey, identityKey, 'Identity key should match');
|
|
361
|
+
});
|
|
362
|
+
test('Revocation: invalid identity key rejected', () => {
|
|
363
|
+
const privKey = PrivateKey.fromRandom();
|
|
364
|
+
const payload = {
|
|
365
|
+
protocol: PROTOCOL_ID,
|
|
366
|
+
type: 'identity-revocation',
|
|
367
|
+
identityKey: 'invalid',
|
|
368
|
+
timestamp: new Date().toISOString(),
|
|
369
|
+
};
|
|
370
|
+
const script = buildPushDropScript(privKey, payload);
|
|
371
|
+
assert(parseRevocationOutput(script) === null, 'Should reject invalid identity key');
|
|
372
|
+
});
|
|
373
|
+
// ============================================================================
|
|
374
|
+
// Topic Manager Simulation Tests
|
|
375
|
+
// ============================================================================
|
|
376
|
+
console.log('\n=== Topic Manager Simulation Tests ===\n');
|
|
377
|
+
test('TopicManager: identity output admitted', async () => {
|
|
378
|
+
const privKey = PrivateKey.fromRandom();
|
|
379
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
380
|
+
const sourceTx = createSourceTransaction(privKey);
|
|
381
|
+
const payload = {
|
|
382
|
+
protocol: PROTOCOL_ID,
|
|
383
|
+
type: 'identity',
|
|
384
|
+
identityKey,
|
|
385
|
+
name: 'test-agent',
|
|
386
|
+
description: 'Test',
|
|
387
|
+
channels: {},
|
|
388
|
+
capabilities: [],
|
|
389
|
+
timestamp: new Date().toISOString(),
|
|
390
|
+
};
|
|
391
|
+
const tx = await createSignedTransaction(privKey, sourceTx, 0, payload);
|
|
392
|
+
const beef = new Beef();
|
|
393
|
+
beef.mergeTransaction(tx);
|
|
394
|
+
const binary = beef.toBinary();
|
|
395
|
+
const result = identifyIdentityOutputs(binary);
|
|
396
|
+
assertEqual(result.outputsToAdmit.length, 1, 'Should admit 1 output');
|
|
397
|
+
assertEqual(result.outputsToAdmit[0], 0, 'Should admit output 0');
|
|
398
|
+
});
|
|
399
|
+
test('TopicManager: revocation output admitted', async () => {
|
|
400
|
+
const privKey = PrivateKey.fromRandom();
|
|
401
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
402
|
+
const sourceTx = createSourceTransaction(privKey);
|
|
403
|
+
const payload = {
|
|
404
|
+
protocol: PROTOCOL_ID,
|
|
405
|
+
type: 'identity-revocation',
|
|
406
|
+
identityKey,
|
|
407
|
+
timestamp: new Date().toISOString(),
|
|
408
|
+
};
|
|
409
|
+
const tx = await createSignedTransaction(privKey, sourceTx, 0, payload);
|
|
410
|
+
const beef = new Beef();
|
|
411
|
+
beef.mergeTransaction(tx);
|
|
412
|
+
const binary = beef.toBinary();
|
|
413
|
+
const result = identifyIdentityOutputs(binary);
|
|
414
|
+
assertEqual(result.outputsToAdmit.length, 1, 'Should admit revocation');
|
|
415
|
+
});
|
|
416
|
+
test('TopicManager: service output admitted', async () => {
|
|
417
|
+
const privKey = PrivateKey.fromRandom();
|
|
418
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
419
|
+
const sourceTx = createSourceTransaction(privKey);
|
|
420
|
+
const payload = {
|
|
421
|
+
protocol: PROTOCOL_ID,
|
|
422
|
+
type: 'service',
|
|
423
|
+
identityKey,
|
|
424
|
+
serviceId: 'test-svc',
|
|
425
|
+
name: 'Test Service',
|
|
426
|
+
description: 'Test',
|
|
427
|
+
pricing: { model: 'per-task', amountSats: 50 },
|
|
428
|
+
timestamp: new Date().toISOString(),
|
|
429
|
+
};
|
|
430
|
+
const tx = await createSignedTransaction(privKey, sourceTx, 0, payload);
|
|
431
|
+
const beef = new Beef();
|
|
432
|
+
beef.mergeTransaction(tx);
|
|
433
|
+
const binary = beef.toBinary();
|
|
434
|
+
const result = identifyServiceOutputs(binary);
|
|
435
|
+
assertEqual(result.outputsToAdmit.length, 1, 'Should admit service');
|
|
436
|
+
});
|
|
437
|
+
test('TopicManager: invalid payload not admitted', async () => {
|
|
438
|
+
const privKey = PrivateKey.fromRandom();
|
|
439
|
+
const sourceTx = createSourceTransaction(privKey);
|
|
440
|
+
// Invalid payload (wrong protocol)
|
|
441
|
+
const payload = {
|
|
442
|
+
protocol: 'wrong',
|
|
443
|
+
type: 'identity',
|
|
444
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
445
|
+
name: 'test',
|
|
446
|
+
description: '',
|
|
447
|
+
channels: {},
|
|
448
|
+
capabilities: [],
|
|
449
|
+
timestamp: new Date().toISOString(),
|
|
450
|
+
};
|
|
451
|
+
const tx = await createSignedTransaction(privKey, sourceTx, 0, payload);
|
|
452
|
+
const beef = new Beef();
|
|
453
|
+
beef.mergeTransaction(tx);
|
|
454
|
+
const binary = beef.toBinary();
|
|
455
|
+
const result = identifyIdentityOutputs(binary);
|
|
456
|
+
assertEqual(result.outputsToAdmit.length, 0, 'Should not admit invalid payload');
|
|
457
|
+
});
|
|
458
|
+
// ============================================================================
|
|
459
|
+
// Transaction Chain Tests
|
|
460
|
+
// ============================================================================
|
|
461
|
+
console.log('\n=== Transaction Chain Tests ===\n');
|
|
462
|
+
test('Chain: two unconfirmed transactions', async () => {
|
|
463
|
+
const privKey = PrivateKey.fromRandom();
|
|
464
|
+
const identityKey = privKey.toPublicKey().toString();
|
|
465
|
+
const pubKeyHash = privKey.toPublicKey().toHash();
|
|
466
|
+
// Grandparent (simulating mined)
|
|
467
|
+
const grandparentTx = createSourceTransaction(privKey, 100000);
|
|
468
|
+
// Parent (first overlay tx)
|
|
469
|
+
const parentPayload = {
|
|
470
|
+
protocol: PROTOCOL_ID,
|
|
471
|
+
type: 'identity',
|
|
472
|
+
identityKey,
|
|
473
|
+
name: 'parent-tx',
|
|
474
|
+
description: 'First',
|
|
475
|
+
channels: {},
|
|
476
|
+
capabilities: [],
|
|
477
|
+
timestamp: new Date().toISOString(),
|
|
478
|
+
};
|
|
479
|
+
const parentTx = await createSignedTransaction(privKey, grandparentTx, 0, parentPayload, 99900);
|
|
480
|
+
// Child (second overlay tx, spending parent's change)
|
|
481
|
+
const childPayload = {
|
|
482
|
+
protocol: PROTOCOL_ID,
|
|
483
|
+
type: 'service',
|
|
484
|
+
identityKey,
|
|
485
|
+
serviceId: 'child-svc',
|
|
486
|
+
name: 'Child Service',
|
|
487
|
+
description: 'Second',
|
|
488
|
+
pricing: { model: 'per-task', amountSats: 25 },
|
|
489
|
+
timestamp: new Date().toISOString(),
|
|
490
|
+
};
|
|
491
|
+
const childTx = new Transaction();
|
|
492
|
+
childTx.addInput({
|
|
493
|
+
sourceTransaction: parentTx,
|
|
494
|
+
sourceOutputIndex: 1, // Change output
|
|
495
|
+
unlockingScriptTemplate: new P2PKH().unlock(privKey),
|
|
496
|
+
});
|
|
497
|
+
childTx.addOutput({
|
|
498
|
+
lockingScript: buildPushDropScript(privKey, childPayload),
|
|
499
|
+
satoshis: 1,
|
|
500
|
+
});
|
|
501
|
+
childTx.addOutput({
|
|
502
|
+
lockingScript: new P2PKH().lock(pubKeyHash),
|
|
503
|
+
satoshis: 99800,
|
|
504
|
+
});
|
|
505
|
+
await childTx.sign();
|
|
506
|
+
// Build BEEF
|
|
507
|
+
const beef = new Beef();
|
|
508
|
+
beef.mergeTransaction(childTx);
|
|
509
|
+
const binary = beef.toBinary();
|
|
510
|
+
const validation = validateBeef(binary);
|
|
511
|
+
assert(validation.valid, validation.error || 'BEEF should be valid');
|
|
512
|
+
assert(validation.txCount >= 3, `Should have at least 3 txs, got ${validation.txCount}`);
|
|
513
|
+
// Verify service output is admitted
|
|
514
|
+
const result = identifyServiceOutputs(binary);
|
|
515
|
+
assertEqual(result.outputsToAdmit.length, 1, 'Should admit service output');
|
|
516
|
+
});
|
|
517
|
+
test('Chain: BEEF ancestry validation', async () => {
|
|
518
|
+
const privKey = PrivateKey.fromRandom();
|
|
519
|
+
const sourceTx = createSourceTransaction(privKey);
|
|
520
|
+
const tx = await createSignedTransaction(privKey, sourceTx, 0, { test: true });
|
|
521
|
+
const beef = new Beef();
|
|
522
|
+
beef.mergeTransaction(tx);
|
|
523
|
+
const binary = beef.toBinary();
|
|
524
|
+
const result = validateBeefAncestry(binary);
|
|
525
|
+
assert(result.valid, result.error || 'Ancestry should be valid');
|
|
526
|
+
assert(result.chain.length >= 2, 'Chain should have at least 2 txids');
|
|
527
|
+
});
|
|
528
|
+
// ============================================================================
|
|
529
|
+
// PushDrop Script Format Tests
|
|
530
|
+
// ============================================================================
|
|
531
|
+
console.log('\n=== PushDrop Script Format Tests ===\n');
|
|
532
|
+
test('Script: PushDrop format with P2PK lock', () => {
|
|
533
|
+
const privKey = PrivateKey.fromRandom();
|
|
534
|
+
const payload = { test: 'data' };
|
|
535
|
+
const script = buildPushDropScript(privKey, payload);
|
|
536
|
+
const chunks = script.chunks;
|
|
537
|
+
// First chunk should be pubkey push (33 bytes)
|
|
538
|
+
assertEqual(chunks[0].op, 33, 'First op should push 33 bytes (pubkey)');
|
|
539
|
+
assert(chunks[0].data !== undefined, 'Should have pubkey data');
|
|
540
|
+
assertEqual(chunks[0].data.length, 33, 'Pubkey should be 33 bytes');
|
|
541
|
+
// Second chunk should be OP_CHECKSIG
|
|
542
|
+
assertEqual(chunks[1].op, OP.OP_CHECKSIG, 'Second op should be OP_CHECKSIG');
|
|
543
|
+
// Should have data field and OP_DROP
|
|
544
|
+
assert(chunks.length >= 4, 'Should have at least 4 chunks');
|
|
545
|
+
});
|
|
546
|
+
test('Script: JSON payload extraction', () => {
|
|
547
|
+
const privKey = PrivateKey.fromRandom();
|
|
548
|
+
const payload = { foo: 'bar', num: 42 };
|
|
549
|
+
const script = buildPushDropScript(privKey, payload);
|
|
550
|
+
const fields = extractPushDropFields(script);
|
|
551
|
+
assert(fields !== null, 'Should extract fields');
|
|
552
|
+
assert(fields.length >= 1, 'Should have at least 1 field');
|
|
553
|
+
const jsonStr = new TextDecoder().decode(new Uint8Array(fields[0]));
|
|
554
|
+
const parsed = JSON.parse(jsonStr);
|
|
555
|
+
assertEqual(parsed.foo, 'bar', 'Foo should match');
|
|
556
|
+
assertEqual(parsed.num, 42, 'Num should match');
|
|
557
|
+
});
|
|
558
|
+
test('Script: large payload handling', () => {
|
|
559
|
+
const privKey = PrivateKey.fromRandom();
|
|
560
|
+
const largePayload = {
|
|
561
|
+
protocol: PROTOCOL_ID,
|
|
562
|
+
type: 'identity',
|
|
563
|
+
identityKey: privKey.toPublicKey().toString(),
|
|
564
|
+
name: 'test',
|
|
565
|
+
description: 'A'.repeat(500), // Large description
|
|
566
|
+
channels: {},
|
|
567
|
+
capabilities: Array(50).fill('cap'), // Many capabilities
|
|
568
|
+
timestamp: new Date().toISOString(),
|
|
569
|
+
};
|
|
570
|
+
const script = buildPushDropScript(privKey, largePayload);
|
|
571
|
+
const fields = extractPushDropFields(script);
|
|
572
|
+
assert(fields !== null, 'Should handle large payload');
|
|
573
|
+
const parsed = JSON.parse(new TextDecoder().decode(new Uint8Array(fields[0])));
|
|
574
|
+
assertEqual(parsed.description.length, 500, 'Description should be preserved');
|
|
575
|
+
});
|
|
576
|
+
// ============================================================================
|
|
577
|
+
// Summary
|
|
578
|
+
// ============================================================================
|
|
579
|
+
// Give async tests time to complete
|
|
580
|
+
setTimeout(() => {
|
|
581
|
+
console.log('\n========================================');
|
|
582
|
+
const passed = results.filter(r => r.passed).length;
|
|
583
|
+
const failed = results.filter(r => !r.passed).length;
|
|
584
|
+
console.log(`Tests completed: ${passed} passed, ${failed} failed`);
|
|
585
|
+
console.log('========================================\n');
|
|
586
|
+
if (failed > 0) {
|
|
587
|
+
console.log('Failed tests:');
|
|
588
|
+
results.filter(r => !r.passed).forEach(r => {
|
|
589
|
+
console.log(` - ${r.name}: ${r.error}`);
|
|
590
|
+
});
|
|
591
|
+
process.exit(1);
|
|
592
|
+
}
|
|
593
|
+
}, 2000);
|
|
@@ -0,0 +1,12 @@
|
|
|
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
|
+
export {};
|
|
@@ -0,0 +1,86 @@
|
|
|
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
|
+
import { PrivateKey, Transaction, P2PKH, CachedKeyDeriver, Utils } from '@bsv/sdk';
|
|
13
|
+
import { brc29ProtocolID } from '@bsv/wallet-toolbox';
|
|
14
|
+
async function assert(condition, message) {
|
|
15
|
+
if (!condition) {
|
|
16
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async function runTests() {
|
|
20
|
+
console.log('๐งช Running Key Derivation Tests...\n');
|
|
21
|
+
// Test setup
|
|
22
|
+
const rootPrivKey = PrivateKey.fromRandom();
|
|
23
|
+
const keyDeriver = new CachedKeyDeriver(rootPrivKey);
|
|
24
|
+
const derivationPrefix = Utils.toBase64(Array.from(Utils.toArray('import', 'utf8')));
|
|
25
|
+
const derivationSuffix = Utils.toBase64(Array.from(Utils.toArray('now', 'utf8')));
|
|
26
|
+
const keyString = `${derivationPrefix} ${derivationSuffix}`;
|
|
27
|
+
const childPrivKey = keyDeriver.derivePrivateKey(brc29ProtocolID, keyString, 'self');
|
|
28
|
+
const pubKey = keyDeriver.derivePublicKey(brc29ProtocolID, keyString, 'self', true);
|
|
29
|
+
const derivedAddress = pubKey.toAddress();
|
|
30
|
+
const hashResult = pubKey.toHash();
|
|
31
|
+
const derivedHash160 = typeof hashResult === 'string'
|
|
32
|
+
? new Uint8Array(hashResult.match(/.{2}/g).map(h => parseInt(h, 16)))
|
|
33
|
+
: new Uint8Array(hashResult);
|
|
34
|
+
// Test 1: Consistency
|
|
35
|
+
console.log('โ Test 1: Derived keys are consistent');
|
|
36
|
+
const keyDeriver2 = new CachedKeyDeriver(rootPrivKey);
|
|
37
|
+
const childPrivKey2 = keyDeriver2.derivePrivateKey(brc29ProtocolID, keyString, 'self');
|
|
38
|
+
await assert(childPrivKey.toHex() === childPrivKey2.toHex(), 'Child keys should be identical');
|
|
39
|
+
// Test 2: Child key matches derived address
|
|
40
|
+
console.log('โ Test 2: Child private key matches derived address');
|
|
41
|
+
const childPubKey = childPrivKey.toPublicKey();
|
|
42
|
+
const childAddress = childPubKey.toAddress();
|
|
43
|
+
await assert(childAddress === derivedAddress, 'Child key address should match derived address');
|
|
44
|
+
// Test 3: Root key does NOT match (critical!)
|
|
45
|
+
console.log('โ Test 3: CRITICAL - Root key does NOT match derived address');
|
|
46
|
+
const rootAddress = rootPrivKey.toPublicKey().toAddress();
|
|
47
|
+
await assert(rootAddress !== derivedAddress, 'Root address must differ from derived address');
|
|
48
|
+
// Test 4: Transaction with child key succeeds
|
|
49
|
+
console.log('โ Test 4: CRITICAL - Transaction signed with child key validates');
|
|
50
|
+
const fundingTx = new Transaction();
|
|
51
|
+
fundingTx.addOutput({
|
|
52
|
+
lockingScript: new P2PKH().lock(Array.from(derivedHash160)),
|
|
53
|
+
satoshis: 1000,
|
|
54
|
+
});
|
|
55
|
+
const spendingTx = new Transaction();
|
|
56
|
+
spendingTx.addInput({
|
|
57
|
+
sourceTransaction: fundingTx,
|
|
58
|
+
sourceOutputIndex: 0,
|
|
59
|
+
unlockingScriptTemplate: new P2PKH().unlock(childPrivKey),
|
|
60
|
+
});
|
|
61
|
+
spendingTx.addOutput({
|
|
62
|
+
lockingScript: new P2PKH().lock(Array.from(derivedHash160)),
|
|
63
|
+
satoshis: 900,
|
|
64
|
+
});
|
|
65
|
+
await spendingTx.sign();
|
|
66
|
+
const inputScript = spendingTx.inputs[0].unlockingScript;
|
|
67
|
+
await assert(!!inputScript, 'Unlocking script should be present');
|
|
68
|
+
await assert(Array.from(inputScript.toBinary()).length > 0, 'Script should have content');
|
|
69
|
+
// Test 5: Different paths produce different addresses
|
|
70
|
+
console.log('โ Test 5: Different derivation paths produce different addresses');
|
|
71
|
+
const path1 = Utils.toBase64(Array.from(Utils.toArray('import', 'utf8'))) + ' ' + Utils.toBase64(Array.from(Utils.toArray('now', 'utf8')));
|
|
72
|
+
const path2 = Utils.toBase64(Array.from(Utils.toArray('import', 'utf8'))) + ' ' + Utils.toBase64(Array.from(Utils.toArray('later', 'utf8')));
|
|
73
|
+
const pubKey1 = keyDeriver.derivePublicKey(brc29ProtocolID, path1, 'self', true);
|
|
74
|
+
const pubKey2 = keyDeriver.derivePublicKey(brc29ProtocolID, path2, 'self', true);
|
|
75
|
+
await assert(pubKey1.toAddress() !== pubKey2.toAddress(), 'Different paths should produce different addresses');
|
|
76
|
+
console.log('\nโ
All tests passed!\n');
|
|
77
|
+
console.log('Key derivation is working correctly:');
|
|
78
|
+
console.log(` Root address: ${rootAddress}`);
|
|
79
|
+
console.log(` Derived address: ${derivedAddress}`);
|
|
80
|
+
console.log(` Child key works: YES`);
|
|
81
|
+
console.log(` Root key works: NO (correctly rejected)\n`);
|
|
82
|
+
}
|
|
83
|
+
runTests().catch((err) => {
|
|
84
|
+
console.error('\nโ Tests failed:', err.message);
|
|
85
|
+
process.exit(1);
|
|
86
|
+
});
|