solagent_plugin_pumpfun/
launch_token_pumpfun.rs

1// Copyright 2025 zTgx
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use 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
49/// Launch a token on Pump.fun.
50///
51/// # Arguments
52///
53/// - `agent` - An instance of `SolanaAgentKit`.
54/// - `tokenName` - The name of the token.
55/// - `tokenTicker` - The ticker of the token.
56/// - `description` - The description of the token.
57/// - `imageUrl` - The URL of the token image.
58/// - `options` - Optional token options which include `twitter`, `telegram`, `website`, `initialLiquiditySOL`, `slippageBps`, and `priorityFee`.
59///
60/// # Returns
61///
62/// If successful, it returns the signature of the transaction, the mint address, and the metadata URI. Otherwise, it returns an error.
63///
64/// To get a transaction for signing and sending with a custom RPC, send a POST request to:
65/// https://pumpportal.fun/local-trading-api/trading-api/
66///
67/// The request body must include the following options:
68///
69/// * `publicKey` - Your wallet's public key.
70/// * `action` - Either "buy" or "sell", indicating the trading action you want to perform.
71/// * `mint` - The contract address of the token you wish to trade. This is the text that appears after the '/' in the pump.fun url for the specific token.
72/// * `amount` - The quantity of SOL or tokens to be traded. When selling, the amount can be specified as a percentage of the tokens in your wallet (e.g., amount: "100%").
73/// * `denominatedInSol` - Set to "true" if the `amount` is specified in SOL, and "false" if it's specified in tokens.
74/// * `slippage` - The percentage of slippage that is allowed during the trading process.
75/// * `priorityFee` - The amount to be used as the priority fee.
76/// * `pool` - (Optional) Currently, 'pump' and 'raydium' are the supported options. The default value is 'pump'.
77///
78///
79/// https://pumpportal.fun/creation
80///
81pub 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    // 0. download image
92    let image_data = fetch_image(&reqwest_client, image_url)
93        .await
94        .expect("fetch_image");
95
96    // 1. fetch token metadata metadataUri
97    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    // 2. Create a new keypair for the mint
109    let mint_keypair = Keypair::new();
110
111    // 3. request pumpportal tx
112    let mut versioned_tx =
113        request_pumpportal_tx(agent, &reqwest_client, &token_metadata, &mint_keypair)
114            .await
115            .expect("request_pumpportal_tx");
116
117    // 4. sign&send transaction
118    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
131// try signed vtx: NotEnoughSigners -> mint_keypair is needed
132async 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")?; // Important: set the correct MIME type
185
186    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}