solforge 0.1.5 → 0.1.7

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).
@@ -275,7 +318,7 @@ Here's the complete schema:
275
318
  "faucetAccounts": ["YourWalletPublicKeyHere"],
276
319
  "port": 8899,
277
320
  "faucetPort": 9900,
278
- "reset": true,
321
+ "reset": false,
279
322
  "logLevel": "info",
280
323
  "bindAddress": "127.0.0.1",
281
324
  "limitLedgerSize": 100000,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "solforge",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Solana localnet orchestration tool for cloning mainnet programs and tokens",
5
5
  "module": "index.ts",
6
6
  "type": "module",
@@ -22,11 +22,11 @@ const defaultConfig: Config = {
22
22
  faucetAccounts: [],
23
23
  port: 8899,
24
24
  faucetPort: 9900,
25
- reset: true,
25
+ reset: false,
26
26
  logLevel: "info",
27
27
  bindAddress: "127.0.0.1",
28
28
  quiet: false,
29
- rpc: "https://mainnet.helius-rpc.com/?api-key=3a3b84ef-2985-4543-9d67-535eb707b6ec",
29
+ rpc: "https://api.mainnet-beta.solana.com",
30
30
  limitLedgerSize: 100000,
31
31
  },
32
32
  };
@@ -5,22 +5,12 @@ import { join } from "path";
5
5
  import { input, select } from "@inquirer/prompts";
6
6
  import { runCommand } from "../utils/shell";
7
7
  import { Keypair, PublicKey } from "@solana/web3.js";
8
-
9
- interface TokenConfig {
10
- symbol: string;
11
- mainnetMint: string;
12
- mintAmount: number;
13
- }
14
-
15
- interface ClonedToken {
16
- config: TokenConfig;
17
- mintAuthorityPath: string;
18
- modifiedAccountPath: string;
19
- mintAuthority: {
20
- publicKey: string;
21
- secretKey: number[];
22
- };
23
- }
8
+ import {
9
+ loadClonedTokens,
10
+ findTokenBySymbol,
11
+ type ClonedToken,
12
+ } from "../utils/token-loader.js";
13
+ import type { TokenConfig } from "../types/config.js";
24
14
 
25
15
  export const mintCommand = new Command()
26
16
  .name("mint")
@@ -71,9 +61,7 @@ export const mintCommand = new Command()
71
61
  // Select token (or use provided symbol)
72
62
  let selectedToken: ClonedToken;
