use std::path::{Path, PathBuf};
use anyhow::Context as _;
use decdn_client::session::ChainAdoption;
use decdn_incentive::buyer_pool_redb::RedbBuyerPoolStore;
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) enum BuyerStoreOwner {
Client {
data_dir: PathBuf,
},
Node {
data_dir: PathBuf,
marker: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DataDirSource {
Flag,
Config,
Default,
}
pub(crate) fn node_buyer_db(data_dir: &Path) -> PathBuf {
data_dir.join(decdn_common::data_dir::NODE_BUYER_DB_FILE)
}
pub(crate) fn client_buyer_db(data_dir: &Path) -> PathBuf {
data_dir.join(decdn_common::data_dir::CLIENT_BUYER_DB_FILE)
}
pub(crate) fn chain_adoption_for_buy(
data_dir: &Path,
keystore: &Path,
) -> anyhow::Result<ChainAdoption> {
let node_owned = |dir: &Path| -> anyhow::Result<bool> {
Ok(matches!(
classify_buyer_store(dir)?,
BuyerStoreOwner::Node { .. }
))
};
let key_dir = keystore.parent().unwrap_or(Path::new(""));
Ok(if node_owned(data_dir)? || node_owned(key_dir)? {
ChainAdoption::Refused
} else {
ChainAdoption::Allowed
})
}
pub(crate) fn classify_buyer_store(data_dir: &Path) -> anyhow::Result<BuyerStoreOwner> {
let marker = decdn_common::data_dir::daemon_marker(data_dir).with_context(|| {
format!(
"cannot tell whether {} belongs to a decdn-node daemon",
data_dir.display()
)
})?;
Ok(match marker {
Some(marker) => BuyerStoreOwner::Node {
data_dir: data_dir.to_path_buf(),
marker,
},
None => BuyerStoreOwner::Client {
data_dir: data_dir.to_path_buf(),
},
})
}
impl BuyerStoreOwner {
pub(crate) fn refuse_escrow(&self, verb: &str) -> anyhow::Result<()> {
let Self::Node { data_dir, marker } = self else {
return Ok(());
};
anyhow::bail!(
"refusing to {verb}: {} belongs to a decdn-node daemon (it holds {marker}), and \
`decdn pool` writes a separate client store ({}) the daemon never reads. The \
escrowed USDC would be invisible to the node, which would then open a second pool \
of its own. The daemon manages its own pool — it opens one at first miss and tops \
it up from blockchain.buyer_working_deposit_micro_usdc. Run `decdn node pools` to \
see what it holds ({} is its copy), or pass --data-dir <client dir> to act as a \
separate buyer.",
data_dir.display(),
client_buyer_db(data_dir).display(),
node_buyer_db(data_dir).display(),
)
}
pub(crate) fn refuse_sweep(&self, verb: &str) -> anyhow::Result<()> {
let Self::Node { data_dir, .. } = self else {
return Ok(());
};
anyhow::bail!(
"refusing to {verb} --all: this data dir belongs to a decdn-node daemon ({}), and \
--all enumerates every pool this keystore owns ON CHAIN — including the one the \
daemon is paying from right now. Run `decdn node pools` to see which pool that is, \
then {verb} the stranded ones individually with --pool <poolId>.",
node_buyer_db(data_dir).display(),
)
}
pub(crate) fn refuse_implicit_node_dir(
&self,
verb: &str,
source: DataDirSource,
) -> anyhow::Result<()> {
let Self::Node { data_dir, marker } = self else {
return Ok(());
};
if source == DataDirSource::Flag {
return Ok(());
}
let named = match source {
DataDirSource::Flag => return Ok(()),
DataDirSource::Config => "identity.data_dir in the config file",
DataDirSource::Default => "the default client data dir",
};
anyhow::bail!(
"refusing to {verb}: {} came from {named}, and it belongs to a decdn-node daemon (it \
holds {marker}). Buying from here opens a client pool ({}) under the node's own \
operator keystore, beside the daemon's own store ({}) — the daemon would report \
that deposit as stranded, and could adopt it and sign a second voucher series on \
one lane. Pass --data-dir <client dir> to buy as a separate client, or pass \
--data-dir {} to say you really mean the node's dir.",
data_dir.display(),
client_buyer_db(data_dir).display(),
node_buyer_db(data_dir).display(),
data_dir.display(),
)
}
pub(crate) fn open_for_write(&self) -> anyhow::Result<Option<RedbBuyerPoolStore>> {
match self {
Self::Client { data_dir } => Ok(Some(RedbBuyerPoolStore::open(data_dir)?)),
Self::Node { .. } => Ok(None),
}
}
}
pub(crate) fn open_client_store_for_escrow(
data_dir: &Path,
verb: &str,
) -> anyhow::Result<RedbBuyerPoolStore> {
let owner = classify_buyer_store(data_dir)?;
owner.refuse_escrow(verb)?;
owner
.open_for_write()?
.ok_or_else(|| anyhow::anyhow!("a node data dir has no writable client store"))
}
pub(crate) fn open_client_store_for_buy(
data_dir: &Path,
source: DataDirSource,
verb: &str,
) -> anyhow::Result<RedbBuyerPoolStore> {
let owner = classify_buyer_store(data_dir)?;
owner.refuse_implicit_node_dir(verb, source)?;
match owner.open_for_write()? {
Some(store) => Ok(store),
None => Ok(RedbBuyerPoolStore::open(data_dir)?),
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] mod tests;