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