1pub mod dca;
2pub mod kamino;
3pub mod limit_v1;
4pub mod limit_v2;
5
6use serde::{Deserialize, Serialize};
7
8use crate::error::Error;
9
10#[cfg(feature = "wasm")]
11pub const DCA_PROGRAM_ID: &str = "DCA265Vj8a9CEuX1eb1LWRnDT7uK6q1xMipnNyatn23M";
12#[cfg(feature = "wasm")]
13pub const LIMIT_V1_PROGRAM_ID: &str = "jupoNjAxXgZ4rjzxzPMP4oxduvQsQtZzyknqvzYNrNu";
14#[cfg(feature = "wasm")]
15pub const LIMIT_V2_PROGRAM_ID: &str = "j1o2qRpjcyUwEvwtcfhEQefh773ZgjxcVRry7LDqg5X";
16#[cfg(feature = "wasm")]
17pub const KAMINO_PROGRAM_ID: &str = "LiMoM9rMhrdYrfzUCxQppvxCSG1FcrUK9G8uLq4A1GF";
18
19#[derive(
21 Debug, Clone, Copy, PartialEq, Eq, Serialize, strum_macros::Display, strum_macros::AsRefStr,
22)]
23#[strum(serialize_all = "snake_case")]
24pub enum Protocol {
25 Dca,
27 LimitV1,
29 LimitV2,
31 Kamino,
33}
34
35impl Protocol {
36 #[cfg(feature = "native")]
38 pub fn from_program_id(program_id: &str) -> Option<Self> {
39 let key: solana_pubkey::Pubkey = program_id.parse().ok()?;
40 match key {
41 carbon_jupiter_dca_decoder::PROGRAM_ID => Some(Self::Dca),
42 carbon_jupiter_limit_order_decoder::PROGRAM_ID => Some(Self::LimitV1),
43 carbon_jupiter_limit_order_2_decoder::PROGRAM_ID => Some(Self::LimitV2),
44 carbon_kamino_limit_order_decoder::PROGRAM_ID => Some(Self::Kamino),
45 _ => None,
46 }
47 }
48
49 #[cfg(all(feature = "wasm", not(feature = "native")))]
50 pub fn from_program_id(program_id: &str) -> Option<Self> {
51 match program_id {
52 DCA_PROGRAM_ID => Some(Self::Dca),
53 LIMIT_V1_PROGRAM_ID => Some(Self::LimitV1),
54 LIMIT_V2_PROGRAM_ID => Some(Self::LimitV2),
55 KAMINO_PROGRAM_ID => Some(Self::Kamino),
56 _ => None,
57 }
58 }
59
60 #[cfg(feature = "native")]
62 pub fn all_program_ids() -> [solana_pubkey::Pubkey; 4] {
63 [
64 carbon_jupiter_dca_decoder::PROGRAM_ID,
65 carbon_jupiter_limit_order_decoder::PROGRAM_ID,
66 carbon_jupiter_limit_order_2_decoder::PROGRAM_ID,
67 carbon_kamino_limit_order_decoder::PROGRAM_ID,
68 ]
69 }
70
71 #[cfg(feature = "wasm")]
72 pub fn program_id_str(&self) -> &'static str {
73 match self {
74 Self::Dca => DCA_PROGRAM_ID,
75 Self::LimitV1 => LIMIT_V1_PROGRAM_ID,
76 Self::LimitV2 => LIMIT_V2_PROGRAM_ID,
77 Self::Kamino => KAMINO_PROGRAM_ID,
78 }
79 }
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, strum_macros::Display, strum_macros::AsRefStr)]
84#[strum(serialize_all = "snake_case")]
85pub enum EventType {
86 Created,
88 FillInitiated,
90 FillCompleted,
92 Cancelled,
94 Expired,
96 Closed,
98 FeeCollected,
100 Withdrawn,
102 Deposited,
104}
105
106#[derive(Debug, Deserialize)]
108pub struct AccountInfo {
109 pub pubkey: String,
111 #[serde(default)]
113 pub is_signer: bool,
114 #[serde(default)]
116 pub is_writable: bool,
117 pub name: Option<String>,
119}
120
121pub struct ProtocolHelpers;
123
124impl ProtocolHelpers {
125 pub fn parse_accounts(accounts_json: &serde_json::Value) -> Result<Vec<AccountInfo>, Error> {
127 serde_json::from_value(accounts_json.clone()).map_err(|e| Error::Protocol {
128 reason: format!("failed to parse accounts: {e}"),
129 })
130 }
131
132 pub fn find_signer(accounts: &[AccountInfo]) -> Option<&str> {
134 accounts
135 .iter()
136 .find(|a| a.is_signer)
137 .map(|a| a.pubkey.as_str())
138 }
139
140 pub fn find_account_by_name<'a>(
142 accounts: &'a [AccountInfo],
143 name: &str,
144 ) -> Option<&'a AccountInfo> {
145 accounts.iter().find(|a| a.name.as_deref() == Some(name))
146 }
147
148 pub fn contains_known_variant(fields: &serde_json::Value, known_names: &[&str]) -> bool {
150 fields
151 .as_object()
152 .is_some_and(|obj| obj.keys().any(|name| known_names.contains(&name.as_str())))
153 }
154
155 pub fn checked_u64_to_i64(value: u64, field: &str) -> Result<i64, Error> {
157 i64::try_from(value).map_err(|_| Error::Protocol {
158 reason: format!("{field} exceeds i64::MAX: {value}"),
159 })
160 }
161
162 pub fn checked_u16_to_i16(value: u16, field: &str) -> Result<i16, Error> {
164 i16::try_from(value).map_err(|_| Error::Protocol {
165 reason: format!("{field} exceeds i16::MAX: {value}"),
166 })
167 }
168}
169
170#[cfg(test)]
171#[expect(clippy::unwrap_used, reason = "test assertions")]
172mod tests {
173 use super::*;
174 use crate::lifecycle::adapters::{ProtocolAdapter, adapter_for};
175 use crate::types::{RawEvent, RawInstruction, ResolveContext};
176 use std::collections::HashSet;
177
178 #[cfg(feature = "native")]
179 #[test]
180 fn protocol_program_id_mapping_and_string_names_are_stable() {
181 let cases = [
182 (
183 &carbon_jupiter_dca_decoder::PROGRAM_ID,
184 Protocol::Dca,
185 "dca",
186 ),
187 (
188 &carbon_jupiter_limit_order_decoder::PROGRAM_ID,
189 Protocol::LimitV1,
190 "limit_v1",
191 ),
192 (
193 &carbon_jupiter_limit_order_2_decoder::PROGRAM_ID,
194 Protocol::LimitV2,
195 "limit_v2",
196 ),
197 (
198 &carbon_kamino_limit_order_decoder::PROGRAM_ID,
199 Protocol::Kamino,
200 "kamino",
201 ),
202 ];
203 for (program_id, expected_protocol, expected_name) in cases {
204 assert_eq!(
205 Protocol::from_program_id(&program_id.to_string()),
206 Some(expected_protocol)
207 );
208 assert_eq!(expected_protocol.as_ref(), expected_name);
209 }
210
211 assert_eq!(Protocol::from_program_id("unknown_program"), None);
212 assert_eq!(Protocol::from_program_id("not_even_base58!@#"), None);
213 assert_eq!(
214 Protocol::all_program_ids(),
215 [
216 carbon_jupiter_dca_decoder::PROGRAM_ID,
217 carbon_jupiter_limit_order_decoder::PROGRAM_ID,
218 carbon_jupiter_limit_order_2_decoder::PROGRAM_ID,
219 carbon_kamino_limit_order_decoder::PROGRAM_ID,
220 ]
221 );
222 }
223
224 #[cfg(all(feature = "native", feature = "wasm"))]
225 #[test]
226 fn hardcoded_program_ids_match_carbon_constants() {
227 assert_eq!(
228 carbon_jupiter_dca_decoder::PROGRAM_ID.to_string(),
229 DCA_PROGRAM_ID
230 );
231 assert_eq!(
232 carbon_jupiter_limit_order_decoder::PROGRAM_ID.to_string(),
233 LIMIT_V1_PROGRAM_ID
234 );
235 assert_eq!(
236 carbon_jupiter_limit_order_2_decoder::PROGRAM_ID.to_string(),
237 LIMIT_V2_PROGRAM_ID
238 );
239 assert_eq!(
240 carbon_kamino_limit_order_decoder::PROGRAM_ID.to_string(),
241 KAMINO_PROGRAM_ID
242 );
243 }
244
245 #[cfg(feature = "wasm")]
246 #[test]
247 fn protocol_program_id_str_roundtrips() {
248 for protocol in [
249 Protocol::Dca,
250 Protocol::LimitV1,
251 Protocol::LimitV2,
252 Protocol::Kamino,
253 ] {
254 assert_eq!(
255 Protocol::from_program_id(protocol.program_id_str()),
256 Some(protocol)
257 );
258 }
259 }
260
261 #[test]
262 fn event_type_strings_match_expected_labels() {
263 let cases = [
264 (EventType::Created, "created"),
265 (EventType::FillInitiated, "fill_initiated"),
266 (EventType::FillCompleted, "fill_completed"),
267 (EventType::Cancelled, "cancelled"),
268 (EventType::Expired, "expired"),
269 (EventType::Closed, "closed"),
270 (EventType::FeeCollected, "fee_collected"),
271 (EventType::Withdrawn, "withdrawn"),
272 (EventType::Deposited, "deposited"),
273 ];
274 for (event_type, expected_label) in cases {
275 assert_eq!(event_type.as_ref(), expected_label);
276 }
277 }
278
279 #[test]
280 fn parse_accounts_supports_defaults_and_find_helpers() {
281 let accounts_json = serde_json::json!([
282 {
283 "pubkey": "signer_pubkey",
284 "is_signer": true,
285 "is_writable": true,
286 "name": "order"
287 },
288 {
289 "pubkey": "readonly_pubkey"
290 }
291 ]);
292
293 let parsed = ProtocolHelpers::parse_accounts(&accounts_json).unwrap();
294 assert_eq!(parsed.len(), 2);
295 assert_eq!(parsed[0].pubkey, "signer_pubkey");
296 assert!(parsed[0].is_signer);
297 assert!(parsed[0].is_writable);
298 assert_eq!(parsed[0].name.as_deref(), Some("order"));
299
300 assert_eq!(parsed[1].pubkey, "readonly_pubkey");
301 assert!(!parsed[1].is_signer);
302 assert!(!parsed[1].is_writable);
303 assert!(parsed[1].name.is_none());
304
305 assert_eq!(ProtocolHelpers::find_signer(&parsed), Some("signer_pubkey"));
306 assert_eq!(
307 ProtocolHelpers::find_account_by_name(&parsed, "order").map(|a| a.pubkey.as_str()),
308 Some("signer_pubkey")
309 );
310 assert!(ProtocolHelpers::find_account_by_name(&parsed, "missing").is_none());
311 }
312
313 #[test]
314 fn parse_accounts_rejects_non_array() {
315 let err = ProtocolHelpers::parse_accounts(&serde_json::json!({"pubkey": "not-an-array"}))
316 .unwrap_err();
317 let Error::Protocol { reason } = err else {
318 panic!("expected protocol error");
319 };
320 assert!(reason.contains("failed to parse accounts"), "{reason}");
321 }
322
323 #[test]
324 fn parse_accounts_rejects_missing_pubkey() {
325 let err =
326 ProtocolHelpers::parse_accounts(&serde_json::json!([{"is_signer": true}])).unwrap_err();
327 let Error::Protocol { reason } = err else {
328 panic!("expected protocol error");
329 };
330 assert!(reason.contains("failed to parse accounts"), "{reason}");
331 }
332
333 #[test]
334 fn find_signer_returns_none_when_no_signer_present() {
335 let accounts = vec![AccountInfo {
336 pubkey: "p1".to_string(),
337 is_signer: false,
338 is_writable: false,
339 name: Some("order".to_string()),
340 }];
341
342 assert_eq!(ProtocolHelpers::find_signer(&accounts), None);
343 }
344
345 fn make_ix(name: &str) -> RawInstruction {
346 RawInstruction {
347 id: 1,
348 signature: "sig".to_string(),
349 instruction_index: 0,
350 program_id: "p".to_string(),
351 inner_program_id: "p".to_string(),
352 instruction_name: name.to_string(),
353 accounts: None,
354 args: None,
355 slot: 1,
356 }
357 }
358
359 fn collect_instruction_event_types(
360 instruction_names: &[&str],
361 adapter: &dyn ProtocolAdapter,
362 ) -> HashSet<String> {
363 instruction_names
364 .iter()
365 .filter_map(|name| adapter.classify_instruction(&make_ix(name)))
366 .map(|et| et.as_ref().to_string())
367 .collect()
368 }
369
370 fn make_event(fields: serde_json::Value) -> RawEvent {
371 RawEvent {
372 id: 1,
373 signature: "sig".to_string(),
374 event_index: 0,
375 program_id: "p".to_string(),
376 inner_program_id: "p".to_string(),
377 event_name: "test".to_string(),
378 fields: Some(fields),
379 slot: 1,
380 }
381 }
382
383 fn resolve_event_type(
384 json: serde_json::Value,
385 adapter: &dyn ProtocolAdapter,
386 ctx: &ResolveContext,
387 ) -> Option<String> {
388 adapter
389 .classify_and_resolve_event(&make_event(json), ctx)
390 .and_then(|r| r.ok())
391 .map(|(et, _, _)| et.as_ref().to_string())
392 }
393
394 #[test]
395 fn event_type_reachability_all_variants_covered() {
396 let mut all_event_types: HashSet<String> = HashSet::new();
397 let default_ctx = ResolveContext {
398 pre_fetched_order_pdas: None,
399 };
400
401 let dca = adapter_for(Protocol::Dca);
402 let dca_ix_names = [
403 "OpenDca",
404 "OpenDcaV2",
405 "InitiateFlashFill",
406 "InitiateDlmmFill",
407 "FulfillFlashFill",
408 "FulfillDlmmFill",
409 "CloseDca",
410 "EndAndClose",
411 "Transfer",
412 "Deposit",
413 "Withdraw",
414 "WithdrawFees",
415 ];
416 all_event_types.extend(collect_instruction_event_types(&dca_ix_names, dca));
417
418 let dca_event_payloads = [
419 serde_json::json!({"OpenedEvent": {"dca_key": "t"}}),
420 serde_json::json!({"FilledEvent": {"dca_key": "t", "in_amount": 1_u64, "out_amount": 1_u64}}),
421 serde_json::json!({"ClosedEvent": {"dca_key": "t", "user_closed": false, "unfilled_amount": 0_u64}}),
422 serde_json::json!({"CollectedFeeEvent": {"dca_key": "t"}}),
423 serde_json::json!({"WithdrawEvent": {"dca_key": "t"}}),
424 serde_json::json!({"DepositEvent": {"dca_key": "t"}}),
425 ];
426 for json in &dca_event_payloads {
427 if let Some(et) = resolve_event_type(json.clone(), dca, &default_ctx) {
428 all_event_types.insert(et);
429 }
430 }
431
432 let v1 = adapter_for(Protocol::LimitV1);
433 let v1_ix_names = [
434 "InitializeOrder",
435 "PreFlashFillOrder",
436 "FillOrder",
437 "FlashFillOrder",
438 "CancelOrder",
439 "CancelExpiredOrder",
440 "WithdrawFee",
441 "InitFee",
442 "UpdateFee",
443 ];
444 all_event_types.extend(collect_instruction_event_types(&v1_ix_names, v1));
445
446 let v1_event_payloads = [
447 serde_json::json!({"CreateOrderEvent": {"order_key": "t"}}),
448 serde_json::json!({"CancelOrderEvent": {"order_key": "t"}}),
449 serde_json::json!({"TradeEvent": {"order_key": "t", "in_amount": 1_u64, "out_amount": 1_u64, "remaining_in_amount": 0_u64, "remaining_out_amount": 0_u64}}),
450 ];
451 for json in &v1_event_payloads {
452 if let Some(et) = resolve_event_type(json.clone(), v1, &default_ctx) {
453 all_event_types.insert(et);
454 }
455 }
456
457 let v2 = adapter_for(Protocol::LimitV2);
458 let v2_ix_names = [
459 "InitializeOrder",
460 "PreFlashFillOrder",
461 "FlashFillOrder",
462 "CancelOrder",
463 "UpdateFee",
464 "WithdrawFee",
465 ];
466 all_event_types.extend(collect_instruction_event_types(&v2_ix_names, v2));
467
468 let v2_event_payloads = [
469 serde_json::json!({"CreateOrderEvent": {"order_key": "t"}}),
470 serde_json::json!({"CancelOrderEvent": {"order_key": "t"}}),
471 serde_json::json!({"TradeEvent": {"order_key": "t", "making_amount": 1_u64, "taking_amount": 1_u64, "remaining_making_amount": 0_u64, "remaining_taking_amount": 0_u64}}),
472 ];
473 for json in &v2_event_payloads {
474 if let Some(et) = resolve_event_type(json.clone(), v2, &default_ctx) {
475 all_event_types.insert(et);
476 }
477 }
478
479 let kamino = adapter_for(Protocol::Kamino);
480 let kamino_ix_names = [
481 "CreateOrder",
482 "TakeOrder",
483 "FlashTakeOrderStart",
484 "FlashTakeOrderEnd",
485 "CloseOrderAndClaimTip",
486 "InitializeGlobalConfig",
487 "InitializeVault",
488 "UpdateGlobalConfig",
489 "UpdateGlobalConfigAdmin",
490 "WithdrawHostTip",
491 "LogUserSwapBalances",
492 ];
493 all_event_types.extend(collect_instruction_event_types(&kamino_ix_names, kamino));
494
495 let kamino_ctx = ResolveContext {
496 pre_fetched_order_pdas: Some(vec!["test_pda".to_string()]),
497 };
498 let kamino_event_payloads = [
499 serde_json::json!({"OrderDisplayEvent": {"status": 1_u8}}),
500 serde_json::json!({"UserSwapBalancesEvent": {}}),
501 ];
502 for json in &kamino_event_payloads {
503 if let Some(et) = resolve_event_type(json.clone(), kamino, &kamino_ctx) {
504 all_event_types.insert(et);
505 }
506 }
507
508 let expected: HashSet<String> = [
509 "created",
510 "fill_initiated",
511 "fill_completed",
512 "cancelled",
513 "expired",
514 "closed",
515 "fee_collected",
516 "withdrawn",
517 "deposited",
518 ]
519 .into_iter()
520 .map(String::from)
521 .collect();
522
523 assert_eq!(
524 all_event_types,
525 expected,
526 "missing EventTypes: {:?}, extra: {:?}",
527 expected.difference(&all_event_types).collect::<Vec<_>>(),
528 all_event_types.difference(&expected).collect::<Vec<_>>()
529 );
530 }
531}