use core::fmt::{Display, Error as FmtError, Formatter};
use core::str::FromStr;
use derive_more::{Display, From};
use ibc_core::host::types::identifiers::{ChannelId, PortId};
use ibc_core::primitives::prelude::*;
#[cfg(feature = "serde")]
use ibc_core::primitives::serializers;
use ibc_proto::ibc::applications::transfer::v1::DenomTrace as RawDenomTrace;
use super::error::TokenTransferError;
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(transparent))]
#[cfg_attr(
feature = "parity-scale-codec",
derive(
parity_scale_codec::Encode,
parity_scale_codec::Decode,
scale_info::TypeInfo
)
)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, Display)]
pub struct BaseDenom(String);
impl BaseDenom {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl FromStr for BaseDenom {
type Err = TokenTransferError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.trim().is_empty() {
Err(TokenTransferError::EmptyBaseDenom)
} else {
Ok(BaseDenom(s.to_owned()))
}
}
}
#[cfg_attr(
feature = "parity-scale-codec",
derive(
parity_scale_codec::Encode,
parity_scale_codec::Decode,
scale_info::TypeInfo
)
)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Ord, PartialOrd, Eq, PartialEq)]
pub struct TracePrefix {
port_id: PortId,
channel_id: ChannelId,
}
impl TracePrefix {
pub fn new(port_id: PortId, channel_id: ChannelId) -> Self {
Self {
port_id,
channel_id,
}
}
pub fn strip(s: &str) -> Option<(Self, Option<&str>)> {
let (port_id_s, remaining) = s.split_once('/')?;
let (channel_id_s, remaining) = remaining
.split_once('/')
.map(|(a, b)| (a, Some(b)))
.unwrap_or_else(|| (remaining, None));
let port_id = port_id_s.parse().ok()?;
let channel_id = channel_id_s.parse().ok()?;
Some((Self::new(port_id, channel_id), remaining))
}
}
impl Display for TracePrefix {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
write!(f, "{}/{}", self.port_id, self.channel_id)
}
}
#[cfg_attr(
feature = "parity-scale-codec",
derive(
parity_scale_codec::Encode,
parity_scale_codec::Decode,
scale_info::TypeInfo
)
)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord, From)]
pub struct TracePath(Vec<TracePrefix>);
impl TracePath {
pub fn starts_with(&self, prefix: &TracePrefix) -> bool {
self.0.last().map(|p| p == prefix).unwrap_or(false)
}
pub fn remove_prefix(&mut self, prefix: &TracePrefix) {
if self.starts_with(prefix) {
self.0.pop();
}
}
pub fn add_prefix(&mut self, prefix: TracePrefix) {
self.0.push(prefix)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn empty() -> Self {
Self(vec![])
}
pub fn trim(s: &str) -> (Self, Option<&str>) {
let mut trace_prefixes = vec![];
let mut current_remaining_opt = Some(s);
loop {
let Some(current_remaining_s) = current_remaining_opt else {
break;
};
let Some((trace_prefix, next_remaining_opt)) = TracePrefix::strip(current_remaining_s)
else {
break;
};
trace_prefixes.push(trace_prefix);
current_remaining_opt = next_remaining_opt;
}
trace_prefixes.reverse();
(Self(trace_prefixes), current_remaining_opt)
}
}
impl FromStr for TracePath {
type Err = TokenTransferError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s.is_empty() {
return Ok(TracePath::empty());
}
let (trace_path, remaining_parts) = TracePath::trim(s);
remaining_parts
.is_none()
.then_some(trace_path)
.ok_or_else(|| TokenTransferError::MalformedTrace(s.to_string()))
}
}
impl Display for TracePath {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
let path = self
.0
.iter()
.rev()
.map(|prefix| prefix.to_string())
.collect::<Vec<String>>()
.join("/");
write!(f, "{path}")
}
}
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
#[cfg_attr(
feature = "parity-scale-codec",
derive(
parity_scale_codec::Encode,
parity_scale_codec::Decode,
scale_info::TypeInfo
)
)]
#[cfg_attr(
feature = "borsh",
derive(borsh::BorshSerialize, borsh::BorshDeserialize)
)]
#[derive(Clone, Debug, Eq, PartialEq, PartialOrd, Ord)]
pub struct PrefixedDenom {
#[cfg_attr(feature = "serde", serde(with = "serializers"))]
#[cfg_attr(feature = "schema", schemars(with = "String"))]
pub trace_path: TracePath,
pub base_denom: BaseDenom,
}
impl PrefixedDenom {
pub fn remove_trace_prefix(&mut self, prefix: &TracePrefix) {
self.trace_path.remove_prefix(prefix)
}
pub fn add_trace_prefix(&mut self, prefix: TracePrefix) {
self.trace_path.add_prefix(prefix)
}
}
pub fn is_sender_chain_source(
source_port: PortId,
source_channel: ChannelId,
denom: &PrefixedDenom,
) -> bool {
!is_receiver_chain_source(source_port, source_channel, denom)
}
pub fn is_receiver_chain_source(
source_port: PortId,
source_channel: ChannelId,
denom: &PrefixedDenom,
) -> bool {
let prefix = TracePrefix::new(source_port, source_channel);
denom.trace_path.starts_with(&prefix)
}
impl FromStr for PrefixedDenom {
type Err = TokenTransferError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match TracePath::trim(s) {
(trace_path, Some(remaining_parts)) => Ok(Self {
trace_path,
base_denom: BaseDenom::from_str(remaining_parts)?,
}),
(_, None) => Ok(Self {
trace_path: TracePath::empty(),
base_denom: BaseDenom::from_str(s)?,
}),
}
}
}
impl TryFrom<RawDenomTrace> for PrefixedDenom {
type Error = TokenTransferError;
fn try_from(value: RawDenomTrace) -> Result<Self, Self::Error> {
let base_denom = BaseDenom::from_str(&value.base_denom)?;
let trace_path = TracePath::from_str(&value.path)?;
Ok(Self {
trace_path,
base_denom,
})
}
}
impl From<PrefixedDenom> for RawDenomTrace {
fn from(value: PrefixedDenom) -> Self {
Self {
path: value.trace_path.to_string(),
base_denom: value.base_denom.to_string(),
}
}
}
impl From<BaseDenom> for PrefixedDenom {
fn from(denom: BaseDenom) -> Self {
Self {
trace_path: TracePath::empty(),
base_denom: denom,
}
}
}
impl Display for PrefixedDenom {
fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
if self.trace_path.0.is_empty() {
write!(f, "{}", self.base_denom)
} else {
write!(f, "{}/{}", self.trace_path, self.base_denom)
}
}
}
#[cfg(test)]
mod tests {
use rstest::rstest;
use super::*;
#[rstest]
#[case("transfer")]
#[case("transfer/channel-1/ica")]
fn test_invalid_raw_demon_trace_parsing(#[case] trace_path: &str) {
let raw_denom_trace = RawDenomTrace {
path: trace_path.to_string(),
base_denom: "uatom".to_string(),
};
PrefixedDenom::try_from(raw_denom_trace).expect_err("failure");
}
#[rstest]
#[case("uatom")]
#[case("atom")]
fn test_accepted_denom(#[case] denom_str: &str) {
BaseDenom::from_str(denom_str).expect("success");
}
#[rstest]
#[case("")]
#[case(" ")]
fn test_rejected_denom(#[case] denom_str: &str) {
BaseDenom::from_str(denom_str).expect_err("failure");
}
#[rstest]
#[case(
"transfer/channel-75",
"factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust"
)]
#[case(
"transfer/channel-75/transfer/channel-123/transfer/channel-1023/transfer/channel-0",
"factory/stars16da2uus9zrsy83h23ur42v3lglg5rmyrpqnju4/dust"
)]
#[case(
"transfer/channel-75/transfer/channel-123/transfer/channel-1023/transfer/channel-0",
"//////////////////////dust"
)]
#[case("transfer/channel-0", "uatom")]
#[case("transfer/channel-0/transfer/channel-1", "uatom")]
#[case("", "/")]
#[case("", "transfer/uatom")]
#[case("", "transfer/atom")]
#[case("", "transfer//uatom")]
#[case("", "/uatom")]
#[case("", "//uatom")]
#[case("", "transfer/")]
#[case("", "(transfer)/channel-0/uatom")]
#[case("", "transfer/(channel-0)/uatom")]
#[case("", "uatom")]
#[case("", "uatom/")]
#[case("", "gamm/pool/1")]
#[case("", "gamm//pool//1")]
#[case("transfer/channel-1", "uatom")]
#[case("customtransfer/channel-1", "uatom")]
#[case("transfer/channel-1", "uatom/")]
#[case(
"transfer/channel-1",
"erc20/0x85bcBCd7e79Ec36f4fBBDc54F90C643d921151AA"
)]
#[case("transfer/channel-1", "gamm/pool/1")]
#[case("transfer/channel-1", "gamm//pool//1")]
#[case("transfer/channel-1/transfer/channel-2", "uatom")]
#[case("customtransfer/channel-1/alternativetransfer/channel-2", "uatom")]
#[case("", "transfer/uatom")]
#[case("", "transfer//uatom")]
#[case("", "channel-1/transfer/uatom")]
#[case("", "uatom/transfer")]
#[case("", "transfer/channel-1")]
#[case("transfer/channel-1", "transfer")]
#[case("", "transfer/channelToA/uatom")]
fn test_strange_but_accepted_prefixed_denom(
#[case] prefix: &str,
#[case] denom: &str,
) -> Result<(), TokenTransferError> {
let pd_s = if prefix.is_empty() {
denom.to_owned()
} else {
format!("{prefix}/{denom}")
};
let pd = PrefixedDenom::from_str(&pd_s)?;
assert_eq!(pd.to_string(), pd_s);
assert_eq!(pd.trace_path.to_string(), prefix);
assert_eq!(pd.base_denom.to_string(), denom);
Ok(())
}
#[rstest]
#[case("")]
#[case(" ")]
#[case("transfer/channel-1/")]
#[case("transfer/channel-1/transfer/channel-2/")]
#[case("transfer/channel-21/transfer/channel-23/ ")]
#[case("transfer/channel-0/")]
#[should_panic(expected = "EmptyBaseDenom")]
fn test_prefixed_empty_base_denom(#[case] pd_s: &str) {
PrefixedDenom::from_str(pd_s).expect("error");
}
#[rstest]
fn test_trace_path_order() {
let mut prefixed_denom =
PrefixedDenom::from_str("customtransfer/channel-1/alternativetransfer/channel-2/uatom")
.expect("no error");
assert_eq!(
prefixed_denom.trace_path.to_string(),
"customtransfer/channel-1/alternativetransfer/channel-2"
);
assert_eq!(prefixed_denom.base_denom.to_string(), "uatom");
let trace_prefix_1 = TracePrefix::new(
"alternativetransfer".parse().unwrap(),
"channel-2".parse().unwrap(),
);
let trace_prefix_2 = TracePrefix::new(
"customtransfer".parse().unwrap(),
"channel-1".parse().unwrap(),
);
let trace_prefix_3 =
TracePrefix::new("transferv2".parse().unwrap(), "channel-10".parse().unwrap());
let trace_prefix_4 = TracePrefix::new(
"transferv3".parse().unwrap(),
"channel-101".parse().unwrap(),
);
prefixed_denom.trace_path.add_prefix(trace_prefix_3.clone());
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_1));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_2));
assert!(prefixed_denom.trace_path.starts_with(&trace_prefix_3));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_4));
assert_eq!(
prefixed_denom.to_string(),
"transferv2/channel-10/customtransfer/channel-1/alternativetransfer/channel-2/uatom"
);
prefixed_denom.trace_path.remove_prefix(&trace_prefix_4);
assert!(!prefixed_denom.trace_path.is_empty());
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_1));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_2));
assert!(prefixed_denom.trace_path.starts_with(&trace_prefix_3));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_4));
assert_eq!(
prefixed_denom.to_string(),
"transferv2/channel-10/customtransfer/channel-1/alternativetransfer/channel-2/uatom"
);
prefixed_denom.trace_path.remove_prefix(&trace_prefix_3);
assert!(!prefixed_denom.trace_path.is_empty());
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_1));
assert!(prefixed_denom.trace_path.starts_with(&trace_prefix_2));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_3));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_4));
assert_eq!(
prefixed_denom.to_string(),
"customtransfer/channel-1/alternativetransfer/channel-2/uatom"
);
prefixed_denom.trace_path.remove_prefix(&trace_prefix_2);
assert!(!prefixed_denom.trace_path.is_empty());
assert!(prefixed_denom.trace_path.starts_with(&trace_prefix_1));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_2));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_3));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_4));
assert_eq!(
prefixed_denom.to_string(),
"alternativetransfer/channel-2/uatom"
);
prefixed_denom.trace_path.remove_prefix(&trace_prefix_1);
assert!(prefixed_denom.trace_path.is_empty());
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_1));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_2));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_3));
assert!(!prefixed_denom.trace_path.starts_with(&trace_prefix_4));
assert_eq!(prefixed_denom.to_string(), "uatom");
}
#[rstest]
#[case("", TracePath::empty(), Some(""))]
#[case("transfer", TracePath::empty(), Some("transfer"))]
#[case("transfer/", TracePath::empty(), Some("transfer/"))]
#[case("transfer/channel-1", TracePath::from(vec![TracePrefix::new("transfer".parse().unwrap(), ChannelId::new(1))]), None)]
#[case("transfer/channel-1/", TracePath::from(vec![TracePrefix::new("transfer".parse().unwrap(), ChannelId::new(1))]), Some(""))]
#[case("transfer/channel-1/uatom", TracePath::from(vec![TracePrefix::new("transfer".parse().unwrap(), ChannelId::new(1))]), Some("uatom"))]
#[case("transfer/channel-1/uatom/", TracePath::from(vec![TracePrefix::new("transfer".parse().unwrap(), ChannelId::new(1))]), Some("uatom/"))]
fn test_trace_path_cases(
#[case] trace_path_s: &str,
#[case] trace_path: TracePath,
#[case] remaining: Option<&str>,
) {
let (parsed_trace_path, parsed_remaining) = TracePath::trim(trace_path_s);
assert_eq!(parsed_trace_path, trace_path);
assert_eq!(parsed_remaining, remaining);
}
#[test]
fn test_trace_path() -> Result<(), TokenTransferError> {
assert!(TracePath::from_str("").is_ok(), "empty trace path");
assert!(
TracePath::from_str("transfer/uatom").is_err(),
"invalid trace path: bad ChannelId"
);
assert!(
TracePath::from_str("transfer//uatom").is_err(),
"malformed trace path: missing ChannelId"
);
assert!(
TracePath::from_str("transfer/channel-0/").is_err(),
"malformed trace path: trailing delimiter"
);
let prefix_1 = TracePrefix::new("transfer".parse().unwrap(), "channel-1".parse().unwrap());
let prefix_2 = TracePrefix::new("transfer".parse().unwrap(), "channel-0".parse().unwrap());
let mut trace_path = TracePath(vec![prefix_1.clone()]);
trace_path.add_prefix(prefix_2.clone());
assert_eq!(
TracePath::from_str("transfer/channel-0/transfer/channel-1")?,
trace_path
);
assert_eq!(
TracePath(vec![prefix_1.clone(), prefix_2.clone()]),
trace_path
);
trace_path.remove_prefix(&prefix_2);
assert_eq!(TracePath::from_str("transfer/channel-1")?, trace_path);
assert_eq!(TracePath(vec![prefix_1.clone()]), trace_path);
trace_path.remove_prefix(&prefix_1);
assert!(trace_path.is_empty());
Ok(())
}
}