use reqwest::{multipart::Part, Client as ReqwestClient};
use serde::{Deserialize, Serialize};
use solagent_core::{
solana_client,
solana_sdk::{
commitment_config::CommitmentConfig, signature::Signer, signer::keypair::Keypair,
transaction::VersionedTransaction,
},
SolanaAgentKit,
};
#[derive(Serialize, Deserialize, Debug)]
pub struct PumpFunTokenOptions {
pub twitter: Option<String>,
pub telegram: Option<String>,
pub website: Option<String>,
pub initial_liquidity_sol: f64,
pub slippage_bps: u16,
pub priority_fee: f64,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct PumpfunTokenResponse {
pub signature: String,
pub mint: String,
pub metadata_uri: String,
}
pub struct TokenMetadata {
pub name: String,
pub symbol: String,
pub uri: String,
}
pub async fn launch_token_pumpfun(
agent: &SolanaAgentKit,
token_name: &str,
token_symbol: &str,
description: &str,
image_url: &str,
options: Option<PumpFunTokenOptions>,
) -> Result<PumpfunTokenResponse, Box<dyn std::error::Error>> {
let reqwest_client = ReqwestClient::new();
let image_data = fetch_image(&reqwest_client, image_url).await.expect("fetch_image");
let token_metadata =
fetch_token_metadata(&reqwest_client, token_name, token_symbol, description, options, &image_data)
.await
.expect("fetch_token_metadata");
let mint_keypair = Keypair::new();
let mut versioned_tx = request_pumpportal_tx(agent, &reqwest_client, &token_metadata, &mint_keypair)
.await
.expect("request_pumpportal_tx");
let signature = sign_and_send_tx(agent, &mut versioned_tx, &mint_keypair).await.expect("sign_and_send_tx");
let res =
PumpfunTokenResponse { signature, mint: mint_keypair.pubkey().to_string(), metadata_uri: token_metadata.uri };
Ok(res)
}
async fn sign_and_send_tx(
agent: &SolanaAgentKit,
vtx: &mut VersionedTransaction,
mint_keypair: &Keypair,
) -> Result<String, Box<std::io::Error>> {
let recent_blockhash = agent.connection.get_latest_blockhash().expect("get_latest_blockhash");
vtx.message.set_recent_blockhash(recent_blockhash);
let signed_vtx = VersionedTransaction::try_new(vtx.message.clone(), &[mint_keypair, &agent.wallet.wallet])
.expect("try signed vtx");
let signature = agent
.connection
.send_and_confirm_transaction_with_spinner_and_config(
&signed_vtx,
CommitmentConfig::finalized(),
solana_client::rpc_config::RpcSendTransactionConfig { skip_preflight: false, ..Default::default() },
)
.expect("send_and_confirm_tx");
Ok(signature.to_string())
}
async fn fetch_image(client: &ReqwestClient, image_url: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
let response = client.get(image_url).send().await?;
if response.status().is_success() {
let image_data = response.bytes().await.expect("image data");
return Ok(image_data.to_vec());
}
Err("fetch image error".into())
}
async fn fetch_token_metadata(
client: &ReqwestClient,
name: &str,
symbol: &str,
description: &str,
options: Option<PumpFunTokenOptions>,
image_data: &[u8],
) -> Result<TokenMetadata, Box<dyn std::error::Error>> {
let part = Part::bytes(image_data.to_vec()).file_name("image_name").mime_str("image/png")?; let mut form = reqwest::multipart::Form::new()
.text("name", name.to_owned())
.text("symbol", symbol.to_owned())
.text("description", description.to_owned())
.part("file", part);
if let Some(option) = options {
if let Some(x) = option.twitter {
form = form.text("twitter", x);
}
if let Some(tele) = option.telegram {
form = form.text("telegram", tele);
}
if let Some(website) = option.website {
form = form.text("website", website);
}
form = form.text("showName", "true");
}
let res = client.post("https://pump.fun/api/ipfs").multipart(form).send().await?;
let status = res.status();
if !status.is_success() {
let text = res.text().await?;
eprintln!("Error response: {}", text);
return Err(format!("Upload failed with status: {}", status).into());
}
let response_json = res.json::<serde_json::Value>().await?;
let md = TokenMetadata {
name: name.to_string(),
symbol: symbol.to_string(),
uri: response_json.get("metadataUri").expect("metadataUri").to_string(),
};
Ok(md)
}
async fn request_pumpportal_tx(
agent: &SolanaAgentKit,
client: &ReqwestClient,
token_matedata: &TokenMetadata,
mint_keypair: &Keypair,
) -> Result<VersionedTransaction, Box<dyn std::error::Error>> {
let request_body = serde_json::json!({
"publicKey": agent.wallet.address.to_string(),
"action": "create",
"tokenMetadata": {
"name": token_matedata.name,
"symbol": token_matedata.symbol,
"uri": token_matedata.uri
},
"mint": mint_keypair.pubkey().to_string(),
"denominatedInSol": "true",
"amount": 1,
"slippage": 10,
"priorityFee": 0.0005,
"pool": "pump"
});
let res = client
.post("https://pumpportal.fun/api/trade-local")
.header("Content-Type", "application/json")
.json(&request_body)
.send()
.await?;
let status = res.status();
if !status.is_success() {
let text = res.text().await?;
eprintln!("Error response: {}", text);
return Err(format!("trade-local failed with status: {}", status).into());
}
if let Ok(bytes) = res.bytes().await {
if let Ok(tx) = bincode::deserialize::<VersionedTransaction>(&bytes) {
return Ok(tx);
}
}
Err("fetch token metadata error".into())
}