bsv-mcp 0.0.36 → 0.2.7
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/CHANGELOG.md +124 -0
- package/README.md +233 -4
- package/dist/index.js +120082 -54669
- package/index.ts +1196 -88
- package/next-env.d.ts +6 -0
- package/package.json +58 -15
- package/prompts/bsvSdk/index.ts +5 -5
- package/prompts/bsvSdk/transaction.ts +2 -2
- package/prompts/bsvSdk/wallet.ts +1 -1
- package/prompts/index.ts +1 -1
- package/resources/bitcom.ts +132 -0
- package/resources/resources.ts +7 -3
- package/smithery.yaml +3 -0
- package/test-mcp-server.ts +157 -0
- package/tools/a2b/discover.ts +13 -13
- package/tools/bap/friend.ts +266 -0
- package/tools/bap/generate.test.ts +350 -0
- package/tools/bap/generate.ts +431 -0
- package/tools/bap/getCurrentAddress.ts +69 -0
- package/tools/bap/getId.test.ts +121 -0
- package/tools/bap/getId.ts +169 -0
- package/tools/bap/index.ts +111 -0
- package/tools/bap/types.ts +66 -0
- package/tools/bap/utils.ts +33 -0
- package/tools/bsocial/bmapFollow.ts +121 -0
- package/tools/bsocial/bmapLikes.ts +111 -0
- package/tools/bsocial/bmapReadPosts.ts +148 -0
- package/tools/bsocial/createPost.ts +136 -0
- package/tools/bsocial/index.ts +45 -0
- package/tools/bsocial/readPosts.ts +255 -0
- package/tools/bsv/decodeTransaction.ts +7 -6
- package/tools/bsv/explore.ts +2 -3
- package/tools/bsv/getPrice.ts +1 -9
- package/tools/bsv/token.ts +14 -26
- package/tools/constants.ts +21 -0
- package/tools/index.ts +91 -18
- package/tools/mnee/getBalance.ts +2 -4
- package/tools/mnee/index.ts +8 -0
- package/tools/mnee/parseTx.ts +4 -4
- package/tools/mnee/sendMnee.ts +6 -6
- package/tools/ordinals/getInscription.ts +2 -3
- package/tools/ordinals/getTokenByIdOrTicker.ts +6 -3
- package/tools/ordinals/marketListings.ts +2 -18
- package/tools/ordinals/marketSales.ts +2 -4
- package/tools/ordinals/searchInscriptions.ts +2 -4
- package/tools/utils/aip.ts +46 -0
- package/tools/utils/conversion.ts +1 -0
- package/tools/utils/errorHandler.ts +76 -0
- package/tools/utils/index.ts +56 -14
- package/tools/utils/installAgentMaster.ts +84 -0
- package/tools/utils/logger.ts +20 -0
- package/tools/utils/toolConfig.ts +80 -0
- package/tools/utils/toolRegistration.ts +66 -0
- package/tools/utils/transactionBuilder.ts +196 -0
- package/tools/wallet/a2bPublishAgent.ts +25 -30
- package/tools/wallet/a2bPublishMcp.ts +43 -44
- package/tools/wallet/createOrdinals.ts +13 -20
- package/tools/wallet/fetchPaymentUtxos.ts +48 -39
- package/tools/wallet/gatherCollectionInfo.ts +370 -0
- package/tools/wallet/getAddress.ts +14 -30
- package/tools/wallet/getBalance.ts +54 -0
- package/tools/wallet/getBalanceDroplet.ts +48 -0
- package/tools/wallet/getPublicKey.ts +3 -16
- package/tools/wallet/integratedWallet.ts +120 -0
- package/tools/wallet/mintCollection.ts +373 -0
- package/tools/wallet/purchaseListing.ts +21 -31
- package/tools/wallet/refreshUtxos.ts +2 -14
- package/tools/wallet/schemas.ts +18 -249
- package/tools/wallet/sendOrdinals.ts +14 -21
- package/tools/wallet/sendToAddress.ts +2 -18
- package/tools/wallet/setupDroplet.ts +161 -0
- package/tools/wallet/tools.ts +55 -210
- package/tools/wallet/transactionHelper.ts +121 -0
- package/tools/wallet/transferOrdToken.ts +19 -24
- package/tools/wallet/utxo.ts +62 -0
- package/tools/wallet/wallet.ts +237 -234
- package/vite.config.ts +15 -0
- package/tools/a2b/call.ts +0 -32
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { Wallet } from "./wallet";
|
|
7
|
+
|
|
8
|
+
const gatherCollectionInfoArgsSchema = z.object({
|
|
9
|
+
folderPath: z
|
|
10
|
+
.string()
|
|
11
|
+
.describe("Path to folder containing images to analyze for collection"),
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
type GatherCollectionInfoArgs = z.infer<typeof gatherCollectionInfoArgsSchema>;
|
|
15
|
+
|
|
16
|
+
interface ImageInfo {
|
|
17
|
+
fileName: string;
|
|
18
|
+
filePath: string;
|
|
19
|
+
size: number;
|
|
20
|
+
sizeKB: number;
|
|
21
|
+
contentType: string;
|
|
22
|
+
valid: boolean;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface CollectionAnalysis {
|
|
27
|
+
folderPath: string;
|
|
28
|
+
totalImages: number;
|
|
29
|
+
validImages: number;
|
|
30
|
+
invalidImages: number;
|
|
31
|
+
totalSizeKB: number;
|
|
32
|
+
imageTypes: Record<string, number>;
|
|
33
|
+
images: ImageInfo[];
|
|
34
|
+
suggestedMetadata: {
|
|
35
|
+
collectionName: string;
|
|
36
|
+
description: string;
|
|
37
|
+
quantity: number;
|
|
38
|
+
};
|
|
39
|
+
costEstimate: {
|
|
40
|
+
collectionInscriptionCost: number;
|
|
41
|
+
perItemCost: number;
|
|
42
|
+
totalItemsCost: number;
|
|
43
|
+
totalCost: number;
|
|
44
|
+
totalCostBSV: number;
|
|
45
|
+
};
|
|
46
|
+
walletInfo: {
|
|
47
|
+
address: string;
|
|
48
|
+
balance: number;
|
|
49
|
+
balanceBSV: number;
|
|
50
|
+
hasEnoughFunds: boolean;
|
|
51
|
+
shortfall?: number;
|
|
52
|
+
};
|
|
53
|
+
warnings: string[];
|
|
54
|
+
errors: string[];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function analyzeImageFile(filePath: string): Promise<ImageInfo> {
|
|
58
|
+
const fileName = path.basename(filePath);
|
|
59
|
+
const ext = path.extname(fileName).toLowerCase();
|
|
60
|
+
|
|
61
|
+
const contentTypeMap: Record<string, string> = {
|
|
62
|
+
".jpg": "image/jpeg",
|
|
63
|
+
".jpeg": "image/jpeg",
|
|
64
|
+
".png": "image/png",
|
|
65
|
+
".gif": "image/gif",
|
|
66
|
+
".webp": "image/webp",
|
|
67
|
+
".svg": "image/svg+xml",
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const stat = await fs.stat(filePath);
|
|
72
|
+
const contentType = contentTypeMap[ext];
|
|
73
|
+
|
|
74
|
+
if (!contentType) {
|
|
75
|
+
return {
|
|
76
|
+
fileName,
|
|
77
|
+
filePath,
|
|
78
|
+
size: stat.size,
|
|
79
|
+
sizeKB: Math.round(stat.size / 1024),
|
|
80
|
+
contentType: "unknown",
|
|
81
|
+
valid: false,
|
|
82
|
+
error: `Unsupported file type: ${ext}`,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Check file size (warn if over 50KB)
|
|
87
|
+
const sizeKB = Math.round(stat.size / 1024);
|
|
88
|
+
const warnings: string[] = [];
|
|
89
|
+
|
|
90
|
+
if (sizeKB > 50) {
|
|
91
|
+
warnings.push(
|
|
92
|
+
`Large file size: ${sizeKB}KB (consider optimizing for lower fees)`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
fileName,
|
|
98
|
+
filePath,
|
|
99
|
+
size: stat.size,
|
|
100
|
+
sizeKB,
|
|
101
|
+
contentType,
|
|
102
|
+
valid: true,
|
|
103
|
+
error: warnings.length > 0 ? warnings.join("; ") : undefined,
|
|
104
|
+
};
|
|
105
|
+
} catch (error) {
|
|
106
|
+
return {
|
|
107
|
+
fileName,
|
|
108
|
+
filePath,
|
|
109
|
+
size: 0,
|
|
110
|
+
sizeKB: 0,
|
|
111
|
+
contentType: "unknown",
|
|
112
|
+
valid: false,
|
|
113
|
+
error: error instanceof Error ? error.message : "Unknown error",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function estimateInscriptionCost(sizeKB: number): number {
|
|
119
|
+
// Base cost for inscription (1 sat)
|
|
120
|
+
const baseCost = 1;
|
|
121
|
+
// Additional cost based on size (rough estimate: 10 sats per KB)
|
|
122
|
+
const sizeCost = Math.ceil(sizeKB * 10);
|
|
123
|
+
// Mining fee estimate
|
|
124
|
+
const miningFee = 500;
|
|
125
|
+
|
|
126
|
+
return baseCost + sizeCost + miningFee;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function registerGatherCollectionInfoTool(
|
|
130
|
+
server: McpServer,
|
|
131
|
+
wallet: Wallet,
|
|
132
|
+
) {
|
|
133
|
+
server.tool(
|
|
134
|
+
"wallet_gatherCollectionInfo",
|
|
135
|
+
"Analyzes a folder of images and gathers all necessary information for minting an ordinals collection. This includes validating images, checking wallet balance, estimating costs, and suggesting metadata. Use this before minting to ensure everything is ready.",
|
|
136
|
+
{ ...gatherCollectionInfoArgsSchema.shape },
|
|
137
|
+
async ({ folderPath }): Promise<CallToolResult> => {
|
|
138
|
+
try {
|
|
139
|
+
const analysis: CollectionAnalysis = {
|
|
140
|
+
folderPath,
|
|
141
|
+
totalImages: 0,
|
|
142
|
+
validImages: 0,
|
|
143
|
+
invalidImages: 0,
|
|
144
|
+
totalSizeKB: 0,
|
|
145
|
+
imageTypes: {},
|
|
146
|
+
images: [],
|
|
147
|
+
suggestedMetadata: {
|
|
148
|
+
collectionName: "",
|
|
149
|
+
description: "",
|
|
150
|
+
quantity: 0,
|
|
151
|
+
},
|
|
152
|
+
costEstimate: {
|
|
153
|
+
collectionInscriptionCost: 0,
|
|
154
|
+
perItemCost: 0,
|
|
155
|
+
totalItemsCost: 0,
|
|
156
|
+
totalCost: 0,
|
|
157
|
+
totalCostBSV: 0,
|
|
158
|
+
},
|
|
159
|
+
walletInfo: {
|
|
160
|
+
address: "",
|
|
161
|
+
balance: 0,
|
|
162
|
+
balanceBSV: 0,
|
|
163
|
+
hasEnoughFunds: false,
|
|
164
|
+
},
|
|
165
|
+
warnings: [],
|
|
166
|
+
errors: [],
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
// Check if folder exists
|
|
170
|
+
try {
|
|
171
|
+
const folderStat = await fs.stat(folderPath);
|
|
172
|
+
if (!folderStat.isDirectory()) {
|
|
173
|
+
throw new Error(`Path is not a directory: ${folderPath}`);
|
|
174
|
+
}
|
|
175
|
+
} catch (error) {
|
|
176
|
+
analysis.errors.push(
|
|
177
|
+
`Folder not found or inaccessible: ${folderPath}`,
|
|
178
|
+
);
|
|
179
|
+
return {
|
|
180
|
+
content: [
|
|
181
|
+
{
|
|
182
|
+
type: "text",
|
|
183
|
+
text: JSON.stringify(analysis, null, 2),
|
|
184
|
+
},
|
|
185
|
+
],
|
|
186
|
+
isError: true,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Get wallet info
|
|
191
|
+
const paymentPk = wallet.getPrivateKey();
|
|
192
|
+
if (!paymentPk) {
|
|
193
|
+
analysis.errors.push("No payment key available in wallet");
|
|
194
|
+
return {
|
|
195
|
+
content: [
|
|
196
|
+
{
|
|
197
|
+
type: "text",
|
|
198
|
+
text: JSON.stringify(analysis, null, 2),
|
|
199
|
+
},
|
|
200
|
+
],
|
|
201
|
+
isError: true,
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
analysis.walletInfo.address = paymentPk.toAddress().toString();
|
|
206
|
+
|
|
207
|
+
// Get wallet balance
|
|
208
|
+
try {
|
|
209
|
+
const { paymentUtxos } = await wallet.getUtxos();
|
|
210
|
+
const balance = paymentUtxos.reduce(
|
|
211
|
+
(sum, utxo) => sum + utxo.satoshis,
|
|
212
|
+
0,
|
|
213
|
+
);
|
|
214
|
+
analysis.walletInfo.balance = balance;
|
|
215
|
+
analysis.walletInfo.balanceBSV = balance / 100000000;
|
|
216
|
+
} catch (error) {
|
|
217
|
+
analysis.warnings.push("Could not fetch wallet balance");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Scan folder for images
|
|
221
|
+
const files = await fs.readdir(folderPath);
|
|
222
|
+
const imageExtensions = [
|
|
223
|
+
".jpg",
|
|
224
|
+
".jpeg",
|
|
225
|
+
".png",
|
|
226
|
+
".gif",
|
|
227
|
+
".webp",
|
|
228
|
+
".svg",
|
|
229
|
+
];
|
|
230
|
+
|
|
231
|
+
for (const file of files) {
|
|
232
|
+
const filePath = path.join(folderPath, file);
|
|
233
|
+
const stat = await fs.stat(filePath);
|
|
234
|
+
|
|
235
|
+
if (stat.isFile()) {
|
|
236
|
+
const ext = path.extname(file).toLowerCase();
|
|
237
|
+
if (imageExtensions.includes(ext)) {
|
|
238
|
+
const imageInfo = await analyzeImageFile(filePath);
|
|
239
|
+
analysis.images.push(imageInfo);
|
|
240
|
+
analysis.totalImages++;
|
|
241
|
+
|
|
242
|
+
if (imageInfo.valid) {
|
|
243
|
+
analysis.validImages++;
|
|
244
|
+
analysis.totalSizeKB += imageInfo.sizeKB;
|
|
245
|
+
|
|
246
|
+
// Track image types
|
|
247
|
+
const type = imageInfo.contentType.split("/")[1] || "unknown";
|
|
248
|
+
analysis.imageTypes[type] =
|
|
249
|
+
(analysis.imageTypes[type] || 0) + 1;
|
|
250
|
+
} else {
|
|
251
|
+
analysis.invalidImages++;
|
|
252
|
+
if (imageInfo.error) {
|
|
253
|
+
analysis.errors.push(
|
|
254
|
+
`${imageInfo.fileName}: ${imageInfo.error}`,
|
|
255
|
+
);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Sort images by name for consistent ordering
|
|
263
|
+
analysis.images.sort((a, b) => a.fileName.localeCompare(b.fileName));
|
|
264
|
+
|
|
265
|
+
if (analysis.validImages === 0) {
|
|
266
|
+
analysis.errors.push("No valid images found in the folder");
|
|
267
|
+
return {
|
|
268
|
+
content: [
|
|
269
|
+
{
|
|
270
|
+
type: "text",
|
|
271
|
+
text: JSON.stringify(analysis, null, 2),
|
|
272
|
+
},
|
|
273
|
+
],
|
|
274
|
+
isError: true,
|
|
275
|
+
};
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Suggest metadata based on folder name
|
|
279
|
+
const folderName = path.basename(folderPath);
|
|
280
|
+
analysis.suggestedMetadata = {
|
|
281
|
+
collectionName: folderName
|
|
282
|
+
.replace(/[-_]/g, " ")
|
|
283
|
+
.replace(/\b\w/g, (l) => l.toUpperCase()),
|
|
284
|
+
description: `A collection of ${analysis.validImages} unique digital artifacts`,
|
|
285
|
+
quantity: analysis.validImages,
|
|
286
|
+
};
|
|
287
|
+
|
|
288
|
+
// Estimate costs
|
|
289
|
+
const collectionIconSizeKB = 1; // SVG icon is small
|
|
290
|
+
analysis.costEstimate.collectionInscriptionCost =
|
|
291
|
+
estimateInscriptionCost(collectionIconSizeKB);
|
|
292
|
+
|
|
293
|
+
// Average cost per item
|
|
294
|
+
const avgItemSizeKB = analysis.totalSizeKB / analysis.validImages;
|
|
295
|
+
analysis.costEstimate.perItemCost =
|
|
296
|
+
estimateInscriptionCost(avgItemSizeKB);
|
|
297
|
+
analysis.costEstimate.totalItemsCost =
|
|
298
|
+
analysis.costEstimate.perItemCost * analysis.validImages;
|
|
299
|
+
analysis.costEstimate.totalCost =
|
|
300
|
+
analysis.costEstimate.collectionInscriptionCost +
|
|
301
|
+
analysis.costEstimate.totalItemsCost;
|
|
302
|
+
analysis.costEstimate.totalCostBSV =
|
|
303
|
+
analysis.costEstimate.totalCost / 100000000;
|
|
304
|
+
|
|
305
|
+
// Check if wallet has enough funds
|
|
306
|
+
analysis.walletInfo.hasEnoughFunds =
|
|
307
|
+
analysis.walletInfo.balance >= analysis.costEstimate.totalCost;
|
|
308
|
+
if (!analysis.walletInfo.hasEnoughFunds) {
|
|
309
|
+
analysis.walletInfo.shortfall =
|
|
310
|
+
analysis.costEstimate.totalCost - analysis.walletInfo.balance;
|
|
311
|
+
analysis.warnings.push(
|
|
312
|
+
`Insufficient funds: need ${analysis.costEstimate.totalCost} sats, have ${analysis.walletInfo.balance} sats (shortfall: ${analysis.walletInfo.shortfall} sats)`,
|
|
313
|
+
);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// Add warnings for large collections
|
|
317
|
+
if (analysis.validImages > 100) {
|
|
318
|
+
analysis.warnings.push(
|
|
319
|
+
`Large collection (${analysis.validImages} items) will take time and multiple transactions to mint`,
|
|
320
|
+
);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (analysis.totalSizeKB > 1000) {
|
|
324
|
+
analysis.warnings.push(
|
|
325
|
+
`Large total size (${Math.round(analysis.totalSizeKB / 1024)}MB) will result in higher fees`,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Generate summary
|
|
330
|
+
const summary = {
|
|
331
|
+
ready:
|
|
332
|
+
analysis.errors.length === 0 && analysis.walletInfo.hasEnoughFunds,
|
|
333
|
+
summary: `Found ${analysis.validImages} valid images in "${folderName}". Total size: ${Math.round(analysis.totalSizeKB)}KB. Estimated cost: ${analysis.costEstimate.totalCost} sats (${analysis.costEstimate.totalCostBSV.toFixed(8)} BSV). Wallet balance: ${analysis.walletInfo.balance} sats. ${
|
|
334
|
+
analysis.walletInfo.hasEnoughFunds
|
|
335
|
+
? "Ready to mint!"
|
|
336
|
+
: "Insufficient funds."
|
|
337
|
+
}`,
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
return {
|
|
341
|
+
content: [
|
|
342
|
+
{
|
|
343
|
+
type: "text",
|
|
344
|
+
text: JSON.stringify(
|
|
345
|
+
{
|
|
346
|
+
...analysis,
|
|
347
|
+
...summary,
|
|
348
|
+
},
|
|
349
|
+
null,
|
|
350
|
+
2,
|
|
351
|
+
),
|
|
352
|
+
},
|
|
353
|
+
],
|
|
354
|
+
};
|
|
355
|
+
} catch (error) {
|
|
356
|
+
const errorMessage =
|
|
357
|
+
error instanceof Error ? error.message : String(error);
|
|
358
|
+
return {
|
|
359
|
+
content: [
|
|
360
|
+
{
|
|
361
|
+
type: "text",
|
|
362
|
+
text: `Error analyzing collection: ${errorMessage}`,
|
|
363
|
+
},
|
|
364
|
+
],
|
|
365
|
+
isError: true,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
},
|
|
369
|
+
);
|
|
370
|
+
}
|
|
@@ -1,45 +1,29 @@
|
|
|
1
|
-
import { PrivateKey } from "@bsv/sdk";
|
|
2
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
-
import {
|
|
2
|
+
import type { Wallet } from "./wallet";
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Register the tool to get the wallet address
|
|
7
6
|
* @param server The MCP server instance
|
|
7
|
+
* @param wallet The initialized custom Wallet instance
|
|
8
8
|
*/
|
|
9
|
-
export function registerGetAddressTool(
|
|
9
|
+
export function registerGetAddressTool(
|
|
10
|
+
server: McpServer,
|
|
11
|
+
wallet: Wallet,
|
|
12
|
+
): void {
|
|
10
13
|
server.tool(
|
|
11
14
|
"wallet_getAddress",
|
|
12
15
|
"Retrieves the current wallet's Bitcoin SV address. This address can be used to receive BSV, ordinals, or tokens, and is derived from the wallet's private key.",
|
|
13
|
-
{
|
|
14
|
-
args: z
|
|
15
|
-
.object({})
|
|
16
|
-
.optional()
|
|
17
|
-
.describe(
|
|
18
|
-
"No parameters required - simply returns the current wallet address",
|
|
19
|
-
),
|
|
20
|
-
},
|
|
16
|
+
{},
|
|
21
17
|
async () => {
|
|
22
18
|
try {
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
text: JSON.stringify({
|
|
30
|
-
error: "No private key available",
|
|
31
|
-
message:
|
|
32
|
-
"Please set PRIVATE_KEY_WIF environment variable with a valid Bitcoin SV private key in WIF format.",
|
|
33
|
-
status: "error",
|
|
34
|
-
}),
|
|
35
|
-
},
|
|
36
|
-
],
|
|
37
|
-
isError: true,
|
|
38
|
-
};
|
|
19
|
+
const address = wallet.getAddress();
|
|
20
|
+
|
|
21
|
+
if (!address) {
|
|
22
|
+
throw new Error(
|
|
23
|
+
"Could not retrieve address from wallet instance. Key might be missing or getAddress method failed.",
|
|
24
|
+
);
|
|
39
25
|
}
|
|
40
26
|
|
|
41
|
-
const privKey = PrivateKey.fromWif(wif);
|
|
42
|
-
const address = privKey.toAddress();
|
|
43
27
|
return {
|
|
44
28
|
content: [
|
|
45
29
|
{
|
|
@@ -55,7 +39,7 @@ export function registerGetAddressTool(server: McpServer): void {
|
|
|
55
39
|
{
|
|
56
40
|
type: "text",
|
|
57
41
|
text: JSON.stringify({
|
|
58
|
-
error: "
|
|
42
|
+
error: "Failed to get wallet address",
|
|
59
43
|
message: msg,
|
|
60
44
|
status: "error",
|
|
61
45
|
}),
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import { toBitcoin } from "satoshi-token"; // For converting satoshis to BSV string
|
|
3
|
+
import type { Wallet } from "./wallet";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Registers the wallet_getBalance tool
|
|
7
|
+
*/
|
|
8
|
+
export function registerWalletGetBalanceTool(
|
|
9
|
+
server: McpServer,
|
|
10
|
+
wallet: Wallet,
|
|
11
|
+
) {
|
|
12
|
+
server.tool(
|
|
13
|
+
"wallet_getBalance",
|
|
14
|
+
"Retrieves the current BSV balance for the wallet based on its UTXOs.",
|
|
15
|
+
{},
|
|
16
|
+
async () => {
|
|
17
|
+
try {
|
|
18
|
+
const { paymentUtxos } = await wallet.getUtxos();
|
|
19
|
+
|
|
20
|
+
let totalSatoshis = 0;
|
|
21
|
+
if (paymentUtxos && paymentUtxos.length > 0) {
|
|
22
|
+
for (const utxo of paymentUtxos) {
|
|
23
|
+
totalSatoshis += utxo.satoshis || 0;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const bsvAmount = toBitcoin(totalSatoshis); // Convert to BSV string
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
content: [
|
|
31
|
+
{
|
|
32
|
+
type: "text",
|
|
33
|
+
text: JSON.stringify({
|
|
34
|
+
satoshis: totalSatoshis,
|
|
35
|
+
bsv: bsvAmount,
|
|
36
|
+
utxoCount: paymentUtxos ? paymentUtxos.length : 0,
|
|
37
|
+
}),
|
|
38
|
+
},
|
|
39
|
+
],
|
|
40
|
+
};
|
|
41
|
+
} catch (error) {
|
|
42
|
+
return {
|
|
43
|
+
content: [
|
|
44
|
+
{
|
|
45
|
+
type: "text",
|
|
46
|
+
text: error instanceof Error ? error.message : String(error),
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
);
|
|
54
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { DropletClient } from "../../utils/droplet";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Register the getBalance tool for Droplet API mode
|
|
6
|
+
*/
|
|
7
|
+
export function registerWalletGetBalanceDropletTool(
|
|
8
|
+
server: McpServer,
|
|
9
|
+
dropletClient: DropletClient,
|
|
10
|
+
) {
|
|
11
|
+
server.tool(
|
|
12
|
+
"wallet_getBalance",
|
|
13
|
+
"Gets the current balance of the wallet (Droplet mode)",
|
|
14
|
+
{},
|
|
15
|
+
async () => {
|
|
16
|
+
try {
|
|
17
|
+
const status = await dropletClient.getFaucetStatus();
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
content: [
|
|
21
|
+
{
|
|
22
|
+
type: "text",
|
|
23
|
+
text: JSON.stringify({
|
|
24
|
+
balance: status.balance_satoshis,
|
|
25
|
+
unspentUtxoCount: status.unspent_utxo_count,
|
|
26
|
+
spendableUtxoCount: status.spendable_utxo_count,
|
|
27
|
+
consolidatingBalance: status.consolidating_balance_satoshis,
|
|
28
|
+
consolidatingUtxoCount: status.consolidating_utxo_count,
|
|
29
|
+
fixedDropSats: status.fixed_drop_sats,
|
|
30
|
+
faucetName: status.faucet_name,
|
|
31
|
+
}),
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
};
|
|
35
|
+
} catch (error) {
|
|
36
|
+
return {
|
|
37
|
+
content: [
|
|
38
|
+
{
|
|
39
|
+
type: "text",
|
|
40
|
+
text: error instanceof Error ? error.message : String(error),
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
isError: true,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -1,16 +1,6 @@
|
|
|
1
1
|
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
|
|
3
|
-
import type {
|
|
4
|
-
ServerNotification,
|
|
5
|
-
ServerRequest,
|
|
6
|
-
} from "@modelcontextprotocol/sdk/types.js";
|
|
7
|
-
import type { z } from "zod";
|
|
8
|
-
import { getPublicKeyArgsSchema } from "./schemas";
|
|
9
2
|
import type { Wallet } from "./wallet";
|
|
10
3
|
|
|
11
|
-
// Use the schema imported from schemas.ts
|
|
12
|
-
export type GetPublicKeyArgs = z.infer<typeof getPublicKeyArgsSchema>;
|
|
13
|
-
|
|
14
4
|
/**
|
|
15
5
|
* Register the getPublicKey tool
|
|
16
6
|
*/
|
|
@@ -18,13 +8,10 @@ export function registerGetPublicKeyTool(server: McpServer, wallet: Wallet) {
|
|
|
18
8
|
server.tool(
|
|
19
9
|
"wallet_getPublicKey",
|
|
20
10
|
"Retrieves the current wallet's public key. This public key can be used for cryptographic operations like signature verification or encryption.",
|
|
21
|
-
{
|
|
22
|
-
async (
|
|
23
|
-
{ args }: { args: GetPublicKeyArgs },
|
|
24
|
-
extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
|
|
25
|
-
) => {
|
|
11
|
+
{},
|
|
12
|
+
async () => {
|
|
26
13
|
try {
|
|
27
|
-
const result = await wallet.getPublicKey(
|
|
14
|
+
const result = await wallet.getPublicKey({});
|
|
28
15
|
return { content: [{ type: "text", text: JSON.stringify(result) }] };
|
|
29
16
|
} catch (err: unknown) {
|
|
30
17
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { P2PKH, type PrivateKey, Script } from "@bsv/sdk";
|
|
2
|
+
import { DropletClient, type DropletConfig } from "../../utils/droplet";
|
|
3
|
+
import { Wallet } from "./wallet";
|
|
4
|
+
|
|
5
|
+
export interface IntegratedWalletConfig {
|
|
6
|
+
// Local wallet config
|
|
7
|
+
paymentKey?: PrivateKey;
|
|
8
|
+
identityKey?: PrivateKey;
|
|
9
|
+
|
|
10
|
+
// Droplet API config
|
|
11
|
+
useDropletApi?: boolean;
|
|
12
|
+
dropletConfig?: DropletConfig;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Integrated wallet that can use either local keys or Droplet API
|
|
17
|
+
*/
|
|
18
|
+
export class IntegratedWallet {
|
|
19
|
+
private localWallet?: Wallet;
|
|
20
|
+
private dropletClient?: DropletClient;
|
|
21
|
+
|
|
22
|
+
constructor(private config: IntegratedWalletConfig) {
|
|
23
|
+
if (config.useDropletApi && config.dropletConfig) {
|
|
24
|
+
// If we have a payment key, use it for auth with Droplet API
|
|
25
|
+
if (config.paymentKey) {
|
|
26
|
+
config.dropletConfig.authKey = config.paymentKey;
|
|
27
|
+
}
|
|
28
|
+
this.dropletClient = new DropletClient(config.dropletConfig);
|
|
29
|
+
console.error("IntegratedWallet: Using Droplet API mode");
|
|
30
|
+
} else if (config.paymentKey) {
|
|
31
|
+
this.localWallet = new Wallet(config.paymentKey, config.identityKey);
|
|
32
|
+
console.error("IntegratedWallet: Using local wallet mode");
|
|
33
|
+
} else {
|
|
34
|
+
console.error(
|
|
35
|
+
"IntegratedWallet: No wallet configured (limited functionality)",
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
get isDropletMode(): boolean {
|
|
41
|
+
return !!this.dropletClient;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
get hasWallet(): boolean {
|
|
45
|
+
return !!this.localWallet || !!this.dropletClient;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async getBalance(): Promise<number> {
|
|
49
|
+
if (this.dropletClient) {
|
|
50
|
+
const status = await this.dropletClient.getFaucetStatus();
|
|
51
|
+
return status.balance_satoshis;
|
|
52
|
+
}
|
|
53
|
+
if (this.localWallet) {
|
|
54
|
+
return this.localWallet.getTotalBalance();
|
|
55
|
+
}
|
|
56
|
+
throw new Error("No wallet configured");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async sendToAddress(
|
|
60
|
+
address: string,
|
|
61
|
+
satoshis: number,
|
|
62
|
+
description?: string,
|
|
63
|
+
): Promise<{ txid: string }> {
|
|
64
|
+
if (this.dropletClient) {
|
|
65
|
+
// For Droplet API, we use the tap endpoint which sends the faucet's fixed amount
|
|
66
|
+
// Note: Droplet API doesn't support custom amounts, it uses fixed_drop_sats
|
|
67
|
+
const response = await this.dropletClient.tap(address);
|
|
68
|
+
return { txid: response.txid };
|
|
69
|
+
}
|
|
70
|
+
if (this.localWallet) {
|
|
71
|
+
const tx = await this.localWallet.createAction({
|
|
72
|
+
description: description || "Send to address",
|
|
73
|
+
outputs: [
|
|
74
|
+
{
|
|
75
|
+
lockingScript: new P2PKH().lock(address).toHex(),
|
|
76
|
+
satoshis,
|
|
77
|
+
outputDescription: `Payment to ${address}`,
|
|
78
|
+
},
|
|
79
|
+
],
|
|
80
|
+
});
|
|
81
|
+
return { txid: tx.txid };
|
|
82
|
+
}
|
|
83
|
+
throw new Error("No wallet configured");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async pushData(data: string[], encoding = "hex"): Promise<{ txid: string }> {
|
|
87
|
+
if (this.dropletClient) {
|
|
88
|
+
const response = await this.dropletClient.push(data, encoding);
|
|
89
|
+
return { txid: response.txid };
|
|
90
|
+
}
|
|
91
|
+
if (this.localWallet) {
|
|
92
|
+
// For local wallet, we need to implement OP_RETURN transaction
|
|
93
|
+
// This is a simplified version - you might want to enhance this
|
|
94
|
+
const opReturnScript = Script.fromASM(
|
|
95
|
+
`OP_FALSE OP_RETURN ${data.join(" ")}`,
|
|
96
|
+
);
|
|
97
|
+
const tx = await this.localWallet.createAction({
|
|
98
|
+
description: "Push data",
|
|
99
|
+
outputs: [
|
|
100
|
+
{
|
|
101
|
+
lockingScript: opReturnScript.toHex(),
|
|
102
|
+
satoshis: 0,
|
|
103
|
+
outputDescription: "OP_RETURN data",
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
return { txid: tx.txid };
|
|
108
|
+
}
|
|
109
|
+
throw new Error("No wallet configured");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Delegate other methods to the appropriate implementation
|
|
113
|
+
getLocalWallet(): Wallet | undefined {
|
|
114
|
+
return this.localWallet;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
getDropletClient(): DropletClient | undefined {
|
|
118
|
+
return this.dropletClient;
|
|
119
|
+
}
|
|
120
|
+
}
|