use std::sync::Arc;
use r402_core::chain::{ChainId, DeployedTokenAmount};
use r402_core::wire;
use crate::chain::{Address, CasperTokenDeployment};
use crate::exact::{CasperExact, CasperPaymentRequirementsExtra, ExactScheme};
use crate::motes::Motes;
pub const DEFAULT_MAX_TIMEOUT_SECONDS: u64 = 300;
impl CasperExact {
#[allow(
clippy::needless_pass_by_value,
reason = "DeployedTokenAmount is consumed for its fields"
)]
pub fn price_tag<A: Into<Address>>(
pay_to: A,
asset: DeployedTokenAmount<Motes, CasperTokenDeployment>,
) -> wire::PriceTag {
let chain_id: ChainId = asset.token.chain_reference.into();
let extra = CasperPaymentRequirementsExtra::new(asset.token.name, asset.token.version)
.with_decimals(asset.token.decimals);
let requirements = wire::PaymentRequirements::new(
ExactScheme.to_string().into(),
chain_id,
asset.amount.to_string().into(),
pay_to.into().to_string().into(),
asset.token.address.to_string().into(),
DEFAULT_MAX_TIMEOUT_SECONDS,
)
.with_optional_extra(serde_json::to_value(extra).ok());
wire::PriceTag {
requirements,
enricher: Some(Arc::new(casper_fee_payer_enricher_v2)),
}
}
}
pub fn casper_fee_payer_enricher_v2(
price_tag: &mut wire::PriceTag,
capabilities: &wire::SupportedResponse,
) {
let network = price_tag.requirements.network.to_string();
let Some(fee_payer) = capabilities
.kinds
.iter()
.find(|kind| {
wire::V2 == kind.x402_version
&& kind.scheme.as_str() == ExactScheme.as_ref()
&& kind.network.as_str() == network
})
.and_then(|kind| kind.extra.as_ref())
.and_then(|extra| extra.get("feePayer"))
.cloned()
else {
return;
};
match price_tag.requirements.extra.as_mut() {
Some(serde_json::Value::Object(existing)) => {
let _ = existing.insert("feePayer".to_owned(), fee_payer);
}
_ => {
price_tag.requirements.extra = Some(serde_json::json!({ "feePayer": fee_payer }));
}
}
}
#[cfg(test)]
mod tests {
use compact_str::CompactString;
use super::*;
use crate::chain::CasperChainReference;
use crate::networks::WCSPR;
const PAYEE: &str = "00fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321";
fn payee() -> Address {
PAYEE.parse().unwrap()
}
#[test]
fn price_tag_carries_casper_wire_fields() {
let tag = CasperExact::price_tag(payee(), WCSPR::casper_test().amount(1_500_000_000));
assert_eq!(tag.requirements.scheme, "exact");
assert_eq!(tag.requirements.network.to_string(), "casper:casper-test");
assert_eq!(tag.requirements.amount, "1500000000");
assert_eq!(tag.requirements.pay_to, PAYEE);
assert_eq!(
tag.requirements.asset,
WCSPR::casper_test().address.to_string()
);
assert_eq!(
tag.requirements.max_timeout_seconds,
DEFAULT_MAX_TIMEOUT_SECONDS
);
}
#[test]
fn price_tag_declares_the_eip712_domain() {
let tag = CasperExact::price_tag(payee(), WCSPR::casper_test().amount(1));
let extra = tag.requirements.extra.as_ref().unwrap();
assert_eq!(extra["name"], "Wrapped CSPR");
assert_eq!(extra["version"], "1");
assert_eq!(extra["decimals"], "9");
}
#[test]
fn price_tag_amount_is_base_units_not_decimal() {
let tag = CasperExact::price_tag(payee(), WCSPR::casper_test().amount(1_000_000_000));
assert_eq!(
tag.requirements.amount, "1000000000",
"wire amounts must stay in motes"
);
}
fn capabilities(fee_payer: &str) -> wire::SupportedResponse {
let mut supported = wire::SupportedResponse::default();
supported.kinds.push(
wire::SupportedPaymentKind::new(2, "exact", "casper:casper-test")
.with_extra(serde_json::json!({ "feePayer": fee_payer })),
);
supported
}
#[test]
fn enricher_merges_fee_payer_without_dropping_domain() {
let mut tag = CasperExact::price_tag(payee(), WCSPR::casper_test().amount(1));
casper_fee_payer_enricher_v2(&mut tag, &capabilities(PAYEE));
let extra = tag.requirements.extra.as_ref().unwrap();
assert_eq!(extra["feePayer"], PAYEE);
assert_eq!(extra["name"], "Wrapped CSPR");
}
#[test]
fn enricher_ignores_other_networks() {
let mut tag = CasperExact::price_tag(payee(), WCSPR::casper_test().amount(1));
let mut supported = wire::SupportedResponse::default();
supported.kinds.push(
wire::SupportedPaymentKind::new(2, "exact", "casper:casper")
.with_extra(serde_json::json!({ "feePayer": PAYEE })),
);
casper_fee_payer_enricher_v2(&mut tag, &supported);
assert!(
tag.requirements
.extra
.as_ref()
.unwrap()
.get("feePayer")
.is_none(),
"fee payer from a different network must not leak in"
);
}
#[test]
fn enricher_populates_extra_when_absent() {
let mut tag = wire::PriceTag {
requirements: wire::PaymentRequirements::new(
CompactString::from("exact"),
CasperChainReference::CASPER_TEST.into(),
CompactString::from("1"),
CompactString::from(PAYEE),
CompactString::from(WCSPR::casper_test().address.to_string()),
DEFAULT_MAX_TIMEOUT_SECONDS,
),
enricher: None,
};
casper_fee_payer_enricher_v2(&mut tag, &capabilities(PAYEE));
assert_eq!(tag.requirements.extra.as_ref().unwrap()["feePayer"], PAYEE);
}
}