diff --git a/abi2/abi2.go b/abi2/abi2.go new file mode 100644 index 00000000..09cf4a97 --- /dev/null +++ b/abi2/abi2.go @@ -0,0 +1,927 @@ +// decode abi data into flat rows using a JSON ABI file for the schema +package abi2 + +import ( + "bytes" + "context" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "log/slog" + "slices" + "strconv" + "strings" + + "github.com/indexsupply/x/bint" + "github.com/indexsupply/x/e2pg" + "github.com/indexsupply/x/isxhash" + + "github.com/holiman/uint256" + "github.com/jackc/pgx/v5" + "github.com/jackc/pgx/v5/pgconn" + "github.com/jackc/pgx/v5/pgxpool" +) + +type atype struct { + kind byte + size int + static bool + + // query annotations + sel bool + pos int + + // tuple + fields []atype + + // array + length int + elem *atype +} + +func (t atype) String() string { + switch t.kind { + case 'a': + switch t.length { + case 0: + return fmt.Sprintf("[]%s", t.elem.String()) + default: + return fmt.Sprintf("[%d]%s", t.length, t.elem.String()) + } + case 't': + var s strings.Builder + s.WriteString("tuple(") + for i := range t.fields { + s.WriteString(t.fields[i].String()) + if i+1 != len(t.fields) { + s.WriteString(",") + } + } + s.WriteString(")") + return s.String() + case 's': + return "static" + case 'd': + return "dynamic" + default: + return fmt.Sprintf("unkown-type=%d", t.kind) + } +} + +func (t atype) hasSelect() bool { + switch { + case t.kind == 'a': + return t.elem.hasSelect() + case t.kind == 't': + for i := range t.fields { + if t.fields[i].hasSelect() { + return true + } + } + return false + default: + return t.sel + } +} + +func (t atype) hasKind(k byte) bool { + switch t.kind { + case 'a': + for tt := t.elem; tt != nil; tt = tt.elem { + if tt.kind == k { + return true + } + } + return false + case 't': + for i := range t.fields { + if t.fields[i].kind == k || t.fields[i].hasKind(k) { + return true + } + } + return false + default: + return false + } +} + +func (t atype) selected() []atype { + var res []atype + switch t.kind { + case 't': + for i := range t.fields { + res = append(res, t.fields[i].selected()...) + } + case 'a': + res = append(res, t.elem.selected()...) + default: + if t.sel { + res = append(res, t) + } + } + return res +} + +func hasStatic(t atype) bool { + switch { + case t.kind == 'd': + return false + case t.kind == 'a' && t.length == 0: + return false + case t.kind == 'a' && !hasStatic(*t.elem): + return false + case t.kind == 't': + for _, f := range t.fields { + if !hasStatic(f) { + return false + } + } + } + return true +} + +func sizeof(t atype) int { + switch t.kind { + case 's': + return 32 + case 'd': + return 0 + case 'a': + return t.length * sizeof(*t.elem) + case 't': + var n int + for i := range t.fields { + n += sizeof(t.fields[i]) + } + return n + default: + panic("unkown type") + } +} + +func static() atype { + return atype{kind: 's', static: true, size: 32} +} + +func dynamic() atype { + return atype{kind: 'd', size: 0} +} + +func array(e atype) atype { + return atype{kind: 'a', elem: &e} +} + +func arrayK(k int, e atype) atype { + t := atype{kind: 'a', elem: &e, length: k} + t.size = sizeof(t) + t.static = hasStatic(t) + return t +} + +func tuple(fields ...atype) atype { + t := atype{kind: 't', fields: fields} + t.size = sizeof(t) + t.static = hasStatic(t) + return t +} + +func sel(pos int, t atype) atype { + t.pos = pos + t.sel = true + return t +} + +type row [][]byte + +func (r *row) write(i int, d []byte) { + (*r)[i] = slices.Grow((*r)[i], len(d)) + (*r)[i] = (*r)[i][:len(d)] + copy((*r)[i], d) +} + +func (r *Result) Len() int { + return r.n +} + +func (r *Result) At(i int) row { + return r.collection[i] +} + +func (r *Result) GetRow() row { + r.n++ + if r.n >= len(r.collection) { + r.collection = append(r.collection, make(row, r.ncols)) + } + return r.collection[r.n-1] +} + +func (r *Result) Bytes() [][][]byte { + var res = make([][][]byte, r.Len()) + for i := 0; i < r.Len(); i++ { + res[i] = append(res[i], r.collection[i]...) + } + return res +} + +type Result struct { + singleton row + collection []row + t atype + n int + ncols int +} + +func NewResult(t atype) *Result { + r := &Result{} + r.t = t + r.ncols = len(t.selected()) + r.singleton = make(row, r.ncols) + return r +} + +// Decodes the ABI input data according to r's type +// that was specified in [NewResult] +func (r *Result) Scan(input []byte) error { + r.n = 0 //reset + if err := scan(r.singleton, r, input, r.t); err != nil { + return err + } + + // if there weren't any variadic selections + // then instantiate a new row and copy + // the singleton into that row + if r.Len() == 0 { + r.GetRow() + } + for i := 0; i < r.Len(); i++ { + for j := 0; j < len(r.singleton); j++ { + if len(r.singleton[j]) > 0 { + r.collection[i].write(j, r.singleton[j]) + } + } + } + return nil +} + +func scan(r row, res *Result, input []byte, t atype) error { + switch t.kind { + case 's': + if len(input) < 32 { + return errors.New("EOF") + } + if t.sel { + r[t.pos] = input[:32] + } + case 'd': + length := int(bint.Decode(input[:32])) + if length == 0 { + return nil + } + if len(input) < 32+length { + return errors.New("EOF") + } + if t.sel { + r[t.pos] = input[32 : 32+length] + } + case 'a': + if !t.hasSelect() { + return nil + } + var length, start, pos = t.length, 0, 0 + if length <= 0 { // dynamic sized array + if len(input) < 32 { + return errors.New("EOF") + } + length, start, pos = int(bint.Decode(input[:32])), 32, 32 + } + for i := 0; i < length; i++ { + if !t.hasKind('a') { + r = res.GetRow() + } + switch { + case t.elem.static: + if len(input) < pos { + return errors.New("EOF") + } + err := scan(r, res, input[pos:], *t.elem) + if err != nil { + return err + } + pos += t.elem.size + default: + if len(input) < pos+32 { + return errors.New("EOF") + } + offset := int(bint.Decode(input[pos : pos+32])) + if len(input) < start+offset { + return errors.New("EOF") + } + err := scan(r, res, input[start+offset:], *t.elem) + if err != nil { + return errors.New("EOF") + } + pos += 32 + } + } + return nil + case 't': + if !t.hasSelect() { + return nil + } + var pos int + for _, f := range t.fields { + switch { + case f.static: + if len(input) < pos { + return errors.New("EOF") + } + err := scan(r, res, input[pos:], f) + if err != nil { + return errors.New("EOF") + } + pos += f.size + default: + if len(input) < pos+32 { + return errors.New("EOF") + } + offset := int(bint.Decode(input[pos : pos+32])) + if len(input) < offset { + return errors.New("EOF") + } + err := scan(r, res, input[offset:], f) + if err != nil { + return errors.New("EOF") + } + pos += 32 + } + } + return nil + default: + panic("unknown type") + } + return nil +} + +type Input struct { + Indexed bool `json:"indexed"` + Name string `json:"name"` + Type string `json:"type"` + Components []Input `json:"components"` + + Column string `json:"column"` + Pos int `json:"column_pos"` +} + +func parseArray(elm atype, s string) atype { + if !strings.Contains(s, "]") { + return elm + } + var num string + for i := len(s) - 2; i != 0; i-- { + if s[i] == '[' { + break + } + num += string(s[i]) + } + if len(num) == 0 { + return array(parseArray(elm, s[:len(s)-2])) + } + k, err := strconv.Atoi(num) + if err != nil { + panic("abi/schema: array contains non-number length") + } + return arrayK(k, parseArray(elm, s[:len(s)-len(num)-2])) +} + +func (inp Input) ABIType(pos int) (int, atype) { + var base atype + switch { + case len(inp.Components) > 0: + var fields []atype + for i := range inp.Components { + var f atype + pos, f = inp.Components[i].ABIType(pos) + fields = append(fields, f) + } + base = tuple(fields...) + case strings.HasPrefix(inp.Type, "bytes"): + switch { + case strings.TrimSuffix(strings.TrimPrefix(inp.Type, "bytes"), "[") == "": + base = dynamic() + default: + base = static() + } + case strings.HasPrefix(inp.Type, "string"): + base = dynamic() + default: + base = static() + } + if len(inp.Column) > 0 { + base.sel = true + base.pos = pos + pos++ + } + return pos, parseArray(base, inp.Type) +} + +func (inp Input) Signature() string { + if !strings.HasPrefix(inp.Type, "tuple") { + return inp.Type + } + var s strings.Builder + s.WriteString("(") + for i, c := range inp.Components { + s.WriteString(c.Signature()) + if i+1 < len(inp.Components) { + s.WriteString(",") + } + } + s.WriteString(")") + return strings.Replace(inp.Type, "tuple", s.String(), 1) +} + +// Returns inputs that have specified a "column" field -- which indicates +// they are to be selected when ABI data is decoded using this Event/Input +// as an ABI schema. +// +// The returned list order coincides with the order of data +// in the log's Topics and the [Result]'s row. +func (inp Input) Selected() []Input { + var res []Input + for i := range inp.Components { + res = append(res, inp.Components[i].Selected()...) + } + if len(inp.Column) > 0 { + res = append(res, inp) + } + return res +} + +type Extra struct { + Table Table `json:"table"` + Metadata []string `json:"metadata"` +} + +type Event struct { + Anon bool `json:"anonymous"` + Name string `json:"name"` + Type string `json:"type"` + Inputs []Input `json:"inputs"` + Extra Extra `json:"extra"` +} + +func (e Event) ABIType() atype { + var fields []atype + var pos = 0 + for i := range e.Inputs { + if e.Inputs[i].Indexed { + continue + } + var f atype + pos, f = e.Inputs[i].ABIType(pos) + fields = append(fields, f) + } + return tuple(fields...) +} + +func (e Event) SignatureHash() []byte { + return isxhash.Keccak([]byte(e.Signature())) +} + +func (e Event) Signature() string { + var s strings.Builder + s.WriteString(e.Name) + s.WriteString("(") + for i := range e.Inputs { + s.WriteString(e.Inputs[i].Signature()) + if i+1 < len(e.Inputs) { + s.WriteString(",") + } + } + s.WriteString(")") + return s.String() +} + +func (e Event) Selected() []Input { + var res []Input + for i := range e.Inputs { + res = append(res, e.Inputs[i].Selected()...) + } + return res +} + +func (e Event) onlyTopics() bool { + for _, inp := range e.Selected() { + if !inp.Indexed { + return false + } + } + return true +} + +type coldef struct { + Input Input + Column Column + Metadata bool +} + +// Implements the [e2pg.Integration] interface +type Integration struct { + Event Event + Columns []string + coldefs []coldef + + onlyTopics bool + resultCache *Result + sighash []byte +} + +type Conn interface { + CopyFrom(context.Context, pgx.Identifier, []string, pgx.CopyFromSource) (int64, error) + Exec(context.Context, string, ...any) (pgconn.CommandTag, error) + QueryRow(context.Context, string, ...any) pgx.Row + Query(context.Context, string, ...any) (pgx.Rows, error) +} + +// Queries the e2pg.events table using the event id. +// Same as calling [New] except the js is loaded from the database. +func Load(ctx context.Context, pgp *pgxpool.Pool, id uint64) (Integration, error) { + const q = `select abi from e2pg.events where id = $1` + var js []byte + err := pgp.QueryRow(ctx, q, id).Scan(&js) + if err != nil { + return Integration{}, fmt.Errorf("loading integration %d: %w", id, err) + } + return New(js) +} + +// js must be a json encoded abi event. +// +// {"name": "MyEent", "type": "event", "inputs": [{"indexed": true, "name": "f", "type": "t"}]} +// +// Each input may include the following field: +// +// "column": "my_column" +// +// This indicates that the input is mapped to the database column `my_column` +// +// A top level `extra` key is required to map an event onto a database table. +// +// {"extra": {"table": {"name": "my_table", column: [{"name": "my_column", "type": "db_type"]}}} +// +// Each column may include a `filter_op` and `filter_arg` key so +// that rows may be removed before they are inserted into the database. +// For example: +// +// {"name": "my_column", "type": "db_type", "filter_op": "contains", "filter_arg": ["0x000"]} +func New(js []byte) (Integration, error) { + ig := Integration{} + if err := json.Unmarshal(js, &ig.Event); err != nil { + return ig, fmt.Errorf("parsing event json: %w", err) + } + ig.onlyTopics = ig.Event.onlyTopics() + ig.resultCache = NewResult(ig.Event.ABIType()) + ig.sighash = ig.Event.SignatureHash() + + var ( + selected = ig.Event.Selected() + md = ig.Event.Extra.Metadata + cols = ig.Event.Extra.Table.Cols + ) + if len(cols) != len(selected)+len(md) { + return ig, fmt.Errorf("number of columns in table definitino must equal number of selected columns + metadata columns") + } + var colCount int + for _, input := range selected { + ig.Columns = append(ig.Columns, cols[colCount].Name) + ig.coldefs = append(ig.coldefs, coldef{ + Input: input, + Column: cols[colCount], + }) + colCount++ + } + for range md { + ig.Columns = append(ig.Columns, cols[colCount].Name) + ig.coldefs = append(ig.coldefs, coldef{ + Metadata: true, + Column: cols[colCount], + }) + colCount++ + } + return ig, nil +} + +func (ig Integration) Table() Table { return ig.Event.Extra.Table } + +func (ig Integration) Events(context.Context) [][]byte { return [][]byte{} } + +func (ig Integration) Delete(context.Context, e2pg.PG, uint64) error { return nil } + +func (ig Integration) Insert(ctx context.Context, pg e2pg.PG, blocks []e2pg.Block) (int64, error) { + var ( + err error + rows [][]any + lwc = &logWithCtx{ctx: ctx} + ) + for bidx := 0; bidx < len(blocks); bidx++ { + lwc.b = &blocks[bidx] + for ridx := 0; ridx < blocks[bidx].Receipts.Len(); ridx++ { + lwc.r = lwc.b.Receipts.At(ridx) + lwc.t = lwc.b.Transactions.At(ridx) + lwc.ridx = ridx + for lidx := 0; lidx < lwc.r.Logs.Len(); lidx++ { + lwc.l = lwc.r.Logs.At(lidx) + lwc.lidx = lidx + if !bytes.Equal(ig.sighash, lwc.l.Topics.At(0)) { + continue + } + rows, err = ig.process(rows, lwc) + if err != nil { + return 0, fmt.Errorf("processing log: %w", err) + } + } + } + } + return pg.CopyFrom( + ctx, + pgx.Identifier{ig.Table().Name}, + ig.Columns, + pgx.CopyFromRows(rows), + ) +} + +type logWithCtx struct { + ctx context.Context + b *e2pg.Block + t *e2pg.Transaction + r *e2pg.Receipt + l *e2pg.Log + ridx, lidx int +} + +func (lwc *logWithCtx) get(name string) any { + switch name { + case "chain_id": + return e2pg.ChainID(lwc.ctx) + case "block_hash": + return lwc.b.Hash() + case "block_num": + return lwc.b.Num() + case "tx_hash": + return lwc.t.Hash() + case "tx_idx": + return lwc.ridx + case "tx_signer": + d, err := lwc.t.Signer() + if err != nil { + slog.ErrorContext(lwc.ctx, "unable to derive signer", err) + return nil + } + return d + case "tx_to": + return lwc.t.To + case "tx_value": + return lwc.t.Value.Dec() + case "log_idx": + return lwc.lidx + case "log_addr": + return lwc.l.Address + default: + return nil + } +} + +func (ig Integration) process(rows [][]any, lwc *logWithCtx) ([][]any, error) { + switch { + case ig.onlyTopics: + row := make([]any, len(ig.coldefs)) + for i, def := range ig.coldefs { + switch { + case def.Input.Indexed: + d := dbtype(def.Input.Type, lwc.l.Topics.At(1+i)) + if b, ok := d.([]byte); ok && !def.Column.Accept(b) { + return nil, nil + } + row[i] = d + case def.Metadata: + d := lwc.get(def.Column.Name) + if b, ok := d.([]byte); ok && !def.Column.Accept(b) { + return nil, nil + } + row[i] = d + default: + return nil, fmt.Errorf("no rows for un-indexed data") + } + } + rows = append(rows, row) + default: + err := ig.resultCache.Scan(lwc.l.Data) + if err != nil { + return nil, fmt.Errorf("scanning abi data: %w", err) + } + for i := 0; i < ig.resultCache.Len(); i++ { + ictr, actr := 1, 0 + row := make([]any, len(ig.coldefs)) + for j, def := range ig.coldefs { + switch { + case def.Input.Indexed: + d := lwc.l.Topics.At(ictr) + row[j] = dbtype(def.Input.Type, d) + ictr++ + case def.Metadata: + d := lwc.get(def.Column.Name) + if b, ok := d.([]byte); ok && !def.Column.Accept(b) { + return nil, nil + } + row[j] = d + default: + d := ig.resultCache.At(i)[actr] + if !def.Column.Accept(d) { + return nil, nil + } + row[j] = dbtype(def.Input.Type, d) + actr++ + } + } + rows = append(rows, row) + } + } + return rows, nil +} + +func dbtype(t string, d []byte) any { + switch { + case strings.HasPrefix(t, "uint"): + bits, err := strconv.Atoi(strings.TrimPrefix(t, "uint")) + if err != nil { + return d + } + switch { + case bits > 64: + var x uint256.Int + x.SetBytes(d) + return x.Dec() + default: + return bint.Decode(d) + } + case t == "address": + if len(d) == 32 { + return d[12:] + } + return d + default: + return d + } +} + +func (ig Integration) Count(ctx context.Context, pg *pgxpool.Pool, chainID uint64) string { + const q = ` + select trim(to_char(count(*), '999,999,999,999')) + from %s + where chain_id = $1 + ` + var ( + res string + fq = fmt.Sprintf(q, ig.Table().Name) + ) + err := pg.QueryRow(ctx, fq, chainID).Scan(&res) + if err != nil { + return err.Error() + } + switch { + case res == "0": + return "pending" + case strings.HasPrefix(res, "-"): + return "pending" + default: + return res + } +} + +func (ig Integration) RecentRows(ctx context.Context, pgp *pgxpool.Pool, chainID uint64) []map[string]any { + var q strings.Builder + q.WriteString("select ") + for i, def := range ig.coldefs { + q.WriteString(def.Column.Name) + q.WriteString("::text") + if i+1 < len(ig.coldefs) { + q.WriteString(", ") + } + } + q.WriteString(" from ") + q.WriteString(ig.Table().Name) + q.WriteString(" where chain_id = $1") + q.WriteString(" order by block_num desc limit 10") + + rows, _ := pgp.Query(ctx, q.String(), chainID) + defer rows.Close() + res, err := pgx.CollectRows(rows, pgx.RowToMap) + if err != nil { + slog.Error("error", fmt.Errorf("querying integration: %w", err)) + return nil + } + return res +} + +type Column struct { + Name string `json:"name"` + Type string `json:"type"` + FilterOp string `json:"filter_op"` + FilterArg []string `json:"filter_arg"` +} + +// Uses FilterOp and FilterArg to check if d passes the filter. +// Current filter_ops include: contains and !contains +func (c Column) Accept(d []byte) bool { + switch { + case strings.HasSuffix(c.FilterOp, "contains"): + var res bool + for i := range c.FilterArg { + hb, _ := hex.DecodeString(c.FilterArg[i]) + if bytes.Equal(hb, d) { + res = true + break + } + } + if strings.HasPrefix(c.FilterOp, "!") { + return !res + } + return res + default: + return true + } +} + +type Table struct { + Name string `json:"name"` + Cols []Column `json:"columns"` +} + +func (t Table) Filters() []Column { + var res []Column + for i := range t.Cols { + if t.Cols[i].FilterOp != "" { + res = append(res, t.Cols[i]) + } + } + return res +} + +func CreateTable(ctx context.Context, pg Conn, t Table) error { + var s strings.Builder + s.WriteString(fmt.Sprintf("create table if not exists %s(", t.Name)) + for i := range t.Cols { + s.WriteString(fmt.Sprintf("%s %s", t.Cols[i].Name, t.Cols[i].Type)) + if i+1 == len(t.Cols) { + s.WriteString(")") + break + } + s.WriteString(",") + } + _, err := pg.Exec(ctx, s.String()) + return err +} + +func Indexes(ctx context.Context, pg Conn, table string) []map[string]any { + const q = ` + select indexname, indexdef + from pg_indexes + where tablename = $1 + ` + rows, _ := pg.Query(ctx, q, table) + res, err := pgx.CollectRows(rows, pgx.RowToMap) + if err != nil { + return []map[string]any{map[string]any{"error": err.Error()}} + } + return res +} + +func RowEstimate(ctx context.Context, pg Conn, table string) string { + const q = ` + select trim(to_char(reltuples, '999,999,999,999')) + from pg_class + where relname = $1 + ` + var res string + if err := pg.QueryRow(ctx, q, table).Scan(&res); err != nil { + return err.Error() + } + switch { + case res == "0": + return "pending" + case strings.HasPrefix(res, "-"): + return "pending" + default: + return res + } +} + +func TableSize(ctx context.Context, pg Conn, table string) string { + const q = `SELECT pg_size_pretty(pg_total_relation_size($1))` + var res string + if err := pg.QueryRow(ctx, q, table).Scan(&res); err != nil { + return err.Error() + } + return res +} diff --git a/abi2/abi2_test.go b/abi2/abi2_test.go new file mode 100644 index 00000000..9b46109e --- /dev/null +++ b/abi2/abi2_test.go @@ -0,0 +1,301 @@ +package abi2 + +import ( + "encoding/hex" + "strings" + "testing" + + "github.com/indexsupply/x/bint" + + "kr.dev/diff" +) + +func TestHasStatic(t *testing.T) { + cases := []struct { + t atype + want bool + }{ + { + static(), + true, + }, + { + dynamic(), + false, + }, + { + array(static()), + false, + }, + { + array(dynamic()), + false, + }, + { + arrayK(2, static()), + true, + }, + { + arrayK(3, arrayK(2, static())), + true, + }, + } + for _, tc := range cases { + got := hasStatic(tc.t) + diff.Test(t, t.Errorf, got, tc.want) + } +} +func TestSizeof(t *testing.T) { + cases := []struct { + t atype + want int + }{ + { + static(), + 32, + }, + { + dynamic(), + 0, + }, + { + array(static()), + 0, + }, + { + array(dynamic()), + 0, + }, + { + arrayK(2, static()), + 64, + }, + { + arrayK(3, arrayK(2, static())), + 192, + }, + } + for _, tc := range cases { + got := sizeof(tc.t) + diff.Test(t, t.Errorf, got, tc.want) + } +} + +func TestHasKind(t *testing.T) { + cases := []struct { + t atype + k byte + want bool + }{ + { + static(), + 's', + false, + }, + { + dynamic(), + 's', + false, + }, + { + array(dynamic()), + 'd', + true, + }, + { + array(array(dynamic())), + 'd', + true, + }, + { + array(array(dynamic())), + 'a', + true, + }, + { + tuple(array(dynamic())), + 's', + false, + }, + } + for _, tc := range cases { + got := tc.t.hasKind(tc.k) + diff.Test(t, t.Errorf, got, tc.want) + } +} + +func hb(s string) []byte { + s = strings.Map(func(r rune) rune { + switch { + case r >= '0' && r <= '9': + return r + case r >= 'a' && r <= 'f': + return r + default: + return -1 + } + }, strings.ToLower(s)) + b, _ := hex.DecodeString(s) + return b +} + +func n2b(x uint64) []byte { + var b [32]byte + bint.Encode(b[:], x) + return b[:] +} + +func TestScan(t *testing.T) { + cases := []struct { + desc string + input []byte + at atype + want [][][]byte + }{ + { + desc: "tuple of numbers", + input: hb(` + 000000000000000000000000000000000000000000000000000000000000002a + 000000000000000000000000000000000000000000000000000000000000002a + `), + at: tuple(sel(0, static()), sel(1, static())), + want: [][][]byte{ + [][]byte{n2b(42), n2b(42)}, + }, + }, + { + desc: "tuple of array with static types", + input: hb(` + 000000000000000000000000000000000000000000000000000000000000002a + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000002 + 000000000000000000000000000000000000000000000000000000000000002b + 000000000000000000000000000000000000000000000000000000000000002c + + `), + at: tuple(sel(0, static()), array(sel(1, static()))), + want: [][][]byte{ + [][]byte{n2b(42), n2b(43)}, + [][]byte{n2b(42), n2b(44)}, + }, + }, + { + desc: "tuple of array with dynamic types", + input: hb(` + 000000000000000000000000000000000000000000000000000000000000002a + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000002 + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000080 + 0000000000000000000000000000000000000000000000000000000000000003 + 666f6f0000000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000003 + 6261720000000000000000000000000000000000000000000000000000000000 + `), + at: tuple(sel(0, static()), array(sel(1, dynamic()))), + want: [][][]byte{ + [][]byte{n2b(42), []byte("foo")}, + [][]byte{n2b(42), []byte("bar")}, + }, + }, + { + desc: "dynamic nested list of dynamic types", + input: hb(` + 0000000000000000000000000000000000000000000000000000000000000002 + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000120 + 0000000000000000000000000000000000000000000000000000000000000002 + 0000000000000000000000000000000000000000000000000000000000000040 + 0000000000000000000000000000000000000000000000000000000000000080 + 0000000000000000000000000000000000000000000000000000000000000005 + 68656c6c6f000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000005 + 776f726c64000000000000000000000000000000000000000000000000000000 + 0000000000000000000000000000000000000000000000000000000000000001 + 0000000000000000000000000000000000000000000000000000000000000020 + 0000000000000000000000000000000000000000000000000000000000000003 + 6279650000000000000000000000000000000000000000000000000000000000 + `), + at: array(array(sel(0, dynamic()))), + want: [][][]byte{ + [][]byte{[]byte("hello")}, + [][]byte{[]byte("world")}, + [][]byte{[]byte("bye")}, + }, + }, + } + for _, tc := range cases { + res := NewResult(tc.at) + err := res.Scan(tc.input) + diff.Test(t, t.Fatalf, err, nil) + diff.Test(t, t.Errorf, res.Bytes(), tc.want) + } +} + +func TestABIType(t *testing.T) { + cases := []struct { + input Input + want atype + n int + }{ + { + Input{Name: "a", Type: "bytes32"}, + static(), + 0, + }, + { + Input{Name: "a", Type: "bytes32[]"}, + array(static()), + 0, + }, + { + Input{ + Name: "a", + Type: "tuple[]", + Components: []Input{ + Input{Name: "b", Type: "uint256"}, + Input{Name: "c", Type: "bytes"}, + }, + }, + array(tuple(static(), dynamic())), + 0, + }, + } + for _, tc := range cases { + _, got := tc.input.ABIType(0) + diff.Test(t, t.Errorf, tc.want, got) + } +} + +func TestSelected(t *testing.T) { + event := Event{ + Name: "test", + Inputs: []Input{ + Input{Name: "z"}, + Input{ + Name: "a", + Column: "a", + Components: []Input{ + Input{Name: "b", Column: "b"}, + Input{Name: "c", Column: "c"}, + }, + }, + Input{Name: "d", Column: "d"}, + Input{Name: "e", Column: ""}, + }, + } + want := []Input{ + Input{Name: "b", Column: "b"}, + Input{Name: "c", Column: "c"}, + Input{ + Name: "a", + Column: "a", + Components: []Input{ + Input{Name: "b", Column: "b"}, + Input{Name: "c", Column: "c"}, + }, + }, + Input{Name: "d", Column: "d"}, + } + diff.Test(t, t.Errorf, want, event.Selected()) +} diff --git a/abi2/integration_test.go b/abi2/integration_test.go new file mode 100644 index 00000000..7e8ef04a --- /dev/null +++ b/abi2/integration_test.go @@ -0,0 +1,155 @@ +package abi2 + +import ( + "database/sql" + "testing" + + "github.com/indexsupply/x/integrations/testhelper" + + "blake.io/pqx/pqxtest" + "github.com/jackc/pgx/v5/stdlib" + "kr.dev/diff" +) + +func TestMain(m *testing.M) { + sql.Register("postgres", stdlib.GetDefaultDriver()) + pqxtest.TestMain(m) +} + +func TestInsert(t *testing.T) { + th := testhelper.New(t) + defer th.Done() + cases := []struct { + blockNum uint64 + query string + }{ + { + 17943843, + ` + select true from seaport_test + where order_hash = '\x796820863892f449ec5dc02582e6708292255cb1ed21f5c3b0ff4bff04328ff7' + `, + }, + } + for _, tc := range cases { + th.Reset() + c, err := New(j) + diff.Test(t, t.Fatalf, nil, err) + diff.Test(t, t.Fatalf, nil, CreateTable(th.Context(), th.PG, c.Table())) + th.Process(c, tc.blockNum) + var found bool + diff.Test(t, t.Errorf, nil, th.PG.QueryRow(th.Context(), tc.query).Scan(&found)) + } +} + +var j = []byte(`{ + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32", + "column": "order_hash" + }, + { + "indexed": true, + "internalType": "address", + "name": "offerer", + "type": "address", + "column": "offerer" + }, + { + "indexed": true, + "internalType": "address", + "name": "zone", + "type": "address", + "column": "zone" + }, + { + "indexed": false, + "internalType": "address", + "name": "recipient", + "type": "address", + "column": "recipient" + }, + { + "components": [ + { + "internalType": "enum ItemType", + "name": "itemType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address", + "column": "offer_token" + }, + { + "internalType": "uint256", + "name": "identifier", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + } + ], + "indexed": false, + "internalType": "struct SpentItem[]", + "name": "offer", + "type": "tuple[]" + }, + { + "components": [ + { + "internalType": "enum ItemType", + "name": "itemType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "uint256", + "name": "identifier", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "amount", + "type": "uint256" + }, + { + "internalType": "address payable", + "name": "recipient", + "type": "address", + "column": "consideration_recipient" + } + ], + "indexed": false, + "internalType": "struct ReceivedItem[]", + "name": "consideration", + "type": "tuple[]" + } + ], + "extra": { + "table": { + "name": "seaport_test", + "columns": [ + {"name": "order_hash", "type": "bytea"}, + {"name": "consideration_recipient", "type": "bytea"}, + {"name": "offer_token", "type": "bytea"}, + {"name": "offerer", "type": "bytea"}, + {"name": "zone", "type": "bytea"}, + {"name": "recipient", "type": "bytea"} + ] + } + }, + "name": "OrderFulfilled", + "type": "event" +}`) diff --git a/geth/testdata/17943842-hashes b/geth/testdata/17943842-hashes new file mode 100644 index 00000000..c6d3fbb5 --- /dev/null +++ b/geth/testdata/17943842-hashes @@ -0,0 +1 @@ + |Ýr4¯Çíåž\á;²Mí}K_ŽëÞ˜*QOîŸÞÍ® \ No newline at end of file diff --git a/geth/testdata/17943843-bodies b/geth/testdata/17943843-bodies new file mode 100644 index 00000000..c9d0d03d Binary files /dev/null and b/geth/testdata/17943843-bodies differ diff --git a/geth/testdata/17943843-hashes b/geth/testdata/17943843-hashes new file mode 100644 index 00000000..435739c0 --- /dev/null +++ b/geth/testdata/17943843-hashes @@ -0,0 +1 @@ + |iˆ0 )8•æãà¤Ò\ØÌ`ŒIuû”g{¿e³îz÷ç \ No newline at end of file diff --git a/geth/testdata/17943843-headers b/geth/testdata/17943843-headers new file mode 100644 index 00000000..14dc024f Binary files /dev/null and b/geth/testdata/17943843-headers differ diff --git a/geth/testdata/17943843-receipts b/geth/testdata/17943843-receipts new file mode 100644 index 00000000..060d1a5f Binary files /dev/null and b/geth/testdata/17943843-receipts differ