znap/
lib.rs

1//! [![Watch the demo](https://res.cloudinary.com/andresmgsl/image/upload/q_auto/f_auto/w_450/v1718845551/ZNAP_cuckvf.png)](https://youtu.be/pmuwP9fWa3M)
2//!
3//! Performance-first Rust Framework to build APIs compatible with the Solana Actions Spec.
4//!
5//! ## Features
6//! - Creating Solana Actions Metadata interfaces
7//! - Creating Solana Actions POST Requests Transactions with and without query params.
8//! - Creating Solana Actions GET Requests Transactions with and without query params.
9//!
10//! ## How to import `znap`
11//!
12//! 1. `cargo add znap`
13//! 2. In your lib.rs file import: `use znap::prelude::*`
14//!
15//! ## Znap ecosystem
16//! - [`znap`](https://docs.rs/znap/latest/znap/)
17//! - [`znap-syn`](https://docs.rs/znap-syn/latest/znap_syn/)
18//! - [`znap-macros`](https://docs.rs/znap-macros/latest/znap_macros/)
19//! - [`znap-cli`](https://docs.rs/znap-cli/latest/znap_cli/)
20//!
21//! ## Example
22//!
23//! ```ignore
24//! use solana_sdk::{message::Message, pubkey, pubkey::Pubkey, transaction::Transaction};
25//! use spl_associated_token_account::get_associated_token_address;
26//! use spl_token::{instruction::transfer, ID as TOKEN_PROGRAM_ID};
27//! use std::str::FromStr;
28//! use znap::prelude::*;
29//!
30//! #[collection]
31//! pub mod my_actions {
32//!     use super::*;
33//!
34//!     pub fn fixed_transfer(ctx: Context<FixedTransferAction>) -> Result<Transaction> {
35//!         let account_pubkey = match Pubkey::from_str(&ctx.payload.account) {
36//!             Ok(account_pubkey) => account_pubkey,
37//!             _ => return Err(Error::from(ActionError::InvalidAccountPublicKey)),
38//!         };
39//!         let mint_pubkey = pubkey!("FtaDaiPPAy52vKtzdrpMLS3bXvG9LVUYJt6TeG6XxMUi");
40//!         let receiver_pubkey = pubkey!("6GBLiSwAPhDMttmdjo3wvEsssEnCiW3yZwVyVZnhFm3G");
41//!         let source_pubkey = get_associated_token_address(&account_pubkey, &mint_pubkey);
42//!         let destination_pubkey = get_associated_token_address(&receiver_pubkey, &mint_pubkey);
43//!         let transfer_instruction = match transfer(
44//!             &TOKEN_PROGRAM_ID,
45//!             &source_pubkey,
46//!             &destination_pubkey,
47//!             &account_pubkey,
48//!             &[&account_pubkey],
49//!             1,
50//!         ) {
51//!             Ok(transfer_instruction) => transfer_instruction,
52//!             _ => return Err(Error::from(ActionError::InvalidInstruction)),
53//!         };
54//!         let transaction_message = Message::new(&[transfer_instruction], None);
55//!
56//!         Ok(Transaction::new_unsigned(transaction_message))
57//!     }
58//! }
59//!
60//! #[derive(Action)]
61//! #[action(
62//!     icon = "https://google.com",
63//!     title = "Fixed transfer",
64//!     description = "Send a fixed transfer to the treasury",
65//!     label = "Send"
66//! )]
67//! pub struct FixedTransferAction;
68//!
69//! #[derive(ErrorCode)]
70//! enum ActionError {
71//!     #[error(msg = "Invalid account public key")]
72//!     InvalidAccountPublicKey,
73//!     #[error(msg = "Invalid instruction")]
74//!     InvalidInstruction,
75//! }
76//! ```
77
78use axum::http::StatusCode;
79use axum::response::{IntoResponse, Response};
80use axum::Json;
81use handlebars::Handlebars;
82use serde::{Deserialize, Serialize};
83use solana_sdk::instruction::{AccountMeta, Instruction};
84use solana_sdk::message::Message;
85use solana_sdk::pubkey;
86use solana_sdk::signature::Keypair;
87use solana_sdk::signer::Signer;
88use solana_sdk::transaction::Transaction;
89pub extern crate base64;
90pub extern crate bincode;
91pub extern crate colored;
92pub extern crate solana_client;
93pub extern crate tower_http;
94pub extern crate znap_macros;
95
96pub mod env;
97pub mod prelude {
98    pub use super::env::Env;
99    pub use super::{
100        Action, ActionLinks, ActionMetadata, ActionResponse, ActionTransaction, Error, ErrorCode,
101        LinkedAction, LinkedActionParameter, Result, ToMetadata,
102    };
103    pub use base64;
104    pub use bincode;
105    pub use colored;
106    pub use solana_client;
107    pub use tower_http;
108    pub use znap_macros::{collection, Action, ErrorCode};
109}
110
111/// Trait used to transform a struct into an Action.
112pub trait Action {}
113
114/// Trait used to transform a struct into an error code.
115pub trait ErrorCode {}
116
117/// Used to rename Resolve and limit errors to those that occur within the program.
118pub type Result<T> = core::result::Result<T, Error>;
119
120/// Allows a struct to capture its internal values and return them as an ActionMetadata interface.
121pub trait ToMetadata {
122    fn to_metadata() -> ActionMetadata;
123}
124
125/// Data structure required to make a POST request to an endpoint of the Solana Actions API.
126#[derive(Debug, Serialize, Deserialize)]
127pub struct CreateActionPayload {
128    pub account: String,
129}
130
131/// Represents the data structure returned by the POST handlers.
132#[derive(Debug, Serialize)]
133pub struct ActionResponse {
134    pub transaction: String,
135    pub message: Option<String>,
136}
137
138/// Represents the data structure returned by a POST request to an endpoint of the Solana Actions API.
139#[derive(Debug, Deserialize, Serialize)]
140pub struct ActionTransaction {
141    pub transaction: Transaction,
142    pub message: Option<String>,
143}
144
145/// Represents the data structure returned by a GET request to an endpoint of the Solana Actions API.
146#[derive(Debug, Deserialize, Serialize, PartialEq)]
147pub struct ActionMetadata {
148    pub icon: String,
149    pub title: String,
150    pub description: String,
151    pub label: String,
152    pub links: Option<ActionLinks>,
153    pub disabled: bool,
154    pub error: Option<ActionError>,
155}
156
157#[derive(Debug, Deserialize, Serialize, PartialEq)]
158pub struct ActionError {
159    pub message: String,
160}
161
162#[derive(Debug, Deserialize, Serialize, PartialEq)]
163pub struct ActionLinks {
164    pub actions: Vec<LinkedAction>,
165}
166
167#[derive(Debug, Deserialize, Serialize, PartialEq)]
168pub struct LinkedAction {
169    pub label: String,
170    pub href: String,
171    pub parameters: Vec<LinkedActionParameter>,
172}
173
174#[derive(Debug, Deserialize, Serialize, PartialEq)]
175pub struct LinkedActionParameter {
176    pub label: String,
177    pub name: String,
178    pub required: bool,
179}
180
181/// Error occurred during the processing of the request.
182#[derive(Debug)]
183pub struct Error {
184    pub code: StatusCode,
185    pub name: String,
186    pub message: String,
187}
188
189impl Error {
190    pub fn new(code: StatusCode, name: String, message: impl Into<String>) -> Self {
191        Self {
192            code,
193            name,
194            message: message.into(),
195        }
196    }
197}
198
199impl IntoResponse for Error {
200    fn into_response(self) -> Response {
201        (
202            self.code,
203            Json(ErrorResponse {
204                name: self.name.clone(),
205                message: self.message.clone(),
206            }),
207        )
208            .into_response()
209    }
210}
211
212#[derive(Serialize, Deserialize)]
213struct ErrorResponse {
214    name: String,
215    message: String,
216}
217
218pub fn add_action_identity_proof(transaction: Transaction, keypair: &Keypair) -> Transaction {
219    let identity_pubkey = keypair.pubkey();
220
221    let reference_keypair = Keypair::new();
222    let reference_pubkey = reference_keypair.pubkey();
223
224    let identity_signature = keypair.sign_message(&reference_pubkey.to_bytes());
225    let identity_message = format!(
226        "solana-action:{}:{}:{}",
227        identity_pubkey, reference_pubkey, identity_signature
228    );
229
230    let mut identity_added = false;
231
232    let mut instructions_with_identity: Vec<Instruction> = transaction
233        .message
234        .instructions
235        .iter()
236        .map(|instruction| {
237            let program_id =
238                transaction.message.account_keys[instruction.program_id_index as usize];
239
240            let mut accounts: Vec<AccountMeta> = instruction
241                .accounts
242                .iter()
243                .map(|account_index| {
244                    let pubkey = transaction.message.account_keys[*account_index as usize];
245
246                    match transaction
247                        .message
248                        .is_maybe_writable(*account_index as usize, None)
249                    {
250                        true => AccountMeta::new(
251                            pubkey,
252                            transaction.message.is_signer(*account_index as usize),
253                        ),
254                        false => AccountMeta::new_readonly(
255                            pubkey,
256                            transaction.message.is_signer(*account_index as usize),
257                        ),
258                    }
259                })
260                .collect();
261
262            if !identity_added
263                && program_id.to_string() != "MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"
264            {
265                accounts.push(AccountMeta::new_readonly(reference_pubkey, false));
266                accounts.push(AccountMeta::new_readonly(identity_pubkey, false));
267
268                identity_added = true;
269            }
270
271            Instruction {
272                program_id,
273                data: instruction.data.clone(),
274                accounts,
275            }
276        })
277        .collect();
278
279    instructions_with_identity.push(Instruction {
280        accounts: vec![],
281        data: identity_message.as_bytes().to_vec(),
282        program_id: pubkey!("MemoSq4gqABAXKb96qnH8TysNcWxMyWCqXgDLGmfcHr"),
283    });
284
285    let transaction_message_with_identity = Message::new(&instructions_with_identity, None);
286
287    Transaction::new_unsigned(transaction_message_with_identity)
288}
289
290pub fn render_source<T>(source: &str, data: &T) -> String
291where
292    T: Serialize,
293{
294    let mut handlebars = Handlebars::new();
295
296    assert!(handlebars
297        .register_template_string("template", source)
298        .is_ok());
299    let output = handlebars.render("template", &data).unwrap();
300
301    handlebars.clear_templates();
302
303    output
304}
305
306pub fn render_parameters<T>(
307    parameters: &[LinkedActionParameter],
308    data: &T,
309) -> Vec<LinkedActionParameter>
310where
311    T: Serialize,
312{
313    parameters
314        .iter()
315        .map(|parameter| {
316            let name = render_source(&parameter.name, &data);
317            let label = render_source(&parameter.label, &data);
318
319            LinkedActionParameter {
320                label,
321                name,
322                required: parameter.required,
323            }
324        })
325        .collect()
326}
327
328pub fn render_action_links<T>(links: Option<&ActionLinks>, data: &T) -> Option<ActionLinks>
329where
330    T: Serialize,
331{
332    match links {
333        Some(ActionLinks { actions }) => Some(ActionLinks {
334            actions: actions
335                .iter()
336                .map(|link| {
337                    let label = render_source(&link.label, &data);
338                    let href = render_source(&link.href, &data);
339
340                    LinkedAction {
341                        label,
342                        href,
343                        parameters: render_parameters(&link.parameters, &data),
344                    }
345                })
346                .collect(),
347        }),
348        _ => None,
349    }
350}
351
352pub fn render_metadata<T>(
353    metadata: &ActionMetadata,
354    data: &T,
355    disabled: bool,
356    error: Option<ActionError>,
357) -> ActionMetadata
358where
359    T: Serialize,
360{
361    let title = render_source(&metadata.title, &data);
362    let description = render_source(&metadata.description, &data);
363    let label = render_source(&metadata.label, &data);
364    let icon = render_source(&metadata.icon, &data);
365    let links = render_action_links(metadata.links.as_ref(), &data);
366
367    ActionMetadata {
368        title,
369        icon,
370        description,
371        label,
372        links,
373        disabled,
374        error,
375    }
376}
377
378#[derive(Serialize, Deserialize, Debug)]
379pub struct Status {
380    pub active: bool,
381}