simulator-api 0.7.2

Wire-protocol types for the Solana simulator backtest WebSocket API
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
use std::{
    collections::{BTreeMap, BTreeSet, HashSet},
    fmt,
};

use base64::{
    DecodeError as Base64DecodeError, Engine as _, engine::general_purpose::STANDARD as BASE64,
};
use serde::{Deserialize, Serialize};
use solana_address::Address;

/// Backtest RPC methods exposed to the client.
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum BacktestRequest {
    CreateBacktestSession(CreateBacktestSessionRequest),
    Continue(ContinueParams),
    ContinueTo(ContinueToParams),
    ContinueSessionV1(ContinueSessionRequestV1),
    ContinueToSessionV1(ContinueToSessionRequestV1),
    CloseBacktestSession,
    CloseSessionV1(CloseSessionRequestV1),
    AttachBacktestSession {
        session_id: String,
        /// Last sequence number the client received. Responses after this sequence
        /// will be replayed from the session's buffer. None = replay entire buffer.
        last_sequence: Option<u64>,
    },
    /// Sent after reattaching and rebuilding any dependent subscriptions.
    /// Allows the manager to resume a session that was paused for handoff.
    ResumeAttachedSession,
    AttachParallelControlSessionV2 {
        control_session_id: String,
        /// Last per-session sequence number received by the client. Responses after
        /// these sequence numbers will be replayed from the manager's per-session
        /// replay store. Missing sessions replay their entire retained history.
        #[serde(default)]
        last_sequences: BTreeMap<String, u64>,
    },
}

/// Versioned payload for `CreateBacktestSession`.
///
/// - `V0` keeps backwards-compatible shape by using `CreateSessionParams` directly.
/// - `V1` keeps the same shape and adds `parallel`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(untagged)]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum CreateBacktestSessionRequest {
    V1(CreateBacktestSessionRequestV1),
    V0(CreateSessionParams),
}

impl CreateBacktestSessionRequest {
    pub fn into_request_options(self) -> CreateBacktestSessionRequestOptions {
        match self {
            Self::V0(request) => CreateBacktestSessionRequestOptions {
                request,
                parallel: false,
            },
            Self::V1(CreateBacktestSessionRequestV1 { request, parallel }) => {
                CreateBacktestSessionRequestOptions { request, parallel }
            }
        }
    }

    pub fn into_request_and_parallel(self) -> (CreateSessionParams, bool) {
        let options = self.into_request_options();
        (options.request, options.parallel)
    }
}

impl From<CreateSessionParams> for CreateBacktestSessionRequest {
    fn from(value: CreateSessionParams) -> Self {
        Self::V0(value)
    }
}

impl From<CreateBacktestSessionRequestV1> for CreateBacktestSessionRequest {
    fn from(value: CreateBacktestSessionRequestV1) -> Self {
        Self::V1(value)
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct CreateBacktestSessionRequestV1 {
    #[serde(flatten)]
    pub request: CreateSessionParams,
    pub parallel: bool,
}

#[derive(Debug, Clone)]
pub struct CreateBacktestSessionRequestOptions {
    pub request: CreateSessionParams,
    pub parallel: bool,
}

#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct ContinueSessionRequestV1 {
    pub session_id: String,
    pub request: ContinueParams,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct ContinueToSessionRequestV1 {
    pub session_id: String,
    pub request: ContinueToParams,
}

#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct CloseSessionRequestV1 {
    pub session_id: String,
}

/// A filter registered at session creation describing which upcoming batches
/// the session should announce ahead of execution via
/// [`BacktestResponse::DiscoveryBatch`] (and its session-event twins). Each
/// filter describes an event of interest (e.g. a specific program executing);
/// the session "discovers" the batch in which that event will occur and
/// emits a `DiscoveryBatch` so the client can pause immediately before it.
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(tag = "kind", content = "value", rename_all = "camelCase")]
pub enum DiscoveryFilter {
    /// Discover batches containing a transaction that invokes this program.
    ProgramExecuted(
        #[serde_as(as = "serde_with::DisplayFromStr")]
        #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
        Address,
    ),
}

