diff --git a/Cargo.lock b/Cargo.lock index 674ddf0d15..4efaaa1b3b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -368,6 +368,32 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4436e0292ab1bb631b42973c61205e704475fe8126af845c8d923c0996328127" +[[package]] +name = "amm-simulator" +version = "0.1.0" +dependencies = [ + "ethabi", + "evm", + "frame-support", + "hex-literal", + "hydra-dx-math", + "hydradx-traits", + "ice-support", + "log", + "module-evm-utility-macro", + "num_enum", + "pallet-liquidation", + "pallet-omnipool", + "pallet-stableswap", + "parity-scale-codec", + "precompile-utils", + "primitive-types 0.13.1", + "primitives", + "sp-arithmetic", + "sp-runtime", + "sp-std", +] + [[package]] name = "android-tzdata" version = "0.1.1" @@ -3805,7 +3831,10 @@ source = "git+https://github.com/AcalaNetwork/ethabi?branch=acala#6e6ef9b3712aec dependencies = [ "ethereum-types", "hex", + "serde", "sha3", + "thiserror 1.0.69", + "uint 0.10.0", ] [[package]] @@ -5781,6 +5810,7 @@ dependencies = [ name = "hydradx-runtime" version = "419.0.0" dependencies = [ + "amm-simulator", "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", "cumulus-pallet-weight-reclaim", @@ -5811,6 +5841,7 @@ dependencies = [ "hydra-dx-math", "hydradx-adapters", "hydradx-traits", + "ice-support", "log", "module-evm-utility-macro", "num_enum", @@ -5862,7 +5893,10 @@ dependencies = [ "pallet-evm-precompile-simple", "pallet-genesis-history", "pallet-hsm", + "pallet-ice", "pallet-identity", + "pallet-intent", + "pallet-lazy-executor", "pallet-lbp", "pallet-liquidation", "pallet-liquidity-mining", @@ -5908,6 +5942,7 @@ dependencies = [ "pretty_assertions", "primitive-types 0.13.1", "primitives", + "route-findr", "scale-info", "serde", "serde_json", @@ -5943,6 +5978,7 @@ version = "4.7.1" dependencies = [ "frame-support", "frame-system", + "hydra-dx-math", "impl-trait-for-tuples", "orml-traits", "pallet-evm", @@ -6074,6 +6110,57 @@ dependencies = [ "cc", ] +[[package]] +name = "ice-solver" +version = "0.1.0" +dependencies = [ + "frame-support", + "hydra-dx-math", + "hydradx-traits", + "ice-support", + "log", + "parity-scale-codec", + "sp-core", + "sp-std", +] + +[[package]] +name = "ice-solver-bench" +version = "0.1.0" +dependencies = [ + "amm-simulator", + "criterion", + "frame-remote-externalities", + "frame-support", + "frame-system", + "hydradx-runtime", + "hydradx-traits", + "ice-solver", + "ice-support", + "log", + "pallet-ema-oracle", + "pallet-intent", + "pallet-omnipool", + "primitives", + "sp-core", + "sp-io", + "sp-runtime", + "tokio", +] + +[[package]] +name = "ice-support" +version = "1.0.0" +dependencies = [ + "frame-support", + "hydra-dx-math", + "hydradx-traits", + "parity-scale-codec", + "scale-info", + "sp-core", + "sp-std", +] + [[package]] name = "icu_collections" version = "2.1.1" @@ -9731,6 +9818,39 @@ dependencies = [ "test-utils", ] +[[package]] +name = "pallet-ice" +version = "1.0.0" +dependencies = [ + "amm-simulator", + "frame-benchmarking", + "frame-support", + "frame-system", + "hydra-dx-math", + "hydradx-traits", + "ice-solver", + "ice-support", + "log", + "num-traits", + "orml-tokens", + "orml-traits", + "pallet-broadcast", + "pallet-intent", + "pallet-route-executor", + "pallet-timestamp", + "parity-scale-codec", + "pretty_assertions", + "primitives", + "scale-info", + "sp-core", + "sp-externalities", + "sp-io", + "sp-runtime", + "sp-runtime-interface", + "sp-std", + "test-utils", +] + [[package]] name = "pallet-identity" version = "41.0.0" @@ -9781,6 +9901,53 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-intent" +version = "1.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "hydra-dx-math", + "hydradx-traits", + "ice-support", + "log", + "orml-tokens", + "orml-traits", + "pallet-timestamp", + "parity-scale-codec", + "pretty_assertions", + "primitives", + "scale-info", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", + "test-utils", +] + +[[package]] +name = "pallet-lazy-executor" +version = "1.0.0" +dependencies = [ + "frame-benchmarking", + "frame-support", + "frame-system", + "hex-literal", + "hydradx-traits", + "log", + "pallet-balances", + "pallet-transaction-payment", + "parity-scale-codec", + "pretty_assertions", + "scale-info", + "serde", + "sp-core", + "sp-io", + "sp-runtime", + "sp-std", +] + [[package]] name = "pallet-lbp" version = "4.13.0" @@ -13709,6 +13876,16 @@ dependencies = [ "staging-xcm-builder", ] +[[package]] +name = "route-findr" +version = "0.1.0" +dependencies = [ + "frame-support", + "hydradx-traits", + "primitives", + "sp-runtime", +] + [[package]] name = "route-recognizer" version = "0.3.1" @@ -13801,6 +13978,7 @@ checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" name = "runtime-integration-tests" version = "1.84.0" dependencies = [ + "amm-simulator", "cumulus-pallet-aura-ext", "cumulus-pallet-parachain-system", "cumulus-pallet-xcm", @@ -13826,6 +14004,8 @@ dependencies = [ "hydradx-adapters", "hydradx-runtime", "hydradx-traits", + "ice-solver", + "ice-support", "libsecp256k1", "liquidation-worker-support", "module-evm-utility-macro", @@ -13863,7 +14043,9 @@ dependencies = [ "pallet-evm-accounts", "pallet-evm-precompile-call-permit", "pallet-hsm", + "pallet-ice", "pallet-im-online", + "pallet-intent", "pallet-lbp", "pallet-liquidation", "pallet-liquidity-mining", diff --git a/Cargo.toml b/Cargo.toml index bcdff1e55c..247af25a56 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,8 +50,15 @@ members = [ 'pallets/broadcast', 'liquidation-worker-support', 'pallets/hsm', - "pallets/signet", - "pallets/dispenser" + 'pallets/signet', + 'pallets/dispenser', + 'pallets/lazy-executor', + 'pallets/intent', + 'pallets/ice', + 'pallets/ice/support', + 'ice/ice-solver', + 'ice/solver-bench', + 'pallets/ice/amm-simulator', ] resolver = "2" @@ -63,6 +70,11 @@ serde = { version = "1.0.209", default-features = false } primitive-types = { version = "0.13.1", default-features = false } borsh = { version = "1.5.7", default-features = false, features = ["derive"] } +# ICE +ice-solver = { path = "ice/ice-solver", default-features = false} +route-findr= { path = "ice/route-findr", default-features = false } + + affix = "0.1.2" alloy-primitives = { version = "0.7", default-features = false } alloy-sol-types = { version = "0.7", default-features = false } @@ -164,6 +176,11 @@ pallet-hsm = { path = "pallets/hsm", default-features = false } pallet-parameters = { path = "pallets/parameters", default-features = false } pallet-signet = { path = "pallets/signet", default-features = false } pallet-dispenser = { path = "pallets/dispenser", default-features = false } +pallet-intent = { path = "pallets/intent", default-features = false } +pallet-ice = { path = "pallets/ice", default-features = false } +ice-support = { path = "pallets/ice/support", default-features = false } +amm-simulator = { path = "pallets/ice/amm-simulator", default-features = false } +pallet-lazy-executor = { path = "pallets/lazy-executor", default-features = false } hydra-dx-build-script-utils = { path = "utils/build-script-utils", default-features = false } scraper = { path = "scraper", default-features = false } diff --git a/ice/ice-solver/Cargo.toml b/ice/ice-solver/Cargo.toml new file mode 100644 index 0000000000..1634e4e801 --- /dev/null +++ b/ice/ice-solver/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "ice-solver" +version = "0.1.0" +edition = "2021" + +[dependencies] +codec = { workspace = true, features = ["derive", "max-encoded-len"] } +frame-support = { workspace = true } +ice-support = { workspace = true } +hydradx-traits = { workspace = true } +hydra-dx-math = { workspace = true } +sp-core = { workspace = true } +sp-std = { workspace = true } +log = { workspace = true } + +[features] +default = ['std'] +std = [ + 'codec/std', + 'frame-support/std', + 'ice-support/std', + 'hydradx-traits/std', + 'hydra-dx-math/std', + 'sp-core/std', + 'sp-std/std', + "log/std", +] + diff --git a/ice/ice-solver/SOLVER.md b/ice/ice-solver/SOLVER.md new file mode 100644 index 0000000000..55235deee6 --- /dev/null +++ b/ice/ice-solver/SOLVER.md @@ -0,0 +1,122 @@ +# ICE Solver — How It Works + +## What It Does + +The ICE solver takes a batch of swap intents (users wanting to trade one asset for another) and figures out the best way to fulfill as many as possible. It does this by combining two strategies: + +- **Direct matching**: pairing users who want opposite trades (Alice sells HDX for HOLLAR, Bob sells HOLLAR for HDX — they trade with each other, no pool needed) +- **AMM routing**: sending leftover volume through on-chain liquidity pools to complete the trades + +The solver runs off-chain, produces a solution, and submits it to the chain for execution. + +## Core Principle: Everyone Gets the Same Price + +All users trading in the same direction get the same rate. If ten people are all selling HDX for HOLLAR, they all receive the same HOLLAR-per-HDX rate — regardless of how much slippage each individual was willing to accept. + +This prevents exploitation. A user who sets a loose limit (willing to accept a bad rate) cannot be taken advantage of — they receive the same rate as everyone else, and any surplus above their minimum contributes to the solution score. + +The two directions of a pair can have different rates. If HDX→HOLLAR sellers get 2000 HOLLAR per HDX, the HOLLAR→HDX sellers might get a slightly different rate. The gap between these rates is surplus captured from direct matching — value that would have been lost to pool slippage if everyone traded individually. + +## The Algorithm + +### 1. Filter Out Hopeless Intents + +First, the solver checks each intent against current market prices. If selling 100 HDX can only get you 180 HOLLAR at the best available rate, but you're asking for 250 HOLLAR minimum — your intent is unsatisfiable and gets removed immediately. No point including it in the calculations. + +This check uses two methods: +- **Route simulation**: actually simulate the trade through available pool routes to see what output you'd get +- **Spot price check**: compare against the best marginal price (the price you'd get for an infinitely small trade). If you can't meet the minimum even at this theoretical best, you definitely can't be satisfied + +### 2. Discover Clearing Prices + +The solver groups intents by asset pair and computes a **clearing price** — the rate at which all included intents can be fulfilled. + +Here's the key insight: when multiple users sell the same asset through the same pool, they share the price impact. Ten users each selling 100 HDX is equivalent to one trade of 1000 HDX, which moves the price more than any individual trade would. The clearing price reflects this combined impact. + +This creates a tension: including more intents means more volume, which means worse rates, which means some intents can't meet their minimums. The solver resolves this by iterating: + +1. Start with all satisfiable intents +2. Compute the clearing price for the combined volume +3. Remove any intents whose minimum can't be met at this rate +4. Recompute with the smaller set — the rate improves since there's less volume +5. Repeat until stable — no more intents need to be removed + +This converges quickly because each round can only remove intents, never add them. + +**Important**: This batch rate is why an intent might be rejected even though it would succeed as an individual trade. Trading alone, your 10 USDT might get you 5.25 HDX. But when 15 other people are also selling USDT for HDX at the same time, the combined 150 USDT pushes the pool price down, and everyone only gets 3.69 HDX per 10 USDT. If your minimum was 5.47 HDX, you get filtered out. + +### 3. Find Ring Trades + +Before touching any pool, the solver looks for **ring trades** — cycles of three assets where users can trade peer-to-peer in a circle. + +Example: Alice sells HDX for HOLLAR, Bob sells HOLLAR for BNC, Charlie sells BNC for HDX. These three can trade directly with each other — Alice's HDX goes to Charlie, Charlie's BNC goes to Bob, Bob's HOLLAR goes to Alice. No pool needed, no slippage, no fees. + +The solver finds these 3-asset cycles, checks that all participants would get at least their minimum at spot prices, and fills them at the bottleneck volume (limited by the smallest leg of the cycle). + +Ring trades are strictly better than pool trades — they capture maximum value from multi-asset matching. + +### 4. Execute AMM Trades for the Remainder + +After ring matching, there's usually leftover volume that can't be matched peer-to-peer. For each asset pair, the solver computes the **net imbalance** — how much more is being sold in one direction than the other — and routes that excess through the AMM. + +The flow analysis for each pair works like this: + +- **One-sided flow** (only sellers in one direction): the entire volume goes through the pool +- **Opposing flow with excess**: the smaller side is fully absorbed by direct matching (they get approximately spot rate — no slippage). Only the excess on the larger side goes to the pool +- **Perfect cancellation**: volumes match exactly, no pool trade needed at all + +For each AMM trade, the solver discovers available routes (which may go through multiple pools), simulates each one, and picks the route with the best output. A small safety margin (0.01%) is subtracted from the simulated output to account for tiny differences between the simulator and the real pool math. + +If the leftover amount is smaller than the asset's existential deposit (dust from near-perfect cancellation), the trade is skipped entirely. + +### 5. Blend Rates and Resolve Intents + +Now the solver has two sources of output for each direction: ring fills (peer-to-peer) and AMM output. It blends these into a single **unified rate** per direction: + +> unified rate = (ring output + AMM output) / total input + +Every intent in the same direction gets this same rate applied to their individual amount. This ensures fairness — no one gets a better deal just because their portion happened to be matched via a ring. + +The solver then checks each intent: does the unified rate give them at least their minimum? If yes, they're resolved. If not, they're dropped. + +### 6. Stabilization + +Sometimes an intent passes the initial clearing price check (step 2) but fails after the actual rates are computed (step 5), because the rates differ slightly due to ring fill blending, rounding, and the safety margin. + +If any intents are dropped during resolution, their volume was already baked into the AMM trades. The solver handles this by re-running steps 3–5 with only the actually-resolved intents. This produces new trades that match the real volumes, and new rates that might allow different intents to resolve. + +This loop repeats until stable — no intents drop during resolution, meaning the trades perfectly match the resolved set. + +### 7. Compute Score + +The **score** is the total surplus across all resolved intents: how much more each user receives compared to their stated minimum. Higher score means more value delivered to users. + +## On-Chain Execution + +The solution is submitted as an unsigned transaction. The pallet executes it in three phases: + +1. **Unlock and collect**: For each resolved intent, unreserve the user's tokens (locked when they submitted the intent) and transfer them to the ICE holding account + +2. **Execute AMM trades**: Run each pool trade from the holding account. The holding account now has a mix of assets from step 1 and AMM outputs from this step + +3. **Pay out**: For each resolved intent, deduct the protocol fee from their output amount and transfer the remainder from the holding account to the user. Verify that all intents in the same direction received the same rate, and that the score matches + +## Summary of Matching Strategies + +| Strategy | How it works | Slippage | When used | +|----------|-------------|----------|-----------| +| **Direct matching** | Opposing intents trade with each other | None | When both directions have volume in a pair | +| **Ring matching** | 3-asset cycles trade peer-to-peer | None | When A→B, B→C, C→A intents all exist | +| **AMM routing** | Excess volume goes through liquidity pools | Yes — shared across all same-direction intents | For net imbalance after matching | + +## Known Limitations + +**Batch slippage can exclude viable intents.** All intents in the same direction share the AMM slippage from their combined volume. An intent that would succeed as an individual trade may be rejected because the batch rate is worse. The solver does not currently optimize which subset to include to maximize the number of resolved intents. + +**No partial fills.** An intent is either fully resolved or fully excluded. An intent that could be 90% filled at a good rate is excluded entirely rather than being partially satisfied. + +**Only 3-asset rings.** Longer cycles (4+ assets) are not detected. Some multi-asset matching opportunities are missed. + +**Single-pass route selection.** The solver picks the best route per pair independently. It does not consider how one trade's impact on pool state affects the available routes for other pairs. + +**Simulation tolerance.** A 0.01% safety margin covers typical rounding differences between the off-chain simulator and on-chain pool math. Larger divergences (from pool state changes between simulation and execution) could cause the on-chain execution to fail. diff --git a/ice/ice-solver/src/common/flow_graph.rs b/ice/ice-solver/src/common/flow_graph.rs new file mode 100644 index 0000000000..ed03c3effa --- /dev/null +++ b/ice/ice-solver/src/common/flow_graph.rs @@ -0,0 +1,88 @@ +//! Flow graph types and construction shared across solver versions. + +use ice_support::{AssetId, Balance, Intent, IntentData, IntentId, Partial}; +use sp_core::U256; +use sp_std::collections::btree_map::BTreeMap; +use sp_std::vec::Vec; + +/// Directed pair (asset_in, asset_out) +pub type Pair = (AssetId, AssetId); + +/// An intent entry in the flow graph, tracking its limit price and remaining fill volume. +#[derive(Debug, Clone)] +pub struct IntentEntry { + pub intent_id: IntentId, + pub asset_in: AssetId, + pub asset_out: AssetId, + pub original_amount_in: Balance, + pub min_amount_out: Balance, + /// Limit price as (numerator, denominator) = min_amount_out / amount_in + pub limit_price: (U256, U256), + /// Remaining amount_in not yet matched + pub remaining_in: Balance, + /// Whether this intent supports partial fills. + /// Partial fill state. Used by v2 solver for variable fill amounts. + pub partial: Partial, +} + +/// The flow graph: intents grouped by directed pair. +pub type FlowGraph = BTreeMap>; + +/// A fill record for one intent. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MatchFill { + pub intent_id: IntentId, + pub amount_in: Balance, + pub amount_out: Balance, +} + +/// Build flow graph from intents with per-intent volume caps. +/// +/// Each entry's `remaining_in` is bounded by the caller-provided `cap` — typically +/// the solver's decided `fill_amount` (fitted via clearing-price binary search) or +/// the intent's `swap.remaining()` when no narrower fill is known. This keeps +/// ring detection honest: it cannot match more volume than the user has reserved +/// and the solver has allocated. +/// +/// `original_amount_in` and `limit_price` remain derived from the intent's original +/// `amount_in`/`amount_out` — so `fills_meet_limits` pro-rata formula keeps the +/// intent's real limit price. A partial whose cap is below `amount_in` will never +/// hit the "full fill" branch (cap ≤ amount_in and fill ≤ cap), so pro-rata is the +/// only enforced bound, as intended. +pub fn build_flow_graph(intents: &[(&Intent, Balance)]) -> FlowGraph { + let mut graph: FlowGraph = BTreeMap::new(); + + for &(intent, cap) in intents { + let IntentData::Swap(swap) = &intent.data else { + continue; + }; + let pair = (swap.asset_in, swap.asset_out); + + let limit_price = (U256::from(swap.amount_out), U256::from(swap.amount_in)); + let remaining_in = cap.min(swap.remaining()); + + let entry = IntentEntry { + intent_id: intent.id, + asset_in: swap.asset_in, + asset_out: swap.asset_out, + original_amount_in: swap.amount_in, + min_amount_out: swap.amount_out, + limit_price, + remaining_in, + partial: swap.partial, + }; + + graph.entry(pair).or_default().push(entry); + } + + // Sort each group by limit price ascending (cheapest sellers first) + for entries in graph.values_mut() { + entries.sort_by(|a, b| { + let lhs = a.limit_price.0.saturating_mul(b.limit_price.1); + let rhs = b.limit_price.0.saturating_mul(a.limit_price.1); + lhs.cmp(&rhs) + }); + } + + graph +} diff --git a/ice/ice-solver/src/common/mod.rs b/ice/ice-solver/src/common/mod.rs new file mode 100644 index 0000000000..cdf46bd644 --- /dev/null +++ b/ice/ice-solver/src/common/mod.rs @@ -0,0 +1,199 @@ +//! Common utilities shared between solver versions. + +pub mod flow_graph; +pub mod ring_detection; + +use hydra_dx_math::types::Ratio; +use ice_support::{AssetId, Balance, Intent, IntentData}; +use sp_core::U256; +use sp_std::collections::btree_map::BTreeMap; +use sp_std::collections::btree_set::BTreeSet; + +/// out = amount_in * (price_in / price_out) +/// = amount_in * price_in.n * price_out.d / (price_in.d * price_out.n) +/// +/// Overflow-safe: handles large Ratio values (128-bit n/d) from real AMM spot prices. +/// Tries multiple computation orders to avoid U256 overflow while preserving precision. +pub fn calc_amount_out(amount_in: Balance, price_in: &Ratio, price_out: &Ratio) -> Option { + let pi_n = U256::from(price_in.n); + let pi_d = U256::from(price_in.d); + let po_n = U256::from(price_out.n); + let po_d = U256::from(price_out.d); + let amt = U256::from(amount_in); + + // Strategy 1: direct — amount_in * (pi.n * po.d) / (pi.d * po.n) + if let (Some(n), Some(d)) = (pi_n.checked_mul(po_d), pi_d.checked_mul(po_n)) { + if let Some(result) = amt.checked_mul(n) { + return result.checked_div(d)?.try_into().ok(); + } + // amount_in * n overflows — split n/d (only useful when n >= d) + if n >= d { + let q = n.checked_div(d)?; + let r = n.checked_rem(d)?; + let base = amt.checked_mul(q)?; + let correction = amt + .checked_mul(r) + .and_then(|v| v.checked_div(d)) + .unwrap_or(U256::zero()); + return base.checked_add(correction)?.try_into().ok(); + } + // n < d: ratio < 1, split loses all precision — fall through to strategy 2/3 + } + + // Strategy 2: cross-cancel — (amount_in * pi.n / po.n) * (po.d / pi.d) + // This works when pi.n and po.n are similar magnitude (both large) so their ratio is small. + if let Some(ratio_n) = amt.checked_mul(pi_n) { + let step1 = ratio_n.checked_div(po_n)?; + if let Some(v) = step1.checked_mul(po_d) { + return v.checked_div(pi_d)?.try_into().ok(); + } + } + + // Strategy 3: (amount_in / pi.d) * pi.n then * po.d / po.n + // Divide early to keep values small. + let step1 = mul_div(amt, pi_n, pi_d)?; + let result = mul_div(step1, po_d, po_n)?; + result.try_into().ok() +} + +/// Compute a * b / c with overflow protection. +pub fn mul_div(a: U256, b: U256, c: U256) -> Option { + if c.is_zero() { + return None; + } + if let Some(v) = a.checked_mul(b) { + return v.checked_div(c); + } + // a * b overflows — use: (a / c) * b + (a % c) * b / c + let q = a.checked_div(c)?; + let r = a.checked_rem(c)?; + let base = q.checked_mul(b)?; + let correction = r.checked_mul(b).and_then(|v| v.checked_div(c)).unwrap_or(U256::zero()); + base.checked_add(correction) +} + +pub fn collect_unique_assets(intents: &[Intent]) -> BTreeSet { + intents + .iter() + .filter_map(|i| { + let IntentData::Swap(swap) = &i.data else { + return None; + }; + Some([swap.asset_in, swap.asset_out]) + }) + .flatten() + .collect() +} + +pub fn is_satisfiable(intent: &Intent, spot_prices: &BTreeMap) -> bool { + let IntentData::Swap(swap) = &intent.data else { + return false; + }; + + let Some(price_in) = spot_prices.get(&swap.asset_in) else { + log::trace!(target: "solver", "intent {}: not satisfiable — no spot price for asset_in {}", intent.id, swap.asset_in); + return false; + }; + let Some(price_out) = spot_prices.get(&swap.asset_out) else { + log::trace!(target: "solver", "intent {}: not satisfiable — no spot price for asset_out {}", intent.id, swap.asset_out); + return false; + }; + + let Some(calculated_out) = calc_amount_out(swap.amount_in, price_in, price_out) else { + log::trace!(target: "solver", "intent {}: not satisfiable — calc_amount_out overflow for {} → {}", intent.id, swap.asset_in, swap.asset_out); + return false; + }; + if calculated_out < swap.amount_out { + log::trace!(target: "solver", "intent {}: not satisfiable — spot output {} < min_out {} for {} → {}", + intent.id, calculated_out, swap.amount_out, swap.asset_in, swap.asset_out); + return false; + } + log::trace!(target: "solver", "intent {}: satisfiable — spot output {} >= min_out {} for {} → {}", + intent.id, calculated_out, swap.amount_out, swap.asset_in, swap.asset_out); + true +} + +/// Analysis of net flow between two assets in opposing directions. +/// +/// Determines how to split volume between direct matching and AMM: +/// - Scarce side (less total value) gets fully matched at spot rate +/// - Excess side gets direct match + AMM for remainder +#[derive(Debug, Clone, Copy)] +pub enum FlowDirection { + /// Only forward (A→B) intents exist. + SingleForward { amount: Balance }, + /// Only backward (B→A) intents exist. + SingleBackward { amount: Balance }, + /// Both directions; A side has more value — excess A goes to AMM. + ExcessForward { + /// B→A rate output: amount of A given to B sellers via direct match + scarce_out: Balance, + /// Amount of B going to A sellers from direct match (= total_b_sold) + direct_match: Balance, + /// Net A to sell through AMM + net_sell: Balance, + }, + /// Both directions; B side has more value — excess B goes to AMM. + ExcessBackward { + /// A→B rate output: amount of B given to A sellers via direct match + scarce_out: Balance, + /// Amount of A going to B sellers from direct match (= total_a_sold) + direct_match: Balance, + /// Net B to sell through AMM + net_sell: Balance, + }, + /// Volumes cancel at spot — no AMM trade needed. + PerfectCancel { a_as_b: Balance, b_as_a: Balance }, +} + +/// Analyze opposing flows to determine direct matching volumes and net AMM requirement. +/// +/// Precondition: at least one of `total_a_sold`, `total_b_sold` must be > 0. +pub fn analyze_pair_flow(total_a_sold: Balance, total_b_sold: Balance, pa: &Ratio, pb: &Ratio) -> FlowDirection { + debug_assert!( + total_a_sold > 0 || total_b_sold > 0, + "analyze_pair_flow called with both volumes zero" + ); + if total_b_sold == 0 { + return FlowDirection::SingleForward { amount: total_a_sold }; + } + if total_a_sold == 0 { + return FlowDirection::SingleBackward { amount: total_b_sold }; + } + + let a_as_b = calc_amount_out(total_a_sold, pa, pb).unwrap_or(0); + + if a_as_b > total_b_sold { + // Excess A: more A value than B value + let matched_a_for_b = calc_amount_out(total_b_sold, pb, pa).unwrap_or(0); + let net_a = total_a_sold.saturating_sub(matched_a_for_b); + if net_a == 0 { + return FlowDirection::PerfectCancel { + a_as_b, + b_as_a: matched_a_for_b, + }; + } + FlowDirection::ExcessForward { + scarce_out: matched_a_for_b, + direct_match: total_b_sold, + net_sell: net_a, + } + } else if total_b_sold > a_as_b || a_as_b == 0 { + // Excess B: more B value than A value + let matched_b_for_a = a_as_b; + let net_b = total_b_sold.saturating_sub(matched_b_for_a); + if net_b == 0 { + let b_as_a = calc_amount_out(total_b_sold, pb, pa).unwrap_or(0); + return FlowDirection::PerfectCancel { a_as_b, b_as_a }; + } + FlowDirection::ExcessBackward { + scarce_out: matched_b_for_a, + direct_match: total_a_sold, + net_sell: net_b, + } + } else { + // a_as_b == total_b_sold: perfect cancel + let b_as_a = calc_amount_out(total_b_sold, pb, pa).unwrap_or(0); + FlowDirection::PerfectCancel { a_as_b, b_as_a } + } +} diff --git a/ice/ice-solver/src/common/ring_detection.rs b/ice/ice-solver/src/common/ring_detection.rs new file mode 100644 index 0000000000..98aba73c82 --- /dev/null +++ b/ice/ice-solver/src/common/ring_detection.rs @@ -0,0 +1,281 @@ +//! Ring trade detection and filling. +//! +//! Detects 3-asset cycles (A→B→C→A) in remaining flow graph and fills them +//! at the bottleneck volume using spot-price-consistent rates. +//! Ring trades avoid AMM interaction entirely — assets flow peer-to-peer around the cycle. + +use crate::common::flow_graph::{FlowGraph, IntentEntry, MatchFill, Pair}; +use crate::common::{calc_amount_out, mul_div}; +use hydra_dx_math::types::Ratio; +use ice_support::{AssetId, Balance}; +use sp_core::U256; +use sp_std::collections::btree_map::BTreeMap; +use sp_std::vec; +use sp_std::vec::Vec; + +/// A ring trade through 3 assets with fills on each edge. +#[derive(Debug, Clone)] +pub struct RingTrade { + /// Three edges forming the cycle: (A→B), (B→C), (C→A) + pub edges: Vec<(Pair, Vec)>, +} + +/// Detect and fill feasible 3-asset cycles (A→B→C→A) in the remaining flow graph. +/// +/// Only detects 3-asset rings. Longer cycles (4+ assets) are not attempted. +/// +/// Uses spot prices to compute fill rates (not limit prices). +/// Limit prices are only used for the feasibility check — ensuring each +/// participant receives at least their minimum. +/// +/// Fills at bottleneck volume and repeats until no more rings are found. +pub fn detect_rings(graph: &mut FlowGraph, spot_prices: &BTreeMap) -> Vec { + let mut rings = Vec::new(); + + loop { + let mut found = false; + + let pairs: Vec = graph.keys().copied().collect(); + + for &(a, b) in &pairs { + let bc_pairs: Vec = pairs + .iter() + .filter(|&&(x, y)| x == b && y != a) + .map(|&(_, y)| y) + .collect(); + + for c in bc_pairs { + if !graph.contains_key(&(c, a)) { + continue; + } + + let ab_has_volume = graph + .get(&(a, b)) + .map(|e| e.iter().any(|i| i.remaining_in > 0)) + .unwrap_or(false); + let bc_has_volume = graph + .get(&(b, c)) + .map(|e| e.iter().any(|i| i.remaining_in > 0)) + .unwrap_or(false); + let ca_has_volume = graph + .get(&(c, a)) + .map(|e| e.iter().any(|i| i.remaining_in > 0)) + .unwrap_or(false); + + if !ab_has_volume || !bc_has_volume || !ca_has_volume { + continue; + } + + // Get spot prices for all 3 assets + let (Some(pa), Some(pb), Some(pc)) = (spot_prices.get(&a), spot_prices.get(&b), spot_prices.get(&c)) + else { + continue; + }; + + // Feasibility: check that each edge's best intent can be satisfied at spot rate + let ab_best = first_with_remaining(graph.get(&(a, b)).expect("edge (a,b) verified above")); + let bc_best = first_with_remaining(graph.get(&(b, c)).expect("edge (b,c) verified above")); + let ca_best = first_with_remaining(graph.get(&(c, a)).expect("edge (c,a) verified above")); + + let (ab_best, bc_best, ca_best) = match (ab_best, bc_best, ca_best) { + (Some(ab), Some(bc), Some(ca)) => (ab, bc, ca), + _ => continue, + }; + + // Check each intent's limit rate is met at spot: compare against the + // intent's full `original_amount_in` → `min_amount_out` bound, which is + // equivalent to asking "is spot_rate ≥ limit_rate?" independent of the + // current `remaining_in` volume. Using `remaining_in` here would compare + // a scaled-down output against an unscaled minimum, spuriously rejecting + // partials whose cap is below their original `amount_in`. + let ab_spot = calc_amount_out(ab_best.original_amount_in, pa, pb); + let bc_spot = calc_amount_out(bc_best.original_amount_in, pb, pc); + let ca_spot = calc_amount_out(ca_best.original_amount_in, pc, pa); + + let (Some(ab_out_at_spot), Some(bc_out_at_spot), Some(ca_out_at_spot)) = (ab_spot, bc_spot, ca_spot) + else { + continue; + }; + + if ab_out_at_spot < ab_best.min_amount_out + || bc_out_at_spot < bc_best.min_amount_out + || ca_out_at_spot < ca_best.min_amount_out + { + continue; + } + + // Compute bottleneck: convert all edge volumes to asset A equivalent at spot + let ab_vol_a = U256::from(ab_best.remaining_in); + let bc_vol_a = calc_amount_out(bc_best.remaining_in, pb, pa) + .map(U256::from) + .unwrap_or(U256::zero()); + let ca_vol_a = calc_amount_out(ca_best.remaining_in, pc, pa) + .map(U256::from) + .unwrap_or(U256::zero()); + + let bottleneck_a = ab_vol_a.min(bc_vol_a).min(ca_vol_a); + if bottleneck_a.is_zero() { + continue; + } + + let bottleneck_a_128: Balance = bottleneck_a.try_into().unwrap_or(0); + if bottleneck_a_128 == 0 { + continue; + } + + // Fill amounts at spot rates + // AB: input = bottleneck_a of A, output = calc_amount_out(bottleneck_a, pa, pb) of B + let ab_amount_in = bottleneck_a_128; + let ab_amount_out = calc_amount_out(ab_amount_in, pa, pb).unwrap_or(0); + + // BC: input = ab_amount_out of B, output at spot + let bc_amount_in = ab_amount_out; + let bc_amount_out = calc_amount_out(bc_amount_in, pb, pc).unwrap_or(0); + + // CA: input = bc_amount_out of C, output at spot. + // Note: ca_amount_out may differ from ab_amount_in by ≤1 due to + // accumulated rounding across the 3 spot conversions. The protocol + // absorbs this dust difference. + let ca_amount_in = bc_amount_out; + let ca_amount_out = calc_amount_out(ca_amount_in, pc, pa).unwrap_or(0); + + if ab_amount_in == 0 + || ab_amount_out == 0 + || bc_amount_in == 0 + || bc_amount_out == 0 + || ca_amount_in == 0 + || ca_amount_out == 0 + { + continue; + } + + // Final feasibility: verify each fill meets the intent's limit + // (spot rate should satisfy, but check after rounding) + let ab_entries = graph.get(&(a, b)).expect("edge (a,b) verified above"); + if !fills_meet_limits(ab_entries, ab_amount_in, ab_amount_out) { + continue; + } + let bc_entries = graph.get(&(b, c)).expect("edge (b,c) verified above"); + if !fills_meet_limits(bc_entries, bc_amount_in, bc_amount_out) { + continue; + } + let ca_entries = graph.get(&(c, a)).expect("edge (c,a) verified above"); + if !fills_meet_limits(ca_entries, ca_amount_in, ca_amount_out) { + continue; + } + + let ab_fill = fill_intent( + graph.get_mut(&(a, b)).expect("edge (a,b) verified above"), + ab_amount_in, + ab_amount_out, + ); + let bc_fill = fill_intent( + graph.get_mut(&(b, c)).expect("edge (b,c) verified above"), + bc_amount_in, + bc_amount_out, + ); + let ca_fill = fill_intent( + graph.get_mut(&(c, a)).expect("edge (c,a) verified above"), + ca_amount_in, + ca_amount_out, + ); + + rings.push(RingTrade { + edges: vec![((a, b), ab_fill), ((b, c), bc_fill), ((c, a), ca_fill)], + }); + + found = true; + break; + } + + if found { + break; + } + } + + if !found { + break; + } + } + + rings +} + +fn first_with_remaining(entries: &[IntentEntry]) -> Option<&IntentEntry> { + entries.iter().find(|e| e.remaining_in > 0) +} + +/// Check that filling `amount_in` with `amount_out` across entries meets all limits. +/// +/// Note: ring fills may partially consume a non-partial intent. This is safe because +/// the remaining volume goes through the normal AMM path, and the final resolution +/// always uses the full `amount_in` with a unified rate. Ring partial fills are +/// internal bookkeeping, not user-visible partial fills. +fn fills_meet_limits(entries: &[IntentEntry], total_in: Balance, total_out: Balance) -> bool { + let mut remaining_in = total_in; + for entry in entries { + if remaining_in == 0 { + break; + } + if entry.remaining_in == 0 { + continue; + } + let fill_in = remaining_in.min(entry.remaining_in); + let fill_out = mul_div(U256::from(fill_in), U256::from(total_out), U256::from(total_in)) + .and_then(|v| v.try_into().ok()) + .unwrap_or(0u128); + + if fill_in == entry.original_amount_in { + // Full fill: must meet the intent's absolute minimum + if fill_out < entry.min_amount_out { + return false; + } + } else { + // Partial fill: must meet pro-rata minimum + let pro_rata_min = mul_div( + U256::from(fill_in), + U256::from(entry.min_amount_out), + U256::from(entry.original_amount_in), + ) + .and_then(|v| v.try_into().ok()) + .unwrap_or(0u128); + if fill_out < pro_rata_min { + return false; + } + } + remaining_in = remaining_in.saturating_sub(fill_in); + } + true +} + +fn fill_intent(entries: &mut [IntentEntry], amount_in: Balance, amount_out: Balance) -> Vec { + let mut fills = Vec::new(); + let mut remaining_in = amount_in; + let mut remaining_out = amount_out; + + for entry in entries { + if remaining_in == 0 { + break; + } + if entry.remaining_in == 0 { + continue; + } + + let fill_in = remaining_in.min(entry.remaining_in); + let fill_out = mul_div(U256::from(fill_in), U256::from(remaining_out), U256::from(remaining_in)) + .and_then(|v| v.try_into().ok()) + .unwrap_or(0); + + entry.remaining_in = entry.remaining_in.saturating_sub(fill_in); + remaining_in = remaining_in.saturating_sub(fill_in); + remaining_out = remaining_out.saturating_sub(fill_out); + + fills.push(MatchFill { + intent_id: entry.intent_id, + amount_in: fill_in, + amount_out: fill_out, + }); + } + + fills +} diff --git a/ice/ice-solver/src/lib.rs b/ice/ice-solver/src/lib.rs new file mode 100644 index 0000000000..af12e77ea3 --- /dev/null +++ b/ice/ice-solver/src/lib.rs @@ -0,0 +1,9 @@ +#![cfg_attr(not(feature = "std"), no_std)] +pub mod common; +pub mod v2; + +#[cfg(feature = "std")] +pub mod replay_format; + +#[cfg(test)] +mod tests; diff --git a/ice/ice-solver/src/replay_format.rs b/ice/ice-solver/src/replay_format.rs new file mode 100644 index 0000000000..9bac33f5ed --- /dev/null +++ b/ice/ice-solver/src/replay_format.rs @@ -0,0 +1,97 @@ +//! Wire format shared between the ICE solver fixture recorder (in +//! `runtime-integration-tests`) and the regression-test replay harness +//! (in `src/tests/regressions.rs`). +//! +//! Defining it in one place prevents silent drift: if a variant or field +//! is added here, both writer and reader pick it up and fail to compile +//! until they're updated. + +use codec::{Decode, Encode}; +use core::fmt::Write; +use hydra_dx_math::types::Ratio; +use hydradx_traits::router::Route; +use ice_support::{AssetId, Balance}; + +/// One recorded AMM method call: inputs the solver passed in, plus the +/// observable result (errors collapse to `()` — only success shape is replayed). +#[derive(Debug, Clone, Encode, Decode)] +pub enum Response { + DiscoverRoutes { + asset_in: AssetId, + asset_out: AssetId, + result: Result>, ()>, + }, + Sell { + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + result: Result<(Balance, Route), ()>, + }, + Buy { + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + result: Result<(Balance, Route), ()>, + }, + SpotPrice { + asset_in: AssetId, + asset_out: AssetId, + result: Result, + }, + ExistentialDeposit { + asset_id: AssetId, + ed: Balance, + }, +} + +/// A full trace: the AMM's price denominator (static across the run) plus +/// the ordered list of calls the solver made. +#[derive(Debug, Clone, Encode, Decode)] +pub struct Trace { + pub price_denominator: AssetId, + pub responses: Vec, +} + +impl Trace { + /// Produce the 3-line hex fixture: SCALE(intents), SCALE(solution), SCALE(trace). + pub fn encode_fixture(intents: &I, solution: &S, trace: &Self) -> String { + let mut out = String::new(); + out.push_str(&encode_hex(&intents.encode())); + out.push('\n'); + out.push_str(&encode_hex(&solution.encode())); + out.push('\n'); + out.push_str(&encode_hex(&trace.encode())); + out.push('\n'); + out + } + + /// Inverse of `encode_fixture`. Returns raw bytes for intents/solution so + /// callers can decode them with their own concrete types, plus the + /// already-decoded `Trace`. + pub fn decode_fixture(raw: &str) -> (Vec, Vec, Self) { + let mut lines = raw.lines().filter(|l| !l.is_empty()); + let intents_hex = lines.next().expect("intents line"); + let solution_hex = lines.next().expect("solution line"); + let trace_hex = lines.next().expect("trace line"); + let intents = decode_hex(intents_hex); + let solution = decode_hex(solution_hex); + let trace = Trace::decode(&mut &decode_hex(trace_hex)[..]).expect("decode trace"); + (intents, solution, trace) + } +} + +fn encode_hex(bytes: &[u8]) -> String { + let mut s = String::with_capacity(bytes.len() * 2); + for b in bytes { + write!(s, "{b:02x}").unwrap(); + } + s +} + +fn decode_hex(s: &str) -> Vec { + assert!(s.len() % 2 == 0, "hex length must be even"); + (0..s.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&s[i..i + 2], 16).expect("valid hex")) + .collect() +} diff --git a/ice/ice-solver/src/tests/fixtures/existential_deposit.hex b/ice/ice-solver/src/tests/fixtures/existential_deposit.hex new file mode 100644 index 0000000000..465c253391 --- /dev/null +++ b/ice/ice-solver/src/tests/fixtures/existential_deposit.hex @@ -0,0 +1,3 @@ +bc7c00000000000000709f41b59d01000000de00000000000000000040b2bac9e0191e020000000000000000a298a2de3e28000000000000000001000000000000000000000000000000007b0000000000000050e03eb59d010000000a000000de000000a40fba0000000000000000000000000000c0d120c57ea798000000000000000001000000000000000000000000000000007900000000000000106239b59d0100000000000000de00000000407a10f35a00000000000000000000000020f84dde700400000000000000000100ff31d72306000000000000000000007800000000000000b07738b59d01000000de00000000000000000080647593c1333c04000000000000000068e85cb77c5500000000000000000100406767d38eba4403000000000000007700000000000000508d37b59d0100000000000000de00000000407a10f35a000000000000000000000040a0b2ce7c9004000000000000000001000000000000000000000000000000007600000000000000508d37b59d010000000a00000000000000005ed0b200000000000000000000000000b0a3990a29aa0c0000000000000000017d1f54110000000000000000000000007500000000000000508d37b59d01000000de00000000000000000080647593c1333c040000000000000000de8ae4711954000000000000000001796acf2b018b131291000000000000007400000000000000f0a236b59d010000000a00000000000000484e9ac900000000000000000000000000f0cd9d41f94b0e000000000000000001537b89130000000000000000000000007300000000000000107436b59d010000000a00000000000000009435770000000000000000000000000000be10b7ae9a08000000000000000001546a8d0b0000000000000000000000007100000000000000107436b59d0100000000000000de00000000407a10f35a000000000000000000000040b77002118f0400000000000000000100000000000000000000000000000000700000000000000070e735b59d0100000000000000de00000000407a10f35a0000000000000000000000403e1072e87604000000000000000001000000000000000000000000000000006f0000000000000090b835b59d0100000000000000de00000000407a10f35a0000000000000000000000403e1072e87604000000000000000001000000000000000000000000000000006d0000000000000030ce34b59d0100000000000000de00000000407a10f35a0000000000000000000000403e1072e87604000000000000000001000000000000000000000000000000006c00000000000000d0e333b59d0100000000000000de00000000407a10f35a0000000000000000000000403e1072e87604000000000000000001000000000000000000000000000000006b0000000000000070f932b59d0100000000000000de00000000407a10f35a0000000000000000000000403e1072e87604000000000000000001000000000000000000000000000000006000000000000000f04f2fb59d01000000000000000a0000000000e8890423c78a0000000000000000e0eeba8207000000000000000000000001000000000000000000000000000000005e0000000000000090652eb59d0100000030450f000000000049e71dcb00000000000000000000000000ac3407e9f24901000000000000000001000000000000000000000000000000005d0000000000000090652eb59d010000000a0000000000000000286bee00000000000000000000000000b087d611304e11000000000000000001a8d41a170000000000000000000000005700000000000000f00e25b59d01000000000000006e0000004c0870245e67d78d00000000000000000000e1ebd0ca902adf0600000000000001000000000000000000000000000000005300000000000000c87423b59d01000000de00000030450f0000003029881a56431000000000000000c0262cce00000000000000000000000001000000000000000000000000000000004d00000000000000f0a920b59d01000000000000000a00000000209e9e8f770b0e0000000000000000d0fd33c100000000000000000000000001ccb136dab1a89a0400000000000000004c00000000000000f0a920b59d01000000de00000000000000000040bd8b5b936b6c000000000000000090ab7624a3b908000000000000000001000000000000000000000000000000004a0000000000000090bf1fb59d01000000000000002200000000008a5d78456301000000000000000000c010378864e101000000000000000000490000000000000090bf1fb59d010000000a00000000000000d60634c100000000000000000000000000209e9e8f770b0e0000000000000000016343b512000000000000000000000000460000000000000030d51eb59d0100000000000000de0000000000e8890423c78a00000000000000000000704c68ebc452db060000000000000100000000000000000000000000000000450000000000000030d51eb59d010000000a000000220000000065cd1d0000000000000000000000000080fa9cf791f702000000000000000001000000000000000000000000000000003f0000000000000010161cb59d01000000de00000000000000000020c65abc8ed70a0000000000000000b02bde068c0701000000000000000001000000000000000000000000000000003d0000000000000010161cb59d01000000de000000000000000000e8890423c78a000000000000000000c86e8aba870b00000000000000000001000000000000000000000000000000003c00000000000000b02b1bb59d01000000de0000000000000000400a8303d7c8b501000000000000000000c16ff2862300000000000000000001000000000000000000000000000000003b00000000000000b02b1bb59d01000000de000000000000000000b03a1856741fae00000000000000000064a7b3b6e00d00000000000000000118337058cb591afe0100000000000000380000000000000050411ab59d010000000a000000de00000000ca9a3b0000000000000000000000000000eecf9a0e7ce036000000000000000100000000000000000000000000000000360000000000000050411ab59d01000000000000000a00000000e00e9a22ebd61e00000000000000005086b4f00100000000000000000000000100000000000000000000000000000000340000000000000050411ab59d01000000de00000000000000000040b2bac9e0191e020000000000000010add73d05242c00000000000000000100000000000000000000000000000000330000000000000050411ab59d01000000000000000a00000000e00e9a22ebd61e0000000000000000208197cf0100000000000000000000000100000000000000000000000000000000320000000000000050411ab59d01000000de000000000000000000a0dec5adc935360000000000000000a0a13840335904000000000000000001000000000000000000000000000000003100000000000000f05619b59d01000000de0000000a0000000000b89d0d6955a0010000000000000053a0c801000000000000000000000000003000000000000000f05619b59d0100000000000000de0000000000f4448291634500000000000000000000fc7e3f4d54046f0300000000000001000000000000000000000000000000002f00000000000000906c18b59d01000000000000000a00000000e00e9a22ebd61e0000000000000000b0d078b901000000000000000000000001000000000000000000000000000000002c00000000000000308217b59d01000000000000000a0000004b68cedb3f9406000000000000000000351d5c0000000000000000000000000000220000000000000070ad15b59d01000000de000000000000000000b49376e2fa18000000000000000063774a73fae50100000000000000000000210000000000000070ad15b59d01000000de00000064000000000050efe2d6e41a1b000000000000000000dbe7baa3aa731a0000000000000001000000000000000000000000000000001e0000000000000070ad15b59d01000000de000000000000004b68479e7219b3190000000000000000c5eca7e866f50100000000000000000000110000000000000090a468b49d010000000a00000030450f0080841e0000000000000000000000000040787d0100000000000000000000000001000000000000000000000000000000001000000000000000602f68b49d010000000a00000030450f0080841e0000000000000000000000000040787d0100000000000000000000000001000000000000000000000000000000000700000000000000000adfb39d01000000de0000003d450f000000986270b34f31010000000000000000c06e31d9100100000000000000000001000000000000000000000000000000000300000000000000c06260b09d010000003d450f000a00000000a031a95fe30000000000000000000040787d0100000000000000000000000000010000000000000000c455b09d0100000030450f00b0440f00d612850e000000000000000000000000000049cbe870610300000000000000000100000000000000000000000000000000 +20220000000000000070ad15b59d01000000de000000000000000000b49376e2fa180000000000000000722757ee4ff801000000000000000000001e0000000000000070ad15b59d01000000de000000000000004b68479e7219b3190000000000000000fc926bf9d60602000000000000000000007600000000000000508d37b59d010000000a000000000000002428a60800000000000000000000000076732ffbf2cf9f000000000000000000017d1f54110000000000000000000000007400000000000000f0a236b59d010000000a00000000000000ab56c009000000000000000000000000bf33f906f22db400000000000000000001537b89130000000000000000000000007300000000000000107436b59d010000000a00000000000000c31ac405000000000000000000000000ce0af302a28a6a00000000000000000001546a8d0b0000000000000000000000007c00000000000000709f41b59d01000000de000000000000000040e60554a802d71100000000000000326f0495a9296801000000000000000001000000000000000000000000000000007500000000000000508d37b59d01000000de0000000000000093f15d6c1a20dae71e0000000000000040a65d1e6bf16f02000000000000000001796acf2b018b131291000000000000007b0000000000000050e03eb59d010000000a000000de000000a40fba000000000000000000000000004048132beff88da9000000000000000001000000000000000000000000000000000c0092992a1800000000000000000000000005b21b058788be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de0000000000000000de993fa457c48af130000000000000007bd1249b3b1adc0300000000000000000403de0000000000000000a40fba000000000000000000000000004048132beff88da9000000000000000008040a000000ea030000026f000000ea030000de000000b46598d7941a0c110000000000000000 +000000003d37000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000030a00000000000000001f3090dfc89cd3575196e833e0a674c1d899820291a9c262d7390d6c0a000000030a0000000000000000e87c2ee98a6671b2f60f73eaa1e861c1d899820291a9c262d7390d6c0a000000030a0000000000000000ac14b163a1f073e5b42c000cfc2d0fdfc7fe758f6afff3b007dc0d040c000000030a0000000000000000bc2dedaac6e0d0e6329dbf41a6d077c1d899820291a9c262d7390d6c0a0000000022000000000000000004100422000000ef0300000268100000ef030000681000000468100000a401000003a40100000000000003220000000000000000e230c2d01f0bdb08bb10f912b8da1f65efe4a911acf0f67cb150178acda5278900640000000000000000100c02640000006400000015000000026900000015000000de00000003de000000000000000c02640000006400000017000000026900000017000000de00000003de00000000000000100264000000640000000a000000040a000000ea030000026f000000ea030000de00000003de00000000000000140264000000640000000a00000002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000003640000000000000000a2b180423dfd2000c43ed78777e90a00dadcf451e8f32b2064ad87151a37778403640000000000000000a8142aab3ff095c3ffbd8b3972e80a00dadcf451e8f32b2064ad87151a377784036400000000000000000cf449430279d36f861f246532e90a00dadcf451e8f32b2064ad87151a377784036400000000000000000185637b67ad943de2ce21134ced1200b20ea335c01ab7539b120a22c894cae5006e00000000000000001008026e0000006e000000de00000003de0000000000000018026e0000006e000000eb03000004eb030000160000000266000000160000000a00000002640000000a00000015000000026900000015000000de00000003de0000000000000018026e0000006e000000eb03000004eb030000160000000266000000160000000a00000002640000000a00000017000000026900000017000000de00000003de0000000000000018026e0000006e000000eb03000004eb030000160000000266000000160000000a000000040a000000ea030000026f000000ea030000de00000003de00000000000000036e0000000000000000722d6c45a3ff0dff9e98d97dbfa11300bafce49d5689dcd717c48befb3ffeef3036e00000000000000004d8b5f2e93b1aae5db463d3320cd0c008e379b255d9ecf8e7034d9fb6b912c9f036e000000000000000067d232f7917014b39ecd53b1e2cb0c008e379b255d9ecf8e7034d9fb6b912c9f036e0000000000000000731d27062f6b883a24079ef09bc20e00c1a8daa8178e0dd4b32bf00b80e683b700de0000000000000000040403de0000000000000003de0000000000000000c12daf47e9feab443d9e57eed3a21100cf043701905b7ee28ede9dbcf62ea7dd00b0440f000000000000040403b0440f000000000003b0440f000000000000482faf00eef803fa7fce88c4f1c72204ed9cdb598bc2a2ca2bf0aa233e43ca8a0030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000330450f00000000000096fac88eb3925b9452fd8ae81adb2205ee855bee59242ff17bd4df2a030000000330450f0000000000008048dbd3cf405dee3d80a45b01539bcb37bcd7704423d7d4b4dbb9587d000000003d450f0000000000000404033d450f0000000000033d450f000000000000d650a05bd74d68fd3447712623209a3e28a00f9aa8c7854d69c13252276f980200de0000000000000000040403de0000000000000001de00000000000000000040b2bac9e0191e020000000000000013f9e5235156692900000000000000000403de00000000000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a40fba0000000000000000000000000000ab9b217fb05658a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a40fba0000000000000000000000000000cc6e96c6d84a3ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a40fba0000000000000000000000000000b170c009577592a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a40fba00000000000000000000000000006dbedea98e4f91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de0000000000000000de0000000004040300000000de0000000100000000de00000000414839cf54000000000000000000000061e7c2a8ae8925040000000000000000040300000000de00000000de0000000000000000040403de0000000000000001de0000000000000000c018fda10407ef38040000000000000075770ac306a2a04f00000000000000000403de000000000000000000000000de0000000004040300000000de0000000100000000de00000000407a10f35a00000000000000000000007255804af96372040000000000000000040300000000de000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000833e7ca1000000000000000000000000000a04efa433633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000833e7ca100000000000000000000000000786557bcd0133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000833e7ca1000000000000000000000000003d4b1e8d65f18d0b00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000833e7ca10000000000000000000000000024588fa5cbc78d0b00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de000000000000008795b0387408ae21ab0300000000000000a3991d92d6a9d64500000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000f5d210b6000000000000000000000000004d274f3840633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000f5d210b600000000000000000000000000bfdbbb06dd133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000f5d210b6000000000000000000000000003880d46d3088020d00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000f5d210b60000000000000000000000000090c861244356020d00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000ac29a86b0000000000000000000000000082293d24ea623e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000ac29a86b0000000000000000000000000020c8b0cc88133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000ac29a86b000000000000000000000000009f8f3009b8c5ba0700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000ac29a86b000000000000000000000000000adef8d7b4aeba0700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de00000000407a10f35a00000000000000000000007255804af96372040000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000407a10f35a00000000000000000000007255804af96372040000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000407a10f35a00000000000000000000007255804af96372040000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000407a10f35a00000000000000000000007255804af96372040000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000407a10f35a00000000000000000000007255804af96372040000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000407a10f35a00000000000000000000007255804af96372040000000000000000040300000000de00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000000000e8890423c78a00000000000000000009335f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000000000e8890423c78a00000000000000000009335f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000000000e8890423c78a0000000000000000000bc3059b0600000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000000000e8890423c78a0000000000000000000822df99060000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a0000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000049e71dcb00000000000000000000000000fe12cc235b2e47010000000000000000040330450f00000000000130450f000000000049e71dcb00000000000000000000000000e08b3ca5625f48010000000000000000100430450f00f103000002915f0100f1030000915f010004915f010029230000032923000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000585350d700000000000000000000000000233934724d633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000585350d700000000000000000000000000432231e5e9133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000585350d7000000000000000000000000000d597b83e2635a0f00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000585350d700000000000000000000000000eea1a59f1a235a0f00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000004c0870245e67d78d00000000000000000016f80c874ca0fbe01106000000000000080300000000de000000026e000000de0000006e00000001000000006e0000004c0870245e67d78d000000000000000000f94f0eea0d69be090300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000004c0870245e67d78d000000000000000000f94f0eea0d69be090300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000004c0870245e67d78d0000000000000000007f7a7efc84dc16ba1006000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000003029881a5643100000000000000000e089feca0000000000000000000000000403de00000030450f0001de00000030450f0000003029881a5643100000000000000000c638e9ca0000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000346e67c4ddce70090000000000000000000b2e5f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000346e67c4ddce7009000000000000000000252e5f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000346e67c4ddce700900000000000000000053ec85800000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000346e67c4ddce7009000000000000000000d9db7880000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000de0000000000000000040403de0000000000000001de00000000000000000040bd8b5b936b6c0000000000000000c37898567303890800000000000000000403de000000000000000000000000220000000004100300000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000001000000002200000000008a5d78456301000000000000000000f7d8db4d0682df010000000000000000100300000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000073c37eae00000000000000000000000000c6a5af193c633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000073c37eae000000000000000000000000009c7d46fad8133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000073c37eae0000000000000000000000000061541f619496790c00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000073c37eae00000000000000000000000000540aef15cb67790c00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a000000000000000000b6ef099630be38f60506000000000000040300000000de000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000065cd1d00000000000000000000000000a5488286d084540000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000065cd1d00000000000000000000000000b507b52b70be520000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000065cd1d00000000000000000000000000f79bdcd907a4ea02000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000065cd1d00000000000000000000000000ba582679fe9dea0200000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000000de0000000000000000040403de0000000000000001de00000000000000000020c65abc8ed70a0000000000000000795890555002dc0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000e8890423c78a000000000000000000043ab0ce2a020b0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000400a8303d7c8b5010000000000000000a4c869165fb9220000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e8cc3fe24cfc5921ac00000000000000006329e814a3827d0d00000000000000000403de00000000000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de00000000ca9a3b0000000000000000000000000010c83bc32c178e1203000000000000000802640000000a00000015000000026900000015000000de000000010a000000de00000000ca9a3b0000000000000000000000000005ab898abdcd080203000000000000000802640000000a00000017000000026900000017000000de000000010a000000de00000000ca9a3b000000000000000000000000009354ada8a5367a52360000000000000008040a000000ea030000026f000000ea030000de000000010a000000de00000000ca9a3b00000000000000000000000000b0664df0b467f65136000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000000e00e9a22ebd61e00000000000000000093325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000000e00e9a22ebd61e00000000000000000096325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000000e00e9a22ebd61e000000000000000000937e019c0100000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000000e00e9a22ebd61e0000000000000000002231d29b010000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000de0000000000000000040403de0000000000000001de00000000000000000040b2bac9e0191e020000000000000013f9e5235156692900000000000000000403de0000000000000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000000e00e9a22ebd61e00000000000000000093325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000000e00e9a22ebd61e00000000000000000096325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000000e00e9a22ebd61e000000000000000000937e019c0100000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000000e00e9a22ebd61e0000000000000000002231d29b010000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000de0000000000000000040403de0000000000000001de000000000000000000a0dec5adc935360000000000000000a218486acfae480400000000000000000403de0000000000000000de0000000a0000000010080269000000de000000150000000264000000150000000a000000080269000000de000000170000000264000000170000000a00000008026f000000de000000ea03000004ea0300000a0000000c026e000000de000000eb03000004eb030000160000000266000000160000000a00000001de0000000a0000000000b89d0d6955a0010000000000000000e563be01000000000000000000000000080269000000de000000150000000264000000150000000a00000001de0000000a0000000000b89d0d6955a00100000000000000009b84be01000000000000000000000000080269000000de000000170000000264000000170000000a00000001de0000000a0000000000b89d0d6955a001000000000000000077a2c80100000000000000000000000008026f000000de000000ea03000004ea0300000a00000001de0000000a0000000000b89d0d6955a0010000000000000000bb76c8010000000000000000000000000c026e000000de000000eb03000004eb030000160000000266000000160000000a0000000000000000de0000000004040300000000de0000000100000000de0000000000f44482916345000000000000000000707f6dab40f90a773103000000000000040300000000de00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000000e00e9a22ebd61e00000000000000000093325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000000e00e9a22ebd61e00000000000000000096325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000000e00e9a22ebd61e000000000000000000937e019c0100000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000000e00e9a22ebd61e0000000000000000002231d29b010000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000004b68cedb3f94060000000000000000000082fc59000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000004b68cedb3f940600000000000000000000d9035a000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000004b68cedb3f94060000000000000000000078515a000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000004b68cedb3f940600000000000000000000d3485a00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000de0000000000000000040403de0000000000000001de000000000000000000b49376e2fa18000000000000000000d3dc653a49fb010000000000000000000403de0000000000000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de00000064000000000050efe2d6e41a1b0000000000000000077f9fbdb59f64991900000000000000080269000000de000000150000000264000000150000006400000001de00000064000000000050efe2d6e41a1b00000000000000009adeaafd5b8ba7981900000000000000080269000000de000000170000000264000000170000006400000001de00000064000000000050efe2d6e41a1b0000000000000000b3674c5c62c8359a19000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de00000064000000000050efe2d6e41a1b0000000000000000a045c4b14493bf97190000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de0000000000000000040403de0000000000000001de000000000000004b68479e7219b319000000000000000000de3fda2ce609020000000000000000000403de00000000000000000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000fee45b010000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000f8c05b010000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000c7515c010000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000005b4f5c010000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000001c185b010000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000002cf45a010000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000a6845b0100000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000003c825b010000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000fee45b010000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000f8c05b010000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000c7515c010000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000005b4f5c010000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000001c185b010000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000002cf45a010000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000a6845b0100000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000003c825b010000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000000986270b34f310100000000000000006f7b29620b01010000000000000000000403de0000003d450f00003d450f000a00000000100c033d450f00de0000000269000000de000000150000000264000000150000000a0000000c033d450f00de0000000269000000de000000170000000264000000170000000a0000000c033d450f00de000000026f000000de000000ea03000004ea0300000a00000010033d450f00de000000026e000000de000000eb03000004eb030000160000000266000000160000000a000000013d450f000a00000000a031a95fe30000000000000000000000ff7422010000000000000000000000000c033d450f00de0000000269000000de000000150000000264000000150000000a000000013d450f000a00000000a031a95fe300000000000000000000007e8822010000000000000000000000000c033d450f00de0000000269000000de000000170000000264000000170000000a000000013d450f000a00000000a031a95fe300000000000000000000007b1626010000000000000000000000000c033d450f00de000000026f000000de000000ea03000004ea0300000a000000013d450f000a00000000a031a95fe3000000000000000000000052fa250100000000000000000000000010033d450f00de000000026e000000de000000eb03000004eb030000160000000266000000160000000a0000000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00d612850e000000000000000000000000002bac5c74137513030000000000000000040330450f00b0440f000130450f00b0440f00d612850e0000000000000000000000000044af86c74aef14030000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000000000000220000000004100300000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000001000000002200000000008a5d78456301000000000000000000f7d8db4d0682df010000000000000000100300000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000a0000000010080269000000de000000150000000264000000150000000a000000080269000000de000000170000000264000000170000000a00000008026f000000de000000ea03000004ea0300000a0000000c026e000000de000000eb03000004eb030000160000000266000000160000000a00000001de0000000a0000000000b89d0d6955a0010000000000000000e563be01000000000000000000000000080269000000de000000150000000264000000150000000a00000001de0000000a0000000000b89d0d6955a00100000000000000009b84be01000000000000000000000000080269000000de000000170000000264000000170000000a00000001de0000000a0000000000b89d0d6955a001000000000000000077a2c80100000000000000000000000008026f000000de000000ea03000004ea0300000a00000001de0000000a0000000000b89d0d6955a0010000000000000000bb76c8010000000000000000000000000c026e000000de000000eb03000004eb030000160000000266000000160000000a00000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e18fa75b3320e05900000000000000000000335f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000e18fa75b3320e05900000000000000000001335f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000e18fa75b3320e0590000000000000000006e3516750400000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e18fa75b3320e05900000000000000000022916c74040000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000bc430bb11610f02c000000000000000000d4325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000bc430bb11610f02c000000000000000000d5325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000bc430bb11610f02c0000000000000000003b9d03510200000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000bc430bb11610f02c000000000000000000f28bba50020000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000ab74c5de0908781600000000000000000028325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000ab74c5de090878160000000000000000002d325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000ab74c5de0908781600000000000000000012cb632e0100000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000ab74c5de09087816000000000000000000559d422e010000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000238da27503043c0b000000000000000000822f5f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000238da27503043c0b000000000000000000942f5f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000238da27503043c0b0000000000000000006c5db3980000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000238da27503043c0b000000000000000000e9aba398000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000090efc83700029e05000000000000000000f8245f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000090efc83700029e0500000000000000000040255f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000090efc83700029e050000000000000000000b34bb4c0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000090efc83700029e050000000000000000008b96b34c000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000954a24a2fe00cf02000000000000000000bbfa5e030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000954a24a2fe00cf02000000000000000000cdfb5e030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000954a24a2fe00cf02000000000000000000c22076260000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000954a24a2fe00cf0200000000000000000011617226000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000017f851d77d806701000000000000000000e04d5e030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000017f851d77d806701000000000000000000e6515e030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000017f851d77d806701000000000000000000b83641130000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000017f851d77d806701000000000000000000ac5a3f13000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000d9cee8713dc0b300000000000000000000664c5b030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000d9cee8713dc0b300000000000000000000835b5b030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000d9cee8713dc0b3000000000000000000007a25a2090000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000d9cee8713dc0b3000000000000000000006638a109000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000003a3a343f1de059000000000000000000009b3947030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000003a3a343f1de05900000000000000000000b67c47030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000003a3a343f1de059000000000000000000004875d1040000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000003a3a343f1de05900000000000000000000fbfed004000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000001a6f89990bf02c000000000000000000002a3150020000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000001a6f89990bf02c00000000000000000000cd6b50020000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000001a6f89990bf02c0000000000000000000024d368020000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000001a6f89990bf02c000000000000000000000c986802000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000005a8a045304781600000000000000000000486730010000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000005a8a045304781600000000000000000000af7b30010000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000005a8a045304781600000000000000000000a86f34010000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000005a8a0453047816000000000000000000001f523401000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000002a977123ff3b0b000000000000000000002a4699000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000002a977123ff3b0b00000000000000000000925199000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000002a977123ff3b0b0000000000000000000035399a000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000002a977123ff3b0b00000000000000000000712a9a00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000009374b00efe9d050000000000000000000034de4c000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000009374b00efe9d05000000000000000000009ee44c000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000009374b00efe9d0500000000000000000000e81c4d000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000009374b00efe9d050000000000000000000086154d00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e636e096fdce0200000000000000000000537d26000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000e636e096fdce0200000000000000000000c58026000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000e636e096fdce0200000000000000000000788e26000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e636e096fdce0200000000000000000000c68a2600000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000006f6d5fc57b6701000000000000000000000b4213000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000006f6d5fc57b670100000000000000000000d64313000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000006f6d5fc57b670100000000000000000000194713000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000006f6d5fc57b6701000000000000000000003f451300000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000b4089fdcbab30000000000000000000000bda109000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000b4089fdcbab30000000000000000000000a8a209000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000b4089fdcbab3000000000000000000000064a309000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000b4089fdcbab3000000000000000000000076a20900000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000027578ff4db59000000000000000000000001d104000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000027578ff4db59000000000000000000000078d104000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000027578ff4db5900000000000000000000009dd104000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000027578ff4db59000000000000000000000026d10400000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000090fd36f4ea2c0000000000000000000000656802000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000090fd36f4ea2c0000000000000000000000a06802000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000090fd36f4ea2c0000000000000000000000a56802000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000090fd36f4ea2c000000000000000000000069680200000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000095515b0074160000000000000000000000213401000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000095515b00741600000000000000000000003f3401000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000095515b00741600000000000000000000003e3401000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000095515b00741600000000000000000000001f340100000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000096246503370b0000000000000000000000e89900000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000096246503370b0000000000000000000000f79900000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000096246503370b0000000000000000000000f69900000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000096246503370b0000000000000000000000e6990000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000048e4a17b98050000000000000000000000ca4c00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000048e4a17b98050000000000000000000000d24c00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000048e4a17b98050000000000000000000000d14c00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000048e4a17b98050000000000000000000000c94c0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000229bc8baca020000000000000000000000502600000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000229bc8baca020000000000000000000000532600000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000229bc8baca020000000000000000000000542600000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000229bc8baca0200000000000000000000004f260000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000002d4aecec63010000000000000000000000141300000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000002d4aecec63010000000000000000000000161300000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000002d4aecec63010000000000000000000000161300000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000002d4aecec6301000000000000000000000014130000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000137765f0ae0000000000000000000000005f0900000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000137765f0ae000000000000000000000000600900000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000137765f0ae000000000000000000000000610900000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000137765f0ae0000000000000000000000005f090000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000870d227254000000000000000000000000850400000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000870d227254000000000000000000000000850400000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000870d227254000000000000000000000000860400000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000870d22725400000000000000000000000084040000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000008e82483c2700000000000000000000000101000000000a0000008e82483c2700000000000000000000000101000000000a0000008e82483c27000000000000000000000000190200000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000008e82483c2700000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000141464241200000000000000000000000101000000000a000000141464241200000000000000000000000101000000000a0000001414642412000000000000000000000000f80000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000141464241200000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000075c210c0600000000000000000000000101000000000a000000075c210c0600000000000000000000000101000000000a000000075c210c06000000000000000000000000520000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000075c210c0600000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000003ae10060300000000000000000000000101000000000a00000003ae10060300000000000000000000000101000000000a00000003ae100603000000000000000000000000280000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000003ae10060300000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000003ae10060300000000000000000000000101000000000a00000003ae10060300000000000000000000000101000000000a00000003ae100603000000000000000000000000280000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000003ae10060300000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e11fa00ea2aa744a000000000000000000f9325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000e11fa00ea2aa744a000000000000000000fa325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000e11fa00ea2aa744a000000000000000000356bd5bd0300000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e11fa00ea2aa744a000000000000000000932c4fbd030000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000006cf6fb8e4e553a25000000000000000000ba325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000006cf6fb8e4e553a25000000000000000000bb325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000006cf6fb8e4e553a25000000000000000000b80595ee0100000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000006cf6fb8e4e553a25000000000000000000db575aee010000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000005b03f80fa62a9d12000000000000000000c0315f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000005b03f80fa62a9d12000000000000000000c7315f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000005b03f80fa62a9d120000000000000000003d6a5cfb0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000005b03f80fa62a9d12000000000000000000476e41fb000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000d30976d051954e09000000000000000000e62d5f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000d30976d051954e09000000000000000000002e5f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000d30976d051954e0900000000000000000099d5b77e0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000d30976d051954e09000000000000000000d0f6aa7e000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000040e36ca7a74aa7040000000000000000008c1e5f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000040e36ca7a74aa704000000000000000000f41e5f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000040e36ca7a74aa704000000000000000000a9fb9e3f0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000040e36ca7a74aa70400000000000000000029b5983f000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000c579309c52a55302000000000000000000d2e05e030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000c579309c52a553020000000000000000005be25e030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000c579309c52a553020000000000000000005957e01f0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000c579309c52a55302000000000000000000753edd1f000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000007459216a8d229010000000000000000000ae15d030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000007459216a8d22901000000000000000000cae65d030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000007459216a8d22901000000000000000000b064f40f0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000007459216a8d22901000000000000000000dbdaf20f000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000a92ac3d352e99400000000000000000000b22e59030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000a92ac3d352e99400000000000000000000aa4459030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000a92ac3d352e99400000000000000000000e240fb070000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000a92ac3d352e994000000000000000000009e7cfa07000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000007a9d5b32a8744a000000000000000000009d3d2f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000007a9d5b32a8744a0000000000000000000002a52f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000007a9d5b32a8744a0000000000000000000012e4fd030000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000007a9d5b32a8744a000000000000000000001982fd03000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000006a8b9197513a25000000000000000000006123f1010000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000006a8b9197513a2500000000000000000000e04af1010000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000006a8b9197513a2500000000000000000000d302ff010000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000006a8b9197513a2500000000000000000000e1d1fe01000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000dacd4294279d12000000000000000000004fd7fc000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000dacd4294279d120000000000000000000082e8fc000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000dacd4294279d12000000000000000000009585ff000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000dacd4294279d12000000000000000000001d6dff00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000009a238548914e0900000000000000000000821c7f000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000009a238548914e09000000000000000000004c267f000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000009a238548914e0900000000000000000000b7c37f000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000009a238548914e09000000000000000000007bb77f00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000002370746347a7040000000000000000000038b63f000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000002370746347a70400000000000000000000a9bb3f000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000002370746347a704000000000000000000000de23f000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000002370746347a70400000000000000000000efdb3f00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000066a7c83a2530200000000000000000000d3e41f000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000066a7c83a2530200000000000000000000b8e71f000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000066a7c83a253020000000000000000000007f11f000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000066a7c83a2530200000000000000000000f7ed1f00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000aff121c0ce290100000000000000000000b5f40f000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000aff121c0ce29010000000000000000000033f60f000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000aff121c0ce29010000000000000000000065f80f000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000aff121c0ce290100000000000000000000dcf60f00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000084b574dee4940000000000000000000000d3fa07000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000084b574dee494000000000000000000000096fb07000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000084b574dee494000000000000000000000011fc07000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000084b574dee49400000000000000000000004cfb0700000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e762b437714a00000000000000000000007efd03000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000e762b437714a0000000000000000000000e1fd03000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000e762b437714a0000000000000000000000f7fd03000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e762b437714a000000000000000000000095fd0300000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000020ee3d1a36250000000000000000000000a5fe01000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000020ee3d1a36250000000000000000000000d6fe01000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000020ee3d1a36250000000000000000000000d8fe01000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000020ee3d1a36250000000000000000000000a6fe0100000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000035ff98d59912000000000000000000000042ff00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000035ff98d5991200000000000000000000005bff00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000035ff98d5991200000000000000000000005bff00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000035ff98d59912000000000000000000000041ff0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000166678724a0900000000000000000000007f7f00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000166678724a0900000000000000000000008c7f00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000166678724a0900000000000000000000008b7f00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000166678724a0900000000000000000000007e7f0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000b8ef9fb7a20400000000000000000000009c3f00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000b8ef9fb7a2040000000000000000000000a23f00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000b8ef9fb7a2040000000000000000000000a23f00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000b8ef9fb7a20400000000000000000000009b3f0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000032d6011b50020000000000000000000000bd1f00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000032d6011b50020000000000000000000000bf1f00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000032d6011b50020000000000000000000000c01f00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000032d6011b50020000000000000000000000bc1f0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000000d1d43df26010000000000000000000000ce0f00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000000d1d43df26010000000000000000000000cf0f00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000000d1d43df26010000000000000000000000cf0f00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000000d1d43df26010000000000000000000000cd0f0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000334b05ee90000000000000000000000000c30700000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000334b05ee90000000000000000000000000c40700000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000334b05ee90000000000000000000000000c50700000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000334b05ee90000000000000000000000000c2070000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000476266f54500000000000000000000000101000000000a000000476266f54500000000000000000000000101000000000a000000476266f545000000000000000000000000bf0300000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000476266f54500000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000009e175f822000000000000000000000000101000000000a0000009e175f822000000000000000000000000101000000000a0000009e175f8220000000000000000000000000bd0100000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000009e175f822000000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000f493a9090f00000000000000000000000101000000000a000000f493a9090f00000000000000000000000101000000000a000000f493a9090f000000000000000000000000cd0000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000f493a9090f00000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000a78638030500000000000000000000000101000000000a000000a78638030500000000000000000000000101000000000a000000a786380305000000000000000000000000430000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000a78638030500000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000053439c810200000000000000000000000101000000000a00000053439c810200000000000000000000000101000000000a00000053439c8102000000000000000000000000210000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000053439c810200000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000053439c810200000000000000000000000101000000000a00000053439c810200000000000000000000000101000000000a00000053439c8102000000000000000000000000210000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000053439c810200000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e1af98c11035093b000000000000000000ed325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000e1af98c11035093b000000000000000000ed325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000e1af98c11035093b000000000000000000935e9a010300000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e1af98c11035093b000000000000000000ca3b3501030000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000001ca9ec6c869a841d00000000000000000088325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000001ca9ec6c869a841d0000000000000000008b325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000001ca9ec6c869a841d000000000000000000b2a6ce8a0100000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000001ca9ec6c869a841d00000000000000000071a3a18a010000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000000b922a41424dc20e000000000000000000fd305f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000000b922a41424dc20e00000000000000000008315f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000000b922a41424dc20e00000000000000000043adfbc70000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000000b922a41424dc20e00000000000000000063afe6c7000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000008386492ba0266107000000000000000000e02a5f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000008386492ba02661070000000000000000000a2b5f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000008386492ba0266107000000000000000000e584a5640000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000008386492ba02661070000000000000000002e6b9b64000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000f0d610174f93b00300000000000000000074125f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000f0d610174f93b00300000000000000000016135f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000f0d610174f93b00300000000000000000088027d320000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000f0d610174f93b003000000000000000000870f7832000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000f5a83c96a649d801000000000000000000a4af5e030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000f5a83c96a649d80100000000000000000009b25e030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000f5a83c96a649d801000000000000000000ea1b49190000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000f5a83c96a649d801000000000000000000f3a84619000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000f791d255d224ec000000000000000000005d0c5d030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000f791d255d224ec000000000000000000004d155d030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000f791d255d224ec00000000000000000000ec35a70c0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000f791d255d224ec0000000000000000000013fea50c000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000079869d35681276000000000000000000002a7554030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000079869d3568127600000000000000000000e59854030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000079869d3568127600000000000000000000144554060000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000079869d356812760000000000000000000091a95306000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000ba00832533093b000000000000000000007790e1020000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000ba00832533093b00000000000000000000a8ffe1020000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000ba00832533093b000000000000000000000d4d2a030000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000ba00832533093b0000000000000000000065ff2903000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000baa7999597841d000000000000000000001a9b8d010000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000baa7999597841d00000000000000000000bbb68d010000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000baa7999597841d00000000000000000000103195010000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000baa7999597841d00000000000000000000420a9501000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000005a1181d54ac20e0000000000000000000092f4c8000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000005a1181d54ac20e00000000000000000000c102c9000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000005a1181d54ac20e00000000000000000000239bca000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000005a1181d54ac20e00000000000000000000bd87ca00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000000ab0986d23610700000000000000000000fee364000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000000ab0986d2361070000000000000000000015ec64000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000000ab0986d23610700000000000000000000204e65000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000000ab0986d236107000000000000000000006d446500000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000b36b38b890b00300000000000000000000d28a32000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000b36b38b890b003000000000000000000003e8f32000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000b36b38b890b003000000000000000000002ca732000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000b36b38b890b0030000000000000000000053a23200000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000269d187047d801000000000000000000007e4b19000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000269d187047d80100000000000000000000d14d19000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000269d187047d80100000000000000000000935319000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000269d187047d8010000000000000000000027511900000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000ef75e4ba21ec00000000000000000000002ba70c000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000ef75e4ba21ec00000000000000000000005da80c000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000ef75e4ba21ec0000000000000000000000b0a90c000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000ef75e4ba21ec00000000000000000000007aa80c00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000054624ae00e760000000000000000000000dc5306000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000054624ae00e760000000000000000000000765406000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000054624ae00e760000000000000000000000be5406000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000054624ae00e76000000000000000000000022540600000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000a76ed97a063b0000000000000000000000f72903000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000a76ed97a063b0000000000000000000000452a03000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000a76ed97a063b0000000000000000000000512a03000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000a76ed97a063b0000000000000000000000022a0300000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000b0de4440811d0000000000000000000000e69401000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000b0de4440811d00000000000000000000000d9501000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000b0de4440811d00000000000000000000000d9501000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000b0de4440811d0000000000000000000000e6940100000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000d5acd6aabf0e000000000000000000000066ca00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000d5acd6aabf0e000000000000000000000079ca00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000d5acd6aabf0e000000000000000000000079ca00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000d5acd6aabf0e000000000000000000000064ca0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000096a78be15d070000000000000000000000176500000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000096a78be15d070000000000000000000000216500000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000096a78be15d070000000000000000000000216500000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000096a78be15d07000000000000000000000016650000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000028fb9df3ac0300000000000000000000006f3200000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000028fb9df3ac030000000000000000000000743200000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000028fb9df3ac030000000000000000000000753200000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000028fb9df3ac0300000000000000000000006f320000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000042113b7bd5010000000000000000000000291900000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000042113b7bd50100000000000000000000002b1900000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000042113b7bd50100000000000000000000002c1900000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000042113b7bd501000000000000000000000029190000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000edef99d1e9000000000000000000000000880c00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000edef99d1e9000000000000000000000000890c00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000edef99d1e9000000000000000000000000890c00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000edef99d1e9000000000000000000000000870c0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000531fa5eb72000000000000000000000000270600000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000531fa5eb72000000000000000000000000280600000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000531fa5eb72000000000000000000000000280600000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000531fa5eb7200000000000000000000000026060000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000007b7aa783700000000000000000000000101000000000a00000007b7aa783700000000000000000000000101000000000a00000007b7aa7837000000000000000000000000f80200000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000007b7aa783700000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000aeac75c81900000000000000000000000101000000000a000000aeac75c81900000000000000000000000101000000000a000000aeac75c819000000000000000000000000610100000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000aeac75c81900000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000d413efee0b00000000000000000000000101000000000a000000d413efee0b00000000000000000000000101000000000a000000d413efee0b000000000000000000000000a20000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000d413efee0b00000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000047b14ffa0300000000000000000000000101000000000a00000047b14ffa0300000000000000000000000101000000000a00000047b14ffa03000000000000000000000000350000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000047b14ffa0300000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000a3d827fd0100000000000000000000000101000000000a000000a3d827fd0100000000000000000000000101000000000a000000a3d827fd010000000000000000000000001a0000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000a3d827fd0100000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000a3d827fd0100000000000000000000000101000000000a000000a3d827fd0100000000000000000000000101000000000a000000a3d827fd010000000000000000000000001a0000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000a3d827fd0100000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e13f91747fbf9d2b000000000000000000d1325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000e13f91747fbf9d2b000000000000000000d2325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000e13f91747fbf9d2b000000000000000000f5863c400200000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e13f91747fbf9d2b000000000000000000caf9f53f020000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000cc5bdd4abedfce150000000000000000001a325f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000cc5bdd4abedfce150000000000000000001f325f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000cc5bdd4abedfce15000000000000000000aeedaa250100000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000cc5bdd4abedfce1500000000000000000062d38a25010000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000bb205d72de6fe70a0000000000000000004a2f5f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000bb205d72de6fe70a0000000000000000005e2f5f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000bb205d72de6fe70a000000000000000000fbd840940000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000bb205d72de6fe70a00000000000000000050a43194000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000033031d86eeb773050000000000000000001b245f030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000033031d86eeb7730500000000000000000067245f030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000033031d86eeb7730500000000000000000057537c4a0000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000033031d86eeb77305000000000000000000e2f0744a000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000a0cab486f6dbb90200000000000000000040f75e030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000a0cab486f6dbb90200000000000000000062f85e030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000a0cab486f6dbb9020000000000000000009f4555250000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000a0cab486f6dbb90200000000000000000095a25125000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000025d84890faed5c01000000000000000000633f5e030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000025d84890faed5c01000000000000000000a4435e030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000025d84890faed5c01000000000000000000156eb0120000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000025d84890faed5c0100000000000000000024a0ae12000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e7de1295fc76ae0000000000000000000002075b030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000e7de1295fc76ae0000000000000000000008175b030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000e7de1295fc76ae000000000000000000005faa59090000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e7de1295fc76ae000000000000000000004cc45809000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000049e277977d3b5700000000000000000000ccaf44030000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000049e277977d3b5700000000000000000000eef744030000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000049e277977d3b57000000000000000000000e32ad040000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000049e277977d3b57000000000000000000003dbfac04000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000fa63aa18be9d2b00000000000000000000ab5c40020000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000fa63aa18be9d2b000000000000000000005d9340020000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000fa63aa18be9d2b000000000000000000003ab056020000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000fa63aa18be9d2b00000000000000000000df765602000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000000ac4a193ddce1500000000000000000000539727010000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000000ac4a193ddce150000000000000000000028ab27010000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000000ac4a193ddce1500000000000000000000d85d2b010000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000000ac4a193ddce15000000000000000000002d412b01000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000da54bf166ee70a000000000000000000005ecb94000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000da54bf166ee70a0000000000000000000080d694000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000da54bf166ee70a0000000000000000000055b095000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000da54bf166ee70a0000000000000000000001a29500000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000007a3cac92b5730500000000000000000000389d4a000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000007a3cac92b57305000000000000000000007aa34a000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000007a3cac92b573050000000000000000000073d84a000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000007a3cac92b573050000000000000000000049d14a00000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000004367fc0cdab90200000000000000000000085c25000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000004367fc0cdab90200000000000000000000635f25000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000004367fc0cdab90200000000000000000000466c25000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000004367fc0cdab90200000000000000000000b1682500000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000046d0b45cec5c010000000000000000000051b112000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000046d0b45cec5c01000000000000000000000fb312000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000046d0b45cec5c01000000000000000000001fb612000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000046d0b45cec5c010000000000000000000054b41200000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a0000002ffaa6b574ae0000000000000000000000695909000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a0000002ffaa6b574ae00000000000000000000004d5a09000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a0000002ffaa6b574ae0000000000000000000000fd5a09000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a0000002ffaa6b574ae0000000000000000000000165a0900000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000240f20e238570000000000000000000000d4ac04000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000240f20e23857000000000000000000000047ad04000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000240f20e2385700000000000000000000006aad04000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000240f20e238570000000000000000000000f6ac0400000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000677afebd9b2b00000000000000000000006d5602000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000677afebd9b2b0000000000000000000000a85602000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000677afebd9b2b0000000000000000000000ab5602000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000677afebd9b2b000000000000000000000071560200000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000040cf4b66cc150000000000000000000000252b01000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000040cf4b66cc150000000000000000000000412b01000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000040cf4b66cc150000000000000000000000412b01000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000040cf4b66cc150000000000000000000000232b0100000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000755a1480e50a0000000000000000000000899500000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000755a1480e50a0000000000000000000000989500000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000755a1480e50a0000000000000000000000969500000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000755a1480e50a000000000000000000000087950000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000016e99e5071050000000000000000000000b04a00000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000016e99e5071050000000000000000000000b84a00000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000016e99e5071050000000000000000000000b74a00000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000016e99e5071050000000000000000000000b04a0000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000098069c2fb7020000000000000000000000442500000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000098069c2fb7020000000000000000000000472500000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000098069c2fb7020000000000000000000000472500000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000098069c2fb702000000000000000000000043250000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000524c74db5a010000000000000000000000981200000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000524c74db5a010000000000000000000000991200000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000524c74db5a010000000000000000000000991200000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000524c74db5a01000000000000000000000097120000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000cdc2f0c3ac000000000000000000000000410900000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a000000cdc2f0c3ac000000000000000000000000420900000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a000000cdc2f0c3ac000000000000000000000000430900000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000cdc2f0c3ac00000000000000000000000041090000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a00000073f344e9540000000000000000000000008b0400000000000000000000000000000c0300000000de0000000269000000de000000150000000264000000150000000a00000001000000000a00000073f344e9540000000000000000000000008b0400000000000000000000000000000c0300000000de0000000269000000de000000170000000264000000170000000a00000001000000000a00000073f344e9540000000000000000000000008c0400000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a00000073f344e9540000000000000000000000008a040000000000000000000000000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000c70beffb2800000000000000000000000101000000000a000000c70beffb2800000000000000000000000101000000000a000000c70beffb28000000000000000000000000310200000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000c70beffb2800000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000be418c0e1300000000000000000000000101000000000a000000be418c0e1300000000000000000000000101000000000a000000be418c0e13000000000000000000000000040100000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000be418c0e1300000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000b49334d40800000000000000000000000101000000000a000000b49334d40800000000000000000000000101000000000a000000b49334d408000000000000000000000000780000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000b49334d40800000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000e7db66f10200000000000000000000000101000000000a000000e7db66f10200000000000000000000000101000000000a000000e7db66f102000000000000000000000000270000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000e7db66f10200000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000f36db3780100000000000000000000000101000000000a000000f36db3780100000000000000000000000101000000000a000000f36db37801000000000000000000000000130000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000f36db3780100000000000000000000000100000000000a00000000100c0300000000de0000000269000000de000000150000000264000000150000000a0000000c0300000000de0000000269000000de000000170000000264000000170000000a0000000c0300000000de000000026f000000de000000ea03000004ea0300000a000000100300000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001000000000a000000f36db3780100000000000000000000000101000000000a000000f36db3780100000000000000000000000101000000000a000000f36db37801000000000000000000000000130000000000000000000000000000000c0300000000de000000026f000000de000000ea03000004ea0300000a00000001000000000a000000f36db37801000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008ae8696301000000000000000000000000b0f2535562633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000008ae869630100000000000000000000000008c3846dfe133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000008ae8696301000000000000000000000000ebae373bd5531e1900000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000008ae86963010000000000000000000000008dc33e1de1c11d1900000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000039f4b4b100000000000000000000000000c6823de83d633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000039f4b4b10000000000000000000000000014e9fac8da133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000039f4b4b10000000000000000000000000053b58bd257b3b30c00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000039f4b4b1000000000000000000000000003424363e3c83b30c00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000167ada580000000000000000000000000065156321ac623e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000167ada5800000000000000000000000000874751244c133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000167ada5800000000000000000000000000bc7a2e374621630600000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000167ada58000000000000000000000000008a7de9cda70f630600000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000053d6d2c00000000000000000000000000e0c565a362603e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000053d6d2c000000000000000000000000005a93013210113d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000053d6d2c00000000000000000000000000346a9eb247e7330300000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000053d6d2c00000000000000000000000000ab4f99651be0330300000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000007d9e3616000000000000000000000000002b61958812573e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000007d9e36160000000000000000000000000044a09667fc073d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000007d9e36160000000000000000000000000016d093ede5899a0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000007d9e3616000000000000000000000000002a6cce7ab9869a0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000384f1b0b000000000000000000000000002da17764be2e3e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000384f1b0b00000000000000000000000000d960d467ece03c0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000384f1b0b000000000000000000000000002576345b966acd0000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000384f1b0b000000000000000000000000002cb67b921a69cd0000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000096a78d0500000000000000000000000000ca580b7e08463d0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000096a78d050000000000000000000000000073d7cf4295043c0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000096a78d0500000000000000000000000000d559f31eb6be660000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000096a78d05000000000000000000000000002e2065dafebd660000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000c5d3c60200000000000000000000000000716b78657966300000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000c5d3c60200000000000000000000000000b29c78856f20300000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000c5d3c60200000000000000000000000000400cc5c5b561330000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000c5d3c60200000000000000000000000000932d05be5b61330000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000dc69630100000000000000000000000000f453f54ef84b190000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000dc696301000000000000000000000000002bc85e052b45190000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000dc696301000000000000000000000000007aab943d71b1190000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000dc696301000000000000000000000000009e57739944b1190000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000e3b4b1000000000000000000000000000034bdfd1634c20c0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000e3b4b10000000000000000000000000000bdd99cd846c00c0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000e3b4b1000000000000000000000000000020982a89ddd80c0000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000e3b4b10000000000000000000000000000e9ff5951c7d80c0000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000006bda58000000000000000000000000000016d0871de066060000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000006bda5800000000000000000000000000000a3c91321d66060000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000006bda580000000000000000000000000000d543ebb9776c060000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000006bda5800000000000000000000000000006aeda19d6c6c060000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000002a6d2c0000000000000000000000000000904185f7cf34030000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000002a6d2c0000000000000000000000000000fb8b23467834030000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000002a6d2c00000000000000000000000000006a098c633d36030000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000002a6d2c000000000000000000000000000074710ccc3736030000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008f36160000000000000000000000000000b15f7cd5be9a010000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000008f3616000000000000000000000000000076f8390a959a010000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000008f36160000000000000000000000000000166d95d91e9b010000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000008f36160000000000000000000000000000c9958d041c9b010000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000411b0b0000000000000000000000000000035394ad74cd000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000411b0b0000000000000000000000000000bee6034260cd000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000411b0b00000000000000000000000000001753341a8fcd000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000411b0b0000000000000000000000000000cfc36ca68dcd000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000958d050000000000000000000000000000392d27e5be66000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000958d050000000000000000000000000000e60b07deb466000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000958d0500000000000000000000000000005024b0c1c666000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000958d0500000000000000000000000000006633ca07c666000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000bfc60200000000000000000000000000000e9884f65f33000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000bfc60200000000000000000000000000009c04fff25a33000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000bfc602000000000000000000000000000090b35b8e6233000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000bfc60200000000000000000000000000001398e41e6233000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000059630100000000000000000000000000002c1b52c4af19000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000005963010000000000000000000000000000eee0cc4bad19000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000005963010000000000000000000000000000d43977cfb019000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000059630100000000000000000000000000002b6b3885b019000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000a1b1000000000000000000000000000000481fb216d70c000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000a1b100000000000000000000000000000040e41cd1d50c000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000a1b1000000000000000000000000000000d4760993d70c000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000a1b1000000000000000000000000000000b82ea764d70c000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000ca58000000000000000000000000000000b57d11136b06000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000ca58000000000000000000000000000000a0223d706a06000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000ca580000000000000000000000000000006ab040516b06000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000ca58000000000000000000000000000000078b0e3a6b06000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000592c0000000000000000000000000000007e02e9983403000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000592c00000000000000000000000000000076436b473403000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000592c000000000000000000000000000000b6d589ca3403000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000592c000000000000000000000000000000769d6cac3403000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000002116000000000000000000000000000000aa4b14659901000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000211600000000000000000000000000000015c0c64e9901000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000002116000000000000000000000000000000a4ad6d909901000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000021160000000000000000000000000000000508db6e9901000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000b0b0000000000000000000000000000009a937a43cc00000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000000b0b00000000000000000000000000000057d58c41cc00000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000000b0b000000000000000000000000000000548d6b62cc00000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000000b0b0000000000000000000000000000000868a151cc00000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000007f050000000000000000000000000000009f99e9966500000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000007f050000000000000000000000000000006887299f6500000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000007f05000000000000000000000000000000ef55e8b86500000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000007f05000000000000000000000000000000cc1d41a76500000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000b402000000000000000000000000000001010a00000000000000b402000000000000000000000000000001010a00000000000000b402000000000000000000000000000001010a00000000000000b402000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000004e01000000000000000000000000000001010a000000000000004e01000000000000000000000000000001010a000000000000004e01000000000000000000000000000001010a000000000000004e01000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000009b00000000000000000000000000000001010a000000000000009b00000000000000000000000000000001010a000000000000009b00000000000000000000000000000001010a000000000000009b00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000004700000000000000000000000000000001010a000000000000004700000000000000000000000000000001010a000000000000004700000000000000000000000000000001010a000000000000004700000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000001800000000000000000000000000000001010a000000000000001800000000000000000000000000000001010a000000000000001800000000000000000000000000000001010a000000000000001800000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000c00000000000000000000000000000001010a000000000000000c00000000000000000000000000000001010a000000000000000c00000000000000000000000000000001010a000000000000000c00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000c00000000000000000000000000000001010a000000000000000c00000000000000000000000000000001010a000000000000000c00000000000000000000000000000001010a000000000000000c00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000f72882a40100000000000000000000000068696ecd65633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000f72882a4010000000000000000000000006836e5c001143d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000f72882a401000000000000000000000000b216dbc7efd5981d00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000f72882a4010000000000000000000000000de75c955c14981d00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000006d1441d2000000000000000000000000004c83a7c84b633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000006d1441d200000000000000000000000000002d8560e8133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000006d1441d2000000000000000000000000003ad34d4c3c45ff0e00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000006d1441d20000000000000000000000000033159c86d306ff0e00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000002f8a2069000000000000000000000000006ff08bb5e3623e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000002f8a206900000000000000000000000000113bf86f82133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000002f8a206900000000000000000000000000dd5bb6b574968c0700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000002f8a206900000000000000000000000000e50ec8d233808c0700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000104590340000000000000000000000000089eb121c42613e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000010459034000000000000000000000000006b613951ea113d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000001045903400000000000000000000000000764272ed0390c90300000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000010459034000000000000000000000000000fa4e2272c87c90300000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008122481a00000000000000000000000000ffba03f8a65a3e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000008122481a00000000000000000000000000811a43c6780b3d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000008122481a000000000000000000000000002d8de951319ae50100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000008122481a000000000000000000000000007189960a5996e50100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000003911240d00000000000000000000000000509c62e4b83e3e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000003911240d0000000000000000000000000015a947205cf03c0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000003911240d00000000000000000000000000b64238edc301f30000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000003911240d00000000000000000000000000dbc746defcfff20000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000950892060000000000000000000000000032752cab5caf3d0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000950892060000000000000000000000000080a9505a60673c0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000950892060000000000000000000000000089b7645d108e790000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000009508920600000000000000000000000000f0ce1328368d790000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000430449030000000000000000000000000083cecb380c8c360000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000004304490300000000000000000000000000ed3c71c172f0350000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000043044903000000000000000000000000006fe130e053ca3c0000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000043044903000000000000000000000000000eba430fe9c93c0000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000001a82a4010000000000000000000000000033bc6127cbcd1d0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000001a82a40100000000000000000000000000d909857683c31d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000001a82a401000000000000000000000000002e395c84fc651e0000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000001a82a4010000000000000000000000000020fd7aa7c7651e0000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000ff40d20000000000000000000000000000fdebedfd0f130f0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000ff40d2000000000000000000000000000002aec5d38e100f0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000ff40d20000000000000000000000000000df52d90832330f0000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000ff40d20000000000000000000000000000e9310eb417330f0000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000007820690000000000000000000000000000ac45074bd391070000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000782069000000000000000000000000000081380813e390070000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000782069000000000000000000000000000056974daca599070000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000078206900000000000000000000000000002c411f789899070000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000002e9034000000000000000000000000000081bf7a66d9ca030000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000002e90340000000000000000000000000000e4140f966fca030000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000002e903400000000000000000000000000005ab3be1fd5cc030000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000002e903400000000000000000000000000005a9e427ccecc030000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000010481a00000000000000000000000000008a67117ee6e5010000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000010481a0000000000000000000000000000236193b2b4e5010000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000010481a0000000000000000000000000000d69e7ee16ae6010000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000010481a0000000000000000000000000000feef758667e6010000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000240d000000000000000000000000000032b6ee0f11f3000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000240d00000000000000000000000000000cf3bee5f8f2000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000240d0000000000000000000000000000314f7a1135f3000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000240d00000000000000000000000000007f96b15a33f3000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000f2910600000000000000000000000000000f6a25178f79000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000f2910600000000000000000000000000000ebf5a1e8379000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000f291060000000000000000000000000000422ed3929979000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000f29106000000000000000000000000000088d5e9a49879000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000eb4803000000000000000000000000000071f6c163c83c000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000eb4803000000000000000000000000000070f6f179c23c000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000eb48030000000000000000000000000000bc4699c9cb3c000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000eb4803000000000000000000000000000017336149cb3c000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000006ea40100000000000000000000000000001561f116641e000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000006ea401000000000000000000000000000099270722611e000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000006ea40100000000000000000000000000007b49d05a651e000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000006ea40100000000000000000000000000008c0eb31a651e000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000029d20000000000000000000000000000003c4b8e2d310f000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000029d2000000000000000000000000000000c914c5a92f0f000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000029d200000000000000000000000000000064e97faa310f000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000029d200000000000000000000000000000091ff2d81310f000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000d690000000000000000000000000000000e6bbf029807000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000000d690000000000000000000000000000005fc9ce409707000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000000d690000000000000000000000000000000d977d4a9807000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000000d69000000000000000000000000000000a83751239807000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000007834000000000000000000000000000000a0aabbebca03000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000078340000000000000000000000000000003ed7ee93ca03000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000078340000000000000000000000000000004d06e418cb03000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000783400000000000000000000000000000009730afcca03000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000002e1a000000000000000000000000000000b43d3760e401000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000002e1a0000000000000000000000000000005b647c3de401000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000002e1a00000000000000000000000000000061c75589e401000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000002e1a00000000000000000000000000000007926468e401000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000100d000000000000000000000000000000002b48a5f100000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000100d0000000000000000000000000000007478df93f100000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000100d000000000000000000000000000000bb8f1cc3f100000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000100d00000000000000000000000000000072eaa2b2f100000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000080060000000000000000000000000000002b800c2c7800000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000008006000000000000000000000000000000b8a14a237800000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000008006000000000000000000000000000000c8bf7d4d7800000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000008006000000000000000000000000000000d88afe3b7800000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000003203000000000000000000000000000001010a000000000000003203000000000000000000000000000001010a000000000000003203000000000000000000000000000001010a000000000000003203000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008b01000000000000000000000000000001010a000000000000008b01000000000000000000000000000001010a000000000000008b01000000000000000000000000000001010a000000000000008b01000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000b700000000000000000000000000000001010a00000000000000b700000000000000000000000000000001010a00000000000000b700000000000000000000000000000001010a00000000000000b700000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000005400000000000000000000000000000001010a000000000000005400000000000000000000000000000001010a000000000000005400000000000000000000000000000001010a000000000000005400000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000001c00000000000000000000000000000001010a000000000000001c00000000000000000000000000000001010a000000000000001c00000000000000000000000000000001010a000000000000001c00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000e00000000000000000000000000000001010a000000000000000e00000000000000000000000000000001010a000000000000000e00000000000000000000000000000001010a000000000000000e00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000e00000000000000000000000000000001010a000000000000000e00000000000000000000000000000001010a000000000000000e00000000000000000000000000000001010a000000000000000e00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000003ec7424d010000000000000000000000008433c7ab60633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000003ec7424d01000000000000000000000000058ed4c3fc133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000003ec7424d0100000000000000000000000050b2641270fd951700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000003ec7424d01000000000000000000000000439b656b477a951700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000009363a1a600000000000000000000000000ffa10a4237633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000009363a1a6000000000000000000000000003d2f3e47d4133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000009363a1a6000000000000000000000000009eefaf83a42eeb0b00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000009363a1a6000000000000000000000000002d76a9c70503eb0b00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000c4b1505300000000000000000000000000eaa0966391623e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000c4b15053000000000000000000000000002d1e5af831133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000c4b15053000000000000000000000000001149a58114c2fd0500000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000c4b15053000000000000000000000000005d19f1acf0b1fd0500000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000dc58a829000000000000000000000000002d0c2f18f75f3e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000dc58a82900000000000000000000000000cbdb1713a7103d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000dc58a82900000000000000000000000000e668c2c5a4ef000300000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000dc58a82900000000000000000000000000b06896a103e9000300000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000682cd41400000000000000000000000000fe51450c56553e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000682cd4140000000000000000000000000093f98beb4b063d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000682cd4140000000000000000000000000011986d15f7fb800100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000682cd414000000000000000000000000009573ff6503f9800100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000002e166a0a00000000000000000000000000be3f2cffc3263e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000002e166a0a00000000000000000000000000c47d0b073cd93c0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000002e166a0a000000000000000000000000000a348342149fc00000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000002e166a0a00000000000000000000000000287fb6c4b19dc00000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000110b350500000000000000000000000000fb109fe582093d0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000110b3505000000000000000000000000001eebc23769cc3b0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000110b350500000000000000000000000000ab05d9f8d157600000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000110b3505000000000000000000000000008a3de1872657600000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000083859a0200000000000000000000000000f1b0524defd92d0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000083859a02000000000000000000000000009938923d48a62d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000083859a02000000000000000000000000006ef9f8e7fa2d300000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000083859a020000000000000000000000000094e5b79ea62d300000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000bc424d0100000000000000000000000000f6e21a6d0bc0170000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000bc424d01000000000000000000000000003c4b91ef25ba170000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000bc424d0100000000000000000000000000602d7eaa8117180000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000bc424d0100000000000000000000000000872617dd5717180000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000052a1a600000000000000000000000000008e5a254e06f80b0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000052a1a600000000000000000000000000001f5228f945f60b0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000052a1a600000000000000000000000000009cfe3a1fe10b0c0000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000052a1a60000000000000000000000000000982cb549cc0b0c0000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000a350530000000000000000000000000000753389bb0c01060000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000a350530000000000000000000000000000cce0a4665800060000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000a350530000000000000000000000000000a5f6b46af805060000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000a350530000000000000000000000000000899b5bf6ed05060000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000046a8290000000000000000000000000000d254310bbb01030000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000046a82900000000000000000000000000000d299e666901030000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000046a8290000000000000000000000000000a41f1073fd02030000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000046a8290000000000000000000000000000db6a4a26f802030000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000001dd414000000000000000000000000000082eb23d12981010000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000001dd41400000000000000000000000000001e60a1ca0281010000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000001dd414000000000000000000000000000043381fcf7e81010000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000001dd41400000000000000000000000000007908751f7c81010000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000096a0a0000000000000000000000000000cd2fba95a7c0000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000096a0a00000000000000000000000000000e60128394c0000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000096a0a0000000000000000000000000000b732ed22bfc0000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000096a0a000000000000000000000000000078e015cbbdc0000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000f9340500000000000000000000000000007016bfba5760000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000f934050000000000000000000000000000a55e40444e60000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000f9340500000000000000000000000000002410e9c45e60000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000f934050000000000000000000000000000232eba0f5e60000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000719a02000000000000000000000000000022737f292c30000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000719a0200000000000000000000000000009a1e88772730000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000719a0200000000000000000000000000001d46af8f2e30000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000719a02000000000000000000000000000097d1d42b2e30000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000334d010000000000000000000000000000d9d63ff01518000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000334d0100000000000000000000000000006c17ff8d1318000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000334d0100000000000000000000000000009fdb90e21618000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000334d0100000000000000000000000000000f9c61a71618000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008ea60000000000000000000000000000005c61a42c0a0c000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000008ea60000000000000000000000000000006e97f70d090c000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000008ea600000000000000000000000000000086be919c0a0c000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000008ea60000000000000000000000000000002c54f87e0a0c000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000004153000000000000000000000000000000cf70099e0406000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000041530000000000000000000000000000004e4067050406000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000415300000000000000000000000000000070ae44df0406000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000004153000000000000000000000000000000d1ef35c70406000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000095290000000000000000000000000000006309a6670103000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000009529000000000000000000000000000000852d84240103000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000009529000000000000000000000000000000e99bcc9a0103000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000095290000000000000000000000000000006f2e417c0103000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000bf140000000000000000000000000000005ebb72cc7f01000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000bf14000000000000000000000000000000471311b47f01000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000bf14000000000000000000000000000000fffc8ef87f01000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000bf14000000000000000000000000000000c23cc5d67f01000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000005a0a000000000000000000000000000000ffbb2977bf00000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000005a0a000000000000000000000000000000d6f83174bf00000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000005a0a00000000000000000000000000000093337c96bf00000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000005a0a000000000000000000000000000000f9809685bf00000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000002705000000000000000000000000000000d489023a5f00000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000027050000000000000000000000000000004130be415f00000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000002705000000000000000000000000000000e1b0315c5f00000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000002705000000000000000000000000000000ccc47c4a5f00000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008802000000000000000000000000000001010a000000000000008802000000000000000000000000000001010a000000000000008802000000000000000000000000000001010a000000000000008802000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000003901000000000000000000000000000001010a000000000000003901000000000000000000000000000001010a000000000000003901000000000000000000000000000001010a000000000000003901000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000009100000000000000000000000000000001010a000000000000009100000000000000000000000000000001010a000000000000009100000000000000000000000000000001010a000000000000009100000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000004300000000000000000000000000000001010a000000000000004300000000000000000000000000000001010a000000000000004300000000000000000000000000000001010a000000000000004300000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000001600000000000000000000000000000001010a000000000000001600000000000000000000000000000001010a000000000000001600000000000000000000000000000001010a000000000000001600000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000b00000000000000000000000000000001010a000000000000000b00000000000000000000000000000001010a000000000000000b00000000000000000000000000000001010a000000000000000b00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000b00000000000000000000000000000001010a000000000000000b00000000000000000000000000000001010a000000000000000b00000000000000000000000000000001010a000000000000000b00000000000000000000000000000001000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000929d9ae100000000000000000000000000275e4a5650633e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000929d9ae100000000000000000000000000d41785c9ec133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000929d9ae100000000000000000000000000072f325a8487131000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000929d9ae100000000000000000000000000e3e21450ca41131000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000c14ecd7000000000000000000000000000a8c197fef5623e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000c14ecd7000000000000000000000000000a33d7e4b94133d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000c14ecd700000000000000000000000000020774ff832a8180800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000c14ecd7000000000000000000000000000c9db78799c8f180800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000005ca76638000000000000000000000000007ae8c3778b613e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000005ca766380000000000000000000000000055d1d7f631123d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000005ca7663800000000000000000000000000474d2aa9dd16100400000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000005ca7663800000000000000000000000000bbbb2497320d100400000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000aa53331c00000000000000000000000000b801f5e4d15b3e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000aa53331c00000000000000000000000000f24cc80f9c0c3d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000aa53331c000000000000000000000000003d374e6158fd080200000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000aa53331c00000000000000000000000000009318b92cf9080200000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000d1a9190e0000000000000000000000000098b5089ccd433e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000d1a9190e00000000000000000000000000a047bf5547f53c0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000d1a9190e00000000000000000000000000e64401af4dbb040100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000d1a9190e000000000000000000000000009bc0bb8062b9040100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000be7e261500000000000000000000000000c5cc9a59c5553e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000be7e2615000000000000000000000000007c06d438b8063d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000be7e26150000000000000000000000000064d2749676eb860100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000be7e261500000000000000000000000000b5cceaca75e8860100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000034e9ac1800000000000000000000000000f258540574593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000034e9ac18000000000000000000000000004ec35cc04d0a3d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000034e9ac18000000000000000000000000009121c1bf2ff8c70100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000034e9ac1800000000000000000000000000897d58219cf4c70100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000f9b3e91600000000000000000000000000ee2d635fd4573e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000f9b3e91600000000000000000000000000d4cc7907b9083d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000f9b3e9160000000000000000000000000039b6bc4fc572a70100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000f9b3e9160000000000000000000000000002dc6fbf7b6fa70100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000964ecb1700000000000000000000000000267a7603b0583e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000964ecb1700000000000000000000000000070db9e28e093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000964ecb170000000000000000000000000091383805b7b5b70100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000964ecb17000000000000000000000000001c14c7ab48b2b70100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000e11b3c18000000000000000000000000002d7c7ac314593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000e11b3c1800000000000000000000000000d79b58fef0093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000e11b3c180000000000000000000000000010a70d3a02d7bf0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000e11b3c1800000000000000000000000000ffa0ae4681d3bf0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000003cb50318000000000000000000000000008c087e1ce3583e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000003cb5031800000000000000000000000000eacc5ca0c0093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000003cb50318000000000000000000000000000dbe3d7160c6bb0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000003cb503180000000000000000000000000023f2aacae8c2bb0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008be81f180000000000000000000000000060aa3d1efc583e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000008be81f18000000000000000000000000007df69ffdd8093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000008be81f180000000000000000000000000027062907b2cebd0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000008be81f180000000000000000000000000063dd24ba35cbbd0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000032022e1800000000000000000000000000734a1c7a08593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000032022e1800000000000000000000000000d85f7e10e5093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000032022e1800000000000000000000000000f8aa5313dad2be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000032022e180000000000000000000000000005d11e735bcfbe0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000005ff526180000000000000000000000000043446d5502593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000005ff52618000000000000000000000000002e1bcefdde093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000005ff5261800000000000000000000000000cd709925c650be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000005ff5261800000000000000000000000000ce8cc2a5484dbe0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000c57b2a18000000000000000000000000003282845e05593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000c57b2a180000000000000000000000000031422607e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000c57b2a1800000000000000000000000000e93dabdfcf91be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000c57b2a18000000000000000000000000001a5fded8518ebe0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000fb3e2c180000000000000000000000000084b990f506593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000fb3e2c1800000000000000000000000000574cd28be3093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000fb3e2c1800000000000000000000000000d0ec37f154b2be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000fb3e2c1800000000000000000000000000fbf2b69dd6aebe0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000005c5d2b18000000000000000000000000002e990a2a06593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000005c5d2b1800000000000000000000000000eb2d3bc0e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000005c5d2b180000000000000000000000000097cde01e12a2be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000005c5d2b1800000000000000000000000000d7c545df939ebe0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000008dec2a180000000000000000000000000034dc87cd05593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000008dec2a1800000000000000000000000000bbbcb063e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000008dec2a18000000000000000000000000003e7ec1bef099be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000008dec2a1800000000000000000000000000efbec6a47296be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000029b42a1800000000000000000000000000862a069605593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000029b42a18000000000000000000000000001b86ac3ee2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000029b42a180000000000000000000000000017253a4fe095be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000029b42a18000000000000000000000000005751d63e6292be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000f3972a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000f3972a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000f3972a18000000000000000000000000001b4fa6cdd793be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000f3972a1800000000000000000000000000432547cb5990be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000000ea62a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000000ea62a18000000000000000000000000001d662a2ce2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000000ea62a18000000000000000000000000007d80700edc94be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000000ea62a1800000000000000000000000000943cd5fb5d91be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000fd9e2a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000fd9e2a18000000000000000000000000001d662a2ce2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000fd9e2a1800000000000000000000000000cfc377ad5994be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000fd9e2a1800000000000000000000000000dc44c099db90be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000789b2a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000789b2a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000789b2a1800000000000000000000000000fd138fbd1894be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000789b2a1800000000000000000000000000cdf549a99a90be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000b1992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000b1992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000b1992a180000000000000000000000000053c093f2f793be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000b1992a1800000000000000000000000000cc2007de7990be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000ce982a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000ce982a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000ce982a1800000000000000000000000000a2ac4f96e793be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000ce982a1800000000000000000000000000d2639f816990be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000040992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000040992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000040992a18000000000000000000000000008e5fabcdef93be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000040992a180000000000000000000000000070070db97190be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000075992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000075992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000075992a180000000000000000000000000085e28b9ff393be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000075992a180000000000000000000000000011dff58a7590be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000093992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000093992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000093992a1800000000000000000000000000bfcc0fc9f593be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000093992a180000000000000000000000000096767eb47790be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000a2992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000a2992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000a2992a1800000000000000000000000000ddc1d1ddf693be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000a2992a1800000000000000000000000000b1cb42c97890be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a000000000000009b992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a000000000000009b992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a000000000000009b992a1800000000000000000000000000b579aa5cf693be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a000000000000009b992a180000000000000000000000000095611a487890be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000040a00000010270000000000000000000000000000040a00000010270000000000000000000000000000040a0000001027000000000000000000000000000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000026043812afb3eb4600000000000000000092ead6a0fa5210cd3803000000000000080300000000de000000026e000000de0000006e00000001000000006e00000026043812afb3eb46000000000000000000a1f1e598d558be090300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000026043812afb3eb460000000000000000006dda1248bc59be090300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000026043812afb3eb460000000000000000002b42ac5351121a5f3803000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000065b7e727d5d97523000000000000000000a33f845a3097e23ea901000000000000080300000000de000000026e000000de0000006e00000001000000006e00000065b7e727d5d97523000000000000000000ce499eb2c119be090300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000065b7e727d5d97523000000000000000000671bf8108f1bbe090300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000065b7e727d5d97523000000000000000000ce4b8b4a80385113a901000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005cb65963e9ecba11000000000000000000af7b25f767d43bf4d700000000000000080300000000de000000026e000000de0000006e00000001000000006e0000005cb65963e9ecba11000000000000000000cb1e8d34da22bd090300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005cb65963e9ecba11000000000000000000657cc7fe2829bd090300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005cb65963e9ecba110000000000000000007b19b9aed3b298e1d700000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d7b512817376dd0800000000000000000027f6bc6855ca53d36c00000000000000080300000000de000000026e000000de0000006e00000001000000006e000000d7b512817376dd08000000000000000000e099dc64584fb9090300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d7b512817376dd0800000000000000000070f9f23c7a69b9090300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d7b512817376dd080000000000000000004f8e392148b1d5ca6c00000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000009435ef8f38bb6e04000000000000000000ae84ed7224aa7aa03600000000000000080300000000de000000026e000000de0000006e00000001000000006e0000009435ef8f38bb6e04000000000000000000297c15c8a210aa090300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000009435ef8f38bb6e040000000000000000003f45e81a7676aa090300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000009435ef8f38bb6e040000000000000000004a38449235cb719c3600000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000073755d179b5d37020000000000000000006ad3dc920f17025e1b00000000000000080300000000de000000026e000000de0000006e00000001000000006e00000073755d179b5d3702000000000000000000ec1773c293b36c090300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000073755d179b5d37020000000000000000005a5c3c5edb386e090300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000073755d179b5d370200000000000000000053736beedc5b0b5c1b00000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000006395145bccae1b010000000000000000002d497c72724774b20d00000000000000080300000000de000000026e000000de0000006e00000001000000006e0000006395145bccae1b010000000000000000004dfef86222656d080300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000006395145bccae1b010000000000000000009f07551da01273080300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000006395145bccae1b010000000000000000000c4bb9332f5c7cb10d00000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005a25f0fc64d78d000000000000000000004b84dd228c2a17da0600000000000000080300000000de000000026e000000de0000006e00000001000000006e0000005a25f0fc64d78d00000000000000000000c86d7128081ba7030300000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005a25f0fc64d78d000000000000000000004aa2512620f5bc030300000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005a25f0fc64d78d00000000000000000000f2ba3a4627129cd90600000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000056eddd4db1eb4600000000000000000000104aa55be3d4426d0300000000000000080300000000de000000026e000000de0000006e00000001000000006e00000056eddd4db1eb4600000000000000000000af2dbf1533f8e2d40200000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000056eddd4db1eb4600000000000000000000f809420bb82548d50200000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000056eddd4db1eb4600000000000000000000c81f1a9cb87e056d0300000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000feabba45d675230000000000000000000028d741f5a221afb60100000000000000080300000000de000000026e000000de0000006e00000001000000006e000000feabba45d6752300000000000000000000f08212689bc5a1ab0100000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000feabba45d675230000000000000000000083e0d756d070c2ab0100000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000feabba45d6752300000000000000000000754a05fffd8390b60100000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000a83043f2e9ba11000000000000000000008b2dd4d3d5f75adb0000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000a83043f2e9ba1100000000000000000000f18c7cc4dde61dd90000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000a83043f2e9ba1100000000000000000000d89d1b4628c52cd90000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000a83043f2e9ba1100000000000000000000e13a864b0bab4bdb0000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000a64ded9772dd08000000000000000000008aa7e80cd03bae6d0000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000a64ded9772dd08000000000000000000000d245c9e26b31d6d0000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000a64ded9772dd080000000000000000000067e76d1fed2d266d0000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000a64ded9772dd08000000000000000000002fbc960b8a95a66d0000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000007c815c1bb86e040000000000000000000066631ba0f846d7360000000000000000080300000000de000000026e000000de0000006e00000001000000006e0000007c815c1bb86e04000000000000000000008f176563ec6caf360000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000007c815c1bb86e0400000000000000000000864b1243011fb4360000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000007c815c1bb86e0400000000000000000000176881a2e372d3360000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000671b14dd5a37020000000000000000000077023c610ca36b1b0000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000671b14dd5a37020000000000000000000031b6283651a35f1b0000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000671b14dd5a3702000000000000000000001c26c8b44d21621b0000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000671b14dd5a3702000000000000000000005ff2bcc5ffb8691b0000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000006c3550dab1b01000000000000000000002cafd0dc89b7b50d0000000000000000080300000000de000000026e000000de0000006e00000001000000006e00000006c3550dab1b010000000000000000000004300ea499b0b10d0000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000006c3550dab1b0100000000000000000000849f45bb9af8b20d0000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000006c3550dab1b010000000000000000000010d7b5fe90c0b40d0000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d5967625d38d000000000000000000000020de408630bfda060000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000d5967625d38d00000000000000000000001d8bce782b39d9060000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d5967625d38d0000000000000000000000494b2bc1c6e0d9060000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d5967625d38d00000000000000000000003dac7d07e643da060000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000014262162e846000000000000000000000038052c108a516d030000000000000000080300000000de000000026e000000de0000006e00000001000000006e00000014262162e8460000000000000000000000644d11c2b4ad6c030000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000014262162e8460000000000000000000000cff76f1569026d030000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000014262162e846000000000000000000000017c3f21588136d030000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005c48dccf71230000000000000000000000d34a9973d48bb6010000000000000000080300000000de000000026e000000de0000006e00000001000000006e0000005c48dccf712300000000000000000000000a5c4b4fad41b6010000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005c48dccf7123000000000000000000000097a1cbc9206bb6010000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005c48dccf7123000000000000000000000004f1fa78076cb6010000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d7fe53b7b7110000000000000000000000bee2decc3437db000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000d7fe53b7b711000000000000000000000037bfd3b66b12db000000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d7fe53b7b711000000000000000000000097f113742527db000000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000d7fe53b7b71100000000000000000000007ca1e4c43e26db000000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000beb4757ad908000000000000000000000077b59b80297e6d000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000beb4757ad908000000000000000000000032c56e411f6a6d000000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000beb4757ad9080000000000000000000000f2dad526d6756d000000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000beb4757ad9080000000000000000000000828aa677ef746d000000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000b18f065c6a040000000000000000000000d86b398d16a236000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000b18f065c6a040000000000000000000000ed74445e6c9636000000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000b18f065c6a0400000000000000000000009a586079d49b36000000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000b18f065c6a04000000000000000000000037a98f28bb9c36000000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000822269fd33020000000000000000000000814b397252421b000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000822269fd33020000000000000000000000b7762e3ccb3c1b000000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000822269fd330200000000000000000000001b188d9a983e1b000000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000822269fd33020000000000000000000000cf68bc497f3f1b000000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000ea6b1ace180100000000000000000000004545185b70920d000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000ea6b1ace180100000000000000000000004e30f3fb138f0d000000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000ea6b1ace180100000000000000000000000b8122abfa8f0d000000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000ea6b1ace180100000000000000000000000b8122abfa8f0d000000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000047ebd8058a000000000000000000000000ce36806ac6ab06000000000000000000080300000000de000000026e000000de0000006e00000001000000006e00000047ebd8058a000000000000000000000000bca1810a00a806000000000000000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000047ebd8058a00000000000000000000000080f2b0b9e6a806000000000000000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000047ebd8058a0000000000000000000000004243e068cda906000000000000000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000000000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000f62ab8a14200000000000000000000000056b9f270713803000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000f62ab8a14200000000000000000000000101000000006e000000f62ab8a14200000000000000000000000101000000006e000000f62ab8a14200000000000000000000000100000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000cdcaa7ef1e000000000000000000000000a94398f4c67e01000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000cdcaa7ef1e00000000000000000000000101000000006e000000cdcaa7ef1e00000000000000000000000101000000006e000000cdcaa7ef1e00000000000000000000000100000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e00000010c039470e0000000000000000000000009cecb198aab000000000000000000000080300000000de000000026e000000de0000006e00000001000000006e00000010c039470e00000000000000000000000101000000006e00000010c039470e00000000000000000000000101000000006e00000010c039470e00000000000000000000000100000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e0000005a9568c2040000000000000000000000008e663b88e33a00000000000000000000080300000000de000000026e000000de0000006e00000001000000006e0000005a9568c20400000000000000000000000101000000006e0000005a9568c20400000000000000000000000101000000006e0000005a9568c20400000000000000000000000100000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000ad4a3461020000000000000000000000003cb61dc4711d00000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000ad4a34610200000000000000000000000101000000006e000000ad4a34610200000000000000000000000101000000006e000000ad4a34610200000000000000000000000100000000006e0000000010080300000000de000000026e000000de0000006e000000180300000000de0000000269000000de000000150000000264000000150000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de0000000269000000de000000170000000264000000170000000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e000000180300000000de000000026f000000de000000ea03000004ea0300000a00000002660000000a000000160000000416000000eb030000026e000000eb0300006e00000001000000006e000000ad4a3461020000000000000000000000003cb61dc4711d00000000000000000000080300000000de000000026e000000de0000006e00000001000000006e000000ad4a34610200000000000000000000000101000000006e000000ad4a34610200000000000000000000000101000000006e000000ad4a34610200000000000000000000000100de0000000000000000040403de0000000000000001de0000000000000007978ec67b77f20ca201000000000000004f1731bbca65352000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002c17ab28ad2bd01fd10000000000000000c866ce994be5581000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007d4b8683c80c3fa9680000000000000000ea5c39e418d03d0800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000a5e5f330567df66d340000000000000000dd414b2074cb240400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bab2aa079d3552501a0000000000000000fc840ddacb61150200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000044190673c01180410d0000000000000000e576653004f20c0100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000088ccb328d2ff16ba060000000000000000d4a0b27c3f8b880000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002ba68a03db7662760300000000000000007fc4ba67204c460000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004619f6705f3288d40100000000000000002e3cce1da129250000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000093dedefd1e099b030100000000000000003d748ae8a497140000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007935206e817b249b0000000000000000005d487f59784e0c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000065e673fcaf2de9660000000000000000004c5da6c2d529080000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000062b96aedc98dcb4c00000000000000000030334e158217060000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e022e6e5d6bdbc3f00000000000000000061748f82570e050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000061e356b8da4e35390000000000000000007ec7ce7bc189040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000a2438fa15c97f1350000000000000000009314ad6c7647040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b761f83fa0c24f34000000000000000000648388705126040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cc0260653fd17e33000000000000000000f47d5d63be15040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004dc1e0a1915f16330000000000000000006fb5f56a750d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004f2c5416b81fe232000000000000000000d15859605009040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d0e18d50cbffc732000000000000000000bb3608db3d07040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cfb07797d7f6ba32000000000000000000b796bb263506040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000979eecba5d72b432000000000000000000982095ccb005040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003d21da221e29b1320000000000000000004b2925116e05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000008fe2d0567e84af3200000000000000000008286db34c05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003943cc702eb2ae32000000000000000000d72b91043c05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cce796270950ae320000000000000000003cd87f3b3405040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d7452fd9f317ae32000000000000000000df839ac82f05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005a5d9585ee09ae32000000000000000000c82ee1ab2e05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bddb1e6e64002e0fa2010000000000000082cff2508e90352000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000036c7456921f0ed20d10000000000000000934598b84efb581000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000059ea3c9a02efcda9680000000000000000545c410744db3d0800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000eb7bb83273ee3d6e3400000000000000009fdfd63f14d1240400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b444f67e2bee75501a00000000000000003def8d929e64150200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000182915a507ee91410d000000000000000015397c376ef30c0100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004a9b24b8f5ed1fba060000000000000000184808abf48b880000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006454acc1eced6676030000000000000000db389a097b4c460000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000039377046e86d8ad4010000000000000000ded26a71ce29250000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003c7b6e55e3269c03010000000000000000fda40293bb97140000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000a54a5190630a259b0000000000000000005792e5ae834e0c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000aafe5efa2075e96600000000000000000050ab626ddb29080000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005c8cc96282b1cb4c0000000000000000006e4aaeea8417060000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000035d3fe16b3cfbc3f00000000000000000063663fed580e050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003bc9b5bdc8573539000000000000000000795c2531c289040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003e441191d39bf13500000000000000000042ec56c77647040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000dca822aedbc44f340000000000000000004fb0dc9d5126040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000eb447095dd27e33000000000000000000711e067abe15040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c5e03d6a20601633000000000000000000ee374976750d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000ba495567ff1fe232000000000000000000329101665009040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000034fee0e5eeffc732000000000000000000b7cedadd3d07040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d9858a58e9f6ba320000000000000000004c2c24283506040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f44fdf916672b432000000000000000000969d48cdb005040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009a07267b2229b13200000000000000000042687d116e05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006d63c96f8084af320000000000000000004ec397b34c05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000057111b6a2fb2ae3200000000000000000097f0a4043c05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b295a79a0950ae32000000000000000000d7fa883b3405040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000792a8afff317ae320000000000000000004a8c9dc82f05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000abcfc298ee09ae320000000000000000004faee2ab2e05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007320af154d896911a201000000000000002e8553e351bb352000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004077e0a995b40b22d1000000000000000099f980d65111591000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003589f3b03cd15caa68000000000000000084eb0f2a6fe63d0800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000030127d34905f856e3400000000000000007ef4535fb4d6240400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000aed641f6b9a699501a0000000000000000e0af0a4b7167150200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000ed3824d74ecaa3410d00000000000000008e25923ed8f40c0100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000c6a954719dc28ba06000000000000000033ae5dd9a98c880000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009c02ce7ffe646b76030000000000000000e79579abd54c460000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002d55ea1b71a98cd40100000000000000001c7b07c5fb29250000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e517feaca7449d0301000000000000000029e87a3dd297140000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d15f82b24599259b00000000000000000088c94b048f4e0c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000ef164af891bce966000000000000000000050c1f18e129080000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000565f28d83ad5cb4c000000000000000000aa610ec08717060000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000898317488fe1bc3f0000000000000000006458ef575a0e050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000014af14c3b660353900000000000000000029047ce6c289040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d94493804aa0f13500000000000000000096ba00227747040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000002f04c1c17c74f34000000000000000000dfd330cb5126040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000050652fad7ad37e3300000000000000000039acae90be15040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003d009b32af60163300000000000000000014b19c81750d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000256756b84620e23200000000000000000048dca96b5009040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000991a347b1200c8320000000000000000000d70ade03d07040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e25a9d19fbf6ba320000000000000000002baf8c293506040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005001d2686f72b432000000000000000000ef23fccdb005040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f7ed71d32629b132000000000000000000de9dd5116e05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004ae4c1888284af32000000000000000000935ec2b34c05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000074df696330b2ae32000000000000000000b2beb8043c05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009843b80d0a50ae32000000000000000000741d923b3405040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000001b0fe525f417ae32000000000000000000b694a0c82f05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000fc41f0abee09ae320000000000000000003237e4ab2e05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000077361b57a4894d1d5603000000000000004e05e51a1e28e13f00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000770b4fae1fa6fd27ab010000000000000095c80b7834b4e32000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000ec1707e5b0c255add50000000000000000dba2412f36a5b21000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000261e6380f9d001f06a0000000000000000e6aba64d6a5b6b0800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000442111ce1dd857913500000000000000007cd64bc49ebc3b0400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d222e8f4afdb02e21a00000000000000000d261dfd56e5200200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009aa35308795d588a0d000000000000000032970d8589b6120100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000fe6309925d1e83de060000000000000000ef478700326e8b0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000784ae4d6cf7e9888030000000000000000accea23bc5bd470000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c09b336eb520a3dd010000000000000000316f5adb7ce2250000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005a66f9c4fb7f28080100000000000000007218018415f4140000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000068233e654b216b9d000000000000000000812581a0af7c0c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002daa7ec046800c680000000000000000007fb5218cf140080000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000090ed1e6ec42f5d4d000000000000000000d13587700f23060000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004dedd0b92f790540000000000000000000c5d3707b1c14050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002beda95fe59d59390000000000000000003b1ececfa28c040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c588b4bd93be03360000000000000000004476a410e748040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e7ba9b6197c05834000000000000000000bea0100b0827040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000a46fadbeec4f8333000000000000000000eaf36faa1916040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000e2818e24389183300000000000000000047db0057a10d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004384cd73ef25e332000000000000000000f2013d2d6509040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005254c6c79882c83200000000000000000020bd453b4807040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000a3c2c271ed30bb32000000000000000000935549c23906040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d6d7a23bc479b432000000000000000000604bdd62b105040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006fe292a02f1eb132000000000000000000fa2a27336d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bce70a536570af3200000000000000000073104c1b4b05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000570c65b7d3a7ae3200000000000000000064454c323b05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b0fc735e3735ae32000000000000000000ac98de1a3205040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c7b837489018ae320000000000000000002f3903d52f05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e62f3d9528a1535be0600000000000000384560ff4300ec7900000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e6332cb73389e1335f0300000000000000e587fbf63d11854000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000059352e4d99a547b3af01000000000000003bc21e6bad9b3a2100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000012362f18ccb3faf2d7000000000000000092940e88b069df1000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000070b6af7de53ad4126c000000000000000041d57d488114820800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009ef66f3072fec022360000000000000000e0479b14382f470400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b516d0893860b72a1b00000000000000000c3de67131a4260200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c12680b61b91b2ae0d0000000000000000688597bd5a97150100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000001035d84c8d29b0f006000000000000000085f8e8eaf2de8c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000077a39ef0d0d8ae9103000000000000000030fc6e9f3876480000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006a73e7e9674d2ee20100000000000000001bc18120bc3e260000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005c3c263fbeea6d0a010000000000000000ba30f7173322150000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000dd3f2b915ed68d9e00000000000000000027f4e3bdbe930c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009dc12dba2ecc9d6800000000000000000081d41c08784c080000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000be6949a721aaa54d000000000000000000d875f53ecf28060000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cf3dd71d1b992940000000000000000000a384d011fa16050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004dba83008dad6b3900000000000000000068c12595118e040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000166674cad09a0c36000000000000000000f1333bfe9a49040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000714ed2d6672e5d34000000000000000000671ab0fb6127040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e0a99b353e5b85330000000000000000002d5d952d4316040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000096570065a97119330000000000000000009be955c6b30d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000032471824d499e3320000000000000000008b4ab65e6e09040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004845a483e9adc832000000000000000000d657e3aa4b07040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000093ab040cff1abb32000000000000000000a366ec043806040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b9de34d08951b43200000000000000000001c2f031ae05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004cf84c32cfecb032000000000000000000fcd872486905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d59dbe0a6757af32000000000000000000899ec01f4905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000dad791cfbd6fae32000000000000000000aabc5abf3605040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005ca6c680d335ae32000000000000000000db4f41273205040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e62e376252c4ec9b80600000000000000e02ceba8e1ca947900000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e60392964bdafd7d5c0300000000000000a962a7daf333544000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000590558853cce5558ae0100000000000000a049323df3b1202100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000001206bbfc34c88145d7000000000000000053ae661a0b10d21000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000070866c3831c517bc6b000000000000000072eed142af4d7b0800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009e464556afc362f735000000000000000089aacdbc34c5430400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b5a63165ee4208151b00000000000000005abca4d385ed240200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c1d6a7ec8d02dba30d00000000000000008f538c069abb140100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000010f562b05d6244eb060000000000000000040426c9f7708c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000077d351b367f5f88e030000000000000000819c835e343f480000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006af3b793ca5bd3e00100000000000000003a72bf543823260000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005c4cfc241e72c009010000000000000000745877ca7014150000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000dd2f8dcc251a379e00000000000000000075f9277edd8c0c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009da155a0296e7268000000000000000000b35662630749080000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000be294bab4dfb8f4d000000000000000000c47b9cee1627060000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cfedc5b0dfc11e40000000000000000000a248eaec1d16050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004dfa7192064266390000000000000000009e9b7084a38d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000165659243ce509360000000000000000004f898bf96349040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000071ae3bccb4d35b34000000000000000000f06d2f7b4627040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e0293e4113ae8433000000000000000000d94b06713516040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009667bf7b421b1933000000000000000000e8dfbfebac0d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000032b7eef7b76ee332000000000000000000531544f36a09040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000486506b67298c8320000000000000000006f0e03f74907040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000938b23367210bb3200000000000000000098e0ad2e3706040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b91e32f6714cb432000000000000000000b50f83caad05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004c6839d671eab0320000000000000000009f9e6d186905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d5bd2ba54f56af3200000000000000000015d396094905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000da37b6ad606fae32000000000000000000157ff7b73605040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005cd6d8efa435ae3200000000000000000064ac8f233205040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e62ef31a39aea83b806000000000000005e369b74336e907900000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e6e723c88c114c5b5c0300000000000000677e9d2c5ec2514000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000059e92648dee9fc46ae0100000000000000ac2bfe2e3b661f2100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000126a28080756d53cd70000000000000000f110720e2765d11000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000702a29681b8cc1b76b000000000000000006c32801f1f67a0800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009e8a299825a737f5350000000000000000dc1ae43a8199430400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b5ba29b0aab4f2131b000000000000000004a680cf96d7240200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c1d2293c6d3b50a30d00000000000000004539552e9db0140100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000010e52982cefefeea06000000000000000034dce406786b8c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000772f4170a243d68e0300000000000000008a26ed27743c480000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006a93351ce902c2e00100000000000000000cd91f24d821260000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005c0047bdafc5b709010000000000000000a27efbacc013150000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000dd7bb8c2efc3329e00000000000000000006be2a6e858c0c0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009d3971c50f437068000000000000000000aaa5255bdb48080000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bed9e411c3e58e4d000000000000000000fffe97ea0027060000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cfa91eb81c371e4000000000000000000054fc11eb1216050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004d4a2440a6fc6539000000000000000000d7c89a039e8d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000016623e4f8ec2093600000000000000000075874f396149040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007126b40b5fc25b3400000000000000000089ee281b4527040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e04906b56aa58433000000000000000000195332c13416040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000965baf89f0161933000000000000000000b81d0594ac0d040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000032a3ec28906ce33200000000000000000073587ec76a09040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000484d8bf85f97c8320000000000000000004bd437e14907040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000093e3712beb0fbb32000000000000000000d590f7233706040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b92ee5c4304cb432000000000000000000d33957c5ad05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004cd49e9153eab0320000000000000000004aee06166905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d565e4ac4156af3200000000000000000098287b084905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000da6f9e055c6fae32000000000000000000cded98b73605040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005cf2cc9ba235ae32000000000000000000645a60233205040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e62cfd8c535fa76a90500000000000000e516a0f9c18c386800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e687066a36e8d3d4d40200000000000000155f6829d87daf3600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000059895140bfd9c0836a01000000000000001bfc0de24086091c00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000120a77ab8352375bb50000000000000000793d45896644340e00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000070ca09e1e58ef2c65a000000000000000065deee7239b8270700000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009e2ad3fb162dd07c2d0000000000000000f629195629ca980300000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b5da37892ffcbed71600000000000000003710a21c5d23cf0100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c132eacfbb6336850b0000000000000000f903f1264bc3e90000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000106543f38117f2db0500000000000000000a985bbaffef760000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000778f407714d94f070300000000000000004d128288847d3d0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006a93ee462ed2fe9c010000000000000000a44c469b13c2200000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005c2016a16a36d6e7000000000000000000b582b9dacb63120000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000dd5bd95bd900428d0000000000000000008d2e9f8f86340b0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009df93ab910e6f75f000000000000000000883bc313db9c070000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000be593cda5bc0524900000000000000000042bc793201d1050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cf09bd6a812d003e00000000000000000039a56cb413eb040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004dcaacc064fc5638000000000000000000e70fcfbf1e78040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000001642f5dd854b823500000000000000000021ace64ea23e040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000071e648fa660b183400000000000000000086290002e621040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e049c3fa06d36233000000000000000000c3193ced8513040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000967b00fbd6360833000000000000000000ebc2b6e2550c040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003283ce880f01db3200000000000000000087b62ecbbf08040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000488db5cf2b66c432000000000000000000a67f68bff406040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000093a379656900b9320000000000000000002046c14b0d06040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b9ae5b30884db332000000000000000000658bed919905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004cb4cc951774b0320000000000000000002f9d03b55f05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d5a534d6af1faf32000000000000000000d83952b44405040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000da2fb9682b5dae3200000000000000000089f335463505040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005c525a4d8a2cae320000000000000000009ee6ae6a3105040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e424a1744ca159ca8050000000000000073a20ff3364b2a6800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000862689e17cb26167d40200000000000000bc6c6091bba3a73600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f96f3528e6be074d6a0100000000000000ebd2d422f165051c00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b2948bcb1ac5da3fb500000000000000007169ae2bbf26320e00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000010a7361d354844b95a0000000000000000d9070dc6eea5260700000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003e300c46c209f9752d0000000000000000d97eba472340980300000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d5f476da886a53d41600000000000000009d6c458721dece0100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002157ac24ec9a80830b0000000000000000f874cc2c9fa0e90000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000900ec7c91d3317db0500000000000000001bfb1d30a6de760000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d792c7bae966e2060300000000000000005d0b6ae0d6743d0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006aacd4941c19c89c010000000000000000bf329b8ebcbd200000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007c5b4e20e9d9bae7000000000000000000e9ecbd46a061120000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bd1098479c52348d000000000000000000240f5dc270330b0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005deb3cdbf50ef15f0000000000000000001b0a892c509c070000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003e8182c3d5544f4900000000000000000071d738bfbbd0050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002f4ca5b7c577fe3d000000000000000000b52053fbf0ea040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cd8243938a215638000000000000000000045189630d78040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f6cc851f20de8135000000000000000000340a58a1993e040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000031c333c7b7d41734000000000000000000a83583abe121040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e0e67db9b6b7623300000000000000000022b692c28313040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b6f8a232362908330000000000000000001626f7cd540c040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000001259c2d042fada3200000000000000000090771941bf08040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000880fd21fc962c4320000000000000000008074a87af406040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005313cd65bffeb832000000000000000000066ef6290d06040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003995ca88ba4cb33200000000000000000078439d819905040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002c56491ab873b032000000000000000000b6a670ad5f05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000158e95c4831faf32000000000000000000574ed3b04405040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009ad22eb81c5dae32000000000000000000eaa10b453505040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bc2315f5822cae32000000000000000000cebd196a3105040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e42aa387e1c4c66720500000000000000fd021ff1ac2f9f6400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000008646839b6bdd7c4cb9020000000000000008f659ba73e3b43400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f98fd759465595bf5c01000000000000002c6cba54bb8bff1a00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b2b401b933912179ae000000000000000005a31a824bf1ab0d00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000010c796682aafe755570000000000000000db261b869ab3e20600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003e5061c025be4ac42b00000000000000004a6ea3345d10760300000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d594466ca3457cfb15000000000000000058890d9280b8bd0100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000021373942620915170b0000000000000000466476605c0ae10000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000908e32ad416be1a405000000000000000017bc77e6a792720000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d772c755cd84c7eb02000000000000000050b56497a04e3b0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006aacf936f7a83a8f010000000000000000f555d5a893aa1f0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007cfbaa1aa823f4e000000000000000000074f7ac8308d8110000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bd706b9964f8d08900000000000000000051cbff15a4ee0a0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005dabcbd8c2623f5e000000000000000000ce967eb1e979070000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003e0194eb8d80764800000000000000000092b4ce9888bf050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002f2cf874730f923d000000000000000000c404998957e2040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cd0292464aee1f38000000000000000000b51747bcc073040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f62c77a251c6663500000000000000000053e66f72733c040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003183515db9490a34000000000000000000038179a6ce20040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e0e6d62d09f45b330000000000000000004db5fa64fa12040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b698191631c90433000000000000000000a7871a44100c040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000012b92217294bd932000000000000000000431ea30e9d08040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000884fa717258cc3320000000000000000001e54e573e306040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000053d3010b3f95b832000000000000000000aa1e85cb0406040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003915af04cc19b3320000000000000000007deb54779505040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002cb68581125cb0320000000000000000004acf3ccd5d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000015ced8cc9914af32000000000000000000998a31d34305040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009a929a657959ae32000000000000000000b1142bfb3405040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bc03cb4bb12aae32000000000000000000d66d29453105040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e425a499b45674b5705000000000000000d1aaa00f433d76200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000865680f8e2720abfab0200000000000000b10ac7815dd2ba3300000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f99fa8727620dcf85501000000000000001856316e546f7c1a00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b2c4bc2f40f7c415ab0000000000000000a6ae8fdf55ca680d00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000010d7460ea56239a4550000000000000000f6a26ee353b7c00600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003ee08b7d579873eb2a000000000000000046bdea50b1f7640300000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d5642eb530b3108f150000000000000000e9f23aa97d25b50100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000021a7ff509d40dfe00a000000000000000051a3c0572ebfdc0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000904ee89e5387c68905000000000000000063e62198a56c700000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d7624723bf133ade0200000000000000005d7665a8843b3a0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000006a2c0c88e4f0738801000000000000000088c14f03ff201f0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007c4bd99787c890dd000000000000000000805e7b953c93110000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bd2055c2484b1f8800000000000000000089cda6bc3dcc0a0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000005d0b9357a98c665d0000000000000000009bbf2e73b668070000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003ec19cff69160a48000000000000000000b5f36605efb6050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002f9ca1534adb5b3d0000000000000000008f4eafd00ade040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cd423920aad4043800000000000000000035d9a2689a71040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f6dcef636a3a59350000000000000000007b16fb5a603b040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000316360283a8403340000000000000000005e71f4234520040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000e06603683292583300000000000000000091a72eb6b512040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b6e8d4872e1903330000000000000000007b452cffed0b040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000012e9523a9c73d832000000000000000000afec67f58b08040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000088ef9113d320c332000000000000000000e6d183f0da06040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000053339cdd7e60b83200000000000000000054804c9c0006040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003955a1c25400b3320000000000000000002bc430729305040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002ce623b53f50b032000000000000000000e0d022dd5c05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000156efad0240faf320000000000000000000ea460644305040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009a7250bca757ae320000000000000000005ebb3ad63405040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004b68fb31e9fbad32000000000000000000b0d9278f2d05040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000bcf32577c829ae32000000000000000000354fb1323105040000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000a5c3ad87447ba3a010500000000000000ddda51cb0380245d00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000036c02e0bb3f6b3b6800200000000000000416aa37977ba9d3000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000046039f21d0e3b07440010000000000000028ca53929d4edb1800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cd24d7ac5e5aaf53a00000000000000000abb662e9b85a930c00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000923573f2a5952e43500000000000000000824b17a013c0540600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002f2da54f02f86e4b7800000000000000009e6f0f04e24c760900000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000061310c21d4c64e4764000000000000000036ac7764ea16e60700000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007ab3bf093dae3e455a000000000000000066669e10ad8f1d0700000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000ce7a197ef1a136445500000000000000004bc38022ee30b90600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007e4d50bb4d86b2c35200000000000000002b3c59edc2fa860600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002cd3be9fa17ef4035400000000000000004fa664be6716a00600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000b420ec8e499015a454000000000000000034e9c528cfa3ac0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000f5685f9af7f104545400000000000000001e0c83d2225da60600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000d9b3af97a22b0d7c540000000000000000ab4de6917980a90600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000047ea4d13f65d119054000000000000000085e838ee2412ab0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000011cf7e55cc440f86540000000000000000306348644f49aa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000c336a179b9220e8154000000000000000038c5dcd4e2e4a90600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000ea0290e7c2b38e835400000000000000009229d61e1917aa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000009385a233c0554e8254000000000000000022a2214bfcfda90600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003e44998dc184ee82540000000000000000ef3320b58a0aaa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000094a3943a429c3e83540000000000000000454204ead110aa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000eee220e7037b1683540000000000000000fec74ba0ac0daa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000008fb8e41325762a8354000000000000000064e0df953d0faa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000007bb68c80166320835400000000000000007842cd6b730eaa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000cebd38ca9d6c258354000000000000000047a2d680d80eaa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000002ebb0e6f61f12783540000000000000000b6465b0b0b0faa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000083abad9f81992683540000000000000000784ed016f00eaa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000059335e8771452783540000000000000000a3ca1591fd0eaa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000004477367b699b2783540000000000000000b088384e040faa0600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000003999227565c62783540000000000000000b4e7c9ac070faa0600000000000000000403de0000000000000004000000000010a5d4e8000000000000000000000004000000000010a5d4e8000000000000000000000004000000000010a5d4e8000000000000000000000004000000000010a5d4e8000000000000000000000004000000000010a5d4e8000000000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004de000000000082dfe40d4700000000000000000004de000000000082dfe40d470000000000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000a4f38e65000000000000000000000000007a022c3b4ee3a3000000000000000000040330450f00000000000130450f0000000000a4f38e6500000000000000000000000000fd081df93454a4000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000ce79c732000000000000000000000000001681f941b80452000000000000000000040330450f00000000000130450f0000000000ce79c732000000000000000000000000001d83b2e63d3352000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000e5bc631900000000000000000000000000ec492989210729000000000000000000040330450f00000000000130450f0000000000e5bc6319000000000000000000000000008726eb1de81b29000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000071deb10c00000000000000000000000000189b2241c28414000000000000000000040330450f00000000000130450f000000000071deb10c00000000000000000000000000c25ddf62868e14000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000036ef5806000000000000000000000000006c35ca80ad420a000000000000000000040330450f00000000000130450f000000000036ef58060000000000000000000000000043e902c567470a000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000099772c030000000000000000000000000079e4cad6692105000000000000000000040330450f00000000000130450f000000000099772c03000000000000000000000000008a801205bd2305000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000cb3b960100000000000000000000000000cb1d73afb99002000000000000000000040330450f00000000000130450f0000000000cb3b960100000000000000000000000000e13a63cae09102000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000e31dcb0000000000000000000000000000dbc64f055e4801000000000000000000040330450f00000000000130450f0000000000e31dcb00000000000000000000000000000f3a5bf5f04801000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000f08e650000000000000000000000000000d39ba34c2fa400000000000000000000040330450f00000000000130450f0000000000f08e65000000000000000000000000000036b1a69d78a400000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000074c7320000000000000000000000000000e885f4b2175200000000000000000000040330450f00000000000130450f000000000074c7320000000000000000000000000000331056533c5200000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000b8631900000000000000000000000000000f0e05db0b2900000000000000000000040330450f00000000000130450f0000000000b863190000000000000000000000000000837586291e2900000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000d8b10c0000000000000000000000000000efc43ce8851400000000000000000000040330450f00000000000130450f0000000000d8b10c0000000000000000000000000000d32caa0f8f1400000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000ea58060000000000000000000000000000fc162ff1420a00000000000000000000040330450f00000000000130450f0000000000ea580600000000000000000000000000007aa48b85470a00000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000732c03000000000000000000000000000009ef6e75210500000000000000000000040330450f00000000000130450f0000000000732c030000000000000000000000000000c31a69c1230500000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000369601000000000000000000000000000067be13b5900200000000000000000000040330450f00000000000130450f000000000036960100000000000000000000000000004afce2dc910200000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000017cb0000000000000000000000000000003e9c1354480100000000000000000000040330450f00000000000130450f000000000017cb0000000000000000000000000000002d7affe8480100000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f00000000008a6500000000000000000000000000000095549d27a40000000000000000000000040330450f00000000000130450f00000000008a650000000000000000000000000000007f6be973a40000000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000c132000000000000000000000000000000c249570d520000000000000000000000040330450f00000000000130450f0000000000c132000000000000000000000000000000703b4934520000000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f00000000005f190000000000000000000000000000006cd93e04290000000000000000000000040330450f00000000000130450f00000000005f1900000000000000000000000000000048248e19290000000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000ac0c000000000000000000000000000000fcdc767c140000000000000000000000040330450f00000000000130450f0000000000ac0c0000000000000000000000000000000e961b87140000000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000520600000000000000000000000000000063f9c3370a0000000000000000000000040330450f00000000000130450f000000000052060000000000000000000000000000004ff9123d0a0000000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000027030000000000000000000000000000010130450f000000000027030000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000092010000000000000000000000000000010130450f000000000092010000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f0000000000c5000000000000000000000000000000010130450f0000000000c5000000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f00000000005f000000000000000000000000000000010130450f00000000005f000000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f00000000002c000000000000000000000000000000010130450f00000000002c000000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000014000000000000000000000000000000010130450f000000000014000000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000006000000000000000000000000000000010130450f000000000006000000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000003000000000000000000000000000000010130450f000000000003000000000000000000000000000000010030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000130450f000000000003000000000000000000000000000000010130450f00000000000300000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000080b2e60e000000000000000000000000004112e8bcdd67540000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000080b2e60e00000000000000000000000000aa5a99624da2520000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000080b2e60e0000000000000000000000000019d4ca93af887501000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000080b2e60e000000000000000000000000004bf7ad31ec85750100000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000003f597307000000000000000000000000003a65d8c20fdc530000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000003f5973070000000000000000000000000099f5890e151c520000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000003f597307000000000000000000000000000612e60505d2ba00000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000003f59730700000000000000000000000000763c3790b3d0ba0000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009facb90300000000000000000000000000ff88ce52a64b4e0000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009facb9030000000000000000000000000075cc81f2280e4d0000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009facb90300000000000000000000000000ec0129196e6c5d00000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009facb9030000000000000000000000000020b00c6ec96b5d0000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000004fd6dc01000000000000000000000000006675789588992d0000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000004fd6dc01000000000000000000000000001d80f5f8a1852d0000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000004fd6dc0100000000000000000000000000bfb06af211b72e00000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000004fd6dc010000000000000000000000000051320aa4c0b62e0000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000276bee0000000000000000000000000000a62e991af722170000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000276bee000000000000000000000000000027d75e6ec61e170000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000276bee00000000000000000000000000001c3479aabf5b1700000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000276bee0000000000000000000000000000140abe25975b170000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000093357700000000000000000000000000004ab1798c49a00b0000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000933577000000000000000000000000000051466fdeca9e0b0000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009335770000000000000000000000000000147a4c78edad0b00000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009335770000000000000000000000000000c1f7ae41d9ad0b0000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000c99a3b000000000000000000000000000010f4455c89d3050000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000c99a3b0000000000000000000000000000b58a012ce4d2050000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000c99a3b0000000000000000000000000000353e871bfad60500000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000c99a3b00000000000000000000000000000b497bf3efd6050000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000064cd1d0000000000000000000000000000aef6796f99ea020000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000064cd1d0000000000000000000000000000a6ef3f594cea020000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000064cd1d0000000000000000000000000000a99f34dc7deb0200000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000064cd1d000000000000000000000000000073e721c878eb020000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000b2e60e0000000000000000000000000000bae068a68175010000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000b2e60e0000000000000000000000000000d7a129405c75010000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000b2e60e00000000000000000000000000002c43d924bf750100000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000b2e60e0000000000000000000000000000b29ab781bc75010000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000005873070000000000000000000000000000c0a0cff5cdba000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000058730700000000000000000000000000003ea97c82bbba000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000058730700000000000000000000000000002c810787dfba0000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000058730700000000000000000000000000004717eb28deba000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000acb9030000000000000000000000000000ce42273a6a5d000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000acb903000000000000000000000000000094ee6426615d000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000acb9030000000000000000000000000000acbcefc66f5d0000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000acb9030000000000000000000000000000ee45e1176f5d000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000d5dc01000000000000000000000000000060a1fdc0b52e000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000d5dc01000000000000000000000000000013ce2937b12e000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000d5dc010000000000000000000000000000beee3dcbb72e0000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000d5dc010000000000000000000000000000a2ea2b67b72e000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000006aee000000000000000000000000000000cacbcbf95a17000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000006aee000000000000000000000000000000f8c66fc15817000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000006aee0000000000000000000000000000006f484bd95b170000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000006aee0000000000000000000000000000003ff52c8e5b17000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000035770000000000000000000000000000002fc2f37cad0b000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000357700000000000000000000000000000049bfc560ac0b000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000035770000000000000000000000000000005454b3ecad0b0000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000035770000000000000000000000000000002e9599baad0b000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009a3b00000000000000000000000000000082d9dc98d605000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009a3b0000000000000000000000000000000fa54e17d605000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009a3b00000000000000000000000000000078abd2e9d6050000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000009a3b000000000000000000000000000000b2293bc4d605000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000cc1d000000000000000000000000000000aac8ce26eb02000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000cc1d0000000000000000000000000000006f9c04e6ea02000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000cc1d0000000000000000000000000000000a38d55beb020000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000cc1d000000000000000000000000000000c44a0949eb02000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000e60e000000000000000000000000000000c1d2dc867501000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000e60e000000000000000000000000000000879102737501000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000e60e000000000000000000000000000000ace5eaad75010000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000e60e0000000000000000000000000000003253fa977501000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000720700000000000000000000000000000005124391ba00000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000072070000000000000000000000000000003ae85187ba00000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000072070000000000000000000000000000007196e0bdba000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a0000002200000072070000000000000000000000000000004e785da6ba00000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000b903000000000000000000000000000001010a00000022000000b903000000000000000000000000000001010a00000022000000b903000000000000000000000000000001010a00000022000000b903000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000dc01000000000000000000000000000001010a00000022000000dc01000000000000000000000000000001010a00000022000000dc01000000000000000000000000000001010a00000022000000dc01000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a00000022000000ed00000000000000000000000000000001010a00000022000000ed00000000000000000000000000000001010a00000022000000ed00000000000000000000000000000001010a00000022000000ed00000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000007600000000000000000000000000000001010a000000220000007600000000000000000000000000000001010a000000220000007600000000000000000000000000000001010a000000220000007600000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000003b00000000000000000000000000000001010a000000220000003b00000000000000000000000000000001010a000000220000003b00000000000000000000000000000001010a000000220000003b00000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000001d00000000000000000000000000000001010a000000220000001d00000000000000000000000000000001010a000000220000001d00000000000000000000000000000001010a000000220000001d00000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000e00000000000000000000000000000001010a000000220000000e00000000000000000000000000000001010a000000220000000e00000000000000000000000000000001010a000000220000000e00000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000600000000000000000000000000000001010a000000220000000600000000000000000000000000000001010a000000220000000600000000000000000000000000000001010a000000220000000600000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000300000000000000000000000000000001010a000000220000000300000000000000000000000000000001010a000000220000000300000000000000000000000000000001010a000000220000000300000000000000000000000000000001000a0000002200000000101802640000000a00000015000000026900000015000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001802640000000a00000017000000026900000017000000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef0300002200000018040a000000ea030000026f000000ea030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef030000220000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000a401000004a401000068100000026810000068100000ef03000004ef03000022000000010a000000220000000100000000000000000000000000000001010a000000220000000100000000000000000000000000000001010a000000220000000100000000000000000000000000000001010a000000220000000100000000000000000000000000000001040a00000010270000000000000000000000000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000d26c2a1e00000000000000000000000000ce101f4c3a9f4f1203000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000d26c2a1e0000000000000000000000000029a6364af6d6cb0103000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000d26c2a1e0000000000000000000000000043844ea5eae8097e1b0000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000d26c2a1e000000000000000000000000004ca3e39193d2d07d1b000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000006736150f00000000000000000000000000fdc86d171f17491103000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000006736150f0000000000000000000000000076cdd054c5a4cc0003000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000006736150f00000000000000000000000000c6917559e8e205bf0d0000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000006736150f00000000000000000000000000f09a3e459eceebbe0d000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000339b8a070000000000000000000000000015c7d90df87f5b0c03000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000339b8a0700000000000000000000000000333d8e8d63fc10fc02000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000339b8a070000000000000000000000000074d40963142d83df060000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000339b8a07000000000000000000000000004c711db74fc076df06000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000994dc503000000000000000000000000004b728cff9a6551db02000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000994dc5030000000000000000000000000034cac98ce68177cf02000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000994dc5030000000000000000000000000069b7b22f1ba5c16f030000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000994dc503000000000000000000000000006a5df58bd695bb6f03000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000cca6e20100000000000000000000000000c7b7f897f62528ad01000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000cca6e2010000000000000000000000000003251a9fc50468ac01000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000cca6e20100000000000000000000000000f6972f67dad5e0b7010000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000cca6e201000000000000000000000000006b31ce02f9d6ddb701000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000006553f10000000000000000000000000000cef2f634975ad2d900000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000006553f1000000000000000000000000000015e98e017f8baad900000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000006553f10000000000000000000000000000701e1043f46af0db000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000006553f10000000000000000000000000000512145cab3edeedb00000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000b2a978000000000000000000000000000023f744bedd2f766d00000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000b2a978000000000000000000000000000029f6a884810e686d00000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000b2a97800000000000000000000000000003fda95914135f86d000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000b2a9780000000000000000000000000000cbbfcae18477f76d00000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000d8543c0000000000000000000000000000409253119e52db3600000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000d8543c0000000000000000000000000000d9cc94000d3dd53600000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000d8543c00000000000000000000000000003491bc7ec619fc36000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000d8543c00000000000000000000000000005683c0b3e6bafb3600000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000006b2a1e0000000000000000000000000000189918dcb58d751b00000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000006b2a1e000000000000000000000000000010dbb4532eb6721b00000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000006b2a1e0000000000000000000000000000d326c6b0fd0b7e1b000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000006b2a1e0000000000000000000000000000b5e7f61aa4db7d1b00000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de00000034150f00000000000000000000000000003f502e2132bcbc0d00000000000000000802640000000a00000015000000026900000015000000de000000010a000000de00000034150f0000000000000000000000000000aeb4c3a8885bbb0d00000000000000000802640000000a00000017000000026900000017000000de000000010a000000de00000034150f0000000000000000000000000000ed8899d0a104bf0d000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de00000034150f0000000000000000000000000000e23faeeb74ecbe0d00000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000998a07000000000000000000000000000043b476db10dade0600000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000998a070000000000000000000000000000ceb2dbff6b2dde0600000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000998a070000000000000000000000000000808648d46781df06000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000998a07000000000000000000000000000048930eb1dc74df0600000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000004bc5030000000000000000000000000000ca976eb2338a6f0300000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000004bc503000000000000000000000000000070c1e416cd346f0300000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000004bc5030000000000000000000000000000878aff0056bf6f03000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000004bc5030000000000000000000000000000c105ed1827b86f0300000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a5e2010000000000000000000000000000f09466aa64ccb70100000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a5e201000000000000000000000000000081f6b2f6b1a1b70100000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a5e2010000000000000000000000000000c801335c36dfb701000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a5e20100000000000000000000000000007747d2e69edbb70100000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de00000052f10000000000000000000000000000002b6e72b21be7db0000000000000000000802640000000a00000015000000026900000015000000de000000010a000000de00000052f1000000000000000000000000000000991a2a6cc2d1db0000000000000000000802640000000a00000017000000026900000017000000de000000010a000000de00000052f1000000000000000000000000000000ed9bfb8626efdb00000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de00000052f1000000000000000000000000000000bc1a267971ecdb0000000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a8780000000000000000000000000000008bbfb883a4f26d0000000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a8780000000000000000000000000000009d6ac91883e76d0000000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a878000000000000000000000000000000aa5b9df3a9f66d00000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a8780000000000000000000000000000009e08adc1daf46d0000000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000533c0000000000000000000000000000000f7225947ff7360000000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000533c00000000000000000000000000000073d7d122d8f2360000000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000533c0000000000000000000000000000001029c1a96bfa3600000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000533c0000000000000000000000000000009ec0c7650ff9360000000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000291e0000000000000000000000000000007e761e73d67a1b0000000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000291e000000000000000000000000000000674384a782781b0000000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000291e0000000000000000000000000000007b10d62cc17c1b00000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000291e0000000000000000000000000000006ed2dd09137c1b0000000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000130f00000000000000000000000000000051587b8b98bb0d0000000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000130f0000000000000000000000000000005011c934e3ba0d0000000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000130f0000000000000000000000000000005528419e02bd0d00000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000130f00000000000000000000000000000014f7c9e036bc0d0000000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de00000089070000000000000000000000000000004ca82b436edc060000000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000008907000000000000000000000000000000465b667b13dc060000000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000008907000000000000000000000000000000c18a12a70cde0600000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de00000089070000000000000000000000000000000e4c521e32dd060000000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000c303000000000000000000000000000001010a000000de000000c303000000000000000000000000000001010a000000de000000c303000000000000000000000000000001010a000000de000000c303000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000e001000000000000000000000000000001010a000000de000000e001000000000000000000000000000001010a000000de000000e001000000000000000000000000000001010a000000de000000e001000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000ef00000000000000000000000000000001010a000000de000000ef00000000000000000000000000000001010a000000de000000ef00000000000000000000000000000001010a000000de000000ef00000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000007700000000000000000000000000000001010a000000de0000007700000000000000000000000000000001010a000000de0000007700000000000000000000000000000001010a000000de0000007700000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000003a00000000000000000000000000000001010a000000de0000003a00000000000000000000000000000001010a000000de0000003a00000000000000000000000000000001010a000000de0000003a00000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000001c00000000000000000000000000000001010a000000de0000001c00000000000000000000000000000001010a000000de0000001c00000000000000000000000000000001010a000000de0000001c00000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000000d00000000000000000000000000000001010a000000de0000000d00000000000000000000000000000001010a000000de0000000d00000000000000000000000000000001010a000000de0000000d00000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000000600000000000000000000000000000001010a000000de0000000600000000000000000000000000000001010a000000de0000000600000000000000000000000000000001010a000000de0000000600000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000000200000000000000000000000000000001010a000000de0000000200000000000000000000000000000001010a000000de0000000200000000000000000000000000000001010a000000de0000000200000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000000100000000000000000000000000000001010a000000de0000000100000000000000000000000000000001010a000000de0000000100000000000000000000000000000001010a000000de0000000100000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000000100000000000000000000000000000001010a000000de0000000100000000000000000000000000000001010a000000de0000000100000000000000000000000000000001010a000000de0000000100000000000000000000000000000001000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000d2075d000000000000000000000000000033bc88f4cdf97b5400000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000d2075d0000000000000000000000000000a95cc31d81d2715400000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000d2075d000000000000000000000000000079b7be3dcf3ac954000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000d2075d0000000000000000000000000000993829495ca8c85400000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000bb8b8b0000000000000000000000000000daf1e9ace1757f7e00000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000bb8b8b00000000000000000000000000005d6a891b364a6e7e00000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000bb8b8b00000000000000000000000000002408f4111cd82d7f000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000bb8b8b00000000000000000000000000001a7b4d8cfdfb2c7f00000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000afcda20000000000000000000000000000edafabbdb979719300000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000afcda20000000000000000000000000000d32e1ba0fc205c9300000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000afcda20000000000000000000000000000885a592147266094000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000afcda20000000000000000000000000000ad7ab6cdde245f9400000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a96eae00000000000000000000000000009bd18f544555e69d00000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a96eae00000000000000000000000000008192da2e55b5ce9d00000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a96eae0000000000000000000000000000d07f62fc5a4df99e000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a96eae0000000000000000000000000000acecffe5ab3af89e00000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000263fb400000000000000000000000000006d6aa9ce7fb21fa300000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000263fb400000000000000000000000000005c58dd8a32e106a300000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000263fb4000000000000000000000000000092bcbc7ee4e045a4000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000263fb40000000000000000000000000000bcab31bfa8c444a400000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000006527b70000000000000000000000000000e1b96f364b1cbca500000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000006527b70000000000000000000000000000fe377114a4aea2a500000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000006527b70000000000000000000000000000ccca2ccd1d2beca6000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000006527b7000000000000000000000000000074c1a365900aeba600000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000849bb80000000000000000000000000000b4a3a647813e0aa700000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000849bb800000000000000000000000000009137f8f83682f0a600000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000849bb800000000000000000000000000005da8a4c5c54f3fa8000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000849bb80000000000000000000000000000bba9ab8eb12b3ea800000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000009455b900000000000000000000000000002357fd946b4cb1a700000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000009455b900000000000000000000000000009b1d996ccf6897a700000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000009455b90000000000000000000000000000297041688ee2e8a8000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000009455b900000000000000000000000000004e23cfc514bee7a800000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000009cb2b90000000000000000000000000000c2bf75b602d204a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000009cb2b900000000000000000000000000000c24931ad4d9eaa700000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000009cb2b9000000000000000000000000000056a924b9f2ab3da9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000009cb2b900000000000000000000000000008c7aee0e5d863ca900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de00000020e1b90000000000000000000000000000175d8646ce942ea800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de00000020e1b90000000000000000000000000000a37bc8da3f9314a800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de00000020e1b900000000000000000000000000003d7b7be1a41068a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de00000020e1b90000000000000000000000000000b07077856aeb66a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de00000062f8b90000000000000000000000000000fb4cdf0bd67443a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de00000062f8b900000000000000000000000000001597b905816f29a800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de00000062f8b900000000000000000000000000008531a0f5fd427da9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de00000062f8b9000000000000000000000000000078ddb540f11d7ca900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000000304ba0000000000000000000000000000fe8405f137e64da800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000000304ba000000000000000000000000000011f02be62cdd33a800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000000304ba0000000000000000000000000000fedfb07f2adc87a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000000304ba0000000000000000000000000000e80d424c4bb686a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000d309ba0000000000000000000000000000a87a11e18a1d53a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000d309ba0000000000000000000000000000b72a62d6021439a800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000d309ba00000000000000000000000000002e4bab1c4c288da9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000d309ba0000000000000000000000000000a14719a461038ca900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000bb0cba00000000000000000000000000004c479804a9b955a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000bb0cba0000000000000000000000000000948b7cce6daf3ba800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000bb0cba0000000000000000000000000000ff6528eb5cce8fa9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000bb0cba0000000000000000000000000000190bfc2678a98ea900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000002f0eba0000000000000000000000000000a205da6a430757a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000002f0eba0000000000000000000000000000e58c894a23fd3ca800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000002f0eba0000000000000000000000000000b7ec6652652191a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000002f0eba00000000000000000000000000007ae45b169afb8fa900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000e90eba00000000000000000000000000001cdafa9d10ae57a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000e90eba0000000000000000000000000000b9299b9e14a33da800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000e90eba0000000000000000000000000000662e0686e9ca91a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000e90eba0000000000000000000000000000ce9014b71fa590a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000460fba0000000000000000000000000000bcbe0ce3eb0158a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000460fba000000000000000000000000000039f5a3480df63da800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000460fba0000000000000000000000000000d3ced59fab1f92a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000460fba000000000000000000000000000098e67087e2f990a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000750fba000000000000000000000000000060b09585d92b58a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000750fba0000000000000000000000000000469e97bce7203ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000750fba0000000000000000000000000000cb1fcb54814a92a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000750fba00000000000000000000000000008ad2a798b82491a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000008c0fba00000000000000000000000000000629da56d04058a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000008c0fba0000000000000000000000000000972ea2d7f6343ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000008c0fba000000000000000000000000000063473887775f92a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000008c0fba000000000000000000000000000058873af8ae3991a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000980fba00000000000000000000000000005de2fd6ac04b58a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000980fba0000000000000000000000000000b64e1ccfe73f3ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000980fba00000000000000000000000000000a5c7c48676a92a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000980fba0000000000000000000000000000e3a20cd19e4491a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de0000009e0fba000000000000000000000000000006bf0f75385158a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de0000009e0fba0000000000000000000000000000c25ed94a60453ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de0000009e0fba00000000000000000000000000005e661e29df6f92a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de0000009e0fba0000000000000000000000000000a9b075bd164a91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a10fba000000000000000000000000000058ad187af45358a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a10fba0000000000000000000000000000c7e6b7881c483ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a10fba0000000000000000000000000000876b6f199b7292a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a10fba00000000000000000000000000008b37aab3d24c91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a20fba000000000000000000000000000074a71bd1dd5458a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a20fba0000000000000000000000000000c9beacf205493ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a20fba00000000000000000000000000003f6d8a69847392a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a20fba0000000000000000000000000000d7b9bb05bc4d91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a30fba000000000000000000000000000090a11e28c75558a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a30fba0000000000000000000000000000cb96a15cef493ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a30fba0000000000000000000000000000f86ea5b96d7492a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a30fba0000000000000000000000000000223ccd57a54e91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a30fba000000000000000000000000000090a11e28c75558a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a30fba0000000000000000000000000000cb96a15cef493ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a30fba0000000000000000000000000000f86ea5b96d7492a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a30fba0000000000000000000000000000223ccd57a54e91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a30fba000000000000000000000000000090a11e28c75558a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a30fba0000000000000000000000000000cb96a15cef493ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a30fba0000000000000000000000000000f86ea5b96d7492a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a30fba0000000000000000000000000000223ccd57a54e91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a30fba000000000000000000000000000090a11e28c75558a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a30fba0000000000000000000000000000cb96a15cef493ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a30fba0000000000000000000000000000f86ea5b96d7492a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a30fba0000000000000000000000000000223ccd57a54e91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a30fba000000000000000000000000000090a11e28c75558a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a30fba0000000000000000000000000000cb96a15cef493ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a30fba0000000000000000000000000000f86ea5b96d7492a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a30fba0000000000000000000000000000223ccd57a54e91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a30fba000000000000000000000000000090a11e28c75558a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a30fba0000000000000000000000000000cb96a15cef493ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a30fba0000000000000000000000000000f86ea5b96d7492a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a30fba0000000000000000000000000000223ccd57a54e91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a40fba0000000000000000000000000000ab9b217fb05658a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a40fba0000000000000000000000000000cc6e96c6d84a3ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a40fba0000000000000000000000000000b170c009577592a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a40fba00000000000000000000000000006dbedea98e4f91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000040a00000010270000000000000000000000000000000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000fee45b010000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000f8c05b010000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000c7515c010000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000005b4f5c010000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000001c185b010000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000002cf45a010000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000a6845b0100000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000003c825b010000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f0000000000000000000000000000240cae000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f003f420f0000000000000000000000000000b2faad000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f003f420f00000000000000000000000000005729ae000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f003f420f00000000000000000000000000001628ae000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f003f420f00000000000000000000000000005da5ad000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f0000000000000000000000000000f593ad000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f00000000000000000000000000007fc2ad0000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f00000000000000000000000000003ec1ad000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa1070000000000000000000000000000700c57000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f001fa1070000000000000000000000000000e50357000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f001fa1070000000000000000000000000000c51457000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f001fa1070000000000000000000000000000201457000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f001fa1070000000000000000000000000000f6d856000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa107000000000000000000000000000070d056000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa107000000000000000000000000000047e1560000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa1070000000000000000000000000000a1e056000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd0030000000000000000000000000000cf872b000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f008fd003000000000000000000000000000089832b000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f008fd0030000000000000000000000000000658a2b000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f008fd00300000000000000000000000000000c8a2b000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f008fd00300000000000000000000000000000d6e2b000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd0030000000000000000000000000000ca692b000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd0030000000000000000000000000000a1702b0000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd003000000000000000000000000000048702b000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e801000000000000000000000000000044c415000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0047e801000000000000000000000000000028c215000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0047e80100000000000000000000000000002ec515000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0047e8010000000000000000000000000000f6c415000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0047e801000000000000000000000000000062b715000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e801000000000000000000000000000047b515000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e80100000000000000000000000000004bb8150000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e801000000000000000000000000000014b815000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f40000000000000000000000000000002ee20a000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0023f40000000000000000000000000000001ae10a000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0023f400000000000000000000000000000092e20a000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0023f400000000000000000000000000000070e20a000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0023f4000000000000000000000000000000bcdb0a000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f4000000000000000000000000000000a9da0a000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f400000000000000000000000000000020dc0a0000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f4000000000000000000000000000000fedb0a000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a000000000000000000000000000000117105000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00117a000000000000000000000000000000877005000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00117a000000000000000000000000000000437105000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00117a0000000000000000000000000000002c7105000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00117a000000000000000000000000000000d86d05000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a0000000000000000000000000000004f6d05000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a0000000000000000000000000000000a6e050000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a000000000000000000000000000000f46d05000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d00000000000000000000000000000077b802000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00083d00000000000000000000000000000032b802000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00083d0000000000000000000000000000009cb802000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00083d0000000000000000000000000000008bb802000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00083d000000000000000000000000000000dbb602000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d00000000000000000000000000000096b602000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d000000000000000000000000000000ffb6020000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d000000000000000000000000000000eeb602000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e000000000000000000000000000000355c01000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00841e000000000000000000000000000000185c01000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00841e0000000000000000000000000000004d5c01000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00841e000000000000000000000000000000455c01000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00841e000000000000000000000000000000675b01000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e0000000000000000000000000000004b5b01000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e0000000000000000000000000000007f5b010000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e000000000000000000000000000000775b01000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f00000000000000000000000000000015ae00000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00420f0000000000000000000000000000000cae00000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00420f00000000000000000000000000000026ae00000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00420f0000000000000000000000000000001cae00000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00420f000000000000000000000000000000aead00000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f000000000000000000000000000000a5ad00000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f000000000000000000000000000000bfad000000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f000000000000000000000000000000b5ad00000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a107000000000000000000000000000000ff5600000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00a107000000000000000000000000000000fa5600000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00a107000000000000000000000000000000135700000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00a107000000000000000000000000000000085700000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00a107000000000000000000000000000000cb5600000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a107000000000000000000000000000000c75600000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a107000000000000000000000000000000df56000000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a107000000000000000000000000000000d55600000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001040a00000010270000000000000000000000000000040a0000001027000000000000000000000000000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000000a877716b728d0d000000000000000020e66109cb6076dc0c00000000000000080269000000de000000150000000264000000150000006400000001de000000640000000000a877716b728d0d0000000000000000fd9c18236d5af9db0c00000000000000080269000000de000000170000000264000000170000006400000001de000000640000000000a877716b728d0d0000000000000000c12808a0524fdbdc0c000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de000000640000000000a877716b728d0d000000000000000093e29efcd5b49fdb0c0000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de00000064000000007881514435b9c6060000000000000000dfde0f770eea1b740600000000000000080269000000de000000150000000264000000150000006400000001de00000064000000007881514435b9c6060000000000000000d0aadee78876ea730600000000000000080269000000de000000170000000264000000170000006400000001de00000064000000007881514435b9c60600000000000000002ba0999af33f4d7406000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de00000064000000007881514435b9c6060000000000000000998116960e62af73060000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de00000064000000007897f3679a5c63030000000000000000436449ec7abef53c0300000000000000080269000000de000000150000000264000000150000006400000001de00000064000000007897f3679a5c63030000000000000000ca9b0d90b0dced3c0300000000000000080269000000de000000170000000264000000170000006400000001de00000064000000007897f3679a5c63030000000000000000ac356ca30e0c0e3d03000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de00000064000000007897f3679a5c63030000000000000000a417b35e24f9be3c030000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000078a2c4f94caeb1010000000000000000bc0eef55a53eda9f0100000000000000080269000000de000000150000000264000000150000006400000001de000000640000000078a2c4f94caeb101000000000000000079c0c7dc07f7e19f0100000000000000080269000000de000000170000000264000000170000006400000001de000000640000000078a2c4f94caeb101000000000000000074421f065a44e69f01000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de000000640000000078a2c4f94caeb1010000000000000000e6a2b9995f9bbe9f010000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000f827ad4226d7d8000000000000000000b49cbdc4641079d00000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000f827ad4226d7d8000000000000000000f8c7b81247b482d00000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000f827ad4226d7d80000000000000000004ec5c31b59067fd000000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000f827ad4226d7d80000000000000000001489045e0d206bd0000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000b86a21e7926b6c000000000000000000c2579672c6856b680000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000b86a21e7926b6c0000000000000000007408b096168d72680000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000b86a21e7926b6c0000000000000000001a78fd5f247d6e6800000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000b86a21e7926b6c000000000000000000ea8f506603826468000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000188c5b39c935360000000000000000007c6f4c603cad43340000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000188c5b39c9353600000000000000000093a88e1ec9e747340000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000188c5b39c935360000000000000000001ba6eeaf7e28453400000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000188c5b39c9353600000000000000000065a3112011284034000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000c89c7862e41a1b000000000000000000626e607e00a7251a0000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000c89c7862e41a1b000000000000000000f61cce7862f7271a0000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000c89c7862e41a1b000000000000000000f5e7cc126364261a00000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000c89c7862e41a1b000000000000000000c58fe7cf79e3231a000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000202507f7718d0d000000000000000000dfe8065cfdd3130d0000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000202507f7718d0d000000000000000000ebc64e84fc09150d0000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000202507f7718d0d0000000000000000007ec8845a2d32140d00000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000202507f7718d0d000000000000000000c15c3c4718f1120d000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000008401987b8c606000000000000000000672cb7bc072c8a060000000000000000080269000000de000000150000000264000000150000006400000001de000000640000000008401987b8c6060000000000000000004f65469c29ca8a060000000000000000080269000000de000000170000000264000000170000006400000001de000000640000000008401987b8c606000000000000000000cbdf4ac5aa5a8a0600000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de000000640000000008401987b8c6060000000000000000000f86122da3b98906000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000c07657095c6303000000000000000000c4d828d80e2745030000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000c07657095c630300000000000000000025c9643d797745030000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000c07657095c6303000000000000000000c469a1405f3e450300000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000c07657095c63030000000000000000007f29e1a167ed4403000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000d8684190adb101000000000000000000fd0985074497a2010000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000d8684190adb1010000000000000000005a98c76624c0a2010000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000d8684190adb1010000000000000000009d53d3f4eba2a20100000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000d8684190adb1010000000000000000006f6e0a068e79a201000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000288beb8dd6d800000000000000000000da983c23ae4cd1000000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000288beb8dd6d800000000000000000000995d41984b60d1000000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000288beb8dd6d800000000000000000000ad0ae7078252d10000000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000288beb8dd6d8000000000000000000005e7ed531623dd100000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000509cc00c6b6c00000000000000000000d9faac8a29a668000000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000509cc00c6b6c00000000000000000000659b02e3fbaf68000000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000509cc00c6b6c00000000000000000000063353cea2a8680000000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000509cc00c6b6c0000000000000000000040e00a81319d6800000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000a0fbf5113536000000000000000000008bbac63c445234000000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000a0fbf51135360000000000000000000056e58d2d2e5734000000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000a0fbf51135360000000000000000000032aa5488f153340000000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000a0fbf511353600000000000000000000ca0f1adee64c3400000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000048ab90141a1b000000000000000000000af1a6a6b5281a000000000000000000080269000000de000000150000000264000000150000006400000001de000000640000000048ab90141a1b0000000000000000000064372807ba2a1a000000000000000000080269000000de000000170000000264000000170000006400000001de000000640000000048ab90141a1b0000000000000000000057c65c4c8c291a0000000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de000000640000000048ab90141a1b00000000000000000000c19e8ea025251a00000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000602c13d08c0d00000000000000000000e25a168a7a130d000000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000602c13d08c0d00000000000000000000e4771160ed140d000000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000602c13d08c0d00000000000000000000026f178856140d0000000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000602c13d08c0d00000000000000000000fedbafdc93120d00000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000a8439ff3c50600000000000000000000f09405876b8806000000000000000000080269000000de000000150000000264000000150000006400000001de0000006400000000a8439ff3c506000000000000000000000d35a578958906000000000000000000080269000000de000000170000000264000000170000006400000001de0000006400000000a8439ff3c5060000000000000000000095bd73314a89060000000000000000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000001de0000006400000000a8439ff3c5060000000000000000000024fbe78587870600000000000000000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000000de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000090789abf62030000000000000000000101de000000640000000090789abf62030000000000000000000101de000000640000000090789abf62030000000000000000000101de000000640000000090789abf62030000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000c0e962ebb0010000000000000000000101de0000006400000000c0e962ebb0010000000000000000000101de0000006400000000c0e962ebb0010000000000000000000101de0000006400000000c0e962ebb0010000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000058224701d8000000000000000000000101de000000640000000058224701d8000000000000000000000101de000000640000000058224701d8000000000000000000000101de000000640000000058224701d8000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000e8676ec66b000000000000000000000101de0000006400000000e8676ec66b000000000000000000000101de0000006400000000e8676ec66b000000000000000000000101de0000006400000000e8676ec66b000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000b00a02a935000000000000000000000101de0000006400000000b00a02a935000000000000000000000101de0000006400000000b00a02a935000000000000000000000101de0000006400000000b00a02a935000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000d0b216601a000000000000000000000101de0000006400000000d0b216601a000000000000000000000101de0000006400000000d0b216601a000000000000000000000101de0000006400000000d0b216601a000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000e006a1bb0c000000000000000000000101de0000006400000000e006a1bb0c000000000000000000000101de0000006400000000e006a1bb0c000000000000000000000101de0000006400000000e006a1bb0c000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de0000006400000000e83066e905000000000000000000000101de0000006400000000e83066e905000000000000000000000101de0000006400000000e83066e905000000000000000000000101de0000006400000000e83066e905000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000030ef7dba02000000000000000000000101de000000640000000030ef7dba02000000000000000000000101de000000640000000030ef7dba02000000000000000000000101de000000640000000030ef7dba02000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000010a5d4e800000000000000000000000101de000000640000000010a5d4e800000000000000000000000101de000000640000000010a5d4e800000000000000000000000101de000000640000000010a5d4e800000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000088526a7400000000000000000000000101de000000640000000088526a7400000000000000000000000101de000000640000000088526a7400000000000000000000000101de000000640000000088526a7400000000000000000000000100de000000640000000010080269000000de0000001500000002640000001500000064000000080269000000de00000017000000026400000017000000640000000c026f000000de000000ea03000004ea0300000a00000002640000000a0000006400000010026e000000de000000eb03000004eb030000160000000266000000160000000a00000002640000000a0000006400000001de000000640000000088526a7400000000000000000000000101de000000640000000088526a7400000000000000000000000101de000000640000000088526a7400000000000000000000000101de000000640000000088526a7400000000000000000000000100de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000009814440dab210800000000000000005ad5aa650000000000000000000000000403de00000030450f0001de00000030450f0000009814440dab21080000000000000000246987650000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000048e7305c86d510040000000000000000b955e0320000000000000000000000000403de00000030450f0001de00000030450f000048e7305c86d510040000000000000000ff68c8320000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000048c12b0bc36a080200000000000000007ee672190000000000000000000000000403de00000030450f0001de00000030450f000048c12b0bc36a08020000000000000000c26165190000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000482ea9626135040100000000000000004222ba0c0000000000000000000000000403de00000030450f0001de00000030450f0000482ea96261350401000000000000000035fcb20c0000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000c8e4678eb01a82000000000000000000e33c5d060000000000000000000000000403de00000030450f0001de00000030450f0000c8e4678eb01a82000000000000000000ef9059060000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000008404724580d4100000000000000000061a92e030000000000000000000000000403de00000030450f0001de00000030450f000008404724580d410000000000000000002bcd2c030000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000a8ed36efab86200000000000000000006a5797010000000000000000000000000403de00000030450f0001de00000030450f0000a8ed36efab8620000000000000000000c16796010000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000078c4aed455431000000000000000000062accb000000000000000000000000000403de00000030450f0001de00000030450f000078c4aed45543100000000000000000002a34cb000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000e0af6ac7aa21080000000000000000005bd665000000000000000000000000000403de00000030450f0001de00000030450f0000e0af6ac7aa2108000000000000000000269a65000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000038f3db1dd5100400000000000000000034eb32000000000000000000000000000403de00000030450f0001de00000030450f000038f3db1dd5100400000000000000000014cd32000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f00004047016c6a08020000000000000000009b7519000000000000000000000000000403de00000030450f0001de00000030450f00004047016c6a0802000000000000000000896619000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000e83e27f0340401000000000000000000caba0c000000000000000000000000000403de00000030450f0001de00000030450f0000e83e27f034040100000000000000000041b30c000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000018ed26551a8200000000000000000000635d06000000000000000000000000000403de00000030450f0001de00000030450f000018ed26551a82000000000000000000009f5906000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000030c4a6070d4100000000000000000000b02e03000000000000000000000000000403de00000030450f0001de00000030450f000030c4a6070d4100000000000000000000cd2c03000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000060fdf93d862000000000000000000000549701000000000000000000000000000403de00000030450f0001de00000030450f000060fdf93d862000000000000000000000639601000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000f89923d9421000000000000000000000a6cb00000000000000000000000000000403de00000030450f0001de00000030450f0000f89923d94210000000000000000000002ecb00000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000a01aa549210800000000000000000000d16500000000000000000000000000000403de00000030450f0001de00000030450f0000a01aa549210800000000000000000000956500000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f00009828f95e100400000000000000000000e53200000000000000000000000000000403de00000030450f0001de00000030450f00009828f95e100400000000000000000000c73200000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000f0e18f0c080200000000000000000000701900000000000000000000000000000403de00000030450f0001de00000030450f0000f0e18f0c080200000000000000000000611900000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000408c6ec0030100000000000000000000b40c00000000000000000000000000000403de00000030450f0001de00000030450f0000408c6ec0030100000000000000000000ad0c00000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000068e15d9a810000000000000000000000560600000000000000000000000000000403de00000030450f0001de00000030450f000068e15d9a810000000000000000000000530600000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000583e42aa400000000000000000000000280300000000000000000000000000000403de00000030450f0001de00000030450f0000583e42aa400000000000000000000000270300000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000d06c3432200000000000000000000000920100000000000000000000000000000403de00000030450f0001de00000030450f0000d06c3432200000000000000000000000920100000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000b0d140d30f0000000000000000000000c50000000000000000000000000000000403de00000030450f0001de00000030450f0000b0d140d30f0000000000000000000000c50000000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f00002004c7a30700000000000000000000005e0000000000000000000000000000000403de00000030450f0001de00000030450f00002004c7a30700000000000000000000005f0000000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000581d0a8c0300000000000000000000002b0000000000000000000000000000000403de00000030450f0001de00000030450f0000581d0a8c0300000000000000000000002c0000000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000505c18a3010000000000000000000000130000000000000000000000000000000403de00000030450f0001de00000030450f0000505c18a3010000000000000000000000140000000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f000070c9b28b000000000000000000000000050000000000000000000000000000000403de00000030450f0001de00000030450f000070c9b28b000000000000000000000000060000000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000b864d945000000000000000000000000020000000000000000000000000000000403de00000030450f0001de00000030450f0000b864d945000000000000000000000000030000000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de00000030450f0000080403de00000030450f001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0001de00000030450f0000b864d945000000000000000000000000020000000000000000000000000000000403de00000030450f0001de00000030450f0000b864d945000000000000000000000000030000000000000000000000000000001003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000004c31b8d9a798000000000000000000834b4b7f8680000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000a458f9d6ec534c000000000000000000915c2f734340000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000a405ed68f629260000000000000000007a1379c62120000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000024dce631fb1413000000000000000000114773e61010000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000064c763967d8a090000000000000000006ab505740808000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000043da2c83ec504000000000000000000a440343a0404000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000d477c1619f62020000000000000000003adc241d0202000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00003c1551ae4f3101000000000000000000fb7e930e0101000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000f0e398d4a79800000000000000000000d66548878000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00009c242de5534c00000000000000000000e616a0434000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000a0eb06f02926000000000000000000008cf0cd212000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00007428e4f214130000000000000000000099abe2101000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00008c6de2768a0900000000000000000000072f6f080800000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00001890e138c50400000000000000000000a96f35040400000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000b07a5197620200000000000000000000df6716020200000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000fc6f894631010000000000000000000095e306010100000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00005011b5a0980000000000000000000000e74981800000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00004c3b3b4b4c0000000000000000000000fc543c400000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000f8f60d2326000000000000000000000069021c200000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000020ae670c1300000000000000000000003cb109100000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000b4891481090000000000000000000000758800080000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00002c9efabd040000000000000000000000251cfe030000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000068a86d5c020000000000000000000000cce5fc010000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000d88617290100000000000000000000001f23fa000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000010766c8f00000000000000000000000066c178000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000aced9642000000000000000000000000891038000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000028d0bb1e0000000000000000000000002fe019000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000b89a3e0a00000000000000000000000052a008000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00005c4d1f05000000000000000000000000285004000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00005c4d1f05000000000000000000000000285004000000000000000000000000000403de0000003d450f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f006b89420700000000000000000000000000198ab4ca90cb89010000000000000000040330450f00b0440f000130450f00b0440f006b8942070000000000000000000000000049d2bd53dd818a010000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00b544a10300000000000000000000000000f87197470aeac4000000000000000000040330450f00b0440f000130450f00b0440f00b544a103000000000000000000000000001203d1d07c43c5000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f005aa2d00100000000000000000000000000924f2b8f157662000000000000000000040330450f00b0440f000130450f00b0440f005aa2d00100000000000000000000000000dc4d48d161a262000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f002d51e8000000000000000000000000000089d61aea4e3b31000000000000000000040330450f00b0440f000130450f00b0440f002d51e80000000000000000000000000000330b36e5595131000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f009628740000000000000000000000000000334caa62b89d18000000000000000000040330450f00b0440f000130450f00b0440f009628740000000000000000000000000000d3f3a14db7a818000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f004b143a0000000000000000000000000000ff398873e04e0c000000000000000000040330450f00b0440f000130450f00b0440f004b143a00000000000000000000000000007325be725e540c000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00250a1d00000000000000000000000000008b10312f712706000000000000000000040330450f00b0440f000130450f00b0440f00250a1d0000000000000000000000000000849fb7c12f2a06000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0012850e000000000000000000000000000017339dc0b81303000000000000000000040330450f00b0440f000130450f00b0440f0012850e000000000000000000000000000029293211181503000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f008942070000000000000000000000000000e81d5871dc8901000000000000000000040330450f00b0440f000130450f00b0440f0089420700000000000000000000000000006b75a1508c8a01000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0044a1030000000000000000000000000000000bcd21eec400000000000000000000040330450f00b0440f000130450f00b0440f0044a1030000000000000000000000000000b928553246c500000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00a2d00100000000000000000000000000001b43f911776200000000000000000000040330450f00b0440f000130450f00b0440f00a2d00100000000000000000000000000003724cf19a36200000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0050e800000000000000000000000000000081e7ff523b3100000000000000000000040330450f00b0440f000130450f00b0440f0050e8000000000000000000000000000000e7e28494513100000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f002874000000000000000000000000000000a05091a99d1800000000000000000000040330450f00b0440f000130450f00b0440f002874000000000000000000000000000000b2af4dcaa81800000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00143a000000000000000000000000000000cf89ccd44e0c00000000000000000000040330450f00b0440f000130450f00b0440f00143a0000000000000000000000000000003168cc87540c00000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00091d000000000000000000000000000000205c2734270600000000000000000000040330450f00b0440f000130450f00b0440f00091d000000000000000000000000000000a38b2b302a0600000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00840e000000000000000000000000000000aa8ff37e130300000000000000000000040330450f00b0440f000130450f00b0440f00840e000000000000000000000000000000f2351518150300000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00420700000000000000000000000000000017cb79bf890100000000000000000000040330450f00b0440f000130450f00b0440f0042070000000000000000000000000000003e9e0a8c8a0100000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00a0030000000000000000000000000000010130450f00b0440f00a0030000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00d0010000000000000000000000000000010130450f00b0440f00d0010000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00e7000000000000000000000000000000010130450f00b0440f00e7000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0073000000000000000000000000000000010130450f00b0440f0073000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0039000000000000000000000000000000010130450f00b0440f0039000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f001c000000000000000000000000000000010130450f00b0440f001c000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f000e000000000000000000000000000000010130450f00b0440f000e000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0006000000000000000000000000000000010130450f00b0440f0006000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0003000000000000000000000000000000010130450f00b0440f0003000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0001000000000000000000000000000000010130450f00b0440f0001000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0001000000000000000000000000000000010130450f00b0440f0001000000000000000000000000000000010430450f0003b50000000000000000000000000000040a00000010270000000000000000000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000092992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000092992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000092992a1800000000000000000000000000a27a9cb6f593be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000092992a18000000000000000000000000000fff0aa27790be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000004de000000000082dfe40d4700000000000000000000de0000000000000000040403de0000000000000001de000000000000003999227565c627835400000000000000008ddc9d7bba17a60600000000000000000403de00000000000000040a00000010270000000000000000000000000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a40fba0000000000000000000000000000ab9b217fb05658a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a40fba0000000000000000000000000000cc6e96c6d84a3ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a40fba000000000000000000000000000081b494ff3c5092a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a40fba00000000000000000000000000006dbedea98e4f91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de00000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e8000000000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004de000000000082dfe40d47000000000000000000040a00000010270000000000000000000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000092992a180000000000000000000000000033a0858305593e0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000092992a1800000000000000000000000000d458a819e2093d0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000092992a1800000000000000000000000000a27a9cb6f593be0100000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000092992a18000000000000000000000000000fff0aa27790be0100000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000004de000000000082dfe40d4700000000000000000000de0000000000000000040403de0000000000000001de00000000000000de993fa457c48af130000000000000000028fbcbe18733dc0300000000000000000403de00000000000000040a00000010270000000000000000000000000000000a000000de00000000100802640000000a00000015000000026900000015000000de0000000802640000000a00000017000000026900000017000000de00000008040a000000ea030000026f000000ea030000de0000000c02660000000a000000160000000416000000eb030000026e000000eb030000de000000010a000000de000000a40fba0000000000000000000000000000ab9b217fb05658a800000000000000000802640000000a00000015000000026900000015000000de000000010a000000de000000a40fba0000000000000000000000000000cc6e96c6d84a3ea800000000000000000802640000000a00000017000000026900000017000000de000000010a000000de000000a40fba000000000000000000000000000081b494ff3c5092a9000000000000000008040a000000ea030000026f000000ea030000de000000010a000000de000000a40fba00000000000000000000000000006dbedea98e4f91a900000000000000000c02660000000a000000160000000416000000eb030000026e000000eb030000de00000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004de000000000082dfe40d47000000000000000000 diff --git a/ice/ice-solver/src/tests/fixtures/funds_unavailable.hex b/ice/ice-solver/src/tests/fixtures/funds_unavailable.hex new file mode 100644 index 0000000000..47add54a2e --- /dev/null +++ b/ice/ice-solver/src/tests/fixtures/funds_unavailable.hex @@ -0,0 +1,3 @@ +945500000000000000e02c146d9d01000000de000000000000000000e8890423c78a00000000000000000020ed36c9e912000000000000000000005300000000000000a09c116d9d01000000de00000000000000000040bd8b5b936b6c0000000000000000801ee15889110f0000000000000000004f0000000000000050680e6d9d010000000a0000000000000000ca9a3b00000000000000000000000000603e9b3ed871060000000000000000004e0000000000000070390e6d9d010000000a0000000000000000ca9a3b00000000000000000000000000603e9b3ed871060000000000000000004c00000000000000900a0e6d9d010000000a0000000000000000ca9a3b00000000000000000000000000603e9b3ed871060000000000000000004b00000000000000900a0e6d9d0100000000000000de00000000588a09e04c0500000000000000000000409ac9b02bf827000000000000000000460000000000000020050b6d9d0100000000000000de0000000080c6a47e8d0300000000000000000000c0bb34ec2d191c000000000000000000450000000000000060a70a6d9d0100000000000000de0000000000f4448291634500000000000000000000741f4b20ab2d260200000000000000440000000000000060a70a6d9d010000000a0000000000000000e40b54020000000000000000000000008045483a12dd45000000000000000000430000000000000060a70a6d9d0100000000000000de0000000000c16ff2862300000000000000000000006c6c558fad07010000000000000000420000000000000060a70a6d9d0100000000000000de00000000004f8c34e814020000000000000000000060de6c5a866d100000000000000000410000000000000060a70a6d9d0100000000000000de000000000064a7b3b6e00d000000000000000000008c035fb409d0780000000000000000400000000000000060a70a6d9d0100000000000000de00000000004f8c34e814020000000000000000000087370f04a3d41000000000000000003e00000000000000c01a0a6d9d0100000000000000de00000000588a09e04c0500000000000000000000c08fbddb27e3290000000000000000003d00000000000000c01a0a6d9d010000000a0000000000000000e40b5402000000000000000000000000c0ef04935828410000000000000000003c0000000000000000bd096d9d010000000a0000000000000000ca9a3b000000000000000000000000000073aed81fcb060000000000000000003b00000000000000b076096d9d0100000000000000de000000000064a7b3b6e00d0000000000000000000016c1fe73159d6200000000000000003a00000000000000b076096d9d0100000000000000de0000000080c6a47e8d030000000000000000000040efe5df5cf61b0000000000000000003800000000000000a0d2086d9d0100000000000000de000000000064a7b3b6e00d00000000000000000000c4daad0bd7366c00000000000000003700000000000000a0d2086d9d0100000000000000de00000000004f8c34e8140200000000000000000000a9ebba9f025d1000000000000000003600000000000000e074086d9d0100000000000000de000000000014bbf08ac6020000000000000000000063a4ae219ece1500000000000000003500000000000000e074086d9d0100000000000000de0000000000c16ff286230000000000000000000000925640139c1701000000000000000034000000000000000046086d9d0100000000000000de0000000060077a30284b020000000000000000000014bdb0a74d0d1200000000000000003300000000000000808a076d9d0100000000000000de0000000060315110e5620100000000000000000000064df380b6e50a0000000000000000310000000000000070e6066d9d0100000000000000de0000000000e8890423c78a0000000000000000000090593bae2e8ad90300000000000000300000000000000090b7066d9d01000000de000000000000000000c84e676dc11b000000000000000000d8c96c5ae803000000000000000000002e000000000000008013066d9d0100000000000000de0000000000e8890423c78a0000000000000000000018c2057fbf4a1004000000000000002d000000000000008013066d9d0100000000000000de00000000002cf61a24a2290000000000000000000066dc9c94dafc5801000000000000002c000000000000009040056d9d0100000000000000de0000000060315110e5620100000000000000000000b828cb6fde080c00000000000000002a0000000000000060cb046d9d0100000000000000de0000000000c16ff286230000000000000000000000f889ea5b1b1b01000000000000000028000000000000005027046d9d0100000000000000de0000000060315110e5620100000000000000000000b828cb6fde080c0000000000000000270000000000000000e1036d9d0100000000000000de0000000000e8890423c78a0000000000000000000018bd25b76d30e30300000000000000250000000000000000e1036d9d0100000000000000de0000000000f4448291634500000000000000000000f8eac0f03bd4080200000000000000220000000000000020b2036d9d0100000000000000de0000000000c16ff286230000000000000000000000f889ea5b1b1b01000000000000000021000000000000006054036d9d0100000000000000de000000000064a7b3b6e00d000000000000000000007020852b03f87200000000000000001f00000000000000c0c7026d9d0100000000000000de00000000008a5d78456301000000000000000000007d57871290090c0000000000000000180000000000000060e3836c9d01000000000000000e00000000a0724e18090000000000000000000000c06e31d9100100000000000000000000 +144f0000000000000050680e6d9d010000000a0000000000000000ca9a3b0000000000000000000000000dc2c201044a31070000000000000000004e0000000000000070390e6d9d010000000a0000000000000000ca9a3b0000000000000000000000000dc2c201044a31070000000000000000004c00000000000000900a0e6d9d010000000a0000000000000000ca9a3b0000000000000000000000000dc2c201044a31070000000000000000003d00000000000000c01a0a6d9d010000000a0000000000000000e40b5402000000000000000000000082949b1128e4ec470000000000000000003c0000000000000000bd096d9d010000000a0000000000000000ca9a3b0000000000000000000000000dc2c201044a31070000000000000000000400000c7742030000000000000000000000c19ca618380cb26400000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000b6bc8893100b69090000000000000000 +000000006902000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000030a0000000000000000b9d9e85900b6436f7be50fe14c8be1fa73c01a98c3e0d1272354637507000000030a0000000000000000f25b0a190ccb81b15275e9c99fb5e3fa73c01a98c3e0d1272354637507000000030a00000000000000008024e7aca4c579179c21a93da2b3809056cc454d5f3c09e33fefaf4c04000000030a000000000000000024c2ed6cc1869a0a287a5961b5d89cfa73c01a98c3e0d1272354637507000000000e00000000000000000404030e00000000000000030e0000000000000000f8f8cae68004c25895ae97de744ad704c0f7d439907d789352a45bf27a2a4d0000de0000000000000000040403de0000000000000003de0000000000000000343638ceb6d6dec520de3afd7cda16005aa6945d3004c54d731e032c1a31a09e00de0000000000000000040403de0000000000000001de000000000000000000e8890423c78a00000000000000000063593663d9ab130000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000040bd8b5b936b6c0000000000000000fff22b349086370f00000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000f5587aa3db40e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b0000000000000000000000000084a5b67612beef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000d8a23e35de2ea90700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000006cf1caedbfb2a80700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000f5587aa3db40e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b0000000000000000000000000084a5b67612beef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000d8a23e35de2ea90700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000006cf1caedbfb2a80700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000f5587aa3db40e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b0000000000000000000000000084a5b67612beef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000d8a23e35de2ea90700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000006cf1caedbfb2a80700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de00000000588a09e04c0500000000000000000000d9d792df3b5ceb230000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000080c6a47e8d0300000000000000000000ed0fee76d68c13180000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000f444829163450000000000000000001408cc8a05c2dc0ac201000000000000040300000000de000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d9c6aa749c44e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000b71e60690ac2ef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000de4a3924e4fd4f4900000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000008c58d3ea73d7494900000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000438729c41893bef00000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000004f8c34e8140200000000000000000074174523692bae160e00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d000000000000000000c278b33e09d428385d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000004f8c34e8140200000000000000000074174523692bae160e00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000588a09e04c0500000000000000000000d9d792df3b5ceb230000000000000000040300000000de000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d9c6aa749c44e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000b71e60690ac2ef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000de4a3924e4fd4f4900000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000008c58d3ea73d7494900000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000f5587aa3db40e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b0000000000000000000000000084a5b67612beef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000d8a23e35de2ea90700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000006cf1caedbfb2a80700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d000000000000000000c278b33e09d428385d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000080c6a47e8d0300000000000000000000ed0fee76d68c13180000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d000000000000000000c278b33e09d428385d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000004f8c34e8140200000000000000000074174523692bae160e00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000014bbf08ac602000000000000000000fbf4c759c90bc6c61200000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000438729c41893bef00000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060077a30284b0200000000000000000073f498524a464d850f00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e56201000000000000000000d8cd15503d34f9620900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a000000000000000000bc2300ab421d15395e03000000000000040300000000de00000000de0000000000000000040403de0000000000000001de000000000000000000c84e676dc11b0000000000000000003db459c735ef030000000000000000000403de000000000000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a000000000000000000bc2300ab421d15395e03000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000002cf61a24a229000000000000000000696b114622b438c71201000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e56201000000000000000000d8cd15503d34f9620900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000438729c41893bef00000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e56201000000000000000000d8cd15503d34f9620900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a000000000000000000bc2300ab421d15395e03000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000f444829163450000000000000000001408cc8a05c2dc0ac201000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000438729c41893bef00000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d000000000000000000c278b33e09d428385d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000008a5d78456301000000000000000000bc719aef276385650900000000000000040300000000de00000000000000000e00000000040403000000000e00000001000000000e00000000a0724e1809000000000000000000000072fa33409000000000000000000000000403000000000e000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000f08296050000000000000000000000005b6ab699a444e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000f0829605000000000000000000000000077a46f112c2ef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000f0829605000000000000000000000000c63c9890069edaa400000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000f08296050000000000000000000000006db93e5bef0dc8a400000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000f095f7eb1b126d00000000000000006bd2d52b82aa4e0f00000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000a7ae373da144e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000a7a0dc730fc2ef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000c1fa670134a0b46400000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000000c7742030000000000000000000000001cc407a1ea4fab6400000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000002847907e5af66c0000000000000000a16e2eb13bcf4a0f00000000000000000403de00000000000000040a00000010270000000000000000000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000a7ae373da144e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000a7a0dc730fc2ef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000c1fa670134a0b46400000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000000c7742030000000000000000000000001cc407a1ea4fab6400000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000004de000000000082dfe40d4700000000000000000000de0000000000000000040403de0000000000000001de0000000000000000002847907e5af66c000000000000000067dddec3ad6ad50d00000000000000000403de0000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a00000010270000000000000000000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000a7ae373da144e40000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000a7a0dc730fc2ef0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000000c774203000000000000000000000000c1fa670134a0b46400000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000000c7742030000000000000000000000001cc407a1ea4fab6400000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e80000000000000000000000 diff --git a/ice/ice-solver/src/tests/fixtures/intent_6127.hex b/ice/ice-solver/src/tests/fixtures/intent_6127.hex new file mode 100644 index 0000000000..22848e051e --- /dev/null +++ b/ice/ice-solver/src/tests/fixtures/intent_6127.hex @@ -0,0 +1,3 @@ +b88e000000000000003007266d9d010000000a0000000000000000e40b540200000000000000000000000040a7f6ba8664490000000000000000008d00000000000000e0c0256d9d010000000a0000000000000000e40b540200000000000000000000000080e54b670a98490000000000000000008b000000000000000092256d9d010000000a0000000000000000e40b540200000000000000000000000000b844011fda490000000000000000008a00000000000000d01c256d9d010000000a0000000000000000e40b54020000000000000000000000004091228a6ce44900000000000000000089000000000000006005256d9d010000000a0000000000000000e40b540200000000000000000000000080f74910d5f549000000000000000000880000000000000080d6246d9d01000000de0000000000000000002059dd64f00c0f0100000000000000807000ef3e1b25000000000000000000870000000000000080d6246d9d010000000a0000000000000000e40b540200000000000000000000000000b92e0499584a0000000000000000008600000000000000a0a7246d9d010000000a0000000000000000e40b5402000000000000000000000000c0862d739c634a00000000000000000085000000000000005061246d9d010000000a0000000000000000e40b54020000000000000000000000004014ec71f4a94a00000000000000000084000000000000007032246d9d010000000a0000000000000000e40b54020000000000000000000000004096cb5602f14a0000000000000000008300000000000000001b246d9d01000000de000000000000000000a0dec5adc935360000000000000000c070fdb43fd007000000000000000000810000000000000020ec236d9d010000000a0000000000000000e40b540200000000000000000000000040a8c9ffcc4e4b000000000000000000800000000000000020ec236d9d010000000a0000000000000000e40b540200000000000000000000000040c25cfa025d4b0000000000000000007f00000000000000a030236d9d010000000a0000000000000000e40b5402000000000000000000000000c0190a40c7f54b0000000000000000007e00000000000000c001236d9d010000000a0000000000000000e40b5402000000000000000000000000c0a193b2185c4c0000000000000000007c00000000000000c001236d9d010000000a0000000000000000e40b54020000000000000000000000008033d7da449a4c0000000000000000007a000000000000004046226d9d010000000a0000000000000000e40b5402000000000000000000000000c0bc10b0c8e84c000000000000000000750000000000000060a0206d9d0100000000000000de0000006273814f3b7c780400000000000000000000e9fcae3806dc1d00000000000000007200000000000000a042206d9d010000000a0000000000000000e40b5402000000000000000000000000c0db809d896f6a000000000000000000710000000000000000b61f6d9d010000000a0000000000000000e40b5402000000000000000000000000c0adeaa2663c5c0000000000000000006f0000000000000040581f6d9d01000000de000000000000000000a0dec5adc93536000000000000000020e3856d9066080000000000000000006e0000000000000040581f6d9d010000000a0000000000000000e40b540200000000000000000000000080f5a65b0fc3550000000000000000006d0000000000000060291f6d9d010000000a0000000000000000e40b540200000000000000000000000040a1c2b80ccc510000000000000000006c00000000000000c09c1e6d9d0100000000000000de0000000000c16ff2862300000000000000000000807b52485799130100000000000000006200000000000000a0ef186d9d01000000de000000000000000000a0dec5adc935360000000000000000e09f70364392080000000000000000005f000000000000004005186d9d01000000de00000000000000000010632d5ec76b050000000000000000f069cf2a3aee000000000000000000005e000000000000004005186d9d01000000de00000000000000000010632d5ec76b050000000000000000408eab993cf5000000000000000000005d000000000000001090176d9d01000000de00000000000000000010632d5ec76b050000000000000000b0eefe1bea00010000000000000000004b00000000000000900a0e6d9d0100000000000000de00000000588a09e04c0500000000000000000000409ac9b02bf827000000000000000000460000000000000020050b6d9d0100000000000000de0000000080c6a47e8d0300000000000000000000c0bb34ec2d191c000000000000000000450000000000000060a70a6d9d0100000000000000de0000000000f4448291634500000000000000000000741f4b20ab2d260200000000000000430000000000000060a70a6d9d0100000000000000de0000000000c16ff2862300000000000000000000006c6c558fad07010000000000000000410000000000000060a70a6d9d0100000000000000de000000000064a7b3b6e00d000000000000000000008c035fb409d07800000000000000003e00000000000000c01a0a6d9d0100000000000000de00000000588a09e04c0500000000000000000000c08fbddb27e3290000000000000000003b00000000000000b076096d9d0100000000000000de000000000064a7b3b6e00d0000000000000000000016c1fe73159d6200000000000000003a00000000000000b076096d9d0100000000000000de0000000080c6a47e8d030000000000000000000040efe5df5cf61b0000000000000000003800000000000000a0d2086d9d0100000000000000de000000000064a7b3b6e00d00000000000000000000c4daad0bd7366c00000000000000003500000000000000e074086d9d0100000000000000de0000000000c16ff286230000000000000000000000925640139c1701000000000000000034000000000000000046086d9d0100000000000000de0000000060077a30284b020000000000000000000014bdb0a74d0d1200000000000000003300000000000000808a076d9d0100000000000000de0000000060315110e5620100000000000000000000064df380b6e50a00000000000000002c000000000000009040056d9d0100000000000000de0000000060315110e5620100000000000000000000b828cb6fde080c00000000000000002a0000000000000060cb046d9d0100000000000000de0000000000c16ff286230000000000000000000000f889ea5b1b1b01000000000000000028000000000000005027046d9d0100000000000000de0000000060315110e5620100000000000000000000b828cb6fde080c0000000000000000270000000000000000e1036d9d0100000000000000de0000000000e8890423c78a0000000000000000000018bd25b76d30e30300000000000000220000000000000020b2036d9d0100000000000000de0000000000c16ff286230000000000000000000000f889ea5b1b1b010000000000000000180000000000000060e3836c9d01000000000000000e00000000a0724e18090000000000000000000000c06e31d9100100000000000000000000 +08880000000000000080d6246d9d01000000de0000000000000000002059dd64f00c0f0100000000000048dbd0f45ca39625000000000000000000750000000000000060a0206d9d0100000000000000de0000006273814f3b7c780400000000000000007c246a8768d64de41e0000000000000000040084dbb5d1748ea228f000000000000000e6674fa521271e2100000000000000000403de00000000000000c47fe17e2702c3080100000000000000 +00000000b902000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000030a00000000000000004ecc2928aad3385afa5f16b52a4864fc186f496971fb7b8d957b497807000000030a00000000000000003c44f8636f242e28f972111ed47566fc186f496971fb7b8d957b497807000000030a0000000000000000aca01c3e3417bad63269007e9eb75191717fe8434cb8a5a8dcc15b4e04000000030a00000000000000003da07143313381714d35eff79c2b1ffc186f496971fb7b8d957b497807000000000e00000000000000000404030e00000000000000030e00000000000000007cbabe3c36e0a6d85182aef95505df044076e4eb726df7172d998f67a0314d0000de0000000000000000040403de0000000000000003de000000000000000008a7bb10b5d0660b0d750ea0b7fd160065e0b81833487a7284b2bf5ff9d8dd9e000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000002059dd64f00c0f010000000000000025721c2f9b15472500000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000a0dec5adc93536000000000000000007cb66b229889a0700000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de0000006273814f3b7c7804000000000000000000b928e59096f06e661e00000000000000040300000000de000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000a0dec5adc93536000000000000000007cb66b229889a0700000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000728124682efee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000d395cff72e6bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000002758eb4e8271e24800000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000001e737dc5ecd6e24800000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000d6b4c42bb2a33ff20000000000000000040300000000de00000000de0000000000000000040403de0000000000000001de000000000000000000a0dec5adc93536000000000000000007cb66b229889a0700000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000010632d5ec76b05000000000000000016054b49ae86c30000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000010632d5ec76b05000000000000000016054b49ae86c30000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000010632d5ec76b05000000000000000016054b49ae86c30000000000000000000403de000000000000000000000000de0000000004040300000000de0000000100000000de00000000588a09e04c0500000000000000000000209ecf8ce5ce24240000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000080c6a47e8d03000000000000000000005a618506830e3a180000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000f444829163450000000000000000004f95c6119c2082f9c401000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000d6b4c42bb2a33ff20000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d000000000000000000b88e0f601e708bce5d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000588a09e04c0500000000000000000000209ecf8ce5ce24240000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d000000000000000000b88e0f601e708bce5d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000080c6a47e8d03000000000000000000005a618506830e3a180000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d000000000000000000b88e0f601e708bce5d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000d6b4c42bb2a33ff20000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060077a30284b020000000000000000001e8599266c13299e0f00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e56201000000000000000000ec04d0fdc8b6ff710900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e56201000000000000000000ec04d0fdc8b6ff710900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000d6b4c42bb2a33ff20000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e56201000000000000000000ec04d0fdc8b6ff710900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a00000000000000000017b0cff04d5c9a106403000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000d6b4c42bb2a33ff20000000000000000040300000000de00000000000000000e00000000040403000000000e00000001000000000e00000000a0724e1809000000000000000000000057d22db28f00000000000000000000000403000000000e000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a00000000000000005cb2ec220000000000000000000000004a52632d38fee20000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a00000000000000005cb2ec22000000000000000000000000c5c12d40396bee0000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a00000000000000005cb2ec220000000000000000000000008f8f7e92662da7ff02000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a00000000000000005cb2ec220000000000000000000000008754381361cdc5fe02000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de0000000000000084db55b03a3c6c5e2601000000000000007ad1790a0c62662800000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000084dbb5d1748ea228f0000000000000000047e806a031001f2100000000000000000403de0000000000000004de000000000082dfe40d4700000000000000000000de0000000000000000040403de0000000000000001de0000000000000084dbb5d1748ea228f0000000000000000047e806a031001f2100000000000000000403de0000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d47000000000000000000 diff --git a/ice/ice-solver/src/tests/fixtures/trading_limit.hex b/ice/ice-solver/src/tests/fixtures/trading_limit.hex new file mode 100644 index 0000000000..cf15e3829c --- /dev/null +++ b/ice/ice-solver/src/tests/fixtures/trading_limit.hex @@ -0,0 +1,3 @@ +98540000000000000020cf136d9d01000000000000000500000000b08c11a0b218000000000000000000c09c2a48170000000000000000000000005300000000000000a09c116d9d01000000de00000000000000000040bd8b5b936b6c0000000000000000801ee15889110f0000000000000000004f0000000000000050680e6d9d010000000a0000000000000000ca9a3b00000000000000000000000000603e9b3ed871060000000000000000004e0000000000000070390e6d9d010000000a0000000000000000ca9a3b00000000000000000000000000603e9b3ed871060000000000000000004c00000000000000900a0e6d9d010000000a0000000000000000ca9a3b00000000000000000000000000603e9b3ed871060000000000000000004b00000000000000900a0e6d9d0100000000000000de00000000588a09e04c0500000000000000000000409ac9b02bf8270000000000000000004700000000000000601e0c6d9d010000000a0000000000000080cf882c0500000000000000000000000080f53c2461c38b000000000000000000460000000000000020050b6d9d0100000000000000de0000000080c6a47e8d0300000000000000000000c0bb34ec2d191c000000000000000000450000000000000060a70a6d9d0100000000000000de0000000000f4448291634500000000000000000000741f4b20ab2d260200000000000000440000000000000060a70a6d9d010000000a0000000000000000e40b54020000000000000000000000008045483a12dd45000000000000000000430000000000000060a70a6d9d0100000000000000de0000000000c16ff2862300000000000000000000006c6c558fad07010000000000000000420000000000000060a70a6d9d0100000000000000de00000000004f8c34e814020000000000000000000060de6c5a866d100000000000000000410000000000000060a70a6d9d0100000000000000de000000000064a7b3b6e00d000000000000000000008c035fb409d0780000000000000000400000000000000060a70a6d9d0100000000000000de00000000004f8c34e814020000000000000000000087370f04a3d41000000000000000003e00000000000000c01a0a6d9d0100000000000000de00000000588a09e04c0500000000000000000000c08fbddb27e3290000000000000000003d00000000000000c01a0a6d9d010000000a0000000000000000e40b5402000000000000000000000000c0ef04935828410000000000000000003c0000000000000000bd096d9d010000000a0000000000000000ca9a3b000000000000000000000000000073aed81fcb060000000000000000003b00000000000000b076096d9d0100000000000000de000000000064a7b3b6e00d0000000000000000000016c1fe73159d6200000000000000003a00000000000000b076096d9d0100000000000000de0000000080c6a47e8d030000000000000000000040efe5df5cf61b0000000000000000003800000000000000a0d2086d9d0100000000000000de000000000064a7b3b6e00d00000000000000000000c4daad0bd7366c00000000000000003700000000000000a0d2086d9d0100000000000000de00000000004f8c34e8140200000000000000000000a9ebba9f025d1000000000000000003600000000000000e074086d9d0100000000000000de000000000014bbf08ac6020000000000000000000063a4ae219ece1500000000000000003500000000000000e074086d9d0100000000000000de0000000000c16ff286230000000000000000000000925640139c1701000000000000000034000000000000000046086d9d0100000000000000de0000000060077a30284b020000000000000000000014bdb0a74d0d1200000000000000003300000000000000808a076d9d0100000000000000de0000000060315110e5620100000000000000000000064df380b6e50a0000000000000000310000000000000070e6066d9d0100000000000000de0000000000e8890423c78a0000000000000000000090593bae2e8ad90300000000000000300000000000000090b7066d9d01000000de000000000000000000c84e676dc11b000000000000000000d8c96c5ae803000000000000000000002e000000000000008013066d9d0100000000000000de0000000000e8890423c78a0000000000000000000018c2057fbf4a1004000000000000002d000000000000008013066d9d0100000000000000de00000000002cf61a24a2290000000000000000000066dc9c94dafc5801000000000000002c000000000000009040056d9d0100000000000000de0000000060315110e5620100000000000000000000b828cb6fde080c00000000000000002a0000000000000060cb046d9d0100000000000000de0000000000c16ff286230000000000000000000000f889ea5b1b1b01000000000000000028000000000000005027046d9d0100000000000000de0000000060315110e5620100000000000000000000b828cb6fde080c0000000000000000270000000000000000e1036d9d0100000000000000de0000000000e8890423c78a0000000000000000000018bd25b76d30e30300000000000000250000000000000000e1036d9d0100000000000000de0000000000f4448291634500000000000000000000f8eac0f03bd4080200000000000000220000000000000020b2036d9d0100000000000000de0000000000c16ff286230000000000000000000000f889ea5b1b1b01000000000000000021000000000000006054036d9d0100000000000000de000000000064a7b3b6e00d000000000000000000007020852b03f87200000000000000001f00000000000000c0c7026d9d0100000000000000de00000000008a5d78456301000000000000000000007d57871290090c0000000000000000180000000000000060e3836c9d01000000000000000e00000000a0724e18090000000000000000000000c06e31d9100100000000000000000000 +044700000000000000601e0c6d9d010000000a0000000000000080cf882c05000000000000000000000099b9a6ebe25cf69d000000000000000000040080cf882c05000000000000000000000071a0d910aa51f29d00000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000009939b1aebefb32120000000000000000 +0000000045020005000000000000000008080405000000e903000003e9030000000000000c0405000000e903000002b2020000e90300000f000000030f0000000000000003050000000000000000c49164bf32d63bf70d4e70dc26518e4f94e0e79206b614eaa1503facc54a000003050000000000000000b02b775d1f710af2ffa2479fd9feeee11b264a726efd74d3fcb396aae0d40000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000030a00000000000000002458471aa97681a2157781e3d79d22fc9178c3f0c361128676dab26407000000030a0000000000000000bc2544037541771ae773ed35f0ca24fc9178c3f0c361128676dab26407000000030a000000000000000083ff0bc7fd693d228fd1d39461034091adec6980a77b2b012f05114304000000030a0000000000000000042e6a33330f9b745181dfb05693ddfb9178c3f0c361128676dab26407000000000e00000000000000000404030e00000000000000030e0000000000000000b07ae85ebb912189da98fdcf6b5ada04806da960cd69a3edf874416fecb44c0000de0000000000000000040403de0000000000000003de0000000000000000a0e9b139139bfab259ea2853bcf716002b7dad123141084b1584de199a3f3d9d0000000000050000000008080300000000e903000004e9030000050000000c03000000000f00000002b20200000f000000e903000004e90300000500000001000000000500000000b08c11a0b2180000000000000000000071f57218170000000000000000000000080300000000e903000004e90300000500000001000000000500000000b08c11a0b218000000000000000000003e7d4e1e1700000000000000000000000c03000000000f00000002b20200000f000000e903000004e90300000500000000de0000000000000000040403de0000000000000001de00000000000000000040bd8b5b936b6c000000000000000037ef6de01bb3a10f00000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000001764a187aa7eea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000004f3822ec494cf60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000652c411a5512df0700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000db166a1ed73ade0700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000001764a187aa7eea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000004f3822ec494cf60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000652c411a5512df0700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000db166a1ed73ade0700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000001764a187aa7eea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000004f3822ec494cf60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000652c411a5512df0700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000db166a1ed73ade0700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de00000000588a09e04c0500000000000000000000b2ae9371913537230000000000000000040300000000de000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000fec35eb78d82ea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000b3bb9d246650f60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000080cf882c0500000000000000000000000099b9a6ebe25cf69d00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000d594fa368104df9d00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de0000000080c6a47e8d03000000000000000000009662b973f6cb9a170000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000f4448291634500000000000000000049b9efe4e54d4252b901000000000000040300000000de000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000005ccc529d8582ea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000e60e00a55d50f60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000003f2c2176d2dd4b4b00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b540200000000000000000000000044792478856e424b00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000957db671502907ec0000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000004f8c34e81402000000000000000000b99fec9852290bd00d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d0000000000000000005c4fb28490e3aa655b00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000004f8c34e81402000000000000000000b99fec9852290bd00d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000588a09e04c0500000000000000000000b2ae9371913537230000000000000000040300000000de000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000005ccc529d8582ea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000e40b5402000000000000000000000000e60e00a55d50f60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000e40b54020000000000000000000000003f2c2176d2dd4b4b00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000e40b540200000000000000000000000044792478856e424b00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000001764a187aa7eea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000000ca9a3b000000000000000000000000004f3822ec494cf60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000652c411a5512df0700000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000000ca9a3b00000000000000000000000000db166a1ed73ade0700000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000000000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d0000000000000000005c4fb28490e3aa655b00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000080c6a47e8d03000000000000000000009662b973f6cb9a170000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d0000000000000000005c4fb28490e3aa655b00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000004f8c34e81402000000000000000000b99fec9852290bd00d00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000014bbf08ac60200000000000000000097376a3243c5a4681200000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000957db671502907ec0000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060077a30284b02000000000000000000170b1d5ba7cc7c370f00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e5620100000000000000000040b610b91aeee7330900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a00000000000000000099f6c2a432e3ceaf4d03000000000000040300000000de00000000de0000000000000000040403de0000000000000001de000000000000000000c84e676dc11b000000000000000000d01932cec10a040000000000000000000403de000000000000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a00000000000000000099f6c2a432e3ceaf4d03000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000002cf61a24a229000000000000000000c4a83ee57fc8336e0d01000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e5620100000000000000000040b610b91aeee7330900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000957db671502907ec0000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000060315110e5620100000000000000000040b610b91aeee7330900000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000e8890423c78a00000000000000000099f6c2a432e3ceaf4d03000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000f4448291634500000000000000000049b9efe4e54d4252b901000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de0000000000c16ff2862300000000000000000000957db671502907ec0000000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de000000000064a7b3b6e00d0000000000000000005c4fb28490e3aa655b00000000000000040300000000de0000000000000000de0000000004040300000000de0000000100000000de00000000008a5d78456301000000000000000000cc602e76805767360900000000000000040300000000de00000000000000000e00000000040403000000000e00000001000000000e00000000a0724e18090000000000000000000000a85ef6098f00000000000000000000000403000000000e000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000080bf0bc30a000000000000000000000000e018614f8f82ea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000080bf0bc30a00000000000000000000000051c889de6750f60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000080bf0bc30a0000000000000000000000003d953f4bf036c62701000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000080bf0bc30a00000000000000000000000044ca738993718f2701000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000080cf3c854876c000000000000000000e7c5be4ca9a50f00000000000000000403de00000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000fec35eb78d82ea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000b3bb9d246650f60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000080cf882c0500000000000000000000000099b9a6ebe25cf69d00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000d594fa368104df9d00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000080cf3c854876c000000000000000000e7c5be4ca9a50f00000000000000000403de00000000000000040a00000010270000000000000000000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000fec35eb78d82ea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000b3bb9d246650f60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000080cf882c0500000000000000000000000099b9a6ebe25cf69d00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000d594fa368104df9d00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000004de000000000082dfe40d4700000000000000000000de0000000000000000040403de0000000000000001de000000000000000000080cf3c854876c0000000000000000d4a14fda97075e0d00000000000000000403de0000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000040a0000001027000000000000000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000fec35eb78d82ea0000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000b3bb9d246650f60000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000000000000010a0000000000000080cf882c0500000000000000000000000099b9a6ebe25cf69d00000000000000000c040a000000ea030000026f000000ea030000de00000003de00000000000000010a0000000000000080cf882c05000000000000000000000000d594fa368104df9d00000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de0000000000000004000000000010a5d4e80000000000000000000000 diff --git a/ice/ice-solver/src/tests/fixtures/unreachable_rate.hex b/ice/ice-solver/src/tests/fixtures/unreachable_rate.hex new file mode 100644 index 0000000000..d40de0387d --- /dev/null +++ b/ice/ice-solver/src/tests/fixtures/unreachable_rate.hex @@ -0,0 +1,3 @@ +2c140000000000000040f6a2b49d01000000de00000000000000000040b2bac9e0191e0200000000000000002cf61a24a22900000000000000000100000000000000000000000000000000120000000000000000789db49d01000000de00000000000000000040b2bac9e0191e020000000000000000f124d7c6532a00000000000000000100000000000000000000000000000000110000000000000090a468b49d010000000a00000030450f0080841e0000000000000000000000000040787d0100000000000000000000000001000000000000000000000000000000001000000000000000602f68b49d010000000a00000030450f0080841e0000000000000000000000000040787d0100000000000000000000000001000000000000000000000000000000000f00000000000000e08564b49d01000000de000000000000000000e8890423c78a000000000000000000e4c95004bf0b00000000000000000001000000000000000000000000000000000c00000000000000201ce9b39d01000000270000000a0000000080c6a47e8d03000000000000000000003e4900000000000000000000000000000b00000000000000d05ee7b39d01000000270000000a0000000080c6a47e8d03000000000000000000003e490000000000000000000000000001000000000000000000000000000000000700000000000000000adfb39d01000000de0000003d450f000000986270b34f31010000000000000000c06e31d9100100000000000000000001000000000000000000000000000000000300000000000000c06260b09d010000003d450f000a00000000a031a95fe30000000000000000000040787d01000000000000000000000000000200000000000000a03e59b09d01000000270000000a0000000020f9af8e800e000000000000000000002d31010000000000000000000000000100000000000000000000000000000000010000000000000000c455b09d0100000030450f00b0440f00d612850e000000000000000000000000000049cbe870610300000000000000000100000000000000000000000000000000 +08140000000000000040f6a2b49d01000000de00000000000000000040b2bac9e0191e02000000000000118cbb09b57e5d2a00000000000000000100000000000000000000000000000000120000000000000000789db49d01000000de00000000000000000040b2bac9e0191e02000000000000118cbb09b57e5d2a000000000000000001000000000000000000000000000000000400000080647593c1333c04000000000000221877136afdba5400000000000000000403de0000000000000022185af87712c5000000000000000000 +00000000ad0d000a0000000000000000100c02640000000a00000015000000026900000015000000de00000003de000000000000000c02640000000a00000017000000026900000017000000de00000003de000000000000000c040a000000ea030000026f000000ea030000de00000003de000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000000000000030a0000000000000000a1343afdda3e33d8b0f3391b662976c7733bb460c20a558f7e4998180a000000030a0000000000000000f23a9d2656332aa8a24278f679417fc7733bb460c20a558f7e4998180a000000030a0000000000000000a6ddb6cdfd0b72d1d91d74e3cfeedce5da0d484a0a9fee2415c3d5a30b000000030a0000000000000000347f11dfb78e2f0807216b55a93c5dc7733bb460c20a558f7e4998180a000000002700000000000000000404032700000000000000032700000000000000004cd84c91bf249c2389c046563c527a1395b06bf96e6a8a009477b07eb9d05d0c00de0000000000000000040403de0000000000000003de00000000000000005d9c9172a48f9d9ac2b95bb06c2c12007e55bdd6f856547dd934df54f740b8d600b0440f000000000000040403b0440f000000000003b0440f000000000000482e86ab58eea234d8188e0de63335045aa89d80e4dbba22fd142f7bf1aa2f880030450f00000000000008040330450f0000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000000000000330450f0000000000009322460010a8c68bf770d562a978390539a96637b851ab64c008d11b030000000330450f00000000000087ff6215768fdfdf93f1aa45d1e0f0ce6210d10e7fb9500ec57273207b000000003d450f0000000000000404033d450f0000000000033d450f00000000000016c4ecb21d87f32e65b6dd3cacf6b03f34b66c82f3883bcc9ba3bb4df1f78b0200de0000000000000000040403de0000000000000001de00000000000000000040b2bac9e0191e02000000000000008461f0d5193fff2b00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000040b2bac9e0191e02000000000000008461f0d5193fff2b00000000000000000403de00000000000000000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000f53f65010000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000be4f65010000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000108165010000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000004e7c65010000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000a50765010000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000006b1765010000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000b748650100000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000f54365010000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000f53f65010000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000be4f65010000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000108165010000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000004e7c65010000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000a50765010000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000006b1765010000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000b748650100000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000f54365010000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0000de0000000000000000040403de0000000000000001de000000000000000000e8890423c78a0000000000000000002a285fe4d2b50b0000000000000000000403de0000000000000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000000080c6a47e8d0300000000000000000000a90848000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a0000000080c6a47e8d0300000000000000000000360548000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a0000000080c6a47e8d0300000000000000000000cc4a48000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000000080c6a47e8d030000000000000000000056444800000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000000080c6a47e8d0300000000000000000000a90848000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a0000000080c6a47e8d0300000000000000000000360548000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a0000000080c6a47e8d0300000000000000000000cc4a48000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000000080c6a47e8d030000000000000000000056444800000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000de0000003d450f0000040403de0000003d450f0001de0000003d450f000000986270b34f310100000000000000006e5ca677b607010000000000000000000403de0000003d450f00003d450f000a00000000100c033d450f00de0000000269000000de000000150000000264000000150000000a0000000c033d450f00de0000000269000000de000000170000000264000000170000000a0000000c033d450f00de000000026f000000de000000ea03000004ea0300000a00000010033d450f00de000000026e000000de000000eb03000004eb030000160000000266000000160000000a000000013d450f000a00000000a031a95fe30000000000000000000000a1441b010000000000000000000000000c033d450f00de0000000269000000de000000150000000264000000150000000a000000013d450f000a00000000a031a95fe3000000000000000000000068361b010000000000000000000000000c033d450f00de0000000269000000de000000170000000264000000170000000a000000013d450f000a00000000a031a95fe3000000000000000000000081f51e010000000000000000000000000c033d450f00de000000026f000000de000000ea03000004ea0300000a000000013d450f000a00000000a031a95fe30000000000000000000000dbdb1e0100000000000000000000000010033d450f00de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000000020f9af8e800e00000000000000000000c72823010000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a0000000020f9af8e800e000000000000000000002b1a23010000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a0000000020f9af8e800e000000000000000000002c1427010000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000000020f9af8e800e00000000000000000000cbf92601000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a0000000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00d612850e000000000000000000000000000399ed61552613030000000000000000040330450f00b0440f000130450f00b0440f00d612850e000000000000000000000000004d6a2dfe2b4c13030000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f0000de0000000000000000040403de0000000000000001de00000000000000000034f73c5b445f1e0200000000000000846d044689a9042c00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000dca88a6b1ba22f0f0100000000000000ed58577b1d46701600000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000dcdb4c9c04d197870000000000000000a90516bed460540b00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000005cf5ad3479e8cb4300000000000000009a1fe6a5d258b10500000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000001c82de8033f4e5210000000000000000fa6c24c2ae79da0200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000007cc8f6a610faf2100000000000000000e4828b7b8eb06d0100000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000aceb023aff7c79080000000000000000ef39974841f5b60000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000044fd888376be3c04000000000000000027235d2fe0815b0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000010064c28325f1e02000000000000000064128c9abfc22d0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000e41135e1862f0f0100000000000000004aea765cd2e1160000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000060102257ba97870000000000000000002de6887005710b0000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000c17a0f8cacb430000000000000000008798f37088b8050000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000f492d762dce52100000000000000000036b75e4445dc020000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000e850f317e5f210000000000000000000544cbb51226e010000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000050b70859607908000000000000000000c4efc0bc0fb7000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000846a93f99d3c0400000000000000000031f27d5c865b000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000b03c51e3451e02000000000000000000201e7d6bc22d000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000034adb7be100f0100000000000000000071270eaddf16000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000085ee3457f87000000000000000000006bd211126f0b000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000e0bd0070ad4300000000000000000000c5a9ebffb505000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000cc6d0f85c42100000000000000000000fa14d376d902000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000543e0f29d91000000000000000000000f5c3d7f66b01000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000098260f7b630800000000000000000000f5bbd936b500000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000028a2968a1f04000000000000000000003b5648125900000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000f05f5a92fd0100000000000000000000ea97ffff2a00000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000d43e3c96ec00000000000000000000005138dbf61300000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000d8a6a5316d00000000000000000000009259db360900000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000048e2e165240000000000000000000000dc1d49120300000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000024f1f032120000000000000000000000ee8e24890100000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000024f1f032120000000000000000000000ee8e24890100000000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000040b2bac9e0191e02000000000000008461f0d5193fff2b00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000600b982ed1262d0300000000000000a9a750bb9ce6c04000000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000f0b7066149adb403000000000000005c0830bd1610d74a00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000380e3e7a8570f803000000000000003026384a9e0ad04f00000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000005cb9d98623521a0400000000000000ea7d5d75c912485200000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000ee8e278df2422b040000000000000099fec060befb825300000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000b7794e105abb3304000000000000009c9036ccb429205400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000801befe1d18df737040000000000000054258b7a17af6e5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000c0cda9abb2a7153a04000000000000003996b3be63ed955400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000008099d528acb4243b040000000000000095515966718ba95400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000c00c1dcf1f3bac3b040000000000000094da284d315ab35400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000000398fba62feef3b040000000000000004b873588041b85400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000080dcf917fbdf113c0400000000000000306888d122b5ba5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000402eaf46c7d0223c04000000000000004ee10ff5f2eebb5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000c049587636492b3c04000000000000003c080d69db8bbc5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000080d72c0e6e852f3c04000000000000006a527b914fdabc5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000c0abc8c180a3313c040000000000000071bad4f88801bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000008008e53393b2323c04000000000000002329e2532615bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000040c4a454133a333c040000000000000056f1a858f41ebd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000c014537ddc7d333c04000000000000003c777403dc23bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000003daa11c19f333c04000000000000000cd1d5d84f26bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000805e8743aab0333c0400000000000000ccba0b1b8927bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000040ef75dc1eb9333c0400000000000000086826bc2528bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000040aa3b4162bd333c0400000000000000bd4f2db57428bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000c0879ef383bf333c040000000000000086cbb0319c28bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000080f6cfcc94c0333c04000000000000009576f2efaf28bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000403b1a2114c1333c040000000000000067ba9926b928bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de000000000000000040d08de35cc1333c04000000000000002cf7666abe28bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de0000000000000000c09ac74481c1333c0400000000000000ed904d0cc128bd5400000000000000000403de0000000000000000de0000000000000000040403de0000000000000001de00000000000000000080647593c1333c0400000000000000eef4405dc228bd5400000000000000000403de0000000000000004de000000000082dfe40d4700000000000000000004de000000000082dfe40d4700000000000000000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000000d05faa06070900000000000000000000c935b6000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a00000000d05faa06070900000000000000000000b12cb6000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a00000000d05faa0607090000000000000000000059b0b7000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000000d05faa06070900000000000000000000ee9fb700000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000705ce2548383040000000000000000000000735b000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000705ce25483830400000000000000000000936e5b000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000705ce25483830400000000000000000000c3d85b000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000705ce254838304000000000000000000008ed05b00000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000070684aaac141020000000000000000000054ce2d000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a00000070684aaac14102000000000000000000002bcc2d000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a00000070684aaac141020000000000000000000086ec2d000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000070684aaac14102000000000000000000006be82d00000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000706efed4e02001000000000000000000004bec16000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000706efed4e02001000000000000000000003beb16000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000706efed4e02001000000000000000000004cf616000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000706efed4e02001000000000000000000003ef41600000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000007071586a709000000000000000000000006c770b000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a0000007071586a70900000000000000000000000e5760b000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a0000007071586a70900000000000000000000000287b0b000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000007071586a70900000000000000000000000207a0b00000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000f07205353848000000000000000000000006bc05000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000f072053538480000000000000000000000c3bb05000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000f07205353848000000000000000000000094bd05000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000f0720535384800000000000000000000000fbd0500000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000b0f35b1a1c24000000000000000000000018de02000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000b0f35b1a1c240000000000000000000000f6dd02000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000b0f35b1a1c240000000000000000000000cade02000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000b0f35b1a1c24000000000000000000000087de0200000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000001034070d0e120000000000000000000000106f01000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a0000001034070d0e120000000000000000000000ff6e01000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a0000001034070d0e120000000000000000000000656f01000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000001034070d0e120000000000000000000000436f0100000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000040d45c060709000000000000000000000088b700000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a00000040d45c06070900000000000000000000007fb700000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a00000040d45c0607090000000000000000000000b2b700000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000040d45c0607090000000000000000000000a0b70000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000090dee08283040000000000000000000000c35b00000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a00000090dee08283040000000000000000000000c05b00000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a00000090dee08283040000000000000000000000d95b00000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000090dee08283040000000000000000000000cf5b0000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000080a949c141020000000000000000000000e02d00000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a00000080a949c141020000000000000000000000df2d00000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a00000080a949c141020000000000000000000000eb2d00000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000080a949c141020000000000000000000000e62d0000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000304957e020010000000000000000000000ee1600000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000304957e020010000000000000000000000ee1600000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000304957e020010000000000000000000000f51600000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000304957e020010000000000000000000000f2160000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000d0de047090000000000000000000000000770b00000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000d0de047090000000000000000000000000760b00000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000d0de0470900000000000000000000000007a0b00000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000d0de047090000000000000000000000000780b0000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000a0a9db3748000000000000000000000000ba0500000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000a0a9db3748000000000000000000000000ba0500000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000a0a9db3748000000000000000000000000bc0500000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000a0a9db3748000000000000000000000000ba050000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000004049a01b2400000000000000000000000101270000000a0000004049a01b2400000000000000000000000101270000000a0000004049a01b24000000000000000000000000dd0200000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000004049a01b2400000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000001099820d1200000000000000000000000101270000000a0000001099820d1200000000000000000000000101270000000a0000001099820d120000000000000000000000006e0100000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000001099820d1200000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c0869a060900000000000000000000000101270000000a000000c0869a060900000000000000000000000101270000000a000000c0869a0609000000000000000000000000b60000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c0869a060900000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000d0b7ff820400000000000000000000000101270000000a000000d0b7ff820400000000000000000000000101270000000a000000d0b7ff82040000000000000000000000005a0000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000d0b7ff820400000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000201659410200000000000000000000000101270000000a000000201659410200000000000000000000000101270000000a00000020165941020000000000000000000000002c0000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000201659410200000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000080ff5e200100000000000000000000000101270000000a00000080ff5e200100000000000000000000000101270000000a00000080ff5e2001000000000000000000000000150000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000080ff5e200100000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000030f4e18f0000000000000000000000000101270000000a00000030f4e18f0000000000000000000000000101270000000a00000030f4e18f000000000000000000000000000a0000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000030f4e18f0000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000005034ca470000000000000000000000000101270000000a0000005034ca470000000000000000000000000101270000000a0000005034ca4700000000000000000000000000040000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000005034ca470000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000006054be230000000000000000000000000101270000000a0000006054be230000000000000000000000000101270000000a0000006054be2300000000000000000000000000010000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000006054be230000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000a09e91110000000000000000000000000101270000000a000000a09e91110000000000000000000000000101270000000a000000a09e911100000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000a09e91110000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c0437b080000000000000000000000000101270000000a000000c0437b080000000000000000000000000101270000000a000000c0437b0800000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c0437b080000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000005016f0030000000000000000000000000101270000000a0000005016f0030000000000000000000000000101270000000a0000005016f00300000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000005016f0030000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000006045d1010000000000000000000000000101270000000a0000006045d1010000000000000000000000000101270000000a0000006045d10100000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000006045d1010000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000020179b000000000000000000000000000101270000000a00000020179b000000000000000000000000000101270000000a00000020179b0000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000020179b000000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000908b4d000000000000000000000000000101270000000a000000908b4d000000000000000000000000000101270000000a000000908b4d0000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000908b4d000000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000908b4d000000000000000000000000000101270000000a000000908b4d000000000000000000000000000101270000000a000000908b4d0000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000908b4d000000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000000406352bfc60100000000000000000000251124000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a00000000406352bfc60100000000000000000000750f24000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a00000000406352bfc601000000000000000000007d2524000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000000406352bfc6010000000000000000000042222400000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c05d22a95fe30000000000000000000000bf0b12000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000c05d22a95fe30000000000000000000000e90a12000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000c05d22a95fe30000000000000000000000c41212000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c05d22a95fe3000000000000000000000027111200000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c08d89d4af710000000000000000000000a90609000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000c08d89d4af7100000000000000000000003f0609000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000c08d89d4af710000000000000000000000630909000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c08d89d4af71000000000000000000000093080900000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c0253dead7380000000000000000000000868304000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000c0253dead7380000000000000000000000518304000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000c0253dead7380000000000000000000000b18404000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c0253dead738000000000000000000000049840400000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c0f116f56b1c0000000000000000000000cf4102000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000c0f116f56b1c0000000000000000000000b54102000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000c0f116f56b1c0000000000000000000000584202000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c0f116f56b1c000000000000000000000024420200000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c0d783fa350e0000000000000000000000ea2001000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000c0d783fa350e0000000000000000000000dd2001000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000c0d783fa350e00000000000000000000002c2101000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c0d783fa350e000000000000000000000011210100000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c04a3afd1a070000000000000000000000759000000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000c04a3afd1a0700000000000000000000006e9000000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000c04a3afd1a070000000000000000000000959000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c04a3afd1a07000000000000000000000087900000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000004084957e8d0300000000000000000000003a4800000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a0000004084957e8d030000000000000000000000374800000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a0000004084957e8d0300000000000000000000004a4800000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000004084957e8d03000000000000000000000043480000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000002143bfc60100000000000000000000001c2400000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000002143bfc60100000000000000000000001b2400000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000002143bfc6010000000000000000000000252400000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000002143bfc601000000000000000000000021240000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000404e925fe30000000000000000000000000e1200000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000404e925fe30000000000000000000000000d1200000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000404e925fe3000000000000000000000000121200000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000404e925fe300000000000000000000000010120000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000000086c1af71000000000000000000000000050900000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a0000000086c1af71000000000000000000000000050900000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a0000000086c1af71000000000000000000000000080900000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000000086c1af7100000000000000000000000006090000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c080d1d738000000000000000000000000810400000000000000000000000000000c0327000000de0000000269000000de000000150000000264000000150000000a00000001270000000a000000c080d1d738000000000000000000000000810400000000000000000000000000000c0327000000de0000000269000000de000000170000000264000000170000000a00000001270000000a000000c080d1d738000000000000000000000000830400000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c080d1d73800000000000000000000000081040000000000000000000000000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000000270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000401fe16b1c00000000000000000000000101270000000a000000401fe16b1c00000000000000000000000101270000000a000000401fe16b1c000000000000000000000000410200000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000401fe16b1c00000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000080eee8350e00000000000000000000000101270000000a00000080eee8350e00000000000000000000000101270000000a00000080eee8350e000000000000000000000000200100000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000080eee8350e00000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000000035e51a0700000000000000000000000101270000000a0000000035e51a0700000000000000000000000101270000000a0000000035e51a070000000000000000000000008f0000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000000035e51a0700000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000004058638d0300000000000000000000000101270000000a0000004058638d0300000000000000000000000101270000000a0000004058638d03000000000000000000000000470000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000004058638d0300000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000000baac60100000000000000000000000101270000000a000000000baac60100000000000000000000000101270000000a000000000baac601000000000000000000000000230000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000000baac60100000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000040c345e30000000000000000000000000101270000000a00000040c345e30000000000000000000000000101270000000a00000040c345e300000000000000000000000000110000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000040c345e30000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000080409b710000000000000000000000000101270000000a00000080409b710000000000000000000000000101270000000a00000080409b7100000000000000000000000000080000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000080409b710000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000005ebe380000000000000000000000000101270000000a000000005ebe380000000000000000000000000101270000000a000000005ebe3800000000000000000000000000030000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000005ebe380000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000c0ec4f1c0000000000000000000000000101270000000a000000c0ec4f1c0000000000000000000000000101270000000a000000c0ec4f1c00000000000000000000000000010000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000c0ec4f1c0000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a0000004055200e0000000000000000000000000101270000000a0000004055200e0000000000000000000000000101270000000a0000004055200e00000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a0000004055200e0000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000808908070000000000000000000000000101270000000a000000808908070000000000000000000000000101270000000a0000008089080700000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000808908070000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000800275030000000000000000000000000101270000000a000000800275030000000000000000000000000101270000000a0000008002750300000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000800275030000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000003fab010000000000000000000000000101270000000a000000003fab010000000000000000000000000101270000000a000000003fab0100000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000003fab010000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000405dc6000000000000000000000000000101270000000a000000405dc6000000000000000000000000000101270000000a000000405dc60000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000405dc6000000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a000000808d5b000000000000000000000000000101270000000a000000808d5b000000000000000000000000000101270000000a000000808d5b0000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a000000808d5b000000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000080841e000000000000000000000000000101270000000a00000080841e000000000000000000000000000101270000000a00000080841e0000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000080841e000000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000040420f000000000000000000000000000101270000000a00000040420f000000000000000000000000000101270000000a00000040420f0000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000040420f000000000000000000000000000100270000000a00000000100c0327000000de0000000269000000de000000150000000264000000150000000a0000000c0327000000de0000000269000000de000000170000000264000000170000000a0000000c0327000000de000000026f000000de000000ea03000004ea0300000a000000100327000000de000000026e000000de000000eb03000004eb030000160000000266000000160000000a00000001270000000a00000040420f000000000000000000000000000101270000000a00000040420f000000000000000000000000000101270000000a00000040420f0000000000000000000000000000000000000000000000000000000000000c0327000000de000000026f000000de000000ea03000004ea0300000a00000001270000000a00000040420f0000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000f53f65010000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000be4f65010000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000108165010000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0080841e00000000000000000000000000004e7c65010000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0080841e0000000000000000000000000000a50765010000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e00000000000000000000000000006b1765010000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000b748650100000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0080841e0000000000000000000000000000f54365010000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f0000000000000000000000000000babab2000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f003f420f0000000000000000000000000000bec2b2000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f003f420f000000000000000000000000000003c1b2000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f003f420f000000000000000000000000000096beb2000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f003f420f0000000000000000000000000000429eb2000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f000000000000000000000000000044a6b2000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f000000000000000000000000000089a4b20000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003f420f00000000000000000000000000001da2b2000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa1070000000000000000000000000000fe6359000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f001fa1070000000000000000000000000000006859000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f001fa10700000000000000000000000000009c6059000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f001fa1070000000000000000000000000000605f59000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f001fa1070000000000000000000000000000ae5559000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa1070000000000000000000000000000b05959000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa10700000000000000000000000000004d52590000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001fa1070000000000000000000000000000115159000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd00300000000000000000000000000009bb32c000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f008fd0030000000000000000000000000000a2b52c000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f008fd003000000000000000000000000000050b02c000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f008fd0030000000000000000000000000000adaf2c000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f008fd00300000000000000000000000000006eac2c000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd003000000000000000000000000000075ae2c000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd003000000000000000000000000000024a92c0000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f008fd003000000000000000000000000000080a82c000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e80100000000000000000000000000002d5a16000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0047e80100000000000000000000000000002a5b16000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0047e8010000000000000000000000000000245816000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0047e8010000000000000000000000000000c75716000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0047e8010000000000000000000000000000955616000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e8010000000000000000000000000000935716000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e80100000000000000000000000000008d54160000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0047e80100000000000000000000000000002f5416000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f4000000000000000000000000000000222d0b000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f0023f4000000000000000000000000000000ac2d0b000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f0023f40000000000000000000000000000000c2c0b000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f0023f4000000000000000000000000000000d82b0b000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f0023f4000000000000000000000000000000562b0b000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f4000000000000000000000000000000e12b0b000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f4000000000000000000000000000000412a0b0000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f0023f40000000000000000000000000000000c2a0b000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a0000000000000000000000000000008b9605000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00117a000000000000000000000000000000d09605000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00117a000000000000000000000000000000009605000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00117a000000000000000000000000000000e09505000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00117a000000000000000000000000000000a59505000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a000000000000000000000000000000ea9505000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a0000000000000000000000000000001a95050000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00117a000000000000000000000000000000fa9405000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d0000000000000000000000000000003acb02000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00083d0000000000000000000000000000005ccb02000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00083d000000000000000000000000000000faca02000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00083d000000000000000000000000000000e4ca02000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00083d000000000000000000000000000000c7ca02000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d000000000000000000000000000000e9ca02000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d00000000000000000000000000000087ca020000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00083d00000000000000000000000000000071ca02000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e0000000000000000000000000000009c6501000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00841e000000000000000000000000000000ad6501000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00841e0000000000000000000000000000007c6501000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00841e000000000000000000000000000000716501000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00841e000000000000000000000000000000636501000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e000000000000000000000000000000746501000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e0000000000000000000000000000004365010000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00841e000000000000000000000000000000386501000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f000000000000000000000000000000ceb200000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00420f000000000000000000000000000000d1b200000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00420f000000000000000000000000000000beb200000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00420f000000000000000000000000000000b2b200000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00420f000000000000000000000000000000b1b200000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f000000000000000000000000000000b4b200000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f000000000000000000000000000000a1b2000000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00420f00000000000000000000000000000096b200000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a107000000000000000000000000000000605900000000000000000000000000000c02640000000a00000015000000026900000015000000de00000003de00000030450f00010a00000030450f00a1070000000000000000000000000000005c5900000000000000000000000000000c02640000000a00000017000000026900000017000000de00000003de00000030450f00010a00000030450f00a1070000000000000000000000000000005e5900000000000000000000000000000c040a000000ea030000026f000000ea030000de00000003de00000030450f00010a00000030450f00a107000000000000000000000000000000535900000000000000000000000000001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f00010a00000030450f00a107000000000000000000000000000000535900000000000000000000000000001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a1070000000000000000000000000000004e5900000000000000000000000000001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a1070000000000000000000000000000005059000000000000000000000000000018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00a107000000000000000000000000000000455900000000000000000000000000001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001010a00000030450f00d003000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001010a00000030450f00e801000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001010a00000030450f00f400000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001010a00000030450f007a00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001010a00000030450f003d00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001010a00000030450f001e00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001010a00000030450f000f00000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001010a00000030450f000700000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001010a00000030450f000300000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001000a00000030450f0000200c02640000000a00000015000000026900000015000000de00000003de00000030450f000c02640000000a00000017000000026900000017000000de00000003de00000030450f000c040a000000ea030000026f000000ea030000de00000003de00000030450f001002660000000a000000160000000416000000eb030000026e000000eb030000de00000003de00000030450f001802640000000a00000015000000026900000015000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001802640000000a00000017000000026900000017000000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f0018040a000000ea030000026f000000ea030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f001c02660000000a000000160000000416000000eb030000026e000000eb030000de00000003de000000292300000429230000915f010002915f0100915f0100f103000004f103000030450f00010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001010a00000030450f000100000000000000000000000000000001040a00000010270000000000000000000000000000040a0000001027000000000000000000000000000000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000004c31b8d9a798000000000000000000d7221d13dc83000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000a458f9d6ec534c0000000000000000007d0b5d3fee41000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000a405ed68f629260000000000000000000016212df720000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000024dce631fb14130000000000000000004788eb997b10000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000064c763967d8a0900000000000000000002dbcacd3d08000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000043da2c83ec504000000000000000000b30a19e71e04000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000d477c1619f62020000000000000000002ac497730f02000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00003c1551ae4f3101000000000000000000ea08cdb90701000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000f0e398d4a79800000000000000000000f825e5dc8300000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00009c242de5534c00000000000000000000eb5b6eee4100000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000a0eb06f0292600000000000000000000370535f72000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00007428e4f2141300000000000000000000d919967b1000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00008c6de2768a09000000000000000000009dd7c83d0800000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00001890e138c504000000000000000000009b35e21e0400000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000b07a5197620200000000000000000000f3ad6c0f0200000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000fc6f89463101000000000000000000004feab1070100000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00005011b5a0980000000000000000000000eabed6830000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00004c3b3b4b4c0000000000000000000000cbf2e6410000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000f8f60d23260000000000000000000000f642f1200000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000020ae670c130000000000000000000000023574100000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000b489148109000000000000000000000007ae35080000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00002c9efabd04000000000000000000000063a018040000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000068a86d5c020000000000000000000000c3190a020000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000d886172901000000000000000000000037a000010000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000010766c8f00000000000000000000000070e37b000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000aced96420000000000000000000000003f8539000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f000028d0bb1e000000000000000000000000628c1a000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f0000b89a3e0a000000000000000000000000b8d908000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00005c4d1f05000000000000000000000000dc6c04000000000000000000000000000403de0000003d450f0000de0000003d450f0000040403de0000003d450f0001de0000003d450f00005c4d1f05000000000000000000000000dc6c04000000000000000000000000000403de0000003d450f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f006b894207000000000000000000000000009d3c71e02ea489010000000000000000040330450f00b0440f000130450f00b0440f006b894207000000000000000000000000009230b0da43b089010000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00b544a103000000000000000000000000008cab289a58d6c4000000000000000000040330450f00b0440f000130450f00b0440f00b544a10300000000000000000000000000a055709eaddac4000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f005aa2d00100000000000000000000000000acbf608a3c6c62000000000000000000040330450f00b0440f000130450f00b0440f005aa2d00100000000000000000000000000d597cfb5f96d62000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f002d51e80000000000000000000000000000dff7305c623631000000000000000000040330450f00b0440f000130450f00b0440f002d51e800000000000000000000000000008e599bd2253731000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f009628740000000000000000000000000000c4a7d618429b18000000000000000000040330450f00b0440f000130450f00b0440f00962874000000000000000000000000000060056a3a9d9b18000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f004b143a000000000000000000000000000088a5e74da54d0c000000000000000000040330450f00b0440f000130450f00b0440f004b143a00000000000000000000000000003cf48766d14d0c000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00250a1d0000000000000000000000000000189e349cd32606000000000000000000040330450f00b0440f000130450f00b0440f00250a1d0000000000000000000000000000a2440d3be92606000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0012850e0000000000000000000000000000653c16f7691303000000000000000000040330450f00b0440f000130450f00b0440f0012850e0000000000000000000000000000700bb1cd741303000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f008942070000000000000000000000000000c187910cb58901000000000000000000040330450f00b0440f000130450f00b0440f008942070000000000000000000000000000fef5b4aeba8901000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0044a1030000000000000000000000000000040a6c6fdac400000000000000000000040330450f00b0440f000130450f00b0440f0044a10300000000000000000000000000004dd85461ddc400000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00a2d00100000000000000000000000000004ee1c6386d6200000000000000000000040330450f00b0440f000130450f00b0440f00a2d00100000000000000000000000000006dc44eb16e6200000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0050e800000000000000000000000000000093d56c66363100000000000000000000040330450f00b0440f000130450f00b0440f0050e8000000000000000000000000000000685a4060373100000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f002874000000000000000000000000000000f66348339b1800000000000000000000040330450f00b0440f000130450f00b0440f00287400000000000000000000000000000003cb2ab09b1800000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00143a0000000000000000000000000000005113a8994d0c00000000000000000000040330450f00b0440f000130450f00b0440f00143a000000000000000000000000000000681ca6fa4d0c00000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00091d0000000000000000000000000000006b429b96260600000000000000000000040330450f00b0440f000130450f00b0440f00091d000000000000000000000000000000d0d1a2e9260600000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00840e000000000000000000000000000000ac592f30130300000000000000000000040330450f00b0440f000130450f00b0440f00840e000000000000000000000000000000fbd8d074130300000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00420700000000000000000000000000000018b01798890100000000000000000000040330450f00b0440f000130450f00b0440f004207000000000000000000000000000000bf6f68ba890100000000000000000000100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00a0030000000000000000000000000000010130450f00b0440f00a0030000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00d0010000000000000000000000000000010130450f00b0440f00d0010000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f00e7000000000000000000000000000000010130450f00b0440f00e7000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0073000000000000000000000000000000010130450f00b0440f0073000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0039000000000000000000000000000000010130450f00b0440f0039000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f001c000000000000000000000000000000010130450f00b0440f001c000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f000e000000000000000000000000000000010130450f00b0440f000e000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0006000000000000000000000000000000010130450f00b0440f0006000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0003000000000000000000000000000000010130450f00b0440f0003000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0001000000000000000000000000000000010130450f00b0440f0001000000000000000000000000000000010030450f00b0440f000008040330450f00b0440f00100430450f00f103000002915f0100f1030000915f010004915f0100292300000329230000b0440f000130450f00b0440f0001000000000000000000000000000000010130450f00b0440f0001000000000000000000000000000000010430450f0003b5000000000000000000000000000004de000000000082dfe40d4700000000000000000000de0000000000000000040403de0000000000000001de00000000000000000080647593c1333c0400000000000000eef4405dc228bd5400000000000000000403de0000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e8000000000000000000000004de000000000082dfe40d4700000000000000000004000000000010a5d4e80000000000000000000000 diff --git a/ice/ice-solver/src/tests/flow_graph.rs b/ice/ice-solver/src/tests/flow_graph.rs new file mode 100644 index 0000000000..e553d80691 --- /dev/null +++ b/ice/ice-solver/src/tests/flow_graph.rs @@ -0,0 +1,121 @@ +use crate::common::flow_graph::build_flow_graph; +use ice_support::{AssetId, Balance, Intent, IntentData, IntentId, Partial, SwapData}; + +fn make(id: IntentId, asset_in: AssetId, asset_out: AssetId, amount_in: Balance, amount_out: Balance) -> Intent { + Intent { + id, + data: IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::No, + }), + } +} + +fn make_partial_filled( + id: IntentId, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + amount_out: Balance, + already_filled: Balance, +) -> Intent { + Intent { + id, + data: IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::Yes(already_filled), + }), + } +} + +fn entries_of<'a>(intents: &[&'a Intent]) -> Vec<(&'a Intent, Balance)> { + intents + .iter() + .map(|i| { + let cap = match &i.data { + IntentData::Swap(s) => s.remaining(), + _ => 0, + }; + (*i, cap) + }) + .collect() +} + +#[test] +fn test_build_flow_graph_groups_by_directed_pair() { + let i1 = make(1, 1, 2, 100, 90); + let i2 = make(2, 1, 2, 50, 45); + let i3 = make(3, 2, 1, 80, 70); + + let graph = build_flow_graph(&entries_of(&[&i1, &i2, &i3])); + + assert_eq!(graph.len(), 2, "expected two directed pairs"); + assert_eq!(graph.get(&(1, 2)).map(Vec::len).unwrap_or(0), 2); + assert_eq!(graph.get(&(2, 1)).map(Vec::len).unwrap_or(0), 1); +} + +#[test] +fn test_build_flow_graph_sorts_by_limit_price_ascending() { + let i1 = make(1, 1, 2, 100, 90); // rate 0.9 + let i2 = make(2, 1, 2, 100, 50); // rate 0.5 ← cheapest + let i3 = make(3, 1, 2, 100, 95); // rate 0.95 ← most expensive + + let graph = build_flow_graph(&entries_of(&[&i1, &i2, &i3])); + let entries = graph.get(&(1, 2)).expect("pair (1,2) should exist"); + + assert_eq!(entries.len(), 3); + assert_eq!(entries[0].intent_id, 2, "cheapest first"); + assert_eq!(entries[2].intent_id, 3, "most expensive last"); +} + +#[test] +fn test_remaining_in_equals_amount_in_for_fresh_non_partial() { + let i1 = make(1, 1, 2, 100, 90); + let graph = build_flow_graph(&entries_of(&[&i1])); + let entries = graph.get(&(1, 2)).unwrap(); + assert_eq!(entries[0].original_amount_in, 100); + assert_eq!(entries[0].remaining_in, 100); +} + +/// A partial intent that has already been partially filled must expose only +/// the unfilled portion via `remaining_in`, not the original amount. +#[test] +fn test_remaining_in_uses_remaining_for_partial() { + let i1 = make_partial_filled(1, 1, 2, 100, 90, 60); + let graph = build_flow_graph(&entries_of(&[&i1])); + let entries = graph.get(&(1, 2)).unwrap(); + assert_eq!( + entries[0].original_amount_in, 100, + "original_amount_in should stay as the intent's amount_in", + ); + assert_eq!( + entries[0].remaining_in, 40, + "remaining_in should be amount_in - filled = 40, got {}", + entries[0].remaining_in, + ); +} + +/// The caller's cap must not let remaining_in exceed `swap.remaining()` even +/// if the caller passes a larger value (e.g. stale fill plan). +#[test] +fn test_cap_bounded_by_remaining() { + let i1 = make_partial_filled(1, 1, 2, 100, 90, 60); // remaining = 40 + let graph = build_flow_graph(&[(&i1, 1_000)]); + let entries = graph.get(&(1, 2)).unwrap(); + assert_eq!(entries[0].remaining_in, 40); +} + +/// A cap smaller than `swap.remaining()` must be honoured. +#[test] +fn test_cap_smaller_than_remaining() { + let i1 = make(1, 1, 2, 100, 90); // remaining = 100 + let graph = build_flow_graph(&[(&i1, 30)]); + let entries = graph.get(&(1, 2)).unwrap(); + assert_eq!(entries[0].remaining_in, 30); +} diff --git a/ice/ice-solver/src/tests/mod.rs b/ice/ice-solver/src/tests/mod.rs new file mode 100644 index 0000000000..9b9d2bcd07 --- /dev/null +++ b/ice/ice-solver/src/tests/mod.rs @@ -0,0 +1,4 @@ +mod flow_graph; +mod regressions; +mod ring_detection; +mod v2_solver; diff --git a/ice/ice-solver/src/tests/regressions.rs b/ice/ice-solver/src/tests/regressions.rs new file mode 100644 index 0000000000..7d4e8c9ab7 --- /dev/null +++ b/ice/ice-solver/src/tests/regressions.rs @@ -0,0 +1,232 @@ +//! Regression tests for the v2 solver. +//! +//! Each test pins the exact solution the solver produced against a real +//! testnet snapshot at the time a bug was discovered. The snapshot is *not* +//! re-loaded; instead every AMM call the solver made during the original +//! integration-test run was recorded and is replayed here in order via a +//! `ReplayAMM` mock. +//! +//! Each `*.hex` fixture has three lines: SCALE-encoded `Vec`, +//! `Solution`, and `Trace { price_denominator, responses }`. + +use crate::replay_format::{Response, Trace}; +use crate::v2::Solver; +use codec::Decode; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::{AMMInterface, TradeExecution}; +use hydradx_traits::router::{PoolEdge, Route}; +use ice_support::{AssetId, Balance, Intent, Solution}; +use std::cell::RefCell; +use std::collections::VecDeque; + +// ---------- replay AMM ---------- + +thread_local! { + static RESPONSES: RefCell> = const { RefCell::new(VecDeque::new()) }; + static PRICE_DENOM: RefCell = const { RefCell::new(0) }; +} + +struct ReplayAMM; + +impl ReplayAMM { + fn install(trace: Trace) { + RESPONSES.with(|q| *q.borrow_mut() = trace.responses.into_iter().collect()); + PRICE_DENOM.with(|d| *d.borrow_mut() = trace.price_denominator); + } + + fn next() -> Response { + RESPONSES.with(|q| { + q.borrow_mut() + .pop_front() + .expect("replay trace exhausted — solver made more calls than were recorded") + }) + } +} + +impl AMMInterface for ReplayAMM { + type Error = (); + type State = (); + + fn discover_routes( + asset_in: AssetId, + asset_out: AssetId, + _state: &Self::State, + ) -> Result>, Self::Error> { + match Self::next() { + Response::DiscoverRoutes { + asset_in: a, + asset_out: b, + result, + } if a == asset_in && b == asset_out => result, + other => panic!("replay mismatch: expected discover_routes({asset_in}, {asset_out}), got {other:?}"), + } + } + + fn sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + match Self::next() { + Response::Sell { + asset_in: a, + asset_out: b, + amount_in: v, + result, + } if a == asset_in && b == asset_out && v == amount_in => result.map(|(amount_out, route)| { + ( + (), + TradeExecution { + amount_in, + amount_out, + route, + }, + ) + }), + other => panic!("replay mismatch: expected sell({asset_in}, {asset_out}, {amount_in}), got {other:?}"), + } + } + + fn buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + match Self::next() { + Response::Buy { + asset_in: a, + asset_out: b, + amount_out: v, + result, + } if a == asset_in && b == asset_out && v == amount_out => result.map(|(amount_in, route)| { + ( + (), + TradeExecution { + amount_in, + amount_out, + route, + }, + ) + }), + other => panic!("replay mismatch: expected buy({asset_in}, {asset_out}, {amount_out}), got {other:?}"), + } + } + + fn get_spot_price( + asset_in: AssetId, + asset_out: AssetId, + _route: Route, + _state: &Self::State, + ) -> Result { + match Self::next() { + Response::SpotPrice { + asset_in: a, + asset_out: b, + result, + } if a == asset_in && b == asset_out => result, + other => panic!("replay mismatch: expected get_spot_price({asset_in}, {asset_out}), got {other:?}"), + } + } + + fn price_denominator() -> AssetId { + PRICE_DENOM.with(|d| *d.borrow()) + } + + fn pool_edges(_state: &Self::State) -> Vec> { + Vec::new() + } + + fn existential_deposit(asset_id: AssetId) -> Balance { + match Self::next() { + Response::ExistentialDeposit { asset_id: a, ed } if a == asset_id => ed, + other => panic!("replay mismatch: expected existential_deposit({asset_id}), got {other:?}"), + } + } +} + +// ---------- fixtures ---------- + +fn run_fixture(raw: &str) -> (Solution, Solution) { + let (intents_bytes, solution_bytes, trace) = Trace::decode_fixture(raw); + let intents = Vec::::decode(&mut &intents_bytes[..]).expect("decode intents"); + let expected = Solution::decode(&mut &solution_bytes[..]).expect("decode solution"); + ReplayAMM::install(trace); + let actual = Solver::::solve(intents, ()).expect("solver should succeed"); + // trace should be fully consumed + let remaining = RESPONSES.with(|q| q.borrow().len()); + assert_eq!( + remaining, 0, + "solver consumed fewer AMM calls than were recorded — {remaining} leftover", + ); + (actual, expected) +} + +// ---------- tests ---------- + +/// Regression: snapshot where one partial intent had an unreachable min rate, +/// which was poisoning the entire pair and dropping all other partial fills +/// on it to zero. After the fix, Alice's two loose-limit 10k-HOLLAR→HDX +/// partial intents resolve and the rest of the pair's intents are dropped +/// individually as the unreachable-rate intent had specified. +/// +/// Snapshot: `SNAPSHOT_notworking` (chain at testnet block referenced in the +/// ICE partial-fill bug report). +#[test] +fn unreachable_rate_poisons_pair() { + let raw = include_str!("fixtures/unreachable_rate.hex"); + let (actual, expected) = run_fixture(raw); + assert_eq!(actual, expected, "solver produced different solution than expected"); +} + +/// Regression: snapshot where the solver produced a resolved intent with +/// amount below the asset's existential deposit, which caused +/// `submit_solution` to fail with `InvalidAmount`. After the fix, the solver +/// enforces ED on every resolved intent. +/// +/// Snapshot: `SNAPSHOT_invalidagain`. +#[test] +fn resolved_respects_existential_deposit() { + let raw = include_str!("fixtures/existential_deposit.hex"); + let (actual, expected) = run_fixture(raw); + assert_eq!(actual, expected, "solver produced different solution than expected"); +} + +/// Regression: snapshot where owners of multiple same-direction intents had +/// their sell-asset balance locked in named reserves from prior rounds, so the +/// pallet's `submit_solution` later failed with `FundsUnavailable`. Pins the +/// solver's selected intents + trade plan for the scenario. +/// +/// Snapshot: `SNAPSHOT_funds`. +#[test] +fn funds_unavailable() { + let raw = include_str!("fixtures/funds_unavailable.hex"); + let (actual, expected) = run_fixture(raw); + assert_eq!(actual, expected, "solver produced different solution than expected"); +} + +/// Regression: snapshot where a single large partial intent hit the pool's +/// per-block trading limit and the solver had to cap fills accordingly. +/// +/// Snapshot: `SNAPSHOT_tradinglimit`. +#[test] +fn trading_limit() { + let raw = include_str!("fixtures/trading_limit.hex"); + let (actual, expected) = run_fixture(raw); + assert_eq!(actual, expected, "solver produced different solution than expected"); +} + +/// Regression: snapshot where the intent with id ending `6127` was being +/// excluded from the solution. Pins the solver's inclusion/exclusion choices +/// across the whole intent set at that state. +/// +/// Snapshot: `SNAPSHOT_6127`. +#[test] +fn intent_6127() { + let raw = include_str!("fixtures/intent_6127.hex"); + let (actual, expected) = run_fixture(raw); + assert_eq!(actual, expected, "solver produced different solution than expected"); +} diff --git a/ice/ice-solver/src/tests/ring_detection.rs b/ice/ice-solver/src/tests/ring_detection.rs new file mode 100644 index 0000000000..cd5ba23510 --- /dev/null +++ b/ice/ice-solver/src/tests/ring_detection.rs @@ -0,0 +1,119 @@ +use crate::common::flow_graph::build_flow_graph; +use crate::common::ring_detection::detect_rings; +use hydra_dx_math::types::Ratio; +use ice_support::{AssetId, Intent, IntentData, Partial, SwapData}; +use sp_std::collections::btree_map::BTreeMap; + +fn make(id: u128, asset_in: u32, asset_out: u32, amount_in: u128, amount_out: u128) -> Intent { + Intent { + id, + data: IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::No, + }), + } +} + +fn unit_prices(assets: &[AssetId]) -> BTreeMap { + let mut m = BTreeMap::new(); + for &a in assets { + m.insert(a, Ratio::new(1, 1)); + } + m +} + +#[test] +fn test_detect_ring_basic_3_cycle() { + // 1 → 2 → 3 → 1 at 1:1, all intents 100. + let i1 = make(1, 1, 2, 100, 90); + let i2 = make(2, 2, 3, 100, 90); + let i3 = make(3, 3, 1, 100, 90); + let intents = [(&i1, 100u128), (&i2, 100), (&i3, 100)]; + + let mut graph = build_flow_graph(&intents); + let prices = unit_prices(&[1, 2, 3]); + let rings = detect_rings(&mut graph, &prices); + + assert_eq!(rings.len(), 1, "one ring expected, got {}", rings.len()); + let ring = &rings[0]; + assert_eq!(ring.edges.len(), 3); + // Each edge has exactly one fill of 100. + let mut amounts: Vec = ring + .edges + .iter() + .flat_map(|(_, fs)| fs.iter().map(|f| f.amount_in)) + .collect(); + amounts.sort(); + assert_eq!(amounts, vec![100, 100, 100]); +} + +#[test] +fn test_ring_respects_entry_remaining_in() { + // Same 3-cycle but (1,2) has only 40 remaining after we manually mutate the graph. + // Bottleneck must be 40, not 100. + let i1 = make(1, 1, 2, 100, 90); + let i2 = make(2, 2, 3, 100, 90); + let i3 = make(3, 3, 1, 100, 90); + let intents = [(&i1, 100u128), (&i2, 100), (&i3, 100)]; + + let mut graph = build_flow_graph(&intents); + // Simulate the (1,2) intent already being 60% filled by a prior round. + { + let entry = graph.get_mut(&(1, 2)).unwrap(); + entry[0].remaining_in = 40; + } + + let prices = unit_prices(&[1, 2, 3]); + let rings = detect_rings(&mut graph, &prices); + + assert_eq!(rings.len(), 1); + // All three legs must be bottlenecked at 40. + for (_, fills) in &rings[0].edges { + for f in fills { + assert!( + f.amount_in <= 40, + "ring leg filled {} but bottleneck should be 40", + f.amount_in, + ); + } + } +} + +#[test] +fn test_ring_skips_when_one_edge_below_min() { + // (1,2) has a tight limit that 1:1 spot can't satisfy: amount_in=100, amount_out=150. + let i1 = make(1, 1, 2, 100, 150); + let i2 = make(2, 2, 3, 100, 90); + let i3 = make(3, 3, 1, 100, 90); + let intents = [(&i1, 100u128), (&i2, 100), (&i3, 100)]; + + let mut graph = build_flow_graph(&intents); + let prices = unit_prices(&[1, 2, 3]); + let rings = detect_rings(&mut graph, &prices); + + assert_eq!( + rings.len(), + 0, + "ring must be rejected when one leg fails min-out at spot" + ); +} + +#[test] +fn test_no_4_cycle_detected() { + // 1→2, 2→3, 3→4, 4→1 — only 4-cycle exists. Current impl does not find it. + // This is a documentation test; if the algorithm is ever extended, update. + let i1 = make(1, 1, 2, 100, 90); + let i2 = make(2, 2, 3, 100, 90); + let i3 = make(3, 3, 4, 100, 90); + let i4 = make(4, 4, 1, 100, 90); + let intents = [(&i1, 100u128), (&i2, 100), (&i3, 100), (&i4, 100)]; + + let mut graph = build_flow_graph(&intents); + let prices = unit_prices(&[1, 2, 3, 4]); + let rings = detect_rings(&mut graph, &prices); + + assert_eq!(rings.len(), 0, "detect_rings currently only looks for 3-cycles"); +} diff --git a/ice/ice-solver/src/tests/v2_solver.rs b/ice/ice-solver/src/tests/v2_solver.rs new file mode 100644 index 0000000000..8398659987 --- /dev/null +++ b/ice/ice-solver/src/tests/v2_solver.rs @@ -0,0 +1,838 @@ +use crate::v2::Solver; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::{AMMInterface, TradeExecution}; +use hydradx_traits::router::{PoolEdge, Route, Trade}; +use ice_support::{ + AssetId, Balance, Intent, IntentData, IntentId, Partial, ResolvedIntent, SwapData, MAX_NUMBER_OF_RESOLVED_INTENTS, +}; +use sp_core::U256; + +// ---------- fixtures ---------- + +fn make_intent(id: IntentId, asset_in: AssetId, asset_out: AssetId, amount_in: Balance, min_out: Balance) -> Intent { + Intent { + id, + data: IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out: min_out, + partial: Partial::No, + }), + } +} + +fn make_partial(id: IntentId, asset_in: AssetId, asset_out: AssetId, amount_in: Balance, min_out: Balance) -> Intent { + make_partial_filled(id, asset_in, asset_out, amount_in, min_out, 0) +} + +fn make_partial_filled( + id: IntentId, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_out: Balance, + already_filled: Balance, +) -> Intent { + Intent { + id, + data: IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out: min_out, + partial: Partial::Yes(already_filled), + }), + } +} + +fn dummy_route(asset_in: u32, asset_out: u32) -> Route { + Route::try_from(vec![Trade { + pool: hydradx_traits::router::PoolType::Omnipool, + asset_in, + asset_out, + }]) + .unwrap() +} + +/// Mirrors the on-chain `validate_price_consistency` predicate: +/// two resolved intents in the same direction are rate-compatible iff +/// `|a.out * b.in - b.out * a.in| <= max(a.in, b.in)` — conservative bound +/// that tolerates 1-sat rounding in either expected-out calculation. +fn same_rate_within(a: &ResolvedIntent, b: &ResolvedIntent, tol: u128) -> bool { + let a_in = a.data.amount_in(); + let a_out = a.data.amount_out(); + let b_in = b.data.amount_in(); + let b_out = b.data.amount_out(); + let lhs = U256::from(a_out) * U256::from(b_in); + let rhs = U256::from(b_out) * U256::from(a_in); + let diff = if lhs >= rhs { lhs - rhs } else { rhs - lhs }; + // Normalise tolerance against the larger side: 1 sat of rounding on each side + // maps to at most max(a.in, b.in) in the cross-product. + let tol_scaled = U256::from(a_in.max(b_in)) * U256::from(tol); + diff <= tol_scaled +} + +/// Sum of `IntentData::surplus` — the formula the pallet uses to recompute score. +fn pallet_score(originals: &[Intent], resolved: &[ResolvedIntent]) -> Balance { + let mut total: Balance = 0; + for r in resolved { + let original = originals.iter().find(|i| i.id == r.id).unwrap(); + let surplus = original.data.surplus(&r.data).expect("surplus should be computable"); + total = total.saturating_add(surplus); + } + total +} + +// ---------- mocks ---------- + +/// 1:1 price, no slippage, zero existential deposit. +struct MockAMMOneToOne; + +impl AMMInterface for MockAMMOneToOne { + type Error = (); + type State = (); + + fn discover_routes(asset_in: u32, asset_out: u32, _s: &Self::State) -> Result>, Self::Error> { + Ok(vec![dummy_route(asset_in, asset_out)]) + } + + fn sell( + asset_in: u32, + asset_out: u32, + amount_in: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + Ok(( + (), + TradeExecution { + amount_in, + amount_out: amount_in, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn buy( + asset_in: u32, + asset_out: u32, + amount_out: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + Ok(( + (), + TradeExecution { + amount_in: amount_out, + amount_out, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn get_spot_price(_: u32, _: u32, _: Route, _: &Self::State) -> Result { + Ok(Ratio::new(1, 1)) + } + fn price_denominator() -> u32 { + 0 + } + fn pool_edges(_: &Self::State) -> Vec> { + Vec::new() + } +} + +/// Asset 1 is worth 2× asset 2; 1% slippage on every sell. +struct MockAMMWithSlippage; + +impl AMMInterface for MockAMMWithSlippage { + type Error = (); + type State = (); + + fn discover_routes(asset_in: u32, asset_out: u32, _s: &Self::State) -> Result>, Self::Error> { + Ok(vec![dummy_route(asset_in, asset_out)]) + } + + fn sell( + asset_in: u32, + asset_out: u32, + amount_in: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + let base_out = if asset_in == 1 && asset_out == 2 { + amount_in * 2 + } else if asset_in == 2 && asset_out == 1 { + amount_in / 2 + } else { + amount_in + }; + let amount_out = base_out * 99 / 100; + Ok(( + (), + TradeExecution { + amount_in, + amount_out, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn buy( + asset_in: u32, + asset_out: u32, + amount_out: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + let amount_in = if asset_in == 1 && asset_out == 2 { + amount_out / 2 + 1 + } else if asset_in == 2 && asset_out == 1 { + amount_out * 2 + 1 + } else { + amount_out + 1 + }; + Ok(( + (), + TradeExecution { + amount_in, + amount_out, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn get_spot_price(asset_in: u32, _: u32, _: Route, _: &Self::State) -> Result { + match asset_in { + 1 => Ok(Ratio::new(2, 1)), + 2 => Ok(Ratio::new(1, 1)), + _ => Ok(Ratio::new(1, 1)), + } + } + fn price_denominator() -> u32 { + 0 + } + fn pool_edges(_: &Self::State) -> Vec> { + Vec::new() + } +} + +/// Sell(1→2) fails for amount_in > 50. Other trades behave as 1:1. +struct MockAMMPartialFailure; + +impl AMMInterface for MockAMMPartialFailure { + type Error = (); + type State = (); + + fn discover_routes(asset_in: u32, asset_out: u32, _s: &Self::State) -> Result>, Self::Error> { + Ok(vec![dummy_route(asset_in, asset_out)]) + } + + fn sell( + asset_in: u32, + asset_out: u32, + amount_in: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + if asset_in == 1 && asset_out == 2 && amount_in > 50 { + return Err(()); + } + Ok(( + (), + TradeExecution { + amount_in, + amount_out: amount_in, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn buy( + asset_in: u32, + asset_out: u32, + amount_out: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + Ok(( + (), + TradeExecution { + amount_in: amount_out, + amount_out, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn get_spot_price(_: u32, _: u32, _: Route, _: &Self::State) -> Result { + Ok(Ratio::new(1, 1)) + } + fn price_denominator() -> u32 { + 0 + } + fn pool_edges(_: &Self::State) -> Vec> { + Vec::new() + } +} + +/// 1:1 price, zero slippage, existential deposit of 10 for every asset. +struct MockAMMWithED; + +impl AMMInterface for MockAMMWithED { + type Error = (); + type State = (); + + fn discover_routes(asset_in: u32, asset_out: u32, _s: &Self::State) -> Result>, Self::Error> { + Ok(vec![dummy_route(asset_in, asset_out)]) + } + + fn sell( + asset_in: u32, + asset_out: u32, + amount_in: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + Ok(( + (), + TradeExecution { + amount_in, + amount_out: amount_in, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn buy( + asset_in: u32, + asset_out: u32, + amount_out: u128, + _route: Route, + _state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + Ok(( + (), + TradeExecution { + amount_in: amount_out, + amount_out, + route: dummy_route(asset_in, asset_out), + }, + )) + } + + fn get_spot_price(_: u32, _: u32, _: Route, _: &Self::State) -> Result { + Ok(Ratio::new(1, 1)) + } + fn price_denominator() -> u32 { + 0 + } + fn pool_edges(_: &Self::State) -> Vec> { + Vec::new() + } + fn existential_deposit(_asset_id: AssetId) -> Balance { + 10 + } +} + +// ---------- v1 parity tests ---------- + +#[test] +fn test_solve_empty() { + let result = Solver::::solve(vec![], ()).unwrap(); + assert!(result.resolved_intents.is_empty()); +} + +#[test] +fn test_solve_single_intent() { + let intents = vec![make_intent(1, 1, 2, 100, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 1); + assert_eq!(result.trades.len(), 1); + assert_eq!(result.resolved_intents[0].data.amount_in(), 100); + assert_eq!(result.resolved_intents[0].data.amount_out(), 100); + assert_eq!(result.score, 10); +} + +#[test] +fn test_uniform_price_two_opposing() { + let intents = vec![make_intent(1, 1, 2, 100, 90), make_intent(2, 2, 1, 100, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + assert_eq!(result.trades.len(), 0); + assert_eq!(result.resolved_intents[0].data.amount_out(), 100); + assert_eq!(result.resolved_intents[1].data.amount_out(), 100); +} + +#[test] +fn test_scarce_side_gets_spot() { + let intents = vec![make_intent(1, 1, 2, 100, 180), make_intent(2, 2, 1, 100, 45)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + let alice = result.resolved_intents.iter().find(|r| r.id == 1).unwrap(); + let bob = result.resolved_intents.iter().find(|r| r.id == 2).unwrap(); + assert_eq!(bob.data.amount_out(), 50, "scarce side should get spot rate"); + assert!(alice.data.amount_out() < 200); + assert!(alice.data.amount_out() >= 195); +} + +#[test] +fn test_same_direction_uniform_rate() { + let intents = vec![ + make_intent(1, 1, 2, 100, 90), + make_intent(2, 1, 2, 200, 180), + make_intent(3, 1, 2, 50, 45), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 3); + let rates: Vec = result + .resolved_intents + .iter() + .map(|r| r.data.amount_out() as f64 / r.data.amount_in() as f64) + .collect(); + for rate in &rates[1..] { + let diff = (rate - rates[0]).abs() / rates[0]; + assert!(diff < 0.001, "Same-direction rates must be uniform, got diff {diff}"); + } +} + +#[test] +fn test_iterative_filtering() { + let intents = vec![ + make_intent(1, 1, 2, 100, 95), + make_intent(2, 2, 1, 100, 95), + make_intent(3, 1, 2, 100, 200), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + let ids: Vec<_> = result.resolved_intents.iter().map(|r| r.id).collect(); + assert!(ids.contains(&1)); + assert!(ids.contains(&2)); + assert!(!ids.contains(&3)); +} + +#[test] +fn test_no_opposing_flow() { + let intents = vec![make_intent(1, 1, 2, 100, 90), make_intent(2, 1, 2, 100, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + assert!(!result.trades.is_empty()); + assert_eq!(result.resolved_intents[0].data.amount_out(), 100); + assert_eq!(result.resolved_intents[1].data.amount_out(), 100); +} + +#[test] +fn test_perfect_match_cancel() { + let intents = vec![make_intent(1, 1, 2, 100, 90), make_intent(2, 2, 1, 100, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + assert_eq!(result.trades.len(), 0); +} + +#[test] +fn test_nonpartial_full_fill() { + let intents = vec![make_intent(1, 1, 2, 100, 90), make_intent(2, 2, 1, 100, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + for ri in &result.resolved_intents { + assert_eq!(ri.data.amount_in(), 100); + } +} + +#[test] +fn test_partial_intent_at_clearing() { + let intents = vec![make_partial(1, 1, 2, 200, 180), make_intent(2, 2, 1, 100, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + let r1 = result.resolved_intents.iter().find(|r| r.id == 1).unwrap(); + assert_eq!(r1.data.amount_in(), 200); + assert!(r1.data.amount_out() >= 180); +} + +#[test] +fn test_asymmetric_volumes_with_slippage() { + let intents = vec![make_partial(1, 1, 2, 200, 360), make_intent(2, 2, 1, 100, 45)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + let alice = result.resolved_intents.iter().find(|r| r.id == 1).unwrap(); + let bob = result.resolved_intents.iter().find(|r| r.id == 2).unwrap(); + assert_eq!(bob.data.amount_out(), 50); + assert!(alice.data.amount_out() < 400); + assert!(alice.data.amount_out() >= 390); +} + +#[test] +fn test_three_asset_ring() { + let intents = vec![ + make_intent(1, 1, 2, 100, 90), + make_intent(2, 2, 3, 100, 90), + make_intent(3, 3, 1, 100, 90), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 3); + assert_eq!(result.trades.len(), 0, "Ring trade should avoid AMM entirely"); + for ri in &result.resolved_intents { + assert_eq!(ri.data.amount_in(), 100); + assert_eq!(ri.data.amount_out(), 100); + } + assert_eq!(result.score, 30); +} + +#[test] +fn test_amm_failure_fallback() { + let intents = vec![make_intent(1, 1, 2, 200, 180), make_intent(2, 2, 1, 50, 45)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + assert_eq!(result.trades.len(), 0); + let r1 = result.resolved_intents.iter().find(|r| r.id == 1).unwrap(); + let r2 = result.resolved_intents.iter().find(|r| r.id == 2).unwrap(); + assert_eq!(r1.data.amount_out(), 200); + assert_eq!(r2.data.amount_out(), 50); +} + +#[test] +fn test_excess_backward_scarce_gets_spot() { + let intents = vec![make_intent(1, 2, 1, 100, 45), make_intent(2, 1, 2, 50, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + let alice = result.resolved_intents.iter().find(|r| r.id == 1).unwrap(); + let bob = result.resolved_intents.iter().find(|r| r.id == 2).unwrap(); + assert_eq!(bob.data.amount_out(), 100, "scarce A→B should get spot rate"); + assert!(alice.data.amount_out() > 0); + assert!(alice.data.amount_out() >= 45); +} + +#[test] +fn test_large_amounts_overflow_safe() { + let unit: Balance = 1_000_000_000_000; + let intents = vec![ + make_intent(1, 1, 2, 1_000_000 * unit, 900_000 * unit), + make_intent(2, 2, 1, 1_000_000 * unit, 900_000 * unit), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + assert_eq!(result.resolved_intents.len(), 2); + assert_eq!(result.trades.len(), 0); + for ri in &result.resolved_intents { + assert_eq!(ri.data.amount_in(), 1_000_000 * unit); + assert_eq!(ri.data.amount_out(), 1_000_000 * unit); + } +} + +// ---------- new v2-specific correctness tests ---------- + +/// Two partials in the same direction must resolve at the same rate within 1 sat +/// (the tolerance enforced by the pallet's `validate_price_consistency`). +#[test] +fn test_two_partials_same_direction_get_same_rate() { + let intents = vec![ + make_partial(1, 1, 2, 100, 80), + make_partial(2, 1, 2, 200, 150), + make_intent(3, 2, 1, 100, 90), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + let p1 = result.resolved_intents.iter().find(|r| r.id == 1).unwrap(); + let p2 = result.resolved_intents.iter().find(|r| r.id == 2).unwrap(); + assert!( + same_rate_within(p1, p2, 1), + "partials {} and {} must share rate within 1 sat: p1 {}→{}, p2 {}→{}", + p1.id, + p2.id, + p1.data.amount_in(), + p1.data.amount_out(), + p2.data.amount_in(), + p2.data.amount_out(), + ); +} + +/// The same input in a different order must produce the same fills. +/// Parameters are tight on purpose: tolerance 1.95 against 2.0 spot with 1% slippage +/// forces the binary search to bite, so order-of-fitting matters. +#[test] +fn test_partial_fill_order_independence() { + let forward = vec![ + make_partial(1, 1, 2, 100, 195), + make_partial(2, 1, 2, 100, 195), + make_intent(3, 2, 1, 20, 9), + ]; + let reversed = vec![ + make_intent(3, 2, 1, 20, 9), + make_partial(2, 1, 2, 100, 195), + make_partial(1, 1, 2, 100, 195), + ]; + let r1 = Solver::::solve(forward, ()).unwrap(); + let r2 = Solver::::solve(reversed, ()).unwrap(); + for id in [1u128, 2, 3] { + let a = r1.resolved_intents.iter().find(|r| r.id == id); + let b = r2.resolved_intents.iter().find(|r| r.id == id); + match (a, b) { + (Some(a), Some(b)) => { + assert_eq!(a.data.amount_in(), b.data.amount_in(), "intent {id} amount_in differs"); + assert_eq!( + a.data.amount_out(), + b.data.amount_out(), + "intent {id} amount_out differs" + ); + } + (None, None) => {} + _ => panic!("intent {id} presence differs between orderings"), + } + } +} + +/// Two identical partials in the same direction must receive identical fills +/// (not just identical rates). The Phase B sequential fit can produce +/// different fills for otherwise-identical partials when the clearing rate +/// degrades as more partials are added. +#[test] +fn test_identical_partials_get_identical_fills() { + // P1 and P2 are identical: 100→195 at 2:1 spot with 1% slippage (~1.98 realised). + // The 1.95 limit is above the combined-volume clearing, forcing binary-search to + // shrink one (or both) of them. Whichever is fitted second should be treated + // the same as the one fitted first. + let intents = vec![ + make_partial(1, 1, 2, 100, 195), + make_partial(2, 1, 2, 100, 195), + make_intent(3, 2, 1, 20, 9), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + let p1 = result.resolved_intents.iter().find(|r| r.id == 1); + let p2 = result.resolved_intents.iter().find(|r| r.id == 2); + match (p1, p2) { + (Some(a), Some(b)) => { + assert_eq!( + a.data.amount_in(), + b.data.amount_in(), + "identical partials got different fills: {} vs {}", + a.data.amount_in(), + b.data.amount_in(), + ); + } + (None, None) => {} // both excluded — still symmetric + _ => panic!("identical partials had asymmetric inclusion: p1={p1:?}, p2={p2:?}"), + } +} + +/// Partial and non-partial in the same direction must share a rate within 1 sat. +#[test] +fn test_partial_plus_non_partial_same_direction_uniform() { + let intents = vec![ + make_partial(1, 1, 2, 200, 180), + make_intent(2, 1, 2, 100, 90), + make_intent(3, 2, 1, 100, 90), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + let r1 = result.resolved_intents.iter().find(|r| r.id == 1).unwrap(); + let r2 = result.resolved_intents.iter().find(|r| r.id == 2).unwrap(); + assert!( + same_rate_within(r1, r2, 1), + "partial {} and non-partial {} must share rate within 1 sat: r1 {}→{}, r2 {}→{}", + r1.id, + r2.id, + r1.data.amount_in(), + r1.data.amount_out(), + r2.data.amount_in(), + r2.data.amount_out(), + ); +} + +/// Ring detection must not over-consume a partial's `remaining()`. With the bug, +/// ring treats the partial as having its full `amount_in` available, inflating the +/// user's output rate past what the AMM (1:1) can actually deliver. The cleanest +/// observable failure: the resolved `amount_out` must be at most `fill * spot_rate`. +#[test] +fn test_ring_respects_partial_remaining() { + // A→B partial: amount_in=100, already filled 60, so remaining=40. + // B→C and C→A are full 100 each. Without the fix, ring consumes 100 of A→B. + let intents = vec![ + make_partial_filled(1, 1, 2, 100, 90, 60), + make_intent(2, 2, 3, 100, 90), + make_intent(3, 3, 1, 100, 90), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + + let p = result.resolved_intents.iter().find(|r| r.id == 1); + if let Some(p) = p { + assert!( + p.data.amount_in() <= 40, + "ring must not fill more than remaining: got amount_in={}, remaining=40", + p.data.amount_in(), + ); + // At 1:1 spot, amount_out is capped by amount_in. + assert!( + p.data.amount_out() <= p.data.amount_in(), + "amount_out={} exceeds amount_in={} at 1:1 spot; ring over-consumed", + p.data.amount_out(), + p.data.amount_in(), + ); + } +} + +/// A partial whose `remaining()` is below the asset's ED must be filtered before Phase B. +#[test] +fn test_partial_below_ed_rejected() { + // ED = 10 (MockAMMWithED). remaining = amount_in - already_filled = 100 - 95 = 5 < 10. + let intents = vec![make_partial_filled(1, 1, 2, 100, 90, 95), make_intent(2, 2, 1, 100, 90)]; + let result = Solver::::solve(intents, ()).unwrap(); + assert!( + result.resolved_intents.iter().all(|r| r.id != 1), + "partial below ED must be filtered; got {:?}", + result.resolved_intents.iter().map(|r| r.id).collect::>(), + ); +} + +/// A partial must never be filled in a way that leaves 0 < remaining < ed. +#[test] +fn test_partial_leaves_no_untradeable_dust() { + let intents = vec![ + // original amount 100, ED 10: a fill of 95 would leave remaining=5 which < ED. + // Solver must either fill all 100 or cap at ≤90. + make_partial(1, 1, 2, 100, 90), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + if let Some(r) = result.resolved_intents.iter().find(|r| r.id == 1) { + let original = 100u128; + let filled = r.data.amount_in(); + let remaining_after = original - filled; + let ed = 10u128; + assert!( + remaining_after == 0 || remaining_after >= ed, + "partial fill left untradeable dust: fill={filled}, remaining_after={remaining_after}, ed={ed}", + ); + } +} + +/// After the `remaining_untradeable` retry, the chosen fill must still satisfy ed_out. +#[test] +fn test_partial_retry_honors_ed_out() { + // All resolved outputs must be ≥ ed_out = 10. + let intents = vec![ + make_partial(1, 1, 2, 100, 90), + make_partial(2, 1, 2, 50, 45), + make_intent(3, 2, 1, 100, 90), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + for r in &result.resolved_intents { + assert!( + r.data.amount_out() >= 10, + "resolved intent {} has amount_out={} below ed_out=10", + r.id, + r.data.amount_out(), + ); + } +} + +/// When more than MAX_NUMBER_OF_RESOLVED_INTENTS fills are viable, the cap should +/// keep the highest-surplus intents, not the first N by input order. +#[test] +fn test_cap_by_surplus_not_input_order() { + let mut intents: Vec = (0..MAX_NUMBER_OF_RESOLVED_INTENTS as u128) + .map(|id| make_intent(id + 1, 1, 2, 100, 99)) + .collect(); + // A high-surplus opposite-direction intent at the end — should survive any cap. + intents.push(make_intent(u128::MAX, 2, 1, 100, 10)); + let result = Solver::::solve(intents, ()).unwrap(); + assert!( + result.resolved_intents.iter().any(|r| r.id == u128::MAX), + "high-surplus intent was dropped by first-N cap", + ); +} + +/// The solver's `solution.score` must equal the pallet's recompute over all +/// resolved intents, exactly. +#[test] +fn test_score_matches_pallet_recompute() { + let intents = vec![ + make_intent(1, 1, 2, 100, 90), + make_intent(2, 2, 1, 100, 90), + make_partial(3, 1, 2, 200, 180), + make_intent(4, 1, 2, 50, 40), + ]; + let result = Solver::::solve(intents.clone(), ()).unwrap(); + let pallet_recomputed = pallet_score(&intents, result.resolved_intents.as_slice()); + assert_eq!( + result.score, pallet_recomputed, + "solver score {} diverges from pallet recompute {}", + result.score, pallet_recomputed, + ); +} + +/// Every resolved intent's amount_in and amount_out must be ≥ ED for their assets. +#[test] +fn test_all_resolved_amounts_above_ed() { + let intents = vec![ + make_intent(1, 1, 2, 100, 90), + make_intent(2, 2, 1, 100, 90), + make_partial(3, 1, 2, 200, 180), + ]; + let result = Solver::::solve(intents, ()).unwrap(); + for r in &result.resolved_intents { + assert!( + r.data.amount_in() >= 10, + "intent {} amount_in {} < ed", + r.id, + r.data.amount_in() + ); + assert!( + r.data.amount_out() >= 10, + "intent {} amount_out {} < ed", + r.id, + r.data.amount_out() + ); + } +} + +/// Accumulating many large intents must not panic from unchecked overflow. +/// Individual intents are below `u128::MAX / 100` so the pair-per-direction +/// totals can't exceed u128, but summing across many directions in the same +/// call touches the DirAccum path. +#[test] +fn test_saturating_accumulation() { + let per_intent: Balance = u128::MAX / 1_000; // safely representable per-intent + let mut intents = Vec::new(); + for i in 0..50u128 { + intents.push(make_intent(i * 2 + 1, 1, 2, per_intent, per_intent / 2)); + intents.push(make_intent(i * 2 + 2, 2, 1, per_intent, per_intent / 2)); + } + // Must not panic. + let _ = Solver::::solve(intents, ()).unwrap(); +} + +/// Simulate two solver calls on the same partial intent, emulating two +/// on-chain rounds. The second call's resolved amount_in must not exceed +/// the remaining after the first fill. +#[test] +fn test_cumulative_partial_fill_across_calls() { + let original_amount_in: Balance = 200; + let intent1 = make_partial(1, 1, 2, original_amount_in, 150); + let opposite = make_intent(2, 2, 1, 100, 90); + + let r1 = Solver::::solve(vec![intent1, opposite.clone()], ()).unwrap(); + let first_fill = r1 + .resolved_intents + .iter() + .find(|r| r.id == 1) + .map(|r| r.data.amount_in()) + .unwrap_or(0); + assert!(first_fill > 0, "first call should resolve at least some of the partial"); + assert!(first_fill <= original_amount_in); + + // Pallet would advance `filled` by first_fill. Second call sees remaining = original - first_fill. + if first_fill < original_amount_in { + let intent2 = make_partial_filled(1, 1, 2, original_amount_in, 150, first_fill); + let r2 = Solver::::solve(vec![intent2, opposite], ()).unwrap(); + let second_fill = r2 + .resolved_intents + .iter() + .find(|r| r.id == 1) + .map(|r| r.data.amount_in()) + .unwrap_or(0); + let remaining_after_first = original_amount_in - first_fill; + assert!( + second_fill <= remaining_after_first, + "second-call fill {second_fill} exceeds remaining {remaining_after_first}", + ); + assert!( + first_fill + second_fill <= original_amount_in, + "cumulative fill {first_fill} + {second_fill} > original {original_amount_in}", + ); + } +} diff --git a/ice/ice-solver/src/v2/mod.rs b/ice/ice-solver/src/v2/mod.rs new file mode 100644 index 0000000000..f0f81b4a41 --- /dev/null +++ b/ice/ice-solver/src/v2/mod.rs @@ -0,0 +1,3 @@ +mod solver; + +pub use solver::Solver; diff --git a/ice/ice-solver/src/v2/solver.rs b/ice/ice-solver/src/v2/solver.rs new file mode 100644 index 0000000000..57f59875c1 --- /dev/null +++ b/ice/ice-solver/src/v2/solver.rs @@ -0,0 +1,1408 @@ +//! ICE Solver v2 — Per-Direction Clearing Prices with Partial Fills +//! +//! Extends v1 with variable fill amounts for partial intents. +//! Non-partial intents behave identically to v1 (binary include/exclude). +//! +//! Partial fill algorithm: +//! 1. Include all non-partial intents that pass clearing (same as v1) +//! 2. For each partial intent: binary search for maximum fill amount +//! where `clearing_rate(total_volume) >= minimum_rate` +//! 3. ED guard: don't leave remaining < existential deposit +//! +//! Everything else (ring detection, AMM trades, unified rates, stabilization) +//! is identical to v1. + +use crate::common; +use crate::common::flow_graph; +use crate::common::ring_detection; +use crate::common::FlowDirection; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::AMMInterface; +use hydradx_traits::router::Route; +use ice_support::{ + AssetId, Balance, Intent, IntentData, IntentId, PoolTrade, ResolvedIntent, ResolvedIntents, Solution, + SolutionTrades, SwapData, SwapType, MAX_NUMBER_OF_RESOLVED_INTENTS, +}; +use sp_core::U256; +use sp_std::collections::btree_map::BTreeMap; +use sp_std::collections::btree_set::BTreeSet; +use sp_std::marker::PhantomData; +use sp_std::vec; +use sp_std::vec::Vec; + +pub struct Solver { + _phantom: PhantomData, +} + +/// Unordered pair key. +type AssetPair = (AssetId, AssetId); + +/// Intents grouped by direction: (forward A→B, backward B→A). +type DirectionGroups = (Vec, Vec); + +/// Per-direction clearing rates for an unordered pair (A, B). +#[derive(Debug, Clone)] +struct PairClearing { + /// A→B direction: rate = n/d (B received per A sold) + forward_n: U256, + forward_d: U256, + /// B→A direction: rate = n/d (A received per B sold) + backward_n: U256, + backward_d: U256, +} + +/// A resolved intent with its fill amount (may be less than amount_in for partial intents). +#[derive(Debug, Clone)] +struct IntentFill<'a> { + intent: &'a Intent, + /// How much of amount_in to fill in this solution. + fill_amount: Balance, +} + +/// `(amount_in, amount_out)` accumulated from ring matches for a single intent. +type RingFill = (Balance, Balance); + +/// Per-direction accumulator used to blend ring fills with AMM output when +/// computing unified rates. +#[derive(Default)] +struct DirAccum { + total_in: Balance, + ring_in: Balance, + ring_out: Balance, +} + +fn empty_solution() -> Solution { + Solution { + resolved_intents: ResolvedIntents::truncate_from(Vec::new()), + trades: SolutionTrades::truncate_from(Vec::new()), + score: 0, + } +} + +fn unordered_pair(a: AssetId, b: AssetId) -> (AssetId, AssetId) { + if a <= b { + (a, b) + } else { + (b, a) + } +} + +/// Compute `amount_in * n / d` (integer floor), saturating to 0 on overflow or +/// division by zero. A zero denominator is always a bug — by construction every +/// clearing rate has a positive denominator — so we log at `warn!` when it +/// happens to aid diagnosis. +fn apply_rate(amount_in: Balance, n: U256, d: U256) -> Balance { + if d.is_zero() { + log::warn!( + target: "solver::v2", + "apply_rate called with zero denominator (amount_in={amount_in}, n={n}); returning 0", + ); + return 0; + } + common::mul_div(U256::from(amount_in), n, d) + .and_then(|v| v.try_into().ok()) + .unwrap_or(0) +} + +/// Same tolerance as v1. +const AMM_SIMULATION_TOLERANCE_BPS: Balance = 1; + +fn adjust_amm_output(simulated_out: Balance) -> Balance { + simulated_out.saturating_sub(simulated_out * AMM_SIMULATION_TOLERANCE_BPS / 10_000) +} + +/// Compute minimum rate for an intent: amount_out / amount_in. +/// Uses original (immutable) values, not remaining. +fn min_rate(swap: &SwapData) -> (U256, U256) { + (U256::from(swap.amount_out), U256::from(swap.amount_in)) +} + +impl Solver { + fn select_best_route( + routes: &[Route], + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + state: &A::State, + ) -> Option<(Route, Balance, A::State)> { + let best = routes + .iter() + .filter_map( + |route| match A::sell(asset_in, asset_out, amount_in, route.clone(), state) { + Ok((new_state, exec)) => Some((route.clone(), exec.amount_out, new_state)), + Err(_) => None, + }, + ) + .max_by_key(|(_, amount_out, _)| *amount_out); + + if let Some((ref route, amount_out, _)) = best { + log::debug!(target: "solver::v2", "best route for {} -> {}: {} hops, amount_out: {}", + asset_in, asset_out, route.as_slice().len(), amount_out); + } + best + } + + /// Get the effective amount to use for an intent in flow calculations. + /// For partial intents, this is the remaining (unfilled) amount. + fn effective_amount(swap: &SwapData) -> Balance { + swap.remaining() + } + + /// Pre-compute spot prices for every asset appearing in the intent set, + /// denominated in `A::price_denominator()`. Each asset is priced via the + /// highest-rate available route to the denominator. Assets without a viable + /// route are simply absent from the returned map; callers fall back to + /// simulation or conservatively reject such intents. + fn collect_spot_prices(intents: &[Intent], state: &A::State) -> BTreeMap { + let denominator = A::price_denominator(); + let mut spot_prices: BTreeMap = BTreeMap::new(); + spot_prices.insert(denominator, Ratio::one()); + + let assets = common::collect_unique_assets(intents); + for asset in assets { + if asset == denominator { + continue; + } + let Ok(price_routes) = A::discover_routes(asset, denominator, state) else { + continue; + }; + for route in price_routes { + let Ok(price) = A::get_spot_price(asset, denominator, route, state) else { + continue; + }; + let better = spot_prices.get(&asset).is_none_or(|existing| { + U256::from(price.n).saturating_mul(U256::from(existing.d)) + > U256::from(existing.n).saturating_mul(U256::from(price.d)) + }); + if better { + spot_prices.insert(asset, price); + } + } + } + spot_prices + } + + /// Decide whether an intent can plausibly be resolved in this round. + /// + /// Preference order: + /// 1. Non-swap intents are dropped. + /// 2. Intents with zero effective amount (fully filled partials) are dropped. + /// 3. If route simulation at the effective amount meets the pro-rata minimum, + /// the intent is kept — this is authoritative and avoids relying on spot. + /// 4. If simulation fails but the intent is partial, keep it; joint fit will + /// find a smaller viable fill. + /// 5. Otherwise fall back to spot-price feasibility. An intent with an unknown + /// spot price for either leg is rejected conservatively. + fn is_satisfiable(intent: &Intent, spot_prices: &BTreeMap, state: &A::State) -> bool { + let IntentData::Swap(swap) = &intent.data else { + return false; + }; + let check_amount = Self::effective_amount(swap); + if check_amount == 0 { + log::debug!(target: "solver::v2", "intent {}: fully filled, skipping", intent.id); + return false; + } + + if let Ok(routes) = A::discover_routes(swap.asset_in, swap.asset_out, state) { + if let Some((_, amount_out, _)) = + Self::select_best_route(&routes, swap.asset_in, swap.asset_out, check_amount, state) + { + let pro_rata_min = apply_rate(check_amount, U256::from(swap.amount_out), U256::from(swap.amount_in)); + if amount_out >= pro_rata_min { + return true; + } + log::debug!(target: "solver::v2", "intent {}: route output {} < pro_rata_min {} for {} -> {}", + intent.id, amount_out, pro_rata_min, swap.asset_in, swap.asset_out); + } + } + + if swap.partial.is_partial() { + // Partials can fit a smaller fill — joint fit will decide. + return true; + } + + let ok = common::is_satisfiable(intent, spot_prices); + if !ok { + log::debug!(target: "solver::v2", "intent {}: unsatisfiable at spot price", intent.id); + } + ok + } + + /// Split satisfiable intents into partial and non-partial `IntentFill`s, each + /// seeded with its effective (unfilled) amount. Intents with zero effective + /// amount are dropped. + fn initial_fill_plan<'a>(satisfiable: &[&'a Intent]) -> (Vec>, Vec>) { + let mut non_partial_fills: Vec> = Vec::new(); + let mut partial_fills: Vec> = Vec::new(); + for &intent in satisfiable { + let IntentData::Swap(swap) = &intent.data else { + continue; + }; + let fill_amount = Self::effective_amount(swap); + if fill_amount == 0 { + continue; + } + let fill = IntentFill { intent, fill_amount }; + if swap.partial.is_partial() { + partial_fills.push(fill); + } else { + non_partial_fills.push(fill); + } + } + (non_partial_fills, partial_fills) + } + + /// Iteratively remove non-partial fills whose per-direction clearing output + /// falls below the intent's absolute `amount_out` minimum. Converges quickly + /// because each round can only drop intents; the clearing rate then improves + /// (less volume through the AMM) for the survivors. + fn stabilize_non_partials<'a>( + non_partial_fills: &mut Vec>, + spot_prices: &BTreeMap, + state: &A::State, + ) { + const MAX_ITERATIONS: u32 = 10; + let mut pair_clearings: BTreeMap = BTreeMap::new(); + + for _iteration in 0..MAX_ITERATIONS { + pair_clearings.clear(); + + let mut pair_groups: BTreeMap>> = BTreeMap::new(); + for fill in non_partial_fills.iter() { + let IntentData::Swap(swap) = &fill.intent.data else { + continue; + }; + let up = unordered_pair(swap.asset_in, swap.asset_out); + let entry = pair_groups.entry(up).or_default(); + if swap.asset_in == up.0 { + entry.0.push(fill); + } else { + entry.1.push(fill); + } + } + + for (&(asset_a, asset_b), (forward, backward)) in &pair_groups { + if let Some(c) = + Self::compute_pair_clearing_with_fills(asset_a, asset_b, forward, backward, spot_prices, state) + { + pair_clearings.insert((asset_a, asset_b), c); + } + } + + let before_count = non_partial_fills.len(); + non_partial_fills.retain(|fill| { + let IntentData::Swap(swap) = &fill.intent.data else { + return true; + }; + let up = unordered_pair(swap.asset_in, swap.asset_out); + let Some(clearing) = pair_clearings.get(&up) else { + return true; + }; + let (rate_n, rate_d) = if swap.asset_in == up.0 { + (clearing.forward_n, clearing.forward_d) + } else { + (clearing.backward_n, clearing.backward_d) + }; + let amount_out = apply_rate(fill.fill_amount, rate_n, rate_d); + if amount_out < swap.amount_out { + log::debug!(target: "solver::v2", "intent {}: filtered out — clearing output {} < min_out {}", + fill.intent.id, amount_out, swap.amount_out); + return false; + } + true + }); + + if non_partial_fills.len() == before_count { + break; + } + } + } + + pub fn solve(intents: Vec, initial_state: A::State) -> Result { + if intents.is_empty() { + return Ok(empty_solution()); + } + + log::debug!(target: "solver::v2", "solve() called with {} intents", intents.len()); + + // 1. Pre-compute spot prices once for every asset that appears in any intent. + let spot_prices = Self::collect_spot_prices(&intents, &initial_state); + + // 2. Filter satisfiable intents. The simulation step is authoritative — if the + // AMM can fulfil the (pro-rata) minimum at the intent's effective volume, keep + // the intent. Only fall back to the spot-price check when simulation fails. + let satisfiable_intents: Vec<&Intent> = intents + .iter() + .filter(|intent| Self::is_satisfiable(intent, &spot_prices, &initial_state)) + .collect(); + + log::debug!(target: "solver::v2", "satisfiable: {}/{} intents", satisfiable_intents.len(), intents.len()); + + if satisfiable_intents.is_empty() { + return Ok(empty_solution()); + } + + if satisfiable_intents.len() == 1 { + return Self::solve_single_intent(satisfiable_intents[0], &initial_state); + } + + // 3. Build the initial fill plan and split by partial/non-partial. + let (mut non_partial_fills, partial_fills) = Self::initial_fill_plan(&satisfiable_intents); + + // Phase A: iteratively drop non-partials that fail at the combined clearing rate. + Self::stabilize_non_partials(&mut non_partial_fills, &spot_prices, &initial_state); + + // Phase B: joint per-pair partial-fill clearing. For each unordered pair (A, B), + // binary-search a single scale factor `t ∈ [0,1]` applied uniformly to the + // partials in both directions. The clearing rate at + // (fixed_f + t·V_f_max, fixed_b + t·V_b_max) is monotonic in `t`, so we find the + // largest `t` where both directions' clearing rates still satisfy the *tightest* + // per-direction pro-rata minimum. Each partial then gets `remaining() · t`, which + // restores same-direction-same-fill-fraction and removes the order dependence of + // the previous per-partial sequential fit. + let mut fills = non_partial_fills; + Self::fit_partials_jointly(&mut fills, partial_fills, &spot_prices, &initial_state); + + if fills.is_empty() { + log::debug!(target: "solver::v2", "all intents filtered out during iterative clearing"); + return Ok(empty_solution()); + } + + log::debug!(target: "solver::v2", "after iterative clearing: {} fills remaining", fills.len()); + + // Cap to MAX_NUMBER_OF_RESOLVED_INTENTS. `ResolvedIntents::truncate_from` would + // silently drop any overflow after score is computed, so we have to truncate up + // front. Sort by estimated surplus descending first so the N best intents — not + // just the first N by input order — survive the cap. + if fills.len() > MAX_NUMBER_OF_RESOLVED_INTENTS as usize { + log::debug!(target: "solver::v2", "capping fills from {} to {} (keeping highest surplus)", + fills.len(), MAX_NUMBER_OF_RESOLVED_INTENTS); + Self::sort_by_estimated_surplus(&mut fills, &spot_prices, &initial_state); + fills.truncate(MAX_NUMBER_OF_RESOLVED_INTENTS as usize); + } + + // Convert fills to included intents for the rest of the pipeline + // (ring detection, AMM trades, unified rates, resolution) + let mut included: Vec<&Intent> = fills.iter().map(|f| f.intent).collect(); + // Track fill amounts separately for resolution + let fill_amounts: BTreeMap = fills.iter().map(|f| (f.intent.id, f.fill_amount)).collect(); + + if included.len() == 1 { + let intent = included[0]; + let fill = fill_amounts.get(&intent.id).copied().unwrap_or(0); + return Self::solve_single_intent_with_fill(intent, fill, &initial_state); + } + + // Stabilization loop: ring detection → AMM trades → unified rates → resolution. + // Intents dropped during resolution trigger a retry with the reduced set. + const MAX_STABILIZATION_ROUNDS: u32 = 5; + + for stabilization_round in 0..MAX_STABILIZATION_ROUNDS { + log::debug!(target: "solver::v2", "stabilization round {}, {} included intents", + stabilization_round, included.len()); + + // Ring detection — cap each intent's volume at its solver-decided fill_amount + // (falling back to `swap.remaining()` for anything that somehow isn't in + // fill_amounts). Without this, ring detection could match more volume than + // the user has reserved or the solver has allocated. + let graph_entries: Vec<(&Intent, Balance)> = included + .iter() + .map(|intent| { + let cap = match &intent.data { + IntentData::Swap(swap) => fill_amounts + .get(&intent.id) + .copied() + .unwrap_or_else(|| swap.remaining()), + _ => 0, + }; + (*intent, cap) + }) + .collect(); + let mut graph = flow_graph::build_flow_graph(&graph_entries); + let rings = ring_detection::detect_rings(&mut graph, &spot_prices); + + let mut ring_fills: BTreeMap = BTreeMap::new(); + for ring in &rings { + for (_pair, ring_fill_list) in &ring.edges { + for fill in ring_fill_list { + let entry = ring_fills.entry(fill.intent_id).or_default(); + entry.0 = entry.0.saturating_add(fill.amount_in); + entry.1 = entry.1.saturating_add(fill.amount_out); + } + } + } + + // AMM trades for net imbalances + let mut state = initial_state.clone(); + let mut executed_trades: Vec = Vec::new(); + + let mut pair_groups: BTreeMap> = BTreeMap::new(); + for intent in &included { + let IntentData::Swap(swap) = &intent.data else { + continue; + }; + let up = unordered_pair(swap.asset_in, swap.asset_out); + let entry = pair_groups.entry(up).or_default(); + if swap.asset_in == up.0 { + entry.0.push((intent.id, swap)); + } else { + entry.1.push((intent.id, swap)); + } + } + + let mut directed_rates: BTreeMap = BTreeMap::new(); + + for (&(asset_a, asset_b), (forward, backward)) in &pair_groups { + // Use fill_amounts for volume calculation instead of raw amount_in + let total_a_sold: Balance = forward + .iter() + .map(|(id, swap)| { + let base = fill_amounts.get(id).copied().unwrap_or(swap.remaining()); + base.saturating_sub(ring_fills.get(id).map(|(a, _)| *a).unwrap_or(0)) + }) + .sum(); + + let total_b_sold: Balance = backward + .iter() + .map(|(id, swap)| { + let base = fill_amounts.get(id).copied().unwrap_or(swap.remaining()); + base.saturating_sub(ring_fills.get(id).map(|(a, _)| *a).unwrap_or(0)) + }) + .sum(); + + if total_a_sold == 0 && total_b_sold == 0 { + continue; + } + + let Some(pa) = spot_prices.get(&asset_a) else { + continue; + }; + let Some(pb) = spot_prices.get(&asset_b) else { + continue; + }; + + let flow = common::analyze_pair_flow(total_a_sold, total_b_sold, pa, pb); + + match flow { + FlowDirection::SingleForward { amount } => { + if amount < A::existential_deposit(asset_a) { + log::debug!(target: "solver::v2", "single forward {asset_a} -> {asset_b}: amount {amount} below ED"); + } else if let Some((route, amount_out, new_state)) = + A::discover_routes(asset_a, asset_b, &state) + .ok() + .and_then(|routes| Self::select_best_route(&routes, asset_a, asset_b, amount, &state)) + { + let adjusted_out = adjust_amm_output(amount_out); + directed_rates.insert((asset_a, asset_b), Ratio::new(adjusted_out, amount)); + executed_trades.push(PoolTrade { + direction: SwapType::ExactIn, + amount_in: amount, + amount_out: adjusted_out, + route, + }); + state = new_state; + } + } + FlowDirection::SingleBackward { amount } => { + if amount < A::existential_deposit(asset_b) { + log::debug!(target: "solver::v2", "single backward {asset_b} -> {asset_a}: amount {amount} below ED"); + } else if let Some((route, amount_out, new_state)) = + A::discover_routes(asset_b, asset_a, &state) + .ok() + .and_then(|routes| Self::select_best_route(&routes, asset_b, asset_a, amount, &state)) + { + let adjusted_out = adjust_amm_output(amount_out); + directed_rates.insert((asset_b, asset_a), Ratio::new(adjusted_out, amount)); + executed_trades.push(PoolTrade { + direction: SwapType::ExactIn, + amount_in: amount, + amount_out: adjusted_out, + route, + }); + state = new_state; + } + } + FlowDirection::ExcessForward { + scarce_out, + direct_match, + net_sell, + } => { + if total_b_sold > 0 { + directed_rates.insert((asset_b, asset_a), Ratio::new(scarce_out, total_b_sold)); + } + if net_sell < A::existential_deposit(asset_a) { + if total_a_sold > 0 { + directed_rates.insert((asset_a, asset_b), Ratio::new(direct_match, total_a_sold)); + } + } else { + let best = A::discover_routes(asset_a, asset_b, &state).ok().and_then(|routes| { + Self::select_best_route(&routes, asset_a, asset_b, net_sell, &state) + }); + match best { + Some((route, amount_out, new_state)) => { + let adjusted_out = adjust_amm_output(amount_out); + let total_out = direct_match.saturating_add(adjusted_out); + if total_a_sold > 0 { + directed_rates.insert((asset_a, asset_b), Ratio::new(total_out, total_a_sold)); + } + executed_trades.push(PoolTrade { + direction: SwapType::ExactIn, + amount_in: net_sell, + amount_out: adjusted_out, + route, + }); + state = new_state; + } + None => { + let fallback = common::calc_amount_out(total_a_sold, pa, pb).unwrap_or(0); + if total_a_sold > 0 { + directed_rates.insert((asset_a, asset_b), Ratio::new(fallback, total_a_sold)); + } + } + } + } + } + FlowDirection::ExcessBackward { + scarce_out, + direct_match, + net_sell, + } => { + if total_a_sold > 0 { + directed_rates.insert((asset_a, asset_b), Ratio::new(scarce_out, total_a_sold)); + } + if net_sell < A::existential_deposit(asset_b) { + if total_b_sold > 0 { + directed_rates.insert((asset_b, asset_a), Ratio::new(direct_match, total_b_sold)); + } + } else { + let best = A::discover_routes(asset_b, asset_a, &state).ok().and_then(|routes| { + Self::select_best_route(&routes, asset_b, asset_a, net_sell, &state) + }); + match best { + Some((route, amount_out, new_state)) => { + let adjusted_out = adjust_amm_output(amount_out); + let total_out = direct_match.saturating_add(adjusted_out); + if total_b_sold > 0 { + directed_rates.insert((asset_b, asset_a), Ratio::new(total_out, total_b_sold)); + } + executed_trades.push(PoolTrade { + direction: SwapType::ExactIn, + amount_in: net_sell, + amount_out: adjusted_out, + route, + }); + state = new_state; + } + None => { + let fallback = common::calc_amount_out(total_b_sold, pb, pa).unwrap_or(0); + if total_b_sold > 0 { + directed_rates.insert((asset_b, asset_a), Ratio::new(fallback, total_b_sold)); + } + } + } + } + } + FlowDirection::PerfectCancel { a_as_b, b_as_a } => { + if total_a_sold > 0 { + directed_rates.insert((asset_a, asset_b), Ratio::new(a_as_b, total_a_sold)); + } + if total_b_sold > 0 { + directed_rates.insert((asset_b, asset_a), Ratio::new(b_as_a, total_b_sold)); + } + } + } + } + + // Unified rates + let mut unified_rates: BTreeMap = BTreeMap::new(); + { + let mut accum: BTreeMap = BTreeMap::new(); + + for intent in &included { + let IntentData::Swap(swap) = &intent.data else { + continue; + }; + let key = (swap.asset_in, swap.asset_out); + let entry = accum.entry(key).or_default(); + let fill = fill_amounts.get(&intent.id).copied().unwrap_or(swap.remaining()); + entry.total_in = entry.total_in.saturating_add(fill); + let (ri, ro) = ring_fills.get(&intent.id).copied().unwrap_or((0, 0)); + entry.ring_in = entry.ring_in.saturating_add(ri); + entry.ring_out = entry.ring_out.saturating_add(ro); + } + + for (key, dir) in &accum { + let remaining_in = dir.total_in.saturating_sub(dir.ring_in); + + let amm_out = if remaining_in > 0 { + if let Some(rate) = directed_rates.get(key) { + apply_rate(remaining_in, U256::from(rate.n), U256::from(rate.d)) + } else { + 0 + } + } else { + 0 + }; + + let total_out = dir.ring_out.saturating_add(amm_out); + if dir.total_in > 0 && total_out > 0 { + unified_rates.insert(*key, Ratio::new(total_out, dir.total_in)); + } + } + } + + // Resolve intents using unified rate and fill amounts + let mut canonical_prices: BTreeMap = BTreeMap::new(); + let mut resolved_intents: Vec = Vec::new(); + let mut total_score: Balance = 0; + + for intent in &included { + let IntentData::Swap(swap) = &intent.data else { + continue; + }; + let directed_key = (swap.asset_in, swap.asset_out); + let fill = fill_amounts.get(&intent.id).copied().unwrap_or(swap.remaining()); + + let total_out = if let Some(canonical) = canonical_prices.get(&directed_key) { + apply_rate(fill, U256::from(canonical.n), U256::from(canonical.d)) + } else if let Some(rate) = unified_rates.get(&directed_key) { + let amount_out = apply_rate(fill, U256::from(rate.n), U256::from(rate.d)); + if fill > 0 && amount_out > 0 { + canonical_prices.insert(directed_key, Ratio::new(amount_out, fill)); + } + amount_out + } else { + 0 + }; + + if fill == 0 || total_out == 0 { + continue; + } + + // Existential-deposit guard. A resolved intent whose `amount_in` + // or `amount_out` is below its asset's ED is rejected on-chain + // with `InvalidAmount` — so the solver must drop it here. The + // stabilization loop will retry without this intent and the + // clearing rate on this pair will improve for the survivors. + let ed_in = A::existential_deposit(swap.asset_in); + let ed_out = A::existential_deposit(swap.asset_out); + if fill < ed_in || total_out < ed_out { + log::debug!( + target: "solver::v2", + "intent {}: dropped — fill={} (ed_in={}) or total_out={} (ed_out={}) below ED", + intent.id, fill, ed_in, total_out, ed_out, + ); + continue; + } + + // Pro-rata minimum for this fill amount + let min_required = apply_rate(fill, U256::from(swap.amount_out), U256::from(swap.amount_in)); + + if total_out < min_required { + log::debug!(target: "solver::v2", "intent {}: skipped — output {} < pro_rata_min {} for fill {}", + intent.id, total_out, min_required, fill); + continue; + } + + let surplus = total_out.saturating_sub(min_required); + total_score = total_score.saturating_add(surplus); + + resolved_intents.push(ResolvedIntent { + id: intent.id, + data: IntentData::Swap(SwapData { + asset_in: swap.asset_in, + asset_out: swap.asset_out, + amount_in: fill, + amount_out: total_out, + partial: swap.partial, + }), + }); + } + + log::debug!(target: "solver::v2", "stabilization round {}: {} resolved, {} trades, score: {} (from {} included)", + stabilization_round, resolved_intents.len(), executed_trades.len(), total_score, included.len()); + + if resolved_intents.len() == included.len() { + return Ok(Solution { + resolved_intents: ResolvedIntents::truncate_from(resolved_intents), + trades: SolutionTrades::truncate_from(executed_trades), + score: total_score, + }); + } + + // Shrink and retry + let resolved_ids: BTreeSet = resolved_intents.iter().map(|r| r.id).collect(); + included.retain(|intent| resolved_ids.contains(&intent.id)); + + log::debug!(target: "solver::v2", "stabilization round {}: dropped intents, {} remaining", + stabilization_round, included.len()); + + if included.is_empty() { + return Ok(empty_solution()); + } + if included.len() == 1 { + let intent = included[0]; + let fill = fill_amounts.get(&intent.id).copied().unwrap_or(0); + return Self::solve_single_intent_with_fill(intent, fill, &initial_state); + } + } + + log::warn!(target: "solver::v2", "stabilization did not converge after {MAX_STABILIZATION_ROUNDS} rounds"); + Ok(empty_solution()) + } + + /// Single intent path, supporting partial fills. + fn solve_single_intent(intent: &Intent, initial_state: &A::State) -> Result { + let IntentData::Swap(swap) = &intent.data else { + return Ok(empty_solution()); + }; + let fill = Self::effective_amount(swap); + Self::solve_single_intent_with_fill(intent, fill, initial_state) + } + + /// Single intent with a specific fill amount. + fn solve_single_intent_with_fill( + intent: &Intent, + fill: Balance, + initial_state: &A::State, + ) -> Result { + let IntentData::Swap(swap) = &intent.data else { + return Ok(empty_solution()); + }; + + if fill == 0 { + return Ok(empty_solution()); + } + + log::debug!(target: "solver::v2", "solving single intent {}: {} -> {}, fill: {}, min_rate: {}/{}", + intent.id, swap.asset_in, swap.asset_out, fill, swap.amount_out, swap.amount_in); + + let routes = A::discover_routes(swap.asset_in, swap.asset_out, initial_state)?; + + // For partial intents, try the fill amount first. If it doesn't meet minimum, + // binary search for the maximum fill that does. + let result = if swap.partial.is_partial() { + Self::find_best_partial_fill(swap, fill, &routes, initial_state) + } else { + // Non-partial: try full amount or nothing + let Some((route, amount_out, _)) = + Self::select_best_route(&routes, swap.asset_in, swap.asset_out, fill, initial_state) + else { + return Ok(empty_solution()); + }; + if amount_out < swap.amount_out { + return Ok(empty_solution()); + } + Some((fill, amount_out, route)) + }; + + let Some((actual_fill, amount_out, route)) = result else { + return Ok(empty_solution()); + }; + + let ed_out = A::existential_deposit(swap.asset_out); + if amount_out < ed_out { + return Ok(empty_solution()); + } + + let pro_rata_min = apply_rate(actual_fill, U256::from(swap.amount_out), U256::from(swap.amount_in)); + let surplus = amount_out.saturating_sub(pro_rata_min); + + let resolved = ResolvedIntent { + id: intent.id, + data: IntentData::Swap(SwapData { + asset_in: swap.asset_in, + asset_out: swap.asset_out, + amount_in: actual_fill, + amount_out, + partial: swap.partial, + }), + }; + + Ok(Solution { + resolved_intents: ResolvedIntents::truncate_from(vec![resolved]), + trades: SolutionTrades::truncate_from(vec![PoolTrade { + direction: SwapType::ExactIn, + amount_in: actual_fill, + amount_out: adjust_amm_output(amount_out), + route, + }]), + score: surplus, + }) + } + + /// Binary search for the maximum partial fill amount where AMM output meets the minimum rate. + /// Returns (fill_amount, amount_out, route) or None if no fill is possible. + fn find_best_partial_fill( + swap: &SwapData, + max_fill: Balance, + routes: &[Route], + state: &A::State, + ) -> Option<(Balance, Balance, Route)> { + let ed = A::existential_deposit(swap.asset_in); + let ed_out = A::existential_deposit(swap.asset_out); + let (min_n, min_d) = min_rate(swap); + + // First try the full remaining amount + if let Some((route, amount_out, _)) = + Self::select_best_route(routes, swap.asset_in, swap.asset_out, max_fill, state) + { + let pro_rata_min = apply_rate(max_fill, min_n, min_d); + if amount_out >= pro_rata_min && amount_out >= ed_out { + return Some((max_fill, amount_out, route)); + } + } + + // Binary search: find max fill where output meets min rate + let mut lo: Balance = ed; // minimum meaningful fill + let mut hi: Balance = max_fill; + let mut best: Option<(Balance, Balance, Route)> = None; + + const MAX_SEARCH_ITERATIONS: u32 = 20; + for _ in 0..MAX_SEARCH_ITERATIONS { + if lo > hi { + break; + } + let mid = lo.saturating_add(hi) / 2; + if mid < ed { + break; + } + + if let Some((route, amount_out, _)) = + Self::select_best_route(routes, swap.asset_in, swap.asset_out, mid, state) + { + let pro_rata_min = apply_rate(mid, min_n, min_d); + if amount_out >= pro_rata_min && amount_out >= ed_out { + best = Some((mid, amount_out, route)); + lo = mid.saturating_add(1); // try larger + } else { + hi = mid.saturating_sub(1); // too much volume + } + } else { + hi = mid.saturating_sub(1); + } + + if hi.saturating_sub(lo) < ed { + break; // convergence within ED precision + } + } + + // ED guard: ensure remaining amount is either zero or large enough to be + // tradeable in the next round. "Tradeable" means both: + // (a) remaining >= ed (input asset ED) — won't be dust in the user's account + // (b) remaining can produce output >= ed_out — the next fill won't be blocked + // If remaining fails either check, try filling everything instead. + if let Some((fill, _, ref route)) = best { + let remaining_after = max_fill.saturating_sub(fill); + let remaining_untradeable = if remaining_after == 0 { + false + } else if remaining_after < ed { + true + } else { + // Check if remaining can produce output above ed_out + let remaining_out = apply_rate(remaining_after, min_n, min_d); + remaining_out < ed_out + }; + + if remaining_untradeable { + // Try filling everything + let fill_all = max_fill; + if let Some((_, all_out, _)) = + Self::select_best_route(routes, swap.asset_in, swap.asset_out, fill_all, state) + { + let pro_rata_min = apply_rate(fill_all, min_n, min_d); + if all_out >= pro_rata_min && all_out >= ed_out { + return Some((fill_all, all_out, route.clone())); + } + } + // Can't fill all — reduce to keep remaining >= ED + let reduced = max_fill.saturating_sub(ed); + if reduced >= ed { + if let Some((route, out, _)) = + Self::select_best_route(routes, swap.asset_in, swap.asset_out, reduced, state) + { + let pro_rata_min = apply_rate(reduced, min_n, min_d); + if out >= pro_rata_min && out >= ed_out { + return Some((reduced, out, route)); + } + } + } + } + } + + best + } + + /// Compute clearing rates from summed per-direction volumes. + fn compute_pair_clearing_from_totals( + asset_a: AssetId, + asset_b: AssetId, + total_a_sold: Balance, + total_b_sold: Balance, + spot_prices: &BTreeMap, + state: &A::State, + ) -> Option { + if total_a_sold == 0 && total_b_sold == 0 { + return None; + } + + let pa = spot_prices.get(&asset_a)?; + let pb = spot_prices.get(&asset_b)?; + + let flow = common::analyze_pair_flow(total_a_sold, total_b_sold, pa, pb); + + match flow { + FlowDirection::SingleForward { amount } => { + let routes = A::discover_routes(asset_a, asset_b, state).ok()?; + let (_, amount_out, _) = Self::select_best_route(&routes, asset_a, asset_b, amount, state)?; + let adjusted_out = adjust_amm_output(amount_out); + Some(PairClearing { + forward_n: U256::from(adjusted_out), + forward_d: U256::from(amount), + backward_n: U256::zero(), + backward_d: U256::one(), + }) + } + FlowDirection::SingleBackward { amount } => { + let routes = A::discover_routes(asset_b, asset_a, state).ok()?; + let (_, amount_out, _) = Self::select_best_route(&routes, asset_b, asset_a, amount, state)?; + let adjusted_out = adjust_amm_output(amount_out); + Some(PairClearing { + forward_n: U256::zero(), + forward_d: U256::one(), + backward_n: U256::from(adjusted_out), + backward_d: U256::from(amount), + }) + } + FlowDirection::ExcessForward { + scarce_out, + direct_match, + net_sell, + } => { + let routes = A::discover_routes(asset_a, asset_b, state).ok()?; + let (_, amount_out, _) = Self::select_best_route(&routes, asset_a, asset_b, net_sell, state)?; + let adjusted_out = adjust_amm_output(amount_out); + Some(PairClearing { + forward_n: U256::from(direct_match.saturating_add(adjusted_out)), + forward_d: U256::from(total_a_sold), + backward_n: U256::from(scarce_out), + backward_d: U256::from(total_b_sold), + }) + } + FlowDirection::ExcessBackward { + scarce_out, + direct_match, + net_sell, + } => { + let routes = A::discover_routes(asset_b, asset_a, state).ok()?; + let (_, amount_out, _) = Self::select_best_route(&routes, asset_b, asset_a, net_sell, state)?; + let adjusted_out = adjust_amm_output(amount_out); + Some(PairClearing { + forward_n: U256::from(scarce_out), + forward_d: U256::from(total_a_sold), + backward_n: U256::from(direct_match.saturating_add(adjusted_out)), + backward_d: U256::from(total_b_sold), + }) + } + FlowDirection::PerfectCancel { a_as_b, b_as_a } => Some(PairClearing { + forward_n: U256::from(a_as_b), + forward_d: U256::from(total_a_sold), + backward_n: U256::from(b_as_a), + backward_d: U256::from(total_b_sold), + }), + } + } + + /// Compute clearing rates using fill amounts (not raw amount_in). + fn compute_pair_clearing_with_fills( + asset_a: AssetId, + asset_b: AssetId, + forward: &[&IntentFill], + backward: &[&IntentFill], + spot_prices: &BTreeMap, + state: &A::State, + ) -> Option { + if forward.is_empty() && backward.is_empty() { + return None; + } + let total_a_sold: Balance = forward.iter().map(|f| f.fill_amount).sum(); + let total_b_sold: Balance = backward.iter().map(|f| f.fill_amount).sum(); + Self::compute_pair_clearing_from_totals(asset_a, asset_b, total_a_sold, total_b_sold, spot_prices, state) + } + + /// Joint per-pair partial-fill fit. + /// + /// For each pair (A,B) with partials in either direction, binary-search the + /// largest `t ∈ [0, 1]` (represented as `u64 ∈ [0, GRANULARITY]`) such that + /// the clearing rate at `(fixed_f + t·V_f_max, fixed_b + t·V_b_max)` still + /// meets the tightest per-direction minimum rate. Then each partial receives + /// `remaining() · t`, which preserves same-direction-same-fill-fraction and + /// makes the fit independent of input order. + fn fit_partials_jointly<'a>( + fills: &mut Vec>, + partial_fills: Vec>, + spot_prices: &BTreeMap, + state: &A::State, + ) { + // Group partials by unordered pair. + let mut partials_by_pair: BTreeMap>> = BTreeMap::new(); + for pf in partial_fills { + let IntentData::Swap(s) = &pf.intent.data else { + continue; + }; + let up = unordered_pair(s.asset_in, s.asset_out); + partials_by_pair.entry(up).or_default().push(pf); + } + + for (pair, pair_partials) in partials_by_pair { + let (asset_a, asset_b) = pair; + + // Split partials by direction. + let mut f_partials: Vec> = Vec::new(); + let mut b_partials: Vec> = Vec::new(); + for pf in pair_partials { + let IntentData::Swap(s) = &pf.intent.data else { + continue; + }; + if s.asset_in == asset_a { + f_partials.push(pf); + } else { + b_partials.push(pf); + } + } + + // Fixed (non-partial) volumes in each direction. + let fixed_f: Balance = fills + .iter() + .filter_map(|f| match &f.intent.data { + IntentData::Swap(s) if s.asset_in == asset_a && s.asset_out == asset_b => Some(f.fill_amount), + _ => None, + }) + .fold(0u128, |acc, v| acc.saturating_add(v)); + let fixed_b: Balance = fills + .iter() + .filter_map(|f| match &f.intent.data { + IntentData::Swap(s) if s.asset_in == asset_b && s.asset_out == asset_a => Some(f.fill_amount), + _ => None, + }) + .fold(0u128, |acc, v| acc.saturating_add(v)); + + const GRANULARITY: u64 = 1_000_000_000; + const MAX_BINARY_SEARCH_ITER: u32 = 30; + + // Iterative drop loop: when the joint fit would set best_t=0, the + // partial with the highest demanded rate in the blocking direction + // is the cause. Drop it and retry; otherwise a single unreachable + // intent poisons every other partial on the pair. + let max_drop_rounds = f_partials.len() + b_partials.len() + 1; + let mut best_t: u64 = 0; + let mut v_f_max: Balance = 0; + let mut v_b_max: Balance = 0; + + for _round in 0..max_drop_rounds { + v_f_max = f_partials + .iter() + .map(|p| p.fill_amount) + .fold(0u128, |acc, v| acc.saturating_add(v)); + v_b_max = b_partials + .iter() + .map(|p| p.fill_amount) + .fold(0u128, |acc, v| acc.saturating_add(v)); + + if v_f_max == 0 && v_b_max == 0 { + break; + } + + // Tightest per-direction minimum rate (n/d) — the highest + // `amount_out/amount_in` demanded by any partial in that + // direction. A single unreachable rate forces best_t = 0 below. + let tight_f = Self::tightest_rate(&f_partials); + let tight_b = Self::tightest_rate(&b_partials); + + // Binary search over `t`. + let mut lo: u64 = 0; + let mut hi: u64 = GRANULARITY; + best_t = 0; + + for _ in 0..MAX_BINARY_SEARCH_ITER { + if lo > hi { + break; + } + let mid = lo.saturating_add(hi) / 2; + let v_f = Self::scale_by_t(v_f_max, mid, GRANULARITY); + let v_b = Self::scale_by_t(v_b_max, mid, GRANULARITY); + + let total_f = fixed_f.saturating_add(v_f); + let total_b = fixed_b.saturating_add(v_b); + + let meets = if total_f == 0 && total_b == 0 { + true + } else if let Some(c) = + Self::compute_pair_clearing_from_totals(asset_a, asset_b, total_f, total_b, spot_prices, state) + { + let f_ok = match tight_f { + Some((tn, td)) => c.forward_n.saturating_mul(td) >= tn.saturating_mul(c.forward_d), + None => true, + }; + let b_ok = match tight_b { + Some((tn, td)) => c.backward_n.saturating_mul(td) >= tn.saturating_mul(c.backward_d), + None => true, + }; + f_ok && b_ok + } else { + false + }; + + if meets { + best_t = mid; + lo = mid.saturating_add(1); + } else { + hi = mid.saturating_sub(1); + } + } + + if best_t > 0 { + break; + } + + // best_t == 0: identify which direction is blocking at minimum + // volume (t=1 granularity unit) and drop that direction's + // tightest-rate partial. Falling back to t=1 keeps the check + // stable — at t=0 both directions look fine (no volume). + let probe_v_f = Self::scale_by_t(v_f_max, 1, GRANULARITY).max(if v_f_max > 0 { 1 } else { 0 }); + let probe_v_b = Self::scale_by_t(v_b_max, 1, GRANULARITY).max(if v_b_max > 0 { 1 } else { 0 }); + let probe_total_f = fixed_f.saturating_add(probe_v_f); + let probe_total_b = fixed_b.saturating_add(probe_v_b); + + let blocking: Option = match Self::compute_pair_clearing_from_totals( + asset_a, + asset_b, + probe_total_f, + probe_total_b, + spot_prices, + state, + ) { + Some(c) => { + let f_blocked = match tight_f { + Some((tn, td)) => c.forward_n.saturating_mul(td) < tn.saturating_mul(c.forward_d), + None => false, + }; + let b_blocked = match tight_b { + Some((tn, td)) => c.backward_n.saturating_mul(td) < tn.saturating_mul(c.backward_d), + None => false, + }; + // Prefer dropping from whichever direction is blocked. + // If both blocked, drop forward first, then backward + // on the next iteration. + if f_blocked { + Some(true) + } else if b_blocked { + Some(false) + } else { + None + } + } + None => { + // Clearing failed entirely — drop from whichever + // direction still has partials. + if !f_partials.is_empty() { + Some(true) + } else if !b_partials.is_empty() { + Some(false) + } else { + None + } + } + }; + + let dropped = match blocking { + Some(true) => Self::drop_tightest(&mut f_partials), + Some(false) => Self::drop_tightest(&mut b_partials), + None => None, + }; + + let Some(dropped_id) = dropped else { + // No progress possible — bail. + break; + }; + log::debug!( + target: "solver::v2", + "fit_partials_jointly: pair ({asset_a}, {asset_b}) best_t=0; dropped tightest partial {dropped_id}, retrying", + ); + } + + if best_t == 0 { + continue; + } + + // Pro-rate best_t-scaled volumes to each partial by its remaining(). + Self::distribute_fills(fills, &f_partials, v_f_max, best_t, GRANULARITY); + Self::distribute_fills(fills, &b_partials, v_b_max, best_t, GRANULARITY); + } + } + + /// Remove and return the id of the partial with the highest + /// `amount_out/amount_in` rate. Returns `None` if the slice is empty. + fn drop_tightest<'a>(partials: &mut Vec>) -> Option { + let mut best_idx: Option = None; + let mut best_n: U256 = U256::zero(); + let mut best_d: U256 = U256::one(); + for (i, p) in partials.iter().enumerate() { + let IntentData::Swap(s) = &p.intent.data else { + continue; + }; + let n = U256::from(s.amount_out); + let d = U256::from(s.amount_in.max(1)); + // n/d > best_n/best_d <=> n*best_d > best_n*d + if best_idx.is_none() || n.saturating_mul(best_d) > best_n.saturating_mul(d) { + best_idx = Some(i); + best_n = n; + best_d = d; + } + } + best_idx.map(|i| partials.remove(i).intent.id) + } + + /// Largest `amount_out/amount_in` demanded by any intent in the list. + /// Encoded as (n, d) with d ≥ 1. + fn tightest_rate<'a>(partials: &[IntentFill<'a>]) -> Option<(U256, U256)> { + let mut best: Option<(U256, U256)> = None; + for p in partials { + let IntentData::Swap(s) = &p.intent.data else { + continue; + }; + let n = U256::from(s.amount_out); + let d = U256::from(s.amount_in.max(1)); + best = match best { + None => Some((n, d)), + Some((cn, cd)) => { + // Compare n/d vs cn/cd: n*cd vs cn*d. + if n.saturating_mul(cd) > cn.saturating_mul(d) { + Some((n, d)) + } else { + Some((cn, cd)) + } + } + }; + } + best + } + + /// `max_vol * t / granularity`, saturating on overflow. + fn scale_by_t(max_vol: Balance, t: u64, granularity: u64) -> Balance { + if max_vol == 0 || t == 0 { + return 0; + } + let product = U256::from(max_vol).saturating_mul(U256::from(t)); + let scaled = product / U256::from(granularity); + scaled.try_into().unwrap_or(Balance::MAX) + } + + /// Sort `fills` by estimated surplus descending so that a later `truncate` + /// keeps the highest-value intents. Surplus is estimated using the clearing + /// rate computed from the current `fills` (all intents, both directions). + /// Ties break by intent id for determinism. + fn sort_by_estimated_surplus<'a>( + fills: &mut [IntentFill<'a>], + spot_prices: &BTreeMap, + state: &A::State, + ) { + // Build per-pair clearings from current volumes. + let mut pair_totals: BTreeMap = BTreeMap::new(); + for f in fills.iter() { + let IntentData::Swap(s) = &f.intent.data else { + continue; + }; + let up = unordered_pair(s.asset_in, s.asset_out); + let entry = pair_totals.entry(up).or_default(); + if s.asset_in == up.0 { + entry.0 = entry.0.saturating_add(f.fill_amount); + } else { + entry.1 = entry.1.saturating_add(f.fill_amount); + } + } + let mut clearings: BTreeMap = BTreeMap::new(); + for (&(a, b), &(ta, tb)) in &pair_totals { + if let Some(c) = Self::compute_pair_clearing_from_totals(a, b, ta, tb, spot_prices, state) { + clearings.insert((a, b), c); + } + } + + // Compute surplus estimate per fill. + let surplus_of = |f: &IntentFill<'a>| -> Balance { + let IntentData::Swap(s) = &f.intent.data else { + return 0; + }; + let up = unordered_pair(s.asset_in, s.asset_out); + let Some(c) = clearings.get(&up) else { + return 0; + }; + let (rn, rd) = if s.asset_in == up.0 { + (c.forward_n, c.forward_d) + } else { + (c.backward_n, c.backward_d) + }; + let output = apply_rate(f.fill_amount, rn, rd); + let pro_rata_min = apply_rate(f.fill_amount, U256::from(s.amount_out), U256::from(s.amount_in)); + output.saturating_sub(pro_rata_min) + }; + + fills.sort_by(|a, b| { + let sa = surplus_of(a); + let sb = surplus_of(b); + // Descending by surplus, then by id for determinism on ties. + sb.cmp(&sa).then(a.intent.id.cmp(&b.intent.id)) + }); + } + + /// Distribute a total fit-volume across a set of partials proportionally to their + /// `remaining()` share, applying per-intent ED guards. Each produced `IntentFill` is + /// pushed to `fills`. + fn distribute_fills<'a>( + fills: &mut Vec>, + partials: &[IntentFill<'a>], + v_max: Balance, + best_t: u64, + granularity: u64, + ) { + if v_max == 0 || best_t == 0 || partials.is_empty() { + return; + } + + for p in partials { + let IntentData::Swap(swap) = &p.intent.data else { + continue; + }; + let ed = A::existential_deposit(swap.asset_in); + + // share = remaining * best_t / granularity. + let raw_share = Self::scale_by_t(p.fill_amount, best_t, granularity).min(p.fill_amount); + if raw_share < ed { + continue; + } + + // ED guard on remaining-after. remaining_after is the unfilled residue of + // the intent after this solution applies: swap.remaining() - raw_share. + let remaining_after = swap.remaining().saturating_sub(raw_share); + let share = if remaining_after > 0 && remaining_after < ed { + // Either fill everything (if still feasible) or trim to leave ed behind. + // We don't re-check feasibility of the full fill here — `best_t` was fitted + // against the tightest rate, and increasing a single partial's share past + // its pro-rata can push the clearing below tolerance. Safer to trim. + let trimmed = swap.remaining().saturating_sub(ed); + if trimmed >= ed { + trimmed + } else { + continue; + } + } else { + raw_share + }; + + fills.push(IntentFill { + intent: p.intent, + fill_amount: share, + }); + } + } +} diff --git a/ice/route-findr/Cargo.toml b/ice/route-findr/Cargo.toml new file mode 100644 index 0000000000..a90a221b30 --- /dev/null +++ b/ice/route-findr/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "route-findr" +version = "0.1.0" +edition = "2021" +description = "Route discovery for Hydration DEX — enumerates all valid multi-hop trading routes for a given asset pair" +license = "Apache-2.0" + +[dependencies] +hydradx-traits = { workspace = true } +primitives = { workspace = true } +sp-runtime = { workspace = true } +frame-support = { workspace = true } + +[features] +default = ["std"] +std = [ + "hydradx-traits/std", + "primitives/std", + "sp-runtime/std", + "frame-support/std", +] +local-logs = ["std"] diff --git a/ice/route-findr/SPEC.md b/ice/route-findr/SPEC.md new file mode 100644 index 0000000000..a872dcef2f --- /dev/null +++ b/ice/route-findr/SPEC.md @@ -0,0 +1,424 @@ +# route-suggester — Architecture Spec + +> **Crate:** `route-suggester` | **Date:** 2026-03-31 +> **Scope:** `src/lib.rs`, `src/types.rs`, `src/graph.rs`, `src/bfs.rs`, `src/strategy.rs` +> **Origin:** Ported from TypeScript SDK `packages/sdk-next/src/sor/route/` + +--- + +## 1. Purpose + +Enumerates **all valid multi-hop trading routes** between two assets on Hydration DEX. This is the route _discovery_ layer — it finds paths, not prices. Downstream consumers (ICE solver, on-chain router, RPC endpoints) use these routes to compute quotes and select the optimal path. + +The existing `RouteProvider::get_route()` in `hydration-node` returns a **single** stored or default route. This crate fills the gap: discovering **every** viable route for a given asset pair. + +--- + +## 2. Types & Dependencies + +All pool routing types come from `hydradx-traits` and `primitives` — **no local duplicates**: + +| Type | Source | Description | +| ---------------------- | ------------------------ | -------------------------------------------------- | +| `AssetId` | `primitives` | Concrete asset identifier (`u32`) | +| `PoolType` | `hydradx_traits::router` | Pool type discriminant | +| `Trade` | `hydradx_traits::router` | Single trade step: `{ pool, asset_in, asset_out }` | +| `Route` | `hydradx_traits::router` | `BoundedVec, ConstU32<9>>` | +| `MAX_NUMBER_OF_TRADES` | `hydradx_traits::router` | `9` — max hops per route | + +Types introduced by this crate: + +| Type | Description | +| -------------- | ------------------------------------------------------------------------------------------ | +| `PoolEdge` | Pool instance for graph building: `{ pool_type: PoolType, assets: Vec }` | +| `PoolProvider` | Trait with associated `State`: `fn get_all_pools(state: &Self::State) -> Vec` | + +### Pool identity via `PoolType` + +`PoolType` is a discriminant, not a unique pool ID. The `` generic exists solely for `Stableswap(AssetId)` where the value is the pool's share token: + +```rust +pub enum PoolType { + XYK, // bare — resolved by (asset_in, asset_out) pair + LBP, // bare — resolved by asset pair + Stableswap(AssetId), // unique per pool instance + Omnipool, // singleton + Aave, // bare + HSM, // bare +} +``` + +The on-chain `pallet-route-executor` resolves the concrete pool from `Trade { pool, asset_in, asset_out }`. For cycle prevention during BFS, this crate uses an internal `pool_index` (position in the input `Vec`). + +--- + +## 3. State & the `PoolProvider` Trait + +### How `PoolProvider` fits in + +`PoolProvider` mirrors this pattern — it accepts `&State` so route discovery uses the same snapshot: + +```rust +pub trait PoolProvider { + type State: Clone; + fn get_all_pools(state: &Self::State) -> Vec; +} +``` + +`State` is an associated type because this crate **cannot know its shape**. The composed state is a tuple whose arity depends on which simulators the runtime configures (`(A, B, C)` vs `(A, B, C, D)`). Only the runtime can destructure it. + +--- + +## 4. Architecture Overview + +### Component Diagram + +``` +Consumer (ICE solver / RPC / pallet) + │ + ▼ +RouteSuggester [lib.rs] + │ + ├── strategy::suggest_routes() [strategy.rs] + │ │ + │ ├── Partition pools: trusted vs isolated + │ ├── Select search strategy based on token placement + │ │ + │ ├── graph::build_graph() [graph.rs] + │ │ └── Vec → AdjacencyMap (BTreeMap>) + │ │ + │ └── bfs::find_all_paths() [bfs.rs] + │ └── BFS over adjacency map → Vec> + │ + └── P::get_all_pools(state) [types.rs — trait, impl in runtime] + └── Extracts tradeable assets from SimulatorSet::State snapshot +``` + +### Standalone Alternative + +```rust +// When you already have the pool list — no PoolProvider/State needed +get_routes(asset_in, asset_out, pools) -> Vec> +``` + +--- + +## 5. Module Breakdown + +### 5.1 `graph.rs` — Graph Construction + +**Ported from:** `packages/sdk-next/src/sor/route/graph.ts` → `getNodesAndEdges()` + +Converts `Vec` into a directed adjacency map. + +**Edge generation:** For a pool with N assets, N×(N-1) directed edges are created — every asset can be swapped for every other asset within that pool. + +| Pool type | Graph behavior | +| ---------------------------- | ----------------------------------------- | +| Omnipool (40 assets) | 1 pool_index, 40×39 = 1560 directed edges | +| Stableswap(100) with [A,B,C] | 1 pool_index, 3×2 = 6 directed edges | +| XYK with [A,B] | 1 pool_index, 2 directed edges | + +**Internal types (crate-private):** + +```rust +struct Edge { + pool_index: usize, // position in input Vec — for cycle prevention + pool_type: PoolType, // flows into Trade output + asset_out: AssetId, +} + +type AdjacencyMap = BTreeMap>; +``` + +### 5.2 `bfs.rs` — Breadth-First Search + +**Ported from:** `packages/sdk-next/src/sor/route/bfs.ts` → `Bfs` class + +Finds all acyclic paths up to `MAX_NUMBER_OF_TRADES` (9) hops. Returns `Vec>` — directly usable by `pallet-route-executor`. + +**Cycle prevention** (mirrors SDK's `Bfs.isNotVisited`): + +1. **Asset revisit** — destination asset already in current path → rejected +2. **Pool reuse** — pool_index already in current path → rejected + +This prevents circular routes (A → B → A) and redundant multi-hop through the same pool (Omnipool A→B→C when A→C is direct). + +**Termination guarantees:** + +- Max path length: 9 hops +- No cycles: asset + pool visited checks +- Finite pool set → finite graph → BFS terminates + +### 5.3 `strategy.rs` — Pool Partitioning Strategy + +**Ported from:** `packages/sdk-next/src/sor/route/suggester.ts` → `RouteSuggester.getProposals()` + +Pools are partitioned: + +| Category | Pool types | Rationale | +| ------------ | ------------------------------------ | ---------------------------------- | +| **Trusted** | Omnipool, Stableswap, LBP, Aave, HSM | Deeper liquidity, protocol-managed | +| **Isolated** | XYK | Permissionless, lower liquidity | + +Strategy selection: + +| `asset_in` trusted? | `asset_out` trusted? | Search over | +| ------------------- | -------------------- | ----------------------------------------------------- | +| No | No | XYK pools containing `asset_in` OR `asset_out` | +| Yes | Yes | All trusted pools | +| Mixed | Mixed | All trusted + XYK pools containing the isolated asset | + +### 5.4 `lib.rs` — Public API + +```rust +// Trait-based: pool list from PoolProvider + state snapshot +RouteSuggester::

::get_routes(asset_in, asset_out, &state) -> Vec> + +// Standalone: pool list provided directly +get_routes(asset_in, asset_out, pools) -> Vec> +``` + +--- + +## 6. Data Flow + +### `RouteSuggester::::get_routes(A, B, &state)` + +``` +1. P::get_all_pools(&state) → Vec + │ (runtime destructures SimulatorSet::State, + │ extracts tradeable asset lists from each AMM snapshot) + │ +2. strategy::suggest_routes(A, B, pools) + │ + ├── Partition: trusted[], isolated[] + ├── Check: A in trusted? B in trusted? + ├── Select pool subset + │ + ├── graph::build_graph(selected_pools) → AdjacencyMap + │ + └── bfs::find_all_paths(adjacency, A, B) + │ + ├── Queue ← [PathNode { asset: A }] + │ + └── While queue not empty: + │ path = queue.pop_front() + ├── path.last == B? → results.push(path_to_route(path)); continue + ├── trade_count > 9? → continue + └── For each edge from path.last.asset: + ├── is_valid_extension? (no asset revisit, no pool reuse) + └── Yes → queue.push(path + edge) + +3. Return: Vec> + (BoundedVec, ConstU32<9>> — directly compatible + with pallet-route-executor and AMMInterface::sell/buy) +``` + +--- + +## 7. Type Mapping: SDK → Rust + +| SDK (TypeScript) | Rust (this crate) | Notes | +| ------------------------------- | -------------------------------------------------- | --------------------------------------------- | +| `PoolBase` | `PoolEdge` | Simplified: only pool_type + assets needed | +| `PoolType` enum | `PoolType` | From `hydradx_traits::router` | +| `Edge = [address, from, to]` | `graph::Edge { pool_index, pool_type, asset_out }` | address → pool_index for cycle checks | +| `Node = [id, from]` | `bfs::PathNode { asset, pool_index, pool_type }` | Carries metadata for cycle prevention | +| `RouteProposal = Edge[]` | `Route` | `BoundedVec>` | +| `Bfs.isNotVisited()` | `bfs::is_valid_extension()` | Same dual check: asset + pool | +| `Bfs.findPaths()` | `bfs::find_all_paths()` | Queue-based BFS | +| `getNodesAndEdges()` | `graph::build_graph()` | Pool → adjacency map | +| `RouteSuggester.getProposals()` | `strategy::suggest_routes()` | 3-case strategy dispatch | +| `Queue` | `VecDeque` | stdlib FIFO queue | +| `MAX_SIZE_OF_PATH = 10` | `MAX_NUMBER_OF_TRADES = 9` | SDK counts nodes (10), Rust counts trades (9) | + +--- + +## 8. Constraints & Invariants + +### Route constraints + +| Constraint | Enforced by | Value | +| -------------------- | ------------------------- | --------------------------- | +| Max trades per route | `bfs::find_all_paths` | 9 (`MAX_NUMBER_OF_TRADES`) | +| No asset revisits | `bfs::is_valid_extension` | Checked against full path | +| No pool reuse | `bfs::is_valid_extension` | Tracked by `pool_index` | +| Output is `Route` | `bfs::path_to_route` | `BoundedVec::truncate_from` | + +### `no_std` compatibility + +| Concern | Solution | +| ------------ | ---------------------------------------------------------------------------- | +| Collections | `BTreeMap` / `VecDeque` from `alloc` | +| Feature gate | `#![cfg_attr(not(feature = "std"), no_std)]` | +| Dependencies | `hydradx-traits`, `primitives`, `sp-runtime`, `frame-support` (all `no_std`) | + +--- + +## 9. Complexity Analysis + +For P pools with at most A assets each: + +- **Graph construction:** O(P × A²) +- **BFS:** O(V × E × L) worst case — V = unique assets, E = total edges, L = 9. Cycle prevention prunes aggressively. +- **Strategy partitioning:** O(P) + +**Practical bounds (Hydration mainnet):** + +- Omnipool: ~40-60 assets → 1 pool, ~2500 edges +- Stableswap: ~5-10 pools, 2-4 assets → ~30-80 edges +- XYK: ~20-50 pairs → ~40-100 edges + +Total graph is small. BFS completes in microseconds for typical queries. + +--- + +## 10. Integration Guide (hydration-node) + +### Step 1: Add dependency + +In the target crate within [`hydration-node`](https://github.com/galacticcouncil/hydration-node): + +```toml +[dependencies] +route-suggester = { git = "https://github.com/galacticcouncil/sdk", subdirectory = "crates/route-suggester", default-features = false } + +[features] +std = ["route-suggester/std"] +``` + +### Step 2: Implement `PoolProvider` + +The implementation lives in the runtime because only the runtime knows the concrete `SimulatorSet::State` shape. It destructures the state and extracts tradeable assets from each AMM's snapshot: + +```rust +use route_suggester::types::{PoolEdge, PoolProvider}; +use hydradx_traits::router::PoolType; +use hydradx_traits::amm::SimulatorSet; +use primitives::AssetId; + +pub struct AllPools; + +impl PoolProvider for AllPools { + // Same State as SimulatorSet — composed tuple of all AMM snapshots + type State = ::State; + + fn get_all_pools(state: &Self::State) -> Vec { + let (omni_state, stable_state, xyk_state) = state; + let mut pools = Vec::new(); + + // Omnipool — single pool, all tradeable assets + let omni_assets: Vec = omni_state + .iter() + .filter(|(_, s)| s.tradeable.contains(Tradability::SELL | Tradability::BUY)) + .map(|(id, _)| *id) + .collect(); + if !omni_assets.is_empty() { + pools.push(PoolEdge { + pool_type: PoolType::Omnipool, + assets: omni_assets, + }); + } + + // Stableswap — one PoolEdge per pool + for pool in stable_state { + pools.push(PoolEdge { + pool_type: PoolType::Stableswap(pool.pool_id), + assets: pool.assets.clone(), + }); + } + + // XYK — one PoolEdge per pair + for pool in xyk_state { + pools.push(PoolEdge { + pool_type: PoolType::XYK, + assets: vec![pool.asset_a, pool.asset_b], + }); + } + + pools + } +} +``` + +### Step 3: Use with the ICE solver + +Within the `AMMInterface` implementation, use `RouteSuggester` for route discovery when no route is provided: + +```rust +use route_suggester::RouteSuggester; + +type RouteFinder = RouteSuggester; + +// Inside AMMInterface::sell implementation: +fn sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + route: Option>, + state: &Self::State, +) -> Result<(Self::State, TradeExecution), Self::Error> { + let route = match route { + Some(r) => r, + None => { + // Discover all viable routes from the current state + let routes = RouteFinder::get_routes(asset_in, asset_out, state); + // Pick the best one (e.g., simulate each, select highest output) + select_best_route(routes, asset_in, amount_in, state)? + } + }; + + execute_along_route(route, amount_in, state) +} +``` + +### Step 4: Standalone use (tests, RPC, off-chain workers) + +```rust +use route_suggester::{get_routes, types::PoolEdge}; +use hydradx_traits::router::PoolType; + +let pools = vec![ + PoolEdge { pool_type: PoolType::Omnipool, assets: vec![0, 1, 2, 5, 10] }, + PoolEdge { pool_type: PoolType::Stableswap(100), assets: vec![10, 11, 12] }, + PoolEdge { pool_type: PoolType::XYK, assets: vec![20, 5] }, +]; + +let routes = get_routes(20, 12, pools); +// Returns: [ +// Route [XYK 20→5, Omnipool 5→10, Stableswap(100) 10→12], +// ... other viable paths +// ] +``` + +--- + +## 11. Test Coverage + +20 tests, all passing: + +| Category | Tests | What's verified | +| -------------------- | ----- | ----------------------------------------------------- | +| Basic routing | 5 | Direct, reverse, multi-hop, multiple routes, no route | +| Edge cases | 2 | Same asset, empty pools | +| Omnipool | 2 | Direct route, no multi-hop through same pool | +| Stableswap | 1 | Direct route with pool ID | +| Cross-pool | 2 | XYK→Omnipool bridge, Stableswap→Omnipool chain | +| Strategy | 2 | Trusted-only excludes XYK, isolated-only filtering | +| Cycle prevention | 2 | No asset revisit in triangle, different pools OK | +| Max trades | 2 | Exactly 9 hops succeeds, 10 hops returns empty | +| PoolProvider + State | 1 | End-to-end with trait-based provider and `&state` | + +--- + +## 12. File Reference + +| File | Purpose | +| ----------------- | -------------------------------------------------------------------------- | +| `Cargo.toml` | Deps: `hydradx-traits`, `primitives`, `sp-runtime`, `frame-support` | +| `src/lib.rs` | Public API (`RouteSuggester`, `get_routes`) + all tests | +| `src/types.rs` | Re-exports from `hydradx-traits`/`primitives` + `PoolEdge`, `PoolProvider` | +| `src/graph.rs` | `build_graph()` → `AdjacencyMap` | +| `src/bfs.rs` | `find_all_paths()`, cycle checks | +| `src/strategy.rs` | Trusted/isolated partitioning, 3-case dispatch | diff --git a/ice/route-findr/src/bfs.rs b/ice/route-findr/src/bfs.rs new file mode 100644 index 0000000000..1176b43161 --- /dev/null +++ b/ice/route-findr/src/bfs.rs @@ -0,0 +1,106 @@ +//! Breadth-first search path finder. +//! +//! Ported from `packages/sdk-next/src/sor/route/bfs.ts`. +//! +//! Discovers every acyclic path (up to `MAX_NUMBER_OF_TRADES` hops). +//! +//! Prevents cycles by checking that a candidate edge does not: +//! 1. Revisit an asset already in the path. +//! 2. Reuse a pool already traversed in the path (tracked by pool index). +//! +//! This mirrors the SDK's `Bfs.isNotVisited` which checks both asset ID +//! and pool address. + +extern crate alloc; +use alloc::collections::VecDeque; +use alloc::vec; +use alloc::vec::Vec; + +use frame_support::BoundedVec; + +use crate::graph::{AdjacencyMap, Edge}; +use crate::types::{AssetId, PoolType, Route, Trade, MAX_NUMBER_OF_TRADES}; + +/// A node in a BFS path under construction. +#[derive(Debug, Clone)] +struct PathNode { + asset: AssetId, + /// Index of the pool used to reach this node (`None` for the start node). + pool_index: Option, + /// Pool type used to reach this node (`None` for the start node). + pool_type: Option>, +} + +/// Check whether extending the path with `edge` would create a cycle. +fn is_valid_extension(path: &[PathNode], edge: &Edge) -> bool { + for node in path { + if node.asset == edge.asset_out { + return false; + } + if let Some(idx) = node.pool_index { + if idx == edge.pool_index { + return false; + } + } + } + true +} + +/// Convert an internal path to a [`Route`]. +fn path_to_route(path: &[PathNode]) -> Route { + let trades: Vec> = path + .windows(2) + .filter_map(|pair| { + pair[1].pool_type.map(|pool| Trade { + pool, + asset_in: pair[0].asset, + asset_out: pair[1].asset, + }) + }) + .collect(); + BoundedVec::truncate_from(trades) +} + +/// Find all acyclic paths from `start` to `end`, up to [`MAX_NUMBER_OF_TRADES`] hops. +pub(crate) fn find_all_paths(graph: &AdjacencyMap, start: AssetId, end: AssetId) -> Vec> { + let max_trades = MAX_NUMBER_OF_TRADES as usize; + let mut results = Vec::new(); + let mut queue: VecDeque> = VecDeque::new(); + + queue.push_back(vec![PathNode { + asset: start, + pool_index: None, + pool_type: None, + }]); + + while let Some(path) = queue.pop_front() { + let trade_count = path.len() - 1; + + if trade_count > max_trades { + continue; + } + + let current_asset = path.last().expect("path is never empty").asset; + + if current_asset == end && trade_count > 0 { + results.push(path_to_route(&path)); + continue; + } + + if let Some(edges) = graph.get(¤t_asset) { + for edge in edges { + if is_valid_extension(&path, edge) { + let mut new_path = path.clone(); + new_path.push(PathNode { + asset: edge.asset_out, + pool_index: Some(edge.pool_index), + pool_type: Some(edge.pool_type), + }); + queue.push_back(new_path); + } + } + } + } + + results +} diff --git a/ice/route-findr/src/graph.rs b/ice/route-findr/src/graph.rs new file mode 100644 index 0000000000..5557760e3f --- /dev/null +++ b/ice/route-findr/src/graph.rs @@ -0,0 +1,52 @@ +//! Graph construction from pool edges. +//! +//! Converts a list of [`PoolEdge`]s into a directed adjacency map where each +//! asset maps to all outgoing swap edges. For a pool with N assets, N×(N-1) +//! directed edges are created (every asset can be swapped for every other). +//! +//! Ported from `packages/sdk-next/src/sor/route/graph.ts`. + +extern crate alloc; +use alloc::collections::BTreeMap; +use alloc::vec::Vec; + +use crate::types::{AssetId, PoolEdge, PoolType}; + +/// A directed edge in the pool graph. +#[derive(Debug, Clone)] +pub(crate) struct Edge { + /// Index of the source pool in the original pool list. + /// Used to prevent reusing the same pool within a single route, + /// mirroring the SDK's pool-address cycle check. + pub pool_index: usize, + /// The pool type (needed to construct `Trade` output). + pub pool_type: PoolType, + /// Destination asset of this edge. + pub asset_out: AssetId, +} + +/// Adjacency list: maps each asset to its outgoing edges. +pub(crate) type AdjacencyMap = BTreeMap>; + +/// Build a directed graph from pool edges. +pub(crate) fn build_graph(pools: &[PoolEdge]) -> AdjacencyMap { + let mut graph = AdjacencyMap::new(); + + for (pool_index, pool) in pools.iter().enumerate() { + for &asset_in in &pool.assets { + let edges = graph.entry(asset_in).or_default(); + for &asset_out in &pool.assets { + if asset_in == asset_out { + continue; + } + edges.push(Edge { + pool_index, + pool_type: pool.pool_type, + asset_out, + }); + } + } + } + + graph +} diff --git a/ice/route-findr/src/lib.rs b/ice/route-findr/src/lib.rs new file mode 100644 index 0000000000..f96a1f37c4 --- /dev/null +++ b/ice/route-findr/src/lib.rs @@ -0,0 +1,364 @@ +//! # route-findr +//! +//! Route discovery for Hydration DEX — enumerates **all valid multi-hop trading +//! routes** for a given asset pair. +//! +//! Ported from the TypeScript SDK (`packages/sdk-next/src/sor/route/`). +//! +//! ## Types +//! +//! Uses canonical types from [`hydradx_traits::router`] and [`primitives`]: +//! - [`AssetId`] — concrete asset identifier from `primitives` +//! - [`PoolType`] — pool type discriminant +//! - [`PoolEdge`] — pool instance with its tradeable assets +//! - [`Trade`] — a single swap step (pool + asset pair) +//! - [`Route`] — bounded vector of trades (`BoundedVec>`) +//! +//! ## Algorithm +//! +//! 1. Pools are partitioned into **trusted** (Omnipool, Stableswap, LBP, Aave, +//! HSM) and **isolated** (XYK). +//! 2. Based on where the input/output assets live, one of three BFS strategies +//! runs over the appropriate pool subset. +//! 3. BFS discovers all acyclic paths up to [`MAX_NUMBER_OF_TRADES`] hops, +//! preventing both asset revisits and same-pool reuse. +//! +//! ## Usage +//! +//! Pool edges come from `AMMInterface::pool_edges()` or `SimulatorSet::pool_edges()`. +//! Pass them to [`get_routes`] for route discovery. +//! +//! [`AssetId`]: primitives::AssetId +//! [`PoolType`]: hydradx_traits::router::PoolType +//! [`PoolEdge`]: hydradx_traits::router::PoolEdge +//! [`Trade`]: hydradx_traits::router::Trade +//! [`Route`]: hydradx_traits::router::Route +//! [`MAX_NUMBER_OF_TRADES`]: hydradx_traits::router::MAX_NUMBER_OF_TRADES + +#![cfg_attr(not(feature = "std"), no_std)] + +extern crate alloc; + +#[allow(unused_macros)] +#[cfg(feature = "local-logs")] +macro_rules! dev_msg { + ($($arg:tt)*) => { std::println!($($arg)*) }; +} + +#[allow(unused_macros)] +#[cfg(not(feature = "local-logs"))] +macro_rules! dev_msg { + ($($arg:tt)*) => {}; +} + +pub mod bfs; +pub mod graph; +pub mod strategy; +pub mod types; + +#[cfg(test)] +pub mod testdata; + +use alloc::vec::Vec; +use types::{AssetId, PoolEdge, Route}; + +/// Discover all valid routes between two assets. +pub fn get_routes(asset_in: AssetId, asset_out: AssetId, pools: Vec) -> Vec> { + strategy::suggest_routes(asset_in, asset_out, pools) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use types::PoolType; + + fn xyk(a: AssetId, b: AssetId) -> PoolEdge { + PoolEdge { + pool_type: PoolType::XYK, + assets: alloc::vec![a, b], + } + } + + fn omnipool(assets: &[AssetId]) -> PoolEdge { + PoolEdge { + pool_type: PoolType::Omnipool, + assets: assets.to_vec(), + } + } + + fn stableswap(id: AssetId, assets: &[AssetId]) -> PoolEdge { + PoolEdge { + pool_type: PoolType::Stableswap(id), + assets: assets.to_vec(), + } + } + + fn trade(pool: PoolType, asset_in: AssetId, asset_out: AssetId) -> types::Trade { + types::Trade { + pool, + asset_in, + asset_out, + } + } + + // -- basic routing -- + + #[test] + fn direct_xyk_route() { + let routes = get_routes(1, 2, alloc::vec![xyk(1, 2)]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 1); + assert_eq!(routes[0][0], trade(PoolType::XYK, 1, 2)); + } + + #[test] + fn reverse_direction() { + let routes = get_routes(2, 1, alloc::vec![xyk(1, 2)]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0][0].asset_in, 2); + assert_eq!(routes[0][0].asset_out, 1); + } + + #[test] + fn multi_hop_xyk() { + let routes = get_routes(1, 3, alloc::vec![xyk(1, 2), xyk(2, 3)]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 2); + assert_eq!(routes[0][0].asset_out, 2); + assert_eq!(routes[0][1].asset_in, 2); + assert_eq!(routes[0][1].asset_out, 3); + } + + #[test] + fn multiple_routes_between_same_pair() { + let routes = get_routes(1, 3, alloc::vec![xyk(1, 2), xyk(2, 3), xyk(1, 3)]); + assert!(routes.len() >= 2); + } + + #[test] + fn no_route_exists() { + let routes = get_routes(1, 4, alloc::vec![xyk(1, 2), xyk(3, 4)]); + assert!(routes.is_empty()); + } + + #[test] + fn same_asset_returns_empty() { + let routes = get_routes(1, 1, alloc::vec![xyk(1, 2)]); + assert!(routes.is_empty()); + } + + #[test] + fn empty_pools_returns_empty() { + let routes = get_routes(1, 2, alloc::vec![]); + assert!(routes.is_empty()); + } + + // -- omnipool specifics -- + + #[test] + fn omnipool_direct_route() { + let routes = get_routes(1, 3, alloc::vec![omnipool(&[1, 2, 3])]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 1); + assert_eq!(routes[0][0].pool, PoolType::Omnipool); + } + + #[test] + fn omnipool_no_multi_hop_through_same_pool() { + let routes = get_routes(1, 3, alloc::vec![omnipool(&[1, 2, 3])]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 1); + } + + // -- stableswap -- + + #[test] + fn stableswap_direct_route() { + let routes = get_routes(1, 3, alloc::vec![stableswap(100, &[1, 2, 3])]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0][0].pool, PoolType::Stableswap(100)); + } + + // -- cross-pool routing -- + + #[test] + fn xyk_bridge_to_omnipool() { + let routes = get_routes(1, 3, alloc::vec![xyk(1, 2), omnipool(&[2, 3])]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 2); + assert_eq!(routes[0][0].pool, PoolType::XYK); + assert_eq!(routes[0][1].pool, PoolType::Omnipool); + } + + #[test] + fn stableswap_then_omnipool() { + let routes = get_routes(1, 3, alloc::vec![stableswap(100, &[1, 2]), omnipool(&[2, 3, 4])]); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 2); + assert_eq!(routes[0][0].pool, PoolType::Stableswap(100)); + assert_eq!(routes[0][1].pool, PoolType::Omnipool); + } + + // -- strategy selection -- + + #[test] + fn trusted_only_excludes_xyk() { + let routes = get_routes(1, 3, alloc::vec![omnipool(&[1, 2, 3]), xyk(1, 2)]); + assert!(routes.iter().all(|r| r.iter().all(|t| t.pool != PoolType::XYK))); + } + + #[test] + fn isolated_only_when_no_trusted_pools_have_assets() { + let routes = get_routes(10, 30, alloc::vec![xyk(10, 20), xyk(20, 30), omnipool(&[1, 2, 3])]); + assert_eq!(routes.len(), 1); + assert!(routes[0].iter().all(|t| t.pool == PoolType::XYK)); + } + + // -- cycle prevention -- + + #[test] + fn no_asset_revisit_in_cycle_graph() { + let routes = get_routes(1, 3, alloc::vec![xyk(1, 2), xyk(2, 3), xyk(3, 1)]); + for route in &routes { + let assets: Vec<_> = core::iter::once(route[0].asset_in) + .chain(route.iter().map(|t| t.asset_out)) + .collect(); + let unique: alloc::collections::BTreeSet<_> = assets.iter().collect(); + assert_eq!(assets.len(), unique.len(), "route revisits an asset"); + } + } + + #[test] + fn different_pool_instances_can_both_be_used() { + let routes = get_routes( + 1, + 4, + alloc::vec![ + stableswap(10, &[1, 2]), + stableswap(20, &[2, 3]), + stableswap(30, &[3, 4]), + ], + ); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 3); + } + + #[test] + fn isolated_only_filters_to_relevant_pools() { + let routes = get_routes(1, 4, alloc::vec![xyk(1, 2), xyk(2, 3), xyk(3, 4)]); + assert!(routes.is_empty()); + } + + // -- max trades limit -- + + #[test] + fn exactly_max_trades_succeeds() { + let pools: Vec<_> = (0u32..9).map(|i| stableswap(i + 100, &[i, i + 1])).collect(); + let routes = get_routes(0, 9, pools); + assert_eq!(routes.len(), 1); + assert_eq!(routes[0].len(), 9); + } + + #[test] + fn exceeding_max_trades_returns_empty() { + let pools: Vec<_> = (0u32..10).map(|i| stableswap(i + 100, &[i, i + 1])).collect(); + let routes = get_routes(0, 10, pools); + assert!(routes.is_empty()); + } + + // -- mainnet snapshot tests -- + + mod mainnet { + use super::*; + use crate::testdata; + + #[test] + fn snapshot_has_expected_pool_count() { + let pools = testdata::mainnet_pools(); + assert_eq!(pools.len(), testdata::POOL_COUNT); + } + + #[test] + fn hdx_to_weth_via_omnipool() { + // HDX=0, WETH=222 — both in Omnipool → direct route expected + let routes = get_routes(0, 222, testdata::mainnet_pools()); + dev_msg!("get_routes 0->222: routes={:#?}", routes); + assert!(!routes.is_empty(), "HDX→WETH should have at least one route"); + assert!(routes.iter().any(|r| r.len() == 1 && r[0].pool == PoolType::Omnipool)); + } + + #[test] + fn usdt_to_usdc_via_stableswap() { + // USDT=10, USDC=22 — both in Stableswap(102) [10, 22, 102] + let routes = get_routes(10, 22, testdata::mainnet_pools()); + dev_msg!("get_routes 10->22: routes={:#?}", routes); + assert!(!routes.is_empty()); + assert!(routes + .iter() + .any(|r| r.iter().any(|t| matches!(t.pool, PoolType::Stableswap(_))))); + } + + #[test] + fn aave_wrapped_to_omnipool_asset() { + // aUSDC=1002 in Aave [10, 1002], Stableswap [1002, ...], HSM [222, 1002] + // WETH=222 in Omnipool — should find multi-hop route + let routes = get_routes(1002, 222, testdata::mainnet_pools()); + dev_msg!("get_routes 1002->222: routes={:#?}", routes); + assert!(!routes.is_empty(), "aUSDC→WETH should find a route"); + } + + #[test] + fn xyk_only_asset_to_omnipool() { + // 27 only in XYK [0, 27], 0 (HDX) in Omnipool + // 222 (WETH) in Omnipool → mixed strategy + let routes = get_routes(27, 222, testdata::mainnet_pools()); + assert!(!routes.is_empty(), "XYK-only asset should bridge to Omnipool"); + assert!(routes.iter().any(|r| r[0].pool == PoolType::XYK)); + } + + #[test] + fn isolated_xyk_pair() { + // 3370 only in XYK [5, 3370], 30 only in XYK [5, 30] + // Neither in trusted pools → isolated-only strategy + let routes = get_routes(3370, 30, testdata::mainnet_pools()); + assert!(routes.iter().all(|r| r.iter().all(|t| t.pool == PoolType::XYK))); + } + + #[test] + fn no_route_to_nonexistent_asset() { + let routes = get_routes(0, 999999, testdata::mainnet_pools()); + assert!(routes.is_empty()); + } + + #[test] + fn all_routes_are_acyclic() { + let routes = get_routes(0, 222, testdata::mainnet_pools()); + for route in &routes { + let assets: Vec<_> = core::iter::once(route[0].asset_in) + .chain(route.iter().map(|t| t.asset_out)) + .collect(); + let unique: alloc::collections::BTreeSet<_> = assets.iter().collect(); + assert_eq!(assets.len(), unique.len(), "route has cycle: {:?}", route); + } + } + + #[test] + fn all_routes_respect_max_trades() { + let routes = get_routes(0, 222, testdata::mainnet_pools()); + for route in &routes { + assert!(route.len() <= 9, "route exceeds MAX_NUMBER_OF_TRADES: {}", route.len()); + } + } + + #[test] + fn hsm_pool_routing() { + // HSM [222, 1002] — both in trusted + let routes = get_routes(222, 1002, testdata::mainnet_pools()); + assert!(routes.iter().any(|r| r.iter().any(|t| t.pool == PoolType::HSM))); + } + } +} diff --git a/ice/route-findr/src/strategy.rs b/ice/route-findr/src/strategy.rs new file mode 100644 index 0000000000..172ad430c5 --- /dev/null +++ b/ice/route-findr/src/strategy.rs @@ -0,0 +1,77 @@ +//! Trusted / isolated pool routing strategy. +//! +//! Ported from `packages/sdk-next/src/sor/route/suggester.ts`. +//! +//! Pools are partitioned into: +//! - **Trusted**: Omnipool, Stableswap, LBP, Aave, HSM — deeper liquidity, preferred. +//! - **Isolated**: XYK — used when assets aren't reachable via trusted pools. +//! +//! The strategy minimises search scope: +//! +//! | `asset_in` in trusted? | `asset_out` in trusted? | Search over | +//! |------------------------|-------------------------|-----------------------| +//! | no | no | relevant isolated | +//! | yes | yes | trusted only | +//! | mixed | mixed | trusted + relevant isolated | + +extern crate alloc; +use alloc::vec::Vec; + +use crate::bfs::find_all_paths; +use crate::graph::build_graph; +use crate::types::{AssetId, PoolEdge, PoolType, Route}; + +/// Returns `true` for pool types considered "trusted" (non-XYK). +fn is_trusted(pool_type: &PoolType) -> bool { + !matches!(pool_type, PoolType::XYK) +} + +/// Check if an asset appears in any of the given pools. +fn asset_in_pools(asset: AssetId, pools: &[PoolEdge]) -> bool { + pools.iter().any(|p| p.assets.contains(&asset)) +} + +/// Discover all valid routes between `asset_in` and `asset_out` using the +/// trusted/isolated pool strategy. +pub fn suggest_routes(asset_in: AssetId, asset_out: AssetId, pools: Vec) -> Vec> { + let (trusted, isolated): (Vec<_>, Vec<_>) = pools.into_iter().partition(|p| is_trusted(&p.pool_type)); + + let in_trusted = asset_in_pools(asset_in, &trusted); + let out_trusted = asset_in_pools(asset_out, &trusted); + + match (in_trusted, out_trusted) { + // Case 1: Neither token in trusted pools → isolated only + (false, false) => { + let relevant: Vec<_> = isolated + .into_iter() + .filter(|p| p.assets.contains(&asset_in) || p.assets.contains(&asset_out)) + .collect(); + let graph = build_graph(&relevant); + find_all_paths(&graph, asset_in, asset_out) + } + + // Case 2: Both tokens in trusted pools → trusted only + (true, true) => { + let graph = build_graph(&trusted); + find_all_paths(&graph, asset_in, asset_out) + } + + // Case 3: Mixed → trusted + relevant isolated + _ => { + let isolated_asset = if !in_trusted { asset_in } else { asset_out }; + let relevant_isolated: Vec<_> = isolated + .into_iter() + .filter(|p| p.assets.contains(&isolated_asset)) + .collect(); + + if relevant_isolated.is_empty() { + return Vec::new(); + } + + let mut combined = trusted; + combined.extend(relevant_isolated); + let graph = build_graph(&combined); + find_all_paths(&graph, asset_in, asset_out) + } + } +} diff --git a/ice/route-findr/src/testdata.rs b/ice/route-findr/src/testdata.rs new file mode 100644 index 0000000000..8615714d7e --- /dev/null +++ b/ice/route-findr/src/testdata.rs @@ -0,0 +1,304 @@ +//! Hydration mainnet pool snapshot for integration tests. +//! +//! Source: SDK `PoolContextProvider.getPools()` — real on-chain state. + +extern crate alloc; +use alloc::vec; +use alloc::vec::Vec; + +use crate::types::{PoolEdge, PoolType}; + +/// Returns the full pool set from a Hydration mainnet snapshot. +pub fn mainnet_pools() -> Vec { + vec![ + // --------------------------------------------------------------- + // Aave pools (19) + // --------------------------------------------------------------- + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![22, 1003], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![10, 1002], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![5, 1001], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![15, 1005], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![1000765, 1006], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![690, 69], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![4200, 420], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![34, 1007], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![103, 1008], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![110, 1110], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![111, 1111], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![112, 1112], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![113, 1113], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![39, 1039], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![43, 1043], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![90001, 9001], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![1000752, 1009], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![44, 1044], + }, + PoolEdge { + pool_type: PoolType::Aave, + assets: vec![10044, 4444], + }, + // --------------------------------------------------------------- + // Omnipool (1) + // --------------------------------------------------------------- + PoolEdge { + pool_type: PoolType::Omnipool, + assets: vec![ + 1000771, 222, 420, 0, 1001, 39, 38, 16, 14, 1000796, 19, 1000795, 35, 33, 15, 1000794, 1000753, + 1000624, 1000765, 9001, 9, 1000752, 1, + ], + }, + // --------------------------------------------------------------- + // Stableswap pools (15) + // --------------------------------------------------------------- + PoolEdge { + pool_type: PoolType::Stableswap(100), + assets: vec![10, 18, 21, 23, 100], + }, + PoolEdge { + pool_type: PoolType::Stableswap(110), + assets: vec![222, 1003, 110], + }, + PoolEdge { + pool_type: PoolType::Stableswap(143), + assets: vec![43, 222, 143], + }, + PoolEdge { + pool_type: PoolType::Stableswap(101), + assets: vec![11, 19, 101], + }, + PoolEdge { + pool_type: PoolType::Stableswap(44), + assets: vec![222, 1044, 10044], + }, + PoolEdge { + pool_type: PoolType::Stableswap(105), + assets: vec![21, 23, 222, 105], + }, + PoolEdge { + pool_type: PoolType::Stableswap(103), + assets: vec![1002, 1000766, 1000767, 103], + }, + PoolEdge { + pool_type: PoolType::Stableswap(111), + assets: vec![222, 1002, 111], + }, + PoolEdge { + pool_type: PoolType::Stableswap(4200), + assets: vec![1007, 1000809, 4200], + }, + PoolEdge { + pool_type: PoolType::Stableswap(104), + assets: vec![20, 1007, 104], + }, + PoolEdge { + pool_type: PoolType::Stableswap(90001), + assets: vec![40, 1009, 90001], + }, + PoolEdge { + pool_type: PoolType::Stableswap(102), + assets: vec![10, 22, 102], + }, + PoolEdge { + pool_type: PoolType::Stableswap(690), + assets: vec![15, 1001, 690], + }, + PoolEdge { + pool_type: PoolType::Stableswap(112), + assets: vec![222, 1000745, 112], + }, + PoolEdge { + pool_type: PoolType::Stableswap(113), + assets: vec![222, 1000625, 113], + }, + // --------------------------------------------------------------- + // HSM pools (4) + // --------------------------------------------------------------- + PoolEdge { + pool_type: PoolType::HSM, + assets: vec![222, 1002], + }, + PoolEdge { + pool_type: PoolType::HSM, + assets: vec![222, 1000745], + }, + PoolEdge { + pool_type: PoolType::HSM, + assets: vec![222, 1000625], + }, + PoolEdge { + pool_type: PoolType::HSM, + assets: vec![222, 1003], + }, + // --------------------------------------------------------------- + // XYK pools (25) + // --------------------------------------------------------------- + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![0, 5], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![0, 27], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![26, 5], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![10, 25], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 30], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![1000081, 34], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 25], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 1000081], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![0, 15], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 3370], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![21, 5], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![0, 10], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![1000085, 0], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 15], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 36], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![252525, 22], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 24], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![1000085, 5], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![39, 222], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![10, 32], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![5, 252525], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![1000081, 15], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![0, 17], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![25, 1000771], + }, + PoolEdge { + pool_type: PoolType::XYK, + assets: vec![1000081, 22], + }, + ] +} + +/// Total number of pools in the mainnet snapshot. +pub const POOL_COUNT: usize = 64; + +/// Total unique asset IDs across all pools. +pub fn unique_asset_count() -> usize { + let pools = mainnet_pools(); + let mut assets = alloc::collections::BTreeSet::new(); + for pool in &pools { + for &a in &pool.assets { + assets.insert(a); + } + } + assets.len() +} diff --git a/ice/route-findr/src/types.rs b/ice/route-findr/src/types.rs new file mode 100644 index 0000000000..e1ef993ec6 --- /dev/null +++ b/ice/route-findr/src/types.rs @@ -0,0 +1,9 @@ +//! Core types for route suggestion. +//! +//! Pool routing types re-exported from `hydradx-traits` and `primitives`. + +pub use hydradx_traits::router::{PoolType, Route, Trade, MAX_NUMBER_OF_TRADES}; +pub use primitives::AssetId; + +/// Concrete `PoolEdge` for this crate's `AssetId`. +pub type PoolEdge = hydradx_traits::router::PoolEdge; diff --git a/ice/solver-bench/Cargo.toml b/ice/solver-bench/Cargo.toml new file mode 100644 index 0000000000..b381ad7eed --- /dev/null +++ b/ice/solver-bench/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "ice-solver-bench" +version = "0.1.0" +edition = "2021" +publish = false + +[dependencies] +hydradx-runtime = { workspace = true, features = ["std"] } +hydradx-traits = { workspace = true, features = ["std"] } +ice-solver = { workspace = true, features = ["std"] } +ice-support = { workspace = true, features = ["std"] } +amm-simulator = { workspace = true, features = ["std"] } +pallet-intent = { workspace = true, features = ["std"] } +pallet-omnipool = { workspace = true, features = ["std"] } +pallet-ema-oracle = { workspace = true, features = ["std"] } +primitives = { workspace = true, features = ["std"] } +frame-support = { workspace = true, features = ["std"] } +frame-system = { workspace = true, features = ["std"] } +frame-remote-externalities = { workspace = true } +sp-core = { workspace = true, features = ["std"] } +sp-runtime = { workspace = true, features = ["std"] } +sp-io = { workspace = true, features = ["std"] } +tokio = { workspace = true } +criterion = { workspace = true } +log = { workspace = true, features = ["std"] } + +[[bench]] +name = "solver" +harness = false diff --git a/ice/solver-bench/baselines/v1.txt b/ice/solver-bench/baselines/v1.txt new file mode 100644 index 0000000000..6787b58204 --- /dev/null +++ b/ice/solver-bench/baselines/v1.txt @@ -0,0 +1,39 @@ +# ICE Solver Benchmark Baseline v1 +# Date: 2026-04-16 +# Machine: Darwin 25.3.0 (Apple Silicon) +# Profile: bench [optimized] +# Snapshot: integration-tests/snapshots/ice/mainnet_apr +# Note: native execution — WASM OCW will be 2-5x slower + +simulator_initial_state 12.99 ms +get_valid_intents/10 0.022 ms +get_valid_intents/50 0.102 ms +get_valid_intents/100 0.200 ms +get_valid_intents/500 1.015 ms +get_valid_intents/1000 2.051 ms +get_valid_intents/5000 10.513 ms +solver_resolvable/10 0.748 ms +solver_resolvable/50 3.503 ms +solver_resolvable/100 6.979 ms +solver_resolvable/200 13.839 ms +solver_unresolvable/10 0.692 ms +solver_unresolvable/50 3.434 ms +solver_unresolvable/100 6.885 ms +solver_unresolvable/500 34.379 ms +solver_unresolvable/1000 69.379 ms +solver_unresolvable/5000 349.680 ms +solver_mixed/50good_50bad 7.307 ms +solver_mixed/50good_500bad 38.748 ms +solver_mixed/50good_5000bad 354.180 ms +solver_mixed/100good_5000bad 355.400 ms +solver_partial/1 0.109 ms +solver_partial/2 1.224 ms +solver_partial/5 3.074 ms +solver_partial/10 5.956 ms +solver_partial/20 12.131 ms +solver_mixed_partial/10np_1p 1.379 ms +solver_mixed_partial/10np_5p 3.845 ms +solver_mixed_partial/10np_10p 6.776 ms +solver_mixed_partial/50np_10p 9.624 ms +solver_mixed_partial/50np_50p 35.150 ms +solver_mixed_partial/100np_20p 19.209 ms diff --git a/ice/solver-bench/benches/solver.rs b/ice/solver-bench/benches/solver.rs new file mode 100644 index 0000000000..5323cf4622 --- /dev/null +++ b/ice/solver-bench/benches/solver.rs @@ -0,0 +1,151 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; + +use amm_simulator::HydrationSimulator; +use ice_solver::v2::Solver as IceSolver; +use ice_solver_bench::{ + clear_intent_storage, generate_mixed_intents, generate_mixed_partial_intents, generate_partial_intents, + generate_resolvable_intents, generate_unresolvable_intents, get_initial_state, load_snapshot, + populate_intent_storage, +}; +use pallet_omnipool::types::SlipFeeConfig; +use sp_runtime::Permill; + +type Solver = IceSolver>; + +const SNAPSHOT_PATH: &str = "../../integration-tests/snapshots/ice/mainnet_apr"; + +fn enable_slip_fees() { + frame_support::assert_ok!(pallet_omnipool::Pallet::::set_slip_fee( + hydradx_runtime::RuntimeOrigin::root(), + Some(SlipFeeConfig { + max_slip_fee: Permill::from_percent(5), + }) + )); +} + +fn bench_initial_state(c: &mut Criterion) { + let mut ext = load_snapshot(SNAPSHOT_PATH); + ext.execute_with(enable_slip_fees); + + c.bench_function("simulator_initial_state", |b| { + b.iter(|| { + ext.execute_with(|| { + black_box(get_initial_state()); + }) + }) + }); +} + +fn bench_resolvable(c: &mut Criterion) { + let mut ext = load_snapshot(SNAPSHOT_PATH); + ext.execute_with(enable_slip_fees); + let state = ext.execute_with(get_initial_state); + + let mut group = c.benchmark_group("solver_resolvable"); + for n in [10, 50, 100, 200] { + let intents = generate_resolvable_intents(n); + group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| { + b.iter(|| ext.execute_with(|| Solver::solve(black_box(intents.clone()), black_box(state.clone())))) + }); + } + group.finish(); +} + +fn bench_unresolvable(c: &mut Criterion) { + let mut ext = load_snapshot(SNAPSHOT_PATH); + ext.execute_with(enable_slip_fees); + let state = ext.execute_with(get_initial_state); + + let mut group = c.benchmark_group("solver_unresolvable"); + for n in [10, 50, 100, 500, 1000, 5000] { + let intents = generate_unresolvable_intents(n); + group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| { + b.iter(|| ext.execute_with(|| Solver::solve(black_box(intents.clone()), black_box(state.clone())))) + }); + } + group.finish(); +} + +fn bench_mixed(c: &mut Criterion) { + let mut ext = load_snapshot(SNAPSHOT_PATH); + ext.execute_with(enable_slip_fees); + let state = ext.execute_with(get_initial_state); + + let mut group = c.benchmark_group("solver_mixed"); + for (good, bad) in [(50, 50), (50, 500), (50, 5000), (100, 5000)] { + let intents = generate_mixed_intents(good, bad); + let label = format!("{}good_{}bad", good, bad); + group.bench_with_input(BenchmarkId::new("intents", &label), &label, |b, _| { + b.iter(|| ext.execute_with(|| Solver::solve(black_box(intents.clone()), black_box(state.clone())))) + }); + } + group.finish(); +} + +fn bench_get_valid_intents(c: &mut Criterion) { + let mut ext = load_snapshot(SNAPSHOT_PATH); + + let mut group = c.benchmark_group("get_valid_intents"); + for n in [10, 50, 100, 500, 1000, 5000] { + // Populate storage with n intents, then benchmark the read + ext.execute_with(|| { + clear_intent_storage(); + populate_intent_storage(n); + }); + + group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| { + b.iter(|| { + ext.execute_with(|| { + black_box(pallet_intent::Pallet::::get_valid_intents()); + }) + }) + }); + } + // Clean up + ext.execute_with(clear_intent_storage); + group.finish(); +} + +fn bench_partial(c: &mut Criterion) { + let mut ext = load_snapshot(SNAPSHOT_PATH); + ext.execute_with(enable_slip_fees); + let state = ext.execute_with(get_initial_state); + + let mut group = c.benchmark_group("solver_partial"); + for n in [1, 2, 5, 10, 20] { + let intents = generate_partial_intents(n); + group.bench_with_input(BenchmarkId::from_parameter(n), &n, |b, _| { + b.iter(|| ext.execute_with(|| Solver::solve(black_box(intents.clone()), black_box(state.clone())))) + }); + } + group.finish(); +} + +fn bench_mixed_partial(c: &mut Criterion) { + let mut ext = load_snapshot(SNAPSHOT_PATH); + ext.execute_with(enable_slip_fees); + let state = ext.execute_with(get_initial_state); + + let mut group = c.benchmark_group("solver_mixed_partial"); + // (non-partial, partial) + for (np, p) in [(10, 1), (10, 5), (10, 10), (50, 10), (50, 50), (100, 20)] { + let intents = generate_mixed_partial_intents(np, p); + let label = format!("{}np_{}p", np, p); + group.bench_with_input(BenchmarkId::new("intents", &label), &label, |b, _| { + b.iter(|| ext.execute_with(|| Solver::solve(black_box(intents.clone()), black_box(state.clone())))) + }); + } + group.finish(); +} + +criterion_group!( + benches, + bench_initial_state, + bench_get_valid_intents, + bench_resolvable, + bench_unresolvable, + bench_mixed, + bench_partial, + bench_mixed_partial +); +criterion_main!(benches); diff --git a/ice/solver-bench/src/lib.rs b/ice/solver-bench/src/lib.rs new file mode 100644 index 0000000000..67e995b613 --- /dev/null +++ b/ice/solver-bench/src/lib.rs @@ -0,0 +1,190 @@ +use frame_support::traits::OnRuntimeUpgrade; +use hydradx_traits::amm::{SimulatorConfig, SimulatorSet}; +use ice_support::{IntentData, Partial, SwapData}; + +// Re-export both Intent types under distinct names +pub use ice_support::Intent as SolverIntent; +pub use pallet_intent::types::Intent as StorageIntent; + +pub type CombinedSimulatorState = + <::Simulators as SimulatorSet>::State; + +pub fn load_snapshot(path: &str) -> frame_remote_externalities::RemoteExternalities { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async { + use frame_remote_externalities::*; + + let snapshot_config = SnapshotConfig::from(String::from(path)); + let offline_config = OfflineConfig { + state_snapshot: snapshot_config, + }; + let mode = Mode::Offline(offline_config); + let builder = Builder::::new().mode(mode); + + let mut p = builder.build().await.unwrap(); + p.execute_with(|| { + pallet_ema_oracle::migrations::v1::MigrateV0ToV1::::on_runtime_upgrade(); + }); + p + }) +} + +/// Must be called inside `execute_with` — reads pool state from storage. +pub fn get_initial_state() -> CombinedSimulatorState { + ::Simulators::initial_state() +} + +/// Generate `count` resolvable intents (alternating HDX→BNC and BNC→HDX). +pub fn generate_resolvable_intents(count: usize) -> Vec { + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + (0..count) + .map(|i| { + let (asset_in, asset_out, amount_in, amount_out) = if i % 2 == 0 { + (hdx, bnc, 500 * hdx_unit, bnc_unit) + } else { + (bnc, hdx, 30 * bnc_unit, hdx_unit) + }; + SolverIntent { + id: i as u128 + 1, + data: IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::No, + }), + } + }) + .collect() +} + +/// Generate `count` unresolvable intents (absurd min_out that no AMM can satisfy). +pub fn generate_unresolvable_intents(count: usize) -> Vec { + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + (0..count) + .map(|i| { + // Sell 1 HDX, demand 1_000_000 BNC — impossible + SolverIntent { + id: (i + 100_000) as u128, + data: IntentData::Swap(SwapData { + asset_in: hdx, + asset_out: bnc, + amount_in: hdx_unit, + amount_out: 1_000_000 * bnc_unit, + partial: Partial::No, + }), + } + }) + .collect() +} + +/// Generate a mixed batch: `resolvable` good intents + `unresolvable` bad intents, interleaved. +pub fn generate_mixed_intents(resolvable: usize, unresolvable: usize) -> Vec { + let good = generate_resolvable_intents(resolvable); + let bad = generate_unresolvable_intents(unresolvable); + + let mut mixed = Vec::with_capacity(good.len() + bad.len()); + let mut gi = good.into_iter(); + let mut bi = bad.into_iter(); + loop { + match (gi.next(), bi.next()) { + (Some(g), Some(b)) => { + mixed.push(g); + mixed.push(b); + } + (Some(g), None) => mixed.push(g), + (None, Some(b)) => mixed.push(b), + (None, None) => break, + } + } + mixed +} + +/// Insert `count` swap intents directly into pallet-intent storage. +/// Must be called inside `execute_with`. +pub fn populate_intent_storage(count: usize) { + use ice_support::SwapData as IceSwapData; + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + for i in 0..count { + let id = (i + 1) as u128; + let (asset_in, asset_out, amount_in, amount_out) = if i % 2 == 0 { + (hdx, bnc, 500 * hdx_unit, bnc_unit) + } else { + (bnc, hdx, 30 * bnc_unit, hdx_unit) + }; + + let intent = StorageIntent { + data: IntentData::Swap(IceSwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::No, + }), + deadline: None, + on_resolved: None, + }; + + pallet_intent::Intents::::insert(id, intent); + } +} + +/// Remove all intents from storage. Must be called inside `execute_with`. +pub fn clear_intent_storage() { + let _ = pallet_intent::Intents::::clear(u32::MAX, None); +} + +/// Generate `count` partial-fill intents (alternating HDX→BNC and BNC→HDX). +/// Uses large amounts with tight limits to exercise the binary search. +pub fn generate_partial_intents(count: usize) -> Vec { + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + (0..count) + .map(|i| { + let (asset_in, asset_out, amount_in, amount_out) = if i % 2 == 0 { + // Large HDX→BNC with tight limit (~0.065 BNC/HDX, spot is ~0.068) + (hdx, bnc, 500_000 * hdx_unit, 32_500 * bnc_unit) + } else { + // Large BNC→HDX with tight limit + (bnc, hdx, 30_000 * bnc_unit, 400_000 * hdx_unit) + }; + SolverIntent { + id: (i + 200_000) as u128, + data: IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::Yes(0), + }), + } + }) + .collect() +} + +/// Generate a batch with `non_partial` non-partial + `partial` partial intents. +pub fn generate_mixed_partial_intents(non_partial: usize, partial: usize) -> Vec { + let mut intents = generate_resolvable_intents(non_partial); + let mut partials = generate_partial_intents(partial); + intents.append(&mut partials); + intents +} diff --git a/integration-tests/Cargo.toml b/integration-tests/Cargo.toml index 62ca08d5a0..fd2cbe6f34 100644 --- a/integration-tests/Cargo.toml +++ b/integration-tests/Cargo.toml @@ -59,6 +59,12 @@ pallet-conviction-voting = { workspace = true } pallet-dispatcher = { workspace = true } pallet-proxy = { workspace = true } pallet-hsm = { workspace = true } +pallet-intent = { workspace = true } +pallet-ice = { workspace = true } +amm-simulator = { workspace = true } + +ice-solver = {workspace = true} +ice-support = {workspace = true} # collator support pallet-collator-selection = { workspace = true } @@ -161,6 +167,7 @@ libsecp256k1 = { workspace = true } [features] default = ["std"] +ice-record = [] std = [ "codec/std", "frame-executive/std", @@ -234,6 +241,12 @@ std = [ "ethereum/std", "pallet-ethereum/std", "fp-self-contained/std", + "fp-self-contained/std", + "pallet-intent/std", + "pallet-ice/std", + "ice-solver/std", + "ice-support/std", + "amm-simulator/std", ] # we don't include integration tests when benchmarking feature is enabled diff --git a/integration-tests/snapshots/ice/mainnet_apr b/integration-tests/snapshots/ice/mainnet_apr new file mode 100644 index 0000000000..8a5c0c56ec Binary files /dev/null and b/integration-tests/snapshots/ice/mainnet_apr differ diff --git a/integration-tests/src/aave_simulator.rs b/integration-tests/src/aave_simulator.rs new file mode 100644 index 0000000000..dd12f606fe --- /dev/null +++ b/integration-tests/src/aave_simulator.rs @@ -0,0 +1,231 @@ +use crate::polkadot_test_net::hydra_live_ext; +use crate::polkadot_test_net::hydradx_run_to_next_block; +use crate::polkadot_test_net::TestNet; +use crate::polkadot_test_net::HDX; +use crate::polkadot_test_net::LRNA; +use crate::polkadot_test_net::UNITS; +use amm_simulator::aave::ReserveData; +use amm_simulator::aave::Simulator; +use frame_support::assert_err; +use hex_literal::hex; +use hydra_dx_math::types::Ratio; +use hydradx_runtime::ice_simulator_provider::Aave; +use hydradx_runtime::Runtime; +use hydradx_traits::amm::AmmSimulator; +use hydradx_traits::amm::SimulatorError; +use hydradx_traits::amm::TradeResult; +use sp_core::U256; +use xcm_emulator::Network; + +const DOT: u32 = 5; +const A_DOT: u32 = 1001; + +pub const PATH_TO_SNAPSHOT: &str = + "snapshots/aave-simulator/7e10e2d20d0eb4293b3b5da688c63cffbb24b2cda27fd3abc85bf13b3656c98c"; + +#[test] +fn create_snapshot_should_work() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + hydradx_run_to_next_block(); + + let expected_dot = ReserveData { + configuration: U256::from_dec_str("753997831161164877079002568592629221489798055993152").unwrap(), + liquidity_index: U256::from_dec_str("1035336136294736724440835214").unwrap(), + current_liquidity_rate: U256::from_dec_str("1065196554024159900141310364").unwrap(), + variable_borrow_index: U256::from_dec_str("51028877334674195433308708").unwrap(), + current_variable_borrow_rate: U256::from_dec_str("79060184166853553946851366").unwrap(), + current_stable_borrow_rate: U256::from_dec_str("149060184166853553946851366").unwrap(), + last_update_timestamp: U256::from_dec_str("1769589174").unwrap(), + id: 3, + atoken_address: sp_core::H160(hex!("02639ec01313c8775fae74f2dad1118c8a8a86da")), + stable_debt_token_address: sp_core::H160(hex!("dc92f2fd6137b0bd5766ddf59c39c828b24f5248")), + variable_debt_token_address: sp_core::H160(hex!("34321cb7334807eb718b3e1ddfaeb0c6c0403f1a")), + interest_rate_strategy_address: sp_core::H160(hex!("b2dc5c391c6ed54880da06fe786f6f28d9fd99a6")), + accrued_to_treasury: U256::from_dec_str("32814671262692").unwrap(), + scaled_total_supply: U256::from_dec_str("99494530926548567").unwrap(), + }; + + let expected_hollar = ReserveData { + configuration: U256::from_dec_str("365354519770431488").unwrap(), + liquidity_index: U256::from_dec_str("1000000000000000000000000000").unwrap(), + current_liquidity_rate: U256::from_dec_str("1017192592529644194792669728").unwrap(), + variable_borrow_index: U256::from_dec_str("0").unwrap(), + current_variable_borrow_rate: U256::from_dec_str("48790164996148630000000000").unwrap(), + current_stable_borrow_rate: U256::from_dec_str("0").unwrap(), + last_update_timestamp: U256::from_dec_str("1769569944").unwrap(), + id: 10, + atoken_address: sp_core::H160(hex!("8c0f3b9602374198974d2b2679d14a386f5b108e")), + stable_debt_token_address: sp_core::H160(hex!("d95d27688f028addbe93fa0e19fb095ee1111dd1")), + variable_debt_token_address: sp_core::H160(hex!("342923782ccaebf9c38dd9cb40436e82c42c73b5")), + interest_rate_strategy_address: sp_core::H160(hex!("6277f67402f9a7032e4c90c796b74343418e3628")), + accrued_to_treasury: U256::from_dec_str("0").unwrap(), + scaled_total_supply: U256::from_dec_str("0").unwrap(), + }; + + let expected_gdot = ReserveData { + configuration: U256::from_dec_str("753997831576548625741237039960066689952748640410356").unwrap(), + liquidity_index: U256::from_dec_str("1000000000000000000000000000").unwrap(), + current_liquidity_rate: U256::from_dec_str("1000000000000000000000000000").unwrap(), + variable_borrow_index: U256::from_dec_str("0").unwrap(), + current_variable_borrow_rate: U256::from_dec_str("0").unwrap(), + current_stable_borrow_rate: U256::from_dec_str("90000000000000000000000000").unwrap(), + last_update_timestamp: U256::from_dec_str("1769585214").unwrap(), + id: 6, + atoken_address: sp_core::H160(hex!("34d5ffb83d14d82f87aaf2f13be895a3c814c2ad")), + stable_debt_token_address: sp_core::H160(hex!("6fc3b2f6584b3bd4502ebbc3738903a0968a8767")), + variable_debt_token_address: sp_core::H160(hex!("6bc2a0ac2495c0cdf5116d0df5d8052fccbc4d4e")), + interest_rate_strategy_address: sp_core::H160(hex!("5383a606ece147e94c1fa0b7375bc778f132b832")), + accrued_to_treasury: U256::from_dec_str("0").unwrap(), + scaled_total_supply: U256::from_dec_str("10487846414586294956464513").unwrap(), + }; + + let expected_geth = ReserveData { + configuration: U256::from_dec_str("1128142248241621894702555553377248808488946780872512").unwrap(), + liquidity_index: U256::from_dec_str("1000000000000000000000000000").unwrap(), + current_liquidity_rate: U256::from_dec_str("1000000000000000000000000000").unwrap(), + variable_borrow_index: U256::from_dec_str("0").unwrap(), + current_variable_borrow_rate: U256::from_dec_str("0").unwrap(), + current_stable_borrow_rate: U256::from_dec_str("90000000000000000000000000").unwrap(), + last_update_timestamp: U256::from_dec_str("1769589342").unwrap(), + id: 7, + atoken_address: sp_core::H160(hex!("8a598fe3e3a471ce865332e330d303502a0e2f52")), + stable_debt_token_address: sp_core::H160(hex!("62a0e4f1c38b4f41aeeac727f29854097b478811")), + variable_debt_token_address: sp_core::H160(hex!("fb2e66d76d2841443ab41102369ff33df9bc9a93")), + interest_rate_strategy_address: sp_core::H160(hex!("5383a606ece147e94c1fa0b7375bc778f132b832")), + accrued_to_treasury: U256::from_dec_str("0").unwrap(), + scaled_total_supply: U256::from_dec_str("2355034935436638803964").unwrap(), + }; + + let expected_usdt = ReserveData { + configuration: U256::from_dec_str("379853410758302483957202436554183033238679701692224").unwrap(), + liquidity_index: U256::from_dec_str("1045395624087717879065064539").unwrap(), + current_liquidity_rate: U256::from_dec_str("1079125208703227655761523015").unwrap(), + variable_borrow_index: U256::from_dec_str("19728462736792637876639013").unwrap(), + current_variable_borrow_rate: U256::from_dec_str("44583604606801630982965448").unwrap(), + current_stable_borrow_rate: U256::from_dec_str("53072950575850203872870681").unwrap(), + last_update_timestamp: U256::from_dec_str("1769589570").unwrap(), + id: 1, + atoken_address: sp_core::H160(hex!("c64980e4eaf9a1151bd21712b9946b81e41e2b92")), + stable_debt_token_address: sp_core::H160(hex!("6863e05d3f794903e76056cc751c1b2006728380")), + variable_debt_token_address: sp_core::H160(hex!("32a8090e20748e530670ff520c4abc903db7e127")), + interest_rate_strategy_address: sp_core::H160(hex!("aa659cf1ce049ec00161d305b17e70a5c1a7382f")), + accrued_to_treasury: U256::from_dec_str("1009336828").unwrap(), + scaled_total_supply: U256::from_dec_str("9468205889716").unwrap(), + }; + + let snapshot = Simulator::>::snapshot(); + + assert_eq!(snapshot.reserves.get(&5), Some(&expected_dot)); + assert_eq!(snapshot.reserves.get(&222), Some(&expected_hollar)); + assert_eq!(snapshot.reserves.get(&690), Some(&expected_gdot)); + assert_eq!(snapshot.reserves.get(&4200), Some(&expected_geth)); + assert_eq!(snapshot.reserves.get(&10), Some(&expected_usdt)); + + assert_eq!(snapshot.reserves.len(), 16); + }); +} + +#[test] +fn simulate_sell_should_fail_when_no_asset_is_reserve_asset() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + hydradx_run_to_next_block(); + + type Sim = Simulator>; + let snapshot = Sim::snapshot(); + + assert_err!( + Sim::simulate_sell(HDX, LRNA, 1_000 * UNITS, 1, &snapshot), + SimulatorError::AssetNotFound + ); + }); +} + +#[test] +fn simulate_buy_should_fail_when_no_asset_is_reserve_asset() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + hydradx_run_to_next_block(); + + type Sim = Simulator>; + let snapshot = Sim::snapshot(); + + assert_err!( + Sim::simulate_buy(HDX, LRNA, 1_000 * UNITS, 1, &snapshot), + SimulatorError::AssetNotFound + ); + }); +} + +#[test] +fn simulate_sell_should_work() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + hydradx_run_to_next_block(); + + type Sim = Simulator>; + let snapshot = Sim::snapshot(); + + let (s, r) = Sim::simulate_sell(DOT, A_DOT, 1_000 * UNITS, 1, &snapshot).unwrap(); + + assert_eq!(s, snapshot); + assert_eq!( + r, + TradeResult { + amount_in: 1_000 * UNITS, + amount_out: 1_000 * UNITS, + } + ) + }); +} + +#[test] +fn simulate_buy_should_work() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + hydradx_run_to_next_block(); + + type Sim = Simulator>; + let snapshot = Sim::snapshot(); + + let (s, r) = Sim::simulate_buy(DOT, A_DOT, 1_000 * UNITS, 1, &snapshot).unwrap(); + + assert_eq!(s, snapshot); + assert_eq!( + r, + TradeResult { + amount_in: 1_000 * UNITS, + amount_out: 1_000 * UNITS, + } + ) + }); +} + +#[test] +fn get_spot_price_should_fail_when_no_asset_is_reserve_asset() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + hydradx_run_to_next_block(); + + type Sim = Simulator>; + let snapshot = Sim::snapshot(); + + assert_err!(Sim::get_spot_price(HDX, LRNA, &snapshot), SimulatorError::AssetNotFound); + }); +} + +#[test] +fn get_spot_price_should_work() { + TestNet::reset(); + hydra_live_ext(PATH_TO_SNAPSHOT).execute_with(|| { + hydradx_run_to_next_block(); + + type Sim = Simulator>; + let snapshot = Sim::snapshot(); + + let sp = Sim::get_spot_price(DOT, A_DOT, &snapshot).unwrap(); + + assert_eq!(sp, Ratio { n: 1, d: 1 }); + }); +} diff --git a/integration-tests/src/account_nonce.rs b/integration-tests/src/account_nonce.rs new file mode 100644 index 0000000000..f0b0897f52 --- /dev/null +++ b/integration-tests/src/account_nonce.rs @@ -0,0 +1,624 @@ +#![cfg(test)] + +use crate::polkadot_test_net::*; +use crate::utils::accounts::*; +use ethabi::ethereum_types::BigEndianHash; +use hydradx_runtime::evm::{Erc20Currency, Executor, Function}; + +use crate::utils::contracts::deploy_contract; +use crate::utils::executive::assert_executive_apply_signed_extrinsic; +use frame_support::dispatch::GetDispatchInfo; +use frame_support::pallet_prelude::ValidateUnsigned; +use frame_support::storage::with_transaction; +use frame_support::traits::fungible::Mutate; +use frame_support::traits::Contains; +use frame_support::{assert_noop, assert_ok, sp_runtime::codec::Encode}; +use frame_system::RawOrigin; +use hydradx_adapters::price::ConvertBalance; +use hydradx_runtime::evm::precompiles::{CALLPERMIT, DISPATCH_ADDR}; +use hydradx_runtime::types::ShortOraclePrice; +use hydradx_runtime::DOT_ASSET_LOCATION; +use hydradx_runtime::XYK; +use hydradx_runtime::{AssetLocation, EVMAccounts, System}; +use hydradx_runtime::{AssetRegistry, TreasuryAccount}; +use hydradx_runtime::{ + Balances, Currencies, DotAssetId, MultiTransactionPayment, Omnipool, RuntimeCall, RuntimeOrigin, Tokens, + XykPaymentAssetSupport, +}; +use hydradx_runtime::{FixedU128, Runtime}; +use hydradx_traits::evm::ERC20; +use hydradx_traits::evm::{CallContext, EVM}; +use hydradx_traits::AssetKind; +use hydradx_traits::Create; +use hydradx_traits::Mutate as AssetRegistryMutate; +use libsecp256k1::{sign, Message, SecretKey}; +use orml_traits::MultiCurrency; +use pallet_evm_accounts::EvmNonceProvider; +use pallet_transaction_multi_payment::EVMPermit; +use polkadot_xcm::v3::Junction::AccountKey20; +use polkadot_xcm::v3::Junctions::X1; +use polkadot_xcm::v3::MultiLocation; +use pretty_assertions::assert_eq; +use primitives::constants::currency::UNITS; +use primitives::{AssetId, Balance, EvmAddress}; +use sp_core::{Pair, H256, U256}; +use sp_runtime::traits::SignedExtension; +use sp_runtime::traits::{Convert, IdentifyAccount}; +use sp_runtime::transaction_validity::InvalidTransaction; +use sp_runtime::transaction_validity::TransactionValidityError; +use sp_runtime::transaction_validity::{TransactionSource, ValidTransaction}; +use sp_runtime::Permill; +use sp_runtime::TransactionOutcome; +use sp_runtime::{DispatchResult, SaturatedConversion}; +use xcm_emulator::TestExt; + +pub const TREASURY_ACCOUNT_INIT_BALANCE: Balance = 1000 * UNITS; + +pub const PATH_TO_SNAPSHOT: &str = "snapshots/hsm/mainnet_nov"; + +fn test_user_evm_account() -> EvmAddress { + alith_evm_address() +} + +fn test_user_new_account() -> MockAccount { + MockAccount::new(alith_evm_account()) +} + +fn treasury_account() -> MockAccount { + MockAccount::new(Treasury::account_id()) +} + +fn deployer() -> hydradx_traits::evm::EvmAddress { + EVMAccounts::evm_address(&Into::::into(ALICE)) +} + +fn deploy_token_contract() -> hydradx_traits::evm::EvmAddress { + deploy_contract("HydraToken", crate::erc20::deployer()) +} + +pub fn bind_erc20(contract: hydradx_traits::evm::EvmAddress) -> AssetId { + let token = CallContext::new_view(contract); + let asset = with_transaction(|| { + TransactionOutcome::Commit(AssetRegistry::register_sufficient_asset( + None, + Some(Erc20Currency::::name(token).unwrap().try_into().unwrap()), + AssetKind::Erc20, + 1, + Some(Erc20Currency::::symbol(token).unwrap().try_into().unwrap()), + Some(Erc20Currency::::decimals(token).unwrap()), + Some(AssetLocation(MultiLocation::new( + 0, + X1(AccountKey20 { + key: contract.into(), + network: None, + }), + ))), + None, + )) + }); + asset.unwrap() +} + +#[test] +fn address_should_have_increased_providers_when_receive_erco20() { + TestNet::reset(); + Hydra::execute_with(|| { + let user_acc = MockAccount::new(evm_account()); + + let token = deploy_token_contract(); + + assert_ok!( as ERC20>::transfer( + CallContext { + contract: token, + sender: deployer(), + origin: deployer() + }, + evm_address(), + 100 + )); + + std::assert_eq!( + Erc20Currency::::balance_of(CallContext::new_view(token), evm_address()), + 100 + ); + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + }); +} + +#[test] +fn haha() { + TestNet::reset(); + Hydra::execute_with(|| { + let user_acc = MockAccount::new(evm_account()); + + let token = deploy_token_contract(); + let context = CallContext { + contract: token, + sender: deployer(), + origin: deployer(), + }; + + let mut data = Into::::into(Function::Transfer).to_be_bytes().to_vec(); + data.extend_from_slice(H256::from(evm_address()).as_bytes()); + data.extend_from_slice(H256::from_uint(&U256::from(100u128.saturated_into::())).as_bytes()); + + let r = Executor::::call(context, data, U256::zero(), 400_000); + + assert_eq!( + Erc20Currency::::balance_of(CallContext::new_view(token), evm_address()), + 100 + ); + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + user_acc.address(), + HDX, + 1_000_000_000_000_000_i128, + )); + + dbg!(user_acc.balance(HDX)); + + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + + assert_ok!( as ERC20>::transfer( + CallContext { + contract: token, + sender: evm_address(), + origin: evm_address() + }, + deployer(), + 100 + )); + + std::assert_eq!( + Erc20Currency::::balance_of(CallContext::new_view(token), evm_address()), + 0 + ); + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + dbg!(user_acc.balance(HDX)); + + //let info = user_acc.account_info(); + //assert_eq!(info.providers, 1); + + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + user_acc.address(), + HDX, + 1_000_000_000_000_000_i128, + )); + + dbg!(user_acc.balance(HDX)); + }); +} + +#[test] +fn account_providers_should_increase_when_transferring_native_asset_to_new_account() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let user_acc = test_user_new_account(); + let treasury_acc = treasury_account(); + + assert!(!frame_system::Account::::contains_key( + user_acc.address() + )); + + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 0, + 1_000_000_000_000_000, + )); + + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + }); +} + +#[test] +fn account_providers_should_increase_when_transferring_nonnative_asset_to_new_account() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let user_acc = test_user_new_account(); + let treasury_acc = treasury_account(); + + assert!(!frame_system::Account::::contains_key( + user_acc.address() + )); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 1, + 1_000_000_000_000_000, + )); + + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + }); +} + +#[test] +fn account_providers_should_increase_when_transferring_erc20_asset_to_new_account() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let user_acc = test_user_new_account(); + let treasury_acc = treasury_account(); + + assert!(!frame_system::Account::::contains_key( + user_acc.address() + )); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 222, + 1_000_000_000_000_000_000_000, + )); + + assert!( + frame_system::Account::::contains_key(user_acc.address()), + "New account with balance not found in the system" + ); + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + }); +} + +#[test] +fn account_providers_should_increase_for_each_new_asset() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let user_acc = test_user_new_account(); + let treasury_acc = treasury_account(); + + assert!(!frame_system::Account::::contains_key( + user_acc.address() + )); + + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 0, + 1_000_000_000_000_000, + )); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 20, + 1_000_000_000_000_000, + )); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 1, + 1_000_000_000_000_000, + )); + + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 3); + }); +} + +#[test] +fn removing_all_but_erc20_should_not_lock_you_out() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let user_acc = test_user_new_account(); + let treasury_acc = treasury_account(); + + assert!(!frame_system::Account::::contains_key( + user_acc.address() + )); + + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 0, + 1_000_000_000_000_000, + )); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 222, + 1_000_000_000_000_000_000_000, + )); + + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(user_acc.address()), + treasury_acc.address().into(), + 0, + 1_000_000_000_000_000, + )); + + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + dbg!(&info); + assert_eq!(info.providers, 1); + }); +} + +#[test] +fn account_nonce_should_correctly_increase_when_signing_transaction() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let (pair, _) = sp_core::sr25519::Pair::generate(); + let user_acc = MockAccount::new(sp_runtime::MultiSigner::from(pair.public()).into_account()); + let treasury_acc = treasury_account(); + hydradx_run_to_next_block(); + + // Send some HDX for fee + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 0, + 1_000_000_000_000_000, + )); + + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 0); + + let remark = hydradx_runtime::RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); + + let _ = assert_executive_apply_signed_extrinsic(remark, pair); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 1); + }); +} + +#[test] +fn account_nonce_should_correctly_increase_when_signing_transaction_with_nonnative_currency() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let (pair, _) = sp_core::sr25519::Pair::generate(); + let user_acc = MockAccount::new(sp_runtime::MultiSigner::from(pair.public()).into_account()); + let treasury_acc = treasury_account(); + hydradx_run_to_next_block(); + + // Send some HDX for fee + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 20, + 100_000_000_000_000_000, + )); + + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 0); + + let payment_asset = pallet_transaction_multi_payment::AccountCurrencyMap::::get(user_acc.address()); + dbg!(payment_asset); + assert_eq!(payment_asset, Some(WETH)); + + let remark = hydradx_runtime::RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); + + let r = assert_executive_apply_signed_extrinsic(remark, pair); + dbg!(r); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 1); + }); +} + +#[test] +fn account_nonce_should_correctly_increase_when_signing_transaction_with_erc20_currrency() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let (pair, _) = sp_core::sr25519::Pair::generate(); + let user_acc = MockAccount::new(sp_runtime::MultiSigner::from(pair.public()).into_account()); + let treasury_acc = treasury_account(); + hydradx_run_to_next_block(); + + // Send some HDX for fee + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 222, + 1_000_000_000_000_000_000, + )); + + /* + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 0); + + let payment_asset = pallet_transaction_multi_payment::AccountCurrencyMap::::get(user_acc.address()); + dbg!(payment_asset); + assert_eq!(payment_asset, Some(WETH)); + + */ + + let remark = hydradx_runtime::RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); + + let r = assert_executive_apply_signed_extrinsic(remark, pair); + dbg!(r); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 1); + }); +} + +#[test] +fn account_with_erc20_only_should_work() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let (pair, _) = sp_core::sr25519::Pair::generate(); + let user_acc = MockAccount::new(sp_runtime::MultiSigner::from(pair.public()).into_account()); + let treasury_acc = treasury_account(); + hydradx_run_to_next_block(); + + // Send some HDX for fee + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 222, + 1_000_000_000_000_000_000, + )); + + /* + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 0); + + let payment_asset = pallet_transaction_multi_payment::AccountCurrencyMap::::get(user_acc.address()); + dbg!(payment_asset); + assert_eq!(payment_asset, Some(WETH)); + + */ + + let call = RuntimeCall::MultiTransactionPayment(pallet_transaction_multi_payment::Call::set_currency { + currency: 222, + }); + + let r = assert_executive_apply_signed_extrinsic(call, pair); + dbg!(r); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 1); + }); +} + +#[test] +fn account_with_erc20_and_hdx_should_work() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let (pair, _) = sp_core::sr25519::Pair::generate(); + let user_acc = MockAccount::new(sp_runtime::MultiSigner::from(pair.public()).into_account()); + let treasury_acc = treasury_account(); + hydradx_run_to_next_block(); + + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 0, + 1_000_000_000_000, + )); + + // Send some HDX for fee + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 222, + 1_000_000_000_000_000_000, + )); + + /* + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 0); + + let payment_asset = pallet_transaction_multi_payment::AccountCurrencyMap::::get(user_acc.address()); + dbg!(payment_asset); + assert_eq!(payment_asset, Some(WETH)); + + */ + + let call = RuntimeCall::MultiTransactionPayment(pallet_transaction_multi_payment::Call::set_currency { + currency: 222, + }); + + let r = assert_executive_apply_signed_extrinsic(call, pair); + dbg!(r); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 1); + + dbg!(user_acc.balance(0)); + dbg!(user_acc.balance(222)); + }); +} + +#[test] +fn account_nonce_should_be_handled_correctly_during_permit_execution() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let (pair, _) = sp_core::sr25519::Pair::generate(); + let user_acc = MockAccount::new(sp_runtime::MultiSigner::from(pair.public()).into_account()); + let treasury_acc = treasury_account(); + hydradx_run_to_next_block(); + + // Send some HDX for fee + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(treasury_acc.address()), + user_acc.address().into(), + 222, + 1_000_000_000_000_000_000, + )); + + /* + assert!(frame_system::Account::::contains_key( + user_acc.address() + )); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 0); + + let payment_asset = pallet_transaction_multi_payment::AccountCurrencyMap::::get(user_acc.address()); + dbg!(payment_asset); + assert_eq!(payment_asset, Some(WETH)); + + */ + + let remark = hydradx_runtime::RuntimeCall::System(frame_system::Call::remark { remark: vec![] }); + + let r = assert_executive_apply_signed_extrinsic(remark, pair); + dbg!(r); + + let info = user_acc.account_info(); + assert_eq!(info.providers, 1); + assert_eq!(info.nonce, 1); + }); +} diff --git a/integration-tests/src/dca.rs b/integration-tests/src/dca.rs index 6571f9be31..63f2b181c6 100644 --- a/integration-tests/src/dca.rs +++ b/integration-tests/src/dca.rs @@ -5407,7 +5407,7 @@ fn add_dot_as_payment_currency_with_details(amount: Balance, price: FixedU128) { //crate::dca::do_trade_to_populate_oracle(DOT, HDX, UNITS); } -mod extra_gas_erc20 { +pub(crate) mod extra_gas_erc20 { use super::*; use hydradx_runtime::{FixedU128, MultiTransactionPayment, Router}; @@ -5982,7 +5982,11 @@ mod extra_gas_erc20 { /// /// # Returns /// The deployed contract's EVM address - fn deploy_conditional_gas_eater(router_address: EvmAddress, gas_to_waste: u64, deployer: EvmAddress) -> EvmAddress { + pub(crate) fn deploy_conditional_gas_eater( + router_address: EvmAddress, + gas_to_waste: u64, + deployer: EvmAddress, + ) -> EvmAddress { use ethabi::{encode, Token}; // Get base bytecode from compiled artifact diff --git a/integration-tests/src/driver/mod.rs b/integration-tests/src/driver/mod.rs index aaba010ad6..2d47044175 100644 --- a/integration-tests/src/driver/mod.rs +++ b/integration-tests/src/driver/mod.rs @@ -1,22 +1,32 @@ mod example; use crate::polkadot_test_net::*; +use amm_simulator::HydrationSimulator; use frame_support::assert_ok; use frame_support::traits::fungible::Mutate; +use frame_support::traits::Time; use frame_support::BoundedVec; use hydradx_runtime::AssetLocation; use hydradx_runtime::*; +use hydradx_traits::amm::{SimulatorConfig, SimulatorSet}; use hydradx_traits::stableswap::AssetAmount; use hydradx_traits::AggregatedPriceOracle; +use ice_support::{DcaParams, IntentDataInput, SwapParams}; use pallet_asset_registry::AssetType; use pallet_stableswap::MAX_ASSETS_IN_POOL; use primitives::constants::chain::{OMNIPOOL_SOURCE, STABLESWAP_SOURCE}; +use primitives::constants::time::MILLISECS_PER_BLOCK; use primitives::{AccountId, AssetId}; use sp_runtime::traits::Convert; use sp_runtime::{FixedU128, Permill}; use sp_std::cell::RefCell; use xcm_emulator::TestExt; +use ice_solver::v2::Solver as IceSolver; +use pallet_omnipool::types::SlipFeeConfig; + +type Solver = IceSolver>; + type BoundedName = BoundedVec::StringLimit>; pub(crate) struct HydrationTestDriver { omnipool_assets: Vec, @@ -403,6 +413,116 @@ impl HydrationTestDriver { }); self } + + pub fn advance(&self, blocks: u32) -> &Self { + self.execute(|| { + for _ in 0..blocks { + hydradx_run_to_next_block(); + } + }); + self + } + + pub fn submit_swap_intent( + &self, + who: AccountId, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + amount_out: Balance, + deadline_in_blocks: Option, + ) -> &Self { + self.execute(|| { + let ts = Timestamp::now(); + let deadline = deadline_in_blocks.map(|d| MILLISECS_PER_BLOCK * d as u64 + ts); + assert_ok!(Intent::submit_intent( + RuntimeOrigin::signed(who), + pallet_intent::types::IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in, + asset_out, + amount_in, + amount_out, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + }); + self + } + + #[expect(dead_code, reason = "will be used once DCA integration tests call this helper")] + pub fn submit_dca_intent( + &self, + who: AccountId, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + amount_out: Balance, + slippage: Permill, + budget: Option, + period: u32, + ) -> &Self { + self.execute(|| { + assert_ok!(Intent::submit_intent( + RuntimeOrigin::signed(who), + pallet_intent::types::IntentInput { + data: IntentDataInput::Dca(DcaParams { + asset_in, + asset_out, + amount_in, + amount_out, + slippage, + budget, + period, + }), + deadline: None, + on_resolved: None, + } + )); + }); + self + } + + pub fn run_solver(&self) -> &Self { + self.execute(||{ + let intents = pallet_intent::Pallet::::get_valid_intents(); + println!("snapshot has {} valid intents", intents.len()); + assert!(!intents.is_empty(), "Snapshot should contain intents"); + + for (id, intent) in &intents { + println!("intent {}: {:?}", id, intent.data); + } + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: <::Simulators as SimulatorSet>::State| + Solver::solve(intents, state).ok() + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + }); + self + } + + pub fn enable_slip_fees(&self, max_slip_fee: Permill) -> &Self { + self.execute(|| { + assert_ok!(hydradx_runtime::Omnipool::set_slip_fee( + RuntimeOrigin::root(), + Some(SlipFeeConfig { max_slip_fee }) + )); + }) + } } #[test] diff --git a/integration-tests/src/ice/dca.rs b/integration-tests/src/ice/dca.rs new file mode 100644 index 0000000000..4db6873f6f --- /dev/null +++ b/integration-tests/src/ice/dca.rs @@ -0,0 +1,1978 @@ +use crate::polkadot_test_net::{hydradx_run_to_next_block, last_hydra_events, TestNet, ALICE, BOB}; +use amm_simulator::HydrationSimulator; +use frame_support::assert_ok; +use frame_support::traits::Time; +use hydradx_runtime::{Currencies, Runtime, RuntimeEvent, RuntimeOrigin}; +use hydradx_traits::amm::{SimulatorConfig, SimulatorSet}; +use ice_solver::v2::Solver as IceSolver; +use ice_support::Solution; +use orml_traits::{MultiCurrency, MultiReservableCurrency}; +use pallet_omnipool::types::SlipFeeConfig; +use primitives::constants::time::MILLISECS_PER_BLOCK; +use primitives::AccountId; +use sp_runtime::Permill; +use xcm_emulator::Network; + +use super::PATH_TO_SNAPSHOT; + +// Asset IDs proven to work in existing solver tests +const HDX: u32 = 0; +const BNC: u32 = 14; + +// Amounts from solver_execute_solution1 — known to work +const TRADE_AMOUNT: u128 = 10_000_000_000_000; +const MIN_OUT_BNC: u128 = 68_795_189_840; + +const PERIOD: u32 = 5; + +// 10% slippage — realistic user setting for recurring DCA trades. +// Oracle limit = estimated_out * 0.90, giving the solver enough room across periods +// as the oracle adjusts between blocks. +const DCA_SLIPPAGE: Permill = Permill::from_percent(10); + +type CombinedSimulatorState = + <::Simulators as SimulatorSet>::State; +type Solver = IceSolver>; + +fn enable_slip_fees() { + assert_ok!(hydradx_runtime::Omnipool::set_slip_fee( + RuntimeOrigin::root(), + Some(SlipFeeConfig { + max_slip_fee: Permill::from_percent(5), + }) + )); +} + +/// Prints `assert_eq!(...)` lines that pin down every field of a Solution. +#[allow(dead_code)] +fn dump_solution(label: &str, solution: &ice_support::Solution) { + println!("// === DUMP_SOLUTION BEGIN: {} ===", label); + println!( + "assert_eq!(solution.resolved_intents.len(), {}, \"resolved count\");", + solution.resolved_intents.len() + ); + println!("assert_eq!(solution.score, {}, \"score\");", solution.score); + println!( + "assert_eq!(solution.trades.len(), {}, \"trades count\");", + solution.trades.len() + ); + for (i, ri) in solution.resolved_intents.iter().enumerate() { + match &ri.data { + ice_support::IntentData::Swap(sw) => { + let partial_str = match sw.partial { + ice_support::Partial::No => "ice_support::Partial::No".to_string(), + ice_support::Partial::Yes(b) => format!("ice_support::Partial::Yes({}u128)", b), + }; + println!( + "{{ let r = &solution.resolved_intents[{i}]; assert_eq!(r.id, {id}); \ + let ice_support::IntentData::Swap(ref s) = r.data else {{ panic!(\"expected Swap\"); }}; \ + assert_eq!(s.asset_in, {ain}); assert_eq!(s.asset_out, {aout}); \ + assert_eq!(s.amount_in, {amin}u128); assert_eq!(s.amount_out, {amout}u128); \ + assert_eq!(s.partial, {pstr}); }}", + i = i, + id = ri.id, + ain = sw.asset_in, + aout = sw.asset_out, + amin = sw.amount_in, + amout = sw.amount_out, + pstr = partial_str, + ); + } + ice_support::IntentData::Dca(d) => { + println!( + "{{ let r = &solution.resolved_intents[{i}]; assert_eq!(r.id, {id}); \ + let ice_support::IntentData::Dca(ref d) = r.data else {{ panic!(\"expected Dca\"); }}; \ + assert_eq!(d.asset_in, {ain}); assert_eq!(d.asset_out, {aout}); \ + assert_eq!(d.amount_in, {amin}u128); assert_eq!(d.amount_out, {amout}u128); \ + assert_eq!(d.remaining_budget, {rb}u128); }}", + i = i, + id = ri.id, + ain = d.asset_in, + aout = d.asset_out, + amin = d.amount_in, + amout = d.amount_out, + rb = d.remaining_budget, + ); + } + } + } + println!("// === DUMP_SOLUTION END: {} ===", label); +} + +/// Prints `assert_eq!(, u128);` for each named variable. +#[allow(unused_macros)] +macro_rules! dump_exact { + ($($var:ident),+ $(,)?) => { + $( + println!("assert_eq!({}, {}u128);", stringify!($var), $var); + )+ + }; +} + +fn run_solver_and_submit() -> Solution { + let block = hydradx_runtime::System::block_number(); + let call = pallet_ice::Pallet::::run( + block, + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + let solution_clone = solution.clone(); + + hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + solution_clone +} + +fn advance_and_solve(n: u32) -> Solution { + for _ in 0..n { + hydradx_run_to_next_block(); + } + run_solver_and_submit() +} + +fn submit_dca_hdx_bnc(who: AccountId, budget: Option) { + submit_dca_hdx_bnc_with_slippage(who, budget, DCA_SLIPPAGE); +} + +fn submit_dca_hdx_bnc_with_slippage(who: AccountId, budget: Option, slippage: Permill) { + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(who), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Dca(ice_support::DcaParams { + asset_in: HDX, + asset_out: BNC, + amount_in: TRADE_AMOUNT, + amount_out: MIN_OUT_BNC, + slippage, + budget, + period: PERIOD, + }), + deadline: None, + on_resolved: None, + } + )); +} + +// === A. Basic Lifecycle === + +#[test] +fn dca_single_trade_execution() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 5 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + enable_slip_fees(); + // 3% slippage — realistic user setting + submit_dca_hdx_bnc_with_slippage(alice.clone(), Some(budget), Permill::from_percent(3)); + + let hdx_before = Currencies::total_balance(HDX, &alice); + let bnc_before = Currencies::total_balance(BNC, &alice); + + assert_eq!( + pallet_intent::Pallet::::get_valid_intents().len(), + 0, + "Not yet eligible" + ); + + let _s = advance_and_solve(PERIOD); + { + let solution = &_s; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + + assert!(Currencies::total_balance(HDX, &alice) < hdx_before, "HDX decreased"); + assert!(Currencies::total_balance(BNC, &alice) > bnc_before, "BNC increased"); + + let remaining: Vec<_> = pallet_intent::Intents::::iter().collect(); + assert_eq!(remaining.len(), 1, "DCA still active"); + match &remaining[0].1.data { + ice_support::IntentData::Dca(dca) => { + assert_eq!(dca.remaining_budget, budget - TRADE_AMOUNT); + } + _ => panic!("Expected DCA"), + } + + // Account index still tracks the active DCA + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 1); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 1); + }); +} + +#[test] +fn dca_multi_period_completes() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 3 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc(alice.clone(), Some(budget)); + + let _s1 = advance_and_solve(PERIOD); + { + let solution = &_s1; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "After trade 1"); + + let _s2 = advance_and_solve(PERIOD); + { + let solution = &_s2; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597724290, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674392914130u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "After trade 2"); + + let _s3 = advance_and_solve(PERIOD); + { + let solution = &_s3; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597490182, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674392680022u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(pallet_intent::Intents::::iter().count(), 0, "Completed"); + + // Account index cleaned up after DCA completion + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + }); +} + +// Period eligibility is tested in unit tests (dca_intent::should_not_include_dca_before_period_elapsed). +// The snapshot-based integration tests use RelayChainBlockNumberProvider which behaves differently +// from the mock, making period timing assertions unreliable here. + +// === B. Rolling Budget === + +#[test] +fn dca_rolling_budget_continues() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, TRADE_AMOUNT * 100) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc(alice.clone(), None); // rolling + + for i in 1..=3 { + let _s = advance_and_solve(PERIOD); + { + let solution = &_s; + let (expected_score, expected_amount_out): (u128, u128) = match i { + 1 => (605597958156, 674393147996), + 2 => (605597724290, 674392914130), + 3 => (605597490182, 674392680022), + _ => unreachable!(), + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, expected_score, "score iter {}", i); + assert_eq!(solution.trades.len(), 1, "trades count"); + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, expected_amount_out, "amount_out iter {}", i); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_eq!( + pallet_intent::Intents::::iter().count(), + 1, + "Rolling after trade {i}" + ); + } + }); +} + +// === C. Direct Matching === + +#[test] +fn dca_matched_with_opposing_swap() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, TRADE_AMOUNT * 100) + .endow_account(bob.clone(), BNC, TRADE_AMOUNT * 100) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc(alice.clone(), Some(5 * TRADE_AMOUNT)); + + for _ in 0..PERIOD { + hydradx_run_to_next_block(); + } + + // Bob opposing swap: BNC → HDX + let ts = hydradx_runtime::Timestamp::now(); + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(bob.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: BNC, + asset_out: HDX, + amount_in: TRADE_AMOUNT, + amount_out: 1_000_000_000_000u128, + partial: false, + }), + deadline: Some(MILLISECS_PER_BLOCK * 100u64 + ts), + on_resolved: None, + } + )); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 2); + let solution = run_solver_and_submit(); + { + let solution = &solution; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 147016637925436, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 147409011313812u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 676421801464u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(solution.resolved_intents.len(), 2); + assert!(solution.score > 0, "Surplus from direct matching"); + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "DCA stays"); + + // Alice's DCA still tracked, Bob's swap resolved and cleaned up + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 1); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 1); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&bob), 0); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&bob).count(), 0); + }); +} + +// === D. Cancellation === + +#[test] +fn dca_cancel_mid_execution() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, TRADE_AMOUNT * 100) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc(alice.clone(), Some(5 * TRADE_AMOUNT)); + + let _s1 = advance_and_solve(PERIOD); + { + let solution = &_s1; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(pallet_intent::Intents::::iter().count(), 1); + + let (id, _) = pallet_intent::Intents::::iter().next().unwrap(); + assert_ok!(hydradx_runtime::Intent::remove_intent( + RuntimeOrigin::signed(alice.clone()), + id + )); + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + + // Account index cleaned up after cancellation + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + }); +} + +// === E. Multiple Users === + +#[test] +fn dca_multiple_users() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, TRADE_AMOUNT * 100) + .endow_account(bob.clone(), HDX, TRADE_AMOUNT * 100) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc(alice.clone(), Some(3 * TRADE_AMOUNT)); + submit_dca_hdx_bnc(bob.clone(), Some(3 * TRADE_AMOUNT)); + + let solution = advance_and_solve(PERIOD); + { + let solution = &solution; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 1211060569414, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674325474547u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674325474547u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(solution.resolved_intents.len(), 2); + assert_eq!(pallet_intent::Intents::::iter().count(), 2); + + // Each user has exactly 1 intent tracked + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 1); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&bob), 1); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 1); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&bob).count(), 1); + }); +} + +// === F. Slippage Levels === + +#[test] +fn dca_with_3_percent_slippage() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 3 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc_with_slippage(alice.clone(), Some(budget), Permill::from_percent(3)); + + // Execute all 3 trades with tight slippage + let _s1 = advance_and_solve(PERIOD); + { + let solution = &_s1; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "After trade 1"); + + let _s2 = advance_and_solve(PERIOD); + { + let solution = &_s2; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597724290, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674392914130u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "After trade 2"); + + let _s3 = advance_and_solve(PERIOD); + { + let solution = &_s3; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597490182, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674392680022u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(pallet_intent::Intents::::iter().count(), 0, "Completed"); + + // Account index cleaned up + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + }); +} + +#[test] +fn dca_with_1_percent_slippage() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, TRADE_AMOUNT * 100) + .execute(|| { + enable_slip_fees(); + // Very tight 1% slippage — single trade + submit_dca_hdx_bnc_with_slippage(alice.clone(), Some(5 * TRADE_AMOUNT), Permill::from_percent(1)); + + let _s = advance_and_solve(PERIOD); + { + let solution = &_s; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + + // Should still work for a single trade on fresh snapshot state + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "DCA still active"); + }); +} + +#[test] +fn ice_dca_driving() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 3 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .enable_slip_fees(Permill::from_percent(5)) + .new_block() + .submit_dca_intent( + alice.clone(), + HDX, + BNC, + TRADE_AMOUNT, + MIN_OUT_BNC, + Permill::from_percent(3), + Some(budget), + PERIOD, + ) + .advance(PERIOD) + .run_solver() + .execute(|| { + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "After trade 1"); + }) + .advance(PERIOD) + .run_solver() + .execute(|| { + assert_eq!(pallet_intent::Intents::::iter().count(), 1, "After trade 2"); + }) + .advance(PERIOD) + .run_solver() + .execute(|| { + assert_eq!(pallet_intent::Intents::::iter().count(), 0, "Completed"); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + }); +} + +#[test] +fn dca_create_schedule_should_work() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 5 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + let hdx_free_before = Currencies::free_balance(HDX, &alice); + + submit_dca_hdx_bnc(alice.clone(), Some(budget)); + + let stored: Vec<_> = pallet_intent::Intents::::iter().collect(); + assert_eq!(stored.len(), 1); + match &stored[0].1.data { + ice_support::IntentData::Dca(dca) => { + assert_eq!(dca.asset_in, HDX); + assert_eq!(dca.asset_out, BNC); + assert_eq!(dca.amount_in, TRADE_AMOUNT); + assert_eq!(dca.period, PERIOD); + assert_eq!(dca.budget, Some(budget)); + assert_eq!(dca.remaining_budget, budget); + } + _ => panic!("Expected DCA intent data"), + } + + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 1); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 1); + assert_eq!(Currencies::reserved_balance(HDX, &alice), budget); + assert_eq!(Currencies::free_balance(HDX, &alice), hdx_free_before - budget); + + let events = last_hydra_events(10); + assert!(events.iter().any(|e| matches!( + e, + RuntimeEvent::Intent(pallet_intent::Event::IntentSubmitted { owner, .. }) + if owner == &alice + ))); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 0); + }); +} + +#[test] +fn dca_rolling_terminates_gracefully_on_funds_exhaustion() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + // Rolling DCA reserves 2*amount_in up front and tries to top up by 1*amount_in + // after each trade. With 5x initial balance we expect termination within ~5 trades. + let alice_initial_hdx = 5 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, alice_initial_hdx) + .execute(|| { + enable_slip_fees(); + let hdx_free_at_start = Currencies::free_balance(HDX, &alice); + + submit_dca_hdx_bnc(alice.clone(), None); + assert_eq!(Currencies::reserved_balance(HDX, &alice), 2 * TRADE_AMOUNT); + + let bnc_before = Currencies::total_balance(BNC, &alice); + let mut trades: usize = 0; + let expected: [(u128, u128); 5] = [ + (605597958156, 674393147996), + (605597724290, 674392914130), + (605597490182, 674392680022), + (605597256317, 674392446157), + (605597022451, 674392212291), + ]; + for _ in 0..20 { + let solution = advance_and_solve(PERIOD); + if trades < expected.len() { + let (expected_score, expected_amount_out) = expected[trades]; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, expected_score, "score iter {}", trades); + assert_eq!(solution.trades.len(), 1, "trades count"); + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, expected_amount_out, "amount_out iter {}", trades); + assert_eq!(s.partial, ice_support::Partial::No); + } + trades += 1; + if pallet_intent::Intents::::iter().count() == 0 { + break; + } + } + + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + assert_eq!(Currencies::reserved_balance(HDX, &alice), 0); + assert!(trades >= 1); + assert!(Currencies::total_balance(BNC, &alice) > bnc_before); + assert!(Currencies::free_balance(HDX, &alice) <= hdx_free_at_start); + }); +} + +#[test] +fn dca_terminate_freshly_created_returns_reserved_budget() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 5 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + let hdx_free_before = Currencies::free_balance(HDX, &alice); + + submit_dca_hdx_bnc(alice.clone(), Some(budget)); + assert_eq!(Currencies::reserved_balance(HDX, &alice), budget); + assert_eq!(Currencies::free_balance(HDX, &alice), hdx_free_before - budget); + + let (id, _) = pallet_intent::Intents::::iter().next().unwrap(); + assert_ok!(hydradx_runtime::Intent::remove_intent( + RuntimeOrigin::signed(alice.clone()), + id + )); + + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + assert_eq!(Currencies::reserved_balance(HDX, &alice), 0); + assert_eq!(Currencies::free_balance(HDX, &alice), hdx_free_before); + }); +} + +#[test] +fn dca_multiple_schedules_same_user_complete_independently() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let b1 = TRADE_AMOUNT; + let b2 = 2 * TRADE_AMOUNT; + let b3 = 3 * TRADE_AMOUNT; + let total = b1 + b2 + b3; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, total * 10) + .execute(|| { + enable_slip_fees(); + + submit_dca_hdx_bnc(alice.clone(), Some(b1)); + submit_dca_hdx_bnc(alice.clone(), Some(b2)); + submit_dca_hdx_bnc(alice.clone(), Some(b3)); + + assert_eq!(pallet_intent::Intents::::iter().count(), 3); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 3); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 3); + assert_eq!(Currencies::reserved_balance(HDX, &alice), total); + + let bnc_before = Currencies::total_balance(BNC, &alice); + + let sol1 = advance_and_solve(PERIOD); + { + let solution = &sol1; + assert_eq!(solution.resolved_intents.len(), 3, "resolved count"); + assert_eq!(solution.score, 1816590152442, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674325240654u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674325240654u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674325240654u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(sol1.resolved_intents.len(), 3); + assert_eq!(pallet_intent::Intents::::iter().count(), 2); + assert_eq!(Currencies::reserved_balance(HDX, &alice), total - 3 * TRADE_AMOUNT); + let bnc_after_1 = Currencies::total_balance(BNC, &alice); + assert!(bnc_after_1 > bnc_before); + + let sol2 = advance_and_solve(PERIOD); + { + let solution = &sol2; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 1211059166118, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674324772899u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674324772899u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(sol2.resolved_intents.len(), 2); + assert_eq!(pallet_intent::Intents::::iter().count(), 1); + assert_eq!(Currencies::reserved_balance(HDX, &alice), total - 5 * TRADE_AMOUNT); + let bnc_after_2 = Currencies::total_balance(BNC, &alice); + assert!(bnc_after_2 > bnc_after_1); + + let sol3 = advance_and_solve(PERIOD); + { + let solution = &sol3; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605596788586, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674391978426u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + } + assert_eq!(sol3.resolved_intents.len(), 1); + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + assert_eq!(Currencies::reserved_balance(HDX, &alice), 0); + assert!(Currencies::total_balance(BNC, &alice) > bnc_after_2); + + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + }); +} + +#[test] +fn dca_emits_trade_executed_and_completed_events() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 2 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc(alice.clone(), Some(budget)); + + let intent_id = pallet_intent::Intents::::iter() + .next() + .map(|(id, _)| id) + .unwrap(); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + + let events1 = last_hydra_events(20); + let trade1 = events1.iter().find_map(|e| match e { + RuntimeEvent::Intent(pallet_intent::Event::DcaTradeExecuted { + id, + amount_in, + remaining_budget, + .. + }) if *id == intent_id => Some((*amount_in, *remaining_budget)), + _ => None, + }); + let (amount_in, remaining_after_1) = trade1.expect("DcaTradeExecuted"); + assert_eq!(amount_in, TRADE_AMOUNT); + assert_eq!(remaining_after_1, TRADE_AMOUNT); + assert!(!events1.iter().any(|e| matches!( + e, + RuntimeEvent::Intent(pallet_intent::Event::DcaCompleted { id }) if *id == intent_id + ))); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597724290, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674392914130u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + + let events2 = last_hydra_events(20); + assert!(events2.iter().any(|e| matches!( + e, + RuntimeEvent::Intent(pallet_intent::Event::DcaCompleted { id }) if *id == intent_id + ))); + assert!(!events2.iter().any(|e| matches!( + e, + RuntimeEvent::Intent(pallet_intent::Event::DcaTradeExecuted { id, .. }) if *id == intent_id + ))); + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + }); +} + +#[test] +fn dca_through_stableswap_single_hop() { + use amm_simulator::stableswap::Simulator as StableswapSimulator; + use hydradx_runtime::{ice_simulator_provider, AssetRegistry, Router}; + use hydradx_traits::amm::AmmSimulator; + use hydradx_traits::router::{AssetPair, RouteProvider}; + use hydradx_traits::BoundErc20; + + TestNet::reset(); + let alice: AccountId = ALICE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + + // Pick a stableswap pool whose first two assets are non-contract and both routable to HDX. + let snapshot = StableswapSimulator::>::snapshot(); + let selected = snapshot.pools.iter().find_map(|(_, pool)| { + if pool.assets.len() < 2 { + return None; + } + let (a, b) = (pool.assets[0], pool.assets[1]); + if AssetRegistry::contract_address(a).is_some() || AssetRegistry::contract_address(b).is_some() { + return None; + } + if Router::get_onchain_route(AssetPair::new(a, HDX)).is_some() + && Router::get_onchain_route(AssetPair::new(b, HDX)).is_some() + { + Some((a, b, pool.reserves[0].decimals, pool.reserves[1].decimals)) + } else { + None + } + }); + let (asset_in, asset_out, decimals_in, decimals_out) = + selected.expect("no suitable stableswap pool in snapshot"); + + let per_trade_in = 100 * 10u128.pow(decimals_in as u32); + let per_trade_out_min = 10u128.pow(decimals_out as u32); + let budget = 2 * per_trade_in; + + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + alice.clone(), + asset_in, + (budget * 10) as i128, + )); + + let in_before = Currencies::total_balance(asset_in, &alice); + let out_before = Currencies::total_balance(asset_out, &alice); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Dca(ice_support::DcaParams { + asset_in, + asset_out, + amount_in: per_trade_in, + amount_out: per_trade_out_min, + slippage: DCA_SLIPPAGE, + budget: Some(budget), + period: PERIOD, + }), + deadline: None, + on_resolved: None, + } + )); + assert_eq!(Currencies::reserved_balance(asset_in, &alice), budget); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 99045134304444271642, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 18); + assert_eq!(s.amount_in, 100000000u128); + assert_eq!(s.amount_out, 100045134304444271642u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 1); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 98383307180234467371, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 18); + assert_eq!(s.amount_in, 100000000u128); + assert_eq!(s.amount_out, 99383307180234467371u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + + assert!(Currencies::total_balance(asset_in, &alice) < in_before); + assert!(Currencies::total_balance(asset_out, &alice) > out_before); + assert_eq!(Currencies::reserved_balance(asset_in, &alice), 0); + }); +} + +#[test] +fn dca_through_omnipool_and_stableswap_multi_hop() { + use amm_simulator::stableswap::Simulator as StableswapSimulator; + use hydradx_runtime::{ice_simulator_provider, AssetRegistry, Router}; + use hydradx_traits::amm::AmmSimulator; + use hydradx_traits::router::{AssetPair, RouteProvider}; + use hydradx_traits::BoundErc20; + + TestNet::reset(); + let alice: AccountId = ALICE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + + // HDX lives in Omnipool, stable leg in Stableswap — any route the solver picks is multi-pool. + let snapshot = StableswapSimulator::>::snapshot(); + let stable_asset_id = snapshot + .pools + .iter() + .find_map(|(_, pool)| { + pool.assets + .iter() + .find(|&&a| { + AssetRegistry::contract_address(a).is_none() + && Router::get_onchain_route(AssetPair::new(HDX, a)).is_some() + && Router::get_onchain_route(AssetPair::new(a, HDX)).is_some() + }) + .copied() + }) + .expect("no stableswap asset with HDX route in snapshot"); + + let per_trade_in = TRADE_AMOUNT; + let per_trade_out_min = + as hydradx_traits::registry::Inspect>::existential_deposit( + stable_asset_id, + ) + .expect("stable asset has ED"); + let budget = 2 * per_trade_in; + + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + alice.clone(), + HDX, + (budget * 10) as i128, + )); + + let hdx_before = Currencies::total_balance(HDX, &alice); + let stable_before = Currencies::total_balance(stable_asset_id, &alice); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Dca(ice_support::DcaParams { + asset_in: HDX, + asset_out: stable_asset_id, + amount_in: per_trade_in, + amount_out: per_trade_out_min, + slippage: DCA_SLIPPAGE, + budget: Some(budget), + period: PERIOD, + }), + deadline: None, + on_resolved: None, + } + )); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 10324, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 10); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 20324u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 1); + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 10324, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 10); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 20324u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + + assert!(Currencies::total_balance(HDX, &alice) < hdx_before); + assert!(Currencies::total_balance(stable_asset_id, &alice) > stable_before); + assert_eq!(Currencies::reserved_balance(HDX, &alice), 0); + }); +} + +#[test] +fn dca_through_aave_pair() { + use amm_simulator::aave::Simulator as AaveSimulator; + use hydradx_runtime::ice_simulator_provider; + use hydradx_traits::amm::AmmSimulator; + + TestNet::reset(); + let alice: AccountId = ALICE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + + let aave_snapshot = AaveSimulator::>::snapshot(); + let picked = aave_snapshot.pairs.iter().find_map(|(a, b)| { + let ed_in = + as hydradx_traits::registry::Inspect>::existential_deposit(*a)?; + let ed_out = + as hydradx_traits::registry::Inspect>::existential_deposit(*b)?; + Some((*a, *b, ed_in, ed_out)) + }); + + let Some((asset_in, asset_out, ed_in, ed_out)) = picked else { + // Snapshot has no Aave pairs — nothing to exercise, not a failure. + return; + }; + + let per_trade_in = ed_in.saturating_mul(100); + let per_trade_out_min = ed_out; + let budget = 2 * per_trade_in; + + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + alice.clone(), + asset_in, + (budget * 10) as i128, + )); + + let in_before = Currencies::total_balance(asset_in, &alice); + let out_before = Currencies::total_balance(asset_out, &alice); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Dca(ice_support::DcaParams { + asset_in, + asset_out, + amount_in: per_trade_in, + amount_out: per_trade_out_min, + slippage: DCA_SLIPPAGE, + budget: Some(budget), + period: PERIOD, + }), + deadline: None, + on_resolved: None, + } + )); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 977591, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 22); + assert_eq!(s.asset_out, 1003); + assert_eq!(s.amount_in, 1000000u128); + assert_eq!(s.amount_out, 1000000u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 1); + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 977591, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 22); + assert_eq!(s.asset_out, 1003); + assert_eq!(s.amount_in, 1000000u128); + assert_eq!(s.amount_out, 1000000u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + + assert!(Currencies::total_balance(asset_in, &alice) < in_before); + assert!(Currencies::total_balance(asset_out, &alice) > out_before); + assert_eq!(Currencies::reserved_balance(asset_in, &alice), 0); + }); +} + +#[test] +fn dca_stays_alive_when_trade_fails_until_lockdown_is_lifted() { + use amm_simulator::stableswap::Simulator as StableswapSimulator; + use hydradx_runtime::{ice_simulator_provider, AssetRegistry, Router}; + use hydradx_traits::amm::AmmSimulator; + use hydradx_traits::router::{AssetPair, RouteProvider}; + use hydradx_traits::BoundErc20; + + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + + let snapshot = StableswapSimulator::>::snapshot(); + let selected = snapshot.pools.iter().find_map(|(pid, pool)| { + if pool.assets.is_empty() { + return None; + } + let a = pool.assets[0]; + if AssetRegistry::contract_address(a).is_some() { + return None; + } + Router::get_onchain_route(AssetPair::new(a, HDX)).map(|_| (*pid, a, pool.reserves[0].decimals)) + }); + let (pool_id, stable_asset_1, decimals_a) = selected.expect("no suitable stableswap pool"); + + let per_trade_in = 10u128 * 10u128.pow(decimals_a as u32); + let budget = 2 * per_trade_in; + let ed_pool = + as hydradx_traits::registry::Inspect>::existential_deposit(pool_id) + .expect("pool_id has ED"); + + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + alice.clone(), + stable_asset_1, + (budget * 10) as i128, + )); + + // Trigger circuit-breaker lockdown on pool_id by pinning and exceeding its deposit limit. + crate::deposit_limiter::update_deposit_limit(pool_id, ed_pool).unwrap(); + assert_ok!(Currencies::deposit(pool_id, &bob, ed_pool * 2)); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Dca(ice_support::DcaParams { + asset_in: stable_asset_1, + asset_out: pool_id, + amount_in: per_trade_in, + amount_out: ed_pool, + slippage: DCA_SLIPPAGE, + budget: Some(budget), + period: PERIOD, + }), + deadline: None, + on_resolved: None, + } + )); + + let (intent_id, snap_before) = pallet_intent::Intents::::iter().next().unwrap(); + let remaining_before = match &snap_before.data { + ice_support::IntentData::Dca(dca) => dca.remaining_budget, + _ => panic!("expected Dca intent"), + }; + + for _ in 0..PERIOD { + hydradx_run_to_next_block(); + } + + // Solver operates off-chain (no circuit breaker there) so it produces a solution; + // on-chain dispatch rejects it because of the lockdown. Intent must stay untouched. + let call = pallet_ice::Pallet::::run(hydradx_runtime::System::block_number(), |intents, state| { + Solver::solve(intents, state).ok() + }); + if let Some(pallet_ice::Call::submit_solution { solution, .. }) = call { + hydradx_run_to_next_block(); + let res = pallet_ice::Pallet::::submit_solution(RuntimeOrigin::none(), solution); + assert!(res.is_err(), "submit must fail during lockdown; got {:?}", res); + } + + let intent_after_failed = pallet_intent::Intents::::get(intent_id).unwrap(); + match intent_after_failed.data { + ice_support::IntentData::Dca(dca) => { + assert_eq!(dca.remaining_budget, remaining_before); + } + _ => panic!("expected Dca intent"), + } + + assert_ok!(hydradx_runtime::CircuitBreaker::force_lift_lockdown( + hydradx_runtime::RuntimeOrigin::root(), + pool_id, + )); + crate::deposit_limiter::update_deposit_limit(pool_id, u128::MAX / 2).unwrap(); + + let alice_pool_before = Currencies::total_balance(pool_id, &alice); + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 9555398534085278591, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 100); + assert_eq!(s.amount_in, 10000000u128); + assert_eq!(s.amount_out, 9555398534085279591u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + + assert!(Currencies::total_balance(pool_id, &alice) > alice_pool_before); + if let Some(intent_after_success) = pallet_intent::Intents::::get(intent_id) { + match intent_after_success.data { + ice_support::IntentData::Dca(dca) => { + assert!(dca.remaining_budget < remaining_before); + } + _ => panic!("expected Dca intent"), + } + } + }); +} + +#[test] +fn dca_works_when_free_balance_is_exactly_ed_after_reserve() { + use hydradx_runtime::Balances; + + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 2 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + + let hdx_ed = + as hydradx_traits::registry::Inspect>::existential_deposit(HDX) + .expect("HDX has ED"); + + // force_set_balance (not endow) because we need the exact value `budget + ED`. + assert_ok!(Balances::force_set_balance( + hydradx_runtime::RuntimeOrigin::root(), + alice.clone(), + budget + hdx_ed, + )); + assert_eq!(Currencies::free_balance(HDX, &alice), budget + hdx_ed); + + let bnc_before = Currencies::total_balance(BNC, &alice); + + submit_dca_hdx_bnc(alice.clone(), Some(budget)); + assert_eq!(Currencies::reserved_balance(HDX, &alice), budget); + assert_eq!(Currencies::free_balance(HDX, &alice), hdx_ed); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(Currencies::free_balance(HDX, &alice), hdx_ed); + assert_eq!(pallet_intent::Intents::::iter().count(), 1); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597724290, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674392914130u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + assert_eq!(pallet_intent::AccountIntents::::iter_prefix(&alice).count(), 0); + assert_eq!(pallet_intent::Pallet::::account_intent_count(&alice), 0); + assert_eq!(Currencies::reserved_balance(HDX, &alice), 0); + assert_eq!(Currencies::free_balance(HDX, &alice), hdx_ed); + assert!(Currencies::total_balance(BNC, &alice) > bnc_before); + }); +} + +#[test] +fn dca_retries_every_block_until_success() { + use amm_simulator::stableswap::Simulator as StableswapSimulator; + use hydradx_runtime::{ice_simulator_provider, AssetRegistry, Router}; + use hydradx_traits::amm::AmmSimulator; + use hydradx_traits::router::{AssetPair, RouteProvider}; + use hydradx_traits::BoundErc20; + + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + + let snapshot = StableswapSimulator::>::snapshot(); + let selected = snapshot.pools.iter().find_map(|(pid, pool)| { + if pool.assets.is_empty() { + return None; + } + let a = pool.assets[0]; + if AssetRegistry::contract_address(a).is_some() { + return None; + } + Router::get_onchain_route(AssetPair::new(a, HDX)).map(|_| (*pid, a, pool.reserves[0].decimals)) + }); + let (pool_id, stable_asset, decimals) = selected.expect("no suitable stableswap pool"); + + let per_trade_in = 10u128 * 10u128.pow(decimals as u32); + let budget = 2 * per_trade_in; + let ed_pool = + as hydradx_traits::registry::Inspect>::existential_deposit(pool_id) + .expect("pool_id has ED"); + + assert_ok!(Currencies::update_balance( + hydradx_runtime::RuntimeOrigin::root(), + alice.clone(), + stable_asset, + (budget * 10) as i128, + )); + + crate::deposit_limiter::update_deposit_limit(pool_id, ed_pool).unwrap(); + assert_ok!(Currencies::deposit(pool_id, &bob, ed_pool * 2)); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Dca(ice_support::DcaParams { + asset_in: stable_asset, + asset_out: pool_id, + amount_in: per_trade_in, + amount_out: ed_pool, + slippage: DCA_SLIPPAGE, + budget: Some(budget), + period: PERIOD, + }), + deadline: None, + on_resolved: None, + } + )); + + let (intent_id, _) = pallet_intent::Intents::::iter().next().unwrap(); + let leb_after_submit = match pallet_intent::Intents::::get(intent_id).unwrap().data { + ice_support::IntentData::Dca(ref dca) => dca.last_execution_block, + _ => panic!("expected DCA"), + }; + + for _ in 0..PERIOD { + hydradx_run_to_next_block(); + } + + let call = pallet_ice::Pallet::::run(hydradx_runtime::System::block_number(), |intents, state| { + Solver::solve(intents, state).ok() + }); + if let Some(pallet_ice::Call::submit_solution { solution, .. }) = call { + hydradx_run_to_next_block(); + let res = pallet_ice::Pallet::::submit_solution(RuntimeOrigin::none(), solution); + assert!(res.is_err()); + } + + // Failure must not advance last_execution_block or consume budget. + let dca_after_fail = match pallet_intent::Intents::::get(intent_id).unwrap().data { + ice_support::IntentData::Dca(ref dca) => dca.clone(), + _ => panic!("expected DCA"), + }; + assert_eq!(dca_after_fail.last_execution_block, leb_after_submit); + assert_eq!(dca_after_fail.remaining_budget, budget); + + // Intent must be eligible on the very next block (not after another full period). + hydradx_run_to_next_block(); + let valid = pallet_intent::Pallet::::get_valid_intents(); + assert!(valid.iter().any(|(id, _)| *id == intent_id)); + + assert_ok!(hydradx_runtime::CircuitBreaker::force_lift_lockdown( + hydradx_runtime::RuntimeOrigin::root(), + pool_id, + )); + crate::deposit_limiter::update_deposit_limit(pool_id, u128::MAX / 2).unwrap(); + + { + let solution = run_solver_and_submit(); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 9555398534085278591, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 100); + assert_eq!(s.amount_in, 10000000u128); + assert_eq!(s.amount_out, 9555398534085279591u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + + if let Some(intent) = pallet_intent::Intents::::get(intent_id) { + match intent.data { + ice_support::IntentData::Dca(ref dca) => { + assert!(dca.last_execution_block > leb_after_submit); + assert_eq!(dca.remaining_budget, budget - per_trade_in); + } + _ => panic!("expected DCA"), + } + } + + assert!(Currencies::total_balance(pool_id, &alice) > 0); + }); +} + +#[test] +fn dca_residual_budget_returned_without_partial_trade() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + // 2.5 × amount_in: after two full trades, 0.5 × amount_in remains and is + // returned to free balance (new DCA does not execute a partial last trade). + let budget = 2 * TRADE_AMOUNT + TRADE_AMOUNT / 2; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + enable_slip_fees(); + + let hdx_free_before = Currencies::free_balance(HDX, &alice); + let bnc_before = Currencies::total_balance(BNC, &alice); + + submit_dca_hdx_bnc(alice.clone(), Some(budget)); + assert_eq!(Currencies::reserved_balance(HDX, &alice), budget); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 1); + + { + let solution = advance_and_solve(PERIOD); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597724290, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674392914130u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + solution + }; + assert_eq!(pallet_intent::Intents::::iter().count(), 0); + + let residual = TRADE_AMOUNT / 2; + assert_eq!( + Currencies::free_balance(HDX, &alice), + hdx_free_before - budget + residual + ); + assert_eq!(Currencies::reserved_balance(HDX, &alice), 0); + assert!(Currencies::total_balance(BNC, &alice) > bnc_before); + }); +} + +// DCA period must be enforced at resolve time, not only in get_valid_intents. +// Fails today: a crafted intent list that skips the period filter gets its +// solution accepted via submit_solution. Fix: add period check to +// validate_dca_intent_resolve in pallets/intent/src/lib.rs. +#[test] +fn dca_period_can_be_bypassed_at_resolve_time() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 5 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + enable_slip_fees(); + submit_dca_hdx_bnc_with_slippage(alice.clone(), Some(budget), Permill::from_percent(3)); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 0); + + hydradx_run_to_next_block(); + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 0); + + let (intent_id, dca_before) = pallet_intent::Intents::::iter() + .next() + .map(|(id, intent)| match intent.data { + ice_support::IntentData::Dca(dca) => (id, dca), + _ => panic!("expected DCA"), + }) + .unwrap(); + + // Simulates a block author bypassing get_valid_intents: transform DCA to + // Swap directly and hand it to the solver without the period filter. + let crafted: Vec = pallet_intent::Intents::::iter() + .map(|(id, intent)| { + let data = match intent.data { + ice_support::IntentData::Dca(ref dca) => { + // Attack simulation: pass the hard limit (not the effective + // limit) to reproduce a malicious collator skipping the + // get_valid_intents pre-filter and submitting at the user's + // absolute floor. + ice_support::IntentData::Swap(dca.to_swap_data(dca.amount_out)) + } + other => other, + }; + ice_support::Intent { id, data } + }) + .collect(); + + let state = <::Simulators as SimulatorSet>::initial_state(); + let solution = Solver::solve(crafted, state).expect("solver fills crafted intent"); + + let hdx_before = Currencies::total_balance(HDX, &alice); + let bnc_before = Currencies::total_balance(BNC, &alice); + + hydradx_run_to_next_block(); + let result = pallet_ice::Pallet::::submit_solution(RuntimeOrigin::none(), solution); + + assert!( + result.is_err(), + "out-of-period trade must be rejected; got {:?}", + result + ); + assert_eq!(Currencies::total_balance(HDX, &alice), hdx_before); + assert_eq!(Currencies::total_balance(BNC, &alice), bnc_before); + + let intent_after = pallet_intent::Intents::::get(intent_id).unwrap(); + match intent_after.data { + ice_support::IntentData::Dca(dca) => { + assert_eq!(dca.remaining_budget, dca_before.remaining_budget); + assert_eq!(dca.last_execution_block, dca_before.last_execution_block); + } + _ => panic!("expected DCA"), + } + }); +} + +// Dynamic slippage must be enforced at resolve time, not only as a pre-filter +// in get_valid_intents. Acrafted solution with resolve.amount_out +// at the hard limit (below the oracle-derived slippage floor) is accepted. +// Fix: enforce resolve.amount_out >= compute_dca_effective_limit(dca) in +// validate_dca_intent_resolve. +#[test] +fn dca_slippage_not_enforced_at_resolve_time() { + TestNet::reset(); + let alice: AccountId = ALICE.into(); + let budget = 5 * TRADE_AMOUNT; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), HDX, budget * 10) + .execute(|| { + enable_slip_fees(); + // Tight 1% slippage but loose hard limit — the gap S2 exposes. + submit_dca_hdx_bnc_with_slippage(alice.clone(), Some(budget), Permill::from_percent(1)); + + let (intent_id, _) = pallet_intent::Intents::::iter().next().unwrap(); + let dca = match pallet_intent::Intents::::get(intent_id).unwrap().data { + ice_support::IntentData::Dca(dca) => dca, + _ => panic!("expected DCA"), + }; + + // Oracle floor should be well above the hard limit for HDX→BNC. + let effective_limit = pallet_intent::Pallet::::compute_dca_effective_limit(&dca); + assert!( + effective_limit > dca.amount_out, + "test requires oracle floor ({}) above hard limit ({})", + effective_limit, + dca.amount_out, + ); + + // Run the honest solver to get a valid solution shape (routes + trades). + let honest_solution = advance_and_solve(PERIOD); + let honest_out = honest_solution.resolved_intents[0].data.amount_out(); + assert!(honest_out >= effective_limit); + + // DCA still active after the first honest trade. + let dca_after_1 = match pallet_intent::Intents::::get(intent_id).unwrap().data { + ice_support::IntentData::Dca(dca) => dca, + _ => panic!("expected DCA"), + }; + + // Advance another period and get a second honest solution for the shape. + for _ in 0..PERIOD { + hydradx_run_to_next_block(); + } + + let block = hydradx_runtime::System::block_number(); + let call = pallet_ice::Pallet::::run(block, |intents, state| Solver::solve(intents, state).ok()) + .expect("solver should produce a solution"); + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + + // Craft a worse solution: set resolve.amount_out to the hard limit + // (below oracle floor). A malicious collator keeps the surplus. + let mut crafted = solution; + crafted.resolved_intents[0] = ice_support::Intent { + id: crafted.resolved_intents[0].id, + data: ice_support::IntentData::Swap(ice_support::SwapData { + asset_in: HDX, + asset_out: BNC, + amount_in: TRADE_AMOUNT, + amount_out: dca.amount_out, // hard limit, below oracle floor + partial: ice_support::Partial::No, + }), + }; + // surplus = resolve.amount_out - dca.amount_out = 0 + crafted.score = 0; + + let hdx_before = Currencies::total_balance(HDX, &alice); + let bnc_before = Currencies::total_balance(BNC, &alice); + + hydradx_run_to_next_block(); + let result = pallet_ice::Pallet::::submit_solution(RuntimeOrigin::none(), crafted); + + // Today: accepts (only hard limit is checked). + assert!( + result.is_err(), + "resolve at hard limit ({}) below oracle floor ({}) must be rejected; got {:?}", + dca.amount_out, + effective_limit, + result, + ); + assert_eq!(Currencies::total_balance(HDX, &alice), hdx_before); + assert_eq!(Currencies::total_balance(BNC, &alice), bnc_before); + + let dca_after = match pallet_intent::Intents::::get(intent_id).unwrap().data { + ice_support::IntentData::Dca(dca) => dca, + _ => panic!("expected DCA"), + }; + assert_eq!(dca_after.remaining_budget, dca_after_1.remaining_budget); + assert_eq!(dca_after.last_execution_block, dca_after_1.last_execution_block); + }); +} diff --git a/integration-tests/src/ice/mod.rs b/integration-tests/src/ice/mod.rs new file mode 100644 index 0000000000..fb8792c6b3 --- /dev/null +++ b/integration-tests/src/ice/mod.rs @@ -0,0 +1,6 @@ +// Snapshot block: 0xfa54abacaf26b68fda809d6284953d328c19df38182014a4f148399b49881ac8 +pub const PATH_TO_SNAPSHOT: &str = "snapshots/ice/mainnet_apr"; + +mod dca; +mod recorder; +mod solver; diff --git a/integration-tests/src/ice/recorder.rs b/integration-tests/src/ice/recorder.rs new file mode 100644 index 0000000000..b9a46628c1 --- /dev/null +++ b/integration-tests/src/ice/recorder.rs @@ -0,0 +1,176 @@ +//! ICE solver fixture recorder. +//! +//! Active only under the `ice-record` feature flag. When on, `TestSimulator` +//! in `super::solver` is transparently wrapped so every AMM call the solver +//! makes is appended to a thread-local trace. Dumping tests then call +//! `dump_fixture` to freeze intents + solution + trace as a 3-line `.hex` +//! fixture consumable by `ice_solver::tests::regressions`. +//! +//! Without the feature, `RecordingAMM` doesn't exist and `clear`/`dump_fixture` +//! compile to no-ops — call sites in tests can stay permanent and untouched. +//! +//! `clear` and `dump_fixture` are `dead_code`-allowed: they exist as a dormant +//! hook. To capture a new regression fixture, temporarily add +//! `recorder::clear(); Solver::solve(...); recorder::dump_fixture("…hex", …)` +//! into an integration test that loads the chain state you want, then run +//! `cargo test -p runtime-integration-tests --features ice-record `. + +#![allow(dead_code)] + +#[cfg(feature = "ice-record")] +mod active { + use codec::Encode; + use hydra_dx_math::types::Ratio; + use hydradx_traits::amm::{AMMInterface, TradeExecution}; + use hydradx_traits::router::{PoolEdge, Route}; + use ice_solver::replay_format::{Response, Trace}; + use ice_support::{AssetId, Balance}; + use std::cell::RefCell; + use std::marker::PhantomData; + + thread_local! { + static TRACE: RefCell> = const { RefCell::new(Vec::new()) }; + } + + pub fn clear() { + TRACE.with(|t| t.borrow_mut().clear()); + } + + fn take_responses() -> Vec { + TRACE.with(|t| t.replace(Vec::new())) + } + + fn record(r: Response) { + TRACE.with(|t| t.borrow_mut().push(r)); + } + + pub struct RecordingAMM(PhantomData); + + impl AMMInterface for RecordingAMM { + type Error = A::Error; + type State = A::State; + + fn discover_routes( + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result>, Self::Error> { + let r = A::discover_routes(asset_in, asset_out, state); + record(Response::DiscoverRoutes { + asset_in, + asset_out, + result: match &r { + Ok(v) => Ok(v.clone()), + Err(_) => Err(()), + }, + }); + r + } + + fn sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + route: Route, + state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + let r = A::sell(asset_in, asset_out, amount_in, route, state); + record(Response::Sell { + asset_in, + asset_out, + amount_in, + result: match &r { + Ok((_, exec)) => Ok((exec.amount_out, exec.route.clone())), + Err(_) => Err(()), + }, + }); + r + } + + fn buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + route: Route, + state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + let r = A::buy(asset_in, asset_out, amount_out, route, state); + record(Response::Buy { + asset_in, + asset_out, + amount_out, + result: match &r { + Ok((_, exec)) => Ok((exec.amount_in, exec.route.clone())), + Err(_) => Err(()), + }, + }); + r + } + + fn get_spot_price( + asset_in: AssetId, + asset_out: AssetId, + route: Route, + state: &Self::State, + ) -> Result { + let r = A::get_spot_price(asset_in, asset_out, route, state); + record(Response::SpotPrice { + asset_in, + asset_out, + result: match &r { + Ok(p) => Ok(*p), + Err(_) => Err(()), + }, + }); + r + } + + fn price_denominator() -> AssetId { + A::price_denominator() + } + + fn pool_edges(state: &Self::State) -> Vec> { + A::pool_edges(state) + } + + fn existential_deposit(asset_id: AssetId) -> Balance { + let ed = A::existential_deposit(asset_id); + record(Response::ExistentialDeposit { asset_id, ed }); + ed + } + } + + pub fn dump_fixture(path: &str, intents: &I, solution: &S, price_denominator: AssetId) { + use std::io::Write; + + let trace = Trace { + price_denominator, + responses: take_responses(), + }; + let fixture = Trace::encode_fixture(intents, solution, &trace); + + let mut f = std::fs::File::create(path).expect("create fixture file"); + f.write_all(fixture.as_bytes()).expect("write fixture file"); + + println!( + "DUMPED fixture to {} ({} responses, {} bytes)", + path, + trace.responses.len(), + fixture.len(), + ); + } +} + +#[cfg(feature = "ice-record")] +#[allow(unused_imports)] +pub use active::{dump_fixture, RecordingAMM}; + +/// No-op without `ice-record`; clears the recorder's thread-local trace with it. +pub fn clear() { + #[cfg(feature = "ice-record")] + active::clear(); +} + +/// No-op without `ice-record`; writes a 3-line hex fixture to `path` with it. +#[cfg(not(feature = "ice-record"))] +pub fn dump_fixture(_path: &str, _intents: &I, _solution: &S, _price_denominator: ice_support::AssetId) {} diff --git a/integration-tests/src/ice/solver.rs b/integration-tests/src/ice/solver.rs new file mode 100644 index 0000000000..0adb19b652 --- /dev/null +++ b/integration-tests/src/ice/solver.rs @@ -0,0 +1,8257 @@ +use crate::polkadot_test_net::{Hydra, TestNet, ALICE, BOB, CHARLIE, DAVE, EVE}; +use amm_simulator::omnipool::Simulator as OmnipoolSimulator; +use amm_simulator::stableswap::Simulator as StableswapSimulator; +use amm_simulator::HydrationSimulator; +use frame_support::assert_ok; +use frame_support::traits::{Get, Time}; +use hydradx_runtime::{ + ice_simulator_provider, AssetRegistry, Currencies, LazyExecutor, Omnipool, Router, Runtime, RuntimeOrigin, + Timestamp, +}; +use hydradx_traits::amm::{AmmSimulator, SimulatorConfig, SimulatorSet}; +use hydradx_traits::registry::Inspect as RegistryInspect; +use hydradx_traits::router::RouteProvider; +use hydradx_traits::BoundErc20; +use ice_solver::v2::Solver as IceSolver; +use ice_support::{Solution, MAX_NUMBER_OF_RESOLVED_INTENTS}; +use orml_traits::MultiCurrency; +use pallet_omnipool::types::SlipFeeConfig; +use primitives::AccountId; +use sp_runtime::Permill; +use xcm_emulator::Network; + +use super::PATH_TO_SNAPSHOT; + +pub type CombinedSimulatorState = + <::Simulators as SimulatorSet>::State; + +#[cfg(not(feature = "ice-record"))] +type TestSimulator = HydrationSimulator; +#[cfg(feature = "ice-record")] +type TestSimulator = super::recorder::RecordingAMM>; + +type Solver = IceSolver; + +// Custom simulator config for Hollar tests with price denominator 222 +pub struct HollarSimulatorConfig; + +pub struct HollarPriceDenominator; +impl Get for HollarPriceDenominator { + fn get() -> u32 { + 222 + } +} + +fn enable_slip_fees() { + assert_ok!(Omnipool::set_slip_fee( + RuntimeOrigin::root(), + Some(SlipFeeConfig { + max_slip_fee: Permill::from_percent(5), + }) + )); +} + +impl SimulatorConfig for HollarSimulatorConfig { + type Simulators = ::Simulators; + type RouteDiscovery = ::RouteDiscovery; + type PriceDenominator = HollarPriceDenominator; +} + +type HollarSimulator = HydrationSimulator; +type HollarSolver = IceSolver; + +/// Prints `assert_eq!(...)` lines that pin down every field of a Solution. +#[allow(dead_code)] +fn dump_solution(label: &str, solution: &ice_support::Solution) { + println!("// === DUMP_SOLUTION BEGIN: {} ===", label); + println!( + "assert_eq!(solution.resolved_intents.len(), {}, \"resolved count\");", + solution.resolved_intents.len() + ); + println!("assert_eq!(solution.score, {}, \"score\");", solution.score); + println!( + "assert_eq!(solution.trades.len(), {}, \"trades count\");", + solution.trades.len() + ); + for (i, ri) in solution.resolved_intents.iter().enumerate() { + match &ri.data { + ice_support::IntentData::Swap(sw) => { + let partial_str = match sw.partial { + ice_support::Partial::No => "ice_support::Partial::No".to_string(), + ice_support::Partial::Yes(b) => format!("ice_support::Partial::Yes({}u128)", b), + }; + println!( + "{{ let r = &solution.resolved_intents[{i}]; assert_eq!(r.id, {id}); \ + let ice_support::IntentData::Swap(ref s) = r.data else {{ panic!(\"expected Swap\"); }}; \ + assert_eq!(s.asset_in, {ain}); assert_eq!(s.asset_out, {aout}); \ + assert_eq!(s.amount_in, {amin}u128); assert_eq!(s.amount_out, {amout}u128); \ + assert_eq!(s.partial, {pstr}); }}", + i = i, + id = ri.id, + ain = sw.asset_in, + aout = sw.asset_out, + amin = sw.amount_in, + amout = sw.amount_out, + pstr = partial_str, + ); + } + ice_support::IntentData::Dca(d) => { + println!( + "{{ let r = &solution.resolved_intents[{i}]; assert_eq!(r.id, {id}); \ + let ice_support::IntentData::Dca(ref d) = r.data else {{ panic!(\"expected Dca\"); }}; \ + assert_eq!(d.asset_in, {ain}); assert_eq!(d.asset_out, {aout}); \ + assert_eq!(d.amount_in, {amin}u128); assert_eq!(d.amount_out, {amout}u128); \ + assert_eq!(d.remaining_budget, {rb}u128); }}", + i = i, + id = ri.id, + ain = d.asset_in, + aout = d.asset_out, + amin = d.amount_in, + amout = d.amount_out, + rb = d.remaining_budget, + ); + } + } + } + println!("// === DUMP_SOLUTION END: {} ===", label); +} + +/// Prints `assert_eq!(, u128);` for each named variable. +#[allow(unused_macros)] +macro_rules! dump_exact { + ($($var:ident),+ $(,)?) => { + $( + println!("assert_eq!({}, {}u128);", stringify!($var), $var); + )+ + }; +} + +#[test] +fn simulator_snapshot() { + TestNet::reset(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + let snapshot = OmnipoolSimulator::>::snapshot(); + + assert!(!snapshot.assets.is_empty(), "Snapshot should contain assets"); + assert!(snapshot.hub_asset_id > 0, "Hub asset id should be set"); + assert!(snapshot.slip_fee.is_some(), "Snapshot should contain slip fees"); + }); +} + +#[test] +fn simulator_sell() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + enable_slip_fees(); + use hydradx_traits::amm::SimulatorError; + + let snapshot = OmnipoolSimulator::>::snapshot(); + + let assets: Vec<_> = snapshot.assets.keys().copied().collect(); + assert!(assets.len() >= 2, "Snapshot should have at least 2 assets"); + + let asset_in = assets[0]; + let asset_out = assets[1]; + + // Skip if using hub asset + if asset_in == snapshot.hub_asset_id || asset_out == snapshot.hub_asset_id { + return; + } + + let amount_in = 1_000_000_000_000u128; + + let result = > as AmmSimulator>::simulate_sell( + asset_in, asset_out, amount_in, 0, &snapshot, + ); + + match result { + Ok((new_snapshot, trade_result)) => { + assert!(trade_result.amount_in > 0, "Amount in should be positive"); + assert!(trade_result.amount_out > 0, "Amount out should be positive"); + + let old_reserve_in = snapshot.assets.get(&asset_in).unwrap().reserve; + let new_reserve_in = new_snapshot.assets.get(&asset_in).unwrap().reserve; + assert!(new_reserve_in > old_reserve_in, "Asset in reserve should increase"); + + let old_reserve_out = snapshot.assets.get(&asset_out).unwrap().reserve; + let new_reserve_out = new_snapshot.assets.get(&asset_out).unwrap().reserve; + assert!(new_reserve_out < old_reserve_out, "Asset out reserve should decrease"); + assert!( + new_snapshot.slip_fee.is_some(), + "New snapshot should have slip fee config" + ); + assert!( + new_snapshot.slip_fee_delta.get(&asset_in).is_some(), + "Asset in slip fee delta should be in snapshot" + ); + assert!( + new_snapshot.slip_fee_delta.get(&asset_out).is_some(), + "Asset out slip fee delta should be in snapshot" + ); + assert!( + new_snapshot.slip_fee_hubreserve_at_block_start.get(&asset_in).is_some(), + "Asset in slip fee hub reserve at block start should be in snapshot" + ); + assert!( + new_snapshot + .slip_fee_hubreserve_at_block_start + .get(&asset_out) + .is_some(), + "Asset out slip fee hub reserve at block start should be in snapshot" + ); + } + Err(e) => { + assert!( + matches!( + e, + SimulatorError::TradeTooSmall | SimulatorError::TradeTooLarge | SimulatorError::Other + ), + "Unexpected error: {:?}", + e + ); + } + } + }); +} + +#[test] +fn stableswap_snapshot() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let stableswap_snapshot = StableswapSimulator::>::snapshot(); + + assert!(!stableswap_snapshot.pools.is_empty(), "Should have stableswap pools"); + assert!( + stableswap_snapshot.min_trading_limit > 0, + "Min trading limit should be set" + ); + + for (_pool_id, pool) in &stableswap_snapshot.pools { + assert!(!pool.assets.is_empty(), "Pool should have assets"); + assert!(pool.amplification > 0, "Amplification should be positive"); + assert!(pool.share_issuance > 0, "Share issuance should be positive"); + assert_eq!( + pool.assets.len(), + pool.reserves.len(), + "Assets and reserves count should match" + ); + + for reserve in pool.reserves.iter() { + assert!(reserve.decimals > 0, "Decimals should be positive"); + } + } + }); +} + +#[test] +fn stableswap_simulator_direct() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + let snapshot = StableswapSimulator::>::snapshot(); + + let pool_id = 104u32; + let Some(pool) = snapshot.pools.get(&pool_id) else { + // Pool 104 not found in snapshot, skip test + return; + }; + + let asset_a = pool.assets[0]; + let asset_b = pool.assets[1]; + let decimals_a = pool.reserves[0].decimals; + + let amount_in = 10u128.pow(decimals_a as u32); + + // Test simulate_sell + let (new_snapshot, result) = + > as AmmSimulator>::simulate_sell( + asset_a, asset_b, amount_in, 0, &snapshot, + ) + .expect("simulate_sell should succeed"); + + assert!(result.amount_in > 0, "Amount in should be positive"); + assert!(result.amount_out > 0, "Amount out should be positive"); + + let new_pool = new_snapshot.pools.get(&pool_id).unwrap(); + let old_reserve_a = pool.reserves[0].amount; + let new_reserve_a = new_pool.reserves[0].amount; + let old_reserve_b = pool.reserves[1].amount; + let new_reserve_b = new_pool.reserves[1].amount; + + assert_eq!( + new_reserve_a - old_reserve_a, + amount_in, + "Reserve A should increase by amount_in" + ); + assert_eq!( + old_reserve_b - new_reserve_b, + result.amount_out, + "Reserve B should decrease by amount_out" + ); + + // Test simulate_buy + let amount_out = 10u128.pow(decimals_a as u32); + let (_new_snapshot, buy_result) = + > as AmmSimulator>::simulate_buy( + asset_a, + asset_b, + amount_out, + u128::MAX, + &snapshot, + ) + .expect("simulate_buy should succeed"); + + assert_eq!(buy_result.amount_out, amount_out, "Amount out should match requested"); + + // Test get_spot_price + let price = > as AmmSimulator>::get_spot_price( + asset_a, asset_b, &snapshot, + ) + .expect("get_spot_price should succeed"); + + assert!(price.n > 0, "Price numerator should be positive"); + assert!(price.d > 0, "Price denominator should be positive"); + }); +} + +/// Test stableswap intent: trade between stableswap pool assets +#[test] +fn stableswap_intent() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT).execute(|| { + use hydradx_traits::router::{AssetPair, RouteProvider}; + + let stableswap_snapshot = StableswapSimulator::>::snapshot(); + let hdx = 0u32; + + // Find a suitable stableswap pool with routes to HDX + let mut selected_pool: Option<(u32, u32, u32, u8)> = None; + for (pid, pool) in &stableswap_snapshot.pools { + if pool.assets.len() < 2 { + continue; + } + let a = pool.assets[0]; + let b = pool.assets[1]; + + if AssetRegistry::contract_address(a).is_some() || AssetRegistry::contract_address(b).is_some() { + continue; + } + let route_a_hdx = Router::get_onchain_route(AssetPair::new(a, hdx)); + let route_b_hdx = Router::get_onchain_route(AssetPair::new(b, hdx)); + if route_a_hdx.is_some() && route_b_hdx.is_some() { + selected_pool = Some((*pid, a, b, pool.reserves[0].decimals)); + break; + } + } + + let Some((_pool_id, asset_a, asset_b, decimals_a)) = selected_pool else { + // No suitable pool found in this snapshot, skip test + assert!(false, "no suitable pool to test stablepool intent"); + return; + }; + + let amount_in = 10u128.pow(decimals_a as u32); + + assert_ok!(Currencies::update_balance( + RuntimeOrigin::root(), + ALICE.into(), + asset_a, + (amount_in * 10) as i128, + )); + + let alice_a_before = Currencies::total_balance(asset_a, &ALICE.into()); + let alice_b_before = Currencies::total_balance(asset_b, &ALICE.into()); + + let ts = Timestamp::now(); + let deadline = Some(6000u64 * 10 + ts); + assert_ok!(pallet_intent::Pallet::::submit_intent( + RuntimeOrigin::signed(ALICE.into()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: asset_a, + asset_out: asset_b, + amount_in, + amount_out: 10_000_000_000_000_000u128, + partial: false, + }), + deadline, + on_resolved: None, + }, + )); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + + let block = hydradx_runtime::System::block_number(); + let call = pallet_ice::Pallet::::run( + block, + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution for mixed intents"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 992925628769399913, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 18); + assert_eq!(s.amount_in, 1000000u128); + assert_eq!(s.amount_out, 1002925628769399913u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_eq!(solution.resolved_intents.len(), 1, "Should resolve the intent"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let alice_a_after = Currencies::total_balance(asset_a, &ALICE.into()); + let alice_b_after = Currencies::total_balance(asset_b, &ALICE.into()); + assert_eq!(alice_a_after, 9000000u128); + assert_eq!(alice_b_after, 1002725043643646034u128); + + assert!(alice_a_after < alice_a_before, "Alice should have less asset_a"); + assert!(alice_b_after > alice_b_before, "Alice should have more asset_b"); + }); +} + +#[test] +fn solver_two_intents() { + TestNet::reset(); + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(ALICE.into(), 0, 1_000_000_000_000_000) + .endow_account(BOB.into(), 5, 1_000_000_000_000_000) + .submit_swap_intent(ALICE.into(), 0, 5, 1_000_000_000_000, 17_540_000u128, Some(2)) + .submit_swap_intent(BOB.into(), 5, 0, 1_000_000_000_000, 1_000_000_000_000u128, Some(2)) + .execute(|| { + enable_slip_fees(); + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2, "Should have 2 intents"); + + let block = hydradx_runtime::System::block_number(); + + let call = pallet_ice::Pallet::::run( + block, + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution for mixed intents"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 62731408053389225, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 1000000000000u128); + assert_eq!(s.amount_out, 62732408053389225u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert!( + !solution.resolved_intents.is_empty(), + "Should resolve at least one intent" + ); + assert!(solution.score > 0, "Solution score should be positive"); + }); +} + +/// Test Direct matching: Alice sells A for B, Bob sells B for A +#[test] +fn solver_execute_solution1() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let asset_a = 0u32; + let asset_b = 14u32; + let amount = 10_000_000_000_000u128; + let min_amount_out_a = 1_000_000_000_000u128; + let min_amount_out_b = 68_795_189_840u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), asset_a, amount * 10) + .endow_account(bob.clone(), asset_b, amount * 10) + .submit_swap_intent(alice.clone(), asset_a, asset_b, amount, min_amount_out_b, Some(10)) + .submit_swap_intent(bob.clone(), asset_b, asset_a, amount, min_amount_out_a, None) //no deadline + .execute(|| { + enable_slip_fees(); + let alice_balance_a_before = Currencies::total_balance(asset_a, &alice); + let alice_balance_b_before = Currencies::total_balance(asset_b, &alice); + let bob_balance_a_before = Currencies::total_balance(asset_a, &bob); + let bob_balance_b_before = Currencies::total_balance(asset_b, &bob); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2, "Should have 2 intents"); + + let block = hydradx_runtime::System::block_number(); + + let call = pallet_ice::Pallet::::run( + block, + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 147016637925436, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 147409011313812u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 676421801464u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + // Verify solution structure + assert_eq!(solution.resolved_intents.len(), 2, "Should resolve both intents"); + assert!(solution.score > 0, "Solution score should be positive"); + + // Verify each resolved intent + for resolved in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref swap_data) = resolved.data else { + panic!("expected Swap"); + }; + assert!(swap_data.amount_in > 0, "amount_in should be positive"); + let min_amount_out = if swap_data.asset_out == asset_a { + min_amount_out_a + } else { + min_amount_out_b + }; + assert!(swap_data.amount_out >= min_amount_out, "amount_out should be >= min"); + } + + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + // Verify intents removed from storage + let remaining_intents = pallet_intent::Pallet::::get_valid_intents(); + assert!(remaining_intents.is_empty(), "All intents should be resolved"); + + // Verify account intent index cleaned up + assert_eq!( + pallet_intent::AccountIntents::::iter_prefix(&alice).count(), + 0, + "Alice's account intents index should be empty" + ); + assert_eq!( + pallet_intent::AccountIntents::::iter_prefix(&bob).count(), + 0, + "Bob's account intents index should be empty" + ); + assert_eq!( + pallet_intent::Pallet::::account_intent_count(&alice), + 0, + "Alice's intent count should be zero" + ); + assert_eq!( + pallet_intent::Pallet::::account_intent_count(&bob), + 0, + "Bob's intent count should be zero" + ); + + let alice_balance_a_after = Currencies::total_balance(asset_a, &alice); + let alice_balance_b_after = Currencies::total_balance(asset_b, &alice); + let bob_balance_a_after = Currencies::total_balance(asset_a, &bob); + let bob_balance_b_after = Currencies::total_balance(asset_b, &bob); + assert_eq!(alice_balance_a_after, 90000000000000u128); + assert_eq!(alice_balance_b_after, 676286517104u128); + assert_eq!(bob_balance_a_after, 147379529511550u128); + assert_eq!(bob_balance_b_after, 90000000000000u128); + + // Verify balance changes direction + assert!( + alice_balance_a_after < alice_balance_a_before, + "Alice's asset_a should decrease" + ); + assert!( + alice_balance_b_after > alice_balance_b_before, + "Alice's asset_b should increase" + ); + assert!( + bob_balance_b_after < bob_balance_b_before, + "Bob's asset_b should decrease" + ); + assert!( + bob_balance_a_after > bob_balance_a_before, + "Bob's asset_a should increase" + ); + + // Verify balance changes match solution + let alice_resolved = solution + .resolved_intents + .iter() + .find(|r| { + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + s.asset_in == asset_a + }) + .expect("Should find Alice's intent"); + let bob_resolved = solution + .resolved_intents + .iter() + .find(|r| { + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + s.asset_in == asset_b + }) + .expect("Should find Bob's intent"); + + let ice_support::IntentData::Swap(ref alice_swap) = alice_resolved.data else { + panic!("expected Swap"); + }; + let ice_support::IntentData::Swap(ref bob_swap) = bob_resolved.data else { + panic!("expected Swap"); + }; + + let ice_fee: Permill = ::Fee::get(); + assert_eq!(alice_balance_a_before - alice_balance_a_after, alice_swap.amount_in); + assert_eq!( + alice_balance_b_after - alice_balance_b_before, + alice_swap.amount_out - ice_fee.mul_floor(alice_swap.amount_out) + ); + assert_eq!(bob_balance_b_before - bob_balance_b_after, bob_swap.amount_in); + assert_eq!( + bob_balance_a_after - bob_balance_a_before, + bob_swap.amount_out - ice_fee.mul_floor(bob_swap.amount_out) + ); + }); +} + +/// Test single ExactOut (buy) intent: Alice wants to buy BNC with HDX +#[test] +fn solver_execute_solution_with_buy_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let asset_a = 0u32; // HDX + let asset_b = 14u32; // BNC + + let alice_wants_amount_out = 20_000_000_000_000u128; + let alice_amount_in = 2_000_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), asset_a, alice_amount_in * 10) + .submit_swap_intent( + alice.clone(), + asset_a, + asset_b, + alice_amount_in, + alice_wants_amount_out, + Some(10), + ) + .execute(|| { + enable_slip_fees(); + let alice_balance_a_before = Currencies::total_balance(asset_a, &alice); + let alice_balance_b_before = Currencies::total_balance(asset_b, &alice); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + + let block = hydradx_runtime::System::block_number(); + + let mut captured_solution: Option = None; + let _result = pallet_ice::Pallet::::run( + block, + |intents: Vec, state: CombinedSimulatorState| { + let solution = Solver::solve(intents, state).ok()?; + captured_solution = Some(solution.clone()); + Some(solution) + }, + ) + .expect("Solver should produce a solution for buy intent"); + + let solution = captured_solution.expect("Solution should be captured"); + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 114869319959659, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 2000000000000000u128); + assert_eq!(s.amount_out, 134869319959659u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + // Verify solution structure + assert_eq!(solution.resolved_intents.len(), 1, "Should resolve intent"); + let resolved = &solution.resolved_intents[0]; + let ice_support::IntentData::Swap(ref swap_data) = resolved.data else { + panic!("expected Swap"); + }; + assert!( + swap_data.amount_out >= alice_wants_amount_out, + "Should buy >= amount requested" + ); + assert!(swap_data.amount_in == alice_amount_in, "Should equal to amount in"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + let alice_balance_a_after = Currencies::total_balance(asset_a, &alice); + let alice_balance_b_after = Currencies::total_balance(asset_b, &alice); + assert_eq!(alice_balance_a_after, 18000000000000000u128); + assert_eq!(alice_balance_b_after, 134842346095668u128); + + // Verify balance changes + assert!( + alice_balance_a_after < alice_balance_a_before, + "Alice's asset_a balance should decrease after paying" + ); + assert!( + alice_balance_b_after > alice_balance_b_before, + "Alice's asset_b balance should increase after buying" + ); + + // Verify exact amounts match solution (received = amount_out - fee) + let ice_fee: Permill = ::Fee::get(); + let paid = alice_balance_a_before - alice_balance_a_after; + let received = alice_balance_b_after - alice_balance_b_before; + assert_eq!(paid, swap_data.amount_in, "Paid amount should match solution"); + assert_eq!( + received, + swap_data.amount_out - ice_fee.mul_floor(swap_data.amount_out), + "Received amount should match solution minus fee" + ); + + // Verify intent removed + let remaining_intents = pallet_intent::Pallet::::get_valid_intents(); + assert!(remaining_intents.is_empty(), "Intent should be resolved"); + }); +} + +/// Test mixed multiple users' intents +#[test] +fn solver_mixed_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let sell_hdx_amount = 100_000_000_000_000u128; + let sell_bnc_amount = 100_000_000_000u128; + let min_hdx_out_amount = 100_000_000_000_000u128; + let min_bnc_out_amount = 68_795_189_840u128; + let in_amount = 10_000_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, in_amount) + .endow_account(alice.clone(), bnc, in_amount) + .endow_account(bob.clone(), hdx, in_amount) + .endow_account(bob.clone(), bnc, in_amount) + .endow_account(charlie.clone(), hdx, in_amount) + .endow_account(charlie.clone(), bnc, in_amount) + .endow_account(dave.clone(), hdx, in_amount) + .endow_account(dave.clone(), bnc, in_amount) + .submit_swap_intent(alice.clone(), hdx, bnc, sell_hdx_amount, min_bnc_out_amount, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, in_amount, min_hdx_out_amount, Some(10)) + .submit_swap_intent( + charlie.clone(), + bnc, + hdx, + sell_bnc_amount, + 1_000_000_000_000u128, + Some(10), + ) + .submit_swap_intent(dave.clone(), hdx, bnc, in_amount, min_bnc_out_amount, Some(10)) + .submit_swap_intent(alice.clone(), hdx, bnc, sell_hdx_amount, min_bnc_out_amount, Some(10)) + .execute(|| { + enable_slip_fees(); + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + let charlie_hdx_before = Currencies::total_balance(hdx, &charlie); + let charlie_bnc_before = Currencies::total_balance(bnc, &charlie); + let dave_hdx_before = Currencies::total_balance(hdx, &dave); + let dave_bnc_before = Currencies::total_balance(bnc, &dave); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 5, "Should have 5 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution for mixed intents"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 5, "resolved count"); + assert_eq!(solution.score, 147347201855228502, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960004); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000000u128); + assert_eq!(s.amount_out, 6764218014644u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 676421801464400u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 100000000000u128); + assert_eq!(s.amount_out, 1467569904334u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 146756990433400000u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[4]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000000u128); + assert_eq!(s.amount_out, 6764218014644u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + // Verify solution structure + assert!( + !solution.resolved_intents.is_empty(), + "Should resolve at least some intents" + ); + assert!(solution.score > 0, "Solution score should be positive"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + let bob_hdx_after = Currencies::total_balance(hdx, &bob); + let bob_bnc_after = Currencies::total_balance(bnc, &bob); + let charlie_hdx_after = Currencies::total_balance(hdx, &charlie); + let charlie_bnc_after = Currencies::total_balance(bnc, &charlie); + let dave_hdx_after = Currencies::total_balance(hdx, &dave); + let dave_bnc_after = Currencies::total_balance(bnc, &dave); + assert_eq!(alice_hdx_after, 9800000000000000u128); + assert_eq!(alice_bnc_after, 10013525730342084u128); + assert_eq!(bob_hdx_after, 156727639035313320u128); + assert_eq!(bob_bnc_after, 0u128); + assert_eq!(charlie_hdx_after, 10001467276390354u128); + assert_eq!(charlie_bnc_after, 9999900000000000u128); + assert_eq!(dave_hdx_after, 0u128); + assert_eq!(dave_bnc_after, 10676286517104108u128); + + // Verify Alice (sells HDX for BNC) + assert!( + alice_hdx_after < alice_hdx_before, + "Alice should have less HDX after selling" + ); + assert!( + alice_bnc_after > alice_bnc_before, + "Alice should have more BNC after selling" + ); + + // Verify Bob (buys HDX with BNC) + assert!(bob_hdx_after > bob_hdx_before, "Bob should have more HDX after buying"); + assert!(bob_bnc_after < bob_bnc_before, "Bob should have less BNC after paying"); + + // Verify Charlie (sells BNC for HDX) + assert!( + charlie_bnc_after < charlie_bnc_before, + "Charlie should have less BNC after selling" + ); + assert!( + charlie_hdx_after > charlie_hdx_before, + "Charlie should have more HDX after selling" + ); + + // Verify Dave (buys BNC with HDX) + assert!( + dave_bnc_after > dave_bnc_before, + "Dave should have more BNC after buying" + ); + assert!( + dave_hdx_after < dave_hdx_before, + "Dave should have less HDX after paying" + ); + }); +} + +/// Test single swap intent: Alice sells HDX for BNC +#[test] +fn solver_v1_single_intent() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let hdx = 0u32; + let bnc = 14u32; + let amount = 10_000_000_000_000u128; + let min_amount_out = 68_795_189_840u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, amount * 10) + .submit_swap_intent(alice.clone(), hdx, bnc, amount, min_amount_out, Some(10)) + .execute(|| { + enable_slip_fees(); + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + let original_intent_id = intents[0].0; + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 605597958156, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 674393147996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + // Verify solution structure + assert_eq!(solution.resolved_intents.len(), 1, "Should resolve exactly 1 intent"); + assert!(solution.score > 0, "Solution score should be positive"); + + // Verify the resolved intent + let resolved = &solution.resolved_intents[0]; + assert_eq!(resolved.id, original_intent_id, "Resolved intent ID should match"); + let ice_support::IntentData::Swap(ref swap_data) = resolved.data else { + panic!("expected Swap"); + }; + assert_eq!(swap_data.asset_in, hdx, "asset_in should be HDX"); + assert_eq!(swap_data.asset_out, bnc, "asset_out should be BNC"); + assert_eq!(swap_data.amount_in, amount, "amount_in should match submitted amount"); + assert!( + swap_data.amount_out >= min_amount_out, + "amount_out should be >= min_amount_out" + ); + + // Verify trades are valid + assert!(!solution.trades.is_empty(), "Should have at least one trade"); + for trade in solution.trades.iter() { + assert!(trade.amount_in > 0, "Trade amount_in should be positive"); + assert!(trade.amount_out > 0, "Trade amount_out should be positive"); + assert!(!trade.route.is_empty(), "Trade route should not be empty"); + } + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + // Verify intent was removed from storage + let remaining_intents = pallet_intent::Pallet::::get_valid_intents(); + assert!( + remaining_intents.is_empty(), + "Intent should be removed after resolution" + ); + + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + assert_eq!(alice_hdx_after, 90000000000000u128); + assert_eq!(alice_bnc_after, 674258269367u128); + + // Verify balance changes match the solution (received = amount_out - fee) + let ice_fee: Permill = ::Fee::get(); + let hdx_spent = alice_hdx_before - alice_hdx_after; + let bnc_received = alice_bnc_after - alice_bnc_before; + + assert_eq!( + hdx_spent, swap_data.amount_in, + "HDX spent should equal resolved amount_in" + ); + assert_eq!( + bnc_received, + swap_data.amount_out - ice_fee.mul_floor(swap_data.amount_out), + "BNC received should equal resolved amount_out minus fee" + ); + }); +} + +/// Test partial direct match: Alice sells large HDX, Bob sells small BNC (opposite directions) +#[test] +fn solver_v1_two_intents_partial_match() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let hdx = 0u32; + let bnc = 14u32; + + let alice_hdx_amount = 1_000_000_000_000_000u128; + let bob_bnc_amount = 500_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_amount * 10) + .endow_account(bob.clone(), bnc, bob_bnc_amount * 10) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_amount, 68_795_189_840u128, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, bob_bnc_amount, 1_000_000_000_000u128, Some(10)) + .execute(|| { + enable_slip_fees(); + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2, "Should have 2 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("V1 Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 73754881218704, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 500000000000u128); + assert_eq!(s.amount_out, 7391837443996u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1000000000000000u128); + assert_eq!(s.amount_out, 67431838964548u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + // Verify both intents resolved + assert_eq!(solution.resolved_intents.len(), 2, "Both intents should be resolved"); + assert!(solution.score > 0, "Solution score should be positive"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + let bob_hdx_after = Currencies::total_balance(hdx, &bob); + let bob_bnc_after = Currencies::total_balance(bnc, &bob); + assert_eq!(alice_hdx_after, 9000000000000000u128); + assert_eq!(alice_bnc_after, 67418352596756u128); + assert_eq!(bob_hdx_after, 7390359076508u128); + assert_eq!(bob_bnc_after, 4500000000000u128); + + // Verify Alice (sells HDX for BNC) + assert!( + alice_hdx_after < alice_hdx_before, + "Alice should have less HDX after selling" + ); + assert!( + alice_bnc_after > alice_bnc_before, + "Alice should have more BNC after selling" + ); + + // Verify Bob (sells BNC for HDX) + assert!(bob_bnc_after < bob_bnc_before, "Bob should have less BNC after selling"); + assert!(bob_hdx_after > bob_hdx_before, "Bob should have more HDX after selling"); + + // Verify balance changes match solution (received = amount_out - fee) + let ice_fee: Permill = ::Fee::get(); + for resolved in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref swap_data) = resolved.data else { + panic!("expected Swap"); + }; + let expected_payout = swap_data.amount_out - ice_fee.mul_floor(swap_data.amount_out); + if swap_data.asset_in == hdx { + // Alice's intent + assert_eq!(alice_hdx_before - alice_hdx_after, swap_data.amount_in); + assert_eq!(alice_bnc_after - alice_bnc_before, expected_payout); + } else { + // Bob's intent + assert_eq!(bob_bnc_before - bob_bnc_after, swap_data.amount_in); + assert_eq!(bob_hdx_after - bob_hdx_before, expected_payout); + } + } + }); +} + +/// Test five mixed intents from different users +#[test] +fn solver_v1_five_mixed_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + let eve: AccountId = EVE.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, 1000 * hdx_unit) + .endow_account(bob.clone(), bnc, 500 * bnc_unit) + .endow_account(charlie.clone(), hdx, 500 * hdx_unit) + .endow_account(dave.clone(), hdx, 500 * hdx_unit) + .endow_account(eve.clone(), bnc, 100 * bnc_unit) + // Alice: sell 500 HDX for BNC + .submit_swap_intent(alice.clone(), hdx, bnc, 500 * hdx_unit, 68_795_189_840u128, Some(10)) + // Bob: sell 300 BNC for HDX + .submit_swap_intent(bob.clone(), bnc, hdx, 300 * bnc_unit, 1_000_000_000_000u128, Some(10)) + // Charlie: sell 200 HDX for BNC + .submit_swap_intent(charlie.clone(), hdx, bnc, 200 * hdx_unit, 168_795_189_840u128, Some(10)) + // Dave: sell 400 HDX for 10 BNC + .submit_swap_intent(dave.clone(), hdx, bnc, 400 * hdx_unit, 10 * bnc_unit, Some(10)) + // Eve: buy max 50 BNC for 500 HDX + .submit_swap_intent(eve.clone(), bnc, hdx, 50 * bnc_unit, 500 * hdx_unit, Some(10)) + .execute(|| { + enable_slip_fees(); + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + let charlie_hdx_before = Currencies::total_balance(hdx, &charlie); + let charlie_bnc_before = Currencies::total_balance(bnc, &charlie); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 5, "Should have 5 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("V1 Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 5, "resolved count"); + assert_eq!(solution.score, 4724256820405969, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960004); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 50000000000000u128); + assert_eq!(s.amount_out, 737298287517795u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 400000000000000u128); + assert_eq!(s.amount_out, 27056872058576u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 200000000000000u128); + assert_eq!(s.amount_out, 13528436029288u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 300000000000000u128); + assert_eq!(s.amount_out, 4423789725106770u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[4]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + // Verify solution structure + assert!( + !solution.resolved_intents.is_empty(), + "Should resolve at least some intents" + ); + assert!(solution.score > 0, "Solution score should be positive"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + let bob_hdx_after = Currencies::total_balance(hdx, &bob); + let bob_bnc_after = Currencies::total_balance(bnc, &bob); + let charlie_hdx_after = Currencies::total_balance(hdx, &charlie); + let charlie_bnc_after = Currencies::total_balance(bnc, &charlie); + assert_eq!(alice_hdx_after, 500000000000000u128); + assert_eq!(alice_bnc_after, 33814325855206u128); + assert_eq!(bob_hdx_after, 4422904967161749u128); + assert_eq!(bob_bnc_after, 200000000000000u128); + assert_eq!(charlie_hdx_after, 300000000000000u128); + assert_eq!(charlie_bnc_after, 13525730342083u128); + + // Verify sellers + assert!(alice_hdx_after < alice_hdx_before, "Alice should have less HDX"); + assert!(alice_bnc_after > alice_bnc_before, "Alice should have more BNC"); + assert!(charlie_hdx_after < charlie_hdx_before, "Charlie should have less HDX"); + assert!(charlie_bnc_after > charlie_bnc_before, "Charlie should have more BNC"); + assert!(bob_bnc_after < bob_bnc_before, "Bob should have less BNC"); + assert!(bob_hdx_after > bob_hdx_before, "Bob should have more HDX"); + }); +} + +/// Test uniform clearing price: multiple sellers of HDX should get proportional BNC +#[test] +fn solver_v1_uniform_price_all_sells() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + let eve: AccountId = EVE.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, 1000 * hdx_unit) + .endow_account(bob.clone(), bnc, 500 * bnc_unit) + .endow_account(charlie.clone(), hdx, 500 * hdx_unit) + .endow_account(dave.clone(), hdx, 500 * hdx_unit) + .endow_account(eve.clone(), hdx, 1000 * hdx_unit) + // All ExactIn (sell) intents + .submit_swap_intent(alice.clone(), hdx, bnc, 500 * hdx_unit, 68_795_189_840u128, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, 300 * bnc_unit, 1_000_000_000_000u128, Some(10)) + .submit_swap_intent(charlie.clone(), hdx, bnc, 200 * hdx_unit, 68_795_189_840u128, Some(10)) + .submit_swap_intent(dave.clone(), hdx, bnc, 100 * hdx_unit, 68_795_189_840u128, Some(10)) + .submit_swap_intent(eve.clone(), hdx, bnc, 500 * hdx_unit, 68_795_189_840u128, Some(10)) // Same as Alice + .execute(|| { + enable_slip_fees(); + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 5, "Should have 5 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("V1 Solver should produce a solution"); + + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let charlie_bnc_before = Currencies::total_balance(bnc, &charlie); + let dave_bnc_before = Currencies::total_balance(bnc, &dave); + let eve_bnc_before = Currencies::total_balance(bnc, &eve); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 5, "resolved count"); + assert_eq!(solution.score, 4511708150660072, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960004); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000000u128); + assert_eq!(s.amount_out, 6764218014644u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 200000000000000u128); + assert_eq!(s.amount_out, 13528436029288u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 300000000000000u128); + assert_eq!(s.amount_out, 4425048497229060u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[4]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + let charlie_bnc_after = Currencies::total_balance(bnc, &charlie); + let dave_bnc_after = Currencies::total_balance(bnc, &dave); + let eve_bnc_after = Currencies::total_balance(bnc, &eve); + assert_eq!(alice_bnc_after, 33814325855206u128); + assert_eq!(charlie_bnc_after, 13525730342083u128); + assert_eq!(dave_bnc_after, 6762865171042u128); + assert_eq!(eve_bnc_after, 33814325855206u128); + + let alice_bnc_received = alice_bnc_after.saturating_sub(alice_bnc_before); + let charlie_bnc_received = charlie_bnc_after.saturating_sub(charlie_bnc_before); + let dave_bnc_received = dave_bnc_after.saturating_sub(dave_bnc_before); + let eve_bnc_received = eve_bnc_after.saturating_sub(eve_bnc_before); + + // Uniform price: Alice and Eve both sold 500 HDX, should receive same BNC + assert_eq!( + alice_bnc_received, eve_bnc_received, + "Alice and Eve should receive exactly the same BNC for selling the same HDX" + ); + + // Proportionality check: Charlie (200 HDX) should get 2/5 of Alice's BNC + let expected_charlie = alice_bnc_received * 200 / 500; + let charlie_diff = charlie_bnc_received.abs_diff(expected_charlie); + assert!( + charlie_diff <= 1, + "Charlie's amount should be proportional to Alice's (diff: {})", + charlie_diff + ); + + // Proportionality check: Dave (100 HDX) should get 1/5 of Alice's BNC + let expected_dave = alice_bnc_received * 100 / 500; + let dave_diff = dave_bnc_received.abs_diff(expected_dave); + assert!( + dave_diff <= 1, + "Dave's amount should be proportional to Alice's (diff: {})", + dave_diff + ); + }); +} + +/// Test uniform price with opposite direction sells (Alice sells HDX, Eve/Bob sell BNC) +#[test] +fn solver_v1_uniform_price_opposite_sells() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let eve: AccountId = EVE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + let eve_bnc_sell = 10_380_308_715_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, 1000 * hdx_unit) + .endow_account(eve.clone(), bnc, 100 * bnc_unit) + .endow_account(bob.clone(), bnc, 500 * bnc_unit) + // Alice sells HDX for BNC + .submit_swap_intent(alice.clone(), hdx, bnc, 500 * hdx_unit, 68_795_189_840u128, Some(10)) + // Eve sells BNC for HDX (opposite direction) + .submit_swap_intent(eve.clone(), bnc, hdx, eve_bnc_sell, 1_000_000_000_000u128, Some(10)) + // Bob sells BNC for HDX (same direction as Eve) + .submit_swap_intent(bob.clone(), bnc, hdx, 200 * bnc_unit, 1_000_000_000_000u128, Some(10)) + .execute(|| { + enable_slip_fees(); + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 3, "Should have 3 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("V1 Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 3, "resolved count"); + assert_eq!(solution.score, 3133623136312489, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 200000000000000u128); + assert_eq!(s.amount_out, 2948822406788253u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 10380308715000u128); + assert_eq!(s.amount_out, 153048434640856u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + // Verify solution structure + assert!(!solution.resolved_intents.is_empty(), "Should resolve intents"); + assert!(solution.score > 0, "Solution score should be positive"); + + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let eve_hdx_before = Currencies::total_balance(hdx, &eve); + let eve_bnc_before = Currencies::total_balance(bnc, &eve); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + let eve_hdx_after = Currencies::total_balance(hdx, &eve); + let eve_bnc_after = Currencies::total_balance(bnc, &eve); + assert_eq!(alice_hdx_after, 500000000000000u128); + assert_eq!(alice_bnc_after, 33814325855206u128); + assert_eq!(eve_hdx_after, 153017824953928u128); + assert_eq!(eve_bnc_after, 89619691285000u128); + + let alice_hdx_spent = alice_hdx_before.saturating_sub(alice_hdx_after); + let alice_bnc_received = alice_bnc_after.saturating_sub(alice_bnc_before); + let eve_bnc_spent = eve_bnc_before.saturating_sub(eve_bnc_after); + let eve_hdx_received = eve_hdx_after.saturating_sub(eve_hdx_before); + + // Verify Alice sold HDX, received BNC + assert!(alice_hdx_spent > 0, "Alice should have spent HDX"); + assert!(alice_bnc_received > 0, "Alice should have received BNC"); + + // Verify Eve sold BNC, received HDX + assert!(eve_bnc_spent > 0, "Eve should have spent BNC"); + assert!(eve_hdx_received > 0, "Eve should have received HDX"); + + // Verify rate consistency (uniform clearing price) + // Alice's rate (BNC/HDX) should equal Eve's inverse rate (BNC/HDX) + let alice_rate = alice_bnc_received as f64 / alice_hdx_spent as f64; + let eve_inverse_rate = eve_bnc_spent as f64 / eve_hdx_received as f64; + let rate_diff_pct = ((alice_rate - eve_inverse_rate).abs() / alice_rate) * 100.0; + + // Allow small difference due to integer rounding and AMM price impact + assert!( + rate_diff_pct < 1.0, + "Rates should be consistent (diff: {:.6}%)", + rate_diff_pct + ); + }); +} + +/// Test intent with on_success callback: Alice sells BNC, callback transfers HDX to Bob +#[test] +fn intent_with_on_success_callback() { + use codec::Encode; + use hydradx_runtime::RuntimeCall; + + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + let hdx_to_transfer = hdx_unit; + let bnc_to_sell = bnc_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), bnc, 10 * bnc_unit) + .execute(|| { + enable_slip_fees(); + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + + // Create callback: transfer HDX to Bob after successful swap + let transfer_call = RuntimeCall::Currencies(pallet_currencies::Call::transfer { + dest: bob.clone(), + currency_id: hdx, + amount: hdx_to_transfer, + }); + let callback_data: pallet_intent::types::CallData = + transfer_call.encode().try_into().expect("callback should fit"); + + let ts = Timestamp::now(); + let deadline = Some(ts + 6000 * 10); + + let min_hdx_out = 1_000_000_000_000u128; + + assert_ok!(pallet_intent::Pallet::::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: bnc, + asset_out: hdx, + amount_in: bnc_to_sell, + amount_out: min_hdx_out, + partial: false, + }), + deadline, + on_resolved: Some(callback_data), + }, + )); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 13739334785212, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 1000000000000u128); + assert_eq!(s.amount_out, 14739334785212u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_eq!(solution.resolved_intents.len(), 1, "Should resolve the intent"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + // After solution, Alice should have received HDX + let alice_hdx_after_solution = Currencies::total_balance(hdx, &alice); + let alice_hdx_received = alice_hdx_after_solution.saturating_sub(alice_hdx_before); + assert!(alice_hdx_received > 0, "Alice should have received some HDX"); + assert!( + alice_hdx_received >= hdx_to_transfer, + "Alice should have received at least {} HDX for the callback", + hdx_to_transfer + ); + + // Dispatch the callback from lazy executor queue + assert_ok!(LazyExecutor::dispatch_top( + RuntimeOrigin::none(), + LazyExecutor::dispatch_next_id() + )); + + // Verify final state + let alice_hdx_final = Currencies::total_balance(hdx, &alice); + let alice_bnc_final = Currencies::total_balance(bnc, &alice); + let bob_hdx_final = Currencies::total_balance(hdx, &bob); + + // Alice spent BNC + assert!(alice_bnc_final < alice_bnc_before, "Alice should have spent BNC"); + + // Bob received HDX from callback + let bob_hdx_received = bob_hdx_final.saturating_sub(bob_hdx_before); + assert_eq!( + bob_hdx_received, hdx_to_transfer, + "Bob should have received {} HDX from callback", + hdx_to_transfer + ); + + // Alice's final HDX should be: received - transferred to Bob + let expected_alice_hdx = alice_hdx_before + alice_hdx_received - hdx_to_transfer; + assert_eq!( + alice_hdx_final, expected_alice_hdx, + "Alice HDX balance should match expected" + ); + }); +} + +/// Test single intent trading USDT (asset 10, 6 decimals) for WETH (asset 20, 18 decimals) +/// This tests route discovery with different decimal assets +#[test] +fn usdt_weth_single_intent() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + + // Asset IDs + let usdt = 10u32; // Tether - 6 decimals + let weth = 20u32; // WETH - 18 decimals + + // Units based on decimals + let usdt_unit = 1_000_000u128; // 10^6 + + // Sell 100 USDT + let amount_in = 100 * usdt_unit; + let min_amount_out = 5_390_835_579_515u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), usdt, amount_in * 10) + .submit_swap_intent(alice.clone(), usdt, weth, amount_in, min_amount_out, Some(10)) + .execute(|| { + enable_slip_fees(); + let alice_usdt_before = Currencies::total_balance(usdt, &alice); + let alice_weth_before = Currencies::total_balance(weth, &alice); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + let original_intent_id = intents[0].0; + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution for USDT->WETH"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 46094859399839102, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 20); + assert_eq!(s.amount_in, 100000000u128); + assert_eq!(s.amount_out, 46100250235418617u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + // Verify solution structure + assert_eq!(solution.resolved_intents.len(), 1, "Should resolve exactly 1 intent"); + assert!(solution.score > 0, "Solution score should be positive"); + + // Verify the resolved intent + let resolved = &solution.resolved_intents[0]; + assert_eq!(resolved.id, original_intent_id, "Resolved intent ID should match"); + let ice_support::IntentData::Swap(ref swap_data) = resolved.data else { + panic!("expected Swap"); + }; + assert_eq!(swap_data.asset_in, usdt, "asset_in should be USDT"); + assert_eq!(swap_data.asset_out, weth, "asset_out should be WETH"); + assert_eq!( + swap_data.amount_in, amount_in, + "amount_in should match submitted amount" + ); + assert!( + swap_data.amount_out >= min_amount_out, + "amount_out should be >= min_amount_out" + ); + + // Verify trades are valid + assert!(!solution.trades.is_empty(), "Should have at least one trade"); + for trade in solution.trades.iter() { + assert!(trade.amount_in > 0, "Trade amount_in should be positive"); + assert!(trade.amount_out > 0, "Trade amount_out should be positive"); + assert!(!trade.route.is_empty(), "Trade route should not be empty"); + } + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + let alice_usdt_after = Currencies::total_balance(usdt, &alice); + let alice_weth_after = Currencies::total_balance(weth, &alice); + assert_eq!(alice_usdt_after, 900000000u128); + assert_eq!(alice_weth_after, 46091030185371534u128); + + // Verify balances changed correctly + assert!( + alice_usdt_after < alice_usdt_before, + "Alice should have less USDT after sell" + ); + assert!( + alice_weth_after > alice_weth_before, + "Alice should have more WETH after sell" + ); + + // Verify exact amounts match solution (received = amount_out - fee) + let ice_fee: Permill = ::Fee::get(); + let usdt_spent = alice_usdt_before - alice_usdt_after; + let weth_received = alice_weth_after - alice_weth_before; + assert_eq!(usdt_spent, swap_data.amount_in, "USDT spent should match solution"); + assert_eq!( + weth_received, + swap_data.amount_out - ice_fee.mul_floor(swap_data.amount_out), + "WETH received should match solution minus fee" + ); + + // Verify intent was resolved + let remaining_intents = pallet_intent::Pallet::::get_valid_intents(); + assert!(remaining_intents.is_empty(), "Intent should be resolved"); + }); +} + +/// Compare trading USDT->WETH via solver vs direct router +/// Both should give the same result for a single intent +#[test] +fn usdt_weth_solver_vs_router() { + use hydradx_traits::router::RouteProvider; + + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + // Asset IDs + let usdt = 10u32; // Tether - 6 decimals + let weth = 20u32; // WETH - 18 decimals + + // Units based on decimals + let usdt_unit = 1_000_000u128; // 10^6 + + // Sell 100 USDT + let amount_in = 100 * usdt_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), usdt, amount_in * 10) + .endow_account(bob.clone(), usdt, amount_in * 10) + .submit_swap_intent(alice.clone(), usdt, weth, amount_in, 5_390_835_579_515u128, Some(10)) + .execute(|| { + enable_slip_fees(); + // ========== SOLVER PATH (Alice) ========== + let alice_usdt_before = Currencies::total_balance(usdt, &alice); + let alice_weth_before = Currencies::total_balance(weth, &alice); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 46094859399839102, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 20); + assert_eq!(s.amount_in, 100000000u128); + assert_eq!(s.amount_out, 46100250235418617u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + let alice_usdt_after = Currencies::total_balance(usdt, &alice); + let alice_weth_after = Currencies::total_balance(weth, &alice); + + let solver_usdt_spent = alice_usdt_before - alice_usdt_after; + let solver_weth_received = alice_weth_after - alice_weth_before; + + // ========== DIRECT ROUTER PATH (Bob) ========== + let bob_usdt_before = Currencies::total_balance(usdt, &bob); + let bob_weth_before = Currencies::total_balance(weth, &bob); + + // Get the route that would be used + let route = Router::get_route(hydradx_traits::router::AssetPair::new(usdt, weth)); + + // Execute sell directly via router + assert_ok!(Router::sell( + RuntimeOrigin::signed(bob.clone()), + usdt, + weth, + amount_in, + 1, // min_amount_out + route.clone(), + )); + + let bob_usdt_after = Currencies::total_balance(usdt, &bob); + let bob_weth_after = Currencies::total_balance(weth, &bob); + assert_eq!(bob_usdt_after, 900000000u128); + assert_eq!(bob_weth_after, 46083861907558025u128); + + let router_usdt_spent = bob_usdt_before - bob_usdt_after; + let router_weth_received = bob_weth_after - bob_weth_before; + + // Both should spend the same amount of USDT + assert_eq!(solver_usdt_spent, router_usdt_spent, "USDT spent should be the same"); + + // WETH received will differ slightly because the pool state changes after the solver trade. + // The solver trades first, so when the router trades afterward, pools have different reserves. + // For a fair comparison, we verify they're within a small percentage of each other. + let diff_pct = if solver_weth_received > router_weth_received { + (solver_weth_received - router_weth_received) * 10000 / router_weth_received + } else { + (router_weth_received - solver_weth_received) * 10000 / solver_weth_received + }; + // Should be within 1% (100 bps) - accounting for pool state change + assert!( + diff_pct < 100, + "WETH difference should be within 1%, got {}bps", + diff_pct + ); + }); +} + +/// Test 2 opposing intents: Alice sells USDT for WETH, Bob sells WETH for USDT +/// These should partially match (direct matching), giving Alice a better price than single intent +#[test] +fn usdt_weth_two_opposing_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + // Asset IDs + let usdt = 10u32; // Tether - 6 decimals + let weth = 20u32; // WETH - 18 decimals + + // Units based on decimals + let usdt_unit = 1_000_000u128; // 10^6 + let weth_unit = 1_000_000_000_000_000_000u128; // 10^18 + + // Alice sells 100 USDT for WETH + let alice_usdt_amount = 100 * usdt_unit; + // Bob sells 0.01 WETH for USDT (roughly equivalent value to create partial match) + let bob_weth_amount = weth_unit / 100; // 0.01 WETH + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), usdt, alice_usdt_amount * 100) + .endow_account(bob.clone(), weth, bob_weth_amount * 100) + // Also give some of the opposite asset for potential edge cases + .endow_account(alice.clone(), weth, weth_unit) + .endow_account(bob.clone(), usdt, 1000 * usdt_unit) + // Alice: sell USDT for WETH + .submit_swap_intent( + alice.clone(), + usdt, + weth, + alice_usdt_amount, + 5_390_835_579_515u128, + Some(10), + ) + // Bob: sell WETH for USDT (opposite direction) + .submit_swap_intent(bob.clone(), weth, usdt, bob_weth_amount, 10_000, Some(10)) + .execute(|| { + enable_slip_fees(); + let alice_weth_before = Currencies::total_balance(weth, &alice); + let bob_usdt_before = Currencies::total_balance(usdt, &bob); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2, "Should have 2 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 46135687764087536, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 20); + assert_eq!(s.asset_out, 10); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 21599412u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 10); + assert_eq!(s.asset_out, 20); + assert_eq!(s.amount_in, 100000000u128); + assert_eq!(s.amount_out, 46141078578077639u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + let alice_weth_after = Currencies::total_balance(weth, &alice); + let bob_usdt_after = Currencies::total_balance(usdt, &bob); + assert_eq!(alice_weth_after, 1046131850362362024u128); + assert_eq!(bob_usdt_after, 1021595093u128); + let alice_weth_received = alice_weth_after - alice_weth_before; + let bob_usdt_received = bob_usdt_after - bob_usdt_before; + + // Verify both intents were resolved + assert!(solution.resolved_intents.len() >= 1, "Should resolve at least 1 intent"); + + // Verify Alice got WETH + assert!(alice_weth_received > 0, "Alice should receive WETH"); + + // Verify Bob got USDT + assert!(bob_usdt_received > 0, "Bob should receive USDT"); + }); +} + +/// Test: Single intent - sell ETH for 3pool +/// ETH (asset 34) - 18 decimals +/// 3pool (asset 103) - 18 decimals +#[test] +fn eth_3pool_single_intent() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + + // Asset IDs + let eth = 34u32; // ETH - 18 decimals + let pool3 = 103u32; // 3pool - 18 decimals + + // Units based on decimals (both 18 decimals) + let unit = 1_000_000_000_000_000_000u128; // 10^18 + + // Alice sells 0.1 ETH for 3pool + let alice_eth_amount = unit / 10; // 0.1 ETH + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), eth, alice_eth_amount * 10) + // Alice: sell ETH for 3pool + .submit_swap_intent( + alice.clone(), + eth, + pool3, + alice_eth_amount, + 20_000_000_000_000_000u128, //ED + Some(10), + ) + .execute(|| { + enable_slip_fees(); + let alice_eth_before = Currencies::total_balance(eth, &alice); + let alice_3pool_before = Currencies::total_balance(pool3, &alice); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| { + HollarSolver::solve(intents, state).ok() + }, + ) + .expect("Solver should produce a solution"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 212076262852531149120, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 34); + assert_eq!(s.asset_out, 103); + assert_eq!(s.amount_in, 100000000000000000u128); + assert_eq!(s.amount_out, 212096262852531149120u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let alice_eth_after = Currencies::total_balance(eth, &alice); + let alice_3pool_after = Currencies::total_balance(pool3, &alice); + assert_eq!(alice_eth_after, 900000000000000000u128); + assert_eq!(alice_3pool_after, 212053843599960642891u128); + + let eth_spent = alice_eth_before - alice_eth_after; + let pool3_received = alice_3pool_after - alice_3pool_before; + + // Verify Alice spent ETH and received 3pool + assert_eq!(eth_spent, alice_eth_amount, "Alice should spend the intent amount"); + assert!(pool3_received > 0, "Alice should receive 3pool"); + }); +} + +/// Test: Compare solver results with direct router trade for ETH -> 3pool +#[test] +fn eth_3pool_solver_vs_router() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + // Asset IDs + let eth = 34u32; // ETH - 18 decimals + let pool3 = 103u32; // 3pool - 18 decimals + + // Units based on decimals (both 18 decimals) + let unit = 1_000_000_000_000_000_000u128; // 10^18 + + // Both sell 0.1 ETH for 3pool + let amount_in = unit / 10; // 0.1 ETH + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), eth, amount_in * 10) + .endow_account(bob.clone(), eth, amount_in * 10) + // Alice: sell ETH for 3pool via intent + .submit_swap_intent( + alice.clone(), + eth, + pool3, + amount_in, + 20_000_000_000_000_000u128, //ED + Some(10), + ) + .execute(|| { + enable_slip_fees(); + // ========== SOLVER PATH (Alice) ========== + let alice_eth_before = Currencies::total_balance(eth, &alice); + let alice_3pool_before = Currencies::total_balance(pool3, &alice); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| { + HollarSolver::solve(intents, state).ok() + }, + ) + .expect("Solver should produce a solution"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 212076262852531149120, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 34); + assert_eq!(s.asset_out, 103); + assert_eq!(s.amount_in, 100000000000000000u128); + assert_eq!(s.amount_out, 212096262852531149120u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let alice_eth_after = Currencies::total_balance(eth, &alice); + let alice_3pool_after = Currencies::total_balance(pool3, &alice); + + let solver_eth_spent = alice_eth_before - alice_eth_after; + let solver_3pool_received = alice_3pool_after - alice_3pool_before; + + // ========== DIRECT ROUTER PATH (Bob) ========== + let bob_eth_before = Currencies::total_balance(eth, &bob); + let bob_3pool_before = Currencies::total_balance(pool3, &bob); + + // Get the route that would be used + let route = Router::get_route(hydradx_traits::router::AssetPair::new(eth, pool3)); + + // Execute sell directly via router + assert_ok!(Router::sell( + RuntimeOrigin::signed(bob.clone()), + eth, + pool3, + amount_in, + 1, // min_amount_out + route, + )); + + let bob_eth_after = Currencies::total_balance(eth, &bob); + let bob_3pool_after = Currencies::total_balance(pool3, &bob); + assert_eq!(bob_eth_after, 900000000000000000u128); + assert_eq!(bob_3pool_after, 211941375053826894046u128); + + let router_eth_spent = bob_eth_before - bob_eth_after; + let router_3pool_received = bob_3pool_after - bob_3pool_before; + + // Both should spend the same amount of ETH + assert_eq!(solver_eth_spent, router_eth_spent, "ETH spent should be the same"); + + // 3pool received will differ slightly because the pool state changes after the solver trade + let diff_pct = if solver_3pool_received > router_3pool_received { + (solver_3pool_received - router_3pool_received) * 10000 / router_3pool_received + } else { + (router_3pool_received - solver_3pool_received) * 10000 / solver_3pool_received + }; + + // Should be within 1% (100 bps) + assert!( + diff_pct < 100, + "3pool difference should be within 1%, got {}bps", + diff_pct + ); + }); +} + +/// Test: Two opposing intents for ETH <-> 3pool (direct matching) +#[test] +fn _eth_3pool_two_opposing_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + // Asset IDs + let eth = 34u32; // ETH - 18 decimals + let pool3 = 103u32; // 3pool - 18 decimals + + // Units based on decimals (both 18 decimals) + let unit = 1_000_000_000_000_000_000u128; // 10^18 + + // Alice sells 0.1 ETH for 3pool + let alice_eth_amount = unit / 10; // 0.1 ETH + // Bob sells 100 3pool for ETH (roughly equivalent value to create partial match) + let bob_3pool_amount = 100 * unit; // 100 3pool + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), eth, alice_eth_amount * 100) + .endow_account(bob.clone(), pool3, bob_3pool_amount * 100) + // Also give some of the opposite asset + .endow_account(alice.clone(), pool3, unit) + .endow_account(bob.clone(), eth, unit) + // Alice: sell ETH for 3pool + .submit_swap_intent( + alice.clone(), + eth, + pool3, + alice_eth_amount, + 20_000_000_000_000_000u128, //ED + Some(10), + ) + // Bob: sell 3pool for ETH (opposite direction) + .submit_swap_intent( + bob.clone(), + pool3, + eth, + bob_3pool_amount, + 20_000_000_000_000_000u128, //ED + Some(10), + ) + .execute(|| { + enable_slip_fees(); + let alice_3pool_before = Currencies::total_balance(pool3, &alice); + let bob_eth_before = Currencies::total_balance(eth, &bob); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2, "Should have 2 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| { + HollarSolver::solve(intents, state).ok() + }, + ) + .expect("Solver should produce a solution"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 214559480050935969509, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 103); + assert_eq!(s.asset_out, 34); + assert_eq!(s.amount_in, 100000000000000000000u128); + assert_eq!(s.amount_out, 46510094180971457u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 34); + assert_eq!(s.asset_out, 103); + assert_eq!(s.amount_in, 100000000000000000u128); + assert_eq!(s.amount_out, 214552969956754998052u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + let alice_3pool_after = Currencies::total_balance(pool3, &alice); + let bob_eth_after = Currencies::total_balance(eth, &bob); + assert_eq!(alice_3pool_after, 215510059362763647053u128); + assert_eq!(bob_eth_after, 1046500792162135263u128); + + let alice_3pool_received = alice_3pool_after - alice_3pool_before; + let bob_eth_received = bob_eth_after - bob_eth_before; + + // Verify both intents were resolved + assert!(solution.resolved_intents.len() >= 1, "Should resolve at least 1 intent"); + + // Verify Alice got 3pool + assert!(alice_3pool_received > 0, "Alice should receive 3pool"); + + // Verify Bob got ETH + assert!(bob_eth_received > 0, "Bob should receive ETH"); + }); +} + +/// Test ring trade: 3 intents forming HDX→BNC→DOT→HDX cycle. +/// Verifies on-chain execution, balance changes, and that ring reduces AMM trades. +#[test] +fn solver_ring_trade_triangle_execute() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + + let hdx = 0u32; + let bnc = 14u32; + let dot = 5u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + let dot_unit = 10_000_000_000u128; + + let alice_hdx_sell = 1_000 * hdx_unit; + let bob_bnc_sell = 5 * bnc_unit; + let charlie_dot_sell = 10 * dot_unit; + + let alice_min_bnc = bnc_unit / 2; + let bob_min_dot = dot_unit / 10; + let charlie_min_hdx = 500 * hdx_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 10) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 10) + .endow_account(charlie.clone(), dot, charlie_dot_sell * 10) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, dot, bob_bnc_sell, bob_min_dot, Some(10)) + .submit_swap_intent(charlie.clone(), dot, hdx, charlie_dot_sell, charlie_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution for ring trade"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 3, "resolved count"); + assert_eq!(solution.score, 5845696825241189, "score"); + assert_eq!(solution.trades.len(), 2, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 100000000000u128); + assert_eq!(s.amount_out, 6278748953562634u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 5000000000000u128); + assert_eq!(s.amount_out, 1173475802u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1000000000000000u128); + assert_eq!(s.amount_out, 67447698202753u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 3, "All 3 intents should be resolved"); + assert!(solution.trades.len() < 3, "Ring should reduce AMM trades below 3"); + + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + let bob_dot_before = Currencies::total_balance(dot, &bob); + let charlie_dot_before = Currencies::total_balance(dot, &charlie); + let charlie_hdx_before = Currencies::total_balance(hdx, &charlie); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + assert!( + pallet_intent::Pallet::::get_valid_intents().is_empty(), + "All intents resolved" + ); + + // Verify balance directions + assert!(Currencies::total_balance(hdx, &alice) < alice_hdx_before); + assert!(Currencies::total_balance(bnc, &alice) > alice_bnc_before); + assert!(Currencies::total_balance(bnc, &bob) < bob_bnc_before); + assert!(Currencies::total_balance(dot, &bob) > bob_dot_before); + assert!(Currencies::total_balance(dot, &charlie) < charlie_dot_before); + assert!(Currencies::total_balance(hdx, &charlie) > charlie_hdx_before); + + // Verify balance changes match solution (received = amount_out - fee) + let ice_fee: Permill = ::Fee::get(); + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + let expected_payout = s.amount_out - ice_fee.mul_floor(s.amount_out); + match (s.asset_in, s.asset_out) { + (0, 14) => { + assert_eq!(alice_hdx_before - Currencies::total_balance(hdx, &alice), s.amount_in); + assert_eq!( + Currencies::total_balance(bnc, &alice) - alice_bnc_before, + expected_payout + ); + } + (14, 5) => { + assert_eq!(bob_bnc_before - Currencies::total_balance(bnc, &bob), s.amount_in); + assert_eq!(Currencies::total_balance(dot, &bob) - bob_dot_before, expected_payout); + } + (5, 0) => { + assert_eq!( + charlie_dot_before - Currencies::total_balance(dot, &charlie), + s.amount_in + ); + assert_eq!( + Currencies::total_balance(hdx, &charlie) - charlie_hdx_before, + expected_payout + ); + } + _ => panic!("Unexpected direction"), + } + } + + // Verify limits met + assert!(Currencies::total_balance(bnc, &alice) - alice_bnc_before >= alice_min_bnc); + assert!(Currencies::total_balance(dot, &bob) - bob_dot_before >= bob_min_dot); + assert!(Currencies::total_balance(hdx, &charlie) - charlie_hdx_before >= charlie_min_hdx); + }); +} + +/// Compare ring trade via solver vs direct trades on identical pool state. +/// Solver should give equal or better output due to ring-matched volume avoiding AMM slippage. +#[test] +fn solver_ring_trade_vs_direct_trades() { + use hydradx_traits::router::{AssetPair, RouteProvider}; + use std::cell::RefCell; + + let hdx = 0u32; + let bnc = 14u32; + let dot = 5u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + let dot_unit = 10_000_000_000u128; + + let alice_hdx_sell = 1_000 * hdx_unit; + let bob_bnc_sell = 5 * bnc_unit; + let charlie_dot_sell = 10 * dot_unit; + + let alice_min_bnc = bnc_unit / 2; + let bob_min_dot = dot_unit / 10; + let charlie_min_hdx = 500 * hdx_unit; + + // Run 1: Direct trades on fresh state + let direct_results: RefCell<(u128, u128, u128)> = RefCell::new((0, 0, 0)); + + TestNet::reset(); + { + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 10) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 10) + .endow_account(charlie.clone(), dot, charlie_dot_sell * 10) + .execute(|| { + enable_slip_fees(); + + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let route = Router::get_route(AssetPair::new(hdx, bnc)); + assert_ok!(Router::sell( + RuntimeOrigin::signed(alice.clone()), + hdx, + bnc, + alice_hdx_sell, + 1, + route + )); + let d_alice = Currencies::total_balance(bnc, &alice) - alice_bnc_before; + + let bob_dot_before = Currencies::total_balance(dot, &bob); + let route = Router::get_route(AssetPair::new(bnc, dot)); + assert_ok!(Router::sell( + RuntimeOrigin::signed(bob.clone()), + bnc, + dot, + bob_bnc_sell, + 1, + route + )); + let d_bob = Currencies::total_balance(dot, &bob) - bob_dot_before; + + let charlie_hdx_before = Currencies::total_balance(hdx, &charlie); + let route = Router::get_route(AssetPair::new(dot, hdx)); + assert_ok!(Router::sell( + RuntimeOrigin::signed(charlie.clone()), + dot, + hdx, + charlie_dot_sell, + 1, + route + )); + let d_charlie = Currencies::total_balance(hdx, &charlie) - charlie_hdx_before; + + *direct_results.borrow_mut() = (d_alice, d_bob, d_charlie); + }); + } + let (_direct_alice, _direct_bob, _direct_charlie) = *direct_results.borrow(); + + // Run 2: Solver on fresh state + TestNet::reset(); + { + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 10) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 10) + .endow_account(charlie.clone(), dot, charlie_dot_sell * 10) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, dot, bob_bnc_sell, bob_min_dot, Some(10)) + .submit_swap_intent(charlie.clone(), dot, hdx, charlie_dot_sell, charlie_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_dot_before = Currencies::total_balance(dot, &bob); + let charlie_hdx_before = Currencies::total_balance(hdx, &charlie); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| { + Solver::solve(intents, state).ok() + }, + ) + .expect("Solver should produce a solution"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 3, "resolved count"); + assert_eq!(solution.score, 5845696825241189, "score"); + assert_eq!(solution.trades.len(), 2, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 100000000000u128); + assert_eq!(s.amount_out, 6278748953562634u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 5000000000000u128); + assert_eq!(s.amount_out, 1173475802u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1000000000000000u128); + assert_eq!(s.amount_out, 67447698202753u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + let solver_alice = Currencies::total_balance(bnc, &alice) - alice_bnc_before; + let solver_bob = Currencies::total_balance(dot, &bob) - bob_dot_before; + let solver_charlie = Currencies::total_balance(hdx, &charlie) - charlie_hdx_before; + + // Verify solver produces valid results (all users get output) + assert!(solver_alice > 0, "Alice should receive BNC"); + assert!(solver_bob > 0, "Bob should receive DOT"); + assert!(solver_charlie > 0, "Charlie should receive HDX"); + }); + } +} + +/// Mixed batch: 12 intents, 5 users, 3 assets. +/// Tests opposing flows, same-direction groups, ring detection, rate uniformity, and execution. +#[test] +fn solver_mixed_batch_12_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + let eve: AccountId = EVE.into(); + + let hdx = 0u32; + let bnc = 14u32; + let dot = 5u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + let dot_unit = 10_000_000_000u128; + + let min_bnc = bnc_unit; + let min_hdx = 200 * hdx_unit; + let min_dot = dot_unit / 10; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, 20_000 * hdx_unit) + .endow_account(alice.clone(), dot, 20 * dot_unit) + .endow_account(bob.clone(), bnc, 100 * bnc_unit) + .endow_account(charlie.clone(), bnc, 100 * bnc_unit) + .endow_account(charlie.clone(), dot, 30 * dot_unit) + .endow_account(dave.clone(), hdx, 20_000 * hdx_unit) + .endow_account(eve.clone(), hdx, 10_000 * hdx_unit) + .endow_account(eve.clone(), dot, 10 * dot_unit) + .submit_swap_intent(alice.clone(), hdx, bnc, 10_000 * hdx_unit, min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, 30 * bnc_unit, min_hdx, Some(10)) + .submit_swap_intent(charlie.clone(), bnc, hdx, 50 * bnc_unit, min_hdx, Some(10)) + .submit_swap_intent(dave.clone(), hdx, bnc, 8_000 * hdx_unit, min_bnc, Some(10)) + .submit_swap_intent(alice.clone(), hdx, dot, 5_000 * hdx_unit, min_dot, Some(10)) + .submit_swap_intent(dave.clone(), hdx, dot, 3_000 * hdx_unit, min_dot, Some(10)) + .submit_swap_intent(eve.clone(), hdx, dot, 4_000 * hdx_unit, min_dot, Some(10)) + .submit_swap_intent(bob.clone(), bnc, dot, 20 * bnc_unit, min_dot, Some(10)) + .submit_swap_intent(charlie.clone(), dot, hdx, 15 * dot_unit, min_hdx, Some(10)) + .submit_swap_intent(eve.clone(), dot, bnc, 5 * dot_unit, min_bnc, Some(10)) + .submit_swap_intent(alice.clone(), dot, bnc, 10 * dot_unit, min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, dot, 10 * bnc_unit, min_dot, Some(10)) + .execute(|| { + enable_slip_fees(); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 12, "Should have 12 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution for 12 intents"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 12, "resolved count"); + assert_eq!(solution.score, 11877410818021263, "score"); + assert_eq!(solution.trades.len(), 3, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960011); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 2346951604u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960010); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000u128); + assert_eq!(s.amount_out, 424438633328854u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960009); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 50000000000u128); + assert_eq!(s.amount_out, 212219316664427u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960008); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 150000000000u128); + assert_eq!(s.amount_out, 9448644910715705u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[4]; + assert_eq!(r.id, 32752052247409382067756072960007); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 20000000000000u128); + assert_eq!(s.amount_out, 4693903208u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[5]; + assert_eq!(r.id, 32752052247409382067756072960006); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 4000000000000000u128); + assert_eq!(s.amount_out, 63473050563u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[6]; + assert_eq!(r.id, 32752052247409382067756072960005); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 3000000000000000u128); + assert_eq!(s.amount_out, 47604787922u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[7]; + assert_eq!(r.id, 32752052247409382067756072960004); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 5000000000000000u128); + assert_eq!(s.amount_out, 79341313203u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[8]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 8000000000000000u128); + assert_eq!(s.amount_out, 539209558340612u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[9]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 50000000000000u128); + assert_eq!(s.amount_out, 739183744399625u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[10]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443510246639775u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[11]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 674011947925765u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + // All 12 should be resolved + assert_eq!(solution.resolved_intents.len(), 12, "All 12 intents should be resolved"); + assert!(solution.score > 0, "Score should be positive"); + + // Rate uniformity: same-direction intents must have same out/in ratio + let mut rates_by_direction: std::collections::BTreeMap<(u32, u32), Vec> = + std::collections::BTreeMap::new(); + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + let rate = s.amount_out as f64 / s.amount_in as f64; + rates_by_direction + .entry((s.asset_in, s.asset_out)) + .or_default() + .push(rate); + } + for ((a, b), rates) in &rates_by_direction { + if rates.len() > 1 { + let max = rates.iter().cloned().fold(f64::NEG_INFINITY, f64::max); + let min = rates.iter().cloned().fold(f64::INFINITY, f64::min); + let diff_pct = if min > 0.0 { (max - min) / min * 100.0 } else { 0.0 }; + assert!( + diff_pct < 0.001, + "Rate spread for {} → {} should be < 0.001%, got {:.6}%", + a, + b, + diff_pct + ); + } + } + + // Submit and verify execution + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + let bob_dot_before = Currencies::total_balance(dot, &bob); + let charlie_hdx_before = Currencies::total_balance(hdx, &charlie); + let charlie_bnc_before = Currencies::total_balance(bnc, &charlie); + let dave_bnc_before = Currencies::total_balance(bnc, &dave); + let dave_dot_before = Currencies::total_balance(dot, &dave); + let eve_bnc_before = Currencies::total_balance(bnc, &eve); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + assert!( + pallet_intent::Pallet::::get_valid_intents().is_empty(), + "All intents resolved" + ); + + // Verify balance directions + assert!( + Currencies::total_balance(hdx, &alice) < alice_hdx_before, + "Alice sold HDX" + ); + assert!( + Currencies::total_balance(bnc, &alice) > alice_bnc_before, + "Alice got BNC" + ); + assert!(Currencies::total_balance(hdx, &bob) > bob_hdx_before, "Bob got HDX"); + assert!(Currencies::total_balance(bnc, &bob) < bob_bnc_before, "Bob sold BNC"); + assert!(Currencies::total_balance(dot, &bob) > bob_dot_before, "Bob got DOT"); + assert!( + Currencies::total_balance(hdx, &charlie) > charlie_hdx_before, + "Charlie got HDX" + ); + assert!( + Currencies::total_balance(bnc, &charlie) < charlie_bnc_before, + "Charlie sold BNC" + ); + assert!(Currencies::total_balance(bnc, &dave) > dave_bnc_before, "Dave got BNC"); + assert!(Currencies::total_balance(dot, &dave) > dave_dot_before, "Dave got DOT"); + assert!(Currencies::total_balance(bnc, &eve) > eve_bnc_before, "Eve got BNC"); + }); +} + +/// Compare 12-intent mixed batch: solver vs 12 sequential direct trades on identical pool state. +#[test] +fn solver_mixed_batch_vs_direct_trades() { + use hydradx_traits::router::{AssetPair, RouteProvider}; + use std::cell::RefCell; + + let hdx = 0u32; + let bnc = 14u32; + let dot = 5u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + let dot_unit = 10_000_000_000u128; + + let min_bnc = bnc_unit; + let min_hdx = 200 * hdx_unit; + let min_dot = dot_unit / 10; + + let trades: Vec<(u32, u32, u128)> = vec![ + (hdx, bnc, 10_000 * hdx_unit), + (bnc, hdx, 30 * bnc_unit), + (bnc, hdx, 50 * bnc_unit), + (hdx, bnc, 8_000 * hdx_unit), + (hdx, dot, 5_000 * hdx_unit), + (hdx, dot, 3_000 * hdx_unit), + (hdx, dot, 4_000 * hdx_unit), + (bnc, dot, 20 * bnc_unit), + (dot, hdx, 15 * dot_unit), + (dot, bnc, 5 * dot_unit), + (dot, bnc, 10 * dot_unit), + (bnc, dot, 10 * bnc_unit), + ]; + + // Run 1: Direct trades on fresh state + let direct_total: RefCell = RefCell::new(0); + + TestNet::reset(); + { + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + let eve: AccountId = EVE.into(); + let users: Vec = vec![ + alice.clone(), + bob.clone(), + charlie.clone(), + dave.clone(), + alice.clone(), + dave.clone(), + eve.clone(), + bob.clone(), + charlie.clone(), + eve.clone(), + alice.clone(), + bob.clone(), + ]; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, 20_000 * hdx_unit) + .endow_account(alice.clone(), dot, 20 * dot_unit) + .endow_account(bob.clone(), bnc, 100 * bnc_unit) + .endow_account(charlie.clone(), bnc, 100 * bnc_unit) + .endow_account(charlie.clone(), dot, 30 * dot_unit) + .endow_account(dave.clone(), hdx, 20_000 * hdx_unit) + .endow_account(eve.clone(), hdx, 10_000 * hdx_unit) + .endow_account(eve.clone(), dot, 10 * dot_unit) + .execute(|| { + enable_slip_fees(); + let mut total = 0u128; + for (i, &(asset_in, asset_out, amount_in)) in trades.iter().enumerate() { + let user = &users[i]; + let before = Currencies::total_balance(asset_out, user); + let route = Router::get_route(AssetPair::new(asset_in, asset_out)); + assert_ok!(Router::sell( + RuntimeOrigin::signed(user.clone()), + asset_in, + asset_out, + amount_in, + 1, + route + )); + total += Currencies::total_balance(asset_out, user) - before; + } + *direct_total.borrow_mut() = total; + }); + } + let _direct = *direct_total.borrow(); + + // Run 2: Solver on fresh state + TestNet::reset(); + { + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + let eve: AccountId = EVE.into(); + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, 20_000 * hdx_unit) + .endow_account(alice.clone(), dot, 20 * dot_unit) + .endow_account(bob.clone(), bnc, 100 * bnc_unit) + .endow_account(charlie.clone(), bnc, 100 * bnc_unit) + .endow_account(charlie.clone(), dot, 30 * dot_unit) + .endow_account(dave.clone(), hdx, 20_000 * hdx_unit) + .endow_account(eve.clone(), hdx, 10_000 * hdx_unit) + .endow_account(eve.clone(), dot, 10 * dot_unit) + .submit_swap_intent(alice.clone(), hdx, bnc, 10_000 * hdx_unit, min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, 30 * bnc_unit, min_hdx, Some(10)) + .submit_swap_intent(charlie.clone(), bnc, hdx, 50 * bnc_unit, min_hdx, Some(10)) + .submit_swap_intent(dave.clone(), hdx, bnc, 8_000 * hdx_unit, min_bnc, Some(10)) + .submit_swap_intent(alice.clone(), hdx, dot, 5_000 * hdx_unit, min_dot, Some(10)) + .submit_swap_intent(dave.clone(), hdx, dot, 3_000 * hdx_unit, min_dot, Some(10)) + .submit_swap_intent(eve.clone(), hdx, dot, 4_000 * hdx_unit, min_dot, Some(10)) + .submit_swap_intent(bob.clone(), bnc, dot, 20 * bnc_unit, min_dot, Some(10)) + .submit_swap_intent(charlie.clone(), dot, hdx, 15 * dot_unit, min_hdx, Some(10)) + .submit_swap_intent(eve.clone(), dot, bnc, 5 * dot_unit, min_bnc, Some(10)) + .submit_swap_intent(alice.clone(), dot, bnc, 10 * dot_unit, min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, dot, 10 * bnc_unit, min_dot, Some(10)) + .execute(|| { + enable_slip_fees(); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| { + Solver::solve(intents, state).ok() + }, + ) + .expect("Solver should produce a solution"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 12, "resolved count"); + assert_eq!(solution.score, 11877410818021263, "score"); + assert_eq!(solution.trades.len(), 3, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960011); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 10000000000000u128); + assert_eq!(s.amount_out, 2346951604u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960010); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000u128); + assert_eq!(s.amount_out, 424438633328854u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960009); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 50000000000u128); + assert_eq!(s.amount_out, 212219316664427u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960008); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 5); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 150000000000u128); + assert_eq!(s.amount_out, 9448644910715705u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[4]; + assert_eq!(r.id, 32752052247409382067756072960007); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 20000000000000u128); + assert_eq!(s.amount_out, 4693903208u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[5]; + assert_eq!(r.id, 32752052247409382067756072960006); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 4000000000000000u128); + assert_eq!(s.amount_out, 63473050563u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[6]; + assert_eq!(r.id, 32752052247409382067756072960005); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 3000000000000000u128); + assert_eq!(s.amount_out, 47604787922u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[7]; + assert_eq!(r.id, 32752052247409382067756072960004); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 5); + assert_eq!(s.amount_in, 5000000000000000u128); + assert_eq!(s.amount_out, 79341313203u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[8]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 8000000000000000u128); + assert_eq!(s.amount_out, 539209558340612u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[9]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 50000000000000u128); + assert_eq!(s.amount_out, 739183744399625u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[10]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443510246639775u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[11]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 674011947925765u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + + // Verify all 12 intents resolved and executed + assert_eq!(solution.resolved_intents.len(), 12, "All 12 intents should be resolved"); + }); + } +} + +/// Test near-perfect cancellation: two opposing intents that almost cancel, +/// leaving only a tiny net imbalance for the AMM. +/// Must produce a valid solution and execute on-chain. +#[test] +fn solver_near_perfect_cancel_ed_remainder() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // Spot: BNC/HDX ≈ 14.7 (1 BNC ≈ 14.7 HDX from snapshot) + // Alice: sell 1000 HDX for BNC (~67.8 BNC at spot) + let alice_hdx_sell = 1000 * hdx_unit; + // Bob: sell 68 BNC for HDX (~1002 HDX at spot) + // Net excess BNC: ~0.2 BNC ≈ 3 HDX to trade through AMM (tiny remainder) + let bob_bnc_sell = 68 * bnc_unit; + + let alice_min_bnc = 50 * bnc_unit; + let bob_min_hdx = 800 * hdx_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 10) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 10) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, bob_bnc_sell, bob_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 2); + + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution for near-perfect cancel"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 222915681098482, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 68000000000000u128); + assert_eq!(s.amount_out, 1005273500952042u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1000000000000000u128); + assert_eq!(s.amount_out, 67642180146440u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 2, "Both intents must be resolved"); + // Near-perfect cancel: at most 1 small AMM trade for the net remainder + assert!(solution.trades.len() <= 1, "Should need at most 1 AMM trade"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + assert!( + pallet_intent::Pallet::::get_valid_intents().is_empty(), + "All intents resolved" + ); + + assert!( + Currencies::total_balance(hdx, &alice) < alice_hdx_before, + "Alice sold HDX" + ); + assert!( + Currencies::total_balance(bnc, &alice) > alice_bnc_before, + "Alice got BNC" + ); + assert!(Currencies::total_balance(bnc, &bob) < bob_bnc_before, "Bob sold BNC"); + assert!(Currencies::total_balance(hdx, &bob) > bob_hdx_before, "Bob got HDX"); + }); +} + +/// Test with amounts at existential deposit level. + +/// Test with near-cancelling amounts where the net AMM remainder is small. +/// Alice sells 100 HDX for BNC (~6.78 BNC at spot). +/// Bob sells 7 BNC for HDX (~103 HDX at spot). +/// Net excess: ~0.22 BNC ≈ 3 HDX — very small AMM trade. +#[test] +fn solver_existential_deposit_amounts() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // Spot: 1 BNC ≈ 14.7 HDX + let alice_hdx_sell = 100 * hdx_unit; + let bob_bnc_sell = 7 * bnc_unit; // 7 BNC + + let alice_min_bnc = 4 * bnc_unit; + let bob_min_hdx = 60 * hdx_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 100) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 100) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, bob_bnc_sell, bob_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 2); + + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must handle near-ED AMM remainder"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 46239141473405, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 7000000000000u128); + assert_eq!(s.amount_out, 103474923458761u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000000u128); + assert_eq!(s.amount_out, 6764218014644u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 2, "Both intents must be resolved"); + assert!( + solution.trades.len() <= 1, + "Near-cancel should need at most 1 small AMM trade" + ); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + assert!(pallet_intent::Pallet::::get_valid_intents().is_empty()); + + assert!( + Currencies::total_balance(hdx, &alice) < alice_hdx_before, + "Alice sold HDX" + ); + assert!( + Currencies::total_balance(bnc, &alice) > alice_bnc_before, + "Alice got BNC" + ); + assert!(Currencies::total_balance(bnc, &bob) < bob_bnc_before, "Bob sold BNC"); + assert!(Currencies::total_balance(hdx, &bob) > bob_hdx_before, "Bob got HDX"); + }); +} + +/// Test where opposing intents nearly cancel, leaving AMM remainder below ED. +/// Alice sells 50 HDX for BNC (~3.37 BNC at spot). +/// Bob sells 3.42 BNC for HDX (~50.4 HDX at spot). +/// Net excess: ~0.05 BNC ≈ 0.7 HDX — below minimum trade size. +/// Both intents resolve in the solution, but execution fails with Token(BelowMinimum) +/// because the dust AMM trade amount is below BNC's existential deposit. +#[test] +fn solver_amm_remainder_below_ed() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // Spot: 1 BNC ≈ 14.7 HDX + // Alice: sell 50 HDX → ~3.37 BNC + let alice_hdx_sell = 50 * hdx_unit; + // Bob: sell 3.42 BNC → ~50.4 HDX + // Net excess BNC: 3.42 - 3.37 = 0.05 BNC ≈ 0.7 HDX — below or near ED + let bob_bnc_sell = 342 * bnc_unit / 100; // 3.42 BNC + + let alice_min_bnc = 2 * bnc_unit; // expect ~3.37, require 2 + let bob_min_hdx = 30 * hdx_unit; // expect ~50.4, require 30 + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 100) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 100) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, bob_bnc_sell, bob_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 2); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 21382109007322, "score"); + assert_eq!(solution.trades.len(), 0, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 3420000000000u128); + assert_eq!(s.amount_out, 50000000000000u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 50000000000000u128); + assert_eq!(s.amount_out, 3382109007322u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 2, "Both intents must be resolved"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + }); +} + +/// Test where opposing intents cancel almost exactly — AMM remainder is dust. +/// Alice sells 50 HDX → ~3.37 BNC at spot. +/// Bob sells 3.39 BNC → ~49.9 HDX at spot. +/// Net excess: ~0.02 BNC ≈ 0.3 HDX — dust level. +/// Both intents resolve in the solution, but execution fails with Token(BelowMinimum) +/// because the dust AMM trade amount is below BNC's existential deposit. +#[test] +fn solver_amm_remainder_dust() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // Spot: 1 BNC ≈ 14.7 HDX + let alice_hdx_sell = 50 * hdx_unit; + // 3.39 BNC ≈ 49.9 HDX — almost exactly cancels Alice's 50 HDX + let bob_bnc_sell = 339 * bnc_unit / 100; // 3.39 BNC + + let alice_min_bnc = 2 * bnc_unit; + let bob_min_hdx = 30 * hdx_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 100) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 100) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, bob_bnc_sell, bob_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 2); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution for dust-level remainder"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 21382109007322, "score"); + assert_eq!(solution.trades.len(), 0, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 3390000000000u128); + assert_eq!(s.amount_out, 50000000000000u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 50000000000000u128); + assert_eq!(s.amount_out, 3382109007322u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 2, "Both intents must be resolved"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + }); +} + +/// 3-intent near-cancel with dust AMM remainder. +/// Alice sells 100 HDX → BNC, Bob+Charlie each sell 3.39 BNC → HDX. +/// Bob+Charlie total: 6.78 BNC ≈ 100.0 HDX — nearly exact cancel with Alice. +/// Net excess BNC is dust — below BNC's ED of 68_795_189_840. +/// The solver detects the dust remainder and skips the AMM trade, +/// resolving all intents via direct matching only. +#[test] +fn solver_three_intent_dust_remainder() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // Spot: 1 BNC ≈ 14.7 HDX + let alice_hdx_sell = 100 * hdx_unit; + // 3.39 BNC ≈ 49.8 HDX each; total 6.78 BNC ≈ 100.0 HDX — nearly cancels Alice + let bob_bnc_sell = 339 * bnc_unit / 100; // 3.39 BNC + let charlie_bnc_sell = 339 * bnc_unit / 100; // 3.39 BNC + + let alice_min_bnc = 4 * bnc_unit; + let bob_min_hdx = 30 * hdx_unit; + let charlie_min_hdx = 30 * hdx_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 100) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 100) + .endow_account(charlie.clone(), bnc, charlie_bnc_sell * 100) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, bob_bnc_sell, bob_min_hdx, Some(10)) + .submit_swap_intent(charlie.clone(), bnc, hdx, charlie_bnc_sell, charlie_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 3); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution for 3-intent dust remainder"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 3, "resolved count"); + assert_eq!(solution.score, 42764218014644, "score"); + assert_eq!(solution.trades.len(), 0, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 3390000000000u128); + assert_eq!(s.amount_out, 50000000000000u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 3390000000000u128); + assert_eq!(s.amount_out, 50000000000000u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000000u128); + assert_eq!(s.amount_out, 6764218014644u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 3, "All three intents must be resolved"); + + // Dust remainder is below ED — solver skips the AMM trade entirely + assert_eq!(solution.trades.len(), 0, "No AMM trades — dust remainder skipped"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + }); +} + +/// Test that the ICE protocol fee is deducted from each resolved intent's output. +/// Two opposing intents (HDX↔BNC) are resolved. Each recipient should receive +/// amount_out * (1 - fee) where fee = 0.02% (Permill::from_parts(200)). +/// The fee remains in the ICE holding pot. +#[test] +fn solver_ice_fee_is_deducted() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // At ~14.7 HDX/BNC: + // Alice: sell 1000 HDX → ~67 BNC + // Bob: sell 100 BNC → ~1474 HDX + // Large spread ensures both resolve comfortably + let alice_hdx_sell = 1000 * hdx_unit; + let bob_bnc_sell = 100 * bnc_unit; + + let alice_min_bnc = 10 * bnc_unit; + let bob_min_hdx = 200 * hdx_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_hdx_sell * 10) + .endow_account(bob.clone(), bnc, bob_bnc_sell * 10) + .submit_swap_intent(alice.clone(), hdx, bnc, alice_hdx_sell, alice_min_bnc, Some(10)) + .submit_swap_intent(bob.clone(), bnc, hdx, bob_bnc_sell, bob_min_hdx, Some(10)) + .execute(|| { + enable_slip_fees(); + + assert_eq!(pallet_intent::Pallet::::get_valid_intents().len(), 2); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 1334519554542507, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 100000000000000u128); + assert_eq!(s.amount_out, 1476877374396067u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1000000000000000u128); + assert_eq!(s.amount_out, 67642180146440u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + assert_eq!(solution.resolved_intents.len(), 2, "Both intents must be resolved"); + + // Capture resolved amounts before execution + let mut alice_resolved_bnc = 0u128; + let mut bob_resolved_hdx = 0u128; + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + if s.asset_out == bnc { + alice_resolved_bnc = s.amount_out; + } else if s.asset_out == hdx { + bob_resolved_hdx = s.amount_out; + } + } + assert!(alice_resolved_bnc > 0, "Alice should receive BNC"); + assert!(bob_resolved_hdx > 0, "Bob should receive HDX"); + + let ice_fee: Permill = ::Fee::get(); + + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let holding_pot = pallet_ice::Pallet::::get_pallet_account(); + + // Pre-fund the pot with native ED so it isn't reaped after the fee-only remainder. + // In production the pot persists across solutions and accumulates fees over time. + assert_ok!(hydradx_runtime::Balances::force_set_balance( + RuntimeOrigin::root(), + holding_pot.clone(), + hdx_unit, + )); + + let pot_bnc_before = Currencies::total_balance(bnc, &holding_pot); + let pot_hdx_before = Currencies::total_balance(hdx, &holding_pot); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + // Verify fee deduction: recipients get amount_out - fee + let alice_fee = ice_fee.mul_floor(alice_resolved_bnc); + let bob_fee = ice_fee.mul_floor(bob_resolved_hdx); + let alice_expected_payout = alice_resolved_bnc - alice_fee; + let bob_expected_payout = bob_resolved_hdx - bob_fee; + + let alice_bnc_received = Currencies::total_balance(bnc, &alice) - alice_bnc_before; + let bob_hdx_received = Currencies::total_balance(hdx, &bob) - bob_hdx_before; + + assert_eq!( + alice_bnc_received, alice_expected_payout, + "Alice should receive amount_out minus fee" + ); + assert_eq!( + bob_hdx_received, bob_expected_payout, + "Bob should receive amount_out minus fee" + ); + + // Verify fees stayed in holding pot + assert!(alice_fee > 0, "Alice fee should be non-zero"); + assert!(bob_fee > 0, "Bob fee should be non-zero"); + + // The holding pot balance after execution should have increased by the fee amounts + // (relative to what it would be with zero fees — i.e., the pot retains the fees) + let pot_bnc_after = Currencies::total_balance(bnc, &holding_pot); + let pot_hdx_after = Currencies::total_balance(hdx, &holding_pot); + assert_eq!(pot_bnc_after, 13528436029u128); + assert_eq!(pot_hdx_after, 1343067981569u128); + assert!( + pot_bnc_after >= pot_bnc_before + alice_fee, + "Holding pot should retain BNC fee: before={}, after={}, fee={}", + pot_bnc_before, + pot_bnc_after, + alice_fee + ); + assert!( + pot_hdx_after >= pot_hdx_before + bob_fee, + "Holding pot should retain HDX fee: before={}, after={}, fee={}", + pot_hdx_before, + pot_hdx_after, + bob_fee + ); + }); +} + +/// V2 partial fill test: small intents + whale. +/// +/// 3 small intents sell 10,000 HDX → BNC each (partial: false, loose limit). +/// 1 whale sells 5,000,000 HDX → BNC (partial: true, tight limit). +/// +/// Without partial fills, the whale's volume would push the batch rate below +/// its limit, and it would be excluded entirely. With v2, the whale gets a +/// partial fill — only as much volume as the AMM can absorb at the minimum rate. +#[test] +fn solver_v2_partial_fill_whale() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); // small + let bob: AccountId = BOB.into(); // small + let charlie: AccountId = CHARLIE.into(); // small + let dave: AccountId = DAVE.into(); // whale + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + // Spot: 1 HDX ≈ 0.068 BNC (from snapshot) + let small_amount = 10_000 * hdx_unit; + let whale_amount = 5_000_000 * hdx_unit; + // Loose limit for small intents: 1 BNC per 10,000 HDX (way below spot) + let small_min_bnc = 1_000_000_000_000u128; + // Tight limit for whale: require ~0.065 BNC per HDX (close to spot of ~0.068) + // 5,000,000 HDX * 0.065 = 325,000 BNC + let whale_min_bnc = 325_000 * 1_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, small_amount * 10) + .endow_account(bob.clone(), hdx, small_amount * 10) + .endow_account(charlie.clone(), hdx, small_amount * 10) + .endow_account(dave.clone(), hdx, whale_amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Submit 3 small non-partial intents + for (who, label) in [ + (alice.clone(), "alice"), + (bob.clone(), "bob"), + (charlie.clone(), "charlie"), + ] { + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(who), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: small_amount, + amount_out: small_min_bnc, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + println!("{}: submitted {} HDX → BNC (non-partial)", label, small_amount); + } + + // Submit whale partial intent + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(dave.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: whale_amount, + amount_out: whale_min_bnc, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + println!( + "dave (whale): submitted {} HDX → BNC (partial, min_bnc={})", + whale_amount, whale_min_bnc + ); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + println!("total intents: {}", intents.len()); + assert_eq!(intents.len(), 4); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 4, "resolved count"); + assert_eq!(solution.score, 1947000006488054, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 650000000060173u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 650000000060173u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 650000000060173u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1048233525000000000u128); + assert_eq!(s.amount_out, 68135179131307535u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + println!( + "\nsolution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + let mut whale_resolved = false; + let mut whale_fill = 0u128; + for (i, ri) in solution.resolved_intents.iter().enumerate() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + continue; + }; + let is_whale = s.amount_in != small_amount || s.partial.is_partial(); + println!( + "resolved[{}]: id={}, amount_in={}, amount_out={}, partial={:?} {}", + i, + ri.id, + s.amount_in, + s.amount_out, + s.partial, + if is_whale { "← WHALE" } else { "" } + ); + if is_whale { + whale_resolved = true; + whale_fill = s.amount_in; + } + } + + // All 3 small intents should be resolved + let small_count = solution + .resolved_intents + .iter() + .filter(|ri| { + let ice_support::IntentData::Swap(ref s) = ri.data else { + return false; + }; + !s.partial.is_partial() && s.amount_in == small_amount + }) + .count(); + println!("\nsmall intents resolved: {}/3", small_count); + assert_eq!(small_count, 3, "All 3 small intents should be resolved"); + + // Whale should be resolved (possibly partially) + assert!(whale_resolved, "Whale should be in the solution"); + println!( + "whale fill: {} / {} ({:.1}%)", + whale_fill, + whale_amount, + (whale_fill as f64 / whale_amount as f64) * 100.0 + ); + + if whale_fill < whale_amount { + println!("whale was PARTIALLY filled — v2 partial fill working!"); + } else { + println!("whale was FULLY filled"); + } + + // Execute the solution + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + println!("submit_solution: OK"); + + // Check whale intent is still open if partially filled + if whale_fill < whale_amount { + let whale_intent_id = intents + .iter() + .find(|(_, intent)| { + let ice_support::IntentData::Swap(ref s) = intent.data else { + return false; + }; + s.partial.is_partial() && s.amount_in == whale_amount + }) + .map(|(id, _)| *id) + .expect("whale intent should exist"); + + let stored = pallet_intent::Pallet::::get_intent(whale_intent_id); + assert!( + stored.is_some(), + "Whale intent should still be in storage after partial fill" + ); + let stored = stored.unwrap(); + let ice_support::IntentData::Swap(ref s) = stored.data else { + panic!("expected Swap") + }; + println!( + "whale intent after fill: amount_in={} (immutable), partial={:?}, remaining={}", + s.amount_in, + s.partial, + s.remaining() + ); + assert_eq!(s.amount_in, whale_amount, "Original amount_in should be immutable"); + assert_eq!(s.partial.filled(), whale_fill, "Filled counter should match"); + assert!(s.remaining() > 0, "Should have remaining to fill"); + } + }); +} + +/// V2 single whale intent — too large for one batch but partially fillable. +/// +/// One intent sells 5,000,000 HDX → BNC with a tight limit close to spot. +/// The full amount would cause too much slippage, but the solver should +/// find the maximum partial fill that meets the minimum rate. +#[test] +fn solver_v2_single_partial_whale() { + TestNet::reset(); + + let dave: AccountId = DAVE.into(); + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + let whale_amount = 5_000_000 * hdx_unit; + // Tight limit: ~0.065 BNC/HDX (spot is ~0.068) + let whale_min_bnc = 325_000 * 1_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(dave.clone(), hdx, whale_amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(dave.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: whale_amount, + amount_out: whale_min_bnc, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have exactly 1 intent"); + println!( + "single whale intent: {} HDX → BNC, min_out={}", + whale_amount, whale_min_bnc + ); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 3046956489, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1081219503238677977u128); + assert_eq!(s.amount_out, 70279270757470557u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + println!( + "solution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + assert_eq!(solution.resolved_intents.len(), 1, "Whale should be resolved"); + + let ri = &solution.resolved_intents[0]; + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap") + }; + + println!( + "fill: {} / {} ({:.1}%)", + s.amount_in, + whale_amount, + (s.amount_in as f64 / whale_amount as f64) * 100.0 + ); + println!("amount_out: {} BNC", s.amount_out); + + // Should be a partial fill — less than full amount + assert!(s.amount_in < whale_amount, "Should be partially filled, not full"); + assert!(s.amount_in > 0, "Should have some fill"); + + // The rate should meet the minimum + // min_rate = whale_min_bnc / whale_amount = 0.065 BNC/HDX + // actual_rate = amount_out / amount_in >= 0.065 + let pro_rata_min = s.amount_in as u128 * whale_min_bnc / whale_amount; + assert!( + s.amount_out >= pro_rata_min, + "Rate should meet minimum: got {} BNC for {} HDX, pro_rata_min={}", + s.amount_out, + s.amount_in, + pro_rata_min + ); + + println!( + "rate: {:.6} BNC/HDX (min: {:.6})", + s.amount_out as f64 / s.amount_in as f64, + whale_min_bnc as f64 / whale_amount as f64 + ); + + // Capture balances before execution + let dave_hdx_before = Currencies::total_balance(hdx, &dave); + let dave_bnc_before = Currencies::total_balance(bnc, &dave); + let fill_amount = s.amount_in; + let expected_bnc_out = s.amount_out; + + // Execute + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + println!("submit_solution: OK"); + + // Verify Dave's balances + let dave_hdx_after = Currencies::total_balance(hdx, &dave); + let dave_bnc_after = Currencies::total_balance(bnc, &dave); + assert_eq!(dave_hdx_after, 8918780496761322023u128); + assert_eq!(dave_bnc_after, 70265214903319063u128); + let hdx_spent = dave_hdx_before.saturating_sub(dave_hdx_after); + let bnc_received = dave_bnc_after.saturating_sub(dave_bnc_before); + + println!( + "dave HDX: {} → {} (spent {})", + dave_hdx_before, dave_hdx_after, hdx_spent + ); + println!( + "dave BNC: {} → {} (received {})", + dave_bnc_before, dave_bnc_after, bnc_received + ); + + assert_eq!( + hdx_spent, fill_amount, + "Dave should have spent exactly the fill amount of HDX" + ); + // BNC received = amount_out - protocol fee (0.02%) + let fee = hydradx_runtime::IceFee::get().mul_floor(expected_bnc_out); + let expected_payout = expected_bnc_out.saturating_sub(fee); + assert_eq!( + bnc_received, expected_payout, + "Dave should receive amount_out minus fee: expected {}, got {}", + expected_payout, bnc_received + ); + println!( + "fee: {} BNC ({:.4}%)", + fee, + fee as f64 / expected_bnc_out as f64 * 100.0 + ); + + // Verify intent still open + let intent_id = intents[0].0; + let stored = pallet_intent::Pallet::::get_intent(intent_id).expect("Intent should still exist"); + let ice_support::IntentData::Swap(ref stored_swap) = stored.data else { + panic!("expected Swap") + }; + println!( + "after fill 1: partial={:?}, remaining={}", + stored_swap.partial, + stored_swap.remaining() + ); + assert_eq!(stored_swap.amount_in, whale_amount, "Original immutable"); + assert!(stored_swap.remaining() > 0, "Should have remaining"); + let filled_after_1 = stored_swap.partial.filled(); + assert!(filled_after_1 > 0, "Should have some filled"); + + // --- Block 2: run solver again on the same (now partially filled) intent --- + println!("\n=== BLOCK 2 ==="); + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let intents2 = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents2.len(), 1, "Whale intent should still be valid"); + + let dave_hdx_before2 = Currencies::total_balance(hdx, &dave); + let dave_bnc_before2 = Currencies::total_balance(bnc, &dave); + + // Check spot rate after block 1 trade + { + use hydradx_traits::amm::AMMInterface; + let state2 = + ::Simulators::initial_state(); + let routes = TestSimulator::discover_routes(hdx, bnc, &state2).unwrap(); + let test_amount = 1_000_000_000_000u128; // 1 HDX + if let Some((_, out, _)) = + ::sell(hdx, bnc, test_amount, routes[0].clone(), &state2) + .ok() + .map(|(s, e)| (routes[0].clone(), e.amount_out, s)) + { + let rate = out as f64 / test_amount as f64; + println!( + "block 2 spot check: 1 HDX → {} BNC (rate: {:.6}, min: 0.065000)", + out, rate + ); + } + } + + let call2 = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ); + + // Block 1's trade moved the pool. Without external arbitrage (static snapshot), + // the spot rate may now be below the whale's minimum. In that case, the solver + // correctly produces no solution — the whale waits for conditions to improve. + if let Some(pallet_ice::Call::submit_solution { + solution: solution2, .. + }) = call2 + { + assert_eq!(solution2.resolved_intents.len(), 1); + let (fill2_amount_in, fill2_amount_out) = { + let ice_support::IntentData::Swap(ref s2) = solution2.resolved_intents[0].data else { + panic!("expected Swap"); + }; + (s2.amount_in, s2.amount_out) + }; + + println!( + "fill 2: {} ({:.1}% of remaining)", + fill2_amount_in, + (fill2_amount_in as f64 / stored_swap.remaining() as f64) * 100.0 + ); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution2, + )); + println!("submit_solution block 2: OK"); + + let stored2 = + pallet_intent::Pallet::::get_intent(intent_id).expect("Intent should still exist"); + let ice_support::IntentData::Swap(ref s2_stored) = stored2.data else { + panic!("expected Swap") + }; + println!( + "after fill 2: filled={}, remaining={}", + s2_stored.partial.filled(), + s2_stored.remaining() + ); + assert!( + s2_stored.partial.filled() > filled_after_1, + "Cumulative filled should increase" + ); + } else { + println!( + "block 2: no solution — pool rate below minimum after block 1 trade (expected in static snapshot)" + ); + // Intent should still be open, waiting for better conditions + let stored2 = pallet_intent::Pallet::::get_intent(intent_id) + .expect("Intent should still exist even without block 2 fill"); + assert_eq!(stored2.data.amount_in(), whale_amount, "Original immutable"); + } + println!("\ntest complete — partial fill across blocks verified"); + }); +} + +/// All intents are partial, same direction (HDX → BNC). +/// +/// Phase A (non-partial stabilization) is a no-op because `non_partial_fills` is empty. +/// Phase B binary-searches each partial intent individually. +/// Charlie has a tighter limit and should get a smaller fill percentage. +#[test] +fn solver_v2_all_partial_same_direction() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + // Spot: ~0.068 BNC/HDX + let alice_amount = 500_000 * hdx_unit; + let bob_amount = 300_000 * hdx_unit; + let charlie_amount = 200_000 * hdx_unit; + + // Loose limit: 0.050 BNC/HDX + let alice_min = 25_000 * 1_000_000_000_000u128; // 500k * 0.050 + let bob_min = 15_000 * 1_000_000_000_000u128; // 300k * 0.050 + // Tight limit: 0.066 BNC/HDX (close to spot ~0.068) + let charlie_min = 13_200 * 1_000_000_000_000u128; // 200k * 0.066 + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_amount * 2) + .endow_account(bob.clone(), hdx, bob_amount * 2) + .endow_account(charlie.clone(), hdx, charlie_amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Submit all 3 as partial intents + for (who, amount, min_out, label) in [ + (alice.clone(), alice_amount, alice_min, "alice"), + (bob.clone(), bob_amount, bob_min, "bob"), + (charlie.clone(), charlie_amount, charlie_min, "charlie"), + ] { + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(who), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: amount, + amount_out: min_out, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + println!("{}: submitted {} HDX → BNC (partial)", label, amount / hdx_unit); + } + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 3, "Should have 3 intents"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 3, "resolved count"); + assert_eq!(solution.score, 8006886170885890, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 125107596400000000u128); + assert_eq!(s.amount_out, 8257101362657178u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 187661394600000000u128); + assert_eq!(s.amount_out, 12385652043985767u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 312768991000000000u128); + assert_eq!(s.amount_out, 20642753406642945u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + println!( + "\nsolution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + // At least some partial intents should be resolved. + // The solver processes partial intents sequentially in Phase B; + // later ones may not find viable fills if earlier fills consumed + // too much AMM capacity. + assert!( + !solution.resolved_intents.is_empty(), + "At least one partial intent should be resolved" + ); + println!("resolved {} out of 3 intents", solution.resolved_intents.len()); + + // Track fills per account + let mut charlie_fill_pct = 0.0f64; + let mut alice_fill_pct = 0.0f64; + let mut bob_fill_pct = 0.0f64; + + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + assert!(s.partial.is_partial(), "All resolved intents should be partial"); + assert!(s.amount_in > 0, "Fill amount must be > 0"); + + // Verify rate constraint: amount_out >= fill_amount * original_min / original_amount_in + let original = intents.iter().find(|(id, _)| *id == ri.id).expect("intent must exist"); + let original_amount_in = original.1.data.amount_in(); + let original_amount_out = original.1.data.amount_out(); + let pro_rata_min = (sp_core::U256::from(s.amount_in) * sp_core::U256::from(original_amount_out) + / sp_core::U256::from(original_amount_in)) + .as_u128(); + assert!( + s.amount_out >= pro_rata_min, + "Rate constraint violated: got {} out for {} in, pro_rata_min={}", + s.amount_out, + s.amount_in, + pro_rata_min + ); + + let pct = s.amount_in as f64 / original_amount_in as f64 * 100.0; + println!( + "resolved id={}: fill {} / {} ({:.1}%), amount_out={}", + ri.id, s.amount_in, original_amount_in, pct, s.amount_out + ); + + if original_amount_in == alice_amount { + alice_fill_pct = pct; + } else if original_amount_in == bob_amount { + bob_fill_pct = pct; + } else if original_amount_in == charlie_amount { + charlie_fill_pct = pct; + } + } + + // Charlie (tight limit) should generally get a smaller fill % than Alice/Bob (loose limit) + // because the binary search is more constrained. + // Note: the solver processes partial intents sequentially, so this relationship + // depends on processing order. Log it for debugging. + println!( + "\nfill percentages: alice={:.1}%, bob={:.1}%, charlie={:.1}%", + alice_fill_pct, bob_fill_pct, charlie_fill_pct + ); + + assert!(solution.score > 0, "Score should be positive"); + + // Execute solution + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + println!("submit_solution: OK"); + + // Verify ED guard invariant: each intent still in storage must have remaining >= ED + let hdx_ed = AssetRegistry::existential_deposit(hdx).unwrap_or(hdx_unit); + for (id, intent) in pallet_intent::Pallet::::get_valid_intents() { + let ice_support::IntentData::Swap(ref s) = intent.data else { + continue; + }; + let remaining = s.remaining(); + println!("intent {}: remaining={}", id, remaining); + assert!( + remaining == 0 || remaining >= hdx_ed, + "ED guard violated: intent {} has remaining={} < ED={}", + id, + remaining, + hdx_ed + ); + } + }); +} + +/// A small partial intent should be fully filled and removed from storage, +/// behaving identically to a non-partial intent. +#[test] +fn solver_v2_small_partial_fully_filled() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + // Tiny amount: 1,000 HDX. No slippage concern. + let amount = 1_000 * hdx_unit; + // Loose limit: 0.050 BNC/HDX → min 50 BNC + let min_out = 50 * 1_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, amount * 10) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: amount, + amount_out: min_out, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1); + let intent_id = intents[0].0; + + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 17436998989586, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1000000000000000u128); + assert_eq!(s.amount_out, 67436998989586u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + assert_eq!(solution.resolved_intents.len(), 1); + let ri = &solution.resolved_intents[0]; + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + + // Small partial should be FULLY filled + assert_eq!(s.amount_in, amount, "Small partial intent should be fully filled"); + assert!(s.amount_out >= min_out, "Rate constraint must be met"); + + println!( + "fully filled: {} HDX → {} BNC (rate: {:.6})", + s.amount_in as f64 / hdx_unit as f64, + s.amount_out as f64 / 1_000_000_000_000f64, + s.amount_out as f64 / s.amount_in as f64 + ); + + let expected_bnc_out = s.amount_out; + + // Execute + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + // Intent should be REMOVED from storage (fully filled partial) + assert!( + pallet_intent::Pallet::::get_intent(intent_id).is_none(), + "Fully filled partial intent should be removed from storage" + ); + + // Verify balances + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + assert_eq!(alice_hdx_after, 9000000000000000u128); + assert_eq!(alice_bnc_after, 67423511589789u128); + let hdx_spent = alice_hdx_before.saturating_sub(alice_hdx_after); + let bnc_received = alice_bnc_after.saturating_sub(alice_bnc_before); + + assert_eq!(hdx_spent, amount, "Alice should spend exactly the fill amount"); + let fee = hydradx_runtime::IceFee::get().mul_floor(expected_bnc_out); + assert_eq!( + bnc_received, + expected_bnc_out.saturating_sub(fee), + "Alice should receive amount_out minus fee" + ); + println!("submit_solution: OK — intent fully resolved and removed"); + }); +} + +/// Mixed: small partial intent alongside non-partial intents. +/// All are small enough to be fully filled. +#[test] +fn solver_v2_mixed_small_partial_and_non_partial() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + let amount_ab = 10_000 * hdx_unit; + let amount_c = 5_000 * hdx_unit; + // Loose limit: 0.050 BNC/HDX + let min_ab = 500 * 1_000_000_000_000u128; // 10k * 0.050 + let min_c = 250 * 1_000_000_000_000u128; // 5k * 0.050 + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, amount_ab * 10) + .endow_account(bob.clone(), hdx, amount_ab * 10) + .endow_account(charlie.clone(), hdx, amount_c * 10) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Alice and Bob: non-partial + for (who, label) in [(alice.clone(), "alice"), (bob.clone(), "bob")] { + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(who), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: amount_ab, + amount_out: min_ab, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + println!("{}: {} HDX → BNC (non-partial)", label, amount_ab / hdx_unit); + } + + // Charlie: partial + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(charlie.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: amount_c, + amount_out: min_c, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + println!("charlie: {} HDX → BNC (partial)", amount_c / hdx_unit); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 3); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 3, "resolved count"); + assert_eq!(solution.score, 434354215996077, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 673741686398431u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 673741686398431u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 5000000000000000u128); + assert_eq!(s.amount_out, 336870843199215u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + assert_eq!(solution.resolved_intents.len(), 3, "All 3 intents should be resolved"); + + // Verify all are fully filled + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + let original = intents.iter().find(|(id, _)| *id == ri.id).expect("intent"); + assert_eq!( + s.amount_in, + original.1.data.amount_in(), + "Intent {} should be fully filled", + ri.id + ); + println!( + "id={}: fill={} (full), amount_out={}, partial={:?}", + ri.id, s.amount_in, s.amount_out, s.partial + ); + } + + // Execute + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + // All intents should be removed from storage (fully resolved) + let remaining = pallet_intent::Pallet::::get_valid_intents(); + assert!( + remaining.is_empty(), + "All intents should be removed after full resolution, but {} remain", + remaining.len() + ); + println!("submit_solution: OK — all 3 intents fully resolved and removed"); + }); +} + +/// Two partial intents in opposing directions (HDX→BNC and BNC→HDX). +/// Both are partial, Phase A is a no-op. +/// Direct matching between partials should give better rates than AMM-only. +#[test] +fn solver_v2_all_partial_opposing_directions() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // Alice: sell 500k HDX for BNC. Loose limit. + let alice_amount = 500_000 * hdx_unit; + let alice_min = 25_000 * bnc_unit; // 0.050 BNC/HDX + + // Bob: sell 20k BNC for HDX. Loose limit. + // At spot ~14.7 HDX/BNC, 20k BNC = ~294k HDX + let bob_amount = 20_000 * bnc_unit; + let bob_min = 200_000 * hdx_unit; // 10 HDX/BNC (well below spot ~14.7) + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_amount * 2) + .endow_account(bob.clone(), bnc, bob_amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Alice: HDX → BNC (partial) + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: alice_amount, + amount_out: alice_min, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + + // Bob: BNC → HDX (partial, opposing direction) + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(bob.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: bnc, + asset_out: hdx, + amount_in: bob_amount, + amount_out: bob_min, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + + println!( + "alice: {} HDX → BNC (partial), bob: {} BNC → HDX (partial)", + alice_amount / hdx_unit, + bob_amount / bnc_unit + ); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 104354787068996481, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000000u128); + assert_eq!(s.amount_out, 33681289309145788u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 20000000000000000u128); + assert_eq!(s.amount_out, 295673497759850693u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + println!( + "solution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + // Both should be resolved + assert!( + solution.resolved_intents.len() >= 2, + "Both opposing partial intents should be resolved" + ); + + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_bnc_before = Currencies::total_balance(bnc, &alice); + let bob_hdx_before = Currencies::total_balance(hdx, &bob); + let bob_bnc_before = Currencies::total_balance(bnc, &bob); + + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + let original = intents.iter().find(|(id, _)| *id == ri.id).expect("intent"); + let orig_in = original.1.data.amount_in(); + let orig_out = original.1.data.amount_out(); + let pro_rata_min = (sp_core::U256::from(s.amount_in) * sp_core::U256::from(orig_out) + / sp_core::U256::from(orig_in)) + .as_u128(); + + assert!(s.amount_in > 0, "Fill must be > 0"); + assert!( + s.amount_out >= pro_rata_min, + "Rate constraint violated for intent {}: out={} < pro_rata_min={}", + ri.id, + s.amount_out, + pro_rata_min + ); + + println!( + "id={}: {} {} → {} {}, fill {:.1}%", + ri.id, + s.amount_in, + s.asset_in, + s.amount_out, + s.asset_out, + s.amount_in as f64 / orig_in as f64 * 100.0 + ); + } + + // Execute + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + println!("submit_solution: OK"); + + // Verify balance changes + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_bnc_after = Currencies::total_balance(bnc, &alice); + let bob_hdx_after = Currencies::total_balance(hdx, &bob); + let bob_bnc_after = Currencies::total_balance(bnc, &bob); + assert_eq!(alice_hdx_after, 500000000000000000u128); + assert_eq!(alice_bnc_after, 33674553051283959u128); + assert_eq!(bob_hdx_after, 295614363060298723u128); + assert_eq!(bob_bnc_after, 20000000000000000u128); + + assert!(alice_hdx_after < alice_hdx_before, "Alice should have spent HDX"); + assert!(alice_bnc_after > alice_bnc_before, "Alice should have received BNC"); + assert!(bob_bnc_after < bob_bnc_before, "Bob should have spent BNC"); + assert!(bob_hdx_after > bob_hdx_before, "Bob should have received HDX"); + + println!( + "alice: HDX {} → {}, BNC {} → {}", + alice_hdx_before, alice_hdx_after, alice_bnc_before, alice_bnc_after + ); + println!( + "bob: BNC {} → {}, HDX {} → {}", + bob_bnc_before, bob_bnc_after, bob_hdx_before, bob_hdx_after + ); + + // ED guard: remaining intents should have remaining >= ED + let hdx_ed = AssetRegistry::existential_deposit(hdx).unwrap_or(hdx_unit); + let bnc_ed = AssetRegistry::existential_deposit(bnc).unwrap_or(bnc_unit); + for (id, intent) in pallet_intent::Pallet::::get_valid_intents() { + let ice_support::IntentData::Swap(ref s) = intent.data else { + continue; + }; + let ed = if s.asset_in == hdx { hdx_ed } else { bnc_ed }; + assert!( + s.remaining() >= ed, + "ED guard: intent {} has remaining={} < ED={}", + id, + s.remaining(), + ed + ); + } + }); +} + +/// Two large partial intents in the same direction competing for limited AMM capacity. +/// The pool can only absorb ~2-3M HDX total before slippage violates the tight rate. +#[test] +fn solver_v2_competing_partial_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + let amount = 3_000_000 * hdx_unit; + // Tight limit: ~0.066 BNC/HDX (spot ~0.068) + let min_out = 198_000 * 1_000_000_000_000u128; // 3M * 0.066 + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, amount * 2) + .endow_account(bob.clone(), hdx, amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + for (who, label) in [(alice.clone(), "alice"), (bob.clone(), "bob")] { + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(who), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: amount, + amount_out: min_out, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + println!("{}: {} HDX → BNC (partial, tight limit)", label, amount / hdx_unit); + } + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 4091488, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 312768990000000000u128); + assert_eq!(s.amount_out, 20642753342045744u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 312768990000000000u128); + assert_eq!(s.amount_out, 20642753342045744u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + println!( + "solution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + // At least one should be resolved. The solver processes partial intents + // sequentially — the second may not find a viable fill if the first + // consumed the AMM capacity at the tight rate. + assert!( + !solution.resolved_intents.is_empty(), + "At least one partial intent should be resolved" + ); + println!("resolved {} out of 2 intents", solution.resolved_intents.len()); + + let mut total_fill = 0u128; + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + assert!(s.amount_in > 0, "Fill must be > 0"); + + let pro_rata_min = (sp_core::U256::from(s.amount_in) * sp_core::U256::from(min_out) + / sp_core::U256::from(amount)) + .as_u128(); + assert!( + s.amount_out >= pro_rata_min, + "Rate constraint violated: out={} < min={}", + s.amount_out, + pro_rata_min + ); + + total_fill += s.amount_in; + println!( + "id={}: fill {} ({:.1}%), out={}", + ri.id, + s.amount_in, + s.amount_in as f64 / amount as f64 * 100.0, + s.amount_out + ); + } + + println!( + "total fill: {} HDX ({:.1}% of combined 6M)", + total_fill, + total_fill as f64 / (2.0 * amount as f64) * 100.0 + ); + assert!(solution.score > 0, "Score should be positive"); + + // Execute + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + println!("submit_solution: OK"); + + // Resolved intents that were partially filled should remain in storage. + // Intents not included in the solution also remain (unfilled). + let remaining_intents = pallet_intent::Pallet::::get_valid_intents(); + println!("{} intents remain in storage", remaining_intents.len()); + + let hdx_ed = AssetRegistry::existential_deposit(hdx).unwrap_or(hdx_unit); + for (id, intent) in &remaining_intents { + let ice_support::IntentData::Swap(ref s) = intent.data else { + continue; + }; + assert!(s.remaining() > 0, "Intent {} should have remaining", id); + assert!( + s.remaining() >= hdx_ed, + "ED guard: intent {} remaining={} < ED={}", + id, + s.remaining(), + hdx_ed + ); + println!( + "intent {}: filled={}, remaining={}", + id, + s.partial.filled(), + s.remaining() + ); + } + }); +} + +/// Non-partial intent + partial intent in opposing directions. +/// Phase A handles the non-partial, Phase B handles the partial. +#[test] +fn solver_v2_partial_with_non_partial_opposing() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // Alice: sell 100k HDX for BNC (non-partial, loose limit) + let alice_amount = 100_000 * hdx_unit; + let alice_min = 5_000 * bnc_unit; // 0.050 BNC/HDX + + // Bob: sell 500k BNC for HDX (partial, loose limit) + // At spot ~14.7 HDX/BNC, 500k BNC = ~7.35M HDX. Alice's 100k HDX is ~6.8k BNC. + // So Alice is the scarce side; most of Bob's volume goes through AMM. + let bob_amount = 500_000 * bnc_unit; + let bob_min = 5_000_000 * hdx_unit; // 10 HDX/BNC + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, alice_amount * 2) + .endow_account(bob.clone(), bnc, bob_amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Alice: non-partial + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: alice_amount, + amount_out: alice_min, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + + // Bob: partial + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(bob.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: bnc, + asset_out: hdx, + amount_in: bob_amount, + amount_out: bob_min, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + + println!( + "alice: {} HDX → BNC (non-partial), bob: {} BNC → HDX (partial)", + alice_amount / hdx_unit, + bob_amount / bnc_unit + ); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 2); + + // Find intent IDs + let alice_intent_id = intents + .iter() + .find(|(_, i)| { + let ice_support::IntentData::Swap(ref s) = i.data else { + return false; + }; + s.asset_in == hdx && !s.partial.is_partial() + }) + .map(|(id, _)| *id) + .expect("alice intent"); + + let bob_intent_id = intents + .iter() + .find(|(_, i)| { + let ice_support::IntentData::Swap(ref s) = i.data else { + return false; + }; + s.asset_in == bnc && s.partial.is_partial() + }) + .map(|(id, _)| *id) + .expect("bob intent"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 2, "resolved count"); + assert_eq!(solution.score, 1119235164848690393, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 100000000000000000u128); + assert_eq!(s.amount_out, 6764218014644052u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 500000000000000000u128); + assert_eq!(s.amount_out, 6117470946834046341u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + println!( + "solution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + // Both should be resolved + assert!(solution.resolved_intents.len() >= 2, "Both intents should be resolved"); + + // Alice should be fully resolved (non-partial) + let alice_resolved = solution + .resolved_intents + .iter() + .find(|ri| ri.id == alice_intent_id) + .expect("Alice should be in solution"); + let ice_support::IntentData::Swap(ref alice_swap) = alice_resolved.data else { + panic!("expected Swap"); + }; + assert_eq!( + alice_swap.amount_in, alice_amount, + "Alice (non-partial) should be fully filled" + ); + println!( + "alice: fully filled {} HDX → {} BNC", + alice_swap.amount_in, alice_swap.amount_out + ); + + // Bob should be resolved (possibly partially) + let bob_resolved = solution + .resolved_intents + .iter() + .find(|ri| ri.id == bob_intent_id) + .expect("Bob should be in solution"); + let ice_support::IntentData::Swap(ref bob_swap) = bob_resolved.data else { + panic!("expected Swap"); + }; + assert!(bob_swap.amount_in > 0, "Bob should have some fill"); + let bob_fill_amount = bob_swap.amount_in; + let bob_pro_rata = (sp_core::U256::from(bob_fill_amount) * sp_core::U256::from(bob_min) + / sp_core::U256::from(bob_amount)) + .as_u128(); + assert!(bob_swap.amount_out >= bob_pro_rata, "Bob rate constraint violated"); + println!( + "bob: fill {} / {} BNC ({:.1}%), out={} HDX", + bob_fill_amount, + bob_amount, + bob_fill_amount as f64 / bob_amount as f64 * 100.0, + bob_swap.amount_out + ); + + // Execute + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + println!("submit_solution: OK"); + + // Alice's intent should be removed (non-partial, fully resolved) + assert!( + pallet_intent::Pallet::::get_intent(alice_intent_id).is_none(), + "Alice's non-partial intent should be removed" + ); + + // Bob's intent should remain if partially filled + if bob_fill_amount < bob_amount { + let stored = pallet_intent::Pallet::::get_intent(bob_intent_id) + .expect("Bob's partial intent should remain"); + let ice_support::IntentData::Swap(ref s) = stored.data else { + panic!("expected Swap"); + }; + assert_eq!(s.partial.filled(), bob_fill_amount); + assert!(s.remaining() > 0); + let bnc_ed = AssetRegistry::existential_deposit(bnc).unwrap_or(bnc_unit); + assert!( + s.remaining() >= bnc_ed, + "ED guard: remaining={} < ED={}", + s.remaining(), + bnc_ed + ); + println!( + "bob intent remains: filled={}, remaining={}", + s.partial.filled(), + s.remaining() + ); + } else { + assert!( + pallet_intent::Pallet::::get_intent(bob_intent_id).is_none(), + "Bob's intent should be removed if fully filled" + ); + println!("bob intent fully resolved and removed"); + } + }); +} + +/// Cancel a partially filled intent. The unfilled portion should be returned to the user. +/// +/// Block 1: Dave submits a large partial intent, solver partially fills it. +/// Block 2: Dave cancels the remaining portion via `remove_intent`. +/// Verify: Dave gets back the unfilled HDX, keeps the BNC from the fill. +#[test] +fn solver_v2_cancel_after_partial_fill() { + TestNet::reset(); + + let dave: AccountId = DAVE.into(); + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + let total_amount = 5_000_000 * hdx_unit; + // Tight limit: ~0.065 BNC/HDX + let min_out = 325_000 * 1_000_000_000_000u128; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(dave.clone(), hdx, total_amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(dave.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: total_amount, + amount_out: min_out, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1); + let intent_id = intents[0].0; + + let dave_hdx_before = Currencies::total_balance(hdx, &dave); + let dave_bnc_before = Currencies::total_balance(bnc, &dave); + + // --- Block 1: Solver partially fills Dave --- + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 3046956489, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1081219503238677977u128); + assert_eq!(s.amount_out, 70279270757470557u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + assert_eq!(solution.resolved_intents.len(), 1); + let ice_support::IntentData::Swap(ref s) = solution.resolved_intents[0].data else { + panic!("expected Swap"); + }; + let fill_amount = s.amount_in; + let fill_bnc_out = s.amount_out; + assert!(fill_amount > 0, "Should have some fill"); + assert!(fill_amount < total_amount, "Should be partial fill, not full"); + + println!( + "fill: {} HDX ({:.1}%), out: {} BNC", + fill_amount, + fill_amount as f64 / total_amount as f64 * 100.0, + fill_bnc_out + ); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + // Verify partial fill state + let stored = pallet_intent::Pallet::::get_intent(intent_id) + .expect("Intent should still exist after partial fill"); + let ice_support::IntentData::Swap(ref stored_swap) = stored.data else { + panic!("expected Swap"); + }; + assert_eq!(stored_swap.partial.filled(), fill_amount); + let remaining = stored_swap.remaining(); + assert!(remaining > 0); + println!( + "after fill: filled={}, remaining={}", + stored_swap.partial.filled(), + remaining + ); + + let dave_hdx_after_fill = Currencies::total_balance(hdx, &dave); + let dave_bnc_after_fill = Currencies::total_balance(bnc, &dave); + + // Dave should have spent the fill amount of HDX and received BNC (minus fee) + let hdx_spent = dave_hdx_before.saturating_sub(dave_hdx_after_fill); + assert_eq!(hdx_spent, fill_amount, "HDX spent should equal fill amount"); + + let fee = hydradx_runtime::IceFee::get().mul_floor(fill_bnc_out); + let expected_bnc = fill_bnc_out.saturating_sub(fee); + let bnc_received = dave_bnc_after_fill.saturating_sub(dave_bnc_before); + assert_eq!(bnc_received, expected_bnc, "BNC received should match payout"); + + // --- Block 2: Dave cancels the remaining intent --- + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(hydradx_runtime::Intent::remove_intent( + RuntimeOrigin::signed(dave.clone()), + intent_id + )); + + // Intent should be gone + assert!( + pallet_intent::Pallet::::get_intent(intent_id).is_none(), + "Intent should be removed after cancellation" + ); + + // Dave should get back the remaining HDX (unreserved) + let dave_hdx_after_cancel = Currencies::total_balance(hdx, &dave); + let hdx_returned = dave_hdx_after_cancel.saturating_sub(dave_hdx_after_fill); + println!( + "after cancel: HDX returned={}, expected remaining={}", + hdx_returned, remaining + ); + // The remaining amount was locked via named reserve. Cancellation unreserves it, + // which increases free balance but total_balance stays the same (reserved → free). + // We check that the total balance didn't change after cancellation (just reserve → free). + assert_eq!( + dave_hdx_after_cancel, dave_hdx_after_fill, + "Total HDX balance should not change on cancel (just unreserves)" + ); + + // Verify account cleanup + assert_eq!( + pallet_intent::AccountIntents::::iter_prefix(&dave).count(), + 0, + "Account intent index should be cleaned up" + ); + assert_eq!( + pallet_intent::Pallet::::account_intent_count(&dave), + 0, + "Account intent count should be 0" + ); + + println!( + "\nfinal state: dave HDX={}, BNC={}", + dave_hdx_after_cancel, dave_bnc_after_fill + ); + println!("cancel after partial fill: OK"); + }); +} + +/// A large partial intent with an extremely loose limit gets fully filled. +/// The minimum rate is trivially met, so the solver fills the entire amount. +#[test] +fn solver_v2_partial_loose_limit_full_fill() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + + let amount = 1_000_000 * hdx_unit; + // Absurdly loose limit: 1 BNC for 1,000,000 HDX + let min_out = 1_000_000_000_000u128; // 1 BNC + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, amount * 2) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: bnc, + amount_in: amount, + amount_out: min_out, + partial: true, + }), + deadline, + on_resolved: None, + } + )); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1); + let intent_id = intents[0].0; + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 65176201724183457, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 1000000000000000000u128); + assert_eq!(s.amount_out, 65177201724183457u128); + assert_eq!(s.partial, ice_support::Partial::Yes(0u128)); + } + + assert_eq!(solution.resolved_intents.len(), 1); + let ri = &solution.resolved_intents[0]; + let ice_support::IntentData::Swap(ref s) = ri.data else { + panic!("expected Swap"); + }; + + // With such a loose limit, the full amount should be fillable + assert_eq!(s.amount_in, amount, "Loose-limit partial intent should be fully filled"); + + // amount_out should be WAY above the 1 BNC minimum + // At spot ~0.068, 1M HDX → ~68k BNC + assert!( + s.amount_out > min_out * 1000, + "Output should massively exceed minimum: {} vs {}", + s.amount_out, + min_out + ); + + // Score should be very large (surplus = amount_out - pro_rata_min ≈ amount_out - 1 BNC) + assert!( + solution.score > s.amount_out / 2, + "Score should be substantial: {} vs amount_out {}", + solution.score, + s.amount_out + ); + + println!( + "fully filled: {} HDX → {} BNC (min was {} BNC), score={}", + s.amount_in / hdx_unit, + s.amount_out / 1_000_000_000_000u128, + min_out / 1_000_000_000_000u128, + solution.score + ); + + // Execute + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + // Intent should be removed (fully filled) + assert!( + pallet_intent::Pallet::::get_intent(intent_id).is_none(), + "Fully filled partial intent should be removed from storage" + ); + println!("submit_solution: OK — loose-limit partial fully resolved and removed"); + }); +} + +/// Single intent: Alice sells HDX for Hydrated Tether (asset 1111, 18 decimals). +/// Tests that the solver can discover a route and execute a trade for an aToken asset. +#[test] +fn solver_v2_single_intent_hdx_to_hydrated_tether() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let hdx = 0u32; + let husdt = 1111u32; // Hydrated Tether — 18 decimals + let hdx_unit = 1_000_000_000_000u128; + let husdt_unit = 1_000_000_000_000_000_000u128; // 10^18 + + // Sell 10,000 HDX — modest amount to avoid slippage issues + let amount_in = 10_000 * hdx_unit; + // Very loose limit: 1 hUSDT (effectively no minimum) + let min_amount_out = 1 * husdt_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, amount_in * 10) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(alice.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: husdt, + amount_in, + amount_out: min_amount_out, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + println!( + "alice: submitted {} HDX → hUSDT (asset {})", + amount_in / hdx_unit, + husdt + ); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + let intent_id = intents[0].0; + + let alice_hdx_before = Currencies::total_balance(hdx, &alice); + let alice_husdt_before = Currencies::total_balance(husdt, &alice); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution for HDX → hUSDT"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 19124562085823431471, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 1111); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 20124562085823431471u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 1, "Should resolve exactly 1 intent"); + assert!(solution.score > 0, "Score should be positive"); + + let resolved = &solution.resolved_intents[0]; + let ice_support::IntentData::Swap(ref s) = resolved.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, hdx, "asset_in should be HDX"); + assert_eq!(s.asset_out, husdt, "asset_out should be hUSDT"); + assert_eq!(s.amount_in, amount_in, "amount_in should match"); + assert!(s.amount_out >= min_amount_out, "amount_out should meet minimum"); + + println!( + "resolved: {} HDX → {} hUSDT (rate: {:.6} hUSDT/HDX)", + s.amount_in as f64 / hdx_unit as f64, + s.amount_out as f64 / husdt_unit as f64, + s.amount_out as f64 / s.amount_in as f64 + ); + + // Log the route + for (i, t) in solution.trades.iter().enumerate() { + println!( + "trade[{}]: {} → {}, amount_in={}, amount_out={}, route={:?}", + i, + t.route.first().map(|r| r.asset_in).unwrap_or(0), + t.route.last().map(|r| r.asset_out).unwrap_or(0), + t.amount_in, + t.amount_out, + t.route + .iter() + .map(|r| format!("{}->{} ({:?})", r.asset_in, r.asset_out, r.pool)) + .collect::>() + ); + } + + let expected_out = s.amount_out; + + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + println!("submit_solution: OK"); + + // Verify intent removed + assert!( + pallet_intent::Pallet::::get_intent(intent_id).is_none(), + "Intent should be removed after resolution" + ); + + // Verify balances + let alice_hdx_after = Currencies::total_balance(hdx, &alice); + let alice_husdt_after = Currencies::total_balance(husdt, &alice); + assert_eq!(alice_hdx_after, 90000000000000000u128); + assert_eq!(alice_husdt_after, 20120537173406266785u128); + let hdx_spent = alice_hdx_before.saturating_sub(alice_hdx_after); + let husdt_received = alice_husdt_after.saturating_sub(alice_husdt_before); + + assert_eq!(hdx_spent, amount_in, "HDX spent should match"); + let fee = hydradx_runtime::IceFee::get().mul_floor(expected_out); + assert_eq!( + husdt_received, + expected_out.saturating_sub(fee), + "hUSDT received should equal amount_out minus fee" + ); + println!( + "alice: spent {} HDX, received {} hUSDT (fee: {} hUSDT)", + hdx_spent as f64 / hdx_unit as f64, + husdt_received as f64 / husdt_unit as f64, + fee as f64 / husdt_unit as f64 + ); + }); +} + +/// Four intents, all selling HDX but each buying a different Hydrated aToken. +/// +/// Alice: HDX → hUSDT (1111) +/// Bob: HDX → hUSDC (1112) +/// Charlie: HDX → hWBTC (1113) +/// Dave: HDX → hUSDT_old/hDOT (1110) +/// +/// Tests that the solver handles multiple aToken destinations in a single batch. +/// Each intent routes through different pools (Omnipool + Stableswap + Aave). +#[test] +fn solver_v2_four_intents_hdx_to_different_atokens() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + + let hdx = 0u32; + let hdx_unit = 1_000_000_000_000u128; + let atoken_unit = 1_000_000_000_000_000_000u128; // 18 decimals for all aTokens + + // Each user sells 10,000 HDX for a different aToken + let amount_in = 10_000 * hdx_unit; + // Very loose limit — 1 unit of each aToken + let min_out = 1 * atoken_unit; + + struct IntentSetup { + who: AccountId, + label: &'static str, + asset_out: u32, + asset_name: &'static str, + } + + let setups = [ + IntentSetup { + who: alice.clone(), + label: "alice", + asset_out: 1111, + asset_name: "hUSDT", + }, + IntentSetup { + who: bob.clone(), + label: "bob", + asset_out: 1112, + asset_name: "hUSDC", + }, + IntentSetup { + who: charlie.clone(), + label: "charlie", + asset_out: 1113, + asset_name: "hWBTC", + }, + IntentSetup { + who: dave.clone(), + label: "dave", + asset_out: 1110, + asset_name: "h1110", + }, + ]; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, amount_in * 10) + .endow_account(bob.clone(), hdx, amount_in * 10) + .endow_account(charlie.clone(), hdx, amount_in * 10) + .endow_account(dave.clone(), hdx, amount_in * 10) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Submit all 4 intents + for s in &setups { + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(s.who.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: hdx, + asset_out: s.asset_out, + amount_in, + amount_out: min_out, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + println!( + "{}: submitted {} HDX → {} (asset {})", + s.label, + amount_in / hdx_unit, + s.asset_name, + s.asset_out + ); + } + + let intents = pallet_intent::Pallet::::get_valid_intents(); + println!("\ntotal valid intents: {}", intents.len()); + assert_eq!(intents.len(), 4, "Should have 4 intents"); + + // Capture balances before + let balances_before: Vec<(AccountId, u32, u128, u128)> = setups + .iter() + .map(|s| { + let hdx_bal = Currencies::total_balance(hdx, &s.who); + let out_bal = Currencies::total_balance(s.asset_out, &s.who); + (s.who.clone(), s.asset_out, hdx_bal, out_bal) + }) + .collect(); + + // Run solver + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 4, "resolved count"); + assert_eq!(solution.score, 76475194787456964279, "score"); + assert_eq!(solution.trades.len(), 4, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 1110); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 20152561532188390027u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 1113); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 20098700243456728695u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 1112); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 20104234873408945429u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 1111); + assert_eq!(s.amount_in, 10000000000000000u128); + assert_eq!(s.amount_out, 20119698138402900128u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + println!( + "\nsolution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + // Log resolved intents + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + continue; + }; + let name = setups + .iter() + .find(|setup| setup.asset_out == s.asset_out) + .map(|setup| setup.asset_name) + .unwrap_or("?"); + println!( + " id={}: {} HDX → {} {} (rate: {:.6})", + ri.id, + s.amount_in as f64 / hdx_unit as f64, + s.amount_out as f64 / atoken_unit as f64, + name, + s.amount_out as f64 / s.amount_in as f64 + ); + } + + // Log trades with routes + for (i, t) in solution.trades.iter().enumerate() { + println!( + " trade[{}]: amount_in={}, amount_out={}, route={:?}", + i, + t.amount_in, + t.amount_out, + t.route + .iter() + .map(|r| format!("{}->{} ({:?})", r.asset_in, r.asset_out, r.pool)) + .collect::>() + ); + } + + // At least some intents should be resolved — some aToken assets might not + // have routes on this snapshot + assert!( + !solution.resolved_intents.is_empty(), + "At least one intent should be resolved" + ); + assert!(solution.score > 0, "Score should be positive"); + + // Execute solution + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + println!("\nsubmit_solution: OK"); + + // Verify balances for each resolved intent + let ice_fee = hydradx_runtime::IceFee::get(); + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + continue; + }; + let setup = setups.iter().find(|setup| setup.asset_out == s.asset_out); + let Some(setup) = setup else { continue }; + + let (_, _, hdx_before, out_before) = balances_before + .iter() + .find(|(who, asset, _, _)| who == &setup.who && *asset == s.asset_out) + .unwrap(); + + let hdx_after = Currencies::total_balance(hdx, &setup.who); + let out_after = Currencies::total_balance(s.asset_out, &setup.who); + assert_eq!(hdx_after, 90000000000000000u128, "{} hdx_after", setup.label); + let expected_out_after: u128 = match setup.asset_out { + 1110 => 20148531019881952349, + 1113 => 20094680503408037350, + 1112 => 20100214026434263640, + 1111 => 20115674198775219548, + other => panic!("unexpected asset_out {}", other), + }; + assert_eq!(out_after, expected_out_after, "{} out_after", setup.label); + + let hdx_spent = hdx_before.saturating_sub(hdx_after); + let out_received = out_after.saturating_sub(*out_before); + let fee = ice_fee.mul_floor(s.amount_out); + let expected_payout = s.amount_out.saturating_sub(fee); + + assert_eq!(hdx_spent, s.amount_in, "{}: HDX spent should match fill", setup.label); + assert_eq!( + out_received, expected_payout, + "{}: {} received should match amount_out minus fee", + setup.label, setup.asset_name + ); + + println!( + "{}: spent {} HDX, received {} {} (fee: {})", + setup.label, + hdx_spent as f64 / hdx_unit as f64, + out_received as f64 / atoken_unit as f64, + setup.asset_name, + fee as f64 / atoken_unit as f64 + ); + } + + // Resolved intents should be removed from storage + for ri in solution.resolved_intents.iter() { + assert!( + pallet_intent::Pallet::::get_intent(ri.id).is_none(), + "Resolved intent {} should be removed from storage", + ri.id + ); + } + }); +} + +/// Cross-aToken trades with direct matching opportunities. +/// +/// Prep: sell HDX to acquire aTokens for each user (can't mint aTokens directly). +/// - Alice gets hUSDT (1111) via HDX → 1111 +/// - Bob gets hUSDT (1111) via HDX → 1111 (extra prep so Bob also has 1111) +/// - Charlie gets hWBTC (1113) via HDX → 1113 +/// - Dave gets h1110 (1110) via HDX → 1110 +/// +/// Then submit cross-aToken intents: +/// - Alice: sells 1111 (hUSDT) → buys 1110 +/// - Bob: sells 1111 (hUSDT) → buys 1113 (hWBTC) +/// - Charlie: sells 1113 (hWBTC) → buys 1111 (hUSDT) +/// - Dave: sells 1110 → buys 1113 (hWBTC) +/// +/// Matching opportunities: +/// - Bob (1111→1113) and Charlie (1113→1111) are opposing — direct match possible +/// - Alice (1111→1110) and Dave (1110→1113) form a partial chain +#[test] +fn solver_v2_cross_atoken_trades_with_matching() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + + let hdx = 0u32; + let hdx_unit = 1_000_000_000_000u128; + let atoken_unit = 1_000_000_000_000_000_000u128; // 18 decimals + + // Assets + let husdt = 1111u32; + let husdc = 1112u32; + let hwbtc = 1113u32; + let h1110 = 1110u32; + let _ = husdc; // not used in this test + + // HDX amount for prep trades — enough to get meaningful aToken balances + let prep_hdx = 10_000 * hdx_unit; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, prep_hdx * 10) + .endow_account(bob.clone(), hdx, prep_hdx * 10) + .endow_account(charlie.clone(), hdx, prep_hdx * 10) + .endow_account(dave.clone(), hdx, prep_hdx * 10) + .execute(|| { + enable_slip_fees(); + + // ============================================================ + // PREP: acquire aTokens by selling HDX through the router + // ============================================================ + println!("=== PREP: acquiring aTokens via HDX trades ===\n"); + + let prep_trades: Vec<(AccountId, &str, u32, &str)> = vec![ + (alice.clone(), "alice", husdt, "hUSDT"), + (bob.clone(), "bob", husdt, "hUSDT"), // Bob also gets hUSDT + (charlie.clone(), "charlie", hwbtc, "hWBTC"), + (dave.clone(), "dave", h1110, "h1110"), + ]; + + for (who, label, asset_out, name) in &prep_trades { + let route = Router::get_route(hydradx_traits::router::AssetPair::new(hdx, *asset_out)); + assert!( + !route.is_empty(), + "No route found for HDX → {} (asset {})", + name, + asset_out + ); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + + let bal_before = Currencies::free_balance(*asset_out, who); + assert_ok!(pallet_route_executor::Pallet::::sell( + RuntimeOrigin::signed(who.clone()), + hdx, + *asset_out, + prep_hdx, + 0, // no min — just acquiring tokens + route.clone(), + )); + let bal_after = Currencies::free_balance(*asset_out, who); + let received = bal_after.saturating_sub(bal_before); + println!( + "{}: sold {} HDX → {} {} (route: {} hops)", + label, + prep_hdx / hdx_unit, + received as f64 / atoken_unit as f64, + name, + route.len() + ); + assert!(received > 0, "{} should have received some {}", label, name); + } + + // Log balances after prep + println!("\n=== Balances after prep ==="); + println!( + "alice hUSDT: {}", + Currencies::free_balance(husdt, &alice) as f64 / atoken_unit as f64 + ); + println!( + "bob hUSDT: {}", + Currencies::free_balance(husdt, &bob) as f64 / atoken_unit as f64 + ); + println!( + "charlie hWBTC: {}", + Currencies::free_balance(hwbtc, &charlie) as f64 / atoken_unit as f64 + ); + println!( + "dave h1110: {}", + Currencies::free_balance(h1110, &dave) as f64 / atoken_unit as f64 + ); + + // ============================================================ + // INTENTS: cross-aToken trades + // ============================================================ + println!("\n=== Submitting cross-aToken intents ===\n"); + + crate::polkadot_test_net::hydradx_run_to_next_block(); + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Use half of each user's aToken balance as the sell amount + let alice_sell = Currencies::free_balance(husdt, &alice) / 2; + let bob_sell = Currencies::free_balance(husdt, &bob) / 2; + let charlie_sell = Currencies::free_balance(hwbtc, &charlie) / 2; + let dave_sell = Currencies::free_balance(h1110, &dave) / 2; + + struct CrossIntent { + who: AccountId, + label: &'static str, + asset_in: u32, + asset_in_name: &'static str, + asset_out: u32, + asset_out_name: &'static str, + amount_in: u128, + } + + let cross_intents = [ + CrossIntent { + who: alice.clone(), + label: "alice", + asset_in: husdt, + asset_in_name: "hUSDT", + asset_out: h1110, + asset_out_name: "h1110", + amount_in: alice_sell, + }, + CrossIntent { + who: bob.clone(), + label: "bob", + asset_in: husdt, + asset_in_name: "hUSDT", + asset_out: hwbtc, + asset_out_name: "hWBTC", + amount_in: bob_sell, + }, + CrossIntent { + who: charlie.clone(), + label: "charlie", + asset_in: hwbtc, + asset_in_name: "hWBTC", + asset_out: husdt, + asset_out_name: "hUSDT", + amount_in: charlie_sell, + }, + CrossIntent { + who: dave.clone(), + label: "dave", + asset_in: h1110, + asset_in_name: "h1110", + asset_out: hwbtc, + asset_out_name: "hWBTC", + amount_in: dave_sell, + }, + ]; + + for ci in &cross_intents { + // Very loose limit: 1 unit of output token + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(ci.who.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: ci.asset_in, + asset_out: ci.asset_out, + amount_in: ci.amount_in, + amount_out: 1 * atoken_unit, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + println!( + "{}: sells {} {} → {} (amount: {})", + ci.label, + ci.asset_in_name, + ci.asset_in, + ci.asset_out_name, + ci.amount_in as f64 / atoken_unit as f64 + ); + } + + let intents = pallet_intent::Pallet::::get_valid_intents(); + println!("\ntotal valid intents: {}", intents.len()); + assert_eq!(intents.len(), 4, "Should have 4 intents"); + + // Capture output balances before solve (we verify the user received the right amount) + let out_bals_before: Vec<(AccountId, u32, u128)> = cross_intents + .iter() + .map(|ci| { + ( + ci.who.clone(), + ci.asset_out, + Currencies::free_balance(ci.asset_out, &ci.who), + ) + }) + .collect(); + + // ============================================================ + // SOLVE + // ============================================================ + println!("\n=== Running solver ===\n"); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 4, "resolved count"); + assert_eq!(solution.score, 36209306650022490967, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 1110); + assert_eq!(s.asset_out, 1113); + assert_eq!(s.amount_in, 10074432980274158268u128); + assert_eq!(s.amount_out, 10052728889469772395u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 1113); + assert_eq!(s.asset_out, 1111); + assert_eq!(s.amount_in, 10052728889469772396u128); + assert_eq!(s.amount_out, 10052728889469772619u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 1111); + assert_eq!(s.asset_out, 1113); + assert_eq!(s.amount_in, 10061330244167713258u128); + assert_eq!(s.amount_out, 10051119981613172761u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 1111); + assert_eq!(s.asset_out, 1110); + assert_eq!(s.amount_in, 10062281042911715735u128); + assert_eq!(s.amount_out, 10052728889469773192u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + println!( + "solution: {} resolved, {} trades, score: {}", + solution.resolved_intents.len(), + solution.trades.len(), + solution.score + ); + + // Log resolved intents + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + continue; + }; + let ci = cross_intents + .iter() + .find(|ci| ci.asset_in == s.asset_in && ci.asset_out == s.asset_out); + let label = ci.map(|c| c.label).unwrap_or("?"); + println!( + " {} (id={}): {} {} → {} {} (rate: {:.6})", + label, + ri.id, + s.amount_in as f64 / atoken_unit as f64, + s.asset_in, + s.amount_out as f64 / atoken_unit as f64, + s.asset_out, + s.amount_out as f64 / s.amount_in as f64 + ); + } + + // Log trades + for (i, t) in solution.trades.iter().enumerate() { + println!( + " trade[{}]: amount_in={}, amount_out={}, route={:?}", + i, + t.amount_in, + t.amount_out, + t.route + .iter() + .map(|r| format!("{}->{} ({:?})", r.asset_in, r.asset_out, r.pool)) + .collect::>() + ); + } + + // Check for matching: if Bob(1111→1113) and Charlie(1113→1111) matched, + // the number of AMM trades should be fewer than 4 + if solution.trades.len() < 4 { + println!( + "\n→ Direct matching detected! {} AMM trades instead of 4", + solution.trades.len() + ); + } else { + println!("\n→ No direct matching (4 AMM trades)"); + } + + assert!( + solution.resolved_intents.len() == 4, + "All 4 intents should be resolved, got {}", + solution.resolved_intents.len() + ); + assert!(solution.score > 0, "Score should be positive"); + + // ============================================================ + // EXECUTE + // ============================================================ + crate::polkadot_test_net::hydradx_run_to_next_block(); + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution.clone(), + )); + println!("\nsubmit_solution: OK"); + + // ============================================================ + // VERIFY + // ============================================================ + let ice_fee = hydradx_runtime::IceFee::get(); + + for ri in solution.resolved_intents.iter() { + let ice_support::IntentData::Swap(ref s) = ri.data else { + continue; + }; + + // Match resolved intent to our setup by asset_in + asset_out + let idx = cross_intents + .iter() + .position(|ci| ci.asset_in == s.asset_in && ci.asset_out == s.asset_out) + .expect("Resolved intent should match a submitted intent"); + let ci = &cross_intents[idx]; + let (_, _, out_before) = &out_bals_before[idx]; + + let out_after = Currencies::free_balance(s.asset_out, &ci.who); + let expected_out_after: u128 = match (s.asset_in, s.asset_out) { + (1110, 1113) => 10050718343691878441, + (1113, 1111) => 10050718343691878665, + (1111, 1113) => 10049109757616850127, + (1111, 1110) => 10050718343691879238, + other => panic!("unexpected (asset_in, asset_out) = {:?}", other), + }; + assert_eq!(out_after, expected_out_after, "{} out_after", ci.label); + let received = out_after.saturating_sub(*out_before); + let fee = ice_fee.mul_floor(s.amount_out); + let expected_payout = s.amount_out.saturating_sub(fee); + + assert_eq!( + received, expected_payout, + "{}: received {} should match amount_out minus fee (expected {})", + ci.label, received, expected_payout + ); + + println!( + "{}: received {} {} (fee: {})", + ci.label, + received as f64 / atoken_unit as f64, + ci.asset_out_name, + fee as f64 / atoken_unit as f64 + ); + } + + // All intents should be removed + for ri in solution.resolved_intents.iter() { + assert!( + pallet_intent::Pallet::::get_intent(ri.id).is_none(), + "Intent {} should be removed after resolution", + ri.id + ); + } + + println!("\ncross-aToken trades with matching: OK"); + }); +} + +/// Same setup as DCA's `dca_succeeds_after_extra_gas_increased_due_to_out_of_gas_error`: +/// deploy a ConditionalGasEater ERC20 contract, add it to the omnipool, then try to +/// sell it for HDX via an ICE intent instead of DCA. +/// +/// The DCA test fails with out-of-gas on the first trade attempt. This test checks +/// whether the same trade via ICE/intent also hits out-of-gas or not. +#[test] +fn ice_intent_with_evm_gas_eater_token() { + use crate::polkadot_test_net::{hydradx_run_to_block, DAI, HDX, LRNA, UNITS}; + use frame_system::RawOrigin; + use hydradx_runtime::{Balances, EVMAccounts, EmaOracle, MultiTransactionPayment, Tokens, Treasury}; + use hydradx_traits::evm::InspectEvmAccounts; + use primitives::constants::chain::OMNIPOOL_SOURCE; + use sp_runtime::FixedU128; + use xcm_emulator::TestExt; + + TestNet::reset(); + Hydra::execute_with(|| { + // ============================================================ + // SETUP: same as the DCA out-of-gas test + // ============================================================ + crate::dca::init_omnipool_with_oracle_for_block_10(); + + let evm_address = EVMAccounts::evm_address(&Router::router_account()); + let contract = + crate::dca::extra_gas_erc20::deploy_conditional_gas_eater(evm_address, 400_000, crate::erc20::deployer()); + let erc20 = crate::erc20::bind_erc20(contract); + assert_ok!(EmaOracle::add_oracle( + RuntimeOrigin::root(), + OMNIPOOL_SOURCE, + (LRNA, erc20) + )); + + // Add new erc20 to omnipool + let bal = Currencies::free_balance(erc20, &ALICE.into()); + assert_ok!(Currencies::transfer( + RuntimeOrigin::signed(ALICE.into()), + pallet_omnipool::Pallet::::protocol_account(), + erc20, + bal / 10, + )); + assert_ok!(pallet_omnipool::Pallet::::add_token( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200), + Permill::from_percent(30), + ALICE.into(), + )); + + assert_ok!(MultiTransactionPayment::add_currency( + RuntimeOrigin::root(), + erc20, + FixedU128::from_rational(1, 200) + )); + + hydradx_run_to_block(11); + + println!("setup complete: erc20 asset id = {}", erc20); + println!( + "alice erc20 balance: {}", + Currencies::free_balance(erc20, &ALICE.into()) + ); + + // ============================================================ + // INTENT: sell the gas-eater ERC20 for HDX (same trade as DCA) + // ============================================================ + let sell_amount = 200_000 * UNITS; + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(ts + 120_000); // 120s deadline + + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(ALICE.into()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in: erc20, + asset_out: HDX, + amount_in: sell_amount, + amount_out: UNITS, // 1 HDX minimum (must be >= ED) + partial: false, + }), + deadline, + on_resolved: None, + } + )); + println!("submitted intent: sell {} erc20 (gas-eater) → HDX", sell_amount); + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!(intents.len(), 1, "Should have 1 intent"); + + // ============================================================ + // RUN SOLVER + // ============================================================ + let alice_hdx_before = Currencies::free_balance(HDX, &ALICE.into()); + let alice_erc20_before = Currencies::free_balance(erc20, &ALICE.into()); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver must produce a solution for the gas-eater ERC20 token"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 1, "resolved count"); + assert_eq!(solution.score, 1993001695769573, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 0); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 1000002); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 200000000000000000u128); + assert_eq!(s.amount_out, 1994001695769573u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + assert_eq!(solution.resolved_intents.len(), 1, "Should resolve exactly 1 intent"); + assert!(solution.score > 0, "Score should be positive"); + + let resolved = &solution.resolved_intents[0]; + let ice_support::IntentData::Swap(ref s) = resolved.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, erc20, "asset_in should be the gas-eater ERC20"); + assert_eq!(s.asset_out, HDX, "asset_out should be HDX"); + assert_eq!(s.amount_in, sell_amount, "amount_in should match"); + assert!(s.amount_out >= UNITS, "amount_out should meet minimum (1 HDX)"); + + let expected_hdx_out = s.amount_out; + + // ============================================================ + // EXECUTE + // ============================================================ + hydradx_run_to_block(12); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + + // Verify intent removed + assert!( + pallet_intent::Pallet::::get_intent(intents[0].0).is_none(), + "Intent should be removed after resolution" + ); + + // Verify Alice received correct HDX amount (minus fee) + let alice_hdx_after = Currencies::free_balance(HDX, &ALICE.into()); + assert_eq!(alice_hdx_after, 2993602895430420u128); + let hdx_received = alice_hdx_after.saturating_sub(alice_hdx_before); + let fee = hydradx_runtime::IceFee::get().mul_floor(expected_hdx_out); + let expected_payout = expected_hdx_out.saturating_sub(fee); + + assert_eq!( + hdx_received, expected_payout, + "Alice should receive amount_out minus fee: expected {}, got {}", + expected_payout, hdx_received + ); + assert!(hdx_received > 0, "Alice must receive some HDX"); + + println!( + "alice: received {} HDX (fee: {} HDX)", + hdx_received as f64 / UNITS as f64, + fee as f64 / UNITS as f64 + ); + }); +} + +/// Verify the solver caps resolved intents at MAX_NUMBER_OF_RESOLVED_INTENTS. +/// +/// When valid intents exceed the limit, the solver must truncate *before* +/// computing the score so the submitted solution is consistent. Without the +/// cap the score would reflect all intents but the BoundedVec would silently +/// drop the overflow, causing a score/solution mismatch. +#[test] +fn solver_caps_at_max_resolved_intents() { + TestNet::reset(); + + let alice: AccountId = ALICE.into(); + let bob: AccountId = BOB.into(); + let charlie: AccountId = CHARLIE.into(); + let dave: AccountId = DAVE.into(); + let eve: AccountId = EVE.into(); + + let hdx = 0u32; + let bnc = 14u32; + let hdx_unit = 1_000_000_000_000u128; + let bnc_unit = 1_000_000_000_000u128; + + // We need more intents than the cap. Each account can hold up to 100. + // Submit ~25 per account across 5 accounts = 125 total (> 100 cap). + let intents_per_account = 25u32; + let total_intents = intents_per_account * 5; + assert!( + total_intents > MAX_NUMBER_OF_RESOLVED_INTENTS, + "test must submit more than MAX_NUMBER_OF_RESOLVED_INTENTS intents" + ); + + let sell_hdx_amount = 500 * hdx_unit; + let sell_bnc_amount = 30 * bnc_unit; + // Loose limits so all intents are satisfiable + let min_bnc = bnc_unit; + let min_hdx = hdx_unit; + + let accounts = [alice.clone(), bob.clone(), charlie.clone(), dave.clone(), eve.clone()]; + + crate::driver::HydrationTestDriver::with_snapshot(PATH_TO_SNAPSHOT) + .endow_account(alice.clone(), hdx, sell_hdx_amount * intents_per_account as u128) + .endow_account(alice.clone(), bnc, sell_bnc_amount * intents_per_account as u128) + .endow_account(bob.clone(), hdx, sell_hdx_amount * intents_per_account as u128) + .endow_account(bob.clone(), bnc, sell_bnc_amount * intents_per_account as u128) + .endow_account(charlie.clone(), hdx, sell_hdx_amount * intents_per_account as u128) + .endow_account(charlie.clone(), bnc, sell_bnc_amount * intents_per_account as u128) + .endow_account(dave.clone(), hdx, sell_hdx_amount * intents_per_account as u128) + .endow_account(dave.clone(), bnc, sell_bnc_amount * intents_per_account as u128) + .endow_account(eve.clone(), hdx, sell_hdx_amount * intents_per_account as u128) + .endow_account(eve.clone(), bnc, sell_bnc_amount * intents_per_account as u128) + .execute(|| { + enable_slip_fees(); + + let ts = hydradx_runtime::Timestamp::now(); + let deadline = Some(primitives::constants::time::MILLISECS_PER_BLOCK * 10u64 + ts); + + // Submit intents: alternate direction per account to create matching flow + for (i, acc) in accounts.iter().enumerate() { + for j in 0..intents_per_account { + // Odd accounts sell BNC→HDX, even sell HDX→BNC; flip on odd j + let sell_hdx = (i % 2 == 0) ^ (j % 2 == 1); + let (asset_in, asset_out, amount_in, amount_out) = if sell_hdx { + (hdx, bnc, sell_hdx_amount, min_bnc) + } else { + (bnc, hdx, sell_bnc_amount, min_hdx) + }; + assert_ok!(hydradx_runtime::Intent::submit_intent( + RuntimeOrigin::signed(acc.clone()), + pallet_intent::types::IntentInput { + data: ice_support::IntentDataInput::Swap(ice_support::SwapParams { + asset_in, + asset_out, + amount_in, + amount_out, + partial: false, + }), + deadline, + on_resolved: None, + } + )); + } + } + + let intents = pallet_intent::Pallet::::get_valid_intents(); + assert_eq!( + intents.len(), + total_intents as usize, + "Should have submitted {} intents", + total_intents + ); + + let call = pallet_ice::Pallet::::run( + hydradx_runtime::System::block_number(), + |intents: Vec, state: CombinedSimulatorState| Solver::solve(intents, state).ok(), + ) + .expect("Solver should produce a solution"); + + let pallet_ice::Call::submit_solution { solution, .. } = call else { + panic!("Expected submit_solution call"); + }; + assert_eq!(solution.resolved_intents.len(), 100, "resolved count"); + assert_eq!(solution.score, 28654009568085706, "score"); + assert_eq!(solution.trades.len(), 1, "trades count"); + { + let r = &solution.resolved_intents[0]; + assert_eq!(r.id, 32752052247409382067756072960001); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[1]; + assert_eq!(r.id, 32752052247409382067756072960003); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[2]; + assert_eq!(r.id, 32752052247409382067756072960005); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[3]; + assert_eq!(r.id, 32752052247409382067756072960007); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[4]; + assert_eq!(r.id, 32752052247409382067756072960009); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[5]; + assert_eq!(r.id, 32752052247409382067756072960011); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[6]; + assert_eq!(r.id, 32752052247409382067756072960013); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[7]; + assert_eq!(r.id, 32752052247409382067756072960015); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[8]; + assert_eq!(r.id, 32752052247409382067756072960017); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[9]; + assert_eq!(r.id, 32752052247409382067756072960019); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[10]; + assert_eq!(r.id, 32752052247409382067756072960021); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[11]; + assert_eq!(r.id, 32752052247409382067756072960023); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[12]; + assert_eq!(r.id, 32752052247409382067756072960025); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[13]; + assert_eq!(r.id, 32752052247409382067756072960027); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[14]; + assert_eq!(r.id, 32752052247409382067756072960029); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[15]; + assert_eq!(r.id, 32752052247409382067756072960031); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[16]; + assert_eq!(r.id, 32752052247409382067756072960033); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[17]; + assert_eq!(r.id, 32752052247409382067756072960035); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[18]; + assert_eq!(r.id, 32752052247409382067756072960037); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[19]; + assert_eq!(r.id, 32752052247409382067756072960039); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[20]; + assert_eq!(r.id, 32752052247409382067756072960041); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[21]; + assert_eq!(r.id, 32752052247409382067756072960043); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[22]; + assert_eq!(r.id, 32752052247409382067756072960045); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[23]; + assert_eq!(r.id, 32752052247409382067756072960047); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[24]; + assert_eq!(r.id, 32752052247409382067756072960049); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[25]; + assert_eq!(r.id, 32752052247409382067756072960051); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[26]; + assert_eq!(r.id, 32752052247409382067756072960053); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[27]; + assert_eq!(r.id, 32752052247409382067756072960055); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[28]; + assert_eq!(r.id, 32752052247409382067756072960057); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[29]; + assert_eq!(r.id, 32752052247409382067756072960059); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[30]; + assert_eq!(r.id, 32752052247409382067756072960061); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[31]; + assert_eq!(r.id, 32752052247409382067756072960063); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[32]; + assert_eq!(r.id, 32752052247409382067756072960065); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[33]; + assert_eq!(r.id, 32752052247409382067756072960067); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[34]; + assert_eq!(r.id, 32752052247409382067756072960069); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[35]; + assert_eq!(r.id, 32752052247409382067756072960071); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[36]; + assert_eq!(r.id, 32752052247409382067756072960073); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[37]; + assert_eq!(r.id, 32752052247409382067756072960075); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[38]; + assert_eq!(r.id, 32752052247409382067756072960077); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[39]; + assert_eq!(r.id, 32752052247409382067756072960079); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[40]; + assert_eq!(r.id, 32752052247409382067756072960081); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[41]; + assert_eq!(r.id, 32752052247409382067756072960083); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[42]; + assert_eq!(r.id, 32752052247409382067756072960085); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[43]; + assert_eq!(r.id, 32752052247409382067756072960087); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[44]; + assert_eq!(r.id, 32752052247409382067756072960089); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[45]; + assert_eq!(r.id, 32752052247409382067756072960091); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[46]; + assert_eq!(r.id, 32752052247409382067756072960093); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[47]; + assert_eq!(r.id, 32752052247409382067756072960095); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[48]; + assert_eq!(r.id, 32752052247409382067756072960097); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[49]; + assert_eq!(r.id, 32752052247409382067756072960099); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[50]; + assert_eq!(r.id, 32752052247409382067756072960101); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[51]; + assert_eq!(r.id, 32752052247409382067756072960103); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[52]; + assert_eq!(r.id, 32752052247409382067756072960105); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[53]; + assert_eq!(r.id, 32752052247409382067756072960107); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[54]; + assert_eq!(r.id, 32752052247409382067756072960109); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[55]; + assert_eq!(r.id, 32752052247409382067756072960111); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[56]; + assert_eq!(r.id, 32752052247409382067756072960113); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[57]; + assert_eq!(r.id, 32752052247409382067756072960115); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[58]; + assert_eq!(r.id, 32752052247409382067756072960117); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[59]; + assert_eq!(r.id, 32752052247409382067756072960119); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[60]; + assert_eq!(r.id, 32752052247409382067756072960121); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[61]; + assert_eq!(r.id, 32752052247409382067756072960123); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 14); + assert_eq!(s.asset_out, 0); + assert_eq!(s.amount_in, 30000000000000u128); + assert_eq!(s.amount_out, 443045292666183u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[62]; + assert_eq!(r.id, 32752052247409382067756072960000); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[63]; + assert_eq!(r.id, 32752052247409382067756072960002); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[64]; + assert_eq!(r.id, 32752052247409382067756072960004); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[65]; + assert_eq!(r.id, 32752052247409382067756072960006); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[66]; + assert_eq!(r.id, 32752052247409382067756072960008); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[67]; + assert_eq!(r.id, 32752052247409382067756072960010); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[68]; + assert_eq!(r.id, 32752052247409382067756072960012); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[69]; + assert_eq!(r.id, 32752052247409382067756072960014); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[70]; + assert_eq!(r.id, 32752052247409382067756072960016); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[71]; + assert_eq!(r.id, 32752052247409382067756072960018); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[72]; + assert_eq!(r.id, 32752052247409382067756072960020); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[73]; + assert_eq!(r.id, 32752052247409382067756072960022); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[74]; + assert_eq!(r.id, 32752052247409382067756072960024); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[75]; + assert_eq!(r.id, 32752052247409382067756072960026); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[76]; + assert_eq!(r.id, 32752052247409382067756072960028); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[77]; + assert_eq!(r.id, 32752052247409382067756072960030); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[78]; + assert_eq!(r.id, 32752052247409382067756072960032); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[79]; + assert_eq!(r.id, 32752052247409382067756072960034); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[80]; + assert_eq!(r.id, 32752052247409382067756072960036); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[81]; + assert_eq!(r.id, 32752052247409382067756072960038); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[82]; + assert_eq!(r.id, 32752052247409382067756072960040); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[83]; + assert_eq!(r.id, 32752052247409382067756072960042); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[84]; + assert_eq!(r.id, 32752052247409382067756072960044); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[85]; + assert_eq!(r.id, 32752052247409382067756072960046); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[86]; + assert_eq!(r.id, 32752052247409382067756072960048); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[87]; + assert_eq!(r.id, 32752052247409382067756072960050); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[88]; + assert_eq!(r.id, 32752052247409382067756072960052); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[89]; + assert_eq!(r.id, 32752052247409382067756072960054); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[90]; + assert_eq!(r.id, 32752052247409382067756072960056); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[91]; + assert_eq!(r.id, 32752052247409382067756072960058); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[92]; + assert_eq!(r.id, 32752052247409382067756072960060); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[93]; + assert_eq!(r.id, 32752052247409382067756072960062); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[94]; + assert_eq!(r.id, 32752052247409382067756072960064); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[95]; + assert_eq!(r.id, 32752052247409382067756072960066); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[96]; + assert_eq!(r.id, 32752052247409382067756072960068); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[97]; + assert_eq!(r.id, 32752052247409382067756072960070); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[98]; + assert_eq!(r.id, 32752052247409382067756072960072); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + { + let r = &solution.resolved_intents[99]; + assert_eq!(r.id, 32752052247409382067756072960074); + let ice_support::IntentData::Swap(ref s) = r.data else { + panic!("expected Swap"); + }; + assert_eq!(s.asset_in, 0); + assert_eq!(s.asset_out, 14); + assert_eq!(s.amount_in, 500000000000000u128); + assert_eq!(s.amount_out, 33821090073220u128); + assert_eq!(s.partial, ice_support::Partial::No); + } + + // The solver must cap at MAX_NUMBER_OF_RESOLVED_INTENTS + assert_eq!( + solution.resolved_intents.len(), + MAX_NUMBER_OF_RESOLVED_INTENTS as usize, + "Solver should cap resolved intents at MAX_NUMBER_OF_RESOLVED_INTENTS ({}), got {}", + MAX_NUMBER_OF_RESOLVED_INTENTS, + solution.resolved_intents.len() + ); + assert!(solution.score > 0, "Score should be positive"); + + // The solution must be submittable — this proves the score is consistent + // with the truncated set (the bug we're guarding against). + crate::polkadot_test_net::hydradx_run_to_next_block(); + + assert_ok!(pallet_ice::Pallet::::submit_solution( + RuntimeOrigin::none(), + solution, + )); + }); +} diff --git a/integration-tests/src/lib.rs b/integration-tests/src/lib.rs index b585590361..fdfb19920f 100644 --- a/integration-tests/src/lib.rs +++ b/integration-tests/src/lib.rs @@ -2,6 +2,7 @@ // DCA pallet uses dummy router for benchmarks and some tests fail when benchmarking feature is enabled #![cfg(not(feature = "runtime-benchmarks"))] mod aave_router; +mod aave_simulator; mod asset_registry; mod bonds; mod call_filter; @@ -22,6 +23,7 @@ mod exchange_asset; mod fee_calculation; mod global_withdraw_limit; mod hsm; +mod ice; mod insufficient_assets_ed; mod liquidation; mod multi_payment; diff --git a/integration-tests/src/polkadot_test_net.rs b/integration-tests/src/polkadot_test_net.rs index 6c6fa97946..8630c04046 100644 --- a/integration-tests/src/polkadot_test_net.rs +++ b/integration-tests/src/polkadot_test_net.rs @@ -68,7 +68,8 @@ pub const ALICE: [u8; 32] = [4u8; 32]; pub const BOB: [u8; 32] = [5u8; 32]; pub const CHARLIE: [u8; 32] = [6u8; 32]; pub const DAVE: [u8; 32] = [7u8; 32]; -pub const UNKNOWN: [u8; 32] = [8u8; 32]; +pub const EVE: [u8; 32] = [8u8; 32]; +pub const UNKNOWN: [u8; 32] = [9u8; 32]; // Private key: 42d8d953e4f9246093a33e9ca6daa078501012f784adfe4bbed57918ff13be14 // Address: 0x222222ff7Be76052e023Ec1a306fCca8F9659D80 @@ -849,6 +850,7 @@ pub fn go_to_block(number: BlockNumber) { hydradx_runtime::EVMAccounts::on_finalize(current_block); hydradx_runtime::Stableswap::on_finalize(current_block); hydradx_runtime::HSM::on_finalize(current_block); + hydradx_runtime::Omnipool::on_finalize(current_block); } // Set relay chain validation data BEFORE initializing the new block @@ -908,9 +910,11 @@ pub fn go_to_block(number: BlockNumber) { hydradx_runtime::EVMAccounts::on_initialize(number); hydradx_runtime::Stableswap::on_initialize(number); hydradx_runtime::HSM::on_initialize(number); + hydradx_runtime::Omnipool::on_initialize(number); } pub fn hydradx_run_to_next_block() { + pallet_aura::CurrentSlot::::kill(); let b = hydradx_runtime::System::block_number(); go_to_block(b + 1); } diff --git a/pallets/dynamic-fees/src/lib.rs b/pallets/dynamic-fees/src/lib.rs index 0290a81f3b..867f1e80d2 100644 --- a/pallets/dynamic-fees/src/lib.rs +++ b/pallets/dynamic-fees/src/lib.rs @@ -376,6 +376,7 @@ where let decay_factor = FixedU128::from_rational(4u128, period); log::trace!(target: "dynamic-fees", "decay factor: {decay_factor:?}"); + /* let fee_updated_at: u128 = current_fee_entry.timestamp.saturated_into(); if !fee_updated_at.is_zero() { debug_assert!( @@ -385,6 +386,7 @@ where raw_entry.updated_at() ); } + */ let asset_fee = recalculate_asset_fee( OracleEntry { diff --git a/pallets/ice/ARCHITECTURE.md b/pallets/ice/ARCHITECTURE.md new file mode 100644 index 0000000000..77dc9eb80e --- /dev/null +++ b/pallets/ice/ARCHITECTURE.md @@ -0,0 +1,157 @@ +# ICE Solver Architecture + +## Overview + +The ICE (Intent Componsing Engine) system enables intent-based trading on Hydration. Users submit trade intents, and an off-chain solver finds optimal execution paths, potentially matching intents directly to reduce AMM fees and slippage. + +## Core Components + +```mermaid +graph TB + subgraph "On-Chain" + Intent[Intent Pallet] + ICE[ICE Pallet] + Router[Router] + Omnipool[Omnipool] + Stableswap[Stableswap] + Other[...] + end + + subgraph "Off-Chain" + Solver[Solver] + Simulator[AMM Simulator] + end + + User -->|submit_intent| Intent + Intent -->|valid intents| ICE + ICE -->|snapshot| Simulator + Simulator -->|state| Solver + Solver -->|solution| ICE + ICE -->|execute trades| Router + Router --> Omnipool + Router --> Stableswap + Router --> Other +``` + +## Component Responsibilities + +| Component | Role | +|-----------|------| +| **Intent Pallet** | Stores user intents with deadlines and parameters | +| **ICE Pallet** | Orchestrates solving, validates and executes solutions | +| **Simulator** | Captures AMM state snapshots, simulates trades off-chain | +| **Solver** | Finds optimal intent resolution with matching algorithm | + +## Traits and Integration + +```mermaid +graph TB + subgraph "Solver Layer" + Solver[SolverV1] + end + + subgraph "Interface Layer" + AMM[AMMInterface] + end + + subgraph "Compositor Layer" + HS[HydrationSimulator] + SC[SimulatorConfig] + end + + subgraph "Simulator Layer" + SS[SimulatorSet] + end + + subgraph "AMM Simulators" + OmniSim[Omnipool::AmmSimulator] + StableSim[Stableswap::AmmSimulator] + end + + Solver -->|"sell/buy/spot_price"| AMM + AMM -.->|implements| HS + HS -->|uses| SC + SC -->|"type Simulators"| SS + SC -->|"type RouteProvider"| Router[Router] + SS -.->|"impl for (A,B)"| OmniSim + SS -.->|"impl for (A,B)"| StableSim +``` + +## Trait Hierarchy + +```mermaid +classDiagram + class AMMInterface { + +sell(asset_in, asset_out, amount, route, state) + +buy(asset_in, asset_out, amount, route, state) + +get_spot_price(asset_in, asset_out, state) + +price_denominator() + } + + class AmmSimulator { + +pool_type() + +matches_pool_type(pool_type) + +snapshot() + +simulate_sell(in, out, amount, min, snapshot) + +simulate_buy(in, out, amount, max, snapshot) + +get_spot_price(in, out, snapshot) + } + + class SimulatorSet { + +initial_state() + +simulate_sell(pool_type, ...) + +simulate_buy(pool_type, ...) + +get_spot_price(pool_type, ...) + } + + class SimulatorConfig { + +Simulators: SimulatorSet + +RouteProvider + +PriceDenominator + } + + AMMInterface <|.. HydrationSimulator : implements + HydrationSimulator --> SimulatorConfig : uses + SimulatorConfig --> SimulatorSet : type + SimulatorSet <|.. Tuple : impl for A,B + AmmSimulator <|.. Omnipool : implements + AmmSimulator <|.. Stableswap : implements +``` + +## Solver Algorithm (Matching) + +```mermaid +flowchart TD + A[Get spot prices for all assets] --> B[Filter satisfiable intents] + B --> C[Calculate net flows per asset] + C --> D{Net surplus/deficit?} + D -->|Surplus| E[Sell excess to AMM] + D -->|Deficit| F[Buy needed from AMM] + E --> G[Distribute at clearing price] + F --> G + G --> H[Return Solution] +``` + +**Matching Benefit:** +- Without matching: Each intent trades through AMM separately +- With matching: Matching intents settle directly, only net imbalance hits AMM +- Result: Lower fees, reduced slippage, better execution for all users + +## Solution Structure + +```rust +Solution { + resolved_intents: Vec, // What each user gets + trades: Vec, // AMM trades to execute + clearing_prices: Map, // Uniform prices used + score: u128, // Solution quality metric +} +``` + +## Key Design Decisions + +1. **Snapshot-based Simulation** - Capture chain state once, simulate multiple scenarios off-chain +2. **Tuple-based SimulatorSet** - Compose multiple AMM simulators with automatic type-safe dispatch +3. **Router Integration** - Use on-chain router for route discovery, simulator for execution +4. **HDX as Price Denominator** - All prices computed relative to HDX for intent matching +5. **Uniform Clearing Price** - All matched intents execute at same price for fairness diff --git a/pallets/ice/Cargo.toml b/pallets/ice/Cargo.toml new file mode 100644 index 0000000000..e0d3f5d14c --- /dev/null +++ b/pallets/ice/Cargo.toml @@ -0,0 +1,87 @@ +[package] +name = "pallet-ice" +version = "1.0.0" +authors = ['GalacticCouncil'] +edition = "2021" +license = "Apache-2.0" +homepage = 'https://github.com/galacticcouncil/hydration-node' +repository = 'https://github.com/galacticcouncil/hydration-node' +description = "" +readme = "README.md" + +[dependencies] +# parity +codec = { workspace = true, features = ["derive", "max-encoded-len"] } +scale-info = { workspace = true } +log = { workspace = true} + +# primitives +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-core = { workspace = true } +sp-runtime-interface = { workspace = true} +sp-externalities = { workspace = true} +num-traits = { workspace = true } + +# FRAME +frame-support = { workspace = true } +frame-system = { workspace = true } + +# Hydration dependencies +hydradx-traits = { workspace = true } +pallet-intent = { workspace = true} +pallet-route-executor = { workspace = true} +ice-support = { workspace = true } +ice-solver = { workspace = true } +amm-simulator = { workspace = true } + +# Math +hydra-dx-math = { workspace = true } + +# ORML dependencies +orml-traits = { workspace = true } + +# Optional imports for benchmarking +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +sp-io = { workspace = true } +test-utils = { workspace = true } +pretty_assertions = { workspace = true } +orml-tokens = { workspace = true, features=["std"] } +pallet-timestamp = { workspace = true } +primitives = { workspace = true } +pallet-broadcast = { workspace = true } + + +[features] +default = ['std'] +std = [ + 'codec/std', + 'scale-info/std', + 'sp-runtime/std', + 'sp-core/std', + 'sp-io/std', + 'sp-std/std', + 'frame-benchmarking/std', + 'hydradx-traits/std', + 'pallet-intent/std', + "sp-runtime-interface/std", + "sp-externalities/std", + "hydra-dx-math/std", + "pallet-route-executor/std", + "orml-traits/std", + "ice-support/std", + "ice-solver/std", + "amm-simulator/std", + "num-traits/std", +] + +runtime-benchmarks = [ + "frame-benchmarking", + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", + "hydra-dx-math/runtime-benchmarks", + "pallet-route-executor/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/ice/amm-simulator/Cargo.toml b/pallets/ice/amm-simulator/Cargo.toml new file mode 100644 index 0000000000..053c493fae --- /dev/null +++ b/pallets/ice/amm-simulator/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "amm-simulator" +version = "0.1.0" +edition = "2021" + +[dependencies] +ice-support = { workspace = true } +hydradx-traits = { workspace = true } +hydra-dx-math = { workspace = true } +frame-support = { workspace = true } +sp-std = { workspace = true } +log = { workspace = true } +primitive-types = { workspace = true } +primitives = { workspace = true } +sp-arithmetic = { workspace = true } +module-evm-utility-macro = { workspace = true } +num_enum = { workspace = true } +codec = { workspace = true } +ethabi = { workspace = true } +precompile-utils = { workspace = true } +evm = { workspace = true, features = ["with-codec"] } +hex-literal = { workspace = true } +pallet-liquidation = { workspace = true } +sp-runtime = { workspace = true } +pallet-omnipool = { workspace = true } +pallet-stableswap = { workspace = true } + + +[features] +default = ['std'] +std = [ + "ice-support/std", + "hydradx-traits/std", + "hydra-dx-math/std", + "frame-support/std", + "sp-std/std", + "log/std", + "primitive-types/std", + "primitives/std", + 'sp-arithmetic/std', + 'codec/std', + 'ethabi/std', + 'precompile-utils/std', + 'evm/std', + 'sp-std/std', + 'pallet-liquidation/std', + 'sp-runtime/std', + 'pallet-omnipool/std', + 'pallet-stableswap/std', +] diff --git a/pallets/ice/amm-simulator/src/aave.rs b/pallets/ice/amm-simulator/src/aave.rs new file mode 100644 index 0000000000..ea297670e3 --- /dev/null +++ b/pallets/ice/amm-simulator/src/aave.rs @@ -0,0 +1,343 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Decode; +use codec::Encode; +use core::marker::PhantomData; +use ethabi::decode; +use ethabi::ParamType; +use evm::ExitReason; +use evm::ExitSucceed; +use frame_support::ensure; +use frame_support::pallet_prelude::RuntimeDebug; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::{AmmSimulator, SimulatorError, TradeResult}; +use hydradx_traits::evm::CallContext; +use hydradx_traits::router::{PoolEdge, PoolType}; +use ice_support::AssetId; +use ice_support::Balance; +use ice_support::Price; +use num_enum::IntoPrimitive; +use num_enum::TryFromPrimitive; +use precompile_utils::evm::writer::EvmDataWriter; +use primitive_types::U256; +use primitives::EvmAddress; +use sp_arithmetic::traits::SaturatedConversion; +use sp_std::boxed::Box; +use sp_std::collections::btree_map::BTreeMap; +use sp_std::vec; +use sp_std::vec::Vec; + +pub trait DataProvider { + fn view(context: CallContext, data: Vec, gas: u64) -> (ExitReason, Vec); + + fn borrowing_contract() -> EvmAddress; + + fn address_to_asset(address: EvmAddress) -> Option; + + fn pairs() -> Vec<(AssetId, AssetId)>; +} + +const GAS_LIMIT: u64 = 1_000_000; +const LOG_TARGET: &str = "aave_simulator"; + +#[module_evm_utility_macro::generate_function_selector] +#[derive(Eq, PartialEq, TryFromPrimitive, IntoPrimitive)] +#[repr(u32)] +pub enum Function { + // Pool + Supply = "supply(address,uint256,address,uint16)", + Withdraw = "withdraw(address,uint256,address)", + GetReserveData = "getReserveData(address)", + GetConfiguration = "getConfiguration(address)", + GetReservesList = "getReservesList()", + // AToken + UnderlyingAssetAddress = "UNDERLYING_ASSET_ADDRESS()", + ScaledTotalSupply = "scaledTotalSupply()", +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, PartialEq, Eq)] +pub struct ReserveData { + pub configuration: U256, + pub liquidity_index: U256, + pub current_liquidity_rate: U256, + pub variable_borrow_index: U256, + pub current_variable_borrow_rate: U256, + pub current_stable_borrow_rate: U256, + pub last_update_timestamp: U256, + pub id: u16, + pub atoken_address: EvmAddress, + pub stable_debt_token_address: EvmAddress, + pub variable_debt_token_address: EvmAddress, + pub interest_rate_strategy_address: EvmAddress, + pub accrued_to_treasury: U256, + pub scaled_total_supply: U256, +} + +#[allow(dead_code)] +impl ReserveData { + fn decimals(&self) -> u8 { + //bit 48-55: Decimals + let mask = U256::from(0xFF) << 48; + ((self.configuration & mask) >> 48).saturated_into() + } + + fn supply_cap_raw(&self) -> U256 { + //bit 116-151 supply cap in whole tokens, supplyCap == 0 => no cap + let mask = U256::from((1u128 << 36) - 1) << 116; + (self.configuration & mask) >> 116 + } + + fn supply_cap(&self) -> U256 { + if self.supply_cap_raw().is_zero() { + U256::MAX + } else { + self.supply_cap_raw().saturating_mul( + U256::from(10) + .checked_pow(self.decimals().into()) + .unwrap_or_else(U256::one), + ) + } + } + + fn current_supply(&self) -> U256 { + self.scaled_total_supply + .saturating_add(self.accrued_to_treasury) + .saturating_mul(self.liquidity_index) + / U256::from(10).pow(27.into()) + } + + fn available_supply(&self) -> U256 { + self.supply_cap().saturating_sub(self.current_supply()) + } +} + +#[derive(Clone, Encode, Decode, RuntimeDebug, Eq, PartialEq)] +pub struct Snapshot { + /// Map of aave reserves + pub reserves: BTreeMap, + /// Aave pool contract address + pub contract: EvmAddress, + + pub pairs: Vec<(AssetId, AssetId)>, +} + +//NOTE: This is tmp. dummy impl. of aave simulator that always trade 1:1 and doesn't do any checks. +pub struct Simulator(PhantomData); + +impl Simulator { + fn get_reserves_list(aave: EvmAddress) -> Result, SimulatorError> { + let ctx = CallContext::new_view(aave); + let data = EvmDataWriter::new_with_selector(Function::GetReservesList).build(); + + let (exit_reason, value) = DP::view(ctx, data, GAS_LIMIT); + if exit_reason != ExitReason::Succeed(ExitSucceed::Returned) { + log::error!(target: LOG_TARGET, "to get reserves list reason: {:?}, value: {:?}", exit_reason, value); + return Err(SimulatorError::Other); + } + + let param_types = vec![ParamType::Array(Box::new(ParamType::Address))]; + + let decoded = decode(¶m_types, value.as_ref()).map_err(|_| { + log::error!(target: LOG_TARGET, "to decore reserves list"); + SimulatorError::Other + })?; + + // Convert decoded addresses to EvmAddress format + let addresses = decoded[0] + .clone() + .into_array() + .ok_or(SimulatorError::Other)? + .into_iter() + .filter_map(|addr| addr.into_address()) + .map(|addr| EvmAddress::from_slice(addr.as_bytes())) + .collect(); + + Ok(addresses) + } + + fn get_reserve_data(aave: EvmAddress, reserve: EvmAddress) -> Result { + let ctc = CallContext::new_view(aave); + let data = EvmDataWriter::new_with_selector(Function::GetReserveData) + .write(reserve) + .build(); + + let (exit_reason, value) = DP::view(ctc, data, GAS_LIMIT); + if exit_reason != ExitReason::Succeed(ExitSucceed::Returned) { + log::error!(target: LOG_TARGET, "to get reserves data, reason: {:?}, value: {:?}", exit_reason, value); + return Err(SimulatorError::Other); + } + + let param_types = vec![ + ParamType::Uint(256), // configuration + ParamType::Uint(256), // liquidityIndex + ParamType::Uint(256), // variableBorrowIndex + ParamType::Uint(256), // currentLiquidityRate + ParamType::Uint(256), // currentVariableBorrowRate + ParamType::Uint(256), // currentStableBorrowRate + ParamType::Uint(256), // lastUpdateTimestamp + ParamType::Uint(16), // id + ParamType::Address, // aTokenAddress + ParamType::Address, // stableDebtTokenAddress + ParamType::Address, // variableDebtTokenAddress + ParamType::Address, // interestRateStrategyAddress + ParamType::Uint(256), // accruedToTreasury + ]; + + let decoded = decode(¶m_types, value.as_ref()).map_err(|_| { + log::error!(target: LOG_TARGET, "to decode reserve data"); + SimulatorError::Other + })?; + + // Ensure sufficient length + ensure!(decoded.len() == param_types.len(), { + log::error!(target: LOG_TARGET, "invalid reserve data"); + SimulatorError::Other + }); + + let a_token = EvmAddress::from_slice(decoded[8].clone().into_address().unwrap_or_default().as_ref()); + Ok(ReserveData { + configuration: decoded[0].clone().into_uint().unwrap_or_default(), + liquidity_index: decoded[1].clone().into_uint().unwrap_or_default(), + current_liquidity_rate: decoded[3].clone().into_uint().unwrap_or_default(), + variable_borrow_index: decoded[2].clone().into_uint().unwrap_or_default(), + current_variable_borrow_rate: decoded[4].clone().into_uint().unwrap_or_default(), + current_stable_borrow_rate: decoded[5].clone().into_uint().unwrap_or_default(), + last_update_timestamp: decoded[6].clone().into_uint().unwrap_or_default(), + id: decoded[7].clone().into_uint().unwrap_or_default().saturated_into(), + atoken_address: a_token, + stable_debt_token_address: EvmAddress::from_slice( + decoded[9].clone().into_address().unwrap_or_default().as_ref(), + ), + variable_debt_token_address: EvmAddress::from_slice( + decoded[10].clone().into_address().unwrap_or_default().as_ref(), + ), + interest_rate_strategy_address: EvmAddress::from_slice( + decoded[11].clone().into_address().unwrap_or_default().as_ref(), + ), + accrued_to_treasury: decoded[12].clone().into_uint().unwrap_or_default(), + scaled_total_supply: Simulator::::get_scaled_total_supply(a_token)?, + }) + } + + fn get_scaled_total_supply(reserve: EvmAddress) -> Result { + let ctx = CallContext::new_view(reserve); + let data = EvmDataWriter::new_with_selector(Function::ScaledTotalSupply).build(); + + let (exit_reason, value) = DP::view(ctx, data, GAS_LIMIT); + if exit_reason != ExitReason::Succeed(ExitSucceed::Returned) { + log::error!(target: LOG_TARGET, "to get scaled total supply, reserve: {:?}, reason: {:?}, value: {:?}", reserve, exit_reason, value ); + return Err(SimulatorError::Other); + } + + ensure!(value.len() <= 32, { + log::error!(target: LOG_TARGET, "invalid scaled total supply"); + SimulatorError::Other + }); + Ok(U256::from_big_endian(value.as_slice())) + } +} + +impl AmmSimulator for Simulator { + type Snapshot = Snapshot; + + fn snapshot() -> Self::Snapshot { + let mut snapshot = Snapshot { + reserves: BTreeMap::new(), + contract: DP::borrowing_contract(), + pairs: DP::pairs(), + }; + + let Ok(reserves) = Self::get_reserves_list(snapshot.contract) else { + return snapshot; + }; + + for addr in reserves { + let Ok(reserve) = Self::get_reserve_data(snapshot.contract, addr) else { + snapshot.reserves.clear(); + break; + }; + + let Some(asset_id) = DP::address_to_asset(addr) else { + debug_assert!(false, "Failed to map reserve address to asset, reserve: {:?}", addr); + log::error!(target: LOG_TARGET, "to map reserve address to asset, reserve: {:?}", addr); + snapshot.reserves.clear(); + break; + }; + + snapshot.reserves.insert(asset_id, reserve); + } + + snapshot + } + + fn pool_type() -> PoolType { + PoolType::Aave + } + + fn simulate_buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + _max_amount_in: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError> { + if !snapshot.reserves.contains_key(&asset_in) && !snapshot.reserves.contains_key(&asset_out) { + return Err(SimulatorError::AssetNotFound); + } + + Ok(( + snapshot.clone(), + TradeResult { + amount_in: amount_out, + amount_out, + }, + )) + } + + fn simulate_sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + _min_amount_out: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError> { + if !snapshot.reserves.contains_key(&asset_in) && !snapshot.reserves.contains_key(&asset_out) { + return Err(SimulatorError::AssetNotFound); + } + + Ok(( + snapshot.clone(), + TradeResult { + amount_in, + amount_out: amount_in, + }, + )) + } + + fn get_spot_price( + asset_in: AssetId, + asset_out: AssetId, + snapshot: &Self::Snapshot, + ) -> Result { + if !snapshot.reserves.contains_key(&asset_in) && !snapshot.reserves.contains_key(&asset_out) { + return Err(SimulatorError::AssetNotFound); + } + Ok(Ratio { n: 1, d: 1 }) + } + + fn can_trade(_asset_in: AssetId, _asset_out: AssetId, _snapshot: &Self::Snapshot) -> Option> { + // no, Dave, you cannot trade this now. + None + } + + fn pool_edges(_snapshot: &Self::Snapshot) -> sp_std::vec::Vec> { + _snapshot + .pairs + .iter() + .map(|(a, b)| PoolEdge { + pool_type: PoolType::Aave, + assets: vec![*a, *b], + }) + .collect() + } +} diff --git a/pallets/ice/amm-simulator/src/lib.rs b/pallets/ice/amm-simulator/src/lib.rs new file mode 100644 index 0000000000..a88d7a5121 --- /dev/null +++ b/pallets/ice/amm-simulator/src/lib.rs @@ -0,0 +1,197 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use frame_support::traits::Get; +use frame_support::BoundedVec; +use hydra_dx_math::support::rational::{round_u512_to_rational, Rounding}; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::{ + AMMInterface, RouteDiscovery, SimulatorConfig, SimulatorError, SimulatorSet, TradeExecution, +}; +use hydradx_traits::router::{AssetPair, PoolEdge, Route, RouteProvider, Trade}; +use primitive_types::U512; +use sp_std::marker::PhantomData; +use sp_std::vec; +use sp_std::vec::Vec; + +pub mod aave; +pub mod omnipool; +pub mod stableswap; + +/// Route discovery using on-chain routes, simulator `can_trade`, and RouteProvider fallback. +/// +/// This is the default strategy. It can be replaced in `SimulatorConfig` with a custom +/// implementation (e.g., one that simulates sells across candidate routes). +pub struct OnChainRouteDiscovery(PhantomData<(RP, Sims)>); + +impl RouteDiscovery for OnChainRouteDiscovery +where + RP: RouteProvider, + Sims: SimulatorSet, +{ + fn discover_routes(asset_in: u32, asset_out: u32, state: &Sims::State) -> Result>, SimulatorError> { + let asset_pair = AssetPair::new(asset_in, asset_out); + + // Priority 1: Check for explicitly configured on-chain route + if let Some(explicit_route) = RP::get_onchain_route(asset_pair) { + return Ok(vec![explicit_route]); + } + + // Priority 2: Ask simulators if they can trade this pair directly + if let Some(pool_type) = Sims::can_trade(asset_in, asset_out, state) { + return Ok(vec![BoundedVec::truncate_from(vec![Trade { + pool: pool_type, + asset_in, + asset_out, + }])]); + } + + // Priority 3: Fall back to the route provider's default + let route = RP::get_route(asset_pair); + if route.is_empty() { + return Err(SimulatorError::AssetNotFound); + } + Ok(vec![route]) + } +} + +/// The Hydration simulator compositor. +/// +/// Implements AMMInterface by composing multiple individual AMM simulators +/// and handling multi-hop routing between them. +pub struct HydrationSimulator(PhantomData); + +impl HydrationSimulator { + /// Get the initial state from all simulators + pub fn initial_state() -> ::State { + C::Simulators::initial_state() + } +} + +impl AMMInterface for HydrationSimulator { + type Error = SimulatorError; + type State = ::State; + + fn discover_routes(asset_in: u32, asset_out: u32, state: &Self::State) -> Result>, Self::Error> { + C::RouteDiscovery::discover_routes(asset_in, asset_out, state) + } + + fn sell( + _asset_in: u32, + _asset_out: u32, + amount_in: u128, + route: Route, + state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + let mut current_state = state.clone(); + let mut current_amount = amount_in; + let original_amount_in = amount_in; + + for trade in route.iter() { + let (new_state, result) = C::Simulators::simulate_sell( + trade.pool, + trade.asset_in, + trade.asset_out, + current_amount, + 0, // No limit check on intermediate hops + ¤t_state, + )?; + + current_state = new_state; + current_amount = result.amount_out; + } + + Ok(( + current_state, + TradeExecution { + amount_in: original_amount_in, + amount_out: current_amount, + route, + }, + )) + } + + fn buy( + _asset_in: u32, + _asset_out: u32, + amount_out: u128, + route: Route, + state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error> { + let mut current_required = amount_out; + + let mut current_state = state.clone(); + let mut current_amount = 0u128; + + for trade in route.iter().rev() { + let (new_state, result) = C::Simulators::simulate_buy( + trade.pool, + trade.asset_in, + trade.asset_out, + current_required, + u128::MAX, // No limit on intermediate hops + ¤t_state, + )?; + + current_state = new_state; + current_amount = result.amount_in; + current_required = result.amount_in; + } + + Ok(( + current_state, + TradeExecution { + amount_in: current_amount, + amount_out, + route, + }, + )) + } + + fn get_spot_price( + _asset_in: u32, + _asset_out: u32, + route: Route, + state: &Self::State, + ) -> Result { + let mut numerator = U512::from(1u128); + let mut denominator = U512::from(1u128); + + for chunk in route.chunks(4) { + let mut chunk_numerator = U512::from(1u128); + let mut chunk_denominator = U512::from(1u128); + + for trade in chunk.iter() { + let hop_price = C::Simulators::get_spot_price(trade.pool, trade.asset_in, trade.asset_out, state)?; + + chunk_numerator = chunk_numerator + .checked_mul(U512::from(hop_price.n)) + .ok_or(SimulatorError::MathError)?; + chunk_denominator = chunk_denominator + .checked_mul(U512::from(hop_price.d)) + .ok_or(SimulatorError::MathError)?; + } + + numerator = numerator + .checked_mul(chunk_numerator) + .ok_or(SimulatorError::MathError)?; + denominator = denominator + .checked_mul(chunk_denominator) + .ok_or(SimulatorError::MathError)?; + } + + let (n, d) = round_u512_to_rational((numerator, denominator), Rounding::Nearest); + Ok(Ratio::new(n, d)) + } + + fn price_denominator() -> u32 { + C::PriceDenominator::get() + } + + fn pool_edges(state: &Self::State) -> Vec> { + C::Simulators::pool_edges(state) + } + + fn existential_deposit(asset_id: u32) -> u128 { + C::existential_deposit(asset_id) + } +} diff --git a/pallets/ice/amm-simulator/src/omnipool.rs b/pallets/ice/amm-simulator/src/omnipool.rs new file mode 100644 index 0000000000..ab511bea90 --- /dev/null +++ b/pallets/ice/amm-simulator/src/omnipool.rs @@ -0,0 +1,490 @@ +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Decode; +use codec::Encode; +use core::marker::PhantomData; +use hydra_dx_math::omnipool::types::SignedBalance; +use hydra_dx_math::omnipool::types::TradeSlipFees; +use hydra_dx_math::support::rational::round_to_rational; +use hydra_dx_math::support::rational::Rounding; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::AmmSimulator; +use hydradx_traits::amm::SimulatorError; +use hydradx_traits::amm::TradeResult; +use hydradx_traits::router::{PoolEdge, PoolType}; +use ice_support::AssetId; +use ice_support::Balance; +use pallet_omnipool::types::AssetReserveState; +use pallet_omnipool::types::AssetState; +use pallet_omnipool::types::SlipFeeConfig; +use pallet_omnipool::types::Tradability; +use primitive_types::U256; +use sp_runtime::traits::Zero; +use sp_runtime::Permill; +use sp_std::collections::btree_map::BTreeMap; + +pub trait DataProvider { + type AccountId; + + fn protocol_account() -> Self::AccountId; + + fn assets() -> impl Iterator)>; + + fn free_balance(currncy_id: AssetId, who: &Self::AccountId) -> Balance; + + fn fee(key: (AssetId, Balance)) -> (Permill, Permill); + + fn hub_asset_id() -> AssetId; + + fn min_trading_limit() -> Balance; + + fn max_in_ratio() -> Balance; + + fn max_out_ratio() -> Balance; + + fn slip_fee() -> Option; +} + +/// Snapshot of Omnipool state for simulation purposes. +/// +/// Contains all asset states needed to simulate trades without +/// accessing chain storage. +#[derive(Clone, Debug, Default, Encode, Decode)] +pub struct OmnipoolSnapshot { + /// Asset states: AssetId -> AssetReserveState + pub assets: BTreeMap>, + /// Asset fees: AssetId -> (asset_fee, protocol_fee) + /// Stored separately to avoid changing AssetReserveState type + pub fees: BTreeMap, + /// Hub asset id + pub hub_asset_id: AssetId, + /// Minimum trading limit + pub min_trading_limit: Balance, + /// Max in ratio + pub max_in_ratio: Balance, + /// Max out ratio + pub max_out_ratio: Balance, + /// Global slip fee configuration. + pub slip_fee: Option, + /// Snapshot of each asset's hub_reserve at the start of the current block. + pub slip_fee_hubreserve_at_block_start: BTreeMap, + /// Cumulative net hub asset delta per asset in the current block. + pub slip_fee_delta: BTreeMap, +} + +impl OmnipoolSnapshot { + pub fn get_asset(&self, asset_id: AssetId) -> Option<&AssetReserveState> { + self.assets.get(&asset_id) + } + + pub fn get_fees(&self, asset_id: AssetId) -> (Permill, Permill) { + self.fees + .get(&asset_id) + .copied() + .unwrap_or((Permill::zero(), Permill::zero())) + } + + pub fn with_updated_asset(mut self, asset_id: AssetId, state: AssetReserveState) -> Self { + self.assets.insert(asset_id, state); + self + } + + pub fn with_q0(mut self, asset_id: AssetId, hub_reserve: Balance) -> Self { + self.slip_fee_hubreserve_at_block_start.insert(asset_id, hub_reserve); + self + } + + pub fn with_slip_delta(mut self, asset_id: AssetId, delta: SignedBalance) -> Self { + self.slip_fee_delta.insert(asset_id, delta); + self + } + + pub fn load_trade_slip_fees( + &self, + asset_in: AssetId, + asset_in_hub_reserve: Balance, + asset_out: AssetId, + asset_out_hub_reserve: Balance, + ) -> Option { + let cfg = self.slip_fee.clone()?; + + Some(TradeSlipFees { + asset_in_hub_reserve: *self + .slip_fee_hubreserve_at_block_start + .get(&asset_in) + .unwrap_or(&asset_in_hub_reserve), + asset_in_delta: *self.slip_fee_delta.get(&asset_in).unwrap_or(&SignedBalance::default()), + asset_out_hub_reserve: *self + .slip_fee_hubreserve_at_block_start + .get(&asset_out) + .unwrap_or(&asset_out_hub_reserve), + asset_out_delta: *self.slip_fee_delta.get(&asset_out).unwrap_or(&SignedBalance::default()), + max_slip_fee: cfg.max_slip_fee, + }) + } + + pub fn get_updated_fee_delta( + &self, + asset_in: AssetId, + delta_hub_in: Balance, + asset_out: AssetId, + delta_hub_out: Balance, + ) -> Option<(SignedBalance, SignedBalance)> { + let d_in = (*self.slip_fee_delta.get(&asset_in).unwrap_or(&SignedBalance::default())) + .checked_add(SignedBalance::Negative(delta_hub_in))?; + + let d_out = (*self.slip_fee_delta.get(&asset_out).unwrap_or(&SignedBalance::default())) + .checked_add(SignedBalance::Positive(delta_hub_out))?; + + Some((d_in, d_out)) + } +} + +pub struct Simulator(PhantomData); + +impl AmmSimulator for Simulator { + type Snapshot = OmnipoolSnapshot; + + fn pool_type() -> PoolType { + PoolType::Omnipool + } + + fn snapshot() -> Self::Snapshot { + let protocol_account = DP::protocol_account(); + + let mut assets: BTreeMap> = BTreeMap::new(); + let mut fees: BTreeMap = BTreeMap::new(); + + for (asset_id, state) in DP::assets() { + let reserve = DP::free_balance(asset_id, &protocol_account); + let (asset_fee, protocol_fee) = DP::fee((asset_id, reserve)); + + let reserve_state = (state, reserve).into(); + assets.insert(asset_id, reserve_state); + fees.insert(asset_id, (asset_fee, protocol_fee)); + } + + OmnipoolSnapshot { + assets, + fees, + hub_asset_id: DP::hub_asset_id(), + min_trading_limit: DP::min_trading_limit(), + max_in_ratio: DP::max_in_ratio(), + max_out_ratio: DP::max_out_ratio(), + slip_fee: DP::slip_fee(), + //NOTE: these are per block and solver is always first in the block so they should be empty + slip_fee_hubreserve_at_block_start: BTreeMap::new(), + slip_fee_delta: BTreeMap::new(), + } + } + + fn simulate_sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError> { + if asset_in == asset_out { + return Err(SimulatorError::Other); + } + + if amount_in < snapshot.min_trading_limit { + return Err(SimulatorError::TradeTooSmall); + } + + // Hub asset not allowed + if asset_in == snapshot.hub_asset_id || asset_out == snapshot.hub_asset_id { + return Err(SimulatorError::Other); + } + + let asset_in_state = snapshot.get_asset(asset_in).ok_or(SimulatorError::AssetNotFound)?; + let asset_out_state = snapshot.get_asset(asset_out).ok_or(SimulatorError::AssetNotFound)?; + + // Check tradability + if !asset_in_state.tradable.contains(Tradability::SELL) { + return Err(SimulatorError::Other); + } + if !asset_out_state.tradable.contains(Tradability::BUY) { + return Err(SimulatorError::Other); + } + + if amount_in + > asset_in_state + .reserve + .checked_div(snapshot.max_in_ratio) + .ok_or(SimulatorError::MathError)? + { + return Err(SimulatorError::TradeTooLarge); + } + + let (asset_fee, _) = snapshot.get_fees(asset_out); + let (_, protocol_fee) = snapshot.get_fees(asset_in); + let withdraw_fee = Permill::from_percent(0); // Not used in trades + + let slip = snapshot.load_trade_slip_fees( + asset_in, + asset_in_state.hub_reserve, + asset_out, + asset_out_state.hub_reserve, + ); + + let state_changes = hydra_dx_math::omnipool::calculate_sell_state_changes( + &asset_in_state.into(), + &asset_out_state.into(), + amount_in, + asset_fee, + protocol_fee, + withdraw_fee, + slip.as_ref(), + ) + .ok_or(SimulatorError::MathError)?; + + let amount_out = *state_changes.asset_out.delta_reserve; + + if amount_out == Balance::zero() { + return Err(SimulatorError::InsufficientLiquidity); + } + + if amount_out < min_amount_out { + return Err(SimulatorError::LimitNotMet); + } + + if amount_out + > asset_out_state + .reserve + .checked_div(snapshot.max_out_ratio) + .ok_or(SimulatorError::MathError)? + { + return Err(SimulatorError::TradeTooLarge); + } + + let new_asset_in_state = apply_state_changes(asset_in_state, &state_changes.asset_in)?; + let new_asset_out_state = apply_state_changes(asset_out_state, &state_changes.asset_out)?; + + let mut new_snapshot = snapshot + .clone() + .with_updated_asset(asset_in, new_asset_in_state) + .with_updated_asset(asset_out, new_asset_out_state); + + if let Some(s_fees) = slip { + let (d_in, d_out) = snapshot + .get_updated_fee_delta( + asset_in, + *state_changes.asset_in.delta_hub_reserve, + asset_out, + *state_changes.asset_out.delta_hub_reserve, + ) + .ok_or(SimulatorError::Other)?; + + new_snapshot = new_snapshot + .with_q0(asset_in, s_fees.asset_in_hub_reserve) + .with_q0(asset_out, s_fees.asset_out_hub_reserve) + .with_slip_delta(asset_in, d_in) + .with_slip_delta(asset_out, d_out); + } + + Ok((new_snapshot, TradeResult::new(amount_in, amount_out))) + } + + fn simulate_buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError> { + if asset_in == asset_out { + return Err(SimulatorError::Other); + } + + if asset_in == snapshot.hub_asset_id || asset_out == snapshot.hub_asset_id { + return Err(SimulatorError::Other); + } + + let asset_in_state = snapshot.get_asset(asset_in).ok_or(SimulatorError::AssetNotFound)?; + let asset_out_state = snapshot.get_asset(asset_out).ok_or(SimulatorError::AssetNotFound)?; + + if !asset_in_state.tradable.contains(Tradability::SELL) { + return Err(SimulatorError::Other); + } + if !asset_out_state.tradable.contains(Tradability::BUY) { + return Err(SimulatorError::Other); + } + + let (asset_fee, _) = snapshot.get_fees(asset_out); + let (_, protocol_fee) = snapshot.get_fees(asset_in); + let withdraw_fee = Permill::from_percent(0); // Not used in trades + + let slip = snapshot.load_trade_slip_fees( + asset_in, + asset_in_state.hub_reserve, + asset_out, + asset_out_state.hub_reserve, + ); + + let state_changes = hydra_dx_math::omnipool::calculate_buy_state_changes( + &asset_in_state.into(), + &asset_out_state.into(), + amount_out, + asset_fee, + protocol_fee, + withdraw_fee, + slip.as_ref(), + ) + .ok_or(SimulatorError::MathError)?; + + let amount_in = *state_changes.asset_in.delta_reserve; + + if amount_in > max_amount_in { + return Err(SimulatorError::LimitNotMet); + } + + if amount_in < snapshot.min_trading_limit { + return Err(SimulatorError::TradeTooSmall); + } + + if amount_in + > asset_in_state + .reserve + .checked_div(snapshot.max_in_ratio) + .ok_or(SimulatorError::MathError)? + { + return Err(SimulatorError::TradeTooLarge); + } + + if amount_out + > asset_out_state + .reserve + .checked_div(snapshot.max_out_ratio) + .ok_or(SimulatorError::MathError)? + { + return Err(SimulatorError::TradeTooLarge); + } + + let new_asset_in_state = apply_state_changes(asset_in_state, &state_changes.asset_in)?; + let new_asset_out_state = apply_state_changes(asset_out_state, &state_changes.asset_out)?; + + let mut new_snapshot = snapshot + .clone() + .with_updated_asset(asset_in, new_asset_in_state) + .with_updated_asset(asset_out, new_asset_out_state); + + if let Some(s_fees) = slip { + let (d_in, d_out) = snapshot + .get_updated_fee_delta( + asset_in, + *state_changes.asset_in.delta_hub_reserve, + asset_out, + *state_changes.asset_out.delta_hub_reserve, + ) + .ok_or(SimulatorError::Other)?; + + new_snapshot = new_snapshot + .with_q0(asset_in, s_fees.asset_in_hub_reserve) + .with_q0(asset_out, s_fees.asset_out_hub_reserve) + .with_slip_delta(asset_in, d_in) + .with_slip_delta(asset_out, d_out); + } + + Ok((new_snapshot, TradeResult::new(amount_in, amount_out))) + } + + fn get_spot_price( + asset_in: AssetId, + asset_out: AssetId, + snapshot: &Self::Snapshot, + ) -> Result { + if asset_in == snapshot.hub_asset_id { + // Price of hub asset in terms of asset_out + // hub_price = reserve_out / hub_reserve_out + let state_out = snapshot.get_asset(asset_out).ok_or(SimulatorError::AssetNotFound)?; + Ok(Ratio::new(state_out.reserve, state_out.hub_reserve)) + } else if asset_out == snapshot.hub_asset_id { + // Price of asset_in in terms of hub asset + // price = hub_reserve_in / reserve_in + let state_in = snapshot.get_asset(asset_in).ok_or(SimulatorError::AssetNotFound)?; + Ok(Ratio::new(state_in.hub_reserve, state_in.reserve)) + } else { + // Cross-rate: price of asset_in in terms of asset_out + // price = (hub_reserve_in / reserve_in) / (hub_reserve_out / reserve_out) + // = (hub_reserve_in * reserve_out) / (reserve_in * hub_reserve_out) + let state_in = snapshot.get_asset(asset_in).ok_or(SimulatorError::AssetNotFound)?; + let state_out = snapshot.get_asset(asset_out).ok_or(SimulatorError::AssetNotFound)?; + + let n = U256::from(state_in.hub_reserve) * U256::from(state_out.reserve); + let d = U256::from(state_in.reserve) * U256::from(state_out.hub_reserve); + + let (n, d) = round_to_rational((n, d), Rounding::Nearest); + Ok(Ratio::new(n, d)) + } + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, snapshot: &Self::Snapshot) -> Option> { + // Hub asset trades are not supported directly + if asset_in == snapshot.hub_asset_id || asset_out == snapshot.hub_asset_id { + return None; + } + + // Both assets must be in the omnipool + let has_in = snapshot.assets.contains_key(&asset_in); + let has_out = snapshot.assets.contains_key(&asset_out); + + if has_in && has_out { + Some(PoolType::Omnipool) + } else { + None + } + } + + fn pool_edges(snapshot: &Self::Snapshot) -> sp_std::vec::Vec> { + let assets: sp_std::vec::Vec = snapshot.assets.keys().copied().collect(); + if assets.is_empty() { + return sp_std::vec::Vec::new(); + } + sp_std::vec![PoolEdge { + pool_type: PoolType::Omnipool, + assets, + }] + } +} + +fn apply_state_changes( + current: &AssetReserveState, + changes: &hydra_dx_math::omnipool::types::AssetStateChange, +) -> Result, SimulatorError> { + use hydra_dx_math::omnipool::types::BalanceUpdate; + + let new_reserve = match &changes.delta_reserve { + BalanceUpdate::Increase(delta) => current.reserve.checked_add(*delta), + BalanceUpdate::Decrease(delta) => current.reserve.checked_sub(*delta), + } + .ok_or(SimulatorError::MathError)?; + + let new_hub_reserve = match &changes.delta_hub_reserve { + BalanceUpdate::Increase(delta) => current.hub_reserve.checked_add(*delta), + BalanceUpdate::Decrease(delta) => current.hub_reserve.checked_sub(*delta), + } + .ok_or(SimulatorError::MathError)?; + + let new_shares = match &changes.delta_shares { + BalanceUpdate::Increase(delta) => current.shares.checked_add(*delta), + BalanceUpdate::Decrease(delta) => current.shares.checked_sub(*delta), + } + .ok_or(SimulatorError::MathError)?; + + let new_protocol_shares = match &changes.delta_protocol_shares { + BalanceUpdate::Increase(delta) => current.protocol_shares.checked_add(*delta), + BalanceUpdate::Decrease(delta) => current.protocol_shares.checked_sub(*delta), + } + .ok_or(SimulatorError::MathError)?; + + Ok(AssetReserveState { + reserve: new_reserve, + hub_reserve: new_hub_reserve, + shares: new_shares, + protocol_shares: new_protocol_shares, + cap: current.cap, + tradable: current.tradable, + }) +} diff --git a/pallets/ice/amm-simulator/src/stableswap.rs b/pallets/ice/amm-simulator/src/stableswap.rs new file mode 100644 index 0000000000..e58ab978b7 --- /dev/null +++ b/pallets/ice/amm-simulator/src/stableswap.rs @@ -0,0 +1,676 @@ +//! Stableswap simulator for off-chain trade simulation. +//! +//! This module provides an `AmmSimulator` implementation for the Stableswap pallet, +//! allowing trades to be simulated without modifying chain state. The simulator +//! supports: +//! - Regular swaps between pool assets +//! - Share asset trades (add/remove liquidity) +//! - Spot price calculation + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::Decode; +use codec::Encode; +use core::marker::PhantomData; +use hydra_dx_math::stableswap::types::AssetReserve; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::AmmSimulator; +use hydradx_traits::amm::SimulatorError; +use hydradx_traits::amm::TradeResult; +use hydradx_traits::router::{PoolEdge, PoolType}; +use ice_support::AssetId; +use ice_support::Balance; +use pallet_stableswap::types::PoolInfo; +use pallet_stableswap::types::PoolPegInfo; +use pallet_stableswap::types::PoolSnapshot; +use sp_runtime::FixedPointNumber; +use sp_std::collections::btree_map::BTreeMap; +use sp_std::vec::Vec; + +const D_ITERATIONS: u8 = hydra_dx_math::stableswap::MAX_D_ITERATIONS; +const Y_ITERATIONS: u8 = hydra_dx_math::stableswap::MAX_Y_ITERATIONS; + +//0.01% +const TEST_SHARES_PERCENTAGE: Balance = 10_000; + +pub struct Simulator(PhantomData); + +/// Snapshot of all Stableswap pools for simulation purposes. +/// +/// Contains all pool snapshots needed to simulate trades without +/// accessing chain storage. The pool_id (share asset id) is used as the key. +#[derive(Clone, Debug, Default, Encode, Decode)] +pub struct StableswapSnapshot { + pub pools: BTreeMap>, + pub min_trading_limit: Balance, +} + +impl StableswapSnapshot { + pub fn get_pool(&self, pool_id: AssetId) -> Option<&PoolSnapshot> { + self.pools.get(&pool_id) + } + + pub fn with_updated_pool(mut self, pool_id: AssetId, snapshot: PoolSnapshot) -> Self { + self.pools.insert(pool_id, snapshot); + self + } +} + +pub trait DataProvider { + type BlockNumber; + + fn pools() -> impl Iterator)>; + + fn pool_pegs(pool_id: AssetId) -> Option>; + + fn create_snapshot(pool_id: AssetId) -> Option>; + + fn min_trading_limit() -> Balance; +} + +impl AmmSimulator for Simulator { + type Snapshot = StableswapSnapshot; + + fn pool_type() -> PoolType { + PoolType::Stableswap(0) // Representative value + } + + /// Override to match any Stableswap pool, regardless of pool_id + fn matches_pool_type(pool_type: PoolType) -> bool { + matches!(pool_type, PoolType::Stableswap(_)) + } + + fn snapshot() -> Self::Snapshot { + let mut pools = BTreeMap::new(); + + for (pool_id, pool) in DP::pools() { + // TODO: we skip incorrect pools - this was likely due to incorrect snapshots used in tests + // but verify! + if let Some(peg_info) = DP::pool_pegs(pool_id) { + if peg_info.current.len() != pool.assets.len() { + debug_assert!(false, "all assets should have pegs"); + continue; + } + } + + if let Some(pool_snapshot) = DP::create_snapshot(pool_id) { + // TODO: same here as above + if pool_snapshot.pegs.len() != pool_snapshot.reserves.len() { + debug_assert!(false, "all reserves should have pegs"); + continue; + } + + // TODO: this should be removed, some pools dont have pegs + // but issue with snapshosting mechanism?! + if pool_snapshot.pegs.is_empty() { + continue; + } + + let assets: Vec = pool_snapshot.assets.iter().copied().collect(); + let snapshot = PoolSnapshot { + assets: assets.try_into().unwrap_or_default(), + reserves: pool_snapshot.reserves, + amplification: pool_snapshot.amplification, + fee: pool_snapshot.fee, + block_fee: pool_snapshot.block_fee, + pegs: pool_snapshot.pegs, + share_issuance: pool_snapshot.share_issuance, + }; + pools.insert(pool_id, snapshot); + } + } + + StableswapSnapshot { + pools, + min_trading_limit: DP::min_trading_limit(), + } + } + + fn simulate_sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError> { + if asset_in == asset_out { + return Err(SimulatorError::Other); + } + + if amount_in < snapshot.min_trading_limit { + return Err(SimulatorError::TradeTooSmall); + } + + let (pool_id, pool_snapshot) = find_pool(asset_in, asset_out, snapshot)?; + + if asset_in == pool_id { + return simulate_remove_liquidity_sell( + pool_id, + asset_out, + amount_in, + min_amount_out, + pool_snapshot, + snapshot, + ); + } + + if asset_out == pool_id { + return simulate_add_liquidity_sell(pool_id, asset_in, amount_in, min_amount_out, pool_snapshot, snapshot); + } + + simulate_regular_sell(asset_in, asset_out, amount_in, min_amount_out, pool_snapshot, snapshot) + } + + fn simulate_buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError> { + if asset_in == asset_out { + return Err(SimulatorError::Other); + } + + let (pool_id, pool_snapshot) = find_pool(asset_in, asset_out, snapshot)?; + + if asset_in == pool_id { + return simulate_remove_liquidity_buy( + pool_id, + asset_out, + amount_out, + max_amount_in, + pool_snapshot, + snapshot, + ); + } + + if asset_out == pool_id { + return simulate_add_liquidity_buy(pool_id, asset_in, amount_out, max_amount_in, pool_snapshot, snapshot); + } + + simulate_regular_buy(asset_in, asset_out, amount_out, max_amount_in, pool_snapshot, snapshot) + } + + fn get_spot_price( + asset_in: AssetId, + asset_out: AssetId, + snapshot: &Self::Snapshot, + ) -> Result { + let (pool_id, pool_snapshot) = find_pool(asset_in, asset_out, snapshot)?; + + if asset_in == pool_id { + // Price = how much asset_out you get per 1 share + // Using a small simulation to determine spot price + let test_shares = pool_snapshot.share_issuance / TEST_SHARES_PERCENTAGE; + if test_shares == 0 { + return Err(SimulatorError::InsufficientLiquidity); + } + + let asset_idx = pool_snapshot + .asset_idx(asset_out) + .ok_or(SimulatorError::AssetNotFound)?; + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + let (amount_out, _fee) = + hydra_dx_math::stableswap::calculate_withdraw_one_asset::( + &pool_snapshot.reserves, + test_shares, + asset_idx, + pool_snapshot.share_issuance, + pool_snapshot.amplification, + pool_snapshot.block_fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + // Price = amount_out / test_shares + return Ok(Ratio::new(amount_out, test_shares)); + } + + if asset_out == pool_id { + // Price = how many shares you get per 1 unit of asset_in + let asset_idx = pool_snapshot.asset_idx(asset_in).ok_or(SimulatorError::AssetNotFound)?; + let decimals = pool_snapshot.reserves[asset_idx].decimals; + let test_amount = 10u128.pow(decimals as u32); // 1 unit of asset + + let mut updated_reserves: Vec = pool_snapshot.reserves.to_vec(); + updated_reserves[asset_idx].amount = updated_reserves[asset_idx] + .amount + .checked_add(test_amount) + .ok_or(SimulatorError::MathError)?; + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + let (shares_out, _fees) = hydra_dx_math::stableswap::calculate_shares::( + &pool_snapshot.reserves, + &updated_reserves, + pool_snapshot.amplification, + pool_snapshot.share_issuance, + pool_snapshot.block_fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + // Price = shares_out / test_amount + return Ok(Ratio::new(shares_out, test_amount)); + } + + let mut decimals_in = 0; + let mut decimals_out = 0; + let assets_with_reserves: Vec<(u32, AssetReserve)> = pool_snapshot + .assets + .iter() + .zip(pool_snapshot.reserves.iter()) + .map(|(id, r)| { + if *id == asset_in { + decimals_in = r.decimals + } + + if *id == asset_out { + decimals_out = r.decimals + } + + (*id, *r) + }) + .collect(); + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + //NOTE: calculate_spot_price returns [in/out] so we have to do 1/spot_price + let spot_price = hydra_dx_math::stableswap::calculate_spot_price( + pool_id, + assets_with_reserves, + pool_snapshot.amplification, + asset_in, + asset_out, + pool_snapshot.share_issuance, + snapshot.min_trading_limit, + Some(pool_snapshot.block_fee), + &pegs, + ) + .ok_or(SimulatorError::MathError)? + .reciprocal() + .ok_or(SimulatorError::MathError)?; + + //NOTE: spot price between 2 assets is normalized to 18 dec. so we have to demormalize it + if decimals_in > decimals_out { + let m = 10u128.pow((decimals_in - decimals_out) as u32); + return Ok(Ratio::new( + spot_price.into_inner(), + sp_runtime::FixedU128::DIV.saturating_mul(m), + )); + } else if decimals_out > decimals_in { + let m = 10u128.pow((decimals_out - decimals_in) as u32); + return Ok(Ratio::new( + spot_price.into_inner().saturating_mul(m), + sp_runtime::FixedU128::DIV, + )); + } + + Ok(Ratio::new(spot_price.into_inner(), sp_runtime::FixedU128::DIV)) + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, snapshot: &Self::Snapshot) -> Option> { + // Use existing find_pool logic to check if both assets are in the same pool + if let Ok((pool_id, _)) = find_pool(asset_in, asset_out, snapshot) { + Some(PoolType::Stableswap(pool_id)) + } else { + None + } + } + + fn pool_edges(snapshot: &Self::Snapshot) -> Vec> { + snapshot + .pools + .iter() + .map(|(&pool_id, pool)| { + let mut assets = pool.assets.to_vec(); + // Include the share asset (pool_id) so route discovery can find + // paths through the pool's share token (e.g., add/remove liquidity routes). + if !assets.contains(&pool_id) { + assets.push(pool_id); + } + PoolEdge { + pool_type: PoolType::Stableswap(pool_id), + assets, + } + }) + .collect() + } +} + +fn find_pool( + asset_a: AssetId, + asset_b: AssetId, + snapshot: &StableswapSnapshot, +) -> Result<(AssetId, &PoolSnapshot), SimulatorError> { + if let Some(pool) = snapshot.pools.get(&asset_a) { + if pool.assets.iter().any(|&a| a == asset_b) { + return Ok((asset_a, pool)); + } + } + + if let Some(pool) = snapshot.pools.get(&asset_b) { + if pool.assets.iter().any(|&a| a == asset_a) { + return Ok((asset_b, pool)); + } + } + + for (pool_id, pool) in &snapshot.pools { + let has_a = pool.assets.iter().any(|&a| a == asset_a); + let has_b = pool.assets.iter().any(|&a| a == asset_b); + if has_a && has_b { + return Ok((*pool_id, pool)); + } + } + + Err(SimulatorError::AssetNotFound) +} + +fn simulate_regular_sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + pool_snapshot: &PoolSnapshot, + snapshot: &StableswapSnapshot, +) -> Result<(StableswapSnapshot, TradeResult), SimulatorError> { + let index_in = pool_snapshot.asset_idx(asset_in).ok_or(SimulatorError::AssetNotFound)?; + let index_out = pool_snapshot + .asset_idx(asset_out) + .ok_or(SimulatorError::AssetNotFound)?; + + let initial_reserves = &pool_snapshot.reserves; + + if initial_reserves[index_in].is_zero() || initial_reserves[index_out].is_zero() { + return Err(SimulatorError::InsufficientLiquidity); + } + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + let (amount_out, _fee) = hydra_dx_math::stableswap::calculate_out_given_in_with_fee::( + initial_reserves, + index_in, + index_out, + amount_in, + pool_snapshot.amplification, + pool_snapshot.fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + if amount_out < min_amount_out { + return Err(SimulatorError::LimitNotMet); + } + + let updated_pool = pool_snapshot.clone().update_reserves( + hydradx_traits::stableswap::AssetAmount::new(asset_in, amount_in), + hydradx_traits::stableswap::AssetAmount::new(asset_out, amount_out), + ); + + let pool_id = find_pool_id_for_snapshot(pool_snapshot, snapshot)?; + let updated_snapshot = snapshot.clone().with_updated_pool(pool_id, updated_pool); + + Ok((updated_snapshot, TradeResult::new(amount_in, amount_out))) +} + +fn simulate_regular_buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + pool_snapshot: &PoolSnapshot, + snapshot: &StableswapSnapshot, +) -> Result<(StableswapSnapshot, TradeResult), SimulatorError> { + let index_in = pool_snapshot.asset_idx(asset_in).ok_or(SimulatorError::AssetNotFound)?; + let index_out = pool_snapshot + .asset_idx(asset_out) + .ok_or(SimulatorError::AssetNotFound)?; + + let initial_reserves = &pool_snapshot.reserves; + + if initial_reserves[index_out].amount <= amount_out || initial_reserves[index_in].is_zero() { + return Err(SimulatorError::InsufficientLiquidity); + } + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + let (amount_in, _fee) = hydra_dx_math::stableswap::calculate_in_given_out_with_fee::( + initial_reserves, + index_in, + index_out, + amount_out, + pool_snapshot.amplification, + pool_snapshot.fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + if amount_in > max_amount_in { + return Err(SimulatorError::LimitNotMet); + } + + // Update reserves + let updated_pool = pool_snapshot.clone().update_reserves( + hydradx_traits::stableswap::AssetAmount::new(asset_in, amount_in), + hydradx_traits::stableswap::AssetAmount::new(asset_out, amount_out), + ); + + let pool_id = find_pool_id_for_snapshot(pool_snapshot, snapshot)?; + let updated_snapshot = snapshot.clone().with_updated_pool(pool_id, updated_pool); + + Ok((updated_snapshot, TradeResult::new(amount_in, amount_out))) +} + +fn simulate_add_liquidity_sell( + pool_id: AssetId, + asset_in: AssetId, + amount_in: Balance, + min_shares_out: Balance, + pool_snapshot: &PoolSnapshot, + snapshot: &StableswapSnapshot, +) -> Result<(StableswapSnapshot, TradeResult), SimulatorError> { + let asset_idx = pool_snapshot.asset_idx(asset_in).ok_or(SimulatorError::AssetNotFound)?; + + let mut updated_reserves: Vec = pool_snapshot.reserves.to_vec(); + updated_reserves[asset_idx].amount = updated_reserves[asset_idx] + .amount + .checked_add(amount_in) + .ok_or(SimulatorError::MathError)?; + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + let (shares_out, _fees) = hydra_dx_math::stableswap::calculate_shares::( + &pool_snapshot.reserves, + &updated_reserves, + pool_snapshot.amplification, + pool_snapshot.share_issuance, + pool_snapshot.block_fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + if shares_out < min_shares_out { + return Err(SimulatorError::LimitNotMet); + } + + let updated_pool = pool_snapshot + .clone() + .update_shares_and_reserve(asset_in, amount_in as i128, shares_out as i128); + let updated_snapshot = snapshot.clone().with_updated_pool(pool_id, updated_pool); + + Ok((updated_snapshot, TradeResult::new(amount_in, shares_out))) +} + +/// Simulate adding liquidity: buy specific amount of shares with asset +fn simulate_add_liquidity_buy( + pool_id: AssetId, + asset_in: AssetId, + shares_out: Balance, + max_amount_in: Balance, + pool_snapshot: &PoolSnapshot, + snapshot: &StableswapSnapshot, +) -> Result<(StableswapSnapshot, TradeResult), SimulatorError> { + let asset_idx = pool_snapshot.asset_idx(asset_in).ok_or(SimulatorError::AssetNotFound)?; + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + // Calculate how much asset is needed to get the desired shares + let (amount_in, _fee) = hydra_dx_math::stableswap::calculate_add_one_asset::( + &pool_snapshot.reserves, + shares_out, + asset_idx, + pool_snapshot.share_issuance, + pool_snapshot.amplification, + pool_snapshot.block_fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + if amount_in > max_amount_in { + return Err(SimulatorError::LimitNotMet); + } + + let updated_pool = pool_snapshot + .clone() + .update_shares_and_reserve(asset_in, amount_in as i128, shares_out as i128); + let updated_snapshot = snapshot.clone().with_updated_pool(pool_id, updated_pool); + + Ok((updated_snapshot, TradeResult::new(amount_in, shares_out))) +} + +fn simulate_remove_liquidity_sell( + pool_id: AssetId, + asset_out: AssetId, + shares_in: Balance, + min_amount_out: Balance, + pool_snapshot: &PoolSnapshot, + snapshot: &StableswapSnapshot, +) -> Result<(StableswapSnapshot, TradeResult), SimulatorError> { + let asset_idx = pool_snapshot + .asset_idx(asset_out) + .ok_or(SimulatorError::AssetNotFound)?; + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + let (amount_out, _fee) = hydra_dx_math::stableswap::calculate_withdraw_one_asset::( + &pool_snapshot.reserves, + shares_in, + asset_idx, + pool_snapshot.share_issuance, + pool_snapshot.amplification, + pool_snapshot.block_fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + if amount_out < min_amount_out { + return Err(SimulatorError::LimitNotMet); + } + + let updated_pool = + pool_snapshot + .clone() + .update_shares_and_reserve(asset_out, -(amount_out as i128), -(shares_in as i128)); + let updated_snapshot = snapshot.clone().with_updated_pool(pool_id, updated_pool); + + Ok((updated_snapshot, TradeResult::new(shares_in, amount_out))) +} + +fn simulate_remove_liquidity_buy( + pool_id: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_shares_in: Balance, + pool_snapshot: &PoolSnapshot, + snapshot: &StableswapSnapshot, +) -> Result<(StableswapSnapshot, TradeResult), SimulatorError> { + let asset_idx = pool_snapshot + .asset_idx(asset_out) + .ok_or(SimulatorError::AssetNotFound)?; + + let pegs: Vec<(Balance, Balance)> = pool_snapshot.pegs.to_vec(); + + let (shares_in, _fees) = hydra_dx_math::stableswap::calculate_shares_for_amount::( + &pool_snapshot.reserves, + asset_idx, + amount_out, + pool_snapshot.amplification, + pool_snapshot.share_issuance, + pool_snapshot.block_fee, + &pegs, + ) + .ok_or(SimulatorError::MathError)?; + + if shares_in > max_shares_in { + return Err(SimulatorError::LimitNotMet); + } + + let updated_pool = + pool_snapshot + .clone() + .update_shares_and_reserve(asset_out, -(amount_out as i128), -(shares_in as i128)); + let updated_snapshot = snapshot.clone().with_updated_pool(pool_id, updated_pool); + + Ok((updated_snapshot, TradeResult::new(shares_in, amount_out))) +} + +fn find_pool_id_for_snapshot( + pool_snapshot: &PoolSnapshot, + snapshot: &StableswapSnapshot, +) -> Result { + for (pool_id, pool) in &snapshot.pools { + if pool.assets == pool_snapshot.assets { + return Ok(*pool_id); + } + } + Err(SimulatorError::AssetNotFound) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_find_pool_with_share_asset() { + let mut pools = BTreeMap::new(); + + let pool_100 = PoolSnapshot { + assets: vec![10u32, 11, 12].try_into().unwrap(), + reserves: vec![ + AssetReserve::new(1000, 18), + AssetReserve::new(1000, 18), + AssetReserve::new(1000, 18), + ] + .try_into() + .unwrap(), + amplification: 100, + fee: sp_runtime::Permill::from_percent(1), + block_fee: sp_runtime::Permill::from_percent(1), + pegs: vec![(1, 1), (1, 1), (1, 1)].try_into().unwrap(), + share_issuance: 3000, + }; + pools.insert(100, pool_100); + + let snapshot = StableswapSnapshot { + pools, + min_trading_limit: 1000, + }; + + let result = find_pool(10, 11, &snapshot); + assert!(result.is_ok()); + assert_eq!(result.unwrap().0, 100); + + let result = find_pool(100, 10, &snapshot); + assert!(result.is_ok()); + assert_eq!(result.unwrap().0, 100); + + let result = find_pool(11, 100, &snapshot); + assert!(result.is_ok()); + assert_eq!(result.unwrap().0, 100); + + let result = find_pool(99, 98, &snapshot); + assert!(result.is_err()); + } +} diff --git a/pallets/ice/src/lib.rs b/pallets/ice/src/lib.rs new file mode 100644 index 0000000000..f4391668be --- /dev/null +++ b/pallets/ice/src/lib.rs @@ -0,0 +1,578 @@ +// This file is part of https://github.com/galacticcouncil/* +// +// $$$$$$$ Licensed under the Apache License, Version 2.0 (the "License") +// $$$$$$$$$$$$$ you may only use this file in compliance with the License +// $$$$$$$$$$$$$$$$$$$ +// $$$$$$$$$ Copyright (C) 2021-2024 Intergalactic, Limited (GIB) +// $$$$$$$$$$$ $$$$$$$$$$ SPDX-License-Identifier: Apache-2.0 +// $$$$$$$$$$$$$$$$$$$$$$$$$$ +// $$$$$$$$$$$$$$$$$$$$$$$ $ Built with <3 for decentralisation +// $$$$$$$$$$$$$$$$$$$ $$$$$$$ +// $$$$$$$ $$$$$$$$$$$$$$$$$$ Unless required by applicable law or agreed to in +// $ $$$$$$$$$$$$$$$$$$$$$$$ writing, software distributed under the License is +// $$$$$$$$$$$$$$$$$$$$$$$$$$ distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +// $$$$$$$$$ $$$$$$$$$$$ OR CONDITIONS OF ANY KIND, either express or implied. +// $$$$$$$$ +// $$$$$$$$$$$$$$$$$$ See the License for the specific language governing +// $$$$$$$$$$$$$ permissions and limitations under the License. +// $$$$$$$ +// $$ +// $$$$$ $$$$$ $$ $ +// $$$ $$$ $$$ $$ $$$$$ $$ $$$ $$$$ $$$$$$$ $$$$ $$$ $$$$$$ $$ $$$$$$ +// $$$ $$$ $$$ $$ $$$ $$$ $$$ $ $$ $$ $$ $$ $$ $$ $$$ $$$ +// $$$$$$$$$$$ $$ $$ $$$ $$ $$ $$$$$$$ $$ $$ $$ $$$ $$ $$ +// $$$ $$$ $$$$ $$$ $$ $$ $$$ $$ $$ $$ $$ $$ $$ $$ +// $$$$$ $$$$$ $$ $$$$$$$$ $ $$$ $$$$$$$$ $$$ $$$$ $$$$$$$ $$$$ $$$$ +// $$$ + +#![recursion_limit = "256"] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod tests; + +pub mod traits; +mod weights; + +use frame_support::dispatch::DispatchResult; +use frame_support::pallet_prelude::*; +use frame_support::traits::ExistenceRequirement::AllowDeath; +use frame_support::traits::Get; +use frame_support::PalletId; +use frame_system::pallet_prelude::*; +use frame_system::Origin; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::{SimulatorConfig, SimulatorSet}; +use hydradx_traits::evm::ExtraGasSupport; +use hydradx_traits::registry::Inspect; +use ice_support::AssetId; +use ice_support::Balance; +use ice_support::Intent; +use ice_support::IntentData; +use ice_support::IntentId; +use ice_support::Price; +use ice_support::ResolvedIntent; +use ice_support::Score; +use ice_support::Solution; +use orml_traits::MultiCurrency; +use pallet_route_executor::AmmTradeWeights; +use sp_core::U256; +use sp_runtime::traits::AccountIdConversion; +use sp_runtime::traits::CheckedConversion; +use sp_runtime::Permill; +use sp_std::borrow::ToOwned; +use sp_std::collections::btree_map::BTreeMap; +use sp_std::collections::btree_set::BTreeSet; +use sp_std::vec::Vec; + +pub use pallet::*; +pub use weights::WeightInfo; + +pub const UNSIGNED_TXS_PRIORITY: u64 = u64::max_value(); +/// Extra gas provided to EVM calls during solution execution. +const EXTRA_GAS: u64 = 1_000_000; +const LOG_TARGET: &str = "ice"; +const OCW_LOG_TARGET: &str = "ice::offchain_worker"; +const LOG_PREFIX: &str = "ICE#pallet_ice"; +pub(crate) const OCW_TAG_PREFIX: &str = "ice-solution"; +pub(crate) const OCW_PROVIDES: &[u8; 15] = b"submit_solution"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_system::offchain::SubmitTransaction; + use hydradx_traits::CreateBare; + use ice_solver::v2::Solver; + use ice_support::SwapType; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: + frame_system::Config + + pallet_intent::Config + + pallet_route_executor::Config + + CreateBare> + { + /// Multi currency mechanism + type Currency: MultiCurrency; + + /// Pallet id - used to create a holding account + #[pallet::constant] + type PalletId: Get; + + /// Asset registry handler + type RegistryHandler: Inspect; + + /// Simulator configuration - provides simulators and route provider for the solver + type Simulator: SimulatorConfig; + + /// Default protocol fee taken from each resolved intent's output amount. + /// Fee stays in the ICE holding account. + /// Can be overridden via governance using `set_protocol_fee`. + #[pallet::constant] + type Fee: Get; + + /// Origin that can set the protocol fee (e.g. TechnicalCommittee or Root). + type AuthorityOrigin: EnsureOrigin; + + /// Extra gas support for EVM token transfers. + type ExtraGasSupport: ExtraGasSupport; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Solution has been executed. + SolutionExecuted { + intents_executed: u64, + trades_executed: u64, + score: Score, + }, + /// Protocol fee has been updated. + ProtocolFeeSet { fee: Permill }, + } + + /// Protocol fee taken from each resolved intent's output amount. + /// Defaults to `T::Fee` constant. Can be overridden via `set_protocol_fee`. + #[pallet::storage] + #[pallet::getter(fn protocol_fee)] + pub type ProtocolFee = StorageValue<_, Permill, ValueQuery, T::Fee>; + + #[pallet::error] + pub enum Error { + /// Provided solution is not valid. + InvalidSolution, + /// Referenced intent doesn't exist. + IntentNotFound, + /// Referenced intent's owner doesn't exist. + IntentOwnerNotFound, + /// Resolution violates user's limit. + LimitViolation, + /// Trade price doesn't match execution price. + PriceInconsistency, + /// Intent was referenced multiple times. + DuplicateIntent, + /// Trade's route is invalid. + InvalidRoute, + /// Provided score doesn't match execution score. + ScoreMismatch, + /// Intent's kind is not supported. + UnsupportedIntentKind, + /// Calculation overflow. + ArithmeticOverflow, + /// Asset with specified id doesn't exists. + AssetNotFound, + /// Traded amount is bellow limit. + InvalidAmount, + } + + #[pallet::call] + impl Pallet { + /// Execute `solution` submitted by OCW. + /// + /// Solution can be executed only as a whole solution. + /// + /// Parameters: + /// - `solution`: solution to execute + /// + /// Emits: + /// - `SolutionExecuted`when `solution` was executed successfully + /// + #[pallet::call_index(0)] + #[pallet::weight({ + let mut total_w = ::WeightInfo::submit_solution().saturating_mul(solution.resolved_intents.len() as u64); + + for t in &solution.trades { + match t.direction { + SwapType::ExactOut => { + total_w = total_w.saturating_add(::WeightInfo::buy_weight(t.route.as_slice())); + } + SwapType::ExactIn => { + total_w = total_w.saturating_add(::WeightInfo::sell_weight(t.route.as_slice())); + } + } + } + + total_w + })] + pub fn submit_solution(origin: OriginFor, solution: Solution) -> DispatchResult { + ensure_none(origin)?; + + // Provide extra gas for EVM token transfers that may need it. + T::ExtraGasSupport::set_extra_gas(EXTRA_GAS); + + log::debug!(target: LOG_TARGET, "{:?}: submit_solution() [EXECUTION PHASE], solution with {:?} resolved intents, {:?} trades, score: {:?}", + LOG_PREFIX, solution.resolved_intents.len(), solution.trades.len(), solution.score); + + // V1 solver may produce solutions with no trades (perfect CoW matching) + ensure!(!solution.resolved_intents.is_empty(), Error::::InvalidSolution); + + let mut processed_intents: BTreeSet = BTreeSet::new(); + let holding_pot = Self::get_pallet_account(); + let holding_origin: OriginFor = Origin::::Signed(holding_pot.clone()).into(); + + // TODO: this is not most perform solution, verify it works and optimize + + for ResolvedIntent { id, data: intent } in &solution.resolved_intents { + Self::validate_intent_amounts(intent)?; + + let owner = pallet_intent::Pallet::::intent_owner(id).ok_or(Error::::IntentOwnerNotFound)?; + pallet_intent::Pallet::::unlock_funds(&owner, intent.asset_in(), intent.amount_in())?; + + log::debug!(target: LOG_TARGET, "{:?}: sumbit_solution(), unlock and transfer amounts, owner: {:?}, asset: {:?}, amount: {:?}", + LOG_PREFIX, owner, intent.asset_in(), intent.amount_in()); + + ::Currency::transfer( + intent.asset_in(), + &owner, + &holding_pot, + intent.amount_in(), + AllowDeath, + )?; + } + + for t in &solution.trades { + let asset_in = t.route.first().ok_or(Error::::InvalidRoute)?.asset_in; + let asset_out = t.route.last().ok_or(Error::::InvalidRoute)?.asset_out; + + let ed_in = ::RegistryHandler::existential_deposit(asset_in).unwrap_or(Balance::MAX); + let ed_out = ::RegistryHandler::existential_deposit(asset_out).unwrap_or(Balance::MAX); + + // Skip dust trades where the amount is below the existential deposit — + // near-perfect intent matching can leave a negligible AMM remainder. + if t.amount_in < ed_in || t.amount_out < ed_out { + log::debug!(target: LOG_TARGET, "{:?}: submit_solution(), skipping dust trade: amount_in {:?} (ed {:?}), amount_out {:?} (ed {:?})", + LOG_PREFIX, t.amount_in, ed_in, t.amount_out, ed_out); + continue; + } + + match t.direction { + SwapType::ExactOut => { + log::debug!(target: LOG_TARGET, "{:?}: sumbit_solution(), buying, asset_in: {:?}, asset_out: {:?}, amount_out: {:?}, max_amount_in: {:?}, route: {:?}", + LOG_PREFIX, t.route.first(), t.route.last(), t.amount_out, t.amount_in, t.route); + + pallet_route_executor::Pallet::::buy( + holding_origin.clone(), + asset_in, + asset_out, + t.amount_out.into(), + t.amount_in.into(), + t.route.clone(), + )?; + } + SwapType::ExactIn => { + log::debug!(target: LOG_TARGET, "{:?}: sumbit_solution(), selling, asset_in: {:?}, asset_out: {:?}, amount_in: {:?}, min_amount_out: {:?}, route: {:?}", + LOG_PREFIX, t.route.first(), t.route.last(), t.amount_in, t.amount_out, t.route); + + pallet_route_executor::Pallet::::sell( + holding_origin.clone(), + asset_in, + asset_out, + t.amount_in.into(), + t.amount_out.into(), + t.route.clone(), + )?; + } + } + } + + let mut exec_score: Score = 0; + let mut exec_prices: BTreeMap<(AssetId, AssetId), Price> = BTreeMap::new(); + for resolved_intent in &solution.resolved_intents { + let ResolvedIntent { id, data: resolve } = resolved_intent; + log::debug!(target: LOG_TARGET, "{:?}: sumbit_solution(), resolving intent, id: {:?}", LOG_PREFIX, id); + + ensure!(processed_intents.insert(*id), Error::::DuplicateIntent); + + let owner = pallet_intent::Pallet::::intent_owner(id).ok_or(Error::::IntentOwnerNotFound)?; + + let fee_amount = Self::protocol_fee().mul_floor(resolve.amount_out()); + let payout = resolve.amount_out().saturating_sub(fee_amount); + + log::debug!(target: LOG_TARGET, "{:?}: sumbit_solution(), transferring, id: {:?}, to: {:?}, amount: {:?}, fee: {:?}", LOG_PREFIX, id, owner, payout, fee_amount); + + ::Currency::transfer(resolve.asset_out(), &holding_pot, &owner, payout, AllowDeath)?; + + Self::validate_price_consistency(&mut exec_prices, resolve)?; + + let intent = pallet_intent::Pallet::::get_intent(id).ok_or(Error::::IntentNotFound)?; + let surplus = pallet_intent::Pallet::::compute_surplus(&intent, resolve) + .ok_or(Error::::ArithmeticOverflow)?; + log::debug!(target: LOG_TARGET, "{:?}: sumbit_solution(), id: {:?}, surplus: {:?}", LOG_PREFIX, id, surplus); + exec_score = exec_score.checked_add(surplus).ok_or(Error::::ArithmeticOverflow)?; + + pallet_intent::Pallet::::intent_resolved(&owner, resolved_intent, fee_amount)?; + } + + log::debug!(target: LOG_TARGET, "{:?}: sumbit_solution(), solution execution finished, exec_score: {:?}, score: {:?}", LOG_PREFIX, exec_score, solution.score); + ensure!(solution.score == exec_score, Error::::ScoreMismatch); + + T::ExtraGasSupport::clear_extra_gas(); + + Self::deposit_event(Event::SolutionExecuted { + intents_executed: solution.resolved_intents.len() as u64, + trades_executed: solution.trades.len() as u64, + score: solution.score, + }); + + Ok(()) + } + + /// Set the protocol fee for resolved intents. + /// + /// If `fee` matches the default constant (`T::Fee`), the storage override + /// is removed — there is no need to store the default value. + /// + /// Can only be called by `AuthorityOrigin` (e.g. TechnicalCommittee or Root). + /// + /// Emits `ProtocolFeeSet`. + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::set_protocol_fee())] + pub fn set_protocol_fee(origin: OriginFor, fee: Permill) -> DispatchResult { + T::AuthorityOrigin::ensure_origin(origin)?; + + if fee == T::Fee::get() { + ProtocolFee::::kill(); + } else { + ProtocolFee::::put(fee); + } + + Self::deposit_event(Event::ProtocolFeeSet { fee }); + + Ok(()) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn on_finalize(_n: BlockNumberFor) {} + + fn offchain_worker(block_number: BlockNumberFor) { + let Some(call) = Self::run(block_number, |intents, state| { + match Solver::>::solve(intents, state) { + Ok(solution) => Some(solution), + Err(e) => { + log::error!(target: OCW_LOG_TARGET, "{:?}: solver failed, err: {:?}", LOG_PREFIX, e); + None + } + } + }) else { + return; + }; + + let tx = >>::create_bare(call.into()); + if let Err(e) = SubmitTransaction::>::submit_transaction(tx) { + log::error!(target: OCW_LOG_TARGET, "{:?}: submit_transaction failed (validate_unsigned rejected the solution), err: {:?}", LOG_PREFIX, e); + }; + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + /// Validates unsigned transactions for solution execution + /// + /// This function ensures that only valid solution transactions originating from + /// offchain workers are accepted, and prevents unauthorized external calls. + fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { + match source { + TransactionSource::Local | TransactionSource::InBlock => { /*OCW or included in block are allowed */ } + _ => { + return InvalidTransaction::Call.into(); + } + }; + + if let Call::submit_solution { solution } = call { + if let Err(e) = Self::validate_unsigned_solution(solution) { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned rejected solution ({} intents, {} trades, score: {}), err: {:?}", + LOG_PREFIX, solution.resolved_intents.len(), solution.trades.len(), solution.score, e); + return InvalidTransaction::Call.into(); + }; + + return ValidTransaction::with_tag_prefix(OCW_TAG_PREFIX) + .priority(UNSIGNED_TXS_PRIORITY) + .and_provides(OCW_PROVIDES.to_vec()) + .longevity(1) + .propagate(false) + .build(); + } + + InvalidTransaction::Call.into() + } + } +} + +impl Pallet { + /// Function provides `holding_pot` account id. + pub fn get_pallet_account() -> T::AccountId { + T::PalletId::get().into_account_truncating() + } + + /// Function validates if intent was resolved based on execution price. + /// Execution prices are computed on demand based on first trade trading `resolve`'s assets in same + /// direction. + /// `exeuction_prices` are [out/in] => [in] * [out/in] = [out] + fn validate_price_consistency( + execution_prices: &mut BTreeMap<(AssetId, AssetId), Price>, + resolve: &IntentData, + ) -> Result<(), DispatchError> { + { + let asset_in = resolve.asset_in(); + let asset_out = resolve.asset_out(); + + let exec_price = if let Some(ep) = execution_prices.get(&(asset_in, asset_out)) { + ep + } else { + let new_price = Ratio { + n: resolve.amount_out(), + d: resolve.amount_in(), + }; + + execution_prices.insert((asset_in, asset_out), new_price); + + &new_price.clone() + }; + + let expected_out: u128 = U256::from(resolve.amount_in()) + .checked_mul(U256::from(exec_price.n)) + .ok_or(Error::::ArithmeticOverflow)? + .checked_div(U256::from(exec_price.d)) + .ok_or(Error::::ArithmeticOverflow)? + .checked_into() + .ok_or(Error::::ArithmeticOverflow)?; + + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_price_consistency(), price: {:?}, amount_in: {:?}, calculated_out: {:?}, intent_out: {:?}", + LOG_PREFIX, exec_price, resolve.amount_in(), expected_out, resolve.amount_out()); + + ensure!( + expected_out.abs_diff(resolve.amount_out()) <= 1, + Error::::PriceInconsistency + ); + + Ok(()) + } + } + + /// Function validates intent's `amount_in` and `amount_out` values are bigger than existential + /// deposit. + fn validate_intent_amounts(intent: &IntentData) -> Result<(), DispatchError> { + let ed_in = + ::RegistryHandler::existential_deposit(intent.asset_in()).ok_or(Error::::AssetNotFound)?; + let ed_out = + ::RegistryHandler::existential_deposit(intent.asset_out()).ok_or(Error::::AssetNotFound)?; + + log::debug!(target: LOG_TARGET, "{:?}: validate_intent_amounts(), ed_in: {:?}, amount_in: {:?}, ed_out: {:?}, amount_out: {:?}", + LOG_PREFIX, ed_in, intent.amount_in(), ed_out, intent.amount_out()); + + if intent.amount_in() < ed_in { + log::error!(target: LOG_TARGET, "{:?}: validate_intent_amounts() FAILED: amount_in {:?} < existential_deposit {:?} for asset {:?}", + LOG_PREFIX, intent.amount_in(), ed_in, intent.asset_in()); + return Err(Error::::InvalidAmount.into()); + } + if intent.amount_out() < ed_out { + log::error!(target: LOG_TARGET, "{:?}: validate_intent_amounts() FAILED: amount_out {:?} < existential_deposit {:?} for asset {:?}", + LOG_PREFIX, intent.amount_out(), ed_out, intent.asset_out()); + return Err(Error::::InvalidAmount.into()); + } + + Ok(()) + } + + /// Function validates provided solution and returns solution's score if solution is + /// valid. + fn validate_unsigned_solution(solution: &Solution) -> Result<(), DispatchError> { + //TODO: + // * add weight rule and make sure solution respects it. + + let mut processed_intents: BTreeSet = BTreeSet::new(); + let mut score: Score = 0; + let mut exec_prices: BTreeMap<(AssetId, AssetId), Price> = BTreeMap::new(); + for ResolvedIntent { id, data: resolve } in &solution.resolved_intents { + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), resolved intent, id: {:?}", LOG_PREFIX, id); + + if let Err(e) = Self::validate_intent_amounts(resolve) { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), intent {:?} failed amount validation: {:?}", LOG_PREFIX, id, e); + return Err(e); + } + + let intent = pallet_intent::Pallet::::get_intent(id).ok_or_else(|| { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), intent {:?} not found in storage", LOG_PREFIX, id); + Error::::IntentNotFound + })?; + + let surplus = + pallet_intent::Pallet::::compute_surplus(&intent, resolve).ok_or(Error::::ArithmeticOverflow)?; + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), id: {:?}, surplus: {:?}", LOG_PREFIX, id, surplus); + score = score.checked_add(surplus).ok_or(Error::::ArithmeticOverflow)?; + + if !processed_intents.insert(*id) { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), intent {:?} is duplicate", LOG_PREFIX, id); + return Err(Error::::DuplicateIntent.into()); + } + + if let Err(e) = pallet_intent::Pallet::::validate_resolve(&intent, resolve) { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), intent {:?} failed resolve validation: {:?}", LOG_PREFIX, id, e); + return Err(e); + } + + if let Err(e) = Self::validate_price_consistency(&mut exec_prices, resolve) { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), intent {:?} failed price consistency: {:?}", LOG_PREFIX, id, e); + return Err(e); + } + } + + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), exec_score: {:?}, score: {:?}", LOG_PREFIX, score, solution.score); + if solution.score != score { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate_unsigned_solution(), score mismatch: solution claims {:?}, computed {:?}", LOG_PREFIX, solution.score, score); + return Err(Error::::ScoreMismatch.into()); + } + Ok(()) + } + + pub fn run(block_no: BlockNumberFor, solve: F) -> Option> + where + F: FnOnce( + Vec, + <::Simulators as SimulatorSet>::State, + ) -> Option, + { + let intents: Vec = pallet_intent::Pallet::::get_valid_intents() + .iter() + .map(|x| Intent { + id: x.0, + data: x.1.data.to_owned(), + }) + .collect(); + + log::debug!(target: OCW_LOG_TARGET, "{:?}: run(), block: {:?}, valid intents: {:?}", LOG_PREFIX, block_no, intents.len()); + + if intents.is_empty() { + return None; + } + + let state = <::Simulator as SimulatorConfig>::Simulators::initial_state(); + + let Some(solution) = solve(intents, state) else { + log::debug!(target: OCW_LOG_TARGET, "{:?}: solver returned no solution, block: {:?}", LOG_PREFIX, block_no); + return None; + }; + + if solution.resolved_intents.is_empty() { + log::debug!(target: OCW_LOG_TARGET, "{:?}: solver returned empty solution (no resolvable intents), block: {:?}", LOG_PREFIX, block_no); + return None; + } + + if let Err(e) = Self::validate_unsigned_solution(&solution) { + log::error!(target: OCW_LOG_TARGET, "{:?}: validate solution, err: {:?}, block: {:?}", LOG_PREFIX, e, block_no); + return None; + } + + Some(Call::submit_solution { solution }) + } +} diff --git a/pallets/ice/src/tests/mock.rs b/pallets/ice/src/tests/mock.rs new file mode 100644 index 0000000000..43cdddd106 --- /dev/null +++ b/pallets/ice/src/tests/mock.rs @@ -0,0 +1,642 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate as pallet_ice; +use crate::*; +use frame_support::parameter_types; +use frame_support::storage::with_transaction; +use frame_support::traits::Everything; +use frame_support::PalletId; +use frame_system::ensure_signed; +use frame_system::pallet_prelude::OriginFor; +use frame_system::EnsureRoot; +use hydra_dx_math::types::Ratio; +use hydradx_traits::amm::{RouteDiscovery, SimulatorConfig, SimulatorError, SimulatorSet, TradeResult}; +use hydradx_traits::registry::Inspect; +use hydradx_traits::router::{PoolType, Route}; +use hydradx_traits::OraclePeriod; +use hydradx_traits::PriceOracle; +use ice_support::SwapType; +use orml_traits::parameter_type_with_key; +use orml_traits::MultiCurrency; +use pallet_intent::types::CallData; +use pallet_intent::types::IntentInput; +use pallet_route_executor::ExecutorError; +use pallet_route_executor::Trade; +use pallet_route_executor::TradeExecution; +pub use primitives::constants::time::SLOT_DURATION; +use sp_core::ConstU32; +use sp_core::ConstU64; +use sp_core::H256; +use sp_runtime::traits::BlakeTwo256; +use sp_runtime::traits::IdentityLookup; +use sp_runtime::BuildStorage; +use sp_runtime::DispatchError; +use sp_runtime::DispatchResult; +use sp_runtime::FixedU128; +use sp_runtime::Permill; +use sp_runtime::TransactionOutcome; + +use std::cell::RefCell; +use std::vec; + +type Block = frame_system::mocking::MockBlock; + +pub type AccountId = u64; +pub type AssetId = u32; +pub type Balance = u128; + +pub(crate) const ONE_DOT: u128 = 10_000_000_000; +pub(crate) const ONE_HDX: u128 = 1_000_000_000_000; +pub(crate) const ONE_QUINTIL: u128 = 1_000_000_000_000_000_000; + +//Assets +pub(crate) const HDX: AssetId = 0; +pub(crate) const HUB_ASSET_ID: AssetId = 1; +pub(crate) const DOT: AssetId = 2; +pub(crate) const _GETH: AssetId = 3; +pub(crate) const ETH: AssetId = 4; + +//5 SEC. +pub(crate) const MAX_INTENT_DEADLINE: pallet_intent::types::Moment = 5 * ONE_SECOND; +pub(crate) const ONE_SECOND: pallet_intent::types::Moment = 1_000; + +//Accounts +//acccounts holding amount in for all router dummy pools +const ROUTER_POOLS_POT: AccountId = 1; +pub(crate) const ALICE: AccountId = 2; +pub(crate) const BOB: AccountId = 3; +pub(crate) const DAVE: AccountId = 4; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Currencies: orml_tokens, + Timestamp: pallet_timestamp, + Intents: pallet_intent, + Router: pallet_route_executor, + Broadcast: pallet_broadcast, + ICE: pallet_ice, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 63; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = u64; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); + type ExtensionsWeightInfo = (); +} + +pub(crate) type Extrinsic = sp_runtime::testing::TestXt; +impl frame_system::offchain::CreateTransactionBase for Test +where + RuntimeCall: From, +{ + type RuntimeCall = RuntimeCall; + type Extrinsic = Extrinsic; +} + +impl hydradx_traits::CreateBare for Test +where + RuntimeCall: From, +{ + fn create_bare(call: Self::RuntimeCall) -> Extrinsic { + Extrinsic::new_bare(call) + } +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: AssetId| -> Balance { + 0 + }; +} + +impl orml_tokens::Config for Test { + type Balance = Balance; + type Amount = i128; + type CurrencyId = AssetId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = Everything; +} + +parameter_types! { + pub const MinimumPeriod: u64 = SLOT_DURATION / 2; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type OnTimestampSet = (); + type MinimumPeriod = MinimumPeriod; + type WeightInfo = (); +} + +pub struct DummyLazyExecutor(sp_std::marker::PhantomData); +impl hydradx_traits::lazy_executor::Mutate for DummyLazyExecutor { + type Error = DispatchError; + type BoundedCall = CallData; + + fn queue( + _src: hydradx_traits::lazy_executor::Source, + _origin: AccountId, + _call: Self::BoundedCall, + ) -> Result<(), Self::Error> { + Ok(()) + } +} + +pub struct DummyRegistry; + +impl Inspect for DummyRegistry { + type AssetId = AssetId; + type Location = u8; + + fn asset_symbol(_id: Self::AssetId) -> Option> { + todo!() + } + + fn is_sufficient(_id: Self::AssetId) -> bool { + todo!() + } + + fn asset_name(_id: Self::AssetId) -> Option> { + todo!() + } + + fn asset_type(_id: Self::AssetId) -> Option { + todo!() + } + + fn is_banned(_id: Self::AssetId) -> bool { + todo!() + } + + fn decimals(_id: Self::AssetId) -> Option { + todo!() + } + + fn exists(_id: Self::AssetId) -> bool { + todo!() + } + + fn existential_deposit(_id: Self::AssetId) -> Option { + Some(1_000) + } +} + +impl pallet_intent::Config for Test { + type Currency = Currencies; + type LazyExecutorHandler = DummyLazyExecutor; + type RegistryHandler = DummyRegistry; + type TimestampProvider = Timestamp; + type HubAssetId = ConstU32; + type MaxAllowedIntentDuration = ConstU64; + type OraclePriceProvider = PairPriceProviderMock; + type BlockNumberProvider = System; + type MinDcaPeriod = ConstU32<5>; + type MaxIntentsPerAccount = ConstU32<100>; + type WeightInfo = (); +} + +pub struct PairPriceProviderMock; +impl hydradx_traits::price::PriceProvider for PairPriceProviderMock { + type Price = hydra_dx_math::ema::EmaPrice; + + fn get_price(asset_a: AssetId, asset_b: AssetId) -> Option { + if asset_a > 2000 || asset_b > 2000 { + return None; + } + Some(hydra_dx_math::ema::EmaPrice::new(88, 100)) + } +} + +impl pallet_broadcast::Config for Test {} + +parameter_types! { + pub const IceId: PalletId = PalletId(*b"iceTest#"); + pub const IceFee: Permill = Permill::from_percent(0); +} + +pub struct NoOpExtraGas; + +impl hydradx_traits::evm::ExtraGasSupport for NoOpExtraGas { + fn set_extra_gas(_gas: u64) {} + fn clear_extra_gas() {} + fn out_of_gas_error() -> sp_runtime::DispatchError { + sp_runtime::DispatchError::Other("OutOfGas") + } +} + +impl pallet_ice::Config for Test { + type Currency = Currencies; + type PalletId = IceId; + type Fee = IceFee; + type AuthorityOrigin = EnsureRoot; + type RegistryHandler = DummyRegistry; + type Simulator = TestSimulatorConfig; + type ExtraGasSupport = NoOpExtraGas; + type WeightInfo = (); +} + +// Mock SimulatorConfig +pub struct TestSimulatorConfig; + +impl SimulatorConfig for TestSimulatorConfig { + type Simulators = MockSimulatorSet; + type RouteDiscovery = MockRouteDiscovery; + type PriceDenominator = NativeCurrencyId; +} + +// Mock SimulatorSet +pub struct MockSimulatorSet; + +impl SimulatorSet for MockSimulatorSet { + type State = (); + + fn initial_state() -> Self::State {} + + fn simulate_sell( + _pool_type: PoolType, + _asset_in: AssetId, + _asset_out: AssetId, + _amount_in: Balance, + _min_amount_out: Balance, + _state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + Err(SimulatorError::Other) + } + + fn simulate_buy( + _pool_type: PoolType, + _asset_in: AssetId, + _asset_out: AssetId, + _amount_out: Balance, + _max_amount_in: Balance, + _state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + Err(SimulatorError::Other) + } + + fn get_spot_price( + _pool_type: PoolType, + _asset_in: AssetId, + _asset_out: AssetId, + _state: &Self::State, + ) -> Result { + Ok(Ratio::new(1, 1)) + } + + fn can_trade( + _asset_in: primitives::AssetId, + _asset_out: primitives::AssetId, + _state: &Self::State, + ) -> Option> { + None + } + + fn pool_edges(_state: &Self::State) -> Vec> { + Vec::new() + } +} + +// Mock RouteDiscovery +pub struct MockRouteDiscovery; + +impl RouteDiscovery<()> for MockRouteDiscovery { + fn discover_routes( + _asset_in: AssetId, + _asset_out: AssetId, + _state: &(), + ) -> Result>, SimulatorError> { + Err(SimulatorError::AssetNotFound) + } +} + +parameter_types! { + pub NativeCurrencyId: AssetId = HDX; + pub DefaultRoutePoolType: PoolType = PoolType::Omnipool; + pub const RouteValidationOraclePeriod: OraclePeriod = OraclePeriod::TenMinutes; + + pub const RouterPalletId: PalletId = PalletId(*b"routerac"); +} + +impl pallet_route_executor::Config for Test { + type AssetId = AssetId; + type Balance = Balance; + type NativeAssetId = NativeCurrencyId; + type Currency = Currencies; + type AMM = RouterPools; + type OraclePriceProvider = PriceProviderMock; + type OraclePeriod = RouteValidationOraclePeriod; + type DefaultRoutePoolType = DefaultRoutePoolType; + type ForceInsertOrigin = EnsureRoot; + type WeightInfo = (); +} + +pub struct PriceProviderMock {} + +impl PriceOracle for PriceProviderMock { + type Price = Ratio; + + fn price(route: &[Trade], _: OraclePeriod) -> Option { + let has_insufficient_asset = route.iter().any(|t| t.asset_in > 2000 || t.asset_out > 2000); + if has_insufficient_asset { + return None; + } + Some(Ratio::new(88, 100)) + } +} + +#[derive(Debug)] +struct RouterSettlement { + trade_type: SwapType, + pool_type: pallet_route_executor::PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount: Balance, + amount_in: Balance, + amount_out: Balance, +} +thread_local! { + pub static ROUTER_SETTLEMENTS: RefCell> = RefCell::new(Vec::default()); +} + +type OriginForRuntime = OriginFor; +pub struct RouterPools; +impl TradeExecution for RouterPools { + type Error = DispatchError; + + fn execute_buy( + who: OriginForRuntime, + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + _max_limit: Balance, + ) -> Result<(), ExecutorError> { + ROUTER_SETTLEMENTS.with(|v| { + let mut m = v.borrow_mut(); + + let idx = m + .iter() + .position(|x| { + //NOTE: who is router account at this point we can't match on it + x.trade_type == SwapType::ExactOut + && x.pool_type == pool_type + && x.asset_in == asset_in + && x.asset_out == asset_out + && x.amount == amount_out + }) + .expect("router result to exist"); + + let p = m.get(idx).expect("item to exits in router pools results"); + + let dest = ensure_signed(who.clone()).expect("origin should works"); + Currencies::transfer(who, ROUTER_POOLS_POT, asset_in, p.amount_in).expect("currencies transfer to works"); + Currencies::deposit(p.asset_out, &dest, p.amount_out).expect("currencies deposit to works"); + + m.remove(idx); + + Ok(()) + }) + } + + fn execute_sell( + who: OriginForRuntime, + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + _min_limit: Balance, + ) -> Result<(), ExecutorError> { + ROUTER_SETTLEMENTS.with(|v| { + let mut m = v.borrow_mut(); + + let idx = m + .iter() + .position(|x| { + //NOTE: who is router account at this point we can't match on it + x.trade_type == SwapType::ExactIn + && x.pool_type == pool_type + && x.asset_in == asset_in + && x.asset_out == asset_out + && x.amount == amount_in + }) + .expect("router result to exist"); + + let p = m.get(idx).expect("item to exits in router pools results"); + + let dest = ensure_signed(who.clone()).expect("origin should works"); + Currencies::transfer(who, ROUTER_POOLS_POT, asset_in, p.amount_in).expect("currencies transfer to works"); + Currencies::deposit(p.asset_out, &dest, p.amount_out).expect("currencies deposit to works"); + + m.remove(idx); + + Ok(()) + }) + } + + fn get_liquidity_depth( + _pool_type: PoolType, + _asset_a: AssetId, + _asset_b: AssetId, + ) -> Result> { + Err(ExecutorError::Error(DispatchError::Other("Not Implemented 1"))) + } + + fn calculate_out_given_in( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + ) -> Result> { + ROUTER_SETTLEMENTS.with(|v| { + let m = v.borrow(); + + let idx = m + .iter() + .position(|x| { + x.trade_type == SwapType::ExactIn + && x.pool_type == pool_type + && x.asset_in == asset_in + && x.asset_out == asset_out + && x.amount == amount_in + }) + .expect("router result to exist"); + + let p = m.get(idx).expect("item to exits in router pools results"); + + Ok(p.amount_out) + }) + } + + fn calculate_in_given_out( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + ) -> Result> { + ROUTER_SETTLEMENTS.with(|v| { + let m = v.borrow(); + + let idx = m + .iter() + .position(|x| { + x.trade_type == SwapType::ExactOut + && x.pool_type == pool_type + && x.asset_in == asset_in + && x.asset_out == asset_out + && x.amount == amount_out + }) + .expect("router result to exist"); + + let p = m.get(idx).expect("item to exits in router pools results"); + + Ok(p.amount_in) + }) + } + + fn calculate_spot_price_with_fee( + _pool_type: PoolType, + _asset_a: AssetId, + _asset_b: AssetId, + ) -> Result> { + Err(ExecutorError::Error(DispatchError::Other("Not Implemented 4"))) + } +} + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, AssetId, Balance)>, + intents: Vec<(AccountId, IntentInput)>, + router_settlements: Vec, +} + +impl Default for ExtBuilder { + fn default() -> Self { + ROUTER_SETTLEMENTS.with(|v| { + v.borrow_mut().clear(); + }); + + Self { + endowed_accounts: vec![], + intents: vec![], + router_settlements: vec![], + } + } +} + +impl ExtBuilder { + pub fn with_endowed_accounts(mut self, accounts: Vec<(AccountId, AssetId, Balance)>) -> Self { + self.endowed_accounts = accounts; + self + } + + pub fn with_intents(mut self, intents: Vec<(AccountId, IntentInput)>) -> Self { + self.intents = intents; + self + } + + pub fn with_router_settlement( + mut self, + trade_type: SwapType, + pool_type: pallet_route_executor::PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount: Balance, + amount_in: Balance, + amount_out: Balance, + ) -> Self { + self.router_settlements.push(RouterSettlement { + trade_type, + pool_type, + asset_in, + asset_out, + amount, + amount_in, + amount_out, + }); + self + } + + pub fn build(self) -> sp_io::TestExternalities { + for rr in self.router_settlements { + ROUTER_SETTLEMENTS.with(|v| { + v.borrow_mut().push(rr); + }); + } + + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self + .endowed_accounts + .iter() + .flat_map(|(x, asset, amount)| vec![(*x, *asset, *amount)]) + .collect(), + } + .assimilate_storage(&mut t) + .unwrap(); + let mut r: sp_io::TestExternalities = t.into(); + + r.execute_with(|| { + frame_system::Pallet::::set_block_number(1); + + let _ = with_transaction(|| { + for (owner, intent) in self.intents { + pallet_intent::Pallet::::add_intent(owner, intent).expect("add_intent should work"); + } + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + + r + } +} diff --git a/pallets/ice/src/tests/mod.rs b/pallets/ice/src/tests/mod.rs new file mode 100644 index 0000000000..9799160b91 --- /dev/null +++ b/pallets/ice/src/tests/mod.rs @@ -0,0 +1,4 @@ +mod mock; +mod ocw; +mod submit_solution; +mod validate_price_consistency; diff --git a/pallets/ice/src/tests/ocw.rs b/pallets/ice/src/tests/ocw.rs new file mode 100644 index 0000000000..8df983f411 --- /dev/null +++ b/pallets/ice/src/tests/ocw.rs @@ -0,0 +1,1230 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_noop; +use ice_support::IntentDataInput; +use ice_support::Partial; +use ice_support::PoolTrade; +use ice_support::SwapData; +use ice_support::SwapParams; +use ice_support::SwapType; +use pallet_intent::types::IntentInput; +use pallet_route_executor::PoolType; +use pallet_route_executor::Trade as RTrade; +use pretty_assertions::assert_eq; + +#[test] +fn validate_unsingned_should_work_when_submitted_solution_is_valid() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 17_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 17_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 17_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 5 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 17_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 1_000_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s }; + + assert_eq!( + ICE::validate_unsigned(TransactionSource::Local, &call), + Ok(ValidTransaction { + priority: UNSIGNED_TXS_PRIORITY, + requires: vec![], + provides: vec![(OCW_TAG_PREFIX, OCW_PROVIDES.to_vec()).encode()], + longevity: 1, + propagate: false + }) + ); + }); +} + +#[test] +fn validate_unsingned_should_not_work_when_submitted_solution_score_is_not_correct() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 17_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 17_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 17_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 5 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 17_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 1_000_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s.clone() }; + + assert_eq!( + ICE::validate_unsigned(TransactionSource::Local, &call), + Ok(ValidTransaction { + priority: UNSIGNED_TXS_PRIORITY, + requires: vec![], + provides: vec![(OCW_TAG_PREFIX, OCW_PROVIDES.to_vec()).encode()], + longevity: 1, + propagate: false + }) + ); + + //Act 1 + let mut s1 = s.clone(); + s1.score -= 1; + let call = Call::submit_solution { solution: s1 }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + + //Act 2 + let mut s2 = s.clone(); + s2.score += 1; + let call = Call::submit_solution { solution: s2 }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + + //Act 3 + let mut s3 = s.clone(); + s3.score = 0; + let call = Call::submit_solution { solution: s3 }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + + //Act 4 + let mut s4 = s.clone(); + s4.score = Score::max_value(); + let call = Call::submit_solution { solution: s4 }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }) +} + +#[test] +fn validate_unsingned_should_not_work_when_intentent_not_found() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 73786976294838206464002_u128, + data: IntentData::Swap(SwapData { + asset_in: 4, + asset_out: 0, + amount_in: 500000000000000000, + amount_out: 16000000000000000000, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464001_u128 - 10, //intent that doesn't exist + data: IntentData::Swap(SwapData { + asset_in: 0, + asset_out: 2, + amount_in: 10000000000000000, + amount_out: 100000000000, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464000_u128, + data: IntentData::Swap(SwapData { + asset_in: 0, + asset_out: 2, + amount_in: 5000000000000000, + amount_out: 50000000000, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s.clone() }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }) +} + +#[test] +fn validate_unsingned_should_not_work_when_solution_has_duplicate_intents() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 73786976294838206464002_u128, + data: IntentData::Swap(SwapData { + asset_in: 4, + asset_out: 0, + amount_in: 500000000000000000, + amount_out: 16000000000000000000, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464001_u128, + data: IntentData::Swap(SwapData { + asset_in: 0, + asset_out: 2, + amount_in: 10000000000000000, + amount_out: 100000000000, + partial: Partial::No, + }), + }, + //Duplicate intent - copy of 1th + ResolvedIntent { + id: 73786976294838206464002_u128, + data: IntentData::Swap(SwapData { + asset_in: 4, + asset_out: 0, + amount_in: 500000000000000000, + amount_out: 16000000000000000000, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s.clone() }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }) +} + +#[test] +fn validate_unsingned_should_not_work_when_solution_have_intent_with_amount_in_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 73786976294838206464002_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464001_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464000_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: DummyRegistry::existential_deposit(HDX).expect("dummy registry to work") - 1, + amount_out: 5 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} + +#[test] +fn validate_unsingned_should_not_work_when_solution_have_intent_with_amount_out_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 73786976294838206464002_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464001_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464000_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: DummyRegistry::existential_deposit(DOT).expect("dummy registry to work") - 1, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} + +#[test] +fn validate_unsigned_should_not_work_when_execution_prices_are_not_consistent() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 73786976294838206464002_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500000000000000000, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464001_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464000_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 6 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} + +#[test] +fn validate_unsigned_should_not_work_when_intent_is_not_resolved_at_execution_price() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 73786976294838206464002_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500000000000000000, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464001_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 73786976294838206464000_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 6 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + let call = Call::submit_solution { solution: s }; + + assert_noop!( + ICE::validate_unsigned(TransactionSource::Local, &call), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} diff --git a/pallets/ice/src/tests/submit_solution.rs b/pallets/ice/src/tests/submit_solution.rs new file mode 100644 index 0000000000..c2b7d28ff2 --- /dev/null +++ b/pallets/ice/src/tests/submit_solution.rs @@ -0,0 +1,1272 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_noop; +use frame_support::assert_ok; +use ice_support::IntentDataInput; +use ice_support::Partial; +use ice_support::PoolTrade; +use ice_support::Solution; +use ice_support::SwapData; +use ice_support::SwapParams; +use ice_support::SwapType; +use pallet_intent::types::IntentInput; +use pallet_route_executor::PoolType; +use pallet_route_executor::Trade as RTrade; +use pretty_assertions::assert_eq; + +#[test] +fn solution_execution_should_work_when_solution_is_valid() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 17_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 17_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 17_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 5 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 17_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 1_000_000_030_000_000_000_u128, + }; + + assert_ok!(ICE::submit_solution(RuntimeOrigin::none(), s)); + }); +} + +#[test] +fn solution_execution_should_not_work_when_score_is_not_valid() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 17_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 17_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 17_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 5 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 17_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_000_000_000_000_u128, + }; + + assert_noop!( + ICE::submit_solution(RuntimeOrigin::none(), s), + Error::::ScoreMismatch + ); + }); +} + +#[test] +fn solution_execution_should_not_work_when_contains_duplicate_intents() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 0_u128, + }; + + assert_noop!( + ICE::submit_solution(RuntimeOrigin::none(), s), + Error::::DuplicateIntent + ); + }); +} + +#[test] +fn solution_execution_should_not_work_when_intent_owner_is_not_found() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 999999999_u128, + data: IntentData::Swap(SwapData { + asset_in: 4, + asset_out: 0, + amount_in: 500000000000000000, + amount_out: 16000000000000000000, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: 0, + asset_out: 2, + amount_in: 10000000000000000, + amount_out: 100000000000, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: 0, + asset_out: 2, + amount_in: 5000000000000000, + amount_out: 50000000000, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + assert_noop!( + ICE::submit_solution(RuntimeOrigin::none(), s), + Error::::IntentOwnerNotFound + ); + }); +} + +#[test] +fn solution_execution_should_work_when_solution_has_single_intent() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 5_000 * ONE_HDX, + 5_000 * ONE_HDX, + 5 * ONE_DOT, + ) + .build() + .execute_with(|| { + let resolved = vec![ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: 0, + asset_out: 2, + amount_in: 5000000000000000, + amount_out: 50000000000, + partial: Partial::No, + }), + }]; + + let trades = vec![PoolTrade { + amount_in: 5_000 * ONE_HDX, + amount_out: 5 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 10_000_000_000_u128, + }; + + assert_ok!(ICE::submit_solution(RuntimeOrigin::none(), s)); + }); +} + +#[test] +fn solution_execution_should_work_when_solution_has_zero_score() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 5 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 5_000 * ONE_HDX, + 5_000 * ONE_HDX, + 5 * ONE_DOT, + ) + .build() + .execute_with(|| { + let resolved = vec![ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: 0, + asset_out: 2, + amount_in: 5000000000000000, + amount_out: 50000000000, + partial: Partial::No, + }), + }]; + + let trades = vec![PoolTrade { + amount_in: 5_000 * ONE_HDX, + amount_out: 5 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 0_u128, + }; + + assert_ok!(ICE::submit_solution(RuntimeOrigin::none(), s)); + }); +} + +#[test] +fn solution_execution_should_not_work_when_solution_have_intent_with_amount_in_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: DummyRegistry::existential_deposit(HDX).expect("dummy registry to work") - 1, + amount_out: 5 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + assert_noop!( + ICE::submit_solution(RuntimeOrigin::none(), s), + Error::::InvalidAmount + ); + }); +} + +#[test] +fn solution_execution_should_not_work_when_solution_have_intent_with_amount_out_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 16_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 16_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500000000000000000, + amount_out: 16_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: DummyRegistry::existential_deposit(DOT).expect("dummy registry to work") - 1, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 12 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 500_000_030_000_000_000_u128, + }; + + assert_noop!( + ICE::submit_solution(RuntimeOrigin::none(), s), + Error::::InvalidAmount + ); + }); +} + +#[test] +fn solution_execution_should_not_work_when_intent_is_not_resolved_at_execution_price() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 10_000 * ONE_HDX), + (ALICE, DOT, 10_000 * ONE_DOT), + (BOB, HDX, 10_000 * ONE_HDX), + (BOB, ETH, 10_000 * ONE_QUINTIL), + (DAVE, HDX, 20_000 * ONE_HDX), + (DAVE, DOT, 20_000 * ONE_DOT), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + DAVE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 8 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: HDX, + amount_in: ONE_QUINTIL / 2, + amount_out: 16_000_000 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .with_router_settlement( + SwapType::ExactIn, + PoolType::XYK, + HDX, + DOT, + 15_000 * ONE_HDX, + 15_000 * ONE_HDX, + 15 * ONE_DOT, + ) + .with_router_settlement( + SwapType::ExactOut, + PoolType::Omnipool, + ETH, + HDX, + 17_000_000 * ONE_HDX, + ONE_QUINTIL / 2, + 17_000_000 * ONE_HDX, + ) + .build() + .execute_with(|| { + let resolved = vec![ + ResolvedIntent { + id: 2_u128, + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: HDX, + amount_in: 500_000_000_000_000_000, + amount_out: 17_000_000 * ONE_HDX, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 1_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 10_000 * ONE_HDX, + amount_out: 10 * ONE_DOT, + partial: Partial::No, + }), + }, + ResolvedIntent { + id: 0_u128, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: 5_000 * ONE_HDX, + amount_out: 4 * ONE_DOT, + partial: Partial::No, + }), + }, + ]; + + let trades = vec![ + PoolTrade { + amount_in: 15_000 * ONE_HDX, + amount_out: 15 * ONE_DOT, + direction: SwapType::ExactIn, + route: vec![RTrade { + pool: PoolType::XYK, + asset_in: HDX, + asset_out: DOT, + }] + .try_into() + .unwrap(), + }, + PoolTrade { + amount_in: ONE_QUINTIL / 2, + amount_out: 17_000_000 * ONE_HDX, + direction: SwapType::ExactOut, + route: vec![RTrade { + pool: PoolType::Omnipool, + asset_in: ETH, + asset_out: HDX, + }] + .try_into() + .unwrap(), + }, + ]; + + let s = Solution { + resolved_intents: resolved.try_into().unwrap(), + trades: trades.try_into().unwrap(), + score: 1_000_000_030_000_000_000_u128, + }; + + assert_noop!( + ICE::submit_solution(RuntimeOrigin::none(), s), + Error::::PriceInconsistency + ); + }); +} diff --git a/pallets/ice/src/tests/validate_price_consistency.rs b/pallets/ice/src/tests/validate_price_consistency.rs new file mode 100644 index 0000000000..2ee6090fcc --- /dev/null +++ b/pallets/ice/src/tests/validate_price_consistency.rs @@ -0,0 +1,116 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_err; +use frame_support::assert_ok; +use ice_support::AssetId; +use ice_support::Partial; +use ice_support::Price; +use ice_support::SwapData; +use pretty_assertions::assert_eq; +use sp_std::collections::btree_map::BTreeMap; + +#[test] +fn should_work_when_price_wasnt_computed_yet_and_reverse_price_is_missing() { + let asset_in = HDX; + let asset_out = DOT; + let amount_in = 100 * ONE_HDX; + let amount_out = 200 * ONE_DOT; + + let resolve = IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::No, + }); + + let mut exec_prices: BTreeMap<(AssetId, AssetId), Price> = BTreeMap::new(); + assert_ok!(ICE::validate_price_consistency(&mut exec_prices, &resolve)); + + assert_eq!( + *exec_prices + .get(&(asset_in, asset_out)) + .expect("excution price to exists"), + Ratio::new(amount_out, amount_in) + ); + + let resolve = IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out, + partial: Partial::No, + }); + + let mut exec_prices: BTreeMap<(AssetId, AssetId), Price> = BTreeMap::new(); + assert_ok!(ICE::validate_price_consistency(&mut exec_prices, &resolve)); + + assert_eq!( + *exec_prices + .get(&(asset_in, asset_out)) + .expect("excution price to exists"), + Ratio::new(amount_out, amount_in) + ); +} + +#[test] +fn should_fail_when_not_resolved_at_execution_price() { + let asset_in = HDX; + let asset_out = DOT; + let amount_in = 100 * ONE_HDX; + let amount_out = 200 * ONE_DOT; + + let resolve = IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + amount_out: amount_out + 2, + partial: Partial::No, + }); + + let mut exec_prices: BTreeMap<(AssetId, AssetId), Price> = BTreeMap::new(); + exec_prices.insert((asset_in, asset_out), Ratio::new(amount_out, amount_in)); + + assert_err!( + ICE::validate_price_consistency(&mut exec_prices, &resolve), + Error::::PriceInconsistency + ); + + assert_eq!(exec_prices.len(), 1); + assert_eq!( + *exec_prices + .get(&(asset_in, asset_out)) + .expect("execution price to exists"), + Ratio::new(amount_out, amount_in) + ); +} + +#[test] +fn should_work_when_not_resolved_within_execution_price_tolerance() { + let asset_in = HDX; + let asset_out = DOT; + let amount_in = 100 * ONE_HDX; + let amount_out = 200 * ONE_DOT; + + let resolve = IntentData::Swap(SwapData { + asset_in, + asset_out, + amount_in, + //NOTE: we have hadrcoded +-1 in case of rounding error + amount_out: amount_out - 1, + partial: Partial::No, + }); + + let mut exec_prices: BTreeMap<(AssetId, AssetId), Price> = BTreeMap::new(); + exec_prices.insert((asset_in, asset_out), Ratio::new(amount_out, amount_in)); + + assert_ok!(ICE::validate_price_consistency(&mut exec_prices, &resolve),); + + assert_eq!(exec_prices.len(), 1); + assert_eq!( + *exec_prices + .get(&(asset_in, asset_out)) + .expect("execution price to exists"), + Ratio::new(amount_out, amount_in) + ); +} diff --git a/pallets/ice/src/traits.rs b/pallets/ice/src/traits.rs new file mode 100644 index 0000000000..0c3dda1cea --- /dev/null +++ b/pallets/ice/src/traits.rs @@ -0,0 +1,3 @@ +// This file can be removed or kept for backwards compatibility. +// The AMMState trait has been replaced by hydradx_traits::amm::SimulatorSet +// which is configured directly in the pallet Config. diff --git a/pallets/ice/src/weights.rs b/pallets/ice/src/weights.rs new file mode 100644 index 0000000000..4e4c29799c --- /dev/null +++ b/pallets/ice/src/weights.rs @@ -0,0 +1,16 @@ +use frame_support::pallet_prelude::Weight; + +pub trait WeightInfo { + fn submit_solution() -> Weight; + fn set_protocol_fee() -> Weight; +} + +impl WeightInfo for () { + fn submit_solution() -> Weight { + Weight::default() + } + + fn set_protocol_fee() -> Weight { + Weight::default() + } +} diff --git a/pallets/ice/support/Cargo.toml b/pallets/ice/support/Cargo.toml new file mode 100644 index 0000000000..446d193bad --- /dev/null +++ b/pallets/ice/support/Cargo.toml @@ -0,0 +1,44 @@ +[package] +name = "ice-support" +version = "1.0.0" +authors = ['GalacticCouncil'] +edition = "2021" +license = "Apache-2.0" +homepage = 'https://github.com/galacticcouncil/hydration-node' +repository = 'https://github.com/galacticcouncil/hydration-node' +description = "" + + +[dependencies] +# parity +codec = { workspace = true, features = ["derive", "max-encoded-len"] } +scale-info = { workspace = true } + +# primitives +sp-std = { workspace = true } +sp-core = { workspace = true } + +# FRAME +frame-support = { workspace = true } + +# Hydration dependencies +hydradx-traits = {workspace = true} + +# Math +hydra-dx-math = { workspace = true } + + +[dev-dependencies] + + +[features] +default = ['std'] +std = [ + 'codec/std', + 'scale-info/std', + 'frame-support/std', + 'sp-std/std', + 'sp-core/std', + 'hydradx-traits/std', + 'hydra-dx-math/std', +] diff --git a/pallets/ice/support/src/lib.rs b/pallets/ice/support/src/lib.rs new file mode 100644 index 0000000000..ac749af53c --- /dev/null +++ b/pallets/ice/support/src/lib.rs @@ -0,0 +1,330 @@ +#![recursion_limit = "256"] +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::{ConstU32, RuntimeDebug, TypeInfo}; +use frame_support::sp_runtime::traits::CheckedConversion; +use frame_support::sp_runtime::Permill; +use frame_support::BoundedVec; +use hydra_dx_math::types::Ratio; +use hydradx_traits::router::Route; +use sp_core::U256; + +pub type AssetId = u32; +pub type Balance = u128; +pub type IntentId = u128; +pub type Score = u128; + +pub type PoolId = AssetId; +pub type Price = Ratio; + +pub const MAX_NUMBER_OF_RESOLVED_INTENTS: u32 = 100; +pub const MAX_NUMBER_OF_SOLUTION_TRADES: u32 = 200; + +pub type ResolvedIntents = BoundedVec>; +pub type SolutionTrades = BoundedVec>; + +pub type ResolvedIntent = Intent; + +#[derive(Clone, DecodeWithMemTracking, Debug, Encode, Decode, TypeInfo, Eq, PartialEq)] +pub struct Intent { + pub id: IntentId, + pub data: IntentData, +} + +#[derive(Clone, DecodeWithMemTracking, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub enum IntentData { + Swap(SwapData), + Dca(DcaData), +} + +/// User-facing intent data for extrinsic submission. +/// Uses SwapParams/DcaParams instead of SwapData/DcaData to avoid exposing internal state. +#[derive(Clone, DecodeWithMemTracking, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub enum IntentDataInput { + Swap(SwapParams), + Dca(DcaParams), +} + +impl IntentDataInput { + pub fn asset_in(&self) -> AssetId { + match self { + IntentDataInput::Swap(s) => s.asset_in, + IntentDataInput::Dca(d) => d.asset_in, + } + } + + pub fn asset_out(&self) -> AssetId { + match self { + IntentDataInput::Swap(s) => s.asset_out, + IntentDataInput::Dca(d) => d.asset_out, + } + } +} + +impl IntentData { + pub fn is_partial(&self) -> bool { + match self { + IntentData::Swap(s) => s.partial.is_partial(), + IntentData::Dca(_) => false, + } + } + + pub fn asset_in(&self) -> AssetId { + match self { + IntentData::Swap(s) => s.asset_in, + IntentData::Dca(d) => d.asset_in, + } + } + + pub fn asset_out(&self) -> AssetId { + match self { + IntentData::Swap(s) => s.asset_out, + IntentData::Dca(d) => d.asset_out, + } + } + + pub fn amount_in(&self) -> Balance { + match self { + IntentData::Swap(s) => s.amount_in, + IntentData::Dca(d) => d.amount_in, + } + } + + pub fn amount_out(&self) -> Balance { + match self { + IntentData::Swap(s) => s.amount_out, + IntentData::Dca(d) => d.amount_out, + } + } + + /// Function calculates surplus amount from `resolved` intent. + /// + /// Surplus must be >= zero + pub fn surplus(&self, resolve: &IntentData) -> Option { + match self { + IntentData::Swap(s) => { + let amt = if s.partial.is_partial() { + self.pro_rata(resolve)? + } else { + s.amount_out + }; + resolve.amount_out().checked_sub(amt) + } + IntentData::Dca(d) => resolve.amount_out().checked_sub(d.amount_out), + } + } + + // Function calculates pro rata amount based on `resolved` intent. + pub fn pro_rata(&self, resolve: &IntentData) -> Option { + match self { + IntentData::Swap(s) => U256::from(resolve.amount_in()) + .checked_mul(U256::from(s.amount_out))? + .checked_div(U256::from(s.amount_in))? + .checked_into(), + IntentData::Dca(_) => None, // DCA is never partial + } + } +} + +/// Whether an intent supports partial fills. +#[derive(Clone, Copy, DecodeWithMemTracking, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub enum Partial { + /// All-or-nothing: intent must be fully resolved or not at all. + No, + /// Partially fillable. `Balance` = cumulative amount_in already filled. + /// Original `amount_in` and `amount_out` are immutable; minimum rate is + /// always derived from their ratio. + Yes(Balance), +} + +impl Partial { + /// Returns the cumulative filled amount, or 0 for non-partial intents. + pub fn filled(&self) -> Balance { + match self { + Partial::No => 0, + Partial::Yes(filled) => *filled, + } + } + + pub fn is_partial(&self) -> bool { + matches!(self, Partial::Yes(_)) + } +} + +impl From for Partial { + fn from(partial: bool) -> Self { + if partial { + Partial::Yes(0) + } else { + Partial::No + } + } +} + +/// User-facing swap parameters for intent submission. +#[derive(Clone, DecodeWithMemTracking, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct SwapParams { + pub asset_in: AssetId, + pub asset_out: AssetId, + pub amount_in: Balance, + pub amount_out: Balance, + pub partial: bool, +} + +/// Stored swap data with partial fill tracking. +/// Original `amount_in` and `amount_out` are immutable — minimum rate is +/// always derived from their ratio. +#[derive(Clone, DecodeWithMemTracking, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct SwapData { + pub asset_in: AssetId, + pub asset_out: AssetId, + pub amount_in: Balance, + pub amount_out: Balance, + pub partial: Partial, +} + +impl SwapData { + /// Remaining amount that can still be filled. + pub fn remaining(&self) -> Balance { + self.amount_in.saturating_sub(self.partial.filled()) + } +} + +impl From<&SwapParams> for SwapData { + fn from(params: &SwapParams) -> Self { + SwapData { + asset_in: params.asset_in, + asset_out: params.asset_out, + amount_in: params.amount_in, + amount_out: params.amount_out, + partial: Partial::from(params.partial), + } + } +} + +/// User-facing DCA parameters for intent submission. +/// Does not include internal state fields (remaining_budget, last_execution_block) +/// which are initialized by the pallet. +#[derive(Clone, DecodeWithMemTracking, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct DcaParams { + /// Asset being sold per trade + pub asset_in: AssetId, + /// Asset being bought per trade + pub asset_out: AssetId, + /// Per-trade exact sell amount + pub amount_in: Balance, + /// Per-trade hard minimum receive (user's absolute floor) + pub amount_out: Balance, + /// Dynamic slippage tolerance applied relative to oracle price + pub slippage: Permill, + /// Total budget: Some(amount) = fixed, None = rolling/indefinite + pub budget: Option, + /// Blocks between executions + pub period: u32, +} + +impl DcaParams { + pub fn into_data(self, remaining_budget: Balance, last_execution_block: u32) -> DcaData { + DcaData { + asset_in: self.asset_in, + asset_out: self.asset_out, + amount_in: self.amount_in, + amount_out: self.amount_out, + slippage: self.slippage, + budget: self.budget, + remaining_budget, + period: self.period, + last_execution_block, + } + } +} + +#[derive(Clone, DecodeWithMemTracking, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct DcaData { + /// Asset being sold per trade + pub asset_in: AssetId, + /// Asset being bought per trade + pub asset_out: AssetId, + /// Per-trade exact sell amount + pub amount_in: Balance, + /// Per-trade hard minimum receive (user's absolute floor) + pub amount_out: Balance, + /// Dynamic slippage tolerance applied relative to oracle price + pub slippage: Permill, + /// Total budget: Some(amount) = fixed, None = rolling/indefinite + pub budget: Option, + /// Remaining reserved funds (mutable, decremented after each trade) + pub remaining_budget: Balance, + /// Blocks between executions + pub period: u32, + /// Block when DCA was last executed (or created); updated on each resolution + pub last_execution_block: u32, +} + +impl DcaData { + /// Convert DCA per-trade parameters to a SwapData for solver presentation. + /// + /// The caller supplies `amount_out` — typically the oracle-derived + /// effective limit (dynamic slippage floor), not the user's hard limit. + /// Using the effective limit keeps the solver's view of the intent in + /// sync with the pallet's resolve-time check in + /// `validate_dca_intent_resolve`. + pub fn to_swap_data(&self, amount_out: Balance) -> SwapData { + SwapData { + asset_in: self.asset_in, + asset_out: self.asset_out, + amount_in: self.amount_in, + amount_out, + partial: Partial::No, + } + } +} + +#[derive( + Copy, + DecodeWithMemTracking, + Clone, + Encode, + Decode, + Eq, + PartialEq, + RuntimeDebug, + MaxEncodedLen, + TypeInfo, + PartialOrd, + Ord, +)] +pub enum SwapType { + ExactIn, + ExactOut, +} + +impl SwapType { + pub fn reverse(&self) -> Self { + if *self == SwapType::ExactIn { + return SwapType::ExactOut; + } + + Self::ExactIn + } +} + +#[derive(Clone, Debug, Encode, Decode, TypeInfo, PartialEq, DecodeWithMemTracking, Eq)] +pub struct Solution { + pub resolved_intents: ResolvedIntents, + pub trades: SolutionTrades, + pub score: Score, +} + +#[derive(Debug, DecodeWithMemTracking, Clone, Encode, Decode, TypeInfo, PartialEq, Eq)] +pub struct PoolTrade { + /// Direction of trade (sell or buy) + pub direction: SwapType, + /// Amount of asset sold + pub amount_in: Balance, + /// Amount of asset bought + pub amount_out: Balance, + /// Type of pool used for this transaction + pub route: Route, +} diff --git a/pallets/intent/Cargo.toml b/pallets/intent/Cargo.toml new file mode 100644 index 0000000000..6412f0d491 --- /dev/null +++ b/pallets/intent/Cargo.toml @@ -0,0 +1,69 @@ +[package] +name = "pallet-intent" +version = "1.0.0" +authors = ['GalacticCouncil'] +edition = "2021" +license = "Apache-2.0" +homepage = 'https://github.com/galacticcouncil/hydration-node' +repository = 'https://github.com/galacticcouncil/hydration-node' +description = "" +readme = "README.md" + +[dependencies] +# parity +codec = { workspace = true, features = ["derive", "max-encoded-len"] } +scale-info = { workspace = true } +log = { workspace = true} + +# primitives +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-core = { workspace = true } + +# FRAME +frame-support = { workspace = true } +frame-system = { workspace = true } + +# HydraDX dependencies +hydradx-traits = { workspace = true } +ice-support = { workspace = true } + +# Math +hydra-dx-math = { workspace = true } + +# ORML dependencies +orml-traits = { workspace = true } + +# Optional imports for benchmarking +frame-benchmarking = { workspace = true, optional = true } + +[dev-dependencies] +sp-io = { workspace = true } +test-utils = { workspace = true } +orml-tokens = { workspace = true, features=["std"] } +pallet-timestamp = { workspace = true } +primitives = { workspace = true } +pretty_assertions = { workspace = true } + +[features] +default = ['std'] +std = [ + 'codec/std', + 'scale-info/std', + 'sp-runtime/std', + 'sp-core/std', + 'sp-io/std', + 'sp-std/std', + 'frame-benchmarking/std', + 'hydradx-traits/std', + 'orml-traits/std', + 'ice-support/std', + 'hydra-dx-math/std', +] + +runtime-benchmarks = [ + "frame-benchmarking", + "frame-system/runtime-benchmarks", + "frame-support/runtime-benchmarks", +] +try-runtime = ["frame-support/try-runtime"] diff --git a/pallets/intent/src/lib.rs b/pallets/intent/src/lib.rs new file mode 100644 index 0000000000..e9ae3b0a91 --- /dev/null +++ b/pallets/intent/src/lib.rs @@ -0,0 +1,882 @@ +// This file is part of https://github.com/galacticcouncil/* +// +// $$$$$$$ Licensed under the Apache License, Version 2.0 (the "License") +// $$$$$$$$$$$$$ you may only use this file in compliance with the License +// $$$$$$$$$$$$$$$$$$$ +// $$$$$$$$$ Copyright (C) 2021-2024 Intergalactic, Limited (GIB) +// $$$$$$$$$$$ $$$$$$$$$$ SPDX-License-Identifier: Apache-2.0 +// $$$$$$$$$$$$$$$$$$$$$$$$$$ +// $$$$$$$$$$$$$$$$$$$$$$$ $ Built with <3 for decentralisation +// $$$$$$$$$$$$$$$$$$$ $$$$$$$ +// $$$$$$$ $$$$$$$$$$$$$$$$$$ Unless required by applicable law or agreed to in +// $ $$$$$$$$$$$$$$$$$$$$$$$ writing, software distributed under the License is +// $$$$$$$$$$$$$$$$$$$$$$$$$$ distributed on an "AS IS" BASIS, WITHOUT WARRANTIES +// $$$$$$$$$ $$$$$$$$$$$ OR CONDITIONS OF ANY KIND, either express or implied. +// $$$$$$$$ +// $$$$$$$$$$$$$$$$$$ See the License for the specific language governing +// $$$$$$$$$$$$$ permissions and limitations under the License. +// $$$$$$$ +// $$ +// $$$$$ $$$$$ $$ $ +// $$$ $$$ $$$ $$ $$$$$ $$ $$$ $$$$ $$$$$$$ $$$$ $$$ $$$$$$ $$ $$$$$$ +// $$$ $$$ $$$ $$ $$$ $$$ $$$ $ $$ $$ $$ $$ $$ $$ $$$ $$$ +// $$$$$$$$$$$ $$ $$ $$$ $$ $$ $$$$$$$ $$ $$ $$ $$$ $$ $$ +// $$$ $$$ $$$$ $$$ $$ $$ $$$ $$ $$ $$ $$ $$ $$ $$ +// $$$$$ $$$$$ $$ $$$$$$$$ $ $$$ $$$$$$$$ $$$ $$$$ $$$$$$$ $$$$ $$$$ +// $$$ + +#![recursion_limit = "256"] +#![cfg_attr(not(feature = "std"), no_std)] + +#[cfg(test)] +mod tests; + +pub mod types; +mod weights; + +use crate::types::IncrementalIntentId; +use crate::types::Intent; +use crate::types::IntentInput; +use crate::types::Moment; +use core::cmp; +use frame_support::pallet_prelude::StorageValue; +use frame_support::pallet_prelude::*; +use frame_support::traits::Time; +use frame_support::Blake2_128Concat; +use frame_support::{dispatch::DispatchResult, require_transactional, traits::Get}; +use frame_system::offchain::SubmitTransaction; +use frame_system::pallet_prelude::*; +use hydra_dx_math::ema::EmaPrice; +use hydradx_traits::lazy_executor::Mutate; +use hydradx_traits::lazy_executor::Source; +use hydradx_traits::price::PriceProvider; +use hydradx_traits::registry::Inspect; +use hydradx_traits::CreateBare; +use ice_support::AssetId; +use ice_support::Balance; +use ice_support::DcaData; +use ice_support::IntentData; +use ice_support::IntentDataInput; +use ice_support::IntentId; +pub use ice_support::Partial; +use ice_support::ResolvedIntent; +pub use ice_support::SwapData; +pub use ice_support::SwapParams; +use orml_traits::NamedMultiReservableCurrency; +pub use pallet::*; +use sp_runtime::traits::BlockNumberProvider; +use sp_runtime::traits::Zero; +use sp_runtime::{FixedPointNumber, FixedU128}; +use sp_std::prelude::*; +pub use weights::WeightInfo; + +pub type NamedReserveIdentifier = [u8; 8]; +pub const NAMED_RESERVE_ID: [u8; 8] = *b"ICE_int#"; + +pub const UNSIGNED_TXS_PRIORITY: u64 = 1000; +const OCW_LOG_TARGET: &str = "intent::offchain_worker"; +const LOG_PREFIX: &str = "ICE#pallet_intent"; +pub(crate) const OCW_TAG_PREFIX: &str = "intent-cleanup"; + +#[frame_support::pallet] +pub mod pallet { + use crate::types::CallData; + + use super::*; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + CreateBare> { + /// Provider for the current timestamp. + type TimestampProvider: Time; + + /// Multi currency mechanism + type Currency: NamedMultiReservableCurrency< + Self::AccountId, + ReserveIdentifier = NamedReserveIdentifier, + CurrencyId = AssetId, + Balance = Balance, + >; + + /// Intents' lazy callback execution handling + type LazyExecutorHandler: Mutate; + + /// Asset registry handler + type RegistryHandler: Inspect; + + /// Asset Id of hub asset + #[pallet::constant] + type HubAssetId: Get; + + /// Maximum deadline for intent in milliseconds. + #[pallet::constant] + type MaxAllowedIntentDuration: Get; + + /// Oracle price provider for DCA dynamic slippage. + /// + /// Pair-based provider: internally consults the on-chain route + /// (e.g. `OraclePriceProviderUsingRoute`), so the + /// composed oracle price reflects the path the trade actually + /// executes through. + type OraclePriceProvider: PriceProvider; + + /// Provider for the current block number (used for DCA scheduling). + type BlockNumberProvider: BlockNumberProvider>; + + /// Minimum DCA period in blocks. + #[pallet::constant] + type MinDcaPeriod: Get; + + /// Maximum number of intents a single account can have at the same time. + #[pallet::constant] + type MaxIntentsPerAccount: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// New intent was submitted. + IntentSubmitted { + id: IntentId, + owner: T::AccountId, + intent: Intent, + }, + /// Intent was resolved as part of ICE solution execution. + IntentResolved { + id: IntentId, + amount_in: Balance, + amount_out: Balance, + fee: Balance, + }, + + /// Portion of intent was resolved as part of ICE solution execution. + IntentResovedPartially { + id: IntentId, + amount_in: Balance, + amount_out: Balance, + fee: Balance, + }, + + /// Intent was canceled. + IntentCanceled { id: IntentId }, + + /// Intent expired. + IntentExpired { id: IntentId }, + + /// Failed to add intent's callback to queue for execution. + FailedToQueueCallback { id: IntentId, error: DispatchError }, + + /// A single DCA trade was executed; intent stays in storage for the next period. + DcaTradeExecuted { + id: IntentId, + amount_in: Balance, + amount_out: Balance, + fee: Balance, + remaining_budget: Balance, + }, + + /// DCA intent completed (budget exhausted). Intent removed from storage. + DcaCompleted { id: IntentId }, + } + + #[pallet::error] + pub enum Error { + /// Invalid deadline + InvalidDeadline, + /// Invalid intent parameters + InvalidIntent, + /// Referenced intent doesn't exist. + IntentNotFound, + /// Referenced intent has expired. + IntentExpired, + /// Referenced intent is still active. + IntentActive, + /// Intent's resolution doesn't match intent's parameters. + ResolveMismatch, + ///Resolution violates intent's limits. + LimitViolation, + /// Calculation overflow. + ArithmeticOverflow, + /// Referenced intent's owner doesn't exist. + IntentOwnerNotFound, + /// Account is not intent's owner. + InvalidOwner, + /// User doesn't have enough reserved funds. + InsufficientReservedBalance, + /// Partial intents are not supported at the moment. + NotImplemented, + /// Asset with specified id doesn't exists. + AssetNotFound, + /// DCA period is below minimum. + InvalidDcaPeriod, + /// DCA budget is less than a single trade amount. + InvalidDcaBudget, + /// DCA intent must not have a deadline. + InvalidDcaDeadline, + /// Account has reached the maximum number of allowed intents. + MaxIntentsReached, + } + + #[pallet::storage] + #[pallet::getter(fn get_intent)] + pub type Intents = StorageMap<_, Blake2_128Concat, IntentId, Intent>; + + #[pallet::storage] + #[pallet::getter(fn intent_owner)] + pub(super) type IntentOwner = StorageMap<_, Blake2_128Concat, IntentId, T::AccountId>; + + #[pallet::storage] + /// Intent id sequencer + #[pallet::getter(fn next_incremental_id)] + pub(super) type NextIncrementalId = StorageValue<_, IncrementalIntentId, ValueQuery>; + + #[pallet::storage] + /// Reverse index mapping account to its intent ids for easy lookup. + pub type AccountIntents = + StorageDoubleMap<_, Blake2_128Concat, T::AccountId, Twox64Concat, IntentId, (), OptionQuery>; + + #[pallet::storage] + /// Number of intents per account. + #[pallet::getter(fn account_intent_count)] + pub type AccountIntentCount = StorageMap<_, Blake2_128Concat, T::AccountId, u32, ValueQuery>; + + #[pallet::call] + impl Pallet { + /// Submit intent by user. + /// + /// This extrinsics reserves fund for intents' execution. + /// WARN: partial intents are not supported at the moment and its' creation is not allowed. + /// + /// Parameters: + /// - `intent`: intent's data + /// + /// Emits: + /// - `IntentSubmitted` when successful + /// + #[pallet::call_index(0)] + #[pallet::weight(::WeightInfo::submit_intent())] //TODO: should probably include length of on_success/on_failure calls too + pub fn submit_intent(origin: OriginFor, intent: IntentInput) -> DispatchResult { + let who = ensure_signed(origin)?; + + Self::add_intent(who, intent)?; + Ok(()) + } + + /// Extrinsic unlocks reserved funds and removes intent. + /// + /// Only intent's owner can cancel intent. + /// + /// Parameters: + /// - `id`: id of intent to be canceled. + /// + /// Emits: + /// - `IntentCanceled` when successful + /// + #[pallet::call_index(1)] + #[pallet::weight(::WeightInfo::remove_intent())] + pub fn remove_intent(origin: OriginFor, id: IntentId) -> DispatchResult { + let who = ensure_signed(origin)?; + Self::cancel_intent(who, id) + } + + /// Extrinsic removes expired intent, queue intent's on failure callback and unlocks funds. + /// + /// Failure to queue callback for future execution doesn't fail clean up function. + /// This is called automatically from OCW to remove expired intents but it can be called also + /// called by any users. + /// + /// Parameters: + /// - `id`: id of intent to be cleaned up from storage. + /// + /// Emits: + /// - `FailedToQueueCallback` when callback's queuing fails + /// - `IntentExpired` when successful + #[pallet::call_index(2)] + #[pallet::weight(::WeightInfo::cleanup_intent())] + pub fn cleanup_intent(origin: OriginFor, id: IntentId) -> DispatchResultWithPostInfo { + if ensure_none(origin.clone()).is_err() { + ensure_signed(origin)?; + } + + Intents::::try_mutate_exists(id, |maybe_intent| { + let intent = maybe_intent.as_mut().ok_or(Error::::IntentNotFound)?; + + ensure!( + intent.deadline.ok_or(Error::::IntentActive)? <= T::TimestampProvider::now(), + Error::::IntentActive + ); + + IntentOwner::::try_mutate_exists(id, |maybe_owner| -> Result<(), DispatchError> { + let owner = maybe_owner.as_ref().ok_or(Error::::IntentOwnerNotFound)?; + + let unlock_amount = match intent.data { + IntentData::Swap(ref s) => s.remaining(), + IntentData::Dca(ref dca) => dca.remaining_budget, + }; + Self::unlock_funds(owner, intent.data.asset_in(), unlock_amount)?; + + Self::deposit_event(Event::::IntentExpired { id }); + + AccountIntents::::remove(owner, id); + AccountIntentCount::::mutate_exists(owner, |maybe_count| { + let count = maybe_count.unwrap_or(0).saturating_sub(1); + *maybe_count = if count > 0 { Some(count) } else { None }; + }); + *maybe_owner = None; + Ok(()) + })?; + + *maybe_intent = None; + Ok(Pays::No.into()) + }) + } + } + + #[pallet::hooks] + impl Hooks> for Pallet { + //NOTE: this is tmp solution for testing. + //TODO: create offchain bot that will do clean up instead of OCW. + fn offchain_worker(_block_number: BlockNumberFor) { + let expired = Self::get_expired_intents(); + + for (i, intent_id) in expired.iter().enumerate() { + if i >= 10 { + break; + } + + let call = Call::cleanup_intent { id: *intent_id }; + let tx = T::create_bare(call.into()); + if let Err(e) = SubmitTransaction::>::submit_transaction(tx) { + debug_assert!(false, "laxy-executorn: failed to submit dispatch_top transaction"); + log::error!(target: OCW_LOG_TARGET, "{:?}: to submit cleanup_intent call, err: {:?}", LOG_PREFIX, e); + }; + } + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(source: TransactionSource, call: &Self::Call) -> TransactionValidity { + if let Call::cleanup_intent { id } = call { + match source { + TransactionSource::Local | TransactionSource::InBlock => { /* OCW or included in block are allowed */ + } + _ => { + return InvalidTransaction::Call.into(); + } + }; + + let Some(intent) = Intents::::get(id) else { + return InvalidTransaction::Call.into(); + }; + + let Some(deadline) = intent.deadline else { + return Err(TransactionValidityError::Invalid(InvalidTransaction::Call)); + }; + + ensure!(deadline <= T::TimestampProvider::now(), InvalidTransaction::Call); + + return ValidTransaction::with_tag_prefix(OCW_TAG_PREFIX) + .priority(UNSIGNED_TXS_PRIORITY) + .and_provides(Encode::encode(id)) + .longevity(1) + .propagate(false) + .build(); + } + InvalidTransaction::Call.into() + } + } +} + +impl Pallet { + /// Function unreserves funds and cancels intent. + #[require_transactional] + pub fn cancel_intent(who: T::AccountId, id: IntentId) -> DispatchResult { + Intents::::try_mutate_exists(id, |maybe_intent| { + let intent = maybe_intent.as_ref().ok_or(Error::::IntentNotFound)?; + + IntentOwner::::try_mutate_exists(id, |maybe_owner| -> Result<(), DispatchError> { + let owner = maybe_owner.clone().ok_or(Error::::IntentOwnerNotFound)?; + + ensure!(owner == who, Error::::InvalidOwner); + + let unlock_amount = match intent.data { + IntentData::Swap(ref s) => s.remaining(), + IntentData::Dca(ref dca) => dca.remaining_budget, + }; + Self::unlock_funds(&who, intent.data.asset_in(), unlock_amount)?; + + Self::deposit_event(Event::::IntentCanceled { id }); + + AccountIntents::::remove(&owner, id); + AccountIntentCount::::mutate_exists(&owner, |maybe_count| { + let count = maybe_count.unwrap_or(0).saturating_sub(1); + *maybe_count = if count > 0 { Some(count) } else { None }; + }); + *maybe_owner = None; + Ok(()) + })?; + + *maybe_intent = None; + Ok(()) + }) + } + + /// Function validates and reserves funds for intent's execution and adds intent to storage + /// WARN: partial intents are not supported at the moment, look at `submit_intent()` + #[require_transactional] + pub fn add_intent(owner: T::AccountId, input: IntentInput) -> Result { + ensure!( + Self::account_intent_count(&owner) < T::MaxIntentsPerAccount::get(), + Error::::MaxIntentsReached + ); + + let now = T::TimestampProvider::now(); + if let Some(deadline) = input.deadline { + log::debug!(target: OCW_LOG_TARGET, "{:?}: add_intent(), deadline: {:?}, now: {:?}, max_deadline: {:?}", + LOG_PREFIX, deadline, now, now.saturating_add(T::MaxAllowedIntentDuration::get())); + + ensure!(deadline > now, Error::::InvalidDeadline); + ensure!( + deadline < (now.saturating_add(T::MaxAllowedIntentDuration::get())), + Error::::InvalidDeadline + ); + } + + let ed_in = T::RegistryHandler::existential_deposit(input.data.asset_in()).ok_or(Error::::AssetNotFound)?; + let ed_out = + T::RegistryHandler::existential_deposit(input.data.asset_out()).ok_or(Error::::AssetNotFound)?; + + let intent_data = match input.data { + IntentDataInput::Swap(ref data) => { + log::debug!(target: OCW_LOG_TARGET, "{:?}: add_intent(), asset_in: {:?}, ed_in: {:?}, amount_in: {:?}, aseet_out: {:?}, ed_out: {:?}, amount_out: {:?}", + LOG_PREFIX, data.asset_in, ed_in, data.amount_in, data.asset_out, ed_out, data.amount_out); + + ensure!(data.amount_in >= ed_in, Error::::InvalidIntent); + ensure!(data.amount_out >= ed_out, Error::::InvalidIntent); + ensure!(data.asset_in != data.asset_out, Error::::InvalidIntent); + ensure!(data.asset_out != T::HubAssetId::get(), Error::::InvalidIntent); + + T::Currency::reserve_named(&NAMED_RESERVE_ID, data.asset_in, &owner, data.amount_in)?; + + IntentData::Swap(ice_support::SwapData::from(data)) + } + IntentDataInput::Dca(ref data) => { + // DCA intents must not have a deadline + ensure!(input.deadline.is_none(), Error::::InvalidDcaDeadline); + + ensure!(data.period >= T::MinDcaPeriod::get(), Error::::InvalidDcaPeriod); + ensure!(data.amount_in >= ed_in, Error::::InvalidIntent); + ensure!(data.amount_out >= ed_out, Error::::InvalidIntent); + ensure!(data.asset_in != data.asset_out, Error::::InvalidIntent); + ensure!(data.asset_out != T::HubAssetId::get(), Error::::InvalidIntent); + let reserve_amount = match data.budget { + Some(budget) => { + ensure!(budget >= data.amount_in, Error::::InvalidDcaBudget); + budget + } + None => data.amount_in.saturating_mul(2), // rolling: 2x buffer + }; + + T::Currency::reserve_named(&NAMED_RESERVE_ID, data.asset_in, &owner, reserve_amount)?; + + let current_block: u32 = T::BlockNumberProvider::current_block_number() + .try_into() + .unwrap_or(u32::MAX); + + IntentData::Dca(data.clone().into_data(reserve_amount, current_block)) + } + }; + + let intent = Intent { + data: intent_data, + deadline: input.deadline, + on_resolved: input.on_resolved, + }; + + let id = Self::generate_new_intent_id(now); + Intents::::insert(id, &intent); + IntentOwner::::insert(id, &owner); + AccountIntents::::insert(&owner, id, ()); + AccountIntentCount::::mutate(&owner, |count| *count = count.saturating_add(1)); + Self::deposit_event(Event::IntentSubmitted { id, owner, intent }); + + Ok(id) + } + + /// Function returns expired intents + pub fn get_expired_intents() -> Vec { + let mut intents: Vec<(IntentId, Intent)> = Intents::::iter().collect(); + intents.sort_by_key(|(_, intent)| intent.deadline); + + let now = T::TimestampProvider::now(); + intents.retain(|(_, intent)| intent.deadline.unwrap_or(Moment::MAX) <= now); + + intents.iter().map(|x| x.0).collect::>() + } + + /// Function returns valid intents. + /// + /// DCA intents are included only when their period has elapsed, budget is sufficient, + /// and oracle price indicates the trade is feasible (pre-filter). + /// They are transformed into `IntentData::Swap` with the hard limit as `amount_out`, + /// so the solver treats them as regular one-shot swaps. + /// The oracle effective limit is used only as a pre-filter gate — if the oracle-derived + /// minimum exceeds what the solver could reasonably fill, the intent is skipped for this block. + pub fn get_valid_intents() -> Vec<(IntentId, Intent)> { + let current_block: u32 = T::BlockNumberProvider::current_block_number() + .try_into() + .unwrap_or(u32::MAX); + + let mut intents: Vec<(IntentId, Intent)> = Intents::::iter() + .filter_map(|(id, intent)| { + match &intent.data { + IntentData::Swap(_) => Some((id, intent)), + IntentData::Dca(dca) => { + // Period eligibility + let next_eligible = dca.last_execution_block.saturating_add(dca.period); + if current_block < next_eligible { + log::debug!(target: OCW_LOG_TARGET, "{:?}: get_valid_intents(), DCA intent {:?} skipped: period not elapsed (current_block: {}, next_eligible: {})", + LOG_PREFIX, id, current_block, next_eligible); + return None; + } + // Budget sufficient for a trade + if dca.remaining_budget < dca.amount_in { + log::debug!(target: OCW_LOG_TARGET, "{:?}: get_valid_intents(), DCA intent {:?} skipped: insufficient budget (remaining: {}, required: {})", + LOG_PREFIX, id, dca.remaining_budget, dca.amount_in); + return None; + } + // Oracle pre-filter + if let Some(oracle_min) = Self::compute_dca_oracle_limit(dca) { + if oracle_min > 0 && dca.amount_out > oracle_min { + log::debug!(target: OCW_LOG_TARGET, "{:?}: get_valid_intents(), DCA intent {:?} skipped: oracle pre-filter (hard_limit: {} > oracle_min: {} for {} -> {})", + LOG_PREFIX, id, dca.amount_out, oracle_min, dca.asset_in, dca.asset_out); + return None; + } + } + // Transform to Swap with hard limit for solver + let swap = dca.to_swap_data(dca.amount_out); + let transformed = Intent { + data: IntentData::Swap(swap), + deadline: intent.deadline, + on_resolved: intent.on_resolved.clone(), + }; + Some((id, transformed)) + } + } + }) + .collect(); + intents.sort_by_key(|(id, _)| Reverse(*id)); + intents + } + + /// Function validates if intent was resolved correctly + pub fn validate_resolve(intent: &Intent, resolve: &IntentData) -> Result<(), DispatchError> { + if let Some(deadline) = intent.deadline { + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_resolve(), deadline: {:?}, now: {:?}", + LOG_PREFIX, deadline, T::TimestampProvider::now()); + + ensure!(deadline > T::TimestampProvider::now(), Error::::IntentExpired); + } + + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_resolve(), orig_asset_in: {:?}, resolve_asset_in: {:?}", + LOG_PREFIX, intent.data.asset_in(), resolve.asset_in()); + ensure!( + intent.data.asset_in() == resolve.asset_in(), + Error::::ResolveMismatch + ); + + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_resolve(), orig_asset_out: {:?}, resolve_asset_out: {:?}", + LOG_PREFIX, intent.data.asset_out(), resolve.asset_out()); + ensure!( + intent.data.asset_out() == resolve.asset_out(), + Error::::ResolveMismatch + ); + + match intent.data { + IntentData::Swap(_) => { + Self::validate_swap_intent_resolve(intent, resolve)?; + } + IntentData::Dca(ref dca) => { + Self::validate_dca_intent_resolve(dca, resolve)?; + } + } + + Ok(()) + } + + fn validate_swap_intent_resolve(intent: &Intent, resolve: &IntentData) -> Result<(), DispatchError> { + let IntentData::Swap(ref swap) = intent.data else { + return Err(Error::::ResolveMismatch.into()); + }; + let IntentData::Swap(ref resolve_swap) = resolve else { + return Err(Error::::ResolveMismatch.into()); + }; + + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_swap_intent_resolve(), partial: {:?}, resolve.partial: {:?}", + LOG_PREFIX, swap.partial, resolve_swap.partial); + + ensure!( + swap.partial.is_partial() == resolve_swap.partial.is_partial(), + Error::::ResolveMismatch + ); + + let remaining = swap.remaining(); + + if swap.partial.is_partial() { + // Resolve amount must not exceed remaining + ensure!(resolve_swap.amount_in <= remaining, Error::::LimitViolation); + + if resolve_swap.amount_in == remaining { + // Full fill of remaining — validate against original rate + // pro_rata uses original amount_out/amount_in, so this check is correct + let limit = intent.data.pro_rata(resolve).ok_or(Error::::ArithmeticOverflow)?; + + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_swap_intent_resolve(), partial fully filling remaining={:?}, limit: {:?}, resolve.amount_out: {:?}", + LOG_PREFIX, remaining, limit, resolve_swap.amount_out); + + ensure!(resolve_swap.amount_out >= limit, Error::::LimitViolation); + } else { + // Partial fill — validate pro-rata minimum (uses original rate) + let limit = intent.data.pro_rata(resolve).ok_or(Error::::ArithmeticOverflow)?; + + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_swap_intent_resolve(), partial fill, remaining={:?}, resolve.amount_in: {:?}, limit: {:?}, resolve.amount_out: {:?}", + LOG_PREFIX, remaining, resolve_swap.amount_in, limit, resolve_swap.amount_out); + + ensure!(resolve_swap.amount_out >= limit, Error::::LimitViolation); + } + } else { + log::debug!(target: OCW_LOG_TARGET, "{:?}: validate_swap_intent_resolve(), non-partial, amount_in: {:?}, resolve.amount_in: {:?}, amount_out: {:?}, resolve.amount_out: {:?}", + LOG_PREFIX, swap.amount_in, resolve_swap.amount_in, swap.amount_out, resolve_swap.amount_out); + + ensure!(resolve_swap.amount_in == swap.amount_in, Error::::LimitViolation); + ensure!(resolve_swap.amount_out >= swap.amount_out, Error::::LimitViolation); + } + + Ok(()) + } + + /// Function resolves intent + pub fn intent_resolved(who: &T::AccountId, resolve: &ResolvedIntent, fee: Balance) -> DispatchResult { + let ResolvedIntent { id, data: resolve } = resolve; + Intents::::try_mutate_exists(id, |maybe_intent| { + let intent = maybe_intent.as_mut().ok_or(Error::::IntentNotFound)?; + let owner = Self::intent_owner(id).ok_or(Error::::IntentOwnerNotFound)?; + + ensure!(owner == *who, Error::::InvalidOwner); + + Self::validate_resolve(intent, resolve)?; + + let (fully_resolved, is_dca) = match intent.data { + IntentData::Swap(ref mut s) => { + let IntentData::Swap(ref r) = resolve else { + return Err(Error::::ResolveMismatch.into()); + }; + (Self::resolve_swap_intent(s, r)?, false) + } + IntentData::Dca(ref mut dca) => (Self::resolve_dca_intent(&owner, dca)?, true), + }; + + if fully_resolved { + // Unreserve any remaining reserved funds. + // For swaps: submit_solution already unlocked and transferred the fill + // amount before calling intent_resolved, so nothing remains to unreserve. + // For DCA: remaining_budget tracks unspent reserved funds. + let unreserve_amount = match intent.data { + IntentData::Swap(_) => 0, + IntentData::Dca(ref dca) => dca.remaining_budget, + }; + if !unreserve_amount.is_zero() { + Self::unlock_funds(&owner, intent.data.asset_in(), unreserve_amount)?; + } + + //NOTE: it's ok to `take`, intent will be removed from storage. + if let Some(cb) = intent.on_resolved.take() { + if let Err(e) = T::LazyExecutorHandler::queue(Source::ICE(*id), who.clone(), cb) { + Self::deposit_event(Event::FailedToQueueCallback { id: *id, error: e }); + }; + } + + *maybe_intent = None; + IntentOwner::::remove(id); + AccountIntents::::remove(&owner, id); + AccountIntentCount::::mutate_exists(&owner, |maybe_count| { + let count = maybe_count.unwrap_or(0).saturating_sub(1); + *maybe_count = if count > 0 { Some(count) } else { None }; + }); + + if is_dca { + Self::deposit_event(Event::DcaCompleted { id: *id }); + } else { + Self::deposit_event(Event::IntentResolved { + id: *id, + amount_in: resolve.amount_in(), + amount_out: resolve.amount_out().saturating_sub(fee), + fee, + }); + } + return Ok(()); + } + + // Not fully resolved + match intent.data { + IntentData::Swap(_) => { + ensure!(intent.data.is_partial(), Error::::LimitViolation); + Self::deposit_event(Event::IntentResovedPartially { + id: *id, + amount_in: resolve.amount_in(), + amount_out: resolve.amount_out().saturating_sub(fee), + fee, + }); + } + IntentData::Dca(ref dca) => { + Self::deposit_event(Event::DcaTradeExecuted { + id: *id, + amount_in: resolve.amount_in(), + amount_out: resolve.amount_out().saturating_sub(fee), + fee, + remaining_budget: dca.remaining_budget, + }); + } + } + + Ok(()) + }) + } + + /// Updates the intent's partial fill state. Original `amount_in`/`amount_out` are immutable. + /// Returns `true` if the intent is now fully resolved. + fn resolve_swap_intent(intent: &mut SwapData, resolve: &SwapData) -> Result { + match intent.partial { + Partial::No => { + // Non-partial: must resolve the full amount in one go + ensure!(resolve.amount_in == intent.amount_in, Error::::LimitViolation); + Ok(true) + } + Partial::Yes(filled) => { + let new_filled = filled + .checked_add(resolve.amount_in) + .ok_or(Error::::ArithmeticOverflow)?; + ensure!(new_filled <= intent.amount_in, Error::::ArithmeticOverflow); + + intent.partial = Partial::Yes(new_filled); + Ok(new_filled == intent.amount_in) + } + } + } + + /// Resolves a DCA intent after a single trade execution. + /// Returns `true` if the DCA is complete (budget exhausted). + fn resolve_dca_intent(owner: &T::AccountId, dca: &mut DcaData) -> Result { + let current_block: u32 = T::BlockNumberProvider::current_block_number() + .try_into() + .unwrap_or(u32::MAX); + + // Deduct per-trade amount from remaining budget + dca.remaining_budget = dca + .remaining_budget + .checked_sub(dca.amount_in) + .ok_or(Error::::ArithmeticOverflow)?; + + // Update last execution block + dca.last_execution_block = current_block; + + // Rolling DCA: try to re-reserve one unit from free balance + if dca.budget.is_none() { + if T::Currency::reserve_named(&NAMED_RESERVE_ID, dca.asset_in, owner, dca.amount_in).is_ok() { + dca.remaining_budget = dca.remaining_budget.saturating_add(dca.amount_in); + } else { + log::debug!(target: OCW_LOG_TARGET, "{:?}: resolve_dca_intent(), rolling DCA re-reserve failed for owner {:?}, asset: {:?}, amount: {:?} (insufficient free balance)", + LOG_PREFIX, owner, dca.asset_in, dca.amount_in); + } + } + + // DCA complete if insufficient budget for another trade + Ok(dca.remaining_budget < dca.amount_in) + } + + /// Validates a DCA intent's resolution. + /// + /// Enforces two floors on `resolve.amount_out`: + /// 1. the user's hard limit (`dca.amount_out`) + /// 2. the oracle-derived effective limit (`compute_dca_effective_limit`) + /// + /// Enforcing the effective limit here — not only in the + /// `get_valid_intents` pre-filter — prevents a malicious collator from + /// bypassing the filter via a hand-crafted `submit_solution` (the call + /// accepts `None` origin) and paying the user only the hard limit while + /// the AMM yields closer to the oracle-derived price. + fn validate_dca_intent_resolve(dca: &DcaData, resolve: &IntentData) -> Result<(), DispatchError> { + // Resolve must spend exactly per-trade amount + ensure!(resolve.amount_in() == dca.amount_in, Error::::LimitViolation); + // Hard limit check (always enforced regardless of oracle) + ensure!(resolve.amount_out() >= dca.amount_out, Error::::LimitViolation); + // Oracle-derived slippage floor (defence against pre-filter bypass) + let effective_limit = Self::compute_dca_effective_limit(dca); + ensure!(resolve.amount_out() >= effective_limit, Error::::LimitViolation); + Ok(()) + } + + /// Computes surplus for score matching. + /// For swap intents: delegates to `IntentData::surplus()`. + /// For DCA intents: uses the hard limit (`amount_out`) — same value used in + /// `get_valid_intents()` transform, ensuring OCW and on-chain produce identical scores. + pub fn compute_surplus(intent: &Intent, resolve: &IntentData) -> Option { + intent.data.surplus(resolve) + } + + /// Returns the effective minimum output for a DCA trade: + /// the tighter (higher) of the oracle-based limit and the user's hard limit. + pub fn compute_dca_effective_limit(dca: &DcaData) -> Balance { + match Self::compute_dca_oracle_limit(dca) { + Some(oracle_min) => cmp::max(oracle_min, dca.amount_out), + None => dca.amount_out, // oracle unavailable → hard limit only + } + } + + /// Computes the oracle-based minimum output for a DCA trade. + /// Returns None if oracle data is unavailable. + fn compute_dca_oracle_limit(dca: &DcaData) -> Option { + let oracle_price = T::OraclePriceProvider::get_price(dca.asset_in, dca.asset_out)?; + // Oracle price for route A→B returns n/d representing asset_in per asset_out + // (i.e., how much A costs per unit of B). + // For sell: estimated_out = amount_in / price = amount_in * d / n + let estimated_out = + FixedU128::checked_from_rational(oracle_price.d, oracle_price.n)?.checked_mul_int(dca.amount_in)?; + if estimated_out == 0 { + return None; + } + let slippage_amount = dca.slippage.mul_floor(estimated_out); + estimated_out.checked_sub(slippage_amount) + } + + /// Function unlocks reserved `amount` of `asset_id` for `who`. + #[inline(always)] + pub fn unlock_funds(who: &T::AccountId, asset_id: AssetId, amount: Balance) -> DispatchResult { + if !T::Currency::unreserve_named(&NAMED_RESERVE_ID, asset_id, who, amount).is_zero() { + return Err(Error::::InsufficientReservedBalance.into()); + } + + Ok(()) + } +} + +impl Pallet { + fn generate_new_intent_id(deadline: Moment) -> IntentId { + // We deliberately overflow here, so if we , for some reason, hit to max value, we will start from 0 again + // it is not an issue, we create new intent id together with created at timestamp, so it is not possible to create two intents with the same id + let incremental_id = NextIncrementalId::::mutate(|id| -> IncrementalIntentId { + let current_id = *id; + (*id, _) = id.overflowing_add(1); + current_id + }); + (deadline as u128) << 64 | incremental_id as u128 + } +} diff --git a/pallets/intent/src/tests/add_intent.rs b/pallets/intent/src/tests/add_intent.rs new file mode 100644 index 0000000000..f922ab7ca1 --- /dev/null +++ b/pallets/intent/src/tests/add_intent.rs @@ -0,0 +1,405 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::storage::with_transaction; +use frame_support::{assert_noop, assert_ok}; +use pretty_assertions::assert_eq; +use sp_runtime::TransactionOutcome; + +fn swap_intent_input( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + amount_out: Balance, + deadline: Option, +) -> IntentInput { + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in, + asset_out, + amount_in, + amount_out, + partial: false, + }), + deadline, + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + } +} + +#[test] +fn should_work_when_intent_is_valid() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let input = swap_intent_input(HDX, DOT, 10 * ONE_HDX, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)); + + //Act + let r = IntentPallet::add_intent(ALICE, input); + let id = match r { + Ok(id) => id, + _ => { + panic!("Expected Ok(_). Got {:#?}", r); + } + }; + + let stored = IntentPallet::get_intent(id).expect("intent should be stored"); + assert_eq!(stored.data.asset_in(), HDX); + assert_eq!(stored.data.asset_out(), DOT); + assert_eq!(stored.data.amount_in(), 10 * ONE_HDX); + assert_eq!(stored.data.amount_out(), 1_000 * ONE_DOT); + assert_eq!(stored.deadline, Some(MAX_INTENT_DEADLINE - 1)); + assert_eq!(IntentPallet::intent_owner(id), Some(ALICE)); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), + 10 * ONE_HDX + ); + assert_eq!(AccountIntents::::get(ALICE, id), Some(())); + assert_eq!(IntentPallet::account_intent_count(ALICE), 1); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_deadline_is_less_than_now() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + assert_ok!(Timestamp::set(RuntimeOrigin::none(), 2 * MAX_INTENT_DEADLINE)); + + let input = swap_intent_input(HDX, DOT, 10 * ONE_HDX, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)); + + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidDeadline); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_deadline_bigger_than_max_allowed_intent_duration() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let input = swap_intent_input(HDX, DOT, 10 * ONE_HDX, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE + 1)); + + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidDeadline); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_amount_in_is_zero() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let input = swap_intent_input(HDX, DOT, 0, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)); + + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidIntent); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_amount_out_is_zero() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let input = swap_intent_input(HDX, DOT, 10 * ONE_HDX, 0, Some(MAX_INTENT_DEADLINE - 1)); + + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidIntent); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_asset_in_eq_asset_out() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let input = swap_intent_input(HDX, HDX, 10 * ONE_HDX, 10 * ONE_HDX, Some(MAX_INTENT_DEADLINE - 1)); + + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidIntent); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_asset_out_is_hub_asset() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let input = swap_intent_input( + HDX, + HUB_ASSET_ID, + 10 * ONE_HDX, + 10 * ONE_HDX, + Some(MAX_INTENT_DEADLINE - 1), + ); + + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidIntent); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_cant_reserve_funds() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let input = swap_intent_input(HDX, DOT, 10 * ONE_HDX, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)); + + assert_noop!( + IntentPallet::add_intent(ALICE, input), + orml_tokens::Error::::BalanceTooLow + ); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_amount_in_is_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let ed = DummyRegistry::existential_deposit(HDX).expect("dummy registry to work"); + + let input = swap_intent_input(HDX, DOT, ed - 1, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)); + + //Act&Assert + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidIntent); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_amount_out_is_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let ed = DummyRegistry::existential_deposit(DOT).expect("dummy registry to work"); + + let input = swap_intent_input(HDX, DOT, 10 * ONE_HDX, ed - 1, Some(MAX_INTENT_DEADLINE - 1)); + + //Act&Assert + assert_noop!(IntentPallet::add_intent(ALICE, input), Error::::InvalidIntent); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_work_when_intent_has_no_deadline() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let input = swap_intent_input(HDX, DOT, 10 * ONE_HDX, 1_000 * ONE_DOT, None); + + //Act + let r = IntentPallet::add_intent(ALICE, input); + let id = match r { + Ok(id) => id, + _ => { + panic!("Expected Ok(_). Got {:#?}", r); + } + }; + + let stored = IntentPallet::get_intent(id).expect("intent should be stored"); + assert_eq!(stored.data.asset_in(), HDX); + assert_eq!(stored.data.asset_out(), DOT); + assert_eq!(stored.deadline, None); + assert_eq!(IntentPallet::intent_owner(id), Some(ALICE)); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), + 10 * ONE_HDX + ); + assert_eq!(AccountIntents::::get(ALICE, id), Some(())); + assert_eq!(IntentPallet::account_intent_count(ALICE), 1); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn account_intents_index_tracks_multiple_intents() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 100 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + // Create 3 intents for ALICE + let id0 = IntentPallet::add_intent( + ALICE, + swap_intent_input(HDX, DOT, 10 * ONE_HDX, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + ) + .expect("should work"); + let id1 = IntentPallet::add_intent( + ALICE, + swap_intent_input(HDX, DOT, 5 * ONE_HDX, 500 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + ) + .expect("should work"); + let id2 = IntentPallet::add_intent( + ALICE, + swap_intent_input(ETH, DOT, ONE_QUINTIL, 1_000 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + ) + .expect("should work"); + + // Create 1 intent for BOB + let id3 = IntentPallet::add_intent( + BOB, + swap_intent_input(ETH, DOT, ONE_QUINTIL, 1_500 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + ) + .expect("should work"); + + // Verify counts + assert_eq!(IntentPallet::account_intent_count(ALICE), 3); + assert_eq!(IntentPallet::account_intent_count(BOB), 1); + assert_eq!(AccountIntents::::iter_prefix(ALICE).count(), 3); + assert_eq!(AccountIntents::::iter_prefix(BOB).count(), 1); + + // Cancel one of ALICE's intents + assert_ok!(IntentPallet::cancel_intent(ALICE, id1)); + + assert_eq!(IntentPallet::account_intent_count(ALICE), 2); + assert_eq!(AccountIntents::::iter_prefix(ALICE).count(), 2); + assert_eq!(AccountIntents::::get(ALICE, id0), Some(())); + assert_eq!(AccountIntents::::get(ALICE, id1), None); + assert_eq!(AccountIntents::::get(ALICE, id2), Some(())); + + // BOB unaffected + assert_eq!(IntentPallet::account_intent_count(BOB), 1); + assert_eq!(AccountIntents::::get(BOB, id3), Some(())); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_max_intents_per_account_reached() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 1_000 * ONE_HDX), (BOB, HDX, 100 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + // MaxIntentsPerAccount is 5 in mock + for i in 0..5u128 { + assert_ok!(IntentPallet::add_intent( + ALICE, + swap_intent_input(HDX, DOT, ONE_HDX, 100 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + )); + assert_eq!(IntentPallet::account_intent_count(ALICE), (i + 1) as u32); + } + + // 6th intent should fail + assert_noop!( + IntentPallet::add_intent( + ALICE, + swap_intent_input(HDX, DOT, ONE_HDX, 100 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + ), + Error::::MaxIntentsReached + ); + + // BOB can still create intents (separate account) + assert_ok!(IntentPallet::add_intent( + BOB, + swap_intent_input(HDX, DOT, ONE_HDX, 100 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + )); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_work_when_intent_canceled_and_slot_freed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 1_000 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + // Fill up to max + let mut ids = Vec::new(); + for _ in 0..5u128 { + let id = IntentPallet::add_intent( + ALICE, + swap_intent_input(HDX, DOT, ONE_HDX, 100 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + ) + .expect("should work"); + ids.push(id); + } + + // At limit — cannot add + assert_noop!( + IntentPallet::add_intent( + ALICE, + swap_intent_input(HDX, DOT, ONE_HDX, 100 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + ), + Error::::MaxIntentsReached + ); + + // Cancel one — frees a slot + assert_ok!(IntentPallet::cancel_intent(ALICE, ids[2])); + assert_eq!(IntentPallet::account_intent_count(ALICE), 4); + + // Now can add again + assert_ok!(IntentPallet::add_intent( + ALICE, + swap_intent_input(HDX, DOT, ONE_HDX, 100 * ONE_DOT, Some(MAX_INTENT_DEADLINE - 1)), + )); + assert_eq!(IntentPallet::account_intent_count(ALICE), 5); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} diff --git a/pallets/intent/src/tests/cancel_intent.rs b/pallets/intent/src/tests/cancel_intent.rs new file mode 100644 index 0000000000..14f6cbbf11 --- /dev/null +++ b/pallets/intent/src/tests/cancel_intent.rs @@ -0,0 +1,402 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_noop; +use frame_support::assert_ok; +use frame_support::storage::with_transaction; +use pretty_assertions::assert_eq; +use sp_runtime::TransactionOutcome; + +#[test] +fn should_work_when_canceled_by_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + //Act + assert_ok!(IntentPallet::cancel_intent(owner, id)); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + assert_eq!(IntentPallet::account_intent_count(owner), 1); // ALICE still has intent 2 + // Other intents unaffected + assert_eq!(AccountIntents::::get(BOB, 1_u128), Some(())); + assert_eq!(AccountIntents::::get(ALICE, 2_u128), Some(())); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_work_when_intent_was_partially_resolved_and_canceled_by_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let id = 0_u128; + let mut resolve = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + //NOTE: It's ICE pallet responsibility is to unlock used fund during solution execution. This is + //to simulate it. + assert_eq!( + Currencies::unreserve_named( + &NAMED_RESERVE_ID, + resolve.data.asset_in(), + &owner, + resolve.data.amount_in() + ), + 0 + ); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, resolve.data.asset_in(), &owner), + 5_000_000_000_000_u128 + ); + assert_ok!(IntentPallet::intent_resolved( + &owner, + &ResolvedIntent { + id, + data: resolve.data.clone() + }, + 0, + )); + + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, resolve.data.asset_in(), &owner), + resolve.data.amount_in(), + ); + + //Act + assert_ok!(IntentPallet::cancel_intent(owner, id)); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, resolve.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_intent_doesnt_exist() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let id = 9_u128; + let owner = ALICE; + + //Act & Assert; + assert_noop!(IntentPallet::cancel_intent(owner, id), Error::::IntentNotFound); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_not_work_when_canceled_non_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let id = 0_u128; + let not_owner = BOB; + + //Act & Assert; + assert_noop!(IntentPallet::cancel_intent(not_owner, id), Error::::InvalidOwner); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_work_when_intent_has_no_deadline_and_canceled_by_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + //Act + assert_ok!(IntentPallet::cancel_intent(owner, id)); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} diff --git a/pallets/intent/src/tests/cleanup_intent.rs b/pallets/intent/src/tests/cleanup_intent.rs new file mode 100644 index 0000000000..283b915b5a --- /dev/null +++ b/pallets/intent/src/tests/cleanup_intent.rs @@ -0,0 +1,358 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_noop; +use frame_support::assert_ok; +use pretty_assertions::assert_eq; + +#[test] +fn should_work_when_intent_is_expired_and_origin_is_none() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!(get_queued_task(Source::ICE(id)), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + assert_ok!(Timestamp::set( + RuntimeOrigin::none(), + intent.deadline.expect("intent with deadline") + 1 + )); + + //Act + assert_ok!(IntentPallet::cleanup_intent(RuntimeOrigin::none(), id)); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + assert_eq!(IntentPallet::account_intent_count(owner), 0); + }); +} + +#[test] +fn should_work_when_intent_is_expired_and_origin_is_signed() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!(get_queued_task(Source::ICE(id)), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + assert_ok!(Timestamp::set( + RuntimeOrigin::none(), + intent.deadline.expect("intent with deadline") + 1 + )); + + //Act + assert_ok!(IntentPallet::cleanup_intent(RuntimeOrigin::signed(CHARLIE), id)); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + assert_eq!(IntentPallet::account_intent_count(owner), 0); + }); +} + +#[test] +fn should_not_work_when_intent_is_not_expired() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!(get_queued_task(Source::ICE(id)), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + //Act as signed + assert_noop!( + IntentPallet::cleanup_intent(RuntimeOrigin::signed(CHARLIE), id), + Error::::IntentActive + ); + + //Assert + assert!(IntentPallet::get_intent(id).is_some()); + assert!(IntentPallet::intent_owner(id).is_some()); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in() + ); + assert_eq!(get_queued_task(Source::ICE(id)), None); + + //Act 2 as none origin + assert_noop!( + IntentPallet::cleanup_intent(RuntimeOrigin::none(), id), + Error::::IntentActive + ); + + //Assert + assert!(IntentPallet::get_intent(id).is_some()); + assert!(IntentPallet::intent_owner(id).is_some()); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in() + ); + assert_eq!(get_queued_task(Source::ICE(id)), None); + }); +} + +#[test] +fn should_not_collect_fees_when_intent_is_expired() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!(get_queued_task(Source::ICE(id)), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + assert_ok!(Timestamp::set( + RuntimeOrigin::none(), + intent.deadline.expect("intent with deadline") + 1 + )); + + //Act + let res = IntentPallet::cleanup_intent(RuntimeOrigin::none(), id); + assert_eq!(res, Ok(Pays::No.into())); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + assert_eq!(IntentPallet::account_intent_count(owner), 0); + }); +} + +#[test] +fn should_not_work_when_intent_has_no_deadline() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!(get_queued_task(Source::ICE(id)), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + assert_ok!(Timestamp::set(RuntimeOrigin::none(), 1_000)); + + //Act + assert_noop!( + IntentPallet::cleanup_intent(RuntimeOrigin::none(), id), + Error::::IntentActive + ); + }); +} diff --git a/pallets/intent/src/tests/dca_intent.rs b/pallets/intent/src/tests/dca_intent.rs new file mode 100644 index 0000000000..e60282a1e4 --- /dev/null +++ b/pallets/intent/src/tests/dca_intent.rs @@ -0,0 +1,659 @@ +use crate::tests::mock::*; +use crate::types::IntentInput; +use crate::{AccountIntentCount, AccountIntents, Error, Event, IntentOwner, Intents}; +use frame_support::storage::with_transaction; +use frame_support::{assert_noop, assert_ok}; +use hydra_dx_math::ema::EmaPrice; +use ice_support::{DcaParams, IntentData, IntentDataInput, Partial, SwapData}; +use sp_runtime::{DispatchResult, Permill, TransactionOutcome}; + +fn dca_intent(amount_in: u128, amount_out: u128, budget: Option) -> IntentInput { + IntentInput { + data: IntentDataInput::Dca(DcaParams { + asset_in: HDX, + asset_out: DOT, + amount_in, + amount_out, + slippage: Permill::from_percent(3), + budget, + period: 10, + }), + deadline: None, + on_resolved: None, + } +} + +// ---- Submission tests ---- + +#[test] +fn should_add_dca_intent_with_fixed_budget() { + let budget = 5 * ONE_HDX; + let amount_in = ONE_HDX; + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + set_block_number(100); + + let _ = with_transaction(|| { + let id = crate::Pallet::::add_intent(ALICE, dca_intent(amount_in, ONE_DOT, Some(budget))) + .expect("should work"); + + let stored = Intents::::get(id).unwrap(); + match stored.data { + IntentData::Dca(dca) => { + assert_eq!(dca.remaining_budget, budget); + assert_eq!(dca.last_execution_block, 100); + assert_eq!(dca.period, 10); + } + _ => panic!("expected DCA intent"), + } + + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, budget); + assert_eq!(AccountIntents::::get(ALICE, id), Some(())); + assert_eq!(AccountIntentCount::::get(ALICE), 1); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_add_dca_intent_with_rolling_budget() { + let amount_in = ONE_HDX; + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + set_block_number(50); + + let _ = with_transaction(|| { + let id = crate::Pallet::::add_intent(ALICE, dca_intent(amount_in, ONE_DOT, None)) + .expect("should work"); + + let stored = Intents::::get(id).unwrap(); + match stored.data { + IntentData::Dca(dca) => { + assert_eq!(dca.remaining_budget, 2 * amount_in); + assert_eq!(dca.last_execution_block, 50); + } + _ => panic!("expected DCA intent"), + } + + assert_eq!( + orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, + 2 * amount_in + ); + assert_eq!(AccountIntents::::get(ALICE, id), Some(())); + assert_eq!(AccountIntentCount::::get(ALICE), 1); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_fail_dca_period_too_small() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let mut intent = dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX)); + if let IntentDataInput::Dca(ref mut d) = intent.data { + d.period = MIN_DCA_PERIOD - 1; + } + assert_noop!( + crate::Pallet::::add_intent(ALICE, intent), + Error::::InvalidDcaPeriod + ); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_fail_dca_budget_less_than_trade() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let intent = dca_intent(ONE_HDX, ONE_DOT, Some(ONE_HDX / 2)); + assert_noop!( + crate::Pallet::::add_intent(ALICE, intent), + Error::::InvalidDcaBudget + ); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_fail_dca_with_deadline() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let mut intent = dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX)); + intent.deadline = Some(99999); + // The general deadline check fires first (deadline must be in future) + assert_noop!( + crate::Pallet::::add_intent(ALICE, intent), + Error::::InvalidDeadline + ); + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +// ---- Cancellation tests ---- + +#[test] +fn should_cancel_dca_unreserve_remaining_budget() { + let budget = 5 * ONE_HDX; + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + let id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(budget))) + .expect("should work"); + + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, budget); + + assert_ok!(crate::Pallet::::cancel_intent(ALICE, id)); + + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, 0); + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).free, 10 * ONE_HDX); + + assert!(Intents::::get(id).is_none()); + assert!(IntentOwner::::get(id).is_none()); + assert_eq!(AccountIntents::::get(ALICE, id), None); + assert_eq!(AccountIntentCount::::get(ALICE), 0); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +// ---- get_valid_intents tests ---- + +#[test] +fn should_not_include_dca_before_period_elapsed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let _id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + // Block 105 < 100 + 10 + set_block_number(105); + let valid = crate::Pallet::::get_valid_intents(); + assert!(valid.is_empty()); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_include_dca_after_period_elapsed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + // Block 110 = 100 + 10 + set_block_number(110); + let valid = crate::Pallet::::get_valid_intents(); + assert_eq!(valid.len(), 1); + assert_eq!(valid[0].0, id); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_transform_dca_to_swap_in_get_valid_intents() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let _id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + set_block_number(110); + let valid = crate::Pallet::::get_valid_intents(); + assert_eq!(valid.len(), 1); + + match &valid[0].1.data { + IntentData::Swap(swap) => { + assert_eq!(swap.asset_in, HDX); + assert_eq!(swap.asset_out, DOT); + assert_eq!(swap.amount_in, ONE_HDX); + assert_eq!(swap.amount_out, ONE_DOT); // hard limit (no oracle) + assert_eq!(swap.partial, Partial::No); + } + _ => panic!("expected Swap (transformed from DCA)"), + } + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_use_hard_limit_in_get_valid_intents_with_oracle() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let _id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + // Oracle says 1 HDX = 2 DOT (n/d with d > n means: d asset_in per n asset_out) + // estimated_out = amount_in * d/n = ONE_HDX * 1/2 = ONE_HDX/2 + set_oracle_price(Some(EmaPrice { n: 2, d: 1 })); + set_block_number(110); + + let valid = crate::Pallet::::get_valid_intents(); + assert_eq!(valid.len(), 1); + + match &valid[0].1.data { + IntentData::Swap(swap) => { + // get_valid_intents uses hard limit, not oracle effective limit + assert_eq!(swap.amount_out, ONE_DOT); + } + _ => panic!("expected Swap"), + } + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_use_hard_limit_when_oracle_unavailable() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let _id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + set_block_number(110); + let valid = crate::Pallet::::get_valid_intents(); + assert_eq!(valid.len(), 1); + + match &valid[0].1.data { + IntentData::Swap(swap) => { + assert_eq!(swap.amount_out, ONE_DOT); + } + _ => panic!("expected Swap"), + } + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +// ---- Resolution tests ---- + +#[test] +fn should_resolve_dca_trade_and_update_state() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + set_block_number(110); + // Simulate ICE unlock (happens in submit_solution before intent_resolved) + assert_ok!(crate::Pallet::::unlock_funds(&ALICE, HDX, ONE_HDX)); + let resolve = ice_support::ResolvedIntent { + id, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: ONE_HDX, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }), + }; + assert_ok!(crate::Pallet::::intent_resolved(&ALICE, &resolve, 0)); + + // Intent still exists + let stored = Intents::::get(id).unwrap(); + match stored.data { + IntentData::Dca(dca) => { + assert_eq!(dca.remaining_budget, 4 * ONE_HDX); + assert_eq!(dca.last_execution_block, 110); + } + _ => panic!("expected DCA intent"), + } + + // Intent index still present (not exhausted yet) + assert_eq!(AccountIntents::::get(ALICE, id), Some(())); + assert_eq!(AccountIntentCount::::get(ALICE), 1); + + // DcaTradeExecuted event + let events = frame_system::Pallet::::events(); + assert!(events + .iter() + .any(|e| matches!(e.event, RuntimeEvent::IntentPallet(Event::DcaTradeExecuted { .. })))); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_complete_dca_when_budget_exhausted() { + let amount_in = ONE_HDX; + let budget = 2 * ONE_HDX; + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(amount_in, ONE_DOT, Some(budget))) + .expect("should work"); + + // First trade - simulate ICE unlock + set_block_number(110); + assert_ok!(crate::Pallet::::unlock_funds(&ALICE, HDX, amount_in)); + let resolve1 = ice_support::ResolvedIntent { + id, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }), + }; + assert_ok!(crate::Pallet::::intent_resolved(&ALICE, &resolve1, 0)); + assert!(Intents::::get(id).is_some()); + + // Second trade — budget exhausted — simulate ICE unlock + set_block_number(120); + assert_ok!(crate::Pallet::::unlock_funds(&ALICE, HDX, amount_in)); + let resolve2 = ice_support::ResolvedIntent { + id, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }), + }; + assert_ok!(crate::Pallet::::intent_resolved(&ALICE, &resolve2, 0)); + + assert!(Intents::::get(id).is_none()); + assert!(IntentOwner::::get(id).is_none()); + assert_eq!(AccountIntents::::get(ALICE, id), None); + assert_eq!(AccountIntentCount::::get(ALICE), 0); + // ICE unlocked 2*amount_in, intent_resolved unreserved remaining (0). Total reserve = 0. + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, 0); + + let events = frame_system::Pallet::::events(); + assert!(events + .iter() + .any(|e| matches!(e.event, RuntimeEvent::IntentPallet(Event::DcaCompleted { .. })))); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_validate_dca_hard_limit() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + set_block_number(110); + // Simulate ICE unlock + assert_ok!(crate::Pallet::::unlock_funds(&ALICE, HDX, ONE_HDX)); + let resolve = ice_support::ResolvedIntent { + id, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: ONE_HDX, + amount_out: ONE_DOT / 2, // below hard limit + partial: Partial::No, + }), + }; + assert_noop!( + crate::Pallet::::intent_resolved(&ALICE, &resolve, 0), + Error::::LimitViolation + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +// ---- compute_surplus tests ---- + +#[test] +fn should_compute_surplus_from_hard_limit_for_dca() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + let intent = Intents::::get(id).unwrap(); + let resolve_data = IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: ONE_HDX, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }); + + // Surplus computed against hard limit (ONE_DOT), not oracle + let surplus = crate::Pallet::::compute_surplus(&intent, &resolve_data); + assert_eq!(surplus, Some(2 * ONE_DOT - ONE_DOT)); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_compute_surplus_with_hard_limit_when_no_oracle() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(ONE_HDX, ONE_DOT, Some(5 * ONE_HDX))) + .expect("should work"); + + let intent = Intents::::get(id).unwrap(); + let resolve_data = IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in: ONE_HDX, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }); + + let surplus = crate::Pallet::::compute_surplus(&intent, &resolve_data); + assert_eq!(surplus, Some(2 * ONE_DOT - ONE_DOT)); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +// ---- Rolling DCA tests ---- + +#[test] +fn should_rolling_dca_re_reserve_after_trade() { + let amount_in = ONE_HDX; + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 10 * ONE_HDX)]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(amount_in, ONE_DOT, None)) + .expect("should work"); + + assert_eq!( + orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, + 2 * amount_in + ); + + set_block_number(110); + // Simulate ICE unlock + assert_ok!(crate::Pallet::::unlock_funds(&ALICE, HDX, amount_in)); + let resolve = ice_support::ResolvedIntent { + id, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }), + }; + assert_ok!(crate::Pallet::::intent_resolved(&ALICE, &resolve, 0)); + + let stored = Intents::::get(id).unwrap(); + match stored.data { + IntentData::Dca(dca) => { + // remaining = 2x - 1x = 1x, then re-reserve 1x = 2x + assert_eq!(dca.remaining_budget, 2 * amount_in); + } + _ => panic!("expected DCA"), + } + + assert_eq!( + orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, + 2 * amount_in + ); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} + +#[test] +fn should_complete_rolling_dca_when_free_balance_insufficient() { + let amount_in = ONE_HDX; + // Give ALICE 2x + a tiny bit extra so rolling DCA can be created + // but NOT enough for continuous re-reservation + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 2 * ONE_HDX), + (BOB, HDX, 10 * ONE_HDX), // holding pot stand-in + ]) + .build() + .execute_with(|| { + let _ = with_transaction(|| { + set_block_number(100); + let id = crate::Pallet::::add_intent(ALICE, dca_intent(amount_in, ONE_DOT, None)) + .expect("should work"); + + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).free, 0); + + // Simulate ICE: unlock + transfer to holding pot (BOB) + set_block_number(110); + assert_ok!(crate::Pallet::::unlock_funds(&ALICE, HDX, amount_in)); + assert_ok!(orml_tokens::Pallet::::transfer( + RuntimeOrigin::signed(ALICE), + BOB, + HDX, + amount_in + )); + // Now ALICE: free=0, reserved=amount_in + + let resolve = ice_support::ResolvedIntent { + id, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }), + }; + assert_ok!(crate::Pallet::::intent_resolved(&ALICE, &resolve, 0)); + + // remaining = 2x - 1x = 1x, re-reserve fails (no free), remaining stays 1x + let stored = Intents::::get(id).unwrap(); + match stored.data { + IntentData::Dca(dca) => { + assert_eq!(dca.remaining_budget, amount_in); + } + _ => panic!("expected DCA"), + } + + // Second trade — simulate ICE: unlock + transfer + set_block_number(120); + assert_ok!(crate::Pallet::::unlock_funds(&ALICE, HDX, amount_in)); + assert_ok!(orml_tokens::Pallet::::transfer( + RuntimeOrigin::signed(ALICE), + BOB, + HDX, + amount_in + )); + + let resolve2 = ice_support::ResolvedIntent { + id, + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DOT, + amount_in, + amount_out: 2 * ONE_DOT, + partial: Partial::No, + }), + }; + assert_ok!(crate::Pallet::::intent_resolved(&ALICE, &resolve2, 0)); + + // DCA completed — removed from storage, no funds left + assert!(Intents::::get(id).is_none()); + assert_eq!(AccountIntents::::get(ALICE, id), None); + assert_eq!(AccountIntentCount::::get(ALICE), 0); + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).reserved, 0); + assert_eq!(orml_tokens::Pallet::::accounts(ALICE, HDX).free, 0); + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); +} diff --git a/pallets/intent/src/tests/intent_resolved.rs b/pallets/intent/src/tests/intent_resolved.rs new file mode 100644 index 0000000000..0bf7ecb030 --- /dev/null +++ b/pallets/intent/src/tests/intent_resolved.rs @@ -0,0 +1,938 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::{assert_noop, assert_ok}; +use pretty_assertions::assert_eq; + +#[test] +fn should_work_with_intent_without_deadline() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: Some(BoundedVec::new()), + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1; + let resolve = IntentPallet::get_intent(1).expect("intent to exist"); + let who = IntentPallet::intent_owner(id).expect("intent owner to exist"); + assert_eq!(get_queued_task(Source::ICE(id)), None); + + assert_ok!(IntentPallet::intent_resolved( + &who, + &ResolvedIntent { id, data: resolve.data }, + 0, + )); + + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!(AccountIntents::::get(who, id), None); + assert_eq!(IntentPallet::account_intent_count(who), 0); + assert_eq!(get_queued_task(Source::ICE(id)), Some((Source::ICE(id), who))); + }); +} + +#[test] +fn non_partial_should_remove_intent_and_owner_when_resolved_exactly() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: Some(BoundedVec::new()), + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1; + let resolve = IntentPallet::get_intent(1).expect("intent to exist"); + let who = IntentPallet::intent_owner(id).expect("intent owner to exist"); + assert_eq!(get_queued_task(Source::ICE(id)), None); + + assert_ok!(IntentPallet::intent_resolved( + &who, + &ResolvedIntent { id, data: resolve.data }, + 0, + )); + + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!(AccountIntents::::get(who, id), None); + assert_eq!(IntentPallet::account_intent_count(who), 0); + assert_eq!(get_queued_task(Source::ICE(id)), Some((Source::ICE(id), who))); + }); +} + +#[test] +fn non_partial_should_remove_intent_and_owner_when_resolved_better_than_limits() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let (id, mut resolve) = IntentPallet::get_valid_intents()[0].to_owned(); + let who = IntentPallet::intent_owner(id).expect("intent owner to exists"); + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out += 1_000_000; + + assert_ok!(IntentPallet::intent_resolved( + &who, + &ResolvedIntent { id, data: resolve.data }, + 0, + )); + + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!(AccountIntents::::get(who, id), None); + assert_eq!(IntentPallet::account_intent_count(who), 0); + }); +} + +#[test] +fn non_partial_should_not_work_when_resolved_bellow_limits() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let who = ALICE; + let id = 0_u128; + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + //amout in is < than ExactIn + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in -= 1; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::LimitViolation + ); + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + //amout in is > than ExactIn + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in += 1; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::LimitViolation + ); + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + //amout out is < than amount out limit + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out -= 1; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::LimitViolation + ); + }); +} + +#[test] +fn should_not_work_when_non_partial_intent_resolved_partially() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1_u128; + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let who = BOB; + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::LimitViolation + ); + }); +} + +#[test] +fn partial_intent_should_remove_intent_and_owner_when_resolved_exactly() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: Some(BoundedVec::new()), + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1; + let resolve = IntentPallet::get_intent(id).expect("intent to exit"); + let who = IntentPallet::intent_owner(id).expect("intent owner to exist"); + + assert_eq!(get_queued_task(Source::ICE(id)), None); + + assert_ok!(IntentPallet::intent_resolved( + &who, + &ResolvedIntent { id, data: resolve.data }, + 0, + ),); + + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!(AccountIntents::::get(who, id), None); + assert_eq!(IntentPallet::account_intent_count(who), 0); + assert_eq!(get_queued_task(Source::ICE(id)), Some((Source::ICE(id), who))); + }); +} + +#[test] +fn partial_intent_should_remove_intent_and_owner_when_resolved_fully_and_better_than_limits() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: Some(BoundedVec::new()), + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1; + let mut resolve = IntentPallet::get_intent(1).expect("intent to exist"); + let who = IntentPallet::intent_owner(id).expect("intent owner to exist"); + assert_eq!(get_queued_task(Source::ICE(id)), None); + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out += 1_000_000; + + assert_ok!(IntentPallet::intent_resolved( + &who, + &ResolvedIntent { id, data: resolve.data }, + 0, + ),); + + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!(AccountIntents::::get(who, id), None); + assert_eq!(IntentPallet::account_intent_count(who), 0); + assert_eq!(get_queued_task(Source::ICE(id)), Some((Source::ICE(id), who))); + }); +} + +#[test] +fn partial_intent_should_not_remove_intent_and_owner_when_not_resolved_fully() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1_u128; + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let who = IntentPallet::intent_owner(id).expect("intent owner to exists"); + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + assert_ok!(IntentPallet::intent_resolved( + &who, + &ResolvedIntent { id, data: resolve.data }, + 0, + ),); + + let expected_intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: Partial::Yes(ONE_QUINTIL / 2), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + assert_eq!(IntentPallet::get_intent(id), Some(expected_intent)); + assert!(IntentPallet::intent_owner(id).is_some()); + // Partial resolution must NOT remove the account index + assert_eq!(AccountIntents::::get(who, id), Some(())); + assert_eq!(IntentPallet::account_intent_count(who), 1); + }); +} + +#[test] +fn partial_intent_should_not_work_when_resolved_fully_and_bellow_limit() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let who = ALICE; + let id = 0_u128; + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + //amount in > intent.exactIn + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in += 1; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::LimitViolation + ); + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + //amount in > intent.amount_out + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out -= 1; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::LimitViolation + ); + }); +} + +#[test] +fn partial_intent_should_not_work_when_resolved_partially_and_bellow_limit() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let who = ALICE; + let id = 0_u128; + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out = r_swap.amount_out / 2 - 1; //bellow limit + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::LimitViolation + ); + }); +} + +#[test] +fn should_not_work_when_intent_doesnt_exist() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1_u128; + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let who = IntentPallet::intent_owner(id).expect("intent owner to exists"); + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + let non_existing_id = 1_000_000_000_000_000_u128; + assert_noop!( + IntentPallet::intent_resolved( + &who, + &ResolvedIntent { + id: non_existing_id, + data: resolve.data + }, + 0, + ), + Error::::IntentNotFound + ); + }); +} + +#[test] +fn should_not_work_when_resolved_as_not_an_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1_u128; + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let non_owner = CHARLIE; + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + assert_noop!( + IntentPallet::intent_resolved(&non_owner, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::InvalidOwner + ); + }); +} + +#[test] +fn should_not_work_when_intent_expired() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1_u128; + let resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let who = BOB; + + assert_ok!(Timestamp::set( + RuntimeOrigin::none(), + resolve.deadline.expect("intent with deadline") + 1 + )); + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::IntentExpired + ); + }); +} + +#[test] +fn should_not_work_when_assets_doesnt_match() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1_u128; + let who = BOB; + + //NOTE: different assetIn + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.asset_in = HDX; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::ResolveMismatch + ); + + //NOTE: different assetOut + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.asset_out = HDX; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::ResolveMismatch + ); + }); +} + +#[test] +fn should_not_work_when_partial_doesnt_match() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + )]) + .build() + .execute_with(|| { + let id = 0_u128; + let who = ALICE; + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.partial = if r_swap.partial.is_partial() { + Partial::No + } else { + Partial::Yes(0) + }; + + assert_noop!( + IntentPallet::intent_resolved(&who, &ResolvedIntent { id, data: resolve.data }, 0), + Error::::ResolveMismatch + ); + }); +} + +#[test] +fn partial_intent_should_not_queue_callback_when_not_fully_resolved() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: Some(BoundedVec::new()), + }, + ), + ]) + .build() + .execute_with(|| { + let id = 1_u128; + let who = BOB; + assert_eq!(get_queued_task(Source::ICE(id)), None); + + let mut resolve = IntentPallet::get_intent(id).expect("intent to exists"); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + assert_ok!(IntentPallet::intent_resolved( + &who, + &ResolvedIntent { id, data: resolve.data }, + 0, + )); + + assert_eq!(get_queued_task(Source::ICE(id)), None); + // Partial resolution must keep account index + assert_eq!(AccountIntents::::get(who, id), Some(())); + assert_eq!(IntentPallet::account_intent_count(who), 1); + }); +} diff --git a/pallets/intent/src/tests/mock.rs b/pallets/intent/src/tests/mock.rs new file mode 100644 index 0000000000..d73fdcf792 --- /dev/null +++ b/pallets/intent/src/tests/mock.rs @@ -0,0 +1,336 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate as pallet_intent; +use crate::types; +use crate::types::IntentInput; +use crate::Config; +use frame_support::parameter_types; +use frame_support::storage::with_transaction; +use frame_support::traits::Everything; +use hydra_dx_math::ema::EmaPrice; +use hydradx_traits::lazy_executor::Source; +use hydradx_traits::price::PriceProvider; +use hydradx_traits::registry::Inspect; +use ice_support::AssetId; +use ice_support::Balance; +use orml_traits::parameter_type_with_key; +use primitives::constants::time::SLOT_DURATION; +use sp_core::ConstU32; +use sp_core::ConstU64; +use sp_core::H256; +use sp_runtime::traits::BlakeTwo256; +use sp_runtime::traits::IdentityLookup; +use sp_runtime::{BuildStorage, DispatchError, DispatchResult, TransactionOutcome}; +use std::cell::RefCell; +use std::vec; + +pub(crate) const ONE_DOT: u128 = 10_000_000_000; +pub(crate) const ONE_HDX: u128 = 1_000_000_000_000; +pub(crate) const ONE_QUINTIL: u128 = 1_000_000_000_000_000_000; + +pub(crate) const HDX: AssetId = 0; +pub(crate) const HUB_ASSET_ID: AssetId = 1; +pub(crate) const DOT: AssetId = 2; +pub(crate) const ETH: AssetId = 3; +pub(crate) const BTC: AssetId = 4; + +pub(crate) const ALICE: AccountId = 2; +pub(crate) const BOB: AccountId = 3; +pub(crate) const CHARLIE: AccountId = 4; + +//5 SEC. +pub(crate) const MAX_INTENT_DEADLINE: pallet_intent::types::Moment = 5 * ONE_SECOND; +pub(crate) const ONE_SECOND: pallet_intent::types::Moment = 1_000; + +type AccountId = u64; +type Block = frame_system::mocking::MockBlock; + +frame_support::construct_runtime!( + pub enum Test { + System: frame_system, + Currencies: orml_tokens, + Timestamp: pallet_timestamp, + IntentPallet: pallet_intent, + } +); + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 63; +} + +impl frame_system::Config for Test { + type BaseCallFilter = Everything; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = BlockHashCount; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = (); + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = SS58Prefix; + type OnSetCode = (); + type MaxConsumers = frame_support::traits::ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); + type ExtensionsWeightInfo = (); +} + +pub(crate) type Extrinsic = sp_runtime::testing::TestXt; +impl frame_system::offchain::CreateTransactionBase for Test +where + RuntimeCall: From, +{ + type RuntimeCall = RuntimeCall; + type Extrinsic = Extrinsic; +} + +impl hydradx_traits::CreateBare for Test +where + RuntimeCall: From, +{ + fn create_bare(call: Self::RuntimeCall) -> Extrinsic { + Extrinsic::new_bare(call) + } +} + +parameter_type_with_key! { + pub ExistentialDeposits: |_currency_id: AssetId| -> Balance { + 0 + }; +} + +impl orml_tokens::Config for Test { + type Balance = Balance; + type Amount = i128; + type CurrencyId = AssetId; + type WeightInfo = (); + type ExistentialDeposits = ExistentialDeposits; + type CurrencyHooks = (); + type MaxLocks = (); + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type DustRemovalWhitelist = Everything; +} + +parameter_types! { + pub const MinimumPeriod: u64 = SLOT_DURATION / 2; +} + +impl pallet_timestamp::Config for Test { + type Moment = u64; + type MinimumPeriod = MinimumPeriod; + type OnTimestampSet = (); + type WeightInfo = (); +} + +pub(crate) const MIN_DCA_PERIOD: u32 = 5; + +thread_local! { + pub static QUEUD_TASKS: RefCell> = RefCell::new(Vec::default()); + pub static ORACLE_PRICE: RefCell> = RefCell::new(None); + pub static BLOCK_NUMBER: RefCell = RefCell::new(1); +} + +pub fn set_oracle_price(price: Option) { + ORACLE_PRICE.with(|v| *v.borrow_mut() = price); +} + +pub fn set_block_number(n: u64) { + BLOCK_NUMBER.with(|v| *v.borrow_mut() = n); +} + +pub struct MockOracleProvider; +impl PriceProvider for MockOracleProvider { + type Price = EmaPrice; + + fn get_price(_asset_a: AssetId, _asset_b: AssetId) -> Option { + ORACLE_PRICE.with(|v| *v.borrow()) + } +} + +pub struct MockBlockNumberProvider; +impl sp_runtime::traits::BlockNumberProvider for MockBlockNumberProvider { + type BlockNumber = u64; + + fn current_block_number() -> Self::BlockNumber { + BLOCK_NUMBER.with(|v| *v.borrow()) + } +} + +pub struct DummyLazyExecutor(sp_std::marker::PhantomData); +impl hydradx_traits::lazy_executor::Mutate for DummyLazyExecutor { + type Error = DispatchError; + type BoundedCall = types::CallData; + + fn queue(src: Source, origin: AccountId, _call: Self::BoundedCall) -> Result<(), Self::Error> { + QUEUD_TASKS.with(|v| { + if get_queued_task(src.clone()).is_some() { + return Err(DispatchError::Other("Duplicate intent")); + } + + v.borrow_mut().push((src, origin)); + + Ok(()) + }) + } +} + +pub fn get_queued_task(src: Source) -> Option<(Source, AccountId)> { + QUEUD_TASKS.with(|v| { + let m = v.borrow(); + + if let Some((_, (_, acc))) = m.clone().into_iter().enumerate().find(|x| x.1 .0 == src) { + Some((src, acc)) + } else { + None + } + }) +} + +pub struct DummyRegistry; + +impl Inspect for DummyRegistry { + type AssetId = AssetId; + type Location = u8; + + fn exists(_id: Self::AssetId) -> bool { + todo!() + } + + fn decimals(_id: Self::AssetId) -> Option { + todo!() + } + + fn is_banned(_id: Self::AssetId) -> bool { + todo!() + } + + fn asset_type(_id: Self::AssetId) -> Option { + todo!() + } + + fn asset_name(_id: Self::AssetId) -> Option> { + todo!() + } + + fn asset_symbol(_id: Self::AssetId) -> Option> { + todo!() + } + + fn is_sufficient(_id: Self::AssetId) -> bool { + todo!() + } + + fn existential_deposit(_id: Self::AssetId) -> Option { + Some(1_000) + } +} + +parameter_types! { + pub const MinDcaPeriod: u32 = MIN_DCA_PERIOD; +} + +impl pallet_intent::Config for Test { + type Currency = Currencies; + type LazyExecutorHandler = DummyLazyExecutor; + type RegistryHandler = DummyRegistry; + type TimestampProvider = Timestamp; + type HubAssetId = ConstU32; + type MaxAllowedIntentDuration = ConstU64; + type OraclePriceProvider = MockOracleProvider; + type BlockNumberProvider = MockBlockNumberProvider; + type MinDcaPeriod = MinDcaPeriod; + type MaxIntentsPerAccount = ConstU32<5>; + type WeightInfo = (); +} + +pub struct ExtBuilder { + endowed_accounts: Vec<(AccountId, AssetId, Balance)>, + intents: Vec<(AccountId, IntentInput)>, +} + +impl Default for ExtBuilder { + fn default() -> Self { + QUEUD_TASKS.with(|v| { + v.borrow_mut().clear(); + }); + ORACLE_PRICE.with(|v| *v.borrow_mut() = None); + BLOCK_NUMBER.with(|v| *v.borrow_mut() = 1); + + Self { + endowed_accounts: vec![], + intents: vec![], + } + } +} + +impl ExtBuilder { + pub fn with_endowed_accounts(mut self, accounts: Vec<(AccountId, AssetId, Balance)>) -> Self { + self.endowed_accounts = accounts; + self + } + + pub fn with_intents(mut self, intents: Vec<(AccountId, IntentInput)>) -> Self { + self.intents = intents; + self + } + + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + orml_tokens::GenesisConfig:: { + balances: self + .endowed_accounts + .iter() + .flat_map(|(x, asset, amount)| vec![(*x, *asset, *amount)]) + .collect(), + } + .assimilate_storage(&mut t) + .unwrap(); + let mut r: sp_io::TestExternalities = t.into(); + + r.execute_with(|| { + frame_system::Pallet::::set_block_number(1); + + let _ = with_transaction(|| { + for (owner, intent) in self.intents { + pallet_intent::Pallet::::add_intent(owner, intent).expect("add_intent should work"); + } + + TransactionOutcome::Commit(DispatchResult::Ok(())) + }); + }); + + r + } +} diff --git a/pallets/intent/src/tests/mod.rs b/pallets/intent/src/tests/mod.rs new file mode 100644 index 0000000000..7d1f819338 --- /dev/null +++ b/pallets/intent/src/tests/mod.rs @@ -0,0 +1,10 @@ +mod add_intent; +mod cancel_intent; +mod cleanup_intent; +mod dca_intent; +mod intent_resolved; +mod mock; +mod ocw; +mod remove_intent; +mod submit_intent; +mod validate_resolve; diff --git a/pallets/intent/src/tests/ocw.rs b/pallets/intent/src/tests/ocw.rs new file mode 100644 index 0000000000..e548dbfd47 --- /dev/null +++ b/pallets/intent/src/tests/ocw.rs @@ -0,0 +1,347 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::{assert_noop, assert_ok}; + +#[test] +fn validate_unsingned_should_work_when_intent_is_expired() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exist"); + + assert_ok!(Timestamp::set( + RuntimeOrigin::none(), + intent.deadline.expect("intent with deadline") + 1 + )); + + let c = Call::cleanup_intent { id }; + assert_eq!( + IntentPallet::validate_unsigned(TransactionSource::Local, &c), + Ok(ValidTransaction { + priority: UNSIGNED_TXS_PRIORITY, + provides: vec![(OCW_TAG_PREFIX, Encode::encode(&id)).encode()], + requires: vec![], + longevity: 1, + propagate: false, + }) + ); + }); +} + +#[test] +fn validate_unsingned_should_not_work_when_intent_doesnt_exists() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + + let c = Call::cleanup_intent { id }; + assert_noop!( + IntentPallet::validate_unsigned(TransactionSource::Local, &c), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} + +#[test] +fn validate_unsingned_should_not_work_when_intent_is_not_expired() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exist"); + + assert_ok!(Timestamp::set( + RuntimeOrigin::none(), + intent.deadline.expect("intent with deadline") - 1 + )); + + let c = Call::cleanup_intent { id }; + assert_noop!( + IntentPallet::validate_unsigned(TransactionSource::Local, &c), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} + +#[test] +fn validate_unsingned_should_not_work_when_tx_is_external() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exist"); + + assert_ok!(Timestamp::set( + RuntimeOrigin::none(), + intent.deadline.expect("intent with deadline") + 1 + )); + + let c = Call::cleanup_intent { id }; + assert_noop!( + IntentPallet::validate_unsigned(TransactionSource::External, &c), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} + +#[test] +fn validate_unsingned_should_not_work_when_intent_has_no_deadline() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + + let c = Call::cleanup_intent { id }; + assert_noop!( + IntentPallet::validate_unsigned(TransactionSource::Local, &c), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} diff --git a/pallets/intent/src/tests/remove_intent.rs b/pallets/intent/src/tests/remove_intent.rs new file mode 100644 index 0000000000..ef4afe5668 --- /dev/null +++ b/pallets/intent/src/tests/remove_intent.rs @@ -0,0 +1,354 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_noop; +use frame_support::assert_ok; +use pretty_assertions::assert_eq; +use sp_runtime::traits::BadOrigin; + +#[test] +fn should_work_when_canceled_by_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let intent = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + intent.data.amount_in(), + ); + + //Act + assert_ok!(IntentPallet::remove_intent(RuntimeOrigin::signed(owner), id)); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, intent.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + assert_eq!(IntentPallet::account_intent_count(owner), 1); // ALICE still has intent 2 + }); +} + +#[test] +fn should_work_when_intent_was_partially_resolved_and_canceled_by_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let mut resolve = IntentPallet::get_intent(id).expect("Intent to exists"); + let owner = ALICE; + + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + //NOTE: It's ICE pallet responsibility is to unlock used fund during solution execution. This is + //to simulate it. + assert_eq!( + Currencies::unreserve_named( + &NAMED_RESERVE_ID, + resolve.data.asset_in(), + &owner, + resolve.data.amount_in() + ), + 0 + ); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, resolve.data.asset_in(), &owner), + 5_000_000_000_000_u128 + ); + assert_ok!(IntentPallet::intent_resolved( + &owner, + &ResolvedIntent { + id, + data: resolve.data.clone() + }, + 0, + )); + + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, resolve.data.asset_in(), &owner), + resolve.data.amount_in(), + ); + + //Act + assert_ok!(IntentPallet::remove_intent(RuntimeOrigin::signed(owner), id)); + + //Assert + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(IntentPallet::intent_owner(id), None); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, resolve.data.asset_in(), &owner), + 0 + ); + assert_eq!(AccountIntents::::get(owner, id), None); + }); +} + +#[test] +fn should_not_work_when_intent_doesnt_exist() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: BTC, + amount_in: 30 * ONE_QUINTIL, + amount_out: ONE_QUINTIL, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 9_u128; + let owner = ALICE; + + //Act & Assert; + assert_noop!( + IntentPallet::remove_intent(RuntimeOrigin::signed(owner), id), + Error::::IntentNotFound + ); + }); +} + +#[test] +fn should_not_work_when_canceled_non_owner() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 0_u128; + let non_owner = BOB; + + //Act & Assert; + assert_noop!( + IntentPallet::remove_intent(RuntimeOrigin::signed(non_owner), id), + Error::::InvalidOwner + ); + }); +} + +#[test] +fn should_not_work_when_origin_is_none() { + ExtBuilder::default() + .with_endowed_accounts(vec![ + (ALICE, HDX, 100 * ONE_HDX), + (ALICE, ETH, 30 * ONE_QUINTIL), + (BOB, ETH, 5 * ONE_QUINTIL), + ]) + .with_intents(vec![ + ( + ALICE, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 100 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ( + BOB, + IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: ETH, + asset_out: DOT, + amount_in: ONE_QUINTIL, + amount_out: 1_500 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }, + ), + ]) + .build() + .execute_with(|| { + let id = 73786976294838206464000_u128; + + //Act & Assert; + assert_noop!(IntentPallet::remove_intent(RuntimeOrigin::none(), id), BadOrigin); + }); +} diff --git a/pallets/intent/src/tests/submit_intent.rs b/pallets/intent/src/tests/submit_intent.rs new file mode 100644 index 0000000000..da152b724b --- /dev/null +++ b/pallets/intent/src/tests/submit_intent.rs @@ -0,0 +1,384 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_noop; +use frame_support::assert_ok; +use pretty_assertions::assert_eq; +use sp_runtime::traits::BadOrigin; + +#[test] +fn should_work_when_origin_signed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let id: IntentId = 0; + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + //Act + assert_ok!(IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0)); + + let stored = IntentPallet::get_intent(id).expect("intent should be stored"); + assert_eq!(stored.data.asset_in(), HDX); + assert_eq!(stored.data.asset_out(), DOT); + assert_eq!(stored.data.amount_in(), 10 * ONE_HDX); + assert_eq!(stored.deadline, Some(MAX_INTENT_DEADLINE - 1)); + assert_eq!(IntentPallet::intent_owner(id), Some(ALICE)); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), + 10 * ONE_HDX + ); + assert_eq!(AccountIntents::::get(ALICE, id), Some(())); + assert_eq!(IntentPallet::account_intent_count(ALICE), 1); + }); +} + +#[test] +fn should_work_when_intent_has_no_deadline() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let id: IntentId = 0; + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: None, + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + //Act + assert_ok!(IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0)); + + let stored = IntentPallet::get_intent(id).expect("intent should be stored"); + assert_eq!(stored.data.asset_in(), HDX); + assert_eq!(stored.data.asset_out(), DOT); + assert_eq!(stored.deadline, None); + assert_eq!(IntentPallet::intent_owner(id), Some(ALICE)); + assert_eq!( + Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), + 10 * ONE_HDX + ); + assert_eq!(AccountIntents::::get(ALICE, id), Some(())); + assert_eq!(IntentPallet::account_intent_count(ALICE), 1); + }); +} + +#[test] +fn should_not_work_when_origin_is_none() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let id: IntentId = 92215273624474048528384; + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + //Act + assert_noop!(IntentPallet::submit_intent(RuntimeOrigin::none(), intent_0), BadOrigin); + }); +} + +#[test] +fn should_not_work_when_deadline_is_less_than_now() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + assert_ok!(Timestamp::set(RuntimeOrigin::none(), 2 * MAX_INTENT_DEADLINE)); + + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0), + Error::::InvalidDeadline + ); + }); +} + +#[test] +fn should_not_work_when_deadline_bigger_than_max_allowed_intent_duration() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE + 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0), + Error::::InvalidDeadline + ); + }); +} + +#[test] +fn should_not_work_when_amount_in_is_zero() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 0, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0), + Error::::InvalidIntent + ); + }); +} + +#[test] +fn should_not_work_when_amount_out_is_zero() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 0, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0), + Error::::InvalidIntent + ); + }); +} + +#[test] +fn should_not_work_when_asset_in_eq_asset_out() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: HDX, + amount_in: 10 * ONE_HDX, + amount_out: 10 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0), + Error::::InvalidIntent + ); + }); +} + +#[test] +fn should_not_work_when_asset_out_is_hub_asset() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: HUB_ASSET_ID, + amount_in: 10 * ONE_HDX, + amount_out: 10 * ONE_HDX, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0), + Error::::InvalidIntent + ); + }); +} + +#[test] +fn should_not_work_when_cant_reserve_funds() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0), + orml_tokens::Error::::BalanceTooLow + ); + }); +} + +#[test] +fn should_work_when_intent_is_partial() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let intent_0 = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: 1_000 * ONE_DOT, + partial: true, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + //Act&assert + assert_ok!(IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent_0)); + }); +} + +#[test] +fn should_not_work_when_amount_in_is_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let id: IntentId = 92215273624474048528384; + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let ed = DummyRegistry::existential_deposit(HDX).expect("dummy registry to work"); + + let intent = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: ed - 1, + amount_out: 1_000 * ONE_DOT, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + //Act&Assert + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent), + Error::::InvalidIntent + ); + }); +} + +#[test] +fn should_not_work_when_amount_out_is_less_than_ed() { + ExtBuilder::default() + .with_endowed_accounts(vec![(ALICE, HDX, 100 * ONE_HDX), (BOB, ETH, 5 * ONE_QUINTIL)]) + .build() + .execute_with(|| { + let id: IntentId = 92215273624474048528384; + assert_eq!(IntentPallet::get_intent(id), None); + assert_eq!(Currencies::reserved_balance_named(&NAMED_RESERVE_ID, HDX, &ALICE), 0); + assert_eq!(Intents::::iter_keys().count(), 0); + + let ed = DummyRegistry::existential_deposit(DOT).expect("dummy registry to work"); + + let intent = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DOT, + amount_in: 10 * ONE_HDX, + amount_out: ed - 1, + partial: false, + }), + deadline: Some(MAX_INTENT_DEADLINE - 1), + on_resolved: Some(BoundedVec::truncate_from(b"success".to_vec())), + }; + + //Act&Assert + assert_noop!( + IntentPallet::submit_intent(RuntimeOrigin::signed(ALICE), intent), + Error::::InvalidIntent + ); + }); +} diff --git a/pallets/intent/src/tests/validate_resolve.rs b/pallets/intent/src/tests/validate_resolve.rs new file mode 100644 index 0000000000..a899cd6e83 --- /dev/null +++ b/pallets/intent/src/tests/validate_resolve.rs @@ -0,0 +1,497 @@ +use crate::tests::mock::*; +use crate::*; +use frame_support::assert_noop; +use frame_support::assert_ok; + +#[test] +fn non_partial_swap_intent_should_work_when_resolved_exactly() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let resolve = intent.clone(); + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let resolve = intent.clone(); + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + }); +} + +#[test] +fn should_work_when_resolved_exactly_and_intent_has_no_deadline() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: None, + on_resolved: None, + }; + + let resolve = intent.clone(); + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + + //ExactOut + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let resolve = intent.clone(); + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + }); +} + +#[test] +fn non_partial_swap_intent_should_work_when_resolved_better() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out += 2 * ONE_HDX; + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out += ONE_DOT; + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + }); +} + +#[test] +fn partial_swap_intent_should_work_when_resolved_exactly() { + ExtBuilder::default().build().execute_with(|| { + //ExactIn + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let resolve = intent.clone(); + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let resolve = intent.clone(); + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + }); +} + +#[test] +fn partial_swap_intent_should_work_when_resolved_better() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out += 2 * ONE_HDX; + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in -= ONE_HDX; + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + }); +} + +#[test] +fn partial_should_work_when_resolved_partially() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out /= 2; + + assert_ok!(IntentPallet::validate_resolve(&intent, &resolve.data)); + }); +} + +#[test] +fn swap_intent_should_not_work_when_asset_in_does_not_match() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.asset_in = ETH; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::ResolveMismatch + ); + }); +} + +#[test] +fn swap_intent_should_not_work_when_asset_out_does_not_match() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.asset_out = ETH; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::ResolveMismatch + ); + }); +} + +#[test] +fn swap_intent_should_not_work_when_partiality_does_not_match() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.partial = if r_swap.partial.is_partial() { + Partial::No + } else { + Partial::Yes(0) + }; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::ResolveMismatch + ); + }); +} + +#[test] +fn non_partial_swap_exact_in_intent_should_not_work_when_amount_out_is_less_than_limit() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out -= 1; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::LimitViolation + ); + }); +} + +#[test] +fn non_partial_swap_exact_in_intent_should_not_work_when_amount_in_is_not_exact() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::No, + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + //smaller than limit + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in -= 1; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::LimitViolation + ); + + //bigger than limit + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in += 1; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::LimitViolation + ); + }); +} + +#[test] +fn partial_swap_exact_in_should_not_work_when_resolved_fully_and_amount_out_is_less_than_limit() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_out -= 1; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::LimitViolation + ); + }); +} + +#[test] +fn partial_swap_exact_in_should_not_work_when_amount_in_is_bigger_limit() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in += 1; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::LimitViolation + ); + }); +} + +#[test] +fn partial_swap_exact_in_should_not_work_when_resolved_partially_and_amount_out_is_less_than_pro_rata_limit() { + ExtBuilder::default().build().execute_with(|| { + let intent = Intent { + data: IntentData::Swap(SwapData { + asset_in: DOT, + asset_out: HDX, + amount_in: 20_000 * ONE_DOT, + amount_out: 10_000 * ONE_HDX, + partial: Partial::Yes(0), + }), + deadline: Some(MAX_INTENT_DEADLINE - ONE_SECOND), + on_resolved: None, + }; + + //NOTE: resolve 50% of intent so amount_out >= pro-rata limit(50%) + let mut resolve = intent.clone(); + let IntentData::Swap(ref mut r_swap) = resolve.data else { + panic!("expected Swap"); + }; + r_swap.amount_in /= 2; + r_swap.amount_out = r_swap.amount_out / 2 - 1; + + assert_noop!( + IntentPallet::validate_resolve(&intent, &resolve.data), + Error::::LimitViolation + ); + }); +} diff --git a/pallets/intent/src/types.rs b/pallets/intent/src/types.rs new file mode 100644 index 0000000000..0b9c100c37 --- /dev/null +++ b/pallets/intent/src/types.rs @@ -0,0 +1,27 @@ +use codec::{Decode, DecodeWithMemTracking, Encode, MaxEncodedLen}; +use frame_support::pallet_prelude::{RuntimeDebug, TypeInfo}; +use frame_support::traits::ConstU32; +use ice_support::{IntentData, IntentDataInput}; +use sp_runtime::BoundedVec; + +pub const MAX_DATA_SIZE: u32 = 512; +pub type Moment = u64; +pub type IncrementalIntentId = u64; +pub type CallData = BoundedVec>; + +/// User-facing intent for extrinsic submission. +/// Uses IntentDataInput which excludes internal DCA state fields. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, DecodeWithMemTracking, TypeInfo)] +pub struct IntentInput { + pub data: IntentDataInput, + pub deadline: Option, + pub on_resolved: Option, +} + +/// Internal intent representation stored on-chain. +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, DecodeWithMemTracking, TypeInfo)] +pub struct Intent { + pub data: IntentData, + pub deadline: Option, + pub on_resolved: Option, +} diff --git a/pallets/intent/src/weights.rs b/pallets/intent/src/weights.rs new file mode 100644 index 0000000000..07d6dd6d80 --- /dev/null +++ b/pallets/intent/src/weights.rs @@ -0,0 +1,21 @@ +use frame_support::pallet_prelude::Weight; + +pub trait WeightInfo { + fn submit_intent() -> Weight; + fn remove_intent() -> Weight; + fn cleanup_intent() -> Weight; +} + +impl WeightInfo for () { + fn submit_intent() -> Weight { + Weight::default() + } + + fn remove_intent() -> Weight { + Weight::default() + } + + fn cleanup_intent() -> Weight { + Weight::default() + } +} diff --git a/pallets/lazy-executor/Cargo.toml b/pallets/lazy-executor/Cargo.toml new file mode 100644 index 0000000000..8e2551947b --- /dev/null +++ b/pallets/lazy-executor/Cargo.toml @@ -0,0 +1,57 @@ +[package] +name = "pallet-lazy-executor" +version = "1.0.0" +authors = ['GalacticCouncil'] +edition = "2021" +license = "Apache-2.0" +homepage = 'https://github.com/galacticcouncil/hydradx-node' +repository = 'https://github.com/galacticcouncil/hydradx-node' +description = "HydraDX Lazy Executor" +readme = "README.md" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] +# parity +scale-info = { workspace = true } +codec = { workspace = true } +serde = { workspace = true, optional = true } +log = { workspace = true } + +sp-runtime = { workspace = true } +sp-std = { workspace = true } +sp-io = { workspace = true } +sp-core = { workspace = true } +pallet-transaction-payment = { workspace = true } +frame-support = { workspace = true } +frame-system = { workspace = true } +frame-benchmarking = { workspace = true, optional = true } +hydradx-traits = { workspace = true } + +[dev-dependencies] +pretty_assertions = { workspace = true } +pallet-balances = { workspace = true } +hex-literal = { workspace = true } + +[features] +default = ["std"] +std = [ + "codec/std", + "frame-benchmarking/std", + "frame-support/std", + "frame-system/std", + "scale-info/std", + "serde", + "sp-io/std", + "sp-runtime/std", + "sp-std/std", + "log/std", + "sp-core/std", + "pallet-balances/std", + "pallet-transaction-payment/std", + "hydradx-traits/std", +] + +runtime-benchmarks = [ "frame-benchmarking/runtime-benchmarks" ] +try-runtime = [ "frame-support/try-runtime" ] diff --git a/pallets/lazy-executor/src/lib.rs b/pallets/lazy-executor/src/lib.rs new file mode 100644 index 0000000000..5adda33688 --- /dev/null +++ b/pallets/lazy-executor/src/lib.rs @@ -0,0 +1,370 @@ +// Copyright (C) 2020-2026 Intergalactic, Limited (GIB). SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +//http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +//! # Lazy-Executor Pallet + +#![cfg_attr(not(feature = "std"), no_std)] + +use codec::{Decode, Encode, MaxEncodedLen}; +use frame_support::{ + dispatch::{GetDispatchInfo, Pays, PostDispatchInfo}, + pallet_prelude::{RuntimeDebug, TypeInfo}, + traits::ConstU32, + transactional, + weights::Weight, +}; +use frame_system::{offchain::SubmitTransaction, pallet_prelude::*, Origin}; +use hydradx_traits::lazy_executor::Source; +use pallet_transaction_payment::OnChargeTransaction; +use sp_runtime::{ + traits::{Dispatchable, One}, + BoundedVec, DispatchError, +}; + +pub use pallet::*; +pub mod weights; +pub use weights::WeightInfo; + +#[cfg(test)] +mod tests; + +pub type CallId = u128; +pub const MAX_DATA_SIZE: u32 = 512; +pub type BoundedCall = BoundedVec>; +type BalanceOf = <::OnChargeTransaction as OnChargeTransaction>::Balance; + +#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, MaxEncodedLen, TypeInfo)] +pub struct CallData { + origin: AccountId, + call: BoundedCall, +} + +const NO_TIP: u32 = 0; +//Encoded call's length offset for additional extrinsic's data in bytes. +//4(length) + 1(version&type) + 32(signer) + 65(signature) + 16(tip) + 40(signedExtras) + 16(tip) +//NOTE: this is approximate number +const CALL_LEN_OFFSET: u32 = 158; +const LOG_TARGET: &str = "runtime::pallet-lazy-executor"; +pub(crate) const OCW_TAG_PREFIX: &str = "lazy-executor-dispatch-top"; + +#[frame_support::pallet] +pub mod pallet { + use super::*; + use frame_support::{ + dispatch::{DispatchInfo, DispatchResult}, + pallet_prelude::{TransactionSource, TransactionValidity, ValueQuery, *}, + }; + use hydradx_traits::CreateBare; + + #[pallet::config] + pub trait Config: + CreateBare> + + frame_system::Config + + pallet_transaction_payment::Config::RuntimeCall> + { + /// The aggregated call type. + type RuntimeCall: Parameter + + Dispatchable + + GetDispatchInfo + + From>; + + /// Configuration for unsigned transaction priority + #[pallet::constant] + type UnsignedPriority: Get; + + /// Configuration for unsigned transaction longevity + #[pallet::constant] + type UnsignedLongevity: Get; + + /// Weight information for extrinsics in this pallet. + type WeightInfo: WeightInfo; + } + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::type_value] + pub(super) fn DefaultMaxTxPerBlock() -> u16 { + 10_u16 + } + + #[pallet::type_value] + pub(super) fn DefaultMaxCallWeight() -> Weight { + //TODO: set reasonable value + Weight::from_parts(10_000_000_000_u64, 5_000_000) + } + + #[pallet::storage] + #[pallet::getter(fn max_txs_per_block)] + pub(super) type MaxTxPerBlock = StorageValue<_, u16, ValueQuery, DefaultMaxTxPerBlock>; + + #[pallet::storage] + #[pallet::getter(fn max_weight_per_call)] + //max weight of the `dispatch_top`. (Inner call's weight should be included) + pub(super) type MaxCallWeight = StorageValue<_, Weight, ValueQuery, DefaultMaxCallWeight>; + + #[pallet::storage] + #[pallet::getter(fn next_call_id)] + pub(super) type Sequencer = StorageValue<_, CallId, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn dispatch_next_id)] + pub(super) type DispatchNextId = StorageValue<_, CallId, ValueQuery>; + + #[pallet::storage] + #[pallet::getter(fn call_queue)] + pub(super) type CallQueue = StorageMap<_, Blake2_128Concat, CallId, CallData>; + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + /// Call was queued for execution. + Queued { + id: CallId, + src: Source, + who: T::AccountId, + fees: BalanceOf, + }, + + /// Call was executed. + Executed { id: CallId, result: DispatchResult }, + } + + #[pallet::error] + pub enum Error { + /// Failed to decode provided call data. + Corrupted, + + /// `id` reached max. value. + IdOverflow, + + /// Arithmetic or type conversion overflow + Overflow, + + /// User failed to pay fees for future execution. + FailedToPayFees, + + /// Failed to deposit collected fees. + FailedToDepositFees, + + /// Queue is empty. + EmptyQueue, + + /// Call's weight is bigger than max allowed weight. + Overweight, + } + + #[pallet::hooks] + impl Hooks> for Pallet { + fn offchain_worker(block_number: BlockNumberFor) { + log::debug!(target: LOG_TARGET, "run offchain worker on block: {:?}", block_number); + + let mut next_id = Self::dispatch_next_id(); + for _ in 0..Self::max_txs_per_block() { + next_id = if let Some(n) = next_id.checked_add(1_u128) { + n + } else { + log::debug!(target: LOG_TARGET, "queue is empty"); + break; + }; + + if CallQueue::::contains_key(next_id) { + let call = Call::dispatch_top { id: next_id }; + let tx = T::create_bare(call.into()); + if let Err(e) = SubmitTransaction::>::submit_transaction(tx) { + debug_assert!(false, "laxy-executorn: failed to submit dispatch_top transaction"); + log::error!(target: LOG_TARGET, "to submit dispatch_top call, err: {:?}", e); + } + } else { + break; + } + } + } + } + + #[pallet::validate_unsigned] + impl ValidateUnsigned for Pallet { + type Call = Call; + + fn validate_unsigned(source: TransactionSource, unsigned_call: &self::Call) -> TransactionValidity { + if let Call::dispatch_top { id } = unsigned_call { + // discard call not coming from the local node + match source { + TransactionSource::Local | TransactionSource::InBlock => { /* allowed */ } + _ => { + return InvalidTransaction::Call.into(); + } + } + + ensure!( + CallQueue::::contains_key(Self::dispatch_next_id()), + InvalidTransaction::Call + ); + + return ValidTransaction::with_tag_prefix(OCW_TAG_PREFIX) + .priority(T::UnsignedPriority::get()) + .and_provides(id) + .longevity(T::UnsignedLongevity::get()) + .propagate(false) + .build(); + } + + InvalidTransaction::Call.into() + } + } + + #[pallet::call] + impl Pallet { + /// Extrinsics dispatches top call from the queue. + /// + /// This is called from OCW. + /// + /// Emits: + /// - `Executed` when successful + #[pallet::call_index(1)] + #[pallet::weight({ + let info = if let Some(call_data) = CallQueue::::get(DispatchNextId::::get()) { + if let Ok(c) = ::RuntimeCall::decode(&mut &call_data.call[..]) { + c.get_dispatch_info() + } else { + DispatchInfo { + call_weight: Default::default(), + extension_weight: Default::default(), + class: DispatchClass::Normal, + pays_fee: Pays::No, + } + } + } else { + DispatchInfo { + call_weight: Default::default(), + extension_weight: Default::default(), + class: DispatchClass::Normal, + pays_fee: Pays::No, + } + }; + + ::WeightInfo::dispatch_top_base_weight().saturating_add(info.call_weight) + })] + pub fn dispatch_top(origin: OriginFor, _id: u128) -> DispatchResult { + ensure_none(origin)?; + + DispatchNextId::::try_mutate(|id| { + let call_data = CallQueue::::take(*id).ok_or(Error::::EmptyQueue)?; + + let result = if let Ok(call) = ::RuntimeCall::decode(&mut &call_data.call[..]) { + let o: OriginFor = Origin::::Signed(call_data.origin).into(); + + call.dispatch(o) + } else { + Err(Error::::Corrupted.into()) + }; + + Self::deposit_event(Event::Executed { + id: *id, + result: result.map(|_| ()).map_err(|e| e.error), + }); + + *id = id.checked_add(One::one()).ok_or(Error::::IdOverflow)?; + + Ok(()) + }) + } + } +} + +impl Pallet { + /// Function adds call to queue for future execution. + /// + /// This function also charges fees for future call execution and fails if `origin` can't pay + /// fees. + #[transactional] + pub fn add_to_queue(src: Source, origin: T::AccountId, bounded_call: BoundedCall) -> Result<(), DispatchError> { + let call = ::RuntimeCall::decode(&mut &bounded_call[..]).map_err(|_| Error::::Corrupted)?; + + let mut info = call.get_dispatch_info(); + info.call_weight = info + .call_weight + .saturating_add(::WeightInfo::dispatch_top_base_weight()); + + if info.call_weight.any_gt(Self::max_weight_per_call()) { + return Err(Error::::Overweight.into()); + } + + let len = Call::::dispatch_top { id: u128::MAX } + .encoded_size() + .saturating_add(CALL_LEN_OFFSET.try_into().map_err(|_| Error::::Overflow)?); + + let fees = pallet_transaction_payment::Pallet::::compute_fee( + len.try_into().map_err(|_| Error::::Overflow)?, + &info, + NO_TIP.into(), + ); + + let already_withdrawn = ::OnChargeTransaction::withdraw_fee( + &origin, + &call, + &info, + fees, + NO_TIP.into(), + ) + .map_err(|_| Error::::FailedToPayFees)?; + + ::OnChargeTransaction::correct_and_deposit_fee( + &origin, + &info, + &PostDispatchInfo { + actual_weight: Some(info.call_weight), + pays_fee: Pays::Yes, + }, + fees, + NO_TIP.into(), + already_withdrawn, + ) + .map_err(|_| Error::::FailedToDepositFees)?; + + let call_id = Self::get_next_call_id()?; + CallQueue::::insert( + call_id, + CallData { + origin: origin.clone(), + call: bounded_call, + }, + ); + + Self::deposit_event(Event::Queued { + id: call_id, + src, + who: origin, + fees, + }); + Ok(()) + } + + fn get_next_call_id() -> Result { + Sequencer::::try_mutate(|current_val| { + let ret = *current_val; + *current_val = current_val.checked_add(One::one()).ok_or(Error::::IdOverflow)?; + + Ok(ret) + }) + } +} + +impl hydradx_traits::lazy_executor::Mutate for Pallet { + type Error = DispatchError; + type BoundedCall = BoundedCall; + + fn queue(src: Source, origin: T::AccountId, call: Self::BoundedCall) -> Result<(), Self::Error> { + Self::add_to_queue(src, origin, call) + } +} diff --git a/pallets/lazy-executor/src/tests/add_to_queue.rs b/pallets/lazy-executor/src/tests/add_to_queue.rs new file mode 100644 index 0000000000..ecbdcf2333 --- /dev/null +++ b/pallets/lazy-executor/src/tests/add_to_queue.rs @@ -0,0 +1,110 @@ +use crate::*; +use frame_support::{assert_noop, assert_ok}; +use pretty_assertions::assert_eq; +use tests::{has_event, mock::*}; + +#[test] +fn should_work_when_call_is_valid() { + ExtBuilder.build().execute_with(|| { + //Arrange + let call: BoundedCall = RuntimeCall::MockPallet(MockPalletCall::dummy_call { + allowed_origin: vec![ALICE, BOB], + weight: Weight::from_parts(1_000_u64, 1_000_u64), + }) + .encode() + .try_into() + .expect("failed to create BoundedCall"); + + //Act&Assert + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(0), ALICE, call)); + + //TODO: make better assertion so we don't have to change it when weight change + assert!(has_event( + Event::Queued { + id: 0, + src: Source::ICE(0), + who: ALICE, + fees: 108_159_175_u128 + } + .into() + )) + }) +} + +#[test] +fn should_fail_when_call_is_not_decodeable() { + ExtBuilder.build().execute_with(|| { + //Arrange + //NOTE: call encoded from PolkadotAPPs with removed last 2 characters + let corrupted_call: BoundedCall = Into::>::into(hex_literal::hex![ + "070346f0b489ac07cb495852eba68e42250209e4d91f472d37a2fc8e4f0d9c74a828070010a5d4" + ]) + .try_into() + .expect("failed to create BoundeCall"); + + //Act&Assert + assert_noop!( + LazyExecutor::add_to_queue(Source::ICE(0), ALICE, corrupted_call), + Error::::Corrupted + ); + }); +} + +#[test] +fn should_fail_when_call_is_overweight() { + ExtBuilder.build().execute_with(|| { + //Arrange + let max_allowed_weight = LazyExecutor::max_weight_per_call(); + + //NOTE: this is overweight because weight of dispatching call is added to call's weight + let overweight_ref_time_call: BoundedCall = RuntimeCall::MockPallet(MockPalletCall::dummy_call { + allowed_origin: vec![BOB], + weight: Weight::from_parts(max_allowed_weight.ref_time(), 1_u64), + }) + .encode() + .try_into() + .expect("failed to create overweight_ref_time BoundedCall"); + + //NOTE: this is overweight because weight of dispatching call is added to call's weight + let overweight_proof_size_cal: BoundedCall = RuntimeCall::MockPallet(MockPalletCall::dummy_call { + allowed_origin: vec![BOB], + weight: Weight::from_parts(1_u64, max_allowed_weight.proof_size()), + }) + .encode() + .try_into() + .expect("failed to create overweight_proof_size BoundeCall"); + + //Act&Assert - 1 + assert_noop!( + LazyExecutor::add_to_queue(Source::ICE(0), ALICE, overweight_ref_time_call), + Error::::Overweight + ); + + //Act&Assert - 2 + assert_noop!( + LazyExecutor::add_to_queue(Source::ICE(0), ALICE, overweight_proof_size_cal), + Error::::Overweight + ); + }); +} + +#[test] +fn should_fail_when_origin_cant_pay_fees() { + ExtBuilder.build().execute_with(|| { + //Arrange + let call: BoundedCall = RuntimeCall::MockPallet(MockPalletCall::dummy_call { + allowed_origin: vec![BOB], + //NOTE: whole call includes dispatch overhead so we need to substract more + weight: Weight::from_parts(100_u64, 100_u64), + }) + .encode() + .try_into() + .expect("failed to create BoundeCall"); + + //Act&Assert + assert_noop!( + LazyExecutor::add_to_queue(Source::ICE(1), ACC_ZERO_BALANCE, call), + Error::::FailedToPayFees + ); + }) +} diff --git a/pallets/lazy-executor/src/tests/mock.rs b/pallets/lazy-executor/src/tests/mock.rs new file mode 100644 index 0000000000..e9af2f45e0 --- /dev/null +++ b/pallets/lazy-executor/src/tests/mock.rs @@ -0,0 +1,298 @@ +// Copyright (C) 2020-2025 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use frame_support::{ + construct_runtime, parameter_types, + traits::{fungible, ConstU128, ConstU32, ConstU64, Contains, Imbalance, OnUnbalanced}, + weights::{RuntimeDbWeight, Weight, WeightToFee as WeightToFeeT}, +}; +use pallet_transaction_payment::FungibleAdapter; +use sp_core::H256; +use sp_runtime::SaturatedConversion; +use sp_runtime::{ + traits::{BlakeTwo256, BlockNumberProvider, IdentityLookup}, + BuildStorage, +}; + +type BlockNumber = u64; +pub type AccountId = u64; +type Block = frame_system::mocking::MockBlock; +type Balance = u128; +pub type MockPalletCall = mock_pallet::Call; +pub type LazyExecutorCall = pallet::Call; + +use crate::{self as pallet_lazy_executor, pallet}; + +const UNIT: Balance = 1_000_000_000_000; +pub const ALICE: AccountId = 1_000; +pub const BOB: AccountId = 1_001; +pub const CHARLIE: AccountId = 1_002; +pub const ACC_ZERO_BALANCE: AccountId = 1_003; + +construct_runtime!( + pub enum Test + { + System: frame_system, + Balances: pallet_balances, + LazyExecutor: pallet_lazy_executor, + MockPallet: mock_pallet, + TransactionPayment: pallet_transaction_payment, + } +); + +pub mod mock_pallet { + pub use pallet::*; + #[frame_support::pallet(dev_mode)] + pub mod pallet { + use crate::tests::mock::AccountId; + use crate::{ensure_signed, OriginFor}; + use frame_support::{ensure, pallet_prelude::*}; + + #[pallet::pallet] + pub struct Pallet(_); + + #[pallet::config] + pub trait Config: frame_system::Config + Sized { + type RuntimeEvent: From> + IsType<::RuntimeEvent>; + } + + #[pallet::event] + #[pallet::generate_deposit(pub(super) fn deposit_event)] + pub enum Event { + CallExecuted { who: T::AccountId, weight: Weight }, + } + + #[pallet::error] + pub enum Error { + // Account is not allowed to perform action + Forbidden, + } + + #[pallet::call] + impl Pallet { + #[pallet::call_index(1)] + #[pallet::weight(*weight)] + pub fn dummy_call(origin: OriginFor, allowed_origin: Vec, weight: Weight) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(allowed_origin.contains(&who), Error::::Forbidden); + + Self::deposit_event(Event::CallExecuted { who, weight }); + + Ok(()) + } + + pub fn filtered_call( + origin: OriginFor, + allowed_origin: Vec, + weight: Weight, + ) -> DispatchResult { + let who = ensure_signed(origin)?; + + ensure!(allowed_origin.contains(&who), Error::::Forbidden); + + Self::deposit_event(Event::CallExecuted { who, weight }); + + Ok(()) + } + } + } +} + +parameter_types! { + pub const BlockHashCount: u64 = 250; + pub const SS58Prefix: u8 = 63; + pub static MockBlockNumberProvider: u64 = 0; + pub const DbWeight: RuntimeDbWeight = RuntimeDbWeight{ + read: 1_u64, write: 1_u64 + }; +} + +impl BlockNumberProvider for MockBlockNumberProvider { + type BlockNumber = BlockNumber; + + fn current_block_number() -> Self::BlockNumber { + System::block_number() + } +} + +pub struct MockBaseFilter; +impl Contains for MockBaseFilter { + fn contains(call: &RuntimeCall) -> bool { + !matches!(call, RuntimeCall::MockPallet(MockPalletCall::filtered_call { .. })) + } +} + +impl frame_system::Config for Test { + type BaseCallFilter = MockBaseFilter; + type BlockWeights = (); + type BlockLength = (); + type RuntimeOrigin = RuntimeOrigin; + type RuntimeCall = RuntimeCall; + type RuntimeTask = RuntimeTask; + type Nonce = u64; + type Block = Block; + type Hash = H256; + type Hashing = BlakeTwo256; + type AccountId = AccountId; + type Lookup = IdentityLookup; + type RuntimeEvent = RuntimeEvent; + type BlockHashCount = ConstU64<250>; + type DbWeight = (); + type Version = (); + type PalletInfo = PalletInfo; + type AccountData = pallet_balances::AccountData; + type OnNewAccount = (); + type OnKilledAccount = (); + type SystemWeightInfo = (); + type SS58Prefix = (); + type OnSetCode = (); + type MaxConsumers = ConstU32<16>; + type SingleBlockMigrations = (); + type MultiBlockMigrator = (); + type PreInherents = (); + type PostInherents = (); + type PostTransactions = (); + type ExtensionsWeightInfo = (); +} + +impl mock_pallet::Config for Test { + type RuntimeEvent = RuntimeEvent; +} + +parameter_types! { + pub const MaxLocks: u32 = 20; +} +impl pallet_balances::Config for Test { + type Balance = Balance; + type DustRemoval = (); + type RuntimeEvent = RuntimeEvent; + type ExistentialDeposit = ConstU128<1>; + type AccountStore = System; + type WeightInfo = (); + type MaxLocks = MaxLocks; + type MaxReserves = ConstU32<50>; + type ReserveIdentifier = [u8; 8]; + type FreezeIdentifier = (); + type MaxFreezes = (); + type RuntimeHoldReason = (); + type RuntimeFreezeReason = (); + type DoneSlashHandler = (); +} + +pub(crate) type Extrinsic = sp_runtime::testing::TestXt; +impl frame_system::offchain::CreateTransactionBase for Test +where + RuntimeCall: From, +{ + type RuntimeCall = RuntimeCall; + type Extrinsic = Extrinsic; +} + +impl hydradx_traits::CreateBare for Test +where + RuntimeCall: From, +{ + fn create_bare(call: Self::RuntimeCall) -> Extrinsic { + Extrinsic::new_bare(call) + } +} + +impl pallet_lazy_executor::Config for Test { + type RuntimeEvent = RuntimeEvent; + type RuntimeCall = RuntimeCall; + type UnsignedPriority = ConstU64<100>; + type UnsignedLongevity = ConstU64<3>; + + type WeightInfo = (); +} + +parameter_types! { + pub static WeightToFee: u128 = 1; + pub static TransactionByteFee: u128 = 1; + pub static OperationalFeeMultiplier: u8 = 5; +} + +impl WeightToFeeT for WeightToFee { + type Balance = u128; + + fn weight_to_fee(weight: &Weight) -> Self::Balance { + Self::Balance::saturated_from(weight.ref_time()).saturating_mul(WEIGHT_TO_FEE.with(|v| *v.borrow())) + } +} + +impl WeightToFeeT for TransactionByteFee { + type Balance = u128; + + fn weight_to_fee(weight: &Weight) -> Self::Balance { + Self::Balance::saturated_from(weight.ref_time()).saturating_mul(TRANSACTION_BYTE_FEE.with(|v| *v.borrow())) + } +} + +parameter_types! { + pub(crate) static TipUnbalancedAmount: u128 = 0; + pub(crate) static FeeUnbalancedAmount: u128 = 0; +} + +pub struct DealWithFees; +impl OnUnbalanced::AccountId, Balances>> for DealWithFees { + fn on_unbalanceds( + mut fees_then_tips: impl Iterator::AccountId, Balances>>, + ) { + if let Some(fees) = fees_then_tips.next() { + FeeUnbalancedAmount::mutate(|a| *a += fees.peek()); + if let Some(tips) = fees_then_tips.next() { + TipUnbalancedAmount::mutate(|a| *a += tips.peek()); + } + } + } +} + +impl pallet_transaction_payment::Config for Test { + type RuntimeEvent = RuntimeEvent; + type OnChargeTransaction = FungibleAdapter; + type OperationalFeeMultiplier = OperationalFeeMultiplier; + type WeightToFee = WeightToFee; + type LengthToFee = TransactionByteFee; + type FeeMultiplierUpdate = (); + type WeightInfo = (); +} + +pub struct ExtBuilder; +impl Default for ExtBuilder { + fn default() -> Self { + ExtBuilder + } +} + +impl ExtBuilder { + pub fn build(self) -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default().build_storage().unwrap(); + + pallet_balances::GenesisConfig:: { + balances: vec![(ALICE, 200_000 * UNIT), (BOB, 150_000 * UNIT), (CHARLIE, 15_000 * UNIT)], + dev_accounts: None, + } + .assimilate_storage(&mut t) + .unwrap(); + + let mut r: sp_io::TestExternalities = t.into(); + r.execute_with(|| { + System::set_block_number(1); + }); + + r + } +} diff --git a/pallets/lazy-executor/src/tests/mod.rs b/pallets/lazy-executor/src/tests/mod.rs new file mode 100644 index 0000000000..fee3cc2a1e --- /dev/null +++ b/pallets/lazy-executor/src/tests/mod.rs @@ -0,0 +1,9 @@ +use mock::System; + +mod add_to_queue; +pub(crate) mod mock; +mod validate_unsigned; + +pub fn has_event(event: mock::RuntimeEvent) -> bool { + System::events().iter().any(|record| record.event == event) +} diff --git a/pallets/lazy-executor/src/tests/validate_unsigned.rs b/pallets/lazy-executor/src/tests/validate_unsigned.rs new file mode 100644 index 0000000000..1cde65962e --- /dev/null +++ b/pallets/lazy-executor/src/tests/validate_unsigned.rs @@ -0,0 +1,80 @@ +use frame_support::pallet_prelude::{ + InvalidTransaction, TransactionSource, TransactionValidityError, ValidTransaction, +}; +use frame_support::{assert_noop, assert_ok, traits::Get}; +use pretty_assertions::assert_eq; +use sp_runtime::traits::ValidateUnsigned; +use tests::mock::*; + +use crate::*; + +use super::mock::{ExtBuilder, LazyExecutor, RuntimeCall}; + +#[test] +fn should_work_when_queue_is_not_empty() { + ExtBuilder.build().execute_with(|| { + //Arrange + MaxTxPerBlock::::set(3); + + let bounded_call: BoundedCall = RuntimeCall::MockPallet(MockPalletCall::dummy_call { + allowed_origin: vec![BOB], + weight: Weight::from_parts(30_000, 10_000), + }) + .encode() + .try_into() + .expect("failed to create BoundedCall"); + + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(0), BOB, bounded_call.clone())); + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(1), BOB, bounded_call.clone())); + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(2), ALICE, bounded_call.clone())); + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(3), BOB, bounded_call.clone())); + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(4), ALICE, bounded_call.clone())); + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(5), CHARLIE, bounded_call)); + + let id = 0_u128; + //Act&Assert + assert_eq!( + LazyExecutor::validate_unsigned(TransactionSource::Local, &LazyExecutorCall::dispatch_top { id }), + Ok(ValidTransaction { + //provides itself + provides: vec![(OCW_TAG_PREFIX, id).encode()], + requires: vec![], + priority: ::UnsignedPriority::get(), + longevity: ::UnsignedLongevity::get(), + propagate: false, + }) + ) + }); +} + +#[test] +fn should_fail_when_source_is_not_local() { + ExtBuilder.build().execute_with(|| { + //Arrange + let bounded_call: BoundedCall = RuntimeCall::MockPallet(MockPalletCall::dummy_call { + allowed_origin: vec![BOB], + weight: Weight::from_parts(10_000, 20_000), + }) + .encode() + .try_into() + .expect("failed to create BoundedCall"); + + assert_ok!(LazyExecutor::add_to_queue(Source::ICE(1), ALICE, bounded_call)); + + //Act&Assert + assert_noop!( + LazyExecutor::validate_unsigned(TransactionSource::External, &LazyExecutorCall::dispatch_top { id: 0 }), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} + +#[test] +fn should_fail_when_queue_is_empty() { + ExtBuilder.build().execute_with(|| { + assert_noop!( + LazyExecutor::validate_unsigned(TransactionSource::Local, &LazyExecutorCall::dispatch_top { id: 0 }), + TransactionValidityError::Invalid(InvalidTransaction::Call) + ); + }); +} diff --git a/pallets/lazy-executor/src/weights.rs b/pallets/lazy-executor/src/weights.rs new file mode 100644 index 0000000000..e3981be62c --- /dev/null +++ b/pallets/lazy-executor/src/weights.rs @@ -0,0 +1,22 @@ +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{ + traits::Get, + weights::{constants::RocksDbWeight, Weight}, +}; +use sp_std::marker::PhantomData; + +/// Weight functions needed for pallet_staking. +pub trait WeightInfo { + fn dispatch_top_base_weight() ->Weight; +} + +/// Weights for pallet_staking using the hydraDX node and recommended hardware. +impl WeightInfo for () { + fn dispatch_top_base_weight() -> Weight { + Weight::from_parts(1_000, 2_000) + } +} diff --git a/pallets/omnipool/src/lib.rs b/pallets/omnipool/src/lib.rs index 04add075ab..6d3bc9b0aa 100644 --- a/pallets/omnipool/src/lib.rs +++ b/pallets/omnipool/src/lib.rs @@ -237,7 +237,7 @@ pub mod pallet { #[pallet::storage] /// State of an asset in the omnipool #[pallet::getter(fn assets)] - pub(super) type Assets = StorageMap<_, Blake2_128Concat, T::AssetId, AssetState>; + pub type Assets = StorageMap<_, Blake2_128Concat, T::AssetId, AssetState>; // LRNA is only allowed to be sold #[pallet::type_value] @@ -2331,8 +2331,10 @@ impl Pallet { ); // And the actual fee taken must be equal to the reported amount! debug_assert!( - actual_fee_taken == taken_fee_total, - "Fee Overdraft - actual taken amount is not equal to reported amount" + actual_fee_taken.abs_diff(taken_fee_total) <= Balance::one(), + "Fee Overdraft - actual taken amount {:?} is not equal to reported amount {:?}", + actual_fee_taken, + taken_fee_total ); let protocol_fee_amount = amount.saturating_sub(taken_fee_total); diff --git a/pallets/omnipool/src/types.rs b/pallets/omnipool/src/types.rs index b42834661b..e78742c67f 100644 --- a/pallets/omnipool/src/types.rs +++ b/pallets/omnipool/src/types.rs @@ -147,7 +147,7 @@ where } /// Asset state representation including asset pool reserve. -#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)] +#[derive(Clone, Copy, Default, Debug, PartialEq, Eq, Encode, Decode)] pub struct AssetReserveState { /// Quantity of asset in omnipool pub reserve: Balance, diff --git a/pallets/route-executor/src/lib.rs b/pallets/route-executor/src/lib.rs index a51f440a06..0a094f47d4 100644 --- a/pallets/route-executor/src/lib.rs +++ b/pallets/route-executor/src/lib.rs @@ -871,6 +871,21 @@ macro_rules! handle_execution_error { } impl RouteProvider for Pallet { + fn get_onchain_route(asset_pair: AssetPair) -> Option> { + let onchain_route = Routes::::get(asset_pair.ordered_pair()); + + match onchain_route { + Some(route) => { + if asset_pair.is_ordered() { + Some(route) + } else { + Some(inverse_route(route)) + } + } + None => None, + } + } + fn get_route(asset_pair: AssetPair) -> Route { let onchain_route = Routes::::get(asset_pair.ordered_pair()); diff --git a/pallets/stableswap/src/lib.rs b/pallets/stableswap/src/lib.rs index dde5364389..978b1238fc 100644 --- a/pallets/stableswap/src/lib.rs +++ b/pallets/stableswap/src/lib.rs @@ -111,8 +111,8 @@ pub const POOL_IDENTIFIER: &[u8] = b"sts"; pub const MAX_ASSETS_IN_POOL: u32 = 5; -const D_ITERATIONS: u8 = hydra_dx_math::stableswap::MAX_D_ITERATIONS; -const Y_ITERATIONS: u8 = hydra_dx_math::stableswap::MAX_Y_ITERATIONS; +pub(crate) const D_ITERATIONS: u8 = hydra_dx_math::stableswap::MAX_D_ITERATIONS; +pub(crate) const Y_ITERATIONS: u8 = hydra_dx_math::stableswap::MAX_Y_ITERATIONS; #[frame_support::pallet] pub mod pallet { diff --git a/pallets/stableswap/src/types.rs b/pallets/stableswap/src/types.rs index 00bee37566..1ee813615e 100644 --- a/pallets/stableswap/src/types.rs +++ b/pallets/stableswap/src/types.rs @@ -231,4 +231,28 @@ impl PoolSnapshot { *b = b.saturating_sub(amount_out.amount); self } + + /// Update share issuance and a single reserve (for add/remove liquidity simulation). + /// + /// # Parameters + /// - `asset_id`: The asset to update reserve for + /// - `reserve_delta`: Change in reserve (positive = add, negative = remove) + /// - `shares_delta`: Change in shares (positive = mint, negative = burn) + pub fn update_shares_and_reserve(mut self, asset_id: AssetId, reserve_delta: i128, shares_delta: i128) -> Self { + if let Some(idx) = self.asset_idx(asset_id) { + if let Some(reserve) = self.reserves.get_mut(idx) { + if reserve_delta >= 0 { + reserve.amount = reserve.amount.saturating_add(reserve_delta as u128); + } else { + reserve.amount = reserve.amount.saturating_sub((-reserve_delta) as u128); + } + } + } + if shares_delta >= 0 { + self.share_issuance = self.share_issuance.saturating_add(shares_delta as u128); + } else { + self.share_issuance = self.share_issuance.saturating_sub((-shares_delta) as u128); + } + self + } } diff --git a/runtime/hydradx/Cargo.toml b/runtime/hydradx/Cargo.toml index 94a1cb35b4..5b3a47c60b 100644 --- a/runtime/hydradx/Cargo.toml +++ b/runtime/hydradx/Cargo.toml @@ -22,6 +22,8 @@ log = { workspace = true } num_enum = { workspace = true, default-features = false } evm = { workspace = true, features = ["with-codec"] } +route-findr = {workspace = true} + # local dependencies primitives = { workspace = true } hydradx-adapters = { workspace = true } @@ -63,6 +65,11 @@ pallet-staking = { workspace = true } pallet-liquidation = { workspace = true } pallet-hsm = { workspace = true } pallet-parameters = { workspace = true } +pallet-intent = { workspace = true } +pallet-ice = { workspace = true } +pallet-lazy-executor = { workspace = true } +amm-simulator = { workspace = true } +ice-support = { workspace = true } # pallets pallet-bags-list = { workspace = true } @@ -384,6 +391,12 @@ std = [ "pallet-dispatcher/std", "pallet-signet/std", "pallet-dispenser/std", + "pallet-intent/std", + "pallet-ice/std", + "pallet-lazy-executor/std", + "amm-simulator/std", + "ice-support/std", + "route-findr/std", ] try-runtime = [ "frame-try-runtime", diff --git a/runtime/hydradx/src/assets.rs b/runtime/hydradx/src/assets.rs index d2070b73aa..da895667a6 100644 --- a/runtime/hydradx/src/assets.rs +++ b/runtime/hydradx/src/assets.rs @@ -20,6 +20,7 @@ use crate::evm::precompiles::erc20_mapping::SetCodeForErc20Precompile; use crate::evm::Erc20Currency; use crate::origins::{EconomicParameters, GeneralAdmin, OmnipoolAdmin, Treasurer}; use crate::system::NativeAssetId; +use crate::types::ShortOraclePrice; use crate::Stableswap; use core::ops::RangeInclusive; use frame_support::{ @@ -31,8 +32,8 @@ use frame_support::{ }, sp_runtime::{FixedU128, Perbill, Permill}, traits::{ - AsEnsureOriginWithArg, ConstU32, Contains, Currency, Defensive, EitherOf, EnsureOrigin, ExistenceRequirement, - Imbalance, LockIdentifier, NeverEnsureOrigin, OnUnbalanced, + AsEnsureOriginWithArg, ConstU32, ConstU64, Contains, Currency, Defensive, EitherOf, EnsureOrigin, + ExistenceRequirement, Imbalance, LockIdentifier, NeverEnsureOrigin, OnUnbalanced, }, BoundedVec, PalletId, }; @@ -45,7 +46,7 @@ use hydradx_adapters::{ }; #[cfg(feature = "runtime-benchmarks")] use hydradx_traits::evm::CallContext; -use hydradx_traits::router::MAX_NUMBER_OF_TRADES; +use hydradx_traits::router::{Route, MAX_NUMBER_OF_TRADES}; pub use hydradx_traits::{ fee::{InspectTransactionFeeCurrency, SwappablePaymentAssetTrader}, registry::Inspect, @@ -54,6 +55,10 @@ pub use hydradx_traits::{ AMM, }; +use amm_simulator::aave::Simulator as AaveSimulator; +use amm_simulator::omnipool::Simulator as OmnipoolSimulator; +use amm_simulator::stableswap::Simulator as StableSwapSimulator; + use orml_traits::{ currency::{MultiCurrency, MultiLockableCurrency, MutationHooks, OnDeposit, OnTransfer}, GetByKey, Handler, Happened, NamedMultiReservableCurrency, @@ -689,6 +694,7 @@ impl Get> for ExtendedDustRemovalWhitelist { BondsPalletId::get().into_account_truncating(), pallet_route_executor::Pallet::::router_account(), EVMAccounts::account_id(crate::evm::HOLDING_ADDRESS), + IcePalletId::get().into_account_truncating(), ]; if let Some((flash_minter, loan_receiver)) = pallet_hsm::GetFlashMinterSupport::::get() { @@ -1415,6 +1421,7 @@ use crate::evm::evm_error_decoder::EvmErrorDecoder; #[cfg(feature = "runtime-benchmarks")] use frame_support::storage::with_transaction; use frame_support::traits::IsSubType; +use hydradx_traits::amm::{SimulatorError, SimulatorSet}; use hydradx_traits::evm::{Erc20Inspect, Erc20OnDust}; #[cfg(feature = "runtime-benchmarks")] use hydradx_traits::price::PriceProvider; @@ -1838,6 +1845,91 @@ impl pallet_hsm::Config for Runtime { type BenchmarkHelper = helpers::benchmark_helpers::HsmBenchmarkHelper; } +impl pallet_lazy_executor::Config for Runtime { + type RuntimeCall = RuntimeCall; + type UnsignedLongevity = ConstU64<2>; + type UnsignedPriority = ConstU64<100>; + type WeightInfo = weights::pallet_lazy_executor::HydraWeight; +} + +parameter_types! { + //24 hours + pub const MaxIntentDuration: u64 = 24 * 3_600 * 1_000; +} + +impl pallet_intent::Config for Runtime { + type LazyExecutorHandler = LazyExecutor; + type RegistryHandler = AssetRegistry; + type Currency = Currencies; + type MaxAllowedIntentDuration = MaxIntentDuration; + type TimestampProvider = Timestamp; + type HubAssetId = LRNA; + type OraclePriceProvider = ShortOraclePrice; + type BlockNumberProvider = System; + type MinDcaPeriod = MinimalPeriod; + type MaxIntentsPerAccount = sp_core::ConstU32<100>; + type WeightInfo = weights::pallet_intent::HydraWeight; +} + +parameter_types! { + pub const IcePalletId: PalletId = PalletId(*b"ice_ice#"); + pub const IceFee: Permill = Permill::from_parts(200); // 0.02% + pub const SimulatorPriceDenom: AssetId = CORE_ASSET_ID; +} + +/// Simulator configuration for the ICE pallet +/// Bundles simulators and route discovery strategy for the solver +pub struct HydrationSimulatorConfig; + +type HydrationSimulators = ( + OmnipoolSimulator>, + StableSwapSimulator>, + AaveSimulator>, +); + +pub struct SmartRouteFinder(sp_std::marker::PhantomData); + +impl hydradx_traits::amm::RouteDiscovery for SmartRouteFinder { + fn discover_routes( + asset_in: AssetId, + asset_out: AssetId, + state: &S::State, + ) -> Result>, SimulatorError> { + let pool_edges = S::pool_edges(state); + let routes = route_findr::get_routes(asset_in, asset_out, pool_edges); + + if routes.is_empty() { + log::debug!(target: "solver", "no routes found for {} -> {}", asset_in, asset_out); + return Err(SimulatorError::NotSupported); + } + + log::debug!(target: "solver", "found {} route(s) for {} -> {}", routes.len(), asset_in, asset_out); + Ok(routes) + } +} + +impl hydradx_traits::amm::SimulatorConfig for HydrationSimulatorConfig { + type Simulators = HydrationSimulators; + //type RouteDiscovery = amm_simulator::OnChainRouteDiscovery; + type RouteDiscovery = SmartRouteFinder; + type PriceDenominator = SimulatorPriceDenom; + + fn existential_deposit(asset_id: AssetId) -> Balance { + ::existential_deposit(asset_id).unwrap_or(0) + } +} + +impl pallet_ice::Config for Runtime { + type Currency = Currencies; + type PalletId = IcePalletId; + type Fee = IceFee; + type AuthorityOrigin = EitherOf, TechCommitteeMajority>; + type RegistryHandler = AssetRegistry; + type Simulator = HydrationSimulatorConfig; + type ExtraGasSupport = Dispatcher; + type WeightInfo = weights::pallet_ice::HydraWeight; +} + parameter_types! { pub const SignetPalletId: PalletId = PalletId(*b"py/signt"); } diff --git a/runtime/hydradx/src/benchmarking/ice.rs b/runtime/hydradx/src/benchmarking/ice.rs new file mode 100644 index 0000000000..6c4e3613a1 --- /dev/null +++ b/runtime/hydradx/src/benchmarking/ice.rs @@ -0,0 +1,163 @@ +use super::*; +use crate::*; + +use frame_benchmarking::account; +use frame_support::BoundedVec; +use frame_system::RawOrigin; +use hydra_dx_math::types::Ratio; +use ice_support::Intent as IntentIce; +use ice_support::IntentData; +use ice_support::IntentDataInput; +use ice_support::IntentId; +use ice_support::Price; +use ice_support::Solution; +use ice_support::SwapData; +use ice_support::SwapType; +use ice_support::MAX_NUMBER_OF_RESOLVED_INTENTS; +use orml_benchmarking::runtime_benchmarks; +use pallet_intent::types::Intent as IntentT; +use pallet_intent::types::IntentInput; +use sp_runtime::DispatchResult; +use sp_std::collections::btree_map::BTreeMap; + +const SEED: u32 = 1; + +const HDX: AssetId = 0; +const DAI: AssetId = 2; + +const TRIL: u128 = 1_000_000_000_000; +const QUINTIL: u128 = 1_000_000_000_000_000_000; + +//Intent's deadline, 12hours +const DEADLINE: Option = Some(12 * 3_600 * 1_000); + +fn fund(to: AccountId, currency: AssetId, amount: Balance) -> DispatchResult { + Currencies::deposit(currency, &to, amount) +} + +runtime_benchmarks! { + {Runtime, pallet_ice } + + submit_solution { + let caller: AccountId = account("caller", 0, SEED); + + //NOTE: treasury need balance otherwise it can't collect fees < ED + Currencies::update_balance( + RawOrigin::Root.into(), + Treasury::account_id(), + HDX, + (10_000 * TRIL) as i128, + )?; + + //NOTE: fund ICE's account so we can resolve intent without trade or another intent + Currencies::update_balance( + RawOrigin::Root.into(), + ICE::get_pallet_account(), + DAI, + (10 * QUINTIL) as i128, + )?; + + + fund(caller.clone(), HDX, 10_000 * TRIL)?; + fund(caller.clone(), DAI, 10_000 * QUINTIL)?; + + let cb: Vec = RuntimeCall::Tokens(orml_tokens::Call::transfer{ + dest: caller.clone(), + currency_id: 5, + amount: 10 * TRIL + }).encode(); + + let swap_data = SwapData { + asset_in: HDX, + asset_out: DAI, + amount_in: 3000 * TRIL, + amount_out: 10 * QUINTIL, + swap_type: SwapType::ExactIn, + partial: false, + }; + + let intent = IntentInput { + data: IntentDataInput::Swap(swap_data.clone()), + deadline: DEADLINE, + on_resolved: Some(cb.clone().try_into().unwrap()), + }; + + Intent::submit_intent(RawOrigin::Signed(caller.clone()).into(), intent)?; + let intents: Vec<(IntentId, IntentT)> = pallet_intent::Intents::::iter().collect(); + assert_eq!(intents.len() , 1); + let (id, _) = intents[0]; + + let resolved_intents = vec![IntentIce { + id, + data: IntentData::Swap(swap_data), + }]; + + let mut cp: BTreeMap = BTreeMap::new(); + assert!(cp.insert(HDX, Ratio{n: 10000, d: 3}).is_none()); + for i in 1..(MAX_NUMBER_OF_RESOLVED_INTENTS * 2) { + assert!(cp.insert(i, Ratio{n: 1, d: 3}).is_none()); + } + + let score = 0; + let s = Solution { + resolved_intents: resolved_intents.try_into().unwrap(), + trades: BoundedVec::new(), + score, + }; + + assert!(LazyExecutor::call_queue(0).is_none()); + assert!(Intent::get_intent(id).is_some()); + }: { ICE::submit_solution(RawOrigin::None.into(), s)? } + verify { + assert!(Intent::get_intent(id).is_none()); + assert!(LazyExecutor::call_queue(0).is_some()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use orml_benchmarking::impl_benchmark_test_suite; + use sp_runtime::BuildStorage; + + const LRNA: AssetId = 1; + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + pallet_asset_registry::GenesisConfig:: { + registered_assets: vec![ + ( + Some(LRNA), + Some(b"LRNA".to_vec().try_into().unwrap()), + 1_000u128, + None, + None, + None, + true, + ), + ( + Some(DAI), + Some(b"DAI".to_vec().try_into().unwrap()), + 1_000u128, + None, + None, + None, + true, + ), + ], + native_asset_name: b"HDX".to_vec().try_into().unwrap(), + native_existential_deposit: NativeExistentialDeposit::get(), + native_decimals: 12, + native_symbol: b"HDX".to_vec().try_into().unwrap(), + } + .assimilate_storage(&mut t) + .unwrap(); + + sp_io::TestExternalities::new(t) + } + + impl_benchmark_test_suite!(new_test_ext(),); +} diff --git a/runtime/hydradx/src/benchmarking/intent.rs b/runtime/hydradx/src/benchmarking/intent.rs new file mode 100644 index 0000000000..ae07f22493 --- /dev/null +++ b/runtime/hydradx/src/benchmarking/intent.rs @@ -0,0 +1,186 @@ +use super::*; +use crate::*; + +use frame_benchmarking::account; +use frame_system::RawOrigin; +use ice_support::IntentDataInput; +use ice_support::IntentId; +use ice_support::SwapData; +use ice_support::SwapType; +use orml_benchmarking::runtime_benchmarks; +use pallet_intent::types::Intent as IntentT; +use pallet_intent::types::IntentInput; +use pallet_intent::types::MAX_DATA_SIZE; +use sp_runtime::DispatchResult; + +const SEED: u32 = 1; + +const HDX: AssetId = 0; +const DAI: AssetId = 2; + +const TRIL: u128 = 1_000_000_000_000; +const QUINTIL: u128 = 1_000_000_000_000_000_000; + +//Intent's deadline, 12hours +const DEADLINE: u64 = 12 * 3_600 * 1_000; + +fn fund(to: AccountId, currency: AssetId, amount: Balance) -> DispatchResult { + Currencies::deposit(currency, &to, amount) +} + +runtime_benchmarks! { + {Runtime, pallet_intent } + + submit_intent { + let caller: AccountId = account("caller", 0, SEED); + + fund(caller.clone(), HDX, 10_000 * TRIL)?; + fund(caller.clone(), DAI, 10_000 * QUINTIL)?; + + //NOTE: it's ok to use junk, we are not really dispatching `cb` + let cb: Vec = vec![255; MAX_DATA_SIZE as usize]; + + let intent = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DAI, + amount_in: 3000 * TRIL, + amount_out: 10 * QUINTIL, + swap_type: SwapType::ExactIn, + partial: false, + }), + deadline: Some(DEADLINE), + on_resolved: Some(cb.clone().try_into().unwrap()), + }; + + let intents: Vec<(IntentId, IntentT)> = pallet_intent::Intents::::iter().collect(); + assert_eq!(intents.len() , 0); + }: _(RawOrigin::Signed(caller), intent) + verify { + let intents: Vec<(IntentId, IntentT)> = pallet_intent::Intents::::iter().collect(); + assert_eq!(intents.len() , 1); + } + + remove_intent { + let caller: AccountId = account("caller", 0, SEED); + + fund(caller.clone(), HDX, 10_000 * TRIL)?; + fund(caller.clone(), DAI, 10_000 * QUINTIL)?; + + //NOTE: it's ok to use junk, we are not really dispatching `cb` + let cb: Vec = vec![255; MAX_DATA_SIZE as usize]; + + let intent = IntentInput { + data: IntentDataInput::Swap(SwapParams { + asset_in: HDX, + asset_out: DAI, + amount_in: 3000 * TRIL, + amount_out: 10 * QUINTIL, + swap_type: SwapType::ExactIn, + partial: false, + }), + deadline: Some(DEADLINE), + on_resolved: Some(cb.clone().try_into().unwrap()), + }; + + Intent::submit_intent(RawOrigin::Signed(caller.clone()).into(), intent)?; + let intents: Vec<(IntentId, IntentT)> = pallet_intent::Intents::::iter().collect(); + assert_eq!(intents.len() , 1); + + let (id, _) = intents[0]; + }: _(RawOrigin::Signed(caller), id) + verify { + assert_eq!(Intent::get_intent(id), None); + } + + cleanup_intent { + let caller: AccountId = account("caller", 0, SEED); + let cleaner: AccountId = account("cleaner", 1, SEED); + + //NOTE: treasury need balance otherwise it can't collect fees < ED + Currencies::update_balance( + RawOrigin::Root.into(), + Treasury::account_id(), + HDX, + (10_000 * TRIL) as i128, + )?; + + fund(caller.clone(), HDX, 10_000 * TRIL)?; + fund(caller.clone(), DAI, 10_000 * QUINTIL)?; + + //NOTE: it's ok to use junk, we are not really dispatching it. + let on_resolved: Vec = vec![255; MAX_DATA_SIZE as usize]; + + let intent = IntentT { + data: IntentData::Swap(SwapData { + asset_in: HDX, + asset_out: DAI, + amount_in: 3000 * TRIL, + amount_out: 10 * QUINTIL, + swap_type: SwapType::ExactIn, + partial: false, + }), + deadline: Some(DEADLINE), + on_resolved: Some(on_resolved.try_into().unwrap()), + }; + + Intent::submit_intent(RawOrigin::Signed(caller.clone()).into(), intent)?; + let intents: Vec<(IntentId, IntentT)> = pallet_intent::Intents::::iter().collect(); + assert_eq!(intents.len() , 1); + + let (id, _) = intents[0]; + + Timestamp::set_timestamp(DEADLINE + 10); + }: _(RawOrigin::Signed(cleaner), id) + verify { + assert_eq!(Intent::get_intent(id), None); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use orml_benchmarking::impl_benchmark_test_suite; + use sp_runtime::BuildStorage; + + const LRNA: AssetId = 1; + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + pallet_asset_registry::GenesisConfig:: { + registered_assets: vec![ + ( + Some(LRNA), + Some(b"LRNA".to_vec().try_into().unwrap()), + 1_000u128, + None, + None, + None, + true, + ), + ( + Some(DAI), + Some(b"DAI".to_vec().try_into().unwrap()), + 1_000u128, + None, + None, + None, + true, + ), + ], + native_asset_name: b"HDX".to_vec().try_into().unwrap(), + native_existential_deposit: NativeExistentialDeposit::get(), + native_decimals: 12, + native_symbol: b"HDX".to_vec().try_into().unwrap(), + } + .assimilate_storage(&mut t) + .unwrap(); + + sp_io::TestExternalities::new(t) + } + + impl_benchmark_test_suite!(new_test_ext(),); +} diff --git a/runtime/hydradx/src/benchmarking/lazy_executor.rs b/runtime/hydradx/src/benchmarking/lazy_executor.rs new file mode 100644 index 0000000000..39cb79a21e --- /dev/null +++ b/runtime/hydradx/src/benchmarking/lazy_executor.rs @@ -0,0 +1,96 @@ +use super::*; +use crate::*; + +use frame_benchmarking::account; +use frame_system::RawOrigin; +use hydradx_traits::lazy_executor::Source; +use orml_benchmarking::runtime_benchmarks; +use sp_runtime::DispatchResult; + +const SEED: u32 = 1; + +const HDX: AssetId = 0; + +const TRIL: u128 = 1_000_000_000_000; + +fn fund(to: AccountId, currency: AssetId, amount: Balance) -> DispatchResult { + Currencies::deposit(currency, &to, amount) +} + +runtime_benchmarks! { + {Runtime, pallet_lazy_executor } + + dispatch_top_base_weight { + + //NOTE: treasury need balance otherwise it can't collect fees < ED + Currencies::update_balance( + RawOrigin::Root.into(), + Treasury::account_id(), + HDX, + (10_000 * TRIL) as i128, + )?; + + let acc = account::("origin", 0, SEED); + fund(acc.clone(), HDX, 10_000 * TRIL)?; + let call: Vec = RuntimeCall::Balances(pallet_balances::Call::transfer_keep_alive{ + dest: acc.clone(), + value: 0 + }).encode(); + + LazyExecutor::add_to_queue(Source::ICE(1_u128), acc, call.try_into().unwrap())?; + + assert!(LazyExecutor::call_queue(0).is_some()); + }: { LazyExecutor::dispatch_top(RawOrigin::None.into(), 0)? } + verify { + assert!(LazyExecutor::call_queue(0).is_none()); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use orml_benchmarking::impl_benchmark_test_suite; + use sp_runtime::BuildStorage; + + const LRNA: AssetId = 1; + const DAI: AssetId = 2; + + fn new_test_ext() -> sp_io::TestExternalities { + let mut t = frame_system::GenesisConfig::::default() + .build_storage() + .unwrap(); + + pallet_asset_registry::GenesisConfig:: { + registered_assets: vec![ + ( + Some(LRNA), + Some(b"LRNA".to_vec().try_into().unwrap()), + 1_000u128, + None, + None, + None, + true, + ), + ( + Some(DAI), + Some(b"DAI".to_vec().try_into().unwrap()), + 1_000u128, + None, + None, + None, + true, + ), + ], + native_asset_name: b"HDX".to_vec().try_into().unwrap(), + native_existential_deposit: NativeExistentialDeposit::get(), + native_decimals: 12, + native_symbol: b"HDX".to_vec().try_into().unwrap(), + } + .assimilate_storage(&mut t) + .unwrap(); + + sp_io::TestExternalities::new(t) + } + + impl_benchmark_test_suite!(new_test_ext(),); +} diff --git a/runtime/hydradx/src/benchmarking/mod.rs b/runtime/hydradx/src/benchmarking/mod.rs index 142f66f5e6..77ca943112 100644 --- a/runtime/hydradx/src/benchmarking/mod.rs +++ b/runtime/hydradx/src/benchmarking/mod.rs @@ -11,6 +11,9 @@ pub mod omnipool; pub mod omnipool_liquidity_mining; pub mod route_executor; //pub mod token_gateway_ismp; +pub mod ice; +pub mod intent; +pub mod lazy_executor; pub mod tokens; pub mod vesting; pub mod xyk; diff --git a/runtime/hydradx/src/evm/mod.rs b/runtime/hydradx/src/evm/mod.rs index ba6d3607f5..e3a280e351 100644 --- a/runtime/hydradx/src/evm/mod.rs +++ b/runtime/hydradx/src/evm/mod.rs @@ -57,7 +57,7 @@ mod accounts_conversion; mod erc20_currency; pub mod evm_error_decoder; mod evm_fee; -mod executor; +pub mod executor; mod gas_to_weight_mapping; pub mod permit; pub mod precompiles; diff --git a/runtime/hydradx/src/ice_simulator_provider.rs b/runtime/hydradx/src/ice_simulator_provider.rs new file mode 100644 index 0000000000..8a3970e4d8 --- /dev/null +++ b/runtime/hydradx/src/ice_simulator_provider.rs @@ -0,0 +1,148 @@ +//! This is temporaty implementation of simulators' `DataProvider` using runtime. +//! This should be removed when we'll move solver from runtime to node. + +use core::marker::PhantomData; +use frame_support::traits::Get; +use hydradx_traits::fee::GetDynamicFee; +use ice_support::AssetId; +use ice_support::Balance; +use orml_traits::MultiCurrency; +use sp_runtime::Permill; +use sp_std::vec; +use sp_std::vec::Vec; + +use amm_simulator::omnipool::DataProvider as OmnipoolDataProvider; +use pallet_omnipool::types::AssetState; + +pub struct Omnipool(PhantomData); + +impl> OmnipoolDataProvider for Omnipool { + type AccountId = T::AccountId; + + fn protocol_account() -> Self::AccountId { + pallet_omnipool::Pallet::::protocol_account() + } + + fn assets() -> impl Iterator)> { + pallet_omnipool::pallet::Assets::::iter() + } + + fn free_balance(currncy_id: AssetId, who: &Self::AccountId) -> Balance { + T::Currency::free_balance(currncy_id, who) + } + + fn fee(key: (AssetId, Balance)) -> (Permill, Permill) { + T::Fee::get(key) + } + + fn hub_asset_id() -> AssetId { + T::HubAssetId::get() + } + + fn min_trading_limit() -> Balance { + T::MinimumTradingLimit::get() + } + + fn max_in_ratio() -> Balance { + T::MaxInRatio::get() + } + + fn max_out_ratio() -> Balance { + T::MaxOutRatio::get() + } + + fn slip_fee() -> Option { + pallet_omnipool::pallet::SlipFee::::get() + } +} + +use amm_simulator::stableswap::DataProvider as StableswapDataProvider; +use frame_system::pallet_prelude::BlockNumberFor; +use pallet_stableswap::types::PoolInfo; +use pallet_stableswap::types::PoolPegInfo; +use pallet_stableswap::types::PoolSnapshot; + +pub struct Stableswap(PhantomData); + +impl> StableswapDataProvider for Stableswap { + type BlockNumber = BlockNumberFor; + + fn pools() -> impl Iterator)> { + pallet_stableswap::pallet::Pools::::iter() + } + + fn pool_pegs(pool_id: AssetId) -> Option> { + pallet_stableswap::pallet::PoolPegs::::get(pool_id) + } + + fn create_snapshot(pool_id: AssetId) -> Option> { + pallet_stableswap::Pallet::::create_snapshot(pool_id) + } + + fn min_trading_limit() -> Balance { + T::MinTradingLimit::get() + } +} + +use crate::evm::aave_trade_executor::AaveTradeExecutor; +use crate::evm::executor::BalanceOf; +use crate::evm::executor::NonceIdOf; +use crate::evm::precompiles::erc20_mapping::HydraErc20Mapping; +use crate::Runtime; +use amm_simulator::aave::DataProvider as AaveDataProvider; +use evm::ExitReason; +use hydradx_traits::evm::CallResult; +use hydradx_traits::evm::Erc20Mapping; +use hydradx_traits::evm::EVM; +use pallet_evm::AddressMapping; +use pallet_liquidation::BorrowingContract; +use primitives::EvmAddress; +use sp_core::U256; + +pub struct Aave(PhantomData); + +impl AaveDataProvider for Aave +where + T: frame_system::Config + pallet_liquidation::Config + pallet_evm::Config + pallet_dispatcher::Config, + BalanceOf: TryFrom + Into, + T::AddressMapping: AddressMapping, + pallet_evm::AccountIdOf: From, + NonceIdOf: Into, +{ + fn view(context: hydradx_traits::evm::CallContext, data: Vec, gas: u64) -> (ExitReason, Vec) { + let CallResult { + exit_reason, + value, + contract: _, + gas_used: _, + gas_limit: _t, + } = crate::evm::Executor::::view(context, data, gas); + + (exit_reason, value) + } + + fn borrowing_contract() -> EvmAddress { + pallet_liquidation::BorrowingContract::::get() + } + + fn address_to_asset(address: EvmAddress) -> Option { + crate::evm::precompiles::erc20_mapping::HydraErc20Mapping::address_to_asset(address) + } + + fn pairs() -> Vec<(AssetId, AssetId)> { + let pool = >::get(); + let reserves = match AaveTradeExecutor::::get_reserves_list(pool) { + Ok(reserves) => reserves, + Err(_) => return vec![], + }; + reserves + .into_iter() + .filter_map(|reserve| { + let data = AaveTradeExecutor::::get_reserve_data(pool, reserve).ok()?; + let reserve_asset = HydraErc20Mapping::address_to_asset(reserve)?; + let atoken_asset = HydraErc20Mapping::address_to_asset(data.atoken_address)?; + Some((reserve_asset, atoken_asset)) + }) + .collect() + } +} diff --git a/runtime/hydradx/src/lib.rs b/runtime/hydradx/src/lib.rs index a492ef4bd7..baa1a1e235 100644 --- a/runtime/hydradx/src/lib.rs +++ b/runtime/hydradx/src/lib.rs @@ -41,6 +41,9 @@ mod system; pub mod types; pub mod xcm; +// tmp. implemenation of ice simualtors' data providers +pub mod ice_simulator_provider; + extern crate alloc; use alloc::borrow::Cow; @@ -202,6 +205,11 @@ construct_runtime!( Signet: pallet_signet = 84, EthDispenser: pallet_dispenser = 85, + //ICE + LazyExecutor: pallet_lazy_executor = 86, + Intent: pallet_intent = 87, + ICE: pallet_ice = 88, + // ORML related modules Tokens: orml_tokens = 77, Currencies: pallet_currencies = 79, @@ -380,6 +388,9 @@ mod benches { //[pallet_token_gateway_ismp, benchmarking::token_gateway_ismp::Benchmark] [pallet_evm_accounts, benchmarking::evm_accounts::Benchmark] [pallet_migrations, MultiBlockMigrations] + [pallet_intent, benchmarking::intent::Benchmark] + [pallet_lazy_executor, benchmarking::lazy_executor::Benchmark] + [pallet_ice, benchmarking::ice::Benchmark] ); } diff --git a/runtime/hydradx/src/weights/mod.rs b/runtime/hydradx/src/weights/mod.rs index 6ba164e867..1201f9fd21 100644 --- a/runtime/hydradx/src/weights/mod.rs +++ b/runtime/hydradx/src/weights/mod.rs @@ -46,6 +46,9 @@ pub mod pallet_stableswap; pub mod pallet_staking; pub mod pallet_state_trie_migration; pub mod pallet_timestamp; +pub mod pallet_ice; +pub mod pallet_intent; +pub mod pallet_lazy_executor; pub mod pallet_transaction_multi_payment; pub mod pallet_transaction_pause; pub mod pallet_transaction_payment; diff --git a/runtime/hydradx/src/weights/pallet_ice.rs b/runtime/hydradx/src/weights/pallet_ice.rs new file mode 100644 index 0000000000..9452ad4636 --- /dev/null +++ b/runtime/hydradx/src/weights/pallet_ice.rs @@ -0,0 +1,110 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2024 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Autogenerated weights for `pallet_ice` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 48.0.0 +//! DATE: 2026-03-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/hydradx +// benchmark +// pallet +// --wasm-execution=compiled +// --pallet +// pallet_ice +// --extrinsic +// * +// --heap-pages +// 4096 +// --steps +// 50 +// --repeat +// 20 +// --template +// scripts/pallet-weight-template.hbs +// --output +// runtime/hydradx/src/weights/pallet_ice.rs +// --quiet + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; +use crate::*; + +/// Weights for `pallet_ice`. +pub struct WeightInfo(PhantomData); + +/// Weights for `pallet_ice` using the HydraDX node and recommended hardware. +pub struct HydraWeight(PhantomData); +impl pallet_ice::WeightInfo for HydraWeight { + /// Storage: `AssetRegistry::Assets` (r:2 w:0) + /// Proof: `AssetRegistry::Assets` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`) + /// Storage: `Intent::IntentOwner` (r:1 w:1) + /// Proof: `Intent::IntentOwner` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + /// Storage: `Balances::Reserves` (r:1 w:1) + /// Proof: `Balances::Reserves` (`max_values`: None, `max_size`: Some(1249), added: 3724, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:3 w:3) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `CircuitBreaker::GlobalAssetOverrides` (r:2 w:0) + /// Proof: `CircuitBreaker::GlobalAssetOverrides` (`max_values`: None, `max_size`: Some(21), added: 2496, mode: `MaxEncodedLen`) + /// Storage: `CircuitBreaker::EgressAccounts` (r:2 w:0) + /// Proof: `CircuitBreaker::EgressAccounts` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `EVMAccounts::AccountExtension` (r:1 w:0) + /// Proof: `EVMAccounts::AccountExtension` (`max_values`: None, `max_size`: Some(48), added: 2523, mode: `MaxEncodedLen`) + /// Storage: `HSM::FlashMinter` (r:1 w:0) + /// Proof: `HSM::FlashMinter` (`max_values`: Some(1), `max_size`: Some(20), added: 515, mode: `MaxEncodedLen`) + /// Storage: `AssetRegistry::BannedAssets` (r:1 w:0) + /// Proof: `AssetRegistry::BannedAssets` (`max_values`: None, `max_size`: Some(20), added: 2495, mode: `MaxEncodedLen`) + /// Storage: `Tokens::Accounts` (r:2 w:2) + /// Proof: `Tokens::Accounts` (`max_values`: None, `max_size`: Some(108), added: 2583, mode: `MaxEncodedLen`) + /// Storage: `MultiTransactionPayment::AccountCurrencyMap` (r:2 w:1) + /// Proof: `MultiTransactionPayment::AccountCurrencyMap` (`max_values`: None, `max_size`: Some(52), added: 2527, mode: `MaxEncodedLen`) + /// Storage: `Intent::Intents` (r:1 w:1) + /// Proof: `Intent::Intents` (`max_values`: None, `max_size`: Some(599), added: 3074, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `LazyExecutor::MaxCallWeight` (r:1 w:0) + /// Proof: `LazyExecutor::MaxCallWeight` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `LazyExecutor::Sequencer` (r:1 w:1) + /// Proof: `LazyExecutor::Sequencer` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `LazyExecutor::CallQueue` (r:0 w:1) + /// Proof: `LazyExecutor::CallQueue` (`max_values`: None, `max_size`: Some(578), added: 3053, mode: `MaxEncodedLen`) + fn submit_solution() -> Weight { + // Proof Size summary in bytes: + // Measured: `3850` + // Estimated: `8799` + // Minimum execution time: 321_976_000 picoseconds. + Weight::from_parts(387_369_000, 8799) + .saturating_add(T::DbWeight::get().reads(22_u64)) + .saturating_add(T::DbWeight::get().writes(11_u64)) + } + + fn set_protocol_fee() -> Weight { + Weight::from_parts(10_000_000, 0) + .saturating_add(T::DbWeight::get().reads(1_u64)) + .saturating_add(T::DbWeight::get().writes(1_u64)) + } +} diff --git a/runtime/hydradx/src/weights/pallet_intent.rs b/runtime/hydradx/src/weights/pallet_intent.rs new file mode 100644 index 0000000000..76b10f3af4 --- /dev/null +++ b/runtime/hydradx/src/weights/pallet_intent.rs @@ -0,0 +1,122 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2024 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Autogenerated weights for `pallet_intent` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 48.0.0 +//! DATE: 2026-03-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/hydradx +// benchmark +// pallet +// --wasm-execution=compiled +// --pallet +// pallet_intent +// --extrinsic +// * +// --heap-pages +// 4096 +// --steps +// 50 +// --repeat +// 20 +// --template +// scripts/pallet-weight-template.hbs +// --output +// runtime/hydradx/src/weights/pallet_intent.rs +// --quiet + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; +use crate::*; + +/// Weights for `pallet_intent`. +pub struct WeightInfo(PhantomData); + +/// Weights for `pallet_intent` using the HydraDX node and recommended hardware. +pub struct HydraWeight(PhantomData); +impl pallet_intent::WeightInfo for HydraWeight { + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `AssetRegistry::Assets` (r:2 w:0) + /// Proof: `AssetRegistry::Assets` (`max_values`: None, `max_size`: Some(125), added: 2600, mode: `MaxEncodedLen`) + /// Storage: `Balances::Reserves` (r:1 w:1) + /// Proof: `Balances::Reserves` (`max_values`: None, `max_size`: Some(1249), added: 3724, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + /// Storage: `Intent::NextIncrementalId` (r:1 w:1) + /// Proof: `Intent::NextIncrementalId` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Intent::Intents` (r:0 w:1) + /// Proof: `Intent::Intents` (`max_values`: None, `max_size`: Some(599), added: 3074, mode: `MaxEncodedLen`) + /// Storage: `Intent::IntentOwner` (r:0 w:1) + /// Proof: `Intent::IntentOwner` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + fn submit_intent() -> Weight { + // Proof Size summary in bytes: + // Measured: `2346` + // Estimated: `6190` + // Minimum execution time: 56_988_000 picoseconds. + Weight::from_parts(71_034_000, 6190) + .saturating_add(T::DbWeight::get().reads(6_u64)) + .saturating_add(T::DbWeight::get().writes(5_u64)) + } + /// Storage: `Intent::Intents` (r:1 w:1) + /// Proof: `Intent::Intents` (`max_values`: None, `max_size`: Some(599), added: 3074, mode: `MaxEncodedLen`) + /// Storage: `Intent::IntentOwner` (r:1 w:1) + /// Proof: `Intent::IntentOwner` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + /// Storage: `Balances::Reserves` (r:1 w:1) + /// Proof: `Balances::Reserves` (`max_values`: None, `max_size`: Some(1249), added: 3724, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn remove_intent() -> Weight { + // Proof Size summary in bytes: + // Measured: `2618` + // Estimated: `4714` + // Minimum execution time: 47_118_000 picoseconds. + Weight::from_parts(63_929_000, 4714) + .saturating_add(T::DbWeight::get().reads(4_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } + /// Storage: `Intent::Intents` (r:1 w:1) + /// Proof: `Intent::Intents` (`max_values`: None, `max_size`: Some(599), added: 3074, mode: `MaxEncodedLen`) + /// Storage: `Timestamp::Now` (r:1 w:0) + /// Proof: `Timestamp::Now` (`max_values`: Some(1), `max_size`: Some(8), added: 503, mode: `MaxEncodedLen`) + /// Storage: `Intent::IntentOwner` (r:1 w:1) + /// Proof: `Intent::IntentOwner` (`max_values`: None, `max_size`: Some(64), added: 2539, mode: `MaxEncodedLen`) + /// Storage: `Balances::Reserves` (r:1 w:1) + /// Proof: `Balances::Reserves` (`max_values`: None, `max_size`: Some(1249), added: 3724, mode: `MaxEncodedLen`) + /// Storage: `System::Account` (r:1 w:1) + /// Proof: `System::Account` (`max_values`: None, `max_size`: Some(128), added: 2603, mode: `MaxEncodedLen`) + fn cleanup_intent() -> Weight { + // Proof Size summary in bytes: + // Measured: `2812` + // Estimated: `4714` + // Minimum execution time: 50_506_000 picoseconds. + Weight::from_parts(63_018_000, 4714) + .saturating_add(T::DbWeight::get().reads(5_u64)) + .saturating_add(T::DbWeight::get().writes(4_u64)) + } +} diff --git a/runtime/hydradx/src/weights/pallet_lazy_executor.rs b/runtime/hydradx/src/weights/pallet_lazy_executor.rs new file mode 100644 index 0000000000..055891d5a0 --- /dev/null +++ b/runtime/hydradx/src/weights/pallet_lazy_executor.rs @@ -0,0 +1,78 @@ +// This file is part of HydraDX. + +// Copyright (C) 2020-2024 Intergalactic, Limited (GIB). +// SPDX-License-Identifier: Apache-2.0 + +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + + +//! Autogenerated weights for `pallet_lazy_executor` +//! +//! THIS FILE WAS AUTO-GENERATED USING THE SUBSTRATE BENCHMARK CLI VERSION 48.0.0 +//! DATE: 2026-03-10, STEPS: `50`, REPEAT: `20`, LOW RANGE: `[]`, HIGH RANGE: `[]` +//! WORST CASE MAP SIZE: `1000000` +//! HOSTNAME: `fedora`, CPU: `AMD Ryzen AI 9 HX PRO 370 w/ Radeon 890M` +//! WASM-EXECUTION: `Compiled`, CHAIN: `None`, DB CACHE: `1024` + +// Executed Command: +// ./target/release/hydradx +// benchmark +// pallet +// --wasm-execution=compiled +// --pallet +// pallet_lazy_executor +// --extrinsic +// * +// --heap-pages +// 4096 +// --steps +// 50 +// --repeat +// 20 +// --template +// scripts/pallet-weight-template.hbs +// --output +// runtime/hydradx/src/weights/pallet_lazy_executor.rs +// --quiet + +#![cfg_attr(rustfmt, rustfmt_skip)] +#![allow(unused_parens)] +#![allow(unused_imports)] +#![allow(missing_docs)] + +use frame_support::{traits::Get, weights::{Weight, constants::RocksDbWeight}}; +use core::marker::PhantomData; +use crate::*; + +/// Weights for `pallet_lazy_executor`. +pub struct WeightInfo(PhantomData); + +/// Weights for `pallet_lazy_executor` using the HydraDX node and recommended hardware. +pub struct HydraWeight(PhantomData); +impl pallet_lazy_executor::WeightInfo for HydraWeight { + /// Storage: `LazyExecutor::DispatchNextId` (r:1 w:1) + /// Proof: `LazyExecutor::DispatchNextId` (`max_values`: Some(1), `max_size`: Some(16), added: 511, mode: `MaxEncodedLen`) + /// Storage: `LazyExecutor::CallQueue` (r:1 w:1) + /// Proof: `LazyExecutor::CallQueue` (`max_values`: None, `max_size`: Some(578), added: 3053, mode: `MaxEncodedLen`) + /// Storage: `TransactionPause::PausedTransactions` (r:1 w:0) + /// Proof: `TransactionPause::PausedTransactions` (`max_values`: None, `max_size`: Some(90), added: 2565, mode: `MaxEncodedLen`) + fn dispatch_top_base_weight() -> Weight { + // Proof Size summary in bytes: + // Measured: `1458` + // Estimated: `4043` + // Minimum execution time: 35_256_000 picoseconds. + Weight::from_parts(82_445_000, 4043) + .saturating_add(T::DbWeight::get().reads(3_u64)) + .saturating_add(T::DbWeight::get().writes(2_u64)) + } +} diff --git a/scripts/dca-monitor/package.json b/scripts/dca-monitor/package.json deleted file mode 100644 index 161d979060..0000000000 --- a/scripts/dca-monitor/package.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "name": "dca-monitor", - "version": "1.0.0", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "keywords": [], - "author": "", - "type": "module", - "license": "ISC", - "description": "Monitor DCA pallet activity on a Chopsticks-based chain", - "dependencies": { - "@polkadot/api": "^16.4.5", - "@polkadot/keyring": "^13.5.5" - } -} diff --git a/scripts/ice-security-probe/README.md b/scripts/ice-security-probe/README.md new file mode 100644 index 0000000000..409a0f8fda --- /dev/null +++ b/scripts/ice-security-probe/README.md @@ -0,0 +1,99 @@ +# ICE security-probe scripts + +Standalone JavaScript probes used during the ICE / DcaIntent security +review. Companion to +`../../integration-tests/src/ice/SECURITY_FINDINGS.md` and +`../../integration-tests/src/ice/ATTACK_IDEATION.md`. Not part of the +cargo build. + +Targets a real zombienet (the `lark*` test networks). No chopsticks +support. + +## Files + +| File | Purpose | +|---|---| +| `ddos.mjs` | Intent-bloat DDoS probe — derives N fresh accounts, funds them, submits an unresolvable intent each. Targets storage growth + solver-starvation hypothesis (`SECURITY_FINDINGS.md §B5`). | + +## Setup + +```bash +cd scripts/ice-security-probe +npm install +``` + +⚠️ **Important — install dependencies LOCALLY**: do not rely on a +`node_modules/` higher up the directory tree. Some dev environments have +a stale `@polkadot/api` (v10.x) at `~/dev/node_modules` that Node will +resolve in preference; the older client encodes extrinsics in a way +that modern Hydration runtimes panic on during `validate_transaction` +(`wasm trap: unreachable`). The local `package.json` here pins +`@polkadot/api ^15.9.1`. + +## Run `ddos.mjs` + +Edit the **top-of-file constants** before running: + +| Constant | Default | Notes | +|---|---|---| +| `ENDPOINT` | `wss://2.lark.hydration.cloud` | Hardcoded on purpose. Change in source — no env override. | +| `FUND_HDX` | `50_000n` | HDX per bloat account. Total funder spend = `N × FUND_HDX`. | +| `FUNDER_URI` | mnemonic | Funder URI (mnemonic / hex seed / dev derivation like `//Alice`). | +| `FUNDER_EXPECTED_ADDRESS` | `7Kg…` | Sanity-check the URI derives to the expected address; abort otherwise. | + +Env knobs (safe — do **not** set ENDPOINT here): + +```bash +N=50 INTENTS_PER_ACCOUNT=1 node ddos.mjs +N=200 INTENTS_PER_ACCOUNT=100 node ddos.mjs # 20 000 intents = max bloat (per-account cap is 100) +``` + +## What it does + +1. **Phase 0** — connect, log chain name + head. +2. **Phase 1** — derive `N` deterministic sr25519 keypairs from + `${ROOT_SEED}//ddos-probe//${i}`. +3. **Phase 2** — fund each via `transferKeepAlive` from `FUNDER_URI`. + Refuses to run unless the derived funder address matches + `FUNDER_EXPECTED_ADDRESS` and the funder has ≥ `N × FUND_HDX` free. + Sequential — submits one transfer at a time and polls + `system.account.nonce` for inclusion before sending the next. +4. **Phase 3** — submit one (or `INTENTS_PER_ACCOUNT`) intent per + account: + + ``` + Swap { + asset_in: HDX (0), + asset_out: BNC (14), + amount_in: 1 HDX (= ED, smallest valid reserve), + amount_out: 10^30 BNC (unreachably high — solver never fills), + partial: false, + } + deadline: now + 23 h (just under MaxAllowedIntentDuration of 24 h) + ``` + + Shape empirically verified on lark to **stay unresolved** for 30+ + blocks of observation (no `IntentResolved` event fires). + +5. **Phase 4** — verify: + - `intent.Intents::entries().length` (global count after the bloat) + - sum of `accountIntentCount` over the bloat accounts + - head-block delta during the run + + Final summary printed as JSON. + +## Cleanup + +This script does **not** auto-cancel its intents — they sit until +either their 23 h deadline elapses (then the OCW's `cleanup_intent` +will reap up to 10 per block) or a separate cancel pass is run. + +## Safety notes + +- `ENDPOINT` is a const, not env-overridable, to avoid accidentally + pointing the script at the wrong network. +- `FUNDER_EXPECTED_ADDRESS` guards against the URI deriving to an + unexpected account on a chain with a different ss58 prefix. +- The unresolvable shape uses `amount_in = 1 HDX` (existential deposit) + precisely to keep the per-intent reserve small. Total reserved across + N intents = `N × 1 HDX`. diff --git a/scripts/ice-security-probe/ddos.mjs b/scripts/ice-security-probe/ddos.mjs new file mode 100644 index 0000000000..851607f309 --- /dev/null +++ b/scripts/ice-security-probe/ddos.mjs @@ -0,0 +1,229 @@ +// ICE intent-bloat DDoS probe. +// +// Endpoint, funder, and per-intent shape are all top-of-file constants — +// edit and re-run. +// +// Env knobs: +// N (default 50) — number of bloat accounts to create +// INTENTS_PER_ACCOUNT(default 1) — intents each account submits +// AMOUNT_IN_HDX (default 1) — HDX reserve per intent (must be ≥ ED) +// ROOT_SEED (default below) — root for //ddos-probe//i derivation +// +// Result: N × INTENTS_PER_ACCOUNT unresolvable intents on the target chain. +// +// Flow: +// Phase 1: derive N deterministic sr25519 keypairs from ROOT_SEED//ddos-probe//i. +// Phase 2: fund each with FUND_HDX via transferKeepAlive from FUNDER_URI +// (verified against FUNDER_EXPECTED_ADDRESS). +// Phase 3: submit one (or N) intent per account with shape: +// Swap { HDX → BNC, amount_in: 1 HDX (ED), amount_out: 10^30 BNC, partial: false } +// Empirically unresolvable — solver never produces a fill for an +// absurd min_out, so each intent sits in storage until cleanup. +// Phase 4: verify total intent.Intents:: rows + our accounts' counts. + +import { ApiPromise, WsProvider } from "@polkadot/api"; +import { Keyring } from "@polkadot/keyring"; +import { cryptoWaitReady } from "@polkadot/util-crypto"; + +// ⚠️ HARDCODED — change this string to repoint. +const ENDPOINT = "wss://2.lark.hydration.cloud"; + +const N = parseInt(process.env.N ?? "50", 10); +const INTENTS_PER_ACCOUNT = parseInt(process.env.INTENTS_PER_ACCOUNT ?? "1", 10); +const AMOUNT_IN_HDX = BigInt(process.env.AMOUNT_IN_HDX ?? "1"); +const FUND_HDX = 50_000n; // HDX per bloat account +const HDX_DEC = 1_000_000_000_000n; // 12 decimals + +// ⚠️ FUNDER — the rich account that pays for funding all bloat accounts. +// Accepts any URI: "//Alice", "//Bob//stash", a 12-word mnemonic, or a hex seed. +// Lark2 funder: 100 M HDX, fresh, no conviction-vote lock. +const FUNDER_URI = "season catalog game bacon onion payment rain spin memory achieve boil traffic"; +const FUNDER_EXPECTED_ADDRESS = "7KgGHHPv3Pp8B5XepJz5qu7MrRXxEZbCq5Mb9Vfy5GzjgvF3"; + +// Deterministic 32-byte hex seed used as a URI prefix. addFromUri treats "0x…" +// as a raw mini-secret, so no BIP39 checksum to worry about. Override via ROOT_SEED. +const ROOT_SEED = process.env.ROOT_SEED + ?? "0xb107b107b107b107b107b107b107b107b107b107b107b107b107b107b107b107"; + +const SS58 = 63; +const HDX = 0; +const BNC = 14; + +console.log(`endpoint: ${ENDPOINT}`); +console.log(`DDoS prep → ${ENDPOINT}, N=${N}, intents/acct=${INTENTS_PER_ACCOUNT}`); + +await cryptoWaitReady(); +const provider = new WsProvider(ENDPOINT, 2500, {}, 60_000); +const api = await ApiPromise.create({ provider, throwOnConnect: true, noInitWarn: true }); + +try { + const chain = (await api.rpc.system.chain()).toString(); + const head0 = (await api.rpc.chain.getHeader()).number.toNumber(); + console.log(`phase 0: chain=${chain} head=#${head0}`); + + // ---------- Phase 1: derive N keypairs ---------- + const t1 = Date.now(); + const keyring = new Keyring({ type: "sr25519", ss58Format: SS58 }); + const accounts = []; + for (let i = 0; i < N; i++) { + const pair = keyring.addFromUri(`${ROOT_SEED}//ddos-probe//${i}`); + accounts.push({ index: i, address: pair.address, pair }); + } + console.log(`phase 1: derived ${N} accounts in ${Date.now() - t1}ms`); + console.log(` first: ${accounts[0].address}`); + console.log(` last: ${accounts[N - 1].address}`); + + // ---------- Phase 2: fund via transferKeepAlive from FUNDER_URI ---------- + const fundAmount = FUND_HDX * HDX_DEC; + const t2 = Date.now(); + // Separate keyring for the funder so it doesn't share state with destinations. + const funderKr = new Keyring({ type: "sr25519", ss58Format: SS58 }); + const funder = funderKr.addFromMnemonic(FUNDER_URI); + console.log(`phase 2: funder derives to ${funder.address}`); + if (FUNDER_EXPECTED_ADDRESS && funder.address !== FUNDER_EXPECTED_ADDRESS) { + console.error(`funder address mismatch: got ${funder.address}, expected ${FUNDER_EXPECTED_ADDRESS}`); + process.exit(5); + } + const funderInfo = (await api.query.system.account(funder.address)).toJSON(); + console.log(`phase 2: funder free=${BigInt(funderInfo.data.free)} reserved=${funderInfo.data.reserved} nonce=${funderInfo.nonce}`); + const totalNeeded = fundAmount * BigInt(N); + if (BigInt(funderInfo.data.free) < totalNeeded) { + console.error(`funder lacks balance: have ${BigInt(funderInfo.data.free)}, need ≥ ${totalNeeded}`); + process.exit(6); + } + // Batch transfers via utility.batch_all in chunks of FUND_BATCH_SIZE per tx. + // Each batch is one signed tx → one block. For N=50 that's 1 tx; N=200 → 4 txs. + const FUND_BATCH_SIZE = 50; + const numBatches = Math.ceil(N / FUND_BATCH_SIZE); + console.log(`phase 2: funding ${N} accounts via ${funder.address} batched transfer_keep_alive (${numBatches} × utility.batch_all of up to ${FUND_BATCH_SIZE}, each acct: ${FUND_HDX} HDX)`); + for (let b = 0; b < numBatches; b++) { + const slice = accounts.slice(b * FUND_BATCH_SIZE, (b + 1) * FUND_BATCH_SIZE); + const calls = slice.map((a) => api.tx.balances.transferKeepAlive(a.address, fundAmount)); + const ext = api.tx.utility.batchAll(calls); + const beforeNonce = (await api.query.system.account(funder.address)).toJSON().nonce; + try { + const hash = await ext.signAndSend(funder); + console.log(` batch ${b + 1}/${numBatches} (${slice.length} txs, hash ${hash.toHex().slice(0, 12)}…)`); + const deadline = Date.now() + 60_000; + while (Date.now() < deadline) { + await new Promise((r) => setTimeout(r, 1_000)); + const cur = (await api.query.system.account(funder.address)).toJSON().nonce; + if (cur > beforeNonce) break; + } + } catch (e) { + console.error(` batch ${b} failed: ${e.message.split("\n")[0]}`); + throw e; + } + } + console.log(`phase 2: funded ${N} accounts in ${Date.now() - t2}ms`); + + // ---------- Phase 2 verify ---------- + let funded = 0; + for (const acc of accounts) { + const sys = (await api.query.system.account(acc.address)).toJSON(); + if (BigInt(sys.data.free) >= fundAmount / 2n) funded++; + } + console.log(`phase 2 verify: ${funded}/${N} accounts have ≥ ${FUND_HDX / 2n} HDX`); + if (funded < N) { + console.error("not all accounts funded; aborting before intent submission"); + process.exit(3); + } + + // ---------- Phase 3: submit unresolvable intents ---------- + const t3 = Date.now(); + const now = (await api.query.timestamp.now()).toNumber(); + const amountIn = AMOUNT_IN_HDX * HDX_DEC; + const amountOut = 10n ** 30n; // unreachably high + const intentInput = { + data: { Swap: { asset_in: HDX, asset_out: BNC, amount_in: amountIn, amount_out: amountOut, partial: false } }, + deadline: now + 23 * 60 * 60 * 1000, // 23h (under the 24h max) + on_resolved: null, + }; + + // Each account submits its INTENTS_PER_ACCOUNT intents bundled into one + // utility.batchAll → one signed tx per account. 50 accts × 100 intents = + // 50 in-flight subscriptions instead of 5000 (OOM-safe). + // If INTENTS_PER_ACCOUNT > BATCH_MAX, split into multiple per-account batches. + const BATCH_MAX = 100; + const submitted = []; + const failed = []; + const allProms = []; + for (const acc of accounts) { + let nonce = (await api.rpc.system.accountNextIndex(acc.address)).toNumber(); + for (let off = 0; off < INTENTS_PER_ACCOUNT; off += BATCH_MAX) { + const count = Math.min(BATCH_MAX, INTENTS_PER_ACCOUNT - off); + const calls = []; + for (let k = 0; k < count; k++) calls.push(api.tx.intent.submitIntent(intentInput)); + const ext = api.tx.utility.batchAll(calls); + const myNonce = nonce++; + allProms.push(new Promise((resolve) => { + let settledLocal = false; + ext.signAndSend(acc.pair, { nonce: myNonce }, (r) => { + if (settledLocal) return; + if (r.isError) { settledLocal = true; failed.push({ acct: acc.index, nonce: myNonce, count, err: "send" }); resolve(); return; } + if (r.status.isInBlock || r.status.isFinalized) { + settledLocal = true; + let ok = true; + let failMsg = null; + for (const { event } of r.events) { + if (api.events.system?.ExtrinsicFailed?.is?.(event)) { + ok = false; + const [de] = event.data; + if (de.isModule) { + const m = api.registry.findMetaError(de.asModule); + failMsg = `${m.section}.${m.name}`; + } else failMsg = de.toString(); + } + if (api.events.utility?.BatchInterrupted?.is?.(event)) { + ok = false; + failMsg = `utility.BatchInterrupted at ${event.data[0]?.toString()}`; + } + } + if (ok) submitted.push({ acct: acc.index, nonce: myNonce, count }); + else failed.push({ acct: acc.index, nonce: myNonce, count, err: failMsg }); + const totalDone = submitted.reduce((s, x) => s + x.count, 0) + failed.reduce((s, x) => s + x.count, 0); + if ((submitted.length + failed.length) % 5 === 0 || (submitted.length + failed.length) === accounts.length * Math.ceil(INTENTS_PER_ACCOUNT / BATCH_MAX)) { + const pct = (totalDone / (N * INTENTS_PER_ACCOUNT) * 100).toFixed(0); + console.log(` progress: ${totalDone}/${N * INTENTS_PER_ACCOUNT} intents (${pct}%) — ${submitted.length} batches ok / ${failed.length} fail @ ${((Date.now() - t3) / 1000).toFixed(1)}s`); + } + resolve(); + } + }).catch((e) => { if (!settledLocal) { settledLocal = true; failed.push({ acct: acc.index, nonce: myNonce, count, err: "exc:" + e.message.split("\n")[0].slice(0,80) }); resolve(); } }); + })); + } + } + await Promise.race([ + Promise.all(allProms), + new Promise((r) => setTimeout(r, 5 * 60_000)), + ]); + const submittedIntents = submitted.reduce((s, x) => s + x.count, 0); + console.log(`phase 3: submitted ${submittedIntents}/${N * INTENTS_PER_ACCOUNT} intents (in ${submitted.length} batches) in ${((Date.now() - t3) / 1000).toFixed(1)}s, failed batches=${failed.length}`); + if (failed.length && failed.length <= 10) console.log(" failures:", failed); + + // ---------- Phase 4: verify bloat ---------- + const t4 = Date.now(); + const totalIntentsEntries = (await api.query.intent.intents.entries()).length; + let accIntentCount = 0; + for (const acc of accounts) accIntentCount += (await api.query.intent.accountIntentCount(acc.address)).toNumber(); + const headFinal = (await api.rpc.chain.getHeader()).number.toNumber(); + console.log(`phase 4: intent.Intents:: total rows = ${totalIntentsEntries}`); + console.log(`phase 4: our accounts hold = ${accIntentCount} intents (${N * INTENTS_PER_ACCOUNT} expected)`); + console.log(`phase 4: head moved ${head0} → ${headFinal} (+${headFinal - head0} blocks)`); + console.log(`phase 4: verified in ${Date.now() - t4}ms`); + + // ---------- Summary ---------- + console.log("---------- summary ----------"); + console.log({ + endpoint: ENDPOINT, + chain, + accounts_created: accounts.length, + intents_submitted: submittedIntents, + intents_failed_batches: failed.length, + total_hdx_reserved_planks: (amountIn * BigInt(submittedIntents)).toString(), + total_intents_onchain: totalIntentsEntries, + elapsed_ms: Date.now() - t1, + }); +} finally { + await api.disconnect(); + await provider.disconnect(); +} diff --git a/scripts/dca-monitor/package-lock.json b/scripts/ice-security-probe/package-lock.json similarity index 68% rename from scripts/dca-monitor/package-lock.json rename to scripts/ice-security-probe/package-lock.json index 6b914deb49..eeb9de48b8 100644 --- a/scripts/dca-monitor/package-lock.json +++ b/scripts/ice-security-probe/package-lock.json @@ -1,16 +1,17 @@ { - "name": "dca-monitor", - "version": "1.0.0", + "name": "ice-security-probe", + "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "dca-monitor", - "version": "1.0.0", - "license": "ISC", + "name": "ice-security-probe", + "version": "0.0.1", "dependencies": { - "@polkadot/api": "^16.4.5", - "@polkadot/keyring": "^13.5.5" + "@polkadot/api": "^15.9.1", + "@polkadot/keyring": "^13.4.3", + "@polkadot/util": "^13.4.3", + "@polkadot/util-crypto": "^13.4.3" } }, "node_modules/@noble/curves": { @@ -113,25 +114,25 @@ "optional": true }, "node_modules/@polkadot/api": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-16.5.4.tgz", - "integrity": "sha512-mX1fwtXCBAHXEyZLSnSrMDGP+jfU2rr7GfDVQBz0cBY1nmY8N34RqPWGrZWj8o4DxVu1DQ91sGncOmlBwEl0Qg==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/api-augment": "16.5.4", - "@polkadot/api-base": "16.5.4", - "@polkadot/api-derive": "16.5.4", - "@polkadot/keyring": "^14.0.1", - "@polkadot/rpc-augment": "16.5.4", - "@polkadot/rpc-core": "16.5.4", - "@polkadot/rpc-provider": "16.5.4", - "@polkadot/types": "16.5.4", - "@polkadot/types-augment": "16.5.4", - "@polkadot/types-codec": "16.5.4", - "@polkadot/types-create": "16.5.4", - "@polkadot/types-known": "16.5.4", - "@polkadot/util": "^14.0.1", - "@polkadot/util-crypto": "^14.0.1", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/api/-/api-15.10.2.tgz", + "integrity": "sha512-UM/510TwdugPjMpfyhhMNOZJ3M2ftRk0Ftxe+WSWev3o1u0dxqGuIN6fN0c224CHXIr58uWXUoMRHi6Cnfaxhw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api-augment": "15.10.2", + "@polkadot/api-base": "15.10.2", + "@polkadot/api-derive": "15.10.2", + "@polkadot/keyring": "^13.4.4", + "@polkadot/rpc-augment": "15.10.2", + "@polkadot/rpc-core": "15.10.2", + "@polkadot/rpc-provider": "15.10.2", + "@polkadot/types": "15.10.2", + "@polkadot/types-augment": "15.10.2", + "@polkadot/types-codec": "15.10.2", + "@polkadot/types-create": "15.10.2", + "@polkadot/types-known": "15.10.2", + "@polkadot/util": "^13.4.4", + "@polkadot/util-crypto": "^13.4.4", "eventemitter3": "^5.0.1", "rxjs": "^7.8.1", "tslib": "^2.8.1" @@ -141,17 +142,17 @@ } }, "node_modules/@polkadot/api-augment": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-16.5.4.tgz", - "integrity": "sha512-9FTohz13ih458V2JBFjRACKHPqfM6j4bmmTbcSaE7hXcIOYzm4ABFo7xq5osLyvItganjsICErL2vRn2zULycw==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/api-augment/-/api-augment-15.10.2.tgz", + "integrity": "sha512-CCli5ltPiJEyQF/8DmTRpTfYKHY4W0B+xQDmzKgFmd+Q64Qot0fGpsaZXZftef1Tuoh0Uqak9qM+6B4APXIPkQ==", "license": "Apache-2.0", "dependencies": { - "@polkadot/api-base": "16.5.4", - "@polkadot/rpc-augment": "16.5.4", - "@polkadot/types": "16.5.4", - "@polkadot/types-augment": "16.5.4", - "@polkadot/types-codec": "16.5.4", - "@polkadot/util": "^14.0.1", + "@polkadot/api-base": "15.10.2", + "@polkadot/rpc-augment": "15.10.2", + "@polkadot/types": "15.10.2", + "@polkadot/types-augment": "15.10.2", + "@polkadot/types-codec": "15.10.2", + "@polkadot/util": "^13.4.4", "tslib": "^2.8.1" }, "engines": { @@ -159,14 +160,14 @@ } }, "node_modules/@polkadot/api-base": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-16.5.4.tgz", - "integrity": "sha512-V69v3ieg5+91yRUCG1vFRSLr7V7MvHPvo/QrzleIUu8tPXWldJ0kyXbWKHVNZEpVBA9LpjGvII+MHUW7EaKMNg==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/api-base/-/api-base-15.10.2.tgz", + "integrity": "sha512-7DJw++5IbPrsLPGcTlIZbMOretfvQJG80CW8+A+t2BLxbbv+I2neWNQ9QV9O28XsbOHzNgKHXuRyirdaG/dvrg==", "license": "Apache-2.0", "dependencies": { - "@polkadot/rpc-core": "16.5.4", - "@polkadot/types": "16.5.4", - "@polkadot/util": "^14.0.1", + "@polkadot/rpc-core": "15.10.2", + "@polkadot/types": "15.10.2", + "@polkadot/util": "^13.4.4", "rxjs": "^7.8.1", "tslib": "^2.8.1" }, @@ -175,19 +176,19 @@ } }, "node_modules/@polkadot/api-derive": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-16.5.4.tgz", - "integrity": "sha512-0JP2a6CaqTviacHsmnUKF4VLRsKdYOzQCqdL9JpwY/QBz/ZLqIKKPiSRg285EVLf8n/hWdTfxbWqQCsRa5NL+Q==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/api": "16.5.4", - "@polkadot/api-augment": "16.5.4", - "@polkadot/api-base": "16.5.4", - "@polkadot/rpc-core": "16.5.4", - "@polkadot/types": "16.5.4", - "@polkadot/types-codec": "16.5.4", - "@polkadot/util": "^14.0.1", - "@polkadot/util-crypto": "^14.0.1", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/api-derive/-/api-derive-15.10.2.tgz", + "integrity": "sha512-tF9DZvdm7hkRIJ1HtJzu73vdqQWBr8935YSN/RNsRb4FhJK5cHaC2uB4NLdRMnyUjmH0JRSnvWFq+HHcVxFJZw==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/api": "15.10.2", + "@polkadot/api-augment": "15.10.2", + "@polkadot/api-base": "15.10.2", + "@polkadot/rpc-core": "15.10.2", + "@polkadot/types": "15.10.2", + "@polkadot/types-codec": "15.10.2", + "@polkadot/util": "^13.4.4", + "@polkadot/util-crypto": "^13.4.4", "rxjs": "^7.8.1", "tslib": "^2.8.1" }, @@ -195,24 +196,6 @@ "node": ">=18" } }, - "node_modules/@polkadot/api/node_modules/@polkadot/keyring": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-14.0.1.tgz", - "integrity": "sha512-kHydQPCeTvJrMC9VQO8LPhAhTUxzxfNF1HEknhZDBPPsxP/XpkYsEy/Ln1QzJmQqD5VsgwzLDE6cExbJ2CT9CA==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/util": "14.0.1", - "@polkadot/util-crypto": "14.0.1", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@polkadot/util": "14.0.1", - "@polkadot/util-crypto": "14.0.1" - } - }, "node_modules/@polkadot/keyring": { "version": "13.5.9", "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-13.5.9.tgz", @@ -231,7 +214,7 @@ "@polkadot/util-crypto": "13.5.9" } }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/networks": { + "node_modules/@polkadot/networks": { "version": "13.5.9", "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-13.5.9.tgz", "integrity": "sha512-nmKUKJjiLgcih0MkdlJNMnhEYdwEml2rv/h59ll2+rAvpsVWMTLCb6Cq6q7UC44+8kiWK2UUJMkFU+3PFFxndA==", @@ -245,140 +228,16 @@ "node": ">=18" } }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/util": { - "version": "13.5.9", - "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-13.5.9.tgz", - "integrity": "sha512-pIK3XYXo7DKeFRkEBNYhf3GbCHg6dKQisSvdzZwuyzA6m7YxQq4DFw4IE464ve4Z7WsJFt3a6C9uII36hl9EWw==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/x-bigint": "13.5.9", - "@polkadot/x-global": "13.5.9", - "@polkadot/x-textdecoder": "13.5.9", - "@polkadot/x-textencoder": "13.5.9", - "@types/bn.js": "^5.1.6", - "bn.js": "^5.2.1", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/util-crypto": { - "version": "13.5.9", - "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz", - "integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==", - "license": "Apache-2.0", - "dependencies": { - "@noble/curves": "^1.3.0", - "@noble/hashes": "^1.3.3", - "@polkadot/networks": "13.5.9", - "@polkadot/util": "13.5.9", - "@polkadot/wasm-crypto": "^7.5.3", - "@polkadot/wasm-util": "^7.5.3", - "@polkadot/x-bigint": "13.5.9", - "@polkadot/x-randomvalues": "13.5.9", - "@scure/base": "^1.1.7", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@polkadot/util": "13.5.9" - } - }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/x-bigint": { - "version": "13.5.9", - "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-13.5.9.tgz", - "integrity": "sha512-JVW6vw3e8fkcRyN9eoc6JIl63MRxNQCP/tuLdHWZts1tcAYao0hpWUzteqJY93AgvmQ91KPsC1Kf3iuuZCi74g==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/x-global": "13.5.9", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/x-global": { - "version": "13.5.9", - "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-13.5.9.tgz", - "integrity": "sha512-zSRWvELHd3Q+bFkkI1h2cWIqLo1ETm+MxkNXLec3lB56iyq/MjWBxfXnAFFYFayvlEVneo7CLHcp+YTFd9aVSA==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/x-randomvalues": { - "version": "13.5.9", - "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-13.5.9.tgz", - "integrity": "sha512-Uuuz3oubf1JCCK97fsnVUnHvk4BGp/W91mQWJlgl5TIOUSSTIRr+lb5GurCfl4kgnQq53Zi5fJV+qR9YumbnZw==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/x-global": "13.5.9", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@polkadot/util": "13.5.9", - "@polkadot/wasm-util": "*" - } - }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/x-textdecoder": { - "version": "13.5.9", - "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-13.5.9.tgz", - "integrity": "sha512-W2HhVNUbC/tuFdzNMbnXAWsIHSg9SC9QWDNmFD3nXdSzlXNgL8NmuiwN2fkYvCQBtp/XSoy0gDLx0C+Fo19cfw==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/x-global": "13.5.9", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polkadot/keyring/node_modules/@polkadot/x-textencoder": { - "version": "13.5.9", - "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-13.5.9.tgz", - "integrity": "sha512-SG0MHnLUgn1ZxFdm0KzMdTHJ47SfqFhdIPMcGA0Mg/jt2rwrfrP3jtEIJMsHfQpHvfsNPfv55XOMmoPWuQnP/Q==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/x-global": "13.5.9", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@polkadot/networks": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/networks/-/networks-14.0.1.tgz", - "integrity": "sha512-wGlBtXDkusRAj4P7uxfPz80gLO1+j99MLBaQi3bEym2xrFrFhgIWVHOZlBit/1PfaBjhX2Z8XjRxaM2w1p7w2w==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/util": "14.0.1", - "@substrate/ss58-registry": "^1.51.0", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/@polkadot/rpc-augment": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-16.5.4.tgz", - "integrity": "sha512-j9v3Ttqv/EYGezHtVksGJAFZhE/4F7LUWooOazh/53ATowMby3lZUdwInrK6bpYmG2whmYMw/Fo283fwDroBtQ==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-augment/-/rpc-augment-15.10.2.tgz", + "integrity": "sha512-9QQ8utyAEdEl7iScteIN59EBu8eNZjZa8AfKBitbdq1Hezd+WPil5LdoYi+wmJOMhZHeDT1s7/j2+kY1Z2Vymg==", "license": "Apache-2.0", "dependencies": { - "@polkadot/rpc-core": "16.5.4", - "@polkadot/types": "16.5.4", - "@polkadot/types-codec": "16.5.4", - "@polkadot/util": "^14.0.1", + "@polkadot/rpc-core": "15.10.2", + "@polkadot/types": "15.10.2", + "@polkadot/types-codec": "15.10.2", + "@polkadot/util": "^13.4.4", "tslib": "^2.8.1" }, "engines": { @@ -386,15 +245,15 @@ } }, "node_modules/@polkadot/rpc-core": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-16.5.4.tgz", - "integrity": "sha512-92LOSTWujPjtmKOPvfCPs8rAaPFU+18wTtkIzwPwKxvxkN/SWsYSGIxmsoags9ramyHB6jp7Lr59TEuGMxIZzQ==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-core/-/rpc-core-15.10.2.tgz", + "integrity": "sha512-vqDvr1WcHH3WPzDV4WTlf2S5cDmIoFPciynJ8eNcKqR3mG7Cqd0iL+MG6s0KFXdSY2Qvtl+0C6yZN0xr4Ha6BQ==", "license": "Apache-2.0", "dependencies": { - "@polkadot/rpc-augment": "16.5.4", - "@polkadot/rpc-provider": "16.5.4", - "@polkadot/types": "16.5.4", - "@polkadot/util": "^14.0.1", + "@polkadot/rpc-augment": "15.10.2", + "@polkadot/rpc-provider": "15.10.2", + "@polkadot/types": "15.10.2", + "@polkadot/util": "^13.4.4", "rxjs": "^7.8.1", "tslib": "^2.8.1" }, @@ -403,19 +262,19 @@ } }, "node_modules/@polkadot/rpc-provider": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-16.5.4.tgz", - "integrity": "sha512-mNAIBRA3jMvpnHsuqAX4InHSIqBdgxFD6ayVUFFAzOX8Fh6Xpd4RdI1dqr6a1pCzjnPSby4nbg+VuadWwauVtg==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/keyring": "^14.0.1", - "@polkadot/types": "16.5.4", - "@polkadot/types-support": "16.5.4", - "@polkadot/util": "^14.0.1", - "@polkadot/util-crypto": "^14.0.1", - "@polkadot/x-fetch": "^14.0.1", - "@polkadot/x-global": "^14.0.1", - "@polkadot/x-ws": "^14.0.1", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/rpc-provider/-/rpc-provider-15.10.2.tgz", + "integrity": "sha512-kqpPW8U0stVW+uOZP8g5d87Xb8rbXJR5PUub6xgGG6AOMbbvvuCU3GSohu/iozo4p9uD7TGH90jvbxj1rjJVMA==", + "license": "Apache-2.0", + "dependencies": { + "@polkadot/keyring": "^13.4.4", + "@polkadot/types": "15.10.2", + "@polkadot/types-support": "15.10.2", + "@polkadot/util": "^13.4.4", + "@polkadot/util-crypto": "^13.4.4", + "@polkadot/x-fetch": "^13.4.4", + "@polkadot/x-global": "^13.4.4", + "@polkadot/x-ws": "^13.4.4", "eventemitter3": "^5.0.1", "mock-socket": "^9.3.1", "nock": "^13.5.5", @@ -428,36 +287,18 @@ "@substrate/connect": "0.8.11" } }, - "node_modules/@polkadot/rpc-provider/node_modules/@polkadot/keyring": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-14.0.1.tgz", - "integrity": "sha512-kHydQPCeTvJrMC9VQO8LPhAhTUxzxfNF1HEknhZDBPPsxP/XpkYsEy/Ln1QzJmQqD5VsgwzLDE6cExbJ2CT9CA==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/util": "14.0.1", - "@polkadot/util-crypto": "14.0.1", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@polkadot/util": "14.0.1", - "@polkadot/util-crypto": "14.0.1" - } - }, "node_modules/@polkadot/types": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-16.5.4.tgz", - "integrity": "sha512-8Oo1QWaL0DkIc/n2wKBIozPWug/0b2dPVhL+XrXHxJX7rIqS0x8sXDRbM9r166sI0nTqJiUho7pRIkt2PR/DMQ==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/types/-/types-15.10.2.tgz", + "integrity": "sha512-/wDwKdDijxSXyNk5YezhVitdFxoQaTSSG9KXa7dEWujtmS/51UHmt9+P3W8b8D8kKaCvumahf/ww3GJI6s0Eqw==", "license": "Apache-2.0", "dependencies": { - "@polkadot/keyring": "^14.0.1", - "@polkadot/types-augment": "16.5.4", - "@polkadot/types-codec": "16.5.4", - "@polkadot/types-create": "16.5.4", - "@polkadot/util": "^14.0.1", - "@polkadot/util-crypto": "^14.0.1", + "@polkadot/keyring": "^13.4.4", + "@polkadot/types-augment": "15.10.2", + "@polkadot/types-codec": "15.10.2", + "@polkadot/types-create": "15.10.2", + "@polkadot/util": "^13.4.4", + "@polkadot/util-crypto": "^13.4.4", "rxjs": "^7.8.1", "tslib": "^2.8.1" }, @@ -466,14 +307,14 @@ } }, "node_modules/@polkadot/types-augment": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-16.5.4.tgz", - "integrity": "sha512-AGjXR+Q9O9UtVkGw/HuOXlbRqVpvG6H8nr+taXP71wuC6RD9gznFBFBqoNkfWHD2w89esNVQLTvXHVxlLpTXqA==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/types-augment/-/types-augment-15.10.2.tgz", + "integrity": "sha512-X/xh+Dzud6OIyr7q8xttAwn+Fb5hKImIWEO1oG8WcInqv+P0vRyu7Tds+2ut9t64sJi3ydJ7I+T+WxZYheCU7g==", "license": "Apache-2.0", "dependencies": { - "@polkadot/types": "16.5.4", - "@polkadot/types-codec": "16.5.4", - "@polkadot/util": "^14.0.1", + "@polkadot/types": "15.10.2", + "@polkadot/types-codec": "15.10.2", + "@polkadot/util": "^13.4.4", "tslib": "^2.8.1" }, "engines": { @@ -481,13 +322,13 @@ } }, "node_modules/@polkadot/types-codec": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-16.5.4.tgz", - "integrity": "sha512-OQtT1pmJu2F3/+Vh1OiXifKoeRy+CU1+Lu7dgTcdO705dnxU4447Zup5JVCJDnxBmMITts/38vbFN2pD225AnA==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/types-codec/-/types-codec-15.10.2.tgz", + "integrity": "sha512-dhwbaukUZiYDW3QAAnLAFThYE5hQGdwBMWOVTt9+aBWxEKovLK93j0V30tEzMUtrZy8xaRWdhdDeQ3DSmxEP6w==", "license": "Apache-2.0", "dependencies": { - "@polkadot/util": "^14.0.1", - "@polkadot/x-bigint": "^14.0.1", + "@polkadot/util": "^13.4.4", + "@polkadot/x-bigint": "^13.4.4", "tslib": "^2.8.1" }, "engines": { @@ -495,13 +336,13 @@ } }, "node_modules/@polkadot/types-create": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-16.5.4.tgz", - "integrity": "sha512-URQnvr/sgvgIRSxIW3lmml6HMSTRRj2hTZIm6nhMTlYSVT4rLWx0ZbYUAjoPBbaJ+BmoqZ6Bbs+tA+5cQViv5Q==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/types-create/-/types-create-15.10.2.tgz", + "integrity": "sha512-vqXwPUSgx/By31qSkhOR5GN6zMbF1MkiX3F1g5KKHaRE8p/DdTry4LhufxhtK1mr9eBWvVGXxCOZdwjQco2M1A==", "license": "Apache-2.0", "dependencies": { - "@polkadot/types-codec": "16.5.4", - "@polkadot/util": "^14.0.1", + "@polkadot/types-codec": "15.10.2", + "@polkadot/util": "^13.4.4", "tslib": "^2.8.1" }, "engines": { @@ -509,16 +350,16 @@ } }, "node_modules/@polkadot/types-known": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-16.5.4.tgz", - "integrity": "sha512-Dd59y4e3AFCrH9xiqMU4xlG5+Zy0OTy7GQvqJVYXZFyAH+4HYDlxXjJGcSidGAmJcclSYfS3wyEkfw+j1EOVEw==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/types-known/-/types-known-15.10.2.tgz", + "integrity": "sha512-vs02WiIlLualrrh/EuA5qzK6QzatVPqBBNqa66dUtmyhJy48OEelBK+QLfOIQvZKU0ModEunoVrnxuY+O1DCmA==", "license": "Apache-2.0", "dependencies": { - "@polkadot/networks": "^14.0.1", - "@polkadot/types": "16.5.4", - "@polkadot/types-codec": "16.5.4", - "@polkadot/types-create": "16.5.4", - "@polkadot/util": "^14.0.1", + "@polkadot/networks": "^13.4.4", + "@polkadot/types": "15.10.2", + "@polkadot/types-codec": "15.10.2", + "@polkadot/types-create": "15.10.2", + "@polkadot/util": "^13.4.4", "tslib": "^2.8.1" }, "engines": { @@ -526,46 +367,28 @@ } }, "node_modules/@polkadot/types-support": { - "version": "16.5.4", - "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-16.5.4.tgz", - "integrity": "sha512-Ra6keCaO73ibxN6MzA56jFq9EReje7jjE4JQfzV5IpyDZdXcmPyJiEfa2Yps/YSP13Gc2e38t9FFyVau0V+SFQ==", + "version": "15.10.2", + "resolved": "https://registry.npmjs.org/@polkadot/types-support/-/types-support-15.10.2.tgz", + "integrity": "sha512-sHamH6MehJa7aGZ/DHTB6vJAhSN5VrJx5lpDpb3xgBFTr0cVc5IsociqgJ/mgvyEIdLF3laraPxREqxCmuxTaQ==", "license": "Apache-2.0", "dependencies": { - "@polkadot/util": "^14.0.1", + "@polkadot/util": "^13.4.4", "tslib": "^2.8.1" }, "engines": { "node": ">=18" } }, - "node_modules/@polkadot/types/node_modules/@polkadot/keyring": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/keyring/-/keyring-14.0.1.tgz", - "integrity": "sha512-kHydQPCeTvJrMC9VQO8LPhAhTUxzxfNF1HEknhZDBPPsxP/XpkYsEy/Ln1QzJmQqD5VsgwzLDE6cExbJ2CT9CA==", - "license": "Apache-2.0", - "dependencies": { - "@polkadot/util": "14.0.1", - "@polkadot/util-crypto": "14.0.1", - "tslib": "^2.8.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@polkadot/util": "14.0.1", - "@polkadot/util-crypto": "14.0.1" - } - }, "node_modules/@polkadot/util": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-14.0.1.tgz", - "integrity": "sha512-764HhxkPV3x5rM0/p6QdynC2dw26n+SaE+jisjx556ViCd4E28Ke4xSPef6C0Spy4aoXf2gt0PuLEcBvd6fVZg==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util/-/util-13.5.9.tgz", + "integrity": "sha512-pIK3XYXo7DKeFRkEBNYhf3GbCHg6dKQisSvdzZwuyzA6m7YxQq4DFw4IE464ve4Z7WsJFt3a6C9uII36hl9EWw==", "license": "Apache-2.0", "dependencies": { - "@polkadot/x-bigint": "14.0.1", - "@polkadot/x-global": "14.0.1", - "@polkadot/x-textdecoder": "14.0.1", - "@polkadot/x-textencoder": "14.0.1", + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-global": "13.5.9", + "@polkadot/x-textdecoder": "13.5.9", + "@polkadot/x-textencoder": "13.5.9", "@types/bn.js": "^5.1.6", "bn.js": "^5.2.1", "tslib": "^2.8.0" @@ -575,28 +398,27 @@ } }, "node_modules/@polkadot/util-crypto": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-14.0.1.tgz", - "integrity": "sha512-Cu7AKUzBTsUkbOtyuNzXcTpDjR9QW0fVR56o3gBmzfUCmvO1vlsuGzmmPzqpHymQQ3rrfqV78CPs62EGhw0R+A==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/util-crypto/-/util-crypto-13.5.9.tgz", + "integrity": "sha512-foUesMhxkTk8CZ0/XEcfvHk6I0O+aICqqVJllhOpyp/ZVnrTBKBf59T6RpsXx2pCtBlMsLRvg/6Mw7RND1HqDg==", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.3.0", "@noble/hashes": "^1.3.3", - "@polkadot/networks": "14.0.1", - "@polkadot/util": "14.0.1", + "@polkadot/networks": "13.5.9", + "@polkadot/util": "13.5.9", "@polkadot/wasm-crypto": "^7.5.3", "@polkadot/wasm-util": "^7.5.3", - "@polkadot/x-bigint": "14.0.1", - "@polkadot/x-randomvalues": "14.0.1", + "@polkadot/x-bigint": "13.5.9", + "@polkadot/x-randomvalues": "13.5.9", "@scure/base": "^1.1.7", - "@scure/sr25519": "^0.2.0", "tslib": "^2.8.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@polkadot/util": "14.0.1" + "@polkadot/util": "13.5.9" } }, "node_modules/@polkadot/wasm-bridge": { @@ -704,12 +526,12 @@ } }, "node_modules/@polkadot/x-bigint": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-14.0.1.tgz", - "integrity": "sha512-gfozjGnebr2rqURs31KtaWumbW4rRZpbiluhlmai6luCNrf5u8pB+oLA35kPEntrsLk9PnIG9OsC/n4hEtx4OQ==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-bigint/-/x-bigint-13.5.9.tgz", + "integrity": "sha512-JVW6vw3e8fkcRyN9eoc6JIl63MRxNQCP/tuLdHWZts1tcAYao0hpWUzteqJY93AgvmQ91KPsC1Kf3iuuZCi74g==", "license": "Apache-2.0", "dependencies": { - "@polkadot/x-global": "14.0.1", + "@polkadot/x-global": "13.5.9", "tslib": "^2.8.0" }, "engines": { @@ -717,12 +539,12 @@ } }, "node_modules/@polkadot/x-fetch": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-14.0.1.tgz", - "integrity": "sha512-yFsnO0xfkp3bIcvH70ZvmeUINYH1YnjOIS1B430f3w6axkqKhAOWCgzzKGMSRgn4dtm3YgwMBKPQ4nyfIsGOJQ==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-fetch/-/x-fetch-13.5.9.tgz", + "integrity": "sha512-urwXQZtT4yYROiRdJS6zHu18J/jCoAGpbgPIAjwdqjT11t9XIq4SjuPMxD19xBRhbYe9ocWV8i1KHuoMbZgKbA==", "license": "Apache-2.0", "dependencies": { - "@polkadot/x-global": "14.0.1", + "@polkadot/x-global": "13.5.9", "node-fetch": "^3.3.2", "tslib": "^2.8.0" }, @@ -731,9 +553,9 @@ } }, "node_modules/@polkadot/x-global": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-14.0.1.tgz", - "integrity": "sha512-aCI44DJU4fU0XXqrrSGIpi7JrZXK2kpe0jaQ2p6oDVXOOYEnZYXnMhTTmBE1lF/xtxzX50MnZrrU87jziU0qbA==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-global/-/x-global-13.5.9.tgz", + "integrity": "sha512-zSRWvELHd3Q+bFkkI1h2cWIqLo1ETm+MxkNXLec3lB56iyq/MjWBxfXnAFFYFayvlEVneo7CLHcp+YTFd9aVSA==", "license": "Apache-2.0", "dependencies": { "tslib": "^2.8.0" @@ -743,29 +565,29 @@ } }, "node_modules/@polkadot/x-randomvalues": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-14.0.1.tgz", - "integrity": "sha512-/XkQcvshzJLHITuPrN3zmQKuFIPdKWoaiHhhVLD6rQWV60lTXA3ajw3ocju8ZN7xRxnweMS9Ce0kMPYa0NhRMg==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-randomvalues/-/x-randomvalues-13.5.9.tgz", + "integrity": "sha512-Uuuz3oubf1JCCK97fsnVUnHvk4BGp/W91mQWJlgl5TIOUSSTIRr+lb5GurCfl4kgnQq53Zi5fJV+qR9YumbnZw==", "license": "Apache-2.0", "dependencies": { - "@polkadot/x-global": "14.0.1", + "@polkadot/x-global": "13.5.9", "tslib": "^2.8.0" }, "engines": { "node": ">=18" }, "peerDependencies": { - "@polkadot/util": "14.0.1", + "@polkadot/util": "13.5.9", "@polkadot/wasm-util": "*" } }, "node_modules/@polkadot/x-textdecoder": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-14.0.1.tgz", - "integrity": "sha512-CcWiPCuPVJsNk4Vq43lgFHqLRBQHb4r9RD7ZIYgmwoebES8TNm4g2ew9ToCzakFKSpzKu6I07Ne9wv/dt5zLuw==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-textdecoder/-/x-textdecoder-13.5.9.tgz", + "integrity": "sha512-W2HhVNUbC/tuFdzNMbnXAWsIHSg9SC9QWDNmFD3nXdSzlXNgL8NmuiwN2fkYvCQBtp/XSoy0gDLx0C+Fo19cfw==", "license": "Apache-2.0", "dependencies": { - "@polkadot/x-global": "14.0.1", + "@polkadot/x-global": "13.5.9", "tslib": "^2.8.0" }, "engines": { @@ -773,12 +595,12 @@ } }, "node_modules/@polkadot/x-textencoder": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-14.0.1.tgz", - "integrity": "sha512-VY51SpQmF1ccmAGLfxhYnAe95Spfz049WZ/+kK4NfsGF9WejxVdU53Im5C80l45r8qHuYQsCWU3+t0FNunh2Kg==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-textencoder/-/x-textencoder-13.5.9.tgz", + "integrity": "sha512-SG0MHnLUgn1ZxFdm0KzMdTHJ47SfqFhdIPMcGA0Mg/jt2rwrfrP3jtEIJMsHfQpHvfsNPfv55XOMmoPWuQnP/Q==", "license": "Apache-2.0", "dependencies": { - "@polkadot/x-global": "14.0.1", + "@polkadot/x-global": "13.5.9", "tslib": "^2.8.0" }, "engines": { @@ -786,12 +608,12 @@ } }, "node_modules/@polkadot/x-ws": { - "version": "14.0.1", - "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-14.0.1.tgz", - "integrity": "sha512-Q18hoSuOl7F4aENNGNt9XYxkrjwZlC6xye9OQrPDeHam1SrvflGv9mSZHyo+mwJs0z1PCz2STpPEN9PKfZvHng==", + "version": "13.5.9", + "resolved": "https://registry.npmjs.org/@polkadot/x-ws/-/x-ws-13.5.9.tgz", + "integrity": "sha512-NKVgvACTIvKT8CjaQu9d0dERkZsWIZngX/4NVSjc01WHmln4F4y/zyBdYn/Z2V0Zw28cISx+lB4qxRmqTe7gbg==", "license": "Apache-2.0", "dependencies": { - "@polkadot/x-global": "14.0.1", + "@polkadot/x-global": "13.5.9", "tslib": "^2.8.0", "ws": "^8.18.0" }, @@ -808,19 +630,6 @@ "url": "https://paulmillr.com/funding/" } }, - "node_modules/@scure/sr25519": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@scure/sr25519/-/sr25519-0.2.0.tgz", - "integrity": "sha512-uUuLP7Z126XdSizKtrCGqYyR3b3hYtJ6Fg/XFUXmc2//k2aXHDLqZwFeXxL97gg4XydPROPVnuaHGF2+xriSKg==", - "license": "MIT", - "dependencies": { - "@noble/curves": "~1.9.2", - "@noble/hashes": "~1.8.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, "node_modules/@substrate/connect": { "version": "0.8.11", "resolved": "https://registry.npmjs.org/@substrate/connect/-/connect-0.8.11.tgz", @@ -884,12 +693,12 @@ } }, "node_modules/@types/node": { - "version": "25.4.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", - "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.18.0" + "undici-types": "~7.19.0" } }, "node_modules/bn.js": { @@ -1080,9 +889,9 @@ "license": "0BSD" }, "node_modules/undici-types": { - "version": "7.18.2", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", - "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", "license": "MIT" }, "node_modules/web-streams-polyfill": { @@ -1095,9 +904,9 @@ } }, "node_modules/ws": { - "version": "8.19.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", - "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "license": "MIT", "engines": { "node": ">=10.0.0" diff --git a/scripts/ice-security-probe/package.json b/scripts/ice-security-probe/package.json new file mode 100644 index 0000000000..1b00b5b6f2 --- /dev/null +++ b/scripts/ice-security-probe/package.json @@ -0,0 +1,13 @@ +{ + "name": "ice-security-probe", + "version": "0.0.1", + "private": true, + "type": "module", + "description": "ICE / DcaIntent security probes against lark testnets.", + "dependencies": { + "@polkadot/api": "^15.9.1", + "@polkadot/keyring": "^13.4.3", + "@polkadot/util": "^13.4.3", + "@polkadot/util-crypto": "^13.4.3" + } +} diff --git a/traits/Cargo.toml b/traits/Cargo.toml index 2ee587bcf9..c947eac1e7 100644 --- a/traits/Cargo.toml +++ b/traits/Cargo.toml @@ -16,6 +16,7 @@ sp-arithmetic = { workspace = true } # Local dependencies primitives = { workspace = true } +hydra-dx-math = { workspace = true } # Substrate dependencies frame-support = { workspace = true } @@ -36,4 +37,5 @@ std = [ "primitives/std", "pallet-evm/std", "orml-traits/std", + "hydra-dx-math/std", ] diff --git a/traits/src/amm.rs b/traits/src/amm.rs new file mode 100644 index 0000000000..ea3d0fc12c --- /dev/null +++ b/traits/src/amm.rs @@ -0,0 +1,1535 @@ +//! AMM Simulation traits for off-chain trade simulation. +//! +//! This module provides traits for simulating AMM trades without modifying chain state. +//! The key abstractions are: +//! +//! - [`SimulatorConfig`] - Configuration bundling simulators and route provider +//! - [`AmmSimulator`] - Individual pool simulator (Omnipool, Stableswap, etc.) +//! - [`SimulatorSet`] - Composite of multiple simulators with automatic dispatch +//! - [`AMMInterface`] - High-level interface for the solver + +use crate::router::{PoolEdge, PoolType, Route}; +use codec::{Decode, Encode}; +use frame_support::traits::Get; +use hydra_dx_math::types::Ratio; +use primitives::{AssetId, Balance}; +use scale_info::TypeInfo; +use sp_std::vec::Vec; + +#[derive(Debug, Clone, PartialEq, Eq, Encode, Decode, TypeInfo)] +pub enum SimulatorError { + /// Pool type not supported by this simulator + NotSupported, + /// Asset not found in the pool + AssetNotFound, + /// Insufficient liquidity for the trade + InsufficientLiquidity, + /// Trade amount too small + TradeTooSmall, + /// Trade amount too large + TradeTooLarge, + /// Limit not met (slippage) + LimitNotMet, + /// Math overflow/underflow + MathError, + /// Other error + Other, +} + +/// Result of a simulated trade +#[derive(Clone, Debug, Encode, Decode, TypeInfo, PartialEq, Eq)] +pub struct TradeResult { + pub amount_in: Balance, + pub amount_out: Balance, +} + +impl TradeResult { + pub fn new(amount_in: Balance, amount_out: Balance) -> Self { + Self { amount_in, amount_out } + } +} + +/// Extended trade result including the route used +#[derive(Clone, Debug)] +pub struct TradeExecution { + pub amount_in: Balance, + pub amount_out: Balance, + pub route: Route, +} + +/// Trait for discovering trade routes given an asset pair and simulator state. +/// +/// Implementations can use on-chain routes, simulator probing, or any custom strategy. +/// The `State` generic allows implementations to inspect simulator state during discovery. +pub trait RouteDiscovery { + fn discover_routes( + asset_in: AssetId, + asset_out: AssetId, + state: &State, + ) -> Result>, SimulatorError>; +} + +/// Configuration trait for the simulator compositor. +/// +/// Bundles together the simulators and route discovery strategy. +/// This is the main configuration type used by the ICE pallet. +/// +/// # Example +/// ```ignore +/// pub struct HydrationSimulatorConfig; +/// +/// impl SimulatorConfig for HydrationSimulatorConfig { +/// type Simulators = (Omnipool, Stableswap, Aave); +/// type RouteDiscovery = OnChainRouteDiscovery; +/// type PriceDenominator = LRNAAssetId; +/// } +/// ``` +pub trait SimulatorConfig { + /// Tuple of simulators implementing SimulatorSet + type Simulators: SimulatorSet; + /// Strategy for discovering trade routes + type RouteDiscovery: RouteDiscovery<::State>; + /// The reference asset all prices are denominated in (e.g., LRNA) + type PriceDenominator: Get; + + /// Existential deposit for the given asset. + /// Returns 0 when ED is unknown or not applicable. + fn existential_deposit(_asset_id: AssetId) -> Balance { + 0 + } +} + +/// Individual pool simulator trait. +/// +/// Each AMM type (Omnipool, Stableswap, etc.) implements this trait +/// to provide simulation capabilities without modifying chain state. +/// +/// The simulator captures a snapshot of the pool state and can simulate +/// trades against that snapshot, returning updated state and trade results. +pub trait AmmSimulator { + /// Snapshot of the pool state needed for simulation. + /// Must be Clone for simulation state updates, Encode for offchain worker serialization. + type Snapshot: Clone + Encode; + + /// Returns the pool type this simulator handles (representative value) + fn pool_type() -> PoolType; + + /// Check if a given pool type is handled by this simulator. + /// By default, uses exact equality, but can be overridden for pool types + /// that have multiple instances (e.g., Stableswap pools with different IDs). + fn matches_pool_type(pool_type: PoolType) -> bool { + pool_type == Self::pool_type() + } + + /// Create a snapshot from current chain state + fn snapshot() -> Self::Snapshot; + + /// Simulate a sell trade + fn simulate_sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError>; + + /// Simulate a buy trade + fn simulate_buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + snapshot: &Self::Snapshot, + ) -> Result<(Self::Snapshot, TradeResult), SimulatorError>; + + /// Get the spot price for a direct pair within this pool. + /// Returns the price of asset_in in terms of asset_out as a Ratio. + fn get_spot_price( + asset_in: AssetId, + asset_out: AssetId, + snapshot: &Self::Snapshot, + ) -> Result; + + /// Check if this simulator can trade the given asset pair directly. + /// Returns Some(PoolType) if the pair can be traded, None otherwise. + /// + /// Each AMM knows its own trading capabilities: + /// - Omnipool: Can trade if both assets are in the omnipool + /// - Stableswap: Can trade if both assets are in the same pool + /// - Aave: Can trade if it's a valid aToken/underlying pair + fn can_trade(_asset_in: AssetId, _asset_out: AssetId, _snapshot: &Self::Snapshot) -> Option> { + // Default implementation: cannot determine trading capability + None + } + + /// Return pool edges describing the tradeable asset sets in this simulator. + fn pool_edges(snapshot: &Self::Snapshot) -> Vec>; +} + +/// A set of simulators that can be dispatched to based on pool type. +/// +/// Implemented for individual `AmmSimulator` types (via blanket impl) and +/// tuples of simulators (via macro), allowing composition of multiple +/// simulators with automatic state management. +/// +/// When using tuples, the state type is automatically derived as a tuple +/// of individual snapshot types. +/// +/// # Example +/// ```ignore +/// // Single simulator - state is OmnipoolSnapshot +/// type Simulators = Omnipool; +/// +/// // Multiple simulators - state is (OmnipoolSnapshot, StableswapSnapshot) +/// type Simulators = (Omnipool, Stableswap); +/// ``` +pub trait SimulatorSet { + /// Composite state type - typically a tuple of individual snapshots. + /// Must be Clone for simulation state updates, Encode for offchain worker serialization. + type State: Clone + Encode; + + /// Create initial state by calling snapshot() on each simulator + fn initial_state() -> Self::State; + + /// Simulate a sell trade, dispatching to the appropriate simulator + fn simulate_sell( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError>; + + /// Simulate a buy trade, dispatching to the appropriate simulator + fn simulate_buy( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError>; + + /// Get spot price, dispatching to the appropriate simulator + fn get_spot_price( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result; + + /// Find a simulator that can trade the given asset pair. + /// Returns Some(PoolType) from the first simulator that can handle it. + fn can_trade(asset_in: AssetId, asset_out: AssetId, state: &Self::State) -> Option>; + + /// Collect pool edges from all simulators. + fn pool_edges(state: &Self::State) -> Vec>; +} + +/// High-level AMM interface for the solver. +/// +/// This is the interface the solver uses - it handles routing +/// and delegates to individual simulators via SimulatorSet. +/// +/// Callers must discover the route explicitly via `discover_route` before +/// calling `sell`, `buy`, or `get_spot_price`. This avoids cycles when +/// route discovery itself needs to simulate trades. +pub trait AMMInterface { + type Error; + type State: Clone; + + /// Discover all viable routes for trading `asset_in` -> `asset_out`. + fn discover_routes( + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result>, Self::Error>; + + fn sell( + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + route: Route, + state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error>; + + fn buy( + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + route: Route, + state: &Self::State, + ) -> Result<(Self::State, TradeExecution), Self::Error>; + + /// Get spot price for an asset pair along the given route. + /// Returns the price of asset_in in terms of asset_out. + fn get_spot_price( + asset_in: AssetId, + asset_out: AssetId, + route: Route, + state: &Self::State, + ) -> Result; + + /// The reference asset all prices can be denominated in (e.g., LRNA) + fn price_denominator() -> AssetId; + + /// Collect pool edges from all configured simulators. + fn pool_edges(state: &Self::State) -> Vec>; + + /// Existential deposit for the given asset. + /// Returns 0 if the asset is unknown (no minimum). + fn existential_deposit(_asset_id: AssetId) -> Balance { + 0 + } +} + +/// Blanket implementation for single simulator. +/// Allows using a single `AmmSimulator` where a `SimulatorSet` is expected. +impl SimulatorSet for S { + type State = S::Snapshot; + + fn initial_state() -> Self::State { + S::snapshot() + } + + fn simulate_sell( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + if !S::matches_pool_type(pool_type) { + return Err(SimulatorError::NotSupported); + } + S::simulate_sell(asset_in, asset_out, amount_in, min_amount_out, state) + } + + fn simulate_buy( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + if !S::matches_pool_type(pool_type) { + return Err(SimulatorError::NotSupported); + } + S::simulate_buy(asset_in, asset_out, amount_out, max_amount_in, state) + } + + fn get_spot_price( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result { + if !S::matches_pool_type(pool_type) { + return Err(SimulatorError::NotSupported); + } + S::get_spot_price(asset_in, asset_out, state) + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, state: &Self::State) -> Option> { + S::can_trade(asset_in, asset_out, state) + } + + fn pool_edges(state: &Self::State) -> Vec> { + S::pool_edges(state) + } +} + +/// Macro to implement SimulatorSet for tuples. +/// +/// This generates implementations for tuples of 2 to N simulators, +/// handling the sequential dispatch and positional state updates. +macro_rules! impl_simulator_set_for_tuple { + // 2-tuple + (($A:ident, $B:ident), ($a:tt, $b:tt)) => { + impl<$A, $B> SimulatorSet for ($A, $B) + where + $A: SimulatorSet, + $B: SimulatorSet, + { + type State = ($A::State, $B::State); + + fn initial_state() -> Self::State { + ($A::initial_state(), $B::initial_state()) + } + + fn simulate_sell( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$a, + ) { + Ok((new_state, result)) => Ok(((new_state, state.$b.clone()), result)), + Err(SimulatorError::NotSupported) => { + match $B::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$b, + ) { + Ok((new_state, result)) => Ok(((state.$a.clone(), new_state), result)), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn simulate_buy( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$a, + ) { + Ok((new_state, result)) => Ok(((new_state, state.$b.clone()), result)), + Err(SimulatorError::NotSupported) => { + match $B::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$b, + ) { + Ok((new_state, result)) => Ok(((state.$a.clone(), new_state), result)), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn get_spot_price( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result { + match $A::get_spot_price(pool_type, asset_in, asset_out, &state.$a) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => $B::get_spot_price(pool_type, asset_in, asset_out, &state.$b), + Err(e) => Err(e), + } + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, state: &Self::State) -> Option> { + if let Some(pool_type) = $A::can_trade(asset_in, asset_out, &state.$a) { + return Some(pool_type); + } + $B::can_trade(asset_in, asset_out, &state.$b) + } + + fn pool_edges(state: &Self::State) -> Vec> { + let mut edges = $A::pool_edges(&state.$a); + edges.extend($B::pool_edges(&state.$b)); + edges + } + } + }; + + // 3-tuple + (($A:ident, $B:ident, $C:ident), ($a:tt, $b:tt, $c:tt)) => { + impl<$A, $B, $C> SimulatorSet for ($A, $B, $C) + where + $A: SimulatorSet, + $B: SimulatorSet, + $C: SimulatorSet, + { + type State = ($A::State, $B::State, $C::State); + + fn initial_state() -> Self::State { + ($A::initial_state(), $B::initial_state(), $C::initial_state()) + } + + fn simulate_sell( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$a, + ) { + Ok((new_state, result)) => Ok(((new_state, state.$b.clone(), state.$c.clone()), result)), + Err(SimulatorError::NotSupported) => { + match $B::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$b, + ) { + Ok((new_state, result)) => Ok(((state.$a.clone(), new_state, state.$c.clone()), result)), + Err(SimulatorError::NotSupported) => { + match $C::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$c, + ) { + Ok((new_state, result)) => { + Ok(((state.$a.clone(), state.$b.clone(), new_state), result)) + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn simulate_buy( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$a, + ) { + Ok((new_state, result)) => Ok(((new_state, state.$b.clone(), state.$c.clone()), result)), + Err(SimulatorError::NotSupported) => { + match $B::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$b, + ) { + Ok((new_state, result)) => Ok(((state.$a.clone(), new_state, state.$c.clone()), result)), + Err(SimulatorError::NotSupported) => { + match $C::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$c, + ) { + Ok((new_state, result)) => { + Ok(((state.$a.clone(), state.$b.clone(), new_state), result)) + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn get_spot_price( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result { + match $A::get_spot_price(pool_type, asset_in, asset_out, &state.$a) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $B::get_spot_price(pool_type, asset_in, asset_out, &state.$b) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + $C::get_spot_price(pool_type, asset_in, asset_out, &state.$c) + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, state: &Self::State) -> Option> { + if let Some(pool_type) = $A::can_trade(asset_in, asset_out, &state.$a) { + return Some(pool_type); + } + if let Some(pool_type) = $B::can_trade(asset_in, asset_out, &state.$b) { + return Some(pool_type); + } + $C::can_trade(asset_in, asset_out, &state.$c) + } + + fn pool_edges(state: &Self::State) -> Vec> { + let mut edges = $A::pool_edges(&state.$a); + edges.extend($B::pool_edges(&state.$b)); + edges.extend($C::pool_edges(&state.$c)); + edges + } + } + }; + + // 4-tuple + (($A:ident, $B:ident, $C:ident, $D:ident), ($a:tt, $b:tt, $c:tt, $d:tt)) => { + impl<$A, $B, $C, $D> SimulatorSet for ($A, $B, $C, $D) + where + $A: SimulatorSet, + $B: SimulatorSet, + $C: SimulatorSet, + $D: SimulatorSet, + { + type State = ($A::State, $B::State, $C::State, $D::State); + + fn initial_state() -> Self::State { + ( + $A::initial_state(), + $B::initial_state(), + $C::initial_state(), + $D::initial_state(), + ) + } + + fn simulate_sell( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$a, + ) { + Ok((new_state, result)) => Ok(( + (new_state, state.$b.clone(), state.$c.clone(), state.$d.clone()), + result, + )), + Err(SimulatorError::NotSupported) => { + match $B::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$b, + ) { + Ok((new_state, result)) => Ok(( + (state.$a.clone(), new_state, state.$c.clone(), state.$d.clone()), + result, + )), + Err(SimulatorError::NotSupported) => { + match $C::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$c, + ) { + Ok((new_state, result)) => Ok(( + (state.$a.clone(), state.$b.clone(), new_state, state.$d.clone()), + result, + )), + Err(SimulatorError::NotSupported) => { + match $D::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$d, + ) { + Ok((new_state, result)) => Ok(( + (state.$a.clone(), state.$b.clone(), state.$c.clone(), new_state), + result, + )), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn simulate_buy( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$a, + ) { + Ok((new_state, result)) => Ok(( + (new_state, state.$b.clone(), state.$c.clone(), state.$d.clone()), + result, + )), + Err(SimulatorError::NotSupported) => { + match $B::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$b, + ) { + Ok((new_state, result)) => Ok(( + (state.$a.clone(), new_state, state.$c.clone(), state.$d.clone()), + result, + )), + Err(SimulatorError::NotSupported) => { + match $C::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$c, + ) { + Ok((new_state, result)) => Ok(( + (state.$a.clone(), state.$b.clone(), new_state, state.$d.clone()), + result, + )), + Err(SimulatorError::NotSupported) => { + match $D::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$d, + ) { + Ok((new_state, result)) => Ok(( + (state.$a.clone(), state.$b.clone(), state.$c.clone(), new_state), + result, + )), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn get_spot_price( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result { + match $A::get_spot_price(pool_type, asset_in, asset_out, &state.$a) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $B::get_spot_price(pool_type, asset_in, asset_out, &state.$b) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $C::get_spot_price(pool_type, asset_in, asset_out, &state.$c) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + $D::get_spot_price(pool_type, asset_in, asset_out, &state.$d) + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, state: &Self::State) -> Option> { + if let Some(pool_type) = $A::can_trade(asset_in, asset_out, &state.$a) { + return Some(pool_type); + } + if let Some(pool_type) = $B::can_trade(asset_in, asset_out, &state.$b) { + return Some(pool_type); + } + if let Some(pool_type) = $C::can_trade(asset_in, asset_out, &state.$c) { + return Some(pool_type); + } + $D::can_trade(asset_in, asset_out, &state.$d) + } + + fn pool_edges(state: &Self::State) -> Vec> { + let mut edges = $A::pool_edges(&state.$a); + edges.extend($B::pool_edges(&state.$b)); + edges.extend($C::pool_edges(&state.$c)); + edges.extend($D::pool_edges(&state.$d)); + edges + } + } + }; + + // 5-tuple + (($A:ident, $B:ident, $C:ident, $D:ident, $E:ident), ($a:tt, $b:tt, $c:tt, $d:tt, $e:tt)) => { + impl<$A, $B, $C, $D, $E> SimulatorSet for ($A, $B, $C, $D, $E) + where + $A: SimulatorSet, + $B: SimulatorSet, + $C: SimulatorSet, + $D: SimulatorSet, + $E: SimulatorSet, + { + type State = ($A::State, $B::State, $C::State, $D::State, $E::State); + + fn initial_state() -> Self::State { + ( + $A::initial_state(), + $B::initial_state(), + $C::initial_state(), + $D::initial_state(), + $E::initial_state(), + ) + } + + fn simulate_sell( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$a, + ) { + Ok((new_state, result)) => Ok(( + ( + new_state, + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $B::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$b, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + new_state, + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $C::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$c, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + new_state, + state.$d.clone(), + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $D::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$d, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + new_state, + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $E::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$e, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + new_state, + ), + result, + )), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn simulate_buy( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$a, + ) { + Ok((new_state, result)) => Ok(( + ( + new_state, + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $B::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$b, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + new_state, + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $C::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$c, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + new_state, + state.$d.clone(), + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $D::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$d, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + new_state, + state.$e.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $E::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$e, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + new_state, + ), + result, + )), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn get_spot_price( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result { + match $A::get_spot_price(pool_type, asset_in, asset_out, &state.$a) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $B::get_spot_price(pool_type, asset_in, asset_out, &state.$b) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $C::get_spot_price(pool_type, asset_in, asset_out, &state.$c) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $D::get_spot_price(pool_type, asset_in, asset_out, &state.$d) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + $E::get_spot_price(pool_type, asset_in, asset_out, &state.$e) + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, state: &Self::State) -> Option> { + if let Some(pool_type) = $A::can_trade(asset_in, asset_out, &state.$a) { + return Some(pool_type); + } + if let Some(pool_type) = $B::can_trade(asset_in, asset_out, &state.$b) { + return Some(pool_type); + } + if let Some(pool_type) = $C::can_trade(asset_in, asset_out, &state.$c) { + return Some(pool_type); + } + if let Some(pool_type) = $D::can_trade(asset_in, asset_out, &state.$d) { + return Some(pool_type); + } + $E::can_trade(asset_in, asset_out, &state.$e) + } + + fn pool_edges(state: &Self::State) -> Vec> { + let mut edges = $A::pool_edges(&state.$a); + edges.extend($B::pool_edges(&state.$b)); + edges.extend($C::pool_edges(&state.$c)); + edges.extend($D::pool_edges(&state.$d)); + edges.extend($E::pool_edges(&state.$e)); + edges + } + } + }; + + // 6-tuple + (($A:ident, $B:ident, $C:ident, $D:ident, $E:ident, $F:ident), ($a:tt, $b:tt, $c:tt, $d:tt, $e:tt, $f:tt)) => { + impl<$A, $B, $C, $D, $E, $F> SimulatorSet for ($A, $B, $C, $D, $E, $F) + where + $A: SimulatorSet, + $B: SimulatorSet, + $C: SimulatorSet, + $D: SimulatorSet, + $E: SimulatorSet, + $F: SimulatorSet, + { + type State = ($A::State, $B::State, $C::State, $D::State, $E::State, $F::State); + + fn initial_state() -> Self::State { + ( + $A::initial_state(), + $B::initial_state(), + $C::initial_state(), + $D::initial_state(), + $E::initial_state(), + $F::initial_state(), + ) + } + + fn simulate_sell( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_in: Balance, + min_amount_out: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$a, + ) { + Ok((new_state, result)) => Ok(( + ( + new_state, + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $B::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$b, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + new_state, + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $C::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$c, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + new_state, + state.$d.clone(), + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $D::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$d, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + new_state, + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $E::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$e, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + new_state, + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $F::simulate_sell( + pool_type, + asset_in, + asset_out, + amount_in, + min_amount_out, + &state.$f, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + new_state, + ), + result, + )), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn simulate_buy( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + amount_out: Balance, + max_amount_in: Balance, + state: &Self::State, + ) -> Result<(Self::State, TradeResult), SimulatorError> { + match $A::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$a, + ) { + Ok((new_state, result)) => Ok(( + ( + new_state, + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $B::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$b, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + new_state, + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $C::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$c, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + new_state, + state.$d.clone(), + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $D::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$d, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + new_state, + state.$e.clone(), + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $E::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$e, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + new_state, + state.$f.clone(), + ), + result, + )), + Err(SimulatorError::NotSupported) => { + match $F::simulate_buy( + pool_type, + asset_in, + asset_out, + amount_out, + max_amount_in, + &state.$f, + ) { + Ok((new_state, result)) => Ok(( + ( + state.$a.clone(), + state.$b.clone(), + state.$c.clone(), + state.$d.clone(), + state.$e.clone(), + new_state, + ), + result, + )), + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn get_spot_price( + pool_type: PoolType, + asset_in: AssetId, + asset_out: AssetId, + state: &Self::State, + ) -> Result { + match $A::get_spot_price(pool_type, asset_in, asset_out, &state.$a) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $B::get_spot_price(pool_type, asset_in, asset_out, &state.$b) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $C::get_spot_price(pool_type, asset_in, asset_out, &state.$c) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $D::get_spot_price(pool_type, asset_in, asset_out, &state.$d) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + match $E::get_spot_price(pool_type, asset_in, asset_out, &state.$e) { + Ok(price) => Ok(price), + Err(SimulatorError::NotSupported) => { + $F::get_spot_price(pool_type, asset_in, asset_out, &state.$f) + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + Err(e) => Err(e), + } + } + + fn can_trade(asset_in: AssetId, asset_out: AssetId, state: &Self::State) -> Option> { + if let Some(pool_type) = $A::can_trade(asset_in, asset_out, &state.$a) { + return Some(pool_type); + } + if let Some(pool_type) = $B::can_trade(asset_in, asset_out, &state.$b) { + return Some(pool_type); + } + if let Some(pool_type) = $C::can_trade(asset_in, asset_out, &state.$c) { + return Some(pool_type); + } + if let Some(pool_type) = $D::can_trade(asset_in, asset_out, &state.$d) { + return Some(pool_type); + } + if let Some(pool_type) = $E::can_trade(asset_in, asset_out, &state.$e) { + return Some(pool_type); + } + $F::can_trade(asset_in, asset_out, &state.$f) + } + + fn pool_edges(state: &Self::State) -> Vec> { + let mut edges = $A::pool_edges(&state.$a); + edges.extend($B::pool_edges(&state.$b)); + edges.extend($C::pool_edges(&state.$c)); + edges.extend($D::pool_edges(&state.$d)); + edges.extend($E::pool_edges(&state.$e)); + edges.extend($F::pool_edges(&state.$f)); + edges + } + } + }; +} + +// Generate implementations for tuples of 2 to 6 elements +impl_simulator_set_for_tuple!((A, B), (0, 1)); +impl_simulator_set_for_tuple!((A, B, C), (0, 1, 2)); +impl_simulator_set_for_tuple!((A, B, C, D), (0, 1, 2, 3)); +impl_simulator_set_for_tuple!((A, B, C, D, E), (0, 1, 2, 3, 4)); +impl_simulator_set_for_tuple!((A, B, C, D, E, F), (0, 1, 2, 3, 4, 5)); diff --git a/traits/src/lazy_executor.rs b/traits/src/lazy_executor.rs new file mode 100644 index 0000000000..14f76f7523 --- /dev/null +++ b/traits/src/lazy_executor.rs @@ -0,0 +1,20 @@ +use codec::Decode; +use codec::DecodeWithMemTracking; +use codec::Encode; +use codec::MaxEncodedLen; +use frame_support::pallet_prelude::RuntimeDebug; +use frame_support::pallet_prelude::TypeInfo; + +pub type Identificator = u128; +#[derive(Clone, Encode, Decode, PartialEq, Eq, RuntimeDebug, TypeInfo, DecodeWithMemTracking, MaxEncodedLen)] +pub enum Source { + ICE(Identificator), +} + +pub trait Mutate { + type Error; + type BoundedCall; + + // Function queue `call` to be lazylly executed as `origin` + fn queue(src: Source, origin: AccountId, call: Self::BoundedCall) -> Result<(), Self::Error>; +} diff --git a/traits/src/lib.rs b/traits/src/lib.rs index 2014162592..8e044b0ee4 100644 --- a/traits/src/lib.rs +++ b/traits/src/lib.rs @@ -18,9 +18,11 @@ #![cfg_attr(not(feature = "std"), no_std)] #![allow(clippy::upper_case_acronyms)] +pub mod amm; pub mod circuit_breaker; pub mod evm; pub mod fee; +pub mod lazy_executor; pub mod liquidity_mining; pub mod nft; pub mod offchain; diff --git a/traits/src/router.rs b/traits/src/router.rs index 44259e8f79..d9c0770964 100644 --- a/traits/src/router.rs +++ b/traits/src/router.rs @@ -70,6 +70,14 @@ impl AssetPair { } pub trait RouteProvider { + /// Get the explicitly configured route from storage, if any. + /// Returns None if no route is explicitly configured (will use default). + fn get_onchain_route(_asset_pair: AssetPair) -> Option> { + // Default: no explicit routes stored + None + } + + /// Get route for asset pair (explicit or default). fn get_route(asset_pair: AssetPair) -> Route { BoundedVec::truncate_from(vec![Trade { pool: PoolType::Omnipool, @@ -103,6 +111,16 @@ pub struct Trade { pub asset_out: AssetId, } +/// A pool instance with its tradeable assets. +/// +/// Used by route discovery to build a graph where every asset pair +/// within a pool becomes a directed edge. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PoolEdge { + pub pool_type: PoolType, + pub assets: Vec, +} + #[derive(Debug, PartialEq)] pub struct AmountInAndOut { pub amount_in: Balance,