solforge 0.2.0 → 0.2.2
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/LICENSE +21 -0
- package/docs/API.md +379 -0
- package/docs/CONFIGURATION.md +407 -0
- package/package.json +67 -45
- package/src/api-server-entry.ts +109 -0
- package/src/commands/add-program.ts +337 -0
- package/src/commands/init.ts +122 -0
- package/src/commands/list.ts +136 -0
- package/src/commands/mint.ts +288 -0
- package/src/commands/start.ts +877 -0
- package/src/commands/status.ts +99 -0
- package/src/commands/stop.ts +406 -0
- package/src/config/manager.ts +157 -0
- package/src/gui/public/build/main.css +1 -0
- package/src/gui/public/build/main.js +303 -0
- package/src/gui/public/build/main.js.txt +231 -0
- package/src/index.ts +188 -0
- package/src/services/api-server.ts +485 -0
- package/src/services/port-manager.ts +177 -0
- package/src/services/process-registry.ts +154 -0
- package/src/services/program-cloner.ts +317 -0
- package/src/services/token-cloner.ts +809 -0
- package/src/services/validator.ts +295 -0
- package/src/types/config.ts +110 -0
- package/src/utils/shell.ts +110 -0
- package/src/utils/token-loader.ts +115 -0
- package/.agi/agi.sqlite +0 -0
- package/.claude/settings.local.json +0 -9
- package/.github/workflows/release-binaries.yml +0 -133
- package/.tmp/.787ebcdbf7b8fde8-00000000.hm +0 -0
- package/.tmp/.bffe6efebdf8aedc-00000000.hm +0 -0
- package/AGENTS.md +0 -271
- package/CLAUDE.md +0 -106
- package/PROJECT_STRUCTURE.md +0 -124
- package/SOLANA_KIT_GUIDE.md +0 -251
- package/SOLFORGE.md +0 -119
- package/biome.json +0 -34
- package/bun.lock +0 -743
- package/drizzle/0000_friendly_millenium_guard.sql +0 -53
- package/drizzle/0001_stale_sentinels.sql +0 -2
- package/drizzle/meta/0000_snapshot.json +0 -329
- package/drizzle/meta/0001_snapshot.json +0 -345
- package/drizzle/meta/_journal.json +0 -20
- package/drizzle.config.ts +0 -12
- package/index.ts +0 -21
- package/mint.sh +0 -47
- package/postcss.config.js +0 -6
- package/rpc-server.ts.backup +0 -519
- package/sf.config.json +0 -38
- package/tailwind.config.js +0 -27
- package/test-client.ts +0 -120
- package/tmp/inspect-html.ts +0 -4
- package/tmp/response-test.ts +0 -5
- package/tmp/test-html.ts +0 -5
- package/tmp/test-server.ts +0 -13
- package/tsconfig.json +0 -29
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
import { spawn, ChildProcess } from "child_process";
|
|
2
|
+
import { existsSync } from "fs";
|
|
3
|
+
import { join } from "path";
|
|
4
|
+
import type {
|
|
5
|
+
ValidatorState,
|
|
6
|
+
ValidatorStatus,
|
|
7
|
+
LocalnetConfig,
|
|
8
|
+
OperationResult,
|
|
9
|
+
} from "../types/config.js";
|
|
10
|
+
|
|
11
|
+
export class ValidatorService {
|
|
12
|
+
private process: ChildProcess | null = null;
|
|
13
|
+
private state: ValidatorState;
|
|
14
|
+
private config: LocalnetConfig;
|
|
15
|
+
|
|
16
|
+
constructor(config: LocalnetConfig) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.state = {
|
|
19
|
+
status: "stopped",
|
|
20
|
+
port: config.port,
|
|
21
|
+
faucetPort: config.faucetPort,
|
|
22
|
+
rpcUrl: `http://${config.bindAddress}:${config.port}`,
|
|
23
|
+
wsUrl: `ws://${config.bindAddress}:${config.port}`,
|
|
24
|
+
logs: [],
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Start the validator with the given configuration
|
|
30
|
+
*/
|
|
31
|
+
async start(
|
|
32
|
+
programs: string[] = [],
|
|
33
|
+
tokens: string[] = []
|
|
34
|
+
): Promise<OperationResult<ValidatorState>> {
|
|
35
|
+
if (this.state.status === "running") {
|
|
36
|
+
return {
|
|
37
|
+
success: false,
|
|
38
|
+
error: "Validator is already running",
|
|
39
|
+
data: this.state,
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
this.updateStatus("starting");
|
|
45
|
+
|
|
46
|
+
const args = this.buildValidatorArgs(programs, tokens);
|
|
47
|
+
|
|
48
|
+
this.process = spawn("solana-test-validator", args, {
|
|
49
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
50
|
+
detached: false,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
this.state.pid = this.process.pid;
|
|
54
|
+
this.state.startTime = new Date();
|
|
55
|
+
|
|
56
|
+
// Handle process events
|
|
57
|
+
this.setupProcessHandlers();
|
|
58
|
+
|
|
59
|
+
// Wait for validator to be ready
|
|
60
|
+
await this.waitForReady();
|
|
61
|
+
|
|
62
|
+
this.updateStatus("running");
|
|
63
|
+
|
|
64
|
+
return {
|
|
65
|
+
success: true,
|
|
66
|
+
data: this.state,
|
|
67
|
+
};
|
|
68
|
+
} catch (error) {
|
|
69
|
+
this.updateStatus("error");
|
|
70
|
+
this.state.error = error instanceof Error ? error.message : String(error);
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
success: false,
|
|
74
|
+
error: this.state.error,
|
|
75
|
+
data: this.state,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Stop the validator
|
|
82
|
+
*/
|
|
83
|
+
async stop(): Promise<OperationResult<ValidatorState>> {
|
|
84
|
+
if (this.state.status === "stopped") {
|
|
85
|
+
return {
|
|
86
|
+
success: true,
|
|
87
|
+
data: this.state,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
this.updateStatus("stopping");
|
|
93
|
+
|
|
94
|
+
if (this.process) {
|
|
95
|
+
this.process.kill("SIGTERM");
|
|
96
|
+
|
|
97
|
+
// Wait for graceful shutdown
|
|
98
|
+
await new Promise<void>((resolve) => {
|
|
99
|
+
const timeout = setTimeout(() => {
|
|
100
|
+
if (this.process) {
|
|
101
|
+
this.process.kill("SIGKILL");
|
|
102
|
+
}
|
|
103
|
+
resolve();
|
|
104
|
+
}, 5000);
|
|
105
|
+
|
|
106
|
+
this.process?.on("exit", () => {
|
|
107
|
+
clearTimeout(timeout);
|
|
108
|
+
resolve();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
this.cleanup();
|
|
114
|
+
this.updateStatus("stopped");
|
|
115
|
+
|
|
116
|
+
return {
|
|
117
|
+
success: true,
|
|
118
|
+
data: this.state,
|
|
119
|
+
};
|
|
120
|
+
} catch (error) {
|
|
121
|
+
return {
|
|
122
|
+
success: false,
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
data: this.state,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Get current validator state
|
|
131
|
+
*/
|
|
132
|
+
getState(): ValidatorState {
|
|
133
|
+
return { ...this.state };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Check if validator is running
|
|
138
|
+
*/
|
|
139
|
+
isRunning(): boolean {
|
|
140
|
+
return this.state.status === "running";
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Get recent logs
|
|
145
|
+
*/
|
|
146
|
+
getLogs(count = 100): string[] {
|
|
147
|
+
return this.state.logs.slice(-count);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Build validator arguments based on configuration
|
|
152
|
+
*/
|
|
153
|
+
private buildValidatorArgs(
|
|
154
|
+
programs: string[] = [],
|
|
155
|
+
tokens: string[] = []
|
|
156
|
+
): string[] {
|
|
157
|
+
const args: string[] = [];
|
|
158
|
+
|
|
159
|
+
// Basic configuration
|
|
160
|
+
args.push("--rpc-port", this.config.port.toString());
|
|
161
|
+
args.push("--faucet-port", this.config.faucetPort.toString());
|
|
162
|
+
args.push("--bind-address", this.config.bindAddress);
|
|
163
|
+
|
|
164
|
+
if (this.config.reset) {
|
|
165
|
+
args.push("--reset");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (this.config.quiet) {
|
|
169
|
+
args.push("--quiet");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (this.config.ledgerPath) {
|
|
173
|
+
args.push("--ledger", this.config.ledgerPath);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Set log level
|
|
177
|
+
args.push("--log");
|
|
178
|
+
|
|
179
|
+
// Clone programs
|
|
180
|
+
for (const programId of programs) {
|
|
181
|
+
args.push("--clone", programId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Clone tokens (these would be mint addresses)
|
|
185
|
+
for (const tokenMint of tokens) {
|
|
186
|
+
args.push("--clone", tokenMint);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Always specify mainnet as the source for cloning
|
|
190
|
+
if (programs.length > 0 || tokens.length > 0) {
|
|
191
|
+
args.push("--url", "https://api.mainnet-beta.solana.com");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return args;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Setup process event handlers
|
|
199
|
+
*/
|
|
200
|
+
private setupProcessHandlers(): void {
|
|
201
|
+
if (!this.process) return;
|
|
202
|
+
|
|
203
|
+
this.process.stdout?.on("data", (data: Buffer) => {
|
|
204
|
+
const log = data.toString().trim();
|
|
205
|
+
this.addLog(`[STDOUT] ${log}`);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
this.process.stderr?.on("data", (data: Buffer) => {
|
|
209
|
+
const log = data.toString().trim();
|
|
210
|
+
this.addLog(`[STDERR] ${log}`);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
this.process.on("error", (error) => {
|
|
214
|
+
this.addLog(`[ERROR] ${error.message}`);
|
|
215
|
+
this.updateStatus("error");
|
|
216
|
+
this.state.error = error.message;
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
this.process.on("exit", (code, signal) => {
|
|
220
|
+
this.addLog(`[EXIT] Process exited with code ${code}, signal ${signal}`);
|
|
221
|
+
this.cleanup();
|
|
222
|
+
|
|
223
|
+
if (this.state.status !== "stopping") {
|
|
224
|
+
this.updateStatus(code === 0 ? "stopped" : "error");
|
|
225
|
+
if (code !== 0) {
|
|
226
|
+
this.state.error = `Process exited with code ${code}`;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Wait for validator to be ready
|
|
234
|
+
*/
|
|
235
|
+
private async waitForReady(timeout = 30000): Promise<void> {
|
|
236
|
+
const startTime = Date.now();
|
|
237
|
+
|
|
238
|
+
while (Date.now() - startTime < timeout) {
|
|
239
|
+
try {
|
|
240
|
+
// Try to connect to the RPC endpoint
|
|
241
|
+
const response = await fetch(this.state.rpcUrl, {
|
|
242
|
+
method: "POST",
|
|
243
|
+
headers: { "Content-Type": "application/json" },
|
|
244
|
+
body: JSON.stringify({
|
|
245
|
+
jsonrpc: "2.0",
|
|
246
|
+
id: 1,
|
|
247
|
+
method: "getHealth",
|
|
248
|
+
}),
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
if (response.ok) {
|
|
252
|
+
return; // Validator is ready
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
// Continue waiting
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
throw new Error("Validator failed to start within timeout period");
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Update validator status
|
|
266
|
+
*/
|
|
267
|
+
private updateStatus(status: ValidatorStatus): void {
|
|
268
|
+
this.state.status = status;
|
|
269
|
+
this.addLog(`[STATUS] Validator status changed to: ${status}`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Add log entry
|
|
274
|
+
*/
|
|
275
|
+
private addLog(message: string): void {
|
|
276
|
+
const timestamp = new Date().toISOString();
|
|
277
|
+
const logEntry = `[${timestamp}] ${message}`;
|
|
278
|
+
this.state.logs.push(logEntry);
|
|
279
|
+
|
|
280
|
+
// Keep only last 1000 log entries
|
|
281
|
+
if (this.state.logs.length > 1000) {
|
|
282
|
+
this.state.logs = this.state.logs.slice(-1000);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
/**
|
|
287
|
+
* Clean up process references
|
|
288
|
+
*/
|
|
289
|
+
private cleanup(): void {
|
|
290
|
+
this.process = null;
|
|
291
|
+
this.state.pid = undefined;
|
|
292
|
+
this.state.startTime = undefined;
|
|
293
|
+
this.state.error = undefined;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
// Token configuration schema
|
|
4
|
+
export const TokenConfigSchema = z.object({
|
|
5
|
+
symbol: z.string().min(1, "Token symbol is required"),
|
|
6
|
+
mainnetMint: z.string().min(1, "Mainnet mint address is required"),
|
|
7
|
+
mintAuthority: z.string().optional(), // Path to keypair file
|
|
8
|
+
mintAmount: z
|
|
9
|
+
.number()
|
|
10
|
+
.positive("Mint amount must be positive")
|
|
11
|
+
.default(1000000), // Amount to mint to mint authority
|
|
12
|
+
recipients: z
|
|
13
|
+
.array(
|
|
14
|
+
z.object({
|
|
15
|
+
wallet: z.string().min(1, "Wallet address is required"),
|
|
16
|
+
amount: z.number().positive("Amount must be positive"),
|
|
17
|
+
})
|
|
18
|
+
)
|
|
19
|
+
.default([]),
|
|
20
|
+
cloneMetadata: z.boolean().default(true), // Whether to clone token metadata
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Program configuration schema
|
|
24
|
+
export const ProgramConfigSchema = z.object({
|
|
25
|
+
name: z.string().optional(),
|
|
26
|
+
mainnetProgramId: z.string().min(1, "Program ID is required"),
|
|
27
|
+
deployPath: z.string().optional(), // Optional path to local .so file
|
|
28
|
+
upgradeable: z.boolean().default(false),
|
|
29
|
+
cluster: z
|
|
30
|
+
.enum(["mainnet-beta", "devnet", "testnet"])
|
|
31
|
+
.default("mainnet-beta"),
|
|
32
|
+
dependencies: z.array(z.string()).default([]), // Other program IDs this program depends on
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// Localnet configuration schema
|
|
36
|
+
export const LocalnetConfigSchema = z.object({
|
|
37
|
+
airdropAmount: z.number().positive().default(100),
|
|
38
|
+
faucetAccounts: z.array(z.string()).default([]),
|
|
39
|
+
logLevel: z.enum(["trace", "debug", "info", "warn", "error"]).default("info"),
|
|
40
|
+
port: z.number().int().min(1000).max(65535).default(8899),
|
|
41
|
+
faucetPort: z.number().int().min(1000).max(65535).default(9900),
|
|
42
|
+
reset: z.boolean().default(false),
|
|
43
|
+
quiet: z.boolean().default(false),
|
|
44
|
+
ledgerPath: z.string().optional(),
|
|
45
|
+
bindAddress: z.string().default("127.0.0.1"),
|
|
46
|
+
limitLedgerSize: z.number().int().positive().default(100000),
|
|
47
|
+
rpc: z
|
|
48
|
+
.string()
|
|
49
|
+
.url("RPC must be a valid URL")
|
|
50
|
+
.default("https://api.mainnet-beta.solana.com"),
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Complete configuration schema
|
|
54
|
+
export const ConfigSchema = z.object({
|
|
55
|
+
name: z.string().default("solforge-localnet"),
|
|
56
|
+
description: z.string().optional(),
|
|
57
|
+
tokens: z.array(TokenConfigSchema).default([]),
|
|
58
|
+
programs: z.array(ProgramConfigSchema).default([]),
|
|
59
|
+
localnet: LocalnetConfigSchema.default({}),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Inferred TypeScript types
|
|
63
|
+
export type TokenConfig = z.infer<typeof TokenConfigSchema>;
|
|
64
|
+
export type ProgramConfig = z.infer<typeof ProgramConfigSchema>;
|
|
65
|
+
export type LocalnetConfig = z.infer<typeof LocalnetConfigSchema>;
|
|
66
|
+
export type Config = z.infer<typeof ConfigSchema>;
|
|
67
|
+
|
|
68
|
+
// Validator status types
|
|
69
|
+
export type ValidatorStatus =
|
|
70
|
+
| "stopped"
|
|
71
|
+
| "starting"
|
|
72
|
+
| "running"
|
|
73
|
+
| "stopping"
|
|
74
|
+
| "error";
|
|
75
|
+
|
|
76
|
+
export interface ValidatorState {
|
|
77
|
+
status: ValidatorStatus;
|
|
78
|
+
pid?: number;
|
|
79
|
+
port: number;
|
|
80
|
+
faucetPort: number;
|
|
81
|
+
rpcUrl: string;
|
|
82
|
+
wsUrl: string;
|
|
83
|
+
startTime?: Date;
|
|
84
|
+
logs: string[];
|
|
85
|
+
error?: string;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Operation result types
|
|
89
|
+
export interface OperationResult<T = any> {
|
|
90
|
+
success: boolean;
|
|
91
|
+
data?: T;
|
|
92
|
+
error?: string;
|
|
93
|
+
details?: any;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface CloneResult {
|
|
97
|
+
type: "program" | "token";
|
|
98
|
+
id: string;
|
|
99
|
+
name?: string;
|
|
100
|
+
success: boolean;
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ValidationResult {
|
|
105
|
+
valid: boolean;
|
|
106
|
+
errors: Array<{
|
|
107
|
+
path: string;
|
|
108
|
+
message: string;
|
|
109
|
+
}>;
|
|
110
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { $ } from "bun";
|
|
2
|
+
import chalk from "chalk";
|
|
3
|
+
|
|
4
|
+
export interface CommandResult {
|
|
5
|
+
success: boolean;
|
|
6
|
+
stdout: string;
|
|
7
|
+
stderr: string;
|
|
8
|
+
exitCode: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Execute a shell command and return the result
|
|
13
|
+
*/
|
|
14
|
+
export async function runCommand(
|
|
15
|
+
command: string,
|
|
16
|
+
args: string[] = [],
|
|
17
|
+
options: {
|
|
18
|
+
silent?: boolean;
|
|
19
|
+
jsonOutput?: boolean;
|
|
20
|
+
debug?: boolean;
|
|
21
|
+
} = {}
|
|
22
|
+
): Promise<CommandResult> {
|
|
23
|
+
const { silent = false, jsonOutput = false, debug = false } = options;
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
if (!silent || debug) {
|
|
27
|
+
console.log(chalk.gray(`$ ${command} ${args.join(" ")}`));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await $`${command} ${args}`.quiet();
|
|
31
|
+
|
|
32
|
+
const stdout = result.stdout.toString();
|
|
33
|
+
const stderr = result.stderr.toString();
|
|
34
|
+
const exitCode = result.exitCode;
|
|
35
|
+
const success = exitCode === 0;
|
|
36
|
+
|
|
37
|
+
if (debug) {
|
|
38
|
+
console.log(chalk.gray(`Exit code: ${exitCode}`));
|
|
39
|
+
if (stdout) {
|
|
40
|
+
console.log(chalk.gray(`Stdout: ${stdout}`));
|
|
41
|
+
}
|
|
42
|
+
if (stderr) {
|
|
43
|
+
console.log(chalk.gray(`Stderr: ${stderr}`));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!silent && !success) {
|
|
48
|
+
console.error(chalk.red(`Command failed with exit code ${exitCode}`));
|
|
49
|
+
if (stderr) {
|
|
50
|
+
console.error(chalk.red(`Error: ${stderr}`));
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// If JSON output is expected, try to parse it
|
|
55
|
+
let parsedOutput = stdout;
|
|
56
|
+
if (jsonOutput && success && stdout.trim()) {
|
|
57
|
+
try {
|
|
58
|
+
parsedOutput = JSON.parse(stdout);
|
|
59
|
+
} catch (e) {
|
|
60
|
+
// If JSON parsing fails, keep original stdout
|
|
61
|
+
console.warn(
|
|
62
|
+
chalk.yellow("Warning: Expected JSON output but got invalid JSON")
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
success,
|
|
69
|
+
stdout: parsedOutput,
|
|
70
|
+
stderr,
|
|
71
|
+
exitCode,
|
|
72
|
+
};
|
|
73
|
+
} catch (error) {
|
|
74
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
75
|
+
|
|
76
|
+
if (!silent) {
|
|
77
|
+
console.error(chalk.red(`Command execution failed: ${errorMessage}`));
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
stdout: "",
|
|
83
|
+
stderr: errorMessage,
|
|
84
|
+
exitCode: 1,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Check if a command exists in PATH
|
|
91
|
+
*/
|
|
92
|
+
export async function commandExists(command: string): Promise<boolean> {
|
|
93
|
+
const result = await runCommand("which", [command], { silent: true });
|
|
94
|
+
return result.success;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Check if solana CLI tools are available
|
|
99
|
+
*/
|
|
100
|
+
export async function checkSolanaTools(): Promise<{
|
|
101
|
+
solana: boolean;
|
|
102
|
+
splToken: boolean;
|
|
103
|
+
}> {
|
|
104
|
+
const [solana, splToken] = await Promise.all([
|
|
105
|
+
commandExists("solana"),
|
|
106
|
+
commandExists("spl-token"),
|
|
107
|
+
]);
|
|
108
|
+
|
|
109
|
+
return { solana, splToken };
|
|
110
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
import { Keypair } from "@solana/web3.js";
|
|
4
|
+
import type { TokenConfig } from "../types/config.js";
|
|
5
|
+
|
|
6
|
+
export interface ClonedToken {
|
|
7
|
+
config: TokenConfig;
|
|
8
|
+
mintAuthorityPath: string;
|
|
9
|
+
modifiedAccountPath: string;
|
|
10
|
+
metadataAccountPath?: string;
|
|
11
|
+
mintAuthority: {
|
|
12
|
+
publicKey: string;
|
|
13
|
+
secretKey: number[];
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Shared utility to load cloned tokens from the work directory.
|
|
19
|
+
* This ensures consistent token loading across CLI and API server.
|
|
20
|
+
*/
|
|
21
|
+
export async function loadClonedTokens(
|
|
22
|
+
tokenConfigs: TokenConfig[],
|
|
23
|
+
workDir: string = ".solforge"
|
|
24
|
+
): Promise<ClonedToken[]> {
|
|
25
|
+
const clonedTokens: ClonedToken[] = [];
|
|
26
|
+
|
|
27
|
+
// Load shared mint authority
|
|
28
|
+
const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
|
|
29
|
+
let sharedMintAuthority: { publicKey: string; secretKey: number[] } | null =
|
|
30
|
+
null;
|
|
31
|
+
|
|
32
|
+
if (existsSync(sharedMintAuthorityPath)) {
|
|
33
|
+
try {
|
|
34
|
+
const fileContent = JSON.parse(
|
|
35
|
+
readFileSync(sharedMintAuthorityPath, "utf8")
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
if (Array.isArray(fileContent)) {
|
|
39
|
+
// New format: file contains just the secret key array
|
|
40
|
+
const keypair = Keypair.fromSecretKey(new Uint8Array(fileContent));
|
|
41
|
+
sharedMintAuthority = {
|
|
42
|
+
publicKey: keypair.publicKey.toBase58(),
|
|
43
|
+
secretKey: Array.from(keypair.secretKey),
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
// Check metadata for consistency
|
|
47
|
+
const metadataPath = join(workDir, "shared-mint-authority-meta.json");
|
|
48
|
+
if (existsSync(metadataPath)) {
|
|
49
|
+
const metadata = JSON.parse(readFileSync(metadataPath, "utf8"));
|
|
50
|
+
if (metadata.publicKey !== sharedMintAuthority.publicKey) {
|
|
51
|
+
sharedMintAuthority.publicKey = metadata.publicKey;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
} else {
|
|
55
|
+
// Old format: file contains {publicKey, secretKey}
|
|
56
|
+
sharedMintAuthority = fileContent;
|
|
57
|
+
}
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.error("Failed to load shared mint authority:", error);
|
|
60
|
+
return [];
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!sharedMintAuthority) {
|
|
65
|
+
return [];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Load each token that has been cloned
|
|
69
|
+
for (const tokenConfig of tokenConfigs) {
|
|
70
|
+
const tokenDir = join(workDir, `token-${tokenConfig.symbol.toLowerCase()}`);
|
|
71
|
+
const modifiedAccountPath = join(tokenDir, "modified.json");
|
|
72
|
+
const metadataAccountPath = join(tokenDir, "metadata.json");
|
|
73
|
+
|
|
74
|
+
// Check if this token has already been cloned
|
|
75
|
+
if (existsSync(modifiedAccountPath)) {
|
|
76
|
+
const clonedToken: ClonedToken = {
|
|
77
|
+
config: tokenConfig,
|
|
78
|
+
mintAuthorityPath: sharedMintAuthorityPath,
|
|
79
|
+
modifiedAccountPath,
|
|
80
|
+
mintAuthority: sharedMintAuthority,
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// Add metadata path if it exists
|
|
84
|
+
if (existsSync(metadataAccountPath)) {
|
|
85
|
+
clonedToken.metadataAccountPath = metadataAccountPath;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
clonedTokens.push(clonedToken);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return clonedTokens;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Find a cloned token by its mint address
|
|
97
|
+
*/
|
|
98
|
+
export function findTokenByMint(
|
|
99
|
+
clonedTokens: ClonedToken[],
|
|
100
|
+
mintAddress: string
|
|
101
|
+
): ClonedToken | undefined {
|
|
102
|
+
return clonedTokens.find((token) => token.config.mainnetMint === mintAddress);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Find a cloned token by its symbol
|
|
107
|
+
*/
|
|
108
|
+
export function findTokenBySymbol(
|
|
109
|
+
clonedTokens: ClonedToken[],
|
|
110
|
+
symbol: string
|
|
111
|
+
): ClonedToken | undefined {
|
|
112
|
+
return clonedTokens.find(
|
|
113
|
+
(token) => token.config.symbol.toLowerCase() === symbol.toLowerCase()
|
|
114
|
+
);
|
|
115
|
+
}
|
package/.agi/agi.sqlite
DELETED
|
Binary file
|