bsv-mcp 0.0.35 → 0.2.0

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 (71) hide show
  1. package/CHANGELOG.md +112 -1
  2. package/README.md +233 -4
  3. package/dist/index.js +111762 -0
  4. package/index.ts +804 -86
  5. package/next-env.d.ts +6 -0
  6. package/package.json +57 -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/bap/friend.ts +267 -0
  16. package/tools/bap/generate.test.ts +350 -0
  17. package/tools/bap/generate.ts +433 -0
  18. package/tools/bap/getCurrentAddress.ts +76 -0
  19. package/tools/bap/getId.test.ts +121 -0
  20. package/tools/bap/getId.ts +169 -0
  21. package/tools/bap/index.ts +111 -0
  22. package/tools/bap/types.ts +66 -0
  23. package/tools/bap/utils.ts +33 -0
  24. package/tools/bigblocks/components.ts +304 -0
  25. package/tools/bigblocks/docs.ts +488 -0
  26. package/tools/bigblocks/examples.ts +527 -0
  27. package/tools/bigblocks/generator.ts +485 -0
  28. package/tools/bigblocks/index.ts +23 -0
  29. package/tools/bsocial/bigblocksApiClient.ts +189 -0
  30. package/tools/bsocial/bmapFollow.ts +125 -0
  31. package/tools/bsocial/bmapLikes.ts +111 -0
  32. package/tools/bsocial/bmapReadPosts.ts +148 -0
  33. package/tools/bsocial/createPost.ts +136 -0
  34. package/tools/bsocial/index.ts +45 -0
  35. package/tools/bsocial/readPosts.ts +255 -0
  36. package/tools/bsv/decodeTransaction.ts +5 -1
  37. package/tools/constants.ts +21 -0
  38. package/tools/index.ts +103 -17
  39. package/tools/mnee/index.ts +8 -0
  40. package/tools/mnee/parseTx.ts +1 -1
  41. package/tools/mnee/sendMnee.ts +1 -1
  42. package/tools/utils/aip.ts +46 -0
  43. package/tools/utils/conversion.ts +1 -0
  44. package/tools/utils/errorHandler.ts +76 -0
  45. package/tools/utils/index.ts +45 -1
  46. package/tools/utils/installAgentMaster.ts +84 -0
  47. package/tools/utils/logger.ts +20 -0
  48. package/tools/utils/toolConfig.ts +80 -0
  49. package/tools/utils/toolRegistration.ts +66 -0
  50. package/tools/utils/transactionBuilder.ts +196 -0
  51. package/tools/wallet/a2bPublishAgent.ts +8 -4
  52. package/tools/wallet/a2bPublishMcp.ts +28 -24
  53. package/tools/wallet/createOrdinals.ts +4 -2
  54. package/tools/wallet/fetchPaymentUtxos.ts +44 -43
  55. package/tools/wallet/gatherCollectionInfo.ts +374 -0
  56. package/tools/wallet/getAddress.ts +13 -21
  57. package/tools/wallet/getBalance.ts +66 -0
  58. package/tools/wallet/getBalanceDroplet.ts +59 -0
  59. package/tools/wallet/integratedWallet.ts +120 -0
  60. package/tools/wallet/mintCollection.ts +378 -0
  61. package/tools/wallet/purchaseListing.ts +4 -4
  62. package/tools/wallet/schemas.ts +18 -249
  63. package/tools/wallet/sendOrdinals.ts +4 -2
  64. package/tools/wallet/sendToAddress.ts +5 -1
  65. package/tools/wallet/setupDroplet.ts +172 -0
  66. package/tools/wallet/tools.ts +71 -156
  67. package/tools/wallet/transactionHelper.ts +121 -0
  68. package/tools/wallet/transferOrdToken.ts +7 -4
  69. package/tools/wallet/utxo.ts +62 -0
  70. package/tools/wallet/wallet.ts +237 -234
  71. package/tools/a2b/call.ts +0 -32
