@@ -5,12 +5,17 @@ import Control.Exception (catch, SomeException)
55import Control.Monad
66import Data.Aeson
77import Data.Aeson.Types (parseEither )
8+ import Data.IORef (IORef , atomicWriteIORef , readIORef )
9+ import Data.Map.Strict (Map )
10+ import Data.Map.Strict qualified as Map
811import Data.Maybe (catMaybes )
912import Data.Sequence (Seq )
1013import Data.Text (Text )
1114import Data.Text qualified as T
15+ import GHC.Generics (Generic )
1216import Network.HTTP.Simple (httpSink , parseRequest , getResponseBody , httpJSON )
1317import Text.HTML.DOM (sinkDoc )
18+ import Text.Read (readMaybe )
1419import Text.XML.Cursor (attributeIs , content , element , fromDocument , ($//) , (&//) )
1520
1621import EVM.Solidity (makeSrcMaps , SrcMap )
@@ -22,9 +27,25 @@ data SourceCode = SourceCode
2227 }
2328 deriving Show
2429
30+ data ChainInfo = ChainInfo
31+ { chainname :: Text
32+ , chainid :: Text
33+ , blockexplorer :: Text
34+ , apiurl :: Text
35+ , status :: Int
36+ } deriving (Show , Generic )
37+
38+ instance FromJSON ChainInfo
39+
40+ newtype ChainlistResponse = ChainlistResponse
41+ { result :: [ChainInfo ]
42+ } deriving (Show , Generic )
43+
44+ instance FromJSON ChainlistResponse
45+
2546fetchContractSource :: Maybe W256 -> Maybe Text -> Addr -> IO (Maybe SourceCode )
2647fetchContractSource chainId apiKey addr = do
27- let chainParam = maybe " &chainid=1" (\ c -> " &chainid=" <> show c ) chainId
48+ let chainParam = maybe " &chainid=1" (\ c -> " &chainid=" <> show ( fromIntegral c :: Integer ) ) chainId
2849 url <- parseRequest $ " https://api.etherscan.io/v2/api?"
2950 <> chainParam
3051 <> " &module=contract"
@@ -61,12 +82,69 @@ fetchContractSource chainId apiKey addr = do
6182 try url (n - 1 )
6283 _ -> pure Nothing
6384
85+ -- | Fetch the chainlist from Etherscan API and return a map of chainId to block explorer URL
86+ fetchChainlist :: IO (Maybe (Map W256 Text ))
87+ fetchChainlist = do
88+ putStr " Fetching Etherscan chainlist... "
89+ url <- parseRequest " https://api.etherscan.io/v2/chainlist"
90+ try url (3 :: Int )
91+ where
92+ try url n = catch
93+ (do
94+ resp <- httpJSON url
95+ let result = getResponseBody resp :: ChainlistResponse
96+ putStrLn " Success!"
97+ let chainMap = Map. fromList
98+ [ (cid, T. dropWhileEnd (== ' /' ) ci. blockexplorer)
99+ | ci <- result. result
100+ , ci. status == 1 -- Only active chains
101+ , Just cid <- [readMaybe $ T. unpack ci. chainid]
102+ ]
103+ pure $ Just chainMap
104+ )
105+ (\ (e :: SomeException ) -> do
106+ if n > 0
107+ then do
108+ putStrLn $ " Retrying (" <> show n <> " left). Error: " <> show e
109+ threadDelay 1000000 -- 1 second
110+ try url (n - 1 )
111+ else do
112+ putStrLn $ " Failed: " <> show e
113+ pure Nothing
114+ )
115+
116+ -- | Get block explorer URL for a chainId, fetching and caching chainlist if needed
117+ getBlockExplorerUrl :: IORef (Maybe (Map W256 Text )) -> W256 -> IO (Maybe Text )
118+ getBlockExplorerUrl cacheRef chainId = do
119+ cacheVal <- readIORef cacheRef
120+ case cacheVal of
121+ Just cache -> pure $ Map. lookup chainId cache
122+ Nothing -> do
123+ -- Fetch and populate cache (done at most once)
124+ maybeChainlist <- fetchChainlist
125+ atomicWriteIORef cacheRef maybeChainlist
126+ pure $ maybeChainlist >>= Map. lookup chainId
127+
64128-- | Unfortunately, Etherscan doesn't expose source maps in the JSON API.
65129-- This function scrapes it from the HTML. Return a tuple where the first element
66130-- is raw srcmap in text format and the second element is a parsed map.
67- fetchContractSourceMap :: Addr -> IO (Maybe (Text , Seq SrcMap ))
68- fetchContractSourceMap addr = do
69- url <- parseRequest $ " https://etherscan.io/address/" <> show addr
131+ fetchContractSourceMap :: IORef (Maybe (Map W256 Text )) -> Maybe W256 -> Addr -> IO (Maybe (Text , Seq SrcMap ))
132+ fetchContractSourceMap cacheRef chainId addr = do
133+ -- Determine block explorer URL
134+ let defaultUrl = " https://etherscan.io"
135+ baseUrl <- case chainId of
136+ Nothing -> pure defaultUrl
137+ Just cid -> do
138+ maybeUrl <- getBlockExplorerUrl cacheRef cid
139+ case maybeUrl of
140+ Just url -> pure $ T. unpack url
141+ Nothing -> do
142+ putStrLn $ " Warning: No block explorer found for chainId "
143+ <> show (fromIntegral cid :: Integer ) <> " , defaulting to mainnet"
144+ pure defaultUrl
145+
146+ -- Scrape HTML from block explorer
147+ url <- parseRequest $ baseUrl <> " /address/" <> show addr
70148 doc <- httpSink url $ const sinkDoc
71149 let cursor = fromDocument doc
72150 -- reverse to start looking from the end
0 commit comments