@sundaeswap/sprinkles 0.6.1 → 0.8.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 (197) hide show
  1. package/README.md +178 -181
  2. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js +590 -0
  3. package/dist/cjs/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  4. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js +193 -0
  5. package/dist/cjs/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  6. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js +304 -0
  7. package/dist/cjs/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  8. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js +1110 -0
  9. package/dist/cjs/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  10. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js +744 -0
  11. package/dist/cjs/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  12. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  13. package/dist/cjs/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  14. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js +711 -0
  15. package/dist/cjs/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  16. package/dist/cjs/Sprinkle/__tests__/native-script.test.js +390 -0
  17. package/dist/cjs/Sprinkle/__tests__/native-script.test.js.map +1 -0
  18. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js +334 -0
  19. package/dist/cjs/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  20. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js +367 -0
  21. package/dist/cjs/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  22. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js +749 -0
  23. package/dist/cjs/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  24. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js +164 -0
  25. package/dist/cjs/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  26. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js +61 -0
  27. package/dist/cjs/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  28. package/dist/cjs/Sprinkle/actions/builtin/index.js +174 -0
  29. package/dist/cjs/Sprinkle/actions/builtin/index.js.map +1 -0
  30. package/dist/cjs/Sprinkle/actions/builtin/native-script.js +139 -0
  31. package/dist/cjs/Sprinkle/actions/builtin/native-script.js.map +1 -0
  32. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js +202 -0
  33. package/dist/cjs/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  34. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js +87 -0
  35. package/dist/cjs/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  36. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js +345 -0
  37. package/dist/cjs/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  38. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js +218 -0
  39. package/dist/cjs/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  40. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js +212 -0
  41. package/dist/cjs/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  42. package/dist/cjs/Sprinkle/actions/cli-adapter.js +390 -0
  43. package/dist/cjs/Sprinkle/actions/cli-adapter.js.map +1 -0
  44. package/dist/cjs/Sprinkle/actions/index.js +139 -0
  45. package/dist/cjs/Sprinkle/actions/index.js.map +1 -0
  46. package/dist/cjs/Sprinkle/actions/mcp-adapter.js +557 -0
  47. package/dist/cjs/Sprinkle/actions/mcp-adapter.js.map +1 -0
  48. package/dist/cjs/Sprinkle/actions/registry.js +92 -0
  49. package/dist/cjs/Sprinkle/actions/registry.js.map +1 -0
  50. package/dist/cjs/Sprinkle/actions/runner.js +190 -0
  51. package/dist/cjs/Sprinkle/actions/runner.js.map +1 -0
  52. package/dist/cjs/Sprinkle/actions/tui-helpers.js +96 -0
  53. package/dist/cjs/Sprinkle/actions/tui-helpers.js.map +1 -0
  54. package/dist/cjs/Sprinkle/actions/types.js +68 -0
  55. package/dist/cjs/Sprinkle/actions/types.js.map +1 -0
  56. package/dist/cjs/Sprinkle/index.js +678 -5
  57. package/dist/cjs/Sprinkle/index.js.map +1 -1
  58. package/dist/cjs/Sprinkle/prompts.js +12 -7
  59. package/dist/cjs/Sprinkle/prompts.js.map +1 -1
  60. package/dist/cjs/Sprinkle/schemas.js +17 -1
  61. package/dist/cjs/Sprinkle/schemas.js.map +1 -1
  62. package/dist/cjs/Sprinkle/type-guards.js +7 -1
  63. package/dist/cjs/Sprinkle/type-guards.js.map +1 -1
  64. package/dist/esm/Sprinkle/__tests__/action-integration.test.js +588 -0
  65. package/dist/esm/Sprinkle/__tests__/action-integration.test.js.map +1 -0
  66. package/dist/esm/Sprinkle/__tests__/action-registry.test.js +192 -0
  67. package/dist/esm/Sprinkle/__tests__/action-registry.test.js.map +1 -0
  68. package/dist/esm/Sprinkle/__tests__/action-runner.test.js +302 -0
  69. package/dist/esm/Sprinkle/__tests__/action-runner.test.js.map +1 -0
  70. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js +1107 -0
  71. package/dist/esm/Sprinkle/__tests__/builtin-actions.test.js.map +1 -0
  72. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js +742 -0
  73. package/dist/esm/Sprinkle/__tests__/cli-adapter.test.js.map +1 -0
  74. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js +15 -1
  75. package/dist/esm/Sprinkle/__tests__/fill-in-struct.test.js.map +1 -1
  76. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js +710 -0
  77. package/dist/esm/Sprinkle/__tests__/mcp-adapter.test.js.map +1 -0
  78. package/dist/esm/Sprinkle/__tests__/native-script.test.js +388 -0
  79. package/dist/esm/Sprinkle/__tests__/native-script.test.js.map +1 -0
  80. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js +332 -0
  81. package/dist/esm/Sprinkle/__tests__/tui-helpers.test.js.map +1 -0
  82. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js +365 -0
  83. package/dist/esm/Sprinkle/__tests__/utility-actions.test.js.map +1 -0
  84. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js +747 -0
  85. package/dist/esm/Sprinkle/__tests__/wallet-transaction-actions.test.js.map +1 -0
  86. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js +159 -0
  87. package/dist/esm/Sprinkle/actions/builtin/addressbook-actions.js.map +1 -0
  88. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js +55 -0
  89. package/dist/esm/Sprinkle/actions/builtin/blaze-helper.js.map +1 -0
  90. package/dist/esm/Sprinkle/actions/builtin/index.js +37 -0
  91. package/dist/esm/Sprinkle/actions/builtin/index.js.map +1 -0
  92. package/dist/esm/Sprinkle/actions/builtin/native-script.js +133 -0
  93. package/dist/esm/Sprinkle/actions/builtin/native-script.js.map +1 -0
  94. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js +197 -0
  95. package/dist/esm/Sprinkle/actions/builtin/profile-actions.js.map +1 -0
  96. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js +81 -0
  97. package/dist/esm/Sprinkle/actions/builtin/settings-actions.js.map +1 -0
  98. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js +340 -0
  99. package/dist/esm/Sprinkle/actions/builtin/transaction-actions.js.map +1 -0
  100. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js +213 -0
  101. package/dist/esm/Sprinkle/actions/builtin/utility-actions.js.map +1 -0
  102. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js +207 -0
  103. package/dist/esm/Sprinkle/actions/builtin/wallet-actions.js.map +1 -0
  104. package/dist/esm/Sprinkle/actions/cli-adapter.js +379 -0
  105. package/dist/esm/Sprinkle/actions/cli-adapter.js.map +1 -0
  106. package/dist/esm/Sprinkle/actions/index.js +12 -0
  107. package/dist/esm/Sprinkle/actions/index.js.map +1 -0
  108. package/dist/esm/Sprinkle/actions/mcp-adapter.js +547 -0
  109. package/dist/esm/Sprinkle/actions/mcp-adapter.js.map +1 -0
  110. package/dist/esm/Sprinkle/actions/registry.js +85 -0
  111. package/dist/esm/Sprinkle/actions/registry.js.map +1 -0
  112. package/dist/esm/Sprinkle/actions/runner.js +182 -0
  113. package/dist/esm/Sprinkle/actions/runner.js.map +1 -0
  114. package/dist/esm/Sprinkle/actions/tui-helpers.js +91 -0
  115. package/dist/esm/Sprinkle/actions/tui-helpers.js.map +1 -0
  116. package/dist/esm/Sprinkle/actions/types.js +61 -0
  117. package/dist/esm/Sprinkle/actions/types.js.map +1 -0
  118. package/dist/esm/Sprinkle/index.js +517 -7
  119. package/dist/esm/Sprinkle/index.js.map +1 -1
  120. package/dist/esm/Sprinkle/prompts.js +12 -7
  121. package/dist/esm/Sprinkle/prompts.js.map +1 -1
  122. package/dist/esm/Sprinkle/schemas.js +16 -0
  123. package/dist/esm/Sprinkle/schemas.js.map +1 -1
  124. package/dist/esm/Sprinkle/type-guards.js +3 -0
  125. package/dist/esm/Sprinkle/type-guards.js.map +1 -1
  126. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts +50 -0
  127. package/dist/types/Sprinkle/actions/builtin/addressbook-actions.d.ts.map +1 -0
  128. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts +39 -0
  129. package/dist/types/Sprinkle/actions/builtin/blaze-helper.d.ts.map +1 -0
  130. package/dist/types/Sprinkle/actions/builtin/index.d.ts +30 -0
  131. package/dist/types/Sprinkle/actions/builtin/index.d.ts.map +1 -0
  132. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts +27 -0
  133. package/dist/types/Sprinkle/actions/builtin/native-script.d.ts.map +1 -0
  134. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts +55 -0
  135. package/dist/types/Sprinkle/actions/builtin/profile-actions.d.ts.map +1 -0
  136. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts +32 -0
  137. package/dist/types/Sprinkle/actions/builtin/settings-actions.d.ts.map +1 -0
  138. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts +70 -0
  139. package/dist/types/Sprinkle/actions/builtin/transaction-actions.d.ts.map +1 -0
  140. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts +48 -0
  141. package/dist/types/Sprinkle/actions/builtin/utility-actions.d.ts.map +1 -0
  142. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts +50 -0
  143. package/dist/types/Sprinkle/actions/builtin/wallet-actions.d.ts.map +1 -0
  144. package/dist/types/Sprinkle/actions/cli-adapter.d.ts +104 -0
  145. package/dist/types/Sprinkle/actions/cli-adapter.d.ts.map +1 -0
  146. package/dist/types/Sprinkle/actions/index.d.ts +13 -0
  147. package/dist/types/Sprinkle/actions/index.d.ts.map +1 -0
  148. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts +116 -0
  149. package/dist/types/Sprinkle/actions/mcp-adapter.d.ts.map +1 -0
  150. package/dist/types/Sprinkle/actions/registry.d.ts +42 -0
  151. package/dist/types/Sprinkle/actions/registry.d.ts.map +1 -0
  152. package/dist/types/Sprinkle/actions/runner.d.ts +45 -0
  153. package/dist/types/Sprinkle/actions/runner.d.ts.map +1 -0
  154. package/dist/types/Sprinkle/actions/tui-helpers.d.ts +53 -0
  155. package/dist/types/Sprinkle/actions/tui-helpers.d.ts.map +1 -0
  156. package/dist/types/Sprinkle/actions/types.d.ts +76 -0
  157. package/dist/types/Sprinkle/actions/types.d.ts.map +1 -0
  158. package/dist/types/Sprinkle/index.d.ts +84 -2
  159. package/dist/types/Sprinkle/index.d.ts.map +1 -1
  160. package/dist/types/Sprinkle/prompts.d.ts.map +1 -1
  161. package/dist/types/Sprinkle/schemas.d.ts +72 -0
  162. package/dist/types/Sprinkle/schemas.d.ts.map +1 -1
  163. package/dist/types/Sprinkle/type-guards.d.ts +4 -1
  164. package/dist/types/Sprinkle/type-guards.d.ts.map +1 -1
  165. package/dist/types/tsconfig.build.tsbuildinfo +1 -1
  166. package/package.json +9 -2
  167. package/src/Sprinkle/__tests__/action-integration.test.ts +558 -0
  168. package/src/Sprinkle/__tests__/action-registry.test.ts +187 -0
  169. package/src/Sprinkle/__tests__/action-runner.test.ts +324 -0
  170. package/src/Sprinkle/__tests__/builtin-actions.test.ts +1022 -0
  171. package/src/Sprinkle/__tests__/cli-adapter.test.ts +736 -0
  172. package/src/Sprinkle/__tests__/fill-in-struct.test.ts +23 -1
  173. package/src/Sprinkle/__tests__/mcp-adapter.test.ts +720 -0
  174. package/src/Sprinkle/__tests__/native-script.test.ts +341 -0
  175. package/src/Sprinkle/__tests__/tui-helpers.test.ts +325 -0
  176. package/src/Sprinkle/__tests__/utility-actions.test.ts +348 -0
  177. package/src/Sprinkle/__tests__/wallet-transaction-actions.test.ts +695 -0
  178. package/src/Sprinkle/actions/builtin/addressbook-actions.ts +168 -0
  179. package/src/Sprinkle/actions/builtin/blaze-helper.ts +89 -0
  180. package/src/Sprinkle/actions/builtin/index.ts +125 -0
  181. package/src/Sprinkle/actions/builtin/native-script.ts +165 -0
  182. package/src/Sprinkle/actions/builtin/profile-actions.ts +229 -0
  183. package/src/Sprinkle/actions/builtin/settings-actions.ts +99 -0
  184. package/src/Sprinkle/actions/builtin/transaction-actions.ts +381 -0
  185. package/src/Sprinkle/actions/builtin/utility-actions.ts +285 -0
  186. package/src/Sprinkle/actions/builtin/wallet-actions.ts +233 -0
  187. package/src/Sprinkle/actions/cli-adapter.ts +446 -0
  188. package/src/Sprinkle/actions/index.ts +33 -0
  189. package/src/Sprinkle/actions/mcp-adapter.ts +638 -0
  190. package/src/Sprinkle/actions/registry.ts +97 -0
  191. package/src/Sprinkle/actions/runner.ts +200 -0
  192. package/src/Sprinkle/actions/tui-helpers.ts +114 -0
  193. package/src/Sprinkle/actions/types.ts +91 -0
  194. package/src/Sprinkle/index.ts +612 -3
  195. package/src/Sprinkle/prompts.ts +118 -72
  196. package/src/Sprinkle/schemas.ts +20 -0
  197. 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
+ });