1use std::sync::{
7 Arc,
8 atomic::{AtomicU16, Ordering},
9};
10
11use alloy::primitives::{Address, U256};
12use alloy::providers::Provider;
13use anyhow::Context;
14use decdn_common::redact::sanitize_err_chain;
15use decdn_incentive::payment_pool::{FeeRouter, PaymentPool};
16
17use crate::chain_events::boot_retry::BootRetry;
18use crate::chain_events::timed;
19
20const BPS_DENOMINATOR: u64 = 10_000;
21
22#[derive(Clone, Debug)]
24pub struct OperatorShares {
25 bps: Arc<AtomicU16>,
26}
27
28impl OperatorShares {
29 #[must_use]
31 pub fn new(bps: u16) -> Self {
32 Self {
33 bps: Arc::new(AtomicU16::new(bps)),
34 }
35 }
36
37 #[must_use]
39 pub fn bps(&self) -> u16 {
40 self.bps.load(Ordering::Relaxed)
41 }
42
43 pub fn store(&self, bps: u16) -> u16 {
47 self.bps.swap(bps, Ordering::Relaxed)
48 }
49}
50
51pub fn operator_bps_from_shares(shares: [U256; 3], addr: &str) -> anyhow::Result<u16> {
57 let operator = shares.first().copied().unwrap_or(U256::ZERO);
58 let raw = u64::try_from(operator).map_err(|_| {
59 anyhow::anyhow!("FeeRouter operator share {operator} at {addr} exceeds u64")
60 })?;
61 anyhow::ensure!(
62 raw <= BPS_DENOMINATOR,
63 "FeeRouter operator share {raw} at {addr} exceeds {BPS_DENOMINATOR} bps"
64 );
65 u16::try_from(raw)
67 .map_err(|_| anyhow::anyhow!("FeeRouter operator share {raw} at {addr} exceeds u16"))
68}
69
70#[derive(Debug, PartialEq, Eq)]
73pub(crate) struct FeeShareSeed {
74 pub(crate) router: Option<Address>,
79 pub(crate) operator_bps: u16,
81}
82
83pub(crate) async fn seed_from_chain<P: Provider + Clone>(
96 provider: P,
97 payment_pool_addr: Address,
98 floor_bps: u16,
99 boot: &BootRetry,
100) -> FeeShareSeed {
101 let pool = PaymentPool::new(payment_pool_addr, provider.clone());
102 let router = match boot
103 .run("PaymentPool.feeRouter()", || async {
104 timed(None, "PaymentPool.feeRouter()", pool.feeRouter().call())
105 .await
106 .with_context(|| format!("PaymentPool.feeRouter() at {payment_pool_addr}"))
107 })
108 .await
109 {
110 Ok(router) => router,
111 Err(err) => {
112 tracing::warn!(
113 error = %sanitize_err_chain(&err),
114 fallback_bps = floor_bps,
115 "PaymentPool.feeRouter() startup read failed; operator fee share seeded to the \
116 FeeRouter OPERATOR_BPS_FLOOR and the fee-shares watcher is not registered"
117 );
118 return FeeShareSeed {
119 router: None,
120 operator_bps: floor_bps,
121 };
122 }
123 };
124
125 let operator_bps = read_operator_bps(&FeeRouter::new(router, provider), floor_bps, boot).await;
126 FeeShareSeed {
127 router: Some(router),
128 operator_bps,
129 }
130}
131
132async fn read_operator_bps<P: Provider + Clone>(
135 fee_router: &FeeRouter::FeeRouterInstance<P>,
136 floor_bps: u16,
137 boot: &BootRetry,
138) -> u16 {
139 let router = *fee_router.address();
140 match boot
141 .run("FeeRouter.getShares()", || async {
142 timed(None, "FeeRouter.getShares()", fee_router.getShares().call())
143 .await
144 .with_context(|| format!("FeeRouter.getShares() at {router}"))
145 })
146 .await
147 {
148 Ok(shares) => match operator_bps_from_shares(shares, &router.to_string()) {
149 Ok(bps) => bps,
150 Err(err) => {
151 tracing::warn!(
152 error = %sanitize_err_chain(&err),
153 fallback_bps = floor_bps,
154 "FeeRouter.getShares() startup read could not be narrowed to operator bps; \
155 falling back to OPERATOR_BPS_FLOOR"
156 );
157 floor_bps
158 }
159 },
160 Err(err) => {
161 tracing::warn!(
162 error = %sanitize_err_chain(&err),
163 fallback_bps = floor_bps,
164 "FeeRouter.getShares() startup read failed; falling back to OPERATOR_BPS_FLOOR"
165 );
166 floor_bps
167 }
168 }
169}
170
171#[cfg(test)]
172#[allow(clippy::unwrap_used)]
173mod tests;