Загрузка данных
// Package abiselectors извлекает из ABI JSON контракта Ethereum
// тип, каноническую сигнатуру и селектор для function / event / error.
package abiselectors
import (
"encoding/hex"
"encoding/json"
"fmt"
"strings"
"golang.org/x/crypto/sha3"
)
// Item — результат для одного элемента ABI.
type Item struct {
Type string `json:"type"` // function | event | error
Name string `json:"name"` // имя
Signature string `json:"signature"` // каноническая сигнатура, напр. transfer(address,uint256)
// Selector:
// function, error — первые 4 байта keccak256(signature), напр. 0xa9059cbb
// event — полный keccak256(signature) (topic0, 32 байта);
// для anonymous-событий topic0 не пишется в лог, но хеш всё равно считается
Selector string `json:"selector"`
Anonymous bool `json:"anonymous,omitempty"` // только для event
}
type abiParam struct {
Name string `json:"name"`
Type string `json:"type"`
Components []abiParam `json:"components,omitempty"`
}
type abiEntry struct {
Type string `json:"type"`
Name string `json:"name"`
Inputs []abiParam `json:"inputs"`
Anonymous bool `json:"anonymous"`
}
// ExtractSelectors разбирает ABI JSON и возвращает элементы типов function, event, error.
// Остальные элементы (constructor, fallback, receive) пропускаются.
func ExtractSelectors(abiJSON []byte) ([]Item, error) {
var entries []abiEntry
if err := json.Unmarshal(abiJSON, &entries); err != nil {
return nil, fmt.Errorf("parse abi json: %w", err)
}
items := make([]Item, 0, len(entries))
for i, e := range entries {
typ := e.Type
if typ == "" { // по спецификации ABI тип по умолчанию — function
typ = "function"
}
if typ != "function" && typ != "event" && typ != "error" {
continue
}
if e.Name == "" {
return nil, fmt.Errorf("entry %d (%s): empty name", i, typ)
}
sig, err := buildSignature(e.Name, e.Inputs)
if err != nil {
return nil, fmt.Errorf("entry %d (%s %s): %w", i, typ, e.Name, err)
}
hash := keccak256([]byte(sig))
item := Item{Type: typ, Name: e.Name, Signature: sig}
if typ == "event" {
item.Selector = "0x" + hex.EncodeToString(hash)
item.Anonymous = e.Anonymous
} else {
item.Selector = "0x" + hex.EncodeToString(hash[:4])
}
items = append(items, item)
}
return items, nil
}
func buildSignature(name string, inputs []abiParam) (string, error) {
types := make([]string, len(inputs))
for i, in := range inputs {
t, err := canonicalType(in)
if err != nil {
return "", fmt.Errorf("input %d: %w", i, err)
}
types[i] = t
}
return name + "(" + strings.Join(types, ",") + ")", nil
}
// canonicalType приводит тип к каноническому виду:
// tuple → (t1,t2,...), tuple[] → (t1,...)[], uint → uint256 и т.д.
func canonicalType(p abiParam) (string, error) {
if p.Type == "" {
return "", fmt.Errorf("empty type")
}
if strings.HasPrefix(p.Type, "tuple") {
suffix := strings.TrimPrefix(p.Type, "tuple") // "", "[]", "[2][]" ...
if suffix != "" && !strings.HasPrefix(suffix, "[") {
return "", fmt.Errorf("invalid tuple type %q", p.Type)
}
parts := make([]string, len(p.Components))
for i, c := range p.Components {
t, err := canonicalType(c)
if err != nil {
return "", fmt.Errorf("component %d: %w", i, err)
}
parts[i] = t
}
return "(" + strings.Join(parts, ",") + ")" + suffix, nil
}
base, suffix := p.Type, ""
if idx := strings.IndexByte(p.Type, '['); idx >= 0 {
base, suffix = p.Type[:idx], p.Type[idx:]
}
switch base {
case "uint":
base = "uint256"
case "int":
base = "int256"
case "byte":
base = "bytes1"
case "fixed":
base = "fixed128x18"
case "ufixed":
base = "ufixed128x18"
}
return base + suffix, nil
}
func keccak256(data []byte) []byte {
h := sha3.NewLegacyKeccak256()
h.Write(data)
return h.Sum(nil)
}