// 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 . package parser import ( "errors" "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/common" "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/shared/constants" "github.com/vulcanize/vulcanizedb/pkg/contract_watcher/shared/types" "github.com/vulcanize/vulcanizedb/pkg/eth" ) // Parser is used to fetch and parse contract ABIs // It is dependent on etherscan's api type Parser interface { Parse(contractAddr string) error ParseAbiStr(abiStr string) error Abi() string ParsedAbi() abi.ABI GetMethods(wanted []string) []types.Method GetSelectMethods(wanted []string) []types.Method GetEvents(wanted []string) map[string]types.Event } type parser struct { client *eth.EtherScanAPI abi string parsedAbi abi.ABI } // NewParser returns a new Parser func NewParser(network string) Parser { url := eth.GenURL(network) return &parser{ client: eth.NewEtherScanClient(url), } } // Abi returns the parser's configured abi string func (p *parser) Abi() string { return p.abi } // ParsedAbi returns the parser's parsed abi func (p *parser) ParsedAbi() abi.ABI { return p.parsedAbi } // Parse retrieves and parses the abi string // for the given contract address func (p *parser) Parse(contractAddr string) error { // If the abi is one our locally stored abis, fetch // TODO: Allow users to pass abis through config knownAbi, err := p.lookUp(contractAddr) if err == nil { p.abi = knownAbi p.parsedAbi, err = eth.ParseAbi(knownAbi) return err } // Try getting abi from etherscan abiStr, err := p.client.GetAbi(contractAddr) if err != nil { return err } //TODO: Implement other ways to fetch abi p.abi = abiStr p.parsedAbi, err = eth.ParseAbi(abiStr) return err } // ParseAbiStr loads and parses an abi from a given abi string func (p *parser) ParseAbiStr(abiStr string) error { var err error p.abi = abiStr p.parsedAbi, err = eth.ParseAbi(abiStr) return err } func (p *parser) lookUp(contractAddr string) (string, error) { if v, ok := constants.ABIs[common.HexToAddress(contractAddr)]; ok { return v, nil } return "", errors.New("ABI not present in lookup table") } // GetSelectMethods returns only specified methods, if they meet the criteria // Returns as array with methods in same order they were specified // Nil or empty wanted array => no events are returned func (p *parser) GetSelectMethods(wanted []string) []types.Method { wLen := len(wanted) if wLen == 0 { return nil } methods := make([]types.Method, wLen) for _, m := range p.parsedAbi.Methods { for i, name := range wanted { if name == m.Name && okTypes(m, wanted) { methods[i] = types.NewMethod(m) } } } return methods } // GetMethods returns wanted methods // Empty wanted array => all methods are returned // Nil wanted array => no methods are returned func (p *parser) GetMethods(wanted []string) []types.Method { if wanted == nil { return nil } methods := make([]types.Method, 0) length := len(wanted) for _, m := range p.parsedAbi.Methods { if length == 0 || stringInSlice(wanted, m.Name) { methods = append(methods, types.NewMethod(m)) } } return methods } // GetEvents returns wanted events as map of types.Events // Empty wanted array => all events are returned // Nil wanted array => no events are returned func (p *parser) GetEvents(wanted []string) map[string]types.Event { events := map[string]types.Event{} if wanted == nil { return events } length := len(wanted) for _, e := range p.parsedAbi.Events { if length == 0 || stringInSlice(wanted, e.Name) { events[e.Name] = types.NewEvent(e) } } return events } func okReturnType(arg abi.Argument) bool { wantedTypes := []byte{ abi.UintTy, abi.IntTy, abi.BoolTy, abi.StringTy, abi.AddressTy, abi.HashTy, abi.BytesTy, abi.FixedBytesTy, abi.FixedPointTy, } for _, ty := range wantedTypes { if arg.Type.T == ty { return true } } return false } func okTypes(m abi.Method, wanted []string) bool { // Only return method if it has less than 3 arguments, a single output value, and it is a method we want or we want all methods (empty 'wanted' slice) if len(m.Inputs) < 3 && len(m.Outputs) == 1 && (len(wanted) == 0 || stringInSlice(wanted, m.Name)) { // Only return methods if inputs are all of accepted types and output is of the accepted types if !okReturnType(m.Outputs[0]) { return false } for _, input := range m.Inputs { switch input.Type.T { // Addresses are properly labeled and caught // But hashes tend to not be explicitly labeled and caught // Instead bytes32 are assumed to be hashes case abi.AddressTy, abi.HashTy: case abi.FixedBytesTy: if input.Type.Size != 32 { return false } default: return false } } return true } return false } func stringInSlice(list []string, s string) bool { for _, b := range list { if b == s { return true } } return false }