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.
Files changed (78) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/README.md +233 -4
  3. package/dist/index.js +120082 -54669
  4. package/index.ts +1196 -88
  5. package/next-env.d.ts +6 -0
  6. package/package.json +58 -15
  7. package/prompts/bsvSdk/index.ts +5 -5
  8. package/prompts/bsvSdk/transaction.ts +2 -2
  9. package/prompts/bsvSdk/wallet.ts +1 -1
  10. package/prompts/index.ts +1 -1
  11. package/resources/bitcom.ts +132 -0
  12. package/resources/resources.ts +7 -3
  13. package/smithery.yaml +3 -0
  14. package/test-mcp-server.ts +157 -0
  15. package/tools/a2b/discover.ts +13 -13
  16. package/tools/bap/friend.ts +266 -0
  17. package/tools/bap/generate.test.ts +350 -0
  18. package/tools/bap/generate.ts +431 -0
  19. package/tools/bap/getCurrentAddress.ts +69 -0
  20. package/tools/bap/getId.test.ts +121 -0
  21. package/tools/bap/getId.ts +169 -0
  22. package/tools/bap/index.ts +111 -0
  23. package/tools/bap/types.ts +66 -0
  24. package/tools/bap/utils.ts +33 -0
  25. package/tools/bsocial/bmapFollow.ts +121 -0
  26. package/tools/bsocial/bmapLikes.ts +111 -0
  27. package/tools/bsocial/bmapReadPosts.ts +148 -0
  28. package/tools/bsocial/createPost.ts +136 -0
  29. package/tools/bsocial/index.ts +45 -0
  30. package/tools/bsocial/readPosts.ts +255 -0
  31. package/tools/bsv/decodeTransaction.ts +7 -6
  32. package/tools/bsv/explore.ts +2 -3
  33. package/tools/bsv/getPrice.ts +1 -9
  34. package/tools/bsv/token.ts +14 -26
  35. package/tools/constants.ts +21 -0
  36. package/tools/index.ts +91 -18
  37. package/tools/mnee/getBalance.ts +2 -4
  38. package/tools/mnee/index.ts +8 -0
  39. package/tools/mnee/parseTx.ts +4 -4
  40. package/tools/mnee/sendMnee.ts +6 -6
  41. package/tools/ordinals/getInscription.ts +2 -3
  42. package/tools/ordinals/getTokenByIdOrTicker.ts +6 -3
  43. package/tools/ordinals/marketListings.ts +2 -18
  44. package/tools/ordinals/marketSales.ts +2 -4
  45. package/tools/ordinals/searchInscriptions.ts +2 -4
  46. package/tools/utils/aip.ts +46 -0
  47. package/tools/utils/conversion.ts +1 -0
  48. package/tools/utils/errorHandler.ts +76 -0
  49. package/tools/utils/index.ts +56 -14
  50. package/tools/utils/installAgentMaster.ts +84 -0
  51. package/tools/utils/logger.ts +20 -0
  52. package/tools/utils/toolConfig.ts +80 -0
  53. package/tools/utils/toolRegistration.ts +66 -0
  54. package/tools/utils/transactionBuilder.ts +196 -0
  55. package/tools/wallet/a2bPublishAgent.ts +25 -30
  56. package/tools/wallet/a2bPublishMcp.ts +43 -44
  57. package/tools/wallet/createOrdinals.ts +13 -20
  58. package/tools/wallet/fetchPaymentUtxos.ts +48 -39
  59. package/tools/wallet/gatherCollectionInfo.ts +370 -0
  60. package/tools/wallet/getAddress.ts +14 -30
  61. package/tools/wallet/getBalance.ts +54 -0
  62. package/tools/wallet/getBalanceDroplet.ts +48 -0
  63. package/tools/wallet/getPublicKey.ts +3 -16
  64. package/tools/wallet/integratedWallet.ts +120 -0
  65. package/tools/wallet/mintCollection.ts +373 -0
  66. package/tools/wallet/purchaseListing.ts +21 -31
  67. package/tools/wallet/refreshUtxos.ts +2 -14
  68. package/tools/wallet/schemas.ts +18 -249
  69. package/tools/wallet/sendOrdinals.ts +14 -21
  70. package/tools/wallet/sendToAddress.ts +2 -18
  71. package/tools/wallet/setupDroplet.ts +161 -0
  72. package/tools/wallet/tools.ts +55 -210
  73. package/tools/wallet/transactionHelper.ts +121 -0
  74. package/tools/wallet/transferOrdToken.ts +19 -24
  75. package/tools/wallet/utxo.ts +62 -0
  76. package/tools/wallet/wallet.ts +237 -234
  77. package/vite.config.ts +15 -0
  78. 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 { z } from "zod";
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(server: McpServer): void {
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 wif = process.env.PRIVATE_KEY_WIF;
24
- if (!wif) {
25
- return {
26
- content: [
27
- {
28
- type: "text",
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: "Invalid private key",
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
- { args: getPublicKeyArgsSchema },
22
- async (
23
- { args }: { args: GetPublicKeyArgs },
24
- extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
25
- ) => {
11
+ {},
12
+ async () => {
26
13
  try {
27
- const result = await wallet.getPublicKey(args);
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
+ }