@sundaeswap/sprinkles 0.6.1 → 0.7.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 (153) hide show
  1. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
  2. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  3. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
  4. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  5. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
  6. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  7. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
  8. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  9. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +722 -0
  10. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  11. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +713 -0
  12. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  13. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
  14. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  15. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
  16. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  17. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
  18. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  19. package/dist/cjs/Sprinkle/actions/builtin/index.js +117 -0
  20. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
  21. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
  22. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  23. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
  24. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  25. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
  26. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  27. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
  28. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  29. package/dist/cjs/Sprinkle/actions/cli-adapter.js +372 -0
  30. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
  31. package/dist/cjs/Sprinkle/actions/index.js +127 -0
  32. package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
  33. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +415 -0
  34. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
  35. package/dist/cjs/Sprinkle/actions/registry.js +92 -0
  36. package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
  37. package/dist/cjs/Sprinkle/actions/runner.js +190 -0
  38. package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
  39. package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
  40. package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
  41. package/dist/cjs/Sprinkle/actions/types.js +68 -0
  42. package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
  43. package/dist/cjs/Sprinkle/index.js +412 -1
  44. package/dist/cjs/Sprinkle/index.js.map +1 -1
  45. package/dist/cjs/Sprinkle/prompts.js +12 -7
  46. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  47. package/dist/cjs/Sprinkle/type-guards.js +7 -1
  48. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  49. package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
  50. package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  51. package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
  52. package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  53. package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
  54. package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  55. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
  56. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  57. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +720 -0
  58. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  59. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +712 -0
  60. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  61. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
  62. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  63. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
  64. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  65. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
  66. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  67. package/dist/esm/Sprinkle/actions/builtin/index.js +32 -0
  68. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
  69. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
  70. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  71. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
  72. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  73. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
  74. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  75. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
  76. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  77. package/dist/esm/Sprinkle/actions/cli-adapter.js +361 -0
  78. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
  79. package/dist/esm/Sprinkle/actions/index.js +12 -0
  80. package/dist/esm/Sprinkle/actions/index.js.map +1 -0
  81. package/dist/esm/Sprinkle/actions/mcp-adapter.js +407 -0
  82. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
  83. package/dist/esm/Sprinkle/actions/registry.js +85 -0
  84. package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
  85. package/dist/esm/Sprinkle/actions/runner.js +182 -0
  86. package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
  87. package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
  88. package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
  89. package/dist/esm/Sprinkle/actions/types.js +61 -0
  90. package/dist/esm/Sprinkle/actions/types.js.map +1 -0
  91. package/dist/esm/Sprinkle/index.js +260 -1
  92. package/dist/esm/Sprinkle/index.js.map +1 -1
  93. package/dist/esm/Sprinkle/prompts.js +12 -7
  94. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  95. package/dist/esm/Sprinkle/type-guards.js +3 -0
  96. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  97. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
  98. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
  99. package/dist/types/Sprinkle/actions/builtin/index.d.ts +26 -0
  100. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
  101. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
  102. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
  103. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
  104. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
  105. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
  106. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
  107. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
  108. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
  109. package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
  110. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
  111. package/dist/types/Sprinkle/actions/index.d.ts +12 -0
  112. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
  113. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +92 -0
  114. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
  115. package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
  116. package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
  117. package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
  118. package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
  119. package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
  120. package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
  121. package/dist/types/Sprinkle/actions/types.d.ts +76 -0
  122. package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
  123. package/dist/types/Sprinkle/index.d.ts +81 -1
  124. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  125. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  126. package/dist/types/Sprinkle/type-guards.d.ts +4 -1
  127. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  128. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  129. package/package.json +9 -2
  130. package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
  131. package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
  132. package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
  133. package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
  134. package/src/Sprinkle/__tests__/cli-adapter.test.ts +715 -0
  135. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +718 -0
  136. package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
  137. package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
  138. package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
  139. package/src/Sprinkle/actions/builtin/index.ts +86 -0
  140. package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
  141. package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
  142. package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
  143. package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
  144. package/src/Sprinkle/actions/cli-adapter.ts +430 -0
  145. package/src/Sprinkle/actions/index.ts +32 -0
  146. package/src/Sprinkle/actions/mcp-adapter.ts +463 -0
  147. package/src/Sprinkle/actions/registry.ts +97 -0
  148. package/src/Sprinkle/actions/runner.ts +200 -0
  149. package/src/Sprinkle/actions/tui-helpers.ts +114 -0
  150. package/src/Sprinkle/actions/types.ts +91 -0
  151. package/src/Sprinkle/index.ts +351 -0
  152. package/src/Sprinkle/prompts.ts +118 -72
  153. package/src/Sprinkle/type-guards.ts +9 -0
