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,326 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baemail commands - paid message forwarding service.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import { PATHS } from '../config.js';
|
|
7
|
+
import { ok, fail } from '../output.js';
|
|
8
|
+
import { loadIdentity } from '../wallet/identity.js';
|
|
9
|
+
import { ensureStateDir } from '../utils/storage.js';
|
|
10
|
+
import { fetchWithTimeout } from '../utils/woc.js';
|
|
11
|
+
|
|
12
|
+
// Types
|
|
13
|
+
export interface BaemailConfig {
|
|
14
|
+
deliveryChannel: string;
|
|
15
|
+
tiers: {
|
|
16
|
+
standard: number;
|
|
17
|
+
priority: number;
|
|
18
|
+
urgent: number;
|
|
19
|
+
};
|
|
20
|
+
maxMessageLength: number;
|
|
21
|
+
blocklist: string[];
|
|
22
|
+
createdAt: string;
|
|
23
|
+
updatedAt: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface BaemailLogEntry {
|
|
27
|
+
requestId: string;
|
|
28
|
+
from: string;
|
|
29
|
+
senderName: string;
|
|
30
|
+
tier: string;
|
|
31
|
+
paidSats: number;
|
|
32
|
+
messageLength: number;
|
|
33
|
+
deliveryChannel: string;
|
|
34
|
+
deliverySuccess: boolean;
|
|
35
|
+
deliveryError: string | null;
|
|
36
|
+
paymentTxid: string;
|
|
37
|
+
refundStatus: string | null;
|
|
38
|
+
refundTxid?: string;
|
|
39
|
+
refundedAt?: string;
|
|
40
|
+
timestamp: string;
|
|
41
|
+
_lineIdx?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Load baemail configuration.
|
|
46
|
+
*/
|
|
47
|
+
export function loadBaemailConfig(): BaemailConfig | null {
|
|
48
|
+
try {
|
|
49
|
+
if (fs.existsSync(PATHS.baemailConfig)) {
|
|
50
|
+
return JSON.parse(fs.readFileSync(PATHS.baemailConfig, 'utf-8'));
|
|
51
|
+
}
|
|
52
|
+
} catch (err) {
|
|
53
|
+
console.warn(`[baemail] Warning: Could not read config: ${(err as Error).message}`);
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save baemail configuration.
|
|
60
|
+
*/
|
|
61
|
+
export function saveBaemailConfig(config: BaemailConfig): void {
|
|
62
|
+
ensureStateDir();
|
|
63
|
+
fs.writeFileSync(PATHS.baemailConfig, JSON.stringify(config, null, 2));
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Setup baemail service with delivery channel and tier pricing.
|
|
68
|
+
*/
|
|
69
|
+
export async function cmdBaemailSetup(
|
|
70
|
+
channel: string | undefined,
|
|
71
|
+
standardStr: string | undefined,
|
|
72
|
+
priorityStr?: string,
|
|
73
|
+
urgentStr?: string
|
|
74
|
+
): Promise<never> {
|
|
75
|
+
if (!channel || !standardStr) {
|
|
76
|
+
return fail('Usage: baemail-setup <channel> <standardSats> [prioritySats] [urgentSats]');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const standard = parseInt(standardStr, 10);
|
|
80
|
+
const priority = priorityStr ? parseInt(priorityStr, 10) : standard * 2;
|
|
81
|
+
const urgent = urgentStr ? parseInt(urgentStr, 10) : standard * 5;
|
|
82
|
+
|
|
83
|
+
if (isNaN(standard) || standard < 1) {
|
|
84
|
+
return fail('Standard rate must be a positive integer (sats)');
|
|
85
|
+
}
|
|
86
|
+
if (priority < standard) {
|
|
87
|
+
return fail('Priority rate must be >= standard rate');
|
|
88
|
+
}
|
|
89
|
+
if (urgent < priority) {
|
|
90
|
+
return fail('Urgent rate must be >= priority rate');
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const config: BaemailConfig = {
|
|
94
|
+
deliveryChannel: channel,
|
|
95
|
+
tiers: { standard, priority, urgent },
|
|
96
|
+
maxMessageLength: 4000,
|
|
97
|
+
blocklist: [],
|
|
98
|
+
createdAt: new Date().toISOString(),
|
|
99
|
+
updatedAt: new Date().toISOString(),
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
saveBaemailConfig(config);
|
|
103
|
+
|
|
104
|
+
return ok({
|
|
105
|
+
configured: true,
|
|
106
|
+
deliveryChannel: channel,
|
|
107
|
+
tiers: config.tiers,
|
|
108
|
+
note: `Advertise with: cli advertise baemail "Baemail" "Paid message forwarding. Pay ${standard}+ sats to reach me." ${standard}`,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* View current baemail configuration.
|
|
114
|
+
*/
|
|
115
|
+
export async function cmdBaemailConfig(): Promise<never> {
|
|
116
|
+
const config = loadBaemailConfig();
|
|
117
|
+
if (!config) {
|
|
118
|
+
return fail('Baemail not configured. Run: baemail-setup <channel> <standardSats> [prioritySats] [urgentSats]');
|
|
119
|
+
}
|
|
120
|
+
return ok(config);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Block a sender from using baemail.
|
|
125
|
+
*/
|
|
126
|
+
export async function cmdBaemailBlock(identityKey: string | undefined): Promise<never> {
|
|
127
|
+
if (!identityKey) return fail('Usage: baemail-block <identityKey>');
|
|
128
|
+
|
|
129
|
+
const config = loadBaemailConfig();
|
|
130
|
+
if (!config) {
|
|
131
|
+
return fail('Baemail not configured. Run baemail-setup first.');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (!config.blocklist) config.blocklist = [];
|
|
135
|
+
if (config.blocklist.includes(identityKey)) {
|
|
136
|
+
return fail('Identity already blocked');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
config.blocklist.push(identityKey);
|
|
140
|
+
config.updatedAt = new Date().toISOString();
|
|
141
|
+
saveBaemailConfig(config);
|
|
142
|
+
|
|
143
|
+
return ok({ blocked: identityKey, totalBlocked: config.blocklist.length });
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Unblock a sender.
|
|
148
|
+
*/
|
|
149
|
+
export async function cmdBaemailUnblock(identityKey: string | undefined): Promise<never> {
|
|
150
|
+
if (!identityKey) return fail('Usage: baemail-unblock <identityKey>');
|
|
151
|
+
|
|
152
|
+
const config = loadBaemailConfig();
|
|
153
|
+
if (!config) {
|
|
154
|
+
return fail('Baemail not configured. Run baemail-setup first.');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
if (!config.blocklist || !config.blocklist.includes(identityKey)) {
|
|
158
|
+
return fail('Identity not in blocklist');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
config.blocklist = config.blocklist.filter(k => k !== identityKey);
|
|
162
|
+
config.updatedAt = new Date().toISOString();
|
|
163
|
+
saveBaemailConfig(config);
|
|
164
|
+
|
|
165
|
+
return ok({ unblocked: identityKey, totalBlocked: config.blocklist.length });
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* View baemail delivery log.
|
|
170
|
+
*/
|
|
171
|
+
export async function cmdBaemailLog(limitStr?: string): Promise<never> {
|
|
172
|
+
const limit = parseInt(limitStr || '20', 10) || 20;
|
|
173
|
+
|
|
174
|
+
if (!fs.existsSync(PATHS.baemailLog)) {
|
|
175
|
+
return ok({ log: [], count: 0 });
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const lines = fs.readFileSync(PATHS.baemailLog, 'utf-8').split('\n').filter(l => l.trim());
|
|
179
|
+
const entries: BaemailLogEntry[] = lines.map(l => {
|
|
180
|
+
try { return JSON.parse(l); } catch { return null; }
|
|
181
|
+
}).filter(Boolean) as BaemailLogEntry[];
|
|
182
|
+
|
|
183
|
+
const recent = entries.slice(-limit).reverse();
|
|
184
|
+
return ok({ log: recent, count: entries.length, showing: recent.length });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Refund a failed baemail delivery.
|
|
189
|
+
*/
|
|
190
|
+
export async function cmdBaemailRefund(requestId: string | undefined): Promise<never> {
|
|
191
|
+
if (!requestId) return fail('Usage: baemail-refund <requestId>');
|
|
192
|
+
|
|
193
|
+
if (!fs.existsSync(PATHS.baemailLog)) {
|
|
194
|
+
return fail('No baemail log found');
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Find the entry
|
|
198
|
+
const lines = fs.readFileSync(PATHS.baemailLog, 'utf-8').split('\n').filter(l => l.trim());
|
|
199
|
+
const entries: BaemailLogEntry[] = lines.map((l, idx) => {
|
|
200
|
+
try { return { ...JSON.parse(l), _lineIdx: idx }; } catch { return null; }
|
|
201
|
+
}).filter(Boolean) as BaemailLogEntry[];
|
|
202
|
+
|
|
203
|
+
const entry = entries.find(e => e.requestId === requestId);
|
|
204
|
+
if (!entry) {
|
|
205
|
+
return fail(`Request ${requestId} not found in baemail log`);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
if (entry.deliverySuccess) {
|
|
209
|
+
return fail('This delivery was successful — no refund needed');
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (entry.refundStatus === 'completed') {
|
|
213
|
+
return fail('Refund already processed for this request');
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Load wallet and SDK
|
|
217
|
+
const { identityKey, privKey } = await loadIdentity();
|
|
218
|
+
const walletIdentityRaw = fs.readFileSync(PATHS.walletIdentity, 'utf-8');
|
|
219
|
+
const walletIdentity = JSON.parse(walletIdentityRaw);
|
|
220
|
+
|
|
221
|
+
// Dynamic import SDK
|
|
222
|
+
let sdk: any;
|
|
223
|
+
try {
|
|
224
|
+
sdk = await import('@bsv/sdk');
|
|
225
|
+
} catch {
|
|
226
|
+
return fail('Cannot load @bsv/sdk for refund transaction');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const { Transaction, P2PKH, PrivateKey, PublicKey, Hash } = sdk;
|
|
230
|
+
|
|
231
|
+
// Calculate refund amount
|
|
232
|
+
const refundSats = entry.paidSats - 1; // Keep 1 sat for tx fee
|
|
233
|
+
if (refundSats < 1) {
|
|
234
|
+
return fail('Amount too small to refund');
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// Derive refund address from sender's identity key
|
|
238
|
+
const senderPubKey = PublicKey.fromString(entry.from);
|
|
239
|
+
const refundAddress = senderPubKey.toAddress().toString();
|
|
240
|
+
|
|
241
|
+
try {
|
|
242
|
+
// Load UTXOs
|
|
243
|
+
const address = walletIdentity.address;
|
|
244
|
+
const utxosResp = await fetchWithTimeout(`https://api.whatsonchain.com/v1/bsv/main/address/${address}/unspent`);
|
|
245
|
+
const utxos = await utxosResp.json();
|
|
246
|
+
|
|
247
|
+
if (!utxos || utxos.length === 0) {
|
|
248
|
+
return fail('No UTXOs available for refund');
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// Build transaction
|
|
252
|
+
const tx = new Transaction();
|
|
253
|
+
let totalInput = 0;
|
|
254
|
+
const rootKey = PrivateKey.fromHex(walletIdentity.rootKeyHex);
|
|
255
|
+
|
|
256
|
+
for (const utxo of utxos) {
|
|
257
|
+
if (totalInput >= refundSats + 50) break;
|
|
258
|
+
tx.addInput({
|
|
259
|
+
sourceTXID: utxo.tx_hash,
|
|
260
|
+
sourceOutputIndex: utxo.tx_pos,
|
|
261
|
+
sourceSatoshis: utxo.value,
|
|
262
|
+
script: new P2PKH().lock(rootKey.toPublicKey().toAddress()).toHex(),
|
|
263
|
+
unlockingScriptTemplate: new P2PKH().unlock(rootKey),
|
|
264
|
+
});
|
|
265
|
+
totalInput += utxo.value;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
if (totalInput < refundSats + 10) {
|
|
269
|
+
return fail('Insufficient funds for refund');
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Refund output
|
|
273
|
+
tx.addOutput({
|
|
274
|
+
satoshis: refundSats,
|
|
275
|
+
lockingScript: new P2PKH().lock(refundAddress),
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Change output
|
|
279
|
+
const fee = 10;
|
|
280
|
+
const change = totalInput - refundSats - fee;
|
|
281
|
+
if (change > 1) {
|
|
282
|
+
tx.addOutput({
|
|
283
|
+
satoshis: change,
|
|
284
|
+
lockingScript: new P2PKH().lock(rootKey.toPublicKey().toAddress()),
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
await tx.sign();
|
|
289
|
+
|
|
290
|
+
// Broadcast
|
|
291
|
+
const broadcastResp = await fetchWithTimeout('https://api.whatsonchain.com/v1/bsv/main/tx/raw', {
|
|
292
|
+
method: 'POST',
|
|
293
|
+
headers: { 'Content-Type': 'application/json' },
|
|
294
|
+
body: JSON.stringify({ txhex: tx.toHex() }),
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
if (!broadcastResp.ok) {
|
|
298
|
+
const errBody = await broadcastResp.text();
|
|
299
|
+
return fail(`Broadcast failed: ${errBody}`);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
const txid = tx.id('hex');
|
|
303
|
+
|
|
304
|
+
// Update log entry
|
|
305
|
+
const updatedLines = lines.map((l, idx) => {
|
|
306
|
+
if (idx === entry._lineIdx) {
|
|
307
|
+
const updated = { ...JSON.parse(l), refundStatus: 'completed', refundTxid: txid, refundedAt: new Date().toISOString() };
|
|
308
|
+
return JSON.stringify(updated);
|
|
309
|
+
}
|
|
310
|
+
return l;
|
|
311
|
+
});
|
|
312
|
+
fs.writeFileSync(PATHS.baemailLog, updatedLines.join('\n') + '\n');
|
|
313
|
+
|
|
314
|
+
return ok({
|
|
315
|
+
refunded: true,
|
|
316
|
+
requestId,
|
|
317
|
+
refundSats,
|
|
318
|
+
refundAddress,
|
|
319
|
+
txid,
|
|
320
|
+
note: `Refunded ${refundSats} sats to sender`,
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
} catch (err) {
|
|
324
|
+
return fail(`Refund failed: ${(err as Error).message}`);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
@@ -0,0 +1,338 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baemail service handler - processes incoming paid messages.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { OVERLAY_URL, PATHS } from '../config.js';
|
|
9
|
+
import { loadBaemailConfig, BaemailLogEntry } from './commands.js';
|
|
10
|
+
import { signRelayMessage } from '../wallet/identity.js';
|
|
11
|
+
import { verifyAndAcceptPayment } from '../messaging/handlers.js';
|
|
12
|
+
import { fetchWithTimeout } from '../utils/woc.js';
|
|
13
|
+
import { ensureStateDir } from '../utils/storage.js';
|
|
14
|
+
|
|
15
|
+
// Dynamic SDK import
|
|
16
|
+
let _sdk: any = null;
|
|
17
|
+
|
|
18
|
+
async function getSdk(): Promise<any> {
|
|
19
|
+
if (_sdk) return _sdk;
|
|
20
|
+
try {
|
|
21
|
+
_sdk = await import('@bsv/sdk');
|
|
22
|
+
return _sdk;
|
|
23
|
+
} catch {
|
|
24
|
+
throw new Error('Cannot load @bsv/sdk');
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
interface BaemailInput {
|
|
29
|
+
message?: string;
|
|
30
|
+
senderName?: string;
|
|
31
|
+
replyIdentityKey?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ServiceMessage {
|
|
35
|
+
id: string;
|
|
36
|
+
from: string;
|
|
37
|
+
payload?: {
|
|
38
|
+
input?: BaemailInput;
|
|
39
|
+
payment?: any;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface ProcessResult {
|
|
44
|
+
id: string;
|
|
45
|
+
type: string;
|
|
46
|
+
serviceId: string;
|
|
47
|
+
action: string;
|
|
48
|
+
tier?: string;
|
|
49
|
+
deliverySuccess?: boolean;
|
|
50
|
+
deliveryError?: string | null | undefined;
|
|
51
|
+
paymentAccepted?: boolean;
|
|
52
|
+
paymentTxid?: string;
|
|
53
|
+
satoshisReceived?: number;
|
|
54
|
+
from: string;
|
|
55
|
+
ack: boolean;
|
|
56
|
+
reason?: string | null;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Process incoming baemail service request.
|
|
61
|
+
*/
|
|
62
|
+
export async function processBaemail(
|
|
63
|
+
msg: ServiceMessage,
|
|
64
|
+
identityKey: string,
|
|
65
|
+
privKey: any
|
|
66
|
+
): Promise<ProcessResult> {
|
|
67
|
+
const input = (msg.payload?.input || msg.payload) as BaemailInput;
|
|
68
|
+
const payment = msg.payload?.payment;
|
|
69
|
+
|
|
70
|
+
// Load config
|
|
71
|
+
const config = loadBaemailConfig();
|
|
72
|
+
if (!config) {
|
|
73
|
+
const rejectPayload = {
|
|
74
|
+
requestId: msg.id,
|
|
75
|
+
serviceId: 'baemail',
|
|
76
|
+
status: 'rejected',
|
|
77
|
+
reason: 'Baemail service not configured on this agent.',
|
|
78
|
+
};
|
|
79
|
+
const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
80
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
81
|
+
method: 'POST',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
84
|
+
});
|
|
85
|
+
return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'not configured', from: msg.from, ack: true };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check blocklist
|
|
89
|
+
if (config.blocklist?.includes(msg.from)) {
|
|
90
|
+
const rejectPayload = {
|
|
91
|
+
requestId: msg.id,
|
|
92
|
+
serviceId: 'baemail',
|
|
93
|
+
status: 'rejected',
|
|
94
|
+
reason: 'Sender is blocked.',
|
|
95
|
+
};
|
|
96
|
+
const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
97
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
98
|
+
method: 'POST',
|
|
99
|
+
headers: { 'Content-Type': 'application/json' },
|
|
100
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
101
|
+
});
|
|
102
|
+
return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'blocked', from: msg.from, ack: true };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Validate message
|
|
106
|
+
const message = input?.message;
|
|
107
|
+
if (!message || typeof message !== 'string' || message.trim().length === 0) {
|
|
108
|
+
const rejectPayload = {
|
|
109
|
+
requestId: msg.id,
|
|
110
|
+
serviceId: 'baemail',
|
|
111
|
+
status: 'rejected',
|
|
112
|
+
reason: 'Missing or empty message. Send {message: "your message"}',
|
|
113
|
+
};
|
|
114
|
+
const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
115
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
116
|
+
method: 'POST',
|
|
117
|
+
headers: { 'Content-Type': 'application/json' },
|
|
118
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
119
|
+
});
|
|
120
|
+
return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'missing message', from: msg.from, ack: true };
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (message.length > (config.maxMessageLength || 4000)) {
|
|
124
|
+
const rejectPayload = {
|
|
125
|
+
requestId: msg.id,
|
|
126
|
+
serviceId: 'baemail',
|
|
127
|
+
status: 'rejected',
|
|
128
|
+
reason: `Message too long. Max ${config.maxMessageLength || 4000} characters.`,
|
|
129
|
+
};
|
|
130
|
+
const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
131
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
132
|
+
method: 'POST',
|
|
133
|
+
headers: { 'Content-Type': 'application/json' },
|
|
134
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
135
|
+
});
|
|
136
|
+
return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'message too long', from: msg.from, ack: true };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Load wallet identity
|
|
140
|
+
const sdk = await getSdk();
|
|
141
|
+
const { PrivateKey, Hash } = sdk;
|
|
142
|
+
|
|
143
|
+
let walletIdentity: any;
|
|
144
|
+
try {
|
|
145
|
+
walletIdentity = JSON.parse(fs.readFileSync(PATHS.walletIdentity, 'utf-8'));
|
|
146
|
+
} catch (err) {
|
|
147
|
+
const rejectPayload = {
|
|
148
|
+
requestId: msg.id,
|
|
149
|
+
serviceId: 'baemail',
|
|
150
|
+
status: 'rejected',
|
|
151
|
+
reason: 'Service temporarily unavailable (wallet error)',
|
|
152
|
+
};
|
|
153
|
+
const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
154
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
155
|
+
method: 'POST',
|
|
156
|
+
headers: { 'Content-Type': 'application/json' },
|
|
157
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
158
|
+
});
|
|
159
|
+
return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'wallet error', from: msg.from, ack: true };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Sender info
|
|
163
|
+
const senderName = input?.senderName || 'Anonymous';
|
|
164
|
+
const replyKey = input?.replyIdentityKey || msg.from;
|
|
165
|
+
|
|
166
|
+
// Check hooks configured
|
|
167
|
+
let hookToken: string | null = null;
|
|
168
|
+
let hookPort = 18789;
|
|
169
|
+
const openclawConfigPath = path.join(os.homedir(), '.openclaw', 'openclaw.json');
|
|
170
|
+
|
|
171
|
+
if (fs.existsSync(openclawConfigPath)) {
|
|
172
|
+
try {
|
|
173
|
+
const openclawConfig = JSON.parse(fs.readFileSync(openclawConfigPath, 'utf-8'));
|
|
174
|
+
hookToken = openclawConfig?.hooks?.token;
|
|
175
|
+
hookPort = openclawConfig?.gateway?.port || 18789;
|
|
176
|
+
} catch {
|
|
177
|
+
// Ignore parse errors
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!hookToken) {
|
|
182
|
+
const rejectPayload = {
|
|
183
|
+
requestId: msg.id,
|
|
184
|
+
serviceId: 'baemail',
|
|
185
|
+
status: 'rejected',
|
|
186
|
+
reason: 'OpenClaw hooks not configured. Payment NOT accepted.',
|
|
187
|
+
};
|
|
188
|
+
const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
189
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
190
|
+
method: 'POST',
|
|
191
|
+
headers: { 'Content-Type': 'application/json' },
|
|
192
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
193
|
+
});
|
|
194
|
+
return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: 'hooks not configured', from: msg.from, ack: true };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
// Verify and accept payment
|
|
198
|
+
const ourHash160 = Hash.hash160(PrivateKey.fromHex(walletIdentity.rootKeyHex).toPublicKey().encode(true));
|
|
199
|
+
const minPrice = config.tiers.standard;
|
|
200
|
+
|
|
201
|
+
const payResult = await verifyAndAcceptPayment(payment, minPrice, msg.from, 'baemail', ourHash160);
|
|
202
|
+
|
|
203
|
+
if (!payResult.accepted) {
|
|
204
|
+
const rejectPayload = {
|
|
205
|
+
requestId: msg.id,
|
|
206
|
+
serviceId: 'baemail',
|
|
207
|
+
status: 'rejected',
|
|
208
|
+
reason: `Payment rejected: ${payResult.error}. Minimum: ${minPrice} sats.`,
|
|
209
|
+
};
|
|
210
|
+
const sig = signRelayMessage(privKey, msg.from, 'service-response', rejectPayload);
|
|
211
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
headers: { 'Content-Type': 'application/json' },
|
|
214
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: rejectPayload, signature: sig }),
|
|
215
|
+
});
|
|
216
|
+
return { id: msg.id, type: 'service-request', serviceId: 'baemail', action: 'rejected', reason: payResult.error, from: msg.from, ack: true };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Determine tier
|
|
220
|
+
const paidSats = payResult.satoshis;
|
|
221
|
+
let tier = 'standard';
|
|
222
|
+
let tierEmoji = '📧';
|
|
223
|
+
if (paidSats >= config.tiers.urgent) {
|
|
224
|
+
tier = 'urgent';
|
|
225
|
+
tierEmoji = '🚨';
|
|
226
|
+
} else if (paidSats >= config.tiers.priority) {
|
|
227
|
+
tier = 'priority';
|
|
228
|
+
tierEmoji = 'âš¡';
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Format message
|
|
232
|
+
const formattedMessage = `${tierEmoji} **Baemail** (${tier.toUpperCase()})
|
|
233
|
+
|
|
234
|
+
**From:** ${senderName}
|
|
235
|
+
**Paid:** ${paidSats} sats
|
|
236
|
+
**Reply to:** \`${replyKey.slice(0, 16)}...\`
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
${message}
|
|
241
|
+
|
|
242
|
+
---
|
|
243
|
+
_Reply via overlay: \`cli send ${replyKey} ping "your reply"\`_`;
|
|
244
|
+
|
|
245
|
+
// Deliver via hooks
|
|
246
|
+
let deliverySuccess = false;
|
|
247
|
+
let deliveryError: string | null = null;
|
|
248
|
+
|
|
249
|
+
try {
|
|
250
|
+
const hookHost = process.env.CLAWDBOT_HOST || process.env.OPENCLAW_HOST || '127.0.0.1';
|
|
251
|
+
const hookUrl = `http://${hookHost}:${hookPort}/hooks/agent`;
|
|
252
|
+
const hookResp = await fetchWithTimeout(hookUrl, {
|
|
253
|
+
method: 'POST',
|
|
254
|
+
headers: {
|
|
255
|
+
'Content-Type': 'application/json',
|
|
256
|
+
'Authorization': `Bearer ${hookToken}`,
|
|
257
|
+
'x-clawdbot-token': hookToken,
|
|
258
|
+
},
|
|
259
|
+
body: JSON.stringify({
|
|
260
|
+
message: formattedMessage,
|
|
261
|
+
name: 'Baemail',
|
|
262
|
+
sessionKey: `baemail:${msg.id}`,
|
|
263
|
+
wakeMode: 'now',
|
|
264
|
+
deliver: true,
|
|
265
|
+
channel: config.deliveryChannel,
|
|
266
|
+
}),
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
if (hookResp.ok) {
|
|
270
|
+
deliverySuccess = true;
|
|
271
|
+
} else {
|
|
272
|
+
const body = await hookResp.text().catch(() => '');
|
|
273
|
+
deliveryError = `Hook failed: ${hookResp.status} ${body}`;
|
|
274
|
+
}
|
|
275
|
+
} catch (err) {
|
|
276
|
+
deliveryError = (err as Error).message;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Log delivery
|
|
280
|
+
ensureStateDir();
|
|
281
|
+
const logEntry: BaemailLogEntry = {
|
|
282
|
+
requestId: msg.id,
|
|
283
|
+
from: msg.from,
|
|
284
|
+
senderName,
|
|
285
|
+
tier,
|
|
286
|
+
paidSats,
|
|
287
|
+
messageLength: message.length,
|
|
288
|
+
deliveryChannel: config.deliveryChannel,
|
|
289
|
+
deliverySuccess,
|
|
290
|
+
deliveryError: deliveryError ?? null,
|
|
291
|
+
paymentTxid: payResult.txid || '',
|
|
292
|
+
refundStatus: deliverySuccess ? null : 'pending',
|
|
293
|
+
timestamp: new Date().toISOString(),
|
|
294
|
+
};
|
|
295
|
+
fs.appendFileSync(PATHS.baemailLog, JSON.stringify(logEntry) + '\n');
|
|
296
|
+
|
|
297
|
+
// Send response
|
|
298
|
+
const responsePayload = {
|
|
299
|
+
requestId: msg.id,
|
|
300
|
+
serviceId: 'baemail',
|
|
301
|
+
status: deliverySuccess ? 'fulfilled' : 'delivery_failed',
|
|
302
|
+
result: {
|
|
303
|
+
delivered: deliverySuccess,
|
|
304
|
+
tier,
|
|
305
|
+
channel: config.deliveryChannel,
|
|
306
|
+
paidSats,
|
|
307
|
+
error: deliveryError,
|
|
308
|
+
replyTo: identityKey,
|
|
309
|
+
refundable: !deliverySuccess,
|
|
310
|
+
note: deliverySuccess ? undefined : 'Delivery failed. Run: baemail-refund ' + msg.id,
|
|
311
|
+
},
|
|
312
|
+
paymentAccepted: true,
|
|
313
|
+
paymentTxid: payResult.txid,
|
|
314
|
+
satoshisReceived: payResult.satoshis,
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
const respSig = signRelayMessage(privKey, msg.from, 'service-response', responsePayload);
|
|
318
|
+
await fetchWithTimeout(`${OVERLAY_URL}/relay/send`, {
|
|
319
|
+
method: 'POST',
|
|
320
|
+
headers: { 'Content-Type': 'application/json' },
|
|
321
|
+
body: JSON.stringify({ from: identityKey, to: msg.from, type: 'service-response', payload: responsePayload, signature: respSig }),
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
id: msg.id,
|
|
326
|
+
type: 'service-request',
|
|
327
|
+
serviceId: 'baemail',
|
|
328
|
+
action: deliverySuccess ? 'fulfilled' : 'delivery_failed',
|
|
329
|
+
tier,
|
|
330
|
+
deliverySuccess,
|
|
331
|
+
deliveryError: deliveryError === null ? undefined : deliveryError,
|
|
332
|
+
paymentAccepted: true,
|
|
333
|
+
paymentTxid: payResult.txid || undefined,
|
|
334
|
+
satoshisReceived: payResult.satoshis,
|
|
335
|
+
from: msg.from,
|
|
336
|
+
ack: true,
|
|
337
|
+
};
|
|
338
|
+
}
|