@candledottv/mcp 0.10.19 → 0.10.21
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/README.md +4 -2
- package/dist/index.js +195 -49
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -57,8 +57,10 @@ Both take decimal amounts, never raw base units, and resolve the scale themselve
|
|
|
57
57
|
|
|
58
58
|
Both launch tools also take `mode: "exp-custom"` with `curve: { virtualQuote, threshold }`: a
|
|
59
59
|
Solana curve with your own V and G, in whole SOL or CNDL as decimal strings, inside limits the
|
|
60
|
-
API enforces and each tool's description states.
|
|
61
|
-
|
|
60
|
+
API enforces and each tool's description states. Any `exp-` mode takes `creatorFeeBps` (0, 100 or
|
|
61
|
+
200). Dry-run first: the dry run returns the launch preview (`curveTerms`: the curve's terms, the
|
|
62
|
+
share your own `buyAmount` takes, the fees and the migration terms) and `devBuyMax`, the dev-buy
|
|
63
|
+
ceiling that applies. `exp-custom` works only where the deployment has switched it on. See
|
|
62
64
|
[Custom curves](https://docs.candle.tv/developers/headless-launch#custom-curves-choose-v-and-g-switched-off).
|
|
63
65
|
|
|
64
66
|
For a full walkthrough of `candle_launch_and_seed` and `candle_trade`, including the keyless read
|
package/dist/index.js
CHANGED
|
@@ -151,7 +151,7 @@ import { lstat, open, realpath } from "node:fs/promises";
|
|
|
151
151
|
import { dirname, isAbsolute, relative, sep } from "node:path";
|
|
152
152
|
|
|
153
153
|
// src/version.ts
|
|
154
|
-
var SERVER_VERSION = "0.10.
|
|
154
|
+
var SERVER_VERSION = "0.10.21";
|
|
155
155
|
|
|
156
156
|
// src/update-notice.ts
|
|
157
157
|
var PLAIN_VERSION = /^\d+\.\d+\.\d+$/;
|
|
@@ -257,13 +257,13 @@ async function missingInside(dirReal, path, fs) {
|
|
|
257
257
|
}
|
|
258
258
|
}
|
|
259
259
|
}
|
|
260
|
-
async function readImageInDir(imageDir, imagePath, fs = nodeFs) {
|
|
260
|
+
async function readImageInDir(imageDir, imagePath, fs = nodeFs, field = "imagePath") {
|
|
261
261
|
if (!isAbsolute(imagePath))
|
|
262
|
-
throw new ImageRefusal("MCP_VALIDATION",
|
|
262
|
+
throw new ImageRefusal("MCP_VALIDATION", `${field} must be an absolute path inside CANDLE_IMAGE_DIR; this server's working directory is not something a caller can know. ${NOTHING_UPLOADED}`);
|
|
263
263
|
const dirReal = await realImageDir(imageDir, fs);
|
|
264
264
|
const outside = () => new ImageRefusal("IMAGE_OUTSIDE_DIR", OUTSIDE_IMAGE_DIR);
|
|
265
|
-
const unreadable = (why) => new ImageRefusal("IMAGE_UNREADABLE", `Could not read the image at
|
|
266
|
-
const tooLarge = () => new ImageRefusal("IMAGE_TOO_LARGE", `The file at
|
|
265
|
+
const unreadable = (why) => new ImageRefusal("IMAGE_UNREADABLE", `Could not read the image at ${field}: ${why}. ${NOTHING_UPLOADED}`);
|
|
266
|
+
const tooLarge = () => new ImageRefusal("IMAGE_TOO_LARGE", `The file at ${field} is over 10MB, the largest image Candle hosts. ${NOTHING_UPLOADED}`);
|
|
267
267
|
let fileReal;
|
|
268
268
|
try {
|
|
269
269
|
fileReal = await fs.realpath(imagePath);
|
|
@@ -321,47 +321,22 @@ async function readImageInDir(imageDir, imagePath, fs = nodeFs) {
|
|
|
321
321
|
}
|
|
322
322
|
const contentType = sniffImageType(bytes);
|
|
323
323
|
if (!contentType)
|
|
324
|
-
throw new ImageRefusal("IMAGE_TYPE_UNSUPPORTED", `The file at
|
|
324
|
+
throw new ImageRefusal("IMAGE_TYPE_UNSUPPORTED", `The file at ${field} is not a PNG, JPEG, GIF or WebP image. The type is read from the file's first bytes, not its name. ${NOTHING_UPLOADED}`);
|
|
325
325
|
return { bytes, contentType };
|
|
326
326
|
}
|
|
327
|
-
async function
|
|
328
|
-
const { imagePath, ...rest } = args;
|
|
329
|
-
const hasPath = typeof imagePath === "string" && imagePath.length > 0;
|
|
330
|
-
const hasUrl = typeof rest.imageUrl === "string" && rest.imageUrl.length > 0;
|
|
331
|
-
const invalid = (message) => ({ refused: { code: "MCP_VALIDATION", message } });
|
|
332
|
-
if (imageDir === undefined) {
|
|
333
|
-
if (imagePath !== undefined)
|
|
334
|
-
return invalid(`imagePath is not available on this server: no CANDLE_IMAGE_DIR is configured. Pass imageUrl, an https URL. ${NOTHING_UPLOADED}`);
|
|
335
|
-
return { args: rest };
|
|
336
|
-
}
|
|
337
|
-
if (hasPath && hasUrl)
|
|
338
|
-
return invalid(`Pass exactly one of imageUrl or imagePath, not both. ${NOTHING_UPLOADED}`);
|
|
339
|
-
if (!hasPath && !hasUrl)
|
|
340
|
-
return invalid("Pass imageUrl (an https URL) or imagePath (an absolute path to an image file inside CANDLE_IMAGE_DIR).");
|
|
341
|
-
if (!hasPath)
|
|
342
|
-
return { args: rest };
|
|
343
|
-
if (!cfg.apiKey)
|
|
344
|
-
return invalid(`CANDLE_AGENT_API_KEY is required to upload an image; the key needs the launch:write scope. ${NOTHING_UPLOADED}`);
|
|
345
|
-
let image;
|
|
346
|
-
try {
|
|
347
|
-
image = await readImageInDir(imageDir, imagePath, fs);
|
|
348
|
-
} catch (error) {
|
|
349
|
-
if (error instanceof ImageRefusal)
|
|
350
|
-
return { refused: { code: error.code, message: error.message } };
|
|
351
|
-
throw error;
|
|
352
|
-
}
|
|
327
|
+
async function uploadLocalImage(image, kind, cfg, apiKey, doFetch) {
|
|
353
328
|
let res;
|
|
354
329
|
try {
|
|
355
|
-
res = await doFetch(`${cfg.apiUrl.replace(/\/$/, "")}/api/v1/uploads/agent-image`, {
|
|
330
|
+
res = await doFetch(`${cfg.apiUrl.replace(/\/$/, "")}/api/v1/uploads/agent-image${kind === "banner" ? "?kind=banner" : ""}`, {
|
|
356
331
|
method: "POST",
|
|
357
|
-
headers: { "Content-Type": image.contentType, "x-api-key":
|
|
332
|
+
headers: { "Content-Type": image.contentType, "x-api-key": apiKey },
|
|
358
333
|
body: image.bytes
|
|
359
334
|
});
|
|
360
335
|
} catch (error) {
|
|
361
336
|
return {
|
|
362
337
|
refused: {
|
|
363
338
|
code: "MCP_TRANSPORT",
|
|
364
|
-
message: `The
|
|
339
|
+
message: `The ${kind} upload failed in transit (${error instanceof Error ? error.message : String(error)}). ` + "No launch was attempted. The upload may or may not have reached Candle; if it did, the file is public."
|
|
365
340
|
}
|
|
366
341
|
};
|
|
367
342
|
}
|
|
@@ -379,15 +354,92 @@ async function resolveLaunchImage(args, imageDir, cfg, doFetch, fs = nodeFs) {
|
|
|
379
354
|
return {
|
|
380
355
|
refused: {
|
|
381
356
|
code: "MCP_TRANSPORT",
|
|
382
|
-
message:
|
|
357
|
+
message: `Candle did not return an https URL for the uploaded ${kind}. No launch was attempted.`
|
|
383
358
|
}
|
|
384
359
|
};
|
|
385
|
-
return {
|
|
360
|
+
return { url: uploaded };
|
|
361
|
+
}
|
|
362
|
+
async function resolveLaunchImage(args, imageDir, cfg, doFetch, fs = nodeFs) {
|
|
363
|
+
const { imagePath, bannerPath, ...rest } = args;
|
|
364
|
+
const hasPath = typeof imagePath === "string" && imagePath.length > 0;
|
|
365
|
+
const hasUrl = typeof rest.imageUrl === "string" && rest.imageUrl.length > 0;
|
|
366
|
+
const hasBannerPath = typeof bannerPath === "string" && bannerPath.length > 0;
|
|
367
|
+
const hasBannerUrl = typeof rest.bannerUrl === "string" && rest.bannerUrl.length > 0;
|
|
368
|
+
const invalid = (message) => ({ refused: { code: "MCP_VALIDATION", message } });
|
|
369
|
+
if (imageDir === undefined) {
|
|
370
|
+
if (imagePath !== undefined)
|
|
371
|
+
return invalid(`imagePath is not available on this server: no CANDLE_IMAGE_DIR is configured. Pass imageUrl, an https URL. ${NOTHING_UPLOADED}`);
|
|
372
|
+
if (bannerPath !== undefined)
|
|
373
|
+
return invalid(`bannerPath is not available on this server: no CANDLE_IMAGE_DIR is configured. Pass bannerUrl, an https URL. ${NOTHING_UPLOADED}`);
|
|
374
|
+
return { args: rest };
|
|
375
|
+
}
|
|
376
|
+
if (hasPath && hasUrl)
|
|
377
|
+
return invalid(`Pass exactly one of imageUrl or imagePath, not both. ${NOTHING_UPLOADED}`);
|
|
378
|
+
if (hasBannerPath && hasBannerUrl)
|
|
379
|
+
return invalid(`Pass at most one of bannerUrl or bannerPath, not both. ${NOTHING_UPLOADED}`);
|
|
380
|
+
if (!hasPath && !hasUrl)
|
|
381
|
+
return invalid("Pass imageUrl (an https URL) or imagePath (an absolute path to an image file inside CANDLE_IMAGE_DIR).");
|
|
382
|
+
if (!hasPath && !hasBannerPath)
|
|
383
|
+
return { args: rest };
|
|
384
|
+
if (!cfg.apiKey)
|
|
385
|
+
return invalid(`CANDLE_AGENT_API_KEY is required to upload an image; the key needs the launch:write scope. ${NOTHING_UPLOADED}`);
|
|
386
|
+
const read = async (path, field) => {
|
|
387
|
+
try {
|
|
388
|
+
return await readImageInDir(imageDir, path, fs, field);
|
|
389
|
+
} catch (error) {
|
|
390
|
+
if (error instanceof ImageRefusal)
|
|
391
|
+
return { refused: { code: error.code, message: error.message } };
|
|
392
|
+
throw error;
|
|
393
|
+
}
|
|
394
|
+
};
|
|
395
|
+
let image;
|
|
396
|
+
let banner;
|
|
397
|
+
if (hasPath) {
|
|
398
|
+
const result = await read(imagePath, "imagePath");
|
|
399
|
+
if ("refused" in result)
|
|
400
|
+
return result;
|
|
401
|
+
image = result;
|
|
402
|
+
}
|
|
403
|
+
if (hasBannerPath) {
|
|
404
|
+
const result = await read(bannerPath, "bannerPath");
|
|
405
|
+
if ("refused" in result)
|
|
406
|
+
return result;
|
|
407
|
+
banner = result;
|
|
408
|
+
}
|
|
409
|
+
const launchArgs = { ...rest };
|
|
410
|
+
let uploaded;
|
|
411
|
+
let uploadedBanner;
|
|
412
|
+
if (image) {
|
|
413
|
+
const result = await uploadLocalImage(image, "image", cfg, cfg.apiKey, doFetch);
|
|
414
|
+
if (!("url" in result))
|
|
415
|
+
return result;
|
|
416
|
+
uploaded = result.url;
|
|
417
|
+
launchArgs.imageUrl = uploaded;
|
|
418
|
+
}
|
|
419
|
+
if (banner) {
|
|
420
|
+
const result = await uploadLocalImage(banner, "banner", cfg, cfg.apiKey, doFetch);
|
|
421
|
+
if (!("url" in result)) {
|
|
422
|
+
if (uploaded === undefined)
|
|
423
|
+
return result;
|
|
424
|
+
const why = "refused" in result ? result.refused.message : result.uploadFailed;
|
|
425
|
+
return invalid(`The banner upload failed: ${why} The image was already uploaded to ${uploaded}; pass that as imageUrl on the retry. No launch was attempted.`);
|
|
426
|
+
}
|
|
427
|
+
uploadedBanner = result.url;
|
|
428
|
+
launchArgs.bannerUrl = uploadedBanner;
|
|
429
|
+
}
|
|
430
|
+
return { args: launchArgs, ...uploaded ? { uploaded } : {}, ...uploadedBanner ? { uploadedBanner } : {} };
|
|
386
431
|
}
|
|
387
432
|
function imageRefusalBody(refused, extra) {
|
|
388
433
|
return JSON.stringify({ ...extra, success: false, error: { code: refused.code, message: refused.message } });
|
|
389
434
|
}
|
|
390
435
|
var UPLOADED_IMAGE_NOTE = "imagePath was uploaded to imageUrl, which is public and permanent. To retry, or to launch after a dry run, pass this imageUrl instead of imagePath, so the file is not uploaded again.";
|
|
436
|
+
var UPLOADED_BANNER_NOTE = "bannerPath was uploaded to bannerUrl, which is public and permanent. To retry, or to launch after a dry run, pass this bannerUrl instead of bannerPath, so the file is not uploaded again.";
|
|
437
|
+
function uploadedFields(image) {
|
|
438
|
+
return {
|
|
439
|
+
...image.uploaded !== undefined ? { imageUrl: image.uploaded, imageNote: UPLOADED_IMAGE_NOTE } : {},
|
|
440
|
+
...image.uploadedBanner !== undefined ? { bannerUrl: image.uploadedBanner, bannerNote: UPLOADED_BANNER_NOTE } : {}
|
|
441
|
+
};
|
|
442
|
+
}
|
|
391
443
|
|
|
392
444
|
// src/orchestrate.ts
|
|
393
445
|
import { randomUUID } from "node:crypto";
|
|
@@ -665,7 +717,7 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
|
|
|
665
717
|
}
|
|
666
718
|
return { text: JSON.stringify({ clientLaunchId, api }), isError: true };
|
|
667
719
|
}
|
|
668
|
-
const uploaded = image
|
|
720
|
+
const uploaded = uploadedFields(image);
|
|
669
721
|
const path = dryRun ? "/api/v1/launch/headless/dry-run" : "/api/v1/launch/headless";
|
|
670
722
|
const posted = await postJson(`${base(cfg)}${path}`, apiKey, {
|
|
671
723
|
...image.args,
|
|
@@ -674,7 +726,7 @@ async function executeLaunchAndSeed(args, cfg, doFetch, imageDir, imageFs) {
|
|
|
674
726
|
}, doFetch);
|
|
675
727
|
if ("thrown" in posted) {
|
|
676
728
|
const failed = transportError("clientLaunchId", clientLaunchId, posted.thrown);
|
|
677
|
-
return
|
|
729
|
+
return Object.keys(uploaded).length === 0 ? failed : { ...failed, text: JSON.stringify({ ...JSON.parse(failed.text), ...uploaded }) };
|
|
678
730
|
}
|
|
679
731
|
if (!posted.ok) {
|
|
680
732
|
return { text: JSON.stringify({ clientLaunchId, ...uploaded, api: posted.body }), isError: true };
|
|
@@ -2422,18 +2474,55 @@ function perpsCandleWindow(opts, now) {
|
|
|
2422
2474
|
return usage(`interval must be one of ${PERPS_CANDLE_INTERVALS.join(" ")}.`);
|
|
2423
2475
|
const count = bounded(opts.count, 100, 1, 5000, "count");
|
|
2424
2476
|
const raw = opts.end;
|
|
2425
|
-
const endTime = raw === undefined ? now :
|
|
2477
|
+
const endTime = raw === undefined ? now : epoch(raw);
|
|
2426
2478
|
const startTime = endTime - count * at(INTERVAL_MS, index);
|
|
2427
2479
|
if (!Number.isSafeInteger(endTime) || endTime < 0 || endTime > now || startTime < 0)
|
|
2428
2480
|
return usage("end must be an ISO 8601 time or epoch milliseconds, no later than now.");
|
|
2429
2481
|
return { interval, count, startTime, endTime };
|
|
2430
2482
|
}
|
|
2483
|
+
function epoch(raw) {
|
|
2484
|
+
return typeof raw === "number" || /^\d+$/.test(raw) ? Number(raw) : /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2}(\.\d+)?)?(Z|[+-]\d{2}:\d{2})$/.test(raw) ? Date.parse(raw) : NaN;
|
|
2485
|
+
}
|
|
2486
|
+
var PERPS_FUNDING_HISTORY_MAX_MS = 30 * 86400000;
|
|
2487
|
+
var PERPS_FUNDING_HISTORY_MAX_ROWS = 500;
|
|
2488
|
+
function perpsFundingWindow(opts, now) {
|
|
2489
|
+
const endTime = opts.end === undefined ? now : epoch(opts.end);
|
|
2490
|
+
if (!Number.isSafeInteger(endTime) || endTime < 0 || endTime > now)
|
|
2491
|
+
return usage("end must be an ISO 8601 time or epoch milliseconds, no later than now.");
|
|
2492
|
+
const startTime = opts.start === undefined ? endTime - 86400000 : epoch(opts.start);
|
|
2493
|
+
if (!Number.isSafeInteger(startTime) || startTime < 0 || startTime > endTime)
|
|
2494
|
+
return usage("start must be an ISO 8601 time or epoch milliseconds, no later than end.");
|
|
2495
|
+
if (endTime - startTime > PERPS_FUNDING_HISTORY_MAX_MS)
|
|
2496
|
+
return usage("start must be at most 30 days before end.");
|
|
2497
|
+
return { startTime, endTime };
|
|
2498
|
+
}
|
|
2431
2499
|
function bounded(value, fallback, min, max, name) {
|
|
2432
2500
|
const n = value ?? fallback;
|
|
2433
2501
|
if (!Number.isInteger(n) || n < min || n > max)
|
|
2434
2502
|
return usage(`${name} must be an integer from ${min} to ${max}.`);
|
|
2435
2503
|
return n;
|
|
2436
2504
|
}
|
|
2505
|
+
function parsePerpsFundingHistory(value, coin, window) {
|
|
2506
|
+
if (!Array.isArray(value))
|
|
2507
|
+
return unavailable();
|
|
2508
|
+
const funding = [];
|
|
2509
|
+
for (const row of value) {
|
|
2510
|
+
if (!object(row) || row.coin !== coin || !integer(row.time) || !decimal2(row.fundingRate) || !decimal2(row.premium))
|
|
2511
|
+
return unavailable();
|
|
2512
|
+
if (row.time >= window.startTime && row.time <= window.endTime)
|
|
2513
|
+
funding.push({
|
|
2514
|
+
time: row.time,
|
|
2515
|
+
fundingRate: row.fundingRate,
|
|
2516
|
+
fundingAprPct: fixed(scaled(row.fundingRate) * 876000n, 2),
|
|
2517
|
+
premium: row.premium
|
|
2518
|
+
});
|
|
2519
|
+
}
|
|
2520
|
+
funding.sort((a, b) => a.time - b.time);
|
|
2521
|
+
return {
|
|
2522
|
+
funding: funding.slice(0, PERPS_FUNDING_HISTORY_MAX_ROWS),
|
|
2523
|
+
truncated: funding.length > PERPS_FUNDING_HISTORY_MAX_ROWS
|
|
2524
|
+
};
|
|
2525
|
+
}
|
|
2437
2526
|
|
|
2438
2527
|
class PerpsMarketReader {
|
|
2439
2528
|
opts;
|
|
@@ -2711,6 +2800,44 @@ class PerpsMarketReader {
|
|
|
2711
2800
|
candles: candles.slice(-window.count)
|
|
2712
2801
|
};
|
|
2713
2802
|
}
|
|
2803
|
+
async fundingHistory(coin, opts = {}) {
|
|
2804
|
+
this.key();
|
|
2805
|
+
const window = perpsFundingWindow(opts, this.now());
|
|
2806
|
+
const [config, core] = await Promise.all([this.config(), this.core()]);
|
|
2807
|
+
const canonical2 = at(core.universe, resolve(core, coin)).name;
|
|
2808
|
+
const value = await info(this.opts.fetcher, this.opts.network, {
|
|
2809
|
+
type: "fundingHistory",
|
|
2810
|
+
coin: canonical2,
|
|
2811
|
+
startTime: window.startTime,
|
|
2812
|
+
endTime: window.endTime
|
|
2813
|
+
});
|
|
2814
|
+
return {
|
|
2815
|
+
...this.envelope(config),
|
|
2816
|
+
coin: canonical2,
|
|
2817
|
+
startTime: window.startTime,
|
|
2818
|
+
endTime: window.endTime,
|
|
2819
|
+
...parsePerpsFundingHistory(value, canonical2, window)
|
|
2820
|
+
};
|
|
2821
|
+
}
|
|
2822
|
+
async predictedFunding() {
|
|
2823
|
+
this.key();
|
|
2824
|
+
const [config, core, extras] = await Promise.all([this.config(), this.core(), this.extras()]);
|
|
2825
|
+
if (extras.predicted === null)
|
|
2826
|
+
return unavailable();
|
|
2827
|
+
const predicted = [];
|
|
2828
|
+
for (const meta of core.universe) {
|
|
2829
|
+
const row = extras.predicted.get(meta.name);
|
|
2830
|
+
if (row)
|
|
2831
|
+
predicted.push({
|
|
2832
|
+
coin: meta.name,
|
|
2833
|
+
fundingRate: row.fundingRate,
|
|
2834
|
+
fundingAprPct: fixed(scaled(row.fundingRate) * 876000n, 2),
|
|
2835
|
+
nextFundingTime: row.nextFundingTime
|
|
2836
|
+
});
|
|
2837
|
+
}
|
|
2838
|
+
predicted.sort((a, b) => a.coin < b.coin ? -1 : a.coin > b.coin ? 1 : 0);
|
|
2839
|
+
return { ...this.envelope(config, extras.warnings, this.extrasCache?.at ?? this.now()), predicted };
|
|
2840
|
+
}
|
|
2714
2841
|
}
|
|
2715
2842
|
|
|
2716
2843
|
// src/perps-history.ts
|
|
@@ -4280,7 +4407,8 @@ async function executePerpsDeposit(args, cfg, env, fetch2) {
|
|
|
4280
4407
|
asset,
|
|
4281
4408
|
amountRaw,
|
|
4282
4409
|
...typeof args.maxSlippageBps === "number" ? { maxSlippageBps: args.maxSlippageBps } : {},
|
|
4283
|
-
...hood ? {} : { perpsWalletId: account.id }
|
|
4410
|
+
...hood ? {} : { perpsWalletId: account.id },
|
|
4411
|
+
...willSubmit ? {} : { preview: true }
|
|
4284
4412
|
});
|
|
4285
4413
|
if (build.job !== undefined)
|
|
4286
4414
|
return { text: JSON.stringify(build) };
|
|
@@ -4289,6 +4417,8 @@ async function executePerpsDeposit(args, cfg, env, fetch2) {
|
|
|
4289
4417
|
return fail("PERPS_BUILD_REFUSED", `Refused to sign this deposit: ${problem}. Nothing was signed.`);
|
|
4290
4418
|
if (!willSubmit || signerPem === null)
|
|
4291
4419
|
return { text: JSON.stringify({ ...build, signed: false, submitted: false }) };
|
|
4420
|
+
if (build.preview === true || typeof build.depositId !== "string")
|
|
4421
|
+
return fail("PERPS_BUILD_REFUSED", "Refused to sign this deposit: Candle answered a preview. Nothing was signed.");
|
|
4292
4422
|
if (!appId || !source.privyWalletId)
|
|
4293
4423
|
return fail("SIGNER_UNAVAILABLE", "The server did not return its relay identifiers.");
|
|
4294
4424
|
const pem = signerPem;
|
|
@@ -4515,7 +4645,7 @@ function planTableMarkdown(table) {
|
|
|
4515
4645
|
|
|
4516
4646
|
// src/tools.ts
|
|
4517
4647
|
var VAMP_CANDIDATES_DESCRIPTION = "Lists Solana tokens that are close to graduating or graduated in the last 6 hours whose ticker other tokens " + "are copying, most-copied in the last 24 hours first. Each entry carries a launch draft (name, ticker, image URL) " + "for `candle_launch_token` or `candle_launch_and_seed`; draft notes identify fields you must supply or adjust. " + "Upload your own image through the agent image upload when the image is unavailable. Use `dryRun: true` first. " + "Counts exclude the candidate itself and cover both Solana and Hood launches at the venues returned in " + "`coverage`, not Candle's own. `coverage.since` is the returned Solana coverage date; Hood history may differ.";
|
|
4518
|
-
var CUSTOM_CURVE_RULE = 'mode "exp-custom" with curve { virtualQuote, threshold } launches a Solana curve with your own V (the virtual ' + "quote it starts with) and G (the quote it raises before graduating), in whole SOL or CNDL as decimal strings, " + "accepted when V is 1 to 100 SOL and G 1 to 250 SOL (66,000 to 6,600,000 and 66,000 to 16,500,000 CNDL), when " + "every price up to 20 x G stays under the ceiling (V + 20G)^2 < V x X at this stakerAllocationBps, when neither " + "is finer than the quote's decimals, and when the dev buy is at most 10% of G; a curve past a limit is " + "CURVE_OUT_OF_RANGE, and at the ceiling it carries maxThresholdRaw, the largest G that fits. " + "Dry-run it first (dryRun: true): the dry run spends nothing and returns curveTerms, the market cap at start and " + "at graduation
|
|
4648
|
+
var CUSTOM_CURVE_RULE = 'mode "exp-custom" with curve { virtualQuote, threshold } launches a Solana curve with your own V (the virtual ' + "quote it starts with) and G (the quote it raises before graduating), in whole SOL or CNDL as decimal strings, " + "accepted when V is 1 to 100 SOL and G 1 to 250 SOL (66,000 to 6,600,000 and 66,000 to 16,500,000 CNDL), when " + "every price up to 20 x G stays under the ceiling (V + 20G)^2 < V x X at this stakerAllocationBps, when neither " + "is finer than the quote's decimals, and when the dev buy is at most 0.5 SOL (2,000,000 CNDL), or 10% of G when " + "that is lower (every exp- mode; an operator may set the 0.5 SOL lower); a curve past a limit is " + "CURVE_OUT_OF_RANGE, and at the ceiling it carries maxThresholdRaw, the largest G that fits. " + "Dry-run it first (dryRun: true): the dry run spends nothing and returns curveTerms, the market cap at start and " + "at graduation, the supply sold by graduation, the share your own buyAmount takes, the fees and the migration " + "terms, and devBuyMax, the dev-buy ceiling that applies. Only where the API has EXP_CUSTOM_CURVES on; " + "elsewhere exp-custom is refused as an unknown mode.";
|
|
4519
4649
|
var localProfileTools = new WeakMap;
|
|
4520
4650
|
var STAKING_DISCLOSURE = "No instant unstake. Read pool.withdrawalTimelockSeconds for the wait. Solana rewards accrue to the holding wallet. sCNDL in any eligible account wallet counts toward Pro and earns the account's Hood staking reward, except a wallet another account also claims, wallets past the 200-wallet cap, and an account with no EVM wallet. CNDL in a pending withdrawal counts toward nothing for Pro.";
|
|
4521
4651
|
var stakingWriteShape = {
|
|
@@ -4945,15 +5075,26 @@ var launchTokenShape = {
|
|
|
4945
5075
|
name: z4.string().describe("Token name"),
|
|
4946
5076
|
symbol: z4.string().describe("Token symbol"),
|
|
4947
5077
|
imageUrl: z4.string().describe("https URL to the token image. Must be roughly SQUARE (aspect ratio at most 1.5:1): it " + "renders as a small circle/square avatar everywhere. Share cards, OG images and banners " + "are rejected with IMAGE_WRONG_SHAPE -- pass those as bannerUrl instead"),
|
|
4948
|
-
bannerUrl: z4.string().optional().describe("Optional https URL to WIDE artwork for the token page's banner strip (wider than 1.5:1, " + "e.g. 1200x630). This is where a share card or OG image belongs. A square image here is " + "rejected with BANNER_WRONG_SHAPE. Omit it and the strip falls back to imageUrl"),
|
|
5078
|
+
bannerUrl: z4.string().optional().describe("Optional https URL to WIDE artwork for the token page's banner strip (wider than 1.5:1, " + "e.g. 1200x630). This is where a share card or OG image belongs. A square image here is " + "rejected with BANNER_WRONG_SHAPE: send the wide original, not a cropped copy. Omit it and the strip " + "falls back to imageUrl"),
|
|
4949
5079
|
chain: z4.string().optional().describe('"solana" or "hood"; defaults to solana'),
|
|
4950
5080
|
quoteAsset: z4.string().optional().describe("Quote asset symbol; defaults per chain"),
|
|
4951
|
-
mode: z4.string().optional().describe('"open" or "exclusive"; defaults to open. "test-open" / "test-exclusive" launch the ' + "low-threshold test curves (~1/80 economics) where the API has ENABLE_TEST_CURVES on. " + '"exp-custom" launches a Solana curve with your own V and G, given in curve, where the API has it ' + "switched on"),
|
|
5081
|
+
mode: z4.string().optional().describe('"open" or "exclusive"; defaults to open. "test-open" / "test-exclusive" launch the ' + "low-threshold test curves (~1/80 economics) where the API has ENABLE_TEST_CURVES on. " + '"exp-a", "exp-b" and "exp-c" are the experimental fast Solana curves (sol or cndl). ' + '"exp-custom" launches a Solana curve with your own V and G, given in curve, where the API has it ' + "switched on"),
|
|
4952
5082
|
curve: z4.object({
|
|
4953
5083
|
virtualQuote: z4.string().describe('V: the virtual quote the curve starts with, in whole quote units, e.g. "12" SOL'),
|
|
4954
5084
|
threshold: z4.string().describe('G: the quote the curve raises before it graduates, in whole quote units, e.g. "40" SOL')
|
|
4955
5085
|
}).strict().optional().describe('mode "exp-custom" only, and required there. Decimal strings in whole units of the quote asset (sol or ' + "cndl), never raw units. Dry-run first: the API checks the limits and returns the curve's terms"),
|
|
5086
|
+
creatorFeeBps: z4.union([z4.literal(0), z4.literal(100), z4.literal(200)]).optional().describe("An exp- mode only: an extra creator fee of 100 or 200 basis points on top of the curve's 1% and the " + "pool's 0.5%, paid to the creator in full. 0 is the same as leaving it out"),
|
|
4956
5087
|
stakerAllocationBps: z4.number().optional().describe("Staker allocation in basis points"),
|
|
5088
|
+
poolFee: z4.union([
|
|
5089
|
+
z4.object({ tier: z4.union([z4.literal(25), z4.literal(50), z4.literal(100)]) }).strict(),
|
|
5090
|
+
z4.object({
|
|
5091
|
+
startBps: z4.number().int().max(2500),
|
|
5092
|
+
endBps: z4.union([z4.literal(25), z4.literal(50), z4.literal(100)]),
|
|
5093
|
+
minutes: z4.number().positive().max(30)
|
|
5094
|
+
}).strict()
|
|
5095
|
+
]).optional().describe("Solana only: the graduated pool's fee. { tier: 25 | 50 | 100 } is a flat fee in bps (default 50), or " + "{ startBps, endBps, minutes } is an anti-snipe fall from startBps (at most 2500, at least endBps) to " + "endBps (25, 50 or 100) over minutes (at most 30). Cannot be combined with creatorFeeBps unless the tier is 50. " + "Omit for the standard 50 bps pool"),
|
|
5096
|
+
poolOpen: z4.enum(["legacy", "curve_price"]).optional().describe(`Solana preset tiers only. "curve_price" opens the pool at the curve's last price (the whole reserve is ` + "seeded and the remaining base burned); stakerAllocationBps plus the team allocation must fit the tier's limit, " + "which the dry run returns as graduation.maxStakerCarveBps. Omit for legacy"),
|
|
5097
|
+
expPoolShape: z4.enum(["floor", "full_range"]).optional().describe('Solana exp- curves only. "full_range" opens the pool across the whole price range. Omit for floor'),
|
|
4957
5098
|
dexVersion: z4.string().nullable().optional().describe('"v3" or "v4"; required for hood launches'),
|
|
4958
5099
|
buyAmount: z4.union([z4.string(), z4.number()]).optional().describe("Initial buy, in quote base units (string or number)"),
|
|
4959
5100
|
description: z4.string().optional(),
|
|
@@ -5075,11 +5216,13 @@ var seedShape = {
|
|
|
5075
5216
|
var launchAndSeedShape = { ...seedableLaunchShape, ...seedShape };
|
|
5076
5217
|
var localImageShape = {
|
|
5077
5218
|
imageUrl: launchTokenShape.imageUrl.optional().describe(`${launchTokenShape.imageUrl.description}. Pass this or imagePath, never both`),
|
|
5078
|
-
imagePath: z4.string().optional().describe("An absolute image path inside CANDLE_IMAGE_DIR. The file is uploaded and becomes public and permanent, " + "including on a dry run. PNG, JPEG, GIF or WebP, at most 10MB, read from the file's first bytes. " + "Uploading spends no funds and does not count against the launch cap; only the upload limit applies. " + "Pass this or imageUrl, never both. The result carries the imageUrl used: pass that on a retry, or " + "after a dry run, instead of uploading the file again.
|
|
5219
|
+
imagePath: z4.string().optional().describe("An absolute image path inside CANDLE_IMAGE_DIR. The file is uploaded and becomes public and permanent, " + "including on a dry run. PNG, JPEG, GIF or WebP, at most 10MB, read from the file's first bytes. " + "Uploading spends no funds and does not count against the launch cap; only the upload limit applies. " + "Pass this or imageUrl, never both. The result carries the imageUrl used: pass that on a retry, or " + "after a dry run, instead of uploading the file again. This is the square token image: Candle crops " + "it to a square, so a wide banner belongs in bannerPath, not here"),
|
|
5220
|
+
bannerUrl: launchTokenShape.bannerUrl.describe(`${launchTokenShape.bannerUrl.description}. Pass this or bannerPath, never both`),
|
|
5221
|
+
bannerPath: z4.string().optional().describe("An absolute path, inside CANDLE_IMAGE_DIR, to WIDE banner artwork for the token page strip (wider than " + "1.5:1, e.g. 2171x724 or 1200x630). The file is uploaded uncropped and becomes public and permanent, " + "including on a dry run; PNG, JPEG, GIF or WebP, at most 10MB. A square image is refused with " + "BANNER_WRONG_SHAPE: send the wide original, not a cropped copy. Pass this or bannerUrl, never both. " + "The result carries the bannerUrl used: pass that on a retry instead of uploading the file again")
|
|
5079
5222
|
};
|
|
5080
5223
|
var localLaunchTokenShape = { ...launchTokenShape, ...localImageShape };
|
|
5081
5224
|
var localLaunchAndSeedShape = { ...seedableLaunchShape, ...localImageShape, ...seedShape };
|
|
5082
|
-
var IMAGE_PATH_DISCLOSURE = " With imagePath, the
|
|
5225
|
+
var IMAGE_PATH_DISCLOSURE = " With imagePath or bannerPath, the file is uploaded first and becomes public and permanent, on a dry run too; " + "nothing about a dry run keeps that file private. imagePath is the square token image (Candle crops it); " + "a wide banner goes in bannerPath, which is not cropped.";
|
|
5083
5226
|
var transferShape = {
|
|
5084
5227
|
chain: z4.enum(["solana", "hood"]).describe("Which chain the transfer executes on"),
|
|
5085
5228
|
asset: z4.enum(["SOL", "USDC", "CNDL", "ETH", "USDG"]).optional().describe("A base asset key. Pass exactly one of asset or mint."),
|
|
@@ -5134,9 +5277,12 @@ function registerToolSubset(server, options) {
|
|
|
5134
5277
|
if ("uploadFailed" in image)
|
|
5135
5278
|
return { content: [{ type: "text", text: image.uploadFailed }], isError: true };
|
|
5136
5279
|
const relayed = await callAndRelay("candle_launch_token", image.args, getConfig(), doFetch);
|
|
5137
|
-
if (image.uploaded === undefined)
|
|
5280
|
+
if (image.uploaded === undefined && image.uploadedBanner === undefined)
|
|
5138
5281
|
return relayed;
|
|
5139
|
-
const uploaded = JSON.stringify({
|
|
5282
|
+
const uploaded = JSON.stringify({
|
|
5283
|
+
...image.uploaded !== undefined ? { imageUrl: image.uploaded, note: UPLOADED_IMAGE_NOTE } : {},
|
|
5284
|
+
...image.uploadedBanner !== undefined ? { bannerUrl: image.uploadedBanner, bannerNote: UPLOADED_BANNER_NOTE } : {}
|
|
5285
|
+
});
|
|
5140
5286
|
return { ...relayed, content: [...relayed.content, { type: "text", text: uploaded }] };
|
|
5141
5287
|
});
|
|
5142
5288
|
register("candle_get_market", {
|
|
@@ -5503,7 +5649,7 @@ MARKET_NOT_FOUND means Candle has no market for that token and this could not ru
|
|
|
5503
5649
|
}, perpsTool("candle_perps_reduce_only"));
|
|
5504
5650
|
register("candle_perps_deposit", {
|
|
5505
5651
|
title: "Deposit to Hyperliquid",
|
|
5506
|
-
description: "MOVES REAL FUNDS unless submit is false. Deposit onto the key's Hyperliquid account through Relay: SOL or " + "USDC from its Solana TEE wallet, or ETH or USDG from its Hood TEE wallet, credited as Hyperliquid perps " + "USDC at the key's EVM TEE wallet's own address (Candle resolves it; no recipient is taken). Refused below a " + "floor that covers Hyperliquid's 1 USDC first-deposit charge. No Candle fee. Needs swap:write
|
|
5652
|
+
description: "MOVES REAL FUNDS unless submit is false. Deposit onto the key's Hyperliquid account through Relay: SOL or " + "USDC from its Solana TEE wallet, or ETH or USDG from its Hood TEE wallet, credited as Hyperliquid perps " + "USDC at the key's EVM TEE wallet's own address (Candle resolves it; no recipient is taken). Refused below a " + "floor that covers Hyperliquid's 1 USDC first-deposit charge. No Candle fee. Needs swap:write (any cap on " + "the asset still applies), and CANDLE_KEY_SIGNER_PEM_FILE to sign. There is no withdrawal from Hyperliquid yet.",
|
|
5507
5653
|
inputSchema: perpsDepositShape
|
|
5508
5654
|
}, async (args) => {
|
|
5509
5655
|
const result = await executePerpsDeposit(args, getConfig(), env, doFetch);
|