@@ -0,0 +1,695 @@
1
+ import { describe, expect, test, mock, beforeEach } from "bun:test";
2
+ import { Type } from "@sinclair/typebox";
3
+ import { ActionError } from "../actions/types.js";
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // Mocks
7
+ // ---------------------------------------------------------------------------
8
+
9
+ // Mock wallet/UTxO data factories
10
+ function makeAddress(bech32 = "addr_test1qz...mock") {
11
+ return { toBech32: () => bech32 };
12
+ }
13
+
14
+ function makeAssetId(policyId: string, assetName: string) {
15
+ return `${policyId}${assetName}`;
16
+ }
17
+
18
+ function makeUtxo(opts: {
19
+ txHash: string;
20
+ index: number;
21
+ lovelace: bigint;
22
+ tokens?: Array<{ policyId: string; assetName: string; quantity: bigint }>;
23
+ }) {
24
+ const tokenEntries = (opts.tokens ?? []).map((t) => [
25
+ makeAssetId(t.policyId, t.assetName),
26
+ t.quantity,
27
+ ]);
28
+
29
+ return {
30
+ input: () => ({
31
+ transactionId: () => ({ toString: () => opts.txHash }),
32
+ index: () => BigInt(opts.index),
33
+ }),
34
+ output: () => ({
35
+ amount: () => ({
36
+ coin: () => opts.lovelace,
37
+ multiasset: () =>
38
+ tokenEntries.length > 0
39
+ ? { entries: () => tokenEntries }
40
+ : undefined,
41
+ }),
42
+ }),
43
+ };
44
+ }
45
+
46
+ // Controllable mock state
47
+ let mockBlaze: any = {};
48
+ let mockIsHot = true;
49
+ let mockBlazeError: Error | null = null;
50
+
51
+ mock.module("../actions/builtin/blaze-helper.js", () => ({
52
+ getBlazeFromContext: async () => {
53
+ if (mockBlazeError) throw mockBlazeError;
54
+ return mockBlaze;
55
+ },
56
+ isHotWallet: () => mockIsHot,
57
+ }));
58
+
59
+ // Mock Core.AssetId static methods used by wallet/transaction actions
60
+ mock.module("@blaze-cardano/sdk", () => ({
61
+ Core: {
62
+ AssetId: {
63
+ getPolicyId: (assetId: string) => assetId.slice(0, 56),
64
+ getAssetName: (assetId: string) => assetId.slice(56),
65
+ },
66
+ Transaction: {
67
+ fromCbor: (cbor: string) => mockTxFromCbor(cbor),
68
+ },
69
+ TxCBOR: (s: string) => s,
70
+ },
71
+ Blaze: class {},
72
+ HotWallet: class {},
73
+ }));
74
+
75
+ // Controllable mock transaction from CBOR
76
+ // These mock objects are compatible with the real countSignatures/getRequiredSigners
77
+ // from tx-dialog.ts, so we don't need to mock that module (which would leak globally).
78
+ let mockTxFromCbor: (cbor: string) => any = () => ({
79
+ body: () => ({
80
+ hash: () => ({ toString: () => "abc123txhash" }),
81
+ inputs: () => ({
82
+ values: () => [
83
+ {
84
+ transactionId: () => ({ toString: () => "input-tx-hash-1" }),
85
+ index: () => 0n,
86
+ },
87
+ ],
88
+ }),
89
+ outputs: () => [
90
+ {
91
+ address: () => ({ toBech32: () => "addr_test1output..." }),
92
+ amount: () => ({
93
+ coin: () => 2_000_000n,
94
+ multiasset: () => undefined,
95
+ }),
96
+ },
97
+ ],
98
+ fee: () => 200_000n,
99
+ requiredSigners: () => null,
100
+ }),
101
+ witnessSet: () => ({
102
+ vkeys: () => null,
103
+ }),
104
+ toCbor: () => "signed-cbor-hex",
105
+ });
106
+
107
+ // Now import the actions (after mocks are set up)
108
+ const { getWalletAddress, getWalletBalance, getWalletUtxos } = await import(
109
+ "../actions/builtin/wallet-actions.js"
110
+ );
111
+ const {
112
+ signTransaction,
113
+ submitTransaction,
114
+ signAndSubmit,
115
+ decodeTransaction,
116
+ } = await import("../actions/builtin/transaction-actions.js");
117
+
118
+ // Minimal context factory
119
+ function makeContext(settingsOverrides: Record<string, unknown> = {}) {
120
+ return {
121
+ sprinkle: {} as any,
122
+ settings: {
123
+ network: "preview",
124
+ provider: { type: "blockfrost", apiKey: "test" },
125
+ wallet: { type: "hot", mnemonic: "test words" },
126
+ ...settingsOverrides,
127
+ } as any,
128
+ };
129
+ }
130
+
131
+ // ---------------------------------------------------------------------------
132
+ // Wallet Actions
133
+ // ---------------------------------------------------------------------------
134
+
135
+ describe("get-wallet-address", () => {
136
+ beforeEach(() => {
137
+ mockBlazeError = null;
138
+ mockIsHot = true;
139
+ });
140
+
141
+ test("returns bech32 address and network", async () => {
142
+ mockBlaze = {
143
+ wallet: {
144
+ getChangeAddress: async () => makeAddress("addr_test1qzabc123"),
145
+ },
146
+ };
147
+ const ctx = makeContext();
148
+ const result = await getWalletAddress.execute({}, ctx);
149
+ expect(result.address).toBe("addr_test1qzabc123");
150
+ expect(result.network).toBe("preview");
151
+ });
152
+
153
+ test("returns 'unknown' network when not in settings", async () => {
154
+ mockBlaze = {
155
+ wallet: {
156
+ getChangeAddress: async () => makeAddress("addr1mainnet"),
157
+ },
158
+ };
159
+ const ctx = makeContext({ network: undefined });
160
+ const result = await getWalletAddress.execute({}, ctx);
161
+ expect(result.network).toBe("unknown");
162
+ });
163
+
164
+ test("throws WALLET_NOT_CONFIGURED when getBlazeFromContext fails", async () => {
165
+ mockBlazeError = new ActionError("Missing settings", "WALLET_NOT_CONFIGURED", {
166
+ missingFields: ["wallet"],
167
+ });
168
+ const ctx = makeContext();
169
+ await expect(getWalletAddress.execute({}, ctx)).rejects.toMatchObject({
170
+ code: "WALLET_NOT_CONFIGURED",
171
+ });
172
+ });
173
+
174
+ test("throws NO_ADDRESS when wallet returns no address", async () => {
175
+ mockBlaze = {
176
+ wallet: {
177
+ getChangeAddress: async () => {
178
+ throw new Error("no address available");
179
+ },
180
+ },
181
+ };
182
+ const ctx = makeContext();
183
+ await expect(getWalletAddress.execute({}, ctx)).rejects.toMatchObject({
184
+ code: "NO_ADDRESS",
185
+ });
186
+ });
187
+
188
+ test("has correct metadata", () => {
189
+ expect(getWalletAddress.name).toBe("get-wallet-address");
190
+ expect(getWalletAddress.category).toBe("wallet");
191
+ expect(getWalletAddress.inputSchema).toBeDefined();
192
+ expect(getWalletAddress.outputSchema).toBeDefined();
193
+ });
194
+ });
195
+
196
+ describe("get-wallet-balance", () => {
197
+ beforeEach(() => {
198
+ mockBlazeError = null;
199
+ });
200
+
201
+ test("returns lovelace, ada, and empty tokens for ADA-only wallet", async () => {
202
+ mockBlaze = {
203
+ wallet: {
204
+ getUnspentOutputs: async () => [
205
+ makeUtxo({ txHash: "tx1", index: 0, lovelace: 5_000_000n }),
206
+ makeUtxo({ txHash: "tx2", index: 1, lovelace: 3_000_000n }),
207
+ ],
208
+ },
209
+ };
210
+ const result = await getWalletBalance.execute({}, makeContext());
211
+ expect(result.lovelace).toBe("8000000");
212
+ expect(result.ada).toBe("8.000000");
213
+ expect(result.tokens).toEqual([]);
214
+ });
215
+
216
+ test("aggregates tokens across UTxOs", async () => {
217
+ const policyId = "a".repeat(56);
218
+ const assetName = "token1";
219
+ mockBlaze = {
220
+ wallet: {
221
+ getUnspentOutputs: async () => [
222
+ makeUtxo({
223
+ txHash: "tx1",
224
+ index: 0,
225
+ lovelace: 2_000_000n,
226
+ tokens: [{ policyId, assetName, quantity: 100n }],
227
+ }),
228
+ makeUtxo({
229
+ txHash: "tx2",
230
+ index: 0,
231
+ lovelace: 1_000_000n,
232
+ tokens: [{ policyId, assetName, quantity: 50n }],
233
+ }),
234
+ ],
235
+ },
236
+ };
237
+ const result = await getWalletBalance.execute({}, makeContext());
238
+ expect(result.lovelace).toBe("3000000");
239
+ expect(result.tokens).toHaveLength(1);
240
+ expect(result.tokens[0].quantity).toBe("150");
241
+ expect(result.tokens[0].policyId).toBe(policyId);
242
+ });
243
+
244
+ test("handles fractional ADA correctly", async () => {
245
+ mockBlaze = {
246
+ wallet: {
247
+ getUnspentOutputs: async () => [
248
+ makeUtxo({ txHash: "tx1", index: 0, lovelace: 1_500_123n }),
249
+ ],
250
+ },
251
+ };
252
+ const result = await getWalletBalance.execute({}, makeContext());
253
+ expect(result.ada).toBe("1.500123");
254
+ });
255
+
256
+ test("handles zero balance", async () => {
257
+ mockBlaze = {
258
+ wallet: { getUnspentOutputs: async () => [] },
259
+ };
260
+ const result = await getWalletBalance.execute({}, makeContext());
261
+ expect(result.lovelace).toBe("0");
262
+ expect(result.ada).toBe("0.000000");
263
+ expect(result.tokens).toEqual([]);
264
+ });
265
+
266
+ test("throws PROVIDER_ERROR on UTxO fetch failure", async () => {
267
+ mockBlaze = {
268
+ wallet: {
269
+ getUnspentOutputs: async () => {
270
+ throw new Error("connection timeout");
271
+ },
272
+ },
273
+ };
274
+ await expect(getWalletBalance.execute({}, makeContext())).rejects.toMatchObject({
275
+ code: "PROVIDER_ERROR",
276
+ });
277
+ });
278
+ });
279
+
280
+ describe("get-wallet-utxos", () => {
281
+ beforeEach(() => {
282
+ mockBlazeError = null;
283
+ });
284
+
285
+ test("returns UTxOs with correct structure", async () => {
286
+ mockBlaze = {
287
+ wallet: {
288
+ getUnspentOutputs: async () => [
289
+ makeUtxo({ txHash: "txhash1", index: 0, lovelace: 5_000_000n }),
290
+ makeUtxo({ txHash: "txhash2", index: 1, lovelace: 3_000_000n }),
291
+ ],
292
+ },
293
+ };
294
+ const result = await getWalletUtxos.execute({}, makeContext());
295
+ expect(result.total).toBe(2);
296
+ expect(result.utxos).toHaveLength(2);
297
+ expect(result.utxos[0].txHash).toBe("txhash1");
298
+ expect(result.utxos[0].outputIndex).toBe(0);
299
+ expect(result.utxos[0].lovelace).toBe("5000000");
300
+ expect(result.utxos[0].tokens).toEqual([]);
301
+ });
302
+
303
+ test("respects limit parameter", async () => {
304
+ mockBlaze = {
305
+ wallet: {
306
+ getUnspentOutputs: async () =>
307
+ Array.from({ length: 10 }, (_, i) =>
308
+ makeUtxo({ txHash: `tx${i}`, index: 0, lovelace: 1_000_000n }),
309
+ ),
310
+ },
311
+ };
312
+ const result = await getWalletUtxos.execute({ limit: 3 }, makeContext());
313
+ expect(result.utxos).toHaveLength(3);
314
+ expect(result.total).toBe(10);
315
+ });
316
+
317
+ test("defaults to limit of 100", async () => {
318
+ mockBlaze = {
319
+ wallet: {
320
+ getUnspentOutputs: async () =>
321
+ Array.from({ length: 150 }, (_, i) =>
322
+ makeUtxo({ txHash: `tx${i}`, index: 0, lovelace: 1_000_000n }),
323
+ ),
324
+ },
325
+ };
326
+ const result = await getWalletUtxos.execute({}, makeContext());
327
+ expect(result.utxos).toHaveLength(100);
328
+ expect(result.total).toBe(150);
329
+ });
330
+
331
+ test("includes tokens on UTxOs", async () => {
332
+ const policyId = "b".repeat(56);
333
+ mockBlaze = {
334
+ wallet: {
335
+ getUnspentOutputs: async () => [
336
+ makeUtxo({
337
+ txHash: "tx1",
338
+ index: 0,
339
+ lovelace: 2_000_000n,
340
+ tokens: [{ policyId, assetName: "mytoken", quantity: 42n }],
341
+ }),
342
+ ],
343
+ },
344
+ };
345
+ const result = await getWalletUtxos.execute({}, makeContext());
346
+ expect(result.utxos[0].tokens).toHaveLength(1);
347
+ expect(result.utxos[0].tokens[0].quantity).toBe("42");
348
+ });
349
+
350
+ test("throws PROVIDER_ERROR on fetch failure", async () => {
351
+ mockBlaze = {
352
+ wallet: {
353
+ getUnspentOutputs: async () => {
354
+ throw new Error("provider down");
355
+ },
356
+ },
357
+ };
358
+ await expect(getWalletUtxos.execute({}, makeContext())).rejects.toMatchObject({
359
+ code: "PROVIDER_ERROR",
360
+ });
361
+ });
362
+ });
363
+
364
+ // ---------------------------------------------------------------------------
365
+ // Transaction Actions
366
+ // ---------------------------------------------------------------------------
367
+
368
+ describe("sign-transaction", () => {
369
+ beforeEach(() => {
370
+ mockBlazeError = null;
371
+ mockIsHot = true;
372
+ });
373
+
374
+ test("signs a transaction and returns signed CBOR, hash, and signature count", async () => {
375
+ mockBlaze = {
376
+ wallet: {},
377
+ signTransaction: async () => ({
378
+ toCbor: () => "signed-tx-cbor",
379
+ body: () => ({
380
+ hash: () => ({ toString: () => "signed-tx-hash" }),
381
+ }),
382
+ witnessSet: () => ({ vkeys: () => ({ size: () => 1 }) }),
383
+ }),
384
+ };
385
+ const result = await signTransaction.execute(
386
+ { txCbor: "valid-cbor" },
387
+ makeContext(),
388
+ );
389
+ expect(result.signedTxCbor).toBe("signed-tx-cbor");
390
+ expect(result.txHash).toBe("signed-tx-hash");
391
+ expect(result.signatureCount).toBe(1);
392
+ });
393
+
394
+ test("throws COLD_WALLET for cold wallets", async () => {
395
+ mockIsHot = false;
396
+ mockBlaze = { wallet: {} };
397
+ await expect(
398
+ signTransaction.execute({ txCbor: "some-cbor" }, makeContext()),
399
+ ).rejects.toMatchObject({ code: "COLD_WALLET" });
400
+ });
401
+
402
+ test("throws INVALID_CBOR for bad CBOR input", async () => {
403
+ mockTxFromCbor = () => {
404
+ throw new Error("invalid cbor");
405
+ };
406
+ mockBlaze = { wallet: {} };
407
+ await expect(
408
+ signTransaction.execute({ txCbor: "bad-cbor" }, makeContext()),
409
+ ).rejects.toMatchObject({ code: "INVALID_CBOR" });
410
+
411
+ // Restore default mock
412
+ mockTxFromCbor = () => ({
413
+ body: () => ({
414
+ hash: () => ({ toString: () => "abc123txhash" }),
415
+ inputs: () => ({ values: () => [] }),
416
+ outputs: () => [],
417
+ fee: () => 200_000n,
418
+ requiredSigners: () => null,
419
+ }),
420
+ witnessSet: () => ({ vkeys: () => null }),
421
+ toCbor: () => "signed-cbor-hex",
422
+ });
423
+ });
424
+
425
+ test("throws SIGN_ERROR when signing fails", async () => {
426
+ mockBlaze = {
427
+ wallet: {},
428
+ signTransaction: async () => {
429
+ throw new Error("hardware wallet disconnected");
430
+ },
431
+ };
432
+ await expect(
433
+ signTransaction.execute({ txCbor: "valid-cbor" }, makeContext()),
434
+ ).rejects.toMatchObject({ code: "SIGN_ERROR" });
435
+ });
436
+
437
+ test("has correct metadata", () => {
438
+ expect(signTransaction.name).toBe("sign-transaction");
439
+ expect(signTransaction.category).toBe("wallet");
440
+ });
441
+ });
442
+
443
+ describe("submit-transaction", () => {
444
+ beforeEach(() => {
445
+ mockBlazeError = null;
446
+ });
447
+
448
+ test("submits a transaction and returns hash", async () => {
449
+ mockBlaze = {
450
+ wallet: {},
451
+ submitTransaction: async () => ({
452
+ toString: () => "submitted-tx-hash",
453
+ }),
454
+ };
455
+ const result = await submitTransaction.execute(
456
+ { txCbor: "valid-signed-cbor" },
457
+ makeContext(),
458
+ );
459
+ expect(result.txHash).toBe("submitted-tx-hash");
460
+ expect(result.submitted).toBe(true);
461
+ });
462
+
463
+ test("throws SUBMISSION_ERROR on failure", async () => {
464
+ mockBlaze = {
465
+ wallet: {},
466
+ submitTransaction: async () => {
467
+ throw new Error("tx already submitted");
468
+ },
469
+ };
470
+ await expect(
471
+ submitTransaction.execute({ txCbor: "some-cbor" }, makeContext()),
472
+ ).rejects.toMatchObject({ code: "SUBMISSION_ERROR" });
473
+ });
474
+
475
+ test("has correct metadata", () => {
476
+ expect(submitTransaction.name).toBe("submit-transaction");
477
+ expect(submitTransaction.category).toBe("wallet");
478
+ });
479
+ });
480
+
481
+ describe("sign-and-submit", () => {
482
+ beforeEach(() => {
483
+ mockBlazeError = null;
484
+ mockIsHot = true;
485
+ });
486
+
487
+ test("signs and submits without waiting for confirmation", async () => {
488
+ mockBlaze = {
489
+ wallet: {},
490
+ signTransaction: async () => ({
491
+ toCbor: () => "signed-cbor",
492
+ body: () => ({
493
+ hash: () => ({ toString: () => "tx-hash" }),
494
+ }),
495
+ witnessSet: () => ({ vkeys: () => ({ size: () => 1 }) }),
496
+ }),
497
+ submitTransaction: async () => ({
498
+ toString: () => "tx-hash",
499
+ }),
500
+ };
501
+ const result = await signAndSubmit.execute(
502
+ { txCbor: "valid-cbor", waitForConfirmation: false },
503
+ makeContext(),
504
+ );
505
+ expect(result.txHash).toBe("tx-hash");
506
+ expect(result.submitted).toBe(true);
507
+ expect(result.signedTxCbor).toBe("signed-cbor");
508
+ expect(result.confirmed).toBeUndefined();
509
+ });
510
+
511
+ test("throws COLD_WALLET for cold wallets", async () => {
512
+ mockIsHot = false;
513
+ mockBlaze = { wallet: {} };
514
+ await expect(
515
+ signAndSubmit.execute({ txCbor: "cbor" }, makeContext()),
516
+ ).rejects.toMatchObject({ code: "COLD_WALLET" });
517
+ });
518
+
519
+ test("throws SUBMISSION_ERROR when submit fails (includes signedTxCbor in details)", async () => {
520
+ mockBlaze = {
521
+ wallet: {},
522
+ signTransaction: async () => ({
523
+ toCbor: () => "signed-but-failed",
524
+ body: () => ({
525
+ hash: () => ({ toString: () => "hash" }),
526
+ }),
527
+ witnessSet: () => ({ vkeys: () => null }),
528
+ }),
529
+ submitTransaction: async () => {
530
+ throw new Error("node rejected");
531
+ },
532
+ };
533
+ try {
534
+ await signAndSubmit.execute({ txCbor: "valid-cbor" }, makeContext());
535
+ expect(true).toBe(false); // should not reach
536
+ } catch (err: any) {
537
+ expect(err.code).toBe("SUBMISSION_ERROR");
538
+ expect(err.details.signedTxCbor).toBe("signed-but-failed");
539
+ }
540
+ });
541
+
542
+ test("has correct metadata", () => {
543
+ expect(signAndSubmit.name).toBe("sign-and-submit");
544
+ expect(signAndSubmit.category).toBe("wallet");
545
+ });
546
+ });
547
+
548
+ describe("decode-transaction", () => {
549
+ beforeEach(() => {
550
+ mockBlazeError = null;
551
+
552
+ // Set up a rich mock transaction for decode tests
553
+ mockTxFromCbor = () => ({
554
+ body: () => ({
555
+ hash: () => ({ toString: () => "decoded-tx-hash" }),
556
+ inputs: () => ({
557
+ values: () => [
558
+ {
559
+ transactionId: () => ({ toString: () => "input-hash-1" }),
560
+ index: () => 0n,
561
+ },
562
+ {
563
+ transactionId: () => ({ toString: () => "input-hash-2" }),
564
+ index: () => 1n,
565
+ },
566
+ ],
567
+ }),
568
+ outputs: () => [
569
+ {
570
+ address: () => ({ toBech32: () => "addr_test1output1" }),
571
+ amount: () => ({
572
+ coin: () => 5_000_000n,
573
+ multiasset: () => undefined,
574
+ }),
575
+ },
576
+ {
577
+ address: () => ({ toBech32: () => "addr_test1output2" }),
578
+ amount: () => ({
579
+ coin: () => 2_000_000n,
580
+ multiasset: () => undefined,
581
+ }),
582
+ },
583
+ ],
584
+ fee: () => 180_000n,
585
+ requiredSigners: () => ({
586
+ values: () => [
587
+ { toString: () => "signer-hash-1" },
588
+ { toString: () => "signer-hash-2" },
589
+ ],
590
+ }),
591
+ }),
592
+ witnessSet: () => ({
593
+ vkeys: () => ({ size: () => 2 }),
594
+ }),
595
+ });
596
+ });
597
+
598
+ test("decodes a transaction and returns all fields", async () => {
599
+ const result = await decodeTransaction.execute(
600
+ { txCbor: "valid-cbor-hex" },
601
+ makeContext(),
602
+ );
603
+ expect(result.txHash).toBe("decoded-tx-hash");
604
+ expect(result.inputs).toHaveLength(2);
605
+ expect(result.inputs[0]).toEqual({ txHash: "input-hash-1", outputIndex: 0 });
606
+ expect(result.inputs[1]).toEqual({ txHash: "input-hash-2", outputIndex: 1 });
607
+ expect(result.outputs).toHaveLength(2);
608
+ expect(result.outputs[0].address).toBe("addr_test1output1");
609
+ expect(result.outputs[0].lovelace).toBe("5000000");
610
+ expect(result.fee).toBe("180000");
611
+ expect(result.signatureCount).toBe(2);
612
+ expect(result.requiredSigners).toEqual(["signer-hash-1", "signer-hash-2"]);
613
+ });
614
+
615
+ test("falls back to hex address when bech32 fails", async () => {
616
+ mockTxFromCbor = () => ({
617
+ body: () => ({
618
+ hash: () => ({ toString: () => "tx-hash" }),
619
+ inputs: () => ({ values: () => [] }),
620
+ outputs: () => [
621
+ {
622
+ address: () => ({
623
+ toBech32: () => {
624
+ throw new Error("cannot convert to bech32");
625
+ },
626
+ toBytes: () => "deadbeef",
627
+ }),
628
+ amount: () => ({
629
+ coin: () => 1_000_000n,
630
+ multiasset: () => undefined,
631
+ }),
632
+ },
633
+ ],
634
+ fee: () => 100_000n,
635
+ requiredSigners: () => null,
636
+ }),
637
+ witnessSet: () => ({ vkeys: () => null }),
638
+ });
639
+
640
+ const result = await decodeTransaction.execute(
641
+ { txCbor: "cbor-with-script-address" },
642
+ makeContext(),
643
+ );
644
+ expect(result.outputs[0].address).toBe("deadbeef");
645
+ });
646
+
647
+ test("handles transaction with tokens in outputs", async () => {
648
+ const policyId = "c".repeat(56);
649
+ const assetName = "mytoken";
650
+ const assetId = `${policyId}${assetName}`;
651
+
652
+ mockTxFromCbor = () => ({
653
+ body: () => ({
654
+ hash: () => ({ toString: () => "tx-hash" }),
655
+ inputs: () => ({ values: () => [] }),
656
+ outputs: () => [
657
+ {
658
+ address: () => ({ toBech32: () => "addr_test1..." }),
659
+ amount: () => ({
660
+ coin: () => 2_000_000n,
661
+ multiasset: () => ({
662
+ entries: () => [[assetId, 500n]],
663
+ }),
664
+ }),
665
+ },
666
+ ],
667
+ fee: () => 200_000n,
668
+ requiredSigners: () => null,
669
+ }),
670
+ witnessSet: () => ({ vkeys: () => null }),
671
+ });
672
+
673
+ const result = await decodeTransaction.execute(
674
+ { txCbor: "cbor-with-tokens" },
675
+ makeContext(),
676
+ );
677
+ expect(result.outputs[0].tokens).toHaveLength(1);
678
+ expect(result.outputs[0].tokens[0].policyId).toBe(policyId);
679
+ expect(result.outputs[0].tokens[0].quantity).toBe("500");
680
+ });
681
+
682
+ test("throws INVALID_CBOR for bad input", async () => {
683
+ mockTxFromCbor = () => {
684
+ throw new Error("bad cbor");
685
+ };
686
+ await expect(
687
+ decodeTransaction.execute({ txCbor: "garbage" }, makeContext()),
688
+ ).rejects.toMatchObject({ code: "INVALID_CBOR" });
689
+ });
690
+
691
+ test("does not require a wallet (category is 'transaction')", () => {
692
+ expect(decodeTransaction.name).toBe("decode-transaction");
693
+ expect(decodeTransaction.category).toBe("transaction");
694
+ });
695
+ });