Skip to main content

perpl_sdk/testing/
mod.rs

1//! Local Anvil-based testing environment.
2//!
3//! [`TestExchange`] spins up Anvil instance with collateral token and exchange
4//! smart contracts deployed and provides convenience methods for perpetual
5//! contracts setup and account creation.
6//!
7//! [`TestPerp`] then can be used to configure perpetual contracts and post
8//! orders, while [`TestAccount`] provides basic information about exchange
9//! account.
10//!
11//! [`Indexer`] wraps snapshot creation and event processing, while providing
12//! convenience methods for synchronization in tests.
13mod account;
14mod indexer;
15mod perp;
16
17use std::{
18    str::FromStr,
19    sync::{
20        Arc,
21        atomic::{AtomicBool, Ordering},
22    },
23    time::Duration,
24};
25
26pub use account::*;
27use alloy::{
28    hex::ToHexExt,
29    network::TransactionBuilder,
30    node_bindings::{Anvil, AnvilInstance},
31    primitives::{Address, Bytes, U256, address, hex},
32    providers::{DynProvider, Provider, ProviderBuilder, ext::AnvilApi},
33    rpc::{client::RpcClient, types::TransactionRequest},
34};
35use dashmap::{DashMap, DashSet};
36use fastnum::{UD64, udec64};
37pub use indexer::*;
38pub use perp::*;
39
40use crate::{
41    Chain,
42    abi::{dex::Exchange, erc1967_proxy::ERC1967Proxy, testing::TestToken},
43    error::DexError,
44    num, state, types,
45};
46
47const CHAIN_ID: u64 = 1337;
48const BLOCK_TIME_SEC: f64 = 0.4;
49const POLL_INTERVAL_MS: u64 = 50;
50
51const USD_DECIMALS: u8 = 6;
52
53/// Creation bytecode of the previous exchange implementation
54/// (`rc_v1.1.7-99-g3afdb99`, contract generation v1.1.7.3b), deployed by
55/// [`TestExchange::new_at_previous_version`].
56///
57/// Only the bytecode is kept: every function the pre-upgrade path calls has an
58/// unchanged signature, so the current bindings drive it, and the current
59/// `ExchangeEvents` still decodes its events - the V1 event signatures are
60/// retained in the ABI for exactly this reason.
61const PREVIOUS_IMPLEMENTATION: &str = include_str!("../../abi/dex/legacy/Exchange.v1.1.7.3.bin");
62
63#[derive(Debug)]
64pub struct TestExchange {
65    pub chain_id: u64,
66    pub rpc_url: String,
67    pub provider: DynProvider,
68    pub exchange: Exchange::ExchangeInstance<DynProvider>,
69    pub token: TestToken::TestTokenInstance<DynProvider>,
70    pub owner: Address,
71    pub owner_pk: String,
72    pub admin: Address,
73    pub admin_pk: String,
74    pub price_admin: Address,
75    pub price_admin_pk: String,
76    pub collateral_converter: num::Converter,
77    perpetual_ids: Arc<DashSet<types::PerpetualId>>,
78    account_address: Arc<DashMap<types::AccountId, Address>>,
79    // True while the deployed generation predates v1.1.7.4 (set by
80    // `new_at_previous_version`, cleared by `upgrade`). That generation's
81    // `addContract` carries two extra genesis-fee args, so `perp` must reach it
82    // through the legacy interface; a v1.1.7.4 deployment uses the current one.
83    legacy: AtomicBool,
84    anvil: AnvilInstance,
85}
86
87impl TestExchange {
88    /// Spins up the environment running the exchange implementation the SDK
89    /// targets ([`state::Exchange::revision`]).
90    pub async fn new() -> Self { Self::deploy(None).await }
91
92    /// Spins up the environment running the *previous* contract generation
93    /// (v1.1.7.3b: V2 information getters, but no version getter, keyed fee
94    /// schedules, builder attribution or existence bitmap) - the generation
95    /// deployed on mainnet before the v1.1.7.4 upgrade.
96    ///
97    /// Use with [`Self::upgrade`] to exercise the SDK across the upgrade
98    /// itself.
99    pub async fn new_at_previous_version() -> Self {
100        Self::deploy(Some(PREVIOUS_IMPLEMENTATION)).await
101    }
102
103    /// Upgrades the proxy to the implementation the SDK targets, seeding the
104    /// exchange-wide fee schedules and repointing every listed perpetual
105    /// contract at the default one.
106    ///
107    /// Runs `initializeV3` in the upgrade transaction, exactly as the real
108    /// upgrade does, so the log sequence a live indexer observes is the real
109    /// one.
110    pub async fn upgrade(
111        &self,
112        taker_fees: [UD64; state::FEE_TIERS],
113        maker_fees: [UD64; state::FEE_TIERS],
114    ) {
115        let implementation = Exchange::deploy(self.provider.clone())
116            .await
117            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
118            .unwrap();
119        let fee_converter = num::fee_converter();
120        let initialize = self
121            .exchange
122            .initializeV3(
123                taker_fees.map(|fee| fee_converter.to_unsigned(fee)),
124                maker_fees.map(|fee| fee_converter.to_unsigned(fee)),
125                // RWA schedule left blank, as in the real upgrade configuration
126                [U256::ZERO; state::FEE_TIERS],
127                [U256::ZERO; state::FEE_TIERS],
128                // Must list every live perpetual: `initializeV3` rejects an
129                // incomplete list rather than leave one on a stale fee key
130                self.perpetual_ids.iter().map(|p| U256::from(*p)).collect(),
131            )
132            .calldata()
133            .clone();
134        self.exchange
135            .upgradeToAndCall(*implementation.address(), initialize)
136            .gas(30_000_000)
137            .send()
138            .await
139            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
140            .unwrap()
141            .get_receipt()
142            .await
143            .unwrap();
144        // The proxy now runs the v1.1.7.4 implementation, whose `addContract`
145        // dropped the two genesis-fee args, so subsequent listings use it.
146        self.legacy.store(false, Ordering::Relaxed);
147    }
148
149    /// Runs the v1.1.7.5 fee-unit migration (`initializeV4`) on the proxy,
150    /// completing the two-hop upgrade that mainnet took: v1.1.7.3b ->
151    /// v1.1.7.4 ([`Self::upgrade`]) -> v1.1.7.5.
152    ///
153    /// A separate transaction because the two are separate reinitializers and
154    /// `upgradeToAndCall` runs one; the live upgrade is likewise a second
155    /// `upgradeToAndCall` months after the first.
156    ///
157    /// **Call it immediately after [`Self::upgrade`].** The implementation
158    /// deployed there is already this one, and its `getFee` divides by 1e6
159    /// while the schedules `initializeV3` seeded are still in
160    /// hundred-thousandths - so any fill in between is charged a tenth of its
161    /// rate. That intermediate state is real (the contract documents it as the
162    /// hazard of a bare `upgradeTo`) but it is not what any deployment should
163    /// trade in.
164    ///
165    /// `taker_fees` / `maker_fees` are the ladder [`Self::upgrade`] seeded: the
166    /// migration attests its pre-image on chain and reverts on a mismatch. The
167    /// rates it writes are `x10 / 2` of them - the unit change and the v1.1.7.5
168    /// rate cut in one exact step.
169    pub async fn upgrade_fee_unit(
170        &self,
171        taker_fees: [UD64; state::FEE_TIERS],
172        maker_fees: [UD64; state::FEE_TIERS],
173    ) {
174        let fee_converter = num::fee_converter();
175        self.exchange
176            .initializeV4(
177                taker_fees.map(|fee| fee_converter.to_unsigned(fee)),
178                maker_fees.map(|fee| fee_converter.to_unsigned(fee)),
179                // Only the DEFAULT schedule holds a non-zero word: `upgrade`
180                // leaves the RWA one blank, as the real upgrade configuration
181                // does, and no custom schedule is ever written here.
182                U256::ONE,
183            )
184            .gas(30_000_000)
185            .send()
186            .await
187            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
188            .unwrap()
189            .get_receipt()
190            .await
191            .unwrap();
192    }
193
194    async fn deploy(implementation: Option<&str>) -> Self {
195        let anvil = Anvil::new()
196            .block_time_f64(BLOCK_TIME_SEC)
197            .chain_id(CHAIN_ID)
198            .args(vec!["--code-size-limit", "131072"])
199            .args(vec!["--gas-limit", "200000000"])
200            .args(vec!["--base-fee", "100000000000"])
201            .args(vec!["--order", "fifo"])
202            .args(vec!["--max-persisted-states", "1000"])
203            .args(vec!["--slots-in-an-epoch", "0"])
204            .try_spawn()
205            .unwrap();
206        let client = RpcClient::builder().http(anvil.endpoint_url());
207        client.set_poll_interval(Duration::from_millis(POLL_INTERVAL_MS));
208        let provider = DynProvider::new(
209            ProviderBuilder::new()
210                .wallet(anvil.wallet().unwrap())
211                .connect_client(client),
212        );
213        // Deploy multicall3 contract (see https://github.com/mds1/multicall3?tab=readme-ov-file#new-deployments)
214        provider
215            .anvil_set_balance(
216                address!("0x05f32b3cc3888453ff71b01135b34ff8e41263f2"),
217                U256::from(1e18 as u64),
218            )
219            .await
220            .unwrap();
221        _ = provider.send_raw_transaction(&hex!("0xf90f538085174876e800830f42408080b90f00608060405234801561001057600080fd5b50610ee0806100206000396000f3fe6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c00331ca0edce47092c0f398cebf3ffc267f05c8e7076e3b89445e0fe50f6332273d4569ba01b0b9d000e19b24c5869b0fc3b22b0d6fa47cd63316875cbbd577d76e6fde086")).await.unwrap();
222
223        let (owner, admin, price_admin) =
224            (anvil.addresses()[0], anvil.addresses()[1], anvil.addresses()[2]);
225
226        // Test USD
227        let token = TestToken::deploy(
228            provider.clone(),
229            "Test USD".to_string(),
230            "USD".to_string(),
231            USD_DECIMALS,
232        )
233        .await
234        .unwrap();
235
236        // Some allocation to owner for the faucet
237        token
238            .mint(owner, usd(1_000_000_000))
239            .send()
240            .await
241            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
242            .unwrap()
243            .get_receipt()
244            .await
245            .unwrap();
246
247        // Exchange implementation and upgradeable proxy. `initialize` is
248        // unchanged across the generations, so its calldata can be built from the
249        // current bindings whichever implementation is deployed.
250        let exchange_impl = match implementation {
251            None => *Exchange::deploy(provider.clone())
252                .await
253                .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
254                .unwrap()
255                .address(),
256            Some(bytecode) => deploy_bytecode(&provider, bytecode).await,
257        };
258        let init_calldata = Exchange::new(exchange_impl, provider.clone())
259            .initialize(*token.address())
260            .calldata()
261            .clone();
262        let proxy = ERC1967Proxy::deploy(provider.clone(), exchange_impl, init_calldata)
263            .await
264            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
265            .unwrap();
266        let exchange = Exchange::new(*proxy.address(), provider.clone());
267
268        // Disable account whitelisting
269        exchange
270            .setWhitelistingEnabled(false)
271            .send()
272            .await
273            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
274            .unwrap()
275            .get_receipt()
276            .await
277            .unwrap();
278
279        // Setup roles
280        exchange
281            .setAdministrator(admin, true)
282            .send()
283            .await
284            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
285            .unwrap()
286            .get_receipt()
287            .await
288            .unwrap();
289        exchange
290            .setPriceAdministrator(price_admin, true)
291            .send()
292            .await
293            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
294            .unwrap()
295            .get_receipt()
296            .await
297            .unwrap();
298
299        Self {
300            chain_id: anvil.chain_id(),
301            rpc_url: anvil.endpoint_url().to_string(),
302            provider,
303            exchange,
304            token,
305            owner,
306            owner_pk: anvil.nth_key(0).unwrap().to_bytes().encode_hex(),
307            admin,
308            admin_pk: anvil.nth_key(1).unwrap().to_bytes().encode_hex(),
309            price_admin,
310            price_admin_pk: anvil.nth_key(2).unwrap().to_bytes().encode_hex(),
311            collateral_converter: num::Converter::new(USD_DECIMALS),
312            perpetual_ids: Arc::new(DashSet::new()),
313            account_address: Arc::new(DashMap::new()),
314            // A specific implementation is only ever requested by
315            // `new_at_previous_version`, so this marks the pre-upgrade generation.
316            legacy: AtomicBool::new(implementation.is_some()),
317            anvil,
318        }
319    }
320
321    pub fn chain(&self) -> Chain {
322        Chain::custom(
323            self.chain_id,
324            *self.token.address(),
325            0,
326            *self.exchange.address(),
327            self.perpetual_ids.iter().map(|p| *p).collect(),
328        )
329    }
330
331    /// Same chain with no perpetual contracts configured, so clients discover
332    /// every listed contract on-chain instead.
333    pub fn chain_with_perpetual_discovery(&self) -> Chain { self.chain().with_perpetuals(vec![]) }
334
335    pub async fn account(&self, idx: usize, usd_balance: u64) -> TestAccount<'_> {
336        let address = self.anvil.addresses()[idx + 3]; // skipping owner, admin and price admin
337        let target_balance = usd(usd_balance);
338        let cur_balance = self.token.balanceOf(address).call().await.unwrap();
339        if target_balance > cur_balance {
340            self.token
341                .mint(address, target_balance - cur_balance)
342                .send()
343                .await
344                .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
345                .unwrap()
346                .get_receipt()
347                .await
348                .unwrap();
349        }
350        self.token
351            .approve(*self.exchange.address(), target_balance)
352            .from(address)
353            .send()
354            .await
355            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
356            .unwrap()
357            .get_receipt()
358            .await
359            .unwrap();
360        let receipt = self
361            .exchange
362            .createAccount(target_balance)
363            .from(address)
364            .send()
365            .await
366            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
367            .unwrap()
368            .get_receipt()
369            .await
370            .unwrap();
371        let log = receipt.decoded_log::<Exchange::AccountCreated>().unwrap();
372        self.account_address.insert(log.id.to(), log.account);
373        TestAccount { id: log.id.to(), address: log.account, exchange: self }
374    }
375
376    /// Sets the exchange-wide default fee schedule, eight `(taker, maker)`
377    /// rates indexed by an account's fee tier. Every perpetual contract
378    /// that has not been repointed resolves its fees from this schedule.
379    pub async fn set_fee_schedule(
380        &self,
381        taker_fees: [UD64; state::FEE_TIERS],
382        maker_fees: [UD64; state::FEE_TIERS],
383    ) {
384        // ppm: the schedule setters only exist from v1.1.7.4, and the only
385        // implementation this harness ever deploys is the current one, whose
386        // getFee reads stored rates as millionths.
387        let fee_converter = num::ppm_fee_converter();
388        self.exchange
389            .setDefaultPerpFeeSchedValues(
390                taker_fees.map(|fee| fee_converter.to_unsigned(fee)),
391                maker_fees.map(|fee| fee_converter.to_unsigned(fee)),
392            )
393            .send()
394            .await
395            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
396            .unwrap()
397            .get_receipt()
398            .await
399            .unwrap();
400    }
401
402    /// Sets the exchange-wide RWA default fee schedule, see
403    /// [`Self::set_fee_schedule`].
404    pub async fn set_rwa_fee_schedule(
405        &self,
406        taker_fees: [UD64; state::FEE_TIERS],
407        maker_fees: [UD64; state::FEE_TIERS],
408    ) {
409        let fee_converter = num::ppm_fee_converter();
410        self.exchange
411            .setDefaultRwaFeeSchedValues(
412                taker_fees.map(|fee| fee_converter.to_unsigned(fee)),
413                maker_fees.map(|fee| fee_converter.to_unsigned(fee)),
414            )
415            .send()
416            .await
417            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
418            .unwrap()
419            .get_receipt()
420            .await
421            .unwrap();
422    }
423
424    /// Assigns fee tiers to accounts, indexing the fee schedule of every
425    /// perpetual contract they trade.
426    pub async fn set_account_fee_tiers(&self, tiers: Vec<(types::AccountId, types::FeeTier)>) {
427        self.exchange
428            .setAccountFeeTiers(
429                tiers
430                    .into_iter()
431                    .map(|(account_id, tier)| Exchange::AccountFeeTier {
432                        accountId: U256::from(account_id),
433                        tier: U256::from(tier),
434                    })
435                    .collect(),
436            )
437            .from(self.admin)
438            .send()
439            .await
440            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
441            .unwrap()
442            .get_receipt()
443            .await
444            .unwrap();
445    }
446
447    /// Adds a perpetual contract, placed on the exchange-wide default fee
448    /// schedule.
449    ///
450    /// Fees are not a listing parameter: the schedule is seeded once at
451    /// deployment, and `addContract` retains its fee arguments for ABI
452    /// stability while ignoring them (v1.1.7.4). Use
453    /// [`Self::set_fee_schedule`] to retune the shared schedule, or
454    /// [`TestPerp::set_fee_schedule`] to give this contract its own.
455    #[allow(clippy::too_many_arguments)]
456    pub async fn perp(
457        &self,
458        name: &str,
459        perp_id: types::PerpetualId,
460        base_price: UD64,
461        price_decimals: u8,
462        size_decimals: u8,
463        initial_margin: UD64,
464        maintenance_margin: UD64,
465    ) -> TestPerp<'_> {
466        let price_converter = num::Converter::new(price_decimals);
467        let leverage_converter = num::Converter::new(2); // Margin and leverage are in 100th
468        if self.legacy.load(Ordering::Relaxed) {
469            // The pre-v1.1.7.4 generation's `addContract` still carries the two
470            // genesis-fee args (dropped in v1.1.7.4); reach it through the legacy
471            // interface so the selector matches the deployed contract. Genesis
472            // fees are seeded to zero and set separately via
473            // `TestPerp::with_legacy_fees`.
474            crate::abi::dex_legacy::LegacyExchange::new(
475                *self.exchange.address(),
476                self.provider.clone(),
477            )
478            .addContract(
479                name.to_string(),
480                name.to_string(),
481                U256::from(perp_id),
482                price_converter.to_unsigned(base_price),
483                U256::from(price_decimals),
484                U256::from(size_decimals),
485                U256::ZERO,
486                U256::ZERO,
487                leverage_converter.to_unsigned(initial_margin),
488                leverage_converter.to_unsigned(maintenance_margin),
489            )
490            .send()
491            .await
492            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
493            .unwrap()
494            .get_receipt()
495            .await
496            .unwrap();
497        } else {
498            self.exchange
499                .addContract(
500                    name.to_string(),
501                    name.to_string(),
502                    U256::from(perp_id),
503                    price_converter.to_unsigned(base_price),
504                    U256::from(price_decimals),
505                    U256::from(size_decimals),
506                    leverage_converter.to_unsigned(initial_margin),
507                    leverage_converter.to_unsigned(maintenance_margin),
508                )
509                .send()
510                .await
511                .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
512                .unwrap()
513                .get_receipt()
514                .await
515                .unwrap();
516        }
517        // Ignore oracle to eliminate ChainLink dependency
518        self.exchange
519            .setIgnOracle(U256::from(perp_id), true)
520            .send()
521            .await
522            .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
523            .unwrap()
524            .get_receipt()
525            .await
526            .unwrap();
527        self.perpetual_ids.insert(perp_id);
528        TestPerp {
529            id: perp_id,
530            name: name.to_string(),
531            price_converter,
532            size_converter: num::Converter::new(size_decimals),
533            leverage_converter,
534            exchange: self,
535        }
536    }
537
538    pub async fn btc_perp(&self) -> TestPerp<'_> {
539        self.perp("BTC", 0x10, udec64!(5000), 1, 5, udec64!(10), udec64!(20))
540            .await
541            .with_mark_price(udec64!(100000))
542            .await
543            .unpause()
544            .await
545    }
546
547    pub async fn eth_perp(&self) -> TestPerp<'_> {
548        self.perp("ETH", 0x20, udec64!(1), 2, 3, udec64!(10), udec64!(20))
549            .await
550            .with_mark_price(udec64!(4000))
551            .await
552            .unpause()
553            .await
554    }
555
556    pub async fn sol_perp(&self) -> TestPerp<'_> {
557        self.perp("SOL", 0x30, udec64!(1), 2, 3, udec64!(10), udec64!(20))
558            .await
559            .with_mark_price(udec64!(200))
560            .await
561            .unpause()
562            .await
563    }
564
565    pub async fn trx_perp(&self) -> TestPerp<'_> {
566        self.perp("TRX", 0x40, udec64!(1), 5, 0, udec64!(10), udec64!(20))
567            .await
568            .with_mark_price(udec64!(0.3))
569            .await
570            .unpause()
571            .await
572    }
573}
574
575/// Deploys a contract from raw creation bytecode, for an implementation the SDK
576/// has no bindings for.
577async fn deploy_bytecode(provider: &DynProvider, bytecode: &str) -> Address {
578    let code = Bytes::from_str(bytecode.trim()).expect("implementation bytecode");
579    provider
580        .send_transaction(TransactionRequest::default().with_deploy_code(code))
581        .await
582        .map_err::<DexError, _>(|err| DexError::Provider(err.into()))
583        .unwrap()
584        .get_receipt()
585        .await
586        .unwrap()
587        .contract_address
588        .expect("implementation deployed")
589}
590
591pub fn scale(amount: u64, decimals: u8) -> U256 {
592    U256::from(amount) * U256::from(10).pow(U256::from(decimals))
593}
594
595pub fn usd(amount: u64) -> U256 { scale(amount, USD_DECIMALS) }