diff --git a/.gitignore b/.gitignore index 7af61243..dc7c69c8 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,5 @@ Gododir/godobin-* test_data_dir/ vendor/ +contracts/* environments/*.toml diff --git a/cmd/run/main.go b/cmd/run/main.go index e04a3d6e..7b9c094c 100644 --- a/cmd/run/main.go +++ b/cmd/run/main.go @@ -17,9 +17,9 @@ func main() { environment := flag.String("environment", "", "Environment name") flag.Parse() config := cmd.LoadConfig(*environment) - fmt.Println("Client Path ", config.Client.IPCPath) repository := repositories.NewPostgres(config.Database) + fmt.Printf("Creating Geth Blockchain to: %s\n", config.Client.IPCPath) listener := blockchain_listener.NewBlockchainListener( geth.NewGethBlockchain(config.Client.IPCPath), []core.BlockchainObserver{ diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go index 3c140426..ced5c5da 100644 --- a/pkg/core/blockchain.go +++ b/pkg/core/blockchain.go @@ -5,4 +5,5 @@ type Blockchain interface { SubscribeToBlocks(blocks chan Block) StartListening() StopListening() + GetContractStateAttribute(contractHash string, attributeName string) (*string, error) } diff --git a/pkg/fakes/blockchain.go b/pkg/fakes/blockchain.go index b7b7c4f8..c45da542 100644 --- a/pkg/fakes/blockchain.go +++ b/pkg/fakes/blockchain.go @@ -3,13 +3,23 @@ package fakes import "github.com/8thlight/vulcanizedb/pkg/core" type Blockchain struct { - blocks map[int64]core.Block - blocksChannel chan core.Block - WasToldToStop bool + blocks map[int64]core.Block + contractAttributes map[string]map[string]string + blocksChannel chan core.Block + WasToldToStop bool +} + +func (blockchain *Blockchain) GetContractStateAttribute(contractHash string, attributeName string) (*string, error) { + result := new(string) + *result = blockchain.contractAttributes[contractHash][attributeName] + return result, nil } func NewBlockchain() *Blockchain { - return &Blockchain{blocks: make(map[int64]core.Block)} + return &Blockchain{ + blocks: make(map[int64]core.Block), + contractAttributes: make(map[string]map[string]string), + } } func NewBlockchainWithBlocks(blocks []core.Block) *Blockchain { @@ -40,3 +50,11 @@ func (*Blockchain) StartListening() {} func (blockchain *Blockchain) StopListening() { blockchain.WasToldToStop = true } + +func (blockchain *Blockchain) SetContractStateAttribute(contractHash string, attributeName string, attributeValue string) { + contractStateAttributes := blockchain.contractAttributes[contractHash] + if contractStateAttributes == nil { + blockchain.contractAttributes[contractHash] = make(map[string]string) + } + blockchain.contractAttributes[contractHash][attributeName] = attributeValue +} diff --git a/pkg/geth/abi.go b/pkg/geth/abi.go new file mode 100644 index 00000000..eba7c25f --- /dev/null +++ b/pkg/geth/abi.go @@ -0,0 +1,27 @@ +package geth + +import ( + "errors" + "io/ioutil" + "strings" + + "github.com/ethereum/go-ethereum/accounts/abi" +) + +var ( + ErrInvalidAbiFile = errors.New("invalid abi") + ErrMissingAbiFile = errors.New("missing abi") +) + +func ParseAbiFile(abiFilePath string) (abi.ABI, error) { + filesBytes, err := ioutil.ReadFile(abiFilePath) + if err != nil { + return abi.ABI{}, ErrMissingAbiFile + } + abiString := string(filesBytes) + parsedAbi, err := abi.JSON(strings.NewReader(abiString)) + if err != nil { + return abi.ABI{}, ErrInvalidAbiFile + } + return parsedAbi, nil +} diff --git a/pkg/geth/abi_test.go b/pkg/geth/abi_test.go new file mode 100644 index 00000000..bf89a165 --- /dev/null +++ b/pkg/geth/abi_test.go @@ -0,0 +1,43 @@ +package geth_test + +import ( + "path/filepath" + + cfg "github.com/8thlight/vulcanizedb/pkg/config" + "github.com/8thlight/vulcanizedb/pkg/geth" + "github.com/ethereum/go-ethereum/accounts/abi" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Reading ABI files", func() { + + It("loads a valid ABI file", func() { + contractHash := "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" + path := filepath.Join(cfg.ProjectRoot(), "contracts", "public", contractHash+".json") + + contractAbi, err := geth.ParseAbiFile(path) + + Expect(contractAbi).NotTo(BeNil()) + Expect(err).To(BeNil()) + }) + + It("returns an error when the file does not exist", func() { + path := filepath.Join(cfg.ProjectRoot(), "contracts", "public", "missing_file.json") + + contractAbi, err := geth.ParseAbiFile(path) + + Expect(contractAbi).To(Equal(abi.ABI{})) + Expect(err).To(Equal(geth.ErrMissingAbiFile)) + }) + + It("returns an error when the file has invalid contents", func() { + path := filepath.Join(cfg.ProjectRoot(), "pkg", "geth", "testing", "invalid_abi.json") + + contractAbi, err := geth.ParseAbiFile(path) + + Expect(contractAbi).To(Equal(abi.ABI{})) + Expect(err).To(Equal(geth.ErrInvalidAbiFile)) + }) + +}) diff --git a/pkg/geth/contract.go b/pkg/geth/contract.go new file mode 100644 index 00000000..474354d2 --- /dev/null +++ b/pkg/geth/contract.go @@ -0,0 +1,37 @@ +package geth + +import ( + "errors" + "fmt" + "path/filepath" + + "github.com/8thlight/vulcanizedb/pkg/config" + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" +) + +var ( + ErrInvalidStateAttribute = errors.New("invalid state attribute") +) + +func (blockchain *GethBlockchain) GetContractStateAttribute(contractHash string, attributeName string) (*string, error) { + boundContract, err := bindContract(common.HexToAddress(contractHash), blockchain.client, blockchain.client) + if err != nil { + return nil, err + } + result := new(string) + err = boundContract.Call(&bind.CallOpts{}, result, attributeName) + if err != nil { + return nil, ErrInvalidStateAttribute + } + return result, nil +} + +func bindContract(address common.Address, caller bind.ContractCaller, transactor bind.ContractTransactor) (*bind.BoundContract, error) { + abiFilePath := filepath.Join(config.ProjectRoot(), "contracts", "public", fmt.Sprintf("%s.json", address.Hex())) + parsed, err := ParseAbiFile(abiFilePath) + if err != nil { + return nil, err + } + return bind.NewBoundContract(address, parsed, caller, transactor), nil +} diff --git a/pkg/geth/contract_test.go b/pkg/geth/contract_test.go new file mode 100644 index 00000000..f9eb3ff8 --- /dev/null +++ b/pkg/geth/contract_test.go @@ -0,0 +1,47 @@ +package geth_test + +//import ( +// cfg "github.com/8thlight/vulcanizedb/pkg/config" +// "github.com/8thlight/vulcanizedb/pkg/geth" +// . "github.com/onsi/ginkgo" +// . "github.com/onsi/gomega" +//) +// +//var _ = Describe("The Geth blockchain", func() { +// +// 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) +// contractHash := "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" +// +// name, err := blockchain.GetContractStateAttribute(contractHash, "name") +// +// Expect(*name).To(Equal("OMGToken")) +// Expect(err).To(BeNil()) +// }) +// +// It("returns an error when there is no ABI for the given contract", func() { +// config, _ := cfg.NewConfig("public") +// blockchain := geth.NewGethBlockchain(config.Client.IPCPath) +// contractHash := "MISSINGHASH" +// +// name, err := blockchain.GetContractStateAttribute(contractHash, "name") +// +// Expect(name).To(BeNil()) +// Expect(err).To(Equal(geth.ErrMissingAbiFile)) +// }) +// +// It("returns an error when asking for an attribute that does not exist", func() { +// config, _ := cfg.NewConfig("public") +// blockchain := geth.NewGethBlockchain(config.Client.IPCPath) +// contractHash := "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07" +// +// name, err := blockchain.GetContractStateAttribute(contractHash, "missing_attribute") +// +// Expect(err).To(Equal(geth.ErrInvalidStateAttribute)) +// Expect(name).To(BeNil()) +// }) +// }) +// +//}) diff --git a/pkg/geth/geth_blockchain.go b/pkg/geth/geth_blockchain.go index 60ea34f8..d2ba8012 100644 --- a/pkg/geth/geth_blockchain.go +++ b/pkg/geth/geth_blockchain.go @@ -25,7 +25,6 @@ func (blockchain *GethBlockchain) GetBlockByNumber(blockNumber int64) core.Block } func NewGethBlockchain(ipcPath string) *GethBlockchain { - fmt.Printf("Creating Geth Blockchain to: %s\n", ipcPath) blockchain := GethBlockchain{} client, _ := ethclient.Dial(ipcPath) blockchain.client = client diff --git a/pkg/geth/testing/invalid_abi.json b/pkg/geth/testing/invalid_abi.json new file mode 100644 index 00000000..2a260b84 --- /dev/null +++ b/pkg/geth/testing/invalid_abi.json @@ -0,0 +1 @@ +bad json \ No newline at end of file diff --git a/pkg/watched_contracts/summary.go b/pkg/watched_contracts/summary.go index 5971c7d6..7eb34b0b 100644 --- a/pkg/watched_contracts/summary.go +++ b/pkg/watched_contracts/summary.go @@ -12,30 +12,33 @@ type ContractSummary struct { ContractHash string NumberOfTransactions int LastTransaction *core.Transaction + blockChain core.Blockchain } var NewContractNotWatchedErr = func(contractHash string) error { return errors.New(fmt.Sprintf("Contract %v not being watched", contractHash)) } -func NewSummary(_ core.Blockchain, repository repositories.Repository, contractHash string) (*ContractSummary, error) { - contract := repository.FindWatchedContract(contractHash) - if contract != nil { - return newContractSummary(*contract), nil +func NewSummary(blockchain core.Blockchain, repository repositories.Repository, contractHash string) (*ContractSummary, error) { + watchedContract := repository.FindWatchedContract(contractHash) + if watchedContract != nil { + return newContractSummary(blockchain, *watchedContract), nil } else { return nil, NewContractNotWatchedErr(contractHash) } } -func (ContractSummary) GetStateAttribute(attributeName string) string { - return "Hello world" +func (contractSummary ContractSummary) GetStateAttribute(attributeName string) string { + result, _ := contractSummary.blockChain.GetContractStateAttribute(contractSummary.ContractHash, attributeName) + return *result } -func newContractSummary(contract core.WatchedContract) *ContractSummary { +func newContractSummary(blockchain core.Blockchain, watchedContract core.WatchedContract) *ContractSummary { return &ContractSummary{ - ContractHash: contract.Hash, - NumberOfTransactions: len(contract.Transactions), - LastTransaction: lastTransaction(contract), + blockChain: blockchain, + ContractHash: watchedContract.Hash, + NumberOfTransactions: len(watchedContract.Transactions), + LastTransaction: lastTransaction(watchedContract), } } diff --git a/pkg/watched_contracts/summary_test.go b/pkg/watched_contracts/summary_test.go index 30d80d72..b2eb113e 100644 --- a/pkg/watched_contracts/summary_test.go +++ b/pkg/watched_contracts/summary_test.go @@ -82,6 +82,18 @@ var _ bool = Describe("The watched contract summary", func() { Expect(contractSummary.LastTransaction.Hash).To(Equal("TRANSACTION2")) }) + + It("gets a state attribute for the contract from the blockchain", func() { + repository := repositories.NewInMemory() + watchedContract := core.WatchedContract{Hash: "0x123"} + repository.CreateWatchedContract(watchedContract) + blockchain := fakes.NewBlockchain() + blockchain.SetContractStateAttribute("0x123", "foo", "bar") + + contractSummary, _ := watched_contracts.NewSummary(blockchain, repository, "0x123") + + Expect(contractSummary.GetStateAttribute("foo")).To(Equal("bar")) + }) }) })