solforge 0.1.1 → 0.1.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/README.md +41 -4
- package/package.json +1 -1
- package/src/api-server-entry.ts +105 -0
- package/src/commands/mint.ts +336 -0
- package/src/commands/start.ts +126 -0
- package/src/commands/stop.ts +20 -3
- package/src/index.ts +2 -2
- package/src/services/api-server.ts +519 -0
- package/src/services/process-registry.ts +4 -5
- package/src/commands/transfer.ts +0 -259
package/src/commands/transfer.ts
DELETED
|
@@ -1,259 +0,0 @@
|
|
|
1
|
-
import { Command } from "commander";
|
|
2
|
-
import chalk from "chalk";
|
|
3
|
-
import { existsSync, readFileSync } from "fs";
|
|
4
|
-
import { join } from "path";
|
|
5
|
-
import { input, select } from "@inquirer/prompts";
|
|
6
|
-
import { runCommand } from "../utils/shell";
|
|
7
|
-
import { Keypair } from "@solana/web3.js";
|
|
8
|
-
|
|
9
|
-
interface TokenBalance {
|
|
10
|
-
mint: string;
|
|
11
|
-
symbol: string;
|
|
12
|
-
address: string;
|
|
13
|
-
balance: string;
|
|
14
|
-
decimals: number;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
interface TokenConfig {
|
|
18
|
-
symbol: string;
|
|
19
|
-
mainnetMint: string;
|
|
20
|
-
mintAmount: number;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
export const transferCommand = new Command()
|
|
24
|
-
.name("transfer")
|
|
25
|
-
.description(
|
|
26
|
-
"Interactively transfer tokens from mint authority to any address"
|
|
27
|
-
)
|
|
28
|
-
.option("--rpc-url <url>", "RPC URL to use", "http://127.0.0.1:8899")
|
|
29
|
-
.action(async (options) => {
|
|
30
|
-
try {
|
|
31
|
-
console.log(chalk.blue("🔄 Interactive Token Transfer"));
|
|
32
|
-
console.log(
|
|
33
|
-
chalk.gray("Send tokens from mint authority to any address\n")
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
// Check if solforge data exists
|
|
37
|
-
const workDir = ".solforge";
|
|
38
|
-
if (!existsSync(workDir)) {
|
|
39
|
-
console.error(
|
|
40
|
-
chalk.red("❌ No solforge data found. Run 'solforge start' first.")
|
|
41
|
-
);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
// Load available tokens and their balances
|
|
46
|
-
const tokens = await loadAvailableTokens(workDir, options.rpcUrl);
|
|
47
|
-
|
|
48
|
-
if (tokens.length === 0) {
|
|
49
|
-
console.error(
|
|
50
|
-
chalk.red(
|
|
51
|
-
"❌ No tokens found. Run 'solforge start' first to clone tokens."
|
|
52
|
-
)
|
|
53
|
-
);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// Display available tokens
|
|
58
|
-
console.log(chalk.cyan("📋 Available Tokens:"));
|
|
59
|
-
tokens.forEach((token, index) => {
|
|
60
|
-
console.log(
|
|
61
|
-
chalk.gray(
|
|
62
|
-
` ${index + 1}. ${token.symbol} (${token.mint}) - Balance: ${
|
|
63
|
-
token.balance
|
|
64
|
-
}`
|
|
65
|
-
)
|
|
66
|
-
);
|
|
67
|
-
});
|
|
68
|
-
console.log();
|
|
69
|
-
|
|
70
|
-
// Select token
|
|
71
|
-
const selectedToken = await select({
|
|
72
|
-
message: "Select a token to transfer:",
|
|
73
|
-
choices: tokens.map((token, index) => ({
|
|
74
|
-
name: `${token.symbol} - Balance: ${token.balance}`,
|
|
75
|
-
value: token,
|
|
76
|
-
})),
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
// Get recipient address
|
|
80
|
-
const recipientAddress = await input({
|
|
81
|
-
message: "Enter recipient address (wallet address or PDA):",
|
|
82
|
-
validate: (value: string) => {
|
|
83
|
-
if (!value.trim()) {
|
|
84
|
-
return "Please enter a valid address";
|
|
85
|
-
}
|
|
86
|
-
// Basic length check for Solana addresses
|
|
87
|
-
if (value.trim().length < 32 || value.trim().length > 44) {
|
|
88
|
-
return "Please enter a valid Solana address (32-44 characters)";
|
|
89
|
-
}
|
|
90
|
-
return true;
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
// Get amount to transfer
|
|
95
|
-
const maxAmount = parseFloat(selectedToken.balance);
|
|
96
|
-
const amount = await input({
|
|
97
|
-
message: `Enter amount to transfer (max: ${selectedToken.balance}):`,
|
|
98
|
-
validate: (value: string) => {
|
|
99
|
-
const num = parseFloat(value);
|
|
100
|
-
if (isNaN(num) || num <= 0) {
|
|
101
|
-
return "Please enter a valid positive number";
|
|
102
|
-
}
|
|
103
|
-
if (num > maxAmount) {
|
|
104
|
-
return `Amount cannot exceed available balance: ${selectedToken.balance}`;
|
|
105
|
-
}
|
|
106
|
-
return true;
|
|
107
|
-
},
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
// Use amount as-is (spl-token expects UI amount, not base units)
|
|
111
|
-
const transferAmount = amount;
|
|
112
|
-
|
|
113
|
-
// Confirm transfer
|
|
114
|
-
const confirm = await input({
|
|
115
|
-
message: `Confirm transfer of ${amount} ${selectedToken.symbol} to ${recipientAddress}? (y/N):`,
|
|
116
|
-
default: "N",
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (confirm.toLowerCase() !== "y" && confirm.toLowerCase() !== "yes") {
|
|
120
|
-
console.log(chalk.yellow("Transfer cancelled."));
|
|
121
|
-
process.exit(0);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
console.log(chalk.blue("🚀 Starting transfer..."));
|
|
125
|
-
|
|
126
|
-
// Execute transfer
|
|
127
|
-
await executeTransfer(
|
|
128
|
-
selectedToken,
|
|
129
|
-
recipientAddress,
|
|
130
|
-
transferAmount.toString(),
|
|
131
|
-
options.rpcUrl,
|
|
132
|
-
workDir
|
|
133
|
-
);
|
|
134
|
-
|
|
135
|
-
console.log(
|
|
136
|
-
chalk.green(
|
|
137
|
-
`✅ Successfully transferred ${amount} ${selectedToken.symbol} to ${recipientAddress}`
|
|
138
|
-
)
|
|
139
|
-
);
|
|
140
|
-
} catch (error) {
|
|
141
|
-
console.error(chalk.red(`❌ Transfer failed: ${error}`));
|
|
142
|
-
process.exit(1);
|
|
143
|
-
}
|
|
144
|
-
});
|
|
145
|
-
|
|
146
|
-
async function loadAvailableTokens(
|
|
147
|
-
workDir: string,
|
|
148
|
-
rpcUrl: string
|
|
149
|
-
): Promise<TokenBalance[]> {
|
|
150
|
-
const tokens: TokenBalance[] = [];
|
|
151
|
-
|
|
152
|
-
try {
|
|
153
|
-
// Load shared mint authority secret key and generate keypair
|
|
154
|
-
const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
|
|
155
|
-
if (!existsSync(sharedMintAuthorityPath)) {
|
|
156
|
-
throw new Error("Shared mint authority not found");
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const secretKeyArray = JSON.parse(
|
|
160
|
-
readFileSync(sharedMintAuthorityPath, "utf8")
|
|
161
|
-
);
|
|
162
|
-
const mintAuthorityKeypair = Keypair.fromSecretKey(
|
|
163
|
-
new Uint8Array(secretKeyArray)
|
|
164
|
-
);
|
|
165
|
-
const mintAuthorityAddress = mintAuthorityKeypair.publicKey.toBase58();
|
|
166
|
-
|
|
167
|
-
// Load token config from sf.config.json
|
|
168
|
-
const configPath = "sf.config.json";
|
|
169
|
-
if (!existsSync(configPath)) {
|
|
170
|
-
throw new Error("sf.config.json not found in current directory");
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
const config = JSON.parse(readFileSync(configPath, "utf8"));
|
|
174
|
-
const tokenConfigs: TokenConfig[] = config.tokens || [];
|
|
175
|
-
|
|
176
|
-
// Get token account balances
|
|
177
|
-
const accountsResult = await runCommand(
|
|
178
|
-
"spl-token",
|
|
179
|
-
[
|
|
180
|
-
"accounts",
|
|
181
|
-
"--owner",
|
|
182
|
-
mintAuthorityAddress,
|
|
183
|
-
"--url",
|
|
184
|
-
rpcUrl,
|
|
185
|
-
"--output",
|
|
186
|
-
"json",
|
|
187
|
-
],
|
|
188
|
-
{ silent: true }
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
if (!accountsResult.success) {
|
|
192
|
-
throw new Error(
|
|
193
|
-
`Failed to fetch token accounts: ${accountsResult.stderr}`
|
|
194
|
-
);
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
const accountsData = JSON.parse(accountsResult.stdout);
|
|
198
|
-
|
|
199
|
-
// Match accounts with token config
|
|
200
|
-
for (const account of accountsData.accounts || []) {
|
|
201
|
-
const tokenInfo = tokenConfigs.find(
|
|
202
|
-
(token: TokenConfig) => token.mainnetMint === account.mint
|
|
203
|
-
);
|
|
204
|
-
if (tokenInfo) {
|
|
205
|
-
tokens.push({
|
|
206
|
-
mint: account.mint,
|
|
207
|
-
symbol: tokenInfo.symbol,
|
|
208
|
-
address: account.address,
|
|
209
|
-
balance: account.tokenAmount.uiAmountString,
|
|
210
|
-
decimals: account.tokenAmount.decimals,
|
|
211
|
-
});
|
|
212
|
-
}
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
return tokens;
|
|
216
|
-
} catch (error) {
|
|
217
|
-
throw new Error(`Failed to load tokens: ${error}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
async function executeTransfer(
|
|
222
|
-
token: TokenBalance,
|
|
223
|
-
recipientAddress: string,
|
|
224
|
-
amount: string,
|
|
225
|
-
rpcUrl: string,
|
|
226
|
-
workDir: string
|
|
227
|
-
): Promise<void> {
|
|
228
|
-
// Load mint authority keypair path
|
|
229
|
-
const sharedMintAuthorityPath = join(workDir, "shared-mint-authority.json");
|
|
230
|
-
|
|
231
|
-
// Transfer tokens directly to wallet address
|
|
232
|
-
// The --fund-recipient flag will automatically create the associated token account if needed
|
|
233
|
-
console.log(chalk.gray("💸 Transferring tokens..."));
|
|
234
|
-
|
|
235
|
-
const transferResult = await runCommand(
|
|
236
|
-
"spl-token",
|
|
237
|
-
[
|
|
238
|
-
"transfer",
|
|
239
|
-
token.mint,
|
|
240
|
-
amount,
|
|
241
|
-
recipientAddress,
|
|
242
|
-
"--from",
|
|
243
|
-
token.address,
|
|
244
|
-
"--owner",
|
|
245
|
-
sharedMintAuthorityPath,
|
|
246
|
-
"--fee-payer",
|
|
247
|
-
sharedMintAuthorityPath,
|
|
248
|
-
"--fund-recipient",
|
|
249
|
-
"--allow-unfunded-recipient",
|
|
250
|
-
"--url",
|
|
251
|
-
rpcUrl,
|
|
252
|
-
],
|
|
253
|
-
{ silent: false }
|
|
254
|
-
);
|
|
255
|
-
|
|
256
|
-
if (!transferResult.success) {
|
|
257
|
-
throw new Error(`Transfer failed: ${transferResult.stderr}`);
|
|
258
|
-
}
|
|
259
|
-
}
|