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 +54 -11
- package/package.json +1 -1
- package/src/api-server-entry.ts +5 -1
- package/src/commands/start.ts +28 -11
- package/src/index.ts +72 -1
- package/src/services/api-server.ts +22 -9
package/README.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# SolForge
|
|
2
2
|
|
|
3
|
+
[](https://badge.fury.io/js/solforge)
|
|
4
|
+
[](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
|
-
##
|
|
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+ (
|
|
26
|
+
- Node.js 18+ or [Bun](https://bun.sh) runtime
|
|
25
27
|
|
|
26
|
-
###
|
|
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
|
|
37
|
-
bun run build
|
|
51
|
+
# Build and install globally
|
|
52
|
+
bun run build:npm
|
|
53
|
+
npm install -g .
|
|
54
|
+
```
|
|
38
55
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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/
|
|
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
package/src/api-server-entry.ts
CHANGED
|
@@ -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,
|
package/src/commands/start.ts
CHANGED
|
@@ -21,7 +21,10 @@ function generateValidatorId(name: string): string {
|
|
|
21
21
|
return `${safeName}-${timestamp}-${randomSuffix}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function startCommand(
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
601
|
+
` - POST http://${endpointHost}:${apiServerPort}/api/airdrop - Airdrop SOL`
|
|
585
602
|
)
|
|
586
603
|
);
|
|
587
604
|
console.log(
|
|
588
605
|
chalk.gray(
|
|
589
|
-
` - GET http
|
|
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/:
|
|
125
|
+
router.post("/tokens/:mintAddress/mint", async (req, res) => {
|
|
125
126
|
try {
|
|
126
|
-
const {
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
345
|
+
(t) => t.config.mainnetMint === mintAddress
|
|
336
346
|
);
|
|
337
347
|
|
|
338
348
|
if (!token) {
|
|
339
|
-
throw new Error(
|
|
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
|
-
|
|
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
|
|
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
|
|
486
|
+
` 📋 Endpoints available at http://${host}:${this.config.port}/api`
|
|
474
487
|
)
|
|
475
488
|
);
|
|
476
489
|
resolve({ success: true });
|