@sundaeswap/sprinkles 0.7.0 → 0.8.1

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 (95) hide show
  1. package/README.md +178 -181
  2. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +4 -4
  3. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -1
  4. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +25 -3
  5. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -1
  6. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  7. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  8. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +7 -9
  9. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -1
  10. package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
  11. package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
  12. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
  13. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  14. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
  15. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  16. package/dist/cjs/Sprinkle/actions/builtin/index.js +60 -3
  17. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -1
  18. package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
  19. package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
  20. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
  21. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  22. package/dist/cjs/Sprinkle/actions/cli-adapter.js +20 -2
  23. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -1
  24. package/dist/cjs/Sprinkle/actions/index.js +12 -0
  25. package/dist/cjs/Sprinkle/actions/index.js.map +1 -1
  26. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +146 -4
  27. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -1
  28. package/dist/cjs/Sprinkle/index.js +282 -6
  29. package/dist/cjs/Sprinkle/index.js.map +1 -1
  30. package/dist/cjs/Sprinkle/schemas.js +17 -1
  31. package/dist/cjs/Sprinkle/schemas.js.map +1 -1
  32. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +4 -4
  33. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -1
  34. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +25 -3
  35. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -1
  36. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  37. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  38. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +7 -9
  39. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -1
  40. package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
  41. package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
  42. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
  43. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  44. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
  45. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  46. package/dist/esm/Sprinkle/actions/builtin/index.js +8 -3
  47. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -1
  48. package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
  49. package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
  50. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
  51. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  52. package/dist/esm/Sprinkle/actions/cli-adapter.js +20 -2
  53. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -1
  54. package/dist/esm/Sprinkle/actions/index.js +1 -1
  55. package/dist/esm/Sprinkle/actions/index.js.map +1 -1
  56. package/dist/esm/Sprinkle/actions/mcp-adapter.js +145 -5
  57. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -1
  58. package/dist/esm/Sprinkle/index.js +260 -9
  59. package/dist/esm/Sprinkle/index.js.map +1 -1
  60. package/dist/esm/Sprinkle/schemas.js +16 -0
  61. package/dist/esm/Sprinkle/schemas.js.map +1 -1
  62. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
  63. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
  64. package/dist/types/Sprinkle/actions/builtin/index.d.ts +6 -2
  65. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -1
  66. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
  67. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
  68. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
  69. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
  70. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -1
  71. package/dist/types/Sprinkle/actions/index.d.ts +2 -1
  72. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -1
  73. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +24 -0
  74. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -1
  75. package/dist/types/Sprinkle/index.d.ts +5 -2
  76. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  77. package/dist/types/Sprinkle/schemas.d.ts +72 -0
  78. package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
  79. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  80. package/package.json +1 -1
  81. package/src/Sprinkle/__tests__/builtin-actions.test.ts +4 -4
  82. package/src/Sprinkle/__tests__/cli-adapter.test.ts +24 -3
  83. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
  84. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +7 -5
  85. package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
  86. package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
  87. package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
  88. package/src/Sprinkle/actions/builtin/index.ts +41 -2
  89. package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
  90. package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
  91. package/src/Sprinkle/actions/cli-adapter.ts +18 -2
  92. package/src/Sprinkle/actions/index.ts +2 -1
  93. package/src/Sprinkle/actions/mcp-adapter.ts +179 -4
  94. package/src/Sprinkle/index.ts +264 -3
  95. package/src/Sprinkle/schemas.ts +20 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sundaeswap/sprinkles",
3
- "version": "0.7.0",
3
+ "version": "0.8.1",
4
4
  "description": "A TypeScript library for building interactive CLI menus and TUI applications with TypeBox schema validation",
5
5
  "type": "module",
6
6
  "main": "./dist/cjs/index.js",
@@ -51,14 +51,14 @@ async function makeSprinkle(
51
51
  }
52
52
 