/// Per-transaction facts a [`DiscoveryFilter`] can inspect when deciding
/// whether to match. Callers build this once per transaction and feed it to
/// every registered filter; new variants add fields here rather than growing
/// the [`DiscoveryFilter::matches`] signature.
pub struct TxMatchContext<'a> {
    /// Programs invoked by the transaction (top-level + CPI observed in logs).
    pub invoked_programs: &'a HashSet<Address>,
}

impl DiscoveryFilter {
    /// Return `true` when this filter is satisfied by the transaction
    /// described by `ctx`.
    pub fn matches(&self, ctx: &TxMatchContext<'_>) -> bool {
        match self {
            Self::ProgramExecuted(target) => ctx.invoked_programs.contains(target),
        }
    }
}

/// Parameters required to start a new backtest session.
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct CreateSessionParams {
    /// First slot (inclusive) to replay.
    pub start_slot: u64,
    /// Last slot (inclusive) to replay.
    pub end_slot: u64,
    #[serde_as(as = "BTreeSet<serde_with::DisplayFromStr>")]
    #[serde(default)]
    #[cfg_attr(feature = "ts-rs", ts(as = "Vec<String>"))]
    /// Skip transactions signed by these addresses.
    pub signer_filter: BTreeSet<Address>,
    /// When true, include a session summary with transaction statistics in client-facing
    /// `Completed` responses. Summary generation remains enabled internally for metrics.
    #[serde(default)]
    pub send_summary: bool,
    /// Maximum seconds to wait for ECS capacity-related startup retries before
    /// failing session creation. If not set (or 0), capacity errors fail immediately.
    #[serde(default)]
    #[cfg_attr(feature = "ts-rs", ts(optional))]
    pub capacity_wait_timeout_secs: Option<u16>,
    /// Maximum seconds to keep the session alive after the control websocket disconnects.
    /// If not set (or 0), the session tears down immediately on disconnect.
    /// Maximum value: 900 (15 minutes).
    #[serde(default)]
    pub disconnect_timeout_secs: Option<u16>,
    /// Extra compute units to add to each transaction's `SetComputeUnitLimit` budget.
    /// Useful when replaying with an account override whose program uses more CU than
    /// the original, causing otherwise-healthy transactions to run out of budget.
    /// Only applied when a `SetComputeUnitLimit` instruction is already present.
    #[serde(default)]
    pub extra_compute_units: Option<u32>,
    /// Agent configurations to run as sidecars alongside this session.
    #[serde(default)]
    pub agents: Vec<AgentParams>,
    /// Events of interest the session should watch for. When an upcoming
    /// batch matches any filter, the server emits
    /// [`BacktestResponse::DiscoveryBatch`] (and its session-event twins)
    /// ahead of execution so the client can follow up with
    /// [`BacktestRequest::ContinueTo`] to pause before the batch. Empty
    /// means no batch discoveries are performed (existing behaviour).
    #[serde(default, skip_serializing_if = "Vec::is_empty")]
    pub discoveries: Vec<DiscoveryFilter>,
}

/// Available agent types for sidecar participation in backtest sessions.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum AgentType {
    Arb,
}

/// Parameters for a circular arbitrage route.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct ArbRouteParams {
    pub base_mint: String,
    pub temp_mint: String,
    #[serde(default)]
    pub buy_dexes: Vec<String>,
    #[serde(default)]
    pub sell_dexes: Vec<String>,
    pub min_input: u64,
    pub max_input: u64,
    #[serde(default)]
    pub min_profit: u64,
}

/// Configuration for an agent to run alongside a backtest session.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct AgentParams {
    pub agent_type: AgentType,
    pub wallet: Option<String>,
    /// Base58-encoded 64-byte keypair for signing transactions (compatible with `solana-keygen`).
    pub keypair: Option<String>,
    pub seed_sol_lamports: Option<u64>,
    #[serde(default)]
    pub seed_token_accounts: BTreeMap<String, u64>,
    #[serde(default)]
    pub arb_routes: Vec<ArbRouteParams>,
}

