├── dockerfiles └── geth.ipc ├── pkg ├── testing │ ├── invalid_abi.json │ ├── valid_abi.json │ ├── helpers.go │ └── sample_abi.json ├── fakes │ ├── mock_header_sync_block_retriever.go │ ├── mock_poller.go │ ├── mock_header_sync_header_repository.go │ ├── mock_parser.go │ └── mock_fetcher.go ├── core │ ├── topics.go │ ├── contract.go │ ├── log.go │ └── fetcher.go ├── filters │ ├── filters_suite_test.go │ ├── filter_query.go │ └── filter_test.go ├── parser │ ├── parser_suite_test.go │ ├── parser.go │ └── parser_test.go ├── fetcher │ ├── fetcher_suite_test.go │ ├── log_fetcher.go │ ├── log_fetcher_test.go │ ├── base_fetcher_test.go │ ├── base_fetcher.go │ ├── interface_fetcher.go │ └── contract_data_fetcher.go ├── contract │ ├── contract_suite_test.go │ ├── contract.go │ └── contract_test.go ├── abi │ ├── abi_suite_test.go │ ├── abi.go │ └── abi_test.go ├── converter │ ├── converter_suite_test.go │ ├── log_converter_test.go │ └── log_converter.go ├── retriever │ ├── retriever_suite_test.go │ ├── block_retriever.go │ ├── block_retriever_test.go │ └── address_retriever.go ├── repository │ └── repository_suite_test.go ├── transformer │ ├── transformer_suite_test.go │ └── transformer_test.go ├── helpers │ ├── helpers.go │ └── test_helpers │ │ ├── test_data.go │ │ └── mocks │ │ ├── parser.go │ │ └── entities.go ├── types │ ├── mode.go │ ├── event.go │ └── method.go ├── constants │ └── interface.go └── config │ └── contract.go ├── temp_rsa.enc ├── environments ├── testing.toml ├── header_sync.toml └── example.toml ├── db ├── migrations │ ├── 00004_create_postgraphile_comments.sql │ ├── 00003_create_checked_headers_table.sql │ ├── 00001_create_nodes_table.sql │ └── 00002_create_headers_table.sql └── schema.sql ├── .dockerignore ├── main.go ├── .github └── workflows │ ├── on-pr.yaml │ ├── on-master.yaml │ └── publish.yaml ├── .gitignore ├── scripts ├── install-postgres-11.sh ├── reset_db └── gomoderator.py ├── go.mod ├── .travis.yml ├── integration_test ├── integration_test_suite_test.go ├── contract_test.go └── interface_fetcher_test.go ├── startup_script.sh ├── Dockerfile ├── version └── version.go ├── cmd ├── version.go ├── watch.go └── root.go ├── docker-compose.yml └── Makefile /dockerfiles/geth.ipc: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /pkg/testing/invalid_abi.json: -------------------------------------------------------------------------------- 1 | bad json -------------------------------------------------------------------------------- /pkg/testing/valid_abi.json: -------------------------------------------------------------------------------- 1 | [{"foo": "bar"}] -------------------------------------------------------------------------------- /temp_rsa.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vulcanize/eth-contract-watcher/HEAD/temp_rsa.enc -------------------------------------------------------------------------------- /environments/testing.toml: -------------------------------------------------------------------------------- 1 | [database] 2 | name = "vulcanize_testing" 3 | hostname = "localhost" 4 | port = 5432 5 | 6 | [client] 7 | rpcPath = "http://127.0.0.1:8545" 8 | -------------------------------------------------------------------------------- /db/migrations/00004_create_postgraphile_comments.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | COMMENT ON TABLE public.nodes IS E'@name NodeInfo'; 3 | COMMENT ON COLUMN public.nodes.node_id IS E'@name ChainNodeID'; 4 | COMMENT ON COLUMN public.headers.node_id IS E'@name HeaderNodeID'; 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .git 2 | .travis.yml 3 | .idea 4 | bin 5 | .gitignore 6 | integration_test 7 | LICENSE 8 | postgraphile 9 | .private_blockchain_password 10 | README.md 11 | scripts 12 | Supfile 13 | test_config 14 | .travis.yml 15 | vulcanizedb.log 16 | Dockerfile 17 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/vulcanize/eth-contract-watcher/cmd" 5 | 6 | "github.com/sirupsen/logrus" 7 | ) 8 | 9 | func main() { 10 | logrus.SetFormatter(&logrus.TextFormatter{ 11 | FullTimestamp: true, 12 | }) 13 | cmd.Execute() 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/on-pr.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Build 2 | 3 | on: [pull_request] 4 | 5 | jobs: 6 | build: 7 | name: Run docker build 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | - name: Run docker build 12 | run: make docker-build 13 | -------------------------------------------------------------------------------- /db/migrations/00003_create_checked_headers_table.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE public.checked_headers ( 3 | id SERIAL PRIMARY KEY, 4 | header_id INTEGER UNIQUE NOT NULL REFERENCES headers (id) ON DELETE CASCADE 5 | ); 6 | 7 | -- +goose Down 8 | DROP TABLE public.checked_headers; 9 | -------------------------------------------------------------------------------- /db/migrations/00001_create_nodes_table.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE nodes ( 3 | id SERIAL PRIMARY KEY, 4 | client_name VARCHAR, 5 | genesis_block VARCHAR(66), 6 | network_id VARCHAR, 7 | node_id VARCHAR(128), 8 | chain_id INTEGER, 9 | CONSTRAINT node_uc UNIQUE (genesis_block, network_id, node_id, chain_id) 10 | ); 11 | 12 | -- +goose Down 13 | DROP TABLE nodes; 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | test_data_dir/ 4 | contracts/* 5 | #environments/*.toml 6 | Vagrantfile 7 | vagrant*.sh 8 | .vagrant 9 | test_scripts/ 10 | eth-contract-watcher 11 | postgraphile/build/ 12 | postgraphile/node_modules/ 13 | postgraphile/package-lock.json 14 | vulcanizedb.log 15 | db/migrations/20*.sql 16 | plugins/*.so 17 | postgraphile/*.toml 18 | postgraphile/schema.graphql 19 | vulcanizedb.pem 20 | -------------------------------------------------------------------------------- /pkg/fakes/mock_header_sync_block_retriever.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | type MockHeaderSyncBlockRetriever struct { 4 | FirstBlock int64 5 | FirstBlockErr error 6 | } 7 | 8 | func (retriever *MockHeaderSyncBlockRetriever) RetrieveFirstBlock() (int64, error) { 9 | return retriever.FirstBlock, retriever.FirstBlockErr 10 | } 11 | 12 | func (retriever *MockHeaderSyncBlockRetriever) RetrieveMostRecentBlock() (int64, error) { 13 | return 0, nil 14 | } 15 | -------------------------------------------------------------------------------- /scripts/install-postgres-11.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -ex 4 | 5 | echo "Installing Postgres 11" 6 | sudo service postgresql stop 7 | sudo apt-get remove -q 'postgresql-*' 8 | sudo apt-get update -q 9 | sudo apt-get install -q postgresql-11 postgresql-client-11 10 | sudo cp /etc/postgresql/{9.6,11}/main/pg_hba.conf 11 | 12 | echo "Restarting Postgres 11" 13 | sudo service postgresql restart 14 | 15 | sudo psql -c 'CREATE ROLE travis SUPERUSER LOGIN CREATEDB;' -U postgres -------------------------------------------------------------------------------- /environments/header_sync.toml: -------------------------------------------------------------------------------- 1 | [database] 2 | name = "vulcanize_public" 3 | hostname = "contact-watcher-db" 4 | port = 5432 5 | user = "vdbm" 6 | password = "password" 7 | 8 | [client] 9 | rpcPath = "http://eth-server:8081" 10 | 11 | [ethereum] 12 | nodeID = "arch1" # $ETH_NODE_ID 13 | clientName = "Geth" # $ETH_CLIENT_NAME 14 | genesisBlock = "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" # $ETH_GENESIS_BLOCK 15 | networkID = "4" # $ETH_NETWORK_ID 16 | chainID = "4" # $ETH_CHAIN_ID -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/vulcanize/eth-contract-watcher 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/ethereum/go-ethereum v1.9.11 7 | github.com/hashicorp/golang-lru v0.5.3 8 | github.com/hpcloud/tail v1.0.0 9 | github.com/jmoiron/sqlx v1.2.0 10 | github.com/lib/pq v1.6.0 11 | github.com/onsi/ginkgo v1.7.0 12 | github.com/onsi/gomega v1.4.3 13 | github.com/sirupsen/logrus v1.6.0 14 | github.com/spf13/cobra v1.0.0 15 | github.com/spf13/viper v1.7.0 16 | github.com/vulcanize/eth-header-sync v0.1.1 17 | golang.org/x/net v0.0.0-20200528225125-3c3fba18258b 18 | golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a 19 | gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 20 | ) 21 | -------------------------------------------------------------------------------- /scripts/reset_db: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # Provide me with a postgres database name, and I will: 3 | # - Drop the database 4 | # - Recreate the database 5 | # - Run the eth-contract-watcher migration 6 | 7 | if [ "$1" = "" ]; then 8 | echo "Provide a database name to reset" 9 | exit 1 10 | fi 11 | 12 | db=$1 13 | dir=$(basename "$(pwd)") 14 | if [ $dir != "eth-contract-watcher" ] 15 | then 16 | echo "Run me from the eth-contract-watcher root dir" 17 | exit 1 18 | fi 19 | 20 | user=$(whoami) 21 | psql -c "DROP DATABASE $db" postgres 22 | if [ $? -eq 0 ]; then 23 | psql -c "CREATE DATABASE $db WITH OWNER $user" postgres 24 | make migrate HOST_NAME=localhost NAME=$db PORT=5432 25 | else 26 | echo "Couldnt drop the database. Are you connected? Does it exist?" 27 | fi 28 | -------------------------------------------------------------------------------- /pkg/core/topics.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package core 18 | 19 | type Topics [4]string 20 | -------------------------------------------------------------------------------- /db/migrations/00002_create_headers_table.sql: -------------------------------------------------------------------------------- 1 | -- +goose Up 2 | CREATE TABLE public.headers 3 | ( 4 | id SERIAL PRIMARY KEY, 5 | hash VARCHAR(66), 6 | block_number BIGINT, 7 | raw JSONB, 8 | block_timestamp NUMERIC, 9 | check_count INTEGER NOT NULL DEFAULT 0, 10 | node_id INTEGER NOT NULL REFERENCES nodes (id) ON DELETE CASCADE, 11 | eth_node_fingerprint VARCHAR(128), 12 | UNIQUE (block_number, hash, eth_node_fingerprint) 13 | ); 14 | 15 | CREATE INDEX headers_block_number 16 | ON public.headers (block_number); 17 | 18 | CREATE INDEX headers_block_timestamp 19 | ON public.headers (block_timestamp); 20 | 21 | -- +goose Down 22 | DROP INDEX public.headers_block_number; 23 | DROP INDEX public.headers_block_timestamp; 24 | 25 | DROP TABLE public.headers; 26 | -------------------------------------------------------------------------------- /pkg/core/contract.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package core 18 | 19 | type Contract struct { 20 | Abi string 21 | Hash string 22 | } 23 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: go 3 | go: 4 | - 1.12 5 | services: 6 | - postgresql 7 | addons: 8 | ssh_known_hosts: arch1.vdb.to 9 | postgresql: '11.2' 10 | go_import_path: github.com/vulcanize/eth-contract-watcher 11 | before_install: 12 | - openssl aes-256-cbc -K $encrypted_e1db309e8776_key -iv $encrypted_e1db309e8776_iv 13 | -in temp_rsa.enc -out temp_rsa -d 14 | - eval "$(ssh-agent -s)" 15 | - chmod 600 temp_rsa 16 | - ssh-add temp_rsa 17 | - ssh -4 -fNL 8545:localhost:8545 geth@arch1.vdb.to 18 | - make installtools 19 | - bash ./scripts/install-postgres-11.sh 20 | - curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add - 21 | - echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list 22 | - sudo apt-get update && sudo apt-get install yarn 23 | script: 24 | - env GO111MODULE=on make test 25 | - env GO111MODULE=on make integrationtest 26 | notifications: 27 | email: false 28 | -------------------------------------------------------------------------------- /pkg/core/log.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package core 18 | 19 | type Log struct { 20 | BlockNumber int64 21 | TxHash string 22 | Address string 23 | Topics 24 | Index int64 25 | Data string 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/on-master.yaml: -------------------------------------------------------------------------------- 1 | name: Docker Compose Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | jobs: 9 | build: 10 | name: Run docker build 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v2 14 | - name: Get the version 15 | id: vars 16 | run: echo ::set-output name=sha::$(echo ${GITHUB_SHA:0:7}) 17 | - name: Run docker build 18 | run: make docker-build 19 | - name: Tag docker image 20 | run: docker tag vulcanize/eth-contract-watcher docker.pkg.github.com/vulcanize/eth-contract-watcher/eth-contract-watcher:${{steps.vars.outputs.sha}} 21 | - name: Docker Login 22 | run: echo ${{ secrets.GITHUB_TOKEN }} | docker login https://docker.pkg.github.com -u vulcanize --password-stdin 23 | - name: Docker Push 24 | run: docker push docker.pkg.github.com/vulcanize/eth-contract-watcher/eth-contract-watcher:${{steps.vars.outputs.sha}} 25 | 26 | -------------------------------------------------------------------------------- /pkg/filters/filters_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package filters_test 18 | 19 | import ( 20 | "testing" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | ) 25 | 26 | func TestFilters(t *testing.T) { 27 | RegisterFailHandler(Fail) 28 | RunSpecs(t, "Filters Suite") 29 | } 30 | -------------------------------------------------------------------------------- /pkg/parser/parser_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package parser_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "log" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | ) 27 | 28 | func TestParser(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Parser Suite Test") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | log.SetOutput(ioutil.Discard) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/fetcher/fetcher_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fetcher_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "log" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | ) 27 | 28 | func TestFetcher(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Fetcher Suite Test") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | log.SetOutput(ioutil.Discard) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/contract/contract_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package contract_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "log" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | ) 27 | 28 | func TestContract(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Contract Suite Test") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | log.SetOutput(ioutil.Discard) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/abi/abi_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package abi_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "testing" 22 | 23 | "github.com/sirupsen/logrus" 24 | 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | ) 28 | 29 | func TestABI(t *testing.T) { 30 | RegisterFailHandler(Fail) 31 | RunSpecs(t, "ABI Test Suite") 32 | } 33 | 34 | var _ = BeforeSuite(func() { 35 | logrus.SetOutput(ioutil.Discard) 36 | }) 37 | -------------------------------------------------------------------------------- /pkg/core/fetcher.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package core 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum" 21 | "github.com/ethereum/go-ethereum/core/types" 22 | ) 23 | 24 | type Fetcher interface { 25 | FetchContractData(abiJSON string, address string, method string, methodArgs []interface{}, result interface{}, blockNumber int64) error 26 | FetchEthLogsWithCustomQuery(query ethereum.FilterQuery) ([]types.Log, error) 27 | } 28 | -------------------------------------------------------------------------------- /pkg/converter/converter_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package converter_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "log" 22 | "testing" 23 | 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | ) 27 | 28 | func TestConverter(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Header Sync Converter Suite Test") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | log.SetOutput(ioutil.Discard) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/retriever/retriever_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package retriever_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | "github.com/sirupsen/logrus" 26 | ) 27 | 28 | func TestRetriever(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Retriever Suite Test") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | logrus.SetOutput(ioutil.Discard) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/repository/repository_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package repository_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | "github.com/sirupsen/logrus" 26 | ) 27 | 28 | func TestRepository(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Repository Suite Test") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | logrus.SetOutput(ioutil.Discard) 35 | }) 36 | -------------------------------------------------------------------------------- /pkg/transformer/transformer_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package transformer_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "testing" 22 | 23 | . "github.com/onsi/ginkgo" 24 | . "github.com/onsi/gomega" 25 | "github.com/sirupsen/logrus" 26 | ) 27 | 28 | func TestTransformer(t *testing.T) { 29 | RegisterFailHandler(Fail) 30 | RunSpecs(t, "Transformer Suite Test") 31 | } 32 | 33 | var _ = BeforeSuite(func() { 34 | logrus.SetOutput(ioutil.Discard) 35 | }) 36 | -------------------------------------------------------------------------------- /integration_test/integration_test_suite_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package integration_test 18 | 19 | import ( 20 | "io/ioutil" 21 | "testing" 22 | 23 | "github.com/sirupsen/logrus" 24 | 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | ) 28 | 29 | func TestIntegrationTest(t *testing.T) { 30 | RegisterFailHandler(Fail) 31 | RunSpecs(t, "IntegrationTest Suite") 32 | } 33 | 34 | var _ = BeforeSuite(func() { 35 | logrus.SetOutput(ioutil.Discard) 36 | }) 37 | -------------------------------------------------------------------------------- /startup_script.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # Runs the db migrations and starts the watcher services 3 | 4 | # Exit if the variable tests fail 5 | set -e 6 | set +x 7 | 8 | # Check the database variables are set 9 | test $DATABASE_HOSTNAME 10 | test $DATABASE_NAME 11 | test $DATABASE_PORT 12 | test $DATABASE_USER 13 | test $DATABASE_PASSWORD 14 | test $VDB_COMMAND 15 | set +e 16 | 17 | # Construct the connection string for postgres 18 | VDB_PG_CONNECT=postgresql://$DATABASE_USER:$DATABASE_PASSWORD@$DATABASE_HOSTNAME:$DATABASE_PORT/$DATABASE_NAME?sslmode=disable 19 | 20 | # Run the DB migrations 21 | echo "Connecting with: $VDB_PG_CONNECT" 22 | echo "Running database migrations" 23 | ./goose -dir migrations/vulcanizedb postgres "$VDB_PG_CONNECT" up 24 | 25 | 26 | # If the db migrations ran without err 27 | if [[ $? -eq 0 ]]; then 28 | echo "Running the VulcanizeDB process" 29 | ./eth-contract-watcher ${VDB_COMMAND} --config=config.toml 30 | else 31 | echo "Could not run migrations. Are the database details correct?" 32 | exit 1 33 | fi 34 | 35 | # If VulcanizeDB process was successful 36 | if [ $? -eq 0 ]; then 37 | echo "VulcanizeDB process ran successfully" 38 | else 39 | echo "Could not start VulcanizeDB process. Is the config file correct?" 40 | exit 1 41 | fi -------------------------------------------------------------------------------- /.github/workflows/publish.yaml: -------------------------------------------------------------------------------- 1 | name: Publish Docker image 2 | on: 3 | release: 4 | types: [published] 5 | jobs: 6 | push_to_registries: 7 | name: Push Docker image to Docker Hub 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Get the version 11 | id: vars 12 | run: | 13 | echo ::set-output name=sha::$(echo ${GITHUB_SHA:0:7}) 14 | echo ::set-output name=tag::$(echo ${GITHUB_REF#refs/tags/}) 15 | - name: Docker Login to Github Registry 16 | run: echo ${{ secrets.GITHUB_TOKEN }} | docker login https://docker.pkg.github.com -u vulcanize --password-stdin 17 | - name: Docker Pull 18 | run: docker pull docker.pkg.github.com/vulcanize/eth-contract-watcher/eth-contract-watcher:${{steps.vars.outputs.sha}} 19 | - name: Docker Login to Docker Registry 20 | run: echo ${{ secrets.VULCANIZEJENKINS_PAT }} | docker login -u vulcanizejenkins --password-stdin 21 | - name: Tag docker image 22 | run: docker tag docker.pkg.github.com/vulcanize/eth-contract-watcher/eth-contract-watcher:${{steps.vars.outputs.sha}} vulcanize/eth-contract-watcher:${{steps.vars.outputs.tag}} 23 | - name: Docker Push to Docker Hub 24 | run: docker push vulcanize/eth-contract-watcher:${{steps.vars.outputs.tag}} 25 | 26 | -------------------------------------------------------------------------------- /pkg/helpers/helpers.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package helpers 18 | 19 | import ( 20 | "math/big" 21 | 22 | "github.com/ethereum/go-ethereum/crypto" 23 | ) 24 | 25 | // BigFromString creates a big.Int from a string 26 | func BigFromString(n string) *big.Int { 27 | b := new(big.Int) 28 | b.SetString(n, 10) 29 | return b 30 | } 31 | 32 | // GenerateSignature returns the keccak256 hash hex of a string 33 | func GenerateSignature(s string) string { 34 | eventSignature := []byte(s) 35 | hash := crypto.Keccak256Hash(eventSignature) 36 | return hash.Hex() 37 | } 38 | -------------------------------------------------------------------------------- /pkg/types/mode.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package types 18 | 19 | // Mode is used to explicitly represent the operating mode of the transformer 20 | type Mode int 21 | 22 | // Mode enums 23 | const ( 24 | HeaderSync Mode = iota 25 | FullSync 26 | ) 27 | 28 | // IsValid returns true is the Mode is valid 29 | func (mode Mode) IsValid() bool { 30 | return mode >= HeaderSync && mode <= FullSync 31 | } 32 | 33 | // String returns the string representation of the mode 34 | func (mode Mode) String() string { 35 | switch mode { 36 | case HeaderSync: 37 | return "header" 38 | case FullSync: 39 | return "full" 40 | default: 41 | return "unknown" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.13-alpine as builder 2 | 3 | RUN apk --update --no-cache add make git g++ linux-headers 4 | 5 | # Get and build eth-contract-watcher 6 | WORKDIR /go/src/github.com/vulcanize/eth-contract-watcher 7 | ADD . . 8 | RUN GO111MODULE=on GCO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -ldflags '-extldflags "-static"' -o eth-contract-watcher . 9 | 10 | # Copy migration tool 11 | WORKDIR / 12 | ARG GOOSE_VER="v2.6.0" 13 | ADD https://github.com/pressly/goose/releases/download/${GOOSE_VER}/goose-linux64 ./goose 14 | RUN chmod +x ./goose 15 | 16 | # app container 17 | FROM alpine 18 | 19 | ARG USER="vdm" 20 | ARG CONFIG_FILE="./environments/example.toml" 21 | 22 | RUN adduser -Du 5000 $USER 23 | WORKDIR /app 24 | RUN chown $USER /app 25 | USER $USER 26 | 27 | # chown first so dir is writable 28 | COPY --chown=$USER:$USER --from=builder /go/src/github.com/vulcanize/eth-contract-watcher/$CONFIG_FILE config.toml 29 | COPY --chown=$USER:$USER --from=builder /go/src/github.com/vulcanize/eth-contract-watcher/startup_script.sh . 30 | 31 | # keep binaries immutable 32 | COPY --from=builder /go/src/github.com/vulcanize/eth-contract-watcher/eth-contract-watcher eth-contract-watcher 33 | COPY --from=builder /goose goose 34 | COPY --from=builder /go/src/github.com/vulcanize/eth-contract-watcher/db/migrations migrations/vulcanizedb 35 | COPY --from=builder /go/src/github.com/vulcanize/eth-contract-watcher/environments environments 36 | 37 | ENTRYPOINT ["/app/startup_script.sh"] -------------------------------------------------------------------------------- /pkg/fakes/mock_poller.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fakes 18 | 19 | import ( 20 | "github.com/vulcanize/eth-contract-watcher/pkg/contract" 21 | ) 22 | 23 | type MockPoller struct { 24 | ContractName string 25 | } 26 | 27 | func (*MockPoller) PollContract(con contract.Contract, lastBlock int64) error { 28 | panic("implement me") 29 | } 30 | 31 | func (*MockPoller) PollContractAt(con contract.Contract, blockNumber int64) error { 32 | panic("implement me") 33 | } 34 | 35 | func (poller *MockPoller) FetchContractData(contractAbi, contractAddress, method string, methodArgs []interface{}, result interface{}, blockNumber int64) error { 36 | if p, ok := result.(*string); ok { 37 | *p = poller.ContractName 38 | } 39 | return nil 40 | } 41 | -------------------------------------------------------------------------------- /version/version.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package version 18 | 19 | import "fmt" 20 | 21 | const ( 22 | Major = 0 // Major version component of the current release 23 | Minor = 1 // Minor version component of the current release 24 | Patch = 0 // Patch version component of the current release 25 | Meta = "alpha" // Version metadata to append to the version string 26 | ) 27 | 28 | // Version holds the textual version string. 29 | var Version = func() string { 30 | return fmt.Sprintf("%d.%d.%d", Major, Minor, Patch) 31 | }() 32 | 33 | // VersionWithMeta holds the textual version string including the metadata. 34 | var VersionWithMeta = func() string { 35 | v := Version 36 | if Meta != "" { 37 | v += "-" + Meta 38 | } 39 | return v 40 | }() 41 | -------------------------------------------------------------------------------- /pkg/testing/helpers.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package testing 18 | 19 | import ( 20 | "os" 21 | 22 | "github.com/sirupsen/logrus" 23 | 24 | "github.com/vulcanize/eth-contract-watcher/pkg/abi" 25 | "github.com/vulcanize/eth-contract-watcher/pkg/core" 26 | ) 27 | 28 | var TestABIsPath = os.Getenv("GOPATH") + "/src/github.com/vulcanize/eth-contract-watcher/pkg/testing/" 29 | 30 | func SampleContract() core.Contract { 31 | return core.Contract{ 32 | Abi: sampleAbiFileContents(), 33 | Hash: "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07", 34 | } 35 | } 36 | 37 | func sampleAbiFileContents() string { 38 | abiFileContents, err := abi.ReadAbiFile(TestABIsPath + "sample_abi.json") 39 | if err != nil { 40 | logrus.Fatal(err) 41 | } 42 | return abiFileContents 43 | } 44 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Copyright © 2020 Vulcanize, Inc 2 | // 3 | // This program is free software: you can redistribute it and/or modify 4 | // it under the terms of the GNU Affero General Public License as published by 5 | // the Free Software Foundation, either version 3 of the License, or 6 | // (at your option) any later version. 7 | // 8 | // This program is distributed in the hope that it will be useful, 9 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | // GNU Affero General Public License for more details. 12 | // 13 | // You should have received a copy of the GNU Affero General Public License 14 | // along with this program. If not, see . 15 | 16 | package cmd 17 | 18 | import ( 19 | log "github.com/sirupsen/logrus" 20 | "github.com/spf13/cobra" 21 | 22 | v "github.com/vulcanize/eth-contract-watcher/version" 23 | ) 24 | 25 | // versionCmd represents the version command 26 | var versionCmd = &cobra.Command{ 27 | Use: "version", 28 | Short: "Prints the version of vulcanizeDB", 29 | Long: `Use this command to fetch the version of vulcanizeDB 30 | 31 | Usage: ./eth-contract-watcher version`, 32 | Run: func(cmd *cobra.Command, args []string) { 33 | subCommand = cmd.CalledAs() 34 | logWithCommand = *log.WithField("SubCommand", subCommand) 35 | logWithCommand.Infof("VulcanizeDB version: %s", v.VersionWithMeta) 36 | }, 37 | } 38 | 39 | func init() { 40 | rootCmd.AddCommand(versionCmd) 41 | } 42 | -------------------------------------------------------------------------------- /pkg/fakes/mock_header_sync_header_repository.go: -------------------------------------------------------------------------------- 1 | package fakes 2 | 3 | import "github.com/vulcanize/eth-header-sync/pkg/core" 4 | 5 | type MockHeaderSyncHeaderRepository struct { 6 | } 7 | 8 | func (*MockHeaderSyncHeaderRepository) AddCheckColumn(id string) error { 9 | return nil 10 | } 11 | 12 | func (*MockHeaderSyncHeaderRepository) AddCheckColumns(ids []string) error { 13 | panic("implement me") 14 | } 15 | 16 | func (*MockHeaderSyncHeaderRepository) MarkHeaderChecked(headerID int64, eventID string) error { 17 | panic("implement me") 18 | } 19 | 20 | func (*MockHeaderSyncHeaderRepository) MarkHeaderCheckedForAll(headerID int64, ids []string) error { 21 | panic("implement me") 22 | } 23 | 24 | func (*MockHeaderSyncHeaderRepository) MarkHeadersCheckedForAll(headers []core.Header, ids []string) error { 25 | panic("implement me") 26 | } 27 | 28 | func (*MockHeaderSyncHeaderRepository) MissingHeaders(startingBlockNumber int64, endingBlockNumber int64, eventID string) ([]core.Header, error) { 29 | panic("implement me") 30 | } 31 | 32 | func (*MockHeaderSyncHeaderRepository) MissingMethodsCheckedEventsIntersection(startingBlockNumber, endingBlockNumber int64, methodIds, eventIds []string) ([]core.Header, error) { 33 | panic("implement me") 34 | } 35 | 36 | func (*MockHeaderSyncHeaderRepository) MissingHeadersForAll(startingBlockNumber, endingBlockNumber int64, ids []string) ([]core.Header, error) { 37 | panic("implement me") 38 | } 39 | 40 | func (*MockHeaderSyncHeaderRepository) CheckCache(key string) (interface{}, bool) { 41 | panic("implement me") 42 | } 43 | -------------------------------------------------------------------------------- /pkg/fakes/mock_parser.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fakes 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum/accounts/abi" 21 | 22 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 23 | ) 24 | 25 | type MockParser struct { 26 | AbiToReturn string 27 | EventName string 28 | Event types.Event 29 | } 30 | 31 | func (*MockParser) Parse(contractAddr string) error { 32 | return nil 33 | } 34 | 35 | func (parser *MockParser) ParseAbiStr(abiStr string) error { 36 | parser.AbiToReturn = abiStr 37 | return nil 38 | } 39 | 40 | func (parser *MockParser) Abi() string { 41 | return parser.AbiToReturn 42 | } 43 | 44 | func (*MockParser) ParsedAbi() abi.ABI { 45 | return abi.ABI{} 46 | } 47 | 48 | func (*MockParser) GetMethods(wanted []string) []types.Method { 49 | panic("implement me") 50 | } 51 | 52 | func (*MockParser) GetSelectMethods(wanted []string) []types.Method { 53 | return []types.Method{} 54 | } 55 | 56 | func (parser *MockParser) GetEvents(wanted []string) map[string]types.Event { 57 | return map[string]types.Event{parser.EventName: parser.Event} 58 | } 59 | -------------------------------------------------------------------------------- /pkg/retriever/block_retriever.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package retriever 18 | 19 | import ( 20 | "github.com/vulcanize/eth-header-sync/pkg/postgres" 21 | ) 22 | 23 | // BlockRetriever is used to retrieve the first block for a given contract and the most recent block 24 | // It requires a vDB synced database with blocks, transactions, receipts, and logs 25 | type BlockRetriever interface { 26 | RetrieveFirstBlock() (int64, error) 27 | RetrieveMostRecentBlock() (int64, error) 28 | } 29 | 30 | type blockRetriever struct { 31 | db *postgres.DB 32 | } 33 | 34 | // NewBlockRetriever returns a new BlockRetriever 35 | func NewBlockRetriever(db *postgres.DB) BlockRetriever { 36 | return &blockRetriever{ 37 | db: db, 38 | } 39 | } 40 | 41 | // RetrieveFirstBlock retrieves block number of earliest header in repo 42 | func (r *blockRetriever) RetrieveFirstBlock() (int64, error) { 43 | var firstBlock int 44 | err := r.db.Get( 45 | &firstBlock, 46 | "SELECT block_number FROM headers ORDER BY block_number LIMIT 1", 47 | ) 48 | 49 | return int64(firstBlock), err 50 | } 51 | 52 | // RetrieveMostRecentBlock retrieves block number of latest header in repo 53 | func (r *blockRetriever) RetrieveMostRecentBlock() (int64, error) { 54 | var lastBlock int 55 | err := r.db.Get( 56 | &lastBlock, 57 | "SELECT block_number FROM headers ORDER BY block_number DESC LIMIT 1", 58 | ) 59 | 60 | return int64(lastBlock), err 61 | } 62 | -------------------------------------------------------------------------------- /integration_test/contract_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package integration 18 | 19 | import ( 20 | "math/big" 21 | 22 | "github.com/ethereum/go-ethereum/common" 23 | "github.com/ethereum/go-ethereum/ethclient" 24 | "github.com/ethereum/go-ethereum/rpc" 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | 28 | "github.com/vulcanize/eth-header-sync/test_config" 29 | 30 | "github.com/vulcanize/eth-contract-watcher/pkg/fetcher" 31 | "github.com/vulcanize/eth-contract-watcher/pkg/testing" 32 | ) 33 | 34 | var _ = Describe("Reading contracts", func() { 35 | Describe("Fetching Contract data", func() { 36 | It("returns the correct attribute for a real contract", func() { 37 | rawRPCClient, err := rpc.Dial(test_config.TestClient.RPCPath) 38 | Expect(err).NotTo(HaveOccurred()) 39 | ethClient := ethclient.NewClient(rawRPCClient) 40 | f := fetcher.NewFetcher(ethClient) 41 | 42 | contract := testing.SampleContract() 43 | var balance = new(big.Int) 44 | 45 | args := make([]interface{}, 1) 46 | args[0] = common.HexToHash("0xd26114cd6ee289accf82350c8d8487fedb8a0c07") 47 | 48 | err = f.FetchContractData(contract.Abi, "0xd26114cd6ee289accf82350c8d8487fedb8a0c07", "balanceOf", args, &balance, 5167471) 49 | Expect(err).NotTo(HaveOccurred()) 50 | expected := new(big.Int) 51 | expected.SetString("10897295492887612977137", 10) 52 | Expect(balance).To(Equal(expected)) 53 | }) 54 | }) 55 | }) 56 | -------------------------------------------------------------------------------- /integration_test/interface_fetcher_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package integration_test 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum/ethclient" 21 | "github.com/ethereum/go-ethereum/rpc" 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | 25 | "github.com/vulcanize/eth-header-sync/test_config" 26 | 27 | a "github.com/vulcanize/eth-contract-watcher/pkg/abi" 28 | "github.com/vulcanize/eth-contract-watcher/pkg/constants" 29 | "github.com/vulcanize/eth-contract-watcher/pkg/fetcher" 30 | ) 31 | 32 | var _ = Describe("Interface Getter", func() { 33 | Describe("GetAbi", func() { 34 | It("Constructs and returns a custom abi based on results from supportsInterface calls", func() { 35 | expectedABI := `[` + constants.AddrChangeInterface + `,` + constants.NameChangeInterface + `,` + constants.ContentChangeInterface + `,` + constants.AbiChangeInterface + `,` + constants.PubkeyChangeInterface + `]` 36 | con := test_config.TestClient 37 | testIPC := con.RPCPath 38 | blockNumber := int64(6885696) 39 | rawRpcClient, err := rpc.Dial(testIPC) 40 | Expect(err).NotTo(HaveOccurred()) 41 | ethClient := ethclient.NewClient(rawRpcClient) 42 | f := fetcher.NewFetcher(ethClient) 43 | abi, err := f.FetchABI(constants.PublicResolverAddress, blockNumber) 44 | Expect(err).NotTo(HaveOccurred()) 45 | Expect(abi).To(Equal(expectedABI)) 46 | _, err = a.ParseAbi(abi) 47 | Expect(err).ToNot(HaveOccurred()) 48 | }) 49 | }) 50 | }) 51 | -------------------------------------------------------------------------------- /pkg/fetcher/log_fetcher.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fetcher 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum" 21 | "github.com/ethereum/go-ethereum/common" 22 | "github.com/ethereum/go-ethereum/core/types" 23 | 24 | "github.com/vulcanize/eth-header-sync/pkg/core" 25 | ) 26 | 27 | // LogFetcher is the fetching interface for eth logs 28 | type LogFetcher interface { 29 | FetchLogs(contractAddresses []string, topics []common.Hash, missingHeader core.Header) ([]types.Log, error) 30 | } 31 | 32 | // FetchLogs checks all topic0s, on all addresses, fetching matching logs for the given header 33 | func (f *Fetcher) FetchLogs(contractAddresses []string, topic0s []common.Hash, header core.Header) ([]types.Log, error) { 34 | addresses := hexStringsToAddresses(contractAddresses) 35 | blockHash := common.HexToHash(header.Hash) 36 | query := ethereum.FilterQuery{ 37 | BlockHash: &blockHash, 38 | Addresses: addresses, 39 | // Search for _any_ of the topics in topic0 position; see docs on `FilterQuery` 40 | Topics: [][]common.Hash{topic0s}, 41 | } 42 | 43 | logs, err := f.FetchEthLogsWithCustomQuery(query) 44 | if err != nil { 45 | // TODO review aggregate fetching error handling 46 | return []types.Log{}, err 47 | } 48 | 49 | return logs, nil 50 | } 51 | 52 | func hexStringsToAddresses(hexStrings []string) []common.Address { 53 | var addresses []common.Address 54 | for _, hexString := range hexStrings { 55 | address := common.HexToAddress(hexString) 56 | addresses = append(addresses, address) 57 | } 58 | 59 | return addresses 60 | } 61 | -------------------------------------------------------------------------------- /pkg/filters/filter_query.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package filters 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "errors" 23 | 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/ethereum/go-ethereum/common/hexutil" 26 | "github.com/vulcanize/eth-contract-watcher/pkg/core" 27 | ) 28 | 29 | type LogFilters []LogFilter 30 | 31 | type LogFilter struct { 32 | Name string `json:"name"` 33 | FromBlock int64 `json:"fromBlock" db:"from_block"` 34 | ToBlock int64 `json:"toBlock" db:"to_block"` 35 | Address string `json:"address"` 36 | core.Topics `json:"topics"` 37 | } 38 | 39 | func (filterQuery *LogFilter) UnmarshalJSON(input []byte) error { 40 | type Alias LogFilter 41 | 42 | var err error 43 | aux := &struct { 44 | ToBlock string `json:"toBlock"` 45 | FromBlock string `json:"fromBlock"` 46 | *Alias 47 | }{ 48 | Alias: (*Alias)(filterQuery), 49 | } 50 | if err = json.Unmarshal(input, &aux); err != nil { 51 | return err 52 | } 53 | if filterQuery.Name == "" { 54 | return errors.New("filters: must provide name for logfilter") 55 | } 56 | filterQuery.ToBlock, err = filterQuery.unmarshalFromToBlock(aux.ToBlock) 57 | if err != nil { 58 | return errors.New("filters: invalid fromBlock") 59 | } 60 | filterQuery.FromBlock, err = filterQuery.unmarshalFromToBlock(aux.FromBlock) 61 | if err != nil { 62 | return errors.New("filters: invalid fromBlock") 63 | } 64 | if !common.IsHexAddress(filterQuery.Address) { 65 | return errors.New("filters: invalid address") 66 | } 67 | 68 | return nil 69 | } 70 | 71 | func (filterQuery *LogFilter) unmarshalFromToBlock(auxBlock string) (int64, error) { 72 | if auxBlock == "" { 73 | return -1, nil 74 | } 75 | block, err := hexutil.DecodeUint64(auxBlock) 76 | if err != nil { 77 | return 0, errors.New("filters: invalid block arg") 78 | } 79 | return int64(block), nil 80 | } 81 | -------------------------------------------------------------------------------- /pkg/fetcher/log_fetcher_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fetcher_test 18 | 19 | import ( 20 | "context" 21 | 22 | "github.com/ethereum/go-ethereum" 23 | "github.com/ethereum/go-ethereum/common" 24 | . "github.com/onsi/ginkgo" 25 | . "github.com/onsi/gomega" 26 | 27 | "github.com/vulcanize/eth-header-sync/pkg/core" 28 | "github.com/vulcanize/eth-header-sync/pkg/fakes" 29 | 30 | f "github.com/vulcanize/eth-contract-watcher/pkg/fetcher" 31 | ) 32 | 33 | var _ = Describe("Fetcher", func() { 34 | Describe("FetchLogs", func() { 35 | It("fetches logs based on the given query", func() { 36 | mockClient := fakes.NewMockEthClient() 37 | fetcher := f.NewFetcher(mockClient) 38 | header := fakes.FakeHeader 39 | 40 | addresses := []string{"0xfakeAddress", "0xanotherFakeAddress"} 41 | topicZeros := [][]common.Hash{{common.BytesToHash([]byte{1, 2, 3, 4, 5})}} 42 | 43 | _, err := fetcher.FetchLogs(addresses, []common.Hash{common.BytesToHash([]byte{1, 2, 3, 4, 5})}, header) 44 | 45 | address1 := common.HexToAddress("0xfakeAddress") 46 | address2 := common.HexToAddress("0xanotherFakeAddress") 47 | Expect(err).NotTo(HaveOccurred()) 48 | 49 | blockHash := common.HexToHash(header.Hash) 50 | expectedQuery := ethereum.FilterQuery{ 51 | BlockHash: &blockHash, 52 | Addresses: []common.Address{address1, address2}, 53 | Topics: topicZeros, 54 | } 55 | mockClient.AssertFilterLogsCalledWith(context.Background(), expectedQuery) 56 | }) 57 | 58 | It("returns an error if fetching the logs fails", func() { 59 | mockClient := fakes.NewMockEthClient() 60 | mockClient.SetFilterLogsErr(fakes.FakeError) 61 | fetcher := f.NewFetcher(mockClient) 62 | 63 | _, err := fetcher.FetchLogs([]string{}, []common.Hash{}, core.Header{}) 64 | 65 | Expect(err).To(HaveOccurred()) 66 | Expect(err).To(MatchError(fakes.FakeError)) 67 | }) 68 | }) 69 | }) 70 | -------------------------------------------------------------------------------- /pkg/fetcher/base_fetcher_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fetcher_test 18 | 19 | import ( 20 | "context" 21 | "math/big" 22 | 23 | "github.com/ethereum/go-ethereum" 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/ethereum/go-ethereum/core/types" 26 | . "github.com/onsi/ginkgo" 27 | . "github.com/onsi/gomega" 28 | 29 | "github.com/vulcanize/eth-header-sync/pkg/fakes" 30 | 31 | f "github.com/vulcanize/eth-contract-watcher/pkg/fetcher" 32 | ) 33 | 34 | var _ = Describe("Geth fetcher", func() { 35 | var ( 36 | mockClient *fakes.MockEthClient 37 | fetcher *f.Fetcher 38 | ) 39 | 40 | BeforeEach(func() { 41 | mockClient = fakes.NewMockEthClient() 42 | fetcher = f.NewFetcher(mockClient) 43 | }) 44 | 45 | Describe("fetching logs with a custom FilterQuery", func() { 46 | It("fetches logs from ethClient", func() { 47 | mockClient.SetFilterLogsReturnLogs([]types.Log{{}}) 48 | address := common.HexToAddress("0x") 49 | startingBlockNumber := big.NewInt(1) 50 | endingBlockNumber := big.NewInt(2) 51 | topic := common.HexToHash("0x") 52 | query := ethereum.FilterQuery{ 53 | FromBlock: startingBlockNumber, 54 | ToBlock: endingBlockNumber, 55 | Addresses: []common.Address{address}, 56 | Topics: [][]common.Hash{{topic}}, 57 | } 58 | 59 | _, err := fetcher.FetchEthLogsWithCustomQuery(query) 60 | 61 | Expect(err).NotTo(HaveOccurred()) 62 | mockClient.AssertFilterLogsCalledWith(context.Background(), query) 63 | }) 64 | 65 | It("returns err if ethClient returns err", func() { 66 | mockClient.SetFilterLogsErr(fakes.FakeError) 67 | startingBlockNumber := big.NewInt(1) 68 | endingBlockNumber := big.NewInt(2) 69 | query := ethereum.FilterQuery{ 70 | FromBlock: startingBlockNumber, 71 | ToBlock: endingBlockNumber, 72 | Addresses: []common.Address{common.HexToAddress(common.BytesToHash([]byte{1, 2, 3, 4, 5}).Hex())}, 73 | Topics: nil, 74 | } 75 | 76 | _, err := fetcher.FetchEthLogsWithCustomQuery(query) 77 | 78 | Expect(err).To(HaveOccurred()) 79 | Expect(err).To(MatchError(fakes.FakeError)) 80 | }) 81 | }) 82 | }) 83 | -------------------------------------------------------------------------------- /pkg/helpers/test_helpers/test_data.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package test_helpers 18 | 19 | import ( 20 | "strings" 21 | 22 | "github.com/vulcanize/eth-contract-watcher/pkg/config" 23 | "github.com/vulcanize/eth-contract-watcher/pkg/constants" 24 | ) 25 | 26 | var ens = strings.ToLower(constants.EnsContractAddress) 27 | var tusd = strings.ToLower(constants.TusdContractAddress) 28 | 29 | var TusdConfig = config.ContractConfig{ 30 | Network: "", 31 | Addresses: map[string]bool{ 32 | tusd: true, 33 | }, 34 | Abis: map[string]string{ 35 | tusd: "", 36 | }, 37 | Events: map[string][]string{ 38 | tusd: {"Transfer"}, 39 | }, 40 | Methods: map[string][]string{ 41 | tusd: nil, 42 | }, 43 | MethodArgs: map[string][]string{ 44 | tusd: nil, 45 | }, 46 | EventArgs: map[string][]string{ 47 | tusd: nil, 48 | }, 49 | StartingBlocks: map[string]int64{ 50 | tusd: 5197514, 51 | }, 52 | } 53 | 54 | var ENSConfig = config.ContractConfig{ 55 | Network: "", 56 | Addresses: map[string]bool{ 57 | ens: true, 58 | }, 59 | Abis: map[string]string{ 60 | ens: "", 61 | }, 62 | Events: map[string][]string{ 63 | ens: {"NewOwner"}, 64 | }, 65 | Methods: map[string][]string{ 66 | ens: nil, 67 | }, 68 | MethodArgs: map[string][]string{ 69 | ens: nil, 70 | }, 71 | EventArgs: map[string][]string{ 72 | ens: nil, 73 | }, 74 | StartingBlocks: map[string]int64{ 75 | ens: 3327417, 76 | }, 77 | } 78 | 79 | var ENSandTusdConfig = config.ContractConfig{ 80 | Network: "", 81 | Addresses: map[string]bool{ 82 | ens: true, 83 | tusd: true, 84 | }, 85 | Abis: map[string]string{ 86 | ens: "", 87 | tusd: "", 88 | }, 89 | Events: map[string][]string{ 90 | ens: {"NewOwner"}, 91 | tusd: {"Transfer"}, 92 | }, 93 | Methods: map[string][]string{ 94 | ens: nil, 95 | tusd: nil, 96 | }, 97 | MethodArgs: map[string][]string{ 98 | ens: nil, 99 | tusd: nil, 100 | }, 101 | EventArgs: map[string][]string{ 102 | ens: nil, 103 | tusd: nil, 104 | }, 105 | StartingBlocks: map[string]int64{ 106 | ens: 3327417, 107 | tusd: 5197514, 108 | }, 109 | } 110 | -------------------------------------------------------------------------------- /pkg/fetcher/base_fetcher.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fetcher 18 | 19 | import ( 20 | "math/big" 21 | "time" 22 | 23 | "github.com/ethereum/go-ethereum" 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/ethereum/go-ethereum/core/types" 26 | "github.com/sirupsen/logrus" 27 | "golang.org/x/net/context" 28 | 29 | "github.com/vulcanize/eth-header-sync/pkg/core" 30 | 31 | "github.com/vulcanize/eth-contract-watcher/pkg/abi" 32 | ) 33 | 34 | type Fetcher struct { 35 | ethClient core.EthClient 36 | timeout time.Duration 37 | } 38 | 39 | func NewFetcher(ethClient core.EthClient, timeout time.Duration) *Fetcher { 40 | return &Fetcher{ 41 | ethClient: ethClient, 42 | timeout: timeout, 43 | } 44 | } 45 | 46 | func (f *Fetcher) FetchEthLogsWithCustomQuery(query ethereum.FilterQuery) ([]types.Log, error) { 47 | ctx, cancel := context.WithTimeout(context.Background(), f.timeout) 48 | defer cancel() 49 | gethLogs, err := f.ethClient.FilterLogs(ctx, query) 50 | logrus.Debug("GetEthLogsWithCustomQuery called") 51 | if err != nil { 52 | return []types.Log{}, err 53 | } 54 | return gethLogs, nil 55 | } 56 | 57 | func (f *Fetcher) FetchContractData(abiJSON string, address string, method string, methodArgs []interface{}, result interface{}, blockNumber int64) error { 58 | parsed, err := abi.ParseAbi(abiJSON) 59 | if err != nil { 60 | return err 61 | } 62 | var input []byte 63 | if methodArgs != nil { 64 | input, err = parsed.Pack(method, methodArgs...) 65 | } else { 66 | input, err = parsed.Pack(method) 67 | } 68 | if err != nil { 69 | return err 70 | } 71 | var bn *big.Int 72 | if blockNumber > 0 { 73 | bn = big.NewInt(blockNumber) 74 | } 75 | output, err := f.callContract(address, input, bn) 76 | if err != nil { 77 | return err 78 | } 79 | return parsed.Unpack(result, method, output) 80 | } 81 | 82 | func (f *Fetcher) callContract(contractHash string, input []byte, blockNumber *big.Int) ([]byte, error) { 83 | to := common.HexToAddress(contractHash) 84 | msg := ethereum.CallMsg{To: &to, Data: input} 85 | ctx, cancel := context.WithTimeout(context.Background(), f.timeout) 86 | defer cancel() 87 | return f.ethClient.CallContract(ctx, msg, blockNumber) 88 | } 89 | -------------------------------------------------------------------------------- /pkg/abi/abi.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package abi 18 | 19 | import ( 20 | "encoding/json" 21 | "errors" 22 | "fmt" 23 | "io/ioutil" 24 | "net/http" 25 | "strings" 26 | "time" 27 | 28 | "github.com/ethereum/go-ethereum/accounts/abi" 29 | ) 30 | 31 | var ( 32 | ErrInvalidAbiFile = errors.New("invalid abi") 33 | ErrMissingAbiFile = errors.New("missing abi") 34 | ErrAPIRequestFailed = errors.New("etherscan api request failed") 35 | ) 36 | 37 | type Response struct { 38 | Status string 39 | Message string 40 | Result string 41 | } 42 | 43 | type EtherScanAPI struct { 44 | client *http.Client 45 | url string 46 | } 47 | 48 | func NewEtherScanClient(url string) *EtherScanAPI { 49 | return &EtherScanAPI{ 50 | client: &http.Client{Timeout: 10 * time.Second}, 51 | url: url, 52 | } 53 | } 54 | 55 | func GenURL(network string) string { 56 | switch network { 57 | case "ropsten": 58 | return "https://ropsten.etherscan.io" 59 | case "kovan": 60 | return "https://kovan.etherscan.io" 61 | case "rinkeby": 62 | return "https://rinkeby.etherscan.io" 63 | default: 64 | return "https://api.etherscan.io" 65 | } 66 | } 67 | 68 | //https://api.etherscan.io/api?module=contract&action=getabi&address=%s 69 | func (e *EtherScanAPI) GetAbi(contractHash string) (string, error) { 70 | target := new(Response) 71 | request := fmt.Sprintf("%s/api?module=contract&action=getabi&address=%s", e.url, contractHash) 72 | r, err := e.client.Get(request) 73 | if err != nil { 74 | return "", ErrAPIRequestFailed 75 | } 76 | defer r.Body.Close() 77 | err = json.NewDecoder(r.Body).Decode(&target) 78 | return target.Result, err 79 | } 80 | 81 | func ParseAbiFile(abiFilePath string) (abi.ABI, error) { 82 | abiString, err := ReadAbiFile(abiFilePath) 83 | if err != nil { 84 | return abi.ABI{}, ErrMissingAbiFile 85 | } 86 | return ParseAbi(abiString) 87 | } 88 | 89 | func ParseAbi(abiString string) (abi.ABI, error) { 90 | parsedAbi, err := abi.JSON(strings.NewReader(abiString)) 91 | if err != nil { 92 | return abi.ABI{}, ErrInvalidAbiFile 93 | } 94 | return parsedAbi, nil 95 | } 96 | 97 | func ReadAbiFile(abiFilePath string) (string, error) { 98 | filesBytes, err := ioutil.ReadFile(abiFilePath) 99 | if err != nil { 100 | return "", ErrMissingAbiFile 101 | } 102 | return string(filesBytes), nil 103 | } 104 | -------------------------------------------------------------------------------- /pkg/retriever/block_retriever_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package retriever_test 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | 23 | "github.com/vulcanize/eth-header-sync/pkg/postgres" 24 | "github.com/vulcanize/eth-header-sync/pkg/repository" 25 | 26 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers" 27 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers/mocks" 28 | "github.com/vulcanize/eth-contract-watcher/pkg/retriever" 29 | ) 30 | 31 | var _ = Describe("Block Retriever", func() { 32 | var db *postgres.DB 33 | var r retriever.BlockRetriever 34 | var headerRepository repository.HeaderRepository 35 | 36 | BeforeEach(func() { 37 | db, _ = test_helpers.SetupDBandClient() 38 | headerRepository = repository.NewHeaderRepository(db) 39 | r = retriever.NewBlockRetriever(db) 40 | }) 41 | 42 | AfterEach(func() { 43 | test_helpers.TearDown(db) 44 | }) 45 | 46 | Describe("RetrieveFirstBlock", func() { 47 | It("Retrieves block number of earliest header in the database", func() { 48 | _, err := headerRepository.CreateOrUpdateHeader(mocks.MockHeader1) 49 | Expect(err).ToNot(HaveOccurred()) 50 | _, err = headerRepository.CreateOrUpdateHeader(mocks.MockHeader2) 51 | Expect(err).ToNot(HaveOccurred()) 52 | _, err = headerRepository.CreateOrUpdateHeader(mocks.MockHeader3) 53 | Expect(err).ToNot(HaveOccurred()) 54 | 55 | i, err := r.RetrieveFirstBlock() 56 | Expect(err).NotTo(HaveOccurred()) 57 | Expect(i).To(Equal(int64(6194632))) 58 | }) 59 | 60 | It("Fails if no headers can be found in the database", func() { 61 | _, err := r.RetrieveFirstBlock() 62 | Expect(err).To(HaveOccurred()) 63 | }) 64 | }) 65 | 66 | Describe("RetrieveMostRecentBlock", func() { 67 | It("Retrieves the latest header's block number", func() { 68 | _, err := headerRepository.CreateOrUpdateHeader(mocks.MockHeader1) 69 | Expect(err).ToNot(HaveOccurred()) 70 | _, err = headerRepository.CreateOrUpdateHeader(mocks.MockHeader2) 71 | Expect(err).ToNot(HaveOccurred()) 72 | _, err = headerRepository.CreateOrUpdateHeader(mocks.MockHeader3) 73 | Expect(err).ToNot(HaveOccurred()) 74 | 75 | i, err := r.RetrieveMostRecentBlock() 76 | Expect(err).ToNot(HaveOccurred()) 77 | Expect(i).To(Equal(int64(6194634))) 78 | }) 79 | 80 | It("Fails if no headers can be found in the database", func() { 81 | i, err := r.RetrieveMostRecentBlock() 82 | Expect(err).To(HaveOccurred()) 83 | Expect(i).To(Equal(int64(0))) 84 | }) 85 | }) 86 | }) 87 | -------------------------------------------------------------------------------- /pkg/testing/sample_abi.json: -------------------------------------------------------------------------------- 1 | [{"constant":true,"inputs":[],"name":"mintingFinished","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"}],"name":"approve","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transferFrom","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"unpause","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_amount","type":"uint256"}],"name":"mint","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"paused","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"finishMinting","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"pause","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_value","type":"uint256"}],"name":"transfer","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_releaseTime","type":"uint256"}],"name":"mintTimelocked","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"newOwner","type":"address"}],"name":"transferOwnership","outputs":[],"payable":false,"type":"function"},{"anonymous":false,"inputs":[{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Mint","type":"event"},{"anonymous":false,"inputs":[],"name":"MintFinished","type":"event"},{"anonymous":false,"inputs":[],"name":"Pause","type":"event"},{"anonymous":false,"inputs":[],"name":"Unpause","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"owner","type":"address"},{"indexed":true,"name":"spender","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Approval","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"from","type":"address"},{"indexed":true,"name":"to","type":"address"},{"indexed":false,"name":"value","type":"uint256"}],"name":"Transfer","type":"event"}] -------------------------------------------------------------------------------- /pkg/types/event.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/ethereum/go-ethereum/accounts/abi" 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/ethereum/go-ethereum/crypto" 26 | ) 27 | 28 | // Event is our custom event type 29 | type Event struct { 30 | Name string 31 | Anonymous bool 32 | Fields []Field 33 | } 34 | 35 | // Field is our custom event field type which associates a postgres type with the field 36 | type Field struct { 37 | abi.Argument // Name, Type, Indexed 38 | PgType string // Holds type used when committing data held in this field to postgres 39 | } 40 | 41 | // Log is used to hold instance of an event log data 42 | type Log struct { 43 | ID int64 // VulcanizeIdLog for full sync and header ID for header sync contract watcher 44 | Values map[string]string // Map of event input names to their values 45 | 46 | // Used for full sync only 47 | Block int64 48 | Tx string 49 | 50 | // Used for headerSync only 51 | LogIndex uint 52 | TransactionIndex uint 53 | Raw []byte // json.Unmarshalled byte array of geth/core/types.Log{} 54 | } 55 | 56 | // NewEvent unpacks abi.Event into our custom Event struct 57 | func NewEvent(e abi.Event) Event { 58 | fields := make([]Field, len(e.Inputs)) 59 | for i, input := range e.Inputs { 60 | fields[i] = Field{} 61 | fields[i].Name = input.Name 62 | fields[i].Type = input.Type 63 | fields[i].Indexed = input.Indexed 64 | // Fill in pg type based on abi type 65 | switch fields[i].Type.T { 66 | case abi.HashTy, abi.AddressTy: 67 | fields[i].PgType = "CHARACTER VARYING(66)" 68 | case abi.IntTy, abi.UintTy: 69 | fields[i].PgType = "NUMERIC" 70 | case abi.BoolTy: 71 | fields[i].PgType = "BOOLEAN" 72 | case abi.BytesTy, abi.FixedBytesTy: 73 | fields[i].PgType = "BYTEA" 74 | case abi.ArrayTy: 75 | fields[i].PgType = "TEXT[]" 76 | case abi.FixedPointTy: 77 | fields[i].PgType = "MONEY" // use shopspring/decimal for fixed point numbers in go and money type in postgres? 78 | default: 79 | fields[i].PgType = "TEXT" 80 | } 81 | } 82 | 83 | return Event{ 84 | Name: e.Name, 85 | Anonymous: e.Anonymous, 86 | Fields: fields, 87 | } 88 | } 89 | 90 | // Sig returns the hash signature for an event 91 | func (e Event) Sig() common.Hash { 92 | types := make([]string, len(e.Fields)) 93 | 94 | for i, input := range e.Fields { 95 | types[i] = input.Type.String() 96 | } 97 | 98 | return crypto.Keccak256Hash([]byte(fmt.Sprintf("%v(%v)", e.Name, strings.Join(types, ",")))) 99 | } 100 | -------------------------------------------------------------------------------- /cmd/watch.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | "time" 22 | 23 | log "github.com/sirupsen/logrus" 24 | "github.com/spf13/cobra" 25 | 26 | "github.com/vulcanize/eth-header-sync/pkg/postgres" 27 | 28 | "github.com/vulcanize/eth-contract-watcher/pkg/config" 29 | st "github.com/vulcanize/eth-contract-watcher/pkg/transformer" 30 | ) 31 | 32 | // watchCmd represents the watch command 33 | var watchCmd = &cobra.Command{ 34 | Use: "watch", 35 | Short: "Watches events at the provided contract address using fully synced vDB", 36 | Long: `Uses input contract address and event filters to watch events 37 | 38 | Expects an ethereum node to be running 39 | Expects an archival node synced into vulcanizeDB 40 | Requires a .toml config file: 41 | 42 | [database] 43 | name = "vulcanize_public" 44 | hostname = "localhost" 45 | port = 5432 46 | 47 | [client] 48 | rpcPath = "/Users/user/Library/Ethereum/geth.ipc" 49 | 50 | [contract] 51 | network = "" 52 | addresses = [ 53 | "contractAddress1", 54 | "contractAddress2" 55 | ] 56 | [contract.contractAddress1] 57 | abi = 'ABI for contract 1' 58 | startingBlock = 982463 59 | [contract.contractAddress2] 60 | abi = 'ABI for contract 2' 61 | events = [ 62 | "event1", 63 | "event2" 64 | ] 65 | eventArgs = [ 66 | "arg1", 67 | "arg2" 68 | ] 69 | methods = [ 70 | "method1", 71 | "method2" 72 | ] 73 | methodArgs = [ 74 | "arg1", 75 | "arg2" 76 | ] 77 | startingBlock = 4448566 78 | piping = true 79 | `, 80 | Run: func(cmd *cobra.Command, args []string) { 81 | subCommand = cmd.CalledAs() 82 | logWithCommand = *log.WithField("SubCommand", subCommand) 83 | watch() 84 | }, 85 | } 86 | 87 | func watch() { 88 | ticker := time.NewTicker(5 * time.Second) 89 | defer ticker.Stop() 90 | 91 | client, node := getClientAndNode() 92 | 93 | db, err := postgres.NewDB(databaseConfig, node) 94 | if err != nil { 95 | logWithCommand.Fatal(err) 96 | } 97 | 98 | con := config.ContractConfig{} 99 | con.PrepConfig() 100 | transformer := st.NewTransformer(con, client, db, timeout) 101 | 102 | if err := transformer.Init(); err != nil { 103 | logWithCommand.Fatal(fmt.Sprintf("Failed to initialize transformer, err: %v ", err)) 104 | } 105 | 106 | for range ticker.C { 107 | err = transformer.Execute() 108 | if err != nil { 109 | logWithCommand.Error("Execution error for transformer: ", transformer.GetConfig().Name, err) 110 | } 111 | } 112 | } 113 | 114 | func init() { 115 | rootCmd.AddCommand(watchCmd) 116 | } 117 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.2' 2 | 3 | services: 4 | dapptools: 5 | restart: unless-stopped 6 | image: vulcanize/dapptools:v0.29.0-statediff-0.0.2 7 | ports: 8 | - "127.0.0.1:8545:8545" 9 | - "127.0.0.1:8546:8546" 10 | 11 | eth-indexer: 12 | restart: unless-stopped 13 | depends_on: 14 | - indexer-db 15 | - dapptools 16 | image: vulcanize/ipld-eth-indexer:v0.3.0-alpha 17 | environment: 18 | DATABASE_NAME: vulcanize_public 19 | DATABASE_HOSTNAME: indexer-db 20 | DATABASE_PORT: 5432 21 | DATABASE_USER: vdbm 22 | DATABASE_PASSWORD: password 23 | ETH_WS_PATH: "dapptools:8546" 24 | ETH_HTTP_PATH: "dapptools:8545" 25 | ETH_CHAIN_ID: 4 26 | ETH_NETWORK_ID: 4 27 | VDB_COMMAND: sync 28 | 29 | eth-server: 30 | depends_on: 31 | - indexer-db 32 | - eth-indexer 33 | image: vulcanize/ipld-eth-server:v0.0.13 34 | environment: 35 | VDB_COMMAND: serve 36 | DATABASE_NAME: vulcanize_public 37 | DATABASE_HOSTNAME: indexer-db 38 | DATABASE_PORT: 5432 39 | DATABASE_USER: vdbm 40 | DATABASE_PASSWORD: password 41 | SERVER_WS_PATH: "0.0.0.0:8080" 42 | SERVER_HTTP_PATH: "0.0.0.0:8081" 43 | LOGRUS_LEVEL: debug 44 | ports: 45 | - "127.0.0.1:8080:8080" 46 | - "127.0.0.1:8081:8081" 47 | 48 | indexer-db: 49 | restart: unless-stopped 50 | image: postgres:10.12-alpine 51 | environment: 52 | - POSTGRES_USER=vdbm 53 | - POSTGRES_DB=vulcanize_public 54 | - POSTGRES_PASSWORD=password 55 | volumes: 56 | - indexer_db_data:/var/lib/postgresql/data 57 | ports: 58 | - "127.0.0.1:8069:5432" 59 | 60 | contact-watcher-db: 61 | restart: unless-stopped 62 | image: postgres:10.12-alpine 63 | environment: 64 | - POSTGRES_USER=vdbm 65 | - POSTGRES_DB=vulcanize_public 66 | - POSTGRES_PASSWORD=password 67 | volumes: 68 | - contact_watcher_db_data:/var/lib/postgresql/data 69 | ports: 70 | - "127.0.0.1:8068:5432" 71 | 72 | eth-header-sync: 73 | restart: unless-stopped 74 | depends_on: 75 | - contact-watcher-db 76 | image: vulcanize/eth-header-sync:v0.1.1 77 | volumes: 78 | - ./environments/header_sync.toml:/app/config.toml 79 | environment: 80 | - STARTING_BLOCK_NUMBER=1 81 | - VDB_COMMAND=sync 82 | - DATABASE_NAME=vulcanize_public 83 | - DATABASE_HOSTNAME=contact-watcher-db 84 | - DATABASE_PORT=5432 85 | - DATABASE_USER=vdbm 86 | - DATABASE_PASSWORD=password 87 | 88 | eth-contract-watcher: 89 | depends_on: 90 | - contact-watcher-db 91 | build: 92 | context: "" 93 | cache_from: 94 | - alpine:latest 95 | - golang:1.13 96 | dockerfile: Dockerfile 97 | volumes: 98 | - ./environments/example.toml:/app/config.toml 99 | environment: 100 | - VDB_COMMAND=watch 101 | - DATABASE_NAME=vulcanize_public 102 | - DATABASE_HOSTNAME=contact-watcher-db 103 | - DATABASE_PORT=5432 104 | - DATABASE_USER=vdbm 105 | - DATABASE_PASSWORD=password 106 | 107 | contract-watcher-graphql: 108 | restart: unless-stopped 109 | depends_on: 110 | - contact-watcher-db 111 | image: vulcanize/postgraphile:v1.0.1 112 | environment: 113 | - PG_HOST=contact-watcher-db 114 | - PG_PORT=5432 115 | - PG_DATABASE=vulcanize_public 116 | - PG_USER=vdbm 117 | - PG_PASSWORD=password 118 | - SCHEMA=public,header_0xd850942ef8811f2a866692a623011bde52a462c1 119 | ports: 120 | - "127.0.0.1:5000:5000" 121 | 122 | volumes: 123 | contact_watcher_db_data: 124 | indexer_db_data: -------------------------------------------------------------------------------- /pkg/types/method.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package types 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/ethereum/go-ethereum/accounts/abi" 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/ethereum/go-ethereum/crypto" 26 | ) 27 | 28 | // Method is our custom method struct 29 | type Method struct { 30 | Name string 31 | Const bool 32 | Args []Field 33 | Return []Field 34 | } 35 | 36 | // Result is used to hold instance of result from method call with given inputs and block 37 | type Result struct { 38 | Method 39 | Inputs []interface{} // Will only use addresses 40 | Output interface{} 41 | PgType string // Holds output pg type 42 | Block int64 43 | } 44 | 45 | // NewMethod unpacks abi.Method into our custom Method struct 46 | func NewMethod(m abi.Method) Method { 47 | inputs := make([]Field, len(m.Inputs)) 48 | for i, input := range m.Inputs { 49 | inputs[i] = Field{} 50 | inputs[i].Name = input.Name 51 | inputs[i].Type = input.Type 52 | inputs[i].Indexed = input.Indexed 53 | switch inputs[i].Type.T { 54 | case abi.HashTy, abi.AddressTy: 55 | inputs[i].PgType = "CHARACTER VARYING(66)" 56 | case abi.IntTy, abi.UintTy: 57 | inputs[i].PgType = "NUMERIC" 58 | case abi.BoolTy: 59 | inputs[i].PgType = "BOOLEAN" 60 | case abi.BytesTy, abi.FixedBytesTy: 61 | inputs[i].PgType = "BYTEA" 62 | case abi.ArrayTy: 63 | inputs[i].PgType = "TEXT[]" 64 | case abi.FixedPointTy: 65 | inputs[i].PgType = "MONEY" // use shopspring/decimal for fixed point numbers in go and money type in postgres? 66 | default: 67 | inputs[i].PgType = "TEXT" 68 | } 69 | } 70 | 71 | outputs := make([]Field, len(m.Outputs)) 72 | for i, output := range m.Outputs { 73 | outputs[i] = Field{} 74 | outputs[i].Name = output.Name 75 | outputs[i].Type = output.Type 76 | outputs[i].Indexed = output.Indexed 77 | switch outputs[i].Type.T { 78 | case abi.HashTy, abi.AddressTy: 79 | outputs[i].PgType = "CHARACTER VARYING(66)" 80 | case abi.IntTy, abi.UintTy: 81 | outputs[i].PgType = "NUMERIC" 82 | case abi.BoolTy: 83 | outputs[i].PgType = "BOOLEAN" 84 | case abi.BytesTy, abi.FixedBytesTy: 85 | outputs[i].PgType = "BYTEA" 86 | case abi.ArrayTy: 87 | outputs[i].PgType = "TEXT[]" 88 | case abi.FixedPointTy: 89 | outputs[i].PgType = "MONEY" // use shopspring/decimal for fixed point numbers in go and money type in postgres? 90 | default: 91 | outputs[i].PgType = "TEXT" 92 | } 93 | } 94 | 95 | return Method{ 96 | Name: m.Name, 97 | Const: m.Const, 98 | Args: inputs, 99 | Return: outputs, 100 | } 101 | } 102 | 103 | // Sig returns the hash signature for the method 104 | func (m Method) Sig() common.Hash { 105 | types := make([]string, len(m.Args)) 106 | i := 0 107 | for _, arg := range m.Args { 108 | types[i] = arg.Type.String() 109 | i++ 110 | } 111 | 112 | return crypto.Keccak256Hash([]byte(fmt.Sprintf("%v(%v)", m.Name, strings.Join(types, ",")))) 113 | } 114 | -------------------------------------------------------------------------------- /pkg/fakes/mock_fetcher.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fakes 18 | 19 | import ( 20 | "math/big" 21 | 22 | "github.com/ethereum/go-ethereum" 23 | "github.com/ethereum/go-ethereum/core/types" 24 | . "github.com/onsi/gomega" 25 | 26 | "github.com/vulcanize/eth-header-sync/pkg/core" 27 | ) 28 | 29 | type MockFetcher struct { 30 | fetchContractDataErr error 31 | fetchContractDataPassedAbi string 32 | fetchContractDataPassedAddress string 33 | fetchContractDataPassedMethod string 34 | fetchContractDataPassedMethodArgs []interface{} 35 | fetchContractDataPassedResult interface{} 36 | fetchContractDataPassedBlockNumber int64 37 | logQuery ethereum.FilterQuery 38 | logQueryErr error 39 | logQueryReturnLogs []types.Log 40 | node core.Node 41 | } 42 | 43 | func NewMockFetcher() *MockFetcher { 44 | return &MockFetcher{} 45 | } 46 | 47 | func (fethcer *MockFetcher) SetFetchContractDataErr(err error) { 48 | fethcer.fetchContractDataErr = err 49 | } 50 | 51 | func (fethcer *MockFetcher) SetGetEthLogsWithCustomQueryErr(err error) { 52 | fethcer.logQueryErr = err 53 | } 54 | 55 | func (fethcer *MockFetcher) SetGetEthLogsWithCustomQueryReturnLogs(logs []types.Log) { 56 | fethcer.logQueryReturnLogs = logs 57 | } 58 | 59 | func (fethcer *MockFetcher) FetchContractData(abiJSON string, address string, method string, methodArgs []interface{}, result interface{}, blockNumber int64) error { 60 | fethcer.fetchContractDataPassedAbi = abiJSON 61 | fethcer.fetchContractDataPassedAddress = address 62 | fethcer.fetchContractDataPassedMethod = method 63 | fethcer.fetchContractDataPassedMethodArgs = methodArgs 64 | fethcer.fetchContractDataPassedResult = result 65 | fethcer.fetchContractDataPassedBlockNumber = blockNumber 66 | return fethcer.fetchContractDataErr 67 | } 68 | 69 | func (fethcer *MockFetcher) GetEthLogsWithCustomQuery(query ethereum.FilterQuery) ([]types.Log, error) { 70 | fethcer.logQuery = query 71 | return fethcer.logQueryReturnLogs, fethcer.logQueryErr 72 | } 73 | 74 | func (fethcer *MockFetcher) CallContract(contractHash string, input []byte, blockNumber *big.Int) ([]byte, error) { 75 | return []byte{}, nil 76 | } 77 | 78 | func (fethcer *MockFetcher) AssertFetchContractDataCalledWith(abiJSON string, address string, method string, methodArgs []interface{}, result interface{}, blockNumber int64) { 79 | Expect(fethcer.fetchContractDataPassedAbi).To(Equal(abiJSON)) 80 | Expect(fethcer.fetchContractDataPassedAddress).To(Equal(address)) 81 | Expect(fethcer.fetchContractDataPassedMethod).To(Equal(method)) 82 | if methodArgs != nil { 83 | Expect(fethcer.fetchContractDataPassedMethodArgs).To(Equal(methodArgs)) 84 | } 85 | Expect(fethcer.fetchContractDataPassedResult).To(BeAssignableToTypeOf(result)) 86 | Expect(fethcer.fetchContractDataPassedBlockNumber).To(Equal(blockNumber)) 87 | } 88 | 89 | func (fethcer *MockFetcher) AssertGetEthLogsWithCustomQueryCalledWith(query ethereum.FilterQuery) { 90 | Expect(fethcer.logQuery).To(Equal(query)) 91 | } 92 | 93 | func (fetcher *MockFetcher) Node() core.Node { 94 | return fetcher.node 95 | } 96 | -------------------------------------------------------------------------------- /pkg/fetcher/interface_fetcher.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fetcher 18 | 19 | import ( 20 | "fmt" 21 | 22 | "github.com/vulcanize/eth-contract-watcher/pkg/constants" 23 | ) 24 | 25 | // InterfaceFetcher is used to derive the interface of a contract 26 | type InterfaceFetcher interface { 27 | FetchABI(resolverAddr string, blockNumber int64) (string, error) 28 | } 29 | 30 | // FetchABI is used to construct a custom ABI based on the results from calling supportsInterface 31 | func (f *Fetcher) FetchABI(resolverAddr string, blockNumber int64) (string, error) { 32 | a := constants.SupportsInterfaceABI 33 | args := make([]interface{}, 1) 34 | args[0] = constants.MetaSig.Bytes() 35 | supports, err := f.getSupportsInterface(a, resolverAddr, blockNumber, args) 36 | if err != nil { 37 | return "", fmt.Errorf("call to getSupportsInterface failed: %v", err) 38 | } 39 | if !supports { 40 | return "", fmt.Errorf("contract does not support interface") 41 | } 42 | 43 | abiStr := `[` 44 | args[0] = constants.AddrChangeSig.Bytes() 45 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 46 | if err == nil && supports { 47 | abiStr += constants.AddrChangeInterface + "," 48 | } 49 | args[0] = constants.NameChangeSig.Bytes() 50 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 51 | if err == nil && supports { 52 | abiStr += constants.NameChangeInterface + "," 53 | } 54 | args[0] = constants.ContentChangeSig.Bytes() 55 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 56 | if err == nil && supports { 57 | abiStr += constants.ContentChangeInterface + "," 58 | } 59 | args[0] = constants.AbiChangeSig.Bytes() 60 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 61 | if err == nil && supports { 62 | abiStr += constants.AbiChangeInterface + "," 63 | } 64 | args[0] = constants.PubkeyChangeSig.Bytes() 65 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 66 | if err == nil && supports { 67 | abiStr += constants.PubkeyChangeInterface + "," 68 | } 69 | args[0] = constants.ContentHashChangeSig.Bytes() 70 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 71 | if err == nil && supports { 72 | abiStr += constants.ContenthashChangeInterface + "," 73 | } 74 | args[0] = constants.MultihashChangeSig.Bytes() 75 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 76 | if err == nil && supports { 77 | abiStr += constants.MultihashChangeInterface + "," 78 | } 79 | args[0] = constants.TextChangeSig.Bytes() 80 | supports, err = f.getSupportsInterface(a, resolverAddr, blockNumber, args) 81 | if err == nil && supports { 82 | abiStr += constants.TextChangeInterface + "," 83 | } 84 | abiStr = abiStr[:len(abiStr)-1] + `]` 85 | 86 | return abiStr, nil 87 | } 88 | 89 | // Use this method to check whether or not a contract supports a given method/event interface 90 | func (f *Fetcher) getSupportsInterface(contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (bool, error) { 91 | return f.FetchBool("supportsInterface", contractAbi, contractAddress, blockNumber, methodArgs) 92 | } 93 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN = $(GOPATH)/bin 2 | BASE = $(GOPATH)/src/$(PACKAGE) 3 | PKGS = go list ./... | grep -v "^vendor/" 4 | 5 | # Tools 6 | ## Testing library 7 | GINKGO = $(BIN)/ginkgo 8 | $(BIN)/ginkgo: 9 | go get -u github.com/onsi/ginkgo/ginkgo 10 | 11 | ## Migration tool 12 | GOOSE = $(BIN)/goose 13 | $(BIN)/goose: 14 | go get -u -d github.com/pressly/goose/cmd/goose 15 | go build -tags='no_mysql no_sqlite' -o $(BIN)/goose github.com/pressly/goose/cmd/goose 16 | 17 | ## Source linter 18 | LINT = $(BIN)/golint 19 | $(BIN)/golint: 20 | go get -u golang.org/x/lint/golint 21 | 22 | ## Combination linter 23 | METALINT = $(BIN)/gometalinter.v2 24 | $(BIN)/gometalinter.v2: 25 | go get -u gopkg.in/alecthomas/gometalinter.v2 26 | $(METALINT) --install 27 | 28 | 29 | .PHONY: installtools 30 | installtools: | $(LINT) $(GOOSE) $(GINKGO) 31 | echo "Installing tools" 32 | 33 | .PHONY: metalint 34 | metalint: | $(METALINT) 35 | $(METALINT) ./... --vendor \ 36 | --fast \ 37 | --exclude="exported (function)|(var)|(method)|(type).*should have comment or be unexported" \ 38 | --format="{{.Path.Abs}}:{{.Line}}:{{if .Col}}{{.Col}}{{end}}:{{.Severity}}: {{.Message}} ({{.Linter}})" 39 | 40 | .PHONY: lint 41 | lint: 42 | $(LINT) $$($(PKGS)) | grep -v -E "exported (function)|(var)|(method)|(type).*should have comment or be unexported" 43 | 44 | #Database 45 | HOST_NAME = localhost 46 | PORT = 5432 47 | NAME = 48 | USER = postgres 49 | CONNECT_STRING=postgresql://$(USER)@$(HOST_NAME):$(PORT)/$(NAME)?sslmode=disable 50 | 51 | #Test 52 | TEST_DB = vulcanize_testing 53 | TEST_CONNECT_STRING = postgresql://$(USER)@$(HOST_NAME):$(PORT)/$(TEST_DB)?sslmode=disable 54 | 55 | .PHONY: test 56 | test: | $(GINKGO) $(LINT) 57 | go vet ./... 58 | go fmt ./... 59 | dropdb --if-exists $(TEST_DB) 60 | createdb $(TEST_DB) 61 | $(GOOSE) -dir db/migrations postgres "$(TEST_CONNECT_STRING)" up 62 | $(GOOSE) -dir db/migrations postgres "$(TEST_CONNECT_STRING)" reset 63 | make migrate NAME=$(TEST_DB) 64 | $(GINKGO) -r --skipPackage=integration_tests,integration 65 | 66 | .PHONY: integrationtest 67 | integrationtest: | $(GINKGO) $(LINT) 68 | go vet ./... 69 | go fmt ./... 70 | dropdb --if-exists $(TEST_DB) 71 | createdb $(TEST_DB) 72 | $(GOOSE) -dir db/migrations "$(TEST_CONNECT_STRING)" up 73 | $(GOOSE) -dir db/migrations "$(TEST_CONNECT_STRING)" reset 74 | make migrate NAME=$(TEST_DB) 75 | $(GINKGO) -r integration_test/ 76 | 77 | build: 78 | go fmt ./... 79 | go build 80 | 81 | # Parameter checks 82 | ## Check that DB variables are provided 83 | .PHONY: checkdbvars 84 | checkdbvars: 85 | test -n "$(HOST_NAME)" # $$HOST_NAME 86 | test -n "$(PORT)" # $$PORT 87 | test -n "$(NAME)" # $$NAME 88 | @echo $(CONNECT_STRING) 89 | 90 | ## Check that the migration variable (id/timestamp) is provided 91 | .PHONY: checkmigration 92 | checkmigration: 93 | test -n "$(MIGRATION)" # $$MIGRATION 94 | 95 | # Check that the migration name is provided 96 | .PHONY: checkmigname 97 | checkmigname: 98 | test -n "$(NAME)" # $$NAME 99 | 100 | # Migration operations 101 | ## Rollback the last migration 102 | .PHONY: rollback 103 | rollback: $(GOOSE) checkdbvars 104 | $(GOOSE) -dir db/migrations postgres "$(CONNECT_STRING)" down 105 | pg_dump -O -s $(CONNECT_STRING) > db/schema.sql 106 | 107 | 108 | ## Rollbackt to a select migration (id/timestamp) 109 | .PHONY: rollback_to 110 | rollback_to: $(GOOSE) checkmigration checkdbvars 111 | $(GOOSE) -dir db/migrations postgres "$(CONNECT_STRING)" down-to "$(MIGRATION)" 112 | 113 | ## Apply all migrations not already run 114 | .PHONY: migrate 115 | migrate: $(GOOSE) checkdbvars 116 | $(GOOSE) -dir db/migrations postgres "$(CONNECT_STRING)" up 117 | pg_dump -O -s $(CONNECT_STRING) > db/schema.sql 118 | 119 | ## Build docker image 120 | .PHONY: docker-build 121 | docker-build: 122 | docker build -t vulcanize/eth-contract-watcher -f Dockerfile . -------------------------------------------------------------------------------- /environments/example.toml: -------------------------------------------------------------------------------- 1 | [database] 2 | name = "vulcanize_public" 3 | hostname = "localhost" 4 | port = 5432 5 | user = "vdbm" 6 | password = "password" 7 | 8 | [client] 9 | rpcPath = "http://eth-server:8081" 10 | 11 | [contract] 12 | network = "" 13 | addresses = [ 14 | "0xd850942ef8811f2a866692a623011bde52a462c1", 15 | ] 16 | [contract.0xd850942ef8811f2a866692a623011bde52a462c1] 17 | abi = '[{"constant":true,"inputs":[],"name":"name","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_amount","type":"uint256"}],"name":"approve","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_newOwner","type":"address"}],"name":"setOwner","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"totalSupply","outputs":[{"name":"supply","type":"uint256"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_from","type":"address"},{"name":"_to","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transferFrom","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"decimals","outputs":[{"name":"","type":"uint8"}],"payable":false,"type":"function"},{"constant":false,"inputs":[],"name":"seal","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_bonus","type":"uint256"}],"name":"offerBonus","outputs":[],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"isSealed","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"balanceOf","outputs":[{"name":"balance","type":"uint256"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"}],"name":"lastMintedTimestamp","outputs":[{"name":"","type":"uint32"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"owner","outputs":[{"name":"","type":"address"}],"payable":false,"type":"function"},{"constant":true,"inputs":[],"name":"symbol","outputs":[{"name":"","type":"string"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_to","type":"address"},{"name":"_amount","type":"uint256"}],"name":"transfer","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_owner","type":"address"},{"name":"_amount","type":"uint256"},{"name":"_isRaw","type":"bool"},{"name":"timestamp","type":"uint32"}],"name":"mint","outputs":[],"payable":false,"type":"function"},{"constant":false,"inputs":[{"name":"_spender","type":"address"},{"name":"_value","type":"uint256"},{"name":"_extraData","type":"bytes"}],"name":"approveAndCall","outputs":[{"name":"success","type":"bool"}],"payable":false,"type":"function"},{"constant":true,"inputs":[{"name":"_owner","type":"address"},{"name":"_spender","type":"address"}],"name":"allowance","outputs":[{"name":"remaining","type":"uint256"}],"payable":false,"type":"function"},{"inputs":[],"payable":false,"type":"constructor"},{"payable":false,"type":"fallback"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_from","type":"address"},{"indexed":true,"name":"_to","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Transfer","type":"event"},{"anonymous":false,"inputs":[{"indexed":true,"name":"_owner","type":"address"},{"indexed":true,"name":"_spender","type":"address"},{"indexed":false,"name":"_value","type":"uint256"}],"name":"Approval","type":"event"}]' 18 | events = [ 19 | "Transfer", 20 | ] 21 | methods = [ 22 | "balanceOf" 23 | ] 24 | startingBlock = 10564606 25 | 26 | [ethereum] 27 | nodeID = "arch1" # $ETH_NODE_ID 28 | clientName = "Geth" # $ETH_CLIENT_NAME 29 | genesisBlock = "0xd4e56740f876aef8c010b86a40d5f56745a118d0906a34e69aec8c0db1cb8fa3" # $ETH_GENESIS_BLOCK 30 | networkID = "1" # $ETH_NETWORK_ID 31 | chainID = "1" # $ETH_CHAIN_ID 32 | 33 | #[log] 34 | # level = "debug" -------------------------------------------------------------------------------- /pkg/retriever/address_retriever.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package retriever 18 | 19 | import ( 20 | "fmt" 21 | "strings" 22 | 23 | "github.com/ethereum/go-ethereum/accounts/abi" 24 | "github.com/ethereum/go-ethereum/common" 25 | 26 | "github.com/vulcanize/eth-header-sync/pkg/postgres" 27 | 28 | "github.com/vulcanize/eth-contract-watcher/pkg/contract" 29 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 30 | ) 31 | 32 | // AddressRetriever is used to retrieve the addresses associated with a contract 33 | type AddressRetriever interface { 34 | RetrieveTokenHolderAddresses(info contract.Contract) (map[common.Address]bool, error) 35 | } 36 | 37 | type addressRetriever struct { 38 | db *postgres.DB 39 | mode types.Mode 40 | } 41 | 42 | // NewAddressRetriever returns a new AddressRetriever 43 | func NewAddressRetriever(db *postgres.DB, mode types.Mode) AddressRetriever { 44 | return &addressRetriever{ 45 | db: db, 46 | mode: mode, 47 | } 48 | } 49 | 50 | // RetrieveTokenHolderAddresses is used to retrieve list of token-holding/contract-related addresses by iterating over available events 51 | // This generic method should work whether or not the argument/input names of the events meet the expected standard 52 | // This could be generalized to iterate over ALL events and pull out any address arguments 53 | func (r *addressRetriever) RetrieveTokenHolderAddresses(info contract.Contract) (map[common.Address]bool, error) { 54 | addrList := make([]string, 0) 55 | 56 | _, ok := info.Filters["Transfer"] 57 | if ok { 58 | addrs, err := r.retrieveTransferAddresses(info) 59 | if err != nil { 60 | return nil, err 61 | } 62 | addrList = append(addrList, addrs...) 63 | } 64 | 65 | _, ok = info.Filters["Mint"] 66 | if ok { 67 | addrs, err := r.retrieveTokenMintees(info) 68 | if err != nil { 69 | return nil, err 70 | } 71 | addrList = append(addrList, addrs...) 72 | } 73 | 74 | contractAddresses := make(map[common.Address]bool) 75 | for _, addr := range addrList { 76 | contractAddresses[common.HexToAddress(addr)] = true 77 | } 78 | 79 | return contractAddresses, nil 80 | } 81 | 82 | func (r *addressRetriever) retrieveTransferAddresses(con contract.Contract) ([]string, error) { 83 | transferAddrs := make([]string, 0) 84 | event := con.Events["Transfer"] 85 | 86 | for _, field := range event.Fields { // Iterate over event fields, finding the ones with address type 87 | 88 | if field.Type.T == abi.AddressTy { // If they have address type, retrieve those addresses 89 | addrs := make([]string, 0) 90 | pgStr := fmt.Sprintf("SELECT %s_ FROM %s_%s.%s_event", strings.ToLower(field.Name), r.mode.String(), strings.ToLower(con.Address), strings.ToLower(event.Name)) 91 | err := r.db.Select(&addrs, pgStr) 92 | if err != nil { 93 | return []string{}, err 94 | } 95 | 96 | transferAddrs = append(transferAddrs, addrs...) // And append them to the growing list 97 | } 98 | } 99 | 100 | return transferAddrs, nil 101 | } 102 | 103 | func (r *addressRetriever) retrieveTokenMintees(con contract.Contract) ([]string, error) { 104 | mintAddrs := make([]string, 0) 105 | event := con.Events["Mint"] 106 | 107 | for _, field := range event.Fields { // Iterate over event fields, finding the ones with address type 108 | 109 | if field.Type.T == abi.AddressTy { // If they have address type, retrieve those addresses 110 | addrs := make([]string, 0) 111 | pgStr := fmt.Sprintf("SELECT %s_ FROM %s_%s.%s_event", strings.ToLower(field.Name), r.mode.String(), strings.ToLower(con.Address), strings.ToLower(event.Name)) 112 | err := r.db.Select(&addrs, pgStr) 113 | if err != nil { 114 | return []string{}, err 115 | } 116 | 117 | mintAddrs = append(mintAddrs, addrs...) // And append them to the growing list 118 | } 119 | } 120 | 121 | return mintAddrs, nil 122 | } 123 | -------------------------------------------------------------------------------- /pkg/abi/abi_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package abi_test 18 | 19 | import ( 20 | "net/http" 21 | 22 | "fmt" 23 | 24 | "github.com/ethereum/go-ethereum/accounts/abi" 25 | . "github.com/onsi/ginkgo" 26 | . "github.com/onsi/gomega" 27 | "github.com/onsi/gomega/ghttp" 28 | 29 | a "github.com/vulcanize/eth-contract-watcher/pkg/abi" 30 | "github.com/vulcanize/eth-contract-watcher/pkg/testing" 31 | ) 32 | 33 | var _ = Describe("ABI files", func() { 34 | 35 | Describe("Reading ABI files", func() { 36 | 37 | It("loads a valid ABI file", func() { 38 | path := testing.TestABIsPath + "valid_abi.json" 39 | 40 | contractAbi, err := a.ParseAbiFile(path) 41 | 42 | Expect(contractAbi).NotTo(BeNil()) 43 | Expect(err).To(BeNil()) 44 | }) 45 | 46 | It("reads the contents of a valid ABI file", func() { 47 | path := testing.TestABIsPath + "valid_abi.json" 48 | 49 | contractAbi, err := a.ReadAbiFile(path) 50 | 51 | Expect(contractAbi).To(Equal("[{\"foo\": \"bar\"}]")) 52 | Expect(err).To(BeNil()) 53 | }) 54 | 55 | It("returns an error when the file does not exist", func() { 56 | path := testing.TestABIsPath + "missing_abi.json" 57 | 58 | contractAbi, err := a.ParseAbiFile(path) 59 | 60 | Expect(contractAbi).To(Equal(abi.ABI{})) 61 | Expect(err).To(Equal(a.ErrMissingAbiFile)) 62 | }) 63 | 64 | It("returns an error when the file has invalid contents", func() { 65 | path := testing.TestABIsPath + "invalid_abi.json" 66 | 67 | contractAbi, err := a.ParseAbiFile(path) 68 | 69 | Expect(contractAbi).To(Equal(abi.ABI{})) 70 | Expect(err).To(Equal(a.ErrInvalidAbiFile)) 71 | }) 72 | 73 | Describe("Request ABI from endpoint", func() { 74 | 75 | var ( 76 | server *ghttp.Server 77 | client *a.EtherScanAPI 78 | abiString string 79 | err error 80 | ) 81 | 82 | BeforeEach(func() { 83 | server = ghttp.NewServer() 84 | client = a.NewEtherScanClient(server.URL()) 85 | path := testing.TestABIsPath + "sample_abi.json" 86 | abiString, err = a.ReadAbiFile(path) 87 | 88 | Expect(err).NotTo(HaveOccurred()) 89 | _, err = a.ParseAbi(abiString) 90 | Expect(err).NotTo(HaveOccurred()) 91 | }) 92 | 93 | AfterEach(func() { 94 | server.Close() 95 | }) 96 | 97 | Describe("Fetching ABI from api (etherscan)", func() { 98 | BeforeEach(func() { 99 | 100 | response := fmt.Sprintf(`{"status":"1","message":"OK","result":%q}`, abiString) 101 | server.AppendHandlers( 102 | ghttp.CombineHandlers( 103 | ghttp.VerifyRequest("GET", "/api", "module=contract&action=getabi&address=0xd26114cd6EE289AccF82350c8d8487fedB8A0C07"), 104 | ghttp.RespondWith(http.StatusOK, response), 105 | ), 106 | ) 107 | }) 108 | 109 | It("should make a GET request with supplied contract hash", func() { 110 | 111 | abi, err := client.GetAbi("0xd26114cd6EE289AccF82350c8d8487fedB8A0C07") 112 | Expect(server.ReceivedRequests()).Should(HaveLen(1)) 113 | Expect(err).ShouldNot(HaveOccurred()) 114 | Expect(abi).Should(Equal(abiString)) 115 | }) 116 | }) 117 | }) 118 | 119 | Describe("Generating etherscan endpoints based on network", func() { 120 | It("should return the main endpoint as the default", func() { 121 | url := a.GenURL("") 122 | Expect(url).To(Equal("https://api.etherscan.io")) 123 | }) 124 | 125 | It("generates various test network endpoint if test network is supplied", func() { 126 | ropstenUrl := a.GenURL("ropsten") 127 | rinkebyUrl := a.GenURL("rinkeby") 128 | kovanUrl := a.GenURL("kovan") 129 | 130 | Expect(ropstenUrl).To(Equal("https://ropsten.etherscan.io")) 131 | Expect(kovanUrl).To(Equal("https://kovan.etherscan.io")) 132 | Expect(rinkebyUrl).To(Equal("https://rinkeby.etherscan.io")) 133 | }) 134 | }) 135 | }) 136 | }) 137 | -------------------------------------------------------------------------------- /pkg/fetcher/contract_data_fetcher.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package fetcher 18 | 19 | import ( 20 | "fmt" 21 | "log" 22 | "math/big" 23 | 24 | "github.com/ethereum/go-ethereum/common" 25 | ) 26 | 27 | // Fetcher serves as the lower level data fetcher that calls the underlying 28 | // blockchain's FetchConctractData method for a given return type 29 | 30 | // ContractFetcher is the interface definition for a fetcher 31 | type ContractFetcher interface { 32 | FetchBigInt(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (big.Int, error) 33 | FetchBool(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (bool, error) 34 | FetchAddress(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Address, error) 35 | FetchString(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (string, error) 36 | FetchHash(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Hash, error) 37 | } 38 | 39 | // Used to create a new Fetcher error for a given error and fetch method 40 | func newFetcherError(err error, fetchMethod string) *fetcherError { 41 | e := fetcherError{err.Error(), fetchMethod} 42 | log.Println(e.Error()) 43 | return &e 44 | } 45 | 46 | // Fetcher error 47 | type fetcherError struct { 48 | err string 49 | fetchMethod string 50 | } 51 | 52 | // Error method 53 | func (fe *fetcherError) Error() string { 54 | return fmt.Sprintf("Error fetching %s: %s", fe.fetchMethod, fe.err) 55 | } 56 | 57 | // FetchBigInt is the method used to fetch big.Int value from contract 58 | func (f Fetcher) FetchBigInt(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (big.Int, error) { 59 | var result = new(big.Int) 60 | err := f.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber) 61 | 62 | if err != nil { 63 | return *result, newFetcherError(err, method) 64 | } 65 | 66 | return *result, nil 67 | } 68 | 69 | // FetchBool is the method used to fetch bool value from contract 70 | func (f Fetcher) FetchBool(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (bool, error) { 71 | var result = new(bool) 72 | err := f.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber) 73 | 74 | if err != nil { 75 | return *result, newFetcherError(err, method) 76 | } 77 | 78 | return *result, nil 79 | } 80 | 81 | // FetchAddress is the method used to fetch address value from contract 82 | func (f Fetcher) FetchAddress(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Address, error) { 83 | var result = new(common.Address) 84 | err := f.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber) 85 | 86 | if err != nil { 87 | return *result, newFetcherError(err, method) 88 | } 89 | 90 | return *result, nil 91 | } 92 | 93 | // FetchString is the method used to fetch string value from contract 94 | func (f Fetcher) FetchString(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (string, error) { 95 | var result = new(string) 96 | err := f.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber) 97 | 98 | if err != nil { 99 | return *result, newFetcherError(err, method) 100 | } 101 | 102 | return *result, nil 103 | } 104 | 105 | // FetchHash is the method used to fetch hash value from contract 106 | func (f Fetcher) FetchHash(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Hash, error) { 107 | var result = new(common.Hash) 108 | err := f.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber) 109 | 110 | if err != nil { 111 | return *result, newFetcherError(err, method) 112 | } 113 | 114 | return *result, nil 115 | } 116 | -------------------------------------------------------------------------------- /pkg/helpers/test_helpers/mocks/parser.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package mocks 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum/accounts/abi" 21 | a "github.com/vulcanize/eth-contract-watcher/pkg/abi" 22 | "github.com/vulcanize/eth-contract-watcher/pkg/parser" 23 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 24 | ) 25 | 26 | // Mock parser 27 | // Is given ABI string instead of address 28 | // Performs all other functions of the real parser 29 | type mockParser struct { 30 | abi string 31 | parsedAbi abi.ABI 32 | } 33 | 34 | func NewParser(abi string) parser.Parser { 35 | return &mockParser{ 36 | abi: abi, 37 | } 38 | } 39 | 40 | func (p *mockParser) Abi() string { 41 | return p.abi 42 | } 43 | 44 | func (p *mockParser) ParsedAbi() abi.ABI { 45 | return p.parsedAbi 46 | } 47 | 48 | func (p *mockParser) ParseAbiStr(abiStr string) error { 49 | panic("implement me") 50 | } 51 | 52 | // Retrieves and parses the abi string 53 | // for the given contract address 54 | func (p *mockParser) Parse(contractAddr string) error { 55 | var err error 56 | p.parsedAbi, err = a.ParseAbi(p.abi) 57 | 58 | return err 59 | } 60 | 61 | // Returns only specified methods, if they meet the criteria 62 | // Returns as array with methods in same order they were specified 63 | // Nil wanted array => no events are returned 64 | func (p *mockParser) GetSelectMethods(wanted []string) []types.Method { 65 | wLen := len(wanted) 66 | if wLen == 0 { 67 | return nil 68 | } 69 | methods := make([]types.Method, wLen) 70 | for _, m := range p.parsedAbi.Methods { 71 | for i, name := range wanted { 72 | if name == m.Name && okTypes(m, wanted) { 73 | methods[i] = types.NewMethod(m) 74 | } 75 | } 76 | } 77 | 78 | return methods 79 | } 80 | 81 | // Returns wanted methods 82 | // Empty wanted array => all methods are returned 83 | // Nil wanted array => no methods are returned 84 | func (p *mockParser) GetMethods(wanted []string) []types.Method { 85 | if wanted == nil { 86 | return nil 87 | } 88 | methods := make([]types.Method, 0) 89 | length := len(wanted) 90 | for _, m := range p.parsedAbi.Methods { 91 | if length == 0 || stringInSlice(wanted, m.Name) { 92 | methods = append(methods, types.NewMethod(m)) 93 | } 94 | } 95 | 96 | return methods 97 | } 98 | 99 | // Returns wanted events as map of types.Events 100 | // If no events are specified, all events are returned 101 | func (p *mockParser) GetEvents(wanted []string) map[string]types.Event { 102 | events := map[string]types.Event{} 103 | 104 | for _, e := range p.parsedAbi.Events { 105 | if len(wanted) == 0 || stringInSlice(wanted, e.Name) { 106 | event := types.NewEvent(e) 107 | events[e.Name] = event 108 | } 109 | } 110 | 111 | return events 112 | } 113 | 114 | func stringInSlice(list []string, s string) bool { 115 | for _, b := range list { 116 | if b == s { 117 | return true 118 | } 119 | } 120 | 121 | return false 122 | } 123 | 124 | func okTypes(m abi.Method, wanted []string) bool { 125 | // Only return method if it has less than 3 arguments, a single output value, and it is a method we want or we want all methods (empty 'wanted' slice) 126 | if len(m.Inputs) < 3 && len(m.Outputs) == 1 && (len(wanted) == 0 || stringInSlice(wanted, m.Name)) { 127 | // Only return methods if inputs are all of accepted types and output is of the accepted types 128 | if !okReturnType(m.Outputs[0]) { 129 | return false 130 | } 131 | for _, input := range m.Inputs { 132 | switch input.Type.T { 133 | case abi.AddressTy, abi.HashTy, abi.BytesTy, abi.FixedBytesTy: 134 | default: 135 | return false 136 | } 137 | } 138 | 139 | return true 140 | } 141 | 142 | return false 143 | } 144 | 145 | func okReturnType(arg abi.Argument) bool { 146 | wantedTypes := []byte{ 147 | abi.UintTy, 148 | abi.IntTy, 149 | abi.BoolTy, 150 | abi.StringTy, 151 | abi.AddressTy, 152 | abi.HashTy, 153 | abi.BytesTy, 154 | abi.FixedBytesTy, 155 | abi.FixedPointTy, 156 | } 157 | 158 | for _, ty := range wantedTypes { 159 | if arg.Type.T == ty { 160 | return true 161 | } 162 | } 163 | 164 | return false 165 | } 166 | -------------------------------------------------------------------------------- /pkg/transformer/transformer_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package transformer_test 18 | 19 | import ( 20 | "database/sql" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | 25 | hf "github.com/vulcanize/eth-header-sync/pkg/fakes" 26 | 27 | "github.com/vulcanize/eth-contract-watcher/pkg/contract" 28 | "github.com/vulcanize/eth-contract-watcher/pkg/fakes" 29 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers/mocks" 30 | "github.com/vulcanize/eth-contract-watcher/pkg/parser" 31 | "github.com/vulcanize/eth-contract-watcher/pkg/poller" 32 | "github.com/vulcanize/eth-contract-watcher/pkg/retriever" 33 | "github.com/vulcanize/eth-contract-watcher/pkg/transformer" 34 | ) 35 | 36 | var _ = Describe("Transformer", func() { 37 | var fakeAddress = "0x1234567890abcdef" 38 | Describe("Init", func() { 39 | It("Initializes transformer's contract objects", func() { 40 | blockRetriever := &fakes.MockHeaderSyncBlockRetriever{} 41 | firstBlock := int64(1) 42 | blockRetriever.FirstBlock = firstBlock 43 | 44 | parsr := &fakes.MockParser{} 45 | fakeAbi := "fake_abi" 46 | parsr.AbiToReturn = fakeAbi 47 | 48 | pollr := &fakes.MockPoller{} 49 | fakeContractName := "fake_contract_name" 50 | pollr.ContractName = fakeContractName 51 | 52 | t := getFakeTransformer(blockRetriever, parsr, pollr) 53 | 54 | err := t.Init() 55 | 56 | Expect(err).ToNot(HaveOccurred()) 57 | 58 | c, ok := t.Contracts[fakeAddress] 59 | Expect(ok).To(Equal(true)) 60 | 61 | Expect(c.StartingBlock).To(Equal(firstBlock)) 62 | Expect(c.Abi).To(Equal(fakeAbi)) 63 | Expect(c.Name).To(Equal(fakeContractName)) 64 | Expect(c.Address).To(Equal(fakeAddress)) 65 | }) 66 | 67 | It("Fails to initialize if first block cannot be fetched from vDB headers table", func() { 68 | blockRetriever := &fakes.MockHeaderSyncBlockRetriever{} 69 | blockRetriever.FirstBlockErr = hf.FakeError 70 | t := getFakeTransformer(blockRetriever, &fakes.MockParser{}, &fakes.MockPoller{}) 71 | 72 | err := t.Init() 73 | 74 | Expect(err).To(HaveOccurred()) 75 | Expect(err.Error()).To(ContainSubstring(hf.FakeError.Error())) 76 | }) 77 | }) 78 | 79 | Describe("Execute", func() { 80 | It("Executes contract transformations", func() { 81 | blockRetriever := &fakes.MockHeaderSyncBlockRetriever{} 82 | firstBlock := int64(1) 83 | blockRetriever.FirstBlock = firstBlock 84 | 85 | parsr := &fakes.MockParser{} 86 | fakeAbi := "fake_abi" 87 | parsr.AbiToReturn = fakeAbi 88 | 89 | pollr := &fakes.MockPoller{} 90 | fakeContractName := "fake_contract_name" 91 | pollr.ContractName = fakeContractName 92 | 93 | t := getFakeTransformer(blockRetriever, parsr, pollr) 94 | 95 | err := t.Init() 96 | 97 | Expect(err).ToNot(HaveOccurred()) 98 | 99 | c, ok := t.Contracts[fakeAddress] 100 | Expect(ok).To(Equal(true)) 101 | 102 | Expect(c.StartingBlock).To(Equal(firstBlock)) 103 | Expect(c.Abi).To(Equal(fakeAbi)) 104 | Expect(c.Name).To(Equal(fakeContractName)) 105 | Expect(c.Address).To(Equal(fakeAddress)) 106 | }) 107 | 108 | It("uses first block from config if vDB headers table has no rows", func() { 109 | blockRetriever := &fakes.MockHeaderSyncBlockRetriever{} 110 | blockRetriever.FirstBlockErr = sql.ErrNoRows 111 | t := getFakeTransformer(blockRetriever, &fakes.MockParser{}, &fakes.MockPoller{}) 112 | 113 | err := t.Init() 114 | 115 | Expect(err).ToNot(HaveOccurred()) 116 | }) 117 | 118 | It("returns error if fetching first block fails for other reason", func() { 119 | blockRetriever := &fakes.MockHeaderSyncBlockRetriever{} 120 | blockRetriever.FirstBlockErr = hf.FakeError 121 | t := getFakeTransformer(blockRetriever, &fakes.MockParser{}, &fakes.MockPoller{}) 122 | 123 | err := t.Init() 124 | 125 | Expect(err).To(HaveOccurred()) 126 | Expect(err.Error()).To(ContainSubstring(hf.FakeError.Error())) 127 | }) 128 | }) 129 | }) 130 | 131 | func getFakeTransformer(blockRetriever retriever.BlockRetriever, parsr parser.Parser, pollr poller.Poller) transformer.Transformer { 132 | return transformer.Transformer{ 133 | Parser: parsr, 134 | Retriever: blockRetriever, 135 | Poller: pollr, 136 | HeaderRepository: &fakes.MockHeaderSyncHeaderRepository{}, 137 | Contracts: map[string]*contract.Contract{}, 138 | Config: mocks.MockConfig, 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /pkg/constants/interface.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package constants 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum/common/hexutil" 21 | ) 22 | 23 | // SupportsInterfaceABI is the basic abi needed to check which interfaces are adhered to 24 | var SupportsInterfaceABI = `[{"constant":true,"inputs":[{"name":"interfaceID","type":"bytes4"}],"name":"supportsInterface","outputs":[{"name":"","type":"bool"}],"payable":false,"type":"function"}]` 25 | 26 | // Individual event interfaces for constructing ABI from 27 | var AddrChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"a","type":"address"}],"name":"AddrChanged","type":"event"}` 28 | var ContentChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"hash","type":"bytes32"}],"name":"ContentChanged","type":"event"}` 29 | var NameChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"name","type":"string"}],"name":"NameChanged","type":"event"}` 30 | var AbiChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":true,"name":"contentType","type":"uint256"}],"name":"ABIChanged","type":"event"}` 31 | var PubkeyChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"x","type":"bytes32"},{"indexed":false,"name":"y","type":"bytes32"}],"name":"PubkeyChanged","type":"event"}` 32 | var TextChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"indexedKey","type":"string"},{"indexed":false,"name":"key","type":"string"}],"name":"TextChanged","type":"event"}` 33 | var MultihashChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"hash","type":"bytes"}],"name":"MultihashChanged","type":"event"}` 34 | var ContenthashChangeInterface = `{"anonymous":false,"inputs":[{"indexed":true,"name":"node","type":"bytes32"},{"indexed":false,"name":"hash","type":"bytes"}],"name":"ContenthashChanged","type":"event"}` 35 | 36 | // Resolver interface signatures 37 | type Interface int 38 | 39 | // Interface enums 40 | const ( 41 | MetaSig Interface = iota 42 | AddrChangeSig 43 | ContentChangeSig 44 | NameChangeSig 45 | AbiChangeSig 46 | PubkeyChangeSig 47 | TextChangeSig 48 | MultihashChangeSig 49 | ContentHashChangeSig 50 | ) 51 | 52 | // Hex returns the hex signature for an interface 53 | func (e Interface) Hex() string { 54 | strings := [...]string{ 55 | "0x01ffc9a7", 56 | "0x3b3b57de", 57 | "0xd8389dc5", 58 | "0x691f3431", 59 | "0x2203ab56", 60 | "0xc8690233", 61 | "0x59d1d43c", 62 | "0xe89401a1", 63 | "0xbc1c58d1", 64 | } 65 | 66 | if e < MetaSig || e > ContentHashChangeSig { 67 | return "Unknown" 68 | } 69 | 70 | return strings[e] 71 | } 72 | 73 | // Bytes returns the bytes signature for an interface 74 | func (e Interface) Bytes() [4]uint8 { 75 | if e < MetaSig || e > ContentHashChangeSig { 76 | return [4]byte{} 77 | } 78 | 79 | str := e.Hex() 80 | by, _ := hexutil.Decode(str) 81 | var byArray [4]uint8 82 | for i := 0; i < 4; i++ { 83 | byArray[i] = by[i] 84 | } 85 | 86 | return byArray 87 | } 88 | 89 | // EventSig returns the event signature for an interface 90 | func (e Interface) EventSig() string { 91 | strings := [...]string{ 92 | "", 93 | "AddrChanged(bytes32,address)", 94 | "ContentChanged(bytes32,bytes32)", 95 | "NameChanged(bytes32,string)", 96 | "ABIChanged(bytes32,uint256)", 97 | "PubkeyChanged(bytes32,bytes32,bytes32)", 98 | "TextChanged(bytes32,string,string)", 99 | "MultihashChanged(bytes32,bytes)", 100 | "ContenthashChanged(bytes32,bytes)", 101 | } 102 | 103 | if e < MetaSig || e > ContentHashChangeSig { 104 | return "Unknown" 105 | } 106 | 107 | return strings[e] 108 | } 109 | 110 | // MethodSig returns the method signature for an interface 111 | func (e Interface) MethodSig() string { 112 | strings := [...]string{ 113 | "supportsInterface(bytes4)", 114 | "addr(bytes32)", 115 | "content(bytes32)", 116 | "name(bytes32)", 117 | "ABI(bytes32,uint256)", 118 | "pubkey(bytes32)", 119 | "text(bytes32,string)", 120 | "multihash(bytes32)", 121 | "setContenthash(bytes32,bytes)", 122 | } 123 | 124 | if e < MetaSig || e > ContentHashChangeSig { 125 | return "Unknown" 126 | } 127 | 128 | return strings[e] 129 | } 130 | -------------------------------------------------------------------------------- /pkg/filters/filter_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package filters_test 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | 25 | "github.com/vulcanize/eth-contract-watcher/pkg/core" 26 | "github.com/vulcanize/eth-contract-watcher/pkg/filters" 27 | ) 28 | 29 | var _ = Describe("Log filters", func() { 30 | It("decodes web3 filter to LogFilter", func() { 31 | 32 | var logFilter filters.LogFilter 33 | jsonFilter := []byte( 34 | `{ 35 | "name": "TestEvent", 36 | "fromBlock": "0x1", 37 | "toBlock": "0x488290", 38 | "address": "0x8888f1f195afa192cfee860698584c030f4c9db1", 39 | "topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null] 40 | }`) 41 | err := json.Unmarshal(jsonFilter, &logFilter) 42 | 43 | Expect(err).ToNot(HaveOccurred()) 44 | Expect(logFilter.Name).To(Equal("TestEvent")) 45 | Expect(logFilter.FromBlock).To(Equal(int64(1))) 46 | Expect(logFilter.ToBlock).To(Equal(int64(4752016))) 47 | Expect(logFilter.Address).To(Equal("0x8888f1f195afa192cfee860698584c030f4c9db1")) 48 | Expect(logFilter.Topics).To(Equal( 49 | core.Topics{ 50 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 51 | "", 52 | "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", 53 | ""})) 54 | }) 55 | 56 | It("decodes array of web3 filters to []LogFilter", func() { 57 | 58 | logFilters := make([]filters.LogFilter, 0) 59 | jsonFilter := []byte( 60 | `[{ 61 | "name": "TestEvent", 62 | "fromBlock": "0x1", 63 | "toBlock": "0x488290", 64 | "address": "0x8888f1f195afa192cfee860698584c030f4c9db1", 65 | "topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null] 66 | }, 67 | { 68 | "name": "TestEvent2", 69 | "fromBlock": "0x3", 70 | "toBlock": "0x4", 71 | "address": "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07", 72 | "topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000006b0949d4c6edfe467db78241b7d5566f3c2bb43e", "0x0000000000000000000000005e44c3e467a49c9ca0296a9f130fc433041aaa28"] 73 | }]`) 74 | err := json.Unmarshal(jsonFilter, &logFilters) 75 | 76 | Expect(err).ToNot(HaveOccurred()) 77 | Expect(len(logFilters)).To(Equal(2)) 78 | Expect(logFilters[0].Name).To(Equal("TestEvent")) 79 | Expect(logFilters[1].Name).To(Equal("TestEvent2")) 80 | }) 81 | 82 | It("requires valid ethereum address", func() { 83 | 84 | var logFilter filters.LogFilter 85 | jsonFilter := []byte( 86 | `{ 87 | "name": "TestEvent", 88 | "fromBlock": "0x1", 89 | "toBlock": "0x2", 90 | "address": "0x8888f1f195afa192cf84c030f4c9db1", 91 | "topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null] 92 | }`) 93 | err := json.Unmarshal(jsonFilter, &logFilter) 94 | Expect(err).To(HaveOccurred()) 95 | 96 | }) 97 | It("requires name", func() { 98 | 99 | var logFilter filters.LogFilter 100 | jsonFilter := []byte( 101 | `{ 102 | "fromBlock": "0x1", 103 | "toBlock": "0x2", 104 | "address": "0x8888f1f195afa192cfee860698584c030f4c9db1", 105 | "topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null] 106 | }`) 107 | err := json.Unmarshal(jsonFilter, &logFilter) 108 | Expect(err).To(HaveOccurred()) 109 | 110 | }) 111 | 112 | It("maps missing fromBlock to -1", func() { 113 | 114 | var logFilter filters.LogFilter 115 | jsonFilter := []byte( 116 | `{ 117 | "name": "TestEvent", 118 | "toBlock": "0x2", 119 | "address": "0x8888f1f195afa192cfee860698584c030f4c9db1", 120 | "topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null] 121 | }`) 122 | err := json.Unmarshal(jsonFilter, &logFilter) 123 | Expect(err).ToNot(HaveOccurred()) 124 | Expect(logFilter.FromBlock).To(Equal(int64(-1))) 125 | 126 | }) 127 | 128 | It("maps missing toBlock to -1", func() { 129 | var logFilter filters.LogFilter 130 | jsonFilter := []byte( 131 | `{ 132 | "name": "TestEvent", 133 | "address": "0x8888f1f195afa192cfee860698584c030f4c9db1", 134 | "topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null] 135 | }`) 136 | err := json.Unmarshal(jsonFilter, &logFilter) 137 | Expect(err).ToNot(HaveOccurred()) 138 | Expect(logFilter.ToBlock).To(Equal(int64(-1))) 139 | 140 | }) 141 | 142 | }) 143 | -------------------------------------------------------------------------------- /scripts/gomoderator.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | import subprocess 4 | import errno 5 | from typing import List, Dict 6 | 7 | """ 8 | Resolves dependency conflicts between a plugin repository's and the core repository's go.mods 9 | 10 | Usage: python3 gomoderator.py {path_to_core_repository} {path_to_plugin_repository} 11 | """ 12 | 13 | ERROR_INVALID_NAME = 123 14 | 15 | 16 | def is_pathname_valid(pathname: str) -> bool: 17 | """ 18 | `True` if the passed pathname is a valid pathname for the current OS; 19 | `False` otherwise. 20 | """ 21 | try: 22 | if not isinstance(pathname, str) or not pathname: 23 | return False 24 | _, pathname = os.path.splitdrive(pathname) 25 | root_dirname = os.environ.get('HOMEDRIVE', 'C:') \ 26 | if sys.platform == 'win32' else os.path.sep 27 | assert os.path.isdir(root_dirname) # ...Murphy and her ironclad Law 28 | root_dirname = root_dirname.rstrip(os.path.sep) + os.path.sep 29 | for pathname_part in pathname.split(os.path.sep): 30 | try: 31 | os.lstat(root_dirname + pathname_part) 32 | except OSError as exc: 33 | if hasattr(exc, 'winerror'): 34 | if exc.winerror == ERROR_INVALID_NAME: 35 | return False 36 | elif exc.errno in {errno.ENAMETOOLONG, errno.ERANGE}: 37 | return False 38 | except TypeError as exc: 39 | return False 40 | else: 41 | return True 42 | 43 | 44 | def map_deps_to_version(deps_arr: List[str]) -> Dict[str, str]: 45 | mapping = {} 46 | for d in deps_arr: 47 | if d.find(' => ') != -1: 48 | ds = d.split(' => ') 49 | d = ds[1] 50 | d = d.replace(" v", "[>v") # might be able to just split on the empty space not _v and skip this :: insertion 51 | d_and_v = d.split("[>") 52 | mapping[d_and_v[0]] = d_and_v[1] 53 | return mapping 54 | 55 | 56 | # argument checks 57 | assert len(sys.argv) == 3, "need core repository and plugin repository path arguments" 58 | core_repository_path = sys.argv[1] 59 | plugin_repository_path = sys.argv[2] 60 | assert is_pathname_valid(core_repository_path), "core repository path argument is not valid" 61 | assert is_pathname_valid(plugin_repository_path), "plugin repository path argument is not valid" 62 | 63 | # collect `go list -m all` output from both repositories; remain in the plugin repository 64 | os.chdir(core_repository_path) 65 | core_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) 66 | os.chdir(plugin_repository_path) 67 | plugin_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) 68 | core_deps = core_deps_b.decode("utf-8") 69 | core_deps_arr = core_deps.splitlines() 70 | del core_deps_arr[0] # first line is the project repo itself 71 | plugin_deps = plugin_deps_b.decode("utf-8") 72 | plugin_deps_arr = plugin_deps.splitlines() 73 | del plugin_deps_arr[0] 74 | core_deps_mapping = map_deps_to_version(core_deps_arr) 75 | plugin_deps_mapping = map_deps_to_version(plugin_deps_arr) 76 | 77 | # iterate over dependency maps for both repos and find version conflicts 78 | # attempt to resolve conflicts by adding adding a `require` for the core version to the plugin's go.mod file 79 | none = True 80 | for dep, core_version in core_deps_mapping.items(): 81 | if dep in plugin_deps_mapping.keys(): 82 | plugin_version = plugin_deps_mapping[dep] 83 | if core_version != plugin_version: 84 | print(f'{dep} has a conflict: core is using version {core_version} ' 85 | f'but the plugin is using version {plugin_version}') 86 | fixed_dep = f'{dep}@{core_version}' 87 | print(f'attempting fix by `go mod edit -require={fixed_dep}') 88 | subprocess.check_call(["go", "mod", "edit", f'-require={fixed_dep}']) 89 | none = False 90 | 91 | if none: 92 | print("no conflicts to resolve") 93 | quit() 94 | 95 | # the above process does not work for all dep conflicts e.g. golang.org/x/text v0.3.0 will not stick this way 96 | # so we will try the `go get {dep}` route for any remaining conflicts 97 | updated_plugin_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) 98 | updated_plugin_deps = updated_plugin_deps_b.decode("utf-8") 99 | updated_plugin_deps_arr = updated_plugin_deps.splitlines() 100 | del updated_plugin_deps_arr[0] 101 | updated_plugin_deps_mapping = map_deps_to_version(updated_plugin_deps_arr) 102 | none = True 103 | for dep, core_version in core_deps_mapping.items(): 104 | if dep in updated_plugin_deps_mapping.keys(): 105 | updated_plugin_version = updated_plugin_deps_mapping[dep] 106 | if core_version != updated_plugin_version: 107 | print(f'{dep} still has a conflict: core is using version {core_version} ' 108 | f'but the plugin is using version {updated_plugin_version}') 109 | fixed_dep = f'{dep}@{core_version}' 110 | print(f'attempting fix by `go get {fixed_dep}') 111 | subprocess.check_call(["go", "get", fixed_dep]) 112 | none = False 113 | 114 | if none: 115 | print("all conflicts have been resolved") 116 | quit() 117 | 118 | # iterate over plugins `go list -m all` output one more time and inform whether or not the above has worked 119 | final_plugin_deps_b = subprocess.check_output(["go", "list", "-m", "all"]) 120 | final_plugin_deps = final_plugin_deps_b.decode("utf-8") 121 | final_plugin_deps_arr = final_plugin_deps.splitlines() 122 | del final_plugin_deps_arr[0] 123 | final_plugin_deps_mapping = map_deps_to_version(final_plugin_deps_arr) 124 | none = True 125 | for dep, core_version in core_deps_mapping.items(): 126 | if dep in final_plugin_deps_mapping.keys(): 127 | final_plugin_version = final_plugin_deps_mapping[dep] 128 | if core_version != final_plugin_version: 129 | print(f'{dep} STILL has a conflict: core is using version {core_version} ' 130 | f'but the plugin is using version {final_plugin_version}') 131 | none = False 132 | 133 | if none: 134 | print("all conflicts have been resolved") 135 | quit() 136 | 137 | print("failed to resolve all conflicts") 138 | -------------------------------------------------------------------------------- /pkg/contract/contract.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package contract 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/ethereum/go-ethereum/accounts/abi" 23 | "github.com/ethereum/go-ethereum/common" 24 | "github.com/ethereum/go-ethereum/common/hexutil" 25 | 26 | "github.com/vulcanize/eth-contract-watcher/pkg/core" 27 | "github.com/vulcanize/eth-contract-watcher/pkg/filters" 28 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 29 | ) 30 | 31 | // Contract object to hold our contract data 32 | type Contract struct { 33 | Name string // Name of the contract 34 | Address string // Address of the contract 35 | Network string // Network on which the contract is deployed; default empty "" is Ethereum mainnet 36 | StartingBlock int64 // Starting block of the contract 37 | Abi string // Abi string 38 | ParsedAbi abi.ABI // Parsed abi 39 | Events map[string]types.Event // List of events to watch 40 | Methods []types.Method // List of methods to poll 41 | Filters map[string]filters.LogFilter // Map of event filters to their event names; used only for full sync watcher 42 | FilterArgs map[string]bool // User-input list of values to filter event logs for 43 | MethodArgs map[string]bool // User-input list of values to limit method polling to 44 | EmittedAddrs map[interface{}]bool // List of all unique addresses collected from converted event logs 45 | EmittedHashes map[interface{}]bool // List of all unique hashes collected from converted event logs 46 | CreateAddrList bool // Whether or not to persist address list to postgres 47 | CreateHashList bool // Whether or not to persist hash list to postgres 48 | Piping bool // Whether or not to pipe method results forward as arguments to subsequent methods 49 | } 50 | 51 | // Init initializes a contract object 52 | // If we will be calling methods that use addr, hash, or byte arrays 53 | // as arguments then we initialize maps to hold these types of values 54 | func (c Contract) Init() *Contract { 55 | for _, method := range c.Methods { 56 | for _, arg := range method.Args { 57 | switch arg.Type.T { 58 | case abi.AddressTy: 59 | c.EmittedAddrs = map[interface{}]bool{} 60 | case abi.HashTy, abi.BytesTy, abi.FixedBytesTy: 61 | c.EmittedHashes = map[interface{}]bool{} 62 | default: 63 | } 64 | } 65 | } 66 | 67 | return &c 68 | } 69 | 70 | // GenerateFilters uses contract info to generate event filters - full sync contract watcher only 71 | func (c *Contract) GenerateFilters() error { 72 | c.Filters = map[string]filters.LogFilter{} 73 | 74 | for name, event := range c.Events { 75 | c.Filters[name] = filters.LogFilter{ 76 | Name: c.Address + "_" + event.Name, 77 | FromBlock: c.StartingBlock, 78 | ToBlock: -1, 79 | Address: common.HexToAddress(c.Address).Hex(), 80 | Topics: core.Topics{event.Sig().Hex()}, 81 | } 82 | } 83 | // If no filters were generated, throw an error (no point in continuing with this contract) 84 | if len(c.Filters) == 0 { 85 | return errors.New("error: no filters created") 86 | } 87 | 88 | return nil 89 | } 90 | 91 | // WantedEventArg returns true if address is in list of arguments to 92 | // filter events for or if no filtering is specified 93 | func (c *Contract) WantedEventArg(arg string) bool { 94 | if c.FilterArgs == nil { 95 | return false 96 | } else if len(c.FilterArgs) == 0 { 97 | return true 98 | } else if a, ok := c.FilterArgs[arg]; ok { 99 | return a 100 | } 101 | 102 | return false 103 | } 104 | 105 | // WantedMethodArg returns true if address is in list of arguments to 106 | // poll methods with or if no filtering is specified 107 | func (c *Contract) WantedMethodArg(arg interface{}) bool { 108 | if c.MethodArgs == nil { 109 | return false 110 | } else if len(c.MethodArgs) == 0 { 111 | return true 112 | } 113 | 114 | // resolve interface to one of the three types we handle as arguments 115 | str := StringifyArg(arg) 116 | 117 | // See if it's hex string has been filtered for 118 | if a, ok := c.MethodArgs[str]; ok { 119 | return a 120 | } 121 | 122 | return false 123 | } 124 | 125 | // PassesEventFilter returns true if any mapping value matches filtered for address or if no filter exists 126 | // Used to check if an event log name-value mapping should be filtered or not 127 | func (c *Contract) PassesEventFilter(args map[string]string) bool { 128 | for _, arg := range args { 129 | if c.WantedEventArg(arg) { 130 | return true 131 | } 132 | } 133 | 134 | return false 135 | } 136 | 137 | // AddEmittedAddr adds event emitted addresses to our list if it passes filter and method polling is on 138 | func (c *Contract) AddEmittedAddr(addresses ...interface{}) { 139 | for _, addr := range addresses { 140 | if c.WantedMethodArg(addr) && c.Methods != nil { 141 | c.EmittedAddrs[addr] = true 142 | } 143 | } 144 | } 145 | 146 | // AddEmittedHash adds event emitted hashes to our list if it passes filter and method polling is on 147 | func (c *Contract) AddEmittedHash(hashes ...interface{}) { 148 | for _, hash := range hashes { 149 | if c.WantedMethodArg(hash) && c.Methods != nil { 150 | c.EmittedHashes[hash] = true 151 | } 152 | } 153 | } 154 | 155 | // StringifyArg resolves a method argument type to string type 156 | func StringifyArg(arg interface{}) (str string) { 157 | switch arg.(type) { 158 | case string: 159 | str = arg.(string) 160 | case common.Address: 161 | a := arg.(common.Address) 162 | str = a.String() 163 | case common.Hash: 164 | a := arg.(common.Hash) 165 | str = a.String() 166 | case []byte: 167 | a := arg.([]byte) 168 | str = hexutil.Encode(a) 169 | } 170 | 171 | return 172 | } 173 | -------------------------------------------------------------------------------- /pkg/parser/parser.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package parser 18 | 19 | import ( 20 | "errors" 21 | 22 | "github.com/ethereum/go-ethereum/accounts/abi" 23 | "github.com/ethereum/go-ethereum/common" 24 | 25 | a "github.com/vulcanize/eth-contract-watcher/pkg/abi" 26 | "github.com/vulcanize/eth-contract-watcher/pkg/constants" 27 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 28 | ) 29 | 30 | // Parser is used to fetch and parse contract ABIs 31 | // It is dependent on etherscan's api 32 | type Parser interface { 33 | Parse(contractAddr string) error 34 | ParseAbiStr(abiStr string) error 35 | Abi() string 36 | ParsedAbi() abi.ABI 37 | GetMethods(wanted []string) []types.Method 38 | GetSelectMethods(wanted []string) []types.Method 39 | GetEvents(wanted []string) map[string]types.Event 40 | } 41 | 42 | type parser struct { 43 | client *a.EtherScanAPI 44 | abi string 45 | parsedAbi abi.ABI 46 | } 47 | 48 | // NewParser returns a new Parser 49 | func NewParser(network string) Parser { 50 | url := a.GenURL(network) 51 | 52 | return &parser{ 53 | client: a.NewEtherScanClient(url), 54 | } 55 | } 56 | 57 | // Abi returns the parser's configured abi string 58 | func (p *parser) Abi() string { 59 | return p.abi 60 | } 61 | 62 | // ParsedAbi returns the parser's parsed abi 63 | func (p *parser) ParsedAbi() abi.ABI { 64 | return p.parsedAbi 65 | } 66 | 67 | // Parse retrieves and parses the abi string 68 | // for the given contract address 69 | func (p *parser) Parse(contractAddr string) error { 70 | // If the abi is one our locally stored abis, fetch 71 | // TODO: Allow users to pass abis through config 72 | knownAbi, err := p.lookUp(contractAddr) 73 | if err == nil { 74 | p.abi = knownAbi 75 | p.parsedAbi, err = a.ParseAbi(knownAbi) 76 | return err 77 | } 78 | // Try getting abi from etherscan 79 | abiStr, err := p.client.GetAbi(contractAddr) 80 | if err != nil { 81 | return err 82 | } 83 | //TODO: Implement other ways to fetch abi 84 | p.abi = abiStr 85 | p.parsedAbi, err = a.ParseAbi(abiStr) 86 | 87 | return err 88 | } 89 | 90 | // ParseAbiStr loads and parses an abi from a given abi string 91 | func (p *parser) ParseAbiStr(abiStr string) error { 92 | var err error 93 | p.abi = abiStr 94 | p.parsedAbi, err = a.ParseAbi(abiStr) 95 | 96 | return err 97 | } 98 | 99 | func (p *parser) lookUp(contractAddr string) (string, error) { 100 | if v, ok := constants.ABIs[common.HexToAddress(contractAddr)]; ok { 101 | return v, nil 102 | } 103 | 104 | return "", errors.New("ABI not present in lookup table") 105 | } 106 | 107 | // GetSelectMethods returns only specified methods, if they meet the criteria 108 | // Returns as array with methods in same order they were specified 109 | // Nil or empty wanted array => no events are returned 110 | func (p *parser) GetSelectMethods(wanted []string) []types.Method { 111 | wLen := len(wanted) 112 | if wLen == 0 { 113 | return nil 114 | } 115 | methods := make([]types.Method, wLen) 116 | for _, m := range p.parsedAbi.Methods { 117 | for i, name := range wanted { 118 | if name == m.Name && okTypes(m, wanted) { 119 | methods[i] = types.NewMethod(m) 120 | } 121 | } 122 | } 123 | 124 | return methods 125 | } 126 | 127 | // GetMethods returns wanted methods 128 | // Empty wanted array => all methods are returned 129 | // Nil wanted array => no methods are returned 130 | func (p *parser) GetMethods(wanted []string) []types.Method { 131 | if wanted == nil { 132 | return nil 133 | } 134 | methods := make([]types.Method, 0) 135 | length := len(wanted) 136 | for _, m := range p.parsedAbi.Methods { 137 | if length == 0 || stringInSlice(wanted, m.Name) { 138 | methods = append(methods, types.NewMethod(m)) 139 | } 140 | } 141 | 142 | return methods 143 | } 144 | 145 | // GetEvents returns wanted events as map of types.Events 146 | // Empty wanted array => all events are returned 147 | // Nil wanted array => no events are returned 148 | func (p *parser) GetEvents(wanted []string) map[string]types.Event { 149 | events := map[string]types.Event{} 150 | if wanted == nil { 151 | return events 152 | } 153 | 154 | length := len(wanted) 155 | for _, e := range p.parsedAbi.Events { 156 | if length == 0 || stringInSlice(wanted, e.Name) { 157 | events[e.Name] = types.NewEvent(e) 158 | } 159 | } 160 | 161 | return events 162 | } 163 | 164 | func okReturnType(arg abi.Argument) bool { 165 | wantedTypes := []byte{ 166 | abi.UintTy, 167 | abi.IntTy, 168 | abi.BoolTy, 169 | abi.StringTy, 170 | abi.AddressTy, 171 | abi.HashTy, 172 | abi.BytesTy, 173 | abi.FixedBytesTy, 174 | abi.FixedPointTy, 175 | } 176 | 177 | for _, ty := range wantedTypes { 178 | if arg.Type.T == ty { 179 | return true 180 | } 181 | } 182 | 183 | return false 184 | } 185 | 186 | func okTypes(m abi.Method, wanted []string) bool { 187 | // Only return method if it has less than 3 arguments, a single output value, and it is a method we want or we want all methods (empty 'wanted' slice) 188 | if len(m.Inputs) < 3 && len(m.Outputs) == 1 && (len(wanted) == 0 || stringInSlice(wanted, m.Name)) { 189 | // Only return methods if inputs are all of accepted types and output is of the accepted types 190 | if !okReturnType(m.Outputs[0]) { 191 | return false 192 | } 193 | for _, input := range m.Inputs { 194 | switch input.Type.T { 195 | // Addresses are properly labeled and caught 196 | // But hashes tend to not be explicitly labeled and caught 197 | // Instead bytes32 are assumed to be hashes 198 | case abi.AddressTy, abi.HashTy: 199 | case abi.FixedBytesTy: 200 | if input.Type.Size != 32 { 201 | return false 202 | } 203 | default: 204 | return false 205 | } 206 | } 207 | return true 208 | } 209 | 210 | return false 211 | } 212 | 213 | func stringInSlice(list []string, s string) bool { 214 | for _, b := range list { 215 | if b == s { 216 | return true 217 | } 218 | } 219 | 220 | return false 221 | } 222 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package cmd 18 | 19 | import ( 20 | "fmt" 21 | "os" 22 | "strings" 23 | "time" 24 | 25 | "github.com/ethereum/go-ethereum/ethclient" 26 | "github.com/ethereum/go-ethereum/rpc" 27 | log "github.com/sirupsen/logrus" 28 | "github.com/spf13/cobra" 29 | "github.com/spf13/viper" 30 | 31 | hc "github.com/vulcanize/eth-header-sync/pkg/config" 32 | "github.com/vulcanize/eth-header-sync/pkg/core" 33 | "github.com/vulcanize/eth-header-sync/pkg/node" 34 | ) 35 | 36 | var ( 37 | cfgFile string 38 | databaseConfig hc.Database 39 | ipc string 40 | subCommand string 41 | logWithCommand log.Entry 42 | timeout time.Duration 43 | ) 44 | 45 | var rootCmd = &cobra.Command{ 46 | Use: "eth-contract-watcher", 47 | PersistentPreRun: initFuncs, 48 | } 49 | 50 | func Execute() { 51 | log.Info("----- Starting vDB -----") 52 | if err := rootCmd.Execute(); err != nil { 53 | log.Fatal(err) 54 | } 55 | } 56 | 57 | func initFuncs(cmd *cobra.Command, args []string) { 58 | setViperConfigs() 59 | logfile := viper.GetString("logfile") 60 | if logfile != "" { 61 | file, err := os.OpenFile(logfile, 62 | os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666) 63 | if err == nil { 64 | log.Infof("Directing output to %s", logfile) 65 | log.SetOutput(file) 66 | } else { 67 | log.SetOutput(os.Stdout) 68 | log.Info("Failed to log to file, using default stdout") 69 | } 70 | } else { 71 | log.SetOutput(os.Stdout) 72 | } 73 | if err := logLevel(); err != nil { 74 | log.Fatal("Could not set log level: ", err) 75 | } 76 | initTimeout() 77 | } 78 | 79 | func setViperConfigs() { 80 | ipc = viper.GetString("client.rpcPath") 81 | databaseConfig = hc.Database{ 82 | Name: viper.GetString("database.name"), 83 | Hostname: viper.GetString("database.hostname"), 84 | Port: viper.GetInt("database.port"), 85 | User: viper.GetString("database.user"), 86 | Password: viper.GetString("database.password"), 87 | } 88 | viper.Set("database.config", databaseConfig) 89 | } 90 | 91 | func logLevel() error { 92 | lvl, err := log.ParseLevel(viper.GetString("log.level")) 93 | if err != nil { 94 | return err 95 | } 96 | log.SetLevel(lvl) 97 | if lvl > log.InfoLevel { 98 | log.SetReportCaller(true) 99 | } 100 | log.Info("Log level set to ", lvl.String()) 101 | return nil 102 | } 103 | 104 | func initTimeout() { 105 | t := viper.GetInt("timeout") 106 | if t < 15 { 107 | t = 15 108 | } 109 | 110 | timeout = time.Second * time.Duration(t) 111 | } 112 | 113 | func init() { 114 | cobra.OnInitialize(initConfig) 115 | // When searching for env variables, replace dots in config keys with underscores 116 | viper.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) 117 | viper.AutomaticEnv() 118 | 119 | rootCmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file location") 120 | rootCmd.PersistentFlags().String("logfile", "", "file path for logging") 121 | rootCmd.PersistentFlags().String("database-name", "vulcanize_public", "database name") 122 | rootCmd.PersistentFlags().Int("database-port", 5432, "database port") 123 | rootCmd.PersistentFlags().String("database-hostname", "localhost", "database hostname") 124 | rootCmd.PersistentFlags().String("database-user", "", "database user") 125 | rootCmd.PersistentFlags().String("database-password", "", "database password") 126 | rootCmd.PersistentFlags().String("client-rpcPath", "", "rpc path to Ethereum JSON-RPC endpoints") 127 | rootCmd.PersistentFlags().String("client-levelDbPath", "", "location of levelDb chaindata") 128 | rootCmd.PersistentFlags().String("filesystem-storageDiffsPath", "", "location of storage diffs csv file") 129 | rootCmd.PersistentFlags().String("storageDiffs-source", "csv", "where to get the state diffs: csv or geth") 130 | rootCmd.PersistentFlags().String("exporter-name", "exporter", "name of exporter plugin") 131 | rootCmd.PersistentFlags().String("log-level", log.InfoLevel.String(), "Log level (trace, debug, info, warn, error, fatal, panic)") 132 | rootCmd.PersistentFlags().Int("timeout", 15, "timeout used for Eth JSON-RPC requests (in seconds)") 133 | 134 | viper.BindPFlag("logfile", rootCmd.PersistentFlags().Lookup("logfile")) 135 | viper.BindPFlag("database.name", rootCmd.PersistentFlags().Lookup("database-name")) 136 | viper.BindPFlag("database.port", rootCmd.PersistentFlags().Lookup("database-port")) 137 | viper.BindPFlag("database.hostname", rootCmd.PersistentFlags().Lookup("database-hostname")) 138 | viper.BindPFlag("database.user", rootCmd.PersistentFlags().Lookup("database-user")) 139 | viper.BindPFlag("database.password", rootCmd.PersistentFlags().Lookup("database-password")) 140 | viper.BindPFlag("client.rpcPath", rootCmd.PersistentFlags().Lookup("client-rpcPath")) 141 | viper.BindPFlag("client.levelDbPath", rootCmd.PersistentFlags().Lookup("client-levelDbPath")) 142 | viper.BindPFlag("filesystem.storageDiffsPath", rootCmd.PersistentFlags().Lookup("filesystem-storageDiffsPath")) 143 | viper.BindPFlag("storageDiffs.source", rootCmd.PersistentFlags().Lookup("storageDiffs-source")) 144 | viper.BindPFlag("exporter.fileName", rootCmd.PersistentFlags().Lookup("exporter-name")) 145 | viper.BindPFlag("log.level", rootCmd.PersistentFlags().Lookup("log-level")) 146 | viper.BindPFlag("timeout", rootCmd.PersistentFlags().Lookup("timeout")) 147 | } 148 | 149 | func initConfig() { 150 | if cfgFile != "" { 151 | viper.SetConfigFile(cfgFile) 152 | if err := viper.ReadInConfig(); err == nil { 153 | log.Printf("Using config file: %s", viper.ConfigFileUsed()) 154 | } else { 155 | log.Fatal(fmt.Sprintf("Couldn't read config file: %s", err.Error())) 156 | } 157 | } else { 158 | log.Warn("No config file passed with --config flag") 159 | } 160 | } 161 | 162 | func getClientAndNode() (*ethclient.Client, core.Node) { 163 | rawRPCClient, err := rpc.Dial(ipc) 164 | if err != nil { 165 | logWithCommand.Fatal(err) 166 | } 167 | return ethclient.NewClient(rawRPCClient), node.MakeNode() 168 | } 169 | -------------------------------------------------------------------------------- /pkg/parser/parser_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package parser_test 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum/accounts/abi" 21 | . "github.com/onsi/ginkgo" 22 | . "github.com/onsi/gomega" 23 | 24 | a "github.com/vulcanize/eth-contract-watcher/pkg/abi" 25 | "github.com/vulcanize/eth-contract-watcher/pkg/constants" 26 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers/mocks" 27 | "github.com/vulcanize/eth-contract-watcher/pkg/parser" 28 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 29 | ) 30 | 31 | var _ = Describe("Parser", func() { 32 | 33 | var p parser.Parser 34 | var err error 35 | 36 | BeforeEach(func() { 37 | p = parser.NewParser("") 38 | }) 39 | 40 | Describe("Mock Parse", func() { 41 | It("Uses parses given abi string", func() { 42 | mp := mocks.NewParser(constants.DaiAbiString) 43 | err = mp.Parse(constants.DaiContractAddress) 44 | Expect(err).ToNot(HaveOccurred()) 45 | 46 | parsedAbi := mp.ParsedAbi() 47 | expectedAbi, err := a.ParseAbi(constants.DaiAbiString) 48 | Expect(err).ToNot(HaveOccurred()) 49 | Expect(parsedAbi).To(Equal(expectedAbi)) 50 | 51 | methods := mp.GetSelectMethods([]string{"balanceOf"}) 52 | Expect(len(methods)).To(Equal(1)) 53 | balOf := methods[0] 54 | Expect(balOf.Name).To(Equal("balanceOf")) 55 | Expect(len(balOf.Args)).To(Equal(1)) 56 | Expect(len(balOf.Return)).To(Equal(1)) 57 | 58 | events := mp.GetEvents([]string{"Transfer"}) 59 | _, ok := events["Mint"] 60 | Expect(ok).To(Equal(false)) 61 | e, ok := events["Transfer"] 62 | Expect(ok).To(Equal(true)) 63 | Expect(len(e.Fields)).To(Equal(3)) 64 | }) 65 | }) 66 | 67 | Describe("Parse", func() { 68 | It("Fetches and parses abi from etherscan using contract address", func() { 69 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" // dai contract address 70 | err = p.Parse(contractAddr) 71 | Expect(err).ToNot(HaveOccurred()) 72 | 73 | expectedAbi := constants.DaiAbiString 74 | Expect(p.Abi()).To(Equal(expectedAbi)) 75 | 76 | expectedParsedAbi, err := a.ParseAbi(expectedAbi) 77 | Expect(err).ToNot(HaveOccurred()) 78 | Expect(p.ParsedAbi()).To(Equal(expectedParsedAbi)) 79 | }) 80 | 81 | It("Fails with a normal, non-contract, account address", func() { 82 | addr := "0xAb2A8F7cB56D9EC65573BA1bE0f92Fa2Ff7dd165" 83 | err = p.Parse(addr) 84 | Expect(err).To(HaveOccurred()) 85 | }) 86 | }) 87 | 88 | Describe("GetEvents", func() { 89 | It("Returns parsed events", func() { 90 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" 91 | err = p.Parse(contractAddr) 92 | Expect(err).ToNot(HaveOccurred()) 93 | 94 | events := p.GetEvents([]string{"Transfer"}) 95 | 96 | e, ok := events["Transfer"] 97 | Expect(ok).To(Equal(true)) 98 | 99 | abiTy := e.Fields[0].Type.T 100 | Expect(abiTy).To(Equal(abi.AddressTy)) 101 | 102 | pgTy := e.Fields[0].PgType 103 | Expect(pgTy).To(Equal("CHARACTER VARYING(66)")) 104 | 105 | abiTy = e.Fields[1].Type.T 106 | Expect(abiTy).To(Equal(abi.AddressTy)) 107 | 108 | pgTy = e.Fields[1].PgType 109 | Expect(pgTy).To(Equal("CHARACTER VARYING(66)")) 110 | 111 | abiTy = e.Fields[2].Type.T 112 | Expect(abiTy).To(Equal(abi.UintTy)) 113 | 114 | pgTy = e.Fields[2].PgType 115 | Expect(pgTy).To(Equal("NUMERIC")) 116 | 117 | _, ok = events["Approval"] 118 | Expect(ok).To(Equal(false)) 119 | }) 120 | }) 121 | 122 | Describe("GetSelectMethods", func() { 123 | It("Parses and returns only methods specified in passed array", func() { 124 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" 125 | err = p.Parse(contractAddr) 126 | Expect(err).ToNot(HaveOccurred()) 127 | 128 | methods := p.GetSelectMethods([]string{"balanceOf"}) 129 | Expect(len(methods)).To(Equal(1)) 130 | 131 | balOf := methods[0] 132 | Expect(balOf.Name).To(Equal("balanceOf")) 133 | Expect(len(balOf.Args)).To(Equal(1)) 134 | Expect(len(balOf.Return)).To(Equal(1)) 135 | 136 | abiTy := balOf.Args[0].Type.T 137 | Expect(abiTy).To(Equal(abi.AddressTy)) 138 | 139 | pgTy := balOf.Args[0].PgType 140 | Expect(pgTy).To(Equal("CHARACTER VARYING(66)")) 141 | 142 | abiTy = balOf.Return[0].Type.T 143 | Expect(abiTy).To(Equal(abi.UintTy)) 144 | 145 | pgTy = balOf.Return[0].PgType 146 | Expect(pgTy).To(Equal("NUMERIC")) 147 | 148 | }) 149 | 150 | It("Parses and returns methods in the order they were specified", func() { 151 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" 152 | err = p.Parse(contractAddr) 153 | Expect(err).ToNot(HaveOccurred()) 154 | 155 | selectMethods := p.GetSelectMethods([]string{"balanceOf", "allowance"}) 156 | Expect(len(selectMethods)).To(Equal(2)) 157 | 158 | balOf := selectMethods[0] 159 | allow := selectMethods[1] 160 | 161 | Expect(balOf.Name).To(Equal("balanceOf")) 162 | Expect(allow.Name).To(Equal("allowance")) 163 | }) 164 | 165 | It("Returns nil if given a nil or empty array", func() { 166 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" 167 | err = p.Parse(contractAddr) 168 | Expect(err).ToNot(HaveOccurred()) 169 | 170 | var nilArr []types.Method 171 | selectMethods := p.GetSelectMethods([]string{}) 172 | Expect(selectMethods).To(Equal(nilArr)) 173 | selectMethods = p.GetMethods(nil) 174 | Expect(selectMethods).To(Equal(nilArr)) 175 | }) 176 | 177 | }) 178 | 179 | Describe("GetMethods", func() { 180 | It("Parses and returns only methods specified in passed array", func() { 181 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" 182 | err = p.Parse(contractAddr) 183 | Expect(err).ToNot(HaveOccurred()) 184 | 185 | methods := p.GetMethods([]string{"balanceOf"}) 186 | Expect(len(methods)).To(Equal(1)) 187 | 188 | balOf := methods[0] 189 | Expect(balOf.Name).To(Equal("balanceOf")) 190 | Expect(len(balOf.Args)).To(Equal(1)) 191 | Expect(len(balOf.Return)).To(Equal(1)) 192 | 193 | abiTy := balOf.Args[0].Type.T 194 | Expect(abiTy).To(Equal(abi.AddressTy)) 195 | 196 | pgTy := balOf.Args[0].PgType 197 | Expect(pgTy).To(Equal("CHARACTER VARYING(66)")) 198 | 199 | abiTy = balOf.Return[0].Type.T 200 | Expect(abiTy).To(Equal(abi.UintTy)) 201 | 202 | pgTy = balOf.Return[0].PgType 203 | Expect(pgTy).To(Equal("NUMERIC")) 204 | 205 | }) 206 | 207 | It("Returns nil if given a nil array", func() { 208 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" 209 | err = p.Parse(contractAddr) 210 | Expect(err).ToNot(HaveOccurred()) 211 | 212 | var nilArr []types.Method 213 | selectMethods := p.GetMethods(nil) 214 | Expect(selectMethods).To(Equal(nilArr)) 215 | }) 216 | 217 | It("Returns every method if given an empty array", func() { 218 | contractAddr := "0x89d24a6b4ccb1b6faa2625fe562bdd9a23260359" 219 | err = p.Parse(contractAddr) 220 | Expect(err).ToNot(HaveOccurred()) 221 | 222 | selectMethods := p.GetMethods([]string{}) 223 | Expect(len(selectMethods)).To(Equal(25)) 224 | }) 225 | }) 226 | }) 227 | -------------------------------------------------------------------------------- /pkg/converter/log_converter_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package converter_test 18 | 19 | import ( 20 | "github.com/ethereum/go-ethereum/common" 21 | "github.com/ethereum/go-ethereum/core/types" 22 | . "github.com/onsi/ginkgo" 23 | . "github.com/onsi/gomega" 24 | 25 | "github.com/vulcanize/eth-contract-watcher/pkg/contract" 26 | "github.com/vulcanize/eth-contract-watcher/pkg/converter" 27 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers" 28 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers" 29 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers/mocks" 30 | ) 31 | 32 | var _ = Describe("Converter", func() { 33 | var con *contract.Contract 34 | var tusdWantedEvents = []string{"Transfer", "Mint"} 35 | var ensWantedEvents = []string{"NewOwner"} 36 | var marketPlaceWantedEvents = []string{"OrderCreated"} 37 | var molochWantedEvents = []string{"SubmitVote"} 38 | var err error 39 | 40 | Describe("Update", func() { 41 | It("Updates contract info held by the converter", func() { 42 | con = test_helpers.SetupTusdContract(tusdWantedEvents, []string{}) 43 | c := converter.Converter{} 44 | c.Update(con) 45 | Expect(c.ContractInfo).To(Equal(con)) 46 | 47 | info := test_helpers.SetupTusdContract([]string{}, []string{}) 48 | c.Update(info) 49 | Expect(c.ContractInfo).To(Equal(info)) 50 | }) 51 | }) 52 | 53 | Describe("Convert", func() { 54 | It("Converts a watched event log to mapping of event input names to values", func() { 55 | con = test_helpers.SetupTusdContract(tusdWantedEvents, []string{}) 56 | _, ok := con.Events["Approval"] 57 | Expect(ok).To(Equal(false)) 58 | 59 | event, ok := con.Events["Transfer"] 60 | Expect(ok).To(Equal(true)) 61 | 62 | c := converter.Converter{} 63 | c.Update(con) 64 | logs, err := c.Convert([]types.Log{mocks.MockTransferLog1, mocks.MockTransferLog2}, event, 232) 65 | Expect(err).ToNot(HaveOccurred()) 66 | Expect(len(logs)).To(Equal(2)) 67 | 68 | sender1 := common.HexToAddress("0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba391") 69 | sender2 := common.HexToAddress("0x000000000000000000000000000000000000000000000000000000000000af21") 70 | value := helpers.BigFromString("1097077688018008265106216665536940668749033598146") 71 | 72 | Expect(logs[0].Values["to"]).To(Equal(sender1.String())) 73 | Expect(logs[0].Values["from"]).To(Equal(sender2.String())) 74 | Expect(logs[0].Values["value"]).To(Equal(value.String())) 75 | Expect(logs[0].ID).To(Equal(int64(232))) 76 | Expect(logs[1].Values["to"]).To(Equal(sender2.String())) 77 | Expect(logs[1].Values["from"]).To(Equal(sender1.String())) 78 | Expect(logs[1].Values["value"]).To(Equal(value.String())) 79 | Expect(logs[1].ID).To(Equal(int64(232))) 80 | }) 81 | 82 | It("Keeps track of addresses it sees if they will be used for method polling", func() { 83 | con = test_helpers.SetupTusdContract(tusdWantedEvents, []string{"balanceOf"}) 84 | event, ok := con.Events["Transfer"] 85 | Expect(ok).To(Equal(true)) 86 | 87 | c := converter.Converter{} 88 | c.Update(con) 89 | _, err := c.Convert([]types.Log{mocks.MockTransferLog1, mocks.MockTransferLog2}, event, 232) 90 | Expect(err).ToNot(HaveOccurred()) 91 | 92 | b, ok := con.EmittedAddrs[common.HexToAddress("0x000000000000000000000000000000000000Af21")] 93 | Expect(ok).To(Equal(true)) 94 | Expect(b).To(Equal(true)) 95 | 96 | b, ok = con.EmittedAddrs[common.HexToAddress("0x09BbBBE21a5975cAc061D82f7b843bCE061BA391")] 97 | Expect(ok).To(Equal(true)) 98 | Expect(b).To(Equal(true)) 99 | 100 | _, ok = con.EmittedAddrs[common.HexToAddress("0x")] 101 | Expect(ok).To(Equal(false)) 102 | 103 | _, ok = con.EmittedAddrs[""] 104 | Expect(ok).To(Equal(false)) 105 | 106 | _, ok = con.EmittedAddrs[common.HexToAddress("0x09THISE21a5IS5cFAKE1D82fAND43bCE06MADEUP")] 107 | Expect(ok).To(Equal(false)) 108 | 109 | _, ok = con.EmittedHashes[common.HexToHash("0x000000000000000000000000c02aaa39b223helloa0e5c4f27ead9083c752553")] 110 | Expect(ok).To(Equal(false)) 111 | }) 112 | 113 | It("Keeps track of hashes it sees if they will be used for method polling", func() { 114 | con = test_helpers.SetupENSContract(ensWantedEvents, []string{"owner"}) 115 | event, ok := con.Events["NewOwner"] 116 | Expect(ok).To(Equal(true)) 117 | 118 | c := converter.Converter{} 119 | c.Update(con) 120 | _, err := c.Convert([]types.Log{mocks.MockNewOwnerLog1, mocks.MockNewOwnerLog2}, event, 232) 121 | Expect(err).ToNot(HaveOccurred()) 122 | Expect(len(con.EmittedHashes)).To(Equal(3)) 123 | 124 | b, ok := con.EmittedHashes[common.HexToHash("0x000000000000000000000000c02aaa39b223helloa0e5c4f27ead9083c752553")] 125 | Expect(ok).To(Equal(true)) 126 | Expect(b).To(Equal(true)) 127 | 128 | b, ok = con.EmittedHashes[common.HexToHash("0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba391")] 129 | Expect(ok).To(Equal(true)) 130 | Expect(b).To(Equal(true)) 131 | 132 | b, ok = con.EmittedHashes[common.HexToHash("0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba400")] 133 | Expect(ok).To(Equal(true)) 134 | Expect(b).To(Equal(true)) 135 | 136 | _, ok = con.EmittedHashes[common.HexToHash("0x9dd48thiscc444isc242510c0made03upa5975cac061dhashb843bce061ba400")] 137 | Expect(ok).To(Equal(false)) 138 | 139 | _, ok = con.EmittedHashes[common.HexToAddress("0x")] 140 | Expect(ok).To(Equal(false)) 141 | 142 | _, ok = con.EmittedHashes[""] 143 | Expect(ok).To(Equal(false)) 144 | 145 | // Does not keep track of emitted addresses if the methods provided will not use them 146 | _, ok = con.EmittedAddrs[common.HexToAddress("0x000000000000000000000000000000000000Af21")] 147 | Expect(ok).To(Equal(false)) 148 | }) 149 | 150 | It("correctly parses bytes32", func() { 151 | con = test_helpers.SetupMarketPlaceContract(marketPlaceWantedEvents, []string{}) 152 | event, ok := con.Events["OrderCreated"] 153 | Expect(ok).To(BeTrue()) 154 | 155 | c := converter.Converter{} 156 | c.Update(con) 157 | result, err := c.Convert([]types.Log{mocks.MockOrderCreatedLog}, event, 232) 158 | Expect(err).NotTo(HaveOccurred()) 159 | 160 | Expect(len(result)).To(Equal(1)) 161 | Expect(result[0].Values["id"]).To(Equal("0x633f94affdcabe07c000231f85c752c97b9cc43966b432ec4d18641e6d178233")) 162 | }) 163 | 164 | It("correctly parses uint8", func() { 165 | con = test_helpers.SetupMolochContract(molochWantedEvents, []string{}) 166 | event, ok := con.Events["SubmitVote"] 167 | Expect(ok).To(BeTrue()) 168 | 169 | c := converter.Converter{} 170 | c.Update(con) 171 | result, err := c.Convert([]types.Log{mocks.MockSubmitVoteLog}, event, 232) 172 | Expect(err).NotTo(HaveOccurred()) 173 | 174 | Expect(len(result)).To(Equal(1)) 175 | Expect(result[0].Values["uintVote"]).To(Equal("1")) 176 | }) 177 | 178 | It("Fails with an empty contract", func() { 179 | event := con.Events["Transfer"] 180 | c := converter.Converter{} 181 | c.Update(&contract.Contract{}) 182 | _, err = c.Convert([]types.Log{mocks.MockTransferLog1}, event, 232) 183 | Expect(err).To(HaveOccurred()) 184 | }) 185 | }) 186 | }) 187 | -------------------------------------------------------------------------------- /db/schema.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | -- Dumped from database version 12.1 6 | -- Dumped by pg_dump version 12.1 7 | 8 | SET statement_timeout = 0; 9 | SET lock_timeout = 0; 10 | SET idle_in_transaction_session_timeout = 0; 11 | SET client_encoding = 'UTF8'; 12 | SET standard_conforming_strings = on; 13 | SELECT pg_catalog.set_config('search_path', '', false); 14 | SET check_function_bodies = false; 15 | SET xmloption = content; 16 | SET client_min_messages = warning; 17 | SET row_security = off; 18 | 19 | SET default_tablespace = ''; 20 | 21 | SET default_table_access_method = heap; 22 | 23 | -- 24 | -- Name: checked_headers; Type: TABLE; Schema: public; Owner: - 25 | -- 26 | 27 | CREATE TABLE public.checked_headers ( 28 | id integer NOT NULL, 29 | header_id integer NOT NULL 30 | ); 31 | 32 | 33 | -- 34 | -- Name: checked_headers_id_seq; Type: SEQUENCE; Schema: public; Owner: - 35 | -- 36 | 37 | CREATE SEQUENCE public.checked_headers_id_seq 38 | AS integer 39 | START WITH 1 40 | INCREMENT BY 1 41 | NO MINVALUE 42 | NO MAXVALUE 43 | CACHE 1; 44 | 45 | 46 | -- 47 | -- Name: checked_headers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 48 | -- 49 | 50 | ALTER SEQUENCE public.checked_headers_id_seq OWNED BY public.checked_headers.id; 51 | 52 | 53 | -- 54 | -- Name: goose_db_version; Type: TABLE; Schema: public; Owner: - 55 | -- 56 | 57 | CREATE TABLE public.goose_db_version ( 58 | id integer NOT NULL, 59 | version_id bigint NOT NULL, 60 | is_applied boolean NOT NULL, 61 | tstamp timestamp without time zone DEFAULT now() 62 | ); 63 | 64 | 65 | -- 66 | -- Name: goose_db_version_id_seq; Type: SEQUENCE; Schema: public; Owner: - 67 | -- 68 | 69 | CREATE SEQUENCE public.goose_db_version_id_seq 70 | AS integer 71 | START WITH 1 72 | INCREMENT BY 1 73 | NO MINVALUE 74 | NO MAXVALUE 75 | CACHE 1; 76 | 77 | 78 | -- 79 | -- Name: goose_db_version_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 80 | -- 81 | 82 | ALTER SEQUENCE public.goose_db_version_id_seq OWNED BY public.goose_db_version.id; 83 | 84 | 85 | -- 86 | -- Name: headers; Type: TABLE; Schema: public; Owner: - 87 | -- 88 | 89 | CREATE TABLE public.headers ( 90 | id integer NOT NULL, 91 | hash character varying(66), 92 | block_number bigint, 93 | raw jsonb, 94 | block_timestamp numeric, 95 | check_count integer DEFAULT 0 NOT NULL, 96 | node_id integer NOT NULL, 97 | eth_node_fingerprint character varying(128) 98 | ); 99 | 100 | 101 | -- 102 | -- Name: COLUMN headers.node_id; Type: COMMENT; Schema: public; Owner: - 103 | -- 104 | 105 | COMMENT ON COLUMN public.headers.node_id IS '@name HeaderNodeID'; 106 | 107 | 108 | -- 109 | -- Name: headers_id_seq; Type: SEQUENCE; Schema: public; Owner: - 110 | -- 111 | 112 | CREATE SEQUENCE public.headers_id_seq 113 | AS integer 114 | START WITH 1 115 | INCREMENT BY 1 116 | NO MINVALUE 117 | NO MAXVALUE 118 | CACHE 1; 119 | 120 | 121 | -- 122 | -- Name: headers_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 123 | -- 124 | 125 | ALTER SEQUENCE public.headers_id_seq OWNED BY public.headers.id; 126 | 127 | 128 | -- 129 | -- Name: nodes; Type: TABLE; Schema: public; Owner: - 130 | -- 131 | 132 | CREATE TABLE public.nodes ( 133 | id integer NOT NULL, 134 | client_name character varying, 135 | genesis_block character varying(66), 136 | network_id character varying, 137 | node_id character varying(128), 138 | chain_id integer 139 | ); 140 | 141 | 142 | -- 143 | -- Name: TABLE nodes; Type: COMMENT; Schema: public; Owner: - 144 | -- 145 | 146 | COMMENT ON TABLE public.nodes IS '@name NodeInfo'; 147 | 148 | 149 | -- 150 | -- Name: COLUMN nodes.node_id; Type: COMMENT; Schema: public; Owner: - 151 | -- 152 | 153 | COMMENT ON COLUMN public.nodes.node_id IS '@name ChainNodeID'; 154 | 155 | 156 | -- 157 | -- Name: nodes_id_seq; Type: SEQUENCE; Schema: public; Owner: - 158 | -- 159 | 160 | CREATE SEQUENCE public.nodes_id_seq 161 | AS integer 162 | START WITH 1 163 | INCREMENT BY 1 164 | NO MINVALUE 165 | NO MAXVALUE 166 | CACHE 1; 167 | 168 | 169 | -- 170 | -- Name: nodes_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: - 171 | -- 172 | 173 | ALTER SEQUENCE public.nodes_id_seq OWNED BY public.nodes.id; 174 | 175 | 176 | -- 177 | -- Name: checked_headers id; Type: DEFAULT; Schema: public; Owner: - 178 | -- 179 | 180 | ALTER TABLE ONLY public.checked_headers ALTER COLUMN id SET DEFAULT nextval('public.checked_headers_id_seq'::regclass); 181 | 182 | 183 | -- 184 | -- Name: goose_db_version id; Type: DEFAULT; Schema: public; Owner: - 185 | -- 186 | 187 | ALTER TABLE ONLY public.goose_db_version ALTER COLUMN id SET DEFAULT nextval('public.goose_db_version_id_seq'::regclass); 188 | 189 | 190 | -- 191 | -- Name: headers id; Type: DEFAULT; Schema: public; Owner: - 192 | -- 193 | 194 | ALTER TABLE ONLY public.headers ALTER COLUMN id SET DEFAULT nextval('public.headers_id_seq'::regclass); 195 | 196 | 197 | -- 198 | -- Name: nodes id; Type: DEFAULT; Schema: public; Owner: - 199 | -- 200 | 201 | ALTER TABLE ONLY public.nodes ALTER COLUMN id SET DEFAULT nextval('public.nodes_id_seq'::regclass); 202 | 203 | 204 | -- 205 | -- Name: checked_headers checked_headers_header_id_key; Type: CONSTRAINT; Schema: public; Owner: - 206 | -- 207 | 208 | ALTER TABLE ONLY public.checked_headers 209 | ADD CONSTRAINT checked_headers_header_id_key UNIQUE (header_id); 210 | 211 | 212 | -- 213 | -- Name: checked_headers checked_headers_pkey; Type: CONSTRAINT; Schema: public; Owner: - 214 | -- 215 | 216 | ALTER TABLE ONLY public.checked_headers 217 | ADD CONSTRAINT checked_headers_pkey PRIMARY KEY (id); 218 | 219 | 220 | -- 221 | -- Name: goose_db_version goose_db_version_pkey; Type: CONSTRAINT; Schema: public; Owner: - 222 | -- 223 | 224 | ALTER TABLE ONLY public.goose_db_version 225 | ADD CONSTRAINT goose_db_version_pkey PRIMARY KEY (id); 226 | 227 | 228 | -- 229 | -- Name: headers headers_block_number_hash_eth_node_fingerprint_key; Type: CONSTRAINT; Schema: public; Owner: - 230 | -- 231 | 232 | ALTER TABLE ONLY public.headers 233 | ADD CONSTRAINT headers_block_number_hash_eth_node_fingerprint_key UNIQUE (block_number, hash, eth_node_fingerprint); 234 | 235 | 236 | -- 237 | -- Name: headers headers_pkey; Type: CONSTRAINT; Schema: public; Owner: - 238 | -- 239 | 240 | ALTER TABLE ONLY public.headers 241 | ADD CONSTRAINT headers_pkey PRIMARY KEY (id); 242 | 243 | 244 | -- 245 | -- Name: nodes node_uc; Type: CONSTRAINT; Schema: public; Owner: - 246 | -- 247 | 248 | ALTER TABLE ONLY public.nodes 249 | ADD CONSTRAINT node_uc UNIQUE (genesis_block, network_id, node_id, chain_id); 250 | 251 | 252 | -- 253 | -- Name: nodes nodes_pkey; Type: CONSTRAINT; Schema: public; Owner: - 254 | -- 255 | 256 | ALTER TABLE ONLY public.nodes 257 | ADD CONSTRAINT nodes_pkey PRIMARY KEY (id); 258 | 259 | 260 | -- 261 | -- Name: headers_block_number; Type: INDEX; Schema: public; Owner: - 262 | -- 263 | 264 | CREATE INDEX headers_block_number ON public.headers USING btree (block_number); 265 | 266 | 267 | -- 268 | -- Name: headers_block_timestamp; Type: INDEX; Schema: public; Owner: - 269 | -- 270 | 271 | CREATE INDEX headers_block_timestamp ON public.headers USING btree (block_timestamp); 272 | 273 | 274 | -- 275 | -- Name: checked_headers checked_headers_header_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 276 | -- 277 | 278 | ALTER TABLE ONLY public.checked_headers 279 | ADD CONSTRAINT checked_headers_header_id_fkey FOREIGN KEY (header_id) REFERENCES public.headers(id) ON DELETE CASCADE; 280 | 281 | 282 | -- 283 | -- Name: headers headers_node_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: - 284 | -- 285 | 286 | ALTER TABLE ONLY public.headers 287 | ADD CONSTRAINT headers_node_id_fkey FOREIGN KEY (node_id) REFERENCES public.nodes(id) ON DELETE CASCADE; 288 | 289 | 290 | -- 291 | -- PostgreSQL database dump complete 292 | -- 293 | 294 | -------------------------------------------------------------------------------- /pkg/contract/contract_test.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package contract_test 18 | 19 | import ( 20 | . "github.com/onsi/ginkgo" 21 | . "github.com/onsi/gomega" 22 | 23 | "github.com/vulcanize/eth-contract-watcher/pkg/contract" 24 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers" 25 | "github.com/vulcanize/eth-contract-watcher/pkg/helpers/test_helpers/mocks" 26 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 27 | ) 28 | 29 | var _ = Describe("Contract", func() { 30 | var err error 31 | var info *contract.Contract 32 | var wantedEvents = []string{"Transfer", "Approval"} 33 | 34 | Describe("GenerateFilters", func() { 35 | 36 | It("Generates filters from contract data", func() { 37 | info = test_helpers.SetupTusdContract(wantedEvents, nil) 38 | err = info.GenerateFilters() 39 | Expect(err).ToNot(HaveOccurred()) 40 | 41 | val, ok := info.Filters["Transfer"] 42 | Expect(ok).To(Equal(true)) 43 | Expect(val).To(Equal(mocks.ExpectedTransferFilter)) 44 | 45 | val, ok = info.Filters["Approval"] 46 | Expect(ok).To(Equal(true)) 47 | Expect(val).To(Equal(mocks.ExpectedApprovalFilter)) 48 | 49 | val, ok = info.Filters["Mint"] 50 | Expect(ok).To(Equal(false)) 51 | 52 | }) 53 | 54 | It("Fails with an empty contract", func() { 55 | info = &contract.Contract{} 56 | err = info.GenerateFilters() 57 | Expect(err).To(HaveOccurred()) 58 | }) 59 | }) 60 | 61 | Describe("IsEventAddr", func() { 62 | 63 | BeforeEach(func() { 64 | info = &contract.Contract{} 65 | info.MethodArgs = map[string]bool{} 66 | info.FilterArgs = map[string]bool{} 67 | }) 68 | 69 | It("Returns true if address is in event address filter list", func() { 70 | info.FilterArgs["testAddress1"] = true 71 | info.FilterArgs["testAddress2"] = true 72 | 73 | is := info.WantedEventArg("testAddress1") 74 | Expect(is).To(Equal(true)) 75 | is = info.WantedEventArg("testAddress2") 76 | Expect(is).To(Equal(true)) 77 | 78 | info.MethodArgs["testAddress3"] = true 79 | is = info.WantedEventArg("testAddress3") 80 | Expect(is).To(Equal(false)) 81 | }) 82 | 83 | It("Returns true if event address filter is empty (no filter)", func() { 84 | is := info.WantedEventArg("testAddress1") 85 | Expect(is).To(Equal(true)) 86 | is = info.WantedEventArg("testAddress2") 87 | Expect(is).To(Equal(true)) 88 | }) 89 | 90 | It("Returns false if address is not in event address filter list", func() { 91 | info.FilterArgs["testAddress1"] = true 92 | info.FilterArgs["testAddress2"] = true 93 | 94 | is := info.WantedEventArg("testAddress3") 95 | Expect(is).To(Equal(false)) 96 | }) 97 | 98 | It("Returns false if event address filter is nil (block all)", func() { 99 | info.FilterArgs = nil 100 | 101 | is := info.WantedEventArg("testAddress1") 102 | Expect(is).To(Equal(false)) 103 | is = info.WantedEventArg("testAddress2") 104 | Expect(is).To(Equal(false)) 105 | }) 106 | }) 107 | 108 | Describe("IsMethodAddr", func() { 109 | BeforeEach(func() { 110 | info = &contract.Contract{} 111 | info.MethodArgs = map[string]bool{} 112 | info.FilterArgs = map[string]bool{} 113 | }) 114 | 115 | It("Returns true if address is in method address filter list", func() { 116 | info.MethodArgs["testAddress1"] = true 117 | info.MethodArgs["testAddress2"] = true 118 | 119 | is := info.WantedMethodArg("testAddress1") 120 | Expect(is).To(Equal(true)) 121 | is = info.WantedMethodArg("testAddress2") 122 | Expect(is).To(Equal(true)) 123 | 124 | info.FilterArgs["testAddress3"] = true 125 | is = info.WantedMethodArg("testAddress3") 126 | Expect(is).To(Equal(false)) 127 | }) 128 | 129 | It("Returns true if method address filter list is empty (no filter)", func() { 130 | is := info.WantedMethodArg("testAddress1") 131 | Expect(is).To(Equal(true)) 132 | is = info.WantedMethodArg("testAddress2") 133 | Expect(is).To(Equal(true)) 134 | }) 135 | 136 | It("Returns false if address is not in method address filter list", func() { 137 | info.MethodArgs["testAddress1"] = true 138 | info.MethodArgs["testAddress2"] = true 139 | 140 | is := info.WantedMethodArg("testAddress3") 141 | Expect(is).To(Equal(false)) 142 | }) 143 | 144 | It("Returns false if method address filter list is nil (block all)", func() { 145 | info.MethodArgs = nil 146 | 147 | is := info.WantedMethodArg("testAddress1") 148 | Expect(is).To(Equal(false)) 149 | is = info.WantedMethodArg("testAddress2") 150 | Expect(is).To(Equal(false)) 151 | }) 152 | }) 153 | 154 | Describe("PassesEventFilter", func() { 155 | var mapping map[string]string 156 | BeforeEach(func() { 157 | info = &contract.Contract{} 158 | info.FilterArgs = map[string]bool{} 159 | mapping = map[string]string{} 160 | 161 | }) 162 | 163 | It("Return true if event log name-value mapping has filtered for address as a value", func() { 164 | info.FilterArgs["testAddress1"] = true 165 | info.FilterArgs["testAddress2"] = true 166 | 167 | mapping["testInputName1"] = "testAddress1" 168 | mapping["testInputName2"] = "testAddress2" 169 | mapping["testInputName3"] = "testAddress3" 170 | 171 | pass := info.PassesEventFilter(mapping) 172 | Expect(pass).To(Equal(true)) 173 | }) 174 | 175 | It("Return true if event address filter list is empty (no filter)", func() { 176 | mapping["testInputName1"] = "testAddress1" 177 | mapping["testInputName2"] = "testAddress2" 178 | mapping["testInputName3"] = "testAddress3" 179 | 180 | pass := info.PassesEventFilter(mapping) 181 | Expect(pass).To(Equal(true)) 182 | }) 183 | 184 | It("Return false if event log name-value mapping does not have filtered for address as a value", func() { 185 | info.FilterArgs["testAddress1"] = true 186 | info.FilterArgs["testAddress2"] = true 187 | 188 | mapping["testInputName3"] = "testAddress3" 189 | 190 | pass := info.PassesEventFilter(mapping) 191 | Expect(pass).To(Equal(false)) 192 | }) 193 | 194 | It("Return false if event address filter list is nil (block all)", func() { 195 | info.FilterArgs = nil 196 | 197 | mapping["testInputName1"] = "testAddress1" 198 | mapping["testInputName2"] = "testAddress2" 199 | mapping["testInputName3"] = "testAddress3" 200 | 201 | pass := info.PassesEventFilter(mapping) 202 | Expect(pass).To(Equal(false)) 203 | }) 204 | }) 205 | 206 | Describe("AddEmittedAddr", func() { 207 | BeforeEach(func() { 208 | info = &contract.Contract{} 209 | info.FilterArgs = map[string]bool{} 210 | info.MethodArgs = map[string]bool{} 211 | info.Methods = []types.Method{} 212 | info.EmittedAddrs = map[interface{}]bool{} 213 | }) 214 | 215 | It("Adds address to list if it is on the method filter address list", func() { 216 | info.MethodArgs["testAddress2"] = true 217 | info.AddEmittedAddr("testAddress2") 218 | b := info.EmittedAddrs["testAddress2"] 219 | Expect(b).To(Equal(true)) 220 | }) 221 | 222 | It("Adds address to list if method filter is empty", func() { 223 | info.AddEmittedAddr("testAddress2") 224 | b := info.EmittedAddrs["testAddress2"] 225 | Expect(b).To(Equal(true)) 226 | }) 227 | 228 | It("Does not add address to list if both filters are closed (nil)", func() { 229 | info.FilterArgs = nil // close both 230 | info.MethodArgs = nil 231 | info.AddEmittedAddr("testAddress1") 232 | b := info.EmittedAddrs["testAddress1"] 233 | Expect(b).To(Equal(false)) 234 | }) 235 | }) 236 | }) 237 | -------------------------------------------------------------------------------- /pkg/helpers/test_helpers/mocks/entities.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package mocks 18 | 19 | import ( 20 | "encoding/json" 21 | 22 | "github.com/ethereum/go-ethereum/common" 23 | "github.com/ethereum/go-ethereum/common/hexutil" 24 | "github.com/ethereum/go-ethereum/core/types" 25 | 26 | c2 "github.com/vulcanize/eth-header-sync/pkg/core" 27 | 28 | "github.com/vulcanize/eth-contract-watcher/pkg/config" 29 | "github.com/vulcanize/eth-contract-watcher/pkg/constants" 30 | "github.com/vulcanize/eth-contract-watcher/pkg/core" 31 | "github.com/vulcanize/eth-contract-watcher/pkg/filters" 32 | ) 33 | 34 | var ExpectedTransferFilter = filters.LogFilter{ 35 | Name: constants.TusdContractAddress + "_" + "Transfer", 36 | Address: constants.TusdContractAddress, 37 | ToBlock: -1, 38 | FromBlock: 6194634, 39 | Topics: core.Topics{constants.TransferEvent.Signature()}, 40 | } 41 | 42 | var ExpectedApprovalFilter = filters.LogFilter{ 43 | Name: constants.TusdContractAddress + "_" + "Approval", 44 | Address: constants.TusdContractAddress, 45 | ToBlock: -1, 46 | FromBlock: 6194634, 47 | Topics: core.Topics{constants.ApprovalEvent.Signature()}, 48 | } 49 | 50 | var rawFakeHeader, _ = json.Marshal(c2.Header{}) 51 | 52 | var MockHeader1 = c2.Header{ 53 | Hash: "0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad123ert", 54 | BlockNumber: 6194632, 55 | Raw: rawFakeHeader, 56 | Timestamp: "50000000", 57 | } 58 | 59 | var MockHeader2 = c2.Header{ 60 | Hash: "0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad456yui", 61 | BlockNumber: 6194633, 62 | Raw: rawFakeHeader, 63 | Timestamp: "50000015", 64 | } 65 | 66 | var MockHeader3 = c2.Header{ 67 | Hash: "0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad234hfs", 68 | BlockNumber: 6194634, 69 | Raw: rawFakeHeader, 70 | Timestamp: "50000030", 71 | } 72 | 73 | var MockHeader4 = c2.Header{ 74 | Hash: "0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad234hfs", 75 | BlockNumber: 6194635, 76 | Raw: rawFakeHeader, 77 | Timestamp: "50000030", 78 | } 79 | 80 | var MockTransferLog1 = types.Log{ 81 | Index: 1, 82 | Address: common.HexToAddress(constants.TusdContractAddress), 83 | BlockNumber: 5488076, 84 | TxIndex: 110, 85 | TxHash: common.HexToHash("0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad6546ae"), 86 | Topics: []common.Hash{ 87 | common.HexToHash(constants.TransferEvent.Signature()), 88 | common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000af21"), 89 | common.HexToHash("0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba391"), 90 | }, 91 | Data: hexutil.MustDecode("0x000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359000000000000000000000000000000000000000000000000392d2e2bda9c00000000000000000000000000000000000000000000000000927f41fa0a4a418000000000000000000000000000000000000000000000000000000000005adcfebe"), 92 | } 93 | 94 | var MockTransferLog2 = types.Log{ 95 | Index: 3, 96 | Address: common.HexToAddress(constants.TusdContractAddress), 97 | BlockNumber: 5488077, 98 | TxIndex: 2, 99 | TxHash: common.HexToHash("0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad6546df"), 100 | Topics: []common.Hash{ 101 | common.HexToHash(constants.TransferEvent.Signature()), 102 | common.HexToHash("0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba391"), 103 | common.HexToHash("0x000000000000000000000000000000000000000000000000000000000000af21"), 104 | }, 105 | Data: hexutil.MustDecode("0x000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc200000000000000000000000089d24a6b4ccb1b6faa2625fe562bdd9a23260359000000000000000000000000000000000000000000000000392d2e2bda9c00000000000000000000000000000000000000000000000000927f41fa0a4a418000000000000000000000000000000000000000000000000000000000005adcfebe"), 106 | } 107 | 108 | var MockNewOwnerLog1 = types.Log{ 109 | Index: 1, 110 | Address: common.HexToAddress(constants.EnsContractAddress), 111 | BlockNumber: 5488076, 112 | TxIndex: 110, 113 | TxHash: common.HexToHash("0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad6546ae"), 114 | Topics: []common.Hash{ 115 | common.HexToHash(constants.NewOwnerEvent.Signature()), 116 | common.HexToHash("0x000000000000000000000000c02aaa39b223helloa0e5c4f27ead9083c752553"), 117 | common.HexToHash("0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba391"), 118 | }, 119 | Data: hexutil.MustDecode("0x000000000000000000000000000000000000000000000000000000000000af21"), 120 | } 121 | 122 | var MockNewOwnerLog2 = types.Log{ 123 | Index: 3, 124 | Address: common.HexToAddress(constants.EnsContractAddress), 125 | BlockNumber: 5488077, 126 | TxIndex: 2, 127 | TxHash: common.HexToHash("0x135391a0962a63944e5908e6fedfff90fb4be3e3290a21017861099bad6546df"), 128 | Topics: []common.Hash{ 129 | common.HexToHash(constants.NewOwnerEvent.Signature()), 130 | common.HexToHash("0x000000000000000000000000c02aaa39b223helloa0e5c4f27ead9083c752553"), 131 | common.HexToHash("0x9dd48110dcc444fdc242510c09bbbbe21a5975cac061d82f7b843bce061ba400"), 132 | }, 133 | Data: hexutil.MustDecode("0x000000000000000000000000000000000000000000000000000000000000af21"), 134 | } 135 | 136 | var MockOrderCreatedLog = types.Log{ 137 | Address: common.HexToAddress(constants.MarketPlaceContractAddress), 138 | Topics: []common.Hash{ 139 | common.HexToHash("0x84c66c3f7ba4b390e20e8e8233e2a516f3ce34a72749e4f12bd010dfba238039"), 140 | common.HexToHash("0xffffffffffffffffffffffffffffff72ffffffffffffffffffffffffffffffd0"), 141 | common.HexToHash("0x00000000000000000000000083b7b6f360a9895d163ea797d9b939b9173b292a"), 142 | }, 143 | Data: hexutil.MustDecode("0x633f94affdcabe07c000231f85c752c97b9cc43966b432ec4d18641e6d178233000000000000000000000000f87e31492faf9a91b02ee0deaad50d51d56d5d4d0000000000000000000000000000000000000000000003da9fbcf4446d6000000000000000000000000000000000000000000000000000000000016db2524880"), 144 | BlockNumber: 8587618, 145 | TxHash: common.HexToHash("0x7ad9e2f88416738f3c7ad0a6d260f71794532206a0e838299f5014b4fe81e66e"), 146 | TxIndex: 93, 147 | BlockHash: common.HexToHash("0x06a1762b7f2e070793fc24cd785de0fa485e728832c4f3469790153ae51a56a2"), 148 | Index: 59, 149 | Removed: false, 150 | } 151 | 152 | var MockSubmitVoteLog = types.Log{ 153 | Address: common.HexToAddress(constants.MolochContractAddress), 154 | Topics: []common.Hash{ 155 | common.HexToHash("0x29bf0061f2faa9daa482f061b116195432d435536d8af4ae6b3c5dd78223679b"), 156 | common.HexToHash("0x0000000000000000000000000000000000000000000000000000000000000061"), 157 | common.HexToHash("0x0000000000000000000000006ddf1b8e6d71b5b33912607098be123ffe62ae53"), 158 | common.HexToHash("0x00000000000000000000000037385081870ef47e055410fefd582e2a95d2960b"), 159 | }, 160 | Data: hexutil.MustDecode("0x0000000000000000000000000000000000000000000000000000000000000001"), 161 | BlockNumber: 8517621, 162 | TxHash: common.HexToHash("0xcc7390a2099812d0dfc9baef201afbc7a44bfae145050c9dc700b77cbd3cd752"), 163 | TxIndex: 103, 164 | BlockHash: common.HexToHash("0x3e82681d8036b1225fcaa8bcd4cdbe757b39f13468286b303cde22146385525e"), 165 | Index: 132, 166 | Removed: false, 167 | } 168 | 169 | var MockConfig = config.ContractConfig{ 170 | Network: "", 171 | Addresses: map[string]bool{ 172 | "0x1234567890abcdef": true, 173 | }, 174 | Abis: map[string]string{ 175 | "0x1234567890abcdef": "fake_abi", 176 | }, 177 | Events: map[string][]string{ 178 | "0x1234567890abcdef": {"Transfer"}, 179 | }, 180 | Methods: map[string][]string{ 181 | "0x1234567890abcdef": nil, 182 | }, 183 | MethodArgs: map[string][]string{ 184 | "0x1234567890abcdef": nil, 185 | }, 186 | EventArgs: map[string][]string{ 187 | "0x1234567890abcdef": nil, 188 | }, 189 | } 190 | -------------------------------------------------------------------------------- /pkg/converter/log_converter.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package converter 18 | 19 | import ( 20 | "encoding/json" 21 | "fmt" 22 | "math/big" 23 | "strconv" 24 | 25 | "github.com/ethereum/go-ethereum/accounts/abi/bind" 26 | "github.com/ethereum/go-ethereum/common" 27 | "github.com/ethereum/go-ethereum/common/hexutil" 28 | gethTypes "github.com/ethereum/go-ethereum/core/types" 29 | 30 | "github.com/vulcanize/eth-contract-watcher/pkg/contract" 31 | "github.com/vulcanize/eth-contract-watcher/pkg/types" 32 | ) 33 | 34 | // LogConverter is the interface for converting geth logs to our custom log type 35 | type LogConverter interface { 36 | Convert(logs []gethTypes.Log, event types.Event, headerID int64) ([]types.Log, error) 37 | ConvertBatch(logs []gethTypes.Log, events map[string]types.Event, headerID int64) (map[string][]types.Log, error) 38 | Update(info *contract.Contract) 39 | } 40 | 41 | // Converter is the underlying struct for the ConverterInterface 42 | type Converter struct { 43 | ContractInfo *contract.Contract 44 | } 45 | 46 | // Update is used to configure the converter with a specific contract 47 | func (c *Converter) Update(info *contract.Contract) { 48 | c.ContractInfo = info 49 | } 50 | 51 | // Convert the given watched event log into a types.Log for the given event 52 | func (c *Converter) Convert(logs []gethTypes.Log, event types.Event, headerID int64) ([]types.Log, error) { 53 | boundContract := bind.NewBoundContract(common.HexToAddress(c.ContractInfo.Address), c.ContractInfo.ParsedAbi, nil, nil, nil) 54 | returnLogs := make([]types.Log, 0, len(logs)) 55 | for _, log := range logs { 56 | values := make(map[string]interface{}) 57 | for _, field := range event.Fields { 58 | var i interface{} 59 | values[field.Name] = i 60 | } 61 | 62 | err := boundContract.UnpackLogIntoMap(values, event.Name, log) 63 | if err != nil { 64 | return nil, err 65 | } 66 | 67 | strValues := make(map[string]string, len(values)) 68 | seenAddrs := make([]interface{}, 0, len(values)) 69 | seenHashes := make([]interface{}, 0, len(values)) 70 | for fieldName, input := range values { 71 | // Postgres cannot handle custom types, resolve everything to strings 72 | switch input.(type) { 73 | case *big.Int: 74 | b := input.(*big.Int) 75 | strValues[fieldName] = b.String() 76 | case common.Address: 77 | a := input.(common.Address) 78 | strValues[fieldName] = a.String() 79 | seenAddrs = append(seenAddrs, a) 80 | case common.Hash: 81 | h := input.(common.Hash) 82 | strValues[fieldName] = h.String() 83 | seenHashes = append(seenHashes, h) 84 | case string: 85 | strValues[fieldName] = input.(string) 86 | case bool: 87 | strValues[fieldName] = strconv.FormatBool(input.(bool)) 88 | case []byte: 89 | b := input.([]byte) 90 | strValues[fieldName] = hexutil.Encode(b) 91 | if len(b) == 32 { 92 | seenHashes = append(seenHashes, common.HexToHash(strValues[fieldName])) 93 | } 94 | case uint8: 95 | u := input.(uint8) 96 | strValues[fieldName] = strconv.Itoa(int(u)) 97 | case [32]uint8: 98 | raw := input.([32]uint8) 99 | converted := convertUintSliceToHash(raw) 100 | strValues[fieldName] = converted.String() 101 | seenHashes = append(seenHashes, converted) 102 | default: 103 | return nil, fmt.Errorf("error: unhandled abi type %T", input) 104 | } 105 | } 106 | 107 | // Only hold onto logs that pass our address filter, if any 108 | if c.ContractInfo.PassesEventFilter(strValues) { 109 | raw, err := json.Marshal(log) 110 | if err != nil { 111 | return nil, err 112 | } 113 | 114 | returnLogs = append(returnLogs, types.Log{ 115 | LogIndex: log.Index, 116 | Values: strValues, 117 | Raw: raw, 118 | TransactionIndex: log.TxIndex, 119 | ID: headerID, 120 | }) 121 | 122 | // Cache emitted values if their caching is turned on 123 | if c.ContractInfo.EmittedAddrs != nil { 124 | c.ContractInfo.AddEmittedAddr(seenAddrs...) 125 | } 126 | if c.ContractInfo.EmittedHashes != nil { 127 | c.ContractInfo.AddEmittedHash(seenHashes...) 128 | } 129 | } 130 | } 131 | 132 | return returnLogs, nil 133 | } 134 | 135 | // ConvertBatch converts the given watched event logs into types.Logs; returns a map of event names to a slice of their converted logs 136 | func (c *Converter) ConvertBatch(logs []gethTypes.Log, events map[string]types.Event, headerID int64) (map[string][]types.Log, error) { 137 | boundContract := bind.NewBoundContract(common.HexToAddress(c.ContractInfo.Address), c.ContractInfo.ParsedAbi, nil, nil, nil) 138 | eventsToLogs := make(map[string][]types.Log) 139 | for _, event := range events { 140 | eventsToLogs[event.Name] = make([]types.Log, 0, len(logs)) 141 | // Iterate through all event logs 142 | for _, log := range logs { 143 | // If the log is of this event type, process it as such 144 | if event.Sig() == log.Topics[0] { 145 | values := make(map[string]interface{}) 146 | err := boundContract.UnpackLogIntoMap(values, event.Name, log) 147 | if err != nil { 148 | return nil, err 149 | } 150 | // Postgres cannot handle custom types, so we will resolve everything to strings 151 | strValues := make(map[string]string, len(values)) 152 | // Keep track of addresses and hashes emitted from events 153 | seenAddrs := make([]interface{}, 0, len(values)) 154 | seenHashes := make([]interface{}, 0, len(values)) 155 | for fieldName, input := range values { 156 | switch input.(type) { 157 | case *big.Int: 158 | b := input.(*big.Int) 159 | strValues[fieldName] = b.String() 160 | case common.Address: 161 | a := input.(common.Address) 162 | strValues[fieldName] = a.String() 163 | seenAddrs = append(seenAddrs, a) 164 | case common.Hash: 165 | h := input.(common.Hash) 166 | strValues[fieldName] = h.String() 167 | seenHashes = append(seenHashes, h) 168 | case string: 169 | strValues[fieldName] = input.(string) 170 | case bool: 171 | strValues[fieldName] = strconv.FormatBool(input.(bool)) 172 | case []byte: 173 | b := input.([]byte) 174 | strValues[fieldName] = hexutil.Encode(b) 175 | if len(b) == 32 { // collect byte arrays of size 32 as hashes 176 | seenHashes = append(seenHashes, common.BytesToHash(b)) 177 | } 178 | case uint8: 179 | u := input.(uint8) 180 | strValues[fieldName] = strconv.Itoa(int(u)) 181 | case [32]uint8: 182 | raw := input.([32]uint8) 183 | converted := convertUintSliceToHash(raw) 184 | strValues[fieldName] = converted.String() 185 | seenHashes = append(seenHashes, converted) 186 | default: 187 | return nil, fmt.Errorf("error: unhandled abi type %T", input) 188 | } 189 | } 190 | 191 | // Only hold onto logs that pass our argument filter, if any 192 | if c.ContractInfo.PassesEventFilter(strValues) { 193 | raw, err := json.Marshal(log) 194 | if err != nil { 195 | return nil, err 196 | } 197 | 198 | eventsToLogs[event.Name] = append(eventsToLogs[event.Name], types.Log{ 199 | LogIndex: log.Index, 200 | Values: strValues, 201 | Raw: raw, 202 | TransactionIndex: log.TxIndex, 203 | ID: headerID, 204 | }) 205 | 206 | // Cache emitted values that pass the argument filter if their caching is turned on 207 | if c.ContractInfo.EmittedAddrs != nil { 208 | c.ContractInfo.AddEmittedAddr(seenAddrs...) 209 | } 210 | if c.ContractInfo.EmittedHashes != nil { 211 | c.ContractInfo.AddEmittedHash(seenHashes...) 212 | } 213 | } 214 | } 215 | } 216 | } 217 | 218 | return eventsToLogs, nil 219 | } 220 | 221 | func convertUintSliceToHash(raw [32]uint8) common.Hash { 222 | var asBytes []byte 223 | for _, u := range raw { 224 | asBytes = append(asBytes, u) 225 | } 226 | return common.BytesToHash(asBytes) 227 | } 228 | -------------------------------------------------------------------------------- /pkg/config/contract.go: -------------------------------------------------------------------------------- 1 | // VulcanizeDB 2 | // Copyright © 2019 Vulcanize 3 | 4 | // This program is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Affero General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | 9 | // This program is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Affero General Public License for more details. 13 | 14 | // You should have received a copy of the GNU Affero General Public License 15 | // along with this program. If not, see . 16 | 17 | package config 18 | 19 | import ( 20 | "strings" 21 | 22 | log "github.com/sirupsen/logrus" 23 | "github.com/spf13/viper" 24 | a "github.com/vulcanize/eth-contract-watcher/pkg/abi" 25 | ) 26 | 27 | // Config struct for generic contract transformer 28 | type ContractConfig struct { 29 | // Name for the transformer 30 | Name string 31 | 32 | // Ethereum network name; default "" is mainnet 33 | Network string 34 | 35 | // List of contract addresses (map to ensure no duplicates) 36 | Addresses map[string]bool 37 | 38 | // Map of contract address to abi 39 | // If an address has no associated abi the parser will attempt to fetch one from etherscan 40 | Abis map[string]string 41 | 42 | // Map of contract address to slice of events 43 | // Used to set which addresses to watch 44 | // If any events are listed in the slice only those will be watched 45 | // Otherwise all events in the contract ABI are watched 46 | Events map[string][]string 47 | 48 | // Map of contract address to slice of methods 49 | // If any methods are listed in the slice only those will be polled 50 | // Otherwise no methods will be polled 51 | Methods map[string][]string 52 | 53 | // Map of contract address to slice of event arguments to filter for 54 | // If arguments are provided then only events which emit those arguments are watched 55 | // Otherwise arguments are not filtered on events 56 | EventArgs map[string][]string 57 | 58 | // Map of contract address to slice of method arguments to limit polling to 59 | // If arguments are provided then only those arguments are allowed as arguments in method polling 60 | // Otherwise any argument of the right type seen emitted from events at that contract will be used in method polling 61 | MethodArgs map[string][]string 62 | 63 | // Map of contract address to their starting block 64 | StartingBlocks map[string]int64 65 | 66 | // Map of contract address to whether or not to pipe method polling results forward into subsequent method calls 67 | Piping map[string]bool 68 | } 69 | 70 | func (contractConfig *ContractConfig) PrepConfig() { 71 | addrs := viper.GetStringSlice("contract.addresses") 72 | contractConfig.Network = viper.GetString("contract.network") 73 | contractConfig.Addresses = make(map[string]bool, len(addrs)) 74 | contractConfig.Abis = make(map[string]string, len(addrs)) 75 | contractConfig.Methods = make(map[string][]string, len(addrs)) 76 | contractConfig.Events = make(map[string][]string, len(addrs)) 77 | contractConfig.MethodArgs = make(map[string][]string, len(addrs)) 78 | contractConfig.EventArgs = make(map[string][]string, len(addrs)) 79 | contractConfig.StartingBlocks = make(map[string]int64, len(addrs)) 80 | contractConfig.Piping = make(map[string]bool, len(addrs)) 81 | // De-dupe addresses 82 | for _, addr := range addrs { 83 | contractConfig.Addresses[strings.ToLower(addr)] = true 84 | } 85 | 86 | // Iterate over addresses to pull out config info for each contract 87 | for _, addr := range addrs { 88 | transformer := viper.GetStringMap("contract." + addr) 89 | 90 | // Get and check abi 91 | var abi string 92 | abiInterface, abiOK := transformer["abi"] 93 | if !abiOK { 94 | log.Warnf("contract %s not configured with an ABI, will attempt to fetch it from Etherscan\r\n", addr) 95 | } else { 96 | abi, abiOK = abiInterface.(string) 97 | if !abiOK { 98 | log.Fatal(addr, "transformer `abi` not of type []string") 99 | } 100 | } 101 | if abi != "" { 102 | if _, abiErr := a.ParseAbi(abi); abiErr != nil { 103 | log.Fatal(addr, "transformer `abi` not valid JSON") 104 | } 105 | } 106 | contractConfig.Abis[strings.ToLower(addr)] = abi 107 | 108 | // Get and check events 109 | events := make([]string, 0) 110 | eventsInterface, eventsOK := transformer["events"] 111 | if !eventsOK { 112 | log.Warnf("contract %s not configured with a list of events to watch, will watch all events\r\n", addr) 113 | events = []string{} 114 | } else { 115 | eventsI, eventsOK := eventsInterface.([]interface{}) 116 | if !eventsOK { 117 | log.Fatal(addr, "transformer `events` not of type []string\r\n") 118 | } 119 | for _, strI := range eventsI { 120 | str, strOK := strI.(string) 121 | if !strOK { 122 | log.Fatal(addr, "transformer `events` not of type []string\r\n") 123 | } 124 | events = append(events, str) 125 | } 126 | } 127 | contractConfig.Events[strings.ToLower(addr)] = events 128 | 129 | // Get and check methods 130 | methods := make([]string, 0) 131 | methodsInterface, methodsOK := transformer["methods"] 132 | if !methodsOK { 133 | log.Warnf("contract %s not configured with a list of methods to poll, will not poll any methods\r\n", addr) 134 | methods = []string{} 135 | } else { 136 | methodsI, methodsOK := methodsInterface.([]interface{}) 137 | if !methodsOK { 138 | log.Fatal(addr, "transformer `methods` not of type []string\r\n") 139 | } 140 | for _, strI := range methodsI { 141 | str, strOK := strI.(string) 142 | if !strOK { 143 | log.Fatal(addr, "transformer `methods` not of type []string\r\n") 144 | } 145 | methods = append(methods, str) 146 | } 147 | } 148 | contractConfig.Methods[strings.ToLower(addr)] = methods 149 | 150 | // Get and check eventArgs 151 | eventArgs := make([]string, 0) 152 | eventArgsInterface, eventArgsOK := transformer["eventArgs"] 153 | if !eventArgsOK { 154 | log.Warnf("contract %s not configured with a list of event arguments to filter for, will not filter events for specific emitted values\r\n", addr) 155 | eventArgs = []string{} 156 | } else { 157 | eventArgsI, eventArgsOK := eventArgsInterface.([]interface{}) 158 | if !eventArgsOK { 159 | log.Fatal(addr, "transformer `eventArgs` not of type []string\r\n") 160 | } 161 | for _, strI := range eventArgsI { 162 | str, strOK := strI.(string) 163 | if !strOK { 164 | log.Fatal(addr, "transformer `eventArgs` not of type []string\r\n") 165 | } 166 | eventArgs = append(eventArgs, str) 167 | } 168 | } 169 | contractConfig.EventArgs[strings.ToLower(addr)] = eventArgs 170 | 171 | // Get and check methodArgs 172 | methodArgs := make([]string, 0) 173 | methodArgsInterface, methodArgsOK := transformer["methodArgs"] 174 | if !methodArgsOK { 175 | log.Warnf("contract %s not configured with a list of method argument values to poll with, will poll methods with all available arguments\r\n", addr) 176 | methodArgs = []string{} 177 | } else { 178 | methodArgsI, methodArgsOK := methodArgsInterface.([]interface{}) 179 | if !methodArgsOK { 180 | log.Fatal(addr, "transformer `methodArgs` not of type []string\r\n") 181 | } 182 | for _, strI := range methodArgsI { 183 | str, strOK := strI.(string) 184 | if !strOK { 185 | log.Fatal(addr, "transformer `methodArgs` not of type []string\r\n") 186 | } 187 | methodArgs = append(methodArgs, str) 188 | } 189 | } 190 | contractConfig.MethodArgs[strings.ToLower(addr)] = methodArgs 191 | 192 | // Get and check startingBlock 193 | startInterface, startOK := transformer["startingblock"] 194 | if !startOK { 195 | log.Fatal(addr, "transformer config is missing `startingBlock` value\r\n") 196 | } 197 | start, startOK := startInterface.(int64) 198 | if !startOK { 199 | log.Fatal(addr, "transformer `startingBlock` not of type int\r\n") 200 | } 201 | contractConfig.StartingBlocks[strings.ToLower(addr)] = start 202 | 203 | // Get pipping 204 | var piping bool 205 | _, pipeOK := transformer["piping"] 206 | if !pipeOK { 207 | log.Warnf("contract %s does not have its `piping` set, by default piping is turned off\r\n", addr) 208 | piping = false 209 | } else { 210 | pipingInterface := transformer["piping"] 211 | piping, pipeOK = pipingInterface.(bool) 212 | if !pipeOK { 213 | log.Fatal(addr, "transformer `piping` not of type bool\r\n") 214 | } 215 | } 216 | contractConfig.Piping[strings.ToLower(addr)] = piping 217 | } 218 | } 219 | --------------------------------------------------------------------------------