Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 0 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,12 @@ All notable changes to DEX.DO are recorded here. Entries are date-based, newest

### Added

- `services/market-manager/`: new market-manager service with Dockerfile, stage configs, and event-list seed (`config/events.stage.json`) — wires DEX market lifecycle off-chain.
- `openapi/openapi.yaml` and `openapi/index.html`: published OpenAPI spec rendered on GitHub Pages. Added `services/api/src/bin/gen-openapi.rs` generator binary, `openapi/generate.sh`, and `.github/workflows/{openapi,pages}.yml` to regenerate and deploy on push.
- `crates/chain/`: new chain-client crate carved out of `infrastructure/chain_sender.rs` (client, DTO, error, test helpers).
- `makerComission` and `takerComission` fields on the `GET /api/v1/markets` response. Signed `DECIMAL` strings (e.g. `"0.00045"`); a negative `makerComission` is a maker rebate credited rather than debited.

### Changed

- Trimmed `services/market-manager/Cargo.toml` and `Dockerfile` to fix the stage image build (#32).
- Regenerated OpenAPI after BE-DEX method sync; moved `openapi/index.html` → `docs/index.html` for Pages serving.

## [2026-05-26]
Expand Down
9 changes: 4 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,9 @@ tvm_executor = { git = "https://github.com/tvmlabs/tvm-sdk.git", tag = 'v2.24.20
tvm_types = { git = "https://github.com/tvmlabs/tvm-sdk.git", tag = 'v2.24.20.an' }
tvm_vm = { git = "https://github.com/tvmlabs/tvm-sdk.git", tag = 'v2.24.20.an' }
# Public chain primitives (PrivateNote, OrderBook, Pmp, RootPn, tvm_client).
# The write-side facade that wraps these lives in `crates/chain/`; both
# the API (trader path) and `market-manager` (deploy tool) reach for it
# via the `dodex-chain` workspace dep. Kept in-tree because the
# upstream facade `bee-dex` ships from a private SSH-only repo and
# staging / CI hosts have no key for it.
# The write-side facade that wraps these lives in `crates/chain/`; the
# API (trader path) and the e2e tests reach for it via the `dodex-chain`
# workspace dep. Kept in-tree because the upstream facade `bee-dex` ships
# from a private SSH-only repo and staging / CI hosts have no key for it.
ackinacki-kit = { git = "https://github.com/gosh-sh/ackinacki-kit.git", branch = "feature/update_dex", default-features = false, features = ["contracts", "default"] }
dodex-chain = { path = "crates/chain" }
180 changes: 163 additions & 17 deletions crates/application/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,12 @@ pub struct MarketBalancesResolution {
/// directly as `u32` without a secondary cast.
pub token_type: u32,
pub orderbook_address: String,
/// Quote-asset on-chain `decimals` (from `ref_tokens` by `token_type`).
/// Outcome `_stakes` amounts are in atoms at this scale, so display
/// scales by the full `decimals` (like `GetAccountUseCase` / `/account`),
/// NOT by `quantity_precision`, which would over-report by
/// 10^(decimals - quantity_precision).
pub decimals: u8,
/// Number of outcomes for this market. `u32` because outcome counts
/// are non-negative; the Postgres `integer` column is cast at the
/// repo boundary (negative DB values → `MarketInconsistent`).
Expand Down Expand Up @@ -1055,11 +1061,19 @@ where
};
let raw_locked =
sums.get(&outcome.outcome_id).cloned().unwrap_or_else(|| "0".to_string());
// `raw_free`/`raw_locked` are chain atoms at the quote-asset
// `decimals` scale. Scale to the human value by `decimals` (like
// `GetAccountUseCase` / `/api/v1/account`), NOT by
// `quantity_precision` — the latter over-reports by
// 10^(decimals - quantity_precision). A grid-strict descale to the
// `quantity_precision` lattice is wrong here: a `buyFullSet` at the
// market price mints outcome amounts of full-`decimals` precision
// (off the order step grid), so they must be rendered as-is.
rows.push(dodex_domain::OutcomeBalance {
outcome_id: outcome.outcome_id,
symbol: outcome.symbol.clone(),
free: scale_decimal(&raw_free, outcome.quantity_precision)?,
locked_in_orders: scale_decimal(&raw_locked, outcome.quantity_precision)?,
free: scale_decimal(&raw_free, res.decimals)?,
locked_in_orders: scale_decimal(&raw_locked, res.decimals)?,
});
}

Expand Down Expand Up @@ -4896,6 +4910,7 @@ mod balances_port_tests {
oracle_list_hash: "67890".to_string(),
token_type: 1,
orderbook_address: "0:orderbook".to_string(),
decimals: 9,
num_outcomes: 2,
outcomes: vec![
BalanceOutcome {
Expand Down Expand Up @@ -5022,6 +5037,11 @@ mod get_market_balances_use_case_tests {
}

fn make_resolution(num_outcomes: u32) -> MarketBalancesResolution {
// NACKL-scale; outcome stake amounts are atoms (1e9).
make_resolution_with_decimals(num_outcomes, 9)
}

fn make_resolution_with_decimals(num_outcomes: u32, decimals: u8) -> MarketBalancesResolution {
let outcomes: Vec<_> = (0..num_outcomes)
.map(|i| BalanceOutcome {
outcome_id: i,
Expand All @@ -5034,6 +5054,7 @@ mod get_market_balances_use_case_tests {
oracle_list_hash: "2".into(),
token_type: 1,
orderbook_address: "0:ob".into(),
decimals,
num_outcomes,
outcomes,
}
Expand All @@ -5056,14 +5077,16 @@ mod get_market_balances_use_case_tests {

#[tokio::test]
async fn happy_path_sums_three_pools_per_outcome() {
// Atom-scale inputs (NACKL decimals=9, quantity_precision=2). The
// three stake pools are summed in atoms, then scaled by the full decimals.
let stake = PnStake {
amount: vec!["10".into(), "5".into()], // outcome 0=10, 1=5
debt_amount: vec!["0".into(), "1".into()], // 0=0, 1=1
coupons_amount: vec!["2".into(), "0".into()], // 0=2, 1=0
amount: vec!["10000000000".into(), "5000000000".into()], // 0=10, 1=5 NACKL
debt_amount: vec!["0".into(), "1000000000".into()], // 0=0, 1=1
coupons_amount: vec!["2000000000".into(), "0".into()], // 0=2, 1=0
};
let pn = make_pn(Some(stake));
let mut sums = std::collections::HashMap::new();
sums.insert(1u32, "100".into()); // outcome 1 has 100 locked
sums.insert(1u32, "100000000000".into()); // outcome 1: 100 NACKL locked
let repo =
StubRepo { resolution: Mutex::new(Ok(make_resolution(2))), sums: Mutex::new(sums) };
let uc = GetMarketBalancesUseCase::new(pn, repo, stub_hasher);
Expand All @@ -5076,21 +5099,144 @@ mod get_market_balances_use_case_tests {
.await
.expect("ok");
assert_eq!(out.balances.len(), 2);
// outcome 0: free = 10+0+2 = 12, scale=2 → "0.12"; locked=0 → "0.00"
// outcome 0: free = 10+0+2 = 12 tokens; locked=0. Scaled by decimals=9.
assert_eq!(out.balances[0].outcome_id, 0);
assert_eq!(out.balances[0].free, "0.12");
assert_eq!(out.balances[0].locked_in_orders, "0.00");
// outcome 1: free = 5+1+0 = 6, scale=2 → "0.06"; locked=100 → "1.00"
assert_eq!(out.balances[0].free, "12.000000000");
assert_eq!(out.balances[0].locked_in_orders, "0.000000000");
// outcome 1: free = 5+1+0 = 6 tokens; locked=100.
assert_eq!(out.balances[1].outcome_id, 1);
assert_eq!(out.balances[1].free, "0.06");
assert_eq!(out.balances[1].locked_in_orders, "1.00");
assert_eq!(out.balances[1].free, "6.000000000");
assert_eq!(out.balances[1].locked_in_orders, "100.000000000");
}

// Outcome `_stakes.amount` is in token ATOMS (NACKL decimals = 9): a
// holding of 12.5 NACKL is `12_500_000_000` atoms and must render as ~12.5,
// not 125_000_000. Scaling atoms by `quantity_precision` (2) instead of the
// full `decimals` over-reports by 10^(9-2); this anchors the expectation to
// a realistic atom-scale value.
#[tokio::test]
async fn market_balances_free_not_overscaled_for_real_atoms() {
let stake = PnStake {
amount: vec!["12500000000".into(), "0".into()], // 12.5 NACKL atoms on outcome 0
debt_amount: vec!["0".into(), "0".into()],
coupons_amount: vec!["0".into(), "0".into()],
};
let pn = make_pn(Some(stake));
let repo = StubRepo {
resolution: Mutex::new(Ok(make_resolution(2))),
sums: Mutex::new(std::collections::HashMap::new()),
};
let uc = GetMarketBalancesUseCase::new(pn, repo, stub_hasher);
let out = uc
.execute(GetMarketBalancesInput {
pn_address: "0:pn".into(),
market_address: MarketAddress("0:m".into()),
now_ms: 0,
})
.await
.expect("ok");
// Exact: 12_500_000_000 atoms / 10^9 → "12.500000000" (not 125_000_000).
assert_eq!(out.balances[0].free, "12.500000000");
}

// Same over-scale on the `locked_in_orders` column, which is summed from
// `live_orders` (also chain atoms). 20 NACKL locked = `20_000_000_000`
// atoms must render as ~20, not 200_000_000.
#[tokio::test]
async fn market_balances_locked_not_overscaled_for_real_atoms() {
let pn = make_pn(Some(PnStake {
amount: vec!["0".into(), "0".into()],
debt_amount: vec!["0".into(), "0".into()],
coupons_amount: vec!["0".into(), "0".into()],
}));
let mut sums = std::collections::HashMap::new();
sums.insert(0u32, "20000000000".into()); // 20 NACKL atoms locked on outcome 0
let repo =
StubRepo { resolution: Mutex::new(Ok(make_resolution(2))), sums: Mutex::new(sums) };
let uc = GetMarketBalancesUseCase::new(pn, repo, stub_hasher);
let out = uc
.execute(GetMarketBalancesInput {
pn_address: "0:pn".into(),
market_address: MarketAddress("0:m".into()),
now_ms: 0,
})
.await
.expect("ok");
// Exact: 20_000_000_000 atoms / 10^9 → "20.000000000" (not 200_000_000).
assert_eq!(out.balances[0].locked_in_orders, "20.000000000");
}

// Golden-fixture regression (captured live on the local stack): a
// `buyFullSet` at the market price splits collateral into outcome amounts
// of full-`decimals` precision, OFF the quantity_precision grid — ~25 NACKL
// → 11_567_164_168 / 13_432_835_808 atoms. Scaling by `quantity_precision`
// would over-report (→ "115671641.68"); a grid-strict descale to the qp
// lattice would have to drop non-zero low digits. Scaling by the full
// `decimals` renders the exact value, which this pins.
#[tokio::test]
async fn market_balances_renders_off_grid_buy_full_set_stake() {
let stake = PnStake {
amount: vec!["11567164168".into(), "13432835808".into()],
debt_amount: vec!["0".into(), "0".into()],
coupons_amount: vec!["0".into(), "0".into()],
};
let pn = make_pn(Some(stake));
let repo = StubRepo {
resolution: Mutex::new(Ok(make_resolution(2))),
sums: Mutex::new(std::collections::HashMap::new()),
};
let uc = GetMarketBalancesUseCase::new(pn, repo, stub_hasher);
let out = uc
.execute(GetMarketBalancesInput {
pn_address: "0:pn".into(),
market_address: MarketAddress("0:m".into()),
now_ms: 0,
})
.await
.expect("off-grid stake must render, not 503");
assert_eq!(out.balances[0].free, "11.567164168");
assert_eq!(out.balances[1].free, "13.432835808");
}

// A `decimals = 0` quote asset (whole-token quote, atoms ARE tokens) must
// render the count verbatim plus the format-invariant ".0" suffix — no
// fractional digits, no spurious scaling. `scale_decimal` is unit-tested at
// the primitive level; this drives the same scale through the balances use
// case, where `ref_tokens.decimals = 0` is reachable (no schema `CHECK > 0`).
#[tokio::test]
async fn market_balances_renders_zero_decimals_verbatim() {
let stake = PnStake {
amount: vec!["125".into(), "0".into()],
debt_amount: vec!["0".into(), "0".into()],
coupons_amount: vec!["0".into(), "0".into()],
};
let pn = make_pn(Some(stake));
let mut sums = std::collections::HashMap::new();
sums.insert(1u32, "7".into()); // 7 whole tokens locked on outcome 1
let repo = StubRepo {
resolution: Mutex::new(Ok(make_resolution_with_decimals(2, 0))),
sums: Mutex::new(sums),
};
let uc = GetMarketBalancesUseCase::new(pn, repo, stub_hasher);
let out = uc
.execute(GetMarketBalancesInput {
pn_address: "0:pn".into(),
market_address: MarketAddress("0:m".into()),
now_ms: 0,
})
.await
.expect("ok");
assert_eq!(out.balances[0].free, "125.0");
assert_eq!(out.balances[0].locked_in_orders, "0.0");
assert_eq!(out.balances[1].free, "0.0");
assert_eq!(out.balances[1].locked_in_orders, "7.0");
}

#[tokio::test]
async fn missing_stake_key_yields_zero_free() {
let pn = make_pn(None); // simulates absent key
let mut sums = std::collections::HashMap::new();
sums.insert(0u32, "500".into());
sums.insert(0u32, "500000000000".into()); // 500 NACKL atoms locked on outcome 0
let repo =
StubRepo { resolution: Mutex::new(Ok(make_resolution(2))), sums: Mutex::new(sums) };
let uc = GetMarketBalancesUseCase::new(pn, repo, stub_hasher);
Expand All @@ -5103,10 +5249,10 @@ mod get_market_balances_use_case_tests {
.await
.expect("ok");
assert_eq!(out.balances.len(), 2);
assert_eq!(out.balances[0].free, "0.00");
assert_eq!(out.balances[0].locked_in_orders, "5.00");
assert_eq!(out.balances[1].free, "0.00");
assert_eq!(out.balances[1].locked_in_orders, "0.00");
assert_eq!(out.balances[0].free, "0.000000000");
assert_eq!(out.balances[0].locked_in_orders, "500.000000000");
assert_eq!(out.balances[1].free, "0.000000000");
assert_eq!(out.balances[1].locked_in_orders, "0.000000000");
}

#[tokio::test]
Expand Down
14 changes: 7 additions & 7 deletions crates/chain/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ license = "LicenseRef-Acki-Nacki-Node-License"
[features]
default = []
# Enables PMP/RootOracle/OracleEventList deploy + setup entry points
# used by the api e2e tests (under [dev-dependencies]) and by the
# `market-manager` staging tool to spawn ephemeral markets. The prod
# api/infrastructure build does not enable this — these methods are
# not reachable from request handlers and stay out of the binary.
# used by the api e2e tests (under [dev-dependencies]) to spawn
# ephemeral markets. The prod api/infrastructure build does not enable
# this — these methods are not reachable from request handlers and stay
# out of the binary.
test-helpers = []

# Inline rather than `.workspace = true` so this crate can be consumed
# from `services/market-manager` (its own Cargo workspace) and from
# inside the market-manager Docker build, neither of which can reach
# the parent workspace's `[workspace.package]` / `[workspace.dependencies]`.
# standalone from a separate Cargo workspace or Docker build that cannot
# reach the parent workspace's `[workspace.package]` /
# `[workspace.dependencies]`.
[dependencies]
ackinacki-kit = { git = "https://github.com/gosh-sh/ackinacki-kit.git", branch = "feature/update_dex", default-features = false, features = ["contracts", "default"] }
serde = { version = "1.0", features = ["derive"] }
Expand Down
21 changes: 10 additions & 11 deletions crates/chain/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@
//
// Production trader-path methods. Each one constructs the relevant
// kit contract handle on demand and forwards. Deploy/setup helpers and
// extra read-only getters used only by e2e tests and `market-manager`
// live in `test_helpers.rs` behind the `test-helpers` feature so the
// prod build does not carry them.
// extra read-only getters used only by the e2e tests live in
// `test_helpers.rs` behind the `test-helpers` feature so the prod build
// does not carry them.

use std::sync::Arc;

Expand All @@ -25,17 +25,17 @@ use ackinacki_kit::tvm_client::ClientContext;
use super::dto::OwnedOrders;
use super::error::ChainResult;

/// Long-lived TVM client + the typed entry points the API, e2e tests,
/// and market-manager reach for.
/// Long-lived TVM client + the typed entry points the API and e2e tests
/// reach for.
pub struct Dex {
pub(crate) ctx: Arc<ClientContext>,
}

impl Dex {
/// Wrap a caller-owned `ClientContext`. Use this when the caller
/// already has a context (e.g. `market-manager` shares one across
/// its trader + admin paths). For the common case of "just give me
/// a `Dex` for these endpoints", reach for `from_endpoints`.
/// already has a context to share across several paths. For the
/// common case of "just give me a `Dex` for these endpoints", reach
/// for `from_endpoints`.
pub fn new(ctx: Arc<ClientContext>) -> Self {
Self { ctx }
}
Expand Down Expand Up @@ -104,9 +104,8 @@ impl Dex {
/// Buy a full set of outcome tokens by depositing `collateral` of
/// the market's quote asset into the PMP. On a market sitting in
/// `AWAITING_FREEZE`, the first successful call also activates the
/// OrderBook — same chain entry point as the staging market-manager
/// uses to seed initial MM liquidity, but signed by the caller's
/// trading PN. See `docs/tech-specs/write-api.md §POST /api/v1/buyFullSet`.
/// OrderBook, after which it stays active for all subsequent callers.
/// See `docs/tech-specs/write-api.md §POST /api/v1/buyFullSet`.
pub async fn split_full_set(
&self,
pn_address: &str,
Expand Down
12 changes: 6 additions & 6 deletions crates/chain/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@
// (`place_order` / `cancel_order` / `place_batch` / `cancel_batch`)
// plus the order-book read used by e2e cleanup polling.
//
// * `dodex-market-manager` + the api e2e tests — both spawn
// ephemeral PMP + OrderBook setups via the deploy entry points
// (`deploy_pmp`, `submit_set_timings`, `submit_resolve`, ...) and
// pull this crate with `features = ["test-helpers"]`. The prod
// api/infrastructure build leaves the feature off, so deploy
// methods stay out of the request-handling binary.
// * the api e2e tests — spawn ephemeral PMP + OrderBook setups via
// the deploy entry points (`deploy_pmp`, `submit_set_timings`,
// `submit_resolve`, ...) and pull this crate with
// `features = ["test-helpers"]`. The prod api/infrastructure build
// leaves the feature off, so deploy methods stay out of the
// request-handling binary.
//
// Each method instantiates the relevant kit contract handle on demand
// from a shared `Arc<ClientContext>`. No rate-limiter, no retries, no
Expand Down
12 changes: 4 additions & 8 deletions crates/chain/src/test_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,10 @@
//
// Methods compiled only with the `test-helpers` feature — deploy +
// setup entry points plus the read-only PN getters used by e2e tests
// to verify on-chain state. Used by:
//
// * the api crate's e2e integration tests, which spawn an
// ephemeral PMP + OrderBook per run before exercising the
// trader write-path, and poll PN getters to assert on-chain
// effects;
// * `market-manager`, the staging tool that deploys real markets
// on shellnet.
// to verify on-chain state. Used by the api crate's e2e integration
// tests, which spawn an ephemeral PMP + OrderBook per run before
// exercising the trader write-path, and poll PN getters to assert
// on-chain effects.
//
// The prod api/infrastructure build leaves `test-helpers` off so
// `Dex` exposes only the trader-path methods in `client.rs`.
Expand Down
Loading
Loading