@@ -0,0 +1,267 @@
1
+ import {
2
+ type BroadcastFailure,
3
+ type BroadcastResponse,
4
+ fromUtxo,
5
+ isBroadcastFailure,
6
+ isBroadcastResponse,
7
+ P2PKH,
8
+ PrivateKey,
9
+ SatoshisPerKilobyte,
10
+ Script,
11
+ Transaction,
12
+ Utils,
13
+ } from "@bsv/sdk";
14
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
16
+ import type {
17
+ CallToolResult,
18
+ ServerNotification,
19
+ ServerRequest,
20
+ } from "@modelcontextprotocol/sdk/types.js";
21
+ import { BAP, MemberID } from "bsv-bap";
22
+ import { z } from "zod";
23
+ import { BsocialBroadcaster } from "../../utils/broadcaster";
24
+ import {
25
+ friendPrivateKeyFromMemberIdKey,
26
+ friendPublicKeyFromSeedString,
27
+ } from "../../utils/keys";
28
+ import { MAP_PREFIX } from "../constants";
29
+ import {
30
+ fetchPaymentUtxos,
31
+ fetchPaymentUtxosFromV5,
32
+ } from "../wallet/fetchPaymentUtxos";
33
+ import type { Wallet } from "../wallet/wallet";
34
+
35
+ const { toArray, toHex } = Utils;
36
+
37
+ const APP_DOMAIN = "bsv-mcp";
38
+
39
+ export const bapFriendArgsSchema = z.object({
40
+ targetBapId: z.string().min(1, "targetBapId is required."),
41
+ });
42
+
43
+ export type BapFriendArgs = z.infer<typeof bapFriendArgsSchema>;
44
+
45
+ export interface BapFriendConfig {
46
+ disableBroadcasting?: boolean;
47
+ }
48
+
49
+ export function registerBapFriendTool(
50
+ server: McpServer,
51
+ wallet: Wallet,
52
+ xprv: string,
53
+ config?: BapFriendConfig,
54
+ ) {
55
+ server.tool(
56
+ "bap_friend",
57
+ "Initiates a friend request to another BAP ID by broadcasting an on-chain MAP transaction.",
58
+ { args: bapFriendArgsSchema },
59
+ async (
60
+ { args }: { args: BapFriendArgs },
61
+ extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
62
+ ): Promise<CallToolResult> => {
63
+ const { targetBapId } = args;
64
+ const logFunc = console.error;
65
+
66
+ try {
67
+ // --- Preconditions ---
68
+ if (!xprv) {
69
+ return {
70
+ isError: true,
71
+ content: [
72
+ {
73
+ type: "text",
74
+ text: "Server does not have a BAP master key (xprv). Cannot derive friend public key.",
75
+ },
76
+ ],
77
+ };
78
+ }
79
+
80
+ const payPk = wallet.getPaymentKey();
81
+ const identityPk = wallet.getIdentityKey();
82
+
83
+ if (!identityPk || !payPk) {
84
+ return {
85
+ isError: true,
86
+ content: [
87
+ {
88
+ type: "text",
89
+ text: !payPk
90
+ ? "Wallet private key not available. Cannot fund friend request transaction."
91
+ : "Wallet identity key not available. Cannot derive friend public key.",
92
+ },
93
+ ],
94
+ };
95
+ }
96
+
97
+ // 3. Derive Initial Identity
98
+ const bap = new BAP(xprv);
99
+ const idpk = PrivateKey.fromWif(
100
+ bap.newId().exportMemberBackup().derivedPrivateKey,
101
+ );
102
+ const identityInstance = new MemberID(idpk);
103
+ const paymentAddress = payPk.toAddress();
104
+
105
+ // --- Derive friend public key ---
106
+ const friendPubKey = friendPrivateKeyFromMemberIdKey(
107
+ identityPk,
108
+ targetBapId,
109
+ )
110
+ .toPublicKey()
111
+ .toString();
112
+
113
+ // --- Build MAP payload ---
114
+ const payloadParts: (string | number[])[] = [
115
+ MAP_PREFIX,
116
+ "SET",
117
+ "app",
118
+ APP_DOMAIN,
119
+ "type",
120
+ "friend",
121
+ "bapID",
122
+ targetBapId,
123
+ "publicKey",
124
+ friendPubKey,
125
+ ];
126
+
127
+ const payloadBuffers = payloadParts.map(
128
+ (part) => toArray(part) as number[],
129
+ );
130
+
131
+ const signedBuffers =
132
+ identityInstance.signOpReturnWithAIP(payloadBuffers);
133
+ const payloadHex = signedBuffers.map((b) => toHex(b));
134
+ const asmPayload = `OP_0 ${payloadHex.join(" ")}`;
135
+ const opReturnScript = Script.fromASM(asmPayload);
136
+
137
+ // --- Build Transaction ---
138
+ const tx = new Transaction();
139
+ tx.addOutput({ lockingScript: opReturnScript, satoshis: 0 });
140
+
141
+ // --- Fund transaction ---
142
+ const utxos = await fetchPaymentUtxosFromV5(paymentAddress);
143
+ if (!utxos || utxos.length === 0) {
144
+ return {
145
+ isError: true,
146
+ content: [
147
+ {
148
+ type: "text",
149
+ text: `No UTXOs available for payment address ${paymentAddress}. Cannot send friend request.`,
150
+ },
151
+ ],
152
+ };
153
+ }
154
+
155
+ const feeModel = new SatoshisPerKilobyte(10);
156
+ let totalInput = 0n;
157
+ let estFee = await feeModel.computeFee(tx);
158
+
159
+ for (const utxo of utxos) {
160
+ if (totalInput >= BigInt(estFee)) break;
161
+
162
+ const unlockTemplate = new P2PKH().unlock(
163
+ payPk,
164
+ "all",
165
+ false,
166
+ utxo.satoshis,
167
+ Script.fromBinary(toArray(utxo.script, "hex")),
168
+ );
169
+
170
+ const input = fromUtxo(utxo, unlockTemplate);
171
+ tx.addInput(input);
172
+ totalInput += BigInt(utxo.satoshis);
173
+ estFee = await feeModel.computeFee(tx);
174
+ }
175
+
176
+ if (totalInput < BigInt(estFee)) {
177
+ return {
178
+ isError: true,
179
+ content: [
180
+ {
181
+ type: "text",
182
+ text: `Not enough funds to cover fee. Needed ${estFee}, have ${totalInput}.`,
183
+ },
184
+ ],
185
+ };
186
+ }
187
+
188
+ const change = totalInput - BigInt(estFee);
189
+ if (change > 0n) {
190
+ tx.addOutput({
191
+ lockingScript: new P2PKH().lock(paymentAddress),
192
+ satoshis: Number(change),
193
+ change: true,
194
+ });
195
+ }
196
+
197
+ await tx.fee(feeModel);
198
+ await tx.sign();
199
+
200
+ const txHex = tx.toHex();
201
+ const txid = tx.id("hex");
202
+
203
+ if (config?.disableBroadcasting) {
204
+ return {
205
+ content: [
206
+ {
207
+ type: "text",
208
+ text: JSON.stringify({
209
+ success: true,
210
+ disabledBroadcast: true,
211
+ txid,
212
+ rawTx: txHex,
213
+ message: `Broadcasting disabled. Friend request transaction built for ${targetBapId}.`,
214
+ }),
215
+ },
216
+ ],
217
+ };
218
+ }
219
+
220
+ // --- Broadcast ---
221
+ const broadcaster = new BsocialBroadcaster();
222
+ const broadcastResult = await tx.broadcast(broadcaster);
223
+
224
+ let success = true;
225
+ let errorMsg: string | undefined;
226
+ let resultTxid = txid;
227
+
228
+ if (isBroadcastResponse(broadcastResult as BroadcastResponse)) {
229
+ resultTxid = (broadcastResult as BroadcastResponse).txid;
230
+ } else if (isBroadcastFailure(broadcastResult as BroadcastFailure)) {
231
+ success = false;
232
+ const failure = broadcastResult as BroadcastFailure;
233
+ errorMsg = `Broadcast failed: ${failure.description} (Code: ${failure.code})`;
234
+ if (failure.txid) resultTxid = failure.txid;
235
+ }
236
+
237
+ return {
238
+ isError: !success,
239
+ content: [
240
+ {
241
+ type: "text",
242
+ text: JSON.stringify({
243
+ success,
244
+ txid: resultTxid,
245
+ rawTx: txHex,
246
+ message: success
247
+ ? `Friend request sent to ${targetBapId}.`
248
+ : (errorMsg ?? "Broadcast failed"),
249
+ }),
250
+ },
251
+ ],
252
+ };
253
+ } catch (err) {
254
+ const errMsg = err instanceof Error ? err.message : String(err);
255
+ return {
256
+ isError: true,
257
+ content: [
258
+ {
259
+ type: "text",
260
+ text: `Failed to send friend request: ${errMsg}`,
261
+ },
262
+ ],
263
+ };
264
+ }
265
+ },
266
+ );
267
+ }
@@ -0,0 +1,350 @@
1
+ import {
2
+ afterEach,
3
+ beforeEach,
4
+ describe,
5
+ expect,
6
+ it,
7
+ mock,
8
+ spyOn,
9
+ } from "bun:test";
10
+ import fs from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import { HD, PrivateKey, Transaction } from "@bsv/sdk";
14
+ import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
15
+ import type { RequestHandlerExtra } from "@modelcontextprotocol/sdk/shared/protocol.js";
16
+ import type {
17
+ CallToolResult,
18
+ ServerNotification,
19
+ ServerRequest,
20
+ } from "@modelcontextprotocol/sdk/types.js";
21
+ import * as passphrasePromptModule from "../../utils/passphrasePrompt";
22
+ import * as utxoUtils from "../wallet/fetchPaymentUtxos";
23
+ import { type BapGenerateArgs, registerBapGenerateTool } from "./generate";
24
+
25
+ type GenericToolHandler = (
26
+ params: { args: BapGenerateArgs },
27
+ extra: RequestHandlerExtra<ServerRequest, ServerNotification>,
28
+ ) => CallToolResult | Promise<CallToolResult>;
29
+
30
+ const mockMcpServerInstance = {
31
+ tool: (..._args: Parameters<McpServer["tool"]>): void => undefined,
32
+ };
33
+
34
+ const KEY_DIR = path.join(os.homedir(), ".bsv-mcp");
35
+ const KEY_FILE_PATH = path.join(KEY_DIR, "keys.json");
36
+
37
+ const MOCK_PAY_PK_WIF = "L3tg5yk23vBrSGZT8pfrDFQBK8n46eeot2Sf91ezNt7rhyReCA53";
38
+ const MOCK_PAY_ADDRESS = PrivateKey.fromWif(MOCK_PAY_PK_WIF).toAddress();
39
+ const MOCK_KEYS_WITH_PAYPK = { payPk: MOCK_PAY_PK_WIF };
40
+ const MOCK_UTXO_SCRIPT_HASH_PLACEHOLDER =
41
+ "1a2b3c4d5e6f7081920a1b2c3d4e5f607081920a";
42
+ const MOCK_UTXO = {
43
+ txid: "a1b2c3d4".repeat(8),
44
+ vout: 0,
45
+ satoshis: 100000,
46
+ script: `76a914${MOCK_UTXO_SCRIPT_HASH_PLACEHOLDER}88ac`,
47
+ };
48
+
49
+ describe("BAP Generate Tool", () => {
50
+ let readFileSyncSpy: ReturnType<typeof spyOn<typeof fs, "readFileSync">>;
51
+ let writeFileSyncSpy: ReturnType<typeof spyOn<typeof fs, "writeFileSync">>;
52
+ let existsSyncSpy: ReturnType<typeof spyOn<typeof fs, "existsSync">>;
53
+ let mkdirSyncSpy: ReturnType<typeof spyOn<typeof fs, "mkdirSync">>;
54
+ let mockServerToolSpy: ReturnType<
55
+ typeof spyOn<typeof mockMcpServerInstance, "tool">
56
+ >;
57
+ let fetchPaymentUtxosSpy: ReturnType<
58
+ typeof spyOn<typeof utxoUtils, "fetchPaymentUtxos">
59
+ >;
60
+ let transactionBroadcastSpy: ReturnType<
61
+ typeof spyOn<Transaction.prototype, "broadcast">
62
+ >;
63
+ let passphrasePromptSpy: ReturnType<
64
+ typeof spyOn<
65
+ typeof passphrasePromptModule,
66
+ "promptForPassphraseWithFallback"
67
+ >
68
+ >;
69
+
70
+ beforeEach(() => {
71
+ mockServerToolSpy = spyOn(mockMcpServerInstance, "tool");
72
+ mockServerToolSpy.mockClear();
73
+
74
+ readFileSyncSpy = spyOn(fs, "readFileSync");
75
+ writeFileSyncSpy = spyOn(fs, "writeFileSync");
76
+ existsSyncSpy = spyOn(fs, "existsSync");
77
+ mkdirSyncSpy = spyOn(fs, "mkdirSync");
78
+ fetchPaymentUtxosSpy = spyOn(utxoUtils, "fetchPaymentUtxos");
79
+ transactionBroadcastSpy = spyOn(Transaction.prototype, "broadcast");
80
+ passphrasePromptSpy = spyOn(
81
+ passphrasePromptModule,
82
+ "promptForPassphraseWithFallback",
83
+ );
84
+
85
+ // Make existsSync path-aware: .bep files return false (skip encrypted path),
86
+ // .json files return true (use legacy path)
87
+ existsSyncSpy.mockImplementation((filePath: fs.PathLike) => {
88
+ const p = String(filePath);
89
+ if (p.endsWith(".bep") || p.endsWith(".bep.backup")) return false;
90
+ return true;
91
+ });
92
+ // Reject passphrase prompts immediately so SecureKeyManager falls through
93
+ // to legacy/unencrypted code paths (simulates user cancelling the prompt)
94
+ passphrasePromptSpy.mockRejectedValue(new Error("cancelled by test"));
95
+ readFileSyncSpy.mockReturnValue(JSON.stringify(MOCK_KEYS_WITH_PAYPK));
96
+ mkdirSyncSpy.mockImplementation(() => undefined);
97
+ writeFileSyncSpy.mockImplementation(() => undefined);
98
+ fetchPaymentUtxosSpy.mockResolvedValue([MOCK_UTXO]);
99
+ transactionBroadcastSpy.mockResolvedValue(
100
+ `mock_txid_broadcasted_${"a".repeat(40)}`,
101
+ );
102
+ });
103
+
104
+ afterEach(() => {
105
+ mockServerToolSpy.mockRestore();
106
+ readFileSyncSpy.mockRestore();
107
+ writeFileSyncSpy.mockRestore();
108
+ existsSyncSpy.mockRestore();
109
+ mkdirSyncSpy.mockRestore();
110
+ fetchPaymentUtxosSpy.mockRestore();
111
+ transactionBroadcastSpy.mockRestore();
112
+ passphrasePromptSpy.mockRestore();
113
+ });
114
+
115
+ it("should register bap_generate tool with the MCP server", () => {
116
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
117
+ expect(mockServerToolSpy).toHaveBeenCalledTimes(1);
118
+ expect(mockServerToolSpy).toHaveBeenCalledWith(
119
+ "bap_generate",
120
+ expect.any(String),
121
+ { args: expect.any(Object) },
122
+ expect.any(Function),
123
+ );
124
+ });
125
+
126
+ const getRegisteredHandler = (): GenericToolHandler => {
127
+ if (mockServerToolSpy.mock.calls.length === 0) {
128
+ throw new Error("Tool handler not registered or spy not called.");
129
+ }
130
+ return mockServerToolSpy.mock.calls[0][3] as GenericToolHandler;
131
+ };
132
+
133
+ it("should generate keys and TX if payPk exists and no BAP keys, and save all to keys.json", async () => {
134
+ const initialKeys = { payPk: MOCK_PAY_PK_WIF };
135
+ readFileSyncSpy.mockReturnValue(JSON.stringify(initialKeys));
136
+ fetchPaymentUtxosSpy.mockResolvedValue([MOCK_UTXO]);
137
+
138
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
139
+ const handler = getRegisteredHandler();
140
+ const mockExtra = {} as RequestHandlerExtra<
141
+ ServerRequest,
142
+ ServerNotification
143
+ >;
144
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
145
+
146
+ expect(result.isError).toBe(false);
147
+ expect(mkdirSyncSpy).toHaveBeenCalledWith(KEY_DIR, {
148
+ recursive: true,
149
+ mode: 0o700,
150
+ });
151
+ expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
152
+ const writtenData = JSON.parse(
153
+ writeFileSyncSpy.mock.calls[0][1] as string,
154
+ ) as Record<string, string>;
155
+
156
+ expect(writtenData.payPk).toBe(MOCK_PAY_PK_WIF);
157
+ expect(writtenData.xprv).toBeString();
158
+ expect(writtenData.xprv?.startsWith("xprv")).toBe(true);
159
+ expect(writtenData.identityPk).toBeString();
160
+
161
+ expect(result.content?.[0]?.text).toContain(
162
+ "BAP HD Key and Initial Identity Generated & Saved",
163
+ );
164
+ expect(result.content?.[0]?.text).toContain("Registration TXID:");
165
+ expect(transactionBroadcastSpy).toHaveBeenCalled();
166
+ });
167
+
168
+ it("should return error if keys.json does not exist", async () => {
169
+ existsSyncSpy.mockReturnValue(false);
170
+
171
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
172
+ const handler = getRegisteredHandler();
173
+ const mockExtra = {} as RequestHandlerExtra<
174
+ ServerRequest,
175
+ ServerNotification
176
+ >;
177
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
178
+
179
+ expect(result.isError).toBe(true);
180
+ expect(result.content?.[0]?.text).toContain(
181
+ "keys.json not found. Payment Private Key (payPk) is required.",
182
+ );
183
+ expect(writeFileSyncSpy).not.toHaveBeenCalled();
184
+ });
185
+
186
+ it("should return error if payPk is missing from keys.json", async () => {
187
+ readFileSyncSpy.mockReturnValue(JSON.stringify({ someOtherKey: "value" }));
188
+
189
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
190
+ const handler = getRegisteredHandler();
191
+ const mockExtra = {} as RequestHandlerExtra<
192
+ ServerRequest,
193
+ ServerNotification
194
+ >;
195
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
196
+
197
+ expect(result.isError).toBe(true);
198
+ expect(result.content?.[0]?.text).toContain(
199
+ "Payment Private Key (payPk) does not exist in keys.json.",
200
+ );
201
+ expect(writeFileSyncSpy).not.toHaveBeenCalled();
202
+ });
203
+
204
+ it("should return error if xprv already exists", async () => {
205
+ const existingKeys = {
206
+ payPk: MOCK_PAY_PK_WIF,
207
+ xprv: "already_exists_xprv",
208
+ };
209
+ readFileSyncSpy.mockReturnValue(JSON.stringify(existingKeys));
210
+
211
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
212
+ const handler = getRegisteredHandler();
213
+ const mockExtra = {} as RequestHandlerExtra<
214
+ ServerRequest,
215
+ ServerNotification
216
+ >;
217
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
218
+
219
+ expect(result.isError).toBe(true);
220
+ expect(result.content?.[0]?.text).toContain(
221
+ "BAP Master Key (xprv) already exists in keys.json.",
222
+ );
223
+ expect(writeFileSyncSpy).not.toHaveBeenCalled();
224
+ });
225
+
226
+ it("should return error if identityPk already exists", async () => {
227
+ const existingKeys = {
228
+ payPk: MOCK_PAY_PK_WIF,
229
+ identityPk: "L5BBNhvVgvpdV1AkMJMNGy89MHEUnyiKhrs11GNAUxWjXvhxgcAg",
230
+ };
231
+ readFileSyncSpy.mockReturnValue(JSON.stringify(existingKeys));
232
+
233
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
234
+ const handler = getRegisteredHandler();
235
+ const mockExtra = {} as RequestHandlerExtra<
236
+ ServerRequest,
237
+ ServerNotification
238
+ >;
239
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
240
+
241
+ expect(result.isError).toBe(true);
242
+ expect(result.content?.[0]?.text).toContain(
243
+ "BAP Identity Key (identityPk) already exists in keys.json.",
244
+ );
245
+ expect(writeFileSyncSpy).not.toHaveBeenCalled();
246
+ });
247
+
248
+ it("should handle HD key generation error gracefully", async () => {
249
+ const originalHDFromRandom = HD.fromRandom;
250
+ HD.fromRandom = () => {
251
+ throw new Error("Test HD generation error");
252
+ };
253
+ readFileSyncSpy.mockReturnValue(JSON.stringify(MOCK_KEYS_WITH_PAYPK));
254
+
255
+ const mockExtra = {} as RequestHandlerExtra<
256
+ ServerRequest,
257
+ ServerNotification
258
+ >;
259
+
260
+ try {
261
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
262
+ const handler = getRegisteredHandler();
263
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
264
+
265
+ expect(result.isError).toBe(true);
266
+ expect(result.content?.[0]?.text).toContain(
267
+ "Failed during BAP key generation/saving or transaction construction: Test HD generation error",
268
+ );
269
+ expect(writeFileSyncSpy).not.toHaveBeenCalled();
270
+ } finally {
271
+ HD.fromRandom = originalHDFromRandom;
272
+ }
273
+ });
274
+
275
+ it("should handle error if keys.json is corrupt", async () => {
276
+ readFileSyncSpy.mockImplementation(() => {
277
+ throw new Error("JSON Parse error: Unexpected identifier 'this'.");
278
+ });
279
+
280
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
281
+ const handler = getRegisteredHandler();
282
+ const mockExtra = {} as RequestHandlerExtra<
283
+ ServerRequest,
284
+ ServerNotification
285
+ >;
286
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
287
+
288
+ expect(result.isError).toBe(true);
289
+ expect(result.content?.[0]?.text).toContain(
290
+ "Could not read or parse keys.json: JSON Parse error",
291
+ );
292
+ expect(writeFileSyncSpy).not.toHaveBeenCalled();
293
+ });
294
+
295
+ it("should handle error if no UTXOs are found for payPk", async () => {
296
+ readFileSyncSpy.mockReturnValue(JSON.stringify(MOCK_KEYS_WITH_PAYPK));
297
+ fetchPaymentUtxosSpy.mockResolvedValue([]);
298
+
299
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
300
+ const handler = getRegisteredHandler();
301
+ const mockExtra = {} as RequestHandlerExtra<
302
+ ServerRequest,
303
+ ServerNotification
304
+ >;
305
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
306
+
307
+ expect(result.isError).toBe(false);
308
+ expect(result.content?.[0]?.text).toContain(
309
+ "No UTXOs found for payment address",
310
+ );
311
+ expect(result.content?.[0]?.text).toContain(
312
+ "Cannot fund BAP registration.",
313
+ );
314
+ expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
315
+ const writtenData = JSON.parse(
316
+ writeFileSyncSpy.mock.calls[0][1] as string,
317
+ ) as Record<string, string>;
318
+ expect(writtenData.xprv).toBeString();
319
+ expect(writtenData.identityPk).toBeString();
320
+ expect(transactionBroadcastSpy).not.toHaveBeenCalled();
321
+ });
322
+
323
+ it("should handle error if transaction broadcast fails", async () => {
324
+ readFileSyncSpy.mockReturnValue(JSON.stringify(MOCK_KEYS_WITH_PAYPK));
325
+ fetchPaymentUtxosSpy.mockResolvedValue([MOCK_UTXO]);
326
+ transactionBroadcastSpy.mockRejectedValue(
327
+ new Error("Broadcast network error"),
328
+ );
329
+
330
+ registerBapGenerateTool(mockMcpServerInstance as unknown as McpServer);
331
+ const handler = getRegisteredHandler();
332
+ const mockExtra = {} as RequestHandlerExtra<
333
+ ServerRequest,
334
+ ServerNotification
335
+ >;
336
+ const result = (await handler({ args: {} }, mockExtra)) as CallToolResult;
337
+
338
+ expect(result.isError).toBe(false);
339
+ expect(result.content?.[0]?.text).toContain(
340
+ "Keys generated and saved, but failed to broadcast BAP registration transaction",
341
+ );
342
+ expect(result.content?.[0]?.text).toContain("Broadcast network error");
343
+ expect(writeFileSyncSpy).toHaveBeenCalledTimes(1);
344
+ const writtenData = JSON.parse(
345
+ writeFileSyncSpy.mock.calls[0][1] as string,
346
+ ) as Record<string, string>;
347
+ expect(writtenData.xprv).toBeString();
348
+ expect(transactionBroadcastSpy).toHaveBeenCalled();
349
+ });
350
+ });