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 CHANGED
@@ -1,10 +1,10 @@
1
1
  ---
2
- name: openclaw-overlay
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
- | `setup` | Initialize wallet |
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
- ## Onboarding
35
+ ## Network Support
38
36
 
39
- On first run, the plugin auto-creates a wallet and wakes you. Guide the user through setup as a conversation:
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
- 1. **Ask for agent name**: "What name do you want for your agent on the network?"
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
- If you need to customize settings, tell the user to add them to their config under `plugins.entries["openclaw-overlay-plugin"]`:
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
- Do NOT use defaults without asking. Do NOT skip the name/description questions.
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
- ## Requesting Services
52
+ ## Discovery & Services
65
53
 
66
- Use `openclaw_overlay({ action: "request", service: "<id>", input: {...} })` to auto-discover the cheapest provider, pay, and send the request. The response arrives asynchronously via the background WebSocket service — you'll be woken when it comes back.
67
-
68
- Set `maxPrice` to cap spending. Requests within `maxAutoPaySats` (default 200) auto-pay.
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
- The background service queues incoming requests and wakes you automatically.
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
- - **Auto-pay**: Requests under `maxAutoPaySats` (default 200 sats) pay automatically
83
- - **Budget**: Daily spending capped at `dailyBudgetSats` (default 1,000 sats/day)
84
- - **Over limit**: Returns an error get user confirmation before retrying with `maxPrice`
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
- ## References
68
+ ## Spending & Security
88
69
 
89
- - [Service catalog and input schemas](references/services.md)
90
- - [Configuration, environment variables, and CLI commands](references/configuration.md)
91
- - [Wallet operations, funding, budget, and import details](references/wallet-operations.md)
92
- - [Overlay protocol specification](references/protocol.md)
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: "openclaw-overlay-plugin",
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 result = await executeOverlayAction({ action }, pluginConfig, api);
359
- return { text: `**Overlay ${action.toUpperCase()}**\n\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}` };
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');
@@ -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 = walletIdentity.address;
198
- const utxosResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent/all`);
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('No UTXOs available for refund');
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('https://api.whatsonchain.com/v1/bsv/main/tx/raw', {
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,9 @@
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
+ export {};
@@ -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: "openclaw-overlay-plugin",
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 result = await executeOverlayAction({ action }, pluginConfig, api);
376
- return { text: `**Overlay ${action.toUpperCase()}**\n\n${typeof result === 'string' ? result : JSON.stringify(result, null, 2)}` };
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');
@@ -1,8 +1,8 @@
1
1
  {
2
- "id": "openclaw-overlay-plugin",
2
+ "id": "overlay",
3
3
  "name": "BSV Overlay Network",
4
4
  "description": "OpenClaw Overlay — decentralized agent marketplace with BSV micropayments",
5
- "version": "0.7.67",
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.68",
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.17",
29
+ "@bsv/wallet-toolbox": "^2.1.18",
30
30
  "dotenv": "^17.3.1",
31
- "knex": "^3.2.8",
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 = walletIdentity.address;
216
- const utxosResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent/all`);
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('No UTXOs available for refund');
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('https://api.whatsonchain.com/v1/bsv/main/tx/raw', {
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() }),
@@ -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
+ });