diff --git a/crates/primitives/src/block_explorer.rs b/crates/primitives/src/block_explorer.rs index 2312910312..68290c79e0 100644 --- a/crates/primitives/src/block_explorer.rs +++ b/crates/primitives/src/block_explorer.rs @@ -14,6 +14,9 @@ pub trait BlockExplorer: Send + Sync { fn get_token_url(&self, _token: &str) -> Option { None } + fn get_nft_url(&self, _contract: &str, _token_id: &str) -> Option { + None + } fn get_validator_url(&self, _validator: &str) -> Option { None } diff --git a/crates/primitives/src/explorers/algorand.rs b/crates/primitives/src/explorers/algorand.rs index 310090db69..b4cb2ecbbb 100644 --- a/crates/primitives/src/explorers/algorand.rs +++ b/crates/primitives/src/explorers/algorand.rs @@ -11,6 +11,7 @@ impl AlgorandAllo { tx_path: TX_PATH, address_path: ACCOUNT_PATH, token_path: None, + nft_path: None, validator_path: Some(ACCOUNT_PATH), }) } diff --git a/crates/primitives/src/explorers/aptos.rs b/crates/primitives/src/explorers/aptos.rs index 1370ec7d28..af71cd089f 100644 --- a/crates/primitives/src/explorers/aptos.rs +++ b/crates/primitives/src/explorers/aptos.rs @@ -8,6 +8,7 @@ pub fn new_aptos_scan() -> Box { tx_path: TRANSACTION_PATH, address_path: ACCOUNT_PATH, token_path: Some(COIN_PATH), + nft_path: None, validator_path: None, }) } @@ -19,6 +20,7 @@ pub fn new_aptos_explorer() -> Box { tx_path: TXN_PATH, address_path: ACCOUNT_PATH, token_path: Some(COIN_PATH), + nft_path: None, validator_path: None, }) } diff --git a/crates/primitives/src/explorers/blockvision.rs b/crates/primitives/src/explorers/blockvision.rs index 9ad8fe03b1..af959a7775 100644 --- a/crates/primitives/src/explorers/blockvision.rs +++ b/crates/primitives/src/explorers/blockvision.rs @@ -15,6 +15,7 @@ impl BlockVision { tx_path: "/txblock", address_path: ACCOUNT_PATH, token_path: Some(COIN_PATH), + nft_path: None, validator_path: Some(VALIDATORS_PATH), }) } diff --git a/crates/primitives/src/explorers/chainflip.rs b/crates/primitives/src/explorers/chainflip.rs index eb7b8b3a38..89ef0cd761 100644 --- a/crates/primitives/src/explorers/chainflip.rs +++ b/crates/primitives/src/explorers/chainflip.rs @@ -11,6 +11,7 @@ impl ChainflipScan { tx_path: TX_PATH, address_path: "", token_path: None, + nft_path: None, validator_path: None, }) } diff --git a/crates/primitives/src/explorers/metadata.rs b/crates/primitives/src/explorers/metadata.rs index 3087460325..7967b797ab 100644 --- a/crates/primitives/src/explorers/metadata.rs +++ b/crates/primitives/src/explorers/metadata.rs @@ -8,6 +8,7 @@ pub const TRANSACTION_PATH: &str = "/transaction"; pub const ADDRESS_PATH: &str = "/address"; pub const ACCOUNT_PATH: &str = "/account"; pub const TOKEN_PATH: &str = "/token"; +pub const NFT_PATH: &str = "/nft"; pub const COIN_PATH: &str = "/coin"; pub const VALIDATOR_PATH: &str = "/validator"; pub const VALIDATORS_PATH: &str = "/validators"; @@ -21,6 +22,7 @@ pub struct Metadata { pub tx_path: &'static str, pub address_path: &'static str, pub token_path: Option<&'static str>, + pub nft_path: Option<&'static str>, pub validator_path: Option<&'static str>, } @@ -33,11 +35,12 @@ impl Metadata { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: None, + nft_path: None, validator_path: None, } } - /// Create a common explorer with /tx, /address, and /token paths + /// Create a common explorer with /tx, /address, /token, and /nft paths pub fn with_token(name: &'static str, base_url: &'static str) -> Self { Self { name, @@ -45,6 +48,7 @@ impl Metadata { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: Some(TOKEN_PATH), + nft_path: Some(NFT_PATH), validator_path: None, } } @@ -57,6 +61,7 @@ impl Metadata { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: None, + nft_path: None, validator_path: Some(VALIDATOR_PATH), } } @@ -69,6 +74,7 @@ impl Metadata { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: Some(TOKEN_PATH), + nft_path: Some(NFT_PATH), validator_path: Some(VALIDATOR_PATH), } } @@ -81,6 +87,7 @@ impl Metadata { tx_path: TRANSACTION_PATH, address_path: ADDRESS_PATH, token_path: None, + nft_path: None, validator_path: None, } } @@ -93,6 +100,7 @@ impl Metadata { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: Some(ASSETS_PATH), + nft_path: None, validator_path: Some(VALIDATORS_PATH), } } @@ -125,6 +133,10 @@ impl BlockExplorer for Explorer { self.config.token_path.map(|path| format!("{}{}/{}", self.config.base_url, path, token)) } + fn get_nft_url(&self, contract: &str, token_id: &str) -> Option { + self.config.nft_path.map(|path| format!("{}{}/{}/{}", self.config.base_url, path, contract, token_id)) + } + fn get_validator_url(&self, validator: &str) -> Option { self.config.validator_path.map(|path| format!("{}{}/{}", self.config.base_url, path, validator)) } @@ -169,6 +181,7 @@ mod tests { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: Some(TOKEN_PATH), + nft_path: Some(NFT_PATH), validator_path: Some(VALIDATOR_PATH), }; let explorer = Explorer::boxed(config); @@ -177,6 +190,7 @@ mod tests { assert_eq!(explorer.get_tx_url("abc123"), "https://test.com/tx/abc123"); assert_eq!(explorer.get_address_url("addr123"), "https://test.com/address/addr123"); assert_eq!(explorer.get_token_url("token123"), Some("https://test.com/token/token123".to_string())); + assert_eq!(explorer.get_nft_url("contract123", "token123"), Some("https://test.com/nft/contract123/token123".to_string())); assert_eq!(explorer.get_validator_url("val123"), Some("https://test.com/validator/val123".to_string())); } @@ -188,11 +202,13 @@ mod tests { tx_path: TRANSACTION_PATH, address_path: ACCOUNT_PATH, token_path: None, + nft_path: None, validator_path: None, }; let explorer = Explorer::boxed(config); assert_eq!(explorer.get_token_url("token123"), None); + assert_eq!(explorer.get_nft_url("contract123", "token123"), None); assert_eq!(explorer.get_validator_url("val123"), None); } @@ -208,23 +224,28 @@ mod tests { let with_token = Metadata::with_token("WithToken", "https://token.com"); assert_eq!(with_token.token_path, Some(TOKEN_PATH)); + assert_eq!(with_token.nft_path, Some(NFT_PATH)); assert_eq!(with_token.validator_path, None); let with_validator = Metadata::with_validator("WithValidator", "https://validator.com"); assert_eq!(with_validator.token_path, None); + assert_eq!(with_validator.nft_path, None); assert_eq!(with_validator.validator_path, Some(VALIDATOR_PATH)); let full = Metadata::full("Full", "https://full.com"); assert_eq!(full.token_path, Some(TOKEN_PATH)); + assert_eq!(full.nft_path, Some(NFT_PATH)); assert_eq!(full.validator_path, Some(VALIDATOR_PATH)); let transaction_style = Metadata::blockchair("Transaction", "https://transaction.com"); assert_eq!(transaction_style.tx_path, TRANSACTION_PATH); assert_eq!(transaction_style.address_path, ADDRESS_PATH); assert_eq!(transaction_style.token_path, None); + assert_eq!(transaction_style.nft_path, None); let cosmos_style = Metadata::mintscan("Cosmos", "https://cosmos.com"); assert_eq!(cosmos_style.token_path, Some(ASSETS_PATH)); + assert_eq!(cosmos_style.nft_path, None); assert_eq!(cosmos_style.validator_path, Some(VALIDATORS_PATH)); } @@ -239,6 +260,7 @@ mod tests { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: None, + nft_path: None, validator_path: None, }, ) @@ -250,6 +272,7 @@ mod tests { tx_path: TRANSACTION_PATH, address_path: ACCOUNT_PATH, token_path: Some(TOKEN_PATH), + nft_path: None, validator_path: None, }, ); diff --git a/crates/primitives/src/explorers/near.rs b/crates/primitives/src/explorers/near.rs index 107a7e7e44..b63a6994a4 100644 --- a/crates/primitives/src/explorers/near.rs +++ b/crates/primitives/src/explorers/near.rs @@ -13,6 +13,7 @@ impl NearBlocks { tx_path: TXNS_PATH, address_path: ADDRESS_PATH, token_path: None, + nft_path: None, validator_path: None, }) } diff --git a/crates/primitives/src/explorers/solana.rs b/crates/primitives/src/explorers/solana.rs index 239b9b8d9c..89feca0a0d 100644 --- a/crates/primitives/src/explorers/solana.rs +++ b/crates/primitives/src/explorers/solana.rs @@ -1,24 +1,106 @@ use crate::block_explorer::BlockExplorer; -use crate::explorers::metadata::{ACCOUNT_PATH, ADDRESS_PATH, Explorer, Metadata, TOKEN_PATH, TX_PATH}; +use crate::explorers::metadata::{ACCOUNT_PATH, ADDRESS_PATH, TOKEN_PATH, TX_PATH}; + +struct SolanaExplorer { + name: &'static str, + base_url: &'static str, + address_path: &'static str, + token_path: &'static str, +} + +impl SolanaExplorer { + fn boxed( + name: &'static str, + base_url: &'static str, + address_path: &'static str, + token_path: &'static str, + ) -> Box { + Box::new(Self { + name, + base_url, + address_path, + token_path, + }) + } +} + +impl BlockExplorer for SolanaExplorer { + fn name(&self) -> String { + self.name.into() + } + + fn get_tx_url(&self, hash: &str) -> String { + format!("{}{}/{}", self.base_url, TX_PATH, hash) + } + + fn get_address_url(&self, address: &str) -> String { + format!("{}{}/{}", self.base_url, self.address_path, address) + } + + fn get_token_url(&self, token: &str) -> Option { + Some(format!("{}{}/{}", self.base_url, self.token_path, token)) + } + + fn get_nft_url(&self, _contract: &str, token_id: &str) -> Option { + self.get_token_url(token_id) + } +} pub fn new_solscan() -> Box { - Explorer::boxed(Metadata { - name: "Solscan", - base_url: "https://solscan.io", - tx_path: TX_PATH, - address_path: ACCOUNT_PATH, - token_path: Some(TOKEN_PATH), - validator_path: None, - }) + SolanaExplorer::boxed("Solscan", "https://solscan.io", ACCOUNT_PATH, TOKEN_PATH) } pub fn new_solana_fm() -> Box { - Explorer::boxed(Metadata { - name: "SolanaFM", - base_url: "https://solana.fm", - tx_path: TX_PATH, - address_path: ADDRESS_PATH, - token_path: Some(ADDRESS_PATH), - validator_path: None, - }) + SolanaExplorer::boxed("SolanaFM", "https://solana.fm", ADDRESS_PATH, ADDRESS_PATH) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_solscan_urls() { + let explorer = new_solscan(); + + assert_eq!(explorer.name(), "Solscan"); + assert_eq!( + explorer.get_tx_url("ArS7DzeHUA54ccRG12SqEZwt7snQePcanZ77Mkm2KRos"), + "https://solscan.io/tx/ArS7DzeHUA54ccRG12SqEZwt7snQePcanZ77Mkm2KRos" + ); + assert_eq!( + explorer.get_address_url("GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2"), + "https://solscan.io/account/GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2" + ); + assert_eq!( + explorer.get_token_url("GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2"), + Some("https://solscan.io/token/GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2".to_string()) + ); + assert_eq!( + explorer.get_nft_url("ignored-contract", "GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2"), + Some("https://solscan.io/token/GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2".to_string()) + ); + } + + #[test] + fn test_solana_fm_urls() { + let explorer = new_solana_fm(); + + assert_eq!(explorer.name(), "SolanaFM"); + assert_eq!( + explorer.get_tx_url("ArS7DzeHUA54ccRG12SqEZwt7snQePcanZ77Mkm2KRos"), + "https://solana.fm/tx/ArS7DzeHUA54ccRG12SqEZwt7snQePcanZ77Mkm2KRos" + ); + assert_eq!( + explorer.get_address_url("GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2"), + "https://solana.fm/address/GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2" + ); + assert_eq!( + explorer.get_token_url("GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2"), + Some("https://solana.fm/address/GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2".to_string()) + ); + assert_eq!( + explorer.get_nft_url("ignored-contract", "GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2"), + Some("https://solana.fm/address/GvhwZwtV32kYUXUw965CUM3KGPdtBsDwPVpi92brY5R2".to_string()) + ); + } } diff --git a/crates/primitives/src/explorers/stellar_expert.rs b/crates/primitives/src/explorers/stellar_expert.rs index 71c4b5315b..a364e9e0b7 100644 --- a/crates/primitives/src/explorers/stellar_expert.rs +++ b/crates/primitives/src/explorers/stellar_expert.rs @@ -8,6 +8,7 @@ pub fn new() -> Box { tx_path: TX_PATH, address_path: ACCOUNT_PATH, token_path: Some(ASSET_PATH), + nft_path: None, validator_path: None, }) } diff --git a/crates/primitives/src/explorers/subscan.rs b/crates/primitives/src/explorers/subscan.rs index a900dd8384..a2fab331a5 100644 --- a/crates/primitives/src/explorers/subscan.rs +++ b/crates/primitives/src/explorers/subscan.rs @@ -11,6 +11,7 @@ impl SubScan { tx_path: "/extrinsic", address_path: ACCOUNT_PATH, token_path: None, + nft_path: None, validator_path: None, }) } diff --git a/crates/primitives/src/explorers/sui.rs b/crates/primitives/src/explorers/sui.rs index 9c2338e592..1600978fea 100644 --- a/crates/primitives/src/explorers/sui.rs +++ b/crates/primitives/src/explorers/sui.rs @@ -8,6 +8,7 @@ pub fn new_sui_scan() -> Box { tx_path: TX_PATH, address_path: ACCOUNT_PATH, token_path: Some(COIN_PATH), + nft_path: None, validator_path: Some(VALIDATOR_PATH), }) } diff --git a/crates/primitives/src/explorers/ton.rs b/crates/primitives/src/explorers/ton.rs index c41da8e3b5..e4b5897501 100644 --- a/crates/primitives/src/explorers/ton.rs +++ b/crates/primitives/src/explorers/ton.rs @@ -8,6 +8,7 @@ pub fn new_ton_viewer() -> Box { tx_path: TRANSACTION_PATH, address_path: "", token_path: Some(""), + nft_path: None, validator_path: Some(""), }) } @@ -22,6 +23,7 @@ impl TonScan { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: Some("/jetton"), + nft_path: None, validator_path: Some(ADDRESS_PATH), }) } diff --git a/crates/primitives/src/explorers/tronscan.rs b/crates/primitives/src/explorers/tronscan.rs index 7d3c6350cb..65a8a7df41 100644 --- a/crates/primitives/src/explorers/tronscan.rs +++ b/crates/primitives/src/explorers/tronscan.rs @@ -11,6 +11,7 @@ impl TronScan { tx_path: TRANSACTION_PATH, address_path: ADDRESS_PATH, token_path: Some("/token20"), + nft_path: None, validator_path: Some(ADDRESS_PATH), }) } diff --git a/crates/primitives/src/explorers/xrpscan.rs b/crates/primitives/src/explorers/xrpscan.rs index 83da2a7860..4217bc2994 100644 --- a/crates/primitives/src/explorers/xrpscan.rs +++ b/crates/primitives/src/explorers/xrpscan.rs @@ -11,6 +11,7 @@ impl XrpScan { tx_path: TX_PATH, address_path: ACCOUNT_PATH, token_path: Some(ACCOUNT_PATH), + nft_path: None, validator_path: None, }; Explorer::boxed(config) diff --git a/crates/primitives/src/explorers/zksync.rs b/crates/primitives/src/explorers/zksync.rs index 64c2f1f5c9..36b608e3f5 100644 --- a/crates/primitives/src/explorers/zksync.rs +++ b/crates/primitives/src/explorers/zksync.rs @@ -11,6 +11,7 @@ impl ZkSync { tx_path: TX_PATH, address_path: ADDRESS_PATH, token_path: Some(ADDRESS_PATH), // ZkSync uses address path for tokens + nft_path: None, validator_path: None, }; Explorer::boxed(config) diff --git a/gemstone/src/block_explorer/mod.rs b/gemstone/src/block_explorer/mod.rs index c6b2b85734..66be1ad2bf 100644 --- a/gemstone/src/block_explorer/mod.rs +++ b/gemstone/src/block_explorer/mod.rs @@ -75,6 +75,10 @@ impl Explorer { get_block_explorer(self.chain, explorer_name).get_token_url(address) } + pub fn get_nft_url(&self, explorer_name: &str, contract_address: &str, token_id: &str) -> Option { + get_block_explorer(self.chain, explorer_name).get_nft_url(contract_address, token_id) + } + pub fn get_validator_url(&self, explorer_name: &str, address: &str) -> Option { get_block_explorer(self.chain, explorer_name).get_validator_url(address) } @@ -121,10 +125,12 @@ mod tests { let account_url = explorer.get_address_url(&explorers[0].name(), "0x1f9090aae28b8a3dceadf281b0f12828e676c326"); let tx_url = explorer.get_transaction_url(&explorers[0].name(), "0xfd96a9ee20a7440bf65a5b8ecf7f884289ed78e28f82d45343a70f459e7a42a0"); let token_url = explorer.get_token_url(&explorers[0].name(), "0xdac17f958d2ee523a2206206994597c13d831ec7"); + let nft_url = explorer.get_nft_url(&explorers[0].name(), "0x47A00fC8590C11bE4c419D9Ae50DEc267B6E24ee", "11871"); assert_eq!(account_url, "https://etherscan.io/address/0x1f9090aae28b8a3dceadf281b0f12828e676c326"); assert_eq!(tx_url, "https://etherscan.io/tx/0xfd96a9ee20a7440bf65a5b8ecf7f884289ed78e28f82d45343a70f459e7a42a0"); assert_eq!(token_url, Some("https://etherscan.io/token/0xdac17f958d2ee523a2206206994597c13d831ec7".to_string())); + assert_eq!(nft_url, Some("https://etherscan.io/nft/0x47A00fC8590C11bE4c419D9Ae50DEc267B6E24ee/11871".to_string())); } #[test]