Get logs for a contract (#99)

* Add ability to fetch logs for a contract and a block

* Test contract related code against Infura, so can run on Travis

* Add godo task for getLogs
This commit is contained in:
Matt K 2017-12-11 15:08:00 -06:00 committed by GitHub
parent 921bde1089
commit 5e64283a12
16 changed files with 420 additions and 119 deletions

View File

@ -36,6 +36,22 @@ func tasks(p *do.Project) {
do.M{"environment": environment, "startingNumber": startingNumber, "$in": "cmd/populate_blocks"})
})
p.Task("getLogs", nil, func(context *do.Context) {
environment := parseEnvironment(context)
blockNumber := context.Args.MayInt(-1, "block-number", "b")
contractHash := context.Args.MayString("", "contract-hash", "c")
if contractHash == "" {
log.Fatalln("--contract-hash required")
}
context.Start(`go run main.go --environment={{.environment}} --contract-hash={{.contractHash}} --block-number={{.blockNumber}}`,
do.M{
"environment": environment,
"contractHash": contractHash,
"blockNumber": blockNumber,
"$in": "cmd/get_logs",
})
})
p.Task("watchContract", nil, func(context *do.Context) {
environment := parseEnvironment(context)
contractHash := context.Args.MayString("", "contract-hash", "c")

View File

@ -86,6 +86,12 @@ The name of the JSON file should correspond the contract's address.
2. Start watching the contract `godo watchContract -- --environment=<some-environment> --contract-hash=<contract-address>`
3. Request summary data `godo showContractSummary -- --environment=<some-environment> --contract-hash=<contract-address>`
## Retrieving Contract Logs
1. Get the logs
- `godo getLogs -- --environment=<some-environment> --contract-hash=<contract-address> --starting-number=<starting-block-number>`
### Configuring Additional Environments
You can create configuration files for additional environments.

40
cmd/get_logs/main.go Normal file
View File

@ -0,0 +1,40 @@
package main
import (
"fmt"
"log"
"flag"
"github.com/8thlight/vulcanizedb/cmd"
"github.com/8thlight/vulcanizedb/pkg/core"
"github.com/8thlight/vulcanizedb/pkg/geth"
)
func main() {
environment := flag.String("environment", "", "Environment name")
contractHash := flag.String("contract-hash", "", "Contract hash to show summary")
_blockNumber := flag.Int64("block-number", -1, "Block number of summary")
flag.Parse()
config := cmd.LoadConfig(*environment)
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
blockNumber := cmd.RequestedBlockNumber(_blockNumber)
logs, err := blockchain.GetLogs(core.Contract{Hash: *contractHash}, blockNumber)
if err != nil {
log.Fatalln(err)
}
for _, l := range logs {
fmt.Println("\tAddress: ", l.Address)
fmt.Println("\tTxHash: ", l.TxHash)
fmt.Println("\tBlockNumber ", l.BlockNumber)
fmt.Println("\tTopics: ")
for i, topic := range l.Topics {
fmt.Printf("\t\tTopic %d: %s\n", i, topic)
}
fmt.Printf("\tData: %s", l.Data)
fmt.Print("\n\n")
}
}

View File

@ -7,8 +7,6 @@ import (
"fmt"
"math/big"
"github.com/8thlight/vulcanizedb/cmd"
"github.com/8thlight/vulcanizedb/pkg/contract_summary"
"github.com/8thlight/vulcanizedb/pkg/geth"
@ -22,7 +20,7 @@ func main() {
config := cmd.LoadConfig(*environment)
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
repository := cmd.LoadPostgres(config.Database, blockchain.Node())
blockNumber := requestedBlockNumber(_blockNumber)
blockNumber := cmd.RequestedBlockNumber(_blockNumber)
contractSummary, err := contract_summary.NewSummary(blockchain, repository, *contractHash, blockNumber)
if err != nil {
@ -31,13 +29,3 @@ func main() {
output := contract_summary.GenerateConsoleOutput(contractSummary)
fmt.Println(output)
}
func requestedBlockNumber(blockNumber *int64) *big.Int {
var _blockNumber *big.Int
if *blockNumber == -1 {
_blockNumber = nil
} else {
_blockNumber = big.NewInt(*blockNumber)
}
return _blockNumber
}

View File

@ -7,6 +7,8 @@ import (
"fmt"
"math/big"
"github.com/8thlight/vulcanizedb/pkg/config"
"github.com/8thlight/vulcanizedb/pkg/core"
"github.com/8thlight/vulcanizedb/pkg/geth"
@ -55,3 +57,13 @@ func GetAbi(abiFilepath string, contractHash string) string {
}
return contractAbiString
}
func RequestedBlockNumber(blockNumber *int64) *big.Int {
var _blockNumber *big.Int
if *blockNumber == -1 {
_blockNumber = nil
} else {
_blockNumber = big.NewInt(*blockNumber)
}
return _blockNumber
}

7
environments/infura.toml Normal file
View File

@ -0,0 +1,7 @@
[database]
name = "vulcanize_private"
hostname = "localhost"
port = 5432
[client]
ipcPath = "https://mainnet.infura.io/J5Vd2fRtGsw0zZ0Ov3BL"

View File

@ -0,0 +1,142 @@
package integration
import (
"math/big"
"log"
cfg "github.com/8thlight/vulcanizedb/pkg/config"
"github.com/8thlight/vulcanizedb/pkg/core"
"github.com/8thlight/vulcanizedb/pkg/geth"
"github.com/8thlight/vulcanizedb/pkg/geth/testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Reading contracts", func() {
Describe("Reading the list of attributes", func() {
It("returns a string attribute for a real contract", func() {
config, err := cfg.NewConfig("infura")
if err != nil {
log.Fatalln(err)
}
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
contractAttributes, err := blockchain.GetAttributes(contract)
Expect(err).To(BeNil())
Expect(len(contractAttributes)).NotTo(Equal(0))
symbolAttribute := *testing.FindAttribute(contractAttributes, "symbol")
Expect(symbolAttribute.Name).To(Equal("symbol"))
Expect(symbolAttribute.Type).To(Equal("string"))
})
It("does not return an attribute that takes an input", func() {
config, err := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
contractAttributes, err := blockchain.GetAttributes(contract)
Expect(err).To(BeNil())
attribute := testing.FindAttribute(contractAttributes, "balanceOf")
Expect(attribute).To(BeNil())
})
It("does not return an attribute that is not constant", func() {
config, _ := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
contractAttributes, err := blockchain.GetAttributes(contract)
Expect(err).To(BeNil())
attribute := testing.FindAttribute(contractAttributes, "unpause")
Expect(attribute).To(BeNil())
})
})
Describe("Getting a contract attribute", func() {
It("returns the correct attribute for a real contract", func() {
config, _ := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
name, err := blockchain.GetAttribute(contract, "name", nil)
Expect(err).To(BeNil())
Expect(name).To(Equal("OMGToken"))
})
It("returns the correct attribute for a real contract", func() {
config, _ := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
name, err := blockchain.GetAttribute(contract, "name", nil)
Expect(err).To(BeNil())
Expect(name).To(Equal("OMGToken"))
})
It("returns the correct attribute for a real contract at a specific block height", func() {
config, _ := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
name, err := blockchain.GetAttribute(contract, "name", big.NewInt(4701536))
Expect(name).To(Equal("OMGToken"))
Expect(err).To(BeNil())
})
It("returns an error when asking for an attribute that does not exist", func() {
config, _ := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
name, err := blockchain.GetAttribute(contract, "missing_attribute", nil)
Expect(err).To(Equal(geth.ErrInvalidStateAttribute))
Expect(name).To(BeNil())
})
It("retrieves the event log for a specific block and contract", func() {
expectedLogZero := core.Log{
BlockNumber: 4703824,
TxHash: "0xf896bfd1eb539d881a1a31102b78de9f25cd591bf1fe1924b86148c0b205fd5d",
Address: "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07",
Topics: []string{
"0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef",
"0x000000000000000000000000fbb1b73c4f0bda4f67dca266ce6ef42f520fbb98",
"0x000000000000000000000000d26114cd6ee289accf82350c8d8487fedb8a0c07",
},
Data: "0x0000000000000000000000000000000000000000000000000c7d713b49da0000"}
config, _ := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
contract := testing.SampleContract()
logs, err := blockchain.GetLogs(contract, big.NewInt(4703824))
Expect(err).To(BeNil())
Expect(len(logs)).To(Equal(3))
Expect(logs[0]).To(Equal(expectedLogZero))
})
It("returns and empty log array when no events for a given block / contract combo", func() {
config, _ := cfg.NewConfig("infura")
blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
logs, err := blockchain.GetLogs(core.Contract{Hash: "x123"}, big.NewInt(4703824))
Expect(err).To(BeNil())
Expect(len(logs)).To(Equal(0))
})
})
})

View File

@ -12,6 +12,8 @@ import (
"errors"
"net/url"
"github.com/BurntSushi/toml"
)
@ -24,6 +26,10 @@ var NewErrConfigFileNotFound = func(environment string) error {
return errors.New(fmt.Sprintf("No configuration found for environment: %v", environment))
}
var NewErrBadConnectionString = func(connectionString string) error {
return errors.New(fmt.Sprintf("connection string is invalid: %v", connectionString))
}
func NewConfig(environment string) (Config, error) {
filenameWithExtension := fmt.Sprintf("%s.toml", environment)
absolutePath := filepath.Join(ProjectRoot(), "environments", filenameWithExtension)
@ -31,7 +37,7 @@ func NewConfig(environment string) (Config, error) {
if err != nil {
return Config{}, NewErrConfigFileNotFound(environment)
} else {
if !filepath.IsAbs(config.Client.IPCPath) {
if !filepath.IsAbs(config.Client.IPCPath) && !isUrl(config.Client.IPCPath) {
config.Client.IPCPath = filepath.Join(ProjectRoot(), config.Client.IPCPath)
}
return config, nil
@ -43,16 +49,31 @@ func ProjectRoot() string {
return path.Join(path.Dir(filename), "..", "..")
}
func isUrl(s string) bool {
_, err := url.ParseRequestURI(s)
if err == nil {
return true
}
return false
}
func fileExists(s string) bool {
_, err := os.Stat(s)
if err == nil {
return true
}
return false
}
func parseConfigFile(filePath string) (Config, error) {
var cfg Config
_, err := os.Stat(filePath)
if err != nil {
return Config{}, err
if !isUrl(filePath) && !fileExists(filePath) {
return Config{}, NewErrBadConnectionString(filePath)
} else {
_, err := toml.DecodeFile(filePath, &cfg)
if err != nil {
return Config{}, err
}
return cfg, err
return cfg, nil
}
}

View File

@ -28,4 +28,14 @@ var _ = Describe("Loading the config", func() {
Expect(err).NotTo(BeNil())
})
It("reads the infura config using the environment", func() {
infuraConfig, err := cfg.NewConfig("infura")
Expect(err).To(BeNil())
Expect(infuraConfig.Database.Hostname).To(Equal("localhost"))
Expect(infuraConfig.Database.Name).To(Equal("vulcanize_private"))
Expect(infuraConfig.Database.Port).To(Equal(5432))
Expect(infuraConfig.Client.IPCPath).To(Equal("https://mainnet.infura.io/J5Vd2fRtGsw0zZ0Ov3BL"))
})
})

View File

@ -10,4 +10,5 @@ type Blockchain interface {
StopListening()
GetAttributes(contract Contract) (ContractAttributes, error)
GetAttribute(contract Contract, attributeName string, blockNumber *big.Int) (interface{}, error)
GetLogs(contract Contract, blockNumber *big.Int) ([]Log, error)
}

9
pkg/core/log.go Normal file
View File

@ -0,0 +1,9 @@
package core
type Log struct {
BlockNumber int64
TxHash string
Address string
Topics []string
Data string
}

View File

@ -9,6 +9,7 @@ import (
)
type Blockchain struct {
logs map[string][]core.Log
blocks map[int64]core.Block
contractAttributes map[string]map[string]string
blocksChannel chan core.Block
@ -16,6 +17,10 @@ type Blockchain struct {
node core.Node
}
func (blockchain *Blockchain) GetLogs(contract core.Contract, blockNumber *big.Int) ([]core.Log, error) {
return blockchain.logs[contract.Hash], nil
}
func (blockchain *Blockchain) Node() core.Node {
return blockchain.node
}
@ -33,6 +38,7 @@ func (blockchain *Blockchain) GetAttribute(contract core.Contract, attributeName
func NewBlockchain() *Blockchain {
return &Blockchain{
blocks: make(map[int64]core.Block),
logs: make(map[string][]core.Log),
contractAttributes: make(map[string]map[string]string),
node: core.Node{GenesisBlock: "GENESIS"},
}

View File

@ -1,101 +0,0 @@
package geth_test
//import (
// "math/big"
//
// cfg "github.com/8thlight/vulcanizedb/pkg/config"
// "github.com/8thlight/vulcanizedb/pkg/geth"
// "github.com/8thlight/vulcanizedb/pkg/geth/testing"
// . "github.com/onsi/ginkgo"
// . "github.com/onsi/gomega"
//)
//
//var _ = Describe("Reading contracts", func() {
//
// Describe("Reading the list of attributes", func() {
// It("returns a string attribute for a real contract", func() {
// config, _ := cfg.NewConfig("public")
// blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
// contract := testing.SampleContract()
//
// contractAttributes, err := blockchain.GetAttributes(contract)
//
// Expect(err).To(BeNil())
// Expect(len(contractAttributes)).NotTo(Equal(0))
// symbolAttribute := *testing.FindAttribute(contractAttributes, "symbol")
// Expect(symbolAttribute.Name).To(Equal("symbol"))
// Expect(symbolAttribute.Type).To(Equal("string"))
// })
//
// It("does not return an attribute that takes an input", func() {
// config, _ := cfg.NewConfig("public")
// blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
// contract := testing.SampleContract()
//
// contractAttributes, err := blockchain.GetAttributes(contract)
//
// Expect(err).To(BeNil())
// attribute := testing.FindAttribute(contractAttributes, "balanceOf")
// Expect(attribute).To(BeNil())
// })
//
// It("does not return an attribute that is not constant", func() {
// config, _ := cfg.NewConfig("public")
// blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
// contract := testing.SampleContract()
//
// contractAttributes, err := blockchain.GetAttributes(contract)
//
// Expect(err).To(BeNil())
// attribute := testing.FindAttribute(contractAttributes, "unpause")
// Expect(attribute).To(BeNil())
// })
// })
//
// Describe("Getting a contract attribute", func() {
// It("returns the correct attribute for a real contract", func() {
// config, _ := cfg.NewConfig("public")
// blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
//
// contract := testing.SampleContract()
// name, err := blockchain.GetAttribute(contract, "name", nil)
//
// Expect(err).To(BeNil())
// Expect(name).To(Equal("OMGToken"))
// })
//
// It("returns the correct attribute for a real contract", func() {
// config, _ := cfg.NewConfig("public")
// blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
// contract := testing.SampleContract()
//
// name, err := blockchain.GetAttribute(contract, "name", nil)
//
// Expect(err).To(BeNil())
// Expect(name).To(Equal("OMGToken"))
// })
//
// It("returns the correct attribute for a real contract at a specific block height", func() {
// config, _ := cfg.NewConfig("public")
// blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
// contract := testing.SampleContract()
//
// name, err := blockchain.GetAttribute(contract, "name", big.NewInt(4652791))
//
// Expect(name).To(Equal("OMGToken"))
// Expect(err).To(BeNil())
// })
//
// It("returns an error when asking for an attribute that does not exist", func() {
// config, _ := cfg.NewConfig("public")
// blockchain := geth.NewGethBlockchain(config.Client.IPCPath)
// contract := testing.SampleContract()
//
// name, err := blockchain.GetAttribute(contract, "missing_attribute", nil)
//
// Expect(err).To(Equal(geth.ErrInvalidStateAttribute))
// Expect(name).To(BeNil())
// })
// })
//
//})

View File

@ -8,6 +8,7 @@ import (
"github.com/8thlight/vulcanizedb/pkg/core"
"github.com/8thlight/vulcanizedb/pkg/geth/node"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/rpc"
@ -22,6 +23,21 @@ type GethBlockchain struct {
node core.Node
}
func (blockchain *GethBlockchain) GetLogs(contract core.Contract, blockNumber *big.Int) ([]core.Log, error) {
contractAddress := common.HexToAddress(contract.Hash)
fc := ethereum.FilterQuery{
FromBlock: blockNumber,
ToBlock: blockNumber,
Addresses: []common.Address{contractAddress},
}
gethLogs, err := blockchain.client.FilterLogs(context.Background(), fc)
if err != nil {
return []core.Log{}, err
}
logs := GethLogsToCoreLogs(gethLogs)
return logs, nil
}
func (blockchain *GethBlockchain) Node() core.Node {
return blockchain.node
}
@ -60,3 +76,8 @@ func (blockchain *GethBlockchain) StartListening() {
func (blockchain *GethBlockchain) StopListening() {
blockchain.newHeadSubscription.Unsubscribe()
}
func (blockchain *GethBlockchain) latestBlock() *big.Int {
block, _ := blockchain.client.HeaderByNumber(context.Background(), nil)
return block.Number
}

View File

@ -0,0 +1,32 @@
package geth
import (
"github.com/8thlight/vulcanizedb/pkg/core"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
)
func GethLogToCoreLog(gethLog types.Log) core.Log {
topics := gethLog.Topics
var hexTopics []string
for _, topic := range topics {
hexTopics = append(hexTopics, topic.Hex())
}
return core.Log{
Address: gethLog.Address.Hex(),
BlockNumber: int64(gethLog.BlockNumber),
Topics: hexTopics,
TxHash: gethLog.TxHash.Hex(),
Data: hexutil.Encode(gethLog.Data),
}
}
func GethLogsToCoreLogs(gethLogs []types.Log) []core.Log {
var logs []core.Log
for _, log := range gethLogs {
log := GethLogToCoreLog(log)
logs = append(logs, log)
}
return logs
}

View File

@ -0,0 +1,91 @@
package geth_test
import (
"github.com/8thlight/vulcanizedb/pkg/core"
"github.com/8thlight/vulcanizedb/pkg/geth"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/common/hexutil"
"github.com/ethereum/go-ethereum/core/types"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
var _ = Describe("Conversion of GethLog to core.Log", func() {
It("converts geth log to internal log format", func() {
gethLog := types.Log{
Address: common.HexToAddress("0xecf8f87f810ecf450940c9f60066b4a7a501d6a7"),
BlockHash: common.HexToHash("0x656c34545f90a730a19008c0e7a7cd4fb3895064b48d6d69761bd5abad681056"),
BlockNumber: 2019236,
Data: hexutil.MustDecode("0x000000000000000000000000000000000000000000000001a055690d9db80000"),
Index: 2,
TxIndex: 3,
TxHash: common.HexToHash("0x3b198bfd5d2907285af009e9ae84a0ecd63677110d89d7e030251acb87f6487e"),
Topics: []common.Hash{
common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
common.HexToHash("0x00000000000000000000000080b2c9d7cbbf30a1b0fc8983c647d754c6525615"),
},
}
expected := core.Log{
Address: gethLog.Address.Hex(),
BlockNumber: int64(gethLog.BlockNumber),
Data: hexutil.Encode(gethLog.Data),
TxHash: gethLog.TxHash.Hex(),
Topics: []string{
common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef").Hex(),
common.HexToHash("0x00000000000000000000000080b2c9d7cbbf30a1b0fc8983c647d754c6525615").Hex(),
},
}
coreLog := geth.GethLogToCoreLog(gethLog)
Expect(coreLog.Address).To(Equal(expected.Address))
Expect(coreLog.BlockNumber).To(Equal(expected.BlockNumber))
Expect(coreLog.Data).To(Equal(expected.Data))
Expect(coreLog.Topics[0]).To(Equal(expected.Topics[0]))
Expect(coreLog.Topics[1]).To(Equal(expected.Topics[1]))
Expect(coreLog.TxHash).To(Equal(expected.TxHash))
})
It("converts geth log array to array of internal logs", func() {
gethLogOne := types.Log{
Address: common.HexToAddress("0xecf8f87f810ecf450940c9f60066b4a7a501d6a7"),
BlockHash: common.HexToHash("0x656c34545f90a730a19008c0e7a7cd4fb3895064b48d6d69761bd5abad681056"),
BlockNumber: 2019236,
Data: hexutil.MustDecode("0x000000000000000000000000000000000000000000000001a055690d9db80000"),
Index: 2,
TxIndex: 3,
TxHash: common.HexToHash("0x3b198bfd5d2907285af009e9ae84a0ecd63677110d89d7e030251acb87f6487e"),
Topics: []common.Hash{
common.HexToHash("0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef"),
common.HexToHash("0x00000000000000000000000080b2c9d7cbbf30a1b0fc8983c647d754c6525615"),
},
}
gethLogTwo := types.Log{
Address: common.HexToAddress("0x123"),
BlockHash: common.HexToHash("0x576"),
BlockNumber: 2019236,
Data: hexutil.MustDecode("0x0000000000000000000000000000000000000000000000000000000000000001"),
Index: 3,
TxIndex: 4,
TxHash: common.HexToHash("0x134"),
Topics: []common.Hash{
common.HexToHash("0xaaa"),
common.HexToHash("0xbbb"),
},
}
expectedOne := geth.GethLogToCoreLog(gethLogOne)
expectedTwo := geth.GethLogToCoreLog(gethLogTwo)
coreLogs := geth.GethLogsToCoreLogs([]types.Log{gethLogOne, gethLogTwo})
Expect(len(coreLogs)).To(Equal(2))
Expect(coreLogs[0]).To(Equal(expectedOne))
Expect(coreLogs[1]).To(Equal(expectedTwo))
})
})