Generic watcher that takes a contract address, grabs the contract abi and starting block number, creates custom event filters, and extracts and transforms event data into postgres. Can configure to look at only a subset of events through CLI flag. Building but needs testing.
This commit is contained in:
parent
57820ff473
commit
8ce75fe5ad
122
cmd/omniWatcher.go
Normal file
122
cmd/omniWatcher.go
Normal file
@ -0,0 +1,122 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/ethclient"
|
||||
"github.com/ethereum/go-ethereum/rpc"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/geth"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/geth/client"
|
||||
vRpc "github.com/vulcanize/vulcanizedb/pkg/geth/converters/rpc"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/geth/node"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/transformer"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/types"
|
||||
)
|
||||
|
||||
// omniWatcherCmd represents the omniWatcher command
|
||||
var omniWatcherCmd = &cobra.Command{
|
||||
Use: "omniWatcher",
|
||||
Short: "Watches events at the provided contract address",
|
||||
Long: `Uses input contract address and event filters to watch events
|
||||
|
||||
Expects an ethereum node to be running
|
||||
Expects an archival node synced into vulcanizeDB
|
||||
Requires a .toml config file:
|
||||
|
||||
[database]
|
||||
name = "vulcanize_public"
|
||||
hostname = "localhost"
|
||||
port = 5432
|
||||
|
||||
[client]
|
||||
ipcPath = "/Users/user/Library/Ethereum/geth.ipc"
|
||||
`,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
omniWatcher()
|
||||
},
|
||||
}
|
||||
|
||||
func omniWatcher() {
|
||||
|
||||
if contractAddress == "" {
|
||||
log.Fatal("Contract address required")
|
||||
}
|
||||
|
||||
if contractEvents == nil {
|
||||
var str string
|
||||
for str != "y" {
|
||||
reader := bufio.NewReader(os.Stdin)
|
||||
fmt.Print("Warning: no events specified, proceeding to watch every event at address" + contractAddress + "? (Y/n)\n> ")
|
||||
resp, err := reader.ReadBytes('\n')
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
str = strings.ToLower(string(resp))
|
||||
if str == "n" {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
rawRpcClient, err := rpc.Dial(ipc)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
rpcClient := client.NewRpcClient(rawRpcClient, ipc)
|
||||
ethClient := ethclient.NewClient(rawRpcClient)
|
||||
client := client.NewEthClient(ethClient)
|
||||
node := node.MakeNode(rpcClient)
|
||||
transactionConverter := vRpc.NewRpcTransactionConverter(ethClient)
|
||||
blockChain := geth.NewBlockChain(client, node, transactionConverter)
|
||||
db, err := postgres.NewDB(databaseConfig, blockChain.Node())
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to initialize database\r\nerr: %v\r\n", err))
|
||||
}
|
||||
|
||||
con := types.Config{
|
||||
DB: db,
|
||||
BC: blockChain,
|
||||
Network: network,
|
||||
}
|
||||
|
||||
t := transformer.NewTransformer(&con)
|
||||
t.Set(contractAddress, contractEvents)
|
||||
|
||||
err = t.Init()
|
||||
if err != nil {
|
||||
log.Fatal(fmt.Sprintf("Failed to initialized generator\r\nerr: %v\r\n", err))
|
||||
}
|
||||
|
||||
log.Fatal(t.Execute())
|
||||
}
|
||||
|
||||
func init() {
|
||||
rootCmd.AddCommand(omniWatcherCmd)
|
||||
|
||||
omniWatcherCmd.Flags().StringVarP(&contractAddress, "contract-address", "a", "", "Single address to generate watchers for")
|
||||
omniWatcherCmd.Flags().StringArrayVarP(&contractEvents, "contract-events", "e", []string{}, "Subset of events to watch- use only with single address")
|
||||
omniWatcherCmd.Flags().StringArrayVarP(&contractAddresses, "contract-addresses", "l", []string{}, "Addresses of the contracts to generate watchers for")
|
||||
omniWatcherCmd.Flags().StringVarP(&network, "network", "n", "", `Network the contract is deployed on; options: "ropsten", "kovan", and "rinkeby"; default is mainnet"`)
|
||||
}
|
@ -32,6 +32,10 @@ var (
|
||||
startingBlockNumber int64
|
||||
syncAll bool
|
||||
endingBlockNumber int64
|
||||
network string
|
||||
contractAddress string
|
||||
contractAddresses []string
|
||||
contractEvents []string
|
||||
)
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
|
@ -1,5 +1,5 @@
|
||||
[database]
|
||||
name = "vulcanize_private"
|
||||
name = "vulcanize_infura"
|
||||
hostname = "localhost"
|
||||
port = 5432
|
||||
|
||||
|
File diff suppressed because one or more lines are too long
94
pkg/omni/converter/converter.go
Normal file
94
pkg/omni/converter/converter.go
Normal file
@ -0,0 +1,94 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package converter
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/ethereum/go-ethereum/accounts/abi/bind"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/examples/generic/helpers"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/types"
|
||||
)
|
||||
|
||||
type Converter interface {
|
||||
Convert(watchedEvent core.WatchedEvent, event *types.Event) error
|
||||
Update(info types.ContractInfo)
|
||||
}
|
||||
|
||||
type converter struct {
|
||||
contractInfo types.ContractInfo
|
||||
}
|
||||
|
||||
func NewConverter(info types.ContractInfo) *converter {
|
||||
|
||||
return &converter{
|
||||
contractInfo: info,
|
||||
}
|
||||
}
|
||||
|
||||
func (c converter) Update(info types.ContractInfo) {
|
||||
c.contractInfo = info
|
||||
}
|
||||
|
||||
func (c converter) Convert(watchedEvent core.WatchedEvent, event *types.Event) error {
|
||||
contract := bind.NewBoundContract(common.HexToAddress(c.contractInfo.Address), c.contractInfo.ParsedAbi, nil, nil, nil)
|
||||
values := make(map[string]interface{})
|
||||
|
||||
for _, field := range event.Fields {
|
||||
var i interface{}
|
||||
values[field.Name] = i
|
||||
|
||||
switch field.Type.T {
|
||||
case abi.StringTy:
|
||||
field.PgType = "CHARACTER VARYING(66) NOT NULL"
|
||||
case abi.IntTy, abi.UintTy:
|
||||
field.PgType = "DECIMAL NOT NULL"
|
||||
case abi.BoolTy:
|
||||
field.PgType = "BOOLEAN NOT NULL"
|
||||
case abi.BytesTy, abi.FixedBytesTy:
|
||||
field.PgType = "BYTEA NOT NULL"
|
||||
case abi.AddressTy:
|
||||
field.PgType = "CHARACTER VARYING(66) NOT NULL"
|
||||
case abi.HashTy:
|
||||
field.PgType = "CHARACTER VARYING(66) NOT NULL"
|
||||
case abi.ArrayTy:
|
||||
field.PgType = "TEXT[] NOT NULL"
|
||||
case abi.FixedPointTy:
|
||||
field.PgType = "MONEY NOT NULL" // use shopspring/decimal for fixed point numbers in go and money type in postgres?
|
||||
case abi.FunctionTy:
|
||||
field.PgType = "TEXT NOT NULL"
|
||||
default:
|
||||
field.PgType = "TEXT NOT NULL"
|
||||
}
|
||||
}
|
||||
|
||||
log := helpers.ConvertToLog(watchedEvent)
|
||||
err := contract.UnpackLogIntoMap(values, event.Name, log)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
eventLog := types.Log{
|
||||
Values: values,
|
||||
Block: watchedEvent.BlockNumber,
|
||||
Tx: watchedEvent.TxHash,
|
||||
}
|
||||
|
||||
event.Logs[watchedEvent.LogID] = eventLog
|
||||
|
||||
return nil
|
||||
}
|
128
pkg/omni/fetcher/fetcher.go
Normal file
128
pkg/omni/fetcher/fetcher.go
Normal file
@ -0,0 +1,128 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package fetcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"math/big"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
)
|
||||
|
||||
// Fetcher serves as the lower level data fetcher that calls the underlying
|
||||
// blockchain's FetchConctractData method for a given return type
|
||||
|
||||
// Interface definition for a Fetcher
|
||||
type Fetcher interface {
|
||||
FetchBigInt(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (big.Int, error)
|
||||
FetchBool(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (bool, error)
|
||||
FetchAddress(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Address, error)
|
||||
FetchString(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (string, error)
|
||||
FetchHash(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Hash, error)
|
||||
}
|
||||
|
||||
// Fetcher struct
|
||||
type fetcher struct {
|
||||
BlockChain core.BlockChain // Underyling Blockchain
|
||||
}
|
||||
|
||||
// Fetcher error
|
||||
type fetcherError struct {
|
||||
err string
|
||||
fetchMethod string
|
||||
}
|
||||
|
||||
func NewFetcher(blockChain core.BlockChain) *fetcher {
|
||||
return &fetcher{
|
||||
BlockChain: blockChain,
|
||||
}
|
||||
}
|
||||
|
||||
// Fetcher error method
|
||||
func (fe *fetcherError) Error() string {
|
||||
return fmt.Sprintf("Error fetching %s: %s", fe.fetchMethod, fe.err)
|
||||
}
|
||||
|
||||
// Used to create a new Fetcher error for a given error and fetch method
|
||||
func newFetcherError(err error, fetchMethod string) *fetcherError {
|
||||
e := fetcherError{err.Error(), fetchMethod}
|
||||
log.Println(e.Error())
|
||||
return &e
|
||||
}
|
||||
|
||||
// Generic Fetcher methods used by Getters to call contract methods
|
||||
|
||||
// Method used to fetch big.Int value from contract
|
||||
func (f fetcher) FetchBigInt(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (big.Int, error) {
|
||||
var result = new(big.Int)
|
||||
err := f.BlockChain.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber)
|
||||
|
||||
if err != nil {
|
||||
return *result, newFetcherError(err, method)
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
// Method used to fetch bool value from contract
|
||||
func (f fetcher) FetchBool(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (bool, error) {
|
||||
var result = new(bool)
|
||||
err := f.BlockChain.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber)
|
||||
|
||||
if err != nil {
|
||||
return *result, newFetcherError(err, method)
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
// Method used to fetch address value from contract
|
||||
func (f fetcher) FetchAddress(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Address, error) {
|
||||
var result = new(common.Address)
|
||||
err := f.BlockChain.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber)
|
||||
|
||||
if err != nil {
|
||||
return *result, newFetcherError(err, method)
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
// Method used to fetch string value from contract
|
||||
func (f fetcher) FetchString(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (string, error) {
|
||||
var result = new(string)
|
||||
err := f.BlockChain.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber)
|
||||
|
||||
if err != nil {
|
||||
return *result, newFetcherError(err, method)
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
||||
|
||||
// Method used to fetch hash value from contract
|
||||
func (f fetcher) FetchHash(method, contractAbi, contractAddress string, blockNumber int64, methodArgs []interface{}) (common.Hash, error) {
|
||||
var result = new(common.Hash)
|
||||
err := f.BlockChain.FetchContractData(contractAbi, contractAddress, method, methodArgs, &result, blockNumber)
|
||||
|
||||
if err != nil {
|
||||
return *result, newFetcherError(err, method)
|
||||
}
|
||||
|
||||
return *result, nil
|
||||
}
|
85
pkg/omni/parser/parser.go
Normal file
85
pkg/omni/parser/parser.go
Normal file
@ -0,0 +1,85 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package parser
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/geth"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/types"
|
||||
)
|
||||
|
||||
type Parser interface {
|
||||
Parse(contractAddr string) error
|
||||
GetAbi() string
|
||||
GetParsedAbi() abi.ABI
|
||||
GetMethods() map[string]*types.Method
|
||||
GetEvents() map[string]*types.Event
|
||||
}
|
||||
|
||||
type parser struct {
|
||||
client *geth.EtherScanAPI
|
||||
abi string
|
||||
parsedAbi abi.ABI
|
||||
}
|
||||
|
||||
func NewParser(network string) *parser {
|
||||
url := geth.GenURL(network)
|
||||
|
||||
return &parser{
|
||||
client: geth.NewEtherScanClient(url),
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) GetAbi() string {
|
||||
return p.abi
|
||||
}
|
||||
|
||||
func (p *parser) GetParsedAbi() abi.ABI {
|
||||
return p.parsedAbi
|
||||
}
|
||||
|
||||
func (p *parser) Parse(contractAddr string) error {
|
||||
abiStr, err := p.client.GetAbi(contractAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.abi = abiStr
|
||||
p.parsedAbi, err = geth.ParseAbi(abiStr)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (p *parser) GetMethods() map[string]*types.Method {
|
||||
methods := map[string]*types.Method{}
|
||||
|
||||
for _, m := range p.parsedAbi.Methods {
|
||||
method := types.NewMethod(m)
|
||||
methods[m.Name] = method
|
||||
}
|
||||
|
||||
return methods
|
||||
}
|
||||
|
||||
func (p *parser) GetEvents() map[string]*types.Event {
|
||||
events := map[string]*types.Event{}
|
||||
|
||||
for _, e := range p.parsedAbi.Events {
|
||||
event := types.NewEvent(e)
|
||||
events[e.Name] = event
|
||||
}
|
||||
|
||||
return events
|
||||
}
|
150
pkg/omni/repository/repository.go
Normal file
150
pkg/omni/repository/repository.go
Normal file
@ -0,0 +1,150 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/types"
|
||||
)
|
||||
|
||||
type DataStore interface {
|
||||
PersistEvents(info types.ContractInfo) error
|
||||
}
|
||||
|
||||
type dataStore struct {
|
||||
*postgres.DB
|
||||
}
|
||||
|
||||
func NewDataStore(db *postgres.DB) *dataStore {
|
||||
return &dataStore{
|
||||
DB: db,
|
||||
}
|
||||
}
|
||||
|
||||
func (d *dataStore) PersistEvents(contract types.ContractInfo) error {
|
||||
|
||||
schemaExists, err := d.CheckForSchema(contract.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !schemaExists {
|
||||
err = d.CreateContractSchema(contract.Name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for eventName, event := range contract.Events {
|
||||
|
||||
tableExists, err := d.CheckForTable(contract.Name, eventName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !tableExists {
|
||||
err = d.CreateEventTable(contract.Name, event)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
for id, log := range event.Logs {
|
||||
// Create postgres command to persist any given event
|
||||
pgStr := fmt.Sprintf("INSERT INTO %s.%s ", strings.ToLower(contract.Name), strings.ToLower(eventName))
|
||||
pgStr = pgStr + "(vulcanize_log_id, token_name, token_address, event_name, block, tx"
|
||||
var data []interface{}
|
||||
data = append(data,
|
||||
id,
|
||||
strings.ToLower(contract.Name),
|
||||
strings.ToLower(contract.Address),
|
||||
strings.ToLower(eventName),
|
||||
log.Block,
|
||||
log.Tx)
|
||||
|
||||
counter := 0
|
||||
for inputName, input := range log.Values {
|
||||
counter += 1
|
||||
pgStr = pgStr + fmt.Sprintf(", %s", strings.ToLower(inputName))
|
||||
data = append(data, input)
|
||||
}
|
||||
|
||||
pgStr = pgStr + ") "
|
||||
appendStr := "VALUES ($1, $2, $3, $4, $5, $6"
|
||||
|
||||
for i := 0; i < counter; i++ {
|
||||
appendStr = appendStr + fmt.Sprintf(", $%d", i+7)
|
||||
}
|
||||
|
||||
appendStr = appendStr + ") "
|
||||
appendStr = appendStr + "ON CONFLICT (vulcanize_log_id) DO NOTHING"
|
||||
pgStr = pgStr + fmt.Sprintf(") %s", appendStr)
|
||||
|
||||
_, err := d.DB.Exec(pgStr, data...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (d *dataStore) CreateEventTable(contractName string, event *types.Event) error {
|
||||
// Create postgres command to create table for any given event
|
||||
pgStr := fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s.%s ", strings.ToLower(contractName), strings.ToLower(event.Name))
|
||||
pgStr = pgStr + `(id SERIAL,
|
||||
vulcanize_log_id INTEGER NOT NULL UNIQUE,
|
||||
token_name CHARACTER VARYING(66) NOT NULL,
|
||||
token_address CHARACTER VARYING(66) NOT NULL,
|
||||
event_name CHARACTER VARYING(66) NOT NULL,
|
||||
block INTEGER NOT NULL,
|
||||
tx CHARACTER VARYING(66) NOT NULL, `
|
||||
for _, field := range event.Fields {
|
||||
pgStr = pgStr + fmt.Sprintf("%s %s NOT NULL, ", field.Name, field.PgType)
|
||||
}
|
||||
pgStr = pgStr + "CONSTRAINT log_index_fk FOREIGN KEY (vulcanize_log_id) REFERENCES logs (id) ON DELETE CASCADE)"
|
||||
|
||||
_, err := d.DB.Exec(pgStr)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *dataStore) CheckForTable(contractName string, eventName string) (bool, error) {
|
||||
pgStr := fmt.Sprintf("SELECT EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = '%s' AND table_name = '%s')", contractName, eventName)
|
||||
var exists bool
|
||||
err := d.DB.Get(exists, pgStr)
|
||||
|
||||
return exists, err
|
||||
}
|
||||
|
||||
func (d *dataStore) CreateContractSchema(contractName string) error {
|
||||
_, err := d.DB.Exec("CREATE SCHEMA IF NOT EXISTS " + contractName)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (d *dataStore) CheckForSchema(contractName string) (bool, error) {
|
||||
pgStr := fmt.Sprintf("SELECT EXISTS (SELECT schema_name FROM information_schema.schemata WHERE schema_name = '%s')", contractName)
|
||||
|
||||
var exists bool
|
||||
err := d.DB.Get(exists, pgStr)
|
||||
|
||||
return exists, err
|
||||
}
|
74
pkg/omni/retriever/retriever.go
Normal file
74
pkg/omni/retriever/retriever.go
Normal file
@ -0,0 +1,74 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package retriever
|
||||
|
||||
import (
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
)
|
||||
|
||||
type Retriever interface {
|
||||
RetrieveFirstBlock(contractAddr string) (int64, error)
|
||||
RetrieveFirstBlockFromLogs(contractAddr string) (int64, error)
|
||||
RetrieveFirstBlockFromReceipts(contractAddr string) (int64, error)
|
||||
}
|
||||
|
||||
type retriever struct {
|
||||
*postgres.DB
|
||||
}
|
||||
|
||||
func NewRetriever(db *postgres.DB) (r *retriever) {
|
||||
|
||||
return &retriever{
|
||||
DB: db,
|
||||
}
|
||||
}
|
||||
|
||||
// For some contracts the creation transaction receipt doesn't have the contract address so this doesn't work (e.g. Sai)
|
||||
func (r *retriever) RetrieveFirstBlockFromReceipts(contractAddr string) (int64, error) {
|
||||
var firstBlock int
|
||||
err := r.DB.Get(
|
||||
&firstBlock,
|
||||
`SELECT number FROM blocks
|
||||
WHERE id = (SELECT block_id FROM receipts
|
||||
WHERE contract_address = $1
|
||||
ORDER BY block_id ASC
|
||||
LIMIT 1)`,
|
||||
contractAddr,
|
||||
)
|
||||
|
||||
return int64(firstBlock), err
|
||||
}
|
||||
|
||||
// This servers as a heuristic to find the first block by finding the first contract event log
|
||||
func (r *retriever) RetrieveFirstBlockFromLogs(contractAddr string) (int64, error) {
|
||||
var firstBlock int
|
||||
err := r.DB.Get(
|
||||
&firstBlock,
|
||||
"SELECT block_number FROM logs WHERE address = $1 ORDER BY block_number ASC LIMIT 1",
|
||||
contractAddr,
|
||||
)
|
||||
|
||||
return int64(firstBlock), err
|
||||
}
|
||||
|
||||
// Try both methods of finding the first block, with the receipt method taking precedence
|
||||
func (r *retriever) RetrieveFirstBlock(contractAddr string) (int64, error) {
|
||||
i, err := r.RetrieveFirstBlockFromReceipts(contractAddr)
|
||||
if err != nil {
|
||||
i, err = r.RetrieveFirstBlockFromLogs(contractAddr)
|
||||
}
|
||||
|
||||
return i, err
|
||||
}
|
160
pkg/omni/transformer/event_transformer.go
Normal file
160
pkg/omni/transformer/event_transformer.go
Normal file
@ -0,0 +1,160 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package transformer
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres/repositories"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/converter"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/fetcher"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/parser"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/repository"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/retriever"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/omni/types"
|
||||
)
|
||||
|
||||
// Top-level object similar to generator
|
||||
// but attempts to solve problem without
|
||||
// automated code generation
|
||||
|
||||
type EventTransformer interface {
|
||||
Init(contractAddr string) error
|
||||
}
|
||||
|
||||
type eventTransformer struct {
|
||||
// Network, database, and blockchain config
|
||||
*types.Config
|
||||
|
||||
// Underlying databases
|
||||
datastore.WatchedEventRepository
|
||||
datastore.FilterRepository
|
||||
repository.DataStore
|
||||
|
||||
// Underlying interfaces
|
||||
parser.Parser // Parses events out of contract abi fetched with addr
|
||||
retriever.Retriever // Retrieves first block with contract addr referenced
|
||||
fetcher.Fetcher // Fetches data from contract methods
|
||||
|
||||
// Store contract info as mapping to contract address
|
||||
ContractInfo map[string]types.ContractInfo
|
||||
|
||||
// Subset of events of interest, stored as map of contract address to events
|
||||
// By default this
|
||||
sets map[string][]string
|
||||
}
|
||||
|
||||
// Transformer takes in config for blockchain, database, and network id
|
||||
func NewTransformer(c *types.Config) (t *eventTransformer) {
|
||||
t.Parser = parser.NewParser(c.Network)
|
||||
t.Retriever = retriever.NewRetriever(c.DB)
|
||||
t.Fetcher = fetcher.NewFetcher(c.BC)
|
||||
t.ContractInfo = map[string]types.ContractInfo{}
|
||||
t.WatchedEventRepository = repositories.WatchedEventRepository{DB: c.DB}
|
||||
t.FilterRepository = repositories.FilterRepository{DB: c.DB}
|
||||
t.DataStore = repository.NewDataStore(c.DB)
|
||||
t.sets = map[string][]string{}
|
||||
|
||||
return t
|
||||
}
|
||||
|
||||
// Used to set which contract addresses and which of their events to watch
|
||||
func (t *eventTransformer) Set(contractAddr string, filterSet []string) {
|
||||
t.sets[contractAddr] = filterSet
|
||||
}
|
||||
|
||||
// Use after creating and setting transformer
|
||||
// Loops over all of the addr => filter sets
|
||||
// Uses parser to pull event info from abi
|
||||
// Use this info to generate event filters
|
||||
func (t *eventTransformer) Init() error {
|
||||
|
||||
for contractAddr, subset := range t.sets {
|
||||
err := t.Parser.Parse(contractAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var ctrName string
|
||||
strName, err1 := t.Fetcher.FetchString("name", t.Parser.GetAbi(), contractAddr, -1, nil)
|
||||
if err1 != nil || strName == "" {
|
||||
hashName, err2 := t.Fetcher.FetchHash("name", t.Parser.GetAbi(), contractAddr, -1, nil)
|
||||
if err2 != nil || hashName.String() == "" {
|
||||
return errors.New(fmt.Sprintf("fetching string: %s and hash: %s names failed\r\nerr1: %v\r\nerr2: %v\r\n ", strName, hashName, err1, err2))
|
||||
}
|
||||
ctrName = hashName.String()
|
||||
} else {
|
||||
ctrName = strName
|
||||
}
|
||||
|
||||
firstBlock, err := t.Retriever.RetrieveFirstBlock(contractAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
info := types.ContractInfo{
|
||||
Name: ctrName,
|
||||
Address: contractAddr,
|
||||
Abi: t.Parser.GetAbi(),
|
||||
ParsedAbi: t.Parser.GetParsedAbi(),
|
||||
StartingBlock: firstBlock,
|
||||
Events: t.Parser.GetEvents(),
|
||||
Methods: t.Parser.GetMethods(),
|
||||
}
|
||||
|
||||
info.GenerateFilters(subset)
|
||||
|
||||
for _, filter := range info.Filters {
|
||||
t.CreateFilter(filter)
|
||||
}
|
||||
|
||||
t.ContractInfo[contractAddr] = info
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (tr eventTransformer) Execute() error {
|
||||
for _, contract := range tr.ContractInfo {
|
||||
|
||||
c := converter.NewConverter(contract)
|
||||
|
||||
for eventName, filter := range contract.Filters {
|
||||
watchedEvents, err := tr.GetWatchedEvents(eventName)
|
||||
if err != nil {
|
||||
log.Println(fmt.Sprintf("Error fetching events for %s:", filter.Name), err)
|
||||
return err
|
||||
}
|
||||
|
||||
for _, we := range watchedEvents {
|
||||
err = c.Convert(*we, contract.Events[eventName])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
err := tr.PersistEvents(contract)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
26
pkg/omni/types/config.go
Normal file
26
pkg/omni/types/config.go
Normal file
@ -0,0 +1,26 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||
)
|
||||
|
||||
type Config struct {
|
||||
Network string
|
||||
BC core.BlockChain
|
||||
DB *postgres.DB
|
||||
}
|
58
pkg/omni/types/contract_info.go
Normal file
58
pkg/omni/types/contract_info.go
Normal file
@ -0,0 +1,58 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
|
||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||
"github.com/vulcanize/vulcanizedb/pkg/filters"
|
||||
)
|
||||
|
||||
type ContractInfo struct {
|
||||
Name string
|
||||
Address string
|
||||
StartingBlock int64
|
||||
Abi string
|
||||
ParsedAbi abi.ABI
|
||||
Events map[string]*Event // Map of events to their names
|
||||
Methods map[string]*Method // Map of methods to their names
|
||||
Filters map[string]filters.LogFilter // Map of event filters to their names
|
||||
}
|
||||
|
||||
func (i *ContractInfo) GenerateFilters(subset []string) {
|
||||
i.Filters = map[string]filters.LogFilter{}
|
||||
for name, event := range i.Events {
|
||||
if len(subset) == 0 || stringInSlice(subset, name) {
|
||||
i.Filters[name] = filters.LogFilter{
|
||||
Name: name,
|
||||
FromBlock: i.StartingBlock,
|
||||
ToBlock: -1,
|
||||
Address: i.Address,
|
||||
Topics: core.Topics{event.Sig()},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stringInSlice(list []string, s string) bool {
|
||||
for _, b := range list {
|
||||
if b == s {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
108
pkg/omni/types/entities.go
Normal file
108
pkg/omni/types/entities.go
Normal file
@ -0,0 +1,108 @@
|
||||
// Copyright 2018 Vulcanize
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/ethereum/go-ethereum/accounts/abi"
|
||||
)
|
||||
|
||||
type Event struct {
|
||||
Name string
|
||||
Anonymous bool
|
||||
Fields []Field
|
||||
Logs map[int64]Log // Map of VulcanizeIdLog to parsed event log
|
||||
}
|
||||
|
||||
type Method struct {
|
||||
Name string
|
||||
Const bool
|
||||
Inputs []Field
|
||||
Outputs []Field
|
||||
}
|
||||
|
||||
type Field struct {
|
||||
abi.Argument
|
||||
Value interface{}
|
||||
PgType string
|
||||
}
|
||||
|
||||
type Log struct {
|
||||
Values map[string]interface{} // Map of event input names to their values
|
||||
Block int64
|
||||
Tx string
|
||||
}
|
||||
|
||||
func NewEvent(e abi.Event) *Event {
|
||||
fields := make([]Field, len(e.Inputs))
|
||||
for i, input := range e.Inputs {
|
||||
fields[i].Name = input.Name
|
||||
fields[i].Type = input.Type
|
||||
fields[i].Indexed = input.Indexed
|
||||
}
|
||||
|
||||
return &Event{
|
||||
Name: e.Name,
|
||||
Anonymous: e.Anonymous,
|
||||
Fields: fields,
|
||||
Logs: map[int64]Log{},
|
||||
}
|
||||
}
|
||||
|
||||
func NewMethod(m abi.Method) *Method {
|
||||
inputs := make([]Field, len(m.Inputs))
|
||||
for i, input := range m.Inputs {
|
||||
inputs[i].Name = input.Name
|
||||
inputs[i].Type = input.Type
|
||||
inputs[i].Indexed = input.Indexed
|
||||
}
|
||||
|
||||
outputs := make([]Field, len(m.Outputs))
|
||||
for i, output := range m.Outputs {
|
||||
outputs[i].Name = output.Name
|
||||
outputs[i].Type = output.Type
|
||||
outputs[i].Indexed = output.Indexed
|
||||
}
|
||||
|
||||
return &Method{
|
||||
Name: m.Name,
|
||||
Const: m.Const,
|
||||
Inputs: inputs,
|
||||
Outputs: outputs,
|
||||
}
|
||||
}
|
||||
|
||||
func (e Event) Sig() string {
|
||||
types := make([]string, len(e.Fields))
|
||||
|
||||
for i, input := range e.Fields {
|
||||
types[i] = input.Type.String()
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v(%v)", e.Name, strings.Join(types, ","))
|
||||
}
|
||||
|
||||
func (m Method) Sig() string {
|
||||
types := make([]string, len(m.Inputs))
|
||||
i := 0
|
||||
for _, input := range m.Inputs {
|
||||
types[i] = input.Type.String()
|
||||
i++
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%v(%v)", m.Name, strings.Join(types, ","))
|
||||
}
|
Loading…
Reference in New Issue
Block a user