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}