Skip to content

Commit

Permalink
Add subcommand download-src
Browse files Browse the repository at this point in the history
  • Loading branch information
10gic committed Dec 12, 2021
1 parent 653e5d5 commit c487ffb
Show file tree
Hide file tree
Showing 3 changed files with 209 additions and 47 deletions.
101 changes: 54 additions & 47 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`
143 changes: 143 additions & 0 deletions cmd/downloadsrc.go
Original file line number Diff line number Diff line change
@@ -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)
}
12 changes: 12 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -122,6 +133,7 @@ func init() {
rootCmd.AddCommand(getCodeCmd)
rootCmd.AddCommand(erc20Cmd)
rootCmd.AddCommand(keccakCmd)
rootCmd.AddCommand(downloadSrcCmd)
}

func initConfig() {
Expand Down

0 comments on commit c487ffb

Please sign in to comment.