53
53
  describe("getBuiltinActions", () => {
54
- test("returns an array of 14 actions", () => {
54
+ test("returns an array of 21 actions", () => {
55
55
  const actions = getBuiltinActions();
56
- expect(actions).toHaveLength(14);
56
+ expect(actions).toHaveLength(21);
57
57
  });
58
58
 
59
59
  test("all actions have a category", () => {
60
60
  const actions = getBuiltinActions();
61
- const validCategories = ["sprinkles", "wallet", "transaction"];
61
+ const validCategories = ["sprinkles", "wallet", "transaction", "utility", "addressbook"];
62
62
  for (const action of actions) {
63
63
  expect(validCategories).toContain((action as any).category);
64
64
  }
@@ -89,7 +89,7 @@ describe("getBuiltinActions", () => {
89
89
  for (const action of actions) {
90
90
  sprinkle.registerAction(action as any);
91
91
  }
92
- expect(sprinkle.listActions()).toHaveLength(14);
92
+ expect(sprinkle.listActions()).toHaveLength(21);
93
93
  });
94
94
  });
95
95
 
@@ -205,9 +205,30 @@ describe("coerceValue", () => {
205
205
  expect(coerceValue("not-json", Type.Object({ x: Type.String() }))).toBe("not-json");
206
206
  });
207
207
 
208
- test("returns already-parsed object as-is for Object schema", () => {
209
- const obj = { x: 1 };
210
- expect(coerceValue(obj, Type.Object({ x: Type.Number() }))).toBe(obj);
208
+ test("coerces properties of already-parsed object for Object schema", () => {
209
+ const obj = { x: "1" };
210
+ expect(coerceValue(obj, Type.Object({ x: Type.Number() }))).toEqual({ x: 1 });
211
+ });
212
+
213
+ test("recursively coerces nested BigInt values inside parsed JSON", () => {
214
+ const schema = Type.Object({
215
+ name: Type.String(),
216
+ reserve_assets: Type.Array(Type.Object({
217
+ asset: Type.Array(Type.String()),
218
+ numerator: Type.BigInt(),
219
+ denominator: Type.BigInt(),
220
+ })),
221
+ });
222
+ const raw = '{"name":"test","reserve_assets":[{"asset":["policy","name"],"numerator":"1","denominator":"2"}]}';
223
+ const result = coerceValue(raw, schema);
224
+ expect(result).toEqual({
225
+ name: "test",
226
+ reserve_assets: [{
227
+ asset: ["policy", "name"],
228
+ numerator: BigInt(1),
229
+ denominator: BigInt(2),
230
+ }],
231
+ });
211
232
  });
212
233
 
213
234
  test("returns malformed JSON string as-is for Object schema", () => {
@@ -91,8 +91,30 @@ describe("FillInStruct", () => {
91
91
  expect(mockInputCancellable.mock.calls[0][0].default).toBe("99");
92
92
  });
93
93
 
94
- test("throws for unsupported types", async () => {
94
+ test("fills a boolean field with select", async () => {
95
+ mockSelectCancellable.mockResolvedValueOnce(true);
96
+
97
+ const result = await sprinkle.FillInStruct(Type.Boolean() as any);
98
+ expect(result).toBe(true);
99
+ });
100
+
101
+ test("boolean field respects default value", async () => {
102
+ mockSelectCancellable.mockResolvedValueOnce(false);
103
+
104
+ await sprinkle.FillInStruct(Type.Boolean() as any, false as any);
105
+ expect(mockSelectCancellable.mock.calls[0][0].default).toBe(false);
106
+ });
107
+
108
+ test("boolean field throws on cancel", async () => {
109
+ mockSelectCancellable.mockResolvedValueOnce(null);
110
+
95
111
  expect(sprinkle.FillInStruct(Type.Boolean() as any)).rejects.toThrow(
112
+ UserCancelledError,
113
+ );
114
+ });
115
+
116
+ test("throws for unsupported types", async () => {
117
+ expect(sprinkle.FillInStruct(Type.Null() as any)).rejects.toThrow(
96
118
  "Unable to fill in struct",
97
119
  );
98
120
  });
@@ -543,7 +543,7 @@ describe("createMcpServer", () => {
543
543
 
544
544
  // --- Tool input schema ---
545
545
 
546
- test("tool input schema is the JSON Schema conversion of the action inputSchema", async () => {
546
+ test("tool input schema is a Zod raw shape matching the action inputSchema", async () => {
547
547
  const sprinkle = makeMockSprinkle([
548
548
  {
549
549
  name: "typed-action",
@@ -560,10 +560,12 @@ describe("createMcpServer", () => {
560
560
  await createMcpServer(sprinkle, "test-server");
561
561
 
562
562
  const schema = registeredTools[0]!.schema;
563
- expect(schema.type).toBe("object");
564
- const props = schema.properties as Record<string, unknown>;
565
- expect(props.name).toEqual({ type: "string" });
566
- expect(props.count).toEqual({ type: "integer" });
563
+ // Schema is now a Zod raw shape (Record<string, ZodType>)
564
+ expect(schema).toHaveProperty("name");
565
+ expect(schema).toHaveProperty("count");
566
+ // Verify they are Zod schema instances (have _zod internal property)
567
+ expect((schema as Record<string, unknown>).name).toHaveProperty("_zod");
568
+ expect((schema as Record<string, unknown>).count).toHaveProperty("_zod");
567
569
  });
568
570
 
569
571
  // --- Tool handler: successful execution ---
@@ -0,0 +1,341 @@
1
+ import { describe, expect, test, mock } from "bun:test";
2
+ import { ActionError } from "../actions/types.js";
3
+
4
+ // ---------------------------------------------------------------------------
5
+ // Mocks — mirror the Blaze SDK surface used by native-script.ts
6
+ // ---------------------------------------------------------------------------
7
+
8
+ let provideScriptCalls: any[] = [];
9
+ const mockCompletedTx = { toCbor: () => "mock-tx-cbor" };
10
+
11
+ mock.module("@blaze-cardano/sdk", () => ({
12
+ Core: {
13
+ Ed25519KeyHashHex: (h: string) => h,
14
+ Slot: (n: number) => n,
15
+ HexBlob: (s: string) => s,
16
+ ScriptPubkey: class {
17
+ hash: string;
18
+ constructor(h: string) {
19
+ this.hash = h;
20
+ }
21
+ },
22
+ ScriptAll: class {
23
+ scripts: any[];
24
+ constructor(s: any[]) {
25
+ this.scripts = s;
26
+ }
27
+ },
28
+ ScriptAny: class {
29
+ scripts: any[];
30
+ constructor(s: any[]) {
31
+ this.scripts = s;
32
+ }
33
+ },
34
+ ScriptNOfK: class {
35
+ scripts: any[];
36
+ n: number;
37
+ constructor(s: any[], n: number) {
38
+ this.scripts = s;
39
+ this.n = n;
40
+ }
41
+ },
42
+ TimelockExpiry: class {
43
+ slot: number;
44
+ constructor(s: number) {
45
+ this.slot = s;
46
+ }
47
+ },
48
+ TimelockStart: class {
49
+ slot: number;
50
+ constructor(s: number) {
51
+ this.slot = s;
52
+ }
53
+ },
54
+ NativeScript: {
55
+ fromCbor: (hex: string) => {
56
+ if (hex === "invalid") throw new Error("Invalid CBOR");
57
+ return { __fromCbor: hex };
58
+ },
59
+ newScriptPubkey: (sp: any) => ({ __type: "pubkey", value: sp }),
60
+ newScriptAll: (sa: any) => ({ __type: "all", value: sa }),
61
+ newScriptAny: (sa: any) => ({ __type: "any", value: sa }),
62
+ newScriptNOfK: (sn: any) => ({ __type: "nofk", value: sn }),
63
+ newTimelockExpiry: (te: any) => ({ __type: "timelockExpiry", value: te }),
64
+ newTimelockStart: (ts: any) => ({ __type: "timelockStart", value: ts }),
65
+ },
66
+ Script: {
67
+ newNativeScript: (ns: any) => ({
68
+ __nativeScript: ns,
69
+ hash: () => {
70
+ // Produce a deterministic hex hash from the native script content
71
+ if (ns?.__type === "pubkey") return ns.value.hash;
72
+ if (ns?.__fromCbor) return "cbor" + ns.__fromCbor;
73
+ return "0".repeat(56);
74
+ },
75
+ }),
76
+ },
77
+ },
78
+ }));
79
+
80
+ const { toNativeScript, completeWithScripts } = await import(
81
+ "../actions/builtin/native-script.js"
82
+ );
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // toNativeScript — CBOR hex input
86
+ // ---------------------------------------------------------------------------
87
+
88
+ describe("toNativeScript — CBOR hex", () => {
89
+ test("converts valid CBOR hex string", () => {
90
+ const result = toNativeScript("82018200");
91
+ expect(result.__nativeScript).toEqual({ __fromCbor: "82018200" });
92
+ expect(typeof result.hash).toBe("function");
93
+ });
94
+
95
+ test("throws INVALID_NATIVE_SCRIPT for invalid CBOR", () => {
96
+ expect(() => toNativeScript("invalid")).toThrow(ActionError);
97
+ try {
98
+ toNativeScript("invalid");
99
+ } catch (err: any) {
100
+ expect(err.code).toBe("INVALID_NATIVE_SCRIPT");
101
+ }
102
+ });
103
+ });
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // toNativeScript — MultisigScript JSON input
107
+ // ---------------------------------------------------------------------------
108
+
109
+ describe("toNativeScript — MultisigScript JSON", () => {
110
+ const keyHash = "a".repeat(56);
111
+
112
+ test("converts Signature", () => {
113
+ const result = toNativeScript({ Signature: { key_hash: keyHash } });
114
+ expect(result.__nativeScript.__type).toBe("pubkey");
115
+ expect(result.__nativeScript.value.hash).toBe(keyHash);
116
+ });
117
+
118
+ test("converts AllOf with nested scripts", () => {
119
+ const result = toNativeScript({
120
+ AllOf: {
121
+ scripts: [
122
+ { Signature: { key_hash: keyHash } },
123
+ { Signature: { key_hash: "b".repeat(56) } },
124
+ ],
125
+ },
126
+ });
127
+ expect(result.__nativeScript.__type).toBe("all");
128
+ expect(result.__nativeScript.value.scripts).toHaveLength(2);
129
+ });
130
+
131
+ test("converts AnyOf", () => {
132
+ const result = toNativeScript({
133
+ AnyOf: {
134
+ scripts: [{ Signature: { key_hash: keyHash } }],
135
+ },
136
+ });
137
+ expect(result.__nativeScript.__type).toBe("any");
138
+ });
139
+
140
+ test("converts AtLeast", () => {
141
+ const result = toNativeScript({
142
+ AtLeast: {
143
+ required: 2n,
144
+ scripts: [
145
+ { Signature: { key_hash: keyHash } },
146
+ { Signature: { key_hash: "b".repeat(56) } },
147
+ { Signature: { key_hash: "c".repeat(56) } },
148
+ ],
149
+ },
150
+ });
151
+ expect(result.__nativeScript.__type).toBe("nofk");
152
+ expect(result.__nativeScript.value.n).toBe(2);
153
+ expect(result.__nativeScript.value.scripts).toHaveLength(3);
154
+ });
155
+
156
+ test("converts Before (TimelockExpiry)", () => {
157
+ const result = toNativeScript({ Before: { time: 1000n } });
158
+ expect(result.__nativeScript.__type).toBe("timelockExpiry");
159
+ expect(result.__nativeScript.value.slot).toBe(1000);
160
+ });
161
+
162
+ test("converts After (TimelockStart)", () => {
163
+ const result = toNativeScript({ After: { time: 500n } });
164
+ expect(result.__nativeScript.__type).toBe("timelockStart");
165
+ expect(result.__nativeScript.value.slot).toBe(500);
166
+ });
167
+
168
+ test("converts Script (script hash reference)", () => {
169
+ const result = toNativeScript({ Script: { script_hash: keyHash } });
170
+ expect(result.__nativeScript.__type).toBe("pubkey");
171
+ expect(result.__nativeScript.value.hash).toBe(keyHash);
172
+ });
173
+
174
+ test("handles deeply nested structures", () => {
175
+ const result = toNativeScript({
176
+ AllOf: {
177
+ scripts: [
178
+ {
179
+ AtLeast: {
180
+ required: 1n,
181
+ scripts: [
182
+ { Signature: { key_hash: keyHash } },
183
+ {
184
+ AnyOf: {
185
+ scripts: [{ Signature: { key_hash: "b".repeat(56) } }],
186
+ },
187
+ },
188
+ ],
189
+ },
190
+ },
191
+ { After: { time: 100n } },
192
+ ],
193
+ },
194
+ });
195
+ expect(result.__nativeScript.__type).toBe("all");
196
+ expect(result.__nativeScript.value.scripts).toHaveLength(2);
197
+ });
198
+ });
199
+
200
+ // ---------------------------------------------------------------------------
201
+ // completeWithScripts
202
+ // ---------------------------------------------------------------------------
203
+
204
+ describe("completeWithScripts", () => {
205
+ function makeTxBuilder() {
206
+ provideScriptCalls = [];
207
+ const builder: any = {
208
+ provideScript: (script: any) => {
209
+ provideScriptCalls.push(script);
210
+ return builder;
211
+ },
212
+ complete: async () => mockCompletedTx,
213
+ };
214
+ return builder;
215
+ }
216
+
217
+ test("completes without scripts when none provided", async () => {
218
+ const builder = makeTxBuilder();
219
+ const tx = await completeWithScripts(builder);
220
+ expect(tx).toBe(mockCompletedTx);
221
+ expect(provideScriptCalls).toHaveLength(0);
222
+ });
223
+
224
+ test("completes without scripts when empty array provided", async () => {
225
+ const builder = makeTxBuilder();
226
+ const tx = await completeWithScripts(builder, []);
227
+ expect(tx).toBe(mockCompletedTx);
228
+ expect(provideScriptCalls).toHaveLength(0);
229
+ });
230
+
231
+ test("attaches scripts before completing", async () => {
232
+ const builder = makeTxBuilder();
233
+ const keyHash = "a".repeat(56);
234
+ const tx = await completeWithScripts(builder, [
235
+ { Signature: { key_hash: keyHash } },
236
+ "82018200",
237
+ ]);
238
+ expect(tx).toBe(mockCompletedTx);
239
+ expect(provideScriptCalls).toHaveLength(2);
240
+ });
241
+
242
+ test("propagates ActionError from invalid script input", async () => {
243
+ const builder = makeTxBuilder();
244
+ await expect(
245
+ completeWithScripts(builder, ["invalid"]),
246
+ ).rejects.toMatchObject({ code: "INVALID_NATIVE_SCRIPT" });
247
+ });
248
+
249
+ test("retries with addressbook script on missing script hash error", async () => {
250
+ const keyHash = "a".repeat(56);
251
+ const expectedScriptHash = keyHash;
252
+ let attempt = 0;
253
+ provideScriptCalls = [];
254
+ const builder: any = {
255
+ provideScript: (script: any) => {
256
+ provideScriptCalls.push(script);
257
+ return builder;
258
+ },
259
+ complete: async () => {
260
+ attempt++;
261
+ if (attempt === 1) {
262
+ throw new Error(
263
+ `complete: Could not resolve script hash ${expectedScriptHash}`,
264
+ );
265
+ }
266
+ return mockCompletedTx;
267
+ },
268
+ };
269
+
270
+ const context = {
271
+ sprinkle: {
272
+ addressbook: {
273
+ "my-multisig": { Signature: { key_hash: keyHash } },
274
+ },
275
+ },
276
+ settings: {},
277
+ } as any;
278
+
279
+ const tx = await completeWithScripts(builder, undefined, context);
280
+ expect(tx).toBe(mockCompletedTx);
281
+ expect(attempt).toBe(2);
282
+ expect(provideScriptCalls).toHaveLength(1);
283
+ });
284
+
285
+ test("throws original error when script hash not in addressbook", async () => {
286
+ const builder: any = {
287
+ provideScript: () => builder,
288
+ complete: async () => {
289
+ throw new Error("complete: Could not resolve script hash deadbeef1234");
290
+ },
291
+ };
292
+
293
+ const context = {
294
+ sprinkle: {
295
+ addressbook: {
296
+ "unrelated": { Signature: { key_hash: "b".repeat(56) } },
297
+ },
298
+ },
299
+ settings: {},
300
+ } as any;
301
+
302
+ await expect(
303
+ completeWithScripts(builder, undefined, context),
304
+ ).rejects.toThrow("Could not resolve script hash deadbeef1234");
305
+ });
306
+
307
+ test("throws original error when no addressbook in context", async () => {
308
+ const builder: any = {
309
+ provideScript: () => builder,
310
+ complete: async () => {
311
+ throw new Error("complete: Could not resolve script hash deadbeef1234");
312
+ },
313
+ };
314
+
315
+ await expect(
316
+ completeWithScripts(builder, undefined, undefined),
317
+ ).rejects.toThrow("Could not resolve script hash deadbeef1234");
318
+ });
319
+
320
+ test("throws non-script errors without retry", async () => {
321
+ const builder: any = {
322
+ provideScript: () => builder,
323
+ complete: async () => {
324
+ throw new Error("some other error");
325
+ },
326
+ };
327
+
328
+ const context = {
329
+ sprinkle: {
330
+ addressbook: {
331
+ "my-sig": { Signature: { key_hash: "a".repeat(56) } },
332
+ },
333
+ },
334
+ settings: {},
335
+ } as any;
336
+
337
+ await expect(
338
+ completeWithScripts(builder, undefined, context),
339
+ ).rejects.toThrow("some other error");
340
+ });
341
+ });