openclaw-overlay-plugin 0.7.22
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +406 -0
- package/SKILL.md +78 -0
- package/clawdbot.plugin.json +106 -0
- package/dist/cli-main.d.ts +7 -0
- package/dist/cli-main.js +192 -0
- package/dist/cli.d.ts +8 -0
- package/dist/cli.js +14 -0
- package/dist/core/config.d.ts +11 -0
- package/dist/core/config.js +13 -0
- package/dist/core/index.d.ts +25 -0
- package/dist/core/index.js +26 -0
- package/dist/core/payment.d.ts +16 -0
- package/dist/core/payment.js +94 -0
- package/dist/core/types.d.ts +94 -0
- package/dist/core/types.js +4 -0
- package/dist/core/verify.d.ts +28 -0
- package/dist/core/verify.js +104 -0
- package/dist/core/wallet.d.ts +99 -0
- package/dist/core/wallet.js +219 -0
- package/dist/scripts/baemail/commands.d.ts +64 -0
- package/dist/scripts/baemail/commands.js +258 -0
- package/dist/scripts/baemail/handler.d.ts +36 -0
- package/dist/scripts/baemail/handler.js +284 -0
- package/dist/scripts/baemail/index.d.ts +5 -0
- package/dist/scripts/baemail/index.js +5 -0
- package/dist/scripts/config.d.ts +48 -0
- package/dist/scripts/config.js +68 -0
- package/dist/scripts/index.d.ts +7 -0
- package/dist/scripts/index.js +7 -0
- package/dist/scripts/messaging/connect.d.ts +8 -0
- package/dist/scripts/messaging/connect.js +114 -0
- package/dist/scripts/messaging/handlers.d.ts +21 -0
- package/dist/scripts/messaging/handlers.js +334 -0
- package/dist/scripts/messaging/inbox.d.ts +11 -0
- package/dist/scripts/messaging/inbox.js +51 -0
- package/dist/scripts/messaging/index.d.ts +8 -0
- package/dist/scripts/messaging/index.js +8 -0
- package/dist/scripts/messaging/poll.d.ts +7 -0
- package/dist/scripts/messaging/poll.js +52 -0
- package/dist/scripts/messaging/send.d.ts +7 -0
- package/dist/scripts/messaging/send.js +43 -0
- package/dist/scripts/output.d.ts +12 -0
- package/dist/scripts/output.js +19 -0
- package/dist/scripts/overlay/discover.d.ts +7 -0
- package/dist/scripts/overlay/discover.js +72 -0
- package/dist/scripts/overlay/index.d.ts +7 -0
- package/dist/scripts/overlay/index.js +7 -0
- package/dist/scripts/overlay/registration.d.ts +19 -0
- package/dist/scripts/overlay/registration.js +176 -0
- package/dist/scripts/overlay/services.d.ts +29 -0
- package/dist/scripts/overlay/services.js +167 -0
- package/dist/scripts/overlay/transaction.d.ts +42 -0
- package/dist/scripts/overlay/transaction.js +103 -0
- package/dist/scripts/payment/build.d.ts +24 -0
- package/dist/scripts/payment/build.js +54 -0
- package/dist/scripts/payment/commands.d.ts +15 -0
- package/dist/scripts/payment/commands.js +73 -0
- package/dist/scripts/payment/index.d.ts +6 -0
- package/dist/scripts/payment/index.js +6 -0
- package/dist/scripts/payment/types.d.ts +56 -0
- package/dist/scripts/payment/types.js +4 -0
- package/dist/scripts/services/index.d.ts +6 -0
- package/dist/scripts/services/index.js +6 -0
- package/dist/scripts/services/queue.d.ts +11 -0
- package/dist/scripts/services/queue.js +28 -0
- package/dist/scripts/services/request.d.ts +7 -0
- package/dist/scripts/services/request.js +82 -0
- package/dist/scripts/services/respond.d.ts +11 -0
- package/dist/scripts/services/respond.js +132 -0
- package/dist/scripts/types.d.ts +107 -0
- package/dist/scripts/types.js +4 -0
- package/dist/scripts/utils/index.d.ts +6 -0
- package/dist/scripts/utils/index.js +6 -0
- package/dist/scripts/utils/merkle.d.ts +12 -0
- package/dist/scripts/utils/merkle.js +47 -0
- package/dist/scripts/utils/storage.d.ts +66 -0
- package/dist/scripts/utils/storage.js +211 -0
- package/dist/scripts/utils/woc.d.ts +26 -0
- package/dist/scripts/utils/woc.js +91 -0
- package/dist/scripts/wallet/balance.d.ts +22 -0
- package/dist/scripts/wallet/balance.js +240 -0
- package/dist/scripts/wallet/identity.d.ts +70 -0
- package/dist/scripts/wallet/identity.js +151 -0
- package/dist/scripts/wallet/index.d.ts +6 -0
- package/dist/scripts/wallet/index.js +6 -0
- package/dist/scripts/wallet/setup.d.ts +15 -0
- package/dist/scripts/wallet/setup.js +105 -0
- package/dist/scripts/x-verification/commands.d.ts +27 -0
- package/dist/scripts/x-verification/commands.js +222 -0
- package/dist/scripts/x-verification/index.d.ts +4 -0
- package/dist/scripts/x-verification/index.js +4 -0
- package/dist/services/built-in/api-proxy/index.d.ts +6 -0
- package/dist/services/built-in/api-proxy/index.js +23 -0
- package/dist/services/built-in/code-develop/index.d.ts +6 -0
- package/dist/services/built-in/code-develop/index.js +23 -0
- package/dist/services/built-in/code-review/index.d.ts +10 -0
- package/dist/services/built-in/code-review/index.js +51 -0
- package/dist/services/built-in/image-analysis/index.d.ts +6 -0
- package/dist/services/built-in/image-analysis/index.js +33 -0
- package/dist/services/built-in/memory-store/index.d.ts +6 -0
- package/dist/services/built-in/memory-store/index.js +22 -0
- package/dist/services/built-in/roulette/index.d.ts +6 -0
- package/dist/services/built-in/roulette/index.js +27 -0
- package/dist/services/built-in/summarize/index.d.ts +6 -0
- package/dist/services/built-in/summarize/index.js +21 -0
- package/dist/services/built-in/tell-joke/handler.d.ts +7 -0
- package/dist/services/built-in/tell-joke/handler.js +122 -0
- package/dist/services/built-in/tell-joke/index.d.ts +9 -0
- package/dist/services/built-in/tell-joke/index.js +31 -0
- package/dist/services/built-in/translate/index.d.ts +6 -0
- package/dist/services/built-in/translate/index.js +21 -0
- package/dist/services/built-in/web-research/index.d.ts +9 -0
- package/dist/services/built-in/web-research/index.js +51 -0
- package/dist/services/index.d.ts +13 -0
- package/dist/services/index.js +14 -0
- package/dist/services/loader.d.ts +77 -0
- package/dist/services/loader.js +292 -0
- package/dist/services/manager.d.ts +86 -0
- package/dist/services/manager.js +255 -0
- package/dist/services/registry.d.ts +98 -0
- package/dist/services/registry.js +204 -0
- package/dist/services/types.d.ts +230 -0
- package/dist/services/types.js +30 -0
- package/dist/test/cli.test.d.ts +7 -0
- package/dist/test/cli.test.js +329 -0
- package/dist/test/comprehensive-overlay.test.d.ts +13 -0
- package/dist/test/comprehensive-overlay.test.js +593 -0
- package/dist/test/key-derivation.test.d.ts +12 -0
- package/dist/test/key-derivation.test.js +86 -0
- package/dist/test/overlay-submit.test.d.ts +10 -0
- package/dist/test/overlay-submit.test.js +460 -0
- package/dist/test/request-response-flow.test.d.ts +5 -0
- package/dist/test/request-response-flow.test.js +209 -0
- package/dist/test/service-system.test.d.ts +5 -0
- package/dist/test/service-system.test.js +190 -0
- package/dist/test/utils/server-logic.d.ts +98 -0
- package/dist/test/utils/server-logic.js +286 -0
- package/dist/test/wallet.test.d.ts +7 -0
- package/dist/test/wallet.test.js +146 -0
- package/index.ts +1965 -0
- package/openclaw.plugin.json +106 -0
- package/package.json +73 -0
- package/src/cli-main.ts +230 -0
- package/src/cli.ts +16 -0
- package/src/core/README.md +246 -0
- package/src/core/config.ts +21 -0
- package/src/core/index.ts +42 -0
- package/src/core/payment.ts +111 -0
- package/src/core/types.ts +102 -0
- package/src/core/verify.ts +119 -0
- package/src/core/wallet.ts +282 -0
- package/src/scripts/baemail/commands.ts +326 -0
- package/src/scripts/baemail/handler.ts +338 -0
- package/src/scripts/baemail/index.ts +6 -0
- package/src/scripts/config.ts +81 -0
- package/src/scripts/index.ts +8 -0
- package/src/scripts/messaging/connect.ts +121 -0
- package/src/scripts/messaging/handlers.ts +394 -0
- package/src/scripts/messaging/inbox.ts +64 -0
- package/src/scripts/messaging/index.ts +9 -0
- package/src/scripts/messaging/poll.ts +59 -0
- package/src/scripts/messaging/send.ts +54 -0
- package/src/scripts/output.ts +21 -0
- package/src/scripts/overlay/discover.ts +81 -0
- package/src/scripts/overlay/index.ts +8 -0
- package/src/scripts/overlay/registration.ts +199 -0
- package/src/scripts/overlay/services.ts +199 -0
- package/src/scripts/overlay/transaction.ts +124 -0
- package/src/scripts/payment/build.ts +65 -0
- package/src/scripts/payment/commands.ts +92 -0
- package/src/scripts/payment/index.ts +7 -0
- package/src/scripts/payment/types.ts +62 -0
- package/src/scripts/services/index.ts +7 -0
- package/src/scripts/services/queue.ts +35 -0
- package/src/scripts/services/request.ts +98 -0
- package/src/scripts/services/respond.ts +149 -0
- package/src/scripts/types.ts +121 -0
- package/src/scripts/utils/index.ts +7 -0
- package/src/scripts/utils/merkle.ts +57 -0
- package/src/scripts/utils/storage.ts +231 -0
- package/src/scripts/utils/woc.ts +106 -0
- package/src/scripts/wallet/balance.ts +277 -0
- package/src/scripts/wallet/identity.ts +203 -0
- package/src/scripts/wallet/index.ts +7 -0
- package/src/scripts/wallet/setup.ts +121 -0
- package/src/scripts/x-verification/commands.ts +256 -0
- package/src/scripts/x-verification/index.ts +5 -0
- package/src/services/built-in/api-proxy/index.ts +26 -0
- package/src/services/built-in/api-proxy/prompt.md +26 -0
- package/src/services/built-in/code-develop/index.ts +26 -0
- package/src/services/built-in/code-develop/prompt.md +35 -0
- package/src/services/built-in/code-review/index.ts +54 -0
- package/src/services/built-in/code-review/prompt.md +105 -0
- package/src/services/built-in/image-analysis/index.ts +36 -0
- package/src/services/built-in/image-analysis/prompt.md +42 -0
- package/src/services/built-in/memory-store/index.ts +25 -0
- package/src/services/built-in/memory-store/prompt.md +45 -0
- package/src/services/built-in/roulette/index.ts +30 -0
- package/src/services/built-in/roulette/prompt.md +35 -0
- package/src/services/built-in/summarize/index.ts +24 -0
- package/src/services/built-in/summarize/prompt.md +27 -0
- package/src/services/built-in/tell-joke/handler.ts +134 -0
- package/src/services/built-in/tell-joke/index.ts +34 -0
- package/src/services/built-in/tell-joke/prompt.md +59 -0
- package/src/services/built-in/translate/index.ts +24 -0
- package/src/services/built-in/translate/prompt.md +23 -0
- package/src/services/built-in/web-research/index.ts +54 -0
- package/src/services/built-in/web-research/prompt.md +110 -0
- package/src/services/index.ts +16 -0
- package/src/services/loader.ts +344 -0
- package/src/services/manager.ts +304 -0
- package/src/services/registry.ts +246 -0
- package/src/services/types.ts +259 -0
- package/src/test/cli.test.ts +352 -0
- package/src/test/comprehensive-overlay.test.ts +729 -0
- package/src/test/key-derivation.test.ts +102 -0
- package/src/test/overlay-submit.test.ts +570 -0
- package/src/test/request-response-flow.test.ts +252 -0
- package/src/test/service-system.test.ts +241 -0
- package/src/test/utils/server-logic.ts +368 -0
- package/src/test/wallet.test.ts +166 -0
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration constants and environment variables for the overlay CLI.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import os from 'node:os';
|
|
7
|
+
import fs from 'node:fs';
|
|
8
|
+
|
|
9
|
+
// Auto-load .env from overlay state dir if it exists
|
|
10
|
+
const overlayEnvPath = path.join(os.homedir(), '.clawdbot', 'bsv-overlay', '.env');
|
|
11
|
+
try {
|
|
12
|
+
if (fs.existsSync(overlayEnvPath)) {
|
|
13
|
+
for (const line of fs.readFileSync(overlayEnvPath, 'utf-8').split('\n')) {
|
|
14
|
+
const match = line.match(/^([A-Z_]+)=(.+)$/);
|
|
15
|
+
if (match && !process.env[match[1]]) {
|
|
16
|
+
process.env[match[1]] = match[2].trim();
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
// Ignore errors loading .env
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Wallet storage directory */
|
|
25
|
+
export const WALLET_DIR = process.env.BSV_WALLET_DIR
|
|
26
|
+
|| path.join(os.homedir(), '.clawdbot', 'bsv-wallet');
|
|
27
|
+
|
|
28
|
+
/** Network to use (mainnet or testnet) */
|
|
29
|
+
export const NETWORK: 'mainnet' | 'testnet' =
|
|
30
|
+
(process.env.BSV_NETWORK as 'mainnet' | 'testnet') || 'mainnet';
|
|
31
|
+
|
|
32
|
+
/** Overlay server URL */
|
|
33
|
+
export const OVERLAY_URL = process.env.OVERLAY_URL || 'https://clawoverlay.com';
|
|
34
|
+
|
|
35
|
+
/** Agent display name on the overlay network */
|
|
36
|
+
export const AGENT_NAME = process.env.AGENT_NAME || 'clawdbot-agent';
|
|
37
|
+
|
|
38
|
+
/** Agent description for the overlay identity */
|
|
39
|
+
export const AGENT_DESCRIPTION = process.env.AGENT_DESCRIPTION ||
|
|
40
|
+
`AI agent on the OpenClaw Overlay Network. Offers services for BSV micropayments.`;
|
|
41
|
+
|
|
42
|
+
/** WhatsOnChain API key (optional, for rate limit bypass) */
|
|
43
|
+
export const WOC_API_KEY = process.env.WOC_API_KEY || '';
|
|
44
|
+
|
|
45
|
+
/** Overlay state directory for registration, services, etc. */
|
|
46
|
+
export const OVERLAY_STATE_DIR = path.join(os.homedir(), '.clawdbot', 'bsv-overlay');
|
|
47
|
+
|
|
48
|
+
/** Protocol identifier for overlay transactions */
|
|
49
|
+
export const PROTOCOL_ID = 'clawdbot overlay v1';
|
|
50
|
+
|
|
51
|
+
/** Topic managers for overlay submissions */
|
|
52
|
+
export const TOPICS = {
|
|
53
|
+
IDENTITY: 'tm_clawdbot_identity',
|
|
54
|
+
SERVICES: 'tm_clawdbot_services',
|
|
55
|
+
X_VERIFICATION: 'tm_clawdbot_x_verification',
|
|
56
|
+
} as const;
|
|
57
|
+
|
|
58
|
+
/** Lookup services for overlay queries */
|
|
59
|
+
export const LOOKUP_SERVICES = {
|
|
60
|
+
AGENTS: 'ls_clawdbot_agents',
|
|
61
|
+
SERVICES: 'ls_clawdbot_services',
|
|
62
|
+
X_VERIFICATIONS: 'ls_clawdbot_x_verifications',
|
|
63
|
+
} as const;
|
|
64
|
+
|
|
65
|
+
/** Paths derived from config */
|
|
66
|
+
export const PATHS = {
|
|
67
|
+
walletIdentity: path.join(WALLET_DIR, 'wallet-identity.json'),
|
|
68
|
+
registration: path.join(OVERLAY_STATE_DIR, 'registration.json'),
|
|
69
|
+
services: path.join(OVERLAY_STATE_DIR, 'services.json'),
|
|
70
|
+
latestChange: path.join(OVERLAY_STATE_DIR, 'latest-change.json'),
|
|
71
|
+
receivedPayments: path.join(OVERLAY_STATE_DIR, 'received-payments.jsonl'),
|
|
72
|
+
researchQueue: path.join(OVERLAY_STATE_DIR, 'research-queue.jsonl'),
|
|
73
|
+
serviceQueue: path.join(OVERLAY_STATE_DIR, 'service-queue.jsonl'),
|
|
74
|
+
notifications: path.join(OVERLAY_STATE_DIR, 'notifications.jsonl'),
|
|
75
|
+
xVerifications: path.join(OVERLAY_STATE_DIR, 'x-verifications.json'),
|
|
76
|
+
pendingXVerification: path.join(OVERLAY_STATE_DIR, 'pending-x-verification.json'),
|
|
77
|
+
xEngagementQueue: path.join(OVERLAY_STATE_DIR, 'x-engagement-queue.jsonl'),
|
|
78
|
+
memoryStore: path.join(WALLET_DIR, 'memory-store.json'),
|
|
79
|
+
baemailConfig: path.join(OVERLAY_STATE_DIR, 'baemail-config.json'),
|
|
80
|
+
baemailLog: path.join(OVERLAY_STATE_DIR, 'baemail-log.jsonl'),
|
|
81
|
+
} as const;
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Connect command: WebSocket real-time message processing.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { OVERLAY_URL, PATHS } from '../config.js';
|
|
7
|
+
import { fail } from '../output.js';
|
|
8
|
+
import { loadIdentity } from '../wallet/identity.js';
|
|
9
|
+
import { processMessage } from './handlers.js';
|
|
10
|
+
import { ensureStateDir } from '../utils/storage.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Connect command: establish WebSocket connection for real-time messaging.
|
|
14
|
+
* Note: This function never returns normally - it runs until SIGINT/SIGTERM.
|
|
15
|
+
*/
|
|
16
|
+
export async function cmdConnect(): Promise<void> {
|
|
17
|
+
let WebSocketClient: any;
|
|
18
|
+
try {
|
|
19
|
+
const ws = await import('ws');
|
|
20
|
+
WebSocketClient = ws.default || (ws as any).WebSocket || ws;
|
|
21
|
+
} catch {
|
|
22
|
+
return fail('WebSocket client not available. Install it: npm install ws');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const { identityKey, privKey } = await loadIdentity();
|
|
26
|
+
const wsUrl = OVERLAY_URL.replace(/^http/, 'ws') + '/relay/subscribe?identity=' + identityKey;
|
|
27
|
+
|
|
28
|
+
let reconnectDelay = 1000;
|
|
29
|
+
let shouldReconnect = true;
|
|
30
|
+
let currentWs: any = null;
|
|
31
|
+
|
|
32
|
+
function shutdown() {
|
|
33
|
+
shouldReconnect = false;
|
|
34
|
+
if (currentWs) {
|
|
35
|
+
try { currentWs.close(); } catch {}
|
|
36
|
+
}
|
|
37
|
+
process.exit(0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.on('SIGINT', shutdown);
|
|
41
|
+
process.on('SIGTERM', shutdown);
|
|
42
|
+
|
|
43
|
+
function connect() {
|
|
44
|
+
const ws = new WebSocketClient(wsUrl);
|
|
45
|
+
currentWs = ws;
|
|
46
|
+
|
|
47
|
+
ws.on('open', () => {
|
|
48
|
+
reconnectDelay = 1000; // reset on successful connect
|
|
49
|
+
console.error(JSON.stringify({ event: 'connected', identity: identityKey, overlay: OVERLAY_URL }));
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
ws.on('message', async (data: any) => {
|
|
53
|
+
try {
|
|
54
|
+
const envelope = JSON.parse(data.toString());
|
|
55
|
+
if (envelope.type === 'message') {
|
|
56
|
+
const result = await processMessage(envelope.message, identityKey, privKey);
|
|
57
|
+
// Output the result as a JSON line to stdout
|
|
58
|
+
console.log(JSON.stringify(result));
|
|
59
|
+
|
|
60
|
+
// Also append to notification log
|
|
61
|
+
ensureStateDir();
|
|
62
|
+
try {
|
|
63
|
+
fs.appendFileSync(PATHS.notifications, JSON.stringify({ ...result, _ts: Date.now() }) + '\n');
|
|
64
|
+
} catch {}
|
|
65
|
+
|
|
66
|
+
// Ack the message
|
|
67
|
+
if (result.ack) {
|
|
68
|
+
try {
|
|
69
|
+
await fetch(OVERLAY_URL + '/relay/ack', {
|
|
70
|
+
method: 'POST',
|
|
71
|
+
headers: { 'Content-Type': 'application/json' },
|
|
72
|
+
body: JSON.stringify({ identity: identityKey, messageIds: [result.id] }),
|
|
73
|
+
});
|
|
74
|
+
} catch (ackErr: any) {
|
|
75
|
+
console.error(JSON.stringify({ event: 'ack-error', id: result.id, message: String(ackErr) }));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Handle service announcements
|
|
81
|
+
if (envelope.type === 'service-announced') {
|
|
82
|
+
const svc = envelope.service || {};
|
|
83
|
+
const announcement = {
|
|
84
|
+
event: 'service-announced',
|
|
85
|
+
serviceId: svc.serviceId,
|
|
86
|
+
name: svc.name,
|
|
87
|
+
description: svc.description,
|
|
88
|
+
priceSats: svc.pricingSats,
|
|
89
|
+
provider: svc.identityKey,
|
|
90
|
+
txid: envelope.txid,
|
|
91
|
+
_ts: Date.now(),
|
|
92
|
+
};
|
|
93
|
+
console.log(JSON.stringify(announcement));
|
|
94
|
+
ensureStateDir();
|
|
95
|
+
try {
|
|
96
|
+
fs.appendFileSync(PATHS.notifications, JSON.stringify(announcement) + '\n');
|
|
97
|
+
} catch {}
|
|
98
|
+
}
|
|
99
|
+
} catch (err: any) {
|
|
100
|
+
console.error(JSON.stringify({ event: 'process-error', message: String(err) }));
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
ws.on('close', () => {
|
|
105
|
+
currentWs = null;
|
|
106
|
+
if (shouldReconnect) {
|
|
107
|
+
console.error(JSON.stringify({ event: 'disconnected', reconnectMs: reconnectDelay }));
|
|
108
|
+
setTimeout(connect, reconnectDelay);
|
|
109
|
+
reconnectDelay = Math.min(reconnectDelay * 2, 30000);
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
ws.on('error', (err: any) => {
|
|
114
|
+
console.error(JSON.stringify({ event: 'error', message: err.message }));
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
connect();
|
|
119
|
+
// Keep the process alive — never resolves
|
|
120
|
+
await new Promise(() => {});
|
|
121
|
+
}
|
|
@@ -0,0 +1,394 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Message type handlers and processMessage function.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { OVERLAY_URL, WALLET_DIR, PATHS } from '../config.js';
|
|
7
|
+
import { signRelayMessage, verifyRelaySignature, loadWalletIdentity } from '../wallet/identity.js';
|
|
8
|
+
import { loadServices, appendToJsonl } from '../utils/storage.js';
|
|
9
|
+
import { fetchWithTimeout } from '../utils/woc.js';
|
|
10
|
+
import { serviceManager } from '../../services/index.js';
|
|
11
|
+
import type { RelayMessage, ProcessMessageResult } from '../types.js';
|
|
12
|
+
|
|
13
|
+
// Dynamic import for @bsv/sdk (needed for hash160 computation)
|
|
14
|
+
let _sdk: any = null;
|
|
15
|
+
|
|
16
|
+
async function getSdk(): Promise<any> {
|
|
17
|
+
if (_sdk) return _sdk;
|
|
18
|
+
|
|
19
|
+
try {
|
|
20
|
+
_sdk = await import('@bsv/sdk');
|
|
21
|
+
return _sdk;
|
|
22
|
+
} catch {
|
|
23
|
+
const { fileURLToPath } = await import('node:url');
|
|
24
|
+
const path = await import('node:path');
|
|
25
|
+
const os = await import('node:os');
|
|
26
|
+
|
|
27
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
const candidates = [
|
|
29
|
+
path.resolve(__dirname, '..', '..', '..', 'node_modules', '@bsv', 'sdk', 'dist', 'esm', 'mod.js'),
|
|
30
|
+
path.resolve(__dirname, '..', '..', '..', '..', '..', 'a2a-bsv', 'packages', 'core', 'node_modules', '@bsv', 'sdk', 'dist', 'esm', 'mod.js'),
|
|
31
|
+
path.resolve(os.homedir(), 'a2a-bsv', 'packages', 'core', 'node_modules', '@bsv', 'sdk', 'dist', 'esm', 'mod.js'),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
for (const p of candidates) {
|
|
35
|
+
try {
|
|
36
|
+
_sdk = await import(p);
|
|
37
|
+
return _sdk;
|
|
38
|
+
} catch {
|
|
39
|
+
// Try next
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
throw new Error('Cannot find @bsv/sdk. Run setup.sh first.');
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
import { BSVAgentWallet } from '../../core/index.js';
|
|
47
|
+
|
|
48
|
+
async function getBSVAgentWallet(): Promise<typeof BSVAgentWallet> {
|
|
49
|
+
return BSVAgentWallet;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Import NETWORK lazily to avoid circular dependencies
|
|
53
|
+
async function getNetwork(): Promise<'mainnet' | 'testnet'> {
|
|
54
|
+
const config = await import('../config.js');
|
|
55
|
+
return config.NETWORK;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Verify and accept a payment from a service request.
|
|
60
|
+
* Uses a2a-bsv wallet.acceptPayment() for proper BRC-29 handling.
|
|
61
|
+
*/
|
|
62
|
+
export async function verifyAndAcceptPayment(
|
|
63
|
+
payment: any,
|
|
64
|
+
minSats: number,
|
|
65
|
+
senderKey: string,
|
|
66
|
+
serviceId: string,
|
|
67
|
+
ourHash160: Uint8Array
|
|
68
|
+
): Promise<{
|
|
69
|
+
accepted: boolean;
|
|
70
|
+
txid: string | null;
|
|
71
|
+
satoshis: number;
|
|
72
|
+
outputIndex: number;
|
|
73
|
+
walletAccepted: boolean;
|
|
74
|
+
error: string | null;
|
|
75
|
+
}> {
|
|
76
|
+
if (!payment) {
|
|
77
|
+
return { accepted: false, txid: null, satoshis: 0, outputIndex: 0, walletAccepted: false, error: 'no payment' };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (payment.error) {
|
|
81
|
+
return { accepted: false, txid: null, satoshis: 0, outputIndex: 0, walletAccepted: false, error: payment.error };
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!payment.beef || !payment.satoshis) {
|
|
85
|
+
return { accepted: false, txid: null, satoshis: 0, outputIndex: 0, walletAccepted: false, error: 'missing beef or satoshis' };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (payment.satoshis < minSats) {
|
|
89
|
+
return { accepted: false, txid: payment.txid || null, satoshis: payment.satoshis, outputIndex: 0, walletAccepted: false, error: `insufficient payment: ${payment.satoshis} < ${minSats}` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Accept the payment using a2a-bsv wallet
|
|
93
|
+
const BSVAgentWallet = await getBSVAgentWallet();
|
|
94
|
+
const network = await getNetwork();
|
|
95
|
+
const wallet = await BSVAgentWallet.load({ network, storageDir: WALLET_DIR });
|
|
96
|
+
|
|
97
|
+
try {
|
|
98
|
+
// First verify the payment structure
|
|
99
|
+
const verifyResult = await wallet.verifyPayment({ beef: payment.beef });
|
|
100
|
+
if (!verifyResult.valid) {
|
|
101
|
+
await wallet.destroy();
|
|
102
|
+
return { accepted: false, txid: payment.txid || null, satoshis: payment.satoshis, outputIndex: 0, walletAccepted: false, error: `verification failed: ${verifyResult.errors.join(', ')}` };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Accept the payment (this broadcasts the transaction)
|
|
106
|
+
const acceptResult = await wallet.acceptPayment({
|
|
107
|
+
beef: payment.beef,
|
|
108
|
+
derivationPrefix: payment.derivationPrefix,
|
|
109
|
+
derivationSuffix: payment.derivationSuffix,
|
|
110
|
+
senderIdentityKey: payment.senderIdentityKey,
|
|
111
|
+
description: `Payment for ${serviceId}`,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await wallet.destroy();
|
|
115
|
+
|
|
116
|
+
if (!acceptResult.accepted) {
|
|
117
|
+
return { accepted: false, txid: payment.txid || null, satoshis: payment.satoshis, outputIndex: 0, walletAccepted: false, error: 'wallet rejected payment' };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return {
|
|
121
|
+
accepted: true,
|
|
122
|
+
txid: payment.txid,
|
|
123
|
+
satoshis: payment.satoshis,
|
|
124
|
+
outputIndex: 0,
|
|
125
|
+
walletAccepted: true,
|
|
126
|
+
error: null,
|
|
127
|
+
};
|
|
128
|
+
} catch (err: any) {
|
|
129
|
+
await wallet.destroy();
|
|
130
|
+
return { accepted: false, txid: payment.txid || null, satoshis: payment.satoshis, outputIndex: 0, walletAccepted: false, error: err.message };
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Queue a service request for agent processing.
|
|
136
|
+
*/
|
|
137
|
+
async function queueForAgent(
|
|
138
|
+
msg: RelayMessage,
|
|
139
|
+
identityKey: string,
|
|
140
|
+
privKey: any,
|
|
141
|
+
serviceId: string
|
|
142
|
+
): Promise<ProcessMessageResult> {
|
|
143
|
+
// Check if this request has already been processed to prevent duplicates
|
|
144
|
+
if (fs.existsSync(PATHS.serviceQueue)) {
|
|
145
|
+
const lines = fs.readFileSync(PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
try {
|
|
148
|
+
const entry = JSON.parse(line);
|
|
149
|
+
if (entry.requestId === msg.id) {
|
|
150
|
+
// Request already exists in queue - return existing status
|
|
151
|
+
return {
|
|
152
|
+
id: msg.id,
|
|
153
|
+
type: 'service-request',
|
|
154
|
+
serviceId,
|
|
155
|
+
action: entry.status === 'pending' ? 'already-queued' : `already-${entry.status}`,
|
|
156
|
+
paymentAccepted: true,
|
|
157
|
+
paymentTxid: entry.paymentTxid,
|
|
158
|
+
satoshisReceived: entry.satoshisReceived,
|
|
159
|
+
from: msg.from,
|
|
160
|
+
ack: true,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
} catch {}
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const sdk = await getSdk();
|
|
168
|
+
const payment = msg.payload?.payment as any;
|
|
169
|
+
const input = msg.payload?.input || msg.payload;
|
|
170
|
+
|
|
171
|
+
// Verify and accept payment
|
|
172
|
+
const walletIdentity = loadWalletIdentity();
|
|
173
|
+
const ourHash160 = sdk.Hash.hash160(sdk.PrivateKey.fromHex(walletIdentity.rootKeyHex).toPublicKey().encode(true));
|
|
174
|
+
|
|
175
|
+
// Find the service price using the service registry
|
|
176
|
+
const serviceDefinition = serviceManager.registry.get(serviceId);
|
|
177
|
+
let minPrice = 5; // default fallback
|
|
178
|
+
|
|
179
|
+
if (serviceDefinition) {
|
|
180
|
+
minPrice = serviceDefinition.defaultPrice;
|
|
181
|
+
|
|
182
|
+
// Validate service input if possible
|
|
183
|
+
const validation = serviceManager.validate(serviceId, input);
|
|
184
|
+
if (!validation.valid) {
|
|
185
|
+
// Send validation rejection
|
|
186
|
+
const rejectPayload = {
|
|
187
|
+
requestId: msg.id,
|
|
188
|
+
serviceId,
|
|
189
|
+
status: 'rejected',
|
|
190
|
+
reason: `Input validation failed: ${validation.error}`
|
|
191
|
+
};
|
|
192
|
+
const sig = await signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
193
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
194
|
+
method: 'POST',
|
|
195
|
+
headers: { 'Content-Type': 'application/json' },
|
|
196
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// Also add the rejected entry to the queue for tracking
|
|
200
|
+
const rejectedEntry = {
|
|
201
|
+
status: 'rejected',
|
|
202
|
+
requestId: msg.id,
|
|
203
|
+
serviceId,
|
|
204
|
+
from: msg.from,
|
|
205
|
+
identityKey,
|
|
206
|
+
input: input,
|
|
207
|
+
paymentTxid: null,
|
|
208
|
+
satoshisReceived: 0,
|
|
209
|
+
walletAccepted: false,
|
|
210
|
+
error: validation.error,
|
|
211
|
+
_ts: Date.now(),
|
|
212
|
+
};
|
|
213
|
+
appendToJsonl(PATHS.serviceQueue, rejectedEntry);
|
|
214
|
+
|
|
215
|
+
return {
|
|
216
|
+
id: msg.id,
|
|
217
|
+
type: 'service-request',
|
|
218
|
+
serviceId,
|
|
219
|
+
action: 'rejected',
|
|
220
|
+
reason: validation.error || 'input validation failed',
|
|
221
|
+
from: msg.from,
|
|
222
|
+
ack: true
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
} else {
|
|
226
|
+
// Fall back to legacy service loading for backward compatibility
|
|
227
|
+
const services = loadServices();
|
|
228
|
+
const svc = services.find(s => s.serviceId === serviceId);
|
|
229
|
+
minPrice = svc?.priceSats || 5;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const payResult = await verifyAndAcceptPayment(payment, minPrice, msg.from, serviceId, ourHash160);
|
|
233
|
+
if (!payResult.accepted) {
|
|
234
|
+
// Send rejection
|
|
235
|
+
const rejectPayload = { requestId: msg.id, serviceId, status: 'rejected', reason: `Payment rejected: ${payResult.error}` };
|
|
236
|
+
const sig = await signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
237
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
238
|
+
method: 'POST',
|
|
239
|
+
headers: { 'Content-Type': 'application/json' },
|
|
240
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// Also add the rejected entry to the queue for tracking
|
|
244
|
+
const rejectedEntry = {
|
|
245
|
+
status: 'rejected',
|
|
246
|
+
requestId: msg.id,
|
|
247
|
+
serviceId,
|
|
248
|
+
from: msg.from,
|
|
249
|
+
identityKey,
|
|
250
|
+
input: input,
|
|
251
|
+
paymentTxid: null,
|
|
252
|
+
satoshisReceived: 0,
|
|
253
|
+
walletAccepted: false,
|
|
254
|
+
error: payResult.error,
|
|
255
|
+
_ts: Date.now(),
|
|
256
|
+
};
|
|
257
|
+
appendToJsonl(PATHS.serviceQueue, rejectedEntry);
|
|
258
|
+
|
|
259
|
+
return { id: msg.id, type: 'service-request', serviceId, action: 'rejected', reason: payResult.error || 'payment rejected', from: msg.from, ack: true };
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Queue for agent processing
|
|
263
|
+
const queueEntry = {
|
|
264
|
+
status: 'pending',
|
|
265
|
+
requestId: msg.id,
|
|
266
|
+
serviceId,
|
|
267
|
+
from: msg.from,
|
|
268
|
+
identityKey,
|
|
269
|
+
input: input,
|
|
270
|
+
paymentTxid: payResult.txid,
|
|
271
|
+
satoshisReceived: payResult.satoshis,
|
|
272
|
+
walletAccepted: payResult.walletAccepted,
|
|
273
|
+
_ts: Date.now(),
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
appendToJsonl(PATHS.serviceQueue, queueEntry);
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
id: msg.id,
|
|
280
|
+
type: 'service-request',
|
|
281
|
+
serviceId,
|
|
282
|
+
action: 'queued-for-agent',
|
|
283
|
+
paymentAccepted: true,
|
|
284
|
+
paymentTxid: payResult.txid,
|
|
285
|
+
satoshisReceived: payResult.satoshis,
|
|
286
|
+
from: msg.from,
|
|
287
|
+
ack: true,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Process a single relay message.
|
|
293
|
+
* Handles pings, service requests, pongs, and service responses.
|
|
294
|
+
*/
|
|
295
|
+
export async function processMessage(
|
|
296
|
+
msg: RelayMessage,
|
|
297
|
+
identityKey: string,
|
|
298
|
+
privKey: any
|
|
299
|
+
): Promise<ProcessMessageResult> {
|
|
300
|
+
// Verify signature if present
|
|
301
|
+
const sigCheck: { valid: boolean | null; reason?: string } = msg.signature
|
|
302
|
+
? await verifyRelaySignature(msg.from, msg.to, msg.type, msg.payload, msg.signature)
|
|
303
|
+
: { valid: null };
|
|
304
|
+
|
|
305
|
+
// Reject unsigned/forged service-requests
|
|
306
|
+
if (msg.type === 'service-request' && sigCheck.valid !== true) {
|
|
307
|
+
console.error(JSON.stringify({ event: 'signature-rejected', type: msg.type, from: msg.from, reason: sigCheck.reason || 'missing signature' }));
|
|
308
|
+
return {
|
|
309
|
+
id: msg.id,
|
|
310
|
+
type: msg.type,
|
|
311
|
+
from: msg.from,
|
|
312
|
+
action: 'rejected',
|
|
313
|
+
reason: 'invalid-signature',
|
|
314
|
+
signatureValid: sigCheck.valid,
|
|
315
|
+
ack: true,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (msg.type === 'ping') {
|
|
320
|
+
// Auto-respond with pong
|
|
321
|
+
const pongPayload = {
|
|
322
|
+
text: 'pong',
|
|
323
|
+
inReplyTo: msg.id,
|
|
324
|
+
originalText: (msg.payload as any)?.text || null,
|
|
325
|
+
};
|
|
326
|
+
const pongSig = await signRelayMessage(privKey, msg.from, 'pong', pongPayload);
|
|
327
|
+
await fetch(`${OVERLAY_URL}/relay/send`, {
|
|
328
|
+
method: 'POST',
|
|
329
|
+
headers: { 'Content-Type': 'application/json' },
|
|
330
|
+
body: JSON.stringify({
|
|
331
|
+
from: identityKey,
|
|
332
|
+
to: msg.from,
|
|
333
|
+
type: 'pong',
|
|
334
|
+
payload: pongPayload,
|
|
335
|
+
signature: pongSig,
|
|
336
|
+
}),
|
|
337
|
+
});
|
|
338
|
+
return { id: msg.id, type: 'ping', action: 'replied-pong', from: msg.from, ack: true };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (msg.type === 'service-request') {
|
|
342
|
+
const serviceId = (msg.payload as any)?.serviceId;
|
|
343
|
+
|
|
344
|
+
// Agent-routed mode: queue for the agent
|
|
345
|
+
if (process.env.AGENT_ROUTED === 'true') {
|
|
346
|
+
return await queueForAgent(msg, identityKey, privKey, serviceId);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// No hardcoded handlers in TypeScript version — always queue
|
|
350
|
+
return await queueForAgent(msg, identityKey, privKey, serviceId);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
if (msg.type === 'pong') {
|
|
354
|
+
return {
|
|
355
|
+
id: msg.id,
|
|
356
|
+
type: 'pong',
|
|
357
|
+
action: 'received',
|
|
358
|
+
from: msg.from,
|
|
359
|
+
text: (msg.payload as any)?.text,
|
|
360
|
+
inReplyTo: (msg.payload as any)?.inReplyTo,
|
|
361
|
+
ack: true,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
if (msg.type === 'service-response') {
|
|
366
|
+
const serviceId = (msg.payload as any)?.serviceId;
|
|
367
|
+
const status = (msg.payload as any)?.status;
|
|
368
|
+
const result = (msg.payload as any)?.result;
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
id: msg.id,
|
|
372
|
+
type: 'service-response',
|
|
373
|
+
action: 'received',
|
|
374
|
+
from: msg.from,
|
|
375
|
+
serviceId,
|
|
376
|
+
status,
|
|
377
|
+
result,
|
|
378
|
+
requestId: (msg.payload as any)?.requestId,
|
|
379
|
+
direction: 'incoming-response',
|
|
380
|
+
ack: true,
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// Unknown type
|
|
385
|
+
return {
|
|
386
|
+
id: msg.id,
|
|
387
|
+
type: msg.type,
|
|
388
|
+
from: msg.from,
|
|
389
|
+
payload: msg.payload,
|
|
390
|
+
signatureValid: sigCheck.valid,
|
|
391
|
+
action: 'unhandled',
|
|
392
|
+
ack: false,
|
|
393
|
+
};
|
|
394
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inbox and ack commands.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { OVERLAY_URL } from '../config.js';
|
|
6
|
+
import { ok, fail } from '../output.js';
|
|
7
|
+
import { loadIdentity, verifyRelaySignature } from '../wallet/identity.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Inbox command: fetch pending messages.
|
|
11
|
+
*/
|
|
12
|
+
export async function cmdInbox(args: string[]): Promise<never> {
|
|
13
|
+
const { identityKey } = await loadIdentity();
|
|
14
|
+
|
|
15
|
+
let since = '';
|
|
16
|
+
for (let i = 0; i < args.length; i++) {
|
|
17
|
+
if (args[i] === '--since' && args[i + 1]) since = `&since=${args[++i]}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const resp = await fetch(`${OVERLAY_URL}/relay/inbox?identity=${identityKey}${since}`);
|
|
21
|
+
if (!resp.ok) {
|
|
22
|
+
const body = await resp.text();
|
|
23
|
+
return fail(`Relay inbox failed (${resp.status}): ${body}`);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const result = await resp.json();
|
|
27
|
+
|
|
28
|
+
// Verify signatures on received messages
|
|
29
|
+
const messages = await Promise.all(
|
|
30
|
+
result.messages.map(async (msg: any) => ({
|
|
31
|
+
...msg,
|
|
32
|
+
signatureValid: msg.signature
|
|
33
|
+
? (await verifyRelaySignature(msg.from, msg.to, msg.type, msg.payload, msg.signature)).valid
|
|
34
|
+
: null,
|
|
35
|
+
}))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
return ok({ messages, count: messages.length, identityKey });
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ack command: acknowledge processed messages.
|
|
43
|
+
*/
|
|
44
|
+
export async function cmdAck(messageIds: string[]): Promise<never> {
|
|
45
|
+
if (!messageIds || messageIds.length === 0) {
|
|
46
|
+
return fail('Usage: ack <messageId> [messageId2 ...]');
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const { identityKey } = await loadIdentity();
|
|
50
|
+
|
|
51
|
+
const resp = await fetch(`${OVERLAY_URL}/relay/ack`, {
|
|
52
|
+
method: 'POST',
|
|
53
|
+
headers: { 'Content-Type': 'application/json' },
|
|
54
|
+
body: JSON.stringify({ identity: identityKey, messageIds }),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
if (!resp.ok) {
|
|
58
|
+
const body = await resp.text();
|
|
59
|
+
return fail(`Relay ack failed (${resp.status}): ${body}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const result = await resp.json();
|
|
63
|
+
return ok({ acked: result.acked, messageIds });
|
|
64
|
+
}
|