/// Account state modifications to apply.
#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize, Default)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
pub struct AccountModifications(
    #[serde_as(as = "BTreeMap<serde_with::DisplayFromStr, _>")]
    #[serde(default)]
    #[cfg_attr(feature = "ts-rs", ts(as = "BTreeMap<String, AccountData>"))]
    pub BTreeMap<Address, AccountData>,
);

/// Arguments used to continue an existing session.
#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct ContinueParams {
    #[serde(default = "ContinueParams::default_advance_count")]
    /// Number of blocks to advance before waiting.
    pub advance_count: u64,
    #[serde(default)]
    /// Base64-encoded transactions to execute before advancing.
    pub transactions: Vec<String>,
    #[serde(default)]
    /// Account state overrides to apply ahead of execution.
    pub modify_account_states: AccountModifications,
}

impl Default for ContinueParams {
    fn default() -> Self {
        Self {
            advance_count: Self::default_advance_count(),
            transactions: Vec::new(),
            modify_account_states: AccountModifications(BTreeMap::new()),
        }
    }
}

impl ContinueParams {
    pub fn default_advance_count() -> u64 {
        1
    }
}

/// Payload emitted when a session halts at a caller-specified point.
/// `batch_index` is `None` for block-boundary pauses and `Some(n)` when the
/// session stopped *before* batch `n` within `slot` (no transaction from
/// batch `n` has been applied). While paused, RPC reads against the session
/// observe partial state up through batch `n - 1`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(rename_all = "camelCase")]
pub struct PausedEvent {
    pub slot: u64,
    #[serde(default, skip_serializing_if = "Option::is_none")]
    pub batch_index: Option<u32>,
}

/// Payload emitted when the session has *discovered* an upcoming batch that
/// matches one or more registered [`DiscoveryFilter`]s from session creation
/// (for example, a batch containing a transaction that invokes a program of
/// interest). The `(slot, batch_index)` pair can be fed directly to
/// [`BacktestRequest::ContinueTo`] to pause immediately before the batch
/// executes. After each `Continue` / `ContinueTo`, the session emits the
/// next `DiscoveryBatchEvent` ahead of the next matching batch, enabling a
/// reactive "pause on every discovery" driver loop.
#[serde_with::serde_as]
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(rename_all = "camelCase")]
pub struct DiscoveryBatchEvent {
    pub slot: u64,
    pub batch_index: u32,
    /// Filters that matched this batch (always non-empty).
    pub matched: Vec<DiscoveryFilter>,
    /// Encoded transactions in this batch that triggered the match. Each
    /// entry carries the serialized `VersionedTransaction` bytes paired with
    /// the encoding used.
    pub transactions: Vec<EncodedBinary>,
}

/// Arguments used to step an existing session to a precise point.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[cfg_attr(feature = "ts-rs", ts(export))]
#[serde(rename_all = "camelCase")]
pub struct ContinueToParams {
    /// Target slot to stop in (or at, if `batch_index` is `None`).
    pub slot: u64,
    /// Batch within the target slot at which to pause, **exclusive** — the
    /// session halts immediately *before* batch `n` executes, so no
    /// transaction in that batch has been applied yet. `None` runs the
    /// whole slot, pausing at the block boundary. While paused, RPC reads
    /// observe partial state up through batch `n - 1`.
    #[serde(default)]
    pub batch_index: Option<u32>,
}

/// Supported binary encodings for account/transaction payloads.
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "lowercase")]
pub enum BinaryEncoding {
    Base64,
}

impl BinaryEncoding {
    pub fn encode(self, bytes: &[u8]) -> String {
        match self {
            Self::Base64 => BASE64.encode(bytes),
        }
    }

    pub fn decode(self, data: &str) -> Result<Vec<u8>, Base64DecodeError> {
        match self {
            Self::Base64 => BASE64.decode(data),
        }
    }
}

/// A blob paired with the encoding needed to decode it.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct EncodedBinary {
    /// Encoded payload.
    pub data: String,
    /// Encoding scheme used for the payload.
    pub encoding: BinaryEncoding,
}

