From 7057a0caf4ae7e091f5dffce71162414d9e13463 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 3 Mar 2023 13:34:01 +0100 Subject: [PATCH 01/21] fix: modify TokenDecimal and TokenValue fields to use int --- response.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/response.go b/response.go index d096aef..a50c1fc 100644 --- a/response.go +++ b/response.go @@ -129,8 +129,8 @@ type ERC1155Transfer struct { TokenID *BigInt `json:"tokenID"` TokenName string `json:"tokenName"` TokenSymbol string `json:"tokenSymbol"` - TokenDecimal uint8 `json:"tokenDecimal,string"` - TokenValue uint8 `json:"tokenValue,string"` + TokenDecimal int `json:"tokenDecimal,string"` + TokenValue int `json:"tokenValue,string"` TransactionIndex int `json:"transactionIndex,string"` Gas int `json:"gas,string"` GasPrice *BigInt `json:"gasPrice"` From a65a8d8fe58f5d64f247f510af919e4ee7212683 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 3 Mar 2023 13:40:03 +0100 Subject: [PATCH 02/21] chore: update package name. bump go version --- go.mod | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 5837de6..c613a9f 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,5 @@ -module github.com/nanmu42/etherscan-api +module github.com/timcki/etherscan-api -go 1.13 +go 1.18 require github.com/google/go-cmp v0.5.7 From d8fcb92b51b5ad699b3ac117e0b8758b115fbe16 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Mar 2023 11:49:21 +0100 Subject: [PATCH 03/21] feat: replace all occurences of BigInt with big.Int --- account_e2e_test.go | 2 +- helper.go | 9 +- logs_e2e_test.go | 2 +- response.go | 208 ++++++++++++++++++++++---------------------- 4 files changed, 111 insertions(+), 110 deletions(-) diff --git a/account_e2e_test.go b/account_e2e_test.go index f7688e3..95325d6 100644 --- a/account_e2e_test.go +++ b/account_e2e_test.go @@ -35,7 +35,7 @@ func TestClient_MultiAccountBalance(t *testing.T) { if item.Account == "" { t.Errorf("bound error on index %v", i) } - if item.Balance.Int().Cmp(big.NewInt(0)) != 1 { + if item.Balance.Cmp(big.NewInt(0)) != 1 { t.Errorf("rich man %s at index %v is no longer rich.", item.Account, i) } } diff --git a/helper.go b/helper.go index 8964970..de0710f 100644 --- a/helper.go +++ b/helper.go @@ -42,11 +42,10 @@ type M map[string]interface{} type BigInt big.Int // UnmarshalText implements the encoding.TextUnmarshaler interface. -func (b *BigInt) UnmarshalText(text []byte) (err error) { +func (b *BigInt) UnmarshalText(text []byte) error { var bigInt = new(big.Int) - err = bigInt.UnmarshalText(text) - if err != nil { - return + if err := bigInt.UnmarshalText(text); err != nil { + return err } *b = BigInt(*bigInt) @@ -54,7 +53,7 @@ func (b *BigInt) UnmarshalText(text []byte) (err error) { } // MarshalText implements the encoding.TextMarshaler -func (b *BigInt) MarshalText() (text []byte, err error) { +func (b *BigInt) MarshalText() ([]byte, error) { return []byte(b.Int().String()), nil } diff --git a/logs_e2e_test.go b/logs_e2e_test.go index e4490d2..fccf2c3 100644 --- a/logs_e2e_test.go +++ b/logs_e2e_test.go @@ -8,7 +8,7 @@ import ( func TestClient_GetLogs(t *testing.T) { expectedLogs := []Log{ - Log{ + { Address: "0x33990122638b9132ca29c723bdf037f1a891a70c", Topics: []string{"0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545", "0x72657075746174696f6e00000000000000000000000000000000000000000000", "0x000000000000000000000000d9b2f59f3b5c7b3c67047d2f03c3e8052470be92"}, Data: "0x", diff --git a/response.go b/response.go index a50c1fc..2dbfbd0 100644 --- a/response.go +++ b/response.go @@ -10,8 +10,10 @@ package etherscan import ( "encoding/json" "fmt" + "math/big" "strconv" "strings" + "time" ) // Envelope is the carrier of nearly every response @@ -26,125 +28,125 @@ type Envelope struct { // AccountBalance account and its balance in pair type AccountBalance struct { - Account string `json:"account"` - Balance *BigInt `json:"balance"` + Account string `json:"account"` + Balance *big.Int `json:"balance"` } // NormalTx holds info from normal tx query type NormalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - Nonce int `json:"nonce,string"` - BlockHash string `json:"blockHash"` - TransactionIndex int `json:"transactionIndex,string"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - Gas int `json:"gas,string"` - GasPrice *BigInt `json:"gasPrice"` - IsError int `json:"isError,string"` - TxReceiptStatus string `json:"txreceipt_status"` - Input string `json:"input"` - ContractAddress string `json:"contractAddress"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - GasUsed int `json:"gasUsed,string"` - Confirmations int `json:"confirmations,string"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp time.Time `json:"timeStamp"` + Hash string `json:"hash"` + Nonce int `json:"nonce,string"` + BlockHash string `json:"blockHash"` + TransactionIndex int `json:"transactionIndex,string"` + From string `json:"from"` + To string `json:"to"` + Value *big.Int `json:"value"` + Gas int `json:"gas,string"` + GasPrice *big.Int `json:"gasPrice"` + IsError int `json:"isError,string"` + TxReceiptStatus string `json:"txreceipt_status"` + Input string `json:"input"` + ContractAddress string `json:"contractAddress"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + GasUsed int `json:"gasUsed,string"` + Confirmations int `json:"confirmations,string"` } // InternalTx holds info from internal tx query type InternalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - ContractAddress string `json:"contractAddress"` - Input string `json:"input"` - Type string `json:"type"` - Gas int `json:"gas,string"` - GasUsed int `json:"gasUsed,string"` - TraceID string `json:"traceId"` - IsError int `json:"isError,string"` - ErrCode string `json:"errCode"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp time.Time `json:"timeStamp"` + Hash string `json:"hash"` + From string `json:"from"` + To string `json:"to"` + Value *big.Int `json:"value"` + ContractAddress string `json:"contractAddress"` + Input string `json:"input"` + Type string `json:"type"` + Gas int `json:"gas,string"` + GasUsed int `json:"gasUsed,string"` + TraceID string `json:"traceId"` + IsError int `json:"isError,string"` + ErrCode string `json:"errCode"` } // ERC20Transfer holds info from ERC20 token transfer event query type ERC20Transfer 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"` - Value *BigInt `json:"value"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal uint8 `json:"tokenDecimal,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"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp time.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"` + Value *big.Int `json:"value"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal uint8 `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *big.Int `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + Confirmations int `json:"confirmations,string"` } // ERC721Transfer holds info from ERC721 token transfer event query type ERC721Transfer 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"` - 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"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp time.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 *big.Int `json:"tokenID"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal uint8 `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *big.Int `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + 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 int `json:"tokenDecimal,string"` - TokenValue int `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"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp time.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 *big.Int `json:"tokenID"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,string"` + TokenValue int `json:"tokenValue,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *big.Int `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"` - TimeStamp Time `json:"timeStamp"` - BlockReward *BigInt `json:"blockReward"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + BlockReward *big.Int `json:"blockReward"` } // ContractSource holds info from query for contract source code @@ -173,16 +175,16 @@ type ExecutionStatus struct { // BlockRewards holds info from query for block and uncle rewards type BlockRewards struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - BlockMiner string `json:"blockMiner"` - BlockReward *BigInt `json:"blockReward"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + BlockMiner string `json:"blockMiner"` + BlockReward *big.Int `json:"blockReward"` Uncles []struct { - Miner string `json:"miner"` - UnclePosition int `json:"unclePosition,string"` - BlockReward *BigInt `json:"blockreward"` + Miner string `json:"miner"` + UnclePosition int `json:"unclePosition,string"` + BlockReward *big.Int `json:"blockreward"` } `json:"uncles"` - UncleInclusionReward *BigInt `json:"uncleInclusionReward"` + UncleInclusionReward *big.Int `json:"uncleInclusionReward"` } // LatestPrice holds info from query for latest ether price From a94601987897cd4e0a5408759cd36c5f536d89f6 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Mar 2023 12:01:10 +0100 Subject: [PATCH 04/21] fix: add back Time --- response.go | 180 ++++++++++++++++++++++++++-------------------------- 1 file changed, 90 insertions(+), 90 deletions(-) diff --git a/response.go b/response.go index 2dbfbd0..c1c3bab 100644 --- a/response.go +++ b/response.go @@ -34,112 +34,112 @@ type AccountBalance struct { // NormalTx holds info from normal tx query type NormalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp time.Time `json:"timeStamp"` - Hash string `json:"hash"` - Nonce int `json:"nonce,string"` - BlockHash string `json:"blockHash"` - TransactionIndex int `json:"transactionIndex,string"` - From string `json:"from"` - To string `json:"to"` - Value *big.Int `json:"value"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - IsError int `json:"isError,string"` - TxReceiptStatus string `json:"txreceipt_status"` - Input string `json:"input"` - ContractAddress string `json:"contractAddress"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - GasUsed int `json:"gasUsed,string"` - Confirmations int `json:"confirmations,string"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + Hash string `json:"hash"` + Nonce int `json:"nonce,string"` + BlockHash string `json:"blockHash"` + TransactionIndex int `json:"transactionIndex,string"` + From string `json:"from"` + To string `json:"to"` + Value *big.Int `json:"value"` + Gas int `json:"gas,string"` + GasPrice *big.Int `json:"gasPrice"` + IsError int `json:"isError,string"` + TxReceiptStatus string `json:"txreceipt_status"` + Input string `json:"input"` + ContractAddress string `json:"contractAddress"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + GasUsed int `json:"gasUsed,string"` + Confirmations int `json:"confirmations,string"` } // InternalTx holds info from internal tx query type InternalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp time.Time `json:"timeStamp"` - Hash string `json:"hash"` - From string `json:"from"` - To string `json:"to"` - Value *big.Int `json:"value"` - ContractAddress string `json:"contractAddress"` - Input string `json:"input"` - Type string `json:"type"` - Gas int `json:"gas,string"` - GasUsed int `json:"gasUsed,string"` - TraceID string `json:"traceId"` - IsError int `json:"isError,string"` - ErrCode string `json:"errCode"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + Hash string `json:"hash"` + From string `json:"from"` + To string `json:"to"` + Value *big.Int `json:"value"` + ContractAddress string `json:"contractAddress"` + Input string `json:"input"` + Type string `json:"type"` + Gas int `json:"gas,string"` + GasUsed int `json:"gasUsed,string"` + TraceID string `json:"traceId"` + IsError int `json:"isError,string"` + ErrCode string `json:"errCode"` } // ERC20Transfer holds info from ERC20 token transfer event query type ERC20Transfer struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp time.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"` - Value *big.Int `json:"value"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal uint8 `json:"tokenDecimal,string"` - TransactionIndex int `json:"transactionIndex,string"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - GasUsed int `json:"gasUsed,string"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - Input string `json:"input"` - Confirmations int `json:"confirmations,string"` + 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"` + Value *big.Int `json:"value"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal uint8 `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *big.Int `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + Confirmations int `json:"confirmations,string"` } // ERC721Transfer holds info from ERC721 token transfer event query type ERC721Transfer struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp time.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 *big.Int `json:"tokenID"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal uint8 `json:"tokenDecimal,string"` - TransactionIndex int `json:"transactionIndex,string"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - GasUsed int `json:"gasUsed,string"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - Input string `json:"input"` - Confirmations int `json:"confirmations,string"` + 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 *big.Int `json:"tokenID"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal uint8 `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *big.Int `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + Confirmations int `json:"confirmations,string"` } // ERC1155Transfer holds info from ERC1155 token transfer event query type ERC1155Transfer struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp time.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 *big.Int `json:"tokenID"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal int `json:"tokenDecimal,string"` - TokenValue int `json:"tokenValue,string"` - TransactionIndex int `json:"transactionIndex,string"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - GasUsed int `json:"gasUsed,string"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - Input string `json:"input"` - Confirmations int `json:"confirmations,string"` + 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 *big.Int `json:"tokenID"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,string"` + TokenValue int `json:"tokenValue,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *big.Int `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 From 24c23544c49487c2ee70028ac48c168b9a558f3e Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Mar 2023 12:05:19 +0100 Subject: [PATCH 05/21] fix: remove import. replace uint8 with int --- response.go | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/response.go b/response.go index c1c3bab..72520d6 100644 --- a/response.go +++ b/response.go @@ -13,7 +13,6 @@ import ( "math/big" "strconv" "strings" - "time" ) // Envelope is the carrier of nearly every response @@ -85,7 +84,7 @@ type ERC20Transfer struct { Value *big.Int `json:"value"` TokenName string `json:"tokenName"` TokenSymbol string `json:"tokenSymbol"` - TokenDecimal uint8 `json:"tokenDecimal,string"` + TokenDecimal int `json:"tokenDecimal,string"` TransactionIndex int `json:"transactionIndex,string"` Gas int `json:"gas,string"` GasPrice *big.Int `json:"gasPrice"` @@ -108,7 +107,7 @@ type ERC721Transfer struct { TokenID *big.Int `json:"tokenID"` TokenName string `json:"tokenName"` TokenSymbol string `json:"tokenSymbol"` - TokenDecimal uint8 `json:"tokenDecimal,string"` + TokenDecimal int `json:"tokenDecimal,string"` TransactionIndex int `json:"transactionIndex,string"` Gas int `json:"gas,string"` GasPrice *big.Int `json:"gasPrice"` From 4a6831145f22af7e828bd8197b54f19e3364acd2 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Mar 2023 12:12:31 +0100 Subject: [PATCH 06/21] fix: go back to using BigInt. Return zero for empty string --- account_e2e_test.go | 2 +- helper.go | 5 ++ response.go | 207 ++++++++++++++++++++++---------------------- 3 files changed, 109 insertions(+), 105 deletions(-) diff --git a/account_e2e_test.go b/account_e2e_test.go index 95325d6..f7688e3 100644 --- a/account_e2e_test.go +++ b/account_e2e_test.go @@ -35,7 +35,7 @@ func TestClient_MultiAccountBalance(t *testing.T) { if item.Account == "" { t.Errorf("bound error on index %v", i) } - if item.Balance.Cmp(big.NewInt(0)) != 1 { + if item.Balance.Int().Cmp(big.NewInt(0)) != 1 { t.Errorf("rich man %s at index %v is no longer rich.", item.Account, i) } } diff --git a/helper.go b/helper.go index de0710f..f8c3502 100644 --- a/helper.go +++ b/helper.go @@ -44,6 +44,11 @@ type BigInt big.Int // UnmarshalText implements the encoding.TextUnmarshaler interface. func (b *BigInt) UnmarshalText(text []byte) error { var bigInt = new(big.Int) + if string(text) == "" { + bigInt.SetInt64(0) + *b = BigInt(*bigInt) + return nil + } if err := bigInt.UnmarshalText(text); err != nil { return err } diff --git a/response.go b/response.go index 72520d6..489b533 100644 --- a/response.go +++ b/response.go @@ -10,7 +10,6 @@ package etherscan import ( "encoding/json" "fmt" - "math/big" "strconv" "strings" ) @@ -27,125 +26,125 @@ type Envelope struct { // AccountBalance account and its balance in pair type AccountBalance struct { - Account string `json:"account"` - Balance *big.Int `json:"balance"` + Account string `json:"account"` + Balance *BigInt `json:"balance"` } // NormalTx holds info from normal tx query type NormalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - Nonce int `json:"nonce,string"` - BlockHash string `json:"blockHash"` - TransactionIndex int `json:"transactionIndex,string"` - From string `json:"from"` - To string `json:"to"` - Value *big.Int `json:"value"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - IsError int `json:"isError,string"` - TxReceiptStatus string `json:"txreceipt_status"` - Input string `json:"input"` - ContractAddress string `json:"contractAddress"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - GasUsed int `json:"gasUsed,string"` - Confirmations int `json:"confirmations,string"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + Hash string `json:"hash"` + Nonce int `json:"nonce,string"` + BlockHash string `json:"blockHash"` + TransactionIndex int `json:"transactionIndex,string"` + From string `json:"from"` + To string `json:"to"` + Value *BigInt `json:"value"` + Gas int `json:"gas,string"` + GasPrice *BigInt `json:"gasPrice"` + IsError int `json:"isError,string"` + TxReceiptStatus string `json:"txreceipt_status"` + Input string `json:"input"` + ContractAddress string `json:"contractAddress"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + GasUsed int `json:"gasUsed,string"` + Confirmations int `json:"confirmations,string"` } // InternalTx holds info from internal tx query type InternalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - From string `json:"from"` - To string `json:"to"` - Value *big.Int `json:"value"` - ContractAddress string `json:"contractAddress"` - Input string `json:"input"` - Type string `json:"type"` - Gas int `json:"gas,string"` - GasUsed int `json:"gasUsed,string"` - TraceID string `json:"traceId"` - IsError int `json:"isError,string"` - ErrCode string `json:"errCode"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + Hash string `json:"hash"` + From string `json:"from"` + To string `json:"to"` + Value *BigInt `json:"value"` + ContractAddress string `json:"contractAddress"` + Input string `json:"input"` + Type string `json:"type"` + Gas int `json:"gas,string"` + GasUsed int `json:"gasUsed,string"` + TraceID string `json:"traceId"` + IsError int `json:"isError,string"` + ErrCode string `json:"errCode"` } // ERC20Transfer holds info from ERC20 token transfer event query type ERC20Transfer 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"` - Value *big.Int `json:"value"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal int `json:"tokenDecimal,string"` - TransactionIndex int `json:"transactionIndex,string"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - GasUsed int `json:"gasUsed,string"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - Input string `json:"input"` - Confirmations int `json:"confirmations,string"` + 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"` + Value *BigInt `json:"value"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,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"` } // ERC721Transfer holds info from ERC721 token transfer event query type ERC721Transfer 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 *big.Int `json:"tokenID"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal int `json:"tokenDecimal,string"` - TransactionIndex int `json:"transactionIndex,string"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - GasUsed int `json:"gasUsed,string"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - Input string `json:"input"` - Confirmations int `json:"confirmations,string"` + 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 int `json:"tokenDecimal,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"` } // 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 *big.Int `json:"tokenID"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal int `json:"tokenDecimal,string"` - TokenValue int `json:"tokenValue,string"` - TransactionIndex int `json:"transactionIndex,string"` - Gas int `json:"gas,string"` - GasPrice *big.Int `json:"gasPrice"` - GasUsed int `json:"gasUsed,string"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - Input string `json:"input"` - Confirmations int `json:"confirmations,string"` + 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 int `json:"tokenDecimal,string"` + TokenValue int `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"` - TimeStamp Time `json:"timeStamp"` - BlockReward *big.Int `json:"blockReward"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + BlockReward *BigInt `json:"blockReward"` } // ContractSource holds info from query for contract source code @@ -174,16 +173,16 @@ type ExecutionStatus struct { // BlockRewards holds info from query for block and uncle rewards type BlockRewards struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - BlockMiner string `json:"blockMiner"` - BlockReward *big.Int `json:"blockReward"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + BlockMiner string `json:"blockMiner"` + BlockReward *BigInt `json:"blockReward"` Uncles []struct { - Miner string `json:"miner"` - UnclePosition int `json:"unclePosition,string"` - BlockReward *big.Int `json:"blockreward"` + Miner string `json:"miner"` + UnclePosition int `json:"unclePosition,string"` + BlockReward *BigInt `json:"blockreward"` } `json:"uncles"` - UncleInclusionReward *big.Int `json:"uncleInclusionReward"` + UncleInclusionReward *BigInt `json:"uncleInclusionReward"` } // LatestPrice holds info from query for latest ether price From 5a3215356b81aa3fde4b9511a78a05667e627cc9 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 23 Mar 2023 14:14:17 +0100 Subject: [PATCH 07/21] fix: remove all unused fields from erc1155 to remove complexity --- helper.go | 2 ++ response.go | 40 ++++++++++++++++++++-------------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/helper.go b/helper.go index f8c3502..4b39c5e 100644 --- a/helper.go +++ b/helper.go @@ -44,11 +44,13 @@ type BigInt big.Int // UnmarshalText implements the encoding.TextUnmarshaler interface. func (b *BigInt) UnmarshalText(text []byte) error { var bigInt = new(big.Int) + if string(text) == "" { bigInt.SetInt64(0) *b = BigInt(*bigInt) return nil } + if err := bigInt.UnmarshalText(text); err != nil { return err } diff --git a/response.go b/response.go index 489b533..686aa6f 100644 --- a/response.go +++ b/response.go @@ -118,26 +118,26 @@ type ERC721Transfer struct { // 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 int `json:"tokenDecimal,string"` - TokenValue int `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"` + 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 int `json:"tokenDecimal,string"` + //TokenValue int `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 From aad6682e864cb7080784532583f648b4ea9d0fb9 Mon Sep 17 00:00:00 2001 From: markusiewiczj Date: Tue, 10 Oct 2023 21:40:02 +0200 Subject: [PATCH 08/21] feat: add EtherscanTx interface --- response.go | 233 +++++++++++++++++++++++++++++----------------------- 1 file changed, 128 insertions(+), 105 deletions(-) diff --git a/response.go b/response.go index 686aa6f..c4bf416 100644 --- a/response.go +++ b/response.go @@ -30,115 +30,138 @@ type AccountBalance struct { Balance *BigInt `json:"balance"` } -// NormalTx holds info from normal tx query -type NormalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - Nonce int `json:"nonce,string"` - BlockHash string `json:"blockHash"` - TransactionIndex int `json:"transactionIndex,string"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - Gas int `json:"gas,string"` - GasPrice *BigInt `json:"gasPrice"` - IsError int `json:"isError,string"` - TxReceiptStatus string `json:"txreceipt_status"` - Input string `json:"input"` - ContractAddress string `json:"contractAddress"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - GasUsed int `json:"gasUsed,string"` - Confirmations int `json:"confirmations,string"` -} +type ( + // EtherscanTx represents + EtherscanTx interface { + GetBlockNumber() int + GetHash() string + } -// InternalTx holds info from internal tx query -type InternalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - ContractAddress string `json:"contractAddress"` - Input string `json:"input"` - Type string `json:"type"` - Gas int `json:"gas,string"` - GasUsed int `json:"gasUsed,string"` - TraceID string `json:"traceId"` - IsError int `json:"isError,string"` - ErrCode string `json:"errCode"` -} + // NormalTx holds info from normal tx query + NormalTx struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + Hash string `json:"hash"` + Nonce int `json:"nonce,string"` + BlockHash string `json:"blockHash"` + TransactionIndex int `json:"transactionIndex,string"` + From string `json:"from"` + To string `json:"to"` + Value *BigInt `json:"value"` + Gas int `json:"gas,string"` + GasPrice *BigInt `json:"gasPrice"` + IsError int `json:"isError,string"` + TxReceiptStatus string `json:"txreceipt_status"` + Input string `json:"input"` + ContractAddress string `json:"contractAddress"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + GasUsed int `json:"gasUsed,string"` + Confirmations int `json:"confirmations,string"` + } -// ERC20Transfer holds info from ERC20 token transfer event query -type ERC20Transfer 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"` - Value *BigInt `json:"value"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal int `json:"tokenDecimal,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"` -} + // InternalTx holds info from internal tx query + InternalTx struct { + BlockNumber int `json:"blockNumber,string"` + TimeStamp Time `json:"timeStamp"` + Hash string `json:"hash"` + From string `json:"from"` + To string `json:"to"` + Value *BigInt `json:"value"` + ContractAddress string `json:"contractAddress"` + Input string `json:"input"` + Type string `json:"type"` + Gas int `json:"gas,string"` + GasUsed int `json:"gasUsed,string"` + TraceID string `json:"traceId"` + IsError int `json:"isError,string"` + ErrCode string `json:"errCode"` + } -// ERC721Transfer holds info from ERC721 token transfer event query -type ERC721Transfer 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 int `json:"tokenDecimal,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"` -} + // ERC20Transfer holds info from ERC20 token transfer event query + ERC20Transfer 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"` + Value *BigInt `json:"value"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,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"` + } -// 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 int `json:"tokenDecimal,string"` - //TokenValue int `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"` -} + // ERC721Transfer holds info from ERC721 token transfer event query + ERC721Transfer 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 int `json:"tokenDecimal,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"` + } + + // ERC1155Transfer holds info from ERC1155 token transfer event query + 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 int `json:"tokenDecimal,string"` + //TokenValue int `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"` + } +) + +func (tx NormalTx) GetBlockNumber() int { return tx.BlockNumber } +func (tx NormalTx) GetHash() string { return tx.Hash } + +func (tx InternalTx) GetBlockNumber() int { return tx.BlockNumber } +func (tx InternalTx) GetHash() string { return tx.Hash } + +func (tx ERC20Transfer) GetBlockNumber() int { return tx.BlockNumber } +func (tx ERC20Transfer) GetHash() string { return tx.Hash } + +func (tx ERC721Transfer) GetBlockNumber() int { return tx.BlockNumber } +func (tx ERC721Transfer) GetHash() string { return tx.Hash } + +func (tx ERC1155Transfer) GetBlockNumber() int { return tx.BlockNumber } +func (tx ERC1155Transfer) GetHash() string { return tx.Hash } // MinedBlock holds info from query for mined block by address type MinedBlock struct { From 96e648c7e52c2a2b29106163616d785f92edd6af Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Thu, 5 Dec 2024 13:15:03 +0100 Subject: [PATCH 09/21] chore(repo): bump go to 1.23 * add pkg/errors dep * add testify dep --- go.mod | 14 ++++++++++++-- go.sum | 14 ++++++++++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c613a9f..5b8bb93 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,15 @@ module github.com/timcki/etherscan-api -go 1.18 +go 1.23.0 -require github.com/google/go-cmp v0.5.7 +require ( + github.com/google/go-cmp v0.5.7 + github.com/pkg/errors v0.9.1 + github.com/stretchr/testify v1.10.0 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum index a6ca3a4..74bb85f 100644 --- a/go.sum +++ b/go.sum @@ -1,4 +1,18 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 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= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= From 7ee4e90daba137e4e6c2ff95c5690a191e9c3dbb Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Thu, 5 Dec 2024 13:21:56 +0100 Subject: [PATCH 10/21] chore(repo): remove error_wrap --- error_wrap.go | 35 ----------------------------------- error_wrap_test.go | 24 ------------------------ 2 files changed, 59 deletions(-) delete mode 100644 error_wrap.go delete mode 100644 error_wrap_test.go diff --git a/error_wrap.go b/error_wrap.go deleted file mode 100644 index 36ea869..0000000 --- a/error_wrap.go +++ /dev/null @@ -1,35 +0,0 @@ -/* - * 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 ( - "fmt" -) - -// wrapErr gives error some context msg -// returns nil if err is nil -func wrapErr(err error, msg string) (errWithContext error) { - if err == nil { - return - } - - errWithContext = fmt.Errorf("%s: %v", msg, err) - return -} - -// wrapfErr gives error some context msg -// with desired format and content -// returns nil if err is nil -func wrapfErr(err error, format string, a ...interface{}) (errWithContext error) { - if err == nil { - return - } - - errWithContext = wrapErr(err, fmt.Sprintf(format, a...)) - return -} diff --git a/error_wrap_test.go b/error_wrap_test.go deleted file mode 100644 index 4cf437c..0000000 --- a/error_wrap_test.go +++ /dev/null @@ -1,24 +0,0 @@ -/* - * 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 ( - "errors" - "testing" -) - -func Test_wrapfErr(t *testing.T) { - const ans = "status 100: continue test" - - err := errors.New("continue test") - err = wrapfErr(err, "%s %v", "status", "100") - - if err.Error() != ans { - t.Fatalf("got %v, want %s", err, ans) - } -} From e642299f9ed46e719608d3b7a0321e1160938765 Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Thu, 5 Dec 2024 13:02:39 +0100 Subject: [PATCH 11/21] wip: better error management --- account.go | 194 ------------- account_e2e_test.go | 3 + client.go | 208 -------------- doc.go | 9 - network.go => internal/network/network.go | 8 +- helper.go => internal/types/types.go | 45 +--- pkg/client/account.go | 285 ++++++++++++++++++++ pkg/client/account_test.go | 255 ++++++++++++++++++ block.go => pkg/client/block.go | 2 +- pkg/client/client.go | 187 +++++++++++++ client_test.go => pkg/client/client_test.go | 6 +- contract.go => pkg/client/contract.go | 2 +- gas_tracker.go => pkg/client/gas_tracker.go | 2 +- logs.go => pkg/client/logs.go | 2 +- response.go => pkg/response/response.go | 222 ++++++++------- 15 files changed, 879 insertions(+), 551 deletions(-) delete mode 100644 account.go delete mode 100644 client.go delete mode 100644 doc.go rename network.go => internal/network/network.go (97%) rename helper.go => internal/types/types.go (64%) create mode 100644 pkg/client/account.go create mode 100644 pkg/client/account_test.go rename block.go => pkg/client/block.go (98%) create mode 100644 pkg/client/client.go rename client_test.go => pkg/client/client_test.go (85%) rename contract.go => pkg/client/contract.go (97%) rename gas_tracker.go => pkg/client/gas_tracker.go (97%) rename logs.go => pkg/client/logs.go (96%) rename response.go => pkg/response/response.go (51%) diff --git a/account.go b/account.go deleted file mode 100644 index e6a9ac1..0000000 --- a/account.go +++ /dev/null @@ -1,194 +0,0 @@ -/* - * 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 - -// AccountBalance gets ether balance for a single address -func (c *Client) AccountBalance(address string) (balance *BigInt, err error) { - param := M{ - "tag": "latest", - "address": address, - } - balance = new(BigInt) - err = c.call("account", "balance", param, balance) - return -} - -// MultiAccountBalance gets ether balance for multiple addresses in a single call -func (c *Client) MultiAccountBalance(addresses ...string) (balances []AccountBalance, err error) { - param := M{ - "tag": "latest", - "address": addresses, - } - balances = make([]AccountBalance, 0, len(addresses)) - err = c.call("account", "balancemulti", param, &balances) - return -} - -// NormalTxByAddress gets a list of "normal" transactions by address -// -// startBlock and endBlock can be nil -// -// if desc is true, result will be sorted in blockNum descendant order. -func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []NormalTx, err error) { - param := M{ - "address": address, - "page": page, - "offset": offset, - } - compose(param, "startblock", startBlock) - compose(param, "endblock", endBlock) - if desc { - param["sort"] = "desc" - } else { - param["sort"] = "asc" - } - - err = c.call("account", "txlist", param, &txs) - return -} - -// InternalTxByAddress gets a list of "internal" transactions by address -// -// startBlock and endBlock can be nil -// -// if desc is true, result will be sorted in descendant order. -func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []InternalTx, err error) { - param := M{ - "address": address, - "page": page, - "offset": offset, - } - compose(param, "startblock", startBlock) - compose(param, "endblock", endBlock) - if desc { - param["sort"] = "desc" - } else { - param["sort"] = "asc" - } - - err = c.call("account", "txlistinternal", param, &txs) - return -} - -// ERC20Transfers get a list of "erc20 - token transfer events" by -// contract address and/or from/to address. -// -// leave undesired condition to nil. -// -// Note on a Etherscan bug: -// Some ERC20 contract does not have valid decimals information in Etherscan. -// When that happens, TokenName, TokenSymbol are empty strings, -// and TokenDecimal is 0. -// -// More information can be found at: -// https://github.com/nanmu42/etherscan-api/issues/8 -func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC20Transfer, 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", "tokentx", param, &txs) - return -} - -// ERC721Transfers get a list of "erc721 - token transfer events" by -// contract address and/or from/to address. -// -// leave undesired condition to nil. -func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC721Transfer, 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", "tokennfttx", param, &txs) - 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{ - "address": address, - "blocktype": "blocks", - "page": page, - "offset": offset, - } - - err = c.call("account", "getminedblocks", param, &mined) - return -} - -// UnclesMinedByAddress gets list of uncles mined by address -func (c *Client) UnclesMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { - param := M{ - "address": address, - "blocktype": "uncles", - "page": page, - "offset": offset, - } - - err = c.call("account", "getminedblocks", param, &mined) - return -} - -// TokenBalance get erc20-token account balance of address for contractAddress -func (c *Client) TokenBalance(contractAddress, address string) (balance *BigInt, err error) { - param := M{ - "contractaddress": contractAddress, - "address": address, - "tag": "latest", - } - - err = c.call("account", "tokenbalance", param, &balance) - return -} diff --git a/account_e2e_test.go b/account_e2e_test.go index f7688e3..842b84b 100644 --- a/account_e2e_test.go +++ b/account_e2e_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * diff --git a/client.go b/client.go deleted file mode 100644 index e69679a..0000000 --- a/client.go +++ /dev/null @@ -1,208 +0,0 @@ -/* - * 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 ( - "bytes" - "encoding/json" - "fmt" - "io" - "net/http" - "net/http/httputil" - "net/url" - "time" -) - -// Client etherscan API client -// Clients are safe for concurrent use by multiple goroutines. -type Client struct { - coon *http.Client - key string - baseURL string - - // Verbose when true, talks a lot - Verbose bool - - // BeforeRequest runs before every client request, in the same goroutine. - // May be used in rate limit. - // Request will be aborted, if BeforeRequest returns non-nil err. - BeforeRequest func(module, action string, param map[string]interface{}) error - - // AfterRequest runs after every client request, even when there is an error. - AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) -} - -// New initialize a new etherscan API client -// please use pre-defined network value -func New(network Network, APIKey string) *Client { - return NewCustomized(Customization{ - Timeout: 30 * time.Second, - Key: APIKey, - BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api?`, network.SubDomain()), - }) -} - -// Customization is used in NewCustomized() -type Customization struct { - // Timeout for API call - Timeout time.Duration - // API key applied from Etherscan - Key string - // Base URL like `https://api.etherscan.io/api?` - BaseURL string - // When true, talks a lot - Verbose bool - // HTTP Client to be used. Specifying this value will ignore the Timeout value set - // Set your own timeout. - Client *http.Client - - // BeforeRequest runs before every client request, in the same goroutine. - // May be used in rate limit. - // Request will be aborted, if BeforeRequest returns non-nil err. - BeforeRequest func(module, action string, param map[string]interface{}) error - - // AfterRequest runs after every client request, even when there is an error. - AfterRequest func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) -} - -// NewCustomized initialize a customized API client, -// useful when calling against etherscan-family API like BscScan. -func NewCustomized(config Customization) *Client { - var httpClient *http.Client - if config.Client != nil { - httpClient = config.Client - } else { - httpClient = &http.Client{ - Timeout: config.Timeout, - } - } - return &Client{ - coon: httpClient, - key: config.Key, - baseURL: config.BaseURL, - Verbose: config.Verbose, - BeforeRequest: config.BeforeRequest, - AfterRequest: config.AfterRequest, - } -} - -// call does almost all the dirty work. -func (c *Client) call(module, action string, param map[string]interface{}, outcome interface{}) (err error) { - // fire hooks if in need - if c.BeforeRequest != nil { - err = c.BeforeRequest(module, action, param) - if err != nil { - err = wrapErr(err, "beforeRequest") - return - } - } - if c.AfterRequest != nil { - defer c.AfterRequest(module, action, param, outcome, err) - } - - // recover if there shall be an panic - defer func() { - if r := recover(); r != nil { - err = fmt.Errorf("[ouch! panic recovered] please report this with what you did and what you expected, panic detail: %v", r) - } - }() - - req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, param), http.NoBody) - if err != nil { - err = wrapErr(err, "http.NewRequest") - return - } - req.Header.Set("User-Agent", "etherscan-api(Go)") - req.Header.Set("Content-Type", "application/json; charset=utf-8") - - if c.Verbose { - var reqDump []byte - reqDump, err = httputil.DumpRequestOut(req, false) - if err != nil { - err = wrapErr(err, "verbose mode req dump failed") - return - } - - fmt.Printf("\n%s\n", reqDump) - - defer func() { - if err != nil { - fmt.Printf("[Error] %v\n", err) - } - }() - } - - res, err := c.coon.Do(req) - if err != nil { - err = wrapErr(err, "sending request") - return - } - defer res.Body.Close() - - if c.Verbose { - var resDump []byte - resDump, err = httputil.DumpResponse(res, true) - if err != nil { - err = wrapErr(err, "verbose mode res dump failed") - return - } - - fmt.Printf("%s\n", resDump) - } - - var content bytes.Buffer - if _, err = io.Copy(&content, res.Body); err != nil { - err = wrapErr(err, "reading response") - return - } - - if res.StatusCode != http.StatusOK { - err = fmt.Errorf("response status %v %s, response body: %s", res.StatusCode, res.Status, content.String()) - return - } - - var envelope Envelope - err = json.Unmarshal(content.Bytes(), &envelope) - if err != nil { - err = wrapErr(err, "json unmarshal envelope") - return - } - if envelope.Status != 1 { - err = fmt.Errorf("etherscan server: %s", envelope.Message) - return - } - - // workaround for missing tokenDecimal for some tokentx calls - if action == "tokentx" { - err = json.Unmarshal(bytes.Replace(envelope.Result, []byte(`"tokenDecimal":""`), []byte(`"tokenDecimal":"0"`), -1), outcome) - } else { - err = json.Unmarshal(envelope.Result, outcome) - } - if err != nil { - err = wrapErr(err, "json unmarshal outcome") - return - } - - return -} - -// craftURL returns desired URL via param provided -func (c *Client) craftURL(module, action string, param map[string]interface{}) (URL string) { - q := url.Values{ - "module": []string{module}, - "action": []string{action}, - "apikey": []string{c.key}, - } - - for k, v := range param { - q[k] = extractValue(v) - } - - URL = c.baseURL + q.Encode() - return -} diff --git a/doc.go b/doc.go deleted file mode 100644 index d345cab..0000000 --- a/doc.go +++ /dev/null @@ -1,9 +0,0 @@ -// Package etherscan provides Go bindings to the Etherscan.io API. -// -// This work is a nearly Full implementation -// (accounts, transactions, tokens, contracts, blocks, stats), -// with full network support(Mainnet, Ropsten, Kovan, Rinkby, Tobalaba), -// and only depending on standard library. -// -// Example can be found at https://github.com/nanmu42/etherscan-api -package etherscan diff --git a/network.go b/internal/network/network.go similarity index 97% rename from network.go rename to internal/network/network.go index 32372ca..8a07ba6 100644 --- a/network.go +++ b/internal/network/network.go @@ -5,7 +5,10 @@ * You may find a license copy in project root. */ -package etherscan +package network + +// Network is ethereum network type (mainnet, ropsten, etc) +type Network string const ( //// Ethereum public networks @@ -24,9 +27,6 @@ const ( Tobalaba Network = "api-tobalaba" ) -// Network is ethereum network type (mainnet, ropsten, etc) -type Network string - // SubDomain returns the subdomain of etherscan API // via n provided. func (n Network) SubDomain() (sub string) { diff --git a/helper.go b/internal/types/types.go similarity index 64% rename from helper.go rename to internal/types/types.go index 4b39c5e..ff94fd0 100644 --- a/helper.go +++ b/internal/types/types.go @@ -5,37 +5,15 @@ * You may find a license copy in project root. */ -package etherscan +package types import ( "math/big" - "reflect" "strconv" "time" -) - -// compose adds input to param, whose key is tag -// if input is nil or nil of some type, compose is a no-op. -func compose(param map[string]interface{}, tag string, input interface{}) { - // simple situation - if input == nil { - return - } - - // needs dig further - v := reflect.ValueOf(input) - switch v.Kind() { - case reflect.Ptr, reflect.Slice, reflect.Interface: - if v.IsNil() { - return - } - } - - param[tag] = input -} -// M is a type shorthand for param input -type M map[string]interface{} + "github.com/pkg/errors" +) // BigInt is a wrapper over big.Int to implement only unmarshalText // for json decoding. @@ -59,26 +37,23 @@ func (b *BigInt) UnmarshalText(text []byte) error { return nil } +// Int returns b's *big.Int form +func (b *BigInt) Int() *big.Int { return (*big.Int)(b) } + // MarshalText implements the encoding.TextMarshaler func (b *BigInt) MarshalText() ([]byte, error) { return []byte(b.Int().String()), nil } -// Int returns b's *big.Int form -func (b *BigInt) Int() *big.Int { - return (*big.Int)(b) -} - // Time is a wrapper over big.Int to implement only unmarshalText // for json decoding. type Time time.Time // UnmarshalText implements the encoding.TextUnmarshaler interface. -func (t *Time) UnmarshalText(text []byte) (err error) { +func (t *Time) UnmarshalText(text []byte) error { input, err := strconv.ParseInt(string(text), 10, 64) if err != nil { - err = wrapErr(err, "strconv.ParseInt") - return + return errors.Wrap(err, "strconv.ParseInt") } var timestamp = time.Unix(input, 0) @@ -88,9 +63,7 @@ func (t *Time) UnmarshalText(text []byte) (err error) { } // Time returns t's time.Time form -func (t Time) Time() time.Time { - return time.Time(t) -} +func (t Time) Time() time.Time { return time.Time(t) } // MarshalText implements the encoding.TextMarshaler func (t Time) MarshalText() (text []byte, err error) { diff --git a/pkg/client/account.go b/pkg/client/account.go new file mode 100644 index 0000000..e66c9b7 --- /dev/null +++ b/pkg/client/account.go @@ -0,0 +1,285 @@ +/* + * 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 client + +import ( + "net/url" + "strconv" + "strings" + + "github.com/timcki/etherscan-api/internal/types" + "github.com/timcki/etherscan-api/pkg/response" +) + +type AccountBalanceParams struct { + Tag string `json:"tag"` + Address string `json:"address"` +} + +type MultiAccountBalanceParams struct { + Tag string `json:"tag"` + Addresses []string `json:"address"` +} + +type TxListParams struct { + Address string `json:"address"` + StartBlock *int `json:"startblock,omitempty"` + EndBlock *int `json:"endblock,omitempty"` + Page int `json:"page"` + Offset int `json:"offset"` + Sort string `json:"sort"` +} + +type TokenTransferParams struct { + ContractAddress *string `json:"contractaddress,omitempty"` + Address *string `json:"address,omitempty"` + StartBlock *int `json:"startblock,omitempty"` + EndBlock *int `json:"endblock,omitempty"` + Page int `json:"page"` + Offset int `json:"offset"` + Sort string `json:"sort"` +} + +type MinedBlockParams struct { + Address string `json:"address"` + BlockType string `json:"blocktype"` + Page int `json:"page"` + Offset int `json:"offset"` +} + +type TokenBalanceParams struct { + ContractAddress string `json:"contractaddress"` + Address string `json:"address"` + Tag string `json:"tag"` +} + +func (p AccountBalanceParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Tag != "" { + values.Add("tag", p.Tag) + } + if p.Address != "" { + values.Add("address", p.Address) + } + return values +} + +func (p MultiAccountBalanceParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Tag != "" { + values.Add("tag", p.Tag) + } + if len(p.Addresses) > 0 { + values.Add("address", strings.Join(p.Addresses, ",")) + } + return values +} + +func (p TxListParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Address != "" { + values.Add("address", p.Address) + } + if p.StartBlock != nil { + values.Add("startblock", strconv.Itoa(*p.StartBlock)) + } + if p.EndBlock != nil { + values.Add("endblock", strconv.Itoa(*p.EndBlock)) + } + values.Add("page", strconv.Itoa(p.Page)) + values.Add("offset", strconv.Itoa(p.Offset)) + if p.Sort != "" { + values.Add("sort", p.Sort) + } + return values +} + +func (p TokenTransferParams) GetUrlValues() url.Values { + values := url.Values{} + if p.ContractAddress != nil { + values.Add("contractaddress", *p.ContractAddress) + } + if p.Address != nil { + values.Add("address", *p.Address) + } + if p.StartBlock != nil { + values.Add("startblock", strconv.Itoa(*p.StartBlock)) + } + if p.EndBlock != nil { + values.Add("endblock", strconv.Itoa(*p.EndBlock)) + } + values.Add("page", strconv.Itoa(p.Page)) + values.Add("offset", strconv.Itoa(p.Offset)) + if p.Sort != "" { + values.Add("sort", p.Sort) + } + return values +} + +func (p MinedBlockParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Address != "" { + values.Add("address", p.Address) + } + if p.BlockType != "" { + values.Add("blocktype", p.BlockType) + } + values.Add("page", strconv.Itoa(p.Page)) + values.Add("offset", strconv.Itoa(p.Offset)) + return values +} + +func (p TokenBalanceParams) GetUrlValues() url.Values { + values := url.Values{} + if p.ContractAddress != "" { + values.Add("contractaddress", p.ContractAddress) + } + if p.Address != "" { + values.Add("address", p.Address) + } + if p.Tag != "" { + values.Add("tag", p.Tag) + } + return values +} + +// Refactored methods +func (c *Client) AccountBalance(address string) (balance *types.BigInt, err error) { + param := AccountBalanceParams{ + Tag: "latest", + Address: address, + } + balance = new(types.BigInt) + err = c.call("account", "balance", param, balance) + return +} + +func (c *Client) MultiAccountBalance(addresses ...string) (balances []response.AccountBalance, err error) { + param := MultiAccountBalanceParams{ + Tag: "latest", + Addresses: addresses, + } + //balances = make([]response.AccountBalance, 0, len(addresses)) + err = c.call("account", "balancemulti", param, &balances) + return +} + +func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []NormalTx, err error) { + param := TxListParams{ + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + err = c.call("account", "txlist", param, &txs) + return +} + +func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []InternalTx, err error) { + param := TxListParams{ + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + err = c.call("account", "txlistinternal", param, &txs) + return +} + +func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC20Transfer, err error) { + param := TokenTransferParams{ + ContractAddress: contractAddress, + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + err = c.call("account", "tokentx", param, &txs) + return +} + +func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC721Transfer, err error) { + param := TokenTransferParams{ + ContractAddress: contractAddress, + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + err = c.call("account", "tokennfttx", param, &txs) + return +} + +func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC1155Transfer, err error) { + param := TokenTransferParams{ + ContractAddress: contractAddress, + Address: address, + StartBlock: startBlock, + EndBlock: endBlock, + Page: page, + Offset: offset, + Sort: "asc", + } + if desc { + param.Sort = "desc" + } + err = c.call("account", "token1155tx", param, &txs) + return +} + +func (c *Client) BlocksMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { + param := MinedBlockParams{ + Address: address, + BlockType: "blocks", + Page: page, + Offset: offset, + } + err = c.call("account", "getminedblocks", param, &mined) + return +} + +func (c *Client) UnclesMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { + param := MinedBlockParams{ + Address: address, + BlockType: "uncles", + Page: page, + Offset: offset, + } + err = c.call("account", "getminedblocks", param, &mined) + return +} + +func (c *Client) TokenBalance(contractAddress, address string) (balance *BigInt, err error) { + param := TokenBalanceParams{ + ContractAddress: contractAddress, + Address: address, + Tag: "latest", + } + err = c.call("account", "tokenbalance", param, &balance) + return +} diff --git a/pkg/client/account_test.go b/pkg/client/account_test.go new file mode 100644 index 0000000..aa59de6 --- /dev/null +++ b/pkg/client/account_test.go @@ -0,0 +1,255 @@ +package client + +import ( + "net/url" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestAccountBalanceParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params AccountBalanceParams + expected url.Values + }{ + { + name: "empty params", + params: AccountBalanceParams{}, + expected: url.Values{}, + }, + { + name: "full params", + params: AccountBalanceParams{ + Tag: "latest", + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + }, + expected: url.Values{ + "tag": []string{"latest"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMultiAccountBalanceParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params MultiAccountBalanceParams + expected url.Values + }{ + { + name: "empty params", + params: MultiAccountBalanceParams{}, + expected: url.Values{}, + }, + { + name: "full params", + params: MultiAccountBalanceParams{ + Tag: "latest", + Addresses: []string{ + "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + "0x742d35Cc6634C0532925a3b844Bc454e4438f44f", + }, + }, + expected: url.Values{ + "tag": []string{"latest"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e,0x742d35Cc6634C0532925a3b844Bc454e4438f44f"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTxListParams_GetUrlValues(t *testing.T) { + startBlock := 12345 + endBlock := 12346 + + tests := []struct { + name string + params TxListParams + expected url.Values + }{ + { + name: "minimal params", + params: TxListParams{ + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + { + name: "full params", + params: TxListParams{ + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + StartBlock: &startBlock, + EndBlock: &endBlock, + Page: 1, + Offset: 10, + Sort: "asc", + }, + expected: url.Values{ + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "startblock": []string{"12345"}, + "endblock": []string{"12346"}, + "page": []string{"1"}, + "offset": []string{"10"}, + "sort": []string{"asc"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTokenTransferParams_GetUrlValues(t *testing.T) { + startBlock := 12345 + endBlock := 12346 + contractAddr := "0x742d35Cc6634C0532925a3b844Bc454e4438f44e" + address := "0x742d35Cc6634C0532925a3b844Bc454e4438f44f" + + tests := []struct { + name string + params TokenTransferParams + expected url.Values + }{ + { + name: "minimal params", + params: TokenTransferParams{ + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + { + name: "full params", + params: TokenTransferParams{ + ContractAddress: &contractAddr, + Address: &address, + StartBlock: &startBlock, + EndBlock: &endBlock, + Page: 1, + Offset: 10, + Sort: "asc", + }, + expected: url.Values{ + "contractaddress": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44f"}, + "startblock": []string{"12345"}, + "endblock": []string{"12346"}, + "page": []string{"1"}, + "offset": []string{"10"}, + "sort": []string{"asc"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestMinedBlockParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params MinedBlockParams + expected url.Values + }{ + { + name: "minimal params", + params: MinedBlockParams{ + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + { + name: "full params", + params: MinedBlockParams{ + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + BlockType: "blocks", + Page: 1, + Offset: 10, + }, + expected: url.Values{ + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "blocktype": []string{"blocks"}, + "page": []string{"1"}, + "offset": []string{"10"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestTokenBalanceParams_GetUrlValues(t *testing.T) { + tests := []struct { + name string + params TokenBalanceParams + expected url.Values + }{ + { + name: "empty params", + params: TokenBalanceParams{}, + expected: url.Values{}, + }, + { + name: "full params", + params: TokenBalanceParams{ + ContractAddress: "0x742d35Cc6634C0532925a3b844Bc454e4438f44e", + Address: "0x742d35Cc6634C0532925a3b844Bc454e4438f44f", + Tag: "latest", + }, + expected: url.Values{ + "contractaddress": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44e"}, + "address": []string{"0x742d35Cc6634C0532925a3b844Bc454e4438f44f"}, + "tag": []string{"latest"}, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := tt.params.GetUrlValues() + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/block.go b/pkg/client/block.go similarity index 98% rename from block.go rename to pkg/client/block.go index aea4ea1..0b090c0 100644 --- a/block.go +++ b/pkg/client/block.go @@ -5,7 +5,7 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "fmt" diff --git a/pkg/client/client.go b/pkg/client/client.go new file mode 100644 index 0000000..851d52e --- /dev/null +++ b/pkg/client/client.go @@ -0,0 +1,187 @@ +/* + * 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 client + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "net/http/httputil" + "net/url" + "time" + + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/internal/network" +) + +type ( + // Client etherscan API client + // Clients are safe for concurrent use by multiple goroutines. + Client struct { + conn *http.Client + key string + baseURL string + + // Verbose when true, talks a lot + Verbose bool + + // BeforeRequest runs before every client request, in the same goroutine. + // May be used in rate limit. + // Request will be aborted, if BeforeRequest returns non-nil err. + BeforeRequest func(module, action string, values url.Values) error + + // AfterRequest runs after every client request, even when there is an error. + AfterRequest func(module, action string, values url.Values, outcome interface{}, requestErr error) error + } + + // Customization is used in NewCustomized() + Customization struct { + // Timeout for API call + Timeout time.Duration + // API key applied from Etherscan + Key string + // Base URL like `https://api.etherscan.io/api?` + BaseURL string + // When true, talks a lot + Verbose bool + // HTTP Client to be used. Specifying this value will ignore the Timeout value set + // Set your own timeout. + Client *http.Client + + // BeforeRequest runs before every client request, in the same goroutine. + // May be used in rate limit. + // Request will be aborted, if BeforeRequest returns non-nil err. + BeforeRequest func(module, action string, values url.Values) error + + // AfterRequest runs after every client request, even when there is an error. + AfterRequest func(module, action string, values url.Values, outcome interface{}, requestErr error) error + } +) + +// NewClient initialize a new etherscan API client +// please use pre-defined network value +func NewClient(network network.Network, APIKey string) *Client { + return NewCustomized(Customization{ + Timeout: 30 * time.Second, + Key: APIKey, + BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api`, network.SubDomain()), + }) +} + +// NewCustomized initialize a customized API client, +// useful when calling against etherscan-family API like BscScan. +func NewCustomized(config Customization) *Client { + var httpClient *http.Client + if config.Client != nil { + httpClient = config.Client + } else { + httpClient = &http.Client{Timeout: config.Timeout} + } + return &Client{ + conn: httpClient, + key: config.Key, + baseURL: config.BaseURL, + Verbose: config.Verbose, + BeforeRequest: config.BeforeRequest, + AfterRequest: config.AfterRequest, + } +} + +func (c *Client) innerCall(module, action string, values url.Values, outcome any) error { + req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, values), http.NoBody) + if err != nil { + return errors.Wrap(err, "creating request") + } + req.Header.Set("User-Agent", "etherscan-api(Go)") + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + if c.Verbose { + reqDump, err := httputil.DumpRequestOut(req, false) + if err != nil { + return errors.Wrap(err, "verbose mode: dumping request") + } + + fmt.Printf("\n%s\n", reqDump) + + defer func() { + if err != nil { + fmt.Printf("[Error] %v\n", err) + } + }() + } + + res, err := c.conn.Do(req) + if err != nil { + return errors.Wrap(err, "sending request") + } + defer res.Body.Close() + + if c.Verbose { + resDump, err := httputil.DumpResponse(res, true) + if err != nil { + return errors.Wrap(err, "verbose mode:dumping response") + } + + fmt.Printf("%s\n", resDump) + } + + if res.StatusCode != http.StatusOK { + return errors.Errorf("got non-200 status code; status: %v, status text: %s, response body: %s", res.StatusCode, res.Status, content.String()) + } + + var content bytes.Buffer + if _, err = io.Copy(&content, res.Body); err != nil { + return errors.Wrap(err, "reading response") + } + + var envelope Envelope + if err = json.Unmarshal(content.Bytes(), &envelope); err != nil { + return errors.Wrap(err, "unmarshaling response") + } + if envelope.Status != 1 { + return errors.Errorf("etherscan server: %s", envelope.Message) + } + + // workaround for missing tokenDecimal for some tokentx calls + if action == "tokentx" { + err = json.Unmarshal(bytes.Replace(envelope.Result, []byte(`"tokenDecimal":""`), []byte(`"tokenDecimal":"0"`), -1), outcome) + } else { + err = json.Unmarshal(envelope.Result, outcome) + } + + return errors.Wrap(err, "unmarshaling result") +} + +// call executes the Before/AfterRequest hooks and executes innerCall in between +func (c *Client) call(module, action string, values url.Values, outcome interface{}) error { + // fire hooks if in need + if c.BeforeRequest != nil { + if err := c.BeforeRequest(module, action, values); err != nil { + return errors.Wrap(err, "running beforeRequest") + } + } + + err := c.innerCall(module, action, values, outcome) + if c.AfterRequest != nil { + if err := c.AfterRequest(module, action, param, outcome, err); err != nil { + return errors.Wrapf(err, "running afterRequest with request Error: %v", err) + } + } + return err +} + +// craftURL returns desired URL via param provided +func (c *Client) craftURL(module, action string, values url.Values) string { + values.Add("module", module) + values.Add("action", action) + values.Add("apikey", c.key) + + return fmt.Sprintf("%s?%s", c.baseURL, values.Encode()) +} diff --git a/client_test.go b/pkg/client/client_test.go similarity index 85% rename from client_test.go rename to pkg/client/client_test.go index a8d045c..bd9afd9 100644 --- a/client_test.go +++ b/pkg/client/client_test.go @@ -5,14 +5,16 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "testing" + + "github.com/timcki/etherscan-api/internal/types" ) func TestClient_craftURL(t *testing.T) { - c := New(Ropsten, "abc123") + c := NewClient(types.Ropsten, "abc123") const expected = `https://api-ropsten.etherscan.io/api?action=craftURL&apikey=abc123&four=d&four=e&four=f&module=testing&one=1&three=1&three=2&three=3&two=2` output := c.craftURL("testing", "craftURL", M{ diff --git a/contract.go b/pkg/client/contract.go similarity index 97% rename from contract.go rename to pkg/client/contract.go index 11e9e35..35b89ee 100644 --- a/contract.go +++ b/pkg/client/contract.go @@ -5,7 +5,7 @@ * You may find a license copy in project root. */ -package etherscan +package client // ContractABI gets contract abi for verified contract source codes func (c *Client) ContractABI(address string) (abi string, err error) { diff --git a/gas_tracker.go b/pkg/client/gas_tracker.go similarity index 97% rename from gas_tracker.go rename to pkg/client/gas_tracker.go index 02fe5ca..fe5e9ad 100644 --- a/gas_tracker.go +++ b/pkg/client/gas_tracker.go @@ -5,7 +5,7 @@ * You may find a license copy in project root. */ -package etherscan +package client import "time" diff --git a/logs.go b/pkg/client/logs.go similarity index 96% rename from logs.go rename to pkg/client/logs.go index 0b21124..0d30b5e 100644 --- a/logs.go +++ b/pkg/client/logs.go @@ -5,7 +5,7 @@ * You may find a license copy in project root. */ -package etherscan +package client // 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) { diff --git a/response.go b/pkg/response/response.go similarity index 51% rename from response.go rename to pkg/response/response.go index c4bf416..7ea84f0 100644 --- a/response.go +++ b/pkg/response/response.go @@ -5,29 +5,63 @@ * You may find a license copy in project root. */ -package etherscan +package response import ( + "bytes" "encoding/json" "fmt" + "io" + "net/http" "strconv" "strings" + + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/internal/types" ) -// Envelope is the carrier of nearly every response -type Envelope struct { +type EtherscanResponse interface { + AccountBalance | + NormalTx | InternalTx | + ERC20Transfer | ERC721Transfer | ERC1155Transfer | + MinedBlock | ContractSource | + ExecutionStatus | BlockRewards | + LatestPrice | Log | GasPrices +} + +// envelope is the carrier of nearly every response +type envelope[T EtherscanResponse] struct { // 1 for good, 0 for error Status int `json:"status,string"` // OK for good, other words when Status equals 0 Message string `json:"message"` // where response lies - Result json.RawMessage `json:"result"` + Result T `json:"result"` +} + +func ReadResponse[T EtherscanResponse](resp *http.Response) (T, error) { + var ret T + var content bytes.Buffer + if _, err := io.Copy(&content, resp.Body); err != nil { + return ret, errors.Wrap(err, "reading response") + } + + var envelope envelope[T] + if err := json.Unmarshal(content.Bytes(), &envelope); err != nil { + return ret, errors.Wrapf(err, "unmarshaling etherscan response; body=%s", content.Bytes()) + } + if envelope.Status != 1 { + return ret, errors.Errorf("etherscan server: %s", envelope.Message) + } + + return envelope.Result, nil + } // AccountBalance account and its balance in pair type AccountBalance struct { - Account string `json:"account"` - Balance *BigInt `json:"balance"` + Account string `json:"account"` + Balance *types.BigInt `json:"balance"` } type ( @@ -39,95 +73,95 @@ type ( // NormalTx holds info from normal tx query NormalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - Nonce int `json:"nonce,string"` - BlockHash string `json:"blockHash"` - TransactionIndex int `json:"transactionIndex,string"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - Gas int `json:"gas,string"` - GasPrice *BigInt `json:"gasPrice"` - IsError int `json:"isError,string"` - TxReceiptStatus string `json:"txreceipt_status"` - Input string `json:"input"` - ContractAddress string `json:"contractAddress"` - CumulativeGasUsed int `json:"cumulativeGasUsed,string"` - GasUsed int `json:"gasUsed,string"` - Confirmations int `json:"confirmations,string"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + Hash string `json:"hash"` + Nonce int `json:"nonce,string"` + BlockHash string `json:"blockHash"` + TransactionIndex int `json:"transactionIndex,string"` + From string `json:"from"` + To string `json:"to"` + Value *types.BigInt `json:"value"` + Gas int `json:"gas,string"` + GasPrice *types.BigInt `json:"gasPrice"` + IsError int `json:"isError,string"` + TxReceiptStatus string `json:"txreceipt_status"` + Input string `json:"input"` + ContractAddress string `json:"contractAddress"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + GasUsed int `json:"gasUsed,string"` + Confirmations int `json:"confirmations,string"` } // InternalTx holds info from internal tx query InternalTx struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` - From string `json:"from"` - To string `json:"to"` - Value *BigInt `json:"value"` - ContractAddress string `json:"contractAddress"` - Input string `json:"input"` - Type string `json:"type"` - Gas int `json:"gas,string"` - GasUsed int `json:"gasUsed,string"` - TraceID string `json:"traceId"` - IsError int `json:"isError,string"` - ErrCode string `json:"errCode"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + Hash string `json:"hash"` + From string `json:"from"` + To string `json:"to"` + Value *types.BigInt `json:"value"` + ContractAddress string `json:"contractAddress"` + Input string `json:"input"` + Type string `json:"type"` + Gas int `json:"gas,string"` + GasUsed int `json:"gasUsed,string"` + TraceID string `json:"traceId"` + IsError int `json:"isError,string"` + ErrCode string `json:"errCode"` } // ERC20Transfer holds info from ERC20 token transfer event query ERC20Transfer 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"` - Value *BigInt `json:"value"` - TokenName string `json:"tokenName"` - TokenSymbol string `json:"tokenSymbol"` - TokenDecimal int `json:"tokenDecimal,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"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.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"` + Value *types.BigInt `json:"value"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *types.BigInt `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + Confirmations int `json:"confirmations,string"` } // ERC721Transfer holds info from ERC721 token transfer event query ERC721Transfer 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 int `json:"tokenDecimal,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"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.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 *types.BigInt `json:"tokenID"` + TokenName string `json:"tokenName"` + TokenSymbol string `json:"tokenSymbol"` + TokenDecimal int `json:"tokenDecimal,string"` + TransactionIndex int `json:"transactionIndex,string"` + Gas int `json:"gas,string"` + GasPrice *types.BigInt `json:"gasPrice"` + GasUsed int `json:"gasUsed,string"` + CumulativeGasUsed int `json:"cumulativeGasUsed,string"` + Input string `json:"input"` + Confirmations int `json:"confirmations,string"` } // ERC1155Transfer holds info from ERC1155 token transfer event query ERC1155Transfer struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - Hash string `json:"hash"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + Hash string `json:"hash"` //Nonce int `json:"nonce,string"` BlockHash string `json:"blockHash"` From string `json:"from"` @@ -165,9 +199,9 @@ func (tx ERC1155Transfer) GetHash() string { return tx.Hash } // MinedBlock holds info from query for mined block by address type MinedBlock struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - BlockReward *BigInt `json:"blockReward"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + BlockReward *types.BigInt `json:"blockReward"` } // ContractSource holds info from query for contract source code @@ -196,24 +230,24 @@ type ExecutionStatus struct { // BlockRewards holds info from query for block and uncle rewards type BlockRewards struct { - BlockNumber int `json:"blockNumber,string"` - TimeStamp Time `json:"timeStamp"` - BlockMiner string `json:"blockMiner"` - BlockReward *BigInt `json:"blockReward"` + BlockNumber int `json:"blockNumber,string"` + TimeStamp types.Time `json:"timeStamp"` + BlockMiner string `json:"blockMiner"` + BlockReward *types.BigInt `json:"blockReward"` Uncles []struct { - Miner string `json:"miner"` - UnclePosition int `json:"unclePosition,string"` - BlockReward *BigInt `json:"blockreward"` + Miner string `json:"miner"` + UnclePosition int `json:"unclePosition,string"` + BlockReward *types.BigInt `json:"blockreward"` } `json:"uncles"` - UncleInclusionReward *BigInt `json:"uncleInclusionReward"` + UncleInclusionReward *types.BigInt `json:"uncleInclusionReward"` } // LatestPrice holds info from query for latest ether price type LatestPrice struct { - ETHBTC float64 `json:"ethbtc,string"` - ETHBTCTimestamp Time `json:"ethbtc_timestamp"` - ETHUSD float64 `json:"ethusd,string"` - ETHUSDTimestamp Time `json:"ethusd_timestamp"` + ETHBTC float64 `json:"ethbtc,string"` + ETHBTCTimestamp types.Time `json:"ethbtc_timestamp"` + ETHUSD float64 `json:"ethusd,string"` + ETHUSDTimestamp types.Time `json:"ethusd_timestamp"` } type Log struct { From d3c2f46301abe399452f200bfc62cde044d258f7 Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Wed, 18 Dec 2024 10:46:48 +0100 Subject: [PATCH 12/21] refactor(client): create structs for requests --- .../types/types_test.go | 6 +- pkg/client/account.go | 94 ++++++++++++------- pkg/client/block.go | 76 ++++++++++----- pkg/client/client.go | 58 +++++++++++- pkg/client/contract.go | 53 ++++++++--- pkg/client/gas_tracker.go | 54 ++++++++--- pkg/client/logs.go | 47 ++++++++-- pkg/response/response.go | 28 +++--- reflect.go | 58 ------------ 9 files changed, 309 insertions(+), 165 deletions(-) rename helper_test.go => internal/types/types_test.go (94%) delete mode 100644 reflect.go diff --git a/helper_test.go b/internal/types/types_test.go similarity index 94% rename from helper_test.go rename to internal/types/types_test.go index 31f9ef9..4f0a01f 100644 --- a/helper_test.go +++ b/internal/types/types_test.go @@ -5,12 +5,14 @@ * You may find a license copy in project root. */ -package etherscan +package types import ( "math/big" "testing" "time" + + "github.com/stretchr/testify/assert" ) func TestBigInt(t *testing.T) { @@ -19,6 +21,8 @@ func TestBigInt(t *testing.T) { b := new(BigInt) err := b.UnmarshalText([]byte(ansStr)) + + assert.E noError(t, err, "BigInt.UnmarshalText") if b.Int().Cmp(ans) != 0 { diff --git a/pkg/client/account.go b/pkg/client/account.go index e66c9b7..168bf3b 100644 --- a/pkg/client/account.go +++ b/pkg/client/account.go @@ -12,6 +12,7 @@ import ( "strconv" "strings" + "github.com/pkg/errors" "github.com/timcki/etherscan-api/internal/types" "github.com/timcki/etherscan-api/pkg/response" ) @@ -149,27 +150,31 @@ func (p TokenBalanceParams) GetUrlValues() url.Values { } // Refactored methods -func (c *Client) AccountBalance(address string) (balance *types.BigInt, err error) { +func (c *Client) AccountBalance(address string) (response.AccountBalance, error) { param := AccountBalanceParams{ Tag: "latest", Address: address, } - balance = new(types.BigInt) - err = c.call("account", "balance", param, balance) - return + body, err := c.execute("account", "balance", param.GetUrlValues()) + if err != nil { + return response.AccountBalance{}, errors.Wrap(err, "executing AccountBalance request") + } + return response.ReadResponse[response.AccountBalance](body) } -func (c *Client) MultiAccountBalance(addresses ...string) (balances []response.AccountBalance, err error) { +func (c *Client) MultiAccountBalance(addresses ...string) ([]response.AccountBalance, error) { param := MultiAccountBalanceParams{ Tag: "latest", Addresses: addresses, } - //balances = make([]response.AccountBalance, 0, len(addresses)) - err = c.call("account", "balancemulti", param, &balances) - return + body, err := c.execute("account", "balancemulti", param.GetUrlValues()) + if err != nil { + return []response.AccountBalance{}, errors.Wrap(err, "executing MultiAccountBalance request") + } + return response.ReadResponse[[]response.AccountBalance](body) } -func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []NormalTx, err error) { +func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.NormalTx, error) { param := TxListParams{ Address: address, StartBlock: startBlock, @@ -181,11 +186,14 @@ func (c *Client) NormalTxByAddress(address string, startBlock *int, endBlock *in if desc { param.Sort = "desc" } - err = c.call("account", "txlist", param, &txs) - return + body, err := c.execute("account", "txlist", param.GetUrlValues()) + if err != nil { + return []response.NormalTx{}, errors.Wrap(err, "executing NormalTxByAddress request") + } + return response.ReadResponse[[]response.NormalTx](body) } -func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []InternalTx, err error) { +func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.InternalTx, error) { param := TxListParams{ Address: address, StartBlock: startBlock, @@ -197,11 +205,15 @@ func (c *Client) InternalTxByAddress(address string, startBlock *int, endBlock * if desc { param.Sort = "desc" } - err = c.call("account", "txlistinternal", param, &txs) - return + + body, err := c.execute("account", "txlistinternal", param.GetUrlValues()) + if err != nil { + return []response.InternalTx{}, errors.Wrap(err, "executing InternalTxByAddress request") + } + return response.ReadResponse[[]response.InternalTx](body) } -func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC20Transfer, err error) { +func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.ERC20Transfer, error) { param := TokenTransferParams{ ContractAddress: contractAddress, Address: address, @@ -214,11 +226,14 @@ func (c *Client) ERC20Transfers(contractAddress, address *string, startBlock *in if desc { param.Sort = "desc" } - err = c.call("account", "tokentx", param, &txs) - return + body, err := c.execute("account", "tokentx", param.GetUrlValues()) + if err != nil { + return []response.ERC20Transfer{}, errors.Wrap(err, "executing ERC20Transfers request") + } + return response.ReadResponse[[]response.ERC20Transfer](body) } -func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC721Transfer, err error) { +func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.ERC721Transfer, error) { param := TokenTransferParams{ ContractAddress: contractAddress, Address: address, @@ -231,11 +246,14 @@ func (c *Client) ERC721Transfers(contractAddress, address *string, startBlock *i if desc { param.Sort = "desc" } - err = c.call("account", "tokennfttx", param, &txs) - return + body, err := c.execute("account", "tokennfttx", param.GetUrlValues()) + if err != nil { + return []response.ERC721Transfer{}, errors.Wrap(err, "executing ERC721Transfers request") + } + return response.ReadResponse[[]response.ERC721Transfer](body) } -func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) (txs []ERC1155Transfer, err error) { +func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock *int, endBlock *int, page int, offset int, desc bool) ([]response.ERC1155Transfer, error) { param := TokenTransferParams{ ContractAddress: contractAddress, Address: address, @@ -248,38 +266,50 @@ func (c *Client) ERC1155Transfers(contractAddress, address *string, startBlock * if desc { param.Sort = "desc" } - err = c.call("account", "token1155tx", param, &txs) - return + body, err := c.execute("account", "token1155tx", param.GetUrlValues()) + if err != nil { + return []response.ERC1155Transfer{}, errors.Wrap(err, "executing ERC1155Transfers request") + } + return response.ReadResponse[[]response.ERC1155Transfer](body) } -func (c *Client) BlocksMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { +func (c *Client) BlocksMinedByAddress(address string, page int, offset int) ([]response.MinedBlock, error) { param := MinedBlockParams{ Address: address, BlockType: "blocks", Page: page, Offset: offset, } - err = c.call("account", "getminedblocks", param, &mined) - return + body, err := c.execute("account", "getminedblocks", param.GetUrlValues()) + if err != nil { + return []response.MinedBlock{}, errors.Wrap(err, "executing BlocksMinedByAddress request") + } + return response.ReadResponse[[]response.MinedBlock](body) } -func (c *Client) UnclesMinedByAddress(address string, page int, offset int) (mined []MinedBlock, err error) { +func (c *Client) UnclesMinedByAddress(address string, page int, offset int) ([]response.MinedBlock, error) { param := MinedBlockParams{ Address: address, BlockType: "uncles", Page: page, Offset: offset, } - err = c.call("account", "getminedblocks", param, &mined) - return + body, err := c.execute("account", "getminedblocks", param.GetUrlValues()) + if err != nil { + return []response.MinedBlock{}, errors.Wrap(err, "executing UnclesMinedByAddress request") + } + return response.ReadResponse[[]response.MinedBlock](body) } -func (c *Client) TokenBalance(contractAddress, address string) (balance *BigInt, err error) { +func (c *Client) TokenBalance(contractAddress, address string) (types.BigInt, error) { param := TokenBalanceParams{ ContractAddress: contractAddress, Address: address, Tag: "latest", } - err = c.call("account", "tokenbalance", param, &balance) - return + body, err := c.execute("account", "tokenbalance", param.GetUrlValues()) + if err != nil { + return types.BigInt{}, errors.Wrap(err, "executing TokenBalance request") + } + return response.ReadResponse[types.BigInt](body) } diff --git a/pkg/client/block.go b/pkg/client/block.go index 0b090c0..26227b0 100644 --- a/pkg/client/block.go +++ b/pkg/client/block.go @@ -9,41 +9,71 @@ package client import ( "fmt" + "net/url" "strconv" + + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/pkg/response" ) -// BlockReward gets block and uncle rewards by block number -func (c *Client) BlockReward(blockNum int) (rewards BlockRewards, err error) { - param := M{ - "blockno": blockNum, - } + type BlockRewardParams struct { + BlockNo int `json:"blockno"` + } + + type BlockNumberParams struct { + Timestamp int64 `json:"timestamp"` + Closest string `json:"closest"` + } + + func (p BlockRewardParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("blockno", strconv.Itoa(p.BlockNo)) + return values + } + + func (p BlockNumberParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("timestamp", strconv.FormatInt(p.Timestamp, 10)) + values.Add("closest", p.Closest) + return values + } - err = c.call("block", "getblockreward", param, &rewards) - return -} + // BlockReward gets block and uncle rewards by block number + func (c *Client) BlockReward(blockNum int) (response.BlockRewards, error) { + param := BlockRewardParams{ + BlockNo: blockNum, + } -// BlockNumber gets the closest block number by UNIX timestamp -// -// valid closest option: before, after -func (c *Client) BlockNumber(timestamp int64, closest string) (blockNumber int, err error) { - var blockNumberStr string + body, err := c.execute("block", "getblockreward", param.GetUrlValues()) + if err != nil { + return response.BlockRewards{}, errors.Wrap(err, "executing BlockReward request") + } + return response.ReadResponse[response.BlockRewards](body) + } - param := M{ - "timestamp": strconv.FormatInt(timestamp, 10), - "closest": closest, + // BlockNumber gets the closest block number by UNIX timestamp + // + // valid closest option: before, after + func (c *Client) BlockNumber(timestamp int64, closest string) (int, error) { + param := BlockNumberParams{ + Timestamp: timestamp, + Closest: closest, } - err = c.call("block", "getblocknobytime", param, &blockNumberStr) + body, err := c.execute("block", "getblocknobytime", param.GetUrlValues()) + if err != nil { + return 0, errors.Wrap(err, "executing BlockNumber request") + } + blockNumberStr, err := response.ReadResponse[string](body) if err != nil { - return + return 0, errors.Wrap(err, "reading response") } - blockNumber, err = strconv.Atoi(blockNumberStr) + blockNumber, err := strconv.Atoi(blockNumberStr) if err != nil { - err = fmt.Errorf("parsing block number %q: %w", blockNumberStr, err) - return + return 0, fmt.Errorf("parsing block number %q: %w", blockNumberStr, err) } - return -} + return blockNumber, nil + } diff --git a/pkg/client/client.go b/pkg/client/client.go index 851d52e..8c2837d 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -9,7 +9,6 @@ package client import ( "bytes" - "encoding/json" "fmt" "io" "net/http" @@ -28,6 +27,7 @@ type ( conn *http.Client key string baseURL string + chainID string // Verbose when true, talks a lot Verbose bool @@ -94,6 +94,58 @@ func NewCustomized(config Customization) *Client { } } +func (c *Client) execute(module, action string, values url.Values) (bytes.Buffer, error) { + var content = bytes.Buffer{} + + req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, values), http.NoBody) + if err != nil { + return content, errors.Wrap(err, "creating request") + } + req.Header.Set("User-Agent", "etherscan-api(Go)") + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + if c.Verbose { + reqDump, err := httputil.DumpRequestOut(req, false) + if err != nil { + return content, errors.Wrap(err, "verbose mode: dumping request") + } + + fmt.Printf("\n%s\n", reqDump) + + defer func() { + if err != nil { + fmt.Printf("[Error] %v\n", err) + } + }() + } + + res, err := c.conn.Do(req) + if err != nil { + return content, errors.Wrap(err, "sending request") + } + defer res.Body.Close() + + if c.Verbose { + resDump, err := httputil.DumpResponse(res, true) + if err != nil { + return content, errors.Wrap(err, "verbose mode:dumping response") + } + + fmt.Printf("%s\n", resDump) + } + + if _, err = io.Copy(&content, res.Body); err != nil { + return content, errors.Wrap(err, "reading response") + } + + if res.StatusCode != http.StatusOK { + return content, errors.Errorf("got non-200 status code; status: %v, status text: %s, response body: %s", res.StatusCode, res.Status, content.String()) + } + + return content, nil +} + +/* func (c *Client) innerCall(module, action string, values url.Values, outcome any) error { req, err := http.NewRequest(http.MethodGet, c.craftURL(module, action, values), http.NoBody) if err != nil { @@ -132,6 +184,8 @@ func (c *Client) innerCall(module, action string, values url.Values, outcome any fmt.Printf("%s\n", resDump) } + resp := response.ReadResponse(res) + if res.StatusCode != http.StatusOK { return errors.Errorf("got non-200 status code; status: %v, status text: %s, response body: %s", res.StatusCode, res.Status, content.String()) } @@ -176,12 +230,14 @@ func (c *Client) call(module, action string, values url.Values, outcome interfac } return err } +*/ // craftURL returns desired URL via param provided func (c *Client) craftURL(module, action string, values url.Values) string { values.Add("module", module) values.Add("action", action) values.Add("apikey", c.key) + values.Add("chainid", c.chainID) return fmt.Sprintf("%s?%s", c.baseURL, values.Encode()) } diff --git a/pkg/client/contract.go b/pkg/client/contract.go index 35b89ee..18b3364 100644 --- a/pkg/client/contract.go +++ b/pkg/client/contract.go @@ -7,22 +7,47 @@ package client -// ContractABI gets contract abi for verified contract source codes -func (c *Client) ContractABI(address string) (abi string, err error) { - param := M{ - "address": address, +import ( + "net/url" + + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/pkg/response" +) + + type ContractParams struct { + Address string `json:"address"` + } + + func (p ContractParams) GetUrlValues() url.Values { + values := url.Values{} + if p.Address != "" { + values.Add("address", p.Address) } + return values + } - err = c.call("contract", "getabi", param, &abi) - return -} + // ContractABI gets contract abi for verified contract source codes + func (c *Client) ContractABI(address string) (string, error) { + param := ContractParams{ + Address: address, + } -// ContractSource gets contract source code for verified contract source codes -func (c *Client) ContractSource(address string) (source []ContractSource, err error) { - param := M{ - "address": address, + body, err := c.execute("contract", "getabi", param.GetUrlValues()) + if err != nil { + return "", errors.Wrap(err, "executing ContractABI request") } + return response.ReadResponse[string](body) + } - err = c.call("contract", "getsourcecode", param, &source) - return -} + // ContractSource gets contract source code for verified contract source codes + func (c *Client) ContractSource(address string) ([]response.ContractSource, error) { + param := ContractParams{ + Address: address, + } + + body, err := c.execute("contract", "getsourcecode", param.GetUrlValues()) + if err != nil { + return nil, errors.Wrap(err, "executing ContractSource request") + } + return response.ReadResponse[[]response.ContractSource](body) + } diff --git a/pkg/client/gas_tracker.go b/pkg/client/gas_tracker.go index fe5e9ad..a993dc6 100644 --- a/pkg/client/gas_tracker.go +++ b/pkg/client/gas_tracker.go @@ -7,21 +7,49 @@ package client -import "time" +import ( + "net/url" + "strconv" + "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) + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/pkg/response" +) + + type GasEstimateParams struct { + GasPrice int `json:"gasPrice"` + } + + func (p GasEstimateParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("gasPrice", strconv.Itoa(p.GasPrice)) + return values + } + + // GasEstimate gets estimated confirmation time (in seconds) at the given gas price + func (c *Client) GasEstimate(gasPrice int) (time.Duration, error) { + param := GasEstimateParams{ + GasPrice: gasPrice, + } + + body, err := c.execute("gastracker", "gasestimate", param.GetUrlValues()) if err != nil { - return + return 0, errors.Wrap(err, "executing GasEstimate request") } + + confTime, err := response.ReadResponse[string](body) + if err != nil { + return 0, errors.Wrap(err, "reading response") + } + 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 -} + // GasOracle gets suggested gas prices (in Gwei) + func (c *Client) GasOracle() (response.GasPrices, error) { + body, err := c.execute("gastracker", "gasoracle", url.Values{}) + if err != nil { + return response.GasPrices{}, errors.Wrap(err, "executing GasOracle request") + } + return response.ReadResponse[response.GasPrices](body) + } diff --git a/pkg/client/logs.go b/pkg/client/logs.go index 0d30b5e..877daeb 100644 --- a/pkg/client/logs.go +++ b/pkg/client/logs.go @@ -7,15 +7,42 @@ package client -// 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, +import ( + "net/url" + "strconv" + + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/pkg/response" +) + + type LogParams struct { + FromBlock int `json:"fromBlock"` + ToBlock int `json:"toBlock"` + Topic0 string `json:"topic0"` + Address string `json:"address"` + } + + func (p LogParams) GetUrlValues() url.Values { + values := url.Values{} + values.Add("fromBlock", strconv.Itoa(p.FromBlock)) + values.Add("toBlock", strconv.Itoa(p.ToBlock)) + values.Add("topic0", p.Topic0) + values.Add("address", p.Address) + return values + } + + // 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) ([]response.Log, error) { + param := LogParams{ + FromBlock: fromBlock, + ToBlock: toBlock, + Topic0: topic, + Address: address, } - err = c.call("logs", "getLogs", param, &logs) - return -} + body, err := c.execute("logs", "getLogs", param.GetUrlValues()) + if err != nil { + return nil, errors.Wrap(err, "executing GetLogs request") + } + return response.ReadResponse[[]response.Log](body) + } diff --git a/pkg/response/response.go b/pkg/response/response.go index 7ea84f0..e0c992e 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -11,8 +11,6 @@ import ( "bytes" "encoding/json" "fmt" - "io" - "net/http" "strconv" "strings" @@ -21,12 +19,20 @@ import ( ) type EtherscanResponse interface { - AccountBalance | - NormalTx | InternalTx | - ERC20Transfer | ERC721Transfer | ERC1155Transfer | - MinedBlock | ContractSource | - ExecutionStatus | BlockRewards | - LatestPrice | Log | GasPrices + AccountBalance | []AccountBalance | + NormalTx | []NormalTx | + InternalTx | []InternalTx | + ERC20Transfer | []ERC20Transfer | + ERC721Transfer | []ERC721Transfer | + ERC1155Transfer | []ERC1155Transfer | + MinedBlock | []MinedBlock | + ContractSource | []ContractSource | + ExecutionStatus | []ExecutionStatus | + BlockRewards | []BlockRewards | + LatestPrice | []LatestPrice | + Log | []Log | + GasPrices | []GasPrices | + types.BigInt | types.Time | string } // envelope is the carrier of nearly every response @@ -39,12 +45,8 @@ type envelope[T EtherscanResponse] struct { Result T `json:"result"` } -func ReadResponse[T EtherscanResponse](resp *http.Response) (T, error) { +func ReadResponse[T EtherscanResponse](content bytes.Buffer) (T, error) { var ret T - var content bytes.Buffer - if _, err := io.Copy(&content, resp.Body); err != nil { - return ret, errors.Wrap(err, "reading response") - } var envelope envelope[T] if err := json.Unmarshal(content.Bytes(), &envelope); err != nil { diff --git a/reflect.go b/reflect.go deleted file mode 100644 index ba0d54a..0000000 --- a/reflect.go +++ /dev/null @@ -1,58 +0,0 @@ -/* - * 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 ( - "fmt" - "reflect" - "strconv" -) - -// extractValue obtains string-formed slice representation via -// input. It only handles string, int, and their slice form, and -// panics otherwise. -func extractValue(input interface{}) (output []string) { - v := direct(reflect.ValueOf(input)) - - if v.Kind() == reflect.Slice { - length := v.Len() - output = make([]string, length) - - for i := 0; i < length; i++ { - output[i] = valueToStr(v.Index(i)) - } - } else { - output = make([]string, 1) - output[0] = valueToStr(v) - } - - return -} - -// valueToStr convert v into proper string representation -// Only handles int and string, panic otherwise. -func valueToStr(v reflect.Value) (str string) { - switch v.Kind() { - case reflect.String: - str = v.String() - case reflect.Int: - str = strconv.FormatInt(v.Int(), 10) - default: - panic(fmt.Sprintf("valueToStr: %v is of unexpected kind %q", v, v.Kind())) - } - return -} - -// direct traverses the pointer chain to fetch -// the solid value -func direct(v reflect.Value) reflect.Value { - for ; v.Kind() == reflect.Ptr; v = v.Elem() { - // relax - } - return v -} From e325d2c9a3726f0d52acd29e57cc7018605fba9c Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Thu, 2 Jan 2025 14:17:23 +0100 Subject: [PATCH 13/21] refactor(network): add support for v2 url scheme * change `Network` to `Chain` * add support for new url scheme - api.etherscan.com/v2/api? instead of scan.com/api? * update tests --- internal/chain/chain.go | 162 ++++++++++++++++++++++++++++++++++++ internal/network/network.go | 34 -------- pkg/client/client.go | 15 ++-- pkg/client/client_test.go | 21 ++--- 4 files changed, 183 insertions(+), 49 deletions(-) create mode 100644 internal/chain/chain.go delete mode 100644 internal/network/network.go diff --git a/internal/chain/chain.go b/internal/chain/chain.go new file mode 100644 index 0000000..acbfaad --- /dev/null +++ b/internal/chain/chain.go @@ -0,0 +1,162 @@ +/* + * 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 chain + +type Chain int + +// ChainIDs supported as of 2025/01/02 +const ( + EthereumMainnet Chain = 1 + OpMainnet Chain = 10 + SepoliaTestnet Chain = 11155111 + HoleskyTestnet Chain = 17000 + CronosMainnet Chain = 25 + ApeChainCurtisTestnet Chain = 33111 + ApeChainMainnet Chain = 33139 + ArbitrumOneMainnet Chain = 42161 + ArbitrumNovaMainnet Chain = 42170 + CeloMainnet Chain = 42220 + AvalancheCChain Chain = 43114 + AvalancheFujiTestnet Chain = 43113 + XDCMainnet Chain = 50 + XDCApothemTestnet Chain = 51 + BNBSmartChainMainnet Chain = 56 + BNBSmartChainTestnet Chain = 97 + Gnosis Chain = 100 + PolygonMainnet Chain = 137 + SonicMainnet Chain = 146 + BitTorrentChainMainnet Chain = 199 + FantomOperaMainnet Chain = 250 + FraxtalMainnet Chain = 252 + KromaMainnet Chain = 255 + zkSyncMainnet Chain = 324 + zkSyncSepoliaTestnet Chain = 300 + MoonbeamMainnet Chain = 1284 + MoonbaseAlphaTestnet Chain = 1287 + MoonriverMainnet Chain = 1285 + BitTorrentChainTestnet Chain = 1028 + PolygonZkEVMMainnet Chain = 1101 + WEMIX30Mainnet Chain = 1111 + WEMIX30Testnet Chain = 1112 + FantomTestnet Chain = 4002 + WorldMainnet Chain = 480 + WorldSepoliaTestnet Chain = 4801 + MantleMainnet Chain = 5000 + MantleSepoliaTestnet Chain = 5003 + BaseMainnet Chain = 8453 + BaseSepoliaTestnet Chain = 84532 + BlastMainnet Chain = 81457 + CeloAlfajoresTestnet Chain = 44787 + PolygonAmoyTestnet Chain = 80002 + PolygonZkEVMCardona Chain = 2442 + FraxtalTestnet Chain = 2522 + KromaSepolia Chain = 2358 + ScrollMainnet Chain = 534352 + ScrollSepoliaTestnet Chain = 534351 + SonicBlazeTestnet Chain = 57054 + LineaMainnet Chain = 59144 + LineaSepoliaTestnet Chain = 59141 + SophonMainnet Chain = 50104 + SophonSepoliaTestnet Chain = 531050104 + TaikoMainnet Chain = 167000 + TaikoHeklaTestnet Chain = 167009 + XaiMainnet Chain = 660279 + XaiSepoliaTestnet Chain = 37714555429 + BlastSepoliaTestnet Chain = 168587773 + OpSepoliaTestnet Chain = 11155420 + ArbitrumSepoliaTestnet Chain = 421614 +) + +var chainNames = map[Chain]string{ + EthereumMainnet: "Ethereum Mainnet", + OpMainnet: "OP Mainnet", + SepoliaTestnet: "Sepolia Testnet", + HoleskyTestnet: "Holesky Testnet", + CronosMainnet: "Cronos Mainnet", + ApeChainCurtisTestnet: "ApeChain Curtis Testnet", + ApeChainMainnet: "ApeChain Mainnet", + ArbitrumOneMainnet: "Arbitrum One Mainnet", + ArbitrumNovaMainnet: "Arbitrum Nova Mainnet", + CeloMainnet: "Celo Mainnet", + AvalancheCChain: "Avalanche C-Chain", + AvalancheFujiTestnet: "Avalanche Fuji Testnet", + XDCMainnet: "XDC Mainnet", + XDCApothemTestnet: "XDC Apothem Testnet", + BNBSmartChainMainnet: "BNB Smart Chain Mainnet", + BNBSmartChainTestnet: "BNB Smart Chain Testnet", + Gnosis: "Gnosis", + PolygonMainnet: "Polygon Mainnet", + SonicMainnet: "Sonic Mainnet", + BitTorrentChainMainnet: "BitTorrent Chain Mainnet", + FantomOperaMainnet: "Fantom Opera Mainnet", + FraxtalMainnet: "Fraxtal Mainnet", + KromaMainnet: "Kroma Mainnet", + zkSyncMainnet: "zkSync Mainnet", + zkSyncSepoliaTestnet: "zkSync Sepolia Testnet", + MoonbeamMainnet: "Moonbeam Mainnet", + MoonbaseAlphaTestnet: "Moonbase Alpha Testnet", + MoonriverMainnet: "Moonriver Mainnet", + BitTorrentChainTestnet: "BitTorrent Chain Testnet", + PolygonZkEVMMainnet: "Polygon zkEVM Mainnet", + WEMIX30Mainnet: "WEMIX3.0 Mainnet", + WEMIX30Testnet: "WEMIX3.0 Testnet", + FantomTestnet: "Fantom Testnet", + WorldMainnet: "World Mainnet", + WorldSepoliaTestnet: "World Sepolia Testnet", + MantleMainnet: "Mantle Mainnet", + MantleSepoliaTestnet: "Mantle Sepolia Testnet", + BaseMainnet: "Base Mainnet", + BaseSepoliaTestnet: "Base Sepolia Testnet", + BlastMainnet: "Blast Mainnet", + CeloAlfajoresTestnet: "Celo Alfajores Testnet", + PolygonAmoyTestnet: "Polygon Amoy Testnet", + PolygonZkEVMCardona: "Polygon zkEVM Cardona Testnet", + FraxtalTestnet: "Fraxtal Testnet", + KromaSepolia: "Kroma Sepolia Testnet", + ScrollMainnet: "Scroll Mainnet", + ScrollSepoliaTestnet: "Scroll Sepolia Testnet", + SonicBlazeTestnet: "Sonic Blaze Testnet", + LineaMainnet: "Linea Mainnet", + LineaSepoliaTestnet: "Linea Sepolia Testnet", + SophonMainnet: "Sophon Mainnet", + SophonSepoliaTestnet: "Sophon Sepolia Testnet", + TaikoMainnet: "Taiko Mainnet", + TaikoHeklaTestnet: "Taiko Hekla L2 Testnet", + XaiMainnet: "Xai Mainnet", + XaiSepoliaTestnet: "Xai Sepolia Testnet", + BlastSepoliaTestnet: "Blast Sepolia Testnet", + OpSepoliaTestnet: "OP Sepolia Testnet", + ArbitrumSepoliaTestnet: "Arbitrum Sepolia Testnet", +} + +// String returns the name of the network +func (n Chain) String() string { + if name, ok := chainNames[n]; ok { + return name + } + return "Unknown Network" +} + +// ChainID returns the chain ID as a uint64 +func (n Chain) ChainID() int { + return int(n) +} + +// NewNetwork creates a Network from a name and chain ID +func NewNetwork(name string, chainID int) Chain { + network := Chain(chainID) + + return network +} + +// GetByChainID returns a Network by its chain ID +func GetByChainID(chainID int) (Chain, bool) { + network := Chain(chainID) + _, exists := chainNames[network] + return network, exists +} diff --git a/internal/network/network.go b/internal/network/network.go deleted file mode 100644 index 8a07ba6..0000000 --- a/internal/network/network.go +++ /dev/null @@ -1,34 +0,0 @@ -/* - * 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 network - -// Network is ethereum network type (mainnet, ropsten, etc) -type Network string - -const ( - //// Ethereum public networks - - // Mainnet Ethereum mainnet for production - Mainnet Network = "api" - // Ropsten Testnet(POW) - Ropsten Network = "api-ropsten" - // Kovan Testnet(POA) - Kovan Network = "api-kovan" - // Rinkby Testnet(CLIQUE) - Rinkby Network = "api-rinkeby" - // Goerli Testnet(CLIQUE) - Goerli Network = "api-goerli" - // Tobalaba Testnet - Tobalaba Network = "api-tobalaba" -) - -// SubDomain returns the subdomain of etherscan API -// via n provided. -func (n Network) SubDomain() (sub string) { - return string(n) -} diff --git a/pkg/client/client.go b/pkg/client/client.go index 8c2837d..ede9847 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -14,10 +14,11 @@ import ( "net/http" "net/http/httputil" "net/url" + "strconv" "time" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/internal/network" + "github.com/timcki/etherscan-api/internal/chain" ) type ( @@ -27,7 +28,7 @@ type ( conn *http.Client key string baseURL string - chainID string + chain chain.Chain // Verbose when true, talks a lot Verbose bool @@ -51,6 +52,8 @@ type ( BaseURL string // When true, talks a lot Verbose bool + // ChainID to be used + Chain chain.Chain // HTTP Client to be used. Specifying this value will ignore the Timeout value set // Set your own timeout. Client *http.Client @@ -67,11 +70,12 @@ type ( // NewClient initialize a new etherscan API client // please use pre-defined network value -func NewClient(network network.Network, APIKey string) *Client { +func NewClient(chain chain.Chain, APIKey string) *Client { return NewCustomized(Customization{ Timeout: 30 * time.Second, Key: APIKey, - BaseURL: fmt.Sprintf(`https://%s.etherscan.io/api`, network.SubDomain()), + Chain: chain, + BaseURL: `https://api.etherscan.io/v2/api`, }) } @@ -87,6 +91,7 @@ func NewCustomized(config Customization) *Client { return &Client{ conn: httpClient, key: config.Key, + chain: config.Chain, baseURL: config.BaseURL, Verbose: config.Verbose, BeforeRequest: config.BeforeRequest, @@ -237,7 +242,7 @@ func (c *Client) craftURL(module, action string, values url.Values) string { values.Add("module", module) values.Add("action", action) values.Add("apikey", c.key) - values.Add("chainid", c.chainID) + values.Add("chainid", strconv.Itoa(c.chain.ChainID())) return fmt.Sprintf("%s?%s", c.baseURL, values.Encode()) } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index bd9afd9..9e30612 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -8,23 +8,24 @@ package client import ( + "net/url" "testing" - "github.com/timcki/etherscan-api/internal/types" + "github.com/stretchr/testify/assert" + "github.com/timcki/etherscan-api/internal/chain" ) func TestClient_craftURL(t *testing.T) { - c := NewClient(types.Ropsten, "abc123") + c := NewClient(chain.EthereumMainnet, "abc123") - const expected = `https://api-ropsten.etherscan.io/api?action=craftURL&apikey=abc123&four=d&four=e&four=f&module=testing&one=1&three=1&three=2&three=3&two=2` - output := c.craftURL("testing", "craftURL", M{ - "one": 1, - "two": "2", - "three": []int{1, 2, 3}, + const expected = `https://api.etherscan.io/v2/api?action=craftURL&apikey=abc123&chainid=1&four=d&four=e&four=f&module=testing&one=1&three=1&three=2&three=3&two=2` + + output := c.craftURL("testing", "craftURL", url.Values{ + "one": []string{"1"}, + "two": []string{"2"}, + "three": []string{"1", "2", "3"}, "four": []string{"d", "e", "f"}, }) - if output != expected { - t.Fatalf("output != expected, got %s, want %s", output, expected) - } + assert.Equal(t, expected, output) } From 0600cfbc47c2f4d9cc18709fa61a38a0499a722a Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Mon, 6 Jan 2025 13:21:21 +0000 Subject: [PATCH 14/21] chore(chain_id): move from internal to pkg --- {internal => pkg}/chain/chain.go | 0 pkg/client/client.go | 2 +- pkg/client/client_test.go | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename {internal => pkg}/chain/chain.go (100%) diff --git a/internal/chain/chain.go b/pkg/chain/chain.go similarity index 100% rename from internal/chain/chain.go rename to pkg/chain/chain.go diff --git a/pkg/client/client.go b/pkg/client/client.go index ede9847..b04c170 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -18,7 +18,7 @@ import ( "time" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/internal/chain" + "github.com/timcki/etherscan-api/pkg/chain" ) type ( diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 9e30612..053f78f 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -12,7 +12,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/timcki/etherscan-api/internal/chain" + "github.com/timcki/etherscan-api/pkg/chain" ) func TestClient_craftURL(t *testing.T) { From 3a29daba4502cebd72ae9f446ad88463f25b65f0 Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Thu, 2 Jan 2025 11:49:07 +0100 Subject: [PATCH 15/21] refactor(client): reorg structure & error handling BREAKING CHANGES: - Move all client-related code to pkg/client directory - Rename test files to follow Go conventions (*_integration_test.go) - Replace custom noError helper with testify/assert - Update response types and error handling in client methods - Move types to internal/types package Features: - Add StatusResponse type for transaction receipt status - Add url.Values based parameter handling - Improve test organization with integration test build tags The changes improve the overall package structure and maintainability while providing better error handling and type safety. Client code is now better organized in its own package with clearer separation of concerns. --- go.mod | 3 +- go.sum | 6 -- internal/types/types_test.go | 9 ++- pkg/client/account.go | 12 ++-- .../client/account_integration_test.go | 30 +++++---- pkg/client/block.go | 34 +++++----- .../client/block_integration_test.go | 15 +++-- pkg/client/client.go | 6 +- pkg/client/client_test.go | 2 +- pkg/client/contract.go | 22 +++---- .../client/contract_integration_test.go | 11 +++- pkg/client/gas_tracker.go | 22 +++---- .../client/gas_tracker_integration_test.go | 15 +++-- pkg/client/logs.go | 16 ++--- .../client/logs_integration_test.go | 18 +++--- .../client/setup_integration_test.go | 26 ++++---- pkg/client/stat.go | 57 +++++++++++++++++ .../client/stat_integration_test.go | 13 ++-- pkg/client/transaction.go | 64 +++++++++++++++++++ .../client/transaction_integration_test.go | 15 +++-- pkg/response/response.go | 9 ++- stat.go | 30 --------- transaction.go | 59 ----------------- 23 files changed, 277 insertions(+), 217 deletions(-) rename account_e2e_test.go => pkg/client/account_integration_test.go (87%) rename block_e2e_test.go => pkg/client/block_integration_test.go (83%) rename contract_e2e_test.go => pkg/client/contract_integration_test.go (98%) rename gas_tracker_e2e_test.go => pkg/client/gas_tracker_integration_test.go (53%) rename logs_e2e_test.go => pkg/client/logs_integration_test.go (75%) rename setup_e2e_test.go => pkg/client/setup_integration_test.go (77%) create mode 100644 pkg/client/stat.go rename stat_e2e_test.go => pkg/client/stat_integration_test.go (81%) create mode 100644 pkg/client/transaction.go rename transaction_e2e_test.go => pkg/client/transaction_integration_test.go (84%) delete mode 100644 stat.go delete mode 100644 transaction.go diff --git a/go.mod b/go.mod index 5b8bb93..d91a7ec 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,8 @@ -module github.com/timcki/etherscan-api +module github.com/timcki/etherscan-api/v2 go 1.23.0 require ( - github.com/google/go-cmp v0.5.7 github.com/pkg/errors v0.9.1 github.com/stretchr/testify v1.10.0 ) diff --git a/go.sum b/go.sum index 74bb85f..33dd589 100644 --- a/go.sum +++ b/go.sum @@ -1,17 +1,11 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -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= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= -github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -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= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/internal/types/types_test.go b/internal/types/types_test.go index 4f0a01f..72d5f36 100644 --- a/internal/types/types_test.go +++ b/internal/types/types_test.go @@ -22,14 +22,13 @@ func TestBigInt(t *testing.T) { b := new(BigInt) err := b.UnmarshalText([]byte(ansStr)) - assert.E - noError(t, err, "BigInt.UnmarshalText") + assert.NoError(t, err, "BigInt.UnmarshalText") if b.Int().Cmp(ans) != 0 { t.Fatalf("BigInt.UnmarshalText not working, got %v, want %v", b.Int(), ans) } textBytes, err := b.MarshalText() - noError(t, err, "BigInt.MarshalText") + assert.NoError(t, err, "BigInt.MarshalText") if string(textBytes) != ansStr { t.Fatalf("BigInt.MarshalText not working, got %s, want %s", textBytes, ansStr) @@ -42,13 +41,13 @@ func TestTime(t *testing.T) { b := new(Time) err := b.UnmarshalText([]byte(ansStr)) - noError(t, err, "Time.UnmarshalText") + assert.NoError(t, err) if !b.Time().Equal(ans) { t.Fatalf("Time.UnmarshalText not working, got %v, want %v", b, ans) } textBytes, err := b.MarshalText() - noError(t, err, "BigInt.MarshalText") + assert.NoError(t, err, "BigInt.MarshalText") if string(textBytes) != ansStr { t.Fatalf("Time.MarshalText not working, got %s, want %s", textBytes, ansStr) diff --git a/pkg/client/account.go b/pkg/client/account.go index 168bf3b..dcf3cbf 100644 --- a/pkg/client/account.go +++ b/pkg/client/account.go @@ -8,13 +8,14 @@ package client import ( + "fmt" "net/url" "strconv" "strings" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/internal/types" - "github.com/timcki/etherscan-api/pkg/response" + "github.com/timcki/etherscan-api/v2/internal/types" + "github.com/timcki/etherscan-api/v2/pkg/response" ) type AccountBalanceParams struct { @@ -150,16 +151,17 @@ func (p TokenBalanceParams) GetUrlValues() url.Values { } // Refactored methods -func (c *Client) AccountBalance(address string) (response.AccountBalance, error) { +func (c *Client) AccountBalance(address string) (types.BigInt, error) { param := AccountBalanceParams{ Tag: "latest", Address: address, } body, err := c.execute("account", "balance", param.GetUrlValues()) if err != nil { - return response.AccountBalance{}, errors.Wrap(err, "executing AccountBalance request") + return types.BigInt{}, errors.Wrap(err, "executing AccountBalance request") } - return response.ReadResponse[response.AccountBalance](body) + fmt.Printf("%s\n", body.Bytes()) + return response.ReadResponse[types.BigInt](body) } func (c *Client) MultiAccountBalance(addresses ...string) ([]response.AccountBalance, error) { diff --git a/account_e2e_test.go b/pkg/client/account_integration_test.go similarity index 87% rename from account_e2e_test.go rename to pkg/client/account_integration_test.go index 842b84b..4a61649 100644 --- a/account_e2e_test.go +++ b/pkg/client/account_integration_test.go @@ -8,18 +8,20 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "encoding/json" "fmt" "math/big" "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_AccountBalance(t *testing.T) { balance, err := api.AccountBalance("0x0000000000000000000000000000000000000000") - noError(t, err, "api.AccountBalance") + assert.NoError(t, err, "api.AccountBalance") if balance.Int().Cmp(big.NewInt(0)) != 1 { t.Fatalf("rich man is no longer rich") @@ -32,7 +34,7 @@ func TestClient_MultiAccountBalance(t *testing.T) { "0x0000000000000000000000000000000000000001", "0x0000000000000000000000000000000000000002", "0x0000000000000000000000000000000000000003") - noError(t, err, "api.MultiAccountBalance") + assert.NoError(t, err, "api.MultiAccountBalance") for i, item := range balances { if item.Account == "" { @@ -49,7 +51,7 @@ func TestClient_NormalTxByAddress(t *testing.T) { var a, b = 54092, 79728 txs, err := api.NormalTxByAddress("0xde0b295669a9fd93d5f28d9ec85e40f4cb697bae", &a, &b, 1, 500, false) - noError(t, err, "api.NormalTxByAddress") + assert.NoError(t, err, "api.NormalTxByAddress") //j, _ := json.MarshalIndent(txs, "", " ") //fmt.Printf("%s\n", j) @@ -64,7 +66,7 @@ func TestClient_InternalTxByAddress(t *testing.T) { var a, b = 0, 2702578 txs, err := api.InternalTxByAddress("0x2c1ba59d6f58433fb1eaee7d20b26ed83bda51a3", &a, &b, 1, 500, false) - noError(t, err, "api.InternalTxByAddress") + assert.NoError(t, err, "api.InternalTxByAddress") //j, _ := json.MarshalIndent(txs, "", " ") //fmt.Printf("%s\n", j) @@ -84,7 +86,7 @@ func TestClient_ERC20Transfers(t *testing.T) { var a, b = 3273004, 3328071 var contract, address = "0xe0b7927c4af23765cb51314a0e0521a9645f0e2a", "0x4e83362442b8d1bec281594cea3050c8eb01311c" txs, err := api.ERC20Transfers(&contract, &address, &a, &b, 1, 500, false) - noError(t, err, "api.ERC20Transfers 1") + assert.NoError(t, err, "api.ERC20Transfers 1") //j, _ := json.MarshalIndent(txs, "", " ") //fmt.Printf("%s\n", j) @@ -94,13 +96,13 @@ func TestClient_ERC20Transfers(t *testing.T) { } txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500, false) - noError(t, err, "api.ERC20Transfers 2 asc") + assert.NoError(t, err, "api.ERC20Transfers 2 asc") if len(txs) != wantLen2 { t.Errorf("got txs length %v, want %v", len(txs), wantLen2) } txs, err = api.ERC20Transfers(nil, &address, nil, &b, 1, 500, true) - noError(t, err, "api.ERC20Transfers 2 desc") + assert.NoError(t, err, "api.ERC20Transfers 2 desc") if len(txs) != wantLen2 { t.Errorf("got txs length %v, want %v", len(txs), wantLen2) @@ -112,7 +114,7 @@ func TestClient_ERC20Transfers(t *testing.T) { var specialStartHeight = 6024142 var specialEndHeight = 6485274 txs, err = api.ERC20Transfers(&specialContract, nil, &specialStartHeight, &specialEndHeight, 1, 500, false) - noError(t, err, "api.ERC20Transfers 2") + assert.NoError(t, err, "api.ERC20Transfers 2") if len(txs) != wantLen3 { t.Errorf("got txs length %v, want %v", len(txs), wantLen3) } @@ -122,7 +124,7 @@ func TestClient_BlocksMinedByAddress(t *testing.T) { const wantLen = 10 blocks, err := api.BlocksMinedByAddress("0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b", 1, wantLen) - noError(t, err, "api.BlocksMinedByAddress") + assert.NoError(t, err, "api.BlocksMinedByAddress") //j, _ := json.MarshalIndent(blocks, "", " ") //fmt.Printf("%s\n", j) @@ -136,7 +138,7 @@ func TestClient_UnclesMinedByAddress(t *testing.T) { const wantLen = 10 blocks, err := api.UnclesMinedByAddress("0x9dd134d14d1e65f84b706d6f205cd5b1cd03a46b", 1, wantLen) - noError(t, err, "api.UnclesMinedByAddress") + assert.NoError(t, err, "api.UnclesMinedByAddress") //j, _ := json.MarshalIndent(blocks, "", " ") //fmt.Printf("%s\n", j) @@ -148,7 +150,7 @@ func TestClient_UnclesMinedByAddress(t *testing.T) { func TestClient_TokenBalance(t *testing.T) { balance, err := api.TokenBalance("0x57d90b64a1a57749b0f932f1a3395792e12e7055", "0xe04f27eb70e025b78871a2ad7eabe85e61212761") - noError(t, err, "api.TokenBalance") + assert.NoError(t, err, "api.TokenBalance") if balance.Int().Cmp(big.NewInt(0)) != 1 { t.Errorf("api.TokenBalance not working, got balance %s", balance.Int().String()) @@ -163,7 +165,7 @@ func TestClient_ERC721Transfers(t *testing.T) { var a, b = 4708442, 9231168 var contract, address = "0x06012c8cf97bead5deae237070f9587f8e7a266d", "0x6975be450864c02b4613023c2152ee0743572325" txs, err := api.ERC721Transfers(&contract, &address, &a, &b, 1, 500, true) - noError(t, err, "api.ERC721Transfers") + assert.NoError(t, err, "api.ERC721Transfers") j, _ := json.MarshalIndent(txs, "", " ") fmt.Printf("%s\n", j) @@ -181,7 +183,7 @@ func TestClient_ERC1155Transfers(t *testing.T) { 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") + assert.NoError(t, err, "api.ERC721Transfers") j, _ := json.MarshalIndent(txs, "", " ") fmt.Printf("%s\n", j) diff --git a/pkg/client/block.go b/pkg/client/block.go index 26227b0..105456e 100644 --- a/pkg/client/block.go +++ b/pkg/client/block.go @@ -13,33 +13,33 @@ import ( "strconv" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/pkg/response" + "github.com/timcki/etherscan-api/v2/pkg/response" ) - type BlockRewardParams struct { +type BlockRewardParams struct { BlockNo int `json:"blockno"` - } +} - type BlockNumberParams struct { +type BlockNumberParams struct { Timestamp int64 `json:"timestamp"` Closest string `json:"closest"` - } +} - func (p BlockRewardParams) GetUrlValues() url.Values { +func (p BlockRewardParams) GetUrlValues() url.Values { values := url.Values{} values.Add("blockno", strconv.Itoa(p.BlockNo)) return values - } +} - func (p BlockNumberParams) GetUrlValues() url.Values { +func (p BlockNumberParams) GetUrlValues() url.Values { values := url.Values{} values.Add("timestamp", strconv.FormatInt(p.Timestamp, 10)) values.Add("closest", p.Closest) return values - } +} - // BlockReward gets block and uncle rewards by block number - func (c *Client) BlockReward(blockNum int) (response.BlockRewards, error) { +// BlockReward gets block and uncle rewards by block number +func (c *Client) BlockReward(blockNum int) (response.BlockRewards, error) { param := BlockRewardParams{ BlockNo: blockNum, } @@ -49,12 +49,12 @@ import ( return response.BlockRewards{}, errors.Wrap(err, "executing BlockReward request") } return response.ReadResponse[response.BlockRewards](body) - } +} - // BlockNumber gets the closest block number by UNIX timestamp - // - // valid closest option: before, after - func (c *Client) BlockNumber(timestamp int64, closest string) (int, error) { +// BlockNumber gets the closest block number by UNIX timestamp +// +// valid closest option: before, after +func (c *Client) BlockNumber(timestamp int64, closest string) (int, error) { param := BlockNumberParams{ Timestamp: timestamp, Closest: closest, @@ -76,4 +76,4 @@ import ( } return blockNumber, nil - } +} diff --git a/block_e2e_test.go b/pkg/client/block_integration_test.go similarity index 83% rename from block_e2e_test.go rename to pkg/client/block_integration_test.go index c49a8ac..498f066 100644 --- a/block_e2e_test.go +++ b/pkg/client/block_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,21 +8,23 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "encoding/json" "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_BlockReward(t *testing.T) { const ans = `{"blockNumber":"2165403","timeStamp":"1472533979","blockMiner":"0x13a06d3dfe21e0db5c016c03ea7d2509f7f8d1e3","blockReward":"5314181600000000000","uncles":[{"miner":"0xbcdfc35b86bedf72f0cda046a3c16829a2ef41d1","unclePosition":"0","blockreward":"3750000000000000000"},{"miner":"0x0d0c9855c722ff0c78f21e43aa275a5b8ea60dce","unclePosition":"1","blockreward":"3750000000000000000"}],"uncleInclusionReward":"312500000000000000"}` reward, err := api.BlockReward(2165403) - noError(t, err, "api.BlockReward") + assert.NoError(t, err, "api.BlockReward") j, err := json.Marshal(reward) - noError(t, err, "json.Marshal") + assert.NoError(t, err, "json.Marshal") if string(j) != ans { t.Errorf("api.BlockReward not working, got %s, want %s", j, ans) } @@ -31,14 +36,14 @@ func TestClient_BlockNumber(t *testing.T) { const ansAfter = 9251483 blockNumber, err := api.BlockNumber(1578638524, "before") - noError(t, err, "api.BlockNumber") + assert.NoError(t, err, "api.BlockNumber") if blockNumber != ansBefore { t.Errorf(`api.BlockNumber(1578638524, "before") not working, got %d, want %d`, blockNumber, ansBefore) } blockNumber, err = api.BlockNumber(1578638524, "after") - noError(t, err, "api.BlockNumber") + assert.NoError(t, err, "api.BlockNumber") if blockNumber != ansAfter { t.Errorf(`api.BlockNumber(1578638524,"after") not working, got %d, want %d`, blockNumber, ansAfter) diff --git a/pkg/client/client.go b/pkg/client/client.go index b04c170..493750b 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -18,7 +18,7 @@ import ( "time" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/pkg/chain" + "github.com/timcki/etherscan-api/v2/pkg/chain" ) type ( @@ -239,6 +239,10 @@ func (c *Client) call(module, action string, values url.Values, outcome interfac // craftURL returns desired URL via param provided func (c *Client) craftURL(module, action string, values url.Values) string { + if values == nil { + values = url.Values{} + } + values.Add("module", module) values.Add("action", action) values.Add("apikey", c.key) diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index 053f78f..bcd59c9 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -12,7 +12,7 @@ import ( "testing" "github.com/stretchr/testify/assert" - "github.com/timcki/etherscan-api/pkg/chain" + "github.com/timcki/etherscan-api/v2/pkg/chain" ) func TestClient_craftURL(t *testing.T) { diff --git a/pkg/client/contract.go b/pkg/client/contract.go index 18b3364..f956881 100644 --- a/pkg/client/contract.go +++ b/pkg/client/contract.go @@ -11,23 +11,23 @@ import ( "net/url" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/pkg/response" + "github.com/timcki/etherscan-api/v2/pkg/response" ) - type ContractParams struct { +type ContractParams struct { Address string `json:"address"` - } +} - func (p ContractParams) GetUrlValues() url.Values { +func (p ContractParams) GetUrlValues() url.Values { values := url.Values{} if p.Address != "" { values.Add("address", p.Address) } return values - } +} - // ContractABI gets contract abi for verified contract source codes - func (c *Client) ContractABI(address string) (string, error) { +// ContractABI gets contract abi for verified contract source codes +func (c *Client) ContractABI(address string) (string, error) { param := ContractParams{ Address: address, } @@ -37,10 +37,10 @@ import ( return "", errors.Wrap(err, "executing ContractABI request") } return response.ReadResponse[string](body) - } +} - // ContractSource gets contract source code for verified contract source codes - func (c *Client) ContractSource(address string) ([]response.ContractSource, error) { +// ContractSource gets contract source code for verified contract source codes +func (c *Client) ContractSource(address string) ([]response.ContractSource, error) { param := ContractParams{ Address: address, } @@ -50,4 +50,4 @@ import ( return nil, errors.Wrap(err, "executing ContractSource request") } return response.ReadResponse[[]response.ContractSource](body) - } +} diff --git a/contract_e2e_test.go b/pkg/client/contract_integration_test.go similarity index 98% rename from contract_e2e_test.go rename to pkg/client/contract_integration_test.go index ca577e7..a8d9a01 100644 --- a/contract_e2e_test.go +++ b/pkg/client/contract_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,17 +8,19 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_ContractABI(t *testing.T) { const answer = `[{"constant":true,"inputs":[{"name":"","type":"uint256"}],"name":"proposals","outputs":[{"name":"recipient","type":"address"},{"name":"amount","type":"uint256"},{"name":"description","type":"string"},{"name":"votingDeadline","type":"uint256"},{"name":"open","type":"bool"},{"name":"proposalPassed","type":"bool"},{"name":"proposalHash","type":"bytes32"},{"name":"proposalDeposit","type":"uint256"},{"name":"newCurator","type":"bool"},{"name":"yea","type":"uint256"},{"name":"nay","type":"uint256"},{"name":"creator","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"minTokensToCreate","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"rewardAccount","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"daoCreator","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"divisor","outputs":[{"name":"divisor","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"extraBalance","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_transactionData","type":"bytes"}],"name":"executeProposal","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"unblockMe","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"totalRewardToken","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"actualBalance","outputs":[{"name":"_actualBalance","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"closingTime","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"allowedRecipients","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferWithoutReward","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"refund","outputs":[],"type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_description","type":"string"},{"name":"_transactionData","type":"bytes"},{"name":"_debatingPeriod","type":"uint256"},{"name":"_newCurator","type":"bool"}],"name":"newProposal","outputs":[{"name":"_proposalID","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"DAOpaidOut","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"minQuorumDivisor","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_newContract","type":"address"}],"name":"newContract","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_recipient","type":"address"},{"name":"_allowed","type":"bool"}],"name":"changeAllowedRecipients","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"halveMinQuorum","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"paidOut","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_newCurator","type":"address"}],"name":"splitDAO","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"DAOrewardAccount","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[],"name":"proposalDeposit","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"numberOfProposals","outputs":[{"name":"_numberOfProposals","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"lastTimeMinQuorumMet","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_toMembers","type":"bool"}],"name":"retrieveDAOReward","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":false,"inputs":[],"name":"receiveEther","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"isFueled","outputs":[{"name":"","type":"bool"}],"type":"function"},{"constant":false,"inputs":[{"name":"_tokenHolder","type":"address"}],"name":"createTokenProxy","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"_proposalID","type":"uint256"}],"name":"getNewDAOAddress","outputs":[{"name":"_newDAO","type":"address"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_supportsProposal","type":"bool"}],"name":"vote","outputs":[{"name":"_voteID","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[],"name":"getMyReward","outputs":[{"name":"_success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"rewardToken","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFromWithoutReward","outputs":[{"name":"success","type":"bool"}],"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"type":"function"},{"constant":false,"inputs":[{"name":"_proposalDeposit","type":"uint256"}],"name":"changeProposalDeposit","outputs":[],"type":"function"},{"constant":true,"inputs":[{"name":"","type":"address"}],"name":"blocked","outputs":[{"name":"","type":"uint256"}],"type":"function"},{"constant":true,"inputs":[],"name":"curator","outputs":[{"name":"","type":"address"}],"type":"function"},{"constant":true,"inputs":[{"name":"_proposalID","type":"uint256"},{"name":"_recipient","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_transactionData","type":"bytes"}],"name":"checkProposalCode","outputs":[{"name":"_codeChecksOut","type":"bool"}],"type":"function"},{"constant":true,"inputs":[],"name":"privateCreation","outputs":[{"name":"","type":"address"}],"type":"function"},{"inputs":[{"name":"_curator","type":"address"},{"name":"_daoCreator","type":"address"},{"name":"_proposalDeposit","type":"uint256"},{"name":"_minTokensToCreate","type":"uint256"},{"name":"_closingTime","type":"uint256"},{"name":"_privateCreation","type":"address"}],"type":"constructor"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_amount","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_amount","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":false,"name":"value","type":"uint256"}],"name":"FuelingToDate","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"amount","type":"uint256"}],"name":"CreatedToken","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Refund","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"proposalID","type":"uint256"},{"indexed":false,"name":"recipient","type":"address"},{"indexed":false,"name":"amount","type":"uint256"},{"indexed":false,"name":"newCurator","type":"bool"},{"indexed":false,"name":"description","type":"string"}],"name":"ProposalAdded","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"proposalID","type":"uint256"},{"indexed":false,"name":"position","type":"bool"},{"indexed":true,"name":"voter","type":"address"}],"name":"Voted","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"proposalID","type":"uint256"},{"indexed":false,"name":"result","type":"bool"},{"indexed":false,"name":"quorum","type":"uint256"}],"name":"ProposalTallied","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_newCurator","type":"address"}],"name":"NewCurator","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_recipient","type":"address"},{"indexed":false,"name":"_allowed","type":"bool"}],"name":"AllowedRecipientChanged","type":"event"}]` abi, err := api.ContractABI("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413") - noError(t, err, "api.ContractABI") + assert.NoError(t, err, "api.ContractABI") //fmt.Println(abi) @@ -26,7 +31,7 @@ func TestClient_ContractABI(t *testing.T) { func TestClient_ContractSource(t *testing.T) { source, err := api.ContractSource("0xBB9bc244D798123fDe783fCc1C72d3Bb8C189413") - noError(t, err, "api.ContractSource") + assert.NoError(t, err, "api.ContractSource") if len(source) != 1 { t.Fatalf("api.ContractSource not working, got len %v, expect 1", len(source)) diff --git a/pkg/client/gas_tracker.go b/pkg/client/gas_tracker.go index a993dc6..bee66b1 100644 --- a/pkg/client/gas_tracker.go +++ b/pkg/client/gas_tracker.go @@ -13,21 +13,21 @@ import ( "time" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/pkg/response" + "github.com/timcki/etherscan-api/v2/pkg/response" ) - type GasEstimateParams struct { +type GasEstimateParams struct { GasPrice int `json:"gasPrice"` - } +} - func (p GasEstimateParams) GetUrlValues() url.Values { +func (p GasEstimateParams) GetUrlValues() url.Values { values := url.Values{} values.Add("gasPrice", strconv.Itoa(p.GasPrice)) return values - } +} - // GasEstimate gets estimated confirmation time (in seconds) at the given gas price - func (c *Client) GasEstimate(gasPrice int) (time.Duration, error) { +// GasEstimate gets estimated confirmation time (in seconds) at the given gas price +func (c *Client) GasEstimate(gasPrice int) (time.Duration, error) { param := GasEstimateParams{ GasPrice: gasPrice, } @@ -43,13 +43,13 @@ import ( } return time.ParseDuration(confTime + "s") - } +} - // GasOracle gets suggested gas prices (in Gwei) - func (c *Client) GasOracle() (response.GasPrices, error) { +// GasOracle gets suggested gas prices (in Gwei) +func (c *Client) GasOracle() (response.GasPrices, error) { body, err := c.execute("gastracker", "gasoracle", url.Values{}) if err != nil { return response.GasPrices{}, errors.Wrap(err, "executing GasOracle request") } return response.ReadResponse[response.GasPrices](body) - } +} diff --git a/gas_tracker_e2e_test.go b/pkg/client/gas_tracker_integration_test.go similarity index 53% rename from gas_tracker_e2e_test.go rename to pkg/client/gas_tracker_integration_test.go index 3905bc3..8a9bfa2 100644 --- a/gas_tracker_e2e_test.go +++ b/pkg/client/gas_tracker_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,22 +8,24 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "testing" + + "github.com/stretchr/testify/assert" ) -//GasEstiamte generates dynamic data. Best we can do is ensure all fields are populated +// 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") + assert.NoError(t, err, "api.GasEstimate") } -//GasOracle generates dynamic data. Best we can do is ensure all fields are populated +// 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") + assert.NoError(t, err, "api.GasOrcale") if 0 == len(gasPrice.GasUsedRatio) { t.Errorf("gasPrice.GasUsedRatio empty") diff --git a/pkg/client/logs.go b/pkg/client/logs.go index 877daeb..77ca1b8 100644 --- a/pkg/client/logs.go +++ b/pkg/client/logs.go @@ -12,27 +12,27 @@ import ( "strconv" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/pkg/response" + "github.com/timcki/etherscan-api/v2/pkg/response" ) - type LogParams struct { +type LogParams struct { FromBlock int `json:"fromBlock"` ToBlock int `json:"toBlock"` Topic0 string `json:"topic0"` Address string `json:"address"` - } +} - func (p LogParams) GetUrlValues() url.Values { +func (p LogParams) GetUrlValues() url.Values { values := url.Values{} values.Add("fromBlock", strconv.Itoa(p.FromBlock)) values.Add("toBlock", strconv.Itoa(p.ToBlock)) values.Add("topic0", p.Topic0) values.Add("address", p.Address) return values - } +} - // 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) ([]response.Log, error) { +// 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) ([]response.Log, error) { param := LogParams{ FromBlock: fromBlock, ToBlock: toBlock, @@ -45,4 +45,4 @@ import ( return nil, errors.Wrap(err, "executing GetLogs request") } return response.ReadResponse[[]response.Log](body) - } +} diff --git a/logs_e2e_test.go b/pkg/client/logs_integration_test.go similarity index 75% rename from logs_e2e_test.go rename to pkg/client/logs_integration_test.go index fccf2c3..e97b1b9 100644 --- a/logs_e2e_test.go +++ b/pkg/client/logs_integration_test.go @@ -1,13 +1,17 @@ -package etherscan +//go:build integration +// +build integration + +package client import ( "testing" - "github.com/google/go-cmp/cmp" + "github.com/stretchr/testify/assert" + "github.com/timcki/etherscan-api/v2/pkg/response" ) func TestClient_GetLogs(t *testing.T) { - expectedLogs := []Log{ + expectedLogs := []response.Log{ { Address: "0x33990122638b9132ca29c723bdf037f1a891a70c", Topics: []string{"0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545", "0x72657075746174696f6e00000000000000000000000000000000000000000000", "0x000000000000000000000000d9b2f59f3b5c7b3c67047d2f03c3e8052470be92"}, @@ -21,11 +25,7 @@ func TestClient_GetLogs(t *testing.T) { actualLogs, err := api.GetLogs(379224, 379225, "0x33990122638b9132ca29c723bdf037f1a891a70c", "0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545") - noError(t, err, "api.GetLogs") - - equal := cmp.Equal(expectedLogs, actualLogs) + assert.NoError(t, err, "api.GetLogs") - if !equal { - t.Errorf("api.GetLogs not working\n: %s\n", cmp.Diff(expectedLogs, actualLogs)) - } + assert.Equal(expectedLogs, actualLogs) } diff --git a/setup_e2e_test.go b/pkg/client/setup_integration_test.go similarity index 77% rename from setup_e2e_test.go rename to pkg/client/setup_integration_test.go index 64f2702..b4cd7d5 100644 --- a/setup_e2e_test.go +++ b/pkg/client/setup_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,13 +8,15 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "fmt" + "net/url" "os" - "testing" "time" + + "github.com/timcki/etherscan-api/v2/internal/chain" ) const apiKeyEnvName = "ETHERSCAN_API_KEY" @@ -23,18 +28,18 @@ var ( bucket *Bucket // apiKey etherscan API key apiKey string + ok bool ) func init() { - apiKey = os.Getenv(apiKeyEnvName) - if apiKey == "" { + if apiKey, ok = os.LookupEnv(apiKeyEnvName); !ok { panic(fmt.Sprintf("API key is empty, set env variable %q with a valid API key to proceed.", apiKeyEnvName)) } bucket = NewBucket(500 * time.Millisecond) - api = New(Mainnet, apiKey) - api.Verbose = true - api.BeforeRequest = func(module string, action string, param map[string]interface{}) error { + api = NewClient(chain.EthereumMainnet, apiKey) + //api.Verbose = true + api.BeforeRequest = func(module string, action string, values url.Values) error { bucket.Take() return nil } @@ -78,10 +83,3 @@ func (b *Bucket) fillRoutine() { b.fill() } } - -// noError checks for testing error -func noError(t *testing.T, err error, msg string) { - if err != nil { - t.Fatalf("%s: %v", msg, err) - } -} diff --git a/pkg/client/stat.go b/pkg/client/stat.go new file mode 100644 index 0000000..6a8b866 --- /dev/null +++ b/pkg/client/stat.go @@ -0,0 +1,57 @@ +/* + * 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 client + +import ( + "net/url" + + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/v2/internal/types" + "github.com/timcki/etherscan-api/v2/pkg/response" +) + +type TokenTotalSupplyParams struct { + ContractAddress string `json:"contractaddress"` +} + +func (p TokenTotalSupplyParams) GetUrlValues() url.Values { + values := url.Values{} + if p.ContractAddress != "" { + values.Add("contractaddress", p.ContractAddress) + } + return values +} + +// EtherTotalSupply gets total supply of ether +func (c *Client) EtherTotalSupply() (totalSupply types.BigInt, err error) { + body, err := c.execute("stats", "ethsupply", nil) + if err != nil { + return types.BigInt{}, errors.Wrap(err, "executing ExecutionStatus request") + } + return response.ReadResponse[types.BigInt](body) +} + +// EtherLatestPrice gets the latest ether price, in BTC and USD +func (c *Client) EtherLatestPrice() (price response.LatestPrice, err error) { + body, err := c.execute("stats", "ethprice", nil) + if err != nil { + return response.LatestPrice{}, errors.Wrap(err, "executing LatestPrice request") + } + return response.ReadResponse[response.LatestPrice](body) +} + +// TokenTotalSupply gets total supply of token on specified contract address +func (c *Client) TokenTotalSupply(contractAddress string) (types.BigInt, error) { + values := TokenTotalSupplyParams{ContractAddress: contractAddress} + + body, err := c.execute("stats", "tokensupply", values.GetUrlValues()) + if err != nil { + return types.BigInt{}, errors.Wrap(err, "executing TokenTotalSupply request") + } + return response.ReadResponse[types.BigInt](body) +} diff --git a/stat_e2e_test.go b/pkg/client/stat_integration_test.go similarity index 81% rename from stat_e2e_test.go rename to pkg/client/stat_integration_test.go index f24551e..ee3fb9f 100644 --- a/stat_e2e_test.go +++ b/pkg/client/stat_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,16 +8,18 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "math/big" "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_EtherTotalSupply(t *testing.T) { totalSupply, err := api.EtherTotalSupply() - noError(t, err, "api.EtherTotalSupply") + assert.NoError(t, err, "api.EtherTotalSupply") if totalSupply.Int().Cmp(big.NewInt(100)) != 1 { t.Errorf("api.EtherTotalSupply not working, totalSupply is %s", totalSupply.Int().String()) @@ -23,7 +28,7 @@ func TestClient_EtherTotalSupply(t *testing.T) { func TestClient_EtherLatestPrice(t *testing.T) { latest, err := api.EtherLatestPrice() - noError(t, err, "api.EtherLatestPrice") + assert.NoError(t, err, "api.EtherLatestPrice") if latest.ETHBTC == 0 { t.Errorf("ETHBTC got 0") @@ -41,7 +46,7 @@ func TestClient_EtherLatestPrice(t *testing.T) { func TestClient_TokenTotalSupply(t *testing.T) { totalSupply, err := api.TokenTotalSupply("0x57d90b64a1a57749b0f932f1a3395792e12e7055") - noError(t, err, "api.TokenTotalSupply") + assert.NoError(t, err, "api.TokenTotalSupply") if totalSupply.Int().Cmp(big.NewInt(100)) != 1 { t.Errorf("api.TokenTotalSupply not working, totalSupply is %s", totalSupply.Int().String()) diff --git a/pkg/client/transaction.go b/pkg/client/transaction.go new file mode 100644 index 0000000..b7155b6 --- /dev/null +++ b/pkg/client/transaction.go @@ -0,0 +1,64 @@ +/* + * 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 client + +import ( + "net/url" + + "github.com/pkg/errors" + "github.com/timcki/etherscan-api/v2/pkg/response" +) + +// ErrPreByzantiumTx transaction before 4,370,000 does not support receipt status check +var ErrPreByzantiumTx = errors.New("pre-byzantium transaction does not support receipt status check") + +type TransactionParams struct { + TxHash string `json:"txhash"` +} + +func (p TransactionParams) GetUrlValues() url.Values { + values := url.Values{} + if p.TxHash != "" { + values.Add("txhash", p.TxHash) + } + return values +} + +// ExecutionStatus checks contract execution status +func (c *Client) ExecutionStatus(txHash string) (response.ExecutionStatus, error) { + param := TransactionParams{TxHash: txHash} + + body, err := c.execute("transaction", "getstatus", param.GetUrlValues()) + if err != nil { + return response.ExecutionStatus{}, errors.Wrap(err, "executing ExecutionStatus request") + } + return response.ReadResponse[response.ExecutionStatus](body) +} + +// ReceiptStatus checks transaction receipt status +func (c *Client) ReceiptStatus(txHash string) (int, error) { + param := TransactionParams{TxHash: txHash} + body, err := c.execute("transaction", "gettxreceiptstatus", param.GetUrlValues()) + if err != nil { + return 0, errors.Wrap(err, "executing ReceiptStatus request") + } + + rawStatus, err := response.ReadResponse[response.StatusReponse](body) + if err != nil { + return 0, err + } + + switch rawStatus.Status { + case "0": + return 0, nil + case "1": + return 1, nil + default: + return -1, ErrPreByzantiumTx + } +} diff --git a/transaction_e2e_test.go b/pkg/client/transaction_integration_test.go similarity index 84% rename from transaction_e2e_test.go rename to pkg/client/transaction_integration_test.go index 255552c..8247f8e 100644 --- a/transaction_e2e_test.go +++ b/pkg/client/transaction_integration_test.go @@ -1,3 +1,6 @@ +//go:build integration +// +build integration + /* * Copyright (c) 2018 LI Zhennan * @@ -5,10 +8,12 @@ * You may find a license copy in project root. */ -package etherscan +package client import ( "testing" + + "github.com/stretchr/testify/assert" ) func TestClient_ExecutionStatus(t *testing.T) { @@ -16,14 +21,14 @@ func TestClient_ExecutionStatus(t *testing.T) { // bad execution bad, err := api.ExecutionStatus("0x15f8e5ea1079d9a0bb04a4c58ae5fe7654b5b2b4463375ff7ffb490aa0032f3a") - noError(t, err, "api.ExecutionStatus") + assert.NoError(t, err, "api.ExecutionStatus") if bad.IsError != 1 && bad.ErrDescription != "Bad jump destination" { t.Errorf("api.ExecutionStatus not working, got\n%+v", bad) } // good execution good, err := api.ExecutionStatus("0xe8253035f1a1e93be24f43a3592a2c6cdbe3360e6f738ff40d46305252b44f5c") - noError(t, err, "api.ExecutionStatus") + assert.NoError(t, err, "api.ExecutionStatus") if good.IsError != 0 && good.ErrDescription != "" { t.Errorf("api.ExecutionStatus not working, got\n%+v", good) } @@ -34,14 +39,14 @@ func TestClient_ReceiptStatus(t *testing.T) { // bad execution bad, err := api.ReceiptStatus("0xe7bbbeb43cf863e20ec865021d63005149c133d7822e8edc1e6cb746d6728d4e") - noError(t, err, "api.ReceiptStatus") + assert.NoError(t, err, "api.ReceiptStatus") if bad != 0 { t.Errorf("api.ExecutionStatus not working, got %v, want 0", bad) } // good execution good, err := api.ReceiptStatus("0xe8253035f1a1e93be24f43a3592a2c6cdbe3360e6f738ff40d46305252b44f5c") - noError(t, err, "api.ReceiptStatus") + assert.NoError(t, err, "api.ReceiptStatus") if good != 1 { t.Errorf("api.ExecutionStatus not working, got %v, want 1", good) } diff --git a/pkg/response/response.go b/pkg/response/response.go index e0c992e..8e5d549 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -15,7 +15,7 @@ import ( "strings" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/internal/types" + "github.com/timcki/etherscan-api/v2/internal/types" ) type EtherscanResponse interface { @@ -32,7 +32,8 @@ type EtherscanResponse interface { LatestPrice | []LatestPrice | Log | []Log | GasPrices | []GasPrices | - types.BigInt | types.Time | string + StatusReponse | []StatusReponse | + types.BigInt | []types.BigInt | types.Time | string } // envelope is the carrier of nearly every response @@ -66,6 +67,10 @@ type AccountBalance struct { Balance *types.BigInt `json:"balance"` } +type StatusReponse struct { + Status string `json:"status"` +} + type ( // EtherscanTx represents EtherscanTx interface { diff --git a/stat.go b/stat.go deleted file mode 100644 index d3dc156..0000000 --- a/stat.go +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 - -// EtherTotalSupply gets total supply of ether -func (c *Client) EtherTotalSupply() (totalSupply *BigInt, err error) { - err = c.call("stats", "ethsupply", nil, &totalSupply) - return -} - -// EtherLatestPrice gets the latest ether price, in BTC and USD -func (c *Client) EtherLatestPrice() (price LatestPrice, err error) { - err = c.call("stats", "ethprice", nil, &price) - return -} - -// TokenTotalSupply gets total supply of token on specified contract address -func (c *Client) TokenTotalSupply(contractAddress string) (totalSupply *BigInt, err error) { - param := M{ - "contractaddress": contractAddress, - } - - err = c.call("stats", "tokensupply", param, &totalSupply) - return -} diff --git a/transaction.go b/transaction.go deleted file mode 100644 index 8461342..0000000 --- a/transaction.go +++ /dev/null @@ -1,59 +0,0 @@ -/* - * 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 "errors" - -// ErrPreByzantiumTx transaction before 4,370,000 does not support receipt status check -var ErrPreByzantiumTx = errors.New("pre-byzantium transaction does not support receipt status check") - -// ExecutionStatus checks contract execution status (if there was an error during contract execution) -// -// note on IsError: 0 = pass, 1 = error -func (c *Client) ExecutionStatus(txHash string) (status ExecutionStatus, err error) { - param := M{ - "txhash": txHash, - } - - err = c.call("transaction", "getstatus", param, &status) - return -} - -// ReceiptStatus checks transaction receipt status -// -// only applicable for post byzantium fork transactions, i.e. after block 4,370,000 -// -// An special err ErrPreByzantiumTx raises for the transaction before byzantium fork. -// -// Note: receiptStatus: 0 = Fail, 1 = Pass. -func (c *Client) ReceiptStatus(txHash string) (receiptStatus int, err error) { - param := M{ - "txhash": txHash, - } - - var rawStatus = struct { - Status string `json:"status"` - }{} - - err = c.call("transaction", "gettxreceiptstatus", param, &rawStatus) - if err != nil { - return - } - - switch rawStatus.Status { - case "0": - receiptStatus = 0 - case "1": - receiptStatus = 1 - default: - receiptStatus = -1 - err = ErrPreByzantiumTx - } - - return -} From 47ff9c580ac3f0c1d28a90085f9bd6baca103bb6 Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Mon, 6 Jan 2025 13:40:41 +0000 Subject: [PATCH 16/21] chore(repo): update README --- README.md | 37 +++++++++++------------ README_ZH.md | 83 ---------------------------------------------------- 2 files changed, 17 insertions(+), 103 deletions(-) delete mode 100644 README_ZH.md diff --git a/README.md b/README.md index 56acd1f..bbe935a 100644 --- a/README.md +++ b/README.md @@ -1,47 +1,46 @@ -**English** | [中文](https://github.com/nanmu42/etherscan-api/blob/master/README_ZH.md) - # etherscan-api -[![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) -[![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions) -[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) -[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) +[![GoDoc](https://godoc.org/github.com/timcki/etherscan-api/v2?status.svg)](https://godoc.org/github.com/timcki/etherscan-api) -Golang client for the Etherscan.io API(and its families like BscScan), with nearly full implementation(accounts, transactions, tokens, contracts, blocks, stats), full network support(Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba), and only depending on standard library. :wink: +Golang client for the Etherscan.io v2 API with nearly full implementation(accounts, transactions, tokens, contracts, blocks, stats) and minimal dependencies. # Usage ```bash -go get github.com/nanmu42/etherscan-api +go get github.com/timcki/etherscan-api/v2 ``` Create an API instance and off you go. :rocket: ```go import ( - "github.com/nanmu42/etherscan-api" + "github.com/nanmu42/etherscan-api/v2/pkg/client" + "github.com/nanmu42/etherscan-api/v2/pkg/chain" "fmt" ) func main() { // create a API client for specified ethereum net // there are many pre-defined network in package - client := etherscan.New(etherscan.Mainnet, "[your API key]") - - // or, if you are working with etherscan-family API like BscScan + client := client.NewClient(chain.EthereumMainnet, "[your API key]") + + // or, if you are working with antoher chain + // client := client.NewClient(chain.OpMainnet, "[your API key]") // + // or more customized // client := etherscan.NewCustomized(etherscan.Customization{ // Timeout: 15 * time.Second, // Key: "You key here", - // BaseURL: "https://api.bscscan.com/api?", + // Chain: chain.NewChain(), + // BaseURL: "", // Verbose: false, // }) // (optional) add hooks, e.g. for rate limit - client.BeforeRequest = func(module, action string, param map[string]interface{}) error { + client.BeforeRequest = func(module, action string, values url.Values) error { // ... } - client.AfterRequest = func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) { + client.AfterRequest = func(module, action string, values url.Values, outcome interface{}, requestErr error) error { // ... } @@ -61,20 +60,18 @@ func main() { } ``` -You may find full method list at [GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api). +You may find full method list at [GoDoc](https://godoc.org/github.com/timcki/etherscan-api/v2). # Etherscan API Key You may apply for an API key on [etherscan](https://etherscan.io/apis). -> The Etherscan Ethereum Developer APIs are provided as a community service and without warranty, so please just use what you need and no more. They support both GET/POST requests and a rate limit of 5 requests/sec (exceed and you will be blocked). +> The Etherscan Ethereum Developer APIs are provided as a community service and without warranty, so please just use what you need and no more. They support both GET/POST requests and a rate limit of 5 requests/sec (exceed and you will be blocked). # Paperwork Things -I am not from Etherscan and I just find their service really useful, so I implement this. :smile: +This library is not affiliated with Etherscan.io, it's developed for internal use in TokenTax. # License Use of this work is governed by an MIT License. - -You may find a license copy in project root. diff --git a/README_ZH.md b/README_ZH.md deleted file mode 100644 index 905a770..0000000 --- a/README_ZH.md +++ /dev/null @@ -1,83 +0,0 @@ -[English](https://github.com/nanmu42/etherscan-api/blob/master/README.md) | **中文** - -# etherscan-api - -[![GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api?status.svg)](https://godoc.org/github.com/nanmu42/etherscan-api) -[![CI status](https://github.com/nanmu42/etherscan-api/actions/workflows/ci.yaml/badge.svg)](https://github.com/nanmu42/etherscan-api/actions) -[![codecov](https://codecov.io/gh/nanmu42/etherscan-api/branch/master/graph/badge.svg)](https://codecov.io/gh/nanmu42/etherscan-api) -[![Go Report Card](https://goreportcard.com/badge/github.com/nanmu42/etherscan-api)](https://goreportcard.com/report/github.com/nanmu42/etherscan-api) - -Etherscan API的Golang客户端, -支持几乎所有功能(accounts, transactions, tokens, contracts, blocks, stats), -所有公共网络(Mainnet, Ropsten, Kovan, Rinkby, Goerli, Tobalaba)。 -本项目只依赖于官方库。 :wink: - -# 使用方法 - -```bash -go get github.com/nanmu42/etherscan-api -``` - -填入网络选项和API Key即可开始使用。 :rocket: - -```go -import ( - "github.com/nanmu42/etherscan-api" - "fmt" -) - -func main() { - // 创建连接指定网络的客户端 - client := etherscan.New(etherscan.Mainnet, "[your API key]") - - // 或者,如果你要调用的是EtherScan家族的BscScan: - // - // client := etherscan.NewCustomized(etherscan.Customization{ - // Timeout: 15 * time.Second, - // Key: "You key here", - // BaseURL: "https://api.bscscan.com/api?", - // Verbose: false, - // }) - - // (可选)按需注册钩子函数,例如用于速率控制 - client.BeforeRequest = func(module, action string, param map[string]interface{}) error { - // ... - } - client.AfterRequest = func(module, action string, param map[string]interface{}, outcome interface{}, requestErr error) { - // ... - } - - // 查询账户以太坊余额 - balance, err := client.AccountBalance("0x281055afc982d96fab65b3a49cac8b878184cb16") - if err != nil { - panic(err) - } - // 余额以 *big.Int 的类型呈现,单位为 wei - fmt.Println(balance.Int()) - - // 查询token余额 - tokenBalance, err := client.TokenBalance("contractAddress", "holderAddress") - - // 查询出入指定地址的ERC20转账列表 - transfers, err := client.ERC20Transfers("contractAddress", "address", startBlock, endBlock, page, offset) -} -``` - -客户端方法列表可在[GoDoc](https://godoc.org/github.com/nanmu42/etherscan-api)查询。 - -# Etherscan API Key - -API Key可以在[etherscan](https://etherscan.io/apis)申请。 - -Etherscan的API服务是一个公开的社区无偿服务,请避免滥用。 -API的调用速率不能高于5次/秒,否则会遭到封禁。 - -# 利益声明 - -我和Etherscan没有任何联系。我仅仅是觉得他们的服务很棒,而自己又恰好需要这样一个库。 :smile: - -# 许可证 - -MIT - -请自由享受开源,欢迎贡献开源。 \ No newline at end of file From 34450acb7619018afcddfd28c91d61e6a349f4eb Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Mon, 6 Jan 2025 17:36:11 +0000 Subject: [PATCH 17/21] fix(response): error handling when status == 0 Return a more comprehensive error when the type of reponse in the envelope is mismatched (when status != 1) --- pkg/chain/chain.go | 6 ++---- pkg/client/client.go | 2 +- pkg/response/response.go | 11 ++++++++++- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/pkg/chain/chain.go b/pkg/chain/chain.go index acbfaad..9b481c4 100644 --- a/pkg/chain/chain.go +++ b/pkg/chain/chain.go @@ -142,10 +142,8 @@ func (n Chain) String() string { return "Unknown Network" } -// ChainID returns the chain ID as a uint64 -func (n Chain) ChainID() int { - return int(n) -} +// ID returns the chain ID as an int +func (n Chain) ID() int { return int(n) } // NewNetwork creates a Network from a name and chain ID func NewNetwork(name string, chainID int) Chain { diff --git a/pkg/client/client.go b/pkg/client/client.go index 493750b..5de2a90 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -246,7 +246,7 @@ func (c *Client) craftURL(module, action string, values url.Values) string { values.Add("module", module) values.Add("action", action) values.Add("apikey", c.key) - values.Add("chainid", strconv.Itoa(c.chain.ChainID())) + values.Add("chainid", strconv.Itoa(c.chain.ID())) return fmt.Sprintf("%s?%s", c.baseURL, values.Encode()) } diff --git a/pkg/response/response.go b/pkg/response/response.go index 8e5d549..1bde2d0 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -46,11 +46,21 @@ type envelope[T EtherscanResponse] struct { Result T `json:"result"` } +type statusEnvelope struct { + Status int `json:"status,string"` +} + func ReadResponse[T EtherscanResponse](content bytes.Buffer) (T, error) { var ret T var envelope envelope[T] if err := json.Unmarshal(content.Bytes(), &envelope); err != nil { + var statusEnv statusEnvelope + if err := json.Unmarshal(content.Bytes(), &statusEnv); err == nil { + if statusEnv.Status != 1 { + return ret, errors.Errorf("unmarshaling response into %T; message=%s", ret, envelope.Message) + } + } return ret, errors.Wrapf(err, "unmarshaling etherscan response; body=%s", content.Bytes()) } if envelope.Status != 1 { @@ -58,7 +68,6 @@ func ReadResponse[T EtherscanResponse](content bytes.Buffer) (T, error) { } return envelope.Result, nil - } // AccountBalance account and its balance in pair From ea46f9c2b7545df23a0407ae6d7600239b0abfa0 Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Tue, 7 Jan 2025 14:23:56 +0000 Subject: [PATCH 18/21] chore(repo): rename package to match new owner --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index d91a7ec..14cb8cb 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/timcki/etherscan-api/v2 +module github.com/TokenTax/etherscan-api/v2 go 1.23.0 From d3e0f95554cc6e58080d1ec9e5ff6992ab8116bd Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Tue, 7 Jan 2025 14:25:06 +0000 Subject: [PATCH 19/21] chore(repo): rename import paths --- README.md | 6 +++--- pkg/client/account.go | 4 ++-- pkg/client/block.go | 2 +- pkg/client/client.go | 2 +- pkg/client/client_test.go | 2 +- pkg/client/contract.go | 2 +- pkg/client/gas_tracker.go | 2 +- pkg/client/logs.go | 2 +- pkg/client/logs_integration_test.go | 2 +- pkg/client/setup_integration_test.go | 2 +- pkg/client/stat.go | 4 ++-- pkg/client/transaction.go | 2 +- pkg/response/response.go | 2 +- 13 files changed, 17 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index bbe935a..f56ba0d 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # etherscan-api -[![GoDoc](https://godoc.org/github.com/timcki/etherscan-api/v2?status.svg)](https://godoc.org/github.com/timcki/etherscan-api) +[![GoDoc](https://godoc.org/github.com/TokenTax/etherscan-api/v2?status.svg)](https://godoc.org/github.com/TokenTax/etherscan-api) Golang client for the Etherscan.io v2 API with nearly full implementation(accounts, transactions, tokens, contracts, blocks, stats) and minimal dependencies. # Usage ```bash -go get github.com/timcki/etherscan-api/v2 +go get github.com/TokenTax/etherscan-api/v2 ``` Create an API instance and off you go. :rocket: @@ -60,7 +60,7 @@ func main() { } ``` -You may find full method list at [GoDoc](https://godoc.org/github.com/timcki/etherscan-api/v2). +You may find full method list at [GoDoc](https://godoc.org/github.com/TokenTax/etherscan-api/v2). # Etherscan API Key diff --git a/pkg/client/account.go b/pkg/client/account.go index dcf3cbf..e77a0d8 100644 --- a/pkg/client/account.go +++ b/pkg/client/account.go @@ -13,9 +13,9 @@ import ( "strconv" "strings" + "github.com/TokenTax/etherscan-api/v2/internal/types" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/internal/types" - "github.com/timcki/etherscan-api/v2/pkg/response" ) type AccountBalanceParams struct { diff --git a/pkg/client/block.go b/pkg/client/block.go index 105456e..311f517 100644 --- a/pkg/client/block.go +++ b/pkg/client/block.go @@ -12,8 +12,8 @@ import ( "net/url" "strconv" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/pkg/response" ) type BlockRewardParams struct { diff --git a/pkg/client/client.go b/pkg/client/client.go index 5de2a90..7c0ee15 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -17,8 +17,8 @@ import ( "strconv" "time" + "github.com/TokenTax/etherscan-api/v2/pkg/chain" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/pkg/chain" ) type ( diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index bcd59c9..38ad198 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -11,8 +11,8 @@ import ( "net/url" "testing" + "github.com/TokenTax/etherscan-api/v2/pkg/chain" "github.com/stretchr/testify/assert" - "github.com/timcki/etherscan-api/v2/pkg/chain" ) func TestClient_craftURL(t *testing.T) { diff --git a/pkg/client/contract.go b/pkg/client/contract.go index f956881..3de00a9 100644 --- a/pkg/client/contract.go +++ b/pkg/client/contract.go @@ -10,8 +10,8 @@ package client import ( "net/url" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/pkg/response" ) type ContractParams struct { diff --git a/pkg/client/gas_tracker.go b/pkg/client/gas_tracker.go index bee66b1..5cf120d 100644 --- a/pkg/client/gas_tracker.go +++ b/pkg/client/gas_tracker.go @@ -12,8 +12,8 @@ import ( "strconv" "time" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/pkg/response" ) type GasEstimateParams struct { diff --git a/pkg/client/logs.go b/pkg/client/logs.go index 77ca1b8..51d9c3b 100644 --- a/pkg/client/logs.go +++ b/pkg/client/logs.go @@ -11,8 +11,8 @@ import ( "net/url" "strconv" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/pkg/response" ) type LogParams struct { diff --git a/pkg/client/logs_integration_test.go b/pkg/client/logs_integration_test.go index e97b1b9..6a7b75a 100644 --- a/pkg/client/logs_integration_test.go +++ b/pkg/client/logs_integration_test.go @@ -6,8 +6,8 @@ package client import ( "testing" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/stretchr/testify/assert" - "github.com/timcki/etherscan-api/v2/pkg/response" ) func TestClient_GetLogs(t *testing.T) { diff --git a/pkg/client/setup_integration_test.go b/pkg/client/setup_integration_test.go index b4cd7d5..cf4c16f 100644 --- a/pkg/client/setup_integration_test.go +++ b/pkg/client/setup_integration_test.go @@ -16,7 +16,7 @@ import ( "os" "time" - "github.com/timcki/etherscan-api/v2/internal/chain" + "github.com/TokenTax/etherscan-api/v2/internal/chain" ) const apiKeyEnvName = "ETHERSCAN_API_KEY" diff --git a/pkg/client/stat.go b/pkg/client/stat.go index 6a8b866..11099f2 100644 --- a/pkg/client/stat.go +++ b/pkg/client/stat.go @@ -10,9 +10,9 @@ package client import ( "net/url" + "github.com/TokenTax/etherscan-api/v2/internal/types" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/internal/types" - "github.com/timcki/etherscan-api/v2/pkg/response" ) type TokenTotalSupplyParams struct { diff --git a/pkg/client/transaction.go b/pkg/client/transaction.go index b7155b6..b335fbd 100644 --- a/pkg/client/transaction.go +++ b/pkg/client/transaction.go @@ -10,8 +10,8 @@ package client import ( "net/url" + "github.com/TokenTax/etherscan-api/v2/pkg/response" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/pkg/response" ) // ErrPreByzantiumTx transaction before 4,370,000 does not support receipt status check diff --git a/pkg/response/response.go b/pkg/response/response.go index 1bde2d0..28c1da7 100644 --- a/pkg/response/response.go +++ b/pkg/response/response.go @@ -14,8 +14,8 @@ import ( "strconv" "strings" + "github.com/TokenTax/etherscan-api/v2/internal/types" "github.com/pkg/errors" - "github.com/timcki/etherscan-api/v2/internal/types" ) type EtherscanResponse interface { From b046ea70d2cbd357b46d3a8d61e2ae1d68080fb2 Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Tue, 7 Jan 2025 14:27:02 +0000 Subject: [PATCH 20/21] fix(readme): rename imports paths typo --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f56ba0d..af9277d 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ Create an API instance and off you go. :rocket: ```go import ( - "github.com/nanmu42/etherscan-api/v2/pkg/client" - "github.com/nanmu42/etherscan-api/v2/pkg/chain" + "github.com/TokenTax/etherscan-api/v2/pkg/client" + "github.com/TokenTax/etherscan-api/v2/pkg/chain" "fmt" ) From ef21cf18110f20ecd8c02a6fa1ad654b745b0fef Mon Sep 17 00:00:00 2001 From: Tim Chmielecki Date: Tue, 7 Jan 2025 14:31:58 +0000 Subject: [PATCH 21/21] fix(test): fix broken import path --- pkg/client/setup_integration_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/client/setup_integration_test.go b/pkg/client/setup_integration_test.go index cf4c16f..d02604e 100644 --- a/pkg/client/setup_integration_test.go +++ b/pkg/client/setup_integration_test.go @@ -16,7 +16,7 @@ import ( "os" "time" - "github.com/TokenTax/etherscan-api/v2/internal/chain" + "github.com/TokenTax/etherscan-api/v2/pkg/chain" ) const apiKeyEnvName = "ETHERSCAN_API_KEY"