├── README.md └── grabber.py /README.md: -------------------------------------------------------------------------------- 1 | # What? 2 | 3 | Etherscan sucks for exploring multi-file verified contracts. This downloads the source code so you can open in your desired editor. 4 | 5 | # Usage 6 | 7 | ``` 8 | python ./grabber.py $SOME_NAME $SOME_ADDRESS $ETHERSCAN_BASED_EXPLORER_APIKEY $CHAIN_NAME $FOUNDRY_COMPATIBLE 9 | ``` 10 | 11 | Pass in a name to call the directory. Pass in an address that is verified on an etherscan based explorer and is *multifile* (doesn't work with single file/flattened contracts). Pass in your api key corresponding to the specified chain. Optionally pass in a chain name. If a chain was passed, you can also pass in a boolean `True` at the end. This will make the directory compatible with Foundry by adding the necessary config. 12 | 13 | By default the following chains are supported: 14 | ``` 15 | chains = { 16 | "arbitrum": "api.arbiscan.io", 17 | "optimism": "api-optimistic.etherscan.io", 18 | "binance": "api.bscscan.com", 19 | "avalanche": "api.snowtrace.io", 20 | "moonbeam": "api-moonbeam.moonscan.io", 21 | "ethereum": "api.etherscan.io" 22 | } 23 | ``` 24 | 25 | If you dont pass in a chain name, ethereum is used. 26 | 27 | Writes to a directory in `/tmp`. No idea if this works on windows, probably not? 28 | 29 | # Example 30 | 31 | Get Uniswap V3 router, and make it foundry compatible so that you can compile it immediately with forge. 32 | 33 | ``` 34 | python ./grabber.py UniV3Router 0xE592427A0AEce92De3Edee1F18E0157C05861564 BADMAD1111111111111111111111111111 ethereum True 35 | ``` 36 | 37 | -------------------------------------------------------------------------------- /grabber.py: -------------------------------------------------------------------------------- 1 | import requests as r 2 | import os 3 | import sys 4 | import json 5 | import toml 6 | 7 | api_key_base = "&apikey=" 8 | endpoint = "/api?module=contract&action=getsourcecode&address=" 9 | default_chain = "api.etherscan.io" 10 | 11 | chains = { 12 | "arbitrum": "api.arbiscan.io", 13 | "optimism": "api-optimistic.etherscan.io", 14 | "binance": "api.bscscan.com", 15 | "avalanche": "api.snowtrace.io", 16 | "moonbeam": "api-moonbeam.moonscan.io", 17 | "ethereum": default_chain, 18 | } 19 | 20 | def constructApiUrl(chain, addr, api_key): 21 | return "https://" + chain + endpoint + addr + api_key 22 | 23 | def main(): 24 | name = sys.argv[1] 25 | addr = sys.argv[2] 26 | api_key = sys.argv[3] 27 | if len(sys.argv) >= 5: 28 | chain = chains[sys.argv[4]] 29 | else: 30 | chain = default_chain 31 | 32 | init_forge_repo = False 33 | if len(sys.argv) >= 6: 34 | init_forge_repo = bool(sys.argv[5]) 35 | 36 | api_key = api_key_base + api_key 37 | 38 | url = constructApiUrl(chain, addr, api_key) 39 | api_resp = getEtherscan(url) 40 | sources = getMultiSource(api_resp) 41 | directory = mkdir(name) 42 | writeContracts(directory, sources) 43 | print("Wrote contracts to: ", directory) 44 | if init_forge_repo: 45 | curr_dir = os.getcwd() 46 | os.chdir(directory) 47 | os.system("forge config > foundry.toml") 48 | with open("./foundry.toml", "r+") as config: 49 | conf = toml.loads(config.read()) 50 | for d in os.listdir(directory): 51 | if os.path.isdir(d): 52 | if d.startswith("src") or d.startswith("contracts"): 53 | continue 54 | else: 55 | conf["default"]["libs"].append(d) 56 | config.seek(0) 57 | config.write(toml.dumps(conf)) 58 | os.chdir(curr_dir) 59 | 60 | 61 | def writeContracts(directory, sources): 62 | for source in sources.keys(): 63 | dirs = source.split("/") 64 | for i in range(len(dirs[:-1])): 65 | subdir = "/".join(dirs[:i+1]) 66 | mksub_dir(directory, subdir) 67 | with open(directory+"/"+source, "w") as f: 68 | f.write(sources[source]["content"]) 69 | 70 | def mkdir(name): 71 | try: 72 | os.mkdir("/tmp/"+name) 73 | except: 74 | pass 75 | return "/tmp/"+name 76 | 77 | def mksub_dir(directory, name): 78 | try: 79 | os.mkdir(directory+"/"+name) 80 | except: 81 | pass 82 | return directory+"/"+name 83 | 84 | def getEtherscan(url): 85 | resp = r.get(url).json() 86 | return resp 87 | 88 | def getMultiSource(resp): 89 | sources = json.loads(resp['result'][0]["SourceCode"][1:-1])['sources'] 90 | return sources 91 | 92 | if __name__ == "__main__": 93 | main() --------------------------------------------------------------------------------