impl EncodedBinary {
    pub fn new(data: String, encoding: BinaryEncoding) -> Self {
        Self { data, encoding }
    }

    pub fn from_bytes(bytes: &[u8], encoding: BinaryEncoding) -> Self {
        Self {
            data: encoding.encode(bytes),
            encoding,
        }
    }

    pub fn decode(&self) -> Result<Vec<u8>, Base64DecodeError> {
        self.encoding.decode(&self.data)
    }
}

/// Account snapshot used to seed or modify state in a session.
#[serde_with::serde_as]
#[derive(Debug, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct AccountData {
    /// Account data bytes and encoding.
    pub data: EncodedBinary,
    /// Whether the account is executable.
    pub executable: bool,
    /// Lamport balance.
    pub lamports: u64,
    #[serde_as(as = "serde_with::DisplayFromStr")]
    #[cfg_attr(feature = "ts-rs", ts(as = "String"))]
    /// Account owner pubkey.
    pub owner: Address,
    /// Allocated space in bytes.
    pub space: u64,
}

/// Responses returned over the backtest RPC channel.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum BacktestResponse {
    SessionCreated {
        session_id: String,
        rpc_endpoint: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        task_id: Option<String>,
    },
    SessionAttached {
        session_id: String,
        rpc_endpoint: String,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        task_id: Option<String>,
    },
    SessionsCreated {
        session_ids: Vec<String>,
    },
    SessionsCreatedV2 {
        control_session_id: String,
        session_ids: Vec<String>,
        #[serde(default)]
        task_ids: Vec<Option<String>>,
    },
    ParallelSessionAttachedV2 {
        control_session_id: String,
        session_ids: Vec<String>,
        #[serde(default)]
        task_ids: Vec<Option<String>>,
    },
    ReadyForContinue,
    SlotNotification(u64),
    Paused(PausedEvent),
    DiscoveryBatch(DiscoveryBatchEvent),
    Error(BacktestError),
    Success,
    Completed {
        /// Session summary with transaction statistics.
        /// The session always computes this summary, but management may omit it from
        /// client-facing responses unless `send_summary` was requested at session creation.
        #[serde(skip_serializing_if = "Option::is_none")]
        summary: Option<SessionSummary>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        agent_stats: Option<Vec<AgentStatsReport>>,
    },
    Status {
        status: BacktestStatus,
    },
    SessionEventV1 {
        session_id: String,
        event: SessionEventV1,
    },
    SessionEventV2 {
        session_id: String,
        seq_id: u64,
        event: SessionEventKind,
    },
}

impl BacktestResponse {
    pub fn is_completed(&self) -> bool {
        matches!(self, BacktestResponse::Completed { .. })
    }

    pub fn is_terminal(&self) -> bool {
        match self {
            BacktestResponse::Completed { .. } => true,
            BacktestResponse::Error(e) => matches!(
                e,
                BacktestError::NoMoreBlocks
                    | BacktestError::AdvanceSlotFailed { .. }
                    | BacktestError::Internal { .. }
            ),
            _ => false,
        }
    }
}

impl From<BacktestStatus> for BacktestResponse {
    fn from(status: BacktestStatus) -> Self {
        Self::Status { status }
    }
}

impl From<String> for BacktestResponse {
    fn from(message: String) -> Self {
        BacktestError::Internal { error: message }.into()
    }
}

impl From<&str> for BacktestResponse {
    fn from(message: &str) -> Self {
        BacktestError::Internal {
            error: message.to_string(),
        }
        .into()
    }
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum SessionEventV1 {
    ReadyForContinue,
    SlotNotification(u64),
    Paused(PausedEvent),
    DiscoveryBatch(DiscoveryBatchEvent),
    Error(BacktestError),
    Success,
    Completed {
        #[serde(skip_serializing_if = "Option::is_none")]
        summary: Option<SessionSummary>,
        #[serde(default, skip_serializing_if = "Option::is_none")]
        agent_stats: Option<Vec<AgentStatsReport>>,
    },
    Status {
        status: BacktestStatus,
    },
}

#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(tag = "method", content = "params", rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub enum SessionEventKind {
    ReadyForContinue,
    SlotNotification(u64),
    Paused(PausedEvent),
    DiscoveryBatch(DiscoveryBatchEvent),
    Error(BacktestError),
    Success,
    Completed {
        #[serde(skip_serializing_if = "Option::is_none")]
        summary: Option<SessionSummary>,
    },
    Status {
        status: BacktestStatus,
    },
}

