Skip to main content

perpl_sdk/state/
version.rs

1//! Deployed contract version and the feature set derived from it.
2//!
3//! The exchange is an upgradeable proxy, so the deployed implementation can lag
4//! behind the ABI the SDK is compiled against. Since v1.1.7.4 the contract
5//! reports its own version via `getContractVersion()` and stamps
6//! `ContractVersionSet` inside the upgrade transaction, which makes capability
7//! detection authoritative rather than inferred: [`ContractFeatures::probe`]
8//! reads the version once while building a snapshot, and
9//! [`ContractFeatures::observe_version`] follows it in both directions from the
10//! event stream.
11//!
12//! Older deployments have no version getter at all, so they are detected by
13//! probing a selector added by the release in question.
14
15use alloy::{eips::BlockId, primitives::U256, providers::Provider};
16
17use crate::{abi::dex, types};
18
19/// Version of the deployed exchange smart contract.
20///
21/// Renders as `v1.<major>.<minor>.<patch>` - the leading `v1` epoch is fixed
22/// for this contract's lifetime and changes only with an entirely new contract.
23#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
24pub struct ContractVersion {
25    major: u64,
26    minor: u64,
27    patch: u64,
28}
29
30impl ContractVersion {
31    /// First version exposing the V2 information getters (`getPerpetualInfoV2`,
32    /// `getPositionV2`) and the corresponding V2 position events.
33    ///
34    /// Predates `getContractVersion`, so this version is never reported by the
35    /// contract itself - it is only reached through selector probing.
36    pub const V2_GETTERS: Self = Self { major: 1, minor: 7, patch: 3 };
37
38    /// First version exposing keyed fee schedules, per-account fee tiers,
39    /// builder attribution, the perpetual-existence bitmap - and
40    /// `getContractVersion` itself.
41    pub const BUILDER_CODES: Self = Self { major: 1, minor: 7, patch: 4 };
42
43    pub const fn new(major: u64, minor: u64, patch: u64) -> Self { Self { major, minor, patch } }
44
45    pub const fn major(&self) -> u64 { self.major }
46
47    pub const fn minor(&self) -> u64 { self.minor }
48
49    pub const fn patch(&self) -> u64 { self.patch }
50}
51
52impl std::fmt::Display for ContractVersion {
53    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
54        write!(f, "v1.{}.{}.{}", self.major, self.minor, self.patch)
55    }
56}
57
58/// Feature set of the deployed exchange smart contract.
59///
60/// Each flag guards a group of selectors/events introduced by a single release,
61/// so the SDK can index and snapshot a contract that has not been upgraded to
62/// the revision the SDK targets ([`crate::state::Exchange::revision`]).
63#[derive(Clone, Copy, Debug)]
64pub struct ContractFeatures {
65    version: Option<ContractVersion>,
66    v2_state_getters: bool,
67    keyed_fee_schedules: bool,
68    builder_attribution: bool,
69    perpetual_discovery: bool,
70}
71
72impl ContractFeatures {
73    /// Everything the SDK targets, with no version reported. Useful as a
74    /// default for locally deployed contracts built from the SDK's own ABI.
75    pub fn current() -> Self {
76        Self {
77            version: None,
78            v2_state_getters: true,
79            keyed_fee_schedules: true,
80            builder_attribution: true,
81            perpetual_discovery: true,
82        }
83    }
84
85    /// Feature set of a known contract version.
86    pub fn of(version: ContractVersion) -> Self {
87        let builder_codes = version >= ContractVersion::BUILDER_CODES;
88        Self {
89            version: Some(version),
90            v2_state_getters: version >= ContractVersion::V2_GETTERS,
91            keyed_fee_schedules: builder_codes,
92            builder_attribution: builder_codes,
93            perpetual_discovery: builder_codes,
94        }
95    }
96
97    /// Version reported by the contract, if it exposes `getContractVersion`
98    /// (v1.1.7.4+).
99    pub fn version(&self) -> Option<ContractVersion> { self.version }
100
101    /// `getPerpetualInfoV2` / `getPositionV2` and the V2 position events
102    /// (`fundingSumScalingExp`, `priceResiduePNSQ16`) are available.
103    pub fn v2_state_getters(&self) -> bool { self.v2_state_getters }
104
105    /// Keyed 8-tier fee schedules with per-account fee tiers are available
106    /// (`getPerpFeeSchedule`, `getFeeScheduleById`, `getAccountFeeTier` and
107    /// the `FeeScheduleSet` / `DefaultPerpFeeScheduleSet` /
108    /// `DefaultRwaFeeScheduleSet` / `PerpFeeSchedIdSet` /
109    /// `AccountFeeTierSet` events).
110    pub fn keyed_fee_schedules(&self) -> bool { self.keyed_fee_schedules }
111
112    /// Builder attribution is available (`execOrderV2` and friends,
113    /// `getOrderV2` and the `OrderRequestV2` / `MakerOrderFilledV2` /
114    /// `TakerOrderFilledV2` events).
115    pub fn builder_attribution(&self) -> bool { self.builder_attribution }
116
117    /// The perpetual-existence bitmap is available
118    /// (`getPerpetualExistsBitmap`), so the set of listed perpetuals can be
119    /// discovered on-chain instead of being configured.
120    pub fn perpetual_discovery(&self) -> bool { self.perpetual_discovery }
121
122    /// Detects the feature set of the deployed contract at `block_id`.
123    ///
124    /// Reads `getContractVersion()`, which is authoritative on v1.1.7.4+ and
125    /// absent before it - so a revert *proves* the contract predates every
126    /// feature that release introduced. What a revert leaves open is whether
127    /// the deployment is v1.1.7.3b or older, resolved by probing
128    /// `getPerpetualInfoV2` against `probe_perpetual`; unlike `getPositionV2`,
129    /// the perpetual getter does not validate account existence, so the probe
130    /// distinguishes selector presence from state. With no perpetual to probe
131    /// against the V2 getters are assumed present - see
132    /// [`Self::probe_v2_state_getters`] to resolve that once one is known.
133    pub(crate) async fn probe<P: Provider>(
134        instance: &dex::Exchange::ExchangeInstance<P>,
135        block_id: BlockId,
136        probe_perpetual: Option<types::PerpetualId>,
137    ) -> Self {
138        if let Ok(v) = instance
139            .getContractVersion()
140            .block(block_id)
141            .call()
142            .await
143            .map(|v| ContractVersion::new(v.major.to(), v.minor.to(), v.patch.to()))
144        {
145            return Self::of(v);
146        }
147
148        let mut features = Self {
149            version: None,
150            v2_state_getters: true,
151            keyed_fee_schedules: false,
152            builder_attribution: false,
153            perpetual_discovery: false,
154        };
155        if let Some(perp_id) = probe_perpetual {
156            features
157                .probe_v2_state_getters(instance, block_id, perp_id)
158                .await;
159        }
160        features
161    }
162
163    /// Resolves [`Self::v2_state_getters`] on an unversioned contract by
164    /// probing `getPerpetualInfoV2` against a known perpetual.
165    ///
166    /// A no-op once the contract reports a version, which settles the question
167    /// outright.
168    pub(crate) async fn probe_v2_state_getters<P: Provider>(
169        &mut self,
170        instance: &dex::Exchange::ExchangeInstance<P>,
171        block_id: BlockId,
172        perp_id: types::PerpetualId,
173    ) {
174        if self.version.is_some() {
175            return;
176        }
177        self.v2_state_getters = instance
178            .getPerpetualInfoV2(U256::from(perp_id))
179            .block(block_id)
180            .call()
181            .await
182            .is_ok();
183    }
184
185    /// Folds a version reported by `ContractVersionSet` into the feature set.
186    ///
187    /// The signal is authoritative, so it is followed in both directions: a
188    /// downgrade below a feature's threshold withdraws that feature.
189    pub(crate) fn observe_version(&mut self, version: ContractVersion) {
190        *self = Self::of(version);
191    }
192}
193
194impl Default for ContractFeatures {
195    fn default() -> Self { Self::current() }
196}
197
198impl std::fmt::Display for ContractFeatures {
199    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
200        match self.version {
201            Some(version) => write!(f, "{version}"),
202            None => write!(
203                f,
204                "unversioned ({})",
205                if self.v2_state_getters { "V2 getters" } else { "V0 getters" },
206            ),
207        }
208    }
209}