forked from cerc-io/ipld-eth-server
VDB-919 Generalise converter (#152)
* Generalise transformer stack to use InsertionModel * Add tests for event repository * Restrict accepted values in InsertionModel * Add call to repository.SetDB * Improve error propagation/clarity on GetABI() * Remove maker references in example * Please golint * refactor rollback error handling in repository * Cleaner errors in repository, refactor tests
This commit is contained in:
parent
6c055a9e12
commit
f7c4a6736d
@ -17,6 +17,8 @@
|
|||||||
package integration_test
|
package integration_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"io/ioutil"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
. "github.com/onsi/ginkgo"
|
. "github.com/onsi/ginkgo"
|
||||||
@ -27,3 +29,7 @@ func TestIntegrationTest(t *testing.T) {
|
|||||||
RegisterFailHandler(Fail)
|
RegisterFailHandler(Fail)
|
||||||
RunSpecs(t, "IntegrationTest Suite")
|
RunSpecs(t, "IntegrationTest Suite")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var _ = BeforeSuite(func() {
|
||||||
|
logrus.SetOutput(ioutil.Discard)
|
||||||
|
})
|
||||||
|
@ -16,9 +16,13 @@
|
|||||||
|
|
||||||
package event
|
package event
|
||||||
|
|
||||||
import "github.com/vulcanize/vulcanizedb/pkg/core"
|
import (
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Converter transforms log data into general InsertionModels the Repository can persist__
|
||||||
type Converter interface {
|
type Converter interface {
|
||||||
ToEntities(contractAbi string, ethLog []core.HeaderSyncLog) ([]interface{}, error)
|
ToModels(contractAbi string, ethLog []core.HeaderSyncLog) ([]InsertionModel, error)
|
||||||
ToModels([]interface{}) ([]interface{}, error)
|
SetDB(db *postgres.DB)
|
||||||
}
|
}
|
||||||
|
@ -16,9 +16,163 @@
|
|||||||
|
|
||||||
package event
|
package event
|
||||||
|
|
||||||
import "github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
import (
|
||||||
|
"database/sql/driver"
|
||||||
|
"fmt"
|
||||||
|
"github.com/vulcanize/vulcanizedb/utils"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/sirupsen/logrus"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||||
|
)
|
||||||
|
|
||||||
|
const SetLogTransformedQuery = `UPDATE public.header_sync_logs SET transformed = true WHERE id = $1`
|
||||||
|
|
||||||
|
// Repository persists transformed values to the DB
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
Create(models []interface{}) error
|
Create(models []InsertionModel) error
|
||||||
SetDB(db *postgres.DB)
|
SetDB(db *postgres.DB)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LogFK is the name of log foreign key columns
|
||||||
|
const LogFK ColumnName = "log_id"
|
||||||
|
|
||||||
|
// AddressFK is the name of address foreign key columns
|
||||||
|
const AddressFK ColumnName = "address_id"
|
||||||
|
|
||||||
|
// HeaderFK is the name of header foreign key columns
|
||||||
|
const HeaderFK ColumnName = "header_id"
|
||||||
|
|
||||||
|
// SchemaName is the schema to work with
|
||||||
|
type SchemaName string
|
||||||
|
|
||||||
|
// TableName identifies the table for inserting the data
|
||||||
|
type TableName string
|
||||||
|
|
||||||
|
// ColumnName identifies columns on the given table
|
||||||
|
type ColumnName string
|
||||||
|
|
||||||
|
// ColumnValues maps a column to the value for insertion. This is restricted to []byte, bool, float64, int64, string, time.Time
|
||||||
|
type ColumnValues map[ColumnName]interface{}
|
||||||
|
|
||||||
|
// ErrUnsupportedValue is thrown when a model supplies a type of value the postgres driver cannot handle.
|
||||||
|
var ErrUnsupportedValue = func(value interface{}) error {
|
||||||
|
return fmt.Errorf("unsupported type of value supplied in model: %v (%T)", value, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertionModel is the generalised data structure a converter returns, and contains everything the repository needs to
|
||||||
|
// persist the converted data.
|
||||||
|
type InsertionModel struct {
|
||||||
|
SchemaName SchemaName
|
||||||
|
TableName TableName
|
||||||
|
OrderedColumns []ColumnName // Defines the fields to insert, and in which order the table expects them
|
||||||
|
ColumnValues ColumnValues // Associated values for columns, restricted to []byte, bool, float64, int64, string, time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// ModelToQuery stores memoised insertion queries to minimise computation
|
||||||
|
var ModelToQuery = map[string]string{}
|
||||||
|
|
||||||
|
// GetMemoizedQuery gets/creates a DB insertion query for the model
|
||||||
|
func GetMemoizedQuery(model InsertionModel) string {
|
||||||
|
// The schema and table name uniquely determines the insertion query, use that for memoization
|
||||||
|
queryKey := string(model.SchemaName) + string(model.TableName)
|
||||||
|
query, queryMemoized := ModelToQuery[queryKey]
|
||||||
|
if !queryMemoized {
|
||||||
|
query = GenerateInsertionQuery(model)
|
||||||
|
ModelToQuery[queryKey] = query
|
||||||
|
}
|
||||||
|
return query
|
||||||
|
}
|
||||||
|
|
||||||
|
// GenerateInsertionQuery creates an SQL insertion query from an insertion model.
|
||||||
|
// Should be called through GetMemoizedQuery, so the query is not generated on each call to Create.
|
||||||
|
func GenerateInsertionQuery(model InsertionModel) string {
|
||||||
|
var valuePlaceholders []string
|
||||||
|
var updateOnConflict []string
|
||||||
|
for i := 0; i < len(model.OrderedColumns); i++ {
|
||||||
|
valuePlaceholder := fmt.Sprintf("$%d", 1+i)
|
||||||
|
valuePlaceholders = append(valuePlaceholders, valuePlaceholder)
|
||||||
|
updateOnConflict = append(updateOnConflict,
|
||||||
|
fmt.Sprintf("%s = %s", model.OrderedColumns[i], valuePlaceholder))
|
||||||
|
}
|
||||||
|
|
||||||
|
baseQuery := `INSERT INTO %v.%v (%v) VALUES(%v)
|
||||||
|
ON CONFLICT (header_id, log_id) DO UPDATE SET %v;`
|
||||||
|
|
||||||
|
return fmt.Sprintf(baseQuery,
|
||||||
|
model.SchemaName,
|
||||||
|
model.TableName,
|
||||||
|
joinOrderedColumns(model.OrderedColumns),
|
||||||
|
strings.Join(valuePlaceholders, ", "),
|
||||||
|
strings.Join(updateOnConflict, ", "))
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
Create generates an insertion query and persists to the DB, given a slice of InsertionModels.
|
||||||
|
ColumnValues are restricted to []byte, bool, float64, int64, string, time.Time.
|
||||||
|
|
||||||
|
testModel = shared.InsertionModel{
|
||||||
|
SchemaName: "public"
|
||||||
|
TableName: "testEvent",
|
||||||
|
OrderedColumns: []string{"header_id", "log_id", "variable1"},
|
||||||
|
ColumnValues: ColumnValues{
|
||||||
|
"header_id": 303
|
||||||
|
"log_id": "808",
|
||||||
|
"variable1": "value1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
func Create(models []InsertionModel, db *postgres.DB) error {
|
||||||
|
if len(models) == 0 {
|
||||||
|
return fmt.Errorf("repository got empty model slice")
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, dbErr := db.Beginx()
|
||||||
|
if dbErr != nil {
|
||||||
|
return dbErr
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, model := range models {
|
||||||
|
// Maps can't be iterated over in a reliable manner, so we rely on OrderedColumns to define the order to insert
|
||||||
|
// tx.Exec is variadically typed in the args, so if we wrap in []interface{} we can apply them all automatically
|
||||||
|
var args []interface{}
|
||||||
|
for _, col := range model.OrderedColumns {
|
||||||
|
value := model.ColumnValues[col]
|
||||||
|
// Check whether or not PG can accept the type of value in the model
|
||||||
|
okPgValue := driver.IsValue(value)
|
||||||
|
if !okPgValue {
|
||||||
|
logrus.WithField("model", model).Errorf("PG cannot handle value of this type: %T", value)
|
||||||
|
return ErrUnsupportedValue(value)
|
||||||
|
}
|
||||||
|
args = append(args, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
insertionQuery := GetMemoizedQuery(model)
|
||||||
|
_, execErr := tx.Exec(insertionQuery, args...) // couldn't pass varying types in bulk with args :: []string
|
||||||
|
|
||||||
|
if execErr != nil {
|
||||||
|
rollbackErr := tx.Rollback()
|
||||||
|
if rollbackErr != nil {
|
||||||
|
logrus.Error("failed to rollback ", rollbackErr)
|
||||||
|
}
|
||||||
|
return execErr
|
||||||
|
}
|
||||||
|
|
||||||
|
_, logErr := tx.Exec(SetLogTransformedQuery, model.ColumnValues[LogFK])
|
||||||
|
|
||||||
|
if logErr != nil {
|
||||||
|
utils.RollbackAndLogFailure(tx, logErr, "header_sync_logs.transformed")
|
||||||
|
return logErr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return tx.Commit()
|
||||||
|
}
|
||||||
|
|
||||||
|
func joinOrderedColumns(columns []ColumnName) string {
|
||||||
|
var stringColumns []string
|
||||||
|
for _, columnName := range columns {
|
||||||
|
stringColumns = append(stringColumns, string(columnName))
|
||||||
|
}
|
||||||
|
return strings.Join(stringColumns, ", ")
|
||||||
|
}
|
||||||
|
204
libraries/shared/factories/event/repository_test.go
Normal file
204
libraries/shared/factories/event/repository_test.go
Normal file
@ -0,0 +1,204 @@
|
|||||||
|
// VulcanizeDB
|
||||||
|
// Copyright © 2019 Vulcanize
|
||||||
|
|
||||||
|
// This program is free software: you can redistribute it and/or modify
|
||||||
|
// it under the terms of the GNU Affero General Public License as published by
|
||||||
|
// the Free Software Foundation, either version 3 of the License, or
|
||||||
|
// (at your option) any later version.
|
||||||
|
|
||||||
|
// This program is distributed in the hope that it will be useful,
|
||||||
|
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
// GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
// You should have received a copy of the GNU Affero General Public License
|
||||||
|
// along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
package event_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
. "github.com/onsi/ginkgo"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/vulcanize/vulcanizedb/libraries/shared/factories/event"
|
||||||
|
"github.com/vulcanize/vulcanizedb/libraries/shared/test_data"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres/repositories"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/fakes"
|
||||||
|
"github.com/vulcanize/vulcanizedb/test_config"
|
||||||
|
"math/big"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ = Describe("Repository", func() {
|
||||||
|
var db *postgres.DB
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
db = test_config.NewTestDB(test_config.NewTestNode())
|
||||||
|
test_config.CleanTestDB(db)
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("Create", func() {
|
||||||
|
const createTestEventTableQuery = `CREATE TABLE public.testEvent(
|
||||||
|
id SERIAL PRIMARY KEY,
|
||||||
|
header_id INTEGER NOT NULL REFERENCES headers (id) ON DELETE CASCADE,
|
||||||
|
log_id BIGINT NOT NULL REFERENCES header_sync_logs (id) ON DELETE CASCADE,
|
||||||
|
variable1 TEXT,
|
||||||
|
UNIQUE (header_id, log_id)
|
||||||
|
);`
|
||||||
|
|
||||||
|
var (
|
||||||
|
headerID, logID int64
|
||||||
|
headerRepository repositories.HeaderRepository
|
||||||
|
testModel event.InsertionModel
|
||||||
|
)
|
||||||
|
|
||||||
|
BeforeEach(func() {
|
||||||
|
_, tableErr := db.Exec(createTestEventTableQuery)
|
||||||
|
Expect(tableErr).NotTo(HaveOccurred())
|
||||||
|
headerRepository = repositories.NewHeaderRepository(db)
|
||||||
|
var insertHeaderErr error
|
||||||
|
headerID, insertHeaderErr = headerRepository.CreateOrUpdateHeader(fakes.FakeHeader)
|
||||||
|
Expect(insertHeaderErr).NotTo(HaveOccurred())
|
||||||
|
headerSyncLog := test_data.CreateTestLog(headerID, db)
|
||||||
|
logID = headerSyncLog.ID
|
||||||
|
|
||||||
|
testModel = event.InsertionModel{
|
||||||
|
SchemaName: "public",
|
||||||
|
TableName: "testEvent",
|
||||||
|
OrderedColumns: []event.ColumnName{
|
||||||
|
event.HeaderFK, event.LogFK, "variable1",
|
||||||
|
},
|
||||||
|
ColumnValues: event.ColumnValues{
|
||||||
|
event.HeaderFK: headerID,
|
||||||
|
event.LogFK: logID,
|
||||||
|
"variable1": "value1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
AfterEach(func() {
|
||||||
|
db.MustExec(`DROP TABLE public.testEvent;`)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Needs to run before the other tests, since those insert keys in map
|
||||||
|
It("memoizes queries", func() {
|
||||||
|
Expect(len(event.ModelToQuery)).To(Equal(0))
|
||||||
|
event.GetMemoizedQuery(testModel)
|
||||||
|
Expect(len(event.ModelToQuery)).To(Equal(1))
|
||||||
|
event.GetMemoizedQuery(testModel)
|
||||||
|
Expect(len(event.ModelToQuery)).To(Equal(1))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("persists a model to postgres", func() {
|
||||||
|
createErr := event.Create([]event.InsertionModel{testModel}, db)
|
||||||
|
Expect(createErr).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var res TestEvent
|
||||||
|
dbErr := db.Get(&res, `SELECT log_id, variable1 FROM public.testEvent;`)
|
||||||
|
Expect(dbErr).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
Expect(res.LogID).To(Equal(fmt.Sprint(testModel.ColumnValues[event.LogFK])))
|
||||||
|
Expect(res.Variable1).To(Equal(testModel.ColumnValues["variable1"]))
|
||||||
|
})
|
||||||
|
|
||||||
|
Describe("returns errors", func() {
|
||||||
|
It("for empty model slice", func() {
|
||||||
|
err := event.Create([]event.InsertionModel{}, db)
|
||||||
|
Expect(err).To(MatchError("repository got empty model slice"))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("for failed SQL inserts", func() {
|
||||||
|
header := fakes.GetFakeHeader(1)
|
||||||
|
headerID, headerErr := headerRepository.CreateOrUpdateHeader(header)
|
||||||
|
Expect(headerErr).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
brokenModel := event.InsertionModel{
|
||||||
|
SchemaName: "public",
|
||||||
|
TableName: "testEvent",
|
||||||
|
// Wrong name of last column compared to DB, will generate incorrect query
|
||||||
|
OrderedColumns: []event.ColumnName{
|
||||||
|
event.HeaderFK, event.LogFK, "variable2",
|
||||||
|
},
|
||||||
|
ColumnValues: event.ColumnValues{
|
||||||
|
event.HeaderFK: headerID,
|
||||||
|
event.LogFK: logID,
|
||||||
|
"variable1": "value1",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove cached queries, or we won't generate a new (incorrect) one
|
||||||
|
delete(event.ModelToQuery, "publictestEvent")
|
||||||
|
|
||||||
|
createErr := event.Create([]event.InsertionModel{brokenModel}, db)
|
||||||
|
// Remove incorrect query, so other tests won't get it
|
||||||
|
delete(event.ModelToQuery, "publictestEvent")
|
||||||
|
|
||||||
|
Expect(createErr).To(HaveOccurred())
|
||||||
|
})
|
||||||
|
|
||||||
|
It("for unsupported types in ColumnValue", func() {
|
||||||
|
unsupportedValue := big.NewInt(5)
|
||||||
|
testModel = event.InsertionModel{
|
||||||
|
SchemaName: "public",
|
||||||
|
TableName: "testEvent",
|
||||||
|
OrderedColumns: []event.ColumnName{
|
||||||
|
event.HeaderFK, event.LogFK, "variable1",
|
||||||
|
},
|
||||||
|
ColumnValues: event.ColumnValues{
|
||||||
|
event.HeaderFK: headerID,
|
||||||
|
event.LogFK: logID,
|
||||||
|
"variable1": unsupportedValue,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createErr := event.Create([]event.InsertionModel{testModel}, db)
|
||||||
|
Expect(createErr).To(MatchError(event.ErrUnsupportedValue(unsupportedValue)))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
It("upserts queries with conflicting source", func() {
|
||||||
|
conflictingModel := event.InsertionModel{
|
||||||
|
SchemaName: "public",
|
||||||
|
TableName: "testEvent",
|
||||||
|
OrderedColumns: []event.ColumnName{
|
||||||
|
event.HeaderFK, event.LogFK, "variable1",
|
||||||
|
},
|
||||||
|
ColumnValues: event.ColumnValues{
|
||||||
|
event.HeaderFK: headerID,
|
||||||
|
event.LogFK: logID,
|
||||||
|
"variable1": "conflictingValue",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
createErr := event.Create([]event.InsertionModel{testModel, conflictingModel}, db)
|
||||||
|
Expect(createErr).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var res TestEvent
|
||||||
|
dbErr := db.Get(&res, `SELECT log_id, variable1 FROM public.testEvent;`)
|
||||||
|
Expect(dbErr).NotTo(HaveOccurred())
|
||||||
|
Expect(res.Variable1).To(Equal(conflictingModel.ColumnValues["variable1"]))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("generates correct queries", func() {
|
||||||
|
actualQuery := event.GenerateInsertionQuery(testModel)
|
||||||
|
expectedQuery := `INSERT INTO public.testEvent (header_id, log_id, variable1) VALUES($1, $2, $3)
|
||||||
|
ON CONFLICT (header_id, log_id) DO UPDATE SET header_id = $1, log_id = $2, variable1 = $3;`
|
||||||
|
Expect(actualQuery).To(Equal(expectedQuery))
|
||||||
|
})
|
||||||
|
|
||||||
|
It("marks log transformed", func() {
|
||||||
|
createErr := event.Create([]event.InsertionModel{testModel}, db)
|
||||||
|
Expect(createErr).NotTo(HaveOccurred())
|
||||||
|
|
||||||
|
var logTransformed bool
|
||||||
|
getErr := db.Get(&logTransformed, `SELECT transformed FROM public.header_sync_logs WHERE id = $1`, logID)
|
||||||
|
Expect(getErr).NotTo(HaveOccurred())
|
||||||
|
Expect(logTransformed).To(BeTrue())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
type TestEvent struct {
|
||||||
|
LogID string `db:"log_id"`
|
||||||
|
Variable1 string
|
||||||
|
}
|
@ -30,6 +30,7 @@ type Transformer struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (transformer Transformer) NewTransformer(db *postgres.DB) transformer.EventTransformer {
|
func (transformer Transformer) NewTransformer(db *postgres.DB) transformer.EventTransformer {
|
||||||
|
transformer.Converter.SetDB(db)
|
||||||
transformer.Repository.SetDB(db)
|
transformer.Repository.SetDB(db)
|
||||||
return transformer
|
return transformer
|
||||||
}
|
}
|
||||||
@ -42,13 +43,7 @@ func (transformer Transformer) Execute(logs []core.HeaderSyncLog) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
entities, err := transformer.Converter.ToEntities(config.ContractAbi, logs)
|
models, err := transformer.Converter.ToModels(config.ContractAbi, logs)
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("error converting logs to entities in %v: %v", transformerName, err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
models, err := transformer.Converter.ToModels(entities)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logrus.Errorf("error converting entities to models in %v: %v", transformerName, err)
|
logrus.Errorf("error converting entities to models in %v: %v", transformerName, err)
|
||||||
return err
|
return err
|
||||||
|
@ -66,12 +66,11 @@ var _ = Describe("Transformer", func() {
|
|||||||
err := t.Execute([]core.HeaderSyncLog{})
|
err := t.Execute([]core.HeaderSyncLog{})
|
||||||
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(converter.ToEntitiesCalledCounter).To(Equal(0))
|
|
||||||
Expect(converter.ToModelsCalledCounter).To(Equal(0))
|
Expect(converter.ToModelsCalledCounter).To(Equal(0))
|
||||||
Expect(repository.CreateCalledCounter).To(Equal(0))
|
Expect(repository.CreateCalledCounter).To(Equal(0))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("converts an eth log to an entity", func() {
|
It("converts an eth log to a model", func() {
|
||||||
err := t.Execute(logs)
|
err := t.Execute(logs)
|
||||||
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
@ -79,26 +78,7 @@ var _ = Describe("Transformer", func() {
|
|||||||
Expect(converter.LogsToConvert).To(Equal(logs))
|
Expect(converter.LogsToConvert).To(Equal(logs))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("returns an error if converter fails", func() {
|
|
||||||
converter.ToEntitiesError = fakes.FakeError
|
|
||||||
|
|
||||||
err := t.Execute(logs)
|
|
||||||
|
|
||||||
Expect(err).To(HaveOccurred())
|
|
||||||
Expect(err).To(MatchError(fakes.FakeError))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("converts an entity to a model", func() {
|
|
||||||
converter.EntitiesToReturn = []interface{}{test_data.GenericEntity{}}
|
|
||||||
|
|
||||||
err := t.Execute(logs)
|
|
||||||
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
|
||||||
Expect(converter.EntitiesToConvert[0]).To(Equal(test_data.GenericEntity{}))
|
|
||||||
})
|
|
||||||
|
|
||||||
It("returns an error if converting to models fails", func() {
|
It("returns an error if converting to models fails", func() {
|
||||||
converter.EntitiesToReturn = []interface{}{test_data.GenericEntity{}}
|
|
||||||
converter.ToModelsError = fakes.FakeError
|
converter.ToModelsError = fakes.FakeError
|
||||||
|
|
||||||
err := t.Execute(logs)
|
err := t.Execute(logs)
|
||||||
@ -108,12 +88,12 @@ var _ = Describe("Transformer", func() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
It("persists the record", func() {
|
It("persists the record", func() {
|
||||||
converter.ModelsToReturn = []interface{}{test_data.GenericModel{}}
|
converter.ModelsToReturn = []event.InsertionModel{test_data.GenericModel}
|
||||||
|
|
||||||
err := t.Execute(logs)
|
err := t.Execute(logs)
|
||||||
|
|
||||||
Expect(err).NotTo(HaveOccurred())
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(repository.PassedModels[0]).To(Equal(test_data.GenericModel{}))
|
Expect(repository.PassedModels[0]).To(Equal(test_data.GenericModel))
|
||||||
})
|
})
|
||||||
|
|
||||||
It("returns error if persisting the record fails", func() {
|
It("returns error if persisting the record fails", func() {
|
||||||
|
@ -16,41 +16,29 @@
|
|||||||
|
|
||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import "github.com/vulcanize/vulcanizedb/pkg/core"
|
import (
|
||||||
|
"github.com/vulcanize/vulcanizedb/libraries/shared/factories/event"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||||
|
)
|
||||||
|
|
||||||
type MockConverter struct {
|
type MockConverter struct {
|
||||||
ToEntitiesError error
|
|
||||||
PassedContractAddresses []string
|
|
||||||
ToModelsError error
|
ToModelsError error
|
||||||
entityConverterError error
|
|
||||||
modelConverterError error
|
|
||||||
ContractAbi string
|
ContractAbi string
|
||||||
LogsToConvert []core.HeaderSyncLog
|
LogsToConvert []core.HeaderSyncLog
|
||||||
EntitiesToConvert []interface{}
|
ModelsToReturn []event.InsertionModel
|
||||||
EntitiesToReturn []interface{}
|
PassedContractAddresses []string
|
||||||
ModelsToReturn []interface{}
|
SetDBCalled bool
|
||||||
ToEntitiesCalledCounter int
|
|
||||||
ToModelsCalledCounter int
|
ToModelsCalledCounter int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (converter *MockConverter) ToEntities(contractAbi string, ethLogs []core.HeaderSyncLog) ([]interface{}, error) {
|
func (converter *MockConverter) ToModels(abi string, logs []core.HeaderSyncLog) ([]event.InsertionModel, error) {
|
||||||
for _, log := range ethLogs {
|
converter.LogsToConvert = logs
|
||||||
converter.PassedContractAddresses = append(converter.PassedContractAddresses, log.Log.Address.Hex())
|
converter.ContractAbi = abi
|
||||||
}
|
converter.ToModelsCalledCounter = converter.ToModelsCalledCounter + 1
|
||||||
converter.ContractAbi = contractAbi
|
|
||||||
converter.LogsToConvert = ethLogs
|
|
||||||
return converter.EntitiesToReturn, converter.ToEntitiesError
|
|
||||||
}
|
|
||||||
|
|
||||||
func (converter *MockConverter) ToModels(entities []interface{}) ([]interface{}, error) {
|
|
||||||
converter.EntitiesToConvert = entities
|
|
||||||
return converter.ModelsToReturn, converter.ToModelsError
|
return converter.ModelsToReturn, converter.ToModelsError
|
||||||
}
|
}
|
||||||
|
|
||||||
func (converter *MockConverter) SetToEntityConverterError(err error) {
|
func (converter *MockConverter) SetDB(db *postgres.DB) {
|
||||||
converter.entityConverterError = err
|
converter.SetDBCalled = true
|
||||||
}
|
|
||||||
|
|
||||||
func (converter *MockConverter) SetToModelConverterError(err error) {
|
|
||||||
converter.modelConverterError = err
|
|
||||||
}
|
}
|
||||||
|
@ -17,17 +17,18 @@
|
|||||||
package mocks
|
package mocks
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"github.com/vulcanize/vulcanizedb/libraries/shared/factories/event"
|
||||||
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MockEventRepository struct {
|
type MockEventRepository struct {
|
||||||
createError error
|
createError error
|
||||||
PassedModels []interface{}
|
PassedModels []event.InsertionModel
|
||||||
SetDbCalled bool
|
SetDbCalled bool
|
||||||
CreateCalledCounter int
|
CreateCalledCounter int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (repository *MockEventRepository) Create(models []interface{}) error {
|
func (repository *MockEventRepository) Create(models []event.InsertionModel) error {
|
||||||
repository.PassedModels = models
|
repository.PassedModels = models
|
||||||
repository.CreateCalledCounter++
|
repository.CreateCalledCounter++
|
||||||
|
|
||||||
|
@ -20,14 +20,12 @@ import (
|
|||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/common/hexutil"
|
"github.com/ethereum/go-ethereum/common/hexutil"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/vulcanize/vulcanizedb/libraries/shared/factories/event"
|
||||||
"github.com/vulcanize/vulcanizedb/libraries/shared/transformer"
|
"github.com/vulcanize/vulcanizedb/libraries/shared/transformer"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
type GenericModel struct{}
|
|
||||||
type GenericEntity struct{}
|
|
||||||
|
|
||||||
var startingBlockNumber = rand.Int63()
|
var startingBlockNumber = rand.Int63()
|
||||||
var topic0 = "0x" + randomString(64)
|
var topic0 = "0x" + randomString(64)
|
||||||
|
|
||||||
@ -44,6 +42,8 @@ var GenericTestLog = func() types.Log {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var GenericModel = event.InsertionModel{}
|
||||||
|
|
||||||
var GenericTestConfig = transformer.EventTransformerConfig{
|
var GenericTestConfig = transformer.EventTransformerConfig{
|
||||||
TransformerName: "generic-test-transformer",
|
TransformerName: "generic-test-transformer",
|
||||||
ContractAddresses: []string{fakeAddress().Hex()},
|
ContractAddresses: []string{fakeAddress().Hex()},
|
||||||
|
37
libraries/shared/test_data/test_helpers.go
Normal file
37
libraries/shared/test_data/test_helpers.go
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
package test_data
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/ethereum/go-ethereum/common"
|
||||||
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
. "github.com/onsi/gomega"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres"
|
||||||
|
"github.com/vulcanize/vulcanizedb/pkg/datastore/postgres/repositories"
|
||||||
|
"math/rand"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a header sync log to reference in an event, returning inserted header sync log
|
||||||
|
func CreateTestLog(headerID int64, db *postgres.DB) core.HeaderSyncLog {
|
||||||
|
log := types.Log{
|
||||||
|
Address: common.Address{},
|
||||||
|
Topics: nil,
|
||||||
|
Data: nil,
|
||||||
|
BlockNumber: 0,
|
||||||
|
TxHash: common.Hash{},
|
||||||
|
TxIndex: uint(rand.Int31()),
|
||||||
|
BlockHash: common.Hash{},
|
||||||
|
Index: 0,
|
||||||
|
Removed: false,
|
||||||
|
}
|
||||||
|
headerSyncLogRepository := repositories.NewHeaderSyncLogRepository(db)
|
||||||
|
insertLogsErr := headerSyncLogRepository.CreateHeaderSyncLogs(headerID, []types.Log{log})
|
||||||
|
Expect(insertLogsErr).NotTo(HaveOccurred())
|
||||||
|
headerSyncLogs, getLogsErr := headerSyncLogRepository.GetUntransformedHeaderSyncLogs()
|
||||||
|
Expect(getLogsErr).NotTo(HaveOccurred())
|
||||||
|
for _, headerSyncLog := range headerSyncLogs {
|
||||||
|
if headerSyncLog.Log.TxIndex == log.TxIndex {
|
||||||
|
return headerSyncLog
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic("couldn't find inserted test log")
|
||||||
|
}
|
@ -47,7 +47,8 @@ var _ = Describe("Interface Getter", func() {
|
|||||||
transactionConverter := rpc2.NewRpcTransactionConverter(ethClient)
|
transactionConverter := rpc2.NewRpcTransactionConverter(ethClient)
|
||||||
blockChain := geth.NewBlockChain(blockChainClient, rpcClient, node, transactionConverter)
|
blockChain := geth.NewBlockChain(blockChainClient, rpcClient, node, transactionConverter)
|
||||||
interfaceGetter := getter.NewInterfaceGetter(blockChain)
|
interfaceGetter := getter.NewInterfaceGetter(blockChain)
|
||||||
abi := interfaceGetter.GetABI(constants.PublicResolverAddress, blockNumber)
|
abi, err := interfaceGetter.GetABI(constants.PublicResolverAddress, blockNumber)
|
||||||
|
Expect(err).NotTo(HaveOccurred())
|
||||||
Expect(abi).To(Equal(expectedABI))
|
Expect(abi).To(Equal(expectedABI))
|
||||||
_, err = geth.ParseAbi(abi)
|
_, err = geth.ParseAbi(abi)
|
||||||
Expect(err).ToNot(HaveOccurred())
|
Expect(err).ToNot(HaveOccurred())
|
||||||
|
@ -17,6 +17,7 @@
|
|||||||
package getter
|
package getter
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"github.com/vulcanize/vulcanizedb/pkg/contract_watcher/shared/constants"
|
"github.com/vulcanize/vulcanizedb/pkg/contract_watcher/shared/constants"
|
||||||
"github.com/vulcanize/vulcanizedb/pkg/contract_watcher/shared/fetcher"
|
"github.com/vulcanize/vulcanizedb/pkg/contract_watcher/shared/fetcher"
|
||||||
"github.com/vulcanize/vulcanizedb/pkg/core"
|
"github.com/vulcanize/vulcanizedb/pkg/core"
|
||||||
@ -40,14 +41,18 @@ func NewInterfaceGetter(blockChain core.BlockChain) *interfaceGetter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Used to construct a custom ABI based on the results from calling supportsInterface
|
// Used to construct a custom ABI based on the results from calling supportsInterface
|
||||||
func (g *interfaceGetter) GetABI(resolverAddr string, blockNumber int64) string {
|
func (g *interfaceGetter) GetABI(resolverAddr string, blockNumber int64) (string, error) {
|
||||||
a := constants.SupportsInterfaceABI
|
a := constants.SupportsInterfaceABI
|
||||||
args := make([]interface{}, 1)
|
args := make([]interface{}, 1)
|
||||||
args[0] = constants.MetaSig.Bytes()
|
args[0] = constants.MetaSig.Bytes()
|
||||||
supports, err := g.getSupportsInterface(a, resolverAddr, blockNumber, args)
|
supports, err := g.getSupportsInterface(a, resolverAddr, blockNumber, args)
|
||||||
if err != nil || !supports {
|
if err != nil {
|
||||||
return ""
|
return "", fmt.Errorf("call to getSupportsInterface failed: %v", err)
|
||||||
}
|
}
|
||||||
|
if !supports {
|
||||||
|
return "", fmt.Errorf("contract does not support interface")
|
||||||
|
}
|
||||||
|
|
||||||
abiStr := `[`
|
abiStr := `[`
|
||||||
args[0] = constants.AddrChangeSig.Bytes()
|
args[0] = constants.AddrChangeSig.Bytes()
|
||||||
supports, err = g.getSupportsInterface(a, resolverAddr, blockNumber, args)
|
supports, err = g.getSupportsInterface(a, resolverAddr, blockNumber, args)
|
||||||
@ -91,7 +96,7 @@ func (g *interfaceGetter) GetABI(resolverAddr string, blockNumber int64) string
|
|||||||
}
|
}
|
||||||
abiStr = abiStr[:len(abiStr)-1] + `]`
|
abiStr = abiStr[:len(abiStr)-1] + `]`
|
||||||
|
|
||||||
return abiStr
|
return abiStr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use this method to check whether or not a contract supports a given method/event interface
|
// Use this method to check whether or not a contract supports a given method/event interface
|
||||||
|
@ -17,7 +17,8 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
log "github.com/sirupsen/logrus"
|
"github.com/jmoiron/sqlx"
|
||||||
|
logrus "github.com/sirupsen/logrus"
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@ -31,7 +32,7 @@ import (
|
|||||||
func LoadPostgres(database config.Database, node core.Node) postgres.DB {
|
func LoadPostgres(database config.Database, node core.Node) postgres.DB {
|
||||||
db, err := postgres.NewDB(database, node)
|
db, err := postgres.NewDB(database, node)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal("Error loading postgres: ", err)
|
logrus.Fatal("Error loading postgres: ", err)
|
||||||
}
|
}
|
||||||
return *db
|
return *db
|
||||||
}
|
}
|
||||||
@ -40,7 +41,7 @@ func ReadAbiFile(abiFilepath string) string {
|
|||||||
abiFilepath = AbsFilePath(abiFilepath)
|
abiFilepath = AbsFilePath(abiFilepath)
|
||||||
abi, err := geth.ReadAbiFile(abiFilepath)
|
abi, err := geth.ReadAbiFile(abiFilepath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("Error reading ABI file at \"%s\"\n %v", abiFilepath, err)
|
logrus.Fatalf("Error reading ABI file at \"%s\"\n %v", abiFilepath, err)
|
||||||
}
|
}
|
||||||
return abi
|
return abi
|
||||||
}
|
}
|
||||||
@ -60,12 +61,12 @@ func GetAbi(abiFilepath string, contractHash string, network string) string {
|
|||||||
} else {
|
} else {
|
||||||
url := geth.GenURL(network)
|
url := geth.GenURL(network)
|
||||||
etherscan := geth.NewEtherScanClient(url)
|
etherscan := geth.NewEtherScanClient(url)
|
||||||
log.Printf("No ABI supplied. Retrieving ABI from Etherscan: %s", url)
|
logrus.Printf("No ABI supplied. Retrieving ABI from Etherscan: %s", url)
|
||||||
contractAbiString, _ = etherscan.GetAbi(contractHash)
|
contractAbiString, _ = etherscan.GetAbi(contractHash)
|
||||||
}
|
}
|
||||||
_, err := geth.ParseAbi(contractAbiString)
|
_, err := geth.ParseAbi(contractAbiString)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln("Invalid ABI: ", err)
|
logrus.Fatalln("Invalid ABI: ", err)
|
||||||
}
|
}
|
||||||
return contractAbiString
|
return contractAbiString
|
||||||
}
|
}
|
||||||
@ -79,3 +80,11 @@ func RequestedBlockNumber(blockNumber *int64) *big.Int {
|
|||||||
}
|
}
|
||||||
return _blockNumber
|
return _blockNumber
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RollbackAndLogFailure(tx *sqlx.Tx, txErr error, fieldName string) {
|
||||||
|
rollbackErr := tx.Rollback()
|
||||||
|
if rollbackErr != nil {
|
||||||
|
logrus.WithFields(logrus.Fields{"rollbackErr": rollbackErr, "txErr": txErr}).
|
||||||
|
Warnf("failed to rollback transaction after failing to insert %s", fieldName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user