diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..71b970a --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1 @@ +github: nanmu42 diff --git a/account.go b/account.go index 5c20824..e6a9ac1 100644 --- a/account.go +++ b/account.go @@ -131,6 +131,30 @@ func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *i return } +// ERC1155Transfers get a list of "erc1155 - token transfer events" by +// contract address and/or from/to address. +// +// leave undesired condition to nil. +func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC1155Transfer, err error) { + param := M{ + "page": page, + "offset": offset, + } + compose(param, "contractaddress", contractAddress) + compose(param, "address", address) + compose(param, "startblock", startBlock) + compose(param, "endblock", endBlock) + + if desc { + param["sort"] = "desc" + } else { + param["sort"] = "asc" + } + + err = c.call("account", "token1155tx", param, &txs) + return +} + // BlocksMinedByAddress gets list of blocks mined by address func (c *Client) BlocksMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { param := M{ diff --git a/account_e2e_test.go b/account_e2e_test.go index ac9094b..f7688e3 100644 --- a/account_e2e_test.go +++ b/account_e2e_test.go @@ -169,3 +169,21 @@ func TestClient_ERC721Transfers(t *testing.T) { t.Errorf("got txs length %v, want %v", len(txs), wantLen) } } + +func TestClient_ERC1155Transfers(t *testing.T) { + const ( + wantLen = 1 + ) + + var a, b = 128135633, 1802672 + var contract, address = "0x3edf71a31b80Ff6a45Fdb0858eC54DE98dF047AA", "0x4b986EF20Bb83532911521FB4F6F5605122a0721" + txs, err := api.ERC1155Transfers(&contract, &address, &b, &a, 0, 0, true) + noError(t, err, "api.ERC721Transfers") + + j, _ := json.MarshalIndent(txs, "", " ") + fmt.Printf("%s\n", j) + + if len(txs) != wantLen { + t.Errorf("got txs length %v, want %v", len(txs), wantLen) + } +} diff --git a/gas_tracker.go b/gas_tracker.go new file mode 100644 index 0000000..02fe5ca --- /dev/null +++ b/gas_tracker.go @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2022 Avi Misra + * + * Use of this work is governed by a MIT License. + * You may find a license copy in project root. + */ + +package etherscan + +import "time" + +// GasEstiamte gets estiamted confirmation time (in seconds) at the given gas price +func (c *Client) GasEstimate(gasPrice int) (confirmationTimeInSec time.Duration, err error) { + params := M{"gasPrice": gasPrice} + var confTime string + err = c.call("gastracker", "gasestimate", params, &confTime) + if err != nil { + return + } + return time.ParseDuration(confTime + "s") +} + +// GasOracle gets suggested gas prices (in Gwei) +func (c *Client) GasOracle() (gasPrices GasPrices, err error) { + err = c.call("gastracker", "gasoracle", M{}, &gasPrices) + return +} diff --git a/gas_tracker_e2e_test.go b/gas_tracker_e2e_test.go new file mode 100644 index 0000000..3905bc3 --- /dev/null +++ b/gas_tracker_e2e_test.go @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2018 LI Zhennan + * + * Use of this work is governed by a MIT License. + * You may find a license copy in project root. + */ + +package etherscan + +import ( + "testing" +) + +//GasEstiamte generates dynamic data. Best we can do is ensure all fields are populated +func TestClient_GasEstimate(t *testing.T) { + _, err := api.GasEstimate(20000000) + noError(t, err, "api.GasEstimate") +} + +//GasOracle generates dynamic data. Best we can do is ensure all fields are populated +func TestClient_GasOracle(t *testing.T) { + gasPrice, err := api.GasOracle() + noError(t, err, "api.GasOrcale") + + if 0 == len(gasPrice.GasUsedRatio) { + t.Errorf("gasPrice.GasUsedRatio empty") + } + +} diff --git a/go.mod b/go.mod index 0a30134..5837de6 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module github.com/nanmu42/etherscan-api go 1.13 + +require github.com/google/go-cmp v0.5.7 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..a6ca3a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +github.com/google/go-cmp v0.5.7 h1:81/ik6ipDQS2aGcBfIN5dHDB36BwrStyeAQquSYCV4o= +github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/logs.go b/logs.go new file mode 100644 index 0000000..0b21124 --- /dev/null +++ b/logs.go @@ -0,0 +1,21 @@ +/* + * Copyright (c) 2022 Avi Misra + * + * Use of this work is governed by a MIT License. + * You may find a license copy in project root. + */ + +package etherscan + +// GetLogs gets logs that match "topic" emitted by the specified "address" between the "fromBlock" and "toBlock" +func (c *Client) GetLogs(fromBlock, toBlock int, address, topic string) (logs []Log, err error) { + param := M{ + "fromBlock": fromBlock, + "toBlock": toBlock, + "topic0": topic, + "address": address, + } + + err = c.call("logs", "getLogs", param, &logs) + return +} diff --git a/logs_e2e_test.go b/logs_e2e_test.go new file mode 100644 index 0000000..e4490d2 --- /dev/null +++ b/logs_e2e_test.go @@ -0,0 +1,31 @@ +package etherscan + +import ( + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestClient_GetLogs(t *testing.T) { + expectedLogs := []Log{ + Log{ + Address: "0x33990122638b9132ca29c723bdf037f1a891a70c", + Topics: []string{"0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545", "0x72657075746174696f6e00000000000000000000000000000000000000000000", "0x000000000000000000000000d9b2f59f3b5c7b3c67047d2f03c3e8052470be92"}, + Data: "0x", + BlockNumber: "0x5c958", + BlockHash: "0xe32a9cac27f823b18454e8d69437d2af41a1b81179c6af2601f1040a72ad444b", + TransactionHash: "0x0b03498648ae2da924f961dda00dc6bb0a8df15519262b7e012b7d67f4bb7e83", + LogIndex: "0x", + }, + } + + actualLogs, err := api.GetLogs(379224, 379225, "0x33990122638b9132ca29c723bdf037f1a891a70c", "0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545") + + noError(t, err, "api.GetLogs") + + equal := cmp.Equal(expectedLogs, actualLogs) + + if !equal { + t.Errorf("api.GetLogs not working\n: %s\n", cmp.Diff(expectedLogs, actualLogs)) + } +} diff --git a/response.go b/response.go index a5efb0a..5c418cf 100644 --- a/response.go +++ b/response.go @@ -7,7 +7,12 @@ package etherscan -import "encoding/json" +import ( + "encoding/json" + "fmt" + "strconv" + "strings" +) // Envelope is the carrier of nearly every response type Envelope struct { @@ -45,6 +50,8 @@ type NormalTx struct { CumulativeGasUsed int `json:"cumulativeGasUsed,string"` GasUsed int `json:"gasUsed,string"` Confirmations int `json:"confirmations,string"` + FunctionName string `json:"functionName"` + MethodId string `json:"methodId"` } // InternalTx holds info from internal tx query @@ -111,6 +118,30 @@ type ERC721Transfer struct { Confirmations int `json:"confirmations,string"` } +// ERC1155Transfer holds info from ERC1155 token transfer event query +type ERC1155Transfer struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + Hash string `json:"hash"` + Nonce int `json:"nonce,string"` + BlockHash string `json:"blockHash"` + From string `json:"from"` + ContractAddress string `json:"contractAddress"` + To string `json:"to"` + TokenID *BigInt `json:"tokenID"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal uint8 `json:"tokenDecimal,string"` + TokenValue uint8 `json:"tokenValue,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *BigInt `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + Confirmations int `json:"confirmations,string"` +} + // MinedBlock holds info from query for mined block by address type MinedBlock struct { BlockNumber int `json:"blockNumber,string"` @@ -127,7 +158,11 @@ type ContractSource struct { OptimizationUsed int `json:"OptimizationUsed,string"` Runs int `json:"Runs,string"` ConstructorArguments string `json:"ConstructorArguments"` + EVMVersion string `json:"EVMVersion"` Library string `json:"Library"` + LicenseType string `json:"LicenseType"` + Proxy string `json:"Proxy"` + Implementation string `json:"Implementation"` SwarmSource string `json:"SwarmSource"` } @@ -159,3 +194,77 @@ type LatestPrice struct { ETHUSD float64 `json:"ethusd,string"` ETHUSDTimestamp Time `json:"ethusd_timestamp"` } + +type Log struct { + Address string `json:"address"` + Topics []string `json:"topics"` + Data string `json:"data"` + BlockNumber string `json:"blockNumber"` + TransactionHash string `json:"transactionHash"` + BlockHash string `json:"blockHash"` + LogIndex string `json:"logIndex"` + Removed bool `json:"removed"` +} + +// GasPrices holds info for Gas Oracle queries +// Gas Prices are returned in Gwei +type GasPrices struct { + LastBlock int + SafeGasPrice float64 + ProposeGasPrice float64 + FastGasPrice float64 + SuggestBaseFeeInGwei float64 `json:"suggestBaseFee"` + GasUsedRatio []float64 `json:"gasUsedRatio"` +} + +func (gp *GasPrices) UnmarshalJSON(data []byte) error { + _gp := struct { + LastBlock string + SafeGasPrice string + ProposeGasPrice string + FastGasPrice string + SuggestBaseFeeInGwei string `json:"suggestBaseFee"` + GasUsedRatio string `json:"gasUsedRatio"` + }{} + + err := json.Unmarshal(data, &_gp) + if err != nil { + return err + } + + gp.LastBlock, err = strconv.Atoi(_gp.LastBlock) + if err != nil { + return fmt.Errorf("Unable to convert LastBlock %s to int: %w", _gp.LastBlock, err) + } + + gp.SafeGasPrice, err = strconv.ParseFloat(_gp.SafeGasPrice, 64) + if err != nil { + return fmt.Errorf("Unable to convert SafeGasPrice %s to float64: %w", _gp.SafeGasPrice, err) + } + + gp.ProposeGasPrice, err = strconv.ParseFloat(_gp.ProposeGasPrice, 64) + if err != nil { + return fmt.Errorf("Unable to convert ProposeGasPrice %s to float64: %w", _gp.ProposeGasPrice, err) + } + + gp.FastGasPrice, err = strconv.ParseFloat(_gp.FastGasPrice, 64) + if err != nil { + return fmt.Errorf("Unable to convert FastGasPrice %s to float64: %w", _gp.FastGasPrice, err) + } + + gp.SuggestBaseFeeInGwei, err = strconv.ParseFloat(_gp.SuggestBaseFeeInGwei, 64) + if err != nil { + return fmt.Errorf("Unable to convert SuggestBaseFeeInGwei %s to float64: %w", _gp.SuggestBaseFeeInGwei, err) + } + + gasRatios := strings.Split(_gp.GasUsedRatio, ",") + gp.GasUsedRatio = make([]float64, len(gasRatios)) + for i, gasRatio := range gasRatios { + gp.GasUsedRatio[i], err = strconv.ParseFloat(gasRatio, 64) + if err != nil { + return fmt.Errorf("Unable to convert gasRatio %s to float64: %w", gasRatio, err) + } + } + + return nil +}