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}