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 WalletId = i64;
75pub type FlowId = i32;
76pub type NodeId = Uuid;
77pub type FlowRunId = Uuid;
78
79pub type Name = String;
81
82pub 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
125pub 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#[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}