Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
197 changes: 196 additions & 1 deletion cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"log"
"os"
"path/filepath"
"regexp"
"strconv"
"strings"
"text/template"
Expand Down Expand Up @@ -49,7 +50,8 @@ func CreateRootCommand() *cobra.Command {
abiCmd := CreateAbiCommand()
dbCmd := CreateDatabaseOperationCommand()
historicalSyncCmd := CreateHistoricalSyncCommand()
rootCmd.AddCommand(completionCmd, versionCmd, blockchainCmd, starknetCmd, evmCmd, crawlerCmd, inspectorCmd, synchronizerCmd, abiCmd, dbCmd, historicalSyncCmd)
generateJSONCmd := CreateGenerateJSONCommand() // Added generateJSON command here
rootCmd.AddCommand(completionCmd, versionCmd, blockchainCmd, starknetCmd, evmCmd, crawlerCmd, inspectorCmd, synchronizerCmd, abiCmd, dbCmd, historicalSyncCmd, generateJSONCmd)

// By default, cobra Command objects write to stderr. We have to forcibly set them to output to
// stdout.
Expand Down Expand Up @@ -1279,3 +1281,196 @@ func checkSpaceSeparatedAddresses(addrs []string) error {
}
return nil
}

// Define a regex pattern to match Solidity struct definitions.
var solidityStructPattern = regexp.MustCompile(`struct\s+(\w+)\s*{([^}]+)}`)

// Field represents a field in a Solidity struct.
type Field struct {
Name string
Type string
}

// parseSolidityStructs reads a Solidity file and extracts struct definitions with field order preserved.
func parseSolidityStructs(filePath string) (map[string][]Field, error) {
content, err := os.ReadFile(filePath)
if err != nil {
return nil, fmt.Errorf("failed to read file: %w", err)
}

structs := make(map[string][]Field) // Struct name -> Slice of fields (in order)

matches := solidityStructPattern.FindAllStringSubmatch(string(content), -1)

for _, match := range matches {
structName := match[1]
structBody := match[2]

fieldPattern := regexp.MustCompile(`(\w+)\s+(\w+);`)
fieldMatches := fieldPattern.FindAllStringSubmatch(structBody, -1)

fields := []Field{}
for _, fieldMatch := range fieldMatches {
fieldType, fieldName := fieldMatch[1], fieldMatch[2]
fields = append(fields, Field{Name: fieldName, Type: fieldType})
}

structs[structName] = fields
}

return structs, nil
}

// generateDynamicJSON generates JSON using the field order from the Solidity struct.
func generateDynamicJSON(structName string, structMap map[string][]Field) (string, error) {
fields, exists := structMap[structName]
if !exists {
return "", fmt.Errorf("struct %s not found", structName)
}

data := make(map[string]interface{})
reader := bufio.NewReader(os.Stdin)

// Iterate over fields in the order defined in the Solidity struct.
for _, field := range fields {
for {
fmt.Printf("Enter %s (%s): ", field.Name, field.Type)
input, _ := reader.ReadString('\n')
input = strings.TrimSpace(input) // Trim whitespace

var err error
switch field.Type {
case "string", "address":
if input == "" {
fmt.Println("Invalid input, expected a non-empty string.")
continue
}
data[field.Name] = input

case "int", "int24", "int32", "int64":
var val int64
val, err = strconv.ParseInt(input, 10, 64)
if err != nil {
fmt.Println("Invalid input, expected an integer.")
continue
}
data[field.Name] = val

case "uint", "uint256", "uint32", "uint64":
// Treat uint256 as a string to avoid overflow issues.
if field.Type == "uint256" {
if input == "" {
fmt.Println("Invalid input, expected a non-empty string.")
continue
}
data[field.Name] = input
} else {
var val uint64
val, err = strconv.ParseUint(input, 10, 64)
if err != nil {
fmt.Println("Invalid input, expected a positive integer.")
continue
}
data[field.Name] = val
}

case "bool":
var val bool
val, err = strconv.ParseBool(input)
if err != nil {
fmt.Println("Invalid input, expected a boolean (true/false).")
continue
}
data[field.Name] = val

default:
fmt.Printf("Unsupported field type: %s\n", field.Type)
}
break
}
}

// Wrap the data in an array to match the expected structure.
jsonData, err := json.MarshalIndent([]map[string]interface{}{data}, "", " ")
if err != nil {
return "", fmt.Errorf("failed to marshal JSON: %w", err)
}

return string(jsonData), nil
}

// saveJSONToFile saves the generated JSON to a file.
func saveJSONToFile(fileName, jsonStr string) error {
var existingData []map[string]interface{}
var newData []map[string]interface{}

// Parse the new JSON data.
if err := json.Unmarshal([]byte(jsonStr), &newData); err != nil {
return fmt.Errorf("failed to parse new JSON data: %w", err)
}

// Check if the file already exists.
if _, err := os.Stat(fileName); err == nil {
file, err := os.ReadFile(fileName)
if err != nil {
return fmt.Errorf("failed to read existing file: %w", err)
}

// If the file is not empty, parse its content.
if len(file) > 0 {
if err := json.Unmarshal(file, &existingData); err != nil {
return fmt.Errorf("failed to parse existing JSON data: %w", err)
}
}
}

// Append the new data to the existing data.
existingData = append(existingData, newData...)

// Convert the combined data to a JSON string with indentation.
jsonData, err := json.MarshalIndent(existingData, "", " ")
if err != nil {
return fmt.Errorf("failed to marshal JSON data: %w", err)
}

// Write the JSON data to the file.
if err := os.WriteFile(fileName, jsonData, 0644); err != nil {
return fmt.Errorf("failed to write to file: %w", err)
}

fmt.Printf("Data successfully saved to %s\n", fileName)
return nil
}

