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, num, 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 /// First version whose stored fee-schedule rates are in millionths (ppm)
44 /// rather than hundred-thousandths, and whose fills charge the schedule fee
45 /// on EVERY position size change rather than on additions only.
46 ///
47 /// The two arrived in the same release and neither has a signal of its own,
48 /// so they share the threshold.
49 pub const PPM_FEE_UNIT: Self = Self { major: 1, minor: 7, patch: 5 };
50
51 pub const fn new(major: u64, minor: u64, patch: u64) -> Self { Self { major, minor, patch } }
52
53 pub const fn major(&self) -> u64 { self.major }
54
55 pub const fn minor(&self) -> u64 { self.minor }
56
57 pub const fn patch(&self) -> u64 { self.patch }
58}
59
60impl std::fmt::Display for ContractVersion {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 write!(f, "v1.{}.{}.{}", self.major, self.minor, self.patch)
63 }
64}
65
66/// Feature set of the deployed exchange smart contract.
67///
68/// Each flag guards a group of selectors/events introduced by a single release,
69/// so the SDK can index and snapshot a contract that has not been upgraded to
70/// the revision the SDK targets ([`crate::state::Exchange::revision`]).
71#[derive(Clone, Copy, Debug)]
72pub struct ContractFeatures {
73 version: Option<ContractVersion>,
74 v2_state_getters: bool,
75 keyed_fee_schedules: bool,
76 builder_attribution: bool,
77 perpetual_discovery: bool,
78 ppm_fee_unit: bool,
79}
80
81impl ContractFeatures {
82 /// Everything the SDK targets, with no version reported. Useful as a
83 /// default for locally deployed contracts built from the SDK's own ABI.
84 pub fn current() -> Self {
85 Self {
86 version: None,
87 v2_state_getters: true,
88 keyed_fee_schedules: true,
89 builder_attribution: true,
90 perpetual_discovery: true,
91 ppm_fee_unit: true,
92 }
93 }
94
95 /// Feature set of a known contract version.
96 pub fn of(version: ContractVersion) -> Self {
97 let builder_codes = version >= ContractVersion::BUILDER_CODES;
98 Self {
99 version: Some(version),
100 v2_state_getters: version >= ContractVersion::V2_GETTERS,
101 keyed_fee_schedules: builder_codes,
102 builder_attribution: builder_codes,
103 perpetual_discovery: builder_codes,
104 ppm_fee_unit: version >= ContractVersion::PPM_FEE_UNIT,
105 }
106 }
107
108 /// Version reported by the contract, if it exposes `getContractVersion`
109 /// (v1.1.7.4+).
110 pub fn version(&self) -> Option<ContractVersion> { self.version }
111
112 /// `getPerpetualInfoV2` / `getPositionV2` and the V2 position events
113 /// (`fundingSumScalingExp`, `priceResiduePNSQ16`) are available.
114 pub fn v2_state_getters(&self) -> bool { self.v2_state_getters }
115
116 /// Keyed 8-tier fee schedules with per-account fee tiers are available
117 /// (`getPerpFeeSchedule`, `getFeeScheduleById`, `getAccountFeeTier` and
118 /// the `FeeScheduleSet` / `DefaultPerpFeeScheduleSet` /
119 /// `DefaultRwaFeeScheduleSet` / `PerpFeeSchedIdSet` /
120 /// `AccountFeeTierSet` events).
121 pub fn keyed_fee_schedules(&self) -> bool { self.keyed_fee_schedules }
122
123 /// Builder attribution is available (`execOrderV2` and friends,
124 /// `getOrderV2` and the `OrderRequestV2` / `MakerOrderFilledV2` /
125 /// `TakerOrderFilledV2` events).
126 pub fn builder_attribution(&self) -> bool { self.builder_attribution }
127
128 /// The perpetual-existence bitmap is available
129 /// (`getPerpetualExistsBitmap`), so the set of listed perpetuals can be
130 /// discovered on-chain instead of being configured.
131 pub fn perpetual_discovery(&self) -> bool { self.perpetual_discovery }
132
133 /// Stored fee-schedule rates are in millionths (ppm) rather than
134 /// hundred-thousandths, and every position size change is charged the
135 /// schedule fee - a close or decrease on the removed notional, netted from
136 /// the exit proceeds, where earlier releases charged additions only.
137 ///
138 /// The per-order builder fee is NOT affected: it stays `Per100K` on the
139 /// wire, in order storage and in every event at any version.
140 pub fn ppm_fee_unit(&self) -> bool { self.ppm_fee_unit }
141
142 /// Converter for the fee-SCHEDULE rates this deployment reports, resolving
143 /// the v1.1.7.5 redenomination.
144 ///
145 /// An unknown version reads as the pre-upgrade unit, which is right for
146 /// every contract old enough not to report one.
147 pub fn fee_rate_converter(&self) -> num::Converter {
148 if self.ppm_fee_unit { num::ppm_fee_converter() } else { num::fee_converter() }
149 }
150
151 /// Detects the feature set of the deployed contract at `block_id`.
152 ///
153 /// Reads `getContractVersion()`, which is authoritative on v1.1.7.4+ and
154 /// absent before it - so a revert *proves* the contract predates every
155 /// feature that release introduced. What a revert leaves open is whether
156 /// the deployment is v1.1.7.3b or older, resolved by probing
157 /// `getPerpetualInfoV2` against `probe_perpetual`; unlike `getPositionV2`,
158 /// the perpetual getter does not validate account existence, so the probe
159 /// distinguishes selector presence from state. With no perpetual to probe
160 /// against the V2 getters are assumed present - see
161 /// [`Self::probe_v2_state_getters`] to resolve that once one is known.
162 pub(crate) async fn probe<P: Provider>(
163 instance: &dex::Exchange::ExchangeInstance<P>,
164 block_id: BlockId,
165 probe_perpetual: Option<types::PerpetualId>,
166 ) -> Self {
167 if let Ok(v) = instance
168 .getContractVersion()
169 .block(block_id)
170 .call()
171 .await
172 .map(|v| ContractVersion::new(v.major.to(), v.minor.to(), v.patch.to()))
173 {
174 return Self::of(v);
175 }
176
177 let mut features = Self {
178 version: None,
179 v2_state_getters: true,
180 keyed_fee_schedules: false,
181 builder_attribution: false,
182 perpetual_discovery: false,
183 ppm_fee_unit: false,
184 };
185 if let Some(perp_id) = probe_perpetual {
186 features
187 .probe_v2_state_getters(instance, block_id, perp_id)
188 .await;
189 }
190 features
191 }
192
193 /// Resolves [`Self::v2_state_getters`] on an unversioned contract by
194 /// probing `getPerpetualInfoV2` against a known perpetual.
195 ///
196 /// A no-op once the contract reports a version, which settles the question
197 /// outright.
198 pub(crate) async fn probe_v2_state_getters<P: Provider>(
199 &mut self,
200 instance: &dex::Exchange::ExchangeInstance<P>,
201 block_id: BlockId,
202 perp_id: types::PerpetualId,
203 ) {
204 if self.version.is_some() {
205 return;
206 }
207 self.v2_state_getters = instance
208 .getPerpetualInfoV2(U256::from(perp_id))
209 .block(block_id)
210 .call()
211 .await
212 .is_ok();
213 }
214
215 /// Folds a version reported by `ContractVersionSet` into the feature set.
216 ///
217 /// The signal is authoritative, so it is followed in both directions: a
218 /// downgrade below a feature's threshold withdraws that feature.
219 pub(crate) fn observe_version(&mut self, version: ContractVersion) {
220 *self = Self::of(version);
221 }
222}
223
224impl Default for ContractFeatures {
225 fn default() -> Self { Self::current() }
226}
227
228impl std::fmt::Display for ContractFeatures {
229 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
230 match self.version {
231 Some(version) => write!(f, "{version}"),
232 None => write!(
233 f,
234 "unversioned ({})",
235 if self.v2_state_getters { "V2 getters" } else { "V0 getters" },
236 ),
237 }
238 }
239}