clodds 1.6.26 → 1.7.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/README.md +147 -59
- package/dist/embeddings/index.d.ts +10 -0
- package/dist/embeddings/index.js +56 -21
- package/dist/embeddings/index.js.map +1 -1
- package/dist/gateway/index.js +20 -0
- package/dist/gateway/index.js.map +1 -1
- package/dist/gateway/launch-routes.d.ts +72 -0
- package/dist/gateway/launch-routes.js +772 -0
- package/dist/gateway/launch-routes.js.map +1 -0
- package/dist/gateway/server.d.ts +1 -0
- package/dist/gateway/server.js +9 -1
- package/dist/gateway/server.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,772 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Launch API Routes — REST endpoints for one-call Solana token launches.
|
|
4
|
+
*
|
|
5
|
+
* Mounted as an Express Router via httpGateway.setLaunchRouter().
|
|
6
|
+
* All endpoints are prefixed with /api/launch by the caller.
|
|
7
|
+
*
|
|
8
|
+
* Built on Meteora Dynamic Bonding Curves with automatic graduation
|
|
9
|
+
* to DAMM v2 AMM. 90/10 fee split — creator keeps 90%.
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.createLaunchRouter = createLaunchRouter;
|
|
46
|
+
const express_1 = require("express");
|
|
47
|
+
const web3_js_1 = require("@solana/web3.js");
|
|
48
|
+
const fs_1 = require("fs");
|
|
49
|
+
const path_1 = require("path");
|
|
50
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
51
|
+
const registry_js_1 = require("../acp/registry.js");
|
|
52
|
+
const STATE_DIR = process.env.CLODDS_STATE_DIR || (0, path_1.join)(process.cwd(), '.clodds');
|
|
53
|
+
const LAUNCHES_FILE = (0, path_1.join)(STATE_DIR, 'launches.json');
|
|
54
|
+
function loadLaunches() {
|
|
55
|
+
try {
|
|
56
|
+
if ((0, fs_1.existsSync)(LAUNCHES_FILE)) {
|
|
57
|
+
return JSON.parse((0, fs_1.readFileSync)(LAUNCHES_FILE, 'utf-8'));
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
logger_js_1.logger.warn('Launch registry: Failed to load launches.json, starting fresh');
|
|
62
|
+
}
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
function saveLaunches(launches) {
|
|
66
|
+
try {
|
|
67
|
+
const dir = (0, path_1.dirname)(LAUNCHES_FILE);
|
|
68
|
+
if (!(0, fs_1.existsSync)(dir))
|
|
69
|
+
(0, fs_1.mkdirSync)(dir, { recursive: true });
|
|
70
|
+
(0, fs_1.writeFileSync)(LAUNCHES_FILE, JSON.stringify(launches, null, 2));
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
logger_js_1.logger.warn({ err }, 'Launch registry: Failed to save launches.json');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const launchRegistry = loadLaunches();
|
|
77
|
+
// ── IPFS / Metadata ──────────────────────────────────────────────────────────
|
|
78
|
+
/**
|
|
79
|
+
* Build a minimal JSON metadata blob and upload to a public IPFS gateway.
|
|
80
|
+
* Falls back through multiple providers so we don't depend on any single service.
|
|
81
|
+
*/
|
|
82
|
+
async function uploadMetadata(params) {
|
|
83
|
+
// Build standard SPL token metadata JSON (Metaplex-compatible)
|
|
84
|
+
const metadata = {
|
|
85
|
+
name: params.name,
|
|
86
|
+
symbol: params.symbol,
|
|
87
|
+
description: params.description ?? params.name,
|
|
88
|
+
showName: true,
|
|
89
|
+
};
|
|
90
|
+
if (params.imageUrl)
|
|
91
|
+
metadata.image = params.imageUrl;
|
|
92
|
+
if (params.twitter)
|
|
93
|
+
metadata.twitter = params.twitter;
|
|
94
|
+
if (params.telegram)
|
|
95
|
+
metadata.telegram = params.telegram;
|
|
96
|
+
if (params.website) {
|
|
97
|
+
metadata.website = params.website;
|
|
98
|
+
metadata.external_url = params.website;
|
|
99
|
+
}
|
|
100
|
+
// Strategy: try multiple IPFS upload services in order
|
|
101
|
+
const providers = [
|
|
102
|
+
uploadViaPumpFun,
|
|
103
|
+
uploadViaNftStorage,
|
|
104
|
+
];
|
|
105
|
+
for (const provider of providers) {
|
|
106
|
+
try {
|
|
107
|
+
const uri = await provider(params, metadata);
|
|
108
|
+
if (uri)
|
|
109
|
+
return uri;
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
logger_js_1.logger.warn({ err, provider: provider.name }, 'Launch API: IPFS provider failed, trying next');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
throw new Error('All metadata upload providers failed. Provide a pre-uploaded uri instead.');
|
|
116
|
+
}
|
|
117
|
+
/** Upload via pump.fun IPFS (fast, free, but third-party) */
|
|
118
|
+
async function uploadViaPumpFun(params, _metadata) {
|
|
119
|
+
const formData = new FormData();
|
|
120
|
+
formData.append('name', params.name);
|
|
121
|
+
formData.append('symbol', params.symbol);
|
|
122
|
+
formData.append('description', params.description ?? params.name);
|
|
123
|
+
if (params.twitter)
|
|
124
|
+
formData.append('twitter', params.twitter);
|
|
125
|
+
if (params.telegram)
|
|
126
|
+
formData.append('telegram', params.telegram);
|
|
127
|
+
if (params.website)
|
|
128
|
+
formData.append('website', params.website);
|
|
129
|
+
formData.append('showName', 'true');
|
|
130
|
+
if (params.imageUrl) {
|
|
131
|
+
const imgBlob = await fetchImageSafe(params.imageUrl);
|
|
132
|
+
if (imgBlob)
|
|
133
|
+
formData.append('file', imgBlob, 'image.png');
|
|
134
|
+
}
|
|
135
|
+
const response = await fetch('https://pump.fun/api/ipfs', {
|
|
136
|
+
method: 'POST',
|
|
137
|
+
body: formData,
|
|
138
|
+
signal: AbortSignal.timeout(15_000),
|
|
139
|
+
});
|
|
140
|
+
if (!response.ok)
|
|
141
|
+
return '';
|
|
142
|
+
const result = await response.json();
|
|
143
|
+
return result.metadataUri ?? '';
|
|
144
|
+
}
|
|
145
|
+
/** Upload via nft.storage (decentralized, free tier) */
|
|
146
|
+
async function uploadViaNftStorage(_params, metadata) {
|
|
147
|
+
const nftStorageKey = process.env.NFT_STORAGE_API_KEY;
|
|
148
|
+
if (!nftStorageKey)
|
|
149
|
+
return '';
|
|
150
|
+
const response = await fetch('https://api.nft.storage/upload', {
|
|
151
|
+
method: 'POST',
|
|
152
|
+
headers: {
|
|
153
|
+
'Authorization': `Bearer ${nftStorageKey}`,
|
|
154
|
+
'Content-Type': 'application/json',
|
|
155
|
+
},
|
|
156
|
+
body: JSON.stringify(metadata),
|
|
157
|
+
signal: AbortSignal.timeout(15_000),
|
|
158
|
+
});
|
|
159
|
+
if (!response.ok)
|
|
160
|
+
return '';
|
|
161
|
+
const result = await response.json();
|
|
162
|
+
const cid = result.value?.cid;
|
|
163
|
+
return cid ? `https://nftstorage.link/ipfs/${cid}` : '';
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Safely fetch an image URL. Blocks internal/private IPs to prevent SSRF.
|
|
167
|
+
*/
|
|
168
|
+
async function fetchImageSafe(url) {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = new URL(url);
|
|
171
|
+
// Block non-HTTP(S) schemes
|
|
172
|
+
if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
|
|
173
|
+
return null;
|
|
174
|
+
// Block internal hostnames
|
|
175
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
176
|
+
if (hostname === 'localhost' ||
|
|
177
|
+
hostname === '127.0.0.1' ||
|
|
178
|
+
hostname === '0.0.0.0' ||
|
|
179
|
+
hostname === '::1' ||
|
|
180
|
+
hostname.endsWith('.local') ||
|
|
181
|
+
hostname.startsWith('10.') ||
|
|
182
|
+
hostname.startsWith('192.168.') ||
|
|
183
|
+
/^172\.(1[6-9]|2\d|3[01])\./.test(hostname) ||
|
|
184
|
+
hostname.startsWith('169.254.')) {
|
|
185
|
+
logger_js_1.logger.warn({ url }, 'Launch API: Blocked SSRF attempt on imageUrl');
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
const response = await fetch(url, {
|
|
189
|
+
signal: AbortSignal.timeout(10_000),
|
|
190
|
+
redirect: 'follow',
|
|
191
|
+
});
|
|
192
|
+
if (!response.ok)
|
|
193
|
+
return null;
|
|
194
|
+
// Cap image size at 10MB
|
|
195
|
+
const contentLength = response.headers.get('content-length');
|
|
196
|
+
if (contentLength && parseInt(contentLength, 10) > 10 * 1024 * 1024)
|
|
197
|
+
return null;
|
|
198
|
+
return await response.blob();
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// ── Validation ───────────────────────────────────────────────────────────────
|
|
205
|
+
const VALID_DECIMALS = new Set([6, 7, 8, 9]);
|
|
206
|
+
function validateLaunchRequest(body) {
|
|
207
|
+
if (!body || typeof body !== 'object') {
|
|
208
|
+
return { valid: false, error: 'Request body must be a JSON object' };
|
|
209
|
+
}
|
|
210
|
+
const b = body;
|
|
211
|
+
// Required fields
|
|
212
|
+
if (!b.name || typeof b.name !== 'string') {
|
|
213
|
+
return { valid: false, error: 'Required: name (string, max 32 chars)' };
|
|
214
|
+
}
|
|
215
|
+
if (b.name.length > 32) {
|
|
216
|
+
return { valid: false, error: 'name must be 32 characters or fewer' };
|
|
217
|
+
}
|
|
218
|
+
if (!b.symbol || typeof b.symbol !== 'string') {
|
|
219
|
+
return { valid: false, error: 'Required: symbol (string, max 10 chars)' };
|
|
220
|
+
}
|
|
221
|
+
if (b.symbol.length > 10) {
|
|
222
|
+
return { valid: false, error: 'symbol must be 10 characters or fewer' };
|
|
223
|
+
}
|
|
224
|
+
if (!b.uri && !b.imageUrl && !b.description) {
|
|
225
|
+
return { valid: false, error: 'Provide either uri (metadata URI) or imageUrl + description for auto-upload' };
|
|
226
|
+
}
|
|
227
|
+
// Validate creatorWallet is a valid Solana pubkey
|
|
228
|
+
if (b.creatorWallet !== undefined) {
|
|
229
|
+
if (typeof b.creatorWallet !== 'string') {
|
|
230
|
+
return { valid: false, error: 'creatorWallet must be a string (Solana public key)' };
|
|
231
|
+
}
|
|
232
|
+
try {
|
|
233
|
+
new web3_js_1.PublicKey(b.creatorWallet);
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return { valid: false, error: 'creatorWallet is not a valid Solana public key' };
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// Numeric fields — check both type AND NaN (typeof NaN === 'number' trap)
|
|
240
|
+
if (b.initialMarketCap !== undefined) {
|
|
241
|
+
if (typeof b.initialMarketCap !== 'number' || Number.isNaN(b.initialMarketCap) || b.initialMarketCap <= 0) {
|
|
242
|
+
return { valid: false, error: 'initialMarketCap must be a positive number (SOL)' };
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (b.graduationMarketCap !== undefined) {
|
|
246
|
+
if (typeof b.graduationMarketCap !== 'number' || Number.isNaN(b.graduationMarketCap) || b.graduationMarketCap <= 0) {
|
|
247
|
+
return { valid: false, error: 'graduationMarketCap must be a positive number (SOL)' };
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// graduationMarketCap must be > initialMarketCap
|
|
251
|
+
const initMcap = (typeof b.initialMarketCap === 'number' && !Number.isNaN(b.initialMarketCap)) ? b.initialMarketCap : 30;
|
|
252
|
+
const gradMcap = (typeof b.graduationMarketCap === 'number' && !Number.isNaN(b.graduationMarketCap)) ? b.graduationMarketCap : 500;
|
|
253
|
+
if (gradMcap <= initMcap) {
|
|
254
|
+
return { valid: false, error: `graduationMarketCap (${gradMcap}) must be greater than initialMarketCap (${initMcap})` };
|
|
255
|
+
}
|
|
256
|
+
if (b.totalSupply !== undefined) {
|
|
257
|
+
if (typeof b.totalSupply !== 'number' || Number.isNaN(b.totalSupply) || b.totalSupply <= 0 || !Number.isFinite(b.totalSupply)) {
|
|
258
|
+
return { valid: false, error: 'totalSupply must be a positive finite number' };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
if (b.decimals !== undefined) {
|
|
262
|
+
if (typeof b.decimals !== 'number' || !VALID_DECIMALS.has(b.decimals)) {
|
|
263
|
+
return { valid: false, error: 'decimals must be 6, 7, 8, or 9' };
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (b.creatorFeePercent !== undefined) {
|
|
267
|
+
if (typeof b.creatorFeePercent !== 'number' || Number.isNaN(b.creatorFeePercent) || b.creatorFeePercent < 0 || b.creatorFeePercent > 100) {
|
|
268
|
+
return { valid: false, error: 'creatorFeePercent must be 0-100' };
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (b.antiSniperFeeBps !== undefined) {
|
|
272
|
+
if (typeof b.antiSniperFeeBps !== 'number' || Number.isNaN(b.antiSniperFeeBps) || b.antiSniperFeeBps < 0 || b.antiSniperFeeBps > 10000) {
|
|
273
|
+
return { valid: false, error: 'antiSniperFeeBps must be 0-10000' };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
if (b.endingFeeBps !== undefined) {
|
|
277
|
+
if (typeof b.endingFeeBps !== 'number' || Number.isNaN(b.endingFeeBps) || b.endingFeeBps < 0 || b.endingFeeBps > 10000) {
|
|
278
|
+
return { valid: false, error: 'endingFeeBps must be 0-10000' };
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
if (b.feeDecayDurationSec !== undefined) {
|
|
282
|
+
if (typeof b.feeDecayDurationSec !== 'number' || Number.isNaN(b.feeDecayDurationSec) || b.feeDecayDurationSec < 0) {
|
|
283
|
+
return { valid: false, error: 'feeDecayDurationSec must be a non-negative number' };
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (b.initialBuySol !== undefined) {
|
|
287
|
+
if (typeof b.initialBuySol !== 'number' || Number.isNaN(b.initialBuySol) || b.initialBuySol < 0) {
|
|
288
|
+
return { valid: false, error: 'initialBuySol must be a non-negative number' };
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
if (b.slippageBps !== undefined) {
|
|
292
|
+
if (typeof b.slippageBps !== 'number' || Number.isNaN(b.slippageBps) || b.slippageBps < 0 || b.slippageBps > 10000) {
|
|
293
|
+
return { valid: false, error: 'slippageBps must be 0-10000' };
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
// antiSniper starting fee should be >= ending fee
|
|
297
|
+
const startFee = typeof b.antiSniperFeeBps === 'number' ? b.antiSniperFeeBps : 500;
|
|
298
|
+
const endFee = typeof b.endingFeeBps === 'number' ? b.endingFeeBps : 100;
|
|
299
|
+
if (endFee > startFee) {
|
|
300
|
+
return { valid: false, error: `endingFeeBps (${endFee}) cannot be greater than antiSniperFeeBps (${startFee})` };
|
|
301
|
+
}
|
|
302
|
+
return { valid: true, params: b };
|
|
303
|
+
}
|
|
304
|
+
// ── SOL conversion helper ────────────────────────────────────────────────────
|
|
305
|
+
/**
|
|
306
|
+
* Convert SOL to lamports using string math to avoid floating-point precision loss.
|
|
307
|
+
* e.g. 1.5 SOL -> "1500000000"
|
|
308
|
+
*/
|
|
309
|
+
function solToLamports(sol) {
|
|
310
|
+
const str = sol.toFixed(9); // max 9 decimal places for lamports
|
|
311
|
+
const [whole, frac = ''] = str.split('.');
|
|
312
|
+
const padded = frac.padEnd(9, '0').slice(0, 9);
|
|
313
|
+
const raw = (whole + padded).replace(/^0+/, '') || '0';
|
|
314
|
+
return raw;
|
|
315
|
+
}
|
|
316
|
+
// ── Router factory ───────────────────────────────────────────────────────────
|
|
317
|
+
function createLaunchRouter(connection, keypair) {
|
|
318
|
+
const router = (0, express_1.Router)();
|
|
319
|
+
// ── GET /api/launch/info ─────────────────────────────────────────────────
|
|
320
|
+
// Service info + pricing (free, no auth)
|
|
321
|
+
router.get('/info', (_req, res) => {
|
|
322
|
+
res.json({
|
|
323
|
+
ok: true,
|
|
324
|
+
data: {
|
|
325
|
+
service: 'Clodds Launch API',
|
|
326
|
+
version: '1.0.0',
|
|
327
|
+
description: 'One-call Solana token launches with bonding curves and automatic AMM graduation.',
|
|
328
|
+
feeSplit: '90/10 — creator keeps 90% of trading fees',
|
|
329
|
+
chain: 'Solana',
|
|
330
|
+
technology: 'Meteora Dynamic Bonding Curves → DAMM v2 AMM',
|
|
331
|
+
features: [
|
|
332
|
+
'One API call to launch a token',
|
|
333
|
+
'90/10 fee split (creator keeps 90%)',
|
|
334
|
+
'Anti-sniper fee protection (decaying high fees at launch)',
|
|
335
|
+
'Automatic AMM graduation at target market cap',
|
|
336
|
+
'Optional initial creator buy at launch with slippage protection',
|
|
337
|
+
'SPL and Token2022 support',
|
|
338
|
+
'Auto metadata upload (or bring your own URI)',
|
|
339
|
+
'Creator wallet support — fees go to your wallet',
|
|
340
|
+
'Configurable bonding curve parameters',
|
|
341
|
+
],
|
|
342
|
+
defaults: {
|
|
343
|
+
initialMarketCap: '30 SOL',
|
|
344
|
+
graduationMarketCap: '500 SOL',
|
|
345
|
+
totalSupply: '1,000,000,000',
|
|
346
|
+
decimals: 6,
|
|
347
|
+
creatorFeePercent: 90,
|
|
348
|
+
antiSniperFeeBps: 500,
|
|
349
|
+
endingFeeBps: 100,
|
|
350
|
+
feeDecayDuration: '1 hour',
|
|
351
|
+
slippageBps: 500,
|
|
352
|
+
},
|
|
353
|
+
pricing: {
|
|
354
|
+
launchFee: '$1.00 USDC (via x402)',
|
|
355
|
+
swapFee: '$0.10 USDC (via x402)',
|
|
356
|
+
claimFees: '$0.10 USDC (via x402)',
|
|
357
|
+
statusCheck: 'free',
|
|
358
|
+
quoteCheck: 'free',
|
|
359
|
+
},
|
|
360
|
+
exampleRequest: {
|
|
361
|
+
name: 'My Token',
|
|
362
|
+
symbol: 'MTK',
|
|
363
|
+
description: 'A token launched via Clodds',
|
|
364
|
+
imageUrl: 'https://example.com/logo.png',
|
|
365
|
+
creatorWallet: '<your-solana-pubkey>',
|
|
366
|
+
initialBuySol: 0.5,
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
});
|
|
370
|
+
});
|
|
371
|
+
// ── GET /api/launch/list ────────────────────────────────────────────────
|
|
372
|
+
// Public directory of all tokens launched via Clodds (free, no auth)
|
|
373
|
+
router.get('/list', (_req, res) => {
|
|
374
|
+
res.json({
|
|
375
|
+
ok: true,
|
|
376
|
+
data: {
|
|
377
|
+
total: launchRegistry.length,
|
|
378
|
+
launches: launchRegistry.slice().reverse(), // newest first
|
|
379
|
+
},
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
// ── POST /api/launch/token ───────────────────────────────────────────────
|
|
383
|
+
// Launch a token — requires a registered Clodds agent
|
|
384
|
+
router.post('/token', async (req, res) => {
|
|
385
|
+
// ── Agent gate: only registered Clodds agents can launch ──────────
|
|
386
|
+
const agentId = req.headers['x-agent-id'] ?? req.body?.agentId;
|
|
387
|
+
if (!agentId || typeof agentId !== 'string') {
|
|
388
|
+
res.status(401).json({
|
|
389
|
+
ok: false,
|
|
390
|
+
error: 'Launching requires a registered Clodds agent. Provide agentId in request body or X-Agent-Id header.',
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
let agentProfile;
|
|
395
|
+
try {
|
|
396
|
+
const registry = (0, registry_js_1.getRegistryService)();
|
|
397
|
+
agentProfile = await registry.getAgent(agentId);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
// Registry not initialized yet — fall through
|
|
401
|
+
}
|
|
402
|
+
if (!agentProfile || agentProfile.status !== 'active') {
|
|
403
|
+
res.status(403).json({
|
|
404
|
+
ok: false,
|
|
405
|
+
error: `Agent "${agentId}" is not a registered active Clodds agent. Register at /api/acp/agents first.`,
|
|
406
|
+
});
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const validation = validateLaunchRequest(req.body);
|
|
410
|
+
if (!validation.valid) {
|
|
411
|
+
res.status(400).json({ ok: false, error: validation.error });
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
const params = validation.params;
|
|
415
|
+
try {
|
|
416
|
+
// Step 1: Resolve metadata URI
|
|
417
|
+
let uri = params.uri;
|
|
418
|
+
if (!uri) {
|
|
419
|
+
logger_js_1.logger.info({ name: params.name, symbol: params.symbol }, 'Launch API: Uploading metadata');
|
|
420
|
+
uri = await uploadMetadata({
|
|
421
|
+
name: params.name,
|
|
422
|
+
symbol: params.symbol,
|
|
423
|
+
description: params.description,
|
|
424
|
+
imageUrl: params.imageUrl,
|
|
425
|
+
twitter: params.twitter,
|
|
426
|
+
telegram: params.telegram,
|
|
427
|
+
website: params.website,
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
// Step 2: Build bonding curve config
|
|
431
|
+
const creatorFee = params.creatorFeePercent ?? 90;
|
|
432
|
+
const graduationMcap = params.graduationMarketCap ?? 500;
|
|
433
|
+
const creatorWallet = params.creatorWallet ?? keypair.publicKey.toBase58();
|
|
434
|
+
const config = {
|
|
435
|
+
totalTokenSupply: params.totalSupply ?? 1_000_000_000,
|
|
436
|
+
tokenDecimals: params.decimals ?? 6,
|
|
437
|
+
initialMarketCap: params.initialMarketCap ?? 30,
|
|
438
|
+
migrationMarketCap: graduationMcap,
|
|
439
|
+
migrationOption: 1, // DAMM v2
|
|
440
|
+
startingFeeBps: params.antiSniperFeeBps ?? 500,
|
|
441
|
+
endingFeeBps: params.endingFeeBps ?? 100,
|
|
442
|
+
feeDecayPeriods: 10,
|
|
443
|
+
feeDecayDurationSec: params.feeDecayDurationSec ?? 3600,
|
|
444
|
+
dynamicFeeEnabled: true,
|
|
445
|
+
creatorTradingFeePercent: creatorFee,
|
|
446
|
+
creatorLiquidityPct: 5,
|
|
447
|
+
creatorLockedPct: 45,
|
|
448
|
+
partnerLiquidityPct: 0,
|
|
449
|
+
partnerLockedPct: 50,
|
|
450
|
+
tokenType: params.token2022 ? 1 : 0,
|
|
451
|
+
collectFeeMode: 0,
|
|
452
|
+
migrationFeeOption: 6,
|
|
453
|
+
migrationFeePercentage: 15,
|
|
454
|
+
creatorMigrationFeePercentage: 50,
|
|
455
|
+
// Route fees to the creator's wallet, not the server
|
|
456
|
+
feeClaimer: creatorWallet,
|
|
457
|
+
leftoverReceiver: creatorWallet,
|
|
458
|
+
};
|
|
459
|
+
// Step 3: Launch token
|
|
460
|
+
let result;
|
|
461
|
+
if (params.initialBuySol && params.initialBuySol > 0) {
|
|
462
|
+
const { createDbcPoolWithFirstBuy } = await Promise.resolve().then(() => __importStar(require('../solana/meteora-dbc.js')));
|
|
463
|
+
const buyLamports = solToLamports(params.initialBuySol);
|
|
464
|
+
const r = await createDbcPoolWithFirstBuy(connection, keypair, {
|
|
465
|
+
name: params.name,
|
|
466
|
+
symbol: params.symbol,
|
|
467
|
+
uri,
|
|
468
|
+
config,
|
|
469
|
+
buyAmountLamports: buyLamports,
|
|
470
|
+
});
|
|
471
|
+
result = { ...r, signatures: r.signatures };
|
|
472
|
+
}
|
|
473
|
+
else {
|
|
474
|
+
const { createDbcPool } = await Promise.resolve().then(() => __importStar(require('../solana/meteora-dbc.js')));
|
|
475
|
+
const r = await createDbcPool(connection, keypair, {
|
|
476
|
+
name: params.name,
|
|
477
|
+
symbol: params.symbol,
|
|
478
|
+
uri,
|
|
479
|
+
config,
|
|
480
|
+
});
|
|
481
|
+
result = { ...r, signatures: [r.signature] };
|
|
482
|
+
}
|
|
483
|
+
const response = {
|
|
484
|
+
mint: result.baseMint,
|
|
485
|
+
pool: result.poolAddress,
|
|
486
|
+
config: result.configAddress,
|
|
487
|
+
signatures: result.signatures,
|
|
488
|
+
explorer: `https://solscan.io/token/${result.baseMint}`,
|
|
489
|
+
feeSplit: `${creatorFee}/${100 - creatorFee} — creator keeps ${creatorFee}%`,
|
|
490
|
+
graduationMarketCap: graduationMcap,
|
|
491
|
+
creatorWallet,
|
|
492
|
+
};
|
|
493
|
+
// Record in public launch registry
|
|
494
|
+
const record = {
|
|
495
|
+
id: launchRegistry.length + 1,
|
|
496
|
+
mint: result.baseMint,
|
|
497
|
+
pool: result.poolAddress,
|
|
498
|
+
name: params.name,
|
|
499
|
+
symbol: params.symbol,
|
|
500
|
+
description: params.description,
|
|
501
|
+
creatorWallet,
|
|
502
|
+
agentId,
|
|
503
|
+
creatorFeePercent: creatorFee,
|
|
504
|
+
graduationMarketCap: graduationMcap,
|
|
505
|
+
launchedAt: new Date().toISOString(),
|
|
506
|
+
explorer: `https://solscan.io/token/${result.baseMint}`,
|
|
507
|
+
imageUrl: params.imageUrl,
|
|
508
|
+
website: params.website,
|
|
509
|
+
twitter: params.twitter,
|
|
510
|
+
telegram: params.telegram,
|
|
511
|
+
feeDelegates: [],
|
|
512
|
+
};
|
|
513
|
+
launchRegistry.push(record);
|
|
514
|
+
saveLaunches(launchRegistry);
|
|
515
|
+
logger_js_1.logger.info({
|
|
516
|
+
launchId: record.id,
|
|
517
|
+
mint: result.baseMint,
|
|
518
|
+
pool: result.poolAddress,
|
|
519
|
+
name: params.name,
|
|
520
|
+
symbol: params.symbol,
|
|
521
|
+
creatorFee,
|
|
522
|
+
creatorWallet,
|
|
523
|
+
}, 'Launch API: Token launched successfully');
|
|
524
|
+
res.json({ ok: true, data: { ...response, launchId: record.id } });
|
|
525
|
+
}
|
|
526
|
+
catch (err) {
|
|
527
|
+
logger_js_1.logger.warn({ err, name: params.name, symbol: params.symbol }, 'Launch API: Token launch failed');
|
|
528
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
529
|
+
res.status(500).json({ ok: false, error: message });
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
// ── GET /api/launch/status/:mint ─────────────────────────────────────────
|
|
533
|
+
// Check pool status, graduation progress, fees (free)
|
|
534
|
+
router.get('/status/:mint', async (req, res) => {
|
|
535
|
+
const { mint } = req.params;
|
|
536
|
+
if (!mint || typeof mint !== 'string') {
|
|
537
|
+
res.status(400).json({ ok: false, error: 'Required: mint address in URL' });
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
// Validate it looks like a base58 pubkey (32-44 chars, no special chars)
|
|
541
|
+
if (!/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(mint)) {
|
|
542
|
+
res.status(400).json({ ok: false, error: 'Invalid mint address format' });
|
|
543
|
+
return;
|
|
544
|
+
}
|
|
545
|
+
try {
|
|
546
|
+
const { getDbcPoolStatus } = await Promise.resolve().then(() => __importStar(require('../solana/meteora-dbc.js')));
|
|
547
|
+
const status = await getDbcPoolStatus(connection, mint);
|
|
548
|
+
if (!status.found) {
|
|
549
|
+
res.status(404).json({ ok: false, error: 'Pool not found for this mint' });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
res.json({
|
|
553
|
+
ok: true,
|
|
554
|
+
data: {
|
|
555
|
+
mint,
|
|
556
|
+
pool: status.poolAddress,
|
|
557
|
+
config: status.configAddress,
|
|
558
|
+
creator: status.creator,
|
|
559
|
+
graduated: status.isMigrated,
|
|
560
|
+
graduationProgress: `${status.progressPercent}%`,
|
|
561
|
+
quoteReserve: status.quoteReserve,
|
|
562
|
+
migrationThreshold: status.migrationThreshold,
|
|
563
|
+
fees: status.fees,
|
|
564
|
+
explorer: `https://solscan.io/token/${mint}`,
|
|
565
|
+
},
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
catch (err) {
|
|
569
|
+
logger_js_1.logger.warn({ err, mint }, 'Launch API: Status check failed');
|
|
570
|
+
res.status(500).json({ ok: false, error: 'Failed to fetch pool status' });
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
// ── GET /api/launch/quote/:pool ──────────────────────────────────────────
|
|
574
|
+
// Get a swap quote for a bonding curve pool (free)
|
|
575
|
+
router.get('/quote/:pool', async (req, res) => {
|
|
576
|
+
const { pool } = req.params;
|
|
577
|
+
const { amountIn, side } = req.query;
|
|
578
|
+
if (!pool || typeof pool !== 'string' || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(pool)) {
|
|
579
|
+
res.status(400).json({ ok: false, error: 'Required: valid pool address in URL' });
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
if (!amountIn || typeof amountIn !== 'string') {
|
|
583
|
+
res.status(400).json({ ok: false, error: 'Required: amountIn query param (lamports)' });
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
const parsedAmount = parseInt(amountIn, 10);
|
|
587
|
+
if (Number.isNaN(parsedAmount) || parsedAmount <= 0 || !Number.isFinite(parsedAmount)) {
|
|
588
|
+
res.status(400).json({ ok: false, error: 'amountIn must be a positive integer (lamports)' });
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
try {
|
|
592
|
+
const { getDbcSwapQuote } = await Promise.resolve().then(() => __importStar(require('../solana/meteora-dbc.js')));
|
|
593
|
+
const quote = await getDbcSwapQuote(connection, {
|
|
594
|
+
poolAddress: pool,
|
|
595
|
+
amountIn: String(parsedAmount),
|
|
596
|
+
swapBaseForQuote: side === 'sell',
|
|
597
|
+
});
|
|
598
|
+
res.json({ ok: true, data: quote });
|
|
599
|
+
}
|
|
600
|
+
catch (err) {
|
|
601
|
+
logger_js_1.logger.warn({ err, pool }, 'Launch API: Quote failed');
|
|
602
|
+
res.status(500).json({ ok: false, error: 'Failed to get quote' });
|
|
603
|
+
}
|
|
604
|
+
});
|
|
605
|
+
// ── POST /api/launch/swap ────────────────────────────────────────────────
|
|
606
|
+
// Buy or sell on a bonding curve pool
|
|
607
|
+
router.post('/swap', async (req, res) => {
|
|
608
|
+
const { pool: poolAddress, amountIn, side, slippageBps } = req.body;
|
|
609
|
+
if (!poolAddress || typeof poolAddress !== 'string' || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(poolAddress)) {
|
|
610
|
+
res.status(400).json({ ok: false, error: 'Required: pool (valid Solana address)' });
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
if (!amountIn || typeof amountIn !== 'string') {
|
|
614
|
+
res.status(400).json({ ok: false, error: 'Required: amountIn (string, lamports)' });
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
if (!side || (side !== 'buy' && side !== 'sell')) {
|
|
618
|
+
res.status(400).json({ ok: false, error: 'Required: side ("buy" or "sell")' });
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const parsedAmount = parseInt(amountIn, 10);
|
|
622
|
+
if (Number.isNaN(parsedAmount) || parsedAmount <= 0 || !Number.isFinite(parsedAmount)) {
|
|
623
|
+
res.status(400).json({ ok: false, error: 'amountIn must be a positive integer (lamports)' });
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
// Slippage protection: default 5% (500 bps), require quote first for zero-slippage
|
|
627
|
+
const slippage = (typeof slippageBps === 'number' && !Number.isNaN(slippageBps) && slippageBps >= 0)
|
|
628
|
+
? slippageBps
|
|
629
|
+
: 500;
|
|
630
|
+
try {
|
|
631
|
+
// Get quote first to calculate minimumAmountOut with slippage
|
|
632
|
+
const { getDbcSwapQuote, swapOnDbcPool } = await Promise.resolve().then(() => __importStar(require('../solana/meteora-dbc.js')));
|
|
633
|
+
const quote = await getDbcSwapQuote(connection, {
|
|
634
|
+
poolAddress,
|
|
635
|
+
amountIn: String(parsedAmount),
|
|
636
|
+
swapBaseForQuote: side === 'sell',
|
|
637
|
+
});
|
|
638
|
+
const expectedOut = BigInt(quote.amountOut || '0');
|
|
639
|
+
const minOut = expectedOut - (expectedOut * BigInt(slippage) / 10000n);
|
|
640
|
+
const minimumAmountOut = minOut > 0n ? minOut.toString() : '0';
|
|
641
|
+
const result = await swapOnDbcPool(connection, keypair, {
|
|
642
|
+
poolAddress,
|
|
643
|
+
amountIn: String(parsedAmount),
|
|
644
|
+
minimumAmountOut,
|
|
645
|
+
swapBaseForQuote: side === 'sell',
|
|
646
|
+
});
|
|
647
|
+
res.json({
|
|
648
|
+
ok: true,
|
|
649
|
+
data: {
|
|
650
|
+
signature: result.signature,
|
|
651
|
+
direction: result.direction,
|
|
652
|
+
expectedAmountOut: quote.amountOut,
|
|
653
|
+
minimumAmountOut,
|
|
654
|
+
slippageBps: slippage,
|
|
655
|
+
explorer: `https://solscan.io/tx/${result.signature}`,
|
|
656
|
+
},
|
|
657
|
+
});
|
|
658
|
+
}
|
|
659
|
+
catch (err) {
|
|
660
|
+
logger_js_1.logger.warn({ err, pool: poolAddress, side }, 'Launch API: Swap failed');
|
|
661
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
662
|
+
res.status(500).json({ ok: false, error: message });
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
// ── POST /api/launch/claim-fees ──────────────────────────────────────────
|
|
666
|
+
// Claim creator trading fees — only the launching agent or a fee delegate
|
|
667
|
+
router.post('/claim-fees', async (req, res) => {
|
|
668
|
+
const { pool: poolAddress } = req.body;
|
|
669
|
+
const callerAgentId = req.headers['x-agent-id'] ?? req.body?.agentId;
|
|
670
|
+
if (!poolAddress || typeof poolAddress !== 'string' || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(poolAddress)) {
|
|
671
|
+
res.status(400).json({ ok: false, error: 'Required: pool (valid Solana address)' });
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
// Find the launch record for this pool
|
|
675
|
+
const launch = launchRegistry.find((l) => l.pool === poolAddress);
|
|
676
|
+
if (launch) {
|
|
677
|
+
// Auth: only the creator agent or an authorized delegate can claim
|
|
678
|
+
if (!callerAgentId || typeof callerAgentId !== 'string') {
|
|
679
|
+
res.status(401).json({ ok: false, error: 'Provide agentId in request body or X-Agent-Id header to claim fees.' });
|
|
680
|
+
return;
|
|
681
|
+
}
|
|
682
|
+
const isCreator = launch.agentId === callerAgentId;
|
|
683
|
+
const isDelegate = launch.feeDelegates?.includes(callerAgentId) ?? false;
|
|
684
|
+
// Also check if the caller's wallet address matches a delegate wallet
|
|
685
|
+
let isDelegateByWallet = false;
|
|
686
|
+
if (!isCreator && !isDelegate) {
|
|
687
|
+
try {
|
|
688
|
+
const registry = (0, registry_js_1.getRegistryService)();
|
|
689
|
+
const callerAgent = await registry.getAgent(callerAgentId);
|
|
690
|
+
if (callerAgent && launch.feeDelegates?.includes(callerAgent.address)) {
|
|
691
|
+
isDelegateByWallet = true;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
catch { /* registry unavailable */ }
|
|
695
|
+
}
|
|
696
|
+
if (!isCreator && !isDelegate && !isDelegateByWallet) {
|
|
697
|
+
res.status(403).json({
|
|
698
|
+
ok: false,
|
|
699
|
+
error: `Only the creator agent (${launch.agentId}) or an authorized fee delegate can claim fees for this pool.`,
|
|
700
|
+
});
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
// If no launch record found (e.g. pre-registry launch), allow claim (backward compat)
|
|
705
|
+
try {
|
|
706
|
+
const { claimDbcCreatorFees } = await Promise.resolve().then(() => __importStar(require('../solana/meteora-dbc.js')));
|
|
707
|
+
const result = await claimDbcCreatorFees(connection, keypair, poolAddress);
|
|
708
|
+
res.json({
|
|
709
|
+
ok: true,
|
|
710
|
+
data: {
|
|
711
|
+
signature: result.signature,
|
|
712
|
+
explorer: `https://solscan.io/tx/${result.signature}`,
|
|
713
|
+
},
|
|
714
|
+
});
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
logger_js_1.logger.warn({ err, pool: poolAddress }, 'Launch API: Fee claim failed');
|
|
718
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
719
|
+
res.status(500).json({ ok: false, error: message });
|
|
720
|
+
}
|
|
721
|
+
});
|
|
722
|
+
// ── POST /api/launch/delegate ──────────────────────────────────────────
|
|
723
|
+
// Add or remove a fee delegate for a launched token
|
|
724
|
+
router.post('/delegate', async (req, res) => {
|
|
725
|
+
const { pool: poolAddress, delegateId, action } = req.body;
|
|
726
|
+
const callerAgentId = req.headers['x-agent-id'] ?? req.body?.agentId;
|
|
727
|
+
if (!poolAddress || typeof poolAddress !== 'string' || !/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(poolAddress)) {
|
|
728
|
+
res.status(400).json({ ok: false, error: 'Required: pool (valid Solana address)' });
|
|
729
|
+
return;
|
|
730
|
+
}
|
|
731
|
+
if (!delegateId || typeof delegateId !== 'string') {
|
|
732
|
+
res.status(400).json({ ok: false, error: 'Required: delegateId (agent ID or wallet address to delegate to)' });
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
735
|
+
if (!callerAgentId || typeof callerAgentId !== 'string') {
|
|
736
|
+
res.status(401).json({ ok: false, error: 'Provide agentId in request body or X-Agent-Id header.' });
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
const launch = launchRegistry.find((l) => l.pool === poolAddress);
|
|
740
|
+
if (!launch) {
|
|
741
|
+
res.status(404).json({ ok: false, error: 'No launch record found for this pool' });
|
|
742
|
+
return;
|
|
743
|
+
}
|
|
744
|
+
// Only the creator agent can manage delegates
|
|
745
|
+
if (launch.agentId !== callerAgentId) {
|
|
746
|
+
res.status(403).json({
|
|
747
|
+
ok: false,
|
|
748
|
+
error: `Only the creator agent (${launch.agentId}) can manage fee delegates.`,
|
|
749
|
+
});
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
752
|
+
if (!launch.feeDelegates)
|
|
753
|
+
launch.feeDelegates = [];
|
|
754
|
+
if (action === 'remove') {
|
|
755
|
+
launch.feeDelegates = launch.feeDelegates.filter((d) => d !== delegateId);
|
|
756
|
+
saveLaunches(launchRegistry);
|
|
757
|
+
logger_js_1.logger.info({ pool: poolAddress, delegateId, action }, 'Launch API: Fee delegate removed');
|
|
758
|
+
res.json({ ok: true, data: { pool: poolAddress, feeDelegates: launch.feeDelegates } });
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
// Default action: add
|
|
762
|
+
if (!launch.feeDelegates.includes(delegateId)) {
|
|
763
|
+
launch.feeDelegates.push(delegateId);
|
|
764
|
+
saveLaunches(launchRegistry);
|
|
765
|
+
logger_js_1.logger.info({ pool: poolAddress, delegateId, action: 'add' }, 'Launch API: Fee delegate added');
|
|
766
|
+
}
|
|
767
|
+
res.json({ ok: true, data: { pool: poolAddress, feeDelegates: launch.feeDelegates } });
|
|
768
|
+
});
|
|
769
|
+
logger_js_1.logger.info('Launch API routes initialized');
|
|
770
|
+
return router;
|
|
771
|
+
}
|
|
772
|
+
//# sourceMappingURL=launch-routes.js.map
|