Skip to main content

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 WalletId = i64;
75pub type FlowId = i32;
76pub type NodeId = Uuid;
77pub type FlowRunId = Uuid;
78
79/// Command name and field name,
80pub type Name = String;
81
82/// Inputs and outputs of commands
83pub type ValueSet = value::Map;
84
85#[derive(
86    Debug,
87    Clone,
88    Copy,
89    PartialEq,
90    Eq,
91    PartialOrd,
92    Ord,
93    Serialize,
94    Deserialize,
95    bincode::Encode,
96    bincode::Decode,
97)]
98pub enum CommandType {
99    #[serde(rename = "native")]
100    Native,
101    #[serde(rename = "mock")]
102    Mock,
103    #[serde(rename = "WASM")]
104    Wasm,
105    #[serde(rename = "deno")]
106    Deno,
107}
108
109#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
110pub struct CmdInputDescription {
111    pub name: Name,
112    pub type_bounds: Vec<ValueType>,
113    pub required: bool,
114    pub passthrough: bool,
115}
116
117#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
118pub struct CmdOutputDescription {
119    pub name: Name,
120    pub r#type: ValueType,
121    #[serde(default = "value::default::bool_false")]
122    pub optional: bool,
123}
124
125/// An input or output gate of a node
126pub type Gate = (NodeId, Name);
127
128#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
129pub struct FlowConfig {
130    pub id: FlowId,
131    pub ctx: ContextConfig,
132    pub nodes: Vec<NodeConfig>,
133    pub edges: Vec<(Gate, Gate)>,
134    #[serde(default)]
135    pub instructions_bundling: client::BundlingMode,
136}
137
138#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
139pub struct NodeConfig {
140    pub id: NodeId,
141    pub command_name: Name,
142    pub form_data: JsonValue,
143    pub client_node_data: client::NodeData,
144}
145
146#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
147pub struct Endpoints {
148    pub flow_server: String,
149    pub supabase: String,
150    pub supabase_anon_key: String,
151}
152
153impl Default for Endpoints {
154    fn default() -> Self {
155        Self {
156            flow_server: "http://localhost:8080".to_owned(),
157            supabase: "http://localhost:8081".to_owned(),
158            supabase_anon_key: String::new(),
159        }
160    }
161}
162
163#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
164pub struct ContextConfig {
165    pub http_client: HttpClientConfig,
166    pub solana_client: SolanaClientConfig,
167    pub environment: HashMap<String, String>,
168    pub endpoints: Endpoints,
169}
170
171impl Default for ContextConfig {
172    fn default() -> Self {
173        ContextConfig {
174            http_client: HttpClientConfig {
175                timeout_in_secs: NonZeroU64::new(100).unwrap(),
176                gzip: true,
177            },
178            solana_client: SolanaClientConfig {
179                url: SolanaNet::Devnet.url(),
180                cluster: SolanaNet::Devnet,
181            },
182            environment: <_>::default(),
183            endpoints: <_>::default(),
184        }
185    }
186}
187
188#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
189pub struct HttpClientConfig {
190    pub timeout_in_secs: NonZeroU64,
191    pub gzip: bool,
192}
193
194#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
195pub struct SolanaClientConfig {
196    pub url: String,
197    pub cluster: SolanaNet,
198}
199
200impl SolanaClientConfig {
201    pub fn build_client(&self, http: Option<reqwest::Client>) -> RpcClient {
202        RpcClient::new_sender(
203            HttpSender::new_with_client(self.url.clone(), http.unwrap_or_default()),
204            RpcClientConfig {
205                commitment_config: CommitmentConfig::finalized(),
206                confirm_transaction_initial_timeout: Some(Duration::from_secs(180)),
207            },
208        )
209    }
210}
211
212impl From<Network> for SolanaClientConfig {
213    fn from(value: Network) -> Self {
214        Self {
215            url: value.url,
216            cluster: value.cluster,
217        }
218    }
219}
220
221impl Default for SolanaClientConfig {
222    fn default() -> Self {
223        let cluster = SolanaNet::Devnet;
224        Self {
225            url: cluster.url().to_owned(),
226            cluster,
227        }
228    }
229}
230
231#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
232pub enum SolanaNet {
233    #[serde(rename = "devnet")]
234    Devnet,
235    #[serde(rename = "testnet")]
236    Testnet,
237    #[serde(rename = "mainnet-beta")]
238    Mainnet,
239}
240
241/// Unknown Sonana network.
242#[derive(Debug, ThisError)]
243#[error("unknown network: {0}")]
244pub struct UnknownNetwork(pub String);
245
246impl FromStr for SolanaNet {
247    type Err = UnknownNetwork;
248
249    fn from_str(s: &str) -> Result<Self, Self::Err> {
250        match s {
251            "devnet" => Ok(Self::Devnet),
252            "testnet" => Ok(Self::Testnet),
253            "mainnet-beta" => Ok(Self::Mainnet),
254            s => Err(UnknownNetwork(s.to_owned())),
255        }
256    }
257}
258
259impl SolanaNet {
260    pub fn url(&self) -> String {
261        match self {
262            SolanaNet::Devnet => {
263                static URL: LazyLock<String> = LazyLock::new(|| {
264                    std::env::var("SOLANA_DEVNET_URL")
265                        .unwrap_or_else(|_| "https://api.devnet.solana.com".to_owned())
266                });
267                URL.clone()
268            }
269            SolanaNet::Testnet => "https://api.testnet.solana.com".to_owned(),
270            SolanaNet::Mainnet => "https://api.mainnet-beta.solana.com".to_owned(),
271        }
272    }
273
274    pub fn as_str(&self) -> &'static str {
275        match self {
276            SolanaNet::Devnet => "devnet",
277            SolanaNet::Testnet => "testnet",
278            SolanaNet::Mainnet => "mainnet-beta",
279        }
280    }
281
282    pub fn from_url(url: &str) -> Result<Self, UnknownNetwork> {
283        if url.contains("devnet") {
284            Ok(SolanaNet::Devnet)
285        } else if url.contains("testnet") {
286            Ok(SolanaNet::Testnet)
287        } else if url.contains("mainnet") {
288            Ok(SolanaNet::Mainnet)
289        } else {
290            Err(UnknownNetwork(url.to_owned()))
291        }
292    }
293}
294
295impl FlowConfig {
296    pub fn new(config: client::ClientConfig) -> Self {
297        fn get_name_from_id(names: &HashMap<Uuid, String>, id: &Uuid) -> Option<String> {
298            match names.get(id) {
299                Some(name) => Some(name.clone()),
300                None => {
301                    tracing::warn!("name not found for edge {}", id);
302                    None
303                }
304            }
305        }
306
307        let source_names = config
308            .nodes
309            .iter()
310            .flat_map(|n| n.data.sources.iter().map(|s| (s.id, s.name.clone())));
311        let target_names = config
312            .nodes
313            .iter()
314            .flat_map(|n| n.data.targets.iter().map(|s| (s.id, s.name.clone())));
315        let names = source_names.chain(target_names).collect::<HashMap<_, _>>();
316
317        let edges = config
318            .edges
319            .iter()
320            .filter_map(|e| {
321                let from: Gate = (e.source, get_name_from_id(&names, &e.source_handle.id)?);
322                let to: Gate = (e.target, get_name_from_id(&names, &e.target_handle)?);
323                Some((from, to))
324            })
325            .collect();
326
327        let nodes = config
328            .nodes
329            .into_iter()
330            .filter(|n| n.data.r#type != CommandType::Mock)
331            .map(|n| NodeConfig {
332                id: n.id,
333                command_name: n.data.node_id.clone(),
334                form_data: n.data.targets_form.form_data.clone(),
335                client_node_data: n.data,
336            })
337            .collect();
338
339        Self {
340            id: config.id,
341            ctx: ContextConfig {
342                http_client: HttpClientConfig {
343                    timeout_in_secs: NonZeroU64::new(100).unwrap(),
344                    gzip: true,
345                },
346                solana_client: SolanaClientConfig {
347                    url: config.sol_network.url,
348                    cluster: config.sol_network.cluster,
349                },
350                environment: config.environment,
351                endpoints: <_>::default(),
352            },
353            nodes,
354            edges,
355            instructions_bundling: config.instructions_bundling,
356        }
357    }
358}