bsv-mcp 0.0.36 → 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.
- package/CHANGELOG.md +111 -0
- package/README.md +233 -4
- package/dist/index.js +100444 -51575
- package/index.ts +808 -87
- package/next-env.d.ts +6 -0
- package/package.json +53 -14
- 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/bap/friend.ts +267 -0
- package/tools/bap/generate.test.ts +350 -0
- package/tools/bap/generate.ts +433 -0
- package/tools/bap/getCurrentAddress.ts +76 -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/bigblocks/components.ts +304 -0
- package/tools/bigblocks/docs.ts +488 -0
- package/tools/bigblocks/examples.ts +527 -0
- package/tools/bigblocks/generator.ts +485 -0
- package/tools/bigblocks/index.ts +23 -0
- package/tools/bsocial/bigblocksApiClient.ts +189 -0
- package/tools/bsocial/bmapFollow.ts +125 -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 +5 -1
- package/tools/constants.ts +21 -0
- package/tools/index.ts +103 -17
- package/tools/mnee/index.ts +8 -0
- package/tools/mnee/parseTx.ts +1 -1
- package/tools/mnee/sendMnee.ts +1 -1
- 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 +45 -1
- 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 +8 -4
- package/tools/wallet/a2bPublishMcp.ts +28 -24
- package/tools/wallet/createOrdinals.ts +4 -2
- package/tools/wallet/fetchPaymentUtxos.ts +44 -43
- package/tools/wallet/gatherCollectionInfo.ts +374 -0
- package/tools/wallet/getAddress.ts +13 -21
- package/tools/wallet/getBalance.ts +66 -0
- package/tools/wallet/getBalanceDroplet.ts +59 -0
- package/tools/wallet/integratedWallet.ts +120 -0
- package/tools/wallet/mintCollection.ts +378 -0
- package/tools/wallet/purchaseListing.ts +4 -4
- package/tools/wallet/schemas.ts +18 -249
- package/tools/wallet/sendOrdinals.ts +4 -2
- package/tools/wallet/sendToAddress.ts +5 -1
- package/tools/wallet/setupDroplet.ts +172 -0
- package/tools/wallet/tools.ts +71 -156
- package/tools/wallet/transactionHelper.ts +121 -0
- package/tools/wallet/transferOrdToken.ts +7 -4
- package/tools/wallet/utxo.ts +62 -0
- package/tools/wallet/wallet.ts +237 -234
- package/tools/a2b/call.ts +0 -32
|
@@ -0,0 +1,433 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import {
|
|
4
|
+
Utils as BSVUtils,
|
|
5
|
+
fromUtxo,
|
|
6
|
+
HD,
|
|
7
|
+
isBroadcastFailure,
|
|
8
|
+
P2PKH,
|
|
9
|
+
PrivateKey,
|
|
10
|
+
Script,
|
|
11
|
+
Transaction,
|
|
12
|
+
} from "@bsv/sdk";
|
|
13
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
15
|
+
import { BAP } from "bsv-bap";
|
|
16
|
+
import { z } from "zod";
|
|
17
|
+
import { V5Broadcaster } from "../../utils/broadcaster";
|
|
18
|
+
import { SecureKeyManager } from "../../utils/keyManager";
|
|
19
|
+
import { BAP_PREFIX } from "../constants";
|
|
20
|
+
import { fetchPaymentUtxos } from "../wallet/fetchPaymentUtxos";
|
|
21
|
+
|
|
22
|
+
// Get toArray from BSV SDK Utils
|
|
23
|
+
const { toArray } = BSVUtils;
|
|
24
|
+
|
|
25
|
+
const KEY_DIR = path.join(os.homedir(), ".bsv-mcp");
|
|
26
|
+
const KEY_FILE_PATH = path.join(KEY_DIR, "keys.json");
|
|
27
|
+
|
|
28
|
+
const bapGenerateArgsSchema = z
|
|
29
|
+
.object({
|
|
30
|
+
alternateName: z.string().optional(),
|
|
31
|
+
description: z.string().optional(),
|
|
32
|
+
})
|
|
33
|
+
.optional();
|
|
34
|
+
|
|
35
|
+
export type BapGenerateArgs = z.infer<typeof bapGenerateArgsSchema>;
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Generates a new BAP HD master key and derives the first identity,
|
|
39
|
+
* saving the keys to secure storage.
|
|
40
|
+
* Attempts to register the ID on-chain, unless broadcasting is disabled.
|
|
41
|
+
*/
|
|
42
|
+
async function generateBapKeys(
|
|
43
|
+
args?: BapGenerateArgs & { disableBroadcasting?: boolean },
|
|
44
|
+
): Promise<{
|
|
45
|
+
success: boolean;
|
|
46
|
+
xprv?: string;
|
|
47
|
+
identityAddress?: string;
|
|
48
|
+
identityKey?: string;
|
|
49
|
+
error?: string;
|
|
50
|
+
message?: string;
|
|
51
|
+
txid?: string;
|
|
52
|
+
rawTx?: string;
|
|
53
|
+
}> {
|
|
54
|
+
const keyManager = new SecureKeyManager({ keyDir: KEY_DIR });
|
|
55
|
+
let tempXprv = "";
|
|
56
|
+
let identityPk: string;
|
|
57
|
+
let payPkToPreserve: PrivateKey | undefined;
|
|
58
|
+
|
|
59
|
+
// 1. Load existing keys and check preconditions
|
|
60
|
+
try {
|
|
61
|
+
let keys: { payPk?: PrivateKey; identityPk?: PrivateKey; xprv?: string };
|
|
62
|
+
let source: string;
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
const result = await keyManager.loadKeys();
|
|
66
|
+
keys = result.keys;
|
|
67
|
+
source = result.source;
|
|
68
|
+
} catch (loadError) {
|
|
69
|
+
const errorMsg =
|
|
70
|
+
loadError instanceof Error ? loadError.message : String(loadError);
|
|
71
|
+
console.error(
|
|
72
|
+
`ERROR: Failed to read keys from secure storage: ${errorMsg}`,
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
// Check for specific error patterns
|
|
76
|
+
if (
|
|
77
|
+
errorMsg.includes("JSON Parse error") ||
|
|
78
|
+
errorMsg.includes("JSON.parse")
|
|
79
|
+
) {
|
|
80
|
+
// Extract just the JSON parse error part
|
|
81
|
+
const match = errorMsg.match(/JSON Parse error[^.]*\.?/);
|
|
82
|
+
const jsonError = match ? match[0] : "JSON Parse error";
|
|
83
|
+
return {
|
|
84
|
+
success: false,
|
|
85
|
+
error: `Could not read or parse keys.json: ${jsonError}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
success: false,
|
|
91
|
+
error: `Failed to read keys: ${errorMsg}`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!keys.payPk) {
|
|
96
|
+
console.error(
|
|
97
|
+
"ERROR: Payment Private Key (payPk) does not exist in secure storage.",
|
|
98
|
+
);
|
|
99
|
+
const errorMessage =
|
|
100
|
+
source === "none"
|
|
101
|
+
? "keys.json not found. Payment Private Key (payPk) is required."
|
|
102
|
+
: "Payment Private Key (payPk) does not exist in keys.json.";
|
|
103
|
+
return {
|
|
104
|
+
success: false,
|
|
105
|
+
error: errorMessage,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
payPkToPreserve = keys.payPk;
|
|
110
|
+
|
|
111
|
+
if (keys.xprv) {
|
|
112
|
+
console.error(
|
|
113
|
+
"ERROR: BAP Master Key (xprv) already exists in secure storage.",
|
|
114
|
+
);
|
|
115
|
+
return {
|
|
116
|
+
success: false,
|
|
117
|
+
error: "BAP Master Key (xprv) already exists in keys.json.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (keys.identityPk) {
|
|
122
|
+
console.error(
|
|
123
|
+
"ERROR: BAP Identity Key (identityPk) already exists. Cannot generate HD key.",
|
|
124
|
+
);
|
|
125
|
+
return {
|
|
126
|
+
success: false,
|
|
127
|
+
error: "BAP Identity Key (identityPk) already exists in keys.json.",
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const status = keyManager.getStatus();
|
|
132
|
+
if (source === "legacy" && !status.hasEncrypted) {
|
|
133
|
+
console.warn(
|
|
134
|
+
"WARN: Using unencrypted keys. Run the server again to encrypt them.",
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
} catch (fileError) {
|
|
138
|
+
console.error("ERROR: Failed to read keys from secure storage:", fileError);
|
|
139
|
+
return {
|
|
140
|
+
success: false,
|
|
141
|
+
error: `Failed to read keys: ${fileError instanceof Error ? fileError.message : String(fileError)}`,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
const payPk = payPkToPreserve;
|
|
147
|
+
const paymentAddress = payPk.toAddress();
|
|
148
|
+
|
|
149
|
+
// 2. Generate HD Key
|
|
150
|
+
const hdKey = HD.fromRandom();
|
|
151
|
+
tempXprv = hdKey.toString();
|
|
152
|
+
|
|
153
|
+
// 3. Create BAP instance and generate identity
|
|
154
|
+
const bapInstance = new BAP(tempXprv);
|
|
155
|
+
|
|
156
|
+
// Create identity attributes
|
|
157
|
+
const identityAttributes = {
|
|
158
|
+
name: {
|
|
159
|
+
value: args?.alternateName || "Anonymous User",
|
|
160
|
+
nonce: BSVUtils.toHex(BSVUtils.toArray(Math.random().toString())),
|
|
161
|
+
},
|
|
162
|
+
description: {
|
|
163
|
+
value: args?.description || "A BAP identity managed by BSV-MCP.",
|
|
164
|
+
nonce: BSVUtils.toHex(BSVUtils.toArray(Math.random().toString())),
|
|
165
|
+
},
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Create new ID with the BAP instance
|
|
169
|
+
const bapId = bapInstance.newId(undefined, identityAttributes);
|
|
170
|
+
|
|
171
|
+
// Get the generated identity information
|
|
172
|
+
const generatedIdentityKey = bapId.getIdentityKey();
|
|
173
|
+
const generatedIdentityAddress = bapId.rootAddress;
|
|
174
|
+
|
|
175
|
+
// Export member backup to get the private key
|
|
176
|
+
const memberBackup = bapId.exportMemberBackup();
|
|
177
|
+
identityPk = memberBackup.derivedPrivateKey;
|
|
178
|
+
|
|
179
|
+
console.error(`INFO: Generated BAP identity key: ${generatedIdentityKey}`);
|
|
180
|
+
console.error(
|
|
181
|
+
`INFO: Generated BAP identity address: ${generatedIdentityAddress}`,
|
|
182
|
+
);
|
|
183
|
+
|
|
184
|
+
// 5. Save Keys to secure storage
|
|
185
|
+
const updatedKeys = {
|
|
186
|
+
payPk: payPkToPreserve,
|
|
187
|
+
identityPk: PrivateKey.fromWif(identityPk),
|
|
188
|
+
xprv: tempXprv,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
await keyManager.saveKeys(updatedKeys);
|
|
192
|
+
const status = keyManager.getStatus();
|
|
193
|
+
|
|
194
|
+
if (status.hasEncrypted) {
|
|
195
|
+
console.error(
|
|
196
|
+
`INFO: BAP HD Master Key and initial Identity Key have been generated and saved (encrypted) to ${KEY_DIR}/keys.bep`,
|
|
197
|
+
);
|
|
198
|
+
} else {
|
|
199
|
+
console.error(
|
|
200
|
+
`INFO: BAP HD Master Key and initial Identity Key have been generated and saved to ${KEY_FILE_PATH}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// Create the ID registration transaction output
|
|
205
|
+
// BAP ID format: OP_0 OP_RETURN <BAP_PREFIX> "ID" <identity_key> <root_address> <current_address>
|
|
206
|
+
const idPayload = [
|
|
207
|
+
toArray(BAP_PREFIX, "utf8"),
|
|
208
|
+
toArray("ID"),
|
|
209
|
+
toArray(generatedIdentityKey),
|
|
210
|
+
toArray(bapId.rootAddress),
|
|
211
|
+
toArray(bapId.rootAddress), // For initial registration, current = root
|
|
212
|
+
];
|
|
213
|
+
|
|
214
|
+
// Sign the ID payload with AIP
|
|
215
|
+
const signedIdPayload = bapId.signOpReturnWithAIP(idPayload);
|
|
216
|
+
|
|
217
|
+
// Convert to hex strings for Script
|
|
218
|
+
const idHexStrings = signedIdPayload.map((bytes) =>
|
|
219
|
+
BSVUtils.toHex(bytes as number[]),
|
|
220
|
+
);
|
|
221
|
+
const idAsmString = `OP_0 OP_RETURN ${idHexStrings.join(" ")}`;
|
|
222
|
+
const idScript = Script.fromASM(idAsmString);
|
|
223
|
+
|
|
224
|
+
// Create the ALIAS transaction output
|
|
225
|
+
// First create the structured data object for the alias
|
|
226
|
+
const aliasData = {
|
|
227
|
+
"@context": "https://schema.org",
|
|
228
|
+
"@type": "Person",
|
|
229
|
+
name: args?.alternateName || "Anonymous User",
|
|
230
|
+
description: args?.description || "A BAP identity managed by BSV-MCP.",
|
|
231
|
+
url: `bitcoin:${paymentAddress}`,
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const aliasPayload = [
|
|
235
|
+
toArray(BAP_PREFIX, "utf8"),
|
|
236
|
+
toArray("ALIAS"),
|
|
237
|
+
toArray(generatedIdentityKey),
|
|
238
|
+
toArray(JSON.stringify(aliasData)),
|
|
239
|
+
];
|
|
240
|
+
|
|
241
|
+
// Sign the ALIAS payload with AIP
|
|
242
|
+
const signedAliasPayload = bapId.signOpReturnWithAIP(aliasPayload);
|
|
243
|
+
|
|
244
|
+
// Convert to hex strings for Script
|
|
245
|
+
const aliasHexStrings = signedAliasPayload.map((bytes) =>
|
|
246
|
+
BSVUtils.toHex(bytes as number[]),
|
|
247
|
+
);
|
|
248
|
+
const aliasAsmString = `OP_0 OP_RETURN ${aliasHexStrings.join(" ")}`;
|
|
249
|
+
const aliasScript = Script.fromASM(aliasAsmString);
|
|
250
|
+
|
|
251
|
+
// 6. Build Transaction
|
|
252
|
+
const tx = new Transaction();
|
|
253
|
+
// Add the ID registration output
|
|
254
|
+
tx.addOutput({
|
|
255
|
+
lockingScript: idScript,
|
|
256
|
+
satoshis: 0,
|
|
257
|
+
});
|
|
258
|
+
// Add the ALIAS output
|
|
259
|
+
tx.addOutput({
|
|
260
|
+
lockingScript: aliasScript,
|
|
261
|
+
satoshis: 0,
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
console.error(
|
|
265
|
+
`INFO: Fetching UTXOs for payment address: ${paymentAddress}`,
|
|
266
|
+
);
|
|
267
|
+
const paymentUtxos = await fetchPaymentUtxos(paymentAddress);
|
|
268
|
+
|
|
269
|
+
if (!paymentUtxos || paymentUtxos.length === 0) {
|
|
270
|
+
console.error(
|
|
271
|
+
`ERROR: No UTXOs found for payment address ${paymentAddress}. Cannot fund BAP registration.`,
|
|
272
|
+
);
|
|
273
|
+
// Keys were generated and saved, but no UTXOs to broadcast
|
|
274
|
+
return {
|
|
275
|
+
success: true,
|
|
276
|
+
identityAddress: generatedIdentityAddress,
|
|
277
|
+
identityKey: generatedIdentityKey,
|
|
278
|
+
message:
|
|
279
|
+
"BAP HD Key and Initial Identity Generated & Saved to secure storage. No UTXOs available to fund registration.",
|
|
280
|
+
error:
|
|
281
|
+
"No UTXOs found for payment address. Cannot fund BAP registration.",
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
console.error(`INFO: Found ${paymentUtxos.length} payment UTXOs.`);
|
|
286
|
+
|
|
287
|
+
// Estimate fee
|
|
288
|
+
let estimatedSize = tx.toHex().length / 2;
|
|
289
|
+
estimatedSize += paymentUtxos.length * 148;
|
|
290
|
+
estimatedSize += 34;
|
|
291
|
+
const estimatedFee = Math.ceil(estimatedSize * 0.05);
|
|
292
|
+
|
|
293
|
+
for (const utxo of paymentUtxos) {
|
|
294
|
+
const input = fromUtxo(utxo, new P2PKH().unlock(payPk));
|
|
295
|
+
tx.addInput(input);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
const totalInputValue = paymentUtxos.reduce(
|
|
299
|
+
(sum: number, utxo: { satoshis: number }) => sum + utxo.satoshis,
|
|
300
|
+
0,
|
|
301
|
+
);
|
|
302
|
+
const changeAmount = totalInputValue - estimatedFee;
|
|
303
|
+
|
|
304
|
+
if (changeAmount > 0) {
|
|
305
|
+
tx.addOutput({
|
|
306
|
+
lockingScript: new P2PKH().lock(paymentAddress),
|
|
307
|
+
satoshis: changeAmount,
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
await tx.sign();
|
|
312
|
+
const rawTxHex = tx.toHex();
|
|
313
|
+
const txid = tx.id("hex");
|
|
314
|
+
|
|
315
|
+
console.error(`INFO: BAP registration transaction created. TXID: ${txid}`);
|
|
316
|
+
console.error(`DEBUG: Raw transaction hex: ${rawTxHex}`);
|
|
317
|
+
|
|
318
|
+
// 7. Broadcast Transaction
|
|
319
|
+
const disableBroadcasting =
|
|
320
|
+
args?.disableBroadcasting || process.env.DISABLE_BROADCASTING === "true";
|
|
321
|
+
|
|
322
|
+
if (disableBroadcasting) {
|
|
323
|
+
console.error(
|
|
324
|
+
"INFO: Broadcasting disabled. Transaction not sent to network.",
|
|
325
|
+
);
|
|
326
|
+
const message = `BAP HD Key and Initial Identity Generated & Saved to secure storage. Registration transaction created but not broadcast (TXID: ${txid}).`;
|
|
327
|
+
return {
|
|
328
|
+
success: true,
|
|
329
|
+
identityAddress: bapId.rootAddress,
|
|
330
|
+
identityKey: generatedIdentityKey,
|
|
331
|
+
txid,
|
|
332
|
+
rawTx: rawTxHex,
|
|
333
|
+
message,
|
|
334
|
+
};
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
try {
|
|
338
|
+
const broadcaster = new V5Broadcaster();
|
|
339
|
+
const broadcastResult = await tx.broadcast(broadcaster);
|
|
340
|
+
|
|
341
|
+
let effectiveTxid = txid;
|
|
342
|
+
if (!isBroadcastFailure(broadcastResult)) {
|
|
343
|
+
effectiveTxid = broadcastResult.txid;
|
|
344
|
+
console.error(
|
|
345
|
+
`INFO: BAP registration transaction broadcast successfully. TXID: ${effectiveTxid}`,
|
|
346
|
+
);
|
|
347
|
+
} else if (isBroadcastFailure(broadcastResult)) {
|
|
348
|
+
const failureTxid = broadcastResult.txid
|
|
349
|
+
? ` (${broadcastResult.txid})`
|
|
350
|
+
: "";
|
|
351
|
+
console.warn(
|
|
352
|
+
`WARN: Transaction broadcast failed${failureTxid}. Code: ${broadcastResult.code}, Description: ${broadcastResult.description}. Original TXID ${txid}.`,
|
|
353
|
+
);
|
|
354
|
+
if (broadcastResult.txid) {
|
|
355
|
+
effectiveTxid = broadcastResult.txid;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const message = `BAP HD Key and Initial Identity Generated & Saved to secure storage. Registration TXID: ${effectiveTxid}.`;
|
|
360
|
+
let overallSuccess = true;
|
|
361
|
+
let errorMessageFromBroadcast: string | undefined;
|
|
362
|
+
if (isBroadcastFailure(broadcastResult)) {
|
|
363
|
+
overallSuccess = false;
|
|
364
|
+
errorMessageFromBroadcast = `Broadcast failed: ${broadcastResult.description} (Code: ${broadcastResult.code})`;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return {
|
|
368
|
+
success: overallSuccess,
|
|
369
|
+
identityAddress: generatedIdentityAddress,
|
|
370
|
+
identityKey: generatedIdentityKey,
|
|
371
|
+
txid: effectiveTxid,
|
|
372
|
+
rawTx: rawTxHex,
|
|
373
|
+
message: isBroadcastFailure(broadcastResult)
|
|
374
|
+
? `${message} Note: ${errorMessageFromBroadcast}`
|
|
375
|
+
: message,
|
|
376
|
+
error: isBroadcastFailure(broadcastResult)
|
|
377
|
+
? errorMessageFromBroadcast
|
|
378
|
+
: undefined,
|
|
379
|
+
};
|
|
380
|
+
} catch (broadcastError) {
|
|
381
|
+
const errorMsg =
|
|
382
|
+
broadcastError instanceof Error
|
|
383
|
+
? broadcastError.message
|
|
384
|
+
: String(broadcastError);
|
|
385
|
+
console.error(
|
|
386
|
+
`ERROR: Failed to broadcast BAP registration transaction ${txid}: ${errorMsg}`,
|
|
387
|
+
);
|
|
388
|
+
// Keys were generated and saved, but broadcast failed
|
|
389
|
+
return {
|
|
390
|
+
success: true,
|
|
391
|
+
identityAddress: generatedIdentityAddress,
|
|
392
|
+
identityKey: generatedIdentityKey,
|
|
393
|
+
txid,
|
|
394
|
+
rawTx: rawTxHex,
|
|
395
|
+
message: `Keys generated and saved, but failed to broadcast BAP registration transaction: ${errorMsg}`,
|
|
396
|
+
error: `Broadcast failed: ${errorMsg}. You can manually broadcast the raw transaction.`,
|
|
397
|
+
};
|
|
398
|
+
}
|
|
399
|
+
} catch (error) {
|
|
400
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
401
|
+
console.error(
|
|
402
|
+
`ERROR: Failed to generate BAP HD key or register identity: ${errorMsg}`,
|
|
403
|
+
);
|
|
404
|
+
return {
|
|
405
|
+
success: false,
|
|
406
|
+
error: `Failed during BAP key generation/saving or transaction construction: ${errorMsg}`,
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Registers the bap_generate tool
|
|
413
|
+
*/
|
|
414
|
+
export function registerBapGenerateTool(server: McpServer) {
|
|
415
|
+
server.tool(
|
|
416
|
+
"bap_generate",
|
|
417
|
+
"Generates a BAP HD master key AND derives the first identity key if no BAP keys (xprv or identityPk) exist. Saves keys to secure storage. Attempts on-chain registration using payPk (honors DISABLE_BROADCASTING). Optionally takes alternateName and description for the profile.",
|
|
418
|
+
{ args: bapGenerateArgsSchema },
|
|
419
|
+
async ({ args }: { args?: BapGenerateArgs }): Promise<CallToolResult> => {
|
|
420
|
+
try {
|
|
421
|
+
const result = await generateBapKeys(args);
|
|
422
|
+
// Format result as JSON
|
|
423
|
+
return {
|
|
424
|
+
content: [{ type: "text", text: JSON.stringify(result, null, 2) }],
|
|
425
|
+
isError: !result.success,
|
|
426
|
+
};
|
|
427
|
+
} catch (e) {
|
|
428
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
429
|
+
return { content: [{ type: "text", text: msg }], isError: true };
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
);
|
|
433
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { PrivateKey } from "@bsv/sdk";
|
|
2
|
+
import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import type { Wallet } from "../wallet/wallet";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Register the tool to get the BAP identity address
|
|
8
|
+
* @param server The MCP server instance
|
|
9
|
+
* @param identityPk The optional identity private key
|
|
10
|
+
*/
|
|
11
|
+
export function registerBapGetCurrentAddressTool(
|
|
12
|
+
server: McpServer,
|
|
13
|
+
identityPk?: PrivateKey,
|
|
14
|
+
): void {
|
|
15
|
+
server.tool(
|
|
16
|
+
"bap_getCurrentAddress",
|
|
17
|
+
"Retrieves the current BAP identity's Bitcoin SV address. This address is derived from the server's configured identity key.",
|
|
18
|
+
{
|
|
19
|
+
args: z
|
|
20
|
+
.object({}) // No arguments needed
|
|
21
|
+
.optional()
|
|
22
|
+
.describe(
|
|
23
|
+
"No parameters required - simply returns the current BAP identity address",
|
|
24
|
+
),
|
|
25
|
+
},
|
|
26
|
+
async () => {
|
|
27
|
+
try {
|
|
28
|
+
let pkToUse = identityPk;
|
|
29
|
+
|
|
30
|
+
// Fallback to environment variable if identityPk wasn't provided
|
|
31
|
+
if (!pkToUse) {
|
|
32
|
+
const identityKeyWifEnv = process.env.IDENTITY_KEY_WIF;
|
|
33
|
+
if (identityKeyWifEnv) {
|
|
34
|
+
try {
|
|
35
|
+
pkToUse = PrivateKey.fromWif(identityKeyWifEnv);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
// Let the final check handle the error
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!pkToUse) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
"Could not retrieve BAP identity address. Identity key not configured.",
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const address = pkToUse.toAddress().toString();
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
content: [
|
|
52
|
+
{
|
|
53
|
+
type: "text",
|
|
54
|
+
text: JSON.stringify({ address, status: "ok" }),
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
};
|
|
58
|
+
} catch (err: unknown) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
return {
|
|
61
|
+
content: [
|
|
62
|
+
{
|
|
63
|
+
type: "text",
|
|
64
|
+
text: JSON.stringify({
|
|
65
|
+
error: "Failed to get BAP identity address",
|
|
66
|
+
message: msg,
|
|
67
|
+
status: "error",
|
|
68
|
+
}),
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
isError: true,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, spyOn } from "bun:test";
|
|
2
|
+
import { fetchProfile } from "./getId";
|
|
3
|
+
import type { IdentityData, SigmaIdentityProfile } from "./types";
|
|
4
|
+
|
|
5
|
+
// Store the original fetch function
|
|
6
|
+
const originalFetch = global.fetch;
|
|
7
|
+
let fetchMock: ReturnType<typeof spyOn>;
|
|
8
|
+
|
|
9
|
+
describe("BAP getId - fetchProfile", () => {
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
// Create a new spy for global.fetch before each test
|
|
12
|
+
// and assign it to global.fetch
|
|
13
|
+
fetchMock = spyOn(globalThis, "fetch");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(() => {
|
|
17
|
+
// Restore the original fetch function after each test
|
|
18
|
+
fetchMock.mockRestore();
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("should return identity data for a valid idKey", async () => {
|
|
22
|
+
const mockIdKey = "testIdKey";
|
|
23
|
+
const mockIdentityData: IdentityData = {
|
|
24
|
+
"@context": ["https://w3id.org/did/v0.11", "https://w3id.org/bap/v1"],
|
|
25
|
+
id: `did:bap:id:${mockIdKey}`,
|
|
26
|
+
publicKey: [
|
|
27
|
+
{
|
|
28
|
+
id: `did:bap:id:${mockIdKey}#root`,
|
|
29
|
+
controller: `did:bap:id:${mockIdKey}`,
|
|
30
|
+
type: "EcdsaSecp256k1VerificationKey2019",
|
|
31
|
+
bitcoinAddress: "1SomeBitcoinAddress",
|
|
32
|
+
},
|
|
33
|
+
],
|
|
34
|
+
authentication: ["#root"],
|
|
35
|
+
assertionMethod: ["#root"],
|
|
36
|
+
};
|
|
37
|
+
const mockApiResponse: SigmaIdentityProfile = { result: mockIdentityData };
|
|
38
|
+
|
|
39
|
+
// fetchMock.mockResolvedValueOnce({
|
|
40
|
+
fetchMock.mockImplementationOnce(async () => ({
|
|
41
|
+
ok: true,
|
|
42
|
+
json: async () => mockApiResponse,
|
|
43
|
+
text: async () => JSON.stringify(mockApiResponse),
|
|
44
|
+
status: 200,
|
|
45
|
+
}));
|
|
46
|
+
|
|
47
|
+
const profile = await fetchProfile(mockIdKey);
|
|
48
|
+
expect(profile).toEqual(mockIdentityData);
|
|
49
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(fetchMock).toHaveBeenCalledWith(
|
|
51
|
+
expect.stringContaining("/identity/get"),
|
|
52
|
+
expect.objectContaining({
|
|
53
|
+
method: "POST",
|
|
54
|
+
body: JSON.stringify({ idKey: mockIdKey }),
|
|
55
|
+
}),
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return null if Sigma API returns 404", async () => {
|
|
60
|
+
const mockIdKey = "nonExistentIdKey";
|
|
61
|
+
// fetchMock.mockResolvedValueOnce({
|
|
62
|
+
fetchMock.mockImplementationOnce(async () => ({
|
|
63
|
+
ok: false,
|
|
64
|
+
status: 404,
|
|
65
|
+
json: async () => ({ error: "not found" }), // Mock error body for 404
|
|
66
|
+
text: async () => "not found",
|
|
67
|
+
}));
|
|
68
|
+
|
|
69
|
+
const profile = await fetchProfile(mockIdKey);
|
|
70
|
+
expect(profile).toBeNull();
|
|
71
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it("should throw an error if Sigma API request fails (non-404)", async () => {
|
|
75
|
+
const mockIdKey = "errorIdKey";
|
|
76
|
+
// fetchMock.mockResolvedValueOnce({
|
|
77
|
+
fetchMock.mockImplementationOnce(async () => ({
|
|
78
|
+
ok: false,
|
|
79
|
+
status: 500,
|
|
80
|
+
json: async () => ({ error: "server error" }),
|
|
81
|
+
text: async () => "server error",
|
|
82
|
+
}));
|
|
83
|
+
|
|
84
|
+
await expect(fetchProfile(mockIdKey)).rejects.toThrow(
|
|
85
|
+
"Sigma API request failed with status 500: server error",
|
|
86
|
+
);
|
|
87
|
+
expect(fetchMock).toHaveBeenCalledTimes(1);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should throw an error if Sigma API response is malformed (missing result)", async () => {
|
|
91
|
+
const mockIdKey = "malformedIdKey";
|
|
92
|
+
// fetchMock.mockResolvedValueOnce({
|
|
93
|
+
fetchMock.mockImplementationOnce(async () => ({
|
|
94
|
+
ok: true,
|
|
95
|
+
status: 200,
|
|
96
|
+
json: async () => ({}), // Empty object, missing 'result'
|
|
97
|
+
text: async () => "{}",
|
|
98
|
+
}));
|
|
99
|
+
|
|
100
|
+
await expect(fetchProfile(mockIdKey)).rejects.toThrow(
|
|
101
|
+
"Sigma API response is missing the 'result' field.",
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should throw an error if Sigma API response is not valid JSON", async () => {
|
|
106
|
+
const mockIdKey = "invalidJsonIdKey";
|
|
107
|
+
// fetchMock.mockResolvedValueOnce({
|
|
108
|
+
fetchMock.mockImplementationOnce(async () => ({
|
|
109
|
+
ok: true,
|
|
110
|
+
status: 200,
|
|
111
|
+
json: async () => {
|
|
112
|
+
throw new Error("Invalid JSON");
|
|
113
|
+
}, // Simulate JSON parsing error
|
|
114
|
+
text: async () => "This is not JSON",
|
|
115
|
+
}));
|
|
116
|
+
|
|
117
|
+
expect(fetchProfile(mockIdKey)).rejects.toThrow(
|
|
118
|
+
"Failed to parse Sigma API response: Invalid JSON",
|
|
119
|
+
);
|
|
120
|
+
});
|
|
121
|
+
});
|