Skip to main content

cast/cmd/tip20/
logo.rs

1use crate::tx::{SendTxOpts, TxParams};
2use alloy_ens::NameOrAddress;
3use foundry_cli::utils::LoadConfig;
4use foundry_common::{
5    provider::ProviderBuilder,
6    tempo::{Tip20LogoUriValidationError, validate_tip20_logo_uri},
7};
8use tempo_alloy::TempoNetwork;
9
10pub(super) fn check(logo_uri: String) -> eyre::Result<()> {
11    validate_logo_uri(&logo_uri)?;
12    sh_println!("Valid TIP-20 logo URI")?;
13    Ok(())
14}
15
16pub(super) async fn set(
17    token: NameOrAddress,
18    logo_uri: String,
19    send_tx: SendTxOpts,
20    tx_opts: TxParams,
21) -> eyre::Result<()> {
22    validate_logo_uri(&logo_uri)?;
23
24    let (signer, tempo_access_key) = super::resolve_tip20_signer(&send_tx, &tx_opts).await?;
25
26    let config = send_tx.eth.rpc.load_config()?;
27    let provider = ProviderBuilder::<TempoNetwork>::from_config(&config)?.build()?;
28    let token_addr = token.resolve(&provider).await?;
29
30    super::send_tip20_transaction(
31        NameOrAddress::Address(token_addr),
32        "setLogoURI(string)",
33        vec![logo_uri],
34        send_tx,
35        tx_opts,
36        signer,
37        tempo_access_key,
38    )
39    .await?;
40
41    Ok(())
42}
43
44pub(super) fn validate_logo_uri(logo_uri: &str) -> eyre::Result<()> {
45    validate_tip20_logo_uri(logo_uri).map_err(|err| match err {
46        Tip20LogoUriValidationError::LogoURITooLong => {
47            eyre::eyre!(
48                "client-side validation failed: LogoURITooLong: logo URI exceeds 256 bytes"
49            )
50        }
51        Tip20LogoUriValidationError::InvalidLogoURI => {
52            eyre::eyre!(
53                "client-side validation failed: InvalidLogoURI: logo URI must use one of: https, http, ipfs, data"
54            )
55        }
56    })
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use alloy_primitives::bytes;
63    use alloy_sol_types::SolCall;
64    use foundry_common::tempo::TIP20_MAX_LOGO_URI_BYTES;
65    use tempo_contracts::precompiles::ITIP20;
66
67    #[test]
68    fn validates_empty_and_allowed_schemes_case_insensitively() {
69        for uri in [
70            "",
71            "https://example.com/logo.png",
72            "HTTP://example.com/logo.png",
73            "ipfs://bafybeigdyrzt",
74            "DATA:image/png;base64,abcd",
75        ] {
76            validate_logo_uri(uri).unwrap();
77        }
78    }
79
80    #[test]
81    fn rejects_invalid_schemes_and_overlong_values() {
82        for uri in [
83            "ftp://example.com/logo.png",
84            "1https://example.com/logo.png",
85            "https+foo://example.com/logo.png",
86            "example.com/logo.png",
87        ] {
88            let invalid = validate_logo_uri(uri).unwrap_err().to_string();
89            assert!(invalid.contains("client-side validation failed: InvalidLogoURI"));
90        }
91
92        let too_long =
93            validate_logo_uri(&format!("https://{}", "a".repeat(249))).unwrap_err().to_string();
94        assert!(too_long.contains("client-side validation failed: LogoURITooLong"));
95    }
96
97    #[test]
98    fn validates_logo_uri_byte_length_boundaries() {
99        validate_logo_uri(&format!("https://{}", "a".repeat(248))).unwrap();
100
101        let multibyte = format!("https://{}", "é".repeat(124));
102        assert_eq!(multibyte.len(), TIP20_MAX_LOGO_URI_BYTES);
103        validate_logo_uri(&multibyte).unwrap();
104
105        let too_long = format!("https://{}é", "a".repeat(247));
106        assert_eq!(too_long.len(), TIP20_MAX_LOGO_URI_BYTES + 1);
107        assert!(validate_logo_uri(&too_long).unwrap_err().to_string().contains("LogoURITooLong"));
108    }
109
110    #[test]
111    fn set_logo_uri_selector_matches_tip20_t5() {
112        let calldata =
113            ITIP20::setLogoURICall { newLogoURI: "https://example.com/logo.png".to_string() }
114                .abi_encode();
115
116        assert_eq!(&calldata[..4], bytes!("c30ff6df").as_ref());
117    }
118}