1use reqwest::{multipart::Part, Client as ReqwestClient};
16use serde::{Deserialize, Serialize};
17use solagent_core::{
18 solana_client,
19 solana_sdk::{
20 commitment_config::CommitmentConfig, signature::Signer, signer::keypair::Keypair,
21 transaction::VersionedTransaction,
22 },
23 SolanaAgentKit,
24};
25
26#[derive(Serialize, Deserialize, Debug)]
27pub struct PumpFunTokenOptions {
28 pub twitter: Option<String>,
29 pub telegram: Option<String>,
30 pub website: Option<String>,
31 pub initial_liquidity_sol: f64,
32 pub slippage_bps: u16,
33 pub priority_fee: f64,
34}
35
36#[derive(Serialize, Deserialize, Debug)]
37pub struct PumpfunTokenResponse {
38 pub signature: String,
39 pub mint: String,
40 pub metadata_uri: String,
41}
42
43pub struct TokenMetadata {
44 pub name: String,
45 pub symbol: String,
46 pub uri: String,
47}
48
49pub async fn launch_token_pumpfun(
82 agent: &SolanaAgentKit,
83 token_name: &str,
84 token_symbol: &str,
85 description: &str,
86 image_url: &str,
87 options: Option<PumpFunTokenOptions>,
88) -> Result<PumpfunTokenResponse, Box<dyn std::error::Error>> {
89 let reqwest_client = ReqwestClient::new();
90
91 let image_data = fetch_image(&reqwest_client, image_url)
93 .await
94 .expect("fetch_image");
95
96 let token_metadata = fetch_token_metadata(
98 &reqwest_client,
99 token_name,
100 token_symbol,
101 description,
102 options,
103 &image_data,
104 )
105 .await
106 .expect("fetch_token_metadata");
107
108 let mint_keypair = Keypair::new();
110
111 let mut versioned_tx =
113 request_pumpportal_tx(agent, &reqwest_client, &token_metadata, &mint_keypair)
114 .await
115 .expect("request_pumpportal_tx");
116
117 let signature = sign_and_send_tx(agent, &mut versioned_tx, &mint_keypair)
119 .await
120 .expect("sign_and_send_tx");
121
122 let res = PumpfunTokenResponse {
123 signature,
124 mint: mint_keypair.pubkey().to_string(),
125 metadata_uri: token_metadata.uri,
126 };
127
128 Ok(res)
129}
130
131async fn sign_and_send_tx(
133 agent: &SolanaAgentKit,
134 vtx: &mut VersionedTransaction,
135 mint_keypair: &Keypair,
136) -> Result<String, Box<std::io::Error>> {
137 let recent_blockhash = agent
138 .connection
139 .get_latest_blockhash()
140 .expect("get_latest_blockhash");
141 vtx.message.set_recent_blockhash(recent_blockhash);
142 let signed_vtx =
143 VersionedTransaction::try_new(vtx.message.clone(), &[mint_keypair, &agent.wallet.keypair])
144 .expect("try signed vtx");
145
146 let signature = agent
147 .connection
148 .send_and_confirm_transaction_with_spinner_and_config(
149 &signed_vtx,
150 CommitmentConfig::finalized(),
151 solana_client::rpc_config::RpcSendTransactionConfig {
152 skip_preflight: false,
153 ..Default::default()
154 },
155 )
156 .expect("send_and_confirm_tx");
157
158 Ok(signature.to_string())
159}
160
161async fn fetch_image(
162 client: &ReqwestClient,
163 image_url: &str,
164) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
165 let response = client.get(image_url).send().await?;
166 if response.status().is_success() {
167 let image_data = response.bytes().await.expect("image data");
168 return Ok(image_data.to_vec());
169 }
170
171 Err("fetch image error".into())
172}
173
174async fn fetch_token_metadata(
175 client: &ReqwestClient,
176 name: &str,
177 symbol: &str,
178 description: &str,
179 options: Option<PumpFunTokenOptions>,
180 image_data: &[u8],
181) -> Result<TokenMetadata, Box<dyn std::error::Error>> {
182 let part = Part::bytes(image_data.to_vec())
183 .file_name("image_name")
184 .mime_str("image/png")?; let mut form = reqwest::multipart::Form::new()
187 .text("name", name.to_owned())
188 .text("symbol", symbol.to_owned())
189 .text("description", description.to_owned())
190 .part("file", part);
191
192 if let Some(option) = options {
193 if let Some(x) = option.twitter {
194 form = form.text("twitter", x);
195 }
196
197 if let Some(tele) = option.telegram {
198 form = form.text("telegram", tele);
199 }
200
201 if let Some(website) = option.website {
202 form = form.text("website", website);
203 }
204
205 form = form.text("showName", "true");
206 }
207
208 let res = client
209 .post("https://pump.fun/api/ipfs")
210 .multipart(form)
211 .send()
212 .await?;
213
214 let status = res.status();
215 if !status.is_success() {
216 let text = res.text().await?;
217 eprintln!("Error response: {}", text);
218 return Err(format!("Upload failed with status: {}", status).into());
219 }
220
221 let response_json = res.json::<serde_json::Value>().await?;
222 let md = TokenMetadata {
223 name: name.to_string(),
224 symbol: symbol.to_string(),
225 uri: response_json
226 .get("metadataUri")
227 .expect("metadataUri")
228 .to_string(),
229 };
230
231 Ok(md)
232}
233
234async fn request_pumpportal_tx(
235 agent: &SolanaAgentKit,
236 client: &ReqwestClient,
237 token_matedata: &TokenMetadata,
238 mint_keypair: &Keypair,
239) -> Result<VersionedTransaction, Box<dyn std::error::Error>> {
240 let request_body = serde_json::json!({
241 "publicKey": agent.wallet.pubkey.to_string(),
242 "action": "create",
243 "tokenMetadata": {
244 "name": token_matedata.name,
245 "symbol": token_matedata.symbol,
246 "uri": token_matedata.uri
247 },
248 "mint": mint_keypair.pubkey().to_string(),
249 "denominatedInSol": "true",
250 "amount": 1,
251 "slippage": 10,
252 "priorityFee": 0.0005,
253 "pool": "pump"
254 });
255
256 let res = client
257 .post("https://pumpportal.fun/api/trade-local")
258 .header("Content-Type", "application/json")
259 .json(&request_body)
260 .send()
261 .await?;
262
263 let status = res.status();
264 if !status.is_success() {
265 let text = res.text().await?;
266 eprintln!("Error response: {}", text);
267 return Err(format!("trade-local failed with status: {}", status).into());
268 }
269
270 if let Ok(bytes) = res.bytes().await {
271 if let Ok(tx) = bincode::deserialize::<VersionedTransaction>(&bytes) {
272 return Ok(tx);
273 }
274 }
275
276 Err("fetch token metadata error".into())
277}