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.keypair])
.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.pubkey.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())
}