impl SessionEventKind {
    pub fn is_terminal(&self) -> bool {
        match self {
            Self::Completed { .. } => true,
            Self::Error(e) => matches!(
                e,
                BacktestError::NoMoreBlocks
                    | BacktestError::AdvanceSlotFailed { .. }
                    | BacktestError::Internal { .. }
            ),
            _ => false,
        }
    }
}

/// Wire format wrapper for responses sent over the control websocket.
/// Sessions with `disconnect_timeout_secs > 0` use this to track client position.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct SequencedResponse {
    pub seq_id: u64,
    #[serde(flatten)]
    pub response: BacktestResponse,
}

/// High-level progress states during a `Continue` call.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub enum BacktestStatus {
    /// Runtime startup is in progress.
    StartingRuntime,
    DecodedTransactions,
    AppliedAccountModifications,
    ReadyToExecuteUserTransactions,
    ExecutedUserTransactions,
    ExecutingBlockTransactions,
    ExecutedBlockTransactions,
    ProgramAccountsLoaded,
}

impl std::fmt::Display for BacktestStatus {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = match self {
            Self::StartingRuntime => "starting runtime",
            Self::DecodedTransactions => "decoded transactions",
            Self::AppliedAccountModifications => "applied account modifications",
            Self::ReadyToExecuteUserTransactions => "ready to execute user transactions",
            Self::ExecutedUserTransactions => "executed user transactions",
            Self::ExecutingBlockTransactions => "executing block transactions",
            Self::ExecutedBlockTransactions => "executed block transactions",
            Self::ProgramAccountsLoaded => "program accounts loaded",
        };
        f.write_str(s)
    }
}

/// Structured stats reported by an agent during a backtest session.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "ts-rs", ts(export))]
pub struct AgentStatsReport {
    pub name: String,
    pub slots_processed: u64,
    pub opportunities_found: u64,
    pub opportunities_skipped: u64,
    pub no_routes: u64,
    pub txs_produced: u64,
    /// Cumulative expected profit per base mint, keyed by mint address.
    pub expected_gain_by_mint: BTreeMap<String, i64>,
    /// Transactions successfully executed by the sidecar.
    #[serde(default)]
    pub txs_submitted: u64,
    /// Transactions that failed execution.
    #[serde(default)]
    pub txs_failed: u64,
    /// Transactions rejected by preflight simulation (unprofitable).
    #[serde(default)]
    pub txs_simulation_rejected: u64,
    /// Preflight simulation RPC calls that errored.
    #[serde(default)]
    pub txs_simulation_failed: u64,
}

/// Summary of transaction execution statistics for a completed backtest session.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub struct SessionSummary {
    /// Number of simulations where simulator outcome matched on-chain outcome
    /// (`true_success + true_failure`).
    pub correct_simulation: usize,
    /// Number of simulations where simulator outcome did not match on-chain outcome
    /// (`false_success + false_failure`).
    pub incorrect_simulation: usize,
    /// Number of transactions that had execution errors in simulation.
    pub execution_errors: usize,
    /// Number of transactions with different balance diffs.
    pub balance_diff: usize,
    /// Number of transactions with different log diffs.
    pub log_diff: usize,
}

impl SessionSummary {
    /// Returns true if there were any execution deviations (errors or mismatched results).
    pub fn has_deviations(&self) -> bool {
        self.incorrect_simulation > 0 || self.execution_errors > 0 || self.balance_diff > 0
    }

    /// Total number of transactions processed.
    pub fn total_transactions(&self) -> usize {
        self.correct_simulation
            + self.incorrect_simulation
            + self.execution_errors
            + self.balance_diff
            + self.log_diff
    }
}

