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.
Files changed (71) hide show
  1. package/CHANGELOG.md +111 -0
  2. package/README.md +233 -4
  3. package/dist/index.js +100444 -51575
  4. package/index.ts +808 -87
  5. package/next-env.d.ts +6 -0
  6. package/package.json +53 -14
  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,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
+ });