diff --git a/api/api.go b/api/api.go index 0fd16d7..3686653 100644 --- a/api/api.go +++ b/api/api.go @@ -123,6 +123,9 @@ func ParseQueryParams(r *http.Request) (QueryParams, error) { log.Error().Err(err).Msg("Error parsing query params") return QueryParams{}, err } + if params.Limit == 0 { + params.Limit = 5 + } return params, nil } diff --git a/docs/docs.go b/docs/docs.go index 845fdac..28c487e 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -77,6 +77,7 @@ const docTemplate = `{ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -200,6 +201,7 @@ const docTemplate = `{ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -265,7 +267,7 @@ const docTemplate = `{ "BasicAuth": [] } ], - "description": "Retrieve logs for a specific contract and event signature", + "description": "Retrieve logs for a specific contract and event signature. When a valid event signature is provided, the response includes decoded log data with both indexed and non-indexed parameters.", "consumes": [ "application/json" ], @@ -293,7 +295,7 @@ const docTemplate = `{ }, { "type": "string", - "description": "Event signature", + "description": "Event signature (e.g., 'Transfer(address,address,uint256)')", "name": "signature", "in": "path", "required": true @@ -330,6 +332,7 @@ const docTemplate = `{ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -359,7 +362,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/handlers.LogModel" + "$ref": "#/definitions/handlers.DecodedLogModel" } } } @@ -446,6 +449,7 @@ const docTemplate = `{ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -569,6 +573,7 @@ const docTemplate = `{ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -634,7 +639,7 @@ const docTemplate = `{ "BasicAuth": [] } ], - "description": "Retrieve transactions for a specific contract and signature (Not implemented yet)", + "description": "Retrieve transactions for a specific contract and signature. When a valid function signature is provided, the response includes decoded transaction data with function inputs.", "consumes": [ "application/json" ], @@ -662,7 +667,7 @@ const docTemplate = `{ }, { "type": "string", - "description": "Function signature", + "description": "Function signature (e.g., 'transfer(address,uint256)')", "name": "signature", "in": "path", "required": true @@ -699,6 +704,7 @@ const docTemplate = `{ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -728,7 +734,7 @@ const docTemplate = `{ "data": { "type": "array", "items": { - "$ref": "#/definitions/handlers.TransactionModel" + "$ref": "#/definitions/handlers.DecodedTransactionModel" } } } @@ -817,9 +823,10 @@ const docTemplate = `{ "properties": { "aggregations": { "description": "@Description Aggregation results", - "type": "object", - "additionalProperties": { - "type": "string" + "type": "array", + "items": { + "type": "object", + "additionalProperties": true } }, "data": { @@ -835,6 +842,169 @@ const docTemplate = `{ } } }, + "handlers.DecodedLogDataModel": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "additionalProperties": true + }, + "name": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "handlers.DecodedLogModel": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "block_hash": { + "type": "string" + }, + "block_number": { + "type": "string" + }, + "block_timestamp": { + "type": "integer" + }, + "chain_id": { + "type": "string" + }, + "data": { + "type": "string" + }, + "decoded": { + "$ref": "#/definitions/handlers.DecodedLogDataModel" + }, + "log_index": { + "type": "integer" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "transaction_hash": { + "type": "string" + }, + "transaction_index": { + "type": "integer" + } + } + }, + "handlers.DecodedTransactionDataModel": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "additionalProperties": true + }, + "name": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "handlers.DecodedTransactionModel": { + "type": "object", + "properties": { + "access_list_json": { + "type": "string" + }, + "blob_gas_price": { + "type": "string" + }, + "blob_gas_used": { + "type": "integer" + }, + "block_hash": { + "type": "string" + }, + "block_number": { + "type": "string" + }, + "block_timestamp": { + "type": "integer" + }, + "chain_id": { + "type": "string" + }, + "contract_address": { + "type": "string" + }, + "cumulative_gas_used": { + "type": "integer" + }, + "data": { + "type": "string" + }, + "decoded": { + "$ref": "#/definitions/handlers.DecodedTransactionDataModel" + }, + "effective_gas_price": { + "type": "string" + }, + "from_address": { + "type": "string" + }, + "gas": { + "type": "integer" + }, + "gas_price": { + "type": "string" + }, + "gas_used": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "logs_bloom": { + "type": "string" + }, + "max_fee_per_gas": { + "type": "string" + }, + "max_priority_fee_per_gas": { + "type": "string" + }, + "nonce": { + "type": "integer" + }, + "r": { + "type": "string" + }, + "s": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "to_address": { + "type": "string" + }, + "transaction_index": { + "type": "integer" + }, + "transaction_type": { + "type": "integer" + }, + "v": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "handlers.LogModel": { "type": "object", "properties": { @@ -879,6 +1049,12 @@ const docTemplate = `{ "access_list_json": { "type": "string" }, + "blob_gas_price": { + "type": "string" + }, + "blob_gas_used": { + "type": "integer" + }, "block_hash": { "type": "string" }, @@ -891,9 +1067,18 @@ const docTemplate = `{ "chain_id": { "type": "string" }, + "contract_address": { + "type": "string" + }, + "cumulative_gas_used": { + "type": "integer" + }, "data": { "type": "string" }, + "effective_gas_price": { + "type": "string" + }, "from_address": { "type": "string" }, @@ -903,9 +1088,15 @@ const docTemplate = `{ "gas_price": { "type": "string" }, + "gas_used": { + "type": "integer" + }, "hash": { "type": "string" }, + "logs_bloom": { + "type": "string" + }, "max_fee_per_gas": { "type": "string" }, @@ -921,6 +1112,9 @@ const docTemplate = `{ "s": { "type": "string" }, + "status": { + "type": "integer" + }, "to_address": { "type": "string" }, diff --git a/docs/swagger.json b/docs/swagger.json index 0a80f34..c7d6aab 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -70,6 +70,7 @@ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -193,6 +194,7 @@ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -258,7 +260,7 @@ "BasicAuth": [] } ], - "description": "Retrieve logs for a specific contract and event signature", + "description": "Retrieve logs for a specific contract and event signature. When a valid event signature is provided, the response includes decoded log data with both indexed and non-indexed parameters.", "consumes": [ "application/json" ], @@ -286,7 +288,7 @@ }, { "type": "string", - "description": "Event signature", + "description": "Event signature (e.g., 'Transfer(address,address,uint256)')", "name": "signature", "in": "path", "required": true @@ -323,6 +325,7 @@ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -352,7 +355,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/handlers.LogModel" + "$ref": "#/definitions/handlers.DecodedLogModel" } } } @@ -439,6 +442,7 @@ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -562,6 +566,7 @@ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -627,7 +632,7 @@ "BasicAuth": [] } ], - "description": "Retrieve transactions for a specific contract and signature (Not implemented yet)", + "description": "Retrieve transactions for a specific contract and signature. When a valid function signature is provided, the response includes decoded transaction data with function inputs.", "consumes": [ "application/json" ], @@ -655,7 +660,7 @@ }, { "type": "string", - "description": "Function signature", + "description": "Function signature (e.g., 'transfer(address,uint256)')", "name": "signature", "in": "path", "required": true @@ -692,6 +697,7 @@ }, { "type": "integer", + "default": 5, "description": "Number of items per page", "name": "limit", "in": "query" @@ -721,7 +727,7 @@ "data": { "type": "array", "items": { - "$ref": "#/definitions/handlers.TransactionModel" + "$ref": "#/definitions/handlers.DecodedTransactionModel" } } } @@ -810,9 +816,10 @@ "properties": { "aggregations": { "description": "@Description Aggregation results", - "type": "object", - "additionalProperties": { - "type": "string" + "type": "array", + "items": { + "type": "object", + "additionalProperties": true } }, "data": { @@ -828,6 +835,169 @@ } } }, + "handlers.DecodedLogDataModel": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "additionalProperties": true + }, + "name": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "handlers.DecodedLogModel": { + "type": "object", + "properties": { + "address": { + "type": "string" + }, + "block_hash": { + "type": "string" + }, + "block_number": { + "type": "string" + }, + "block_timestamp": { + "type": "integer" + }, + "chain_id": { + "type": "string" + }, + "data": { + "type": "string" + }, + "decoded": { + "$ref": "#/definitions/handlers.DecodedLogDataModel" + }, + "log_index": { + "type": "integer" + }, + "topics": { + "type": "array", + "items": { + "type": "string" + } + }, + "transaction_hash": { + "type": "string" + }, + "transaction_index": { + "type": "integer" + } + } + }, + "handlers.DecodedTransactionDataModel": { + "type": "object", + "properties": { + "inputs": { + "type": "object", + "additionalProperties": true + }, + "name": { + "type": "string" + }, + "signature": { + "type": "string" + } + } + }, + "handlers.DecodedTransactionModel": { + "type": "object", + "properties": { + "access_list_json": { + "type": "string" + }, + "blob_gas_price": { + "type": "string" + }, + "blob_gas_used": { + "type": "integer" + }, + "block_hash": { + "type": "string" + }, + "block_number": { + "type": "string" + }, + "block_timestamp": { + "type": "integer" + }, + "chain_id": { + "type": "string" + }, + "contract_address": { + "type": "string" + }, + "cumulative_gas_used": { + "type": "integer" + }, + "data": { + "type": "string" + }, + "decoded": { + "$ref": "#/definitions/handlers.DecodedTransactionDataModel" + }, + "effective_gas_price": { + "type": "string" + }, + "from_address": { + "type": "string" + }, + "gas": { + "type": "integer" + }, + "gas_price": { + "type": "string" + }, + "gas_used": { + "type": "integer" + }, + "hash": { + "type": "string" + }, + "logs_bloom": { + "type": "string" + }, + "max_fee_per_gas": { + "type": "string" + }, + "max_priority_fee_per_gas": { + "type": "string" + }, + "nonce": { + "type": "integer" + }, + "r": { + "type": "string" + }, + "s": { + "type": "string" + }, + "status": { + "type": "integer" + }, + "to_address": { + "type": "string" + }, + "transaction_index": { + "type": "integer" + }, + "transaction_type": { + "type": "integer" + }, + "v": { + "type": "string" + }, + "value": { + "type": "string" + } + } + }, "handlers.LogModel": { "type": "object", "properties": { @@ -872,6 +1042,12 @@ "access_list_json": { "type": "string" }, + "blob_gas_price": { + "type": "string" + }, + "blob_gas_used": { + "type": "integer" + }, "block_hash": { "type": "string" }, @@ -884,9 +1060,18 @@ "chain_id": { "type": "string" }, + "contract_address": { + "type": "string" + }, + "cumulative_gas_used": { + "type": "integer" + }, "data": { "type": "string" }, + "effective_gas_price": { + "type": "string" + }, "from_address": { "type": "string" }, @@ -896,9 +1081,15 @@ "gas_price": { "type": "string" }, + "gas_used": { + "type": "integer" + }, "hash": { "type": "string" }, + "logs_bloom": { + "type": "string" + }, "max_fee_per_gas": { "type": "string" }, @@ -914,6 +1105,9 @@ "s": { "type": "string" }, + "status": { + "type": "integer" + }, "to_address": { "type": "string" }, diff --git a/docs/swagger.yaml b/docs/swagger.yaml index a3fe83b..687a17c 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -42,10 +42,11 @@ definitions: description: QueryResponse represents the response structure for a query properties: aggregations: - additionalProperties: - type: string description: '@Description Aggregation results' - type: object + items: + additionalProperties: true + type: object + type: array data: description: '@Description Query result data' meta: @@ -53,6 +54,114 @@ definitions: - $ref: '#/definitions/api.Meta' description: '@Description Metadata for the query response' type: object + handlers.DecodedLogDataModel: + properties: + inputs: + additionalProperties: true + type: object + name: + type: string + signature: + type: string + type: object + handlers.DecodedLogModel: + properties: + address: + type: string + block_hash: + type: string + block_number: + type: string + block_timestamp: + type: integer + chain_id: + type: string + data: + type: string + decoded: + $ref: '#/definitions/handlers.DecodedLogDataModel' + log_index: + type: integer + topics: + items: + type: string + type: array + transaction_hash: + type: string + transaction_index: + type: integer + type: object + handlers.DecodedTransactionDataModel: + properties: + inputs: + additionalProperties: true + type: object + name: + type: string + signature: + type: string + type: object + handlers.DecodedTransactionModel: + properties: + access_list_json: + type: string + blob_gas_price: + type: string + blob_gas_used: + type: integer + block_hash: + type: string + block_number: + type: string + block_timestamp: + type: integer + chain_id: + type: string + contract_address: + type: string + cumulative_gas_used: + type: integer + data: + type: string + decoded: + $ref: '#/definitions/handlers.DecodedTransactionDataModel' + effective_gas_price: + type: string + from_address: + type: string + gas: + type: integer + gas_price: + type: string + gas_used: + type: integer + hash: + type: string + logs_bloom: + type: string + max_fee_per_gas: + type: string + max_priority_fee_per_gas: + type: string + nonce: + type: integer + r: + type: string + s: + type: string + status: + type: integer + to_address: + type: string + transaction_index: + type: integer + transaction_type: + type: integer + v: + type: string + value: + type: string + type: object handlers.LogModel: properties: address: @@ -82,6 +191,10 @@ definitions: properties: access_list_json: type: string + blob_gas_price: + type: string + blob_gas_used: + type: integer block_hash: type: string block_number: @@ -90,16 +203,26 @@ definitions: type: integer chain_id: type: string + contract_address: + type: string + cumulative_gas_used: + type: integer data: type: string + effective_gas_price: + type: string from_address: type: string gas: type: integer gas_price: type: string + gas_used: + type: integer hash: type: string + logs_bloom: + type: string max_fee_per_gas: type: string max_priority_fee_per_gas: @@ -110,6 +233,8 @@ definitions: type: string s: type: string + status: + type: integer to_address: type: string transaction_index: @@ -161,7 +286,8 @@ paths: in: query name: page type: integer - - description: Number of items per page + - default: 5 + description: Number of items per page in: query name: limit type: integer @@ -239,7 +365,8 @@ paths: in: query name: page type: integer - - description: Number of items per page + - default: 5 + description: Number of items per page in: query name: limit type: integer @@ -285,7 +412,9 @@ paths: get: consumes: - application/json - description: Retrieve logs for a specific contract and event signature + description: Retrieve logs for a specific contract and event signature. When + a valid event signature is provided, the response includes decoded log data + with both indexed and non-indexed parameters. parameters: - description: Chain ID in: path @@ -297,7 +426,7 @@ paths: name: contract required: true type: string - - description: Event signature + - description: Event signature (e.g., 'Transfer(address,address,uint256)') in: path name: signature required: true @@ -322,7 +451,8 @@ paths: in: query name: page type: integer - - description: Number of items per page + - default: 5 + description: Number of items per page in: query name: limit type: integer @@ -344,7 +474,7 @@ paths: - properties: data: items: - $ref: '#/definitions/handlers.LogModel' + $ref: '#/definitions/handlers.DecodedLogModel' type: array type: object "400": @@ -395,7 +525,8 @@ paths: in: query name: page type: integer - - description: Number of items per page + - default: 5 + description: Number of items per page in: query name: limit type: integer @@ -473,7 +604,8 @@ paths: in: query name: page type: integer - - description: Number of items per page + - default: 5 + description: Number of items per page in: query name: limit type: integer @@ -519,8 +651,9 @@ paths: get: consumes: - application/json - description: Retrieve transactions for a specific contract and signature (Not - implemented yet) + description: Retrieve transactions for a specific contract and signature. When + a valid function signature is provided, the response includes decoded transaction + data with function inputs. parameters: - description: Chain ID in: path @@ -532,7 +665,7 @@ paths: name: to required: true type: string - - description: Function signature + - description: Function signature (e.g., 'transfer(address,uint256)') in: path name: signature required: true @@ -557,7 +690,8 @@ paths: in: query name: page type: integer - - description: Number of items per page + - default: 5 + description: Number of items per page in: query name: limit type: integer @@ -579,7 +713,7 @@ paths: - properties: data: items: - $ref: '#/definitions/handlers.TransactionModel' + $ref: '#/definitions/handlers.DecodedTransactionModel' type: array type: object "400": diff --git a/internal/common/abi.go b/internal/common/abi.go index 909423e..b552997 100644 --- a/internal/common/abi.go +++ b/internal/common/abi.go @@ -8,13 +8,34 @@ import ( "github.com/ethereum/go-ethereum/accounts/abi" ) -func ConstructFunctionABI(signature string) (*abi.Method, error) { +func ConstructEventABI(signature string) (*abi.Event, error) { + // Regex to extract the event name and parameters regex := regexp.MustCompile(`^(\w+)\((.*)\)$`) matches := regex.FindStringSubmatch(strings.TrimSpace(signature)) if len(matches) != 3 { return nil, fmt.Errorf("invalid event signature format") } + eventName := matches[1] + parameters := matches[2] + + inputs, err := parseParamsToAbiArguments(parameters) + if err != nil { + return nil, fmt.Errorf("failed to parse params to abi arguments '%s': %v", parameters, err) + } + + event := abi.NewEvent(eventName, eventName, false, inputs) + + return &event, nil +} + +func ConstructFunctionABI(signature string) (*abi.Method, error) { + regex := regexp.MustCompile(`^(\w+)\((.*)\)$`) + matches := regex.FindStringSubmatch(strings.TrimSpace(signature)) + if len(matches) != 3 { + return nil, fmt.Errorf("invalid function signature format") + } + functionName := matches[1] params := matches[2] @@ -70,7 +91,7 @@ func splitParams(params string) []string { } func parseParamToAbiArgument(param string, fallbackName string) (*abi.Argument, error) { - argName, paramType, err := getArgNameAndType(param, fallbackName) + argName, paramType, indexed, err := getArgNameAndType(param, fallbackName) if err != nil { return nil, fmt.Errorf("failed to get arg name and type '%s': %v", param, err) } @@ -80,8 +101,9 @@ func parseParamToAbiArgument(param string, fallbackName string) (*abi.Argument, return nil, fmt.Errorf("failed to marshal tuple: %v", err) } return &abi.Argument{ - Name: argName, - Type: argType, + Name: argName, + Type: argType, + Indexed: indexed, }, nil } else { argType, err := abi.NewType(paramType, paramType, nil) @@ -89,33 +111,49 @@ func parseParamToAbiArgument(param string, fallbackName string) (*abi.Argument, return nil, fmt.Errorf("failed to parse type '%s': %v", paramType, err) } return &abi.Argument{ - Name: argName, - Type: argType, + Name: argName, + Type: argType, + Indexed: indexed, }, nil } } -func getArgNameAndType(param string, fallbackName string) (name string, paramType string, err error) { +func getArgNameAndType(param string, fallbackName string) (name string, paramType string, indexed bool, err error) { + param, indexed = checkIfParamIsIndexed(param) if isTuple(param) { lastParenIndex := strings.LastIndex(param, ")") if lastParenIndex == -1 { - return "", "", fmt.Errorf("invalid tuple format") + return "", "", false, fmt.Errorf("invalid tuple format") } if len(param)-1 == lastParenIndex { - return fallbackName, param, nil + return fallbackName, param, indexed, nil } paramsEndIdx := lastParenIndex + 1 if strings.HasPrefix(param[paramsEndIdx:], "[]") { paramsEndIdx = lastParenIndex + 3 } - return strings.TrimSpace(param[paramsEndIdx:]), param[:paramsEndIdx], nil + return strings.TrimSpace(param[paramsEndIdx:]), param[:paramsEndIdx], indexed, nil } else { tokens := strings.Fields(param) if len(tokens) == 1 { - return fallbackName, strings.TrimSpace(tokens[0]), nil + return fallbackName, strings.TrimSpace(tokens[0]), indexed, nil + } + return strings.TrimSpace(tokens[len(tokens)-1]), strings.Join(tokens[:len(tokens)-1], " "), indexed, nil + } +} + +func checkIfParamIsIndexed(param string) (string, bool) { + tokens := strings.Fields(param) + indexed := false + for i, token := range tokens { + if token == "indexed" || strings.HasPrefix(token, "index_topic_") { + tokens = append(tokens[:i], tokens[i+1:]...) + indexed = true + break } - return strings.TrimSpace(tokens[len(tokens)-1]), strings.Join(tokens[:len(tokens)-1], " "), nil } + param = strings.Join(tokens, " ") + return param, indexed } func isTuple(param string) bool { @@ -142,7 +180,7 @@ func marshalParamArguments(param string) ([]abi.ArgumentMarshaling, error) { paramList := splitParams(param) components := []abi.ArgumentMarshaling{} for idx, param := range paramList { - argName, paramType, err := getArgNameAndType(param, fmt.Sprintf("field%d", idx)) + argName, paramType, indexed, err := getArgNameAndType(param, fmt.Sprintf("field%d", idx)) if err != nil { return nil, fmt.Errorf("failed to get arg name and type '%s': %v", param, err) } @@ -155,11 +193,13 @@ func marshalParamArguments(param string) ([]abi.ArgumentMarshaling, error) { Type: "tuple", Name: argName, Components: subComponents, + Indexed: indexed, }) } else { components = append(components, abi.ArgumentMarshaling{ - Type: paramType, - Name: argName, + Type: paramType, + Name: argName, + Indexed: indexed, }) } } diff --git a/internal/common/log.go b/internal/common/log.go index d23ddfe..0338c1b 100644 --- a/internal/common/log.go +++ b/internal/common/log.go @@ -1,12 +1,18 @@ package common import ( + "encoding/hex" + "fmt" "math/big" + + "github.com/ethereum/go-ethereum/accounts/abi" + gethCommon "github.com/ethereum/go-ethereum/common" + "github.com/rs/zerolog/log" ) type Log struct { - ChainId *big.Int `json:"chain_id"` - BlockNumber *big.Int `json:"block_number"` + ChainId *big.Int `json:"chain_id" swaggertype:"string"` + BlockNumber *big.Int `json:"block_number" swaggertype:"string"` BlockHash string `json:"block_hash"` BlockTimestamp uint64 `json:"block_timestamp"` TransactionHash string `json:"transaction_hash"` @@ -20,3 +26,120 @@ type Log struct { type RawLogs = []map[string]interface{} type RawReceipts = []RawReceipt type RawReceipt = map[string]interface{} + +type DecodedLogData struct { + Name string `json:"name"` + Signature string `json:"signature"` + IndexedParams map[string]interface{} `json:"indexedParams"` + NonIndexedParams map[string]interface{} `json:"nonIndexedParams"` +} + +type DecodedLog struct { + Log + Decoded DecodedLogData `json:"decodedData"` +} + +func (l *Log) Decode(eventABI *abi.Event) *DecodedLog { + + decodedIndexed := make(map[string]interface{}) + indexedArgs := abi.Arguments{} + for _, arg := range eventABI.Inputs { + if arg.Indexed { + indexedArgs = append(indexedArgs, arg) + } + } + // Decode indexed parameters + for i, arg := range indexedArgs { + if len(l.Topics) <= i+1 { + log.Warn().Msgf("missing topic for indexed parameter: %s, signature: %s", arg.Name, eventABI.Sig) + return &DecodedLog{Log: *l} + } + decodedValue, err := decodeIndexedArgument(arg.Type, l.Topics[i+1]) + if err != nil { + log.Warn().Msgf("failed to decode indexed parameter %s: %v, signature: %s", arg.Name, err, eventABI.Sig) + return &DecodedLog{Log: *l} + } + decodedIndexed[arg.Name] = decodedValue + } + + // Decode non-indexed parameters + decodedNonIndexed := make(map[string]interface{}) + dataBytes := gethCommon.Hex2Bytes(l.Data[2:]) + err := eventABI.Inputs.UnpackIntoMap(decodedNonIndexed, dataBytes) + if err != nil { + log.Warn().Msgf("failed to decode non-indexed parameters: %v, signature: %s", err, eventABI.Sig) + return &DecodedLog{Log: *l} + } + + return &DecodedLog{ + Log: *l, + Decoded: DecodedLogData{ + Name: eventABI.Name, + Signature: eventABI.Sig, + IndexedParams: decodedIndexed, + NonIndexedParams: convertBytesAndNumericToHex(decodedNonIndexed).(map[string]interface{}), + }, + } +} + +func decodeIndexedArgument(argType abi.Type, topic string) (interface{}, error) { + topicBytes := gethCommon.Hex2Bytes(topic[2:]) // Remove "0x" prefix + switch argType.T { + case abi.AddressTy: + return gethCommon.BytesToAddress(topicBytes), nil + case abi.UintTy, abi.IntTy: + return new(big.Int).SetBytes(topicBytes), nil + case abi.BoolTy: + return topicBytes[0] != 0, nil + case abi.StringTy: + return string(topicBytes), nil + case abi.BytesTy, abi.FixedBytesTy: + return "0x" + gethCommon.Bytes2Hex(topicBytes), nil + case abi.HashTy: + if len(topicBytes) != 32 { + return nil, fmt.Errorf("invalid hash length: expected 32, got %d", len(topicBytes)) + } + return gethCommon.BytesToHash(topicBytes), nil + case abi.FixedPointTy: + bi := new(big.Int).SetBytes(topicBytes) + bf := new(big.Float).SetInt(bi) + return bf, nil + case abi.SliceTy, abi.ArrayTy, abi.TupleTy: + return nil, fmt.Errorf("type %s is not supported for indexed parameters", argType.String()) + default: + return nil, fmt.Errorf("unsupported indexed type: %s", argType.String()) + } +} + +func convertBytesAndNumericToHex(data interface{}) interface{} { + switch v := data.(type) { + case map[string]interface{}: + for key, value := range v { + v[key] = convertBytesAndNumericToHex(value) + } + return v + case []interface{}: + for i, value := range v { + v[i] = convertBytesAndNumericToHex(value) + } + return v + case []byte: + return fmt.Sprintf("0x%s", hex.EncodeToString(v)) + case []uint: + hexStrings := make([]string, len(v)) + for i, num := range v { + hexStrings[i] = fmt.Sprintf("0x%x", num) + } + return hexStrings + case [32]uint8: + return fmt.Sprintf("0x%s", hex.EncodeToString(v[:])) + case [64]uint8: + return fmt.Sprintf("0x%s", hex.EncodeToString(v[:])) + case [128]uint8: + return fmt.Sprintf("0x%s", hex.EncodeToString(v[:])) + case [256]uint8: + return fmt.Sprintf("0x%s", hex.EncodeToString(v[:])) + default: + return v + } +} diff --git a/internal/common/log_test.go b/internal/common/log_test.go new file mode 100644 index 0000000..f7d6734 --- /dev/null +++ b/internal/common/log_test.go @@ -0,0 +1,45 @@ +package common + +import ( + "math/big" + "testing" + + gethCommon "github.com/ethereum/go-ethereum/common" + "github.com/stretchr/testify/assert" +) + +func TestDecodeLog(t *testing.T) { + topics := make([]string, 3) + topics[0] = "0x7be266734f0c132a415c32a35b76cbf3d8a02fa3d88628b286dcf713f53f1e2d" + topics[1] = "0xc148159472ef0bbd3a304d3d3637b8deeda456572700669fda4f8d0fad814402" + topics[2] = "0x000000000000000000000000ff0cb0351a356ad16987e5809a8daaaf34f5adbe" + event := Log{ + Data: "0x000000000000000000000000000000000000000000000000b2da0f6658944b0600000000000000000000000000000000000000000000000000000000000000003492dc030870ae719a0babc07807601edd3fc7e150a6b4878d1c5571bd9995c00000000000000000000000000000000000000000000000e076c8d70085af000000000000000000000000000000000000000000000000000000469c6478f693140000000000000000000000000000000000000000000000000000000000000000", + Topics: topics, + } + + eventABI, err := ConstructEventABI("LogCanonicalOrderFilled(bytes32 indexed orderHash,address indexed orderMaker,uint256 fillAmount,uint256 triggerPrice,bytes32 orderFlags,(uint256 price,uint128 fee,bool isNegativeFee) fill)") + assert.NoError(t, err) + decodedEvent := event.Decode(eventABI) + + assert.Equal(t, "LogCanonicalOrderFilled", decodedEvent.Decoded.Name) + assert.Equal(t, "0xc148159472ef0bbd3a304d3d3637b8deeda456572700669fda4f8d0fad814402", decodedEvent.Decoded.IndexedParams["orderHash"]) + assert.Equal(t, gethCommon.HexToAddress("0xff0cb0351a356ad16987e5809a8daaaf34f5adbe"), decodedEvent.Decoded.IndexedParams["orderMaker"]) + + expectedFillAmountValue := big.NewInt(0) + expectedFillAmountValue.SetString("12887630215921289990", 10) + assert.Equal(t, expectedFillAmountValue, decodedEvent.Decoded.NonIndexedParams["fillAmount"]) + assert.Equal(t, "0x3492dc030870ae719a0babc07807601edd3fc7e150a6b4878d1c5571bd9995c0", decodedEvent.Decoded.NonIndexedParams["orderFlags"]) + expectedTriggerPriceValue := big.NewInt(0) + assert.Equal(t, expectedTriggerPriceValue.String(), decodedEvent.Decoded.NonIndexedParams["triggerPrice"].(*big.Int).String()) + + fillTuple := decodedEvent.Decoded.NonIndexedParams["fill"].(struct { + Price *big.Int `json:"price"` + Fee *big.Int `json:"fee"` + IsNegativeFee bool `json:"isNegativeFee"` + }) + + assert.Equal(t, "4140630000000000000000", fillTuple.Price.String()) + assert.Equal(t, "19875203709834004", fillTuple.Fee.String()) + assert.Equal(t, false, fillTuple.IsNegativeFee) +} diff --git a/internal/common/transaction.go b/internal/common/transaction.go index 68e0ea6..95ee93e 100644 --- a/internal/common/transaction.go +++ b/internal/common/transaction.go @@ -10,33 +10,33 @@ import ( ) type Transaction struct { - ChainId *big.Int `json:"chain_id"` + ChainId *big.Int `json:"chain_id" swaggertype:"string"` Hash string `json:"hash"` Nonce uint64 `json:"nonce"` BlockHash string `json:"block_hash"` - BlockNumber *big.Int `json:"block_number"` + BlockNumber *big.Int `json:"block_number" swaggertype:"string"` BlockTimestamp uint64 `json:"block_timestamp"` TransactionIndex uint64 `json:"transaction_index"` FromAddress string `json:"from_address"` ToAddress string `json:"to_address"` - Value *big.Int `json:"value"` + Value *big.Int `json:"value" swaggertype:"string"` Gas uint64 `json:"gas"` - GasPrice *big.Int `json:"gas_price"` + GasPrice *big.Int `json:"gas_price" swaggertype:"string"` Data string `json:"data"` FunctionSelector string `json:"function_selector"` - MaxFeePerGas *big.Int `json:"max_fee_per_gas"` - MaxPriorityFeePerGas *big.Int `json:"max_priority_fee_per_gas"` + MaxFeePerGas *big.Int `json:"max_fee_per_gas" swaggertype:"string"` + MaxPriorityFeePerGas *big.Int `json:"max_priority_fee_per_gas" swaggertype:"string"` TransactionType uint8 `json:"transaction_type"` - R *big.Int `json:"r"` - S *big.Int `json:"s"` - V *big.Int `json:"v"` + R *big.Int `json:"r" swaggertype:"string"` + S *big.Int `json:"s" swaggertype:"string"` + V *big.Int `json:"v" swaggertype:"string"` AccessListJson *string `json:"access_list_json"` ContractAddress *string `json:"contract_address"` GasUsed *uint64 `json:"gas_used"` CumulativeGasUsed *uint64 `json:"cumulative_gas_used"` - EffectiveGasPrice *big.Int `json:"effective_gas_price"` + EffectiveGasPrice *big.Int `json:"effective_gas_price" swaggertype:"string"` BlobGasUsed *uint64 `json:"blob_gas_used"` - BlobGasPrice *big.Int `json:"blob_gas_price"` + BlobGasPrice *big.Int `json:"blob_gas_price" swaggertype:"string"` LogsBloom *string `json:"logs_bloom"` Status *uint64 `json:"status"` } diff --git a/internal/handlers/logs_handlers.go b/internal/handlers/logs_handlers.go index d30dde9..f2ea9a1 100644 --- a/internal/handlers/logs_handlers.go +++ b/internal/handlers/logs_handlers.go @@ -4,6 +4,7 @@ import ( "net/http" "sync" + "github.com/ethereum/go-ethereum/accounts/abi" "github.com/ethereum/go-ethereum/crypto" "github.com/gin-gonic/gin" "github.com/rs/zerolog/log" @@ -34,6 +35,17 @@ type LogModel struct { Topics []string `json:"topics"` } +type DecodedLogDataModel struct { + Name string `json:"name"` + Signature string `json:"signature"` + Inputs map[string]interface{} `json:"inputs"` +} + +type DecodedLogModel struct { + LogModel + Decoded DecodedLogDataModel `json:"decoded"` +} + // @Summary Get all logs // @Description Retrieve all logs across all contracts // @Tags events @@ -46,7 +58,7 @@ type LogModel struct { // @Param sort_by query string false "Field to sort results by" // @Param sort_order query string false "Sort order (asc or desc)" // @Param page query int false "Page number for pagination" -// @Param limit query int false "Number of items per page" +// @Param limit query int false "Number of items per page" default(5) // @Param aggregate query []string false "List of aggregate functions to apply" // @Success 200 {object} api.QueryResponse{data=[]LogModel} // @Failure 400 {object} api.Error @@ -54,7 +66,7 @@ type LogModel struct { // @Failure 500 {object} api.Error // @Router /{chainId}/events [get] func GetLogs(c *gin.Context) { - handleLogsRequest(c, "", "") + handleLogsRequest(c, "", "", nil) } // @Summary Get logs by contract @@ -70,7 +82,7 @@ func GetLogs(c *gin.Context) { // @Param sort_by query string false "Field to sort results by" // @Param sort_order query string false "Sort order (asc or desc)" // @Param page query int false "Page number for pagination" -// @Param limit query int false "Number of items per page" +// @Param limit query int false "Number of items per page" default(5) // @Param aggregate query []string false "List of aggregate functions to apply" // @Success 200 {object} api.QueryResponse{data=[]LogModel} // @Failure 400 {object} api.Error @@ -79,26 +91,26 @@ func GetLogs(c *gin.Context) { // @Router /{chainId}/events/{contract} [get] func GetLogsByContract(c *gin.Context) { contractAddress := c.Param("contract") - handleLogsRequest(c, contractAddress, "") + handleLogsRequest(c, contractAddress, "", nil) } // @Summary Get logs by contract and event signature -// @Description Retrieve logs for a specific contract and event signature +// @Description Retrieve logs for a specific contract and event signature. When a valid event signature is provided, the response includes decoded log data with both indexed and non-indexed parameters. // @Tags events // @Accept json // @Produce json // @Security BasicAuth // @Param chainId path string true "Chain ID" // @Param contract path string true "Contract address" -// @Param signature path string true "Event signature" +// @Param signature path string true "Event signature (e.g., 'Transfer(address,address,uint256)')" // @Param filter query string false "Filter parameters" // @Param group_by query string false "Field to group results by" // @Param sort_by query string false "Field to sort results by" // @Param sort_order query string false "Sort order (asc or desc)" // @Param page query int false "Page number for pagination" -// @Param limit query int false "Number of items per page" +// @Param limit query int false "Number of items per page" default(5) // @Param aggregate query []string false "List of aggregate functions to apply" -// @Success 200 {object} api.QueryResponse{data=[]LogModel} +// @Success 200 {object} api.QueryResponse{data=[]DecodedLogModel} // @Failure 400 {object} api.Error // @Failure 401 {object} api.Error // @Failure 500 {object} api.Error @@ -107,10 +119,14 @@ func GetLogsByContractAndSignature(c *gin.Context) { contractAddress := c.Param("contract") eventSignature := c.Param("signature") strippedSignature := common.StripPayload(eventSignature) - handleLogsRequest(c, contractAddress, strippedSignature) + eventABI, err := common.ConstructEventABI(eventSignature) + if err != nil { + log.Debug().Err(err).Msgf("Unable to construct event ABI for %s", eventSignature) + } + handleLogsRequest(c, contractAddress, strippedSignature, eventABI) } -func handleLogsRequest(c *gin.Context, contractAddress, signature string) { +func handleLogsRequest(c *gin.Context, contractAddress, signature string, eventABI *abi.Event) { chainId, err := api.GetChainId(c) if err != nil { api.BadRequestErrorHandler(c, err) @@ -185,7 +201,16 @@ func handleLogsRequest(c *gin.Context, contractAddress, signature string) { api.InternalErrorHandler(c) return } - queryResult.Data = logsResult.Data + if eventABI != nil { + decodedLogs := []*common.DecodedLog{} + for _, log := range logsResult.Data { + decodedLog := log.Decode(eventABI) + decodedLogs = append(decodedLogs, decodedLog) + } + queryResult.Data = decodedLogs + } else { + queryResult.Data = logsResult.Data + } queryResult.Meta.TotalItems = len(logsResult.Data) } diff --git a/internal/handlers/transactions_handlers.go b/internal/handlers/transactions_handlers.go index 7120383..5794502 100644 --- a/internal/handlers/transactions_handlers.go +++ b/internal/handlers/transactions_handlers.go @@ -15,26 +15,45 @@ import ( // TransactionModel represents a simplified Transaction structure for Swagger documentation type TransactionModel struct { - ChainId string `json:"chain_id"` - Hash string `json:"hash"` - Nonce uint64 `json:"nonce"` - BlockHash string `json:"block_hash"` - BlockNumber string `json:"block_number"` - BlockTimestamp uint64 `json:"block_timestamp"` - TransactionIndex uint64 `json:"transaction_index"` - FromAddress string `json:"from_address"` - ToAddress string `json:"to_address"` - Value string `json:"value"` - Gas uint64 `json:"gas"` - GasPrice string `json:"gas_price"` - Data string `json:"data"` - MaxFeePerGas string `json:"max_fee_per_gas"` - MaxPriorityFeePerGas string `json:"max_priority_fee_per_gas"` - TransactionType uint8 `json:"transaction_type"` - R string `json:"r"` - S string `json:"s"` - V string `json:"v"` - AccessListJson string `json:"access_list_json"` + ChainId string `json:"chain_id"` + Hash string `json:"hash"` + Nonce uint64 `json:"nonce"` + BlockHash string `json:"block_hash"` + BlockNumber string `json:"block_number"` + BlockTimestamp uint64 `json:"block_timestamp"` + TransactionIndex uint64 `json:"transaction_index"` + FromAddress string `json:"from_address"` + ToAddress string `json:"to_address"` + Value string `json:"value"` + Gas uint64 `json:"gas"` + GasPrice string `json:"gas_price"` + Data string `json:"data"` + MaxFeePerGas string `json:"max_fee_per_gas"` + MaxPriorityFeePerGas string `json:"max_priority_fee_per_gas"` + TransactionType uint8 `json:"transaction_type"` + R string `json:"r"` + S string `json:"s"` + V string `json:"v"` + AccessListJson *string `json:"access_list_json"` + ContractAddress *string `json:"contract_address"` + GasUsed *uint64 `json:"gas_used"` + CumulativeGasUsed *uint64 `json:"cumulative_gas_used"` + EffectiveGasPrice *string `json:"effective_gas_price"` + BlobGasUsed *uint64 `json:"blob_gas_used"` + BlobGasPrice *string `json:"blob_gas_price"` + LogsBloom *string `json:"logs_bloom"` + Status *uint64 `json:"status"` +} + +type DecodedTransactionDataModel struct { + Name string `json:"name"` + Signature string `json:"signature"` + Inputs map[string]interface{} `json:"inputs"` +} + +type DecodedTransactionModel struct { + TransactionModel + Decoded DecodedTransactionDataModel `json:"decoded"` } // @Summary Get all transactions @@ -49,7 +68,7 @@ type TransactionModel struct { // @Param sort_by query string false "Field to sort results by" // @Param sort_order query string false "Sort order (asc or desc)" // @Param page query int false "Page number for pagination" -// @Param limit query int false "Number of items per page" +// @Param limit query int false "Number of items per page" default(5) // @Param aggregate query []string false "List of aggregate functions to apply" // @Success 200 {object} api.QueryResponse{data=[]TransactionModel} // @Failure 400 {object} api.Error @@ -73,7 +92,7 @@ func GetTransactions(c *gin.Context) { // @Param sort_by query string false "Field to sort results by" // @Param sort_order query string false "Sort order (asc or desc)" // @Param page query int false "Page number for pagination" -// @Param limit query int false "Number of items per page" +// @Param limit query int false "Number of items per page" default(5) // @Param aggregate query []string false "List of aggregate functions to apply" // @Success 200 {object} api.QueryResponse{data=[]TransactionModel} // @Failure 400 {object} api.Error @@ -86,22 +105,22 @@ func GetTransactionsByContract(c *gin.Context) { } // @Summary Get transactions by contract and signature -// @Description Retrieve transactions for a specific contract and signature (Not implemented yet) +// @Description Retrieve transactions for a specific contract and signature. When a valid function signature is provided, the response includes decoded transaction data with function inputs. // @Tags transactions // @Accept json // @Produce json // @Security BasicAuth // @Param chainId path string true "Chain ID" // @Param to path string true "Contract address" -// @Param signature path string true "Function signature" +// @Param signature path string true "Function signature (e.g., 'transfer(address,uint256)')" // @Param filter query string false "Filter parameters" // @Param group_by query string false "Field to group results by" // @Param sort_by query string false "Field to sort results by" // @Param sort_order query string false "Sort order (asc or desc)" // @Param page query int false "Page number for pagination" -// @Param limit query int false "Number of items per page" +// @Param limit query int false "Number of items per page" default(5) // @Param aggregate query []string false "List of aggregate functions to apply" -// @Success 200 {object} api.QueryResponse{data=[]TransactionModel} +// @Success 200 {object} api.QueryResponse{data=[]DecodedTransactionModel} // @Failure 400 {object} api.Error // @Failure 401 {object} api.Error // @Failure 500 {object} api.Error