// CreateGenerateJSONCommand returns a new command for generating JSON from structs.
func CreateGenerateJSONCommand() *cobra.Command {
return &cobra.Command{
Use: "generate-json [solFilePath] [structName]",
Short: "Generate a JSON representation of a struct from a Solidity file",
Args: cobra.ExactArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
solFilePath, structName := args[0], args[1]

structMap, err := parseSolidityStructs(solFilePath)
if err != nil {
return fmt.Errorf("failed to parse Solidity structs: %w", err)
}

jsonStr, err := generateDynamicJSON(structName, structMap)
if err != nil {
return fmt.Errorf("failed to generate JSON: %w", err)
}

fmt.Println("Generated JSON:")
fmt.Println(jsonStr)

outputFileName := structName + ".json"
if err := saveJSONToFile(outputFileName, jsonStr); err != nil {
return fmt.Errorf("failed to save JSON to file: %w", err)
}

fmt.Printf("JSON saved to %s\n", outputFileName)
return nil
},
}
}
15 changes: 15 additions & 0 deletions test_struct_to_json.sol
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.28;

struct MintParams {
address token0;
address token1;
int24 tickLower;
int24 tickUpper;
uint256 amount0Desired;
uint256 amount1Desired;
uint256 amount0Min;
uint256 amount1Min;
address recipient;
uint256 deadline;
}
107 changes: 107 additions & 0 deletions test_struct_to_json_command.bash
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
#!/usr/bin/env bash

# ------------------------------------------------------------------------------
# Test script for the generate-json CLI command.
#
# This script tests the following:
# 1. The CLI command generates the correct Go structs.
# 2. The CLI command creates the expected output file.
# 3. The content of the output file matches the expected JSON.
#
# Prerequisites:
# - The Go program is built and available in your PATH.
# - A sample Solidity file (test_solidity.sol) exists in the current directory.
# - `jq` is installed for JSON processing.
# ------------------------------------------------------------------------------

set -euo pipefail # Exit on error, undefined variable, or pipeline failure

# Variables
SOLIDITY_FILE="test_struct_to_json.sol"
STRUCT_NAME="MintParams"
OUTPUT_FILE="${STRUCT_NAME}.json"
EXPECTED_JSON='[
{
"amount0Desired": "1000000000000000000",
"amount0Min": "900000000000000000",
"amount1Desired": "1000000",
"amount1Min": "900000",
"deadline": "1695660000",
"recipient": "0xhidd3n",
"tickLower": -887220,
"tickUpper": 887220,
"token0": "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2",
"token1": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48"
}
]'

# Cleanup function
cleanup() {
if [[ -f "$OUTPUT_FILE" ]]; then
rm "$OUTPUT_FILE"
echo "Cleaned up $OUTPUT_FILE"
fi
}

# Register cleanup function to run on script exit
trap cleanup EXIT

# Check prerequisites
if ! command -v jq &> /dev/null; then
echo "Error: 'jq' is not installed. Please install it and try again."
exit 1
fi

if [[ ! -f "$SOLIDITY_FILE" ]]; then
echo "Error: Solidity file $SOLIDITY_FILE not found!"
exit 1
fi

# Test 1: Check if the program generates the correct JSON
echo "Running Test 1: Generate JSON from Solidity file"
go run . generate-json "$SOLIDITY_FILE" "$STRUCT_NAME" <<EOF
0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2
0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48
-887220
887220
1000000000000000000
1000000
900000000000000000
900000
0xhidd3n
1695660000
EOF

# Check if the CLI command succeeded
if [[ $? -ne 0 ]]; then
echo "Error: CLI command failed!"
exit 1
fi

# Test 2: Check if the output file was created
echo "Running Test 2: Save JSON to file"
if [[ ! -f "$OUTPUT_FILE" ]]; then
echo "Error: Output file $OUTPUT_FILE not found!"
exit 1
fi
echo "Output file $OUTPUT_FILE created successfully."

# Test 3: Verify the content of the output file
echo "Running Test 3: Verify JSON content"
ACTUAL_JSON=$(cat "$OUTPUT_FILE")

# Normalize JSON (remove whitespace and newlines)
NORMALIZED_EXPECTED=$(echo "$EXPECTED_JSON" | jq -c .)
NORMALIZED_ACTUAL=$(echo "$ACTUAL_JSON" | jq -c .)

if [[ "$NORMALIZED_ACTUAL" != "$NORMALIZED_EXPECTED" ]]; then
echo "Error: JSON content does not match expected output!"
echo "Expected:"
echo "$NORMALIZED_EXPECTED"
echo "Got:"
echo "$NORMALIZED_ACTUAL"
exit 1
fi
echo "JSON content matches expected output."

echo "All tests passed!"