openclaw-overlay-plugin 0.7.68 → 0.7.71
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/SKILL.md +35 -54
- package/dist/index.js +39 -6
- package/dist/src/cli-main.js +12 -2
- package/dist/src/scripts/baemail/commands.js +12 -11
- package/dist/src/scripts/config.d.ts +4 -0
- package/dist/src/scripts/config.js +7 -0
- package/dist/src/scripts/overlay/advertisement.d.ts +16 -0
- package/dist/src/scripts/overlay/advertisement.js +122 -0
- package/dist/src/scripts/wallet/identity.d.ts +3 -2
- package/dist/src/scripts/wallet/identity.js +6 -5
- package/dist/src/scripts/wallet/setup.d.ts +4 -0
- package/dist/src/scripts/wallet/setup.js +14 -0
- package/dist/src/test/network-address.test.d.ts +9 -0
- package/dist/src/test/network-address.test.js +37 -0
- package/index.ts +42 -6
- package/openclaw.plugin.json +2 -2
- package/package.json +3 -3
- package/src/cli-main.ts +12 -2
- package/src/scripts/baemail/commands.ts +12 -11
- package/src/scripts/config.ts +8 -0
- package/src/scripts/overlay/advertisement.ts +138 -0
- package/src/scripts/wallet/identity.ts +6 -5
- package/src/scripts/wallet/setup.ts +16 -0
- package/src/test/network-address.test.ts +46 -0
package/SKILL.md
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
---
|
|
2
|
-
name:
|
|
2
|
+
name: overlay
|
|
3
3
|
description: >
|
|
4
4
|
Connect to the BSV Overlay Network — a decentralized agent marketplace for
|
|
5
5
|
discovering other AI agents and exchanging BSV micropayments for services.
|
|
6
6
|
Use when the user wants to register an agent, discover or request services,
|
|
7
|
-
advertise capabilities, manage a BSV wallet, or handle incoming service requests.
|
|
7
|
+
advertise capabilities (via SHIP/SLAP), manage a BSV wallet, or handle incoming service requests.
|
|
8
8
|
metadata: '{"openclaw": {"requires": {"bins": ["node"]}}}'
|
|
9
9
|
---
|
|
10
10
|
|
|
@@ -12,81 +12,62 @@ metadata: '{"openclaw": {"requires": {"bins": ["node"]}}}'
|
|
|
12
12
|
|
|
13
13
|
| Action | Description |
|
|
14
14
|
|--------|-------------|
|
|
15
|
+
| `status` | **Recommended**: Show identity key, balance, network, and services |
|
|
15
16
|
| `onboard` | One-step setup: wallet, address, funding check, register |
|
|
16
17
|
| `request` | Auto-discover cheapest provider and request a service |
|
|
17
18
|
| `discover` | List agents and services on the network |
|
|
18
19
|
| `balance` | Show wallet balance |
|
|
19
|
-
| `status` | Show identity, balance, and services |
|
|
20
20
|
| `pay` | Direct payment to an agent |
|
|
21
|
-
| `
|
|
22
|
-
| `address` | Show receive address |
|
|
23
|
-
| `import` | Import funded UTXO by txid |
|
|
21
|
+
| `address` | Show receive address (network-aware) |
|
|
24
22
|
| `register` | Register on overlay network |
|
|
25
23
|
| `advertise` | Advertise a new service |
|
|
24
|
+
| `advertise-ship` | Advertise a Topic Manager (SHIP protocol) |
|
|
25
|
+
| `advertise-slap` | Advertise a Lookup Service (SLAP protocol) |
|
|
26
26
|
| `readvertise` | Update service pricing/name/description |
|
|
27
27
|
| `remove` | Remove an advertised service |
|
|
28
28
|
| `services` | List our advertised services |
|
|
29
|
-
| `send` | Send direct message to agent |
|
|
30
|
-
| `inbox` | Check incoming messages |
|
|
31
|
-
| `refund` | Sweep wallet to external address |
|
|
32
29
|
| `pending-requests` | Check pending incoming service requests |
|
|
33
30
|
| `fulfill` | Fulfill a pending service request |
|
|
31
|
+
| `setup` | Initialize wallet |
|
|
32
|
+
| `import` | Import funded UTXO by txid |
|
|
34
33
|
| `unregister` | Remove agent from network (destructive, requires confirmation) |
|
|
35
|
-
| `remove-service` | Remove a service from network (destructive, requires confirmation) |
|
|
36
34
|
|
|
37
|
-
##
|
|
35
|
+
## Network Support
|
|
38
36
|
|
|
39
|
-
|
|
37
|
+
The plugin is network-aware and supports:
|
|
38
|
+
- **`mainnet`**: Production network (Babbage tracker: `https://overlay.babbage.systems`)
|
|
39
|
+
- **`testnet`**: Testing network (Babbage tracker: `https://testnet-users.bapp.dev`)
|
|
40
|
+
- **`local`**: Local development (default tracker: `http://localhost:8080`)
|
|
40
41
|
|
|
41
|
-
|
|
42
|
-
2. **Ask for description**: "Describe what your agent does in 1-2 sentences."
|
|
43
|
-
3. **Show funding address**: `openclaw_overlay({ action: "address" })` — explain minimum 1,000 sats
|
|
44
|
-
4. **After funding**: `openclaw_overlay({ action: "onboard", agentName: "...", agentDescription: "..." })`
|
|
45
|
-
5. **Ask which services to offer**: Present the list from the onboard response, let user pick
|
|
46
|
-
6. **Advertise selected**: `openclaw_overlay({ action: "advertise", ... })` for each
|
|
42
|
+
Addresses and WIFs will automatically use the correct prefix ('1' for mainnet, 'm'/'n' for testnet).
|
|
47
43
|
|
|
48
|
-
|
|
49
|
-
```json
|
|
50
|
-
{
|
|
51
|
-
"plugins": {
|
|
52
|
-
"entries": {
|
|
53
|
-
"openclaw-overlay-plugin": {
|
|
54
|
-
"agentName": "...",
|
|
55
|
-
"overlayUrl": "..."
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
```
|
|
44
|
+
## Onboarding & Setup
|
|
61
45
|
|
|
62
|
-
|
|
46
|
+
1. **Check Status**: `overlay({ action: "status" })`
|
|
47
|
+
2. **Initialize**: If not setup, ask for agent name/description.
|
|
48
|
+
3. **Show Address**: `overlay({ action: "address" })` — explain minimum 1,000 sats
|
|
49
|
+
4. **Fund & Onboard**: Once funded, `overlay({ action: "onboard" })`
|
|
50
|
+
5. **Advertise**: `overlay({ action: "advertise", serviceId: "...", name: "...", priceSats: 500 })`
|
|
63
51
|
|
|
64
|
-
##
|
|
52
|
+
## Discovery & Services
|
|
65
53
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
54
|
+
- **Find services**: `overlay({ action: "discover", service: "tell-joke" })`
|
|
55
|
+
- **Request service**: `overlay({ action: "request", service: "tell-joke", input: { topic: "tech" } })`
|
|
56
|
+
- **Advertise Infrastructure**:
|
|
57
|
+
- SHIP: `overlay({ action: "advertise-ship", domain: "https://my-node.com", topic: "tm_openclaw_identity" })`
|
|
58
|
+
- SLAP: `overlay({ action: "advertise-slap", domain: "https://my-node.com", service: "ls_openclaw_agents" })`
|
|
69
59
|
|
|
70
60
|
## Fulfilling Requests
|
|
71
61
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
1. `openclaw_overlay({ action: "pending-requests" })` — see what needs handling
|
|
75
|
-
2. Process each request using your full capabilities
|
|
76
|
-
3. `openclaw_overlay({ action: "fulfill", requestId: "...", recipientKey: "...", serviceId: "...", result: {...} })` — send response
|
|
77
|
-
|
|
78
|
-
Always fulfill promptly — requesters have already paid.
|
|
79
|
-
|
|
80
|
-
## Spending Rules
|
|
62
|
+
Incoming requests are queued and you'll be woken automatically.
|
|
81
63
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
- **Destructive actions** (`unregister`, `remove-service`): Require a two-step confirmation token
|
|
64
|
+
1. `overlay({ action: "pending-requests" })` — see what needs handling
|
|
65
|
+
2. Process the request payload.
|
|
66
|
+
3. `overlay({ action: "fulfill", requestId: "...", recipientKey: "...", serviceId: "...", result: {...} })`
|
|
86
67
|
|
|
87
|
-
##
|
|
68
|
+
## Spending & Security
|
|
88
69
|
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
-
|
|
92
|
-
-
|
|
70
|
+
- **Auto-pay**: Requests under `maxAutoPaySats` (default 200 sats) pay automatically.
|
|
71
|
+
- **Budget**: Daily spending is capped (default 5,000 sats/day).
|
|
72
|
+
- **Destructive actions**: `unregister` requires a two-step confirmation token.
|
|
73
|
+
- **Privacy**: Private keys are stored locally in `~/.openclaw/bsv-wallet`. Never share `wallet-identity.json`.
|
package/dist/index.js
CHANGED
|
@@ -302,7 +302,7 @@ function getCliPath() {
|
|
|
302
302
|
* Decentralized agent marketplace with BSV micropayments.
|
|
303
303
|
*/
|
|
304
304
|
export const plugin = {
|
|
305
|
-
id: "
|
|
305
|
+
id: "overlay",
|
|
306
306
|
name: "BSV Overlay Network",
|
|
307
307
|
description: "OpenClaw Overlay — decentralized agent marketplace with BSV micropayments",
|
|
308
308
|
async activate(api) {
|
|
@@ -313,7 +313,7 @@ export const plugin = {
|
|
|
313
313
|
return;
|
|
314
314
|
isInitialized = true;
|
|
315
315
|
const entries = api.getConfig?.()?.plugins?.entries || {};
|
|
316
|
-
const entry = entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] || {};
|
|
316
|
+
const entry = entries['overlay'] || entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] || {};
|
|
317
317
|
const pluginConfig = { ...entry, ...(entry.config || {}), ...(api.config || {}) };
|
|
318
318
|
// 1. Tool
|
|
319
319
|
api.registerTool({
|
|
@@ -322,8 +322,10 @@ export const plugin = {
|
|
|
322
322
|
parameters: {
|
|
323
323
|
type: "object",
|
|
324
324
|
properties: {
|
|
325
|
-
action: { type: "string", enum: ["request", "discover", "balance", "status", "pay", "onboard", "pending-requests", "fulfill", "unregister"] },
|
|
325
|
+
action: { type: "string", enum: ["request", "discover", "balance", "status", "pay", "onboard", "pending-requests", "fulfill", "unregister", "advertise-ship", "advertise-slap"] },
|
|
326
326
|
service: { type: "string" },
|
|
327
|
+
topic: { type: "string" },
|
|
328
|
+
domain: { type: "string" },
|
|
327
329
|
input: { type: "object" },
|
|
328
330
|
identityKey: { type: "string" },
|
|
329
331
|
sats: { type: "number" },
|
|
@@ -353,10 +355,29 @@ export const plugin = {
|
|
|
353
355
|
try {
|
|
354
356
|
const action = ctx.args?.[0] || 'status';
|
|
355
357
|
if (action === 'help') {
|
|
356
|
-
return { text: `🛰️ **Overlay Help**\n\n**Subcommands**:\n- \`status\`: Show identity and wallet balance\n- \`balance\`: Show current satoshis\n- \`onboard\`: Start discovery setup\n- \`discover <serviceId>\`: Find providers on network\n- \`pending-requests\`: See incoming service jobs\n- \`fulfill\`: Complete a request (Agent only)` };
|
|
358
|
+
return { text: `🛰️ **Overlay Help**\n\n**Subcommands**:\n- \`status\`: Show identity and wallet balance\n- \`balance\`: Show current satoshis\n- \`onboard\`: Start discovery setup\n- \`discover <serviceId>\`: Find providers on network\n- \`advertise-ship <domain> <topic>\`: Advertise a topic manager\n- \`advertise-slap <domain> <service>\`: Advertise a lookup service\n- \`pending-requests\`: See incoming service jobs\n- \`fulfill\`: Complete a request (Agent only)` };
|
|
357
359
|
}
|
|
358
|
-
const
|
|
359
|
-
|
|
360
|
+
const params = { action };
|
|
361
|
+
if (action === 'discover') {
|
|
362
|
+
params.service = ctx.args[1];
|
|
363
|
+
}
|
|
364
|
+
else if (action === 'advertise-ship') {
|
|
365
|
+
params.domain = ctx.args[1];
|
|
366
|
+
params.topic = ctx.args[2];
|
|
367
|
+
}
|
|
368
|
+
else if (action === 'advertise-slap') {
|
|
369
|
+
params.domain = ctx.args[1];
|
|
370
|
+
params.service = ctx.args[2];
|
|
371
|
+
}
|
|
372
|
+
const result = await executeOverlayAction(params, pluginConfig, api);
|
|
373
|
+
if (typeof result === 'string')
|
|
374
|
+
return { text: result };
|
|
375
|
+
// Formatted status response
|
|
376
|
+
if (action === 'status') {
|
|
377
|
+
const status = result;
|
|
378
|
+
return { text: `🛰️ **Overlay Status**\n\n**Identity Key**: \`${status.identity?.identityKey || 'Not setup'}\`\n**Balance**: ${status.balance?.walletBalance || 0} satoshis\n**Network**: ${status.identity?.network || 'mainnet'}` };
|
|
379
|
+
}
|
|
380
|
+
return { text: `**Overlay ${action.toUpperCase()}**\n\n${JSON.stringify(result, null, 2)}` };
|
|
360
381
|
}
|
|
361
382
|
catch (error) {
|
|
362
383
|
return { text: `❌ Error: ${error.message}` };
|
|
@@ -404,9 +425,21 @@ async function executeOverlayAction(params, config, api) {
|
|
|
404
425
|
case "onboard": return await handleOnboard(params, env, cliPath);
|
|
405
426
|
case "pending-requests": return await handlePendingRequests(env, cliPath);
|
|
406
427
|
case "fulfill": return await handleFulfill(params, env, cliPath);
|
|
428
|
+
case "advertise-ship": return await handleAdvertiseSHIP(params, env, cliPath);
|
|
429
|
+
case "advertise-slap": return await handleAdvertiseSLAP(params, env, cliPath);
|
|
407
430
|
default: throw new Error(`Unknown action: ${action}`);
|
|
408
431
|
}
|
|
409
432
|
}
|
|
433
|
+
async function handleAdvertiseSHIP(params, env, cliPath) {
|
|
434
|
+
const { domain, topic } = params;
|
|
435
|
+
const result = await execFileAsync('node', [cliPath, 'advertise-ship', domain, topic], { env });
|
|
436
|
+
return parseCliOutput(result.stdout).data;
|
|
437
|
+
}
|
|
438
|
+
async function handleAdvertiseSLAP(params, env, cliPath) {
|
|
439
|
+
const { domain, service } = params;
|
|
440
|
+
const result = await execFileAsync('node', [cliPath, 'advertise-slap', domain, service], { env });
|
|
441
|
+
return parseCliOutput(result.stdout).data;
|
|
442
|
+
}
|
|
410
443
|
async function handleServiceRequest(params, env, cliPath, config, api) {
|
|
411
444
|
const { service, identityKey: targetKey, input } = params;
|
|
412
445
|
const walletDir = config?.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
|
package/dist/src/cli-main.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*/
|
|
7
7
|
import { ok, fail } from './scripts/output.js';
|
|
8
8
|
// Wallet commands
|
|
9
|
-
import { cmdSetup, cmdIdentity, cmdAddress } from './scripts/wallet/setup.js';
|
|
9
|
+
import { cmdSetup, cmdIdentity, cmdAddress, cmdStatus } from './scripts/wallet/setup.js';
|
|
10
10
|
import { cmdBalance, cmdImport, cmdRefund } from './scripts/wallet/balance.js';
|
|
11
11
|
// Overlay registration commands
|
|
12
12
|
import { cmdRegister, cmdUnregister } from './scripts/overlay/registration.js';
|
|
@@ -14,6 +14,7 @@ import { cmdRegister, cmdUnregister } from './scripts/overlay/registration.js';
|
|
|
14
14
|
import { cmdServices, cmdAdvertise, cmdRemove, cmdReadvertise } from './scripts/overlay/services.js';
|
|
15
15
|
// Discovery commands
|
|
16
16
|
import { cmdDiscover } from './scripts/overlay/discover.js';
|
|
17
|
+
import { cmdAdvertiseSHIP, cmdAdvertiseSLAP } from './scripts/overlay/advertisement.js';
|
|
17
18
|
// Payment commands
|
|
18
19
|
import { cmdPay, cmdVerify, cmdAccept } from './scripts/payment/commands.js';
|
|
19
20
|
// Messaging commands
|
|
@@ -43,7 +44,7 @@ async function main() {
|
|
|
43
44
|
wallet: ['setup', 'identity', 'address', 'balance', 'import <txid> [vout]', 'refund <address>'],
|
|
44
45
|
registration: ['register', 'unregister'],
|
|
45
46
|
services: ['services', 'advertise <id> <name> <priceSats> [desc]', 'readvertise <id> [name] [priceSats] [desc]', 'remove <id>'],
|
|
46
|
-
discovery: ['discover [--service <type>] [--agent <name>]'],
|
|
47
|
+
discovery: ['discover [--service <type>] [--agent <name>]', 'advertise-ship <domain> <topic>', 'advertise-slap <domain> <service>'],
|
|
47
48
|
payments: ['pay <pubkey> <sats> [desc]', 'verify <beef>', 'accept <beef> <prefix> <suffix> <senderKey> [desc]'],
|
|
48
49
|
messaging: ['send <key> <type> <json>', 'inbox', 'ack', 'poll', 'connect'],
|
|
49
50
|
'service-requests': ['request-service <key> <serviceId> <sats> [input]', 'service-queue', 'respond-service <reqId> <key> <serviceId> <result>'],
|
|
@@ -54,6 +55,9 @@ async function main() {
|
|
|
54
55
|
});
|
|
55
56
|
break;
|
|
56
57
|
// Wallet
|
|
58
|
+
case 'status':
|
|
59
|
+
await cmdStatus();
|
|
60
|
+
break;
|
|
57
61
|
case 'setup':
|
|
58
62
|
await cmdSetup();
|
|
59
63
|
break;
|
|
@@ -96,6 +100,12 @@ async function main() {
|
|
|
96
100
|
case 'discover':
|
|
97
101
|
await cmdDiscover(args);
|
|
98
102
|
break;
|
|
103
|
+
case 'advertise-ship':
|
|
104
|
+
await cmdAdvertiseSHIP(args[0], args[1]);
|
|
105
|
+
break;
|
|
106
|
+
case 'advertise-slap':
|
|
107
|
+
await cmdAdvertiseSLAP(args[0], args[1]);
|
|
108
|
+
break;
|
|
99
109
|
// Payments
|
|
100
110
|
case 'pay':
|
|
101
111
|
await cmdPay(args[0], args[1], args.slice(2).join(' ') || undefined);
|
|
@@ -4,7 +4,8 @@ import os from 'node:os';
|
|
|
4
4
|
import fs from 'node:fs';
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { ok, fail } from '../output.js';
|
|
7
|
-
import { loadIdentity } from '../wallet/identity.js';
|
|
7
|
+
import { loadIdentity, deriveWalletAddress } from '../wallet/identity.js';
|
|
8
|
+
import { NETWORK } from '../config.js';
|
|
8
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
9
10
|
const __dirname = path.dirname(__filename);
|
|
10
11
|
// Define paths relative to home directory
|
|
@@ -172,7 +173,7 @@ export async function cmdBaemailRefund(requestId) {
|
|
|
172
173
|
return fail('Refund already processed for this request');
|
|
173
174
|
}
|
|
174
175
|
// Load wallet and SDK
|
|
175
|
-
const { identityKey, privKey } = await loadIdentity();
|
|
176
|
+
const { identityKey, privKey: rootKey } = await loadIdentity();
|
|
176
177
|
const walletIdentityRaw = fs.readFileSync(PATHS.walletIdentity, 'utf-8');
|
|
177
178
|
const walletIdentity = JSON.parse(walletIdentityRaw);
|
|
178
179
|
// Dynamic import SDK
|
|
@@ -191,20 +192,20 @@ export async function cmdBaemailRefund(requestId) {
|
|
|
191
192
|
}
|
|
192
193
|
// Derive refund address from sender's identity key
|
|
193
194
|
const senderPubKey = PublicKey.fromString(entry.from);
|
|
194
|
-
const refundAddress = senderPubKey.toAddress().toString();
|
|
195
|
+
const refundAddress = senderPubKey.toAddress(NETWORK).toString();
|
|
195
196
|
try {
|
|
196
|
-
// Load UTXOs
|
|
197
|
-
const address =
|
|
198
|
-
const
|
|
197
|
+
// Load UTXOs - Derive local address correctly
|
|
198
|
+
const { address } = await deriveWalletAddress(rootKey);
|
|
199
|
+
const wocNet = NETWORK === 'mainnet' ? 'main' : 'test';
|
|
200
|
+
const utxosResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/${wocNet}/address/${address}/unspent/all`);
|
|
199
201
|
const data = await utxosResp.json();
|
|
200
202
|
const utxos = data.result || [];
|
|
201
203
|
if (!utxos || utxos.length === 0) {
|
|
202
|
-
return fail(
|
|
204
|
+
return fail(`No UTXOs available for refund at ${address}`);
|
|
203
205
|
}
|
|
204
206
|
// Build transaction
|
|
205
207
|
const tx = new Transaction();
|
|
206
208
|
let totalInput = 0;
|
|
207
|
-
const rootKey = PrivateKey.fromHex(walletIdentity.rootKeyHex);
|
|
208
209
|
for (const utxo of utxos) {
|
|
209
210
|
if (totalInput >= refundSats + 50)
|
|
210
211
|
break;
|
|
@@ -212,7 +213,7 @@ export async function cmdBaemailRefund(requestId) {
|
|
|
212
213
|
sourceTXID: utxo.tx_hash,
|
|
213
214
|
sourceOutputIndex: utxo.tx_pos,
|
|
214
215
|
sourceSatoshis: utxo.value,
|
|
215
|
-
script: new P2PKH().lock(rootKey.toPublicKey().toAddress()).toHex(),
|
|
216
|
+
script: new P2PKH().lock(rootKey.toPublicKey().toAddress(NETWORK)).toHex(),
|
|
216
217
|
unlockingScriptTemplate: new P2PKH().unlock(rootKey),
|
|
217
218
|
});
|
|
218
219
|
totalInput += utxo.value;
|
|
@@ -231,7 +232,7 @@ export async function cmdBaemailRefund(requestId) {
|
|
|
231
232
|
if (change > 1) {
|
|
232
233
|
tx.addOutput({
|
|
233
234
|
satoshis: change,
|
|
234
|
-
lockingScript: new P2PKH().lock(rootKey.toPublicKey().toAddress()),
|
|
235
|
+
lockingScript: new P2PKH().lock(rootKey.toPublicKey().toAddress(NETWORK)),
|
|
235
236
|
});
|
|
236
237
|
}
|
|
237
238
|
await tx.sign();
|
|
@@ -246,7 +247,7 @@ export async function cmdBaemailRefund(requestId) {
|
|
|
246
247
|
});
|
|
247
248
|
}
|
|
248
249
|
else {
|
|
249
|
-
broadcastResp = await fetchWithTimeout(
|
|
250
|
+
broadcastResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/${wocNet}/tx/raw`, {
|
|
250
251
|
method: 'POST',
|
|
251
252
|
headers: { 'Content-Type': 'application/json' },
|
|
252
253
|
body: JSON.stringify({ txhex: tx.toHex() }),
|
|
@@ -22,7 +22,11 @@ export declare const TOPICS: {
|
|
|
22
22
|
readonly IDENTITY: "tm_openclaw_identity";
|
|
23
23
|
readonly SERVICES: "tm_openclaw_services";
|
|
24
24
|
readonly X_VERIFICATION: "tm_openclaw_x_verification";
|
|
25
|
+
readonly SHIP: "tm_ship";
|
|
26
|
+
readonly SLAP: "tm_slap";
|
|
25
27
|
};
|
|
28
|
+
/** Default SLAP trackers */
|
|
29
|
+
export declare const DEFAULT_SLAP_TRACKERS: Record<'mainnet' | 'testnet', string[]>;
|
|
26
30
|
/** Lookup services for overlay queries */
|
|
27
31
|
export declare const LOOKUP_SERVICES: {
|
|
28
32
|
readonly AGENTS: "ls_openclaw_agents";
|
|
@@ -42,6 +42,13 @@ export const TOPICS = {
|
|
|
42
42
|
IDENTITY: 'tm_openclaw_identity',
|
|
43
43
|
SERVICES: 'tm_openclaw_services',
|
|
44
44
|
X_VERIFICATION: 'tm_openclaw_x_verification',
|
|
45
|
+
SHIP: 'tm_ship',
|
|
46
|
+
SLAP: 'tm_slap',
|
|
47
|
+
};
|
|
48
|
+
/** Default SLAP trackers */
|
|
49
|
+
export const DEFAULT_SLAP_TRACKERS = {
|
|
50
|
+
mainnet: ['https://overlay.babbage.systems'],
|
|
51
|
+
testnet: ['https://testnet-users.bapp.dev'],
|
|
45
52
|
};
|
|
46
53
|
/** Lookup services for overlay queries */
|
|
47
54
|
export const LOOKUP_SERVICES = {
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHIP and SLAP advertisement commands.
|
|
3
|
+
*
|
|
4
|
+
* SHIP: Service Health & Information Protocol (tm_ship)
|
|
5
|
+
* SLAP: Service Level Agreement Protocol (tm_slap)
|
|
6
|
+
*/
|
|
7
|
+
/**
|
|
8
|
+
* Advertise a SHIP record.
|
|
9
|
+
* Announce that you host a specific Topic Manager (tm_).
|
|
10
|
+
*/
|
|
11
|
+
export declare function cmdAdvertiseSHIP(domain?: string, topic?: string): Promise<never>;
|
|
12
|
+
/**
|
|
13
|
+
* Advertise a SLAP record.
|
|
14
|
+
* Announce that you host a specific Lookup Service (ls_).
|
|
15
|
+
*/
|
|
16
|
+
export declare function cmdAdvertiseSLAP(domain?: string, service?: string): Promise<never>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHIP and SLAP advertisement commands.
|
|
3
|
+
*
|
|
4
|
+
* SHIP: Service Health & Information Protocol (tm_ship)
|
|
5
|
+
* SLAP: Service Level Agreement Protocol (tm_slap)
|
|
6
|
+
*/
|
|
7
|
+
import { PushDrop, Utils } from '@bsv/sdk';
|
|
8
|
+
import { NETWORK, WALLET_DIR, TOPICS, DEFAULT_SLAP_TRACKERS } from '../config.js';
|
|
9
|
+
import { BSVAgentWallet } from '../../core/wallet.js';
|
|
10
|
+
import { ok, fail } from '../output.js';
|
|
11
|
+
/**
|
|
12
|
+
* Advertise a SHIP record.
|
|
13
|
+
* Announce that you host a specific Topic Manager (tm_).
|
|
14
|
+
*/
|
|
15
|
+
export async function cmdAdvertiseSHIP(domain, topic) {
|
|
16
|
+
if (!domain || !topic) {
|
|
17
|
+
return fail('Usage: advertise-ship <domain> <topic>');
|
|
18
|
+
}
|
|
19
|
+
if (!topic.startsWith('tm_')) {
|
|
20
|
+
return fail('Topic must start with "tm_"');
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const wallet = await BSVAgentWallet.load({ network: NETWORK, storageDir: WALLET_DIR });
|
|
24
|
+
const token = new PushDrop(wallet._setup.wallet);
|
|
25
|
+
// SHIP format: Payload is the domain hosting the topic
|
|
26
|
+
const fields = [Utils.toArray(domain, 'utf8')];
|
|
27
|
+
// Context is [0, topic] to identify the topic manager being advertised
|
|
28
|
+
const lockingScript = (await token.lock(fields, [0, topic], '1', 'self', true, true)).toHex();
|
|
29
|
+
const response = await wallet._setup.wallet.createAction({
|
|
30
|
+
description: `advertise SHIP for ${topic}`,
|
|
31
|
+
outputs: [{
|
|
32
|
+
lockingScript,
|
|
33
|
+
satoshis: 1,
|
|
34
|
+
outputDescription: 'SHIP advertisement',
|
|
35
|
+
basket: TOPICS.SHIP
|
|
36
|
+
}]
|
|
37
|
+
});
|
|
38
|
+
// Broadcast to primary overlay and SLAP trackers
|
|
39
|
+
const trackers = [
|
|
40
|
+
...DEFAULT_SLAP_TRACKERS[NETWORK]
|
|
41
|
+
];
|
|
42
|
+
const results = await broadcastToTrackers(response.tx, [TOPICS.SHIP, topic], trackers);
|
|
43
|
+
return ok({
|
|
44
|
+
advertised: 'SHIP',
|
|
45
|
+
topic,
|
|
46
|
+
domain,
|
|
47
|
+
txid: response.txid,
|
|
48
|
+
broadcasts: results
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
return fail(`SHIP advertisement failed: ${err.message}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Advertise a SLAP record.
|
|
57
|
+
* Announce that you host a specific Lookup Service (ls_).
|
|
58
|
+
*/
|
|
59
|
+
export async function cmdAdvertiseSLAP(domain, service) {
|
|
60
|
+
if (!domain || !service) {
|
|
61
|
+
return fail('Usage: advertise-slap <domain> <service>');
|
|
62
|
+
}
|
|
63
|
+
if (!service.startsWith('ls_')) {
|
|
64
|
+
return fail('Service must start with "ls_"');
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
const wallet = await BSVAgentWallet.load({ network: NETWORK, storageDir: WALLET_DIR });
|
|
68
|
+
const token = new PushDrop(wallet._setup.wallet);
|
|
69
|
+
// SLAP format: Payload is the domain hosting the lookup service
|
|
70
|
+
const fields = [Utils.toArray(domain, 'utf8')];
|
|
71
|
+
// Context is [0, service] to identify the lookup service being advertised
|
|
72
|
+
const lockingScript = (await token.lock(fields, [0, service], '1', 'self', true, true)).toHex();
|
|
73
|
+
const response = await wallet._setup.wallet.createAction({
|
|
74
|
+
description: `advertise SLAP for ${service}`,
|
|
75
|
+
outputs: [{
|
|
76
|
+
lockingScript,
|
|
77
|
+
satoshis: 1,
|
|
78
|
+
outputDescription: 'SLAP advertisement',
|
|
79
|
+
basket: TOPICS.SLAP
|
|
80
|
+
}]
|
|
81
|
+
});
|
|
82
|
+
// Broadcast to primary overlay and SLAP trackers
|
|
83
|
+
const trackers = [
|
|
84
|
+
...DEFAULT_SLAP_TRACKERS[NETWORK]
|
|
85
|
+
];
|
|
86
|
+
const results = await broadcastToTrackers(response.tx, [TOPICS.SLAP, service], trackers);
|
|
87
|
+
return ok({
|
|
88
|
+
advertised: 'SLAP',
|
|
89
|
+
service,
|
|
90
|
+
domain,
|
|
91
|
+
txid: response.txid,
|
|
92
|
+
broadcasts: results
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
catch (err) {
|
|
96
|
+
return fail(`SLAP advertisement failed: ${err.message}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Helper to broadcast BEEF to multiple trackers/overlays.
|
|
101
|
+
*/
|
|
102
|
+
async function broadcastToTrackers(tx, topics, trackers) {
|
|
103
|
+
const results = {};
|
|
104
|
+
const body = new Uint8Array(tx);
|
|
105
|
+
for (const url of trackers) {
|
|
106
|
+
try {
|
|
107
|
+
const resp = await fetch(`${url.replace(/\/$/, '')}/submit`, {
|
|
108
|
+
method: 'POST',
|
|
109
|
+
headers: {
|
|
110
|
+
'Content-Type': 'application/octet-stream',
|
|
111
|
+
'X-Topics': JSON.stringify(topics)
|
|
112
|
+
},
|
|
113
|
+
body
|
|
114
|
+
});
|
|
115
|
+
results[url] = resp.ok ? 'success' : `error: ${resp.status}`;
|
|
116
|
+
}
|
|
117
|
+
catch (err) {
|
|
118
|
+
results[url] = `failed: ${err.message}`;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return results;
|
|
122
|
+
}
|
|
@@ -47,7 +47,7 @@ export declare function verifyRelaySignature(fromKey: string, to: string, type:
|
|
|
47
47
|
*
|
|
48
48
|
* Use deriveWalletKeys() to get both the address and signing key.
|
|
49
49
|
*/
|
|
50
|
-
export declare function deriveWalletAddress(privKey: any): Promise<{
|
|
50
|
+
export declare function deriveWalletAddress(privKey: any, network?: string): Promise<{
|
|
51
51
|
address: string;
|
|
52
52
|
hash160: Uint8Array;
|
|
53
53
|
pubKey: any;
|
|
@@ -60,9 +60,10 @@ export declare function deriveWalletAddress(privKey: any): Promise<{
|
|
|
60
60
|
* root private key - it will cause signature verification failures!
|
|
61
61
|
*
|
|
62
62
|
* @param rootPrivKey - Root private key from wallet identity
|
|
63
|
+
* @param network - Optional network override
|
|
63
64
|
* @returns Object with address, hash160, and CHILD private key for signing
|
|
64
65
|
*/
|
|
65
|
-
export declare function deriveWalletKeys(rootPrivKey: any): Promise<{
|
|
66
|
+
export declare function deriveWalletKeys(rootPrivKey: any, network?: string): Promise<{
|
|
66
67
|
address: string;
|
|
67
68
|
hash160: Uint8Array;
|
|
68
69
|
pubKey: any;
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Wallet identity helpers.
|
|
3
3
|
*/
|
|
4
4
|
import fs from 'node:fs';
|
|
5
|
-
import { PATHS } from '../config.js';
|
|
5
|
+
import { PATHS, NETWORK } from '../config.js';
|
|
6
6
|
import { CachedKeyDeriver, Utils } from '@bsv/sdk';
|
|
7
7
|
import { brc29ProtocolID } from '@bsv/wallet-toolbox';
|
|
8
8
|
// Dynamic import for @bsv/sdk
|
|
@@ -119,10 +119,10 @@ export async function verifyRelaySignature(fromKey, to, type, payload, signature
|
|
|
119
119
|
*
|
|
120
120
|
* Use deriveWalletKeys() to get both the address and signing key.
|
|
121
121
|
*/
|
|
122
|
-
export async function deriveWalletAddress(privKey) {
|
|
122
|
+
export async function deriveWalletAddress(privKey, network = NETWORK) {
|
|
123
123
|
const keyDeriver = new CachedKeyDeriver(privKey);
|
|
124
124
|
const pubKey = keyDeriver.derivePublicKey(brc29ProtocolID, Utils.toBase64(Utils.toArray('import')) + ' ' + Utils.toBase64(Utils.toArray('now')), 'self', true);
|
|
125
|
-
const address = pubKey.toAddress();
|
|
125
|
+
const address = pubKey.toAddress(network);
|
|
126
126
|
const hash160 = Buffer.from(pubKey.toHash());
|
|
127
127
|
return { address, hash160, pubKey };
|
|
128
128
|
}
|
|
@@ -134,9 +134,10 @@ export async function deriveWalletAddress(privKey) {
|
|
|
134
134
|
* root private key - it will cause signature verification failures!
|
|
135
135
|
*
|
|
136
136
|
* @param rootPrivKey - Root private key from wallet identity
|
|
137
|
+
* @param network - Optional network override
|
|
137
138
|
* @returns Object with address, hash160, and CHILD private key for signing
|
|
138
139
|
*/
|
|
139
|
-
export async function deriveWalletKeys(rootPrivKey) {
|
|
140
|
+
export async function deriveWalletKeys(rootPrivKey, network = NETWORK) {
|
|
140
141
|
const keyDeriver = new CachedKeyDeriver(rootPrivKey);
|
|
141
142
|
const derivationPrefix = Utils.toBase64(Utils.toArray('import'));
|
|
142
143
|
const derivationSuffix = Utils.toBase64(Utils.toArray('now'));
|
|
@@ -145,7 +146,7 @@ export async function deriveWalletKeys(rootPrivKey) {
|
|
|
145
146
|
const childPrivKey = keyDeriver.derivePrivateKey(brc29ProtocolID, keyString, 'self');
|
|
146
147
|
// Derive child public key (for address)
|
|
147
148
|
const pubKey = keyDeriver.derivePublicKey(brc29ProtocolID, keyString, 'self', true);
|
|
148
|
-
const address = pubKey.toAddress();
|
|
149
|
+
const address = pubKey.toAddress(network);
|
|
149
150
|
const hash160 = Buffer.from(pubKey.toHash());
|
|
150
151
|
return { address, hash160, pubKey, childPrivKey };
|
|
151
152
|
}
|
|
@@ -9,6 +9,10 @@ export declare function cmdSetup(): Promise<never>;
|
|
|
9
9
|
* Identity command: show identity public key.
|
|
10
10
|
*/
|
|
11
11
|
export declare function cmdIdentity(): Promise<never>;
|
|
12
|
+
/**
|
|
13
|
+
* Status command: show identity and balance.
|
|
14
|
+
*/
|
|
15
|
+
export declare function cmdStatus(): Promise<never>;
|
|
12
16
|
/**
|
|
13
17
|
* Address command: show P2PKH receive address.
|
|
14
18
|
*/
|
|
@@ -83,6 +83,20 @@ export async function cmdIdentity() {
|
|
|
83
83
|
await wallet.destroy();
|
|
84
84
|
return ok({ identityKey });
|
|
85
85
|
}
|
|
86
|
+
/**
|
|
87
|
+
* Status command: show identity and balance.
|
|
88
|
+
*/
|
|
89
|
+
export async function cmdStatus() {
|
|
90
|
+
const BSVAgentWallet = await getBSVAgentWallet();
|
|
91
|
+
const wallet = await BSVAgentWallet.load({ network: NETWORK, storageDir: WALLET_DIR });
|
|
92
|
+
const identityKey = await wallet.getIdentityKey();
|
|
93
|
+
const total = await wallet.getBalance();
|
|
94
|
+
await wallet.destroy();
|
|
95
|
+
return ok({
|
|
96
|
+
identity: { identityKey, network: NETWORK },
|
|
97
|
+
balance: { walletBalance: total }
|
|
98
|
+
});
|
|
99
|
+
}
|
|
86
100
|
/**
|
|
87
101
|
* Address command: show P2PKH receive address.
|
|
88
102
|
*/
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for network-specific address generation.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that address generation correctly uses the specified
|
|
5
|
+
* network prefix (mainnet vs testnet).
|
|
6
|
+
*
|
|
7
|
+
* Run: npx tsx src/test/network-address.test.ts
|
|
8
|
+
*/
|
|
9
|
+
import { PrivateKey } from '@bsv/sdk';
|
|
10
|
+
import { deriveWalletAddress } from '../scripts/wallet/identity.js';
|
|
11
|
+
async function assert(condition, message) {
|
|
12
|
+
if (!condition) {
|
|
13
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
async function runTests() {
|
|
17
|
+
console.log('🧪 Running Network Address Generation Tests...\n');
|
|
18
|
+
const privKey = PrivateKey.fromRandom();
|
|
19
|
+
// Test 1: Mainnet Address Generation
|
|
20
|
+
console.log('✓ Test 1: Mainnet address starts with 1');
|
|
21
|
+
const mainnet = await deriveWalletAddress(privKey, 'mainnet');
|
|
22
|
+
console.log(` Mainnet: ${mainnet.address}`);
|
|
23
|
+
await assert(mainnet.address.startsWith('1'), 'Mainnet address should start with 1');
|
|
24
|
+
// Test 2: Testnet Address Generation
|
|
25
|
+
console.log('✓ Test 2: Testnet address starts with m or n');
|
|
26
|
+
const testnet = await deriveWalletAddress(privKey, 'testnet');
|
|
27
|
+
console.log(` Testnet: ${testnet.address}`);
|
|
28
|
+
await assert(testnet.address.startsWith('m') || testnet.address.startsWith('n'), 'Testnet address should start with m or n');
|
|
29
|
+
// Test 3: Addresses are different
|
|
30
|
+
console.log('✓ Test 3: Mainnet and testnet addresses for same key are different');
|
|
31
|
+
await assert(mainnet.address !== testnet.address, 'Addresses should be different across networks');
|
|
32
|
+
console.log('\n✅ All network address tests passed!\n');
|
|
33
|
+
}
|
|
34
|
+
runTests().catch((err) => {
|
|
35
|
+
console.error('\n❌ Tests failed:', err.message);
|
|
36
|
+
process.exit(1);
|
|
37
|
+
});
|
package/index.ts
CHANGED
|
@@ -314,7 +314,7 @@ function getCliPath() {
|
|
|
314
314
|
* Decentralized agent marketplace with BSV micropayments.
|
|
315
315
|
*/
|
|
316
316
|
export const plugin = {
|
|
317
|
-
id: "
|
|
317
|
+
id: "overlay",
|
|
318
318
|
name: "BSV Overlay Network",
|
|
319
319
|
description: "OpenClaw Overlay — decentralized agent marketplace with BSV micropayments",
|
|
320
320
|
|
|
@@ -327,7 +327,7 @@ export const plugin = {
|
|
|
327
327
|
isInitialized = true;
|
|
328
328
|
|
|
329
329
|
const entries = api.getConfig?.()?.plugins?.entries || {};
|
|
330
|
-
const entry = entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] || {};
|
|
330
|
+
const entry = entries['overlay'] || entries['openclaw-overlay-plugin'] || entries['openclaw-overlay'] || {};
|
|
331
331
|
const pluginConfig = { ...entry, ...(entry.config || {}), ...(api.config || {}) };
|
|
332
332
|
|
|
333
333
|
// 1. Tool
|
|
@@ -337,8 +337,10 @@ export const plugin = {
|
|
|
337
337
|
parameters: {
|
|
338
338
|
type: "object",
|
|
339
339
|
properties: {
|
|
340
|
-
action: { type: "string", enum: ["request", "discover", "balance", "status", "pay", "onboard", "pending-requests", "fulfill", "unregister"] },
|
|
340
|
+
action: { type: "string", enum: ["request", "discover", "balance", "status", "pay", "onboard", "pending-requests", "fulfill", "unregister", "advertise-ship", "advertise-slap"] },
|
|
341
341
|
service: { type: "string" },
|
|
342
|
+
topic: { type: "string" },
|
|
343
|
+
domain: { type: "string" },
|
|
342
344
|
input: { type: "object" },
|
|
343
345
|
identityKey: { type: "string" },
|
|
344
346
|
sats: { type: "number" },
|
|
@@ -369,11 +371,31 @@ export const plugin = {
|
|
|
369
371
|
const action = ctx.args?.[0] || 'status';
|
|
370
372
|
|
|
371
373
|
if (action === 'help') {
|
|
372
|
-
return { text: `🛰️ **Overlay Help**\n\n**Subcommands**:\n- \`status\`: Show identity and wallet balance\n- \`balance\`: Show current satoshis\n- \`onboard\`: Start discovery setup\n- \`discover <serviceId>\`: Find providers on network\n- \`pending-requests\`: See incoming service jobs\n- \`fulfill\`: Complete a request (Agent only)` };
|
|
374
|
+
return { text: `🛰️ **Overlay Help**\n\n**Subcommands**:\n- \`status\`: Show identity and wallet balance\n- \`balance\`: Show current satoshis\n- \`onboard\`: Start discovery setup\n- \`discover <serviceId>\`: Find providers on network\n- \`advertise-ship <domain> <topic>\`: Advertise a topic manager\n- \`advertise-slap <domain> <service>\`: Advertise a lookup service\n- \`pending-requests\`: See incoming service jobs\n- \`fulfill\`: Complete a request (Agent only)` };
|
|
373
375
|
}
|
|
374
376
|
|
|
375
|
-
const
|
|
376
|
-
|
|
377
|
+
const params: any = { action };
|
|
378
|
+
if (action === 'discover') {
|
|
379
|
+
params.service = ctx.args[1];
|
|
380
|
+
} else if (action === 'advertise-ship') {
|
|
381
|
+
params.domain = ctx.args[1];
|
|
382
|
+
params.topic = ctx.args[2];
|
|
383
|
+
} else if (action === 'advertise-slap') {
|
|
384
|
+
params.domain = ctx.args[1];
|
|
385
|
+
params.service = ctx.args[2];
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const result = await executeOverlayAction(params, pluginConfig, api);
|
|
389
|
+
|
|
390
|
+
if (typeof result === 'string') return { text: result };
|
|
391
|
+
|
|
392
|
+
// Formatted status response
|
|
393
|
+
if (action === 'status') {
|
|
394
|
+
const status = result as any;
|
|
395
|
+
return { text: `🛰️ **Overlay Status**\n\n**Identity Key**: \`${status.identity?.identityKey || 'Not setup'}\`\n**Balance**: ${status.balance?.walletBalance || 0} satoshis\n**Network**: ${status.identity?.network || 'mainnet'}` };
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return { text: `**Overlay ${action.toUpperCase()}**\n\n${JSON.stringify(result, null, 2)}` };
|
|
377
399
|
} catch (error: any) {
|
|
378
400
|
return { text: `❌ Error: ${error.message}` };
|
|
379
401
|
}
|
|
@@ -425,10 +447,24 @@ async function executeOverlayAction(params: any, config: any, api: any) {
|
|
|
425
447
|
case "onboard": return await handleOnboard(params, env, cliPath);
|
|
426
448
|
case "pending-requests": return await handlePendingRequests(env, cliPath);
|
|
427
449
|
case "fulfill": return await handleFulfill(params, env, cliPath);
|
|
450
|
+
case "advertise-ship": return await handleAdvertiseSHIP(params, env, cliPath);
|
|
451
|
+
case "advertise-slap": return await handleAdvertiseSLAP(params, env, cliPath);
|
|
428
452
|
default: throw new Error(`Unknown action: ${action}`);
|
|
429
453
|
}
|
|
430
454
|
}
|
|
431
455
|
|
|
456
|
+
async function handleAdvertiseSHIP(params: any, env: any, cliPath: string) {
|
|
457
|
+
const { domain, topic } = params;
|
|
458
|
+
const result = await execFileAsync('node', [cliPath, 'advertise-ship', domain, topic], { env });
|
|
459
|
+
return parseCliOutput(result.stdout).data;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async function handleAdvertiseSLAP(params: any, env: any, cliPath: string) {
|
|
463
|
+
const { domain, service } = params;
|
|
464
|
+
const result = await execFileAsync('node', [cliPath, 'advertise-slap', domain, service], { env });
|
|
465
|
+
return parseCliOutput(result.stdout).data;
|
|
466
|
+
}
|
|
467
|
+
|
|
432
468
|
async function handleServiceRequest(params: any, env: any, cliPath: string, config: any, api: any) {
|
|
433
469
|
const { service, identityKey: targetKey, input } = params;
|
|
434
470
|
const walletDir = config?.walletDir || path.join(os.homedir(), '.openclaw', 'bsv-wallet');
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "overlay",
|
|
3
3
|
"name": "BSV Overlay Network",
|
|
4
4
|
"description": "OpenClaw Overlay — decentralized agent marketplace with BSV micropayments",
|
|
5
|
-
"version": "0.7.
|
|
5
|
+
"version": "0.7.70",
|
|
6
6
|
"skills": [
|
|
7
7
|
"./SKILL.md"
|
|
8
8
|
],
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-overlay-plugin",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.71",
|
|
4
4
|
"description": "Openclaw BSV Overlay — agent discovery, service marketplace, and micropayments on the BSV blockchain",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -26,9 +26,9 @@
|
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
28
|
"@bsv/sdk": "^2.0.13",
|
|
29
|
-
"@bsv/wallet-toolbox": "^2.1.
|
|
29
|
+
"@bsv/wallet-toolbox": "^2.1.18",
|
|
30
30
|
"dotenv": "^17.3.1",
|
|
31
|
-
"knex": "^3.2.
|
|
31
|
+
"knex": "^3.2.9",
|
|
32
32
|
"sqlite3": "^5.1.7"
|
|
33
33
|
},
|
|
34
34
|
"devDependencies": {
|
package/src/cli-main.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { ok, fail } from './scripts/output.js';
|
|
9
9
|
|
|
10
10
|
// Wallet commands
|
|
11
|
-
import { cmdSetup, cmdIdentity, cmdAddress } from './scripts/wallet/setup.js';
|
|
11
|
+
import { cmdSetup, cmdIdentity, cmdAddress, cmdStatus } from './scripts/wallet/setup.js';
|
|
12
12
|
import { cmdBalance, cmdImport, cmdRefund } from './scripts/wallet/balance.js';
|
|
13
13
|
|
|
14
14
|
// Overlay registration commands
|
|
@@ -19,6 +19,7 @@ import { cmdServices, cmdAdvertise, cmdRemove, cmdReadvertise } from './scripts/
|
|
|
19
19
|
|
|
20
20
|
// Discovery commands
|
|
21
21
|
import { cmdDiscover } from './scripts/overlay/discover.js';
|
|
22
|
+
import { cmdAdvertiseSHIP, cmdAdvertiseSLAP } from './scripts/overlay/advertisement.js';
|
|
22
23
|
|
|
23
24
|
// Payment commands
|
|
24
25
|
import { cmdPay, cmdVerify, cmdAccept } from './scripts/payment/commands.js';
|
|
@@ -69,7 +70,7 @@ async function main() {
|
|
|
69
70
|
wallet: ['setup', 'identity', 'address', 'balance', 'import <txid> [vout]', 'refund <address>'],
|
|
70
71
|
registration: ['register', 'unregister'],
|
|
71
72
|
services: ['services', 'advertise <id> <name> <priceSats> [desc]', 'readvertise <id> [name] [priceSats] [desc]', 'remove <id>'],
|
|
72
|
-
discovery: ['discover [--service <type>] [--agent <name>]'],
|
|
73
|
+
discovery: ['discover [--service <type>] [--agent <name>]', 'advertise-ship <domain> <topic>', 'advertise-slap <domain> <service>'],
|
|
73
74
|
payments: ['pay <pubkey> <sats> [desc]', 'verify <beef>', 'accept <beef> <prefix> <suffix> <senderKey> [desc]'],
|
|
74
75
|
messaging: ['send <key> <type> <json>', 'inbox', 'ack', 'poll', 'connect'],
|
|
75
76
|
'service-requests': ['request-service <key> <serviceId> <sats> [input]', 'service-queue', 'respond-service <reqId> <key> <serviceId> <result>'],
|
|
@@ -81,6 +82,9 @@ async function main() {
|
|
|
81
82
|
break;
|
|
82
83
|
|
|
83
84
|
// Wallet
|
|
85
|
+
case 'status':
|
|
86
|
+
await cmdStatus();
|
|
87
|
+
break;
|
|
84
88
|
case 'setup':
|
|
85
89
|
await cmdSetup();
|
|
86
90
|
break;
|
|
@@ -126,6 +130,12 @@ async function main() {
|
|
|
126
130
|
case 'discover':
|
|
127
131
|
await cmdDiscover(args);
|
|
128
132
|
break;
|
|
133
|
+
case 'advertise-ship':
|
|
134
|
+
await cmdAdvertiseSHIP(args[0], args[1]);
|
|
135
|
+
break;
|
|
136
|
+
case 'advertise-slap':
|
|
137
|
+
await cmdAdvertiseSLAP(args[0], args[1]);
|
|
138
|
+
break;
|
|
129
139
|
|
|
130
140
|
// Payments
|
|
131
141
|
case 'pay':
|
|
@@ -5,7 +5,8 @@ import fs from 'node:fs';
|
|
|
5
5
|
import process from 'node:process';
|
|
6
6
|
import { Buffer } from 'node:buffer';
|
|
7
7
|
import { ok, fail } from '../output.js';
|
|
8
|
-
import { loadIdentity } from '../wallet/identity.js';
|
|
8
|
+
import { loadIdentity, deriveWalletAddress } from '../wallet/identity.js';
|
|
9
|
+
import { NETWORK } from '../config.js';
|
|
9
10
|
|
|
10
11
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
12
|
const __dirname = path.dirname(__filename);
|
|
@@ -186,7 +187,7 @@ export async function cmdBaemailRefund(requestId: string | undefined): Promise<n
|
|
|
186
187
|
}
|
|
187
188
|
|
|
188
189
|
// Load wallet and SDK
|
|
189
|
-
const { identityKey, privKey } = await loadIdentity();
|
|
190
|
+
const { identityKey, privKey: rootKey } = await loadIdentity();
|
|
190
191
|
const walletIdentityRaw = fs.readFileSync(PATHS.walletIdentity, 'utf-8');
|
|
191
192
|
const walletIdentity = JSON.parse(walletIdentityRaw);
|
|
192
193
|
|
|
@@ -208,23 +209,23 @@ export async function cmdBaemailRefund(requestId: string | undefined): Promise<n
|
|
|
208
209
|
|
|
209
210
|
// Derive refund address from sender's identity key
|
|
210
211
|
const senderPubKey = PublicKey.fromString(entry.from);
|
|
211
|
-
const refundAddress = senderPubKey.toAddress().toString();
|
|
212
|
+
const refundAddress = senderPubKey.toAddress(NETWORK).toString();
|
|
212
213
|
|
|
213
214
|
try {
|
|
214
|
-
// Load UTXOs
|
|
215
|
-
const address =
|
|
216
|
-
const
|
|
215
|
+
// Load UTXOs - Derive local address correctly
|
|
216
|
+
const { address } = await deriveWalletAddress(rootKey);
|
|
217
|
+
const wocNet = NETWORK === 'mainnet' ? 'main' : 'test';
|
|
218
|
+
const utxosResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/${wocNet}/address/${address}/unspent/all`);
|
|
217
219
|
const data = await utxosResp.json();
|
|
218
220
|
const utxos = data.result || [];
|
|
219
221
|
|
|
220
222
|
if (!utxos || utxos.length === 0) {
|
|
221
|
-
return fail(
|
|
223
|
+
return fail(`No UTXOs available for refund at ${address}`);
|
|
222
224
|
}
|
|
223
225
|
|
|
224
226
|
// Build transaction
|
|
225
227
|
const tx = new Transaction();
|
|
226
228
|
let totalInput = 0;
|
|
227
|
-
const rootKey = PrivateKey.fromHex(walletIdentity.rootKeyHex);
|
|
228
229
|
|
|
229
230
|
for (const utxo of utxos) {
|
|
230
231
|
if (totalInput >= refundSats + 50) break;
|
|
@@ -232,7 +233,7 @@ export async function cmdBaemailRefund(requestId: string | undefined): Promise<n
|
|
|
232
233
|
sourceTXID: utxo.tx_hash,
|
|
233
234
|
sourceOutputIndex: utxo.tx_pos,
|
|
234
235
|
sourceSatoshis: utxo.value,
|
|
235
|
-
script: new P2PKH().lock(rootKey.toPublicKey().toAddress()).toHex(),
|
|
236
|
+
script: new P2PKH().lock(rootKey.toPublicKey().toAddress(NETWORK)).toHex(),
|
|
236
237
|
unlockingScriptTemplate: new P2PKH().unlock(rootKey),
|
|
237
238
|
});
|
|
238
239
|
totalInput += utxo.value;
|
|
@@ -254,7 +255,7 @@ export async function cmdBaemailRefund(requestId: string | undefined): Promise<n
|
|
|
254
255
|
if (change > 1) {
|
|
255
256
|
tx.addOutput({
|
|
256
257
|
satoshis: change,
|
|
257
|
-
lockingScript: new P2PKH().lock(rootKey.toPublicKey().toAddress()),
|
|
258
|
+
lockingScript: new P2PKH().lock(rootKey.toPublicKey().toAddress(NETWORK)),
|
|
258
259
|
});
|
|
259
260
|
}
|
|
260
261
|
|
|
@@ -271,7 +272,7 @@ export async function cmdBaemailRefund(requestId: string | undefined): Promise<n
|
|
|
271
272
|
body: JSON.stringify({ rawTx: tx.toHex() }),
|
|
272
273
|
});
|
|
273
274
|
} else {
|
|
274
|
-
broadcastResp = await fetchWithTimeout(
|
|
275
|
+
broadcastResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/${wocNet}/tx/raw`, {
|
|
275
276
|
method: 'POST',
|
|
276
277
|
headers: { 'Content-Type': 'application/json' },
|
|
277
278
|
body: JSON.stringify({ txhex: tx.toHex() }),
|
package/src/scripts/config.ts
CHANGED
|
@@ -53,8 +53,16 @@ export const TOPICS = {
|
|
|
53
53
|
IDENTITY: 'tm_openclaw_identity',
|
|
54
54
|
SERVICES: 'tm_openclaw_services',
|
|
55
55
|
X_VERIFICATION: 'tm_openclaw_x_verification',
|
|
56
|
+
SHIP: 'tm_ship',
|
|
57
|
+
SLAP: 'tm_slap',
|
|
56
58
|
} as const;
|
|
57
59
|
|
|
60
|
+
/** Default SLAP trackers */
|
|
61
|
+
export const DEFAULT_SLAP_TRACKERS: Record<'mainnet' | 'testnet', string[]> = {
|
|
62
|
+
mainnet: ['https://overlay.babbage.systems'],
|
|
63
|
+
testnet: ['https://testnet-users.bapp.dev'],
|
|
64
|
+
};
|
|
65
|
+
|
|
58
66
|
/** Lookup services for overlay queries */
|
|
59
67
|
export const LOOKUP_SERVICES = {
|
|
60
68
|
AGENTS: 'ls_openclaw_agents',
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SHIP and SLAP advertisement commands.
|
|
3
|
+
*
|
|
4
|
+
* SHIP: Service Health & Information Protocol (tm_ship)
|
|
5
|
+
* SLAP: Service Level Agreement Protocol (tm_slap)
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { PushDrop, Utils } from '@bsv/sdk';
|
|
9
|
+
import { NETWORK, WALLET_DIR, TOPICS, DEFAULT_SLAP_TRACKERS } from '../config.js';
|
|
10
|
+
import { BSVAgentWallet } from '../../core/wallet.js';
|
|
11
|
+
import { ok, fail } from '../output.js';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Advertise a SHIP record.
|
|
15
|
+
* Announce that you host a specific Topic Manager (tm_).
|
|
16
|
+
*/
|
|
17
|
+
export async function cmdAdvertiseSHIP(domain?: string, topic?: string): Promise<never> {
|
|
18
|
+
if (!domain || !topic) {
|
|
19
|
+
return fail('Usage: advertise-ship <domain> <topic>');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (!topic.startsWith('tm_')) {
|
|
23
|
+
return fail('Topic must start with "tm_"');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
try {
|
|
27
|
+
const wallet = await BSVAgentWallet.load({ network: NETWORK, storageDir: WALLET_DIR });
|
|
28
|
+
const token = new PushDrop(wallet._setup.wallet);
|
|
29
|
+
|
|
30
|
+
// SHIP format: Payload is the domain hosting the topic
|
|
31
|
+
const fields = [Utils.toArray(domain, 'utf8')];
|
|
32
|
+
// Context is [0, topic] to identify the topic manager being advertised
|
|
33
|
+
const lockingScript = (await token.lock(fields, [0, topic], '1', 'self', true, true)).toHex();
|
|
34
|
+
|
|
35
|
+
const response = await wallet._setup.wallet.createAction({
|
|
36
|
+
description: `advertise SHIP for ${topic}`,
|
|
37
|
+
outputs: [{
|
|
38
|
+
lockingScript,
|
|
39
|
+
satoshis: 1,
|
|
40
|
+
outputDescription: 'SHIP advertisement',
|
|
41
|
+
basket: TOPICS.SHIP
|
|
42
|
+
}]
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Broadcast to primary overlay and SLAP trackers
|
|
46
|
+
const trackers = [
|
|
47
|
+
...DEFAULT_SLAP_TRACKERS[NETWORK]
|
|
48
|
+
];
|
|
49
|
+
|
|
50
|
+
const results = await broadcastToTrackers(response.tx as number[], [TOPICS.SHIP, topic], trackers);
|
|
51
|
+
|
|
52
|
+
return ok({
|
|
53
|
+
advertised: 'SHIP',
|
|
54
|
+
topic,
|
|
55
|
+
domain,
|
|
56
|
+
txid: response.txid,
|
|
57
|
+
broadcasts: results
|
|
58
|
+
});
|
|
59
|
+
} catch (err: any) {
|
|
60
|
+
return fail(`SHIP advertisement failed: ${err.message}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Advertise a SLAP record.
|
|
66
|
+
* Announce that you host a specific Lookup Service (ls_).
|
|
67
|
+
*/
|
|
68
|
+
export async function cmdAdvertiseSLAP(domain?: string, service?: string): Promise<never> {
|
|
69
|
+
if (!domain || !service) {
|
|
70
|
+
return fail('Usage: advertise-slap <domain> <service>');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (!service.startsWith('ls_')) {
|
|
74
|
+
return fail('Service must start with "ls_"');
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
const wallet = await BSVAgentWallet.load({ network: NETWORK, storageDir: WALLET_DIR });
|
|
79
|
+
const token = new PushDrop(wallet._setup.wallet);
|
|
80
|
+
|
|
81
|
+
// SLAP format: Payload is the domain hosting the lookup service
|
|
82
|
+
const fields = [Utils.toArray(domain, 'utf8')];
|
|
83
|
+
// Context is [0, service] to identify the lookup service being advertised
|
|
84
|
+
const lockingScript = (await token.lock(fields, [0, service], '1', 'self', true, true)).toHex();
|
|
85
|
+
|
|
86
|
+
const response = await wallet._setup.wallet.createAction({
|
|
87
|
+
description: `advertise SLAP for ${service}`,
|
|
88
|
+
outputs: [{
|
|
89
|
+
lockingScript,
|
|
90
|
+
satoshis: 1,
|
|
91
|
+
outputDescription: 'SLAP advertisement',
|
|
92
|
+
basket: TOPICS.SLAP
|
|
93
|
+
}]
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Broadcast to primary overlay and SLAP trackers
|
|
97
|
+
const trackers = [
|
|
98
|
+
...DEFAULT_SLAP_TRACKERS[NETWORK]
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
const results = await broadcastToTrackers(response.tx as number[], [TOPICS.SLAP, service], trackers);
|
|
102
|
+
|
|
103
|
+
return ok({
|
|
104
|
+
advertised: 'SLAP',
|
|
105
|
+
service,
|
|
106
|
+
domain,
|
|
107
|
+
txid: response.txid,
|
|
108
|
+
broadcasts: results
|
|
109
|
+
});
|
|
110
|
+
} catch (err: any) {
|
|
111
|
+
return fail(`SLAP advertisement failed: ${err.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Helper to broadcast BEEF to multiple trackers/overlays.
|
|
117
|
+
*/
|
|
118
|
+
async function broadcastToTrackers(tx: number[], topics: string[], trackers: string[]) {
|
|
119
|
+
const results: Record<string, any> = {};
|
|
120
|
+
const body = new Uint8Array(tx);
|
|
121
|
+
|
|
122
|
+
for (const url of trackers) {
|
|
123
|
+
try {
|
|
124
|
+
const resp = await fetch(`${url.replace(/\/$/, '')}/submit`, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
headers: {
|
|
127
|
+
'Content-Type': 'application/octet-stream',
|
|
128
|
+
'X-Topics': JSON.stringify(topics)
|
|
129
|
+
},
|
|
130
|
+
body
|
|
131
|
+
});
|
|
132
|
+
results[url] = resp.ok ? 'success' : `error: ${resp.status}`;
|
|
133
|
+
} catch (err: any) {
|
|
134
|
+
results[url] = `failed: ${err.message}`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return results;
|
|
138
|
+
}
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import fs from 'node:fs';
|
|
6
|
-
import { PATHS } from '../config.js';
|
|
6
|
+
import { PATHS, NETWORK } from '../config.js';
|
|
7
7
|
import type { WalletIdentity } from '../types.js';
|
|
8
8
|
import { CachedKeyDeriver, Utils } from '@bsv/sdk';
|
|
9
9
|
import { brc29ProtocolID } from '@bsv/wallet-toolbox';
|
|
@@ -139,7 +139,7 @@ export async function verifyRelaySignature(
|
|
|
139
139
|
*
|
|
140
140
|
* Use deriveWalletKeys() to get both the address and signing key.
|
|
141
141
|
*/
|
|
142
|
-
export async function deriveWalletAddress(privKey: any): Promise<{
|
|
142
|
+
export async function deriveWalletAddress(privKey: any, network: string = NETWORK): Promise<{
|
|
143
143
|
address: string;
|
|
144
144
|
hash160: Uint8Array;
|
|
145
145
|
pubKey: any;
|
|
@@ -153,7 +153,7 @@ export async function deriveWalletAddress(privKey: any): Promise<{
|
|
|
153
153
|
true
|
|
154
154
|
);
|
|
155
155
|
|
|
156
|
-
const address = pubKey.toAddress();
|
|
156
|
+
const address = pubKey.toAddress(network);
|
|
157
157
|
const hash160 = Buffer.from(pubKey.toHash());
|
|
158
158
|
|
|
159
159
|
return { address, hash160, pubKey };
|
|
@@ -167,9 +167,10 @@ export async function deriveWalletAddress(privKey: any): Promise<{
|
|
|
167
167
|
* root private key - it will cause signature verification failures!
|
|
168
168
|
*
|
|
169
169
|
* @param rootPrivKey - Root private key from wallet identity
|
|
170
|
+
* @param network - Optional network override
|
|
170
171
|
* @returns Object with address, hash160, and CHILD private key for signing
|
|
171
172
|
*/
|
|
172
|
-
export async function deriveWalletKeys(rootPrivKey: any): Promise<{
|
|
173
|
+
export async function deriveWalletKeys(rootPrivKey: any, network: string = NETWORK): Promise<{
|
|
173
174
|
address: string;
|
|
174
175
|
hash160: Uint8Array;
|
|
175
176
|
pubKey: any;
|
|
@@ -196,7 +197,7 @@ export async function deriveWalletKeys(rootPrivKey: any): Promise<{
|
|
|
196
197
|
true
|
|
197
198
|
);
|
|
198
199
|
|
|
199
|
-
const address = pubKey.toAddress();
|
|
200
|
+
const address = pubKey.toAddress(network);
|
|
200
201
|
const hash160 = Buffer.from(pubKey.toHash());
|
|
201
202
|
|
|
202
203
|
return { address, hash160, pubKey, childPrivKey };
|
|
@@ -97,6 +97,22 @@ export async function cmdIdentity(): Promise<never> {
|
|
|
97
97
|
return ok({ identityKey });
|
|
98
98
|
}
|
|
99
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Status command: show identity and balance.
|
|
102
|
+
*/
|
|
103
|
+
export async function cmdStatus(): Promise<never> {
|
|
104
|
+
const BSVAgentWallet = await getBSVAgentWallet();
|
|
105
|
+
const wallet = await BSVAgentWallet.load({ network: NETWORK, storageDir: WALLET_DIR });
|
|
106
|
+
const identityKey = await wallet.getIdentityKey();
|
|
107
|
+
const total = await wallet.getBalance();
|
|
108
|
+
await wallet.destroy();
|
|
109
|
+
|
|
110
|
+
return ok({
|
|
111
|
+
identity: { identityKey, network: NETWORK },
|
|
112
|
+
balance: { walletBalance: total }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
100
116
|
/**
|
|
101
117
|
* Address command: show P2PKH receive address.
|
|
102
118
|
*/
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for network-specific address generation.
|
|
3
|
+
*
|
|
4
|
+
* These tests verify that address generation correctly uses the specified
|
|
5
|
+
* network prefix (mainnet vs testnet).
|
|
6
|
+
*
|
|
7
|
+
* Run: npx tsx src/test/network-address.test.ts
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { PrivateKey } from '@bsv/sdk';
|
|
11
|
+
import { deriveWalletAddress } from '../scripts/wallet/identity.js';
|
|
12
|
+
|
|
13
|
+
async function assert(condition: boolean, message: string) {
|
|
14
|
+
if (!condition) {
|
|
15
|
+
throw new Error(`Assertion failed: ${message}`);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function runTests() {
|
|
20
|
+
console.log('🧪 Running Network Address Generation Tests...\n');
|
|
21
|
+
|
|
22
|
+
const privKey = PrivateKey.fromRandom();
|
|
23
|
+
|
|
24
|
+
// Test 1: Mainnet Address Generation
|
|
25
|
+
console.log('✓ Test 1: Mainnet address starts with 1');
|
|
26
|
+
const mainnet = await deriveWalletAddress(privKey, 'mainnet');
|
|
27
|
+
console.log(` Mainnet: ${mainnet.address}`);
|
|
28
|
+
await assert(mainnet.address.startsWith('1'), 'Mainnet address should start with 1');
|
|
29
|
+
|
|
30
|
+
// Test 2: Testnet Address Generation
|
|
31
|
+
console.log('✓ Test 2: Testnet address starts with m or n');
|
|
32
|
+
const testnet = await deriveWalletAddress(privKey, 'testnet');
|
|
33
|
+
console.log(` Testnet: ${testnet.address}`);
|
|
34
|
+
await assert(testnet.address.startsWith('m') || testnet.address.startsWith('n'), 'Testnet address should start with m or n');
|
|
35
|
+
|
|
36
|
+
// Test 3: Addresses are different
|
|
37
|
+
console.log('✓ Test 3: Mainnet and testnet addresses for same key are different');
|
|
38
|
+
await assert(mainnet.address !== testnet.address, 'Addresses should be different across networks');
|
|
39
|
+
|
|
40
|
+
console.log('\n✅ All network address tests passed!\n');
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
runTests().catch((err) => {
|
|
44
|
+
console.error('\n❌ Tests failed:', err.message);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
});
|