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