flow_lib/config/
mod.rs

1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3use serde_json::Value as JsonValue;
4use solana_commitment_config::CommitmentConfig;
5use solana_rpc_client::{
6    http_sender::HttpSender, nonblocking::rpc_client::RpcClient, rpc_client::RpcClientConfig,
7};
8use std::{collections::HashMap, num::NonZeroU64, str::FromStr, sync::LazyLock, time::Duration};
9use thiserror::Error as ThisError;
10use uuid::Uuid;
11
12use self::client::Network;
13
14pub mod client;
15pub mod node;
16
17/// Use to describe input types and output types of nodes.
18#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
19pub enum ValueType {
20    #[serde(rename = "bool")]
21    Bool,
22    #[serde(rename = "u8")]
23    U8,
24    #[serde(rename = "u16")]
25    U16,
26    #[serde(rename = "u32")]
27    U32,
28    #[serde(rename = "u64")]
29    U64,
30    #[serde(rename = "u128")]
31    U128,
32    #[serde(rename = "i8")]
33    I8,
34    #[serde(rename = "i16")]
35    I16,
36    #[serde(rename = "i32")]
37    I32,
38    #[serde(rename = "i64")]
39    I64,
40    #[serde(rename = "i128")]
41    I128,
42    #[serde(rename = "f32")]
43    F32,
44    #[serde(rename = "f64")]
45    F64,
46    #[serde(alias = "number")]
47    #[serde(rename = "decimal")]
48    Decimal,
49    #[serde(rename = "pubkey")]
50    Pubkey,
51    // Wormhole address
52    #[serde(rename = "address")]
53    Address,
54    #[serde(rename = "keypair")]
55    Keypair,
56    #[serde(rename = "signature")]
57    Signature,
58    #[serde(rename = "string")]
59    String,
60    #[serde(rename = "bytes")]
61    Bytes,
62    #[serde(rename = "array")]
63    Array,
64    #[serde(rename = "object")]
65    Map,
66    #[serde(rename = "json")]
67    Json,
68    #[serde(rename = "free")]
69    Free,
70    #[serde(other)]
71    Other,
72}
73
74pub type FlowId = i32;
75pub type NodeId = Uuid;
76pub type FlowRunId = Uuid;
77
78/// Command name and field name,
79pub type Name = String;
80
81/// Inputs and outputs of commands
82pub type ValueSet = value::Map;
83
84#[derive(
85    Debug,
86    Clone,
87    Copy,
88    PartialEq,
89    Eq,
90    PartialOrd,
91    Ord,
92    Serialize,
93    Deserialize,
94    bincode::Encode,
95    bincode::Decode,
96)]
97pub enum CommandType {
98    #[serde(rename = "native")]
99    Native,
100    #[serde(rename = "mock")]
101    Mock,
102    #[serde(rename = "WASM")]
103    Wasm,
104    #[serde(rename = "deno")]
105    Deno,
106}
107
108#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
109pub struct CmdInputDescription {
110    pub name: Name,
111    pub type_bounds: Vec<ValueType>,
112    pub required: bool,
113    pub passthrough: bool,
114}
115
116#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
117pub struct CmdOutputDescription {
118    pub name: Name,
119    pub r#type: ValueType,
120    #[serde(default = "value::default::bool_false")]
121    pub optional: bool,
122}
123
124/// An input or output gate of a node
125pub type Gate = (NodeId, Name);
126
127#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
128pub struct FlowConfig {
129    pub id: FlowId,
130    pub ctx: ContextConfig,
131    pub nodes: Vec<NodeConfig>,
132    pub edges: Vec<(Gate, Gate)>,
133    #[serde(default)]
134    pub instructions_bundling: client::BundlingMode,
135}
136
137#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
138pub struct NodeConfig {
139    pub id: NodeId,
140    pub command_name: Name,
141    pub form_data: JsonValue,
142    pub client_node_data: client::NodeData,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
146pub struct Endpoints {
147    pub flow_server: String,
148    pub supabase: String,
149    pub supabase_anon_key: String,
150}
151
152impl Default for Endpoints {
153    fn default() -> Self {
154        Self {
155            flow_server: "http://localhost:8080".to_owned(),
156            supabase: "http://localhost:8081".to_owned(),
157            supabase_anon_key: String::new(),
158        }
159    }
160}
161
162#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
163pub struct ContextConfig {
164    pub http_client: HttpClientConfig,
165    pub solana_client: SolanaClientConfig,
166    pub environment: HashMap<String, String>,
167    pub endpoints: Endpoints,
168}
169
170impl Default for ContextConfig {
171    fn default() -> Self {
172        ContextConfig {
173            http_client: HttpClientConfig {
174                timeout_in_secs: NonZeroU64::new(100).unwrap(),
175                gzip: true,
176            },
177            solana_client: SolanaClientConfig {
178                url: SolanaNet::Devnet.url(),
179                cluster: SolanaNet::Devnet,
180            },
181            environment: <_>::default(),
182            endpoints: <_>::default(),
183        }
184    }
185}
186
187#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
188pub struct HttpClientConfig {
189    pub timeout_in_secs: NonZeroU64,
190    pub gzip: bool,
191}
192
193#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
194pub struct SolanaClientConfig {
195    pub url: String,
196    pub cluster: SolanaNet,
197}
198
199impl SolanaClientConfig {
200    pub fn build_client(&self, http: Option<reqwest::Client>) -> RpcClient {
201        RpcClient::new_sender(
202            HttpSender::new_with_client(self.url.clone(), http.unwrap_or_default()),
203            RpcClientConfig {
204                commitment_config: CommitmentConfig::finalized(),
205                confirm_transaction_initial_timeout: Some(Duration::from_secs(180)),
206            },
207        )
208    }
209}
210
211impl From<Network> for SolanaClientConfig {
212    fn from(value: Network) -> Self {
213        Self {
214            url: value.url,
215            cluster: value.cluster,
216        }
217    }
218}
219
220impl Default for SolanaClientConfig {
221    fn default() -> Self {
222        let cluster = SolanaNet::Devnet;
223        Self {
224            url: cluster.url().to_owned(),
225            cluster,
226        }
227    }
228}
229
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
231pub enum SolanaNet {
232    #[serde(rename = "devnet")]
233    Devnet,
234    #[serde(rename = "testnet")]
235    Testnet,
236    #[serde(rename = "mainnet-beta")]
237    Mainnet,
238}
239
240/// Unknown Sonana network.
241#[derive(Debug, ThisError)]
242#[error("unknown network: {0}")]
243pub struct UnknownNetwork(pub String);
244
245impl FromStr for SolanaNet {
246    type Err = UnknownNetwork;
247
248    fn from_str(s: &str) -> Result<Self, Self::Err> {
249        match s {
250            "devnet" => Ok(Self::Devnet),
251            "testnet" => Ok(Self::Testnet),
252            "mainnet-beta" => Ok(Self::Mainnet),
253            s => Err(UnknownNetwork(s.to_owned())),
254        }
255    }
256}
257
258impl SolanaNet {
259    pub fn url(&self) -> String {
260        match self {
261            SolanaNet::Devnet => {
262                static URL: LazyLock<String> = LazyLock::new(|| {
263                    std::env::var("SOLANA_DEVNET_URL")
264                        .unwrap_or_else(|_| "https://api.devnet.solana.com".to_owned())
265                });
266                URL.clone()
267            }
268            SolanaNet::Testnet => {
269                static URL: LazyLock<String> = LazyLock::new(|| {
270                    std::env::var("SOLANA_TESTNET_URL")
271                        .unwrap_or_else(|_| "https://api.testnet.solana.com".to_owned())
272                });
273                URL.clone()
274            }
275            SolanaNet::Mainnet => {
276                static URL: LazyLock<String> = LazyLock::new(|| {
277                    std::env::var("SOLANA_MAINNET_URL")
278                        .unwrap_or_else(|_| "https://api.mainnet-beta.solana.com".to_owned())
279                });
280                URL.clone()
281            }
282        }
283    }
284
285    pub fn as_str(&self) -> &'static str {
286        match self {
287            SolanaNet::Devnet => "devnet",
288            SolanaNet::Testnet => "testnet",
289            SolanaNet::Mainnet => "mainnet-beta",
290        }
291    }
292
293    pub fn from_url(url: &str) -> Result<Self, UnknownNetwork> {
294        if url.contains("devnet") {
295            Ok(SolanaNet::Devnet)
296        } else if url.contains("testnet") {
297            Ok(SolanaNet::Testnet)
298        } else if url.contains("mainnet") {
299            Ok(SolanaNet::Mainnet)
300        } else {
301            Err(UnknownNetwork(url.to_owned()))
302        }
303    }
304}
305
306impl FlowConfig {
307    pub fn new(config: client::ClientConfig) -> Self {
308        fn get_name_from_id(names: &HashMap<Uuid, String>, id: &Uuid) -> Option<String> {
309            match names.get(id) {
310                Some(name) => Some(name.clone()),
311                None => {
312                    tracing::warn!("name not found for edge {}", id);
313                    None
314                }
315            }
316        }
317
318        let source_names = config
319            .nodes
320            .iter()
321            .flat_map(|n| n.data.sources.iter().map(|s| (s.id, s.name.clone())));
322        let target_names = config
323            .nodes
324            .iter()
325            .flat_map(|n| n.data.targets.iter().map(|s| (s.id, s.name.clone())));
326        let names = source_names.chain(target_names).collect::<HashMap<_, _>>();
327
328        let edges = config
329            .edges
330            .iter()
331            .filter_map(|e| {
332                let from: Gate = (e.source, get_name_from_id(&names, &e.source_handle.id)?);
333                let to: Gate = (e.target, get_name_from_id(&names, &e.target_handle)?);
334                Some((from, to))
335            })
336            .collect();
337
338        let nodes = config
339            .nodes
340            .into_iter()
341            .filter(|n| n.data.r#type != CommandType::Mock)
342            .map(|n| NodeConfig {
343                id: n.id,
344                command_name: n.data.node_id.clone(),
345                form_data: n.data.targets_form.form_data.clone(),
346                client_node_data: n.data,
347            })
348            .collect();
349
350        Self {
351            id: config.id,
352            ctx: ContextConfig {
353                http_client: HttpClientConfig {
354                    timeout_in_secs: NonZeroU64::new(100).unwrap(),
355                    gzip: true,
356                },
357                solana_client: SolanaClientConfig {
358                    url: config.sol_network.url,
359                    cluster: config.sol_network.cluster,
360                },
361                environment: config.environment,
362                endpoints: <_>::default(),
363            },
364            nodes,
365            edges,
366            instructions_bundling: config.instructions_bundling,
367        }
368    }
369}