feat: on-chain context for each pause in quoting, and pool settings kept
Each of the newest pauses now says what happened on-chain around QTC while it ran, so a reader can tell "not quoting but still trading" from "offline": - pool swaps by known solvers, with the QTC they sold into and bought out of the pool; - direct pool swaps by anyone else; - 1Click fills, which a pause should have none of; - bridge deposits arriving on NEAR; - the largest solver QTC holding at the pause's start and end, from the probe's own readings; - changes to the pool's state or whitelist. That last needs data that was not kept: the pool's state and whitelist were read every minute and held only in memory. ref_pool_settings (migration 0023) records them when either changes, and only a row with a predecessor counts as a change. The first row is how the pool was first seen. Context is computed for the 12 newest pauses, a few aggregate queries each. It is counts and amounts only, and the page says it does not explain why quotes stopped. Refs #45 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015tyzzRVjH9AdwGtYwsH5VC
This commit is contained in:
+44
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n select trader = any($5) as \"solver!\", side,\n count(*) as \"n!\",\n coalesce(sum(case when side = 'sell' then amount_in else amount_out end), 0)::text\n as \"asset!\"\n from ref_swap\n where chain = $1 and asset = $2 and at between $3 and $4\n group by 1, 2\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "solver!",
|
||||
"type_info": "Bool"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "side",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "n!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "asset!",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "12be2703a83db80a597fdc663118cec20c949efcb11c9bd66caeb796d0e1dee4"
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n select count(*) as \"n!\", coalesce(sum(amount), 0)::text as \"total!\"\n from bridge_mint where chain = $1 and at between $2 and $3\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "n!",
|
||||
"type_info": "Int8"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "total!",
|
||||
"type_info": "Text"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "221f8d14ab56268c724b145bba2e7d7cc0d123f3eed1377d066cb7809b3717ba"
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n insert into ref_pool_settings (chain, pool_id, at, state, whitelist)\n select $1, $2, $3, $4, $5\n where not exists (\n select 1 from (\n select state, whitelist from ref_pool_settings\n where chain = $1 and pool_id = $2\n order by at desc limit 1\n ) last\n where last.state = $4 and last.whitelist = $5\n )\n on conflict do nothing\n ",
|
||||
"describe": {
|
||||
"columns": [],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Text",
|
||||
"Jsonb"
|
||||
]
|
||||
},
|
||||
"nullable": []
|
||||
},
|
||||
"hash": "c8100168daaac6edbe235ed6b0bbb2d8f37cb64203a870913ecaa6066f9766e3"
|
||||
}
|
||||
+26
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n select count(*) as \"n!\" from intents_transfer\n where chain = $1 and asset = $2 and at between $3 and $4\n and (from_account = any($5) or to_account = any($5))\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "n!",
|
||||
"type_info": "Int8"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz",
|
||||
"TextArray"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "ea0415a16bad88feb5dceb8acd3b92fa05e13ee2cc8d289d88279fc01bfc4a13"
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"db_name": "PostgreSQL",
|
||||
"query": "\n select at as \"at!\", pool_id as \"pool_id!\", state as \"state!\",\n whitelist as \"whitelist!\", prev_state as \"prev_state!\",\n prev_whitelist as \"prev_whitelist!\"\n from (\n select *, lag(state) over w as prev_state, lag(whitelist) over w as prev_whitelist\n from ref_pool_settings\n where chain = $1\n window w as (partition by pool_id order by at)\n ) s\n where at between $2 and $3 and prev_state is not null\n order by at\n ",
|
||||
"describe": {
|
||||
"columns": [
|
||||
{
|
||||
"ordinal": 0,
|
||||
"name": "at!",
|
||||
"type_info": "Timestamptz"
|
||||
},
|
||||
{
|
||||
"ordinal": 1,
|
||||
"name": "pool_id!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 2,
|
||||
"name": "state!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 3,
|
||||
"name": "whitelist!",
|
||||
"type_info": "Jsonb"
|
||||
},
|
||||
{
|
||||
"ordinal": 4,
|
||||
"name": "prev_state!",
|
||||
"type_info": "Text"
|
||||
},
|
||||
{
|
||||
"ordinal": 5,
|
||||
"name": "prev_whitelist!",
|
||||
"type_info": "Jsonb"
|
||||
}
|
||||
],
|
||||
"parameters": {
|
||||
"Left": [
|
||||
"Text",
|
||||
"Timestamptz",
|
||||
"Timestamptz"
|
||||
]
|
||||
},
|
||||
"nullable": [
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
null,
|
||||
null
|
||||
]
|
||||
},
|
||||
"hash": "f4517ce12291b0574331092fbd3459a9037cf365f877324bc22f80087fa62e9a"
|
||||
}
|
||||
@@ -371,6 +371,24 @@ async fn record_history(
|
||||
if let Err(e) = store.record_pool_snapshot(&snapshot).await {
|
||||
tracing::warn!(%pool_id, error = %e, "pool snapshot not recorded");
|
||||
}
|
||||
// Its state and whitelist, kept only when they change (#45): whether the
|
||||
// pool was running and who could swap in it is the context a pause in
|
||||
// quoting needs, and it was held in memory alone.
|
||||
let mut whitelist: Vec<String> =
|
||||
serde_json::from_value(state["whitelist"].clone()).unwrap_or_default();
|
||||
whitelist.sort();
|
||||
if let Err(e) = store
|
||||
.record_pool_settings(
|
||||
chain,
|
||||
pool_id,
|
||||
at,
|
||||
state["state"].as_str().unwrap_or_default(),
|
||||
&serde_json::json!(whitelist),
|
||||
)
|
||||
.await
|
||||
{
|
||||
tracing::warn!(%pool_id, error = %e, "pool settings not recorded");
|
||||
}
|
||||
// Sorted by point so equal books compare equal in the store.
|
||||
let mut levels: Vec<Value> = order_book
|
||||
.as_object()
|
||||
|
||||
@@ -5181,7 +5181,8 @@ async fn intents_quotes(
|
||||
.quote_probes(&id, &config.asset, lookback)
|
||||
.await
|
||||
.map_err(database_unavailable)?;
|
||||
let pauses = quote_pauses(&state, &config, &all_rows, since).await;
|
||||
let solvers = crate::intents::solver_set(&runtime, &state.store).await;
|
||||
let pauses = quote_pauses(&state, &id, &config, &solvers, &all_rows, since).await;
|
||||
let rows: Vec<blackbeard_data::store::QuoteProbeRow> =
|
||||
all_rows.into_iter().filter(|r| r.at >= since).collect();
|
||||
let days = state
|
||||
@@ -5277,7 +5278,9 @@ async fn intents_quotes(
|
||||
/// depend on which sizes happened to be asked.
|
||||
async fn quote_pauses(
|
||||
state: &AppState,
|
||||
chain: &blackbeard_entities::ChainId,
|
||||
config: &crate::config::IntentsConfig,
|
||||
solvers: &[String],
|
||||
rows: &[blackbeard_data::store::QuoteProbeRow],
|
||||
since: chrono::DateTime<chrono::Utc>,
|
||||
) -> Vec<blackbeard_entities::QuotePause> {
|
||||
@@ -5377,14 +5380,81 @@ async fn quote_pauses(
|
||||
price_end: nearest(&price, end_ms, NEAR_MS),
|
||||
price_1h: nearest(&price, end_ms + 3_600_000, NEAR_MS),
|
||||
price_4h: nearest(&price, end_ms + 4 * 3_600_000, 2 * NEAR_MS),
|
||||
context: None,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
out.sort_by_key(|p| std::cmp::Reverse(p.start));
|
||||
// Context for the newest: a handful of aggregate queries each, which a
|
||||
// page of a dozen pauses can afford and a page of hundreds cannot.
|
||||
for p in out.iter_mut().take(PAUSE_CONTEXTS) {
|
||||
let to = p.resumed_at.unwrap_or(p.last);
|
||||
let Ok(c) = state
|
||||
.store
|
||||
.pause_context(chain, &config.asset, solvers, p.start, to)
|
||||
.await
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
// The largest holder of the asset, from the probes' own readings: the
|
||||
// buy probes, whose output token is the asset.
|
||||
let held: Vec<&blackbeard_data::store::QuoteProbeRow> = rows
|
||||
.iter()
|
||||
.filter(|r| r.direction == "buy" && r.at >= p.start && r.at <= to)
|
||||
.filter(|r| r.best_held.is_some())
|
||||
.collect();
|
||||
let first = held.first();
|
||||
let last = held.last();
|
||||
p.context = Some(blackbeard_entities::QuotePauseContext {
|
||||
solver_swaps: c.solver_swaps.max(0) as u32,
|
||||
solver_sold: BigUintDec(c.solver_sold),
|
||||
solver_bought: BigUintDec(c.solver_bought),
|
||||
other_swaps: c.other_swaps.max(0) as u32,
|
||||
other_sold: BigUintDec(c.other_sold),
|
||||
other_bought: BigUintDec(c.other_bought),
|
||||
fills: c.fills.max(0) as u32,
|
||||
minted_count: c.minted_count.max(0) as u32,
|
||||
minted: BigUintDec(c.minted),
|
||||
holder_start: first.and_then(|r| r.best_solver.clone()),
|
||||
held_start: first.and_then(|r| r.best_held.clone()).map(BigUintDec),
|
||||
holder_end: last.and_then(|r| r.best_solver.clone()),
|
||||
held_end: last.and_then(|r| r.best_held.clone()).map(BigUintDec),
|
||||
pool_changes: c
|
||||
.settings
|
||||
.into_iter()
|
||||
.map(|ch| {
|
||||
let list = |v: &serde_json::Value| -> Vec<String> {
|
||||
serde_json::from_value(v.clone()).unwrap_or_default()
|
||||
};
|
||||
let (before, after) = (list(&ch.whitelist.0), list(&ch.whitelist.1));
|
||||
let changed = ch.state.0 != ch.state.1;
|
||||
blackbeard_entities::PoolChange {
|
||||
at: ch.at,
|
||||
pool_id: ch.pool_id,
|
||||
state_from: changed.then(|| ch.state.0.clone()),
|
||||
state_to: changed.then(|| ch.state.1.clone()),
|
||||
whitelisted: after
|
||||
.iter()
|
||||
.filter(|a| !before.contains(a))
|
||||
.cloned()
|
||||
.collect(),
|
||||
delisted: before
|
||||
.iter()
|
||||
.filter(|a| !after.contains(a))
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Pauses given on-chain context, newest first.
|
||||
const PAUSE_CONTEXTS: usize = 12;
|
||||
|
||||
/// `GET /v1/chains/{chain}/tx/{hash}`: one extrinsic by its hash
|
||||
/// (blackbeard/observer#48).
|
||||
///
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
-- A Ref DCL pool's state and swap whitelist, kept when they change.
|
||||
-- blackbeard/observer#45.
|
||||
--
|
||||
-- Both are read every minute (refdcl::pools) and were only ever held in memory,
|
||||
-- so a pool paused, or a solver dropped from its whitelist, during a stretch
|
||||
-- with no quotes would leave no trace afterwards. That is exactly the context
|
||||
-- a pause needs: "the pool stopped" and "the quotes stopped while the pool ran"
|
||||
-- are different events. A row is written only when either differs from the
|
||||
-- latest one for the pool, so the table is a list of changes, and the first
|
||||
-- row is the state as first seen.
|
||||
create table ref_pool_settings (
|
||||
chain text not null references chain (id) on delete cascade,
|
||||
pool_id text not null,
|
||||
at timestamptz not null,
|
||||
state text not null,
|
||||
-- Sorted, so equal whitelists compare equal.
|
||||
whitelist jsonb not null,
|
||||
primary key (chain, pool_id, at)
|
||||
);
|
||||
@@ -4746,3 +4746,180 @@ impl Store {
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// One change to a pool's settings.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SettingsChange {
|
||||
/// When it was first seen.
|
||||
pub at: DateTime<Utc>,
|
||||
/// Which pool.
|
||||
pub pool_id: String,
|
||||
/// State before and after.
|
||||
pub state: (String, String),
|
||||
/// Whitelist before and after.
|
||||
pub whitelist: (serde_json::Value, serde_json::Value),
|
||||
}
|
||||
|
||||
/// What happened on-chain around the asset during one stretch of time, for
|
||||
/// the context of a pause in quoting (blackbeard/observer#45).
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct PauseContextRow {
|
||||
/// Pool swaps by known solvers: count, asset sold into the pool, asset
|
||||
/// bought out of it (base units).
|
||||
pub solver_swaps: i64,
|
||||
/// Asset solvers sold into the pool.
|
||||
pub solver_sold: String,
|
||||
/// Asset solvers bought out of it.
|
||||
pub solver_bought: String,
|
||||
/// Pool swaps by anyone else, calling the pool directly.
|
||||
pub other_swaps: i64,
|
||||
/// Asset others sold into the pool.
|
||||
pub other_sold: String,
|
||||
/// Asset others bought out of it.
|
||||
pub other_bought: String,
|
||||
/// Transfers of the asset inside intents.near with a solver on one side:
|
||||
/// 1Click fills.
|
||||
pub fills: i64,
|
||||
/// Bridge mints: QTC arriving on NEAR.
|
||||
pub minted_count: i64,
|
||||
/// How much.
|
||||
pub minted: String,
|
||||
/// Pool settings that changed in the stretch: when, which pool, and the
|
||||
/// state and whitelist before and after. A pool's first row is how it was
|
||||
/// first seen, not a change, and is never one of these.
|
||||
pub settings: Vec<SettingsChange>,
|
||||
}
|
||||
|
||||
impl Store {
|
||||
/// Keep a pool's state and whitelist if either changed since the last row.
|
||||
pub async fn record_pool_settings(
|
||||
&self,
|
||||
chain: &ChainId,
|
||||
pool_id: &str,
|
||||
at: DateTime<Utc>,
|
||||
state: &str,
|
||||
whitelist: &serde_json::Value,
|
||||
) -> Result<(), DataError> {
|
||||
sqlx::query!(
|
||||
r#"
|
||||
insert into ref_pool_settings (chain, pool_id, at, state, whitelist)
|
||||
select $1, $2, $3, $4, $5
|
||||
where not exists (
|
||||
select 1 from (
|
||||
select state, whitelist from ref_pool_settings
|
||||
where chain = $1 and pool_id = $2
|
||||
order by at desc limit 1
|
||||
) last
|
||||
where last.state = $4 and last.whitelist = $5
|
||||
)
|
||||
on conflict do nothing
|
||||
"#,
|
||||
chain.as_str(),
|
||||
pool_id,
|
||||
at,
|
||||
state,
|
||||
whitelist,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// What happened around the asset between `from` and `to`.
|
||||
pub async fn pause_context(
|
||||
&self,
|
||||
chain: &ChainId,
|
||||
asset: &str,
|
||||
solvers: &[String],
|
||||
from: DateTime<Utc>,
|
||||
to: DateTime<Utc>,
|
||||
) -> Result<PauseContextRow, DataError> {
|
||||
let mut out = PauseContextRow::default();
|
||||
let swaps = sqlx::query!(
|
||||
r#"
|
||||
select trader = any($5) as "solver!", side,
|
||||
count(*) as "n!",
|
||||
coalesce(sum(case when side = 'sell' then amount_in else amount_out end), 0)::text
|
||||
as "asset!"
|
||||
from ref_swap
|
||||
where chain = $1 and asset = $2 and at between $3 and $4
|
||||
group by 1, 2
|
||||
"#,
|
||||
chain.as_str(),
|
||||
asset,
|
||||
from,
|
||||
to,
|
||||
solvers,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?;
|
||||
out.solver_sold = "0".into();
|
||||
out.solver_bought = "0".into();
|
||||
out.other_sold = "0".into();
|
||||
out.other_bought = "0".into();
|
||||
for r in swaps {
|
||||
match (r.solver, r.side.as_str()) {
|
||||
(true, "sell") => (out.solver_swaps += r.n, out.solver_sold = r.asset),
|
||||
(true, _) => (out.solver_swaps += r.n, out.solver_bought = r.asset),
|
||||
(false, "sell") => (out.other_swaps += r.n, out.other_sold = r.asset),
|
||||
(false, _) => (out.other_swaps += r.n, out.other_bought = r.asset),
|
||||
};
|
||||
}
|
||||
out.fills = sqlx::query_scalar!(
|
||||
r#"
|
||||
select count(*) as "n!" from intents_transfer
|
||||
where chain = $1 and asset = $2 and at between $3 and $4
|
||||
and (from_account = any($5) or to_account = any($5))
|
||||
"#,
|
||||
chain.as_str(),
|
||||
asset,
|
||||
from,
|
||||
to,
|
||||
solvers,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
let minted = sqlx::query!(
|
||||
r#"
|
||||
select count(*) as "n!", coalesce(sum(amount), 0)::text as "total!"
|
||||
from bridge_mint where chain = $1 and at between $2 and $3
|
||||
"#,
|
||||
chain.as_str(),
|
||||
from,
|
||||
to,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await?;
|
||||
out.minted_count = minted.n;
|
||||
out.minted = minted.total;
|
||||
out.settings = sqlx::query!(
|
||||
r#"
|
||||
select at as "at!", pool_id as "pool_id!", state as "state!",
|
||||
whitelist as "whitelist!", prev_state as "prev_state!",
|
||||
prev_whitelist as "prev_whitelist!"
|
||||
from (
|
||||
select *, lag(state) over w as prev_state, lag(whitelist) over w as prev_whitelist
|
||||
from ref_pool_settings
|
||||
where chain = $1
|
||||
window w as (partition by pool_id order by at)
|
||||
) s
|
||||
where at between $2 and $3 and prev_state is not null
|
||||
order by at
|
||||
"#,
|
||||
chain.as_str(),
|
||||
from,
|
||||
to,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|r| SettingsChange {
|
||||
at: r.at,
|
||||
pool_id: r.pool_id,
|
||||
state: (r.prev_state, r.state),
|
||||
whitelist: (r.prev_whitelist, r.whitelist),
|
||||
})
|
||||
.collect();
|
||||
Ok(out)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -57,8 +57,8 @@ pub use miner::{
|
||||
};
|
||||
pub use near::{
|
||||
IntentsAccount, IntentsAccountEvent, IntentsBalance, NearAction, NearAmount, NearEvent,
|
||||
NearMemo, NearPart, NearReceipt, NearToken, NearTx, NearTxSource, QuantusTxRef,
|
||||
QuoteAvailability, QuoteDay, QuotePause, QuoteProbe, QuoteProbes,
|
||||
NearMemo, NearPart, NearReceipt, NearToken, NearTx, NearTxSource, PoolChange, QuantusTxRef,
|
||||
QuoteAvailability, QuoteDay, QuotePause, QuotePauseContext, QuoteProbe, QuoteProbes,
|
||||
};
|
||||
pub use reversible::{PendingTransfer, ReversibleState};
|
||||
pub use roles::{AccountRoleEntry, ChainRoles, GenesisDetail, NamedAccount, RoleSource};
|
||||
|
||||
@@ -462,6 +462,66 @@ pub struct QuotePause {
|
||||
pub price_1h: Option<f64>,
|
||||
/// Four hours after the end.
|
||||
pub price_4h: Option<f64>,
|
||||
/// What happened on-chain around the asset during it. Computed for the
|
||||
/// newest pauses only.
|
||||
pub context: Option<QuotePauseContext>,
|
||||
}
|
||||
|
||||
/// What happened on-chain around the asset during a pause in quoting: whether
|
||||
/// solvers were still trading, whether QTC kept arriving to sell, whether the
|
||||
/// pool itself changed. None of it says why quotes stopped; all of it is what
|
||||
/// a reader needs before guessing.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export, export_to = "QuotePauseContext.ts")]
|
||||
pub struct QuotePauseContext {
|
||||
/// Pool swaps by known solvers.
|
||||
pub solver_swaps: u32,
|
||||
/// Asset solvers sold into the pool, base units.
|
||||
pub solver_sold: BigUintDec,
|
||||
/// Asset solvers bought out of the pool.
|
||||
pub solver_bought: BigUintDec,
|
||||
/// Pool swaps by anyone else, calling the pool directly.
|
||||
pub other_swaps: u32,
|
||||
/// Asset they sold into it.
|
||||
pub other_sold: BigUintDec,
|
||||
/// Asset they bought out of it.
|
||||
pub other_bought: BigUintDec,
|
||||
/// Transfers of the asset inside intents.near with a solver on one side:
|
||||
/// fills, which a pause in quoting should have none of.
|
||||
pub fills: u32,
|
||||
/// Bridge mints in the pause: QTC arriving on NEAR.
|
||||
pub minted_count: u32,
|
||||
/// How much, base units.
|
||||
pub minted: BigUintDec,
|
||||
/// The solver holding the most of the asset at the pause's first probe,
|
||||
/// and how much.
|
||||
pub holder_start: Option<String>,
|
||||
/// What it held then.
|
||||
pub held_start: Option<BigUintDec>,
|
||||
/// The largest holder at the pause's last probe.
|
||||
pub holder_end: Option<String>,
|
||||
/// What it held then.
|
||||
pub held_end: Option<BigUintDec>,
|
||||
/// Changes to the pool's state or whitelist during it.
|
||||
pub pool_changes: Vec<PoolChange>,
|
||||
}
|
||||
|
||||
/// One change to a pool's settings.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
|
||||
#[ts(export, export_to = "PoolChange.ts")]
|
||||
pub struct PoolChange {
|
||||
/// When it was first seen.
|
||||
pub at: DateTime<Utc>,
|
||||
/// Which pool.
|
||||
pub pool_id: String,
|
||||
/// The state before, when it changed.
|
||||
pub state_from: Option<String>,
|
||||
/// The state after, when it changed.
|
||||
pub state_to: Option<String>,
|
||||
/// Accounts added to the swap whitelist.
|
||||
pub whitelisted: Vec<String>,
|
||||
/// Accounts removed from it.
|
||||
pub delisted: Vec<String>,
|
||||
}
|
||||
|
||||
/// One UTC day of probes for one direction and counter.
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
/**
|
||||
* One change to a pool's settings.
|
||||
*/
|
||||
export type PoolChange = {
|
||||
/**
|
||||
* When it was first seen.
|
||||
*/
|
||||
at: string,
|
||||
/**
|
||||
* Which pool.
|
||||
*/
|
||||
pool_id: string,
|
||||
/**
|
||||
* The state before, when it changed.
|
||||
*/
|
||||
state_from: string | null,
|
||||
/**
|
||||
* The state after, when it changed.
|
||||
*/
|
||||
state_to: string | null,
|
||||
/**
|
||||
* Accounts added to the swap whitelist.
|
||||
*/
|
||||
whitelisted: Array<string>,
|
||||
/**
|
||||
* Accounts removed from it.
|
||||
*/
|
||||
delisted: Array<string>, };
|
||||
@@ -1,4 +1,5 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { QuotePauseContext } from "./QuotePauseContext";
|
||||
|
||||
/**
|
||||
* A run of consecutive unquoted minutes in one direction and pair.
|
||||
@@ -67,4 +68,9 @@ price_1h: number | null,
|
||||
/**
|
||||
* Four hours after the end.
|
||||
*/
|
||||
price_4h: number | null, };
|
||||
price_4h: number | null,
|
||||
/**
|
||||
* What happened on-chain around the asset during it. Computed for the
|
||||
* newest pauses only.
|
||||
*/
|
||||
context: QuotePauseContext | null, };
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { BigUintDec } from "./BigUintDec";
|
||||
import type { PoolChange } from "./PoolChange";
|
||||
|
||||
/**
|
||||
* What happened on-chain around the asset during a pause in quoting: whether
|
||||
* solvers were still trading, whether QTC kept arriving to sell, whether the
|
||||
* pool itself changed. None of it says why quotes stopped; all of it is what
|
||||
* a reader needs before guessing.
|
||||
*/
|
||||
export type QuotePauseContext = {
|
||||
/**
|
||||
* Pool swaps by known solvers.
|
||||
*/
|
||||
solver_swaps: number,
|
||||
/**
|
||||
* Asset solvers sold into the pool, base units.
|
||||
*/
|
||||
solver_sold: BigUintDec,
|
||||
/**
|
||||
* Asset solvers bought out of the pool.
|
||||
*/
|
||||
solver_bought: BigUintDec,
|
||||
/**
|
||||
* Pool swaps by anyone else, calling the pool directly.
|
||||
*/
|
||||
other_swaps: number,
|
||||
/**
|
||||
* Asset they sold into it.
|
||||
*/
|
||||
other_sold: BigUintDec,
|
||||
/**
|
||||
* Asset they bought out of it.
|
||||
*/
|
||||
other_bought: BigUintDec,
|
||||
/**
|
||||
* Transfers of the asset inside intents.near with a solver on one side:
|
||||
* fills, which a pause in quoting should have none of.
|
||||
*/
|
||||
fills: number,
|
||||
/**
|
||||
* Bridge mints in the pause: QTC arriving on NEAR.
|
||||
*/
|
||||
minted_count: number,
|
||||
/**
|
||||
* How much, base units.
|
||||
*/
|
||||
minted: BigUintDec,
|
||||
/**
|
||||
* The solver holding the most of the asset at the pause's first probe,
|
||||
* and how much.
|
||||
*/
|
||||
holder_start: string | null,
|
||||
/**
|
||||
* What it held then.
|
||||
*/
|
||||
held_start: BigUintDec | null,
|
||||
/**
|
||||
* The largest holder at the pause's last probe.
|
||||
*/
|
||||
holder_end: string | null,
|
||||
/**
|
||||
* What it held then.
|
||||
*/
|
||||
held_end: BigUintDec | null,
|
||||
/**
|
||||
* Changes to the pool's state or whitelist during it.
|
||||
*/
|
||||
pool_changes: Array<PoolChange>, };
|
||||
@@ -299,11 +299,12 @@ export function QuotePanel({ chain, symbol: chainSymbol }: { chain: string; symb
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.pauses.slice(0, 12).map((p) => (
|
||||
<PauseRow
|
||||
<PauseRows
|
||||
key={`${p.direction}-${p.counter}-${p.start}`}
|
||||
p={p}
|
||||
label={label}
|
||||
symbol={symbol}
|
||||
decimals={meta(data.asset)?.decimals ?? 12}
|
||||
/>
|
||||
))}
|
||||
</tbody>
|
||||
@@ -532,14 +533,16 @@ function span(p: QuotePause): string {
|
||||
return `${h} h ${minutes % 60} min`
|
||||
}
|
||||
|
||||
function PauseRow({
|
||||
function PauseRows({
|
||||
p,
|
||||
label,
|
||||
symbol,
|
||||
decimals,
|
||||
}: {
|
||||
p: QuotePause
|
||||
label: (token: string) => string
|
||||
symbol: string
|
||||
decimals: number
|
||||
}) {
|
||||
const until =
|
||||
p.end === 'resumed' && p.resumed_at
|
||||
@@ -551,35 +554,85 @@ function PauseRow({
|
||||
.map((v) => (v === null ? '—' : fmtPrice(v)))
|
||||
.join(' → ')
|
||||
return (
|
||||
<tr>
|
||||
<td className="left">
|
||||
{p.direction === 'sell' ? 'selling' : 'buying'} {symbol} · {label(p.counter)}
|
||||
</td>
|
||||
<td className="left numeral event-when" title={utc(p.start)}>
|
||||
{p.began_before ? 'by ' : ''}
|
||||
{when(p.start)}
|
||||
</td>
|
||||
<td
|
||||
className="left numeral event-when"
|
||||
title={p.resumed_at ? utc(p.resumed_at) : utc(p.last)}
|
||||
>
|
||||
{until}
|
||||
</td>
|
||||
<td className="numeral">
|
||||
{p.began_before ? '≥ ' : ''}
|
||||
{span(p)}
|
||||
</td>
|
||||
<td className="numeral">
|
||||
{p.solver_able} of {p.unquoted} min
|
||||
{p.unanswered > 0 && <span className="receipt-meta"> · {p.unanswered} unanswered</span>}
|
||||
</td>
|
||||
<td className="numeral" title="at the start → at the end → an hour later → four hours later">
|
||||
{prices}
|
||||
</td>
|
||||
</tr>
|
||||
<>
|
||||
<tr>
|
||||
<td className="left">
|
||||
{p.direction === 'sell' ? 'selling' : 'buying'} {symbol} · {label(p.counter)}
|
||||
</td>
|
||||
<td className="left numeral event-when" title={utc(p.start)}>
|
||||
{p.began_before ? 'by ' : ''}
|
||||
{when(p.start)}
|
||||
</td>
|
||||
<td
|
||||
className="left numeral event-when"
|
||||
title={p.resumed_at ? utc(p.resumed_at) : utc(p.last)}
|
||||
>
|
||||
{until}
|
||||
</td>
|
||||
<td className="numeral">
|
||||
{p.began_before ? '≥ ' : ''}
|
||||
{span(p)}
|
||||
</td>
|
||||
<td className="numeral">
|
||||
{p.solver_able} of {p.unquoted} min
|
||||
{p.unanswered > 0 && <span className="receipt-meta"> · {p.unanswered} unanswered</span>}
|
||||
</td>
|
||||
<td
|
||||
className="numeral"
|
||||
title="at the start → at the end → an hour later → four hours later"
|
||||
>
|
||||
{prices}
|
||||
</td>
|
||||
</tr>
|
||||
{p.context && (
|
||||
<tr className="pause-context">
|
||||
<td colSpan={6} className="left receipt-meta">
|
||||
{contextText(p, symbol, decimals)}
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* What happened on-chain during a pause, in a sentence. Counts and amounts
|
||||
* only: none of it says why the quotes stopped.
|
||||
*/
|
||||
function contextText(p: QuotePause, symbol: string, decimals: number): string {
|
||||
const c = p.context
|
||||
if (!c) return ''
|
||||
const q = (raw: string) => `${fmtTokens(raw, decimals)} ${symbol}`
|
||||
const parts: string[] = []
|
||||
parts.push(
|
||||
c.solver_swaps > 0
|
||||
? `solvers swapped in the pool ${c.solver_swaps} times (sold ${q(c.solver_sold)}, bought ${q(c.solver_bought)})`
|
||||
: 'no solver swapped in the pool',
|
||||
)
|
||||
if (c.other_swaps > 0)
|
||||
parts.push(
|
||||
`others swapped with the pool directly ${c.other_swaps} times (sold ${q(c.other_sold)}, bought ${q(c.other_bought)})`,
|
||||
)
|
||||
parts.push(c.fills > 0 ? `${c.fills} fills through 1Click` : 'no fills through 1Click')
|
||||
if (c.minted_count > 0)
|
||||
parts.push(`${c.minted_count} bridge deposits brought ${q(c.minted)} to NEAR`)
|
||||
if (c.holder_start && c.held_start && c.held_end)
|
||||
parts.push(
|
||||
`the largest ${symbol} holder among solvers, ${shortNear(c.holder_start)}, held ${q(c.held_start)}${
|
||||
c.holder_end === c.holder_start ? ` → ${q(c.held_end)}` : ''
|
||||
}`,
|
||||
)
|
||||
for (const ch of c.pool_changes) {
|
||||
const bits = [
|
||||
ch.state_from && ch.state_to ? `state ${ch.state_from} → ${ch.state_to}` : null,
|
||||
ch.whitelisted.length ? `whitelisted ${ch.whitelisted.map(shortNear).join(', ')}` : null,
|
||||
ch.delisted.length ? `removed from whitelist ${ch.delisted.map(shortNear).join(', ')}` : null,
|
||||
].filter(Boolean)
|
||||
if (bits.length) parts.push(`pool ${when(ch.at)}: ${bits.join('; ')}`)
|
||||
}
|
||||
return `During it: ${parts.join('; ')}.`
|
||||
}
|
||||
|
||||
/**
|
||||
* Buy availability against sell availability, per counter: the gap the
|
||||
* launch evening made plain, when buying was quoted and selling never was.
|
||||
|
||||
@@ -2282,3 +2282,11 @@ tr.mine .share-bar > i {
|
||||
order: 10;
|
||||
}
|
||||
}
|
||||
|
||||
/* A pause's on-chain context, under its row: part of the same record, so no
|
||||
rule between them. */
|
||||
.pause-context td {
|
||||
border-top: 0;
|
||||
padding-top: 0;
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user