candle-launch-helper 0.0.0-stage → 0.1.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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +146 -2
- package/bin/candle-launch-helper +7 -0
- package/dist/cli.d.ts +11 -0
- package/dist/cli.js +141 -0
- package/dist/config.d.ts +35 -0
- package/dist/config.js +63 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +5 -0
- package/dist/launcher.d.ts +202 -0
- package/dist/launcher.js +626 -0
- package/dist/ledger.d.ts +38 -0
- package/dist/ledger.js +34 -0
- package/examples/launch.json +13 -0
- package/package.json +60 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Changelog: candle-launch-helper
|
|
2
|
+
|
|
3
|
+
## 0.1.1
|
|
4
|
+
|
|
5
|
+
- On candle-bot-core 0.5.0 (pinned exactly). Same CLI, config, state dir, ledger, `status.json` fields, events and
|
|
6
|
+
order handling: every 0.5.0 core addition is opt-in and this bot turns none on. One core fix reaches it (TEE wallet
|
|
7
|
+
only): a build of transaction version 1 or later is never signed (the order ends `TX_VERSION`, alerted) instead of
|
|
8
|
+
having a wrong signature read from it. Legacy and v0 transactions, which Candle builds today, go as before.
|
|
9
|
+
- Transactions of version 1 are now read (fills, launch transactions); before, the RPC refused them.
|
|
10
|
+
|
|
11
|
+
## 0.1.0
|
|
12
|
+
|
|
13
|
+
First release, on candle-bot-core 0.4.0 (pinned exactly).
|
|
14
|
+
|
|
15
|
+
- One Candle launch per spec through the atomic route (payer main): build, the dev buy's expected fill checked,
|
|
16
|
+
submit within 45 s; the dev buy as the bundle's first buy.
|
|
17
|
+
- `clientLaunchId` saved before the first build, never replaced; every state saved before its call; restarts,
|
|
18
|
+
timeouts and errors reconcile from Candle's launch job (`building` saved before the build: a restart never builds).
|
|
19
|
+
- `RETRY_LAUNCH` builds once more under the same id only after a job read says `failed` or `JOB_NOT_FOUND`, and not
|
|
20
|
+
under `PAUSE_BUYS` or `STOP`; otherwise it is ignored with why.
|
|
21
|
+
- Before the first build: `minDevBuyOutRaw` required (live), the dev buy within `lifetimeSol` and `maxOpen`, `wallet`
|
|
22
|
+
the key's embedded wallet, and no earlier launch by the account with the same name and symbol (`launch.newLaunch`
|
|
23
|
+
overrides).
|
|
24
|
+
- The dev bag becomes a core position from its transaction on chain (core `openFromFill`); exits, `sell`, `STOP`,
|
|
25
|
+
`PAUSE_BUYS` as every bot. A missing or wrong dev-buy signature is found in the wallet's history for the mint;
|
|
26
|
+
`adopt <signature>` books it by hand. `run` waits for a launch call in flight before closing on STOP or SIGTERM.
|
|
27
|
+
- Dry mode calls nothing. `plan` (with the API key: Candle's `dryRunAtomicLaunch` preview of the dev buy's fill),
|
|
28
|
+
`run`, `status`, `sell`, `adopt`.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 candle-launch-bot contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# candle-launch-helper
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fires the [Candle](https://candle.tv) launch you describe, with your dev buy in the same bundle, then manages the dev
|
|
4
|
+
bag with exits and controls. One launch per name and symbol; a run never launches twice. Built on Candle's official SDK,
|
|
5
|
+
[`@candledottv/agent-sdk`](https://www.npmjs.com/package/@candledottv/agent-sdk) (pinned at 0.6.14), and
|
|
6
|
+
[candle-bot-core](https://www.npmjs.com/package/candle-bot-core).
|
|
7
|
+
|
|
8
|
+
Built by [Bruce Jeet](https://github.com/BruceJeet).
|
|
9
|
+
|
|
10
|
+
- **The launch:** name, symbol, image, description, socials and launch mode from the config, on Solana.
|
|
11
|
+
- **The dev buy:** the bundle's first buy, from the same wallet, landing in the launch's own block (Candle's atomic
|
|
12
|
+
route, a Jito bundle: all or nothing). Candle's expected fill for it is checked before anything is sent.
|
|
13
|
+
- **The bag:** once the launch lands, the dev buy is read from its transaction on chain (amount and cost) and becomes a
|
|
14
|
+
position the core manages: `tpsl`, `time`, `ladder`, `preGrad`, `hold` or your own exit, `STOP`, `PAUSE_BUYS`,
|
|
15
|
+
`sell`.
|
|
16
|
+
- **Dry mode** calls nothing: it prints the launch it would send. `plan` with the API key set asks Candle for a dry-run
|
|
17
|
+
preview of the dev buy's fill (`dryRunAtomicLaunch`: the build's checks, nothing recorded, reserved or sent).
|
|
18
|
+
|
|
19
|
+
## Quick start
|
|
20
|
+
|
|
21
|
+
Node 22.18 or newer. A Candle API key with the `launch:write`, `swap:write` and `launch:read` scopes, on a plan that
|
|
22
|
+
allows atomic launches, and the account's embedded wallet funded.
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
npm install -g candle-launch-helper
|
|
26
|
+
cp "$(npm root -g)/candle-launch-helper/examples/launch.json" launch.json # your coin's details
|
|
27
|
+
candle-launch-helper plan --config launch.json # the exact launch body; with CANDLE_API_KEY set, Candle's
|
|
28
|
+
# dry-run preview of the dev buy's fill (--offline: none)
|
|
29
|
+
candle-launch-helper run --config launch.json # dry: logs the plan, runs the loop
|
|
30
|
+
export CANDLE_API_KEY=cndl_live_...
|
|
31
|
+
# set "mode": "live", your embedded wallet and launch.minDevBuyOutRaw (from plan's preview) in launch.json, then:
|
|
32
|
+
candle-launch-helper run --config launch.json # launches once, then manages the dev bag
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## The route, and why
|
|
36
|
+
|
|
37
|
+
Candle's atomic launch (`buildAtomicLaunch`, then `submitAtomicLaunch`) from the account's main wallet:
|
|
38
|
+
|
|
39
|
+
- the dev buy is a first-buy leg of the bundle, in the launch's block: nobody buys before it;
|
|
40
|
+
- Candle signs the main wallet's legs itself, so nothing is signed on this machine;
|
|
41
|
+
- the launch has one identity, its `clientLaunchId`, which Candle uses to keep a second launch from being built.
|
|
42
|
+
|
|
43
|
+
`payer` must be `"embedded"`: Candle refuses a TEE wallet on the atomic route.
|
|
44
|
+
|
|
45
|
+
## A run never launches twice
|
|
46
|
+
|
|
47
|
+
- **Before the first build** a live run checks, and sends nothing unless all pass (an alert says which failed; a
|
|
48
|
+
read that failed is tried again every 30 s):
|
|
49
|
+
- `launch.minDevBuyOutRaw` is set;
|
|
50
|
+
- the dev buy fits `buy.lifetimeSol` and `buy.maxOpen`;
|
|
51
|
+
- `wallet` is the key's embedded (main) wallet, where the dev bag lands (Candle's portfolio);
|
|
52
|
+
- the account has not launched a token with the same name and symbol (Candle's list of the account's launched
|
|
53
|
+
tokens, case and spaces ignored). Another state dir with the same spec is refused. A listed token without a name
|
|
54
|
+
or symbol, or a list that ends without saying it is done, is refused too (it cannot be judged).
|
|
55
|
+
`launch.newLaunch: true` launches the same name and symbol again on purpose. The check relies on Candle's list
|
|
56
|
+
showing a launch from minutes ago; that was not verified live.
|
|
57
|
+
- The `clientLaunchId` is written to the ledger **before** the first build and is never replaced. A state dir holds
|
|
58
|
+
one launch; a config for another coin in the same state dir is refused (alert), nothing is sent.
|
|
59
|
+
- The build is checked before the submit: the dev buy's leg carries `minAmountOut: 0` inside the bundle, so Candle's
|
|
60
|
+
`expectedFill` is the check (present, above zero, and at least `launch.minDevBuyOutRaw`). The submit goes out within
|
|
61
|
+
45 s of the build (the legs share one blockhash), and only from the process that built it.
|
|
62
|
+
- Every state is saved before the call it leads to (`building` before the build). A restart during a build, after a
|
|
63
|
+
build that was not submitted, or during a submit, a timeout, or any error after a request was sent reads Candle's
|
|
64
|
+
launch job (`getLaunchJob`) every 30 s: it resumes on `confirmed`, stops on `failed`, and waits otherwise. A restart
|
|
65
|
+
never builds. A launch unresolved for `launch.unresolvedAlertMin` minutes raises an alert with its `clientLaunchId`.
|
|
66
|
+
- **The bot never builds again on its own.** `RETRY_LAUNCH` in the state dir asks for one more build under the same
|
|
67
|
+
id. It acts only when a job read just then says `failed` or `JOB_NOT_FOUND` (the SDK's recovery steps), and not
|
|
68
|
+
while `PAUSE_BUYS` or `STOP` is set. Any other answer uses the file up (`RETRY_LAUNCH.ignored`) and logs and alerts
|
|
69
|
+
why. A job that says `confirmed` is taken as landed. A 409 on the rebuild (Candle still resolving the first attempt)
|
|
70
|
+
goes back to reading the job. A new `clientLaunchId` is never sent while a launch may land.
|
|
71
|
+
- `PAUSE_BUYS`: no launch fires (and none is submitted after a build). Dry mode makes no call at all.
|
|
72
|
+
- STOP, a signal and PAUSE_BUYS are read again right before every build.
|
|
73
|
+
- On `STOP` or SIGTERM the run waits for a launch call in flight (a submit can take about 160 s) before it closes the
|
|
74
|
+
ledger. Under systemd give the unit `TimeoutStopSec=200` or more; a kill in the middle is still safe (the state was
|
|
75
|
+
saved before the call, and the restart reads the job).
|
|
76
|
+
|
|
77
|
+
## The dev bag
|
|
78
|
+
|
|
79
|
+
Once the launch lands, the dev buy's transaction (the submit's buy-leg signature, else Candle's job) is booked from
|
|
80
|
+
the chain by the core (`openFromFill`: this wallet's tokens and SOL spent, never an API's number). When Candle names no
|
|
81
|
+
dev-buy transaction, or one that gave the wallet no tokens, the wallet's token-account history for the mint is read
|
|
82
|
+
whole (paged), oldest first. The first transaction that gave the wallet this coin is the dev buy (the bundle bought
|
|
83
|
+
first and opened the account): it is booked if it passes the core's checks. The scan never walks past it to a later
|
|
84
|
+
buy. If it cannot be read yet, the scan tries again later; if it fails the checks (no SOL spent, already booked), or
|
|
85
|
+
the history runs past 10,000 signatures, an alert asks for a hand booking:
|
|
86
|
+
|
|
87
|
+
```sh
|
|
88
|
+
candle-launch-helper adopt <signature> --config launch.json [--mint <mint>] [--force] # with the bot stopped
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`adopt` books the bag from that transaction through the same checks (this wallet, this mint, SOL spent, tokens
|
|
92
|
+
received, not booked already) and marks the launch open. On a launch whose bag is already open it refuses (a second
|
|
93
|
+
position would count twice in the caps and the exits); `--force` books it anyway.
|
|
94
|
+
|
|
95
|
+
## Config
|
|
96
|
+
|
|
97
|
+
The core's sections as in every candle-bot-core bot (`mode`, `payer`, `apiKeyEnv`, `rpcUrlEnv`, `wallet`, `stateDir`,
|
|
98
|
+
`buy`, `sell`, `exit`, `retry`, `alerts`), plus:
|
|
99
|
+
|
|
100
|
+
```json
|
|
101
|
+
{
|
|
102
|
+
"launch": {
|
|
103
|
+
"name": "My Coin", "symbol": "MINE", "imageUrl": "https://.../logo.png",
|
|
104
|
+
"description": "...", "website": "https://...", "twitter": "https://x.com/...", "telegram": "https://t.me/...",
|
|
105
|
+
"mode": "open",
|
|
106
|
+
"devBuySol": 0.05,
|
|
107
|
+
"minDevBuyOutRaw": "1000000000000",
|
|
108
|
+
"unresolvedAlertMin": 10
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| Key | Meaning |
|
|
114
|
+
|---|---|
|
|
115
|
+
| `launch.name`, `symbol`, `imageUrl` | required; the image is a hosted https URL |
|
|
116
|
+
| `launch.description`, `website`, `twitter`, `telegram` | optional |
|
|
117
|
+
| `launch.mode` | Candle launch mode (default `open`) |
|
|
118
|
+
| `launch.devBuySol` | the dev buy, SOL; at most `buy.sol`; counts against `buy.lifetimeSol` |
|
|
119
|
+
| `launch.minDevBuyOutRaw` | required in live mode: refuse to submit if Candle's expected fill is lower (raw token units, as a string). `plan` previews the fill |
|
|
120
|
+
| `launch.newLaunch` | `true` to launch even though the account already launched this name and symbol (default `false`: refused) |
|
|
121
|
+
| `launch.unresolvedAlertMin` | alert when a sent launch stays unresolved this long (default 10) |
|
|
122
|
+
|
|
123
|
+
## State dir
|
|
124
|
+
|
|
125
|
+
| File | What |
|
|
126
|
+
|---|---|
|
|
127
|
+
| `ledger.sqlite` | the `launches` row (id, state, bundle, mint, signatures), the dev bag's position and its orders, alerts |
|
|
128
|
+
| `status.json` | the launch's state, mint and position; the bag's value and exits |
|
|
129
|
+
| `events.jsonl` | `launch_preflight_refused`, `launch_id_saved`, `launch_built`, `launch_landed`, `launch_unresolved`, `launch_retry`, `launch_retry_ignored`, `dev_buy_found_on_chain`, `dev_bag_open`, sells |
|
|
130
|
+
| `RETRY_LAUNCH`, `.done`, `.ignored` | the operator's retry request, and what became of it |
|
|
131
|
+
|
|
132
|
+
## Known limits
|
|
133
|
+
|
|
134
|
+
- **Embedded payer only** (the atomic route). One dev-buy leg.
|
|
135
|
+
- **Image by URL.** The image must already be hosted.
|
|
136
|
+
- **The dev-buy transaction** is taken from the submit's signatures (the buy leg's index), Candle's launch job, or the
|
|
137
|
+
wallet's history for the mint; until one is booked, the bag is not managed (alert; `adopt` books it by hand).
|
|
138
|
+
- **The same-spec check** reads up to 20 pages (1,000 tokens) of the account's launches; an account with more is
|
|
139
|
+
refused unless `launch.newLaunch` is set.
|
|
140
|
+
- **A launch refused at the submit check** (expected fill under the floor) leaves a built bundle Candle never got; its
|
|
141
|
+
job may not answer `failed` or `JOB_NOT_FOUND`, so `RETRY_LAUNCH` may be ignored. Use a new state dir (with
|
|
142
|
+
`launch.newLaunch: true` only if the first launch is known not to exist).
|
|
143
|
+
- **Solana only.**
|
|
144
|
+
|
|
145
|
+
## License
|
|
146
|
+
|
|
147
|
+
MIT
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// The published package runs the compiled dist/; a source checkout runs src/ directly (Node strips types).
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
const dist = new URL("../dist/cli.js", import.meta.url);
|
|
5
|
+
const entry = existsSync(dist) ? dist : new URL("../src/cli.ts", import.meta.url);
|
|
6
|
+
const { main } = await import(entry.href);
|
|
7
|
+
process.exit(await main(process.argv.slice(2)));
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type Config } from "./config.ts";
|
|
2
|
+
import { type LaunchClient } from "./launcher.ts";
|
|
3
|
+
export declare const HELP = "candle-launch-helper: fires the Candle launch you describe, with its dev buy in the same bundle, then manages the\ndev bag with the core's exits and controls.\n\n candle-launch-helper plan --config launch.json [--offline] print the launch body and, with the API key set,\n Candle's dry-run preview of the dev buy's fill\n (dryRunAtomicLaunch: records and sends nothing)\n candle-launch-helper run --config launch.json [--mode dry|live] launch (live), then manage the bag\n candle-launch-helper status --config launch.json print status.json\n candle-launch-helper sell <mint> --config launch.json [--fraction 1]\n candle-launch-helper adopt <signature> --config launch.json [--mint <mint>] [--force]\n book the dev bag from its transaction (bot stopped)\n\nOne launch per spec: a live run refuses when the account already launched the same name and symbol (launch.newLaunch\noverrides), and a state dir never launches twice. Controls: STOP, PAUSE_BUYS (no launch fires), RETRY_LAUNCH (one more\nbuild under the same clientLaunchId, only when Candle's job says failed or not found). Docs: the README.\n";
|
|
4
|
+
/**
|
|
5
|
+
* `plan`: the launch body, and with a client Candle's dry-run preview of the dev buy's fill (dryRunAtomicLaunch:
|
|
6
|
+
* the build's checks; records, reserves and sends nothing). Its clientLaunchId is a throwaway, never built.
|
|
7
|
+
*/
|
|
8
|
+
export declare function planOutput(cfg: Config, client: LaunchClient | null, skipped: string | null): Promise<Record<string, unknown>>;
|
|
9
|
+
/** Longest a launch call may take (the SDK: a submit up to ~160 s). */
|
|
10
|
+
export declare const LAUNCH_TIMEOUT_MS = 180000;
|
|
11
|
+
export declare function main(argv?: string[]): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/** The command line: plan, run, status, sell, adopt. */
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CandleClient } from "@candledottv/agent-sdk";
|
|
6
|
+
import { timedFetch } from "candle-bot-core/exec/trader";
|
|
7
|
+
import { acquireLock } from "candle-bot-core/engine";
|
|
8
|
+
import { args, context as coreContext, die, guardMain, parts, run, sell, status } from "candle-bot-core/runtime";
|
|
9
|
+
import { loadConfig, ConfigError } from "./config.js";
|
|
10
|
+
import { Ledger } from "./ledger.js";
|
|
11
|
+
import { Engine, launchBody } from "./launcher.js";
|
|
12
|
+
export const HELP = `candle-launch-helper: fires the Candle launch you describe, with its dev buy in the same bundle, then manages the
|
|
13
|
+
dev bag with the core's exits and controls.
|
|
14
|
+
|
|
15
|
+
candle-launch-helper plan --config launch.json [--offline] print the launch body and, with the API key set,
|
|
16
|
+
Candle's dry-run preview of the dev buy's fill
|
|
17
|
+
(dryRunAtomicLaunch: records and sends nothing)
|
|
18
|
+
candle-launch-helper run --config launch.json [--mode dry|live] launch (live), then manage the bag
|
|
19
|
+
candle-launch-helper status --config launch.json print status.json
|
|
20
|
+
candle-launch-helper sell <mint> --config launch.json [--fraction 1]
|
|
21
|
+
candle-launch-helper adopt <signature> --config launch.json [--mint <mint>] [--force]
|
|
22
|
+
book the dev bag from its transaction (bot stopped)
|
|
23
|
+
|
|
24
|
+
One launch per spec: a live run refuses when the account already launched the same name and symbol (launch.newLaunch
|
|
25
|
+
overrides), and a state dir never launches twice. Controls: STOP, PAUSE_BUYS (no launch fires), RETRY_LAUNCH (one more
|
|
26
|
+
build under the same clientLaunchId, only when Candle's job says failed or not found). Docs: the README.
|
|
27
|
+
`;
|
|
28
|
+
function context(configPath, modeOverride) {
|
|
29
|
+
let cfg;
|
|
30
|
+
try {
|
|
31
|
+
cfg = loadConfig(configPath);
|
|
32
|
+
}
|
|
33
|
+
catch (e) {
|
|
34
|
+
if (e instanceof ConfigError)
|
|
35
|
+
die(e.message);
|
|
36
|
+
throw e;
|
|
37
|
+
}
|
|
38
|
+
return coreContext(cfg, modeOverride);
|
|
39
|
+
}
|
|
40
|
+
async function makeEngine(ctx, ledger) {
|
|
41
|
+
const { strategy, alerter, executor } = await parts(ctx, ledger, { maxBuyImpactBps: 10_000 });
|
|
42
|
+
// Live: Candle's client for the launch calls, with a longer timeout than trades (a submit can take ~160 s and a
|
|
43
|
+
// same-id rebuild ~95 s, per the SDK). Dry: none, so nothing can be built or sent.
|
|
44
|
+
const apiKey = process.env[ctx.cfg.apiKeyEnv];
|
|
45
|
+
if (ctx.cfg.mode === "live" && !apiKey)
|
|
46
|
+
die(`${ctx.cfg.apiKeyEnv} is not set (the Candle API key)`);
|
|
47
|
+
const client = ctx.cfg.mode === "live" ? new CandleClient({ apiUrl: ctx.cfg.apiUrl, apiKey, fetch: timedFetch(LAUNCH_TIMEOUT_MS) }) : null;
|
|
48
|
+
const engine = new Engine({ cfg: ctx.cfg, ledger, client, executor, strategy, rpc: ctx.rpc, log: ctx.log, alerter });
|
|
49
|
+
return { engine, trigger: engine.trigger };
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* `plan`: the launch body, and with a client Candle's dry-run preview of the dev buy's fill (dryRunAtomicLaunch:
|
|
53
|
+
* the build's checks; records, reserves and sends nothing). Its clientLaunchId is a throwaway, never built.
|
|
54
|
+
*/
|
|
55
|
+
export async function planOutput(cfg, client, skipped) {
|
|
56
|
+
const body = launchBody(cfg.launch);
|
|
57
|
+
const out = { route: "atomic (build, check the dev buy's expected fill, submit)", body, minDevBuyOutRaw: cfg.launch.minDevBuyOutRaw?.toString() ?? null };
|
|
58
|
+
if (!client?.dryRunAtomicLaunch) {
|
|
59
|
+
out.preview = skipped ?? "skipped (no client)";
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const r = await client.dryRunAtomicLaunch({ ...body, clientLaunchId: `clh-plan-${randomUUID()}` });
|
|
64
|
+
const fill = r.fills?.[0]?.expectedFill ?? null;
|
|
65
|
+
out.preview = { devBuyExpectedOutRaw: fill?.amountOutRaw ?? null, devBuyMinOutRaw: fill?.minAmountOutRaw ?? null, graduatesInBundle: r.graduatesInBundle ?? null, note: "set launch.minDevBuyOutRaw at or below devBuyExpectedOutRaw (the submit is refused under it)" };
|
|
66
|
+
}
|
|
67
|
+
catch (e) {
|
|
68
|
+
out.preview = { error: String(e?.message ?? e).slice(0, 300) };
|
|
69
|
+
}
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
72
|
+
/** Longest a launch call may take (the SDK: a submit up to ~160 s). */
|
|
73
|
+
export const LAUNCH_TIMEOUT_MS = 180_000;
|
|
74
|
+
const openLedger = (ctx) => () => new Ledger(join(ctx.cfg.stateDir, "ledger.sqlite"));
|
|
75
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
76
|
+
return guardMain(async () => {
|
|
77
|
+
const { pos, opt } = args(argv);
|
|
78
|
+
const cmd = pos[0];
|
|
79
|
+
if (!cmd || cmd === "help" || opt.help) {
|
|
80
|
+
process.stdout.write(HELP);
|
|
81
|
+
return cmd || opt.help ? 0 : 2;
|
|
82
|
+
}
|
|
83
|
+
if (cmd === "version" || opt.version) {
|
|
84
|
+
process.stdout.write(JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version + "\n");
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
const config = opt.config ?? "launch.json";
|
|
88
|
+
switch (cmd) {
|
|
89
|
+
case "plan": {
|
|
90
|
+
let cfg;
|
|
91
|
+
try {
|
|
92
|
+
cfg = loadConfig(config);
|
|
93
|
+
}
|
|
94
|
+
catch (e) {
|
|
95
|
+
if (e instanceof ConfigError)
|
|
96
|
+
die(e.message);
|
|
97
|
+
throw e;
|
|
98
|
+
}
|
|
99
|
+
const apiKey = process.env[cfg.apiKeyEnv];
|
|
100
|
+
const skip = opt.offline ? "skipped (--offline)" : !apiKey ? `skipped (${cfg.apiKeyEnv} not set)` : null;
|
|
101
|
+
const client = skip ? null : new CandleClient({ apiUrl: cfg.apiUrl, apiKey, fetch: timedFetch(LAUNCH_TIMEOUT_MS) });
|
|
102
|
+
const out = await planOutput(cfg, client, skip);
|
|
103
|
+
process.stdout.write(JSON.stringify(out, null, 2) + "\n");
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
case "run": {
|
|
107
|
+
const ctx = context(config, opt.mode);
|
|
108
|
+
return run(ctx, openLedger(ctx), (ledger) => makeEngine(ctx, ledger));
|
|
109
|
+
}
|
|
110
|
+
case "status":
|
|
111
|
+
return status(context(config));
|
|
112
|
+
case "sell": {
|
|
113
|
+
if (!pos[1])
|
|
114
|
+
die("sell <mint> [--fraction f]");
|
|
115
|
+
const ctx = context(config);
|
|
116
|
+
return sell(ctx, pos[1], opt.fraction ? Number(opt.fraction) : 1, openLedger(ctx), (ledger) => makeEngine(ctx, ledger));
|
|
117
|
+
}
|
|
118
|
+
case "adopt": {
|
|
119
|
+
if (!pos[1])
|
|
120
|
+
die("adopt <signature> [--mint <mint>]");
|
|
121
|
+
const ctx = context(config);
|
|
122
|
+
const lock = acquireLock(ctx.cfg.stateDir);
|
|
123
|
+
if (!lock)
|
|
124
|
+
die(`the bot is running (${ctx.cfg.stateDir}/lock): stop it, then adopt`);
|
|
125
|
+
const ledger = openLedger(ctx)();
|
|
126
|
+
try {
|
|
127
|
+
const { engine } = await makeEngine(ctx, ledger);
|
|
128
|
+
const r = await engine.launcher.adopt(pos[1], opt.mint, opt.force === "true");
|
|
129
|
+
process.stdout.write(r + "\n");
|
|
130
|
+
return /^position \d+/.test(r) ? 0 : 1;
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
ledger.close();
|
|
134
|
+
lock.release();
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
default:
|
|
138
|
+
die(`unknown command ${cmd}`);
|
|
139
|
+
}
|
|
140
|
+
});
|
|
141
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one config file: the core's shared sections (candle-bot-core) plus `launch`, the launch spec. Unknown keys are
|
|
3
|
+
* refused at every level. The launch goes through Candle's atomic route from the account's main (embedded) wallet:
|
|
4
|
+
* `payer` must be "embedded".
|
|
5
|
+
*/
|
|
6
|
+
import { type Config as CoreConfig, type SectionParser } from "candle-bot-core";
|
|
7
|
+
export { ConfigError } from "candle-bot-core/config";
|
|
8
|
+
export declare const LAUNCH_MODES: readonly ["open", "exclusive", "test-open", "test-exclusive", "exp-a", "exp-b", "exp-c", "exp-custom"];
|
|
9
|
+
export type LaunchMode = (typeof LAUNCH_MODES)[number];
|
|
10
|
+
export interface LaunchSpec {
|
|
11
|
+
name: string;
|
|
12
|
+
symbol: string;
|
|
13
|
+
imageUrl: string;
|
|
14
|
+
description?: string;
|
|
15
|
+
website?: string;
|
|
16
|
+
twitter?: string;
|
|
17
|
+
telegram?: string;
|
|
18
|
+
mode: LaunchMode;
|
|
19
|
+
/** The dev buy, SOL: the bundle's first buy, from the same wallet, in the launch's block. At most buy.sol. */
|
|
20
|
+
devBuySol: number;
|
|
21
|
+
/** Refuse to submit unless Candle's expected fill for the dev buy is at least this (raw token units). */
|
|
22
|
+
minDevBuyOutRaw?: bigint;
|
|
23
|
+
/** Alert when a sent launch is still unresolved after this many minutes. */
|
|
24
|
+
unresolvedAlertMin: number;
|
|
25
|
+
/** Launch even when the account already launched a token with this name and symbol (default false: refused). */
|
|
26
|
+
newLaunch: boolean;
|
|
27
|
+
}
|
|
28
|
+
export type Config = CoreConfig & {
|
|
29
|
+
launch: LaunchSpec;
|
|
30
|
+
};
|
|
31
|
+
export declare const SECTIONS: {
|
|
32
|
+
launch: SectionParser<LaunchSpec>;
|
|
33
|
+
};
|
|
34
|
+
export declare function parseConfig(raw: unknown, baseDir?: string): Config;
|
|
35
|
+
export declare function loadConfig(path: string): Config;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The one config file: the core's shared sections (candle-bot-core) plus `launch`, the launch spec. Unknown keys are
|
|
3
|
+
* refused at every level. The launch goes through Candle's atomic route from the account's main (embedded) wallet:
|
|
4
|
+
* `payer` must be "embedded".
|
|
5
|
+
*/
|
|
6
|
+
import { parseConfig as parseCore, loadConfig as loadCore, ConfigError, isObj, only, num, str } from "candle-bot-core";
|
|
7
|
+
export { ConfigError } from "candle-bot-core/config";
|
|
8
|
+
export const LAUNCH_MODES = ["open", "exclusive", "test-open", "test-exclusive", "exp-a", "exp-b", "exp-c", "exp-custom"];
|
|
9
|
+
const parseLaunch = (raw, core) => {
|
|
10
|
+
if (!isObj(raw))
|
|
11
|
+
throw new ConfigError("config.launch: required object (name, symbol, imageUrl, devBuySol, ...)");
|
|
12
|
+
only(raw, ["name", "symbol", "imageUrl", "description", "website", "twitter", "telegram", "mode", "devBuySol", "minDevBuyOutRaw", "unresolvedAlertMin", "newLaunch"], "config.launch");
|
|
13
|
+
const name = str(raw, "name", "config.launch");
|
|
14
|
+
const symbol = str(raw, "symbol", "config.launch");
|
|
15
|
+
const imageUrl = str(raw, "imageUrl", "config.launch");
|
|
16
|
+
if (!/^https:\/\//.test(imageUrl))
|
|
17
|
+
throw new ConfigError("config.launch.imageUrl: must be an https:// URL");
|
|
18
|
+
const mode = str(raw, "mode", "config.launch", { def: "open" });
|
|
19
|
+
if (!LAUNCH_MODES.includes(mode))
|
|
20
|
+
throw new ConfigError(`config.launch.mode: "${mode}" is not a Candle launch mode (${LAUNCH_MODES.join(", ")})`);
|
|
21
|
+
const devBuySol = num(raw, "devBuySol", "config.launch", { min: 0.000001 });
|
|
22
|
+
if (devBuySol > core.buy.sol)
|
|
23
|
+
throw new ConfigError(`config.launch.devBuySol: ${devBuySol} is above buy.sol (${core.buy.sol})`);
|
|
24
|
+
let minDevBuyOutRaw;
|
|
25
|
+
if (raw.minDevBuyOutRaw !== undefined) {
|
|
26
|
+
const v = String(raw.minDevBuyOutRaw);
|
|
27
|
+
if (!/^\d+$/.test(v))
|
|
28
|
+
throw new ConfigError("config.launch.minDevBuyOutRaw: a whole number of raw token units (a string)");
|
|
29
|
+
minDevBuyOutRaw = BigInt(v);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
name, symbol, imageUrl, mode, devBuySol, minDevBuyOutRaw,
|
|
33
|
+
description: str(raw, "description", "config.launch", { optional: true }),
|
|
34
|
+
website: str(raw, "website", "config.launch", { optional: true }),
|
|
35
|
+
twitter: str(raw, "twitter", "config.launch", { optional: true }),
|
|
36
|
+
telegram: str(raw, "telegram", "config.launch", { optional: true }),
|
|
37
|
+
unresolvedAlertMin: num(raw, "unresolvedAlertMin", "config.launch", { min: 1, def: 10 }),
|
|
38
|
+
newLaunch: bool(raw, "newLaunch"),
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
function bool(o, k) {
|
|
42
|
+
const v = o[k];
|
|
43
|
+
if (v === undefined)
|
|
44
|
+
return false;
|
|
45
|
+
if (typeof v !== "boolean")
|
|
46
|
+
throw new ConfigError(`config.launch.${k}: true or false`);
|
|
47
|
+
return v;
|
|
48
|
+
}
|
|
49
|
+
export const SECTIONS = { launch: parseLaunch };
|
|
50
|
+
function check(cfg) {
|
|
51
|
+
if (cfg.payer !== "embedded")
|
|
52
|
+
throw new ConfigError('config.payer: the launch helper launches from the account\'s main wallet through Candle\'s atomic route; set "payer": "embedded" (Candle refuses a TEE wallet on that route)');
|
|
53
|
+
// The live run's pre-flight refuses too (a --mode live override is checked there).
|
|
54
|
+
if (cfg.mode === "live" && cfg.launch.minDevBuyOutRaw === undefined)
|
|
55
|
+
throw new ConfigError("config.launch.minDevBuyOutRaw: required in live mode (the dev buy's on-chain minimum is 0 inside the bundle; `plan` previews Candle's fill to set it from)");
|
|
56
|
+
return cfg;
|
|
57
|
+
}
|
|
58
|
+
export function parseConfig(raw, baseDir = process.cwd()) {
|
|
59
|
+
return check(parseCore(raw, baseDir, SECTIONS));
|
|
60
|
+
}
|
|
61
|
+
export function loadConfig(path) {
|
|
62
|
+
return check(loadCore(path, SECTIONS));
|
|
63
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** candle-launch-helper: the launcher, its trigger and engine, the config and the ledger, for use as a library. */
|
|
2
|
+
export { Launcher, LaunchTrigger, Engine, launchBody, specHash, BUILD_SUBMIT_MS, JOB_READ_MS, RETRY_FILE, LAUNCH_LIST_PAGES, DEV_BUY_PAGE, DEV_BUY_PAGES } from "./launcher.ts";
|
|
3
|
+
export type { LaunchClient, LauncherDeps, EngineDeps } from "./launcher.ts";
|
|
4
|
+
export { parseConfig, loadConfig, SECTIONS, ConfigError, LAUNCH_MODES } from "./config.ts";
|
|
5
|
+
export type { Config, LaunchSpec, LaunchMode } from "./config.ts";
|
|
6
|
+
export { Ledger } from "./ledger.ts";
|
|
7
|
+
export type { LaunchRow, LaunchState } from "./ledger.ts";
|
|
8
|
+
export { main, HELP, planOutput } from "./cli.ts";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** candle-launch-helper: the launcher, its trigger and engine, the config and the ledger, for use as a library. */
|
|
2
|
+
export { Launcher, LaunchTrigger, Engine, launchBody, specHash, BUILD_SUBMIT_MS, JOB_READ_MS, RETRY_FILE, LAUNCH_LIST_PAGES, DEV_BUY_PAGE, DEV_BUY_PAGES } from "./launcher.js";
|
|
3
|
+
export { parseConfig, loadConfig, SECTIONS, ConfigError, LAUNCH_MODES } from "./config.js";
|
|
4
|
+
export { Ledger } from "./ledger.js";
|
|
5
|
+
export { main, HELP, planOutput } from "./cli.js";
|