solforge 0.1.4 → 0.1.6

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 CHANGED
@@ -1,5 +1,8 @@
1
1
  # SolForge
2
2
 
3
+ [![npm version](https://badge.fury.io/js/solforge.svg)](https://badge.fury.io/js/solforge)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
3
6
  **SolForge** is a powerful Solana localnet orchestration tool that simplifies the process of setting up and managing local Solana development environments. It allows you to clone mainnet programs and tokens to your local validator, making it easy to develop and test Solana applications with real-world data.
4
7
 
5
8
  ## ✨ Features
@@ -15,15 +18,27 @@
15
18
  - 🎨 **Beautiful CLI** - Colorful, intuitive command-line interface
16
19
  - 🌐 **REST API** - Background API server for programmatic access to validator operations
17
20
 
18
- ## 🚀 Quick Start
21
+ ## 📦 Installation
19
22
 
20
23
  ### Prerequisites
21
24
 
22
- - [Bun](https://bun.sh) runtime
23
25
  - [Solana CLI tools](https://docs.solana.com/cli/install-solana-cli-tools) installed and configured
24
- - Node.js 18+ (for compatibility)
26
+ - Node.js 18+ or [Bun](https://bun.sh) runtime
25
27
 
26
- ### Installation
28
+ ### Install from npm (Recommended)
29
+
30
+ ```bash
31
+ # Install globally with npm
32
+ npm install -g solforge
33
+
34
+ # Or with bun
35
+ bun install -g solforge
36
+
37
+ # Or with yarn
38
+ yarn global add solforge
39
+ ```
40
+
41
+ ### Install from Source
27
42
 
28
43
  ```bash
29
44
  # Clone the repository
@@ -33,14 +48,20 @@ cd solforge
33
48
  # Install dependencies
34
49
  bun install
35
50
 
36
- # Build the project
37
- bun run build
51
+ # Build and install globally
52
+ bun run build:npm
53
+ npm install -g .
54
+ ```
38
55
 
39
- # Install globally (optional)
40
- bun run build:binary
41
- bun run install:binary
56
+ ### Verify Installation
57
+
58
+ ```bash
59
+ solforge --version
60
+ solforge --help
42
61
  ```
43
62
 
63
+ ## 🚀 Quick Start
64
+
44
65
  ### Basic Usage
45
66
 
46
67
  1. **Initialize a new project**:
@@ -78,8 +99,8 @@ solforge start
78
99
  # API available at http://127.0.0.1:3000/api
79
100
  curl http://127.0.0.1:3000/api/health
80
101
 
81
- # Mint tokens via API
82
- curl -X POST http://127.0.0.1:3000/api/tokens/USDC/mint \
102
+ # Mint tokens via API (using mint address)
103
+ curl -X POST http://127.0.0.1:3000/api/tokens/EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v/mint \
83
104
  -H "Content-Type: application/json" \
84
105
  -d '{"walletAddress": "YOUR_WALLET_ADDRESS", "amount": 1000}'
85
106
 
@@ -121,11 +142,14 @@ Start a localnet validator with the current configuration.
121
142
  ```bash
122
143
  solforge start # Start with default settings
123
144
  solforge start --debug # Start with debug logging
145
+ solforge start --network # Make API server accessible over network
146
+ solforge start --debug --network # Debug mode + network access
124
147
  ```
125
148
 
126
149
  **Options:**
127
150
 
128
151
  - `--debug` - Enable debug logging to see detailed command output
152
+ - `--network` - Make API server accessible over network (binds to 0.0.0.0 instead of 127.0.0.1)
129
153
 
130
154
  #### `solforge list`
131
155
 
@@ -228,6 +252,25 @@ When run without arguments, `solforge mint` will:
228
252
  - Prompt for amount to mint
229
253
  - Handle SPL token account creation automatically
230
254
 
255
+ #### `solforge api-server [options]`
256
+
257
+ Start the API server as a standalone service (without validator).
258
+
259
+ ```bash
260
+ solforge api-server # Start on default port 3000
261
+ solforge api-server --port 8080 # Custom port
262
+ solforge api-server --host 0.0.0.0 # Network accessible
263
+ solforge api-server --rpc-url http://localhost:8899 # Custom RPC
264
+ ```
265
+
266
+ **Options:**
267
+
268
+ - `-p, --port <port>` - Port for API server (default: 3000)
269
+ - `--host <host>` - Host to bind to (default: 127.0.0.1, use 0.0.0.0 for network access)
270
+ - `--rpc-url <url>` - Validator RPC URL (default: http://127.0.0.1:8899)
271
+ - `--faucet-url <url>` - Validator faucet URL (default: http://127.0.0.1:9900)
272
+ - `--work-dir <dir>` - Work directory (default: ./.solforge)
273
+
231
274
  #### `solforge reset`
232
275
 
233
276
  Reset localnet ledger (coming soon).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solforge",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "description": "Solana localnet orchestration tool for cloning mainnet programs and tokens",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -9,6 +9,7 @@ async function main() {
9
9
  // Parse command line arguments
10
10
  const args = process.argv.slice(2);
11
11
  const portIndex = args.indexOf("--port");
12
+ const hostIndex = args.indexOf("--host");
12
13
  const configIndex = args.indexOf("--config");
13
14
  const rpcIndex = args.indexOf("--rpc-url");
14
15
  const faucetIndex = args.indexOf("--faucet-url");
@@ -27,12 +28,14 @@ async function main() {
27
28
  !args[workDirIndex + 1]
28
29
  ) {
29
30
  console.error(
30
- "Usage: api-server-entry --port <port> --config <config-path> --rpc-url <url> --faucet-url <url> --work-dir <dir>"
31
+ "Usage: api-server-entry --port <port> --config <config-path> --rpc-url <url> --faucet-url <url> --work-dir <dir> [--host <host>]"
31
32
  );
32
33
  process.exit(1);
33
34
  }
34
35
 
35
36
  const port = parseInt(args[portIndex + 1]!);
37
+ const host =
38
+ hostIndex !== -1 && args[hostIndex + 1] ? args[hostIndex + 1] : undefined;
36
39
  const configPath = args[configIndex + 1]!;
37
40
  const rpcUrl = args[rpcIndex + 1]!;
38
41
  const faucetUrl = args[faucetIndex + 1]!;
@@ -45,6 +48,7 @@ async function main() {
45
48
  // Create and start API server
46
49
  const apiServer = new APIServer({
47
50
  port,
51
+ host,
48
52
  validatorRpcUrl: rpcUrl,
49
53
  validatorFaucetUrl: faucetUrl,
50
54
  config,
@@ -21,7 +21,10 @@ function generateValidatorId(name: string): string {
21
21
  return `${safeName}-${timestamp}-${randomSuffix}`;
22
22
  }
23
23
 
24
- export async function startCommand(debug: boolean = false): Promise<void> {
24
+ export async function startCommand(
25
+ debug: boolean = false,
26
+ network: boolean = false
27
+ ): Promise<void> {
25
28
  // Check prerequisites
26
29
  const tools = await checkSolanaTools();
27
30
  if (!tools.solana) {
@@ -360,7 +363,8 @@ export async function startCommand(debug: boolean = false): Promise<void> {
360
363
  const workDir = join(currentDir, ".solforge");
361
364
 
362
365
  // Start API server in background using runCommand with nohup
363
- const apiServerCommand = `nohup bun run "${apiServerScript}" --port ${apiServerPort} --config "${configPath}" --rpc-url "http://127.0.0.1:${config.localnet.port}" --faucet-url "http://127.0.0.1:${config.localnet.faucetPort}" --work-dir "${workDir}" > /dev/null 2>&1 &`;
366
+ const hostFlag = network ? ` --host "0.0.0.0"` : "";
367
+ const apiServerCommand = `nohup bun run "${apiServerScript}" --port ${apiServerPort} --config "${configPath}" --rpc-url "http://127.0.0.1:${config.localnet.port}" --faucet-url "http://127.0.0.1:${config.localnet.faucetPort}" --work-dir "${workDir}"${hostFlag} > /dev/null 2>&1 &`;
364
368
 
365
369
  const startResult = await runCommand("sh", ["-c", apiServerCommand], {
366
370
  silent: !debug,
@@ -373,8 +377,9 @@ export async function startCommand(debug: boolean = false): Promise<void> {
373
377
 
374
378
  // Test if the API server is responding
375
379
  try {
380
+ const healthCheckHost = network ? "0.0.0.0" : "127.0.0.1";
376
381
  const response = await fetch(
377
- `http://127.0.0.1:${apiServerPort}/api/health`
382
+ `http://${healthCheckHost}:${apiServerPort}/api/health`
378
383
  );
379
384
  if (response.ok) {
380
385
  apiResult = { success: true };
@@ -385,7 +390,10 @@ export async function startCommand(debug: boolean = false): Promise<void> {
385
390
  { silent: true, debug: false }
386
391
  );
387
392
  if (pidResult.success && pidResult.stdout.trim()) {
388
- apiServerPid = parseInt(pidResult.stdout.trim().split("\n")[0]);
393
+ const pidLine = pidResult.stdout.trim().split("\n")[0];
394
+ if (pidLine) {
395
+ apiServerPid = parseInt(pidLine);
396
+ }
389
397
  }
390
398
  } else {
391
399
  apiResult = {
@@ -436,7 +444,7 @@ export async function startCommand(debug: boolean = false): Promise<void> {
436
444
  status: "running",
437
445
  apiServerPort: apiResult.success ? apiServerPort : undefined,
438
446
  apiServerUrl: apiResult.success
439
- ? `http://127.0.0.1:${apiServerPort}`
447
+ ? `http://${network ? "0.0.0.0" : "127.0.0.1"}:${apiServerPort}`
440
448
  : undefined,
441
449
  apiServerPid: apiResult.success ? apiServerPid : undefined,
442
450
  };
@@ -457,9 +465,17 @@ export async function startCommand(debug: boolean = false): Promise<void> {
457
465
  )
458
466
  );
459
467
  if (apiResult.success) {
468
+ const displayHost = network ? "0.0.0.0" : "127.0.0.1";
460
469
  console.log(
461
- chalk.cyan(`🚀 API Server: http://127.0.0.1:${apiServerPort}/api`)
470
+ chalk.cyan(`🚀 API Server: http://${displayHost}:${apiServerPort}/api`)
462
471
  );
472
+ if (network) {
473
+ console.log(
474
+ chalk.yellow(
475
+ " 🌐 Network mode enabled - API server accessible from other devices"
476
+ )
477
+ );
478
+ }
463
479
  }
464
480
 
465
481
  // Airdrop SOL to mint authority if tokens were cloned
@@ -563,30 +579,31 @@ export async function startCommand(debug: boolean = false): Promise<void> {
563
579
  chalk.gray(" - Run `solforge stop --all` to stop all validators")
564
580
  );
565
581
  if (apiResult.success) {
582
+ const endpointHost = network ? "0.0.0.0" : "127.0.0.1";
566
583
  console.log(chalk.blue("\n🔌 API Endpoints:"));
567
584
  console.log(
568
585
  chalk.gray(
569
- ` - GET http://127.0.0.1:${apiServerPort}/api/tokens - List cloned tokens`
586
+ ` - GET http://${endpointHost}:${apiServerPort}/api/tokens - List cloned tokens`
570
587
  )
571
588
  );
572
589
  console.log(
573
590
  chalk.gray(
574
- ` - GET http://127.0.0.1:${apiServerPort}/api/programs - List cloned programs`
591
+ ` - GET http://${endpointHost}:${apiServerPort}/api/programs - List cloned programs`
575
592
  )
576
593
  );
577
594
  console.log(
578
595
  chalk.gray(
579
- ` - POST http://127.0.0.1:${apiServerPort}/api/tokens/{symbol}/mint - Mint tokens`
596
+ ` - POST http://${endpointHost}:${apiServerPort}/api/tokens/{mintAddress}/mint - Mint tokens`
580
597
  )
581
598
  );
582
599
  console.log(
583
600
  chalk.gray(
584
- ` - POST http://127.0.0.1:${apiServerPort}/api/airdrop - Airdrop SOL`
601
+ ` - POST http://${endpointHost}:${apiServerPort}/api/airdrop - Airdrop SOL`
585
602
  )
586
603
  );
587
604
  console.log(
588
605
  chalk.gray(
589
- ` - GET http://127.0.0.1:${apiServerPort}/api/wallet/{address}/balances - Get balances`
606
+ ` - GET http://${endpointHost}:${apiServerPort}/api/wallet/{address}/balances - Get balances`
590
607
  )
591
608
  );
592
609
  }
package/src/index.ts CHANGED
@@ -47,6 +47,7 @@ program
47
47
  .command("start")
48
48
  .description("Start localnet with current sf.config.json")
49
49
  .option("--debug", "Enable debug logging to see commands and detailed output")
50
+ .option("--network", "Make API server accessible over network (binds to 0.0.0.0 instead of 127.0.0.1)")
50
51
  .action(async (options) => {
51
52
  const configPath = findConfig();
52
53
  if (!configPath) {
@@ -57,7 +58,7 @@ program
57
58
  process.exit(1);
58
59
  }
59
60
 
60
- await startCommand(options.debug || false);
61
+ await startCommand(options.debug || false, options.network || false);
61
62
  });
62
63
 
63
64
  program
@@ -86,6 +87,76 @@ program
86
87
  await killCommand(validatorId, options);
87
88
  });
88
89
 
90
+ program
91
+ .command("api-server")
92
+ .description("Start API server standalone")
93
+ .option("-p, --port <port>", "Port for API server", "3000")
94
+ .option("--host <host>", "Host to bind to (default: 127.0.0.1, use 0.0.0.0 for network access)")
95
+ .option("--rpc-url <url>", "Validator RPC URL", "http://127.0.0.1:8899")
96
+ .option("--faucet-url <url>", "Validator faucet URL", "http://127.0.0.1:9900")
97
+ .option("--work-dir <dir>", "Work directory", "./.solforge")
98
+ .action(async (options) => {
99
+ const configPath = findConfig();
100
+ if (!configPath) {
101
+ console.error(
102
+ chalk.red("❌ No sf.config.json found in current directory")
103
+ );
104
+ console.log(chalk.yellow("💡 Run `solforge init` to create one"));
105
+ process.exit(1);
106
+ }
107
+
108
+ // Import API server components
109
+ const { APIServer } = await import("./services/api-server.js");
110
+ const { configManager } = await import("./config/manager.js");
111
+
112
+ try {
113
+ await configManager.load(configPath);
114
+ const config = configManager.getConfig();
115
+
116
+ const apiServer = new APIServer({
117
+ port: parseInt(options.port),
118
+ host: options.host,
119
+ validatorRpcUrl: options.rpcUrl,
120
+ validatorFaucetUrl: options.faucetUrl,
121
+ config,
122
+ workDir: options.workDir,
123
+ });
124
+
125
+ const result = await apiServer.start();
126
+ if (result.success) {
127
+ console.log(chalk.green("✅ API Server started successfully!"));
128
+
129
+ // Keep the process alive
130
+ process.on("SIGTERM", async () => {
131
+ console.log(chalk.yellow("📡 API Server received SIGTERM, shutting down..."));
132
+ await apiServer.stop();
133
+ process.exit(0);
134
+ });
135
+
136
+ process.on("SIGINT", async () => {
137
+ console.log(chalk.yellow("📡 API Server received SIGINT, shutting down..."));
138
+ await apiServer.stop();
139
+ process.exit(0);
140
+ });
141
+
142
+ // Keep process alive
143
+ setInterval(() => {}, 1000);
144
+ } else {
145
+ console.error(chalk.red(`❌ Failed to start API server: ${result.error}`));
146
+ process.exit(1);
147
+ }
148
+ } catch (error) {
149
+ console.error(
150
+ chalk.red(
151
+ `❌ API Server error: ${
152
+ error instanceof Error ? error.message : String(error)
153
+ }`
154
+ )
155
+ );
156
+ process.exit(1);
157
+ }
158
+ });
159
+
89
160
  program
90
161
  .command("add-program")
91
162
  .description("Add a program to sf.config.json")
@@ -16,6 +16,7 @@ import type { ClonedToken } from "./token-cloner.js";
16
16
 
17
17
  export interface APIServerConfig {
18
18
  port: number;
19
+ host?: string;
19
20
  validatorRpcUrl: string;
20
21
  validatorFaucetUrl: string;
21
22
  config: Config;
@@ -121,9 +122,9 @@ export class APIServer {
121
122
  });
122
123
 
123
124
  // Mint tokens to a wallet
124
- router.post("/tokens/:symbol/mint", async (req, res) => {
125
+ router.post("/tokens/:mintAddress/mint", async (req, res) => {
125
126
  try {
126
- const { symbol } = req.params;
127
+ const { mintAddress } = req.params;
127
128
  const { walletAddress, amount } = req.body;
128
129
 
129
130
  if (!walletAddress || !amount) {
@@ -132,6 +133,15 @@ export class APIServer {
132
133
  });
133
134
  }
134
135
 
136
+ // Validate mint address
137
+ try {
138
+ new PublicKey(mintAddress);
139
+ } catch {
140
+ return res.status(400).json({
141
+ error: "Invalid mint address",
142
+ });
143
+ }
144
+
135
145
  // Validate wallet address
136
146
  try {
137
147
  new PublicKey(walletAddress);
@@ -149,7 +159,7 @@ export class APIServer {
149
159
  }
150
160
 
151
161
  const result = await this.mintTokenToWallet(
152
- symbol,
162
+ mintAddress,
153
163
  walletAddress,
154
164
  amount
155
165
  );
@@ -326,17 +336,19 @@ export class APIServer {
326
336
  }
327
337
 
328
338
  private async mintTokenToWallet(
329
- symbol: string,
339
+ mintAddress: string,
330
340
  walletAddress: string,
331
341
  amount: number
332
342
  ): Promise<any> {
333
343
  const clonedTokens = await this.getClonedTokens();
334
344
  const token = clonedTokens.find(
335
- (t) => t.config.symbol.toLowerCase() === symbol.toLowerCase()
345
+ (t) => t.config.mainnetMint === mintAddress
336
346
  );
337
347
 
338
348
  if (!token) {
339
- throw new Error(`Token ${symbol} not found in cloned tokens`);
349
+ throw new Error(
350
+ `Token with mint address ${mintAddress} not found in cloned tokens`
351
+ );
340
352
  }
341
353
 
342
354
  // Use the shared minting function from the mint command
@@ -462,15 +474,16 @@ export class APIServer {
462
474
  async start(): Promise<{ success: boolean; error?: string }> {
463
475
  return new Promise((resolve) => {
464
476
  try {
465
- this.server = this.app.listen(this.config.port, "127.0.0.1", () => {
477
+ const host = this.config.host || "127.0.0.1";
478
+ this.server = this.app.listen(this.config.port, host, () => {
466
479
  console.log(
467
480
  chalk.green(
468
- `🚀 API Server started on http://127.0.0.1:${this.config.port}`
481
+ `🚀 API Server started on http://${host}:${this.config.port}`
469
482
  )
470
483
  );
471
484
  console.log(
472
485
  chalk.gray(
473
- ` 📋 Endpoints available at http://127.0.0.1:${this.config.port}/api`
486
+ ` 📋 Endpoints available at http://${host}:${this.config.port}/api`
474
487
  )
475
488
  );
476
489
  resolve({ success: true });