73
63
  if (options.symbol) {
74
- const token = tokens.find(
75
- (t) => t.config.symbol.toLowerCase() === options.symbol.toLowerCase()
76
- );
64
+ const token = findTokenBySymbol(tokens, options.symbol);
77
65
  if (!token) {
78
66
  console.error(
79
67
  chalk.red(`❌ Token ${options.symbol} not found in cloned tokens`)
@@ -181,8 +169,6 @@ export const mintCommand = new Command()
181
169
  });
182
170
 
183
171
  async function loadAvailableTokens(workDir: string): Promise<ClonedToken[]> {
184
- const tokens: ClonedToken[] = [];
185
-
186
172
  try {
187
173
  // Load token config from sf.config.json
188
174
  const configPath = "sf.config.json";
@@ -193,42 +179,8 @@ async function loadAvailableTokens(workDir: string): Promise<ClonedToken[]> {
193
179
  const config = JSON.parse(readFileSync(configPath, "utf8"));
194
180
  const tokenConfigs: TokenConfig[] = config.tokens || [];
195
181
 
196
- // Load shared mint authority
197
- const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
198
- if (!existsSync(sharedMintAuthorityPath)) {
199
- throw new Error("Shared mint authority not found");
200
- }
201
-
202
- const secretKeyArray = JSON.parse(
203
- readFileSync(sharedMintAuthorityPath, "utf8")
204
- );
205
- const mintAuthorityKeypair = Keypair.fromSecretKey(
206
- new Uint8Array(secretKeyArray)
207
- );
208
- const mintAuthority = {
209
- publicKey: mintAuthorityKeypair.publicKey.toBase58(),
210
- secretKey: Array.from(mintAuthorityKeypair.secretKey),
211
- };
212
-
213
- // Build cloned tokens list
214
- for (const tokenConfig of tokenConfigs) {
215
- const tokenDir = join(
216
- workDir,
217
- `token-${tokenConfig.symbol.toLowerCase()}`
218
- );
219
- const modifiedAccountPath = join(tokenDir, "modified.json");
220
-
221
- if (existsSync(modifiedAccountPath)) {
222
- tokens.push({
223
- config: tokenConfig,
224
- mintAuthorityPath: sharedMintAuthorityPath,
225
- modifiedAccountPath,
226
- mintAuthority,
227
- });
228
- }
229
- }
230
-
231
- return tokens;
182
+ // Use the shared token loader
183
+ return await loadClonedTokens(tokenConfigs, workDir);
232
184
  } catch (error) {
233
185
  throw new Error(`Failed to load tokens: ${error}`);
234
186
  }
@@ -6,11 +6,10 @@ import { join } from "path";
6
6
  import { runCommand, checkSolanaTools } from "../utils/shell.js";
7
7
  import { configManager } from "../config/manager.js";
8
8
  import { TokenCloner } from "../services/token-cloner.js";
9
- import { ProgramCloner } from "../services/program-cloner.js";
10
9
  import { processRegistry } from "../services/process-registry.js";
11
10
  import { portManager } from "../services/port-manager.js";
12
11
 
13
- import type { Config, TokenConfig, ProgramConfig } from "../types/config.js";
12
+ import type { Config, TokenConfig } from "../types/config.js";
14
13
  import type { ClonedToken } from "../services/token-cloner.js";
15
14
  import type { RunningValidator } from "../services/process-registry.js";
16
15
 
@@ -593,7 +592,7 @@ export async function startCommand(
593
592
  );
594
593
  console.log(
595
594
  chalk.gray(
596
- ` - POST http://${endpointHost}:${apiServerPort}/api/tokens/{symbol}/mint - Mint tokens`
595
+ ` - POST http://${endpointHost}:${apiServerPort}/api/tokens/{mintAddress}/mint - Mint tokens`
597
596
  )
598
597
  );
599
598
  console.log(
@@ -11,8 +11,12 @@ import { runCommand } from "../utils/shell.js";
11
11
  import { TokenCloner } from "./token-cloner.js";
12
12
  import { ProgramCloner } from "./program-cloner.js";
13
13
  import { mintTokenToWallet as mintTokenToWalletShared } from "../commands/mint.js";
14
+ import {
15
+ loadClonedTokens,
16
+ findTokenByMint,
17
+ type ClonedToken,
18
+ } from "../utils/token-loader.js";
14
19
  import type { Config } from "../types/config.js";
15
- import type { ClonedToken } from "./token-cloner.js";
16
20
 
17
21
  export interface APIServerConfig {
18
22
  port: number;
@@ -122,9 +126,9 @@ export class APIServer {
122
126
  });
123
127
 
124
128
  // Mint tokens to a wallet
125
- router.post("/tokens/:symbol/mint", async (req, res) => {
129
+ router.post("/tokens/:mintAddress/mint", async (req, res) => {
126
130
  try {
127
- const { symbol } = req.params;
131
+ const { mintAddress } = req.params;
128
132
  const { walletAddress, amount } = req.body;
129
133
 
130
134
  if (!walletAddress || !amount) {
@@ -133,6 +137,15 @@ export class APIServer {
133
137
  });
134
138
  }
135
139
 
140
+ // Validate mint address
141
+ try {
142
+ new PublicKey(mintAddress);
143
+ } catch {
144
+ return res.status(400).json({
145
+ error: "Invalid mint address",
146
+ });
147
+ }
148
+
136
149
  // Validate wallet address
137
150
  try {
138
151
  new PublicKey(walletAddress);
@@ -150,7 +163,7 @@ export class APIServer {
150
163
  }
151
164
 
152
165
  const result = await this.mintTokenToWallet(
153
- symbol,
166
+ mintAddress,
154
167
  walletAddress,
155
168
  amount
156
169
  );
@@ -247,57 +260,10 @@ export class APIServer {
247
260
  }
248
261
 
249
262
  private async getClonedTokens(): Promise<ClonedToken[]> {
250
- const clonedTokens: ClonedToken[] = [];
251
-
252
- for (const tokenConfig of this.config.config.tokens) {
253
- const tokenDir = join(
254
- this.config.workDir,
255
- `token-${tokenConfig.symbol.toLowerCase()}`
256
- );
257
- const modifiedAccountPath = join(tokenDir, "modified.json");
258
- const sharedMintAuthorityPath = join(
259
- this.config.workDir,
260
- "shared-mint-authority.json"
261
- );
262
-
263
- if (
264
- existsSync(modifiedAccountPath) &&
265
- existsSync(sharedMintAuthorityPath)
266
- ) {
267
- try {
268
- const mintAuthorityData = JSON.parse(
269
- readFileSync(sharedMintAuthorityPath, "utf8")
270
- );
271
- let mintAuthority;
272
-
273
- if (Array.isArray(mintAuthorityData)) {
274
- const keypair = Keypair.fromSecretKey(
275
- new Uint8Array(mintAuthorityData)
276
- );
277
- mintAuthority = {
278
- publicKey: keypair.publicKey.toBase58(),
279
- secretKey: Array.from(keypair.secretKey),
280
- };
281
- } else {
282
- mintAuthority = mintAuthorityData;
283
- }
284
-
285
- clonedTokens.push({
286
- config: tokenConfig,
287
- mintAuthorityPath: sharedMintAuthorityPath,
288
- modifiedAccountPath,
289
- mintAuthority,
290
- });
291
- } catch (error) {
292
- console.error(
293
- `Failed to load cloned token ${tokenConfig.symbol}:`,
294
- error
295
- );
296
- }
297
- }
298
- }
299
-
300
- return clonedTokens;
263
+ return await loadClonedTokens(
264
+ this.config.config.tokens,
265
+ this.config.workDir
266
+ );
301
267
  }
302
268
 
303
269
  private async getClonedPrograms(): Promise<
@@ -327,17 +293,15 @@ export class APIServer {
327
293
  }
328
294
 
329
295
  private async mintTokenToWallet(
330
- symbol: string,
296
+ mintAddress: string,
331
297
  walletAddress: string,
332
298
  amount: number
333
299
  ): Promise<any> {
334
300
  const clonedTokens = await this.getClonedTokens();
335
- const token = clonedTokens.find(
336
- (t) => t.config.symbol.toLowerCase() === symbol.toLowerCase()
337
- );
301
+ const token = findTokenByMint(clonedTokens, mintAddress);
338
302
 
339
303
  if (!token) {
340
- throw new Error(`Token ${symbol} not found in cloned tokens`);
304
+ throw new Error(`Token ${mintAddress} not found in cloned tokens`);
341
305
  }
342
306
 
343
307
  // Use the shared minting function from the mint command
@@ -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
+ }