@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.
- package/.cursor/rules/marketplace-docs-sync.mdc +32 -0
- package/CHANGELOG.md +40 -0
- package/PUBLISH_CHECKLIST.md +40 -88
- package/README.md +446 -78
- package/dist/client.d.ts +1 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +2 -1
- package/dist/http.js +42 -6
- package/dist/index.js +12 -3
- package/dist/tools/batch_orders.d.ts +5 -0
- package/dist/tools/batch_orders.d.ts.map +1 -1
- package/dist/tools/batch_orders.js +35 -1
- package/dist/tools/cancel_order.js +1 -1
- package/dist/tools/dead_mans_switch.d.ts +1 -1
- package/dist/tools/dead_mans_switch.d.ts.map +1 -1
- package/dist/tools/dead_mans_switch.js +33 -3
- package/dist/tools/lightning.d.ts.map +1 -1
- package/dist/tools/lightning.js +15 -1
- package/dist/tools/place_order.d.ts.map +1 -1
- package/dist/tools/place_order.js +31 -0
- package/dist/tools/receive_addresses.d.ts +5 -0
- package/dist/tools/receive_addresses.d.ts.map +1 -1
- package/dist/tools/receive_addresses.js +26 -2
- package/dist/tools/remittances.d.ts +5 -0
- package/dist/tools/remittances.d.ts.map +1 -1
- package/dist/tools/remittances.js +27 -3
- package/dist/tools/technical_indicators.d.ts.map +1 -1
- package/dist/tools/technical_indicators.js +2 -1
- package/dist/tools/withdrawals.d.ts.map +1 -1
- package/dist/tools/withdrawals.js +11 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +4 -1
- package/dist/validation.d.ts +6 -0
- package/dist/validation.d.ts.map +1 -1
- package/dist/validation.js +26 -0
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +8 -1
- package/marketplace/README.md +1 -1
- package/marketplace/claude-listing.md +75 -4
- package/marketplace/gemini-tools.json +325 -2
- package/marketplace/openapi.yaml +160 -1
- package/package.json +2 -1
- package/server.json +2 -2
- package/src/client.ts +3 -1
- package/src/http.ts +50 -6
- package/src/index.ts +10 -3
- package/src/tools/batch_orders.ts +40 -1
- package/src/tools/cancel_order.ts +1 -1
- package/src/tools/dead_mans_switch.ts +39 -3
- package/src/tools/lightning.ts +17 -1
- package/src/tools/place_order.ts +32 -0
- package/src/tools/receive_addresses.ts +29 -3
- package/src/tools/remittances.ts +30 -4
- package/src/tools/technical_indicators.ts +2 -1
- package/src/tools/withdrawals.ts +12 -1
- package/src/utils.ts +3 -1
- package/src/validation.ts +29 -0
- package/src/version.ts +11 -3
- package/test/unit.ts +261 -18
package/src/tools/remittances.ts
CHANGED
|
@@ -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
|
-
"
|
|
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,
|
package/src/tools/withdrawals.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
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: "
|
|
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: "
|
|
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
|
// ----------------------------------------------------------------
|