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#[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 #[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
78pub type Name = String;
80
81pub 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
124pub 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#[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}