impl std::fmt::Display for SessionSummary {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let total = self.total_transactions();
        write!(
            f,
            "Session summary: {total} transactions\n\
             \x20  - {} correct simulation\n\
             \x20  - {} incorrect simulation\n\
             \x20  - {} execution errors\n\
             \x20  - {} balance diffs\n\
             \x20  - {} log diffs",
            self.correct_simulation,
            self.incorrect_simulation,
            self.execution_errors,
            self.balance_diff,
            self.log_diff,
        )
    }
}

/// Error variants surfaced to backtest RPC clients.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[cfg_attr(feature = "ts-rs", derive(ts_rs::TS))]
#[serde(rename_all = "camelCase")]
pub enum BacktestError {
    InvalidTransactionEncoding {
        index: usize,
        error: String,
    },
    InvalidTransactionFormat {
        index: usize,
        error: String,
    },
    InvalidAccountEncoding {
        address: String,
        encoding: BinaryEncoding,
        error: String,
    },
    InvalidAccountOwner {
        address: String,
        error: String,
    },
    InvalidAccountPubkey {
        address: String,
        error: String,
    },
    NoMoreBlocks,
    AdvanceSlotFailed {
        slot: u64,
        error: String,
    },
    InvalidRequest {
        error: String,
    },
    Internal {
        error: String,
    },
    InvalidBlockhashFormat {
        slot: u64,
        error: String,
    },
    InitializingSysvarsFailed {
        slot: u64,
        error: String,
    },
    ClerkError {
        error: String,
    },
    SimulationError {
        error: String,
    },
    SessionNotFound {
        session_id: String,
    },
    SessionOwnerMismatch,
    /// Session ownership is in transition (e.g. the previous manager is
    /// shutting down, or another attach raced this one). Clients should retry
    /// the attach within their reconnect budget; the route is expected to
    /// become claimable shortly.
    SessionOwnershipBusy {
        reason: String,
    },
}

/// One contiguous block range available on the history clerk.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AvailableRange {
    pub bundle_start_slot: u64,
    pub bundle_start_slot_utc: Option<String>,
    pub max_bundle_end_slot: Option<u64>,
    pub max_bundle_end_slot_utc: Option<String>,
    pub max_bundle_size: Option<u64>,
}

/// Split a user-requested `[start_slot, end_slot]` range across the available
/// bundle ranges, returning a list of non-overlapping `(start, end)` pairs — one
/// per bundle that intersects the requested range.
///
/// Returns an error if the requested start slot is not covered by any range.
pub fn split_range(
    ranges: &[AvailableRange],
    requested_start: u64,
    requested_end: u64,
) -> Result<Vec<(u64, u64)>, String> {
    if requested_end < requested_start {
        return Err(format!(
            "invalid range: start_slot {requested_start} > end_slot {requested_end}"
        ));
    }

    let mut candidates: Vec<(u64, u64)> = ranges
        .iter()
        .filter_map(|r| Some((r.bundle_start_slot, r.max_bundle_end_slot?)))
        .filter(|(start, end)| {
            end > start
                && *start >= requested_start
                && *start < requested_end
                && *end > requested_start
        })
        .collect();

    candidates.sort_by(|a, b| a.0.cmp(&b.0).then(b.1.cmp(&a.1)));
    candidates.dedup_by_key(|(start, _)| *start);

    if candidates.is_empty() || candidates.first().unwrap().0 != requested_start {
        return Err(format!(
            "start_slot {requested_start} is not covered by any available bundle range"
        ));
    }

    let mut result: Vec<(u64, u64)> = Vec::new();
    let mut current_slot = requested_start;
    let mut i = 0;
    let mut best_end = 0u64;

    while current_slot <= requested_end {
        while i < candidates.len() && candidates[i].0 <= current_slot {
            best_end = best_end.max(candidates[i].1);
            i += 1;
        }
        if best_end < current_slot {
            return Err(format!("gap in coverage at slot {current_slot}"));
        }
        let range_end = best_end.min(requested_end);
        result.push((current_slot, range_end));
        current_slot = range_end + 1;
    }

    Ok(result)
}

impl From<BacktestError> for BacktestResponse {
    fn from(error: BacktestError) -> Self {
        Self::Error(error)
    }
}

impl std::error::Error for BacktestError {}

impl fmt::Display for BacktestError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            BacktestError::InvalidTransactionEncoding { index, error } => {
                write!(f, "invalid transaction encoding at index {index}: {error}")
            }
            BacktestError::InvalidTransactionFormat { index, error } => {
                write!(f, "invalid transaction format at index {index}: {error}")
            }
            BacktestError::InvalidAccountEncoding {
                address,
                encoding,
                error,
            } => write!(
                f,
                "invalid encoding for account {address} ({encoding:?}): {error}"
            ),
            BacktestError::InvalidAccountOwner { address, error } => {
                write!(f, "invalid owner for account {address}: {error}")
            }
            BacktestError::InvalidAccountPubkey { address, error } => {
                write!(f, "invalid account pubkey {address}: {error}")
            }
            BacktestError::NoMoreBlocks => write!(f, "no more blocks available"),
            BacktestError::AdvanceSlotFailed { slot, error } => {
                write!(f, "failed to advance to slot {slot}: {error}")
            }
            BacktestError::InvalidRequest { error } => write!(f, "invalid request: {error}"),
            BacktestError::Internal { error } => write!(f, "internal error: {error}"),
            BacktestError::InvalidBlockhashFormat { slot, error } => {
                write!(f, "invalid blockhash at slot {slot}: {error}")
            }
            BacktestError::InitializingSysvarsFailed { slot, error } => {
                write!(f, "failed to initialize sysvars at slot {slot}: {error}")
            }
            BacktestError::ClerkError { error } => write!(f, "clerk error: {error}"),
            BacktestError::SimulationError { error } => {
                write!(f, "simulation error: {error}")
            }
            BacktestError::SessionNotFound { session_id } => {
                write!(f, "session not found: {session_id}")
            }
            BacktestError::SessionOwnerMismatch => {
                write!(f, "session owner mismatch")
            }
            BacktestError::SessionOwnershipBusy { reason } => {
                write!(f, "session ownership busy: {reason}")
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn range(start: u64, end: u64) -> AvailableRange {
        AvailableRange {
            bundle_start_slot: start,
            bundle_start_slot_utc: None,
            max_bundle_end_slot: Some(end),
            max_bundle_end_slot_utc: None,
            max_bundle_size: None,
        }
    }

    #[test]
    fn split_range_valid() {
        // single
        let ranges = vec![range(100, 300)];
        assert_eq!(split_range(&ranges, 100, 300).unwrap(), vec![(100, 300)]);

        // multi
        let ranges = vec![range(100, 200), range(201, 300), range(301, 400)];
        assert_eq!(
            split_range(&ranges, 100, 300).unwrap(),
            vec![(100, 200), (201, 300)]
        );

        // nested
        // smaller bundles are contained within large ones but don't bridge the gap
        // the range is still coverable and should not return an error
        let ranges = vec![
            range(100, 500),
            range(110, 150),
            range(150, 190),
            range(501, 900),
        ];
        assert_eq!(
            split_range(&ranges, 100, 900).unwrap(),
            vec![(100, 500), (501, 900)]
        );
    }

    #[test]
    fn split_range_err() {
        // start must be exact
        let ranges = vec![range(200, 400)];
        assert!(split_range(&ranges, 100, 400).is_err());

        let ranges = vec![range(200, 400)];
        assert!(split_range(&ranges, 300, 400).is_err());

        // end not covered
        let ranges = vec![range(100, 200)];
        assert!(split_range(&ranges, 100, 300).is_err());

        // gap
        let ranges = vec![range(100, 200), range(210, 300)];
        assert!(split_range(&ranges, 100, 300).is_err());

        // inverted
        let ranges = vec![range(100, 300)];
        assert!(split_range(&ranges, 300, 100).is_err());
    }
}