From c487ffbcb3c0969cefefe357570287ee18fe3ba7 Mon Sep 17 00:00:00 2001 From: 10gic <2391796+10gic@users.noreply.github.com> Date: Sun, 12 Dec 2021 21:29:42 +0800 Subject: [PATCH] Add subcommand `download-src` --- README.md | 101 +++++++++++++++++--------------- cmd/downloadsrc.go | 143 +++++++++++++++++++++++++++++++++++++++++++++ cmd/root.go | 12 ++++ 3 files changed, 209 insertions(+), 47 deletions(-) create mode 100644 cmd/downloadsrc.go diff --git a/README.md b/README.md index 89d095d..d98d4af 100644 --- a/README.md +++ b/README.md @@ -6,53 +6,7 @@ An Ethereum util, can transfer eth, check balance, call any contract function et GO111MODULE=on go install github.com/10gic/ethutil@latest ``` -# Usage -```txt -An Ethereum util, can transfer eth, check balance, call any contract function etc - -Usage: - ethutil [command] - -Available Commands: - balance Check eth balance for address - transfer Transfer amount of eth to target-address - call Invokes the (paid) contract method - query Invokes the (constant) contract method - deploy Deploy contract - deploy-erc20 Deploy an ERC20 token - drop-tx Drop pending tx for address - encode-param Encode input arguments, it's useful when you call contract's method manually - gen-private-key Generate eth private key and its address - dump-address Dump address from private key or mnemonic - compute-contract-addr Compute contract address before deployment - decode-tx Decode raw transaction - code Get runtime bytecode of a contract on the blockchain - erc20 Call ERC20 contract, a helper for subcommand call/query - keccak Compute keccak hash - help Help about any command - - -Flags: - --dry-run do not broadcast tx - --gas-limit uint the gas limit - --gas-price string the gas price, unit is gwei. - -h, --help help for ethutil - --max-fee-per-gas string maximum fee per gas they are willing to pay total, unit is gwei. see eip1559 - --max-priority-fee-per-gas string maximum fee per gas they are willing to give to miners, unit is gwei. see eip1559 - --node string mainnet | ropsten | kovan | rinkeby | goerli | sokol | bsc | heco, the node type (default "kovan") - --node-url string the target connection node url, if this option specified, the --node option is ignored - --nonce int the nonce, -1 means check online (default -1) - -k, --private-key string the private key, eth would be send from this account - --show-estimate-gas print estimate gas of tx - --show-input-data print input data of tx - --show-raw-tx print raw signed tx - --terse produce terse output - --tx-type string eip155 | eip1559, the type of tx your want to send (default "eip155") - -Use "ethutil [command] --help" for more information about a command. -``` - -# Example +# Usage Example ## Check Balance Check balance of an address: ```shell @@ -230,6 +184,59 @@ $ echo -n "abc" | ethutil keccak - 4e03657aea45a94fc7d47ba826c8d667c0d1e6e33a64a036ec44f58fa12d6c45 - ``` +## Download source of verified contract +```shell +$ ethutil --node mainnet download-src 0xdac17f958d2ee523a2206206994597c13d831ec7 -d output +2021/12/12 21:25:44 Current network is mainnet +2021/12/12 21:25:45 saving output/TetherToken.sol +``` + +# Documentation +```txt +An Ethereum util, can transfer eth, check balance, call any contract function etc + +Usage: + ethutil [command] + +Available Commands: + balance Check eth balance for address + transfer Transfer amount of eth to target-address + call Invokes the (paid) contract method + query Invokes the (constant) contract method + deploy Deploy contract + deploy-erc20 Deploy an ERC20 token + drop-tx Drop pending tx for address + encode-param Encode input arguments, it's useful when you call contract's method manually + gen-private-key Generate eth private key and its address + dump-address Dump address from private key or mnemonic + compute-contract-addr Compute contract address before deployment + decode-tx Decode raw transaction + code Get runtime bytecode of a contract on the blockchain + erc20 Call ERC20 contract, a helper for subcommand call/query + keccak Compute keccak hash + download-src Download source code of contract from block explorer platform, eg. etherscan. + help Help about any command + +Flags: + --dry-run do not broadcast tx + --gas-limit uint the gas limit + --gas-price string the gas price, unit is gwei. + -h, --help help for ethutil + --max-fee-per-gas string maximum fee per gas they are willing to pay total, unit is gwei. see eip1559 + --max-priority-fee-per-gas string maximum fee per gas they are willing to give to miners, unit is gwei. see eip1559 + --node string mainnet | ropsten | kovan | rinkeby | goerli | sokol | bsc | heco, the node type (default "kovan") + --node-url string the target connection node url, if this option specified, the --node option is ignored + --nonce int the nonce, -1 means check online (default -1) + -k, --private-key string the private key, eth would be send from this account + --show-estimate-gas print estimate gas of tx + --show-input-data print input data of tx + --show-raw-tx print raw signed tx + --terse produce terse output + --tx-type string eip155 | eip1559, the type of tx your want to send (default "eip155") + +Use "ethutil [command] --help" for more information about a command. +``` + # Issue ## daily request count exceeded, request rate limited If `panic: daily request count exceeded, request rate limited` appears, please use your own node url. It can be changed by option `--node-url`, for example `--node-url wss://mainnet.infura.io/ws/v3/YOUR_INFURA_PROJECT_ID` diff --git a/cmd/downloadsrc.go b/cmd/downloadsrc.go new file mode 100644 index 0000000..69565c0 --- /dev/null +++ b/cmd/downloadsrc.go @@ -0,0 +1,143 @@ +package cmd + +import ( + "encoding/json" + "fmt" + "github.com/spf13/cobra" + "io" + "log" + "net/http" + "os" + "path/filepath" + "strings" +) + +var downloadSrcCmdSaveDir string + +func init() { + downloadSrcCmd.Flags().StringVarP(&downloadSrcCmdSaveDir, "directory", "d", "./", "the directory of contract") +} + +var downloadSrcCmd = &cobra.Command{ + Use: "download-src [flags] contract-address", + Short: "Download source code of contract from block explorer platform, eg. etherscan.", + Run: func(cmd *cobra.Command, args []string) { + if len(args) == 0 { + // if no file specified, read from stdin + panic("no contract-address") + return + } + + if err := downloadSrc(args[0]); err != nil { + log.Fatalf("downloadSrc failed %v", err) + } + }, +} + +func downloadSrc(contractAddress string) error { + var requestUrl = fmt.Sprintf(nodeApiUrlMap[globalOptNode], contractAddress) + + resp, err := http.Get(requestUrl) + if err != nil { + // handle error + return err + } + defer resp.Body.Close() + body, err := io.ReadAll(resp.Body) + if err != nil { + // handle error + return err + } + + type respMsg struct { + Status string `json:"status"` + Message string `json:"message"` + Result []struct{ + SourceCode string `json:"SourceCode"` + ContractName string `json:"ContractName"` + } `json:"result"` + } + + var data respMsg + if err := json.Unmarshal(body, &data); err != nil { + return err + } + + // log.Printf("%+v", data) + + var sourceCode = data.Result[0].SourceCode + if len(sourceCode) == 0 { + log.Fatalf("Contract %v is not found or not verified", contractAddress) + } + + // make sure downloadSrcCmdSaveDir exist + err = os.MkdirAll(downloadSrcCmdSaveDir, os.ModePerm) + checkErr(err) + + if strings.HasPrefix(sourceCode, "{{") { + // Solidity Standard Json-Input + // An example: https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xa7EE3E16367D6Bd9dC59cc32Cdcc2eE51b663a4F + + // Remove one leading "{" and one trailing "}" + sourceCode = strings.TrimPrefix(sourceCode, "{") + sourceCode = strings.TrimSuffix(sourceCode, "}") + + type SourcesInfo struct { + Content string `json:"content"` + } + type sourceJson struct { + Language string `json:"language"` + Sources map[string]*SourcesInfo `json:"sources"` + } + + var data sourceJson + if err := json.Unmarshal([]byte(sourceCode), &data); err != nil { + return err + } + + // log.Printf("%+v", data) + + for contractName, contractContent := range data.Sources { + var contractFileName = filepath.Join(downloadSrcCmdSaveDir, contractName) + // Make sure parent directory of contractFileName exist + var dir = filepath.Dir(contractFileName) + err = os.MkdirAll(dir, os.ModePerm) + checkErr(err) + + saveContract(contractFileName, contractContent.Content) + checkErr(err) + } + + } else if strings.HasPrefix(sourceCode, "{") { + // Solidity Multiple files format + // An example: https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0x35036A4b7b012331f23F2945C08A5274CED38AC2 + + type sourceJson struct { + X map[string]struct{ + Content string `json:"content"` + } `json:"-"` // Rest of the fields should go here. + // See https://stackoverflow.com/questions/33436730/unmarshal-json-with-some-known-and-some-unknown-field-names + } + var data sourceJson + if err := json.Unmarshal([]byte(sourceCode), &data.X); err != nil { + return err + } + + for contractName, contractContent := range data.X { + saveContract(filepath.Join(downloadSrcCmdSaveDir, contractName), contractContent.Content) + checkErr(err) + } + } else { + // Solidity Single file + // An example: https://api.etherscan.io/api?module=contract&action=getsourcecode&address=0xdac17f958d2ee523a2206206994597c13d831ec7 + saveContract(filepath.Join(downloadSrcCmdSaveDir, data.Result[0].ContractName + ".sol"), sourceCode) + } + + return nil +} + +func saveContract(fileName string, data string) { + log.Printf("saving %v", fileName) + err := os.WriteFile(fileName, []byte(data), 0644) + checkErr(err) +} diff --git a/cmd/root.go b/cmd/root.go index 73b554b..2e2ac60 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -83,6 +83,17 @@ var nodeTxExplorerUrlMap = map[string]string{ nodeHeco: "https://scan.hecochain.com/tx/", } +var nodeApiUrlMap = map[string]string { + nodeMainnet: "https://api.etherscan.io/api?module=contract&action=getsourcecode&address=%s", + nodeRopsten: "https://api-ropsten.etherscan.io/api?module=contract&action=getsourcecode&address=%s", + nodeKovan: "https://api-kovan.etherscan.io/api?module=contract&action=getsourcecode&address=%s", + nodeRinkeby: "https://api-rinkeby.etherscan.io/api?module=contract&action=getsourcecode&address=%s", + nodeGoerli: "https://api-goerli.etherscan.io/api?module=contract&action=getsourcecode&address=%s", + nodeSokol: "https://blockscout.com/poa/sokol/api?module=contract&action=getsourcecode&address=%s", + nodeBsc: "https://api.bscscan.com/api?module=contract&action=getsourcecode&address=%s", + nodeHeco: "https://api.hecoinfo.com/api?module=contract&action=getsourcecode&address=%s", +} + // Execute cobra root command func Execute() error { return rootCmd.Execute() @@ -122,6 +133,7 @@ func init() { rootCmd.AddCommand(getCodeCmd) rootCmd.AddCommand(erc20Cmd) rootCmd.AddCommand(keccakCmd) + rootCmd.AddCommand(downloadSrcCmd) } func initConfig() {