├── 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 |
--------------------------------------------------------------------------------