forked from cerc-io/ipld-eth-server
Add Filters (#133)
* Add LogFilter struct * Add log_filters table * Add view for events watching * Add cmd line "add_filter" to mimic eventual endpoint * Allow multiple filters in config
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/8thlight/vulcanizedb/pkg/core"
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||
)
|
||||
|
||||
type LogFilters []LogFilter
|
||||
|
||||
type LogFilter struct {
|
||||
Name string `json:"name"`
|
||||
FromBlock int64 `json:"fromBlock"`
|
||||
ToBlock int64 `json:"toBlock"`
|
||||
Address string `json:"address"`
|
||||
core.Topics `json:"topics"`
|
||||
}
|
||||
|
||||
func (filterQuery *LogFilter) UnmarshalJSON(input []byte) error {
|
||||
type Alias LogFilter
|
||||
|
||||
var err error
|
||||
aux := &struct {
|
||||
ToBlock string `json:"toBlock"`
|
||||
FromBlock string `json:"fromBlock"`
|
||||
*Alias
|
||||
}{
|
||||
Alias: (*Alias)(filterQuery),
|
||||
}
|
||||
if err := json.Unmarshal(input, &aux); err != nil {
|
||||
return err
|
||||
}
|
||||
if filterQuery.Name == "" {
|
||||
return errors.New("filters: must provide name for logfilter")
|
||||
}
|
||||
filterQuery.ToBlock, err = filterQuery.unmarshalFromToBlock(aux.ToBlock)
|
||||
if err != nil {
|
||||
return errors.New("filters: invalid fromBlock")
|
||||
}
|
||||
filterQuery.FromBlock, err = filterQuery.unmarshalFromToBlock(aux.FromBlock)
|
||||
if err != nil {
|
||||
return errors.New("filters: invalid fromBlock")
|
||||
}
|
||||
if !common.IsHexAddress(filterQuery.Address) {
|
||||
return errors.New("filters: invalid address")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (filterQuery *LogFilter) unmarshalFromToBlock(auxBlock string) (int64, error) {
|
||||
if auxBlock == "" {
|
||||
return -1, nil
|
||||
}
|
||||
block, err := hexutil.DecodeUint64(auxBlock)
|
||||
if err != nil {
|
||||
return 0, errors.New("filters: invalid block arg")
|
||||
}
|
||||
return int64(block), nil
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package filters_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/8thlight/vulcanizedb/pkg/core"
|
||||
"github.com/8thlight/vulcanizedb/pkg/filters"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Log filters", func() {
|
||||
It("decodes web3 filter to LogFilter", func() {
|
||||
|
||||
var logFilter filters.LogFilter
|
||||
jsonFilter := []byte(
|
||||
`{
|
||||
"name": "TestEvent",
|
||||
"fromBlock": "0x1",
|
||||
"toBlock": "0x488290",
|
||||
"address": "0x8888f1f195afa192cfee860698584c030f4c9db1",
|
||||
"topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]
|
||||
}`)
|
||||
err := json.Unmarshal(jsonFilter, &logFilter)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(logFilter.Name).To(Equal("TestEvent"))
|
||||
Expect(logFilter.FromBlock).To(Equal(int64(1)))
|
||||
Expect(logFilter.ToBlock).To(Equal(int64(4752016)))
|
||||
Expect(logFilter.Address).To(Equal("0x8888f1f195afa192cfee860698584c030f4c9db1"))
|
||||
Expect(logFilter.Topics).To(Equal(
|
||||
core.Topics{
|
||||
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"",
|
||||
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
""}))
|
||||
})
|
||||
|
||||
It("decodes array of web3 filters to []LogFilter", func() {
|
||||
|
||||
logFilters := make([]filters.LogFilter, 0)
|
||||
jsonFilter := []byte(
|
||||
`[{
|
||||
"name": "TestEvent",
|
||||
"fromBlock": "0x1",
|
||||
"toBlock": "0x488290",
|
||||
"address": "0x8888f1f195afa192cfee860698584c030f4c9db1",
|
||||
"topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]
|
||||
},
|
||||
{
|
||||
"name": "TestEvent2",
|
||||
"fromBlock": "0x3",
|
||||
"toBlock": "0x4",
|
||||
"address": "0xd26114cd6EE289AccF82350c8d8487fedB8A0C07",
|
||||
"topics": ["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef", "0x0000000000000000000000006b0949d4c6edfe467db78241b7d5566f3c2bb43e", "0x0000000000000000000000005e44c3e467a49c9ca0296a9f130fc433041aaa28"]
|
||||
}]`)
|
||||
err := json.Unmarshal(jsonFilter, &logFilters)
|
||||
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(len(logFilters)).To(Equal(2))
|
||||
Expect(logFilters[0].Name).To(Equal("TestEvent"))
|
||||
Expect(logFilters[1].Name).To(Equal("TestEvent2"))
|
||||
})
|
||||
|
||||
It("requires valid ethereum address", func() {
|
||||
|
||||
var logFilter filters.LogFilter
|
||||
jsonFilter := []byte(
|
||||
`{
|
||||
"name": "TestEvent",
|
||||
"fromBlock": "0x1",
|
||||
"toBlock": "0x2",
|
||||
"address": "0x8888f1f195afa192cf84c030f4c9db1",
|
||||
"topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]
|
||||
}`)
|
||||
err := json.Unmarshal(jsonFilter, &logFilter)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
})
|
||||
It("requires name", func() {
|
||||
|
||||
var logFilter filters.LogFilter
|
||||
jsonFilter := []byte(
|
||||
`{
|
||||
"fromBlock": "0x1",
|
||||
"toBlock": "0x2",
|
||||
"address": "0x8888f1f195afa192cfee860698584c030f4c9db1",
|
||||
"topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]
|
||||
}`)
|
||||
err := json.Unmarshal(jsonFilter, &logFilter)
|
||||
Expect(err).To(HaveOccurred())
|
||||
|
||||
})
|
||||
|
||||
It("maps missing fromBlock to -1", func() {
|
||||
|
||||
var logFilter filters.LogFilter
|
||||
jsonFilter := []byte(
|
||||
`{
|
||||
"name": "TestEvent",
|
||||
"toBlock": "0x2",
|
||||
"address": "0x8888f1f195afa192cfee860698584c030f4c9db1",
|
||||
"topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]
|
||||
}`)
|
||||
err := json.Unmarshal(jsonFilter, &logFilter)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(logFilter.FromBlock).To(Equal(int64(-1)))
|
||||
|
||||
})
|
||||
|
||||
It("maps missing toBlock to -1", func() {
|
||||
var logFilter filters.LogFilter
|
||||
jsonFilter := []byte(
|
||||
`{
|
||||
"name": "TestEvent",
|
||||
"address": "0x8888f1f195afa192cfee860698584c030f4c9db1",
|
||||
"topics": ["0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null, "0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b", null]
|
||||
}`)
|
||||
err := json.Unmarshal(jsonFilter, &logFilter)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(logFilter.ToBlock).To(Equal(int64(-1)))
|
||||
|
||||
})
|
||||
|
||||
})
|
||||
@@ -0,0 +1,13 @@
|
||||
package filters_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
func TestQueryBuilder(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
RunSpecs(t, "QueryBuilder Suite")
|
||||
}
|
||||
@@ -3,7 +3,10 @@ package repositories
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"errors"
|
||||
|
||||
"github.com/8thlight/vulcanizedb/pkg/core"
|
||||
"github.com/8thlight/vulcanizedb/pkg/filters"
|
||||
)
|
||||
|
||||
type InMemory struct {
|
||||
@@ -11,16 +14,27 @@ type InMemory struct {
|
||||
receipts map[string]core.Receipt
|
||||
contracts map[string]core.Contract
|
||||
logs map[string][]core.Log
|
||||
logFilters map[string]filters.LogFilter
|
||||
CreateOrUpdateBlockCallCount int
|
||||
}
|
||||
|
||||
func (repository *InMemory) AddFilter(filter filters.LogFilter) error {
|
||||
key := filter.Name
|
||||
if _, ok := repository.logFilters[key]; ok || key == "" {
|
||||
return errors.New("filter name not unique")
|
||||
}
|
||||
repository.logFilters[key] = filter
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewInMemory() *InMemory {
|
||||
return &InMemory{
|
||||
CreateOrUpdateBlockCallCount: 0,
|
||||
blocks: make(map[int64]core.Block),
|
||||
receipts: make(map[string]core.Receipt),
|
||||
contracts: make(map[string]core.Contract),
|
||||
logs: make(map[string][]core.Log),
|
||||
blocks: make(map[int64]core.Block),
|
||||
receipts: make(map[string]core.Receipt),
|
||||
contracts: make(map[string]core.Contract),
|
||||
logs: make(map[string][]core.Log),
|
||||
logFilters: make(map[string]filters.LogFilter),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
"github.com/8thlight/vulcanizedb/pkg/config"
|
||||
"github.com/8thlight/vulcanizedb/pkg/core"
|
||||
"github.com/8thlight/vulcanizedb/pkg/filters"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/lib/pq"
|
||||
)
|
||||
@@ -303,7 +304,7 @@ func (repository Postgres) createTransaction(tx *sql.Tx, blockId int64, transact
|
||||
err := tx.QueryRow(
|
||||
`INSERT INTO transactions
|
||||
(block_id, tx_hash, tx_nonce, tx_to, tx_from, tx_gaslimit, tx_gasprice, tx_value, tx_input_data)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, cast(NULLIF($8, '') as NUMERIC), $9)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, cast(NULLIF($8, '') AS NUMERIC), $9)
|
||||
RETURNING id`,
|
||||
blockId, transaction.Hash, transaction.Nonce, transaction.To, transaction.From, transaction.GasLimit, transaction.GasPrice, transaction.Value, transaction.Data).
|
||||
Scan(&transactionId)
|
||||
@@ -381,6 +382,18 @@ func (repository Postgres) CreateLogs(logs []core.Log) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (repository Postgres) AddFilter(query filters.LogFilter) error {
|
||||
_, err := repository.Db.Exec(
|
||||
`INSERT INTO log_filters
|
||||
(name, from_block, to_block, address, topic0, topic1, topic2, topic3)
|
||||
VALUES ($1, NULLIF($2, -1), NULLIF($3, -1), $4, NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''), NULLIF($8, ''))`,
|
||||
query.Name, query.FromBlock, query.ToBlock, query.Address, query.Topics[0], query.Topics[1], query.Topics[2], query.Topics[3])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadReceipt(receiptsRow *sql.Row) (core.Receipt, error) {
|
||||
var contractAddress string
|
||||
var txHash string
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package repositories
|
||||
|
||||
import "github.com/8thlight/vulcanizedb/pkg/core"
|
||||
import (
|
||||
"github.com/8thlight/vulcanizedb/pkg/core"
|
||||
"github.com/8thlight/vulcanizedb/pkg/filters"
|
||||
)
|
||||
|
||||
const (
|
||||
blocksFromHeadBeforeFinal = 20
|
||||
@@ -19,4 +22,5 @@ type Repository interface {
|
||||
CreateLogs(log []core.Log) error
|
||||
FindLogs(address string, blockNumber int64) []core.Log
|
||||
SetBlocksStatus(chainHead int64)
|
||||
AddFilter(filter filters.LogFilter) error
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
"math/big"
|
||||
|
||||
"github.com/8thlight/vulcanizedb/pkg/core"
|
||||
"github.com/8thlight/vulcanizedb/pkg/filters"
|
||||
"github.com/8thlight/vulcanizedb/pkg/repositories"
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
@@ -18,6 +19,7 @@ func ClearData(postgres repositories.Postgres) {
|
||||
postgres.Db.MustExec("DELETE FROM blocks")
|
||||
postgres.Db.MustExec("DELETE FROM logs")
|
||||
postgres.Db.MustExec("DELETE FROM receipts")
|
||||
postgres.Db.MustExec("DELETE FROM log_filters")
|
||||
}
|
||||
|
||||
func AssertRepositoryBehavior(buildRepository func(node core.Node) repositories.Repository) {
|
||||
@@ -600,6 +602,43 @@ func AssertRepositoryBehavior(buildRepository func(node core.Node) repositories.
|
||||
Expect(err).To(HaveOccurred())
|
||||
Expect(receipt).To(BeZero())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("LogFilter", func() {
|
||||
|
||||
It("inserts filter into watched events", func() {
|
||||
|
||||
logFilter := filters.LogFilter{
|
||||
Name: "TestFilter",
|
||||
FromBlock: 1,
|
||||
ToBlock: 2,
|
||||
Address: "0x8888f1f195afa192cfee860698584c030f4c9db1",
|
||||
Topics: core.Topics{
|
||||
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"",
|
||||
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"",
|
||||
},
|
||||
}
|
||||
err := repository.AddFilter(logFilter)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
})
|
||||
|
||||
It("returns error if name is not provided", func() {
|
||||
|
||||
logFilter := filters.LogFilter{
|
||||
FromBlock: 1,
|
||||
ToBlock: 2,
|
||||
Address: "0x8888f1f195afa192cfee860698584c030f4c9db1",
|
||||
Topics: core.Topics{
|
||||
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"",
|
||||
"0x000000000000000000000000a94f5374fce5edbc8e2a8697c15331677e6ebf0b",
|
||||
"",
|
||||
},
|
||||
}
|
||||
err := repository.AddFilter(logFilter)
|
||||
Expect(err).To(HaveOccurred())
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user