@guiie/buda-mcp 1.5.0 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.cursor/rules/marketplace-docs-sync.mdc +32 -0
  2. package/CHANGELOG.md +40 -0
  3. package/PUBLISH_CHECKLIST.md +40 -88
  4. package/README.md +446 -78
  5. package/dist/client.d.ts +1 -0
  6. package/dist/client.d.ts.map +1 -1
  7. package/dist/client.js +2 -1
  8. package/dist/http.js +42 -6
  9. package/dist/index.js +12 -3
  10. package/dist/tools/batch_orders.d.ts +5 -0
  11. package/dist/tools/batch_orders.d.ts.map +1 -1
  12. package/dist/tools/batch_orders.js +35 -1
  13. package/dist/tools/cancel_order.js +1 -1
  14. package/dist/tools/dead_mans_switch.d.ts +1 -1
  15. package/dist/tools/dead_mans_switch.d.ts.map +1 -1
  16. package/dist/tools/dead_mans_switch.js +33 -3
  17. package/dist/tools/lightning.d.ts.map +1 -1
  18. package/dist/tools/lightning.js +15 -1
  19. package/dist/tools/place_order.d.ts.map +1 -1
  20. package/dist/tools/place_order.js +31 -0
  21. package/dist/tools/receive_addresses.d.ts +5 -0
  22. package/dist/tools/receive_addresses.d.ts.map +1 -1
  23. package/dist/tools/receive_addresses.js +26 -2
  24. package/dist/tools/remittances.d.ts +5 -0
  25. package/dist/tools/remittances.d.ts.map +1 -1
  26. package/dist/tools/remittances.js +27 -3
  27. package/dist/tools/technical_indicators.d.ts.map +1 -1
  28. package/dist/tools/technical_indicators.js +2 -1
  29. package/dist/tools/withdrawals.d.ts.map +1 -1
  30. package/dist/tools/withdrawals.js +11 -1
  31. package/dist/utils.d.ts.map +1 -1
  32. package/dist/utils.js +4 -1
  33. package/dist/validation.d.ts +6 -0
  34. package/dist/validation.d.ts.map +1 -1
  35. package/dist/validation.js +26 -0
  36. package/dist/version.d.ts.map +1 -1
  37. package/dist/version.js +8 -1
  38. package/marketplace/README.md +1 -1
  39. package/marketplace/claude-listing.md +75 -4
  40. package/marketplace/gemini-tools.json +325 -2
  41. package/marketplace/openapi.yaml +160 -1
  42. package/package.json +2 -1
  43. package/server.json +2 -2
  44. package/src/client.ts +3 -1
  45. package/src/http.ts +50 -6
  46. package/src/index.ts +10 -3
  47. package/src/tools/batch_orders.ts +40 -1
  48. package/src/tools/cancel_order.ts +1 -1
  49. package/src/tools/dead_mans_switch.ts +39 -3
  50. package/src/tools/lightning.ts +17 -1
  51. package/src/tools/place_order.ts +32 -0
  52. package/src/tools/receive_addresses.ts +29 -3
  53. package/src/tools/remittances.ts +30 -4
  54. package/src/tools/technical_indicators.ts +2 -1
  55. package/src/tools/withdrawals.ts +12 -1
  56. package/src/utils.ts +3 -1
  57. package/src/validation.ts +29 -0
  58. package/src/version.ts +11 -3
  59. package/test/unit.ts +261 -18
@@ -34,7 +34,8 @@ export const quoteRemittanceToolSchema = {
34
34
  "Requests a price quote for a fiat remittance to a saved recipient. " +
35
35
  "Returns a remittance object in 'quoted' state with an expiry timestamp. " +
36
36
  "NOT idempotent — creates a new remittance record each call. " +
37
- "To execute, call accept_remittance_quote with the returned ID before it expires. " +
37
+ "IMPORTANT: Pass confirmation_token='CONFIRM' to execute. " +
38
+ "To execute the transfer, call accept_remittance_quote with the returned ID before it expires. " +
38
39
  "Requires BUDA_API_KEY and BUDA_API_SECRET. " +
39
40
  "Example: 'Get a remittance quote to send 100000 CLP to recipient 5.'",
40
41
  inputSchema: {
@@ -52,8 +53,12 @@ export const quoteRemittanceToolSchema = {
52
53
  type: "number",
53
54
  description: "ID of the saved remittance recipient.",
54
55
  },
56
+ confirmation_token: {
57
+ type: "string",
58
+ description: "Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to create the quote.",
59
+ },
55
60
  },
56
- required: ["currency", "amount", "recipient_id"],
61
+ required: ["currency", "amount", "recipient_id", "confirmation_token"],
57
62
  },
58
63
  };
59
64
 
@@ -178,10 +183,28 @@ export async function handleGetRemittance(
178
183
  }
179
184
 
180
185
  export async function handleQuoteRemittance(
181
- args: { currency: string; amount: number; recipient_id: number },
186
+ args: { currency: string; amount: number; recipient_id: number; confirmation_token: string },
182
187
  client: BudaClient,
183
188
  ): Promise<{ content: Array<{ type: "text"; text: string }>; isError?: boolean }> {
184
- const { currency, amount, recipient_id } = args;
189
+ const { currency, amount, recipient_id, confirmation_token } = args;
190
+
191
+ if (confirmation_token !== "CONFIRM") {
192
+ return {
193
+ content: [
194
+ {
195
+ type: "text",
196
+ text: JSON.stringify({
197
+ error:
198
+ "Remittance quote not created. confirmation_token must equal 'CONFIRM' to execute. " +
199
+ "Review the details and set confirmation_token='CONFIRM' to proceed.",
200
+ code: "CONFIRMATION_REQUIRED",
201
+ preview: { currency, amount, recipient_id },
202
+ }),
203
+ },
204
+ ],
205
+ isError: true,
206
+ };
207
+ }
185
208
 
186
209
  const validationError = validateCurrency(currency);
187
210
  if (validationError) {
@@ -283,6 +306,9 @@ export function register(server: McpServer, client: BudaClient): void {
283
306
  currency: z.string().min(2).max(10).describe("Fiat currency code (e.g. 'CLP', 'COP')."),
284
307
  amount: z.number().positive().describe("Amount to remit (positive number)."),
285
308
  recipient_id: z.number().int().positive().describe("ID of the saved remittance recipient."),
309
+ confirmation_token: z
310
+ .string()
311
+ .describe("Safety confirmation. Must equal exactly 'CONFIRM' (case-sensitive) to create the quote."),
286
312
  },
287
313
  (args) => handleQuoteRemittance(args, client),
288
314
  );
@@ -191,7 +191,7 @@ export async function handleTechnicalIndicators(
191
191
  const macdResult = macd(closes, 12, 26, 9);
192
192
  const bbResult = bollingerBands(closes, 20, 2);
193
193
  const sma20 = parseFloat(sma(closes, 20).toFixed(2));
194
- const sma50 = parseFloat(sma(closes, 50).toFixed(2));
194
+ const sma50 = closes.length >= 50 ? parseFloat(sma(closes, 50).toFixed(2)) : null;
195
195
  const lastClose = closes[closes.length - 1];
196
196
 
197
197
  // Signal interpretations
@@ -230,6 +230,7 @@ export async function handleTechnicalIndicators(
230
230
  bollinger_bands: bbResult,
231
231
  sma_20: sma20,
232
232
  sma_50: sma50,
233
+ sma_50_warning: sma50 === null ? `insufficient data (need 50 candles, have ${closes.length})` : undefined,
233
234
  },
234
235
  signals: {
235
236
  rsi_signal: rsiSignal,
@@ -1,7 +1,7 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { z } from "zod";
3
3
  import { BudaClient, BudaApiError } from "../client.js";
4
- import { validateCurrency } from "../validation.js";
4
+ import { validateCurrency, validateCryptoAddress } from "../validation.js";
5
5
  import { flattenAmount } from "../utils.js";
6
6
  import type { WithdrawalsResponse, SingleWithdrawalResponse, Withdrawal } from "../types.js";
7
7
 
@@ -120,6 +120,7 @@ export const createWithdrawalToolSchema = {
120
120
  description:
121
121
  "Create a withdrawal on Buda.com. Supports both crypto (address) and fiat (bank_account_id) withdrawals. " +
122
122
  "Exactly one of address or bank_account_id must be provided. " +
123
+ "WARNING: Crypto withdrawals are irreversible — verify the destination address carefully before confirming. " +
123
124
  "IMPORTANT: Pass confirmation_token='CONFIRM' to execute. " +
124
125
  "Requires BUDA_API_KEY and BUDA_API_SECRET.",
125
126
  inputSchema: {
@@ -213,6 +214,16 @@ export async function handleCreateWithdrawal(
213
214
  };
214
215
  }
215
216
 
217
+ if (hasAddress) {
218
+ const addrError = validateCryptoAddress(address!, currency);
219
+ if (addrError) {
220
+ return {
221
+ content: [{ type: "text", text: JSON.stringify({ error: addrError, code: "INVALID_ADDRESS" }) }],
222
+ isError: true,
223
+ };
224
+ }
225
+ }
226
+
216
227
  try {
217
228
  const payload: Record<string, unknown> = { amount: String(amount) };
218
229
  if (hasAddress) {
package/src/utils.ts CHANGED
@@ -5,7 +5,9 @@ import type { Amount, OhlcvCandle } from "./types.js";
5
5
  * All numeric strings are cast to float via parseFloat.
6
6
  */
7
7
  export function flattenAmount(amount: Amount): { value: number; currency: string } {
8
- return { value: parseFloat(amount[0]), currency: amount[1] };
8
+ const value = parseFloat(amount[0]);
9
+ if (isNaN(value)) throw new Error(`Invalid amount value: "${amount[0]}"`);
10
+ return { value, currency: amount[1] };
9
11
  }
10
12
 
11
13
  /**
package/src/validation.ts CHANGED
@@ -30,3 +30,32 @@ export function validateCurrency(id: string): string | null {
30
30
  }
31
31
  return null;
32
32
  }
33
+
34
+ // Per-currency address format rules.
35
+ // Unknown currencies pass through (undefined rule) — the exchange validates those.
36
+ const ADDRESS_RULES: Record<string, RegExp> = {
37
+ BTC: /^(bc1[a-z0-9]{6,87}|[13][a-zA-HJ-NP-Z0-9]{25,34})$/,
38
+ ETH: /^0x[0-9a-fA-F]{40}$/,
39
+ USDC: /^0x[0-9a-fA-F]{40}$/,
40
+ USDT: /^0x[0-9a-fA-F]{40}$/,
41
+ LTC: /^(ltc1[a-z0-9]{6,87}|[LM3][a-zA-HJ-NP-Z0-9]{25,34})$/,
42
+ BCH: /^(bitcoincash:)?[qp][a-z0-9]{41}$/,
43
+ XRP: /^r[1-9A-HJ-NP-Za-km-z]{24,33}$/,
44
+ };
45
+
46
+ /**
47
+ * Validates a crypto withdrawal address against known per-currency formats.
48
+ * Returns an error message string if the address is invalid, or null if valid
49
+ * (including null for unknown currencies, where the exchange is the last line of defence).
50
+ */
51
+ export function validateCryptoAddress(address: string, currency: string): string | null {
52
+ const rule = ADDRESS_RULES[currency.toUpperCase()];
53
+ if (!rule) return null;
54
+ if (!rule.test(address)) {
55
+ return (
56
+ `Invalid ${currency.toUpperCase()} address format. ` +
57
+ `Double-check the destination address — crypto withdrawals are irreversible.`
58
+ );
59
+ }
60
+ return null;
61
+ }
package/src/version.ts CHANGED
@@ -3,6 +3,14 @@ import { fileURLToPath } from "url";
3
3
  import { dirname, join } from "path";
4
4
 
5
5
  const _dir = dirname(fileURLToPath(import.meta.url));
6
- export const VERSION: string = (
7
- JSON.parse(readFileSync(join(_dir, "../package.json"), "utf8")) as { version: string }
8
- ).version;
6
+
7
+ let _version = "unknown";
8
+ try {
9
+ _version = (
10
+ JSON.parse(readFileSync(join(_dir, "../package.json"), "utf8")) as { version: string }
11
+ ).version;
12
+ } catch {
13
+ // package.json not found in deployment — use fallback
14
+ }
15
+
16
+ export const VERSION: string = _version;
package/test/unit.ts CHANGED
@@ -6,7 +6,7 @@
6
6
  import { createHmac } from "crypto";
7
7
  import { BudaClient, BudaApiError } from "../src/client.js";
8
8
  import { MemoryCache } from "../src/cache.js";
9
- import { validateMarketId } from "../src/validation.js";
9
+ import { validateMarketId, validateCryptoAddress } from "../src/validation.js";
10
10
  import { handlePlaceOrder } from "../src/tools/place_order.js";
11
11
  import { handleCancelOrder } from "../src/tools/cancel_order.js";
12
12
  import { flattenAmount, getLiquidityRating, aggregateTradesToCandles } from "../src/utils.js";
@@ -1911,7 +1911,7 @@ await test("handleCreateReceiveAddress: INVALID_CURRENCY without fetch", async (
1911
1911
  };
1912
1912
  try {
1913
1913
  const client = {} as BudaClient;
1914
- const result = await handleCreateReceiveAddress({ currency: "!!!!" }, client);
1914
+ const result = await handleCreateReceiveAddress({ currency: "!!!!", confirmation_token: "CONFIRM" }, client);
1915
1915
  assert(result.isError === true, "should be error");
1916
1916
  assert(!fetchCalled.value, "fetch should not have been called");
1917
1917
  const parsed = JSON.parse(result.content[0].text) as { code: string };
@@ -1938,7 +1938,7 @@ await test("handleCreateReceiveAddress: happy path", async () => {
1938
1938
  );
1939
1939
  try {
1940
1940
  const client = new BudaClient("https://www.buda.com/api/v2");
1941
- const result = await handleCreateReceiveAddress({ currency: "BTC" }, client);
1941
+ const result = await handleCreateReceiveAddress({ currency: "BTC", confirmation_token: "CONFIRM" }, client);
1942
1942
  assert(!result.isError, "should not be error");
1943
1943
  const parsed = JSON.parse(result.content[0].text) as {
1944
1944
  id: number;
@@ -1961,7 +1961,7 @@ await test("handleCreateReceiveAddress: fiat currency API error passthrough", as
1961
1961
  new Response(JSON.stringify({ message: "Not found" }), { status: 404 });
1962
1962
  try {
1963
1963
  const client = new BudaClient("https://www.buda.com/api/v2");
1964
- const result = await handleCreateReceiveAddress({ currency: "CLP" }, client);
1964
+ const result = await handleCreateReceiveAddress({ currency: "CLP", confirmation_token: "CONFIRM" }, client);
1965
1965
  assert(result.isError === true, "should be error for fiat");
1966
1966
  const parsed = JSON.parse(result.content[0].text) as { code: number };
1967
1967
  assertEqual(parsed.code, 404, "code should be 404");
@@ -1985,7 +1985,7 @@ await test("handleQuoteRemittance: INVALID_CURRENCY without fetch", async () =>
1985
1985
  };
1986
1986
  try {
1987
1987
  const client = {} as BudaClient;
1988
- const result = await handleQuoteRemittance({ currency: "!!!", amount: 100, recipient_id: 1 }, client);
1988
+ const result = await handleQuoteRemittance({ currency: "!!!", amount: 100, recipient_id: 1, confirmation_token: "CONFIRM" }, client);
1989
1989
  assert(result.isError === true, "should be error");
1990
1990
  assert(!fetchCalled.value, "fetch should not have been called");
1991
1991
  const parsed = JSON.parse(result.content[0].text) as { code: string };
@@ -2014,7 +2014,7 @@ await test("handleQuoteRemittance: happy path", async () => {
2014
2014
  );
2015
2015
  try {
2016
2016
  const client = new BudaClient("https://www.buda.com/api/v2");
2017
- const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 5 }, client);
2017
+ const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 5, confirmation_token: "CONFIRM" }, client);
2018
2018
  assert(!result.isError, "should not be error");
2019
2019
  const parsed = JSON.parse(result.content[0].text) as {
2020
2020
  id: number;
@@ -2035,7 +2035,7 @@ await test("handleQuoteRemittance: 404 unknown recipient passthrough", async ()
2035
2035
  new Response(JSON.stringify({ message: "Not found" }), { status: 404 });
2036
2036
  try {
2037
2037
  const client = new BudaClient("https://www.buda.com/api/v2");
2038
- const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 9999 }, client);
2038
+ const result = await handleQuoteRemittance({ currency: "CLP", amount: 100000, recipient_id: 9999, confirmation_token: "CONFIRM" }, client);
2039
2039
  assert(result.isError === true, "should be error");
2040
2040
  const parsed = JSON.parse(result.content[0].text) as { code: number };
2041
2041
  assertEqual(parsed.code, 404, "code should be 404");
@@ -2732,19 +2732,19 @@ await test("handleCreateWithdrawal: crypto path + CONFIRM", async () => {
2732
2732
  currency: "BTC",
2733
2733
  amount: ["0.01", "BTC"],
2734
2734
  fee: ["0.0001", "BTC"],
2735
- address: "bc1q...",
2736
- tx_hash: null,
2737
- bank_account_id: null,
2738
- created_at: "2024-01-01T00:00:00Z",
2739
- updated_at: "2024-01-01T00:00:00Z",
2740
- },
2741
- }),
2742
- { status: 201, headers: { "Content-Type": "application/json" } },
2743
- );
2735
+ address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq",
2736
+ tx_hash: null,
2737
+ bank_account_id: null,
2738
+ created_at: "2024-01-01T00:00:00Z",
2739
+ updated_at: "2024-01-01T00:00:00Z",
2740
+ },
2741
+ }),
2742
+ { status: 201, headers: { "Content-Type": "application/json" } },
2743
+ );
2744
2744
  try {
2745
2745
  const client = new BudaClient("https://www.buda.com/api/v2");
2746
2746
  const result = await handleCreateWithdrawal(
2747
- { currency: "BTC", amount: 0.01, address: "bc1q...", confirmation_token: "CONFIRM" },
2747
+ { currency: "BTC", amount: 0.01, address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", confirmation_token: "CONFIRM" },
2748
2748
  client,
2749
2749
  );
2750
2750
  assert(!result.isError, "should not be error");
@@ -2804,7 +2804,7 @@ await test("handleCreateWithdrawal: 422 insufficient balance passthrough", async
2804
2804
  try {
2805
2805
  const client = new BudaClient("https://www.buda.com/api/v2");
2806
2806
  const result = await handleCreateWithdrawal(
2807
- { currency: "BTC", amount: 999, address: "bc1q...", confirmation_token: "CONFIRM" },
2807
+ { currency: "BTC", amount: 999, address: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", confirmation_token: "CONFIRM" },
2808
2808
  client,
2809
2809
  );
2810
2810
  assert(result.isError === true, "should be error");
@@ -3064,6 +3064,249 @@ await test("handleCreateLightningInvoice: API error passthrough", async () => {
3064
3064
  }
3065
3065
  });
3066
3066
 
3067
+ // ----------------------------------------------------------------
3068
+ // Fix 3 — validateCryptoAddress
3069
+ // ----------------------------------------------------------------
3070
+
3071
+ section("validateCryptoAddress");
3072
+
3073
+ await test("validateCryptoAddress: valid BTC bech32 address passes", () => {
3074
+ assertEqual(validateCryptoAddress("bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", "BTC"), null, "valid bech32 should pass");
3075
+ });
3076
+
3077
+ await test("validateCryptoAddress: valid BTC legacy P2PKH address passes", () => {
3078
+ assertEqual(validateCryptoAddress("1BpEi6DfDAUFd153wiGrvkiKyhSua3FrN", "BTC"), null, "valid P2PKH should pass");
3079
+ });
3080
+
3081
+ await test("validateCryptoAddress: BTC address too short after bc1 prefix is rejected", () => {
3082
+ // bc1 + fewer than 6 alphanumeric chars — too short to be a real address
3083
+ assert(validateCryptoAddress("bc1qa", "BTC") !== null, "bc1 + 1 char should fail");
3084
+ });
3085
+
3086
+ await test("validateCryptoAddress: BTC address with wrong prefix is rejected", () => {
3087
+ assert(validateCryptoAddress("XpubBadAddress1234567890abcdefgh", "BTC") !== null, "wrong prefix should fail");
3088
+ });
3089
+
3090
+ await test("validateCryptoAddress: valid ETH address passes", () => {
3091
+ // Standard 40-hex-char Ethereum address (checksummed)
3092
+ assertEqual(validateCryptoAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "ETH"), null, "valid ETH address should pass");
3093
+ });
3094
+
3095
+ await test("validateCryptoAddress: ETH address wrong length is rejected", () => {
3096
+ assert(validateCryptoAddress("0xde0B295669a9FD93d5F28D9", "ETH") !== null, "short ETH address should fail");
3097
+ });
3098
+
3099
+ await test("validateCryptoAddress: valid XRP address passes", () => {
3100
+ assertEqual(validateCryptoAddress("rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "XRP"), null, "valid XRP address should pass");
3101
+ });
3102
+
3103
+ await test("validateCryptoAddress: XRP address with wrong prefix is rejected", () => {
3104
+ assert(validateCryptoAddress("xHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh", "XRP") !== null, "XRP with 'x' prefix should fail");
3105
+ });
3106
+
3107
+ await test("validateCryptoAddress: unknown currency returns null (pass-through)", () => {
3108
+ // ALGO not in ADDRESS_RULES — should pass through to let exchange validate
3109
+ assertEqual(validateCryptoAddress("SOMEADDRESS123", "ALGO"), null, "unknown currency should pass through");
3110
+ });
3111
+
3112
+ await test("validateCryptoAddress: USDC treated as EVM address", () => {
3113
+ assertEqual(validateCryptoAddress("0x742d35Cc6634C0532925a3b844Bc454e4438f44e", "USDC"), null, "valid EVM address for USDC should pass");
3114
+ assert(validateCryptoAddress("not-an-address", "USDC") !== null, "invalid EVM address for USDC should fail");
3115
+ });
3116
+
3117
+ // ----------------------------------------------------------------
3118
+ // Fix 4 — handleLightningWithdrawal BOLT-11 validation
3119
+ // ----------------------------------------------------------------
3120
+
3121
+ section("handleLightningWithdrawal — BOLT-11 format guard");
3122
+
3123
+ await test("handleLightningWithdrawal: non-BOLT11 string returns INVALID_INVOICE before API call", async () => {
3124
+ let fetchCalled = false;
3125
+ const savedFetch = globalThis.fetch;
3126
+ globalThis.fetch = async () => {
3127
+ fetchCalled = true;
3128
+ return new Response("{}", { status: 200 });
3129
+ };
3130
+ try {
3131
+ const client = new BudaClient(undefined, "key", "secret");
3132
+ const result = await handleLightningWithdrawal(
3133
+ { invoice: "not-a-lightning-invoice-at-all-just-garbage-string-here", confirmation_token: "CONFIRM" },
3134
+ client,
3135
+ );
3136
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3137
+ assertEqual(parsed.code, "INVALID_INVOICE", "should return INVALID_INVOICE");
3138
+ assert(result.isError === true, "isError should be true");
3139
+ assert(!fetchCalled, "fetch should NOT have been called");
3140
+ } finally {
3141
+ globalThis.fetch = savedFetch;
3142
+ }
3143
+ });
3144
+
3145
+ await test("handleLightningWithdrawal: testnet invoice prefix 'lntb' is accepted (passes format check)", async () => {
3146
+ // A syntactically valid lntb prefix — API will reject it, but format guard should pass
3147
+ const savedFetch = globalThis.fetch;
3148
+ globalThis.fetch = async () => new Response(
3149
+ JSON.stringify({ message: "invalid invoice" }), { status: 422 },
3150
+ );
3151
+ try {
3152
+ const client = new BudaClient(undefined, "key", "secret");
3153
+ const fakeInvoice = "lntb1230n1pj8ygappp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypq";
3154
+ const result = await handleLightningWithdrawal(
3155
+ { invoice: fakeInvoice, confirmation_token: "CONFIRM" },
3156
+ client,
3157
+ );
3158
+ // Should NOT return INVALID_INVOICE — the format guard passes; API error is expected
3159
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3160
+ assert(parsed.code !== "INVALID_INVOICE", "BOLT-11 prefix should pass format guard");
3161
+ } finally {
3162
+ globalThis.fetch = savedFetch;
3163
+ }
3164
+ });
3165
+
3166
+ await test("handleLightningWithdrawal: invoice without BOLT-11 prefix still blocked by INVALID_INVOICE after CONFIRM", async () => {
3167
+ const client = new BudaClient(undefined, "key", "secret");
3168
+ const result = await handleLightningWithdrawal(
3169
+ { invoice: "bc1qar0srrr7xfkvy5l643lydnw9re59gtzzwf5mdq", confirmation_token: "CONFIRM" },
3170
+ client,
3171
+ );
3172
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3173
+ assertEqual(parsed.code, "INVALID_INVOICE", "BTC address passed as invoice should fail BOLT-11 check");
3174
+ });
3175
+
3176
+ // ----------------------------------------------------------------
3177
+ // Fix 5 — Dead man's switch: TRANSPORT_NOT_SUPPORTED on HTTP
3178
+ // ----------------------------------------------------------------
3179
+
3180
+ section("handleScheduleCancelAll — HTTP transport guard");
3181
+
3182
+ await test("handleScheduleCancelAll: transport='http' returns TRANSPORT_NOT_SUPPORTED", async () => {
3183
+ const client = new BudaClient(undefined, "key", "secret");
3184
+ // Simulate what register() does on HTTP transport by calling handleScheduleCancelAll
3185
+ // We test the transport guard via the register() wrapper indirectly through a fake server tool.
3186
+ // Since handleScheduleCancelAll itself doesn't know the transport, we test the guard
3187
+ // by verifying the pattern: a fake dispatch that mirrors the http register() closure.
3188
+ const transportGuard = (transport: "stdio" | "http") => {
3189
+ if (transport === "http") {
3190
+ return Promise.resolve({
3191
+ content: [{ type: "text" as const, text: JSON.stringify({ code: "TRANSPORT_NOT_SUPPORTED" }) }],
3192
+ isError: true,
3193
+ });
3194
+ }
3195
+ return handleScheduleCancelAll(
3196
+ { market_id: "BTC-CLP", ttl_seconds: 30, confirmation_token: "CONFIRM" },
3197
+ client,
3198
+ );
3199
+ };
3200
+
3201
+ const result = await transportGuard("http");
3202
+ const parsed = JSON.parse(result.content[0].text) as { code: string };
3203
+ assertEqual(parsed.code, "TRANSPORT_NOT_SUPPORTED", "HTTP transport should return TRANSPORT_NOT_SUPPORTED");
3204
+ assert(result.isError === true, "isError should be true");
3205
+ });
3206
+
3207
+ await test("handleScheduleCancelAll: transport='stdio' still works normally", async () => {
3208
+ const client = new BudaClient(undefined, "key", "secret");
3209
+ const result = await handleScheduleCancelAll(
3210
+ { market_id: "BTC-CLP", ttl_seconds: 10, confirmation_token: "CONFIRM" },
3211
+ client,
3212
+ );
3213
+ const parsed = JSON.parse(result.content[0].text) as { active: boolean; warning: string };
3214
+ assert(parsed.active === true, "stdio transport should arm the switch");
3215
+ assert(typeof parsed.warning === "string", "should include in-memory warning");
3216
+ // Clean up timer
3217
+ const { handleDisarmCancelTimer: disarm } = await import("../src/tools/dead_mans_switch.js");
3218
+ disarm({ market_id: "BTC-CLP" });
3219
+ });
3220
+
3221
+ // ----------------------------------------------------------------
3222
+ // Fix 6 — place_batch_orders: max_notional cap
3223
+ // ----------------------------------------------------------------
3224
+
3225
+ section("handlePlaceBatchOrders — max_notional cap");
3226
+
3227
+ await test("handlePlaceBatchOrders: exceeding max_notional rejects before any API call", async () => {
3228
+ let fetchCalled = false;
3229
+ const savedFetch = globalThis.fetch;
3230
+ globalThis.fetch = async () => {
3231
+ fetchCalled = true;
3232
+ return new Response("{}", { status: 200 });
3233
+ };
3234
+ try {
3235
+ const client = new BudaClient(undefined, "key", "secret");
3236
+ const result = await handlePlaceBatchOrders(
3237
+ {
3238
+ orders: [
3239
+ { market_id: "BTC-CLP", type: "Bid", price_type: "limit", amount: 1, limit_price: 50_000_000 },
3240
+ { market_id: "BTC-CLP", type: "Bid", price_type: "limit", amount: 0.5, limit_price: 50_000_000 },
3241
+ ],
3242
+ max_notional: 60_000_000,
3243
+ confirmation_token: "CONFIRM",
3244
+ },
3245
+ client,
3246
+ );
3247
+ // total notional = 1*50_000_000 + 0.5*50_000_000 = 75_000_000 > 60_000_000
3248
+ const parsed = JSON.parse(result.content[0].text) as { code: string; total_notional: number };
3249
+ assertEqual(parsed.code, "NOTIONAL_CAP_EXCEEDED", "should return NOTIONAL_CAP_EXCEEDED");
3250
+ assert(parsed.total_notional === 75_000_000, "total_notional should be computed correctly");
3251
+ assert(result.isError === true, "isError should be true");
3252
+ assert(!fetchCalled, "fetch should NOT have been called");
3253
+ } finally {
3254
+ globalThis.fetch = savedFetch;
3255
+ }
3256
+ });
3257
+
3258
+ await test("handlePlaceBatchOrders: within max_notional proceeds to API calls", async () => {
3259
+ const savedFetch = globalThis.fetch;
3260
+ globalThis.fetch = async () => new Response(
3261
+ JSON.stringify({ order: { id: 99, state: "pending", market_id: "btc-clp" } }),
3262
+ { status: 200 },
3263
+ );
3264
+ try {
3265
+ const client = new BudaClient(undefined, "key", "secret");
3266
+ const result = await handlePlaceBatchOrders(
3267
+ {
3268
+ orders: [
3269
+ { market_id: "BTC-CLP", type: "Bid", price_type: "limit", amount: 0.001, limit_price: 50_000_000 },
3270
+ ],
3271
+ max_notional: 100_000,
3272
+ confirmation_token: "CONFIRM",
3273
+ },
3274
+ client,
3275
+ );
3276
+ // total notional = 0.001 * 50_000_000 = 50_000 < 100_000 — should proceed
3277
+ const parsed = JSON.parse(result.content[0].text) as { succeeded: number };
3278
+ assertEqual(parsed.succeeded, 1, "order within cap should succeed");
3279
+ } finally {
3280
+ globalThis.fetch = savedFetch;
3281
+ }
3282
+ });
3283
+
3284
+ await test("handlePlaceBatchOrders: market orders contribute 0 to notional (cap not triggered)", async () => {
3285
+ const savedFetch = globalThis.fetch;
3286
+ globalThis.fetch = async () => new Response(
3287
+ JSON.stringify({ order: { id: 100, state: "pending", market_id: "btc-clp" } }),
3288
+ { status: 200 },
3289
+ );
3290
+ try {
3291
+ const client = new BudaClient(undefined, "key", "secret");
3292
+ const result = await handlePlaceBatchOrders(
3293
+ {
3294
+ orders: [
3295
+ { market_id: "BTC-CLP", type: "Bid", price_type: "market", amount: 999 },
3296
+ ],
3297
+ max_notional: 1,
3298
+ confirmation_token: "CONFIRM",
3299
+ },
3300
+ client,
3301
+ );
3302
+ // Market order contributes 0, so notional = 0 < 1 — cap should NOT trigger
3303
+ const parsed = JSON.parse(result.content[0].text) as { code?: string; succeeded?: number };
3304
+ assert(parsed.code !== "NOTIONAL_CAP_EXCEEDED", "market order should not trigger notional cap");
3305
+ } finally {
3306
+ globalThis.fetch = savedFetch;
3307
+ }
3308
+ });
3309
+
3067
3310
  // ----------------------------------------------------------------
3068
3311
  // Summary
3069
3312
  // ----------------------------------------------------------------