use serde::{Deserialize, Serialize};
use crate::method::ControlMethod;
use crate::results;
use crate::traits::ControlCall;
macro_rules! control_call {
($ty:ty => $method:expr, $out:ty) => {
impl ControlCall for $ty {
const METHOD: ControlMethod = $method;
type Output = $out;
}
};
}
macro_rules! no_params {
($(#[$doc:meta])* $name:ident => $method:expr, $out:ty) => {
$(#[$doc])*
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct $name {}
control_call!($name => $method, $out);
};
}
no_params!(
StatusParams => ControlMethod::Status, results::StatusResult
);
no_params!(
ConfigGetParams => ControlMethod::ConfigGet, results::ConfigResult
);
no_params!(
CacheGetParams => ControlMethod::CacheGet, results::CacheView
);
no_params!(
CacheClearParams => ControlMethod::CacheClear, results::CacheClearResult
);
no_params!(
HostedStoresListParams => ControlMethod::HostedStoresList, results::HostedStoresListResult
);
no_params!(
SyncStatusParams => ControlMethod::SyncStatus, results::SyncStatusResult
);
no_params!(
UpdaterStatusParams => ControlMethod::UpdaterStatus, serde_json::Value
);
no_params!(
UpdaterResumeParams => ControlMethod::UpdaterResume, serde_json::Value
);
no_params!(
UpdaterCheckNowParams => ControlMethod::UpdaterCheckNow, serde_json::Value
);
no_params!(
PairingListParams => ControlMethod::PairingList, serde_json::Value
);
no_params!(
PeerStatusParams => ControlMethod::PeerStatus, serde_json::Value
);
no_params!(
ListSubscriptionsParams => ControlMethod::ListSubscriptions, results::ListSubscriptionsResult
);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetUpstreamParams {
pub upstream: String,
}
control_call!(SetUpstreamParams => ControlMethod::ConfigSetUpstream, results::SetUpstreamResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetLevelParams {
pub filter: String,
}
control_call!(SetLevelParams => ControlMethod::LogSetLevel, results::SetLevelResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetCapParams {
pub cap_bytes: u64,
}
control_call!(SetCapParams => ControlMethod::CacheSetCap, results::SetCapResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PinParams {
pub store: String,
}
control_call!(PinParams => ControlMethod::HostedStoresPin, results::PinResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnpinParams {
pub store: String,
}
control_call!(UnpinParams => ControlMethod::HostedStoresUnpin, results::UnpinResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct HostedStoreStatusParams {
pub store: String,
}
control_call!(HostedStoreStatusParams => ControlMethod::HostedStoresStatus, results::HostedStoreStatusResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncTriggerParams {
pub store: String,
}
control_call!(SyncTriggerParams => ControlMethod::SyncTrigger, results::SyncTriggerResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SetChannelParams {
pub channel: String,
}
control_call!(SetChannelParams => ControlMethod::UpdaterSetChannel, serde_json::Value);
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PauseParams {
#[serde(skip_serializing_if = "Option::is_none", default)]
pub until: Option<u64>,
}
control_call!(PauseParams => ControlMethod::UpdaterPause, serde_json::Value);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ApproveParams {
pub pairing_id: String,
}
control_call!(ApproveParams => ControlMethod::PairingApprove, results::PairingApproveResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RevokeParams {
pub token_id: String,
}
control_call!(RevokeParams => ControlMethod::PairingRevoke, results::PairingRevokeResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeersConnectParams {
pub peer: String,
}
control_call!(PeersConnectParams => ControlMethod::PeersConnect, results::PeersConnectResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeersDisconnectParams {
pub peer: String,
}
control_call!(PeersDisconnectParams => ControlMethod::PeersDisconnect, results::PeersDisconnectResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SubscribeParams {
pub store_id: String,
}
control_call!(SubscribeParams => ControlMethod::Subscribe, results::SubscribeResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct UnsubscribeParams {
pub store_id: String,
}
control_call!(UnsubscribeParams => ControlMethod::Unsubscribe, results::UnsubscribeResult);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Asset {
Xch,
Dig,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalletBalanceParams {
pub address: String,
pub asset: Asset,
}
control_call!(WalletBalanceParams => ControlMethod::WalletBalance, results::WalletBalanceResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalletCoinsParams {
pub address: String,
pub asset: Asset,
}
control_call!(WalletCoinsParams => ControlMethod::WalletCoins, results::WalletCoinsResult);
const COIN_ID_HEX_LEN: usize = 64;
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct WalletCoinByIdParams {
pub coin_id: String,
}
control_call!(WalletCoinByIdParams => ControlMethod::WalletCoinById, results::WalletCoinByIdResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct WalletCoinSpendParams {
pub coin_id: String,
}
control_call!(WalletCoinSpendParams => ControlMethod::WalletCoinSpend, results::WalletCoinSpendResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct WalletCoinsByParentParams {
pub parent_coin_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub after_coin_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
control_call!(WalletCoinsByParentParams => ControlMethod::WalletCoinsByParent, results::WalletCoinsByParentResult);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub struct WalletArrivalsParams {
#[serde(default)]
pub after_seq: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub limit: Option<u32>,
}
control_call!(WalletArrivalsParams => ControlMethod::WalletArrivals, results::WalletArrivalsResult);
fn normalize_coin_id(coin_id: &str) -> Option<&str> {
let normalized = coin_id.strip_prefix("0x").unwrap_or(coin_id);
let well_formed = normalized.len() == COIN_ID_HEX_LEN
&& normalized
.bytes()
.all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b));
well_formed.then_some(normalized)
}
macro_rules! coin_id_params {
($ty:ident, $field:ident, $raw:ident, $error:expr) => {
impl<'de> Deserialize<'de> for $ty {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct $raw {
$field: String,
}
let raw = $raw::deserialize(deserializer)?;
let $field = normalize_coin_id(&raw.$field)
.ok_or_else(|| serde::de::Error::custom($error))?
.to_owned();
Ok(Self { $field })
}
}
impl $ty {
pub fn validated(self) -> Result<Self, crate::error::ControlError> {
let normalized = normalize_coin_id(&self.$field).ok_or_else(|| {
crate::error::ControlError::of(
crate::error::ControlErrorCode::InvalidParams,
$error,
)
})?;
Ok($ty {
$field: normalized.to_owned(),
})
}
}
};
}
const COIN_ID_ERROR: &str = "coin_id must be lowercase 64-hex, optionally 0x-prefixed";
const PARENT_COIN_ID_ERROR: &str =
"parent_coin_id must be lowercase 64-hex, optionally 0x-prefixed";
const AFTER_COIN_ID_ERROR: &str = "after_coin_id must be lowercase 64-hex, optionally 0x-prefixed";
coin_id_params!(
WalletCoinByIdParams,
coin_id,
RawWalletCoinByIdParams,
COIN_ID_ERROR
);
coin_id_params!(
WalletCoinSpendParams,
coin_id,
RawWalletCoinSpendParams,
COIN_ID_ERROR
);
pub const COINS_BY_PARENT_DEFAULT_LIMIT: u32 = 100;
pub const COINS_BY_PARENT_MAX_LIMIT: u32 = 1_000;
const COINS_BY_PARENT_LIMIT_ERROR: &str = "limit must be between 1 and 1000";
impl<'de> Deserialize<'de> for WalletCoinsByParentParams {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
struct RawWalletCoinsByParentParams {
parent_coin_id: String,
#[serde(default)]
after_coin_id: Option<String>,
#[serde(default)]
limit: Option<u32>,
}
let raw = RawWalletCoinsByParentParams::deserialize(deserializer)?;
let parent_coin_id = normalize_coin_id(&raw.parent_coin_id)
.ok_or_else(|| serde::de::Error::custom(PARENT_COIN_ID_ERROR))?
.to_owned();
let after_coin_id = raw
.after_coin_id
.map(|id| {
normalize_coin_id(&id)
.map(str::to_owned)
.ok_or_else(|| serde::de::Error::custom(AFTER_COIN_ID_ERROR))
})
.transpose()?;
if !raw.limit.map_or(true, is_legal_page) {
return Err(serde::de::Error::custom(COINS_BY_PARENT_LIMIT_ERROR));
}
Ok(Self {
parent_coin_id,
after_coin_id,
limit: raw.limit,
})
}
}
fn is_legal_page(limit: u32) -> bool {
(1..=COINS_BY_PARENT_MAX_LIMIT).contains(&limit)
}
impl WalletCoinsByParentParams {
pub fn first_page(parent_coin_id: impl Into<String>) -> Self {
Self {
parent_coin_id: parent_coin_id.into(),
after_coin_id: None,
limit: None,
}
}
pub fn effective_limit(&self) -> u32 {
self.limit.unwrap_or(COINS_BY_PARENT_DEFAULT_LIMIT)
}
pub fn validated(self) -> Result<Self, crate::error::ControlError> {
fn invalid(message: &'static str) -> crate::error::ControlError {
crate::error::ControlError::of(crate::error::ControlErrorCode::InvalidParams, message)
}
let parent_coin_id = normalize_coin_id(&self.parent_coin_id)
.ok_or_else(|| invalid(PARENT_COIN_ID_ERROR))?
.to_owned();
let after_coin_id = self
.after_coin_id
.as_deref()
.map(|id| {
normalize_coin_id(id)
.map(str::to_owned)
.ok_or_else(|| invalid(AFTER_COIN_ID_ERROR))
})
.transpose()?;
if !self.limit.map_or(true, is_legal_page) {
return Err(invalid(COINS_BY_PARENT_LIMIT_ERROR));
}
Ok(WalletCoinsByParentParams {
parent_coin_id,
after_coin_id,
limit: self.limit,
})
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalletPeakParams {}
control_call!(WalletPeakParams => ControlMethod::WalletPeak, results::WalletPeakResult);
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct PeerCountsParams {}
control_call!(PeerCountsParams => ControlMethod::PeerCounts, results::PeerCountsResult);
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalletSyncStatusParams {}
control_call!(WalletSyncStatusParams => ControlMethod::WalletSyncStatus, results::WalletSyncStatusResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct WalletBroadcastParams {
pub signed_bundle_hex: String,
}
control_call!(WalletBroadcastParams => ControlMethod::WalletBroadcast, results::WalletBroadcastResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct RequestParams {
pub client_name: String,
}
control_call!(RequestParams => ControlMethod::PairingRequest, results::PairingRequestResult);
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct PollParams {
pub pairing_id: String,
}
control_call!(PollParams => ControlMethod::PairingPoll, results::PairingPollResult);
#[cfg(test)]
mod tests {
use super::*;
use crate::traits::build_request;
use serde_json::json;
#[test]
fn no_param_call_serializes_params_to_empty_object() {
let req = build_request(1.into(), &StatusParams {});
assert_eq!(req.method, "control.status");
assert_eq!(req.params, json!({}));
}
#[test]
fn data_param_call_carries_its_fields() {
let req = build_request(2.into(), &SetCapParams { cap_bytes: 128 });
assert_eq!(req.method, "control.cache.setCap");
assert_eq!(req.params, json!({ "cap_bytes": 128 }));
}
#[test]
fn pause_omits_until_when_indefinite() {
assert_eq!(
serde_json::to_value(PauseParams { until: None }).unwrap(),
json!({})
);
assert_eq!(
serde_json::to_value(PauseParams { until: Some(99) }).unwrap(),
json!({ "until": 99 })
);
}
#[test]
fn method_binding_matches_the_catalog_name() {
assert_eq!(
SetUpstreamParams::METHOD.name(),
"control.config.setUpstream"
);
assert_eq!(RequestParams::METHOD.name(), "pairing.request");
assert_eq!(PollParams::METHOD.name(), "pairing.poll");
}
}