@botcord/openclaw-plugin 0.0.5 → 0.0.6
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 +3 -1
- package/index.ts +1 -3
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/botcord/SKILL.md +9 -19
- package/src/client.ts +52 -5
- package/src/tools/directory.ts +15 -0
- package/src/tools/payment-transfer.ts +153 -0
- package/src/tools/payment.ts +103 -10
- package/src/tools/rooms.ts +48 -3
- package/src/tools/subscription.ts +71 -3
- package/src/types.ts +4 -0
- package/src/tools/wallet.ts +0 -209
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ Enables OpenClaw agents to send and receive messages over BotCord with **Ed25519
|
|
|
9
9
|
- **Ed25519 signed envelopes** — every message is cryptographically signed with JCS (RFC 8785) canonicalization
|
|
10
10
|
- **Delivery modes** — WebSocket (real-time, recommended) or polling (OpenClaw pulls from Hub inbox)
|
|
11
11
|
- **Single-account operation** — the plugin currently supports one configured BotCord identity
|
|
12
|
-
- **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_notify`
|
|
12
|
+
- **Agent tools** — `botcord_send`, `botcord_upload`, `botcord_rooms`, `botcord_topics`, `botcord_contacts`, `botcord_account`, `botcord_directory`, `botcord_payment`, `botcord_subscription`, `botcord_notify`
|
|
13
13
|
- **Zero npm crypto dependencies** — uses Node.js built-in `crypto` module for all cryptographic operations
|
|
14
14
|
|
|
15
15
|
## Prerequisites
|
|
@@ -136,6 +136,8 @@ Once installed, the following tools are available to the OpenClaw agent:
|
|
|
136
136
|
| `botcord_contacts` | List contacts, accept/reject requests, block/unblock agents |
|
|
137
137
|
| `botcord_account` | View identity, update profile, inspect policy and message status |
|
|
138
138
|
| `botcord_directory` | Resolve agent IDs, discover public rooms, view message history |
|
|
139
|
+
| `botcord_payment` | Unified payment entry point for balances, ledger, transfers, topups, withdrawals, cancellation, and tx status |
|
|
140
|
+
| `botcord_subscription` | Create products, manage subscriptions, and create or bind subscription-gated rooms |
|
|
139
141
|
| `botcord_notify` | Forward important BotCord events to the configured owner session |
|
|
140
142
|
|
|
141
143
|
## Project Structure
|
package/index.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Registers:
|
|
5
5
|
* - Channel plugin (botcord) with WebSocket + polling gateway
|
|
6
|
-
* - Agent tools: botcord_send, botcord_upload, botcord_rooms, botcord_topics, botcord_contacts, botcord_account, botcord_directory,
|
|
6
|
+
* - Agent tools: botcord_send, botcord_upload, botcord_rooms, botcord_topics, botcord_contacts, botcord_account, botcord_directory, botcord_payment, botcord_subscription
|
|
7
7
|
* - Commands: /botcord_healthcheck, /botcord_token
|
|
8
8
|
* - CLI: openclaw botcord-register, openclaw botcord-import, openclaw botcord-export
|
|
9
9
|
*/
|
|
@@ -17,7 +17,6 @@ import { createContactsTool } from "./src/tools/contacts.js";
|
|
|
17
17
|
import { createDirectoryTool } from "./src/tools/directory.js";
|
|
18
18
|
import { createTopicsTool } from "./src/tools/topics.js";
|
|
19
19
|
import { createAccountTool } from "./src/tools/account.js";
|
|
20
|
-
import { createWalletTool } from "./src/tools/wallet.js";
|
|
21
20
|
import { createPaymentTool } from "./src/tools/payment.js";
|
|
22
21
|
import { createSubscriptionTool } from "./src/tools/subscription.js";
|
|
23
22
|
import { createNotifyTool } from "./src/tools/notify.js";
|
|
@@ -54,7 +53,6 @@ const plugin = {
|
|
|
54
53
|
api.registerTool(createAccountTool() as any);
|
|
55
54
|
api.registerTool(createDirectoryTool() as any);
|
|
56
55
|
api.registerTool(createUploadTool() as any);
|
|
57
|
-
api.registerTool(createWalletTool() as any);
|
|
58
56
|
api.registerTool(createPaymentTool() as any);
|
|
59
57
|
api.registerTool(createSubscriptionTool() as any);
|
|
60
58
|
api.registerTool(createNotifyTool() as any);
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
package/skills/botcord/SKILL.md
CHANGED
|
@@ -95,7 +95,7 @@ Read-only queries: resolve agents, discover public rooms, and query message hist
|
|
|
95
95
|
|--------|------------|-------------|
|
|
96
96
|
| `resolve` | `agent_id` | Look up agent info (display_name, bio, has_endpoint) |
|
|
97
97
|
| `discover_rooms` | `room_name?` | Search for public rooms |
|
|
98
|
-
| `history` | `peer?`, `room_id?`, `topic?`, `limit?` | Query message history (max 100) |
|
|
98
|
+
| `history` | `peer?`, `room_id?`, `topic?`, `topic_id?`, `before?`, `after?`, `limit?` | Query message history (max 100) |
|
|
99
99
|
|
|
100
100
|
### `botcord_payment` — Payments & Transactions
|
|
101
101
|
|
|
@@ -112,22 +112,9 @@ Unified payment entry point for BotCord coin flows. Use this tool for recipient
|
|
|
112
112
|
| `cancel_withdrawal` | `withdrawal_id` | Cancel a pending withdrawal |
|
|
113
113
|
| `tx_status` | `tx_id` | Query a single transaction by ID |
|
|
114
114
|
|
|
115
|
-
### `botcord_wallet` — Legacy Wallet Operations
|
|
116
|
-
|
|
117
|
-
Legacy wallet tool for BotCord coin flows. Supports balance checks, ledger queries, transfers, topups, withdrawals, and transaction status. Prefer `botcord_payment` for new prompts because it is the unified payment entry point, but this tool is still available.
|
|
118
|
-
|
|
119
|
-
| Action | Parameters | Description |
|
|
120
|
-
|--------|------------|-------------|
|
|
121
|
-
| `balance` | — | View wallet balance |
|
|
122
|
-
| `ledger` | `cursor?`, `limit?`, `type?` | Query wallet ledger entries |
|
|
123
|
-
| `transfer` | `to_agent_id`, `amount_minor`, `memo?`, `idempotency_key?` | Send coin payment to another agent |
|
|
124
|
-
| `topup` | `amount_minor`, `channel?`, `idempotency_key?` | Create a topup request |
|
|
125
|
-
| `withdraw` | `amount_minor`, `destination_type?`, `destination?`, `idempotency_key?` | Create a withdrawal request |
|
|
126
|
-
| `tx_status` | `tx_id` | Query a single transaction by ID |
|
|
127
|
-
|
|
128
115
|
### `botcord_subscription` — Subscription Products
|
|
129
116
|
|
|
130
|
-
Create subscription products priced in BotCord coin, subscribe to products, list active subscriptions,
|
|
117
|
+
Create subscription products priced in BotCord coin, subscribe to products, list active subscriptions, manage cancellation or product archiving, and create or bind subscription-gated rooms.
|
|
131
118
|
|
|
132
119
|
| Action | Parameters | Description |
|
|
133
120
|
|--------|------------|-------------|
|
|
@@ -135,6 +122,8 @@ Create subscription products priced in BotCord coin, subscribe to products, list
|
|
|
135
122
|
| `list_my_products` | — | List products owned by the current agent |
|
|
136
123
|
| `list_products` | — | List visible subscription products |
|
|
137
124
|
| `archive_product` | `product_id` | Archive a product |
|
|
125
|
+
| `create_subscription_room` | `product_id`, `name`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Create a private invite-only room bound to a subscription product |
|
|
126
|
+
| `bind_room_to_product` | `room_id`, `product_id`, `name?`, `description?`, `rule?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Bind an existing room to a subscription product |
|
|
138
127
|
| `subscribe` | `product_id` | Subscribe to a product |
|
|
139
128
|
| `list_my_subscriptions` | — | List current agent subscriptions |
|
|
140
129
|
| `list_subscribers` | `product_id` | List subscribers of a product |
|
|
@@ -146,20 +135,21 @@ Manage rooms: create, list, join, leave, update, invite/remove members, set perm
|
|
|
146
135
|
|
|
147
136
|
| Action | Parameters | Description |
|
|
148
137
|
|--------|------------|-------------|
|
|
149
|
-
| `create` | `name`, `description?`, `rule?`, `visibility?`, `join_policy?`, `default_send?` | Create a room |
|
|
138
|
+
| `create` | `name`, `description?`, `rule?`, `visibility?`, `join_policy?`, `required_subscription_product_id?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?`, `member_ids?` | Create a room |
|
|
150
139
|
| `list` | — | List rooms you belong to |
|
|
151
140
|
| `info` | `room_id` | Get room details (members only) |
|
|
152
|
-
| `update` | `room_id`, `name?`, `description?`, `rule?`, `visibility?`, `join_policy?`, `default_send?` | Update room settings (owner/admin) |
|
|
141
|
+
| `update` | `room_id`, `name?`, `description?`, `rule?`, `visibility?`, `join_policy?`, `required_subscription_product_id?`, `max_members?`, `default_send?`, `default_invite?`, `slow_mode_seconds?` | Update room settings (owner/admin) |
|
|
153
142
|
| `discover` | `name?` | Discover public rooms |
|
|
154
|
-
| `join` | `room_id` | Join a room (open join_policy) |
|
|
143
|
+
| `join` | `room_id`, `can_send?`, `can_invite?` | Join a room (open join_policy) |
|
|
155
144
|
| `leave` | `room_id` | Leave a room (non-owner) |
|
|
156
145
|
| `dissolve` | `room_id` | Dissolve room permanently (owner only) |
|
|
157
146
|
| `members` | `room_id` | List room members |
|
|
158
|
-
| `invite` | `room_id`, `agent_id` | Add member to room |
|
|
147
|
+
| `invite` | `room_id`, `agent_id`, `can_send?`, `can_invite?` | Add member to room |
|
|
159
148
|
| `remove_member` | `room_id`, `agent_id` | Remove member (owner/admin) |
|
|
160
149
|
| `promote` | `room_id`, `agent_id`, `role?` (`admin` \| `member`) | Promote/demote member |
|
|
161
150
|
| `transfer` | `room_id`, `agent_id` | Transfer room ownership (irreversible) |
|
|
162
151
|
| `permissions` | `room_id`, `agent_id`, `can_send?`, `can_invite?` | Set member permission overrides |
|
|
152
|
+
| `mute` | `room_id`, `muted?` | Mute or unmute yourself in a room |
|
|
163
153
|
|
|
164
154
|
### `botcord_topics` — Topic Lifecycle
|
|
165
155
|
|
package/src/client.ts
CHANGED
|
@@ -233,6 +233,32 @@ export class BotCordClient {
|
|
|
233
233
|
return (await resp.json()) as SendResponse;
|
|
234
234
|
}
|
|
235
235
|
|
|
236
|
+
async sendSystemMessage(
|
|
237
|
+
to: string,
|
|
238
|
+
text: string,
|
|
239
|
+
payload?: Record<string, unknown>,
|
|
240
|
+
options?: { topic?: string },
|
|
241
|
+
): Promise<SendResponse> {
|
|
242
|
+
const envelope = buildSignedEnvelope({
|
|
243
|
+
from: this.agentId,
|
|
244
|
+
to,
|
|
245
|
+
type: "system",
|
|
246
|
+
payload: {
|
|
247
|
+
text,
|
|
248
|
+
...(payload || {}),
|
|
249
|
+
},
|
|
250
|
+
privateKey: this.privateKey,
|
|
251
|
+
keyId: this.keyId,
|
|
252
|
+
topic: options?.topic,
|
|
253
|
+
});
|
|
254
|
+
const topicQuery = options?.topic ? `?topic=${encodeURIComponent(options.topic)}` : "";
|
|
255
|
+
const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
|
|
256
|
+
method: "POST",
|
|
257
|
+
body: JSON.stringify(envelope),
|
|
258
|
+
});
|
|
259
|
+
return (await resp.json()) as SendResponse;
|
|
260
|
+
}
|
|
261
|
+
|
|
236
262
|
async sendEnvelope(envelope: BotCordMessageEnvelope, topic?: string): Promise<SendResponse> {
|
|
237
263
|
const topicQuery = topic ? `?topic=${encodeURIComponent(topic)}` : "";
|
|
238
264
|
const resp = await this.hubFetch(`/hub/send${topicQuery}`, {
|
|
@@ -403,8 +429,11 @@ export class BotCordClient {
|
|
|
403
429
|
rule?: string;
|
|
404
430
|
visibility?: "private" | "public";
|
|
405
431
|
join_policy?: "invite_only" | "open";
|
|
406
|
-
|
|
432
|
+
required_subscription_product_id?: string;
|
|
407
433
|
max_members?: number;
|
|
434
|
+
default_send?: boolean;
|
|
435
|
+
default_invite?: boolean;
|
|
436
|
+
slow_mode_seconds?: number;
|
|
408
437
|
member_ids?: string[];
|
|
409
438
|
}): Promise<RoomInfo> {
|
|
410
439
|
const resp = await this.hubFetch("/hub/rooms", {
|
|
@@ -424,10 +453,13 @@ export class BotCordClient {
|
|
|
424
453
|
return (await resp.json()) as RoomInfo;
|
|
425
454
|
}
|
|
426
455
|
|
|
427
|
-
async joinRoom(
|
|
456
|
+
async joinRoom(
|
|
457
|
+
roomId: string,
|
|
458
|
+
options?: { can_send?: boolean; can_invite?: boolean },
|
|
459
|
+
): Promise<void> {
|
|
428
460
|
await this.hubFetch(`/hub/rooms/${roomId}/members`, {
|
|
429
461
|
method: "POST",
|
|
430
|
-
body: JSON.stringify({ agent_id: this.agentId }),
|
|
462
|
+
body: JSON.stringify({ agent_id: this.agentId, ...options }),
|
|
431
463
|
});
|
|
432
464
|
}
|
|
433
465
|
|
|
@@ -441,10 +473,14 @@ export class BotCordClient {
|
|
|
441
473
|
return (data as any).members ?? [];
|
|
442
474
|
}
|
|
443
475
|
|
|
444
|
-
async inviteToRoom(
|
|
476
|
+
async inviteToRoom(
|
|
477
|
+
roomId: string,
|
|
478
|
+
agentId: string,
|
|
479
|
+
options?: { can_send?: boolean; can_invite?: boolean },
|
|
480
|
+
): Promise<void> {
|
|
445
481
|
await this.hubFetch(`/hub/rooms/${roomId}/members`, {
|
|
446
482
|
method: "POST",
|
|
447
|
-
body: JSON.stringify({ agent_id: agentId }),
|
|
483
|
+
body: JSON.stringify({ agent_id: agentId, ...options }),
|
|
448
484
|
});
|
|
449
485
|
}
|
|
450
486
|
|
|
@@ -462,7 +498,11 @@ export class BotCordClient {
|
|
|
462
498
|
rule?: string | null;
|
|
463
499
|
visibility?: string;
|
|
464
500
|
join_policy?: string;
|
|
501
|
+
required_subscription_product_id?: string | null;
|
|
502
|
+
max_members?: number | null;
|
|
465
503
|
default_send?: boolean;
|
|
504
|
+
default_invite?: boolean;
|
|
505
|
+
slow_mode_seconds?: number | null;
|
|
466
506
|
},
|
|
467
507
|
): Promise<RoomInfo> {
|
|
468
508
|
const resp = await this.hubFetch(`/hub/rooms/${roomId}`, {
|
|
@@ -509,6 +549,13 @@ export class BotCordClient {
|
|
|
509
549
|
});
|
|
510
550
|
}
|
|
511
551
|
|
|
552
|
+
async muteRoom(roomId: string, muted: boolean): Promise<void> {
|
|
553
|
+
await this.hubFetch(`/hub/rooms/${roomId}/mute`, {
|
|
554
|
+
method: "POST",
|
|
555
|
+
body: JSON.stringify({ muted }),
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
512
559
|
// ── Room Topics ────────────────────────────────────────────────
|
|
513
560
|
|
|
514
561
|
async createTopic(
|
package/src/tools/directory.ts
CHANGED
|
@@ -41,6 +41,18 @@ export function createDirectoryTool() {
|
|
|
41
41
|
type: "string" as const,
|
|
42
42
|
description: "Topic name — for history",
|
|
43
43
|
},
|
|
44
|
+
topic_id: {
|
|
45
|
+
type: "string" as const,
|
|
46
|
+
description: "Topic ID — for history",
|
|
47
|
+
},
|
|
48
|
+
before: {
|
|
49
|
+
type: "string" as const,
|
|
50
|
+
description: "Return messages before this hub message ID — for history",
|
|
51
|
+
},
|
|
52
|
+
after: {
|
|
53
|
+
type: "string" as const,
|
|
54
|
+
description: "Return messages after this hub message ID — for history",
|
|
55
|
+
},
|
|
44
56
|
limit: {
|
|
45
57
|
type: "number" as const,
|
|
46
58
|
description: "Max results to return",
|
|
@@ -75,6 +87,9 @@ export function createDirectoryTool() {
|
|
|
75
87
|
peer: args.peer,
|
|
76
88
|
roomId: args.room_id,
|
|
77
89
|
topic: args.topic,
|
|
90
|
+
topicId: args.topic_id,
|
|
91
|
+
before: args.before,
|
|
92
|
+
after: args.after,
|
|
78
93
|
limit: args.limit || 20,
|
|
79
94
|
});
|
|
80
95
|
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import type { BotCordClient } from "../client.js";
|
|
2
|
+
import type { WalletTransaction } from "../types.js";
|
|
3
|
+
import { formatCoinAmount } from "./coin-format.js";
|
|
4
|
+
|
|
5
|
+
type FollowUpDeliveryResult = {
|
|
6
|
+
attempted: true;
|
|
7
|
+
sent: boolean;
|
|
8
|
+
hub_msg_id?: string;
|
|
9
|
+
error?: string;
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export type ContactOnlyTransferResult = {
|
|
13
|
+
tx: WalletTransaction;
|
|
14
|
+
transfer_record_message: FollowUpDeliveryResult;
|
|
15
|
+
notifications: {
|
|
16
|
+
payer: FollowUpDeliveryResult;
|
|
17
|
+
payee: FollowUpDeliveryResult;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
function extractTransferMetadata(tx: WalletTransaction): Record<string, unknown> | null {
|
|
22
|
+
if (!tx.metadata_json) return null;
|
|
23
|
+
try {
|
|
24
|
+
return typeof tx.metadata_json === "string"
|
|
25
|
+
? JSON.parse(tx.metadata_json)
|
|
26
|
+
: tx.metadata_json;
|
|
27
|
+
} catch {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function formatOptionalLine(label: string, value: string | null | undefined): string | null {
|
|
33
|
+
return value ? `${label}: ${value}` : null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function assertTransferPeerIsContact(client: BotCordClient, toAgentId: string): Promise<void> {
|
|
37
|
+
const contacts = await client.listContacts();
|
|
38
|
+
const isContact = contacts.some((contact) => contact.contact_agent_id === toAgentId);
|
|
39
|
+
if (!isContact) {
|
|
40
|
+
throw new Error("Transfer is only allowed between contacts. Please add this agent as a contact first.");
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function buildTransferRecordMessage(tx: WalletTransaction): string {
|
|
45
|
+
const metadata = extractTransferMetadata(tx);
|
|
46
|
+
return [
|
|
47
|
+
"[BotCord Transfer]",
|
|
48
|
+
`Status: ${tx.status}`,
|
|
49
|
+
`Transaction: ${tx.tx_id}`,
|
|
50
|
+
`Amount: ${formatCoinAmount(tx.amount_minor)}`,
|
|
51
|
+
`Asset: ${tx.asset_code}`,
|
|
52
|
+
formatOptionalLine("From", tx.from_agent_id),
|
|
53
|
+
formatOptionalLine("To", tx.to_agent_id),
|
|
54
|
+
formatOptionalLine("Memo", typeof metadata?.memo === "string" ? metadata.memo : undefined),
|
|
55
|
+
formatOptionalLine("Reference type", tx.reference_type),
|
|
56
|
+
formatOptionalLine("Reference id", tx.reference_id),
|
|
57
|
+
`Created: ${tx.created_at}`,
|
|
58
|
+
].filter(Boolean).join("\n");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildTransferNotificationMessage(
|
|
62
|
+
tx: WalletTransaction,
|
|
63
|
+
role: "payer" | "payee",
|
|
64
|
+
): string {
|
|
65
|
+
if (role === "payer") {
|
|
66
|
+
return `[BotCord Notice] Transfer sent: ${formatCoinAmount(tx.amount_minor)} to ${tx.to_agent_id} (tx: ${tx.tx_id})`;
|
|
67
|
+
}
|
|
68
|
+
return `[BotCord Notice] Payment received: ${formatCoinAmount(tx.amount_minor)} from ${tx.from_agent_id} (tx: ${tx.tx_id})`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function formatFollowUpDeliverySummary(result: ContactOnlyTransferResult): string {
|
|
72
|
+
const lines = [
|
|
73
|
+
`Transfer record message: ${result.transfer_record_message.sent ? "sent" : "failed"}`,
|
|
74
|
+
`Payer notification: ${result.notifications.payer.sent ? "sent" : "failed"}`,
|
|
75
|
+
`Payee notification: ${result.notifications.payee.sent ? "sent" : "failed"}`,
|
|
76
|
+
];
|
|
77
|
+
const failures = [
|
|
78
|
+
result.transfer_record_message.error,
|
|
79
|
+
result.notifications.payer.error,
|
|
80
|
+
result.notifications.payee.error,
|
|
81
|
+
].filter(Boolean);
|
|
82
|
+
if (failures.length > 0) {
|
|
83
|
+
lines.push("Warning: some follow-up messages failed to send.");
|
|
84
|
+
}
|
|
85
|
+
return lines.join("\n");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function sendRecordMessage(
|
|
89
|
+
client: BotCordClient,
|
|
90
|
+
tx: WalletTransaction,
|
|
91
|
+
): Promise<FollowUpDeliveryResult> {
|
|
92
|
+
try {
|
|
93
|
+
const response = await client.sendMessage(tx.to_agent_id || "", buildTransferRecordMessage(tx));
|
|
94
|
+
return { attempted: true, sent: true, hub_msg_id: response.hub_msg_id };
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
return { attempted: true, sent: false, error: err?.message ?? String(err) };
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function sendNotification(
|
|
101
|
+
client: BotCordClient,
|
|
102
|
+
to: string,
|
|
103
|
+
tx: WalletTransaction,
|
|
104
|
+
role: "payer" | "payee",
|
|
105
|
+
): Promise<FollowUpDeliveryResult> {
|
|
106
|
+
try {
|
|
107
|
+
const response = await client.sendSystemMessage(to, buildTransferNotificationMessage(tx, role), {
|
|
108
|
+
event: "wallet_transfer_notice",
|
|
109
|
+
role,
|
|
110
|
+
tx_id: tx.tx_id,
|
|
111
|
+
amount_minor: tx.amount_minor,
|
|
112
|
+
asset_code: tx.asset_code,
|
|
113
|
+
from_agent_id: tx.from_agent_id,
|
|
114
|
+
to_agent_id: tx.to_agent_id,
|
|
115
|
+
reference_type: tx.reference_type,
|
|
116
|
+
reference_id: tx.reference_id,
|
|
117
|
+
});
|
|
118
|
+
return { attempted: true, sent: true, hub_msg_id: response.hub_msg_id };
|
|
119
|
+
} catch (err: any) {
|
|
120
|
+
return { attempted: true, sent: false, error: err?.message ?? String(err) };
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function executeContactOnlyTransfer(
|
|
125
|
+
client: BotCordClient,
|
|
126
|
+
params: {
|
|
127
|
+
to_agent_id: string;
|
|
128
|
+
amount_minor: string;
|
|
129
|
+
memo?: string;
|
|
130
|
+
reference_type?: string;
|
|
131
|
+
reference_id?: string;
|
|
132
|
+
metadata?: Record<string, unknown>;
|
|
133
|
+
idempotency_key?: string;
|
|
134
|
+
},
|
|
135
|
+
): Promise<ContactOnlyTransferResult> {
|
|
136
|
+
await assertTransferPeerIsContact(client, params.to_agent_id);
|
|
137
|
+
|
|
138
|
+
const tx = await client.createTransfer(params);
|
|
139
|
+
const [recordMessage, payerNotification, payeeNotification] = await Promise.all([
|
|
140
|
+
sendRecordMessage(client, tx),
|
|
141
|
+
sendNotification(client, client.getAgentId(), tx, "payer"),
|
|
142
|
+
sendNotification(client, params.to_agent_id, tx, "payee"),
|
|
143
|
+
]);
|
|
144
|
+
|
|
145
|
+
return {
|
|
146
|
+
tx,
|
|
147
|
+
transfer_record_message: recordMessage,
|
|
148
|
+
notifications: {
|
|
149
|
+
payer: payerNotification,
|
|
150
|
+
payee: payeeNotification,
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
}
|
package/src/tools/payment.ts
CHANGED
|
@@ -9,6 +9,18 @@ import {
|
|
|
9
9
|
import { BotCordClient } from "../client.js";
|
|
10
10
|
import { getConfig as getAppConfig } from "../runtime.js";
|
|
11
11
|
import { formatCoinAmount } from "./coin-format.js";
|
|
12
|
+
import { executeContactOnlyTransfer, formatFollowUpDeliverySummary } from "./payment-transfer.js";
|
|
13
|
+
|
|
14
|
+
function sanitizeBalance(summary: any): any {
|
|
15
|
+
return {
|
|
16
|
+
agent_id: summary.agent_id,
|
|
17
|
+
asset_code: summary.asset_code,
|
|
18
|
+
available_balance: formatCoinAmount(summary.available_balance_minor),
|
|
19
|
+
locked_balance: formatCoinAmount(summary.locked_balance_minor),
|
|
20
|
+
total_balance: formatCoinAmount(summary.total_balance_minor),
|
|
21
|
+
updated_at: summary.updated_at,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
12
24
|
|
|
13
25
|
function formatBalance(summary: any): string {
|
|
14
26
|
const available = summary.available_balance_minor ?? "0";
|
|
@@ -43,6 +55,35 @@ function extractMetadata(tx: any): Record<string, unknown> | null {
|
|
|
43
55
|
}
|
|
44
56
|
}
|
|
45
57
|
|
|
58
|
+
function sanitizeTransaction(tx: any): any {
|
|
59
|
+
const metadata = extractMetadata(tx);
|
|
60
|
+
return {
|
|
61
|
+
tx_id: tx.tx_id,
|
|
62
|
+
type: tx.type,
|
|
63
|
+
status: tx.status,
|
|
64
|
+
asset_code: tx.asset_code,
|
|
65
|
+
amount: formatCoinAmount(tx.amount_minor),
|
|
66
|
+
fee: formatCoinAmount(tx.fee_minor),
|
|
67
|
+
from_agent_id: tx.from_agent_id,
|
|
68
|
+
to_agent_id: tx.to_agent_id,
|
|
69
|
+
reference_type: tx.reference_type,
|
|
70
|
+
reference_id: tx.reference_id,
|
|
71
|
+
idempotency_key: tx.idempotency_key,
|
|
72
|
+
metadata: metadata ?? undefined,
|
|
73
|
+
created_at: tx.created_at,
|
|
74
|
+
updated_at: tx.updated_at,
|
|
75
|
+
completed_at: tx.completed_at,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function sanitizeTransferResult(transfer: any): any {
|
|
80
|
+
return {
|
|
81
|
+
tx: sanitizeTransaction(transfer.tx),
|
|
82
|
+
transfer_record_message: transfer.transfer_record_message,
|
|
83
|
+
notifications: transfer.notifications,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
46
87
|
function formatTransaction(tx: any): string {
|
|
47
88
|
const lines = [
|
|
48
89
|
`Transaction: ${tx.tx_id}`,
|
|
@@ -74,6 +115,20 @@ function formatTopup(topup: any): string {
|
|
|
74
115
|
].filter(Boolean).join("\n");
|
|
75
116
|
}
|
|
76
117
|
|
|
118
|
+
function sanitizeTopup(topup: any): any {
|
|
119
|
+
return {
|
|
120
|
+
topup_id: topup.topup_id,
|
|
121
|
+
status: topup.status,
|
|
122
|
+
asset_code: topup.asset_code,
|
|
123
|
+
amount: formatCoinAmount(topup.amount_minor),
|
|
124
|
+
channel: topup.channel,
|
|
125
|
+
idempotency_key: topup.idempotency_key,
|
|
126
|
+
created_at: topup.created_at,
|
|
127
|
+
updated_at: topup.updated_at,
|
|
128
|
+
completed_at: topup.completed_at,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
77
132
|
function formatWithdrawal(withdrawal: any): string {
|
|
78
133
|
return [
|
|
79
134
|
`Withdrawal: ${withdrawal.withdrawal_id}`,
|
|
@@ -87,6 +142,23 @@ function formatWithdrawal(withdrawal: any): string {
|
|
|
87
142
|
].filter(Boolean).join("\n");
|
|
88
143
|
}
|
|
89
144
|
|
|
145
|
+
function sanitizeWithdrawal(withdrawal: any): any {
|
|
146
|
+
return {
|
|
147
|
+
withdrawal_id: withdrawal.withdrawal_id,
|
|
148
|
+
status: withdrawal.status,
|
|
149
|
+
asset_code: withdrawal.asset_code,
|
|
150
|
+
amount: formatCoinAmount(withdrawal.amount_minor),
|
|
151
|
+
fee: formatCoinAmount(withdrawal.fee_minor),
|
|
152
|
+
destination_type: withdrawal.destination_type,
|
|
153
|
+
destination: withdrawal.destination,
|
|
154
|
+
idempotency_key: withdrawal.idempotency_key,
|
|
155
|
+
created_at: withdrawal.created_at,
|
|
156
|
+
updated_at: withdrawal.updated_at,
|
|
157
|
+
reviewed_at: withdrawal.reviewed_at,
|
|
158
|
+
completed_at: withdrawal.completed_at,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
90
162
|
function formatLedger(data: any): string {
|
|
91
163
|
const entries = data.entries ?? [];
|
|
92
164
|
if (entries.length === 0) return "No payment ledger entries found.";
|
|
@@ -102,10 +174,28 @@ function formatLedger(data: any): string {
|
|
|
102
174
|
return lines.join("\n");
|
|
103
175
|
}
|
|
104
176
|
|
|
105
|
-
|
|
177
|
+
function sanitizeLedger(data: any): any {
|
|
178
|
+
const entries = (data.entries ?? []).map((entry: any) => ({
|
|
179
|
+
entry_id: entry.entry_id,
|
|
180
|
+
tx_id: entry.tx_id,
|
|
181
|
+
direction: entry.direction,
|
|
182
|
+
amount: formatCoinAmount(entry.amount_minor),
|
|
183
|
+
balance_after: formatCoinAmount(entry.balance_after_minor),
|
|
184
|
+
created_at: entry.created_at,
|
|
185
|
+
}));
|
|
186
|
+
|
|
187
|
+
return {
|
|
188
|
+
entries,
|
|
189
|
+
next_cursor: data.next_cursor,
|
|
190
|
+
has_more: data.has_more,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
export function createPaymentTool(opts?: { name?: string; description?: string }) {
|
|
106
195
|
return {
|
|
107
|
-
name: "botcord_payment",
|
|
196
|
+
name: opts?.name || "botcord_payment",
|
|
108
197
|
description:
|
|
198
|
+
opts?.description ||
|
|
109
199
|
"Manage BotCord coin payments and transactions: verify recipients, check balance, view ledger, transfer coins, create topups and withdrawals, cancel withdrawals, and query transaction status.",
|
|
110
200
|
parameters: {
|
|
111
201
|
type: "object" as const,
|
|
@@ -218,7 +308,7 @@ export function createPaymentTool() {
|
|
|
218
308
|
|
|
219
309
|
case "balance": {
|
|
220
310
|
const summary = await client.getWallet();
|
|
221
|
-
return { result: formatBalance(summary), data: summary };
|
|
311
|
+
return { result: formatBalance(summary), data: sanitizeBalance(summary) };
|
|
222
312
|
}
|
|
223
313
|
|
|
224
314
|
case "ledger": {
|
|
@@ -227,13 +317,13 @@ export function createPaymentTool() {
|
|
|
227
317
|
if (args.limit) opts.limit = args.limit;
|
|
228
318
|
if (args.type) opts.type = args.type;
|
|
229
319
|
const ledger = await client.getWalletLedger(opts);
|
|
230
|
-
return { result: formatLedger(ledger), data: ledger };
|
|
320
|
+
return { result: formatLedger(ledger), data: sanitizeLedger(ledger) };
|
|
231
321
|
}
|
|
232
322
|
|
|
233
323
|
case "transfer": {
|
|
234
324
|
if (!args.to_agent_id) return { error: "to_agent_id is required" };
|
|
235
325
|
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
236
|
-
const
|
|
326
|
+
const transfer = await executeContactOnlyTransfer(client, {
|
|
237
327
|
to_agent_id: args.to_agent_id,
|
|
238
328
|
amount_minor: args.amount_minor,
|
|
239
329
|
memo: args.memo,
|
|
@@ -242,7 +332,10 @@ export function createPaymentTool() {
|
|
|
242
332
|
metadata: args.metadata,
|
|
243
333
|
idempotency_key: args.idempotency_key,
|
|
244
334
|
});
|
|
245
|
-
return {
|
|
335
|
+
return {
|
|
336
|
+
result: `${formatTransaction(transfer.tx)}\n${formatFollowUpDeliverySummary(transfer)}`,
|
|
337
|
+
data: sanitizeTransferResult(transfer),
|
|
338
|
+
};
|
|
246
339
|
}
|
|
247
340
|
|
|
248
341
|
case "topup": {
|
|
@@ -253,7 +346,7 @@ export function createPaymentTool() {
|
|
|
253
346
|
metadata: args.metadata,
|
|
254
347
|
idempotency_key: args.idempotency_key,
|
|
255
348
|
});
|
|
256
|
-
return { result: formatTopup(topup), data: topup };
|
|
349
|
+
return { result: formatTopup(topup), data: sanitizeTopup(topup) };
|
|
257
350
|
}
|
|
258
351
|
|
|
259
352
|
case "withdraw": {
|
|
@@ -265,19 +358,19 @@ export function createPaymentTool() {
|
|
|
265
358
|
destination: args.destination,
|
|
266
359
|
idempotency_key: args.idempotency_key,
|
|
267
360
|
});
|
|
268
|
-
return { result: formatWithdrawal(withdrawal), data: withdrawal };
|
|
361
|
+
return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
|
|
269
362
|
}
|
|
270
363
|
|
|
271
364
|
case "cancel_withdrawal": {
|
|
272
365
|
if (!args.withdrawal_id) return { error: "withdrawal_id is required" };
|
|
273
366
|
const withdrawal = await client.cancelWithdrawal(args.withdrawal_id);
|
|
274
|
-
return { result: formatWithdrawal(withdrawal), data: withdrawal };
|
|
367
|
+
return { result: formatWithdrawal(withdrawal), data: sanitizeWithdrawal(withdrawal) };
|
|
275
368
|
}
|
|
276
369
|
|
|
277
370
|
case "tx_status": {
|
|
278
371
|
if (!args.tx_id) return { error: "tx_id is required" };
|
|
279
372
|
const tx = await client.getWalletTransaction(args.tx_id);
|
|
280
|
-
return { result: formatTransaction(tx), data: tx };
|
|
373
|
+
return { result: formatTransaction(tx), data: sanitizeTransaction(tx) };
|
|
281
374
|
}
|
|
282
375
|
|
|
283
376
|
default:
|
package/src/tools/rooms.ts
CHANGED
|
@@ -24,7 +24,7 @@ export function createRoomsTool() {
|
|
|
24
24
|
"create", "list", "info", "update", "discover",
|
|
25
25
|
"join", "leave", "dissolve",
|
|
26
26
|
"members", "invite", "remove_member",
|
|
27
|
-
"promote", "transfer", "permissions",
|
|
27
|
+
"promote", "transfer", "permissions", "mute",
|
|
28
28
|
],
|
|
29
29
|
description: "Room action to perform",
|
|
30
30
|
},
|
|
@@ -58,6 +58,27 @@ export function createRoomsTool() {
|
|
|
58
58
|
type: "boolean" as const,
|
|
59
59
|
description: "Whether all members can post — for create, update",
|
|
60
60
|
},
|
|
61
|
+
default_invite: {
|
|
62
|
+
type: "boolean" as const,
|
|
63
|
+
description: "Whether members can invite by default — for create, update",
|
|
64
|
+
},
|
|
65
|
+
max_members: {
|
|
66
|
+
type: "number" as const,
|
|
67
|
+
description: "Maximum room members — for create, update",
|
|
68
|
+
},
|
|
69
|
+
slow_mode_seconds: {
|
|
70
|
+
type: "number" as const,
|
|
71
|
+
description: "Slow mode interval in seconds — for create, update",
|
|
72
|
+
},
|
|
73
|
+
required_subscription_product_id: {
|
|
74
|
+
type: "string" as const,
|
|
75
|
+
description: "Subscription product required to access this room — for create, update",
|
|
76
|
+
},
|
|
77
|
+
member_ids: {
|
|
78
|
+
type: "array" as const,
|
|
79
|
+
items: { type: "string" as const },
|
|
80
|
+
description: "Initial member agent IDs — for create",
|
|
81
|
+
},
|
|
61
82
|
agent_id: {
|
|
62
83
|
type: "string" as const,
|
|
63
84
|
description: "Agent ID — for invite, remove_member, promote, transfer, permissions",
|
|
@@ -75,6 +96,10 @@ export function createRoomsTool() {
|
|
|
75
96
|
type: "boolean" as const,
|
|
76
97
|
description: "Invite permission override — for permissions",
|
|
77
98
|
},
|
|
99
|
+
muted: {
|
|
100
|
+
type: "boolean" as const,
|
|
101
|
+
description: "Mute or unmute the current member in a room — for mute",
|
|
102
|
+
},
|
|
78
103
|
},
|
|
79
104
|
required: ["action"],
|
|
80
105
|
},
|
|
@@ -101,7 +126,12 @@ export function createRoomsTool() {
|
|
|
101
126
|
rule: args.rule,
|
|
102
127
|
visibility: args.visibility || "private",
|
|
103
128
|
join_policy: args.join_policy,
|
|
129
|
+
required_subscription_product_id: args.required_subscription_product_id,
|
|
130
|
+
max_members: args.max_members,
|
|
104
131
|
default_send: args.default_send,
|
|
132
|
+
default_invite: args.default_invite,
|
|
133
|
+
slow_mode_seconds: args.slow_mode_seconds,
|
|
134
|
+
member_ids: args.member_ids,
|
|
105
135
|
});
|
|
106
136
|
|
|
107
137
|
case "list":
|
|
@@ -119,7 +149,11 @@ export function createRoomsTool() {
|
|
|
119
149
|
rule: args.rule,
|
|
120
150
|
visibility: args.visibility,
|
|
121
151
|
join_policy: args.join_policy,
|
|
152
|
+
required_subscription_product_id: args.required_subscription_product_id,
|
|
153
|
+
max_members: args.max_members,
|
|
122
154
|
default_send: args.default_send,
|
|
155
|
+
default_invite: args.default_invite,
|
|
156
|
+
slow_mode_seconds: args.slow_mode_seconds,
|
|
123
157
|
});
|
|
124
158
|
|
|
125
159
|
case "discover":
|
|
@@ -127,7 +161,10 @@ export function createRoomsTool() {
|
|
|
127
161
|
|
|
128
162
|
case "join":
|
|
129
163
|
if (!args.room_id) return { error: "room_id is required" };
|
|
130
|
-
await client.joinRoom(args.room_id
|
|
164
|
+
await client.joinRoom(args.room_id, {
|
|
165
|
+
can_send: args.can_send,
|
|
166
|
+
can_invite: args.can_invite,
|
|
167
|
+
});
|
|
131
168
|
return { ok: true, joined: args.room_id };
|
|
132
169
|
|
|
133
170
|
case "leave":
|
|
@@ -146,7 +183,10 @@ export function createRoomsTool() {
|
|
|
146
183
|
|
|
147
184
|
case "invite":
|
|
148
185
|
if (!args.room_id || !args.agent_id) return { error: "room_id and agent_id are required" };
|
|
149
|
-
await client.inviteToRoom(args.room_id, args.agent_id
|
|
186
|
+
await client.inviteToRoom(args.room_id, args.agent_id, {
|
|
187
|
+
can_send: args.can_send,
|
|
188
|
+
can_invite: args.can_invite,
|
|
189
|
+
});
|
|
150
190
|
return { ok: true, invited: args.agent_id, room: args.room_id };
|
|
151
191
|
|
|
152
192
|
case "remove_member":
|
|
@@ -172,6 +212,11 @@ export function createRoomsTool() {
|
|
|
172
212
|
});
|
|
173
213
|
return { ok: true, agent: args.agent_id, room: args.room_id };
|
|
174
214
|
|
|
215
|
+
case "mute":
|
|
216
|
+
if (!args.room_id) return { error: "room_id is required" };
|
|
217
|
+
await client.muteRoom(args.room_id, args.muted ?? true);
|
|
218
|
+
return { ok: true, room: args.room_id, muted: args.muted ?? true };
|
|
219
|
+
|
|
175
220
|
default:
|
|
176
221
|
return { error: `Unknown action: ${args.action}` };
|
|
177
222
|
}
|
|
@@ -59,6 +59,8 @@ export function createSubscriptionTool() {
|
|
|
59
59
|
"list_my_products",
|
|
60
60
|
"list_products",
|
|
61
61
|
"archive_product",
|
|
62
|
+
"create_subscription_room",
|
|
63
|
+
"bind_room_to_product",
|
|
62
64
|
"subscribe",
|
|
63
65
|
"list_my_subscriptions",
|
|
64
66
|
"list_subscribers",
|
|
@@ -68,19 +70,27 @@ export function createSubscriptionTool() {
|
|
|
68
70
|
},
|
|
69
71
|
product_id: {
|
|
70
72
|
type: "string" as const,
|
|
71
|
-
description: "Product ID — for archive_product, subscribe, list_subscribers",
|
|
73
|
+
description: "Product ID — for archive_product, create_subscription_room, bind_room_to_product, subscribe, list_subscribers",
|
|
72
74
|
},
|
|
73
75
|
subscription_id: {
|
|
74
76
|
type: "string" as const,
|
|
75
77
|
description: "Subscription ID — for cancel",
|
|
76
78
|
},
|
|
79
|
+
room_id: {
|
|
80
|
+
type: "string" as const,
|
|
81
|
+
description: "Room ID — for bind_room_to_product",
|
|
82
|
+
},
|
|
77
83
|
name: {
|
|
78
84
|
type: "string" as const,
|
|
79
|
-
description: "Product name — for create_product",
|
|
85
|
+
description: "Product name — for create_product, or room name — for create_subscription_room",
|
|
80
86
|
},
|
|
81
87
|
description: {
|
|
82
88
|
type: "string" as const,
|
|
83
|
-
description: "Product description — for create_product",
|
|
89
|
+
description: "Product description — for create_product, or room description — for create_subscription_room",
|
|
90
|
+
},
|
|
91
|
+
rule: {
|
|
92
|
+
type: "string" as const,
|
|
93
|
+
description: "Room rule/instructions — for create_subscription_room or bind_room_to_product",
|
|
84
94
|
},
|
|
85
95
|
amount_minor: {
|
|
86
96
|
type: "string" as const,
|
|
@@ -95,6 +105,22 @@ export function createSubscriptionTool() {
|
|
|
95
105
|
type: "string" as const,
|
|
96
106
|
description: "Asset code — for create_product",
|
|
97
107
|
},
|
|
108
|
+
max_members: {
|
|
109
|
+
type: "number" as const,
|
|
110
|
+
description: "Maximum room members — for create_subscription_room or bind_room_to_product",
|
|
111
|
+
},
|
|
112
|
+
default_send: {
|
|
113
|
+
type: "boolean" as const,
|
|
114
|
+
description: "Whether members can post by default — for create_subscription_room or bind_room_to_product",
|
|
115
|
+
},
|
|
116
|
+
default_invite: {
|
|
117
|
+
type: "boolean" as const,
|
|
118
|
+
description: "Whether members can invite by default — for create_subscription_room or bind_room_to_product",
|
|
119
|
+
},
|
|
120
|
+
slow_mode_seconds: {
|
|
121
|
+
type: "number" as const,
|
|
122
|
+
description: "Slow mode interval in seconds — for create_subscription_room or bind_room_to_product",
|
|
123
|
+
},
|
|
98
124
|
},
|
|
99
125
|
required: ["action"],
|
|
100
126
|
},
|
|
@@ -143,6 +169,48 @@ export function createSubscriptionTool() {
|
|
|
143
169
|
return { result: formatProduct(product), data: product };
|
|
144
170
|
}
|
|
145
171
|
|
|
172
|
+
case "create_subscription_room": {
|
|
173
|
+
if (!args.product_id) return { error: "product_id is required" };
|
|
174
|
+
if (!args.name) return { error: "name is required" };
|
|
175
|
+
const room = await client.createRoom({
|
|
176
|
+
name: args.name,
|
|
177
|
+
description: args.description,
|
|
178
|
+
rule: args.rule,
|
|
179
|
+
visibility: "private",
|
|
180
|
+
join_policy: "invite_only",
|
|
181
|
+
required_subscription_product_id: args.product_id,
|
|
182
|
+
max_members: args.max_members,
|
|
183
|
+
default_send: args.default_send,
|
|
184
|
+
default_invite: args.default_invite,
|
|
185
|
+
slow_mode_seconds: args.slow_mode_seconds,
|
|
186
|
+
});
|
|
187
|
+
return {
|
|
188
|
+
result: `Subscription room created: ${room.room_id} bound to ${args.product_id}`,
|
|
189
|
+
data: room,
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
case "bind_room_to_product": {
|
|
194
|
+
if (!args.room_id) return { error: "room_id is required" };
|
|
195
|
+
if (!args.product_id) return { error: "product_id is required" };
|
|
196
|
+
const room = await client.updateRoom(args.room_id, {
|
|
197
|
+
name: args.name,
|
|
198
|
+
description: args.description,
|
|
199
|
+
rule: args.rule,
|
|
200
|
+
visibility: "private",
|
|
201
|
+
join_policy: "invite_only",
|
|
202
|
+
required_subscription_product_id: args.product_id,
|
|
203
|
+
max_members: args.max_members,
|
|
204
|
+
default_send: args.default_send,
|
|
205
|
+
default_invite: args.default_invite,
|
|
206
|
+
slow_mode_seconds: args.slow_mode_seconds,
|
|
207
|
+
});
|
|
208
|
+
return {
|
|
209
|
+
result: `Room ${room.room_id} bound to subscription product ${args.product_id}`,
|
|
210
|
+
data: room,
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
146
214
|
case "subscribe": {
|
|
147
215
|
if (!args.product_id) return { error: "product_id is required" };
|
|
148
216
|
const subscription = await client.subscribeToProduct(args.product_id);
|
package/src/types.ts
CHANGED
|
@@ -90,7 +90,11 @@ export type RoomInfo = {
|
|
|
90
90
|
rule?: string | null;
|
|
91
91
|
visibility: "private" | "public";
|
|
92
92
|
join_policy: "invite_only" | "open";
|
|
93
|
+
required_subscription_product_id?: string | null;
|
|
94
|
+
max_members?: number | null;
|
|
93
95
|
default_send: boolean;
|
|
96
|
+
default_invite?: boolean;
|
|
97
|
+
slow_mode_seconds?: number | null;
|
|
94
98
|
member_count: number;
|
|
95
99
|
created_at: string;
|
|
96
100
|
};
|
package/src/tools/wallet.ts
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* botcord_wallet — Manage the agent's coin wallet: balance, ledger, transfers, topups, withdrawals.
|
|
3
|
-
*/
|
|
4
|
-
import {
|
|
5
|
-
getSingleAccountModeError,
|
|
6
|
-
resolveAccountConfig,
|
|
7
|
-
isAccountConfigured,
|
|
8
|
-
} from "../config.js";
|
|
9
|
-
import { BotCordClient } from "../client.js";
|
|
10
|
-
import { getConfig as getAppConfig } from "../runtime.js";
|
|
11
|
-
import { formatCoinAmount } from "./coin-format.js";
|
|
12
|
-
|
|
13
|
-
function formatBalance(summary: any): string {
|
|
14
|
-
const available = summary.available_balance_minor ?? "0";
|
|
15
|
-
const locked = summary.locked_balance_minor ?? "0";
|
|
16
|
-
const total = summary.total_balance_minor ?? "0";
|
|
17
|
-
return [
|
|
18
|
-
`Asset: ${summary.asset_code}`,
|
|
19
|
-
`Available: ${formatCoinAmount(available)}`,
|
|
20
|
-
`Locked: ${formatCoinAmount(locked)}`,
|
|
21
|
-
`Total: ${formatCoinAmount(total)}`,
|
|
22
|
-
`Updated: ${summary.updated_at}`,
|
|
23
|
-
].join("\n");
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function extractMemo(tx: any): string | null {
|
|
27
|
-
if (!tx.metadata_json) return null;
|
|
28
|
-
try {
|
|
29
|
-
const meta = typeof tx.metadata_json === "string"
|
|
30
|
-
? JSON.parse(tx.metadata_json)
|
|
31
|
-
: tx.metadata_json;
|
|
32
|
-
return meta?.memo ?? null;
|
|
33
|
-
} catch {
|
|
34
|
-
return null;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
function formatTransaction(tx: any): string {
|
|
39
|
-
const lines = [
|
|
40
|
-
`Transaction: ${tx.tx_id}`,
|
|
41
|
-
`Type: ${tx.type}`,
|
|
42
|
-
`Status: ${tx.status}`,
|
|
43
|
-
`Amount: ${formatCoinAmount(tx.amount_minor)}`,
|
|
44
|
-
`Fee: ${formatCoinAmount(tx.fee_minor)}`,
|
|
45
|
-
];
|
|
46
|
-
if (tx.from_agent_id) lines.push(`From: ${tx.from_agent_id}`);
|
|
47
|
-
if (tx.to_agent_id) lines.push(`To: ${tx.to_agent_id}`);
|
|
48
|
-
const memo = extractMemo(tx);
|
|
49
|
-
if (memo) lines.push(`Memo: ${memo}`);
|
|
50
|
-
lines.push(`Created: ${tx.created_at}`);
|
|
51
|
-
if (tx.completed_at) lines.push(`Completed: ${tx.completed_at}`);
|
|
52
|
-
return lines.join("\n");
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function formatLedger(data: any): string {
|
|
56
|
-
const entries = data.entries ?? [];
|
|
57
|
-
if (entries.length === 0) return "No ledger entries found.";
|
|
58
|
-
|
|
59
|
-
const lines = entries.map((e: any) => {
|
|
60
|
-
const dir = e.direction === "credit" ? "+" : "-";
|
|
61
|
-
return `${e.created_at} | ${dir}${formatCoinAmount(e.amount_minor)} | bal=${formatCoinAmount(e.balance_after_minor)} | tx=${e.tx_id}`;
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
if (data.has_more) {
|
|
65
|
-
lines.push(`\n(More entries available — use cursor: "${data.next_cursor}")`);
|
|
66
|
-
}
|
|
67
|
-
return lines.join("\n");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
export function createWalletTool() {
|
|
71
|
-
return {
|
|
72
|
-
name: "botcord_wallet",
|
|
73
|
-
description:
|
|
74
|
-
"Manage your BotCord coin wallet: check balance, view ledger, transfer coins, request topup/withdrawal, check transaction status.",
|
|
75
|
-
parameters: {
|
|
76
|
-
type: "object" as const,
|
|
77
|
-
properties: {
|
|
78
|
-
action: {
|
|
79
|
-
type: "string" as const,
|
|
80
|
-
enum: ["balance", "ledger", "transfer", "topup", "withdraw", "tx_status"],
|
|
81
|
-
description: "Wallet action to perform",
|
|
82
|
-
},
|
|
83
|
-
to_agent_id: {
|
|
84
|
-
type: "string" as const,
|
|
85
|
-
description: "Recipient agent ID (ag_...) — for transfer",
|
|
86
|
-
},
|
|
87
|
-
amount_minor: {
|
|
88
|
-
type: "string" as const,
|
|
89
|
-
description: "Amount in minor units (string) — for transfer, topup, withdraw",
|
|
90
|
-
},
|
|
91
|
-
memo: {
|
|
92
|
-
type: "string" as const,
|
|
93
|
-
description: "Optional memo — for transfer",
|
|
94
|
-
},
|
|
95
|
-
idempotency_key: {
|
|
96
|
-
type: "string" as const,
|
|
97
|
-
description: "Optional idempotency key (UUID) — for transfer, withdraw",
|
|
98
|
-
},
|
|
99
|
-
channel: {
|
|
100
|
-
type: "string" as const,
|
|
101
|
-
description: "Topup channel (e.g. 'mock') — for topup",
|
|
102
|
-
},
|
|
103
|
-
destination_type: {
|
|
104
|
-
type: "string" as const,
|
|
105
|
-
description: "Withdrawal destination type — for withdraw",
|
|
106
|
-
},
|
|
107
|
-
destination: {
|
|
108
|
-
type: "object" as const,
|
|
109
|
-
description: "Withdrawal destination details — for withdraw",
|
|
110
|
-
},
|
|
111
|
-
tx_id: {
|
|
112
|
-
type: "string" as const,
|
|
113
|
-
description: "Transaction ID — for tx_status",
|
|
114
|
-
},
|
|
115
|
-
cursor: {
|
|
116
|
-
type: "string" as const,
|
|
117
|
-
description: "Pagination cursor — for ledger",
|
|
118
|
-
},
|
|
119
|
-
limit: {
|
|
120
|
-
type: "number" as const,
|
|
121
|
-
description: "Max entries to return — for ledger",
|
|
122
|
-
},
|
|
123
|
-
type: {
|
|
124
|
-
type: "string" as const,
|
|
125
|
-
description: "Filter by transaction type — for ledger",
|
|
126
|
-
},
|
|
127
|
-
},
|
|
128
|
-
required: ["action"],
|
|
129
|
-
},
|
|
130
|
-
execute: async (toolCallId: any, args: any, signal?: any, onUpdate?: any) => {
|
|
131
|
-
const cfg = getAppConfig();
|
|
132
|
-
if (!cfg) return { error: "No configuration available" };
|
|
133
|
-
const singleAccountError = getSingleAccountModeError(cfg);
|
|
134
|
-
if (singleAccountError) return { error: singleAccountError };
|
|
135
|
-
|
|
136
|
-
const acct = resolveAccountConfig(cfg);
|
|
137
|
-
if (!isAccountConfigured(acct)) {
|
|
138
|
-
return { error: "BotCord is not configured." };
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const client = new BotCordClient(acct);
|
|
142
|
-
|
|
143
|
-
try {
|
|
144
|
-
switch (args.action) {
|
|
145
|
-
case "balance": {
|
|
146
|
-
const summary = await client.getWallet();
|
|
147
|
-
return { result: formatBalance(summary), data: summary };
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
case "ledger": {
|
|
151
|
-
const opts: { cursor?: string; limit?: number; type?: string } = {};
|
|
152
|
-
if (args.cursor) opts.cursor = args.cursor;
|
|
153
|
-
if (args.limit) opts.limit = args.limit;
|
|
154
|
-
if (args.type) opts.type = args.type;
|
|
155
|
-
const ledger = await client.getWalletLedger(opts);
|
|
156
|
-
return { result: formatLedger(ledger), data: ledger };
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
case "transfer": {
|
|
160
|
-
if (!args.to_agent_id) return { error: "to_agent_id is required" };
|
|
161
|
-
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
162
|
-
const tx = await client.createTransfer({
|
|
163
|
-
to_agent_id: args.to_agent_id,
|
|
164
|
-
amount_minor: args.amount_minor,
|
|
165
|
-
memo: args.memo,
|
|
166
|
-
idempotency_key: args.idempotency_key,
|
|
167
|
-
});
|
|
168
|
-
return { result: formatTransaction(tx), data: tx };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
case "topup": {
|
|
172
|
-
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
173
|
-
const topup = await client.createTopup({
|
|
174
|
-
amount_minor: args.amount_minor,
|
|
175
|
-
channel: args.channel,
|
|
176
|
-
idempotency_key: args.idempotency_key,
|
|
177
|
-
});
|
|
178
|
-
return { result: `Topup request created: ${JSON.stringify(topup)}`, data: topup };
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
case "withdraw": {
|
|
182
|
-
if (!args.amount_minor) return { error: "amount_minor is required" };
|
|
183
|
-
const withdrawal = await client.createWithdrawal({
|
|
184
|
-
amount_minor: args.amount_minor,
|
|
185
|
-
destination_type: args.destination_type,
|
|
186
|
-
destination: args.destination,
|
|
187
|
-
idempotency_key: args.idempotency_key,
|
|
188
|
-
});
|
|
189
|
-
return {
|
|
190
|
-
result: `Withdrawal request created: ${JSON.stringify(withdrawal)}`,
|
|
191
|
-
data: withdrawal,
|
|
192
|
-
};
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
case "tx_status": {
|
|
196
|
-
if (!args.tx_id) return { error: "tx_id is required" };
|
|
197
|
-
const tx = await client.getWalletTransaction(args.tx_id);
|
|
198
|
-
return { result: formatTransaction(tx), data: tx };
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
default:
|
|
202
|
-
return { error: `Unknown action: ${args.action}` };
|
|
203
|
-
}
|
|
204
|
-
} catch (err: any) {
|
|
205
|
-
return { error: `Wallet action failed: ${err.message}` };
|
|
206
|
-
}
|
|
207
|
-
},
|
|
208
|
-
};
|
|
209
|
-
}
|