From 52a855b9efc8907059c4f58a731807dc1fe1f477 Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Mon, 1 Sep 2025 19:18:16 -0700 Subject: [PATCH 1/3] initial parsing commit --- .../eg-3/contract_cchepgth_bindings.go | 184 +++ ingest/tutorial/eg-3/event_parser.go | 1379 +++++++++++++++++ 2 files changed, 1563 insertions(+) create mode 100644 ingest/tutorial/eg-3/contract_cchepgth_bindings.go create mode 100644 ingest/tutorial/eg-3/event_parser.go diff --git a/ingest/tutorial/eg-3/contract_cchepgth_bindings.go b/ingest/tutorial/eg-3/contract_cchepgth_bindings.go new file mode 100644 index 0000000000..af9054b6b3 --- /dev/null +++ b/ingest/tutorial/eg-3/contract_cchepgth_bindings.go @@ -0,0 +1,184 @@ +// SEP-48 Contract Bindings +// Auto-generated from Soroban contract specification +// Contract ID: CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2 +// +// This file contains: +// - Event structures and parsers +// - Function interfaces +// - Type definitions +// - Error constants + +package contracts + +import ( + "fmt" + "math/big" + "github.com/stellar/go/xdr" +) + +// Contract metadata +const ContractID = "CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2" + +// ============================================================================ +// CONTRACT EVENTS (Complete SEP-48 Implementation) +// ============================================================================ + +// DefaultEventEvent represents the 'DefaultEvent' contract event +// +// Event Structure: +// - Prefix Topics: [default_event] +// - Data Format: Map +// - Topic Parameters: 2 +// - Data Parameters: 3 +type DefaultEventEvent struct { + // Event metadata (for validation) + EventName string `json:"event_name"` + Prefix0 string `json:"prefix_0"` // Expected: "default_event" + + // Topic parameters (indexed, searchable) + Addr string `json:"addr"` // Topic: string + Num uint32 `json:"num"` // Topic: uint32 + + // Data parameters (event payload) + Bignum *big.Int `json:"bignum"` // Data: *big.Int + Nested []map[string]int64 `json:"nested"` // Data: []map[string]int64 + Any interface{} `json:"any"` // Data: interface{} +} + +// ParseDefaultEventEvent parses a 'DefaultEvent' event from Stellar ContractEvent XDR +// +// This parser validates: +// - Topic count and structure +// - Prefix topic values +// - Data format (Map) +// - Parameter types and conversion +// +// Returns: (*DefaultEventEvent, error) +func ParseDefaultEventEvent(contractEvent xdr.ContractEvent) (*DefaultEventEvent, error) { + // Extract event components from XDR + topics := contractEvent.Body.V0.Topics + data := contractEvent.Body.V0.Data + + // Validate topic structure + if len(topics) < 3 { + return nil, fmt.Errorf("invalid 'DefaultEvent' event: expected at least 3 topics, got %d", len(topics)) + } + + // Validate prefix topics (event signature) + if topics[0].Str() == nil || string(*topics[0].Str()) != "default_event" { + return nil, fmt.Errorf("invalid event signature: expected 'default_event' at topic[0]") + } + + // Validate data format (expected: Map) + if data.Map() == nil { + return nil, fmt.Errorf("invalid event data: expected Map format") + } + dataMap := data.Map() + + // Create event instance + event := &DefaultEventEvent{ + EventName: "DefaultEvent", + Prefix0: "default_event", + } + + // Extract and convert topic parameters + // Topic parameter: addr (string) + topic1 := topics[1] + + var topic1Value string + if topic1.Str() != nil { + topic1Value = string(*topic1.Str()) + } else if topic1.Sym() != nil { + topic1Value = string(*topic1.Sym()) + } else { + return nil, fmt.Errorf("expected string value for topic1") + } event.Addr = topic1Value + + // Topic parameter: num (uint32) + topic2 := topics[2] + + var topic2Value uint32 + if topic2.U32() != nil { + topic2Value = uint32(*topic2.U32()) + } else { + return nil, fmt.Errorf("expected uint32 value for topic2") + } event.Num = topic2Value + + // Extract and convert data parameters + // Extract parameters from data map + // Data parameter: bignum (*big.Int) + if bignumVal, exists := (*dataMap)["bignum"]; exists { + + var bignumValValue *big.Int + if bignumVal.I128() != nil { + bignumValValue = new(big.Int) + bignumValValue.SetBytes((*bignumVal.I128())[:]) + } else if bignumVal.U128() != nil { + bignumValValue = new(big.Int) + bignumValValue.SetBytes((*bignumVal.U128())[:]) + } else { + return nil, fmt.Errorf("expected 128-bit int value for %!s(MISSING)") + } event.Bignum = bignumValValue + } else { + return nil, fmt.Errorf("missing required data parameter: bignum") + } + + // Data parameter: nested ([]map[string]int64) + if nestedVal, exists := (*dataMap)["nested"]; exists { + + // TODO: Convert nestedVal to []map[string]int64 + // This requires custom conversion logic for user-defined types + nestedValValue := nestedVal // Placeholder event.Nested = nestedValValue + } else { + return nil, fmt.Errorf("missing required data parameter: nested") + } + + // Data parameter: any (interface{}) + if anyVal, exists := (*dataMap)["any"]; exists { + + // TODO: Convert anyVal to interface{} + // This requires custom conversion logic for user-defined types + anyValValue := anyVal // Placeholder event.Any = anyValValue + } else { + return nil, fmt.Errorf("missing required data parameter: any") + } + + return event, nil +} + +// ParseContractEvent attempts to parse any contract event +// Returns the parsed event as an interface{} or an error +func ParseContractEvent(contractEvent xdr.ContractEvent) (interface{}, error) { + topics := contractEvent.Body().V0.Topics + if len(topics) == 0 { + return nil, fmt.Errorf("event has no topics") + } + + // Try to identify event by first topic (event name/prefix) + firstTopic := topics[0] + var eventName string + if firstTopic.Str() != nil { + eventName = string(*firstTopic.Str()) + } else { + return nil, fmt.Errorf("cannot identify event: first topic is not a string") + } + + // Dispatch to appropriate parser based on event signature + switch eventName { + case "default_event": + return ParseDefaultEventEvent(contractEvent) + default: + return nil, fmt.Errorf("unknown event type: %s", eventName) + } +} + +// ============================================================================ +// CONTRACT FUNCTIONS +// ============================================================================ + +// ContractClient defines the interface for interacting with the contract +type ContractClient interface { + emit() error + +} + diff --git a/ingest/tutorial/eg-3/event_parser.go b/ingest/tutorial/eg-3/event_parser.go new file mode 100644 index 0000000000..2a1acecd16 --- /dev/null +++ b/ingest/tutorial/eg-3/event_parser.go @@ -0,0 +1,1379 @@ +package main + +import ( + "bytes" + "encoding/binary" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/stellar/go/strkey" + "github.com/stellar/go/xdr" +) + +/* +================================================================================ +SECTION 1: SOROBAN RPC CLIENT - WASM FETCHING +================================================================================ +This section implements the same WASM fetching logic as the Stellar JavaScript SDK. +The process is: +1. Contract ID → Contract Instance (via getLedgerEntries) +2. Contract Instance → WASM Hash (from instance executable) +3. WASM Hash → WASM Bytecode (via getLedgerEntries) + +This mirrors the JS SDK's Contract.getFootprint() and related methods. +*/ + +// SorobanRPCClient handles communication with Soroban RPC endpoints +type SorobanRPCClient struct { + endpoint string // RPC endpoint URL + client *http.Client // HTTP client for making requests +} + +// Request/Response structures for Soroban RPC getLedgerEntries method +type GetLedgerEntriesRequest struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params struct { + Keys []string `json:"keys"` // XDR-encoded ledger keys + } `json:"params"` +} + +type GetLedgerEntriesResponse struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Result struct { + Entries []struct { + Key string `json:"key"` // XDR-encoded ledger key + XDR string `json:"xdr"` // XDR-encoded ledger entry data + } `json:"entries"` + } `json:"result"` + Error *struct { + Code int `json:"code"` + Message string `json:"message"` + } `json:"error,omitempty"` +} + +// NewSorobanRPCClient creates a new RPC client instance +func NewSorobanRPCClient(endpoint string) *SorobanRPCClient { + return &SorobanRPCClient{ + endpoint: endpoint, + client: &http.Client{}, + } +} + +// createContractInstanceLedgerKey creates a ledger key for fetching contract instance data +// This is equivalent to JS SDK's Contract.getFootprint() method +func createContractInstanceLedgerKey(contractId string) (string, error) { + // Decode the contract ID from StrKey format (C...) + decoded, err := strkey.Decode(strkey.VersionByteContract, contractId) + if err != nil { + return "", fmt.Errorf("invalid contract ID format: %w", err) + } + var destinationContractId xdr.ContractId + copy(destinationContractId[:], decoded) + + // Create ScAddress for the contract + contractAddress := xdr.ScAddress{Type: xdr.ScAddressTypeScAddressTypeContract, ContractId: &destinationContractId} + + // Create ledger key for contract instance data + // This targets the persistent storage entry containing contract metadata + ledgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractData, + ContractData: &xdr.LedgerKeyContractData{ + Contract: contractAddress, + Key: xdr.ScVal{Type: xdr.ScValTypeScvLedgerKeyContractInstance}, + Durability: xdr.ContractDataDurabilityPersistent, + }, + } + + // Convert to base64 XDR for RPC transmission + return xdr.MarshalBase64(ledgerKey) +} + +// extractWasmHashFromContractData extracts the WASM hash from contract instance XDR data +// The contract instance contains a reference to the WASM code via its hash +func extractWasmHashFromContractData(contractDataXdr string) (string, error) { + // Decode the contract data XDR + var ledgerEntry xdr.LedgerEntryData + + if err := xdr.SafeUnmarshalBase64(contractDataXdr, &ledgerEntry); err != nil { + return "", fmt.Errorf("failed to decode contract data XDR: %w", err) + } + + // Extract WASM hash from the contract instance + // The instance.executable.wasmHash points to the actual WASM code + wasmHash := ledgerEntry.ContractData.Val.Instance.Executable.WasmHash + if wasmHash == nil { + return "", fmt.Errorf("contract instance does not contain WASM hash") + } + + // Create ledger key for the WASM code entry + wasmLedgerKey := xdr.LedgerKey{ + Type: xdr.LedgerEntryTypeContractCode, + ContractCode: &xdr.LedgerKeyContractCode{Hash: *wasmHash}, + } + + return xdr.MarshalBase64(wasmLedgerKey) +} + +// FetchContractWasm fetches the complete WASM bytecode for a given contract ID +// This implements the same logic as the JS SDK's contract WASM fetching +func (client *SorobanRPCClient) FetchContractWasm(contractId string) ([]byte, error) { + fmt.Printf("šŸ” Fetching WASM for contract: %s\n", contractId) + + // Step 1: Get the contract instance to find WASM hash + contractLedgerKey, err := createContractInstanceLedgerKey(contractId) + if err != nil { + return nil, fmt.Errorf("failed to create contract instance key: %w", err) + } + + contractResponse, err := client.getLedgerEntries([]string{contractLedgerKey}) + if err != nil { + return nil, fmt.Errorf("failed to fetch contract instance: %w", err) + } + if len(contractResponse.Result.Entries) == 0 { + return nil, fmt.Errorf("contract instance not found for ID: %s", contractId) + } + + // Step 2: Extract WASM hash from contract instance + wasmLedgerKey, err := extractWasmHashFromContractData(contractResponse.Result.Entries[0].XDR) + if err != nil { + return nil, fmt.Errorf("failed to extract WASM hash: %w", err) + } + + // Step 3: Fetch the actual WASM bytecode using the hash + wasmResponse, err := client.getLedgerEntries([]string{wasmLedgerKey}) + if err != nil { + return nil, fmt.Errorf("failed to fetch WASM code: %w", err) + } + if len(wasmResponse.Result.Entries) == 0 { + return nil, fmt.Errorf("WASM code not found") + } + + // Step 4: Decode WASM bytes from XDR + var wasmEntry xdr.LedgerEntryData + if err := xdr.SafeUnmarshalBase64(wasmResponse.Result.Entries[0].XDR, &wasmEntry); err != nil { + return nil, fmt.Errorf("failed to decode WASM XDR: %w", err) + } + + wasmBytes := wasmEntry.ContractCode.Code + fmt.Printf("āœ… Successfully fetched %d bytes of WASM\n", len(wasmBytes)) + return wasmBytes, nil +} + +// getLedgerEntries makes a JSON-RPC call to fetch ledger entries by their keys +func (client *SorobanRPCClient) getLedgerEntries(keys []string) (*GetLedgerEntriesResponse, error) { + // Prepare JSON-RPC request + req := GetLedgerEntriesRequest{ + JSONRPC: "2.0", + ID: 1, + Method: "getLedgerEntries", + } + req.Params.Keys = keys + + // Marshal request to JSON + reqBody, err := json.Marshal(req) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + // Make HTTP POST request + httpResp, err := client.client.Post(client.endpoint, "application/json", bytes.NewReader(reqBody)) + if err != nil { + return nil, fmt.Errorf("HTTP request failed: %w", err) + } + defer httpResp.Body.Close() + + // Parse JSON response + var resp GetLedgerEntriesResponse + if err := json.NewDecoder(httpResp.Body).Decode(&resp); err != nil { + return nil, fmt.Errorf("failed to decode response: %w", err) + } + + // Check for RPC errors + if resp.Error != nil { + return nil, fmt.Errorf("RPC error %d: %s", resp.Error.Code, resp.Error.Message) + } + + return &resp, nil +} + +/* +================================================================================ +SECTION 2: WASM BINARY PARSER +================================================================================ +This section parses the WebAssembly binary format to extract custom sections. +SEP-48 specifications are stored in the "contractspecv0" custom section. + +WASM format: +- Magic number: 0x6d736100 (\0asm) +- Version: 0x01000000 (version 1) +- Sections: Each section has a type ID and LEB128-encoded size +- Custom sections (type 0) contain the contract specifications +*/ + +// WASM binary format constants +const ( + WasmMagic = 0x6d736100 // \0asm magic number + WasmVersion = 0x00000001 // WASM version 1 in little-endian +) + +// parseWasmCustomSections extracts all custom sections from a WASM binary +// Custom sections contain metadata like SEP-48 contract specifications +func parseWasmCustomSections(wasmBytes []byte) (map[string][]byte, error) { + reader := bytes.NewReader(wasmBytes) + + // Validate WASM header (magic + version) + var magic, version uint32 + if err := binary.Read(reader, binary.LittleEndian, &magic); err != nil { + return nil, fmt.Errorf("failed to read WASM magic: %w", err) + } + if err := binary.Read(reader, binary.LittleEndian, &version); err != nil { + return nil, fmt.Errorf("failed to read WASM version: %w", err) + } + + if magic != WasmMagic { + return nil, fmt.Errorf("invalid WASM magic number: got 0x%x, expected 0x%x", magic, WasmMagic) + } + if version != WasmVersion { + return nil, fmt.Errorf("unsupported WASM version: got 0x%x, expected 0x%x", version, WasmVersion) + } + + customSections := make(map[string][]byte) + + // Parse all sections in the WASM binary + for { + // Read section type (1 byte) + sectionType, err := reader.ReadByte() + if err == io.EOF { + break // End of file + } + if err != nil { + return nil, fmt.Errorf("failed to read section type: %w", err) + } + + // Read section size (LEB128 encoded) + size, err := readLEB128(reader) + if err != nil { + return nil, fmt.Errorf("failed to read section size: %w", err) + } + + // Read section data + sectionData := make([]byte, size) + if _, err := io.ReadFull(reader, sectionData); err != nil { + return nil, fmt.Errorf("failed to read section data: %w", err) + } + + // Process custom sections (section type 0) + // These contain metadata like contract specifications + if sectionType == 0 { + name, payload, err := parseCustomSection(sectionData) + if err == nil && len(payload) > 0 { + customSections[name] = payload + fmt.Printf("šŸ“¦ Found custom section: %s (%d bytes)\n", name, len(payload)) + } + } + } + + return customSections, nil +} + +// parseCustomSection extracts the name and payload from a custom section +// Custom sections format: [name_length:LEB128][name:bytes][payload:bytes] +func parseCustomSection(data []byte) (string, []byte, error) { + reader := bytes.NewReader(data) + + // Read section name length (LEB128) + nameLength, err := readLEB128(reader) + if err != nil { + return "", nil, fmt.Errorf("failed to read name length: %w", err) + } + + // Read section name + nameBytes := make([]byte, nameLength) + if _, err := io.ReadFull(reader, nameBytes); err != nil { + return "", nil, fmt.Errorf("failed to read section name: %w", err) + } + + // Read remaining data as payload + payload := make([]byte, reader.Len()) + if _, err := io.ReadFull(reader, payload); err != nil { + return "", nil, fmt.Errorf("failed to read section payload: %w", err) + } + + return string(nameBytes), payload, nil +} + +// readLEB128 reads a Little Endian Base 128 encoded unsigned integer +// This is the standard variable-length integer encoding used in WASM +func readLEB128(reader *bytes.Reader) (uint32, error) { + var result uint32 + var shift uint + + for { + b, err := reader.ReadByte() + if err != nil { + return 0, err + } + + // Extract 7 bits and shift into position + result |= uint32(b&0x7F) << shift + + // If high bit is 0, we're done + if (b & 0x80) == 0 { + break + } + + shift += 7 + // Prevent overflow for malformed LEB128 + if shift >= 32 { + return 0, fmt.Errorf("LEB128 value too large") + } + } + + return result, nil +} + +/* +================================================================================ +SECTION 3: SEP-48 SPECIFICATION PARSER +================================================================================ +This section extracts and parses XDR-encoded ScSpecEntry objects from the +"contractspecv0" custom section. SEP-48 defines how smart contracts specify +their interface including functions, types, events, and errors. + +Reference: https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0048.md +*/ + +// extractContractSpec parses SEP-48 contract specifications from WASM +// The contractspecv0 section contains XDR-encoded ScSpecEntry objects +func extractContractSpec(wasmBytes []byte) ([]xdr.ScSpecEntry, error) { + fmt.Printf("šŸ” Extracting SEP-48 contract specifications...\n") + + // Parse WASM to find custom sections + customSections, err := parseWasmCustomSections(wasmBytes) + if err != nil { + return nil, fmt.Errorf("failed to parse WASM custom sections: %w", err) + } + + // Look for the contractspecv0 section (SEP-48 standard) + specData, exists := customSections["contractspecv0"] + if !exists { + return nil, fmt.Errorf("contractspecv0 section not found - contract may not follow SEP-48") + } + + fmt.Printf("šŸ“‹ Found contractspecv0 section (%d bytes)\n", len(specData)) + + // Parse XDR-encoded specification entries + var entries []xdr.ScSpecEntry + reader := bytes.NewReader(specData) + + // Each ScSpecEntry is XDR-encoded sequentially + for reader.Len() > 0 { + var entry xdr.ScSpecEntry + + // Unmarshal one ScSpecEntry from XDR + bytesRead, err := xdr.Unmarshal(reader, &entry) + if err != nil { + return nil, fmt.Errorf("failed to unmarshal ScSpecEntry: %w", err) + } + + entries = append(entries, entry) + fmt.Printf("šŸ“ Parsed spec entry: %s (%d bytes)\n", entry.Kind, bytesRead) + } + + fmt.Printf("āœ… Successfully parsed %d specification entries\n", len(entries)) + return entries, nil +} + +/* +================================================================================ +SECTION 4: CONTRACT ANALYSIS WITH COMPLETE EVENT PARSING +================================================================================ +This section analyzes the parsed SEP-48 specifications and extracts all +contract components: functions, types, events, and errors. + +The event parsing here follows the same logic as the TypeScript implementation +referenced by the user, properly handling: +- Prefix topics (like "default_event") +- Topic parameters vs data parameters +- Data formats (Map, Vec, SingleValue) +- Parameter types and locations +*/ + +// ContractAnalysis represents the complete analysis of a contract's specifications +type ContractAnalysis struct { + ContractID string // Contract identifier + Functions []FunctionSpec // Contract functions/methods + Types []TypeSpec // User-defined types (structs, enums) + Events []EventSpec // Event specifications (SEP-48) + Errors []ErrorSpec // Error definitions +} + +// FunctionSpec represents a contract function signature +type FunctionSpec struct { + Name string // Function name + Inputs []ParameterSpec // Input parameters + Outputs []ParameterSpec // Return values + Description string // Documentation string +} + +// ParameterSpec represents a function parameter +type ParameterSpec struct { + Name string // Parameter name + Type string // Go type representation +} + +// TypeSpec represents a user-defined type (struct or enum) +type TypeSpec struct { + Name string // Type name + Kind string // "struct" or "enum" + Fields []FieldSpec // Struct fields (empty for enums) +} + +// FieldSpec represents a struct field +type FieldSpec struct { + Name string // Field name + Type string // Go type representation +} + +// ErrorSpec represents a contract error definition +type ErrorSpec struct { + Name string // Error name + Value uint32 // Error code +} + +// EventSpec represents a complete event specification following SEP-48 +// This matches the structure used in the TypeScript SDK +type EventSpec struct { + Name string // Event name (e.g., "transfer") + PrefixTopics []string // Fixed prefix topics (e.g., ["default_event"]) + TopicParams []EventParamSpec // Parameters stored in event topics + DataParams []EventParamSpec // Parameters stored in event data + DataFormat string // "Map", "Vec", or "SingleValue" + Description string // Event documentation +} + +// EventParamSpec represents an event parameter +type EventParamSpec struct { + Name string // Parameter name + Type string // Go type representation + Location string // "topic" or "data" +} + +// analyzeContract performs complete analysis of SEP-48 contract specifications +func analyzeContract(contractId string, specEntries []xdr.ScSpecEntry) (*ContractAnalysis, error) { + fmt.Printf("šŸ”¬ Analyzing contract specifications...\n") + + analysis := &ContractAnalysis{ContractID: contractId} + + // Process each specification entry + for _, entry := range specEntries { + switch entry.Kind { + case xdr.ScSpecEntryKindScSpecEntryFunctionV0: + // Analyze function specifications + fn := analyzeFunctionSpec(entry.FunctionV0) + analysis.Functions = append(analysis.Functions, fn) + fmt.Printf(" šŸ“‹ Function: %s\n", fn.Name) + + case xdr.ScSpecEntryKindScSpecEntryUdtStructV0: + // Analyze struct type definitions + typ := analyzeStructSpec(entry.UdtStructV0) + analysis.Types = append(analysis.Types, typ) + fmt.Printf(" šŸ—ļø Struct: %s\n", typ.Name) + + case xdr.ScSpecEntryKindScSpecEntryUdtEnumV0: + // Analyze enum type definitions + typ := analyzeEnumSpec(entry.UdtEnumV0) + analysis.Types = append(analysis.Types, typ) + fmt.Printf(" šŸ“ Enum: %s\n", typ.Name) + + case xdr.ScSpecEntryKindScSpecEntryUdtErrorEnumV0: + // Analyze error definitions + errors := analyzeErrorEnumSpec(entry.UdtErrorEnumV0) + analysis.Errors = append(analysis.Errors, errors...) + fmt.Printf(" āš ļø Error enum with %d errors\n", len(errors)) + + case xdr.ScSpecEntryKindScSpecEntryEventV0: + // Analyze event specifications (KEY FEATURE) + event := analyzeEventSpec(entry.EventV0) + analysis.Events = append(analysis.Events, event) + fmt.Printf(" šŸ”” Event: %s (format: %s)\n", event.Name, event.DataFormat) + } + } + + fmt.Printf("āœ… Analysis complete: %d functions, %d types, %d events, %d errors\n", + len(analysis.Functions), len(analysis.Types), len(analysis.Events), len(analysis.Errors)) + + return analysis, nil +} + +// analyzeEventSpec performs detailed analysis of event specifications +// This is the core function that properly parses SEP-48 events +func analyzeEventSpec(eventSpec *xdr.ScSpecEventV0) EventSpec { + event := EventSpec{ + Name: string(eventSpec.Name), + Description: string(eventSpec.Doc), + } + + // Extract prefix topics (e.g., "default_event", "contract_event") + // These are fixed strings that appear at the beginning of every event + for _, prefixTopic := range eventSpec.PrefixTopics { + event.PrefixTopics = append(event.PrefixTopics, string(prefixTopic)) + } + + // Determine data format for event payload + // This affects how parameters are encoded in the event data + switch eventSpec.DataFormat { + case xdr.ScSpecEventDataFormatScSpecEventDataFormatMap: + event.DataFormat = "Map" // Key-value pairs in data + case xdr.ScSpecEventDataFormatScSpecEventDataFormatVec: + event.DataFormat = "Vec" // Array of values in data + case xdr.ScSpecEventDataFormatScSpecEventDataFormatSingleValue: + event.DataFormat = "SingleValue" // Single value in data + } + + // Process event parameters and classify by location + // SEP-48 allows parameters to be stored in topics or data sections + for _, param := range eventSpec.Params { + eventParam := EventParamSpec{ + Name: string(param.Name), + Type: convertScSpecTypeDef(param.Type), + } + + // Determine parameter location: topic vs data + switch param.Location { + case xdr.ScSpecEventParamLocationV0ScSpecEventParamLocationTopicList: + // Topic parameters are indexed and searchable + eventParam.Location = "topic" + event.TopicParams = append(event.TopicParams, eventParam) + case xdr.ScSpecEventParamLocationV0ScSpecEventParamLocationData: + // Data parameters contain the bulk event payload + eventParam.Location = "data" + event.DataParams = append(event.DataParams, eventParam) + } + } + + return event +} + +// analyzeFunctionSpec extracts function signature information +func analyzeFunctionSpec(fn *xdr.ScSpecFunctionV0) FunctionSpec { + spec := FunctionSpec{ + Name: string(fn.Name), + Description: string(fn.Doc), + } + + // Process input parameters + for _, input := range fn.Inputs { + spec.Inputs = append(spec.Inputs, ParameterSpec{ + Name: string(input.Name), + Type: convertScSpecTypeDef(input.Type), + }) + } + + // Process output parameters + for _, output := range fn.Outputs { + spec.Outputs = append(spec.Outputs, ParameterSpec{ + Type: convertScSpecTypeDef(output), + }) + } + + return spec +} + +// analyzeStructSpec extracts struct type information +func analyzeStructSpec(st *xdr.ScSpecUdtStructV0) TypeSpec { + spec := TypeSpec{ + Name: string(st.Name), + Kind: "struct", + } + + // Process struct fields + for _, field := range st.Fields { + spec.Fields = append(spec.Fields, FieldSpec{ + Name: string(field.Name), + Type: convertScSpecTypeDef(field.Type), + }) + } + + return spec +} + +// analyzeEnumSpec extracts enum type information +func analyzeEnumSpec(en *xdr.ScSpecUdtEnumV0) TypeSpec { + // Note: Enum cases are not exposed in the current XDR structure + return TypeSpec{ + Name: string(en.Name), + Kind: "enum", + } +} + +// analyzeErrorEnumSpec extracts error definitions +func analyzeErrorEnumSpec(err *xdr.ScSpecUdtErrorEnumV0) []ErrorSpec { + var errors []ErrorSpec + for _, case_ := range err.Cases { + errors = append(errors, ErrorSpec{ + Name: string(case_.Name), + Value: uint32(case_.Value), + }) + } + return errors +} + +// convertScSpecTypeDef converts XDR type definitions to Go type strings +// This handles the mapping between Stellar/XDR types and Go types +func convertScSpecTypeDef(typeDef xdr.ScSpecTypeDef) string { + switch typeDef.Type { + case xdr.ScSpecTypeScSpecTypeBool: + return "bool" + case xdr.ScSpecTypeScSpecTypeI32: + return "int32" + case xdr.ScSpecTypeScSpecTypeU32: + return "uint32" + case xdr.ScSpecTypeScSpecTypeI64: + return "int64" + case xdr.ScSpecTypeScSpecTypeU64: + return "uint64" + case xdr.ScSpecTypeScSpecTypeI128: + return "*big.Int" // 128-bit integers require big.Int + case xdr.ScSpecTypeScSpecTypeU128: + return "*big.Int" + case xdr.ScSpecTypeScSpecTypeString: + return "string" + case xdr.ScSpecTypeScSpecTypeSymbol: + return "string" // Symbols are represented as strings + case xdr.ScSpecTypeScSpecTypeBytes: + return "[]byte" + case xdr.ScSpecTypeScSpecTypeAddress: + return "string" // Stellar addresses as strings + case xdr.ScSpecTypeScSpecTypeVec: + // Recursive type for vectors/arrays + elementType := convertScSpecTypeDef(typeDef.Vec.ElementType) + return fmt.Sprintf("[]%s", elementType) + case xdr.ScSpecTypeScSpecTypeMap: + // Recursive types for maps + keyType := convertScSpecTypeDef(typeDef.Map.KeyType) + valueType := convertScSpecTypeDef(typeDef.Map.ValueType) + return fmt.Sprintf("map[%s]%s", keyType, valueType) + case xdr.ScSpecTypeScSpecTypeUdt: + // User-defined types use their names directly + return string(typeDef.Udt.Name) + default: + return "interface{}" // Fallback for unknown types + } +} + +/* +================================================================================ +SECTION 5: XDR VALUE CONVERSION UTILITIES +================================================================================ +This section provides utilities for converting XDR values to native Go types. +These functions are essential for the event parsing code generation. +*/ + +// XDRValueConverter provides methods to convert XDR values to Go types +type XDRValueConverter struct{} + +// ConvertScValToGoCode generates Go code to convert an XDR ScVal to a native type +// This is used in the generated event parsers +func (c *XDRValueConverter) ConvertScValToGoCode(varName, targetType string) string { + switch targetType { + case "string": + return fmt.Sprintf(` + var %sValue string + if %s.Str() != nil { + %sValue = string(*%s.Str()) + } else if %s.Sym() != nil { + %sValue = string(*%s.Sym()) + } else { + return nil, fmt.Errorf("expected string value for %s") + }`, varName, varName, varName, varName, varName, varName, varName, varName) + + case "bool": + return fmt.Sprintf(` + var %sValue bool + if %s.B() != nil { + %sValue = bool(*%s.B()) + } else { + return nil, fmt.Errorf("expected bool value for %s") + }`, varName, varName, varName, varName, varName) + + case "int32": + return fmt.Sprintf(` + var %sValue int32 + if %s.I32() != nil { + %sValue = int32(*%s.I32()) + } else { + return nil, fmt.Errorf("expected int32 value for %s") + }`, varName, varName, varName, varName, varName) + + case "uint32": + return fmt.Sprintf(` + var %sValue uint32 + if %s.U32() != nil { + %sValue = uint32(*%s.U32()) + } else { + return nil, fmt.Errorf("expected uint32 value for %s") + }`, varName, varName, varName, varName, varName) + + case "int64": + return fmt.Sprintf(` + var %sValue int64 + if %s.I64() != nil { + %sValue = int64(*%s.I64()) + } else { + return nil, fmt.Errorf("expected int64 value for %s") + }`, varName, varName, varName, varName, varName) + + case "uint64": + return fmt.Sprintf(` + var %sValue uint64 + if %s.U64() != nil { + %sValue = uint64(*%s.U64()) + } else { + return nil, fmt.Errorf("expected uint64 value for %s") + }`, varName, varName, varName, varName, varName) + + case "*big.Int": + return fmt.Sprintf(` + var %sValue *big.Int + if %s.I128() != nil { + %sValue = new(big.Int) + %sValue.SetBytes((*%s.I128())[:]) + } else if %s.U128() != nil { + %sValue = new(big.Int) + %sValue.SetBytes((*%s.U128())[:]) + } else { + return nil, fmt.Errorf("expected 128-bit int value for %s") + }`, varName, varName, varName, varName, varName, varName, varName, varName, varName) + + case "[]byte": + return fmt.Sprintf(` + var %sValue []byte + if %s.Bytes() != nil { + %sValue = []byte(*%s.Bytes()) + } else { + return nil, fmt.Errorf("expected bytes value for %s") + }`, varName, varName, varName, varName, varName) + + default: + return fmt.Sprintf(` + // TODO: Convert %s to %s + // This requires custom conversion logic for user-defined types + %sValue := %s // Placeholder`, varName, targetType, varName, varName) + } +} + +/* +================================================================================ +SECTION 6: GO CODE GENERATION WITH COMPLETE EVENT PARSING +================================================================================ +This section generates complete Go bindings for the contract, including +fully functional event parsers that can convert Stellar ContractEvent XDR +into native Go structs. +*/ + +// generateGoCode creates complete Go bindings for the analyzed contract +func generateGoCode(analysis *ContractAnalysis) string { + var output strings.Builder + + // Write file header and imports + output.WriteString("// SEP-48 Contract Bindings\n") + output.WriteString("// Auto-generated from Soroban contract specification\n") + output.WriteString(fmt.Sprintf("// Contract ID: %s\n", analysis.ContractID)) + output.WriteString("//\n") + output.WriteString("// This file contains:\n") + output.WriteString("// - Event structures and parsers\n") + output.WriteString("// - Function interfaces\n") + output.WriteString("// - Type definitions\n") + output.WriteString("// - Error constants\n\n") + + output.WriteString("package contracts\n\n") + output.WriteString("import (\n") + output.WriteString("\t\"fmt\"\n") + output.WriteString("\t\"math/big\"\n") + output.WriteString("\t\"github.com/stellar/go/xdr\"\n") + output.WriteString(")\n\n") + + // Generate contract metadata + output.WriteString("// Contract metadata\n") + output.WriteString(fmt.Sprintf("const ContractID = \"%s\"\n\n", analysis.ContractID)) + + // Generate event structures and parsers + if len(analysis.Events) > 0 { + output.WriteString("// ============================================================================\n") + output.WriteString("// CONTRACT EVENTS (Complete SEP-48 Implementation)\n") + output.WriteString("// ============================================================================\n\n") + + for _, event := range analysis.Events { + generateCompleteEventStruct(&output, event) + generateCompleteEventParser(&output, event) + } + + // Generate event dispatcher + generateEventDispatcher(&output, analysis.Events) + } + + // Generate function interfaces + if len(analysis.Functions) > 0 { + output.WriteString("// ============================================================================\n") + output.WriteString("// CONTRACT FUNCTIONS\n") + output.WriteString("// ============================================================================\n\n") + generateClientInterface(&output, analysis.Functions) + } + + // Generate type definitions + if len(analysis.Types) > 0 { + output.WriteString("// ============================================================================\n") + output.WriteString("// CONTRACT TYPES\n") + output.WriteString("// ============================================================================\n\n") + for _, typ := range analysis.Types { + generateTypeDefinition(&output, typ) + } + } + + // Generate error constants + if len(analysis.Errors) > 0 { + output.WriteString("// ============================================================================\n") + output.WriteString("// CONTRACT ERRORS\n") + output.WriteString("// ============================================================================\n\n") + generateErrorDefinitions(&output, analysis.Errors) + } + + return output.String() +} + +// generateCompleteEventStruct creates a complete Go struct for an event +func generateCompleteEventStruct(output *strings.Builder, event EventSpec) { + eventName := strings.Title(event.Name) + "Event" + + // Write documentation + fmt.Fprintf(output, "// %s represents the '%s' contract event\n", eventName, event.Name) + if event.Description != "" { + fmt.Fprintf(output, "// %s\n", event.Description) + } + fmt.Fprintf(output, "//\n") + fmt.Fprintf(output, "// Event Structure:\n") + fmt.Fprintf(output, "// - Prefix Topics: %v\n", event.PrefixTopics) + fmt.Fprintf(output, "// - Data Format: %s\n", event.DataFormat) + fmt.Fprintf(output, "// - Topic Parameters: %d\n", len(event.TopicParams)) + fmt.Fprintf(output, "// - Data Parameters: %d\n", len(event.DataParams)) + fmt.Fprintf(output, "type %s struct {\n", eventName) + + // Add metadata fields for validation + output.WriteString("\t// Event metadata (for validation)\n") + output.WriteString("\tEventName string `json:\"event_name\"`\n") + for i, prefix := range event.PrefixTopics { + fmt.Fprintf(output, "\tPrefix%d string `json:\"prefix_%d\"` // Expected: \"%s\"\n", i, i, prefix) + } + output.WriteString("\n") + + // Add topic parameters + if len(event.TopicParams) > 0 { + output.WriteString("\t// Topic parameters (indexed, searchable)\n") + for _, param := range event.TopicParams { + fmt.Fprintf(output, "\t%s %s `json:\"%s\"` // Topic: %s\n", + strings.Title(param.Name), param.Type, param.Name, param.Type) + } + output.WriteString("\n") + } + + // Add data parameters + if len(event.DataParams) > 0 { + output.WriteString("\t// Data parameters (event payload)\n") + for _, param := range event.DataParams { + fmt.Fprintf(output, "\t%s %s `json:\"%s\"` // Data: %s\n", + strings.Title(param.Name), param.Type, param.Name, param.Type) + } + } + + output.WriteString("}\n\n") +} + +// generateCompleteEventParser creates a fully functional event parser +func generateCompleteEventParser(output *strings.Builder, event EventSpec) { + eventName := strings.Title(event.Name) + "Event" + expectedTopicCount := len(event.PrefixTopics) + len(event.TopicParams) + converter := &XDRValueConverter{} + + // Function signature and documentation + fmt.Fprintf(output, "// Parse%s parses a '%s' event from Stellar ContractEvent XDR\n", eventName, event.Name) + fmt.Fprintf(output, "//\n") + fmt.Fprintf(output, "// This parser validates:\n") + fmt.Fprintf(output, "// - Topic count and structure\n") + fmt.Fprintf(output, "// - Prefix topic values\n") + fmt.Fprintf(output, "// - Data format (%s)\n", event.DataFormat) + fmt.Fprintf(output, "// - Parameter types and conversion\n") + fmt.Fprintf(output, "//\n") + fmt.Fprintf(output, "// Returns: (*%s, error)\n", eventName) + fmt.Fprintf(output, "func Parse%s(contractEvent xdr.ContractEvent) (*%s, error) {\n", eventName, eventName) + + // Extract topics and data from XDR + output.WriteString("\t// Extract event components from XDR\n") + output.WriteString("\ttopics := contractEvent.Body().V0.Topics\n") + output.WriteString("\tdata := contractEvent.Body().V0.Data\n\n") + + // Validate topic count + fmt.Fprintf(output, "\t// Validate topic structure\n") + fmt.Fprintf(output, "\tif len(topics) < %d {\n", expectedTopicCount) + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid '%s' event: expected at least %d topics, got %%d\", len(topics))\n", event.Name, expectedTopicCount) + output.WriteString("\t}\n\n") + + // Validate prefix topics + if len(event.PrefixTopics) > 0 { + output.WriteString("\t// Validate prefix topics (event signature)\n") + for i, prefix := range event.PrefixTopics { + fmt.Fprintf(output, "\tif topics[%d].Str() == nil || string(*topics[%d].Str()) != \"%s\" {\n", i, i, prefix) + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid event signature: expected '%s' at topic[%d]\")\n", prefix, i) + output.WriteString("\t}\n") + } + output.WriteString("\n") + } + + // Validate data format + fmt.Fprintf(output, "\t// Validate data format (expected: %s)\n", event.DataFormat) + switch event.DataFormat { + case "Map": + output.WriteString("\tif data.Map() == nil {\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"invalid event data: expected Map format\")\n") + output.WriteString("\t}\n") + output.WriteString("\tdataMap := data.Map()\n\n") + case "Vec": + output.WriteString("\tif data.Vec() == nil {\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"invalid event data: expected Vec format\")\n") + output.WriteString("\t}\n") + output.WriteString("\tdataVec := data.Vec()\n\n") + case "SingleValue": + output.WriteString("\t// Single value data format\n") + output.WriteString("\tdataValue := data\n\n") + } + + // Create event instance + fmt.Fprintf(output, "\t// Create event instance\n") + fmt.Fprintf(output, "\tevent := &%s{\n", eventName) + fmt.Fprintf(output, "\t\tEventName: \"%s\",\n", event.Name) + + // Set prefix values + for i, prefix := range event.PrefixTopics { + fmt.Fprintf(output, "\t\tPrefix%d: \"%s\",\n", i, prefix) + } + output.WriteString("\t}\n\n") + + // Extract topic parameters with proper XDR conversion + if len(event.TopicParams) > 0 { + output.WriteString("\t// Extract and convert topic parameters\n") + for i, param := range event.TopicParams { + topicIndex := len(event.PrefixTopics) + i + paramVarName := fmt.Sprintf("topic%d", topicIndex) + + fmt.Fprintf(output, "\t// Topic parameter: %s (%s)\n", param.Name, param.Type) + fmt.Fprintf(output, "\t%s := topics[%d]\n", paramVarName, topicIndex) + + // Generate type conversion code + conversionCode := converter.ConvertScValToGoCode(paramVarName, param.Type) + output.WriteString(conversionCode) + fmt.Fprintf(output, "\tevent.%s = %sValue\n\n", strings.Title(param.Name), paramVarName) + } + } + + // Extract data parameters with proper XDR conversion + if len(event.DataParams) > 0 { + output.WriteString("\t// Extract and convert data parameters\n") + + switch event.DataFormat { + case "Map": + // Map format: extract by key + output.WriteString("\t// Extract parameters from data map\n") + for _, param := range event.DataParams { + fmt.Fprintf(output, "\t// Data parameter: %s (%s)\n", param.Name, param.Type) + fmt.Fprintf(output, "\tif %sVal, exists := (*dataMap)[\"%s\"]; exists {\n", param.Name, param.Name) + + // Generate conversion code + conversionCode := converter.ConvertScValToGoCode(fmt.Sprintf("%sVal", param.Name), param.Type) + indentedCode := strings.ReplaceAll(conversionCode, "\n", "\n\t") + output.WriteString("\t" + indentedCode) + fmt.Fprintf(output, "\t\tevent.%s = %sValValue\n", strings.Title(param.Name), param.Name) + output.WriteString("\t} else {\n") + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"missing required data parameter: %s\")\n", param.Name) + output.WriteString("\t}\n\n") + } + + case "Vec": + // Vec format: extract by index + output.WriteString("\t// Extract parameters from data vector\n") + fmt.Fprintf(output, "\tif len(*dataVec) < %d {\n", len(event.DataParams)) + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"insufficient data parameters: expected %d, got %%d\", len(*dataVec))\n", len(event.DataParams)) + output.WriteString("\t}\n\n") + + for i, param := range event.DataParams { + fmt.Fprintf(output, "\t// Data parameter[%d]: %s (%s)\n", i, param.Name, param.Type) + paramVarName := fmt.Sprintf("dataParam%d", i) + fmt.Fprintf(output, "\t%s := (*dataVec)[%d]\n", paramVarName, i) + + // Generate conversion code + conversionCode := converter.ConvertScValToGoCode(paramVarName, param.Type) + output.WriteString(conversionCode) + fmt.Fprintf(output, "\tevent.%s = %sValue\n\n", strings.Title(param.Name), paramVarName) + } + + case "SingleValue": + // Single value format: only one data parameter allowed + if len(event.DataParams) > 1 { + output.WriteString("\t// ERROR: SingleValue format supports only one data parameter\n") + } else if len(event.DataParams) == 1 { + param := event.DataParams[0] + fmt.Fprintf(output, "\t// Single data parameter: %s (%s)\n", param.Name, param.Type) + + // Generate conversion code + conversionCode := converter.ConvertScValToGoCode("dataValue", param.Type) + output.WriteString(conversionCode) + fmt.Fprintf(output, "\tevent.%s = dataValueValue\n\n", strings.Title(param.Name)) + } + } + } + + output.WriteString("\treturn event, nil\n") + output.WriteString("}\n\n") +} + +// generateEventDispatcher creates a function to parse any contract event +func generateEventDispatcher(output *strings.Builder, events []EventSpec) { + output.WriteString("// ParseContractEvent attempts to parse any contract event\n") + output.WriteString("// Returns the parsed event as an interface{} or an error\n") + output.WriteString("func ParseContractEvent(contractEvent xdr.ContractEvent) (interface{}, error) {\n") + output.WriteString("\ttopics := contractEvent.Body().V0.Topics\n") + output.WriteString("\tif len(topics) == 0 {\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"event has no topics\")\n") + output.WriteString("\t}\n\n") + + output.WriteString("\t// Try to identify event by first topic (event name/prefix)\n") + output.WriteString("\tfirstTopic := topics[0]\n") + output.WriteString("\tvar eventName string\n") + output.WriteString("\tif firstTopic.Str() != nil {\n") + output.WriteString("\t\teventName = string(*firstTopic.Str())\n") + output.WriteString("\t} else {\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"cannot identify event: first topic is not a string\")\n") + output.WriteString("\t}\n\n") + + output.WriteString("\t// Dispatch to appropriate parser based on event signature\n") + output.WriteString("\tswitch eventName {\n") + + for _, event := range events { + if len(event.PrefixTopics) > 0 { + eventName := strings.Title(event.Name) + "Event" + fmt.Fprintf(output, "\tcase \"%s\":\n", event.PrefixTopics[0]) + fmt.Fprintf(output, "\t\treturn Parse%s(contractEvent)\n", eventName) + } + } + + output.WriteString("\tdefault:\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"unknown event type: %s\", eventName)\n") + output.WriteString("\t}\n") + output.WriteString("}\n\n") +} + +// generateClientInterface creates an interface for contract function calls +func generateClientInterface(output *strings.Builder, functions []FunctionSpec) { + output.WriteString("// ContractClient defines the interface for interacting with the contract\n") + output.WriteString("type ContractClient interface {\n") + + for _, fn := range functions { + // Build parameter list + var params []string + for _, param := range fn.Inputs { + params = append(params, fmt.Sprintf("%s %s", param.Name, param.Type)) + } + + // Determine return type + returnType := "error" + if len(fn.Outputs) > 0 { + if len(fn.Outputs) == 1 { + returnType = fmt.Sprintf("(%s, error)", fn.Outputs[0].Type) + } else { + var outputTypes []string + for _, output := range fn.Outputs { + outputTypes = append(outputTypes, output.Type) + } + returnType = fmt.Sprintf("(%s, error)", strings.Join(outputTypes, ", ")) + } + } + + // Add documentation if available + if fn.Description != "" { + fmt.Fprintf(output, "\t// %s\n", fn.Description) + } + fmt.Fprintf(output, "\t%s(%s) %s\n\n", fn.Name, strings.Join(params, ", "), returnType) + } + + output.WriteString("}\n\n") +} + +// generateTypeDefinition creates Go struct definitions for contract types +func generateTypeDefinition(output *strings.Builder, typ TypeSpec) { + if typ.Kind == "struct" && len(typ.Fields) > 0 { + fmt.Fprintf(output, "// %s represents the contract struct type\n", typ.Name) + fmt.Fprintf(output, "type %s struct {\n", typ.Name) + + for _, field := range typ.Fields { + fmt.Fprintf(output, "\t%s %s `json:\"%s\"`\n", + strings.Title(field.Name), field.Type, strings.ToLower(field.Name)) + } + + output.WriteString("}\n\n") + } else if typ.Kind == "enum" { + fmt.Fprintf(output, "// %s represents the contract enum type\n", typ.Name) + fmt.Fprintf(output, "type %s int\n\n", typ.Name) + + // Note: Enum values are not available in current XDR structure + fmt.Fprintf(output, "// TODO: Define %s enum values when available in XDR\n\n", typ.Name) + } +} + +// generateErrorDefinitions creates constants for contract errors +func generateErrorDefinitions(output *strings.Builder, errors []ErrorSpec) { + output.WriteString("// Contract error codes\n") + output.WriteString("const (\n") + + for _, err := range errors { + fmt.Fprintf(output, "\tError%s uint32 = %d\n", strings.Title(err.Name), err.Value) + } + + output.WriteString(")\n\n") + + // Generate error name lookup + output.WriteString("// GetErrorName returns the error name for a given error code\n") + output.WriteString("func GetErrorName(code uint32) string {\n") + output.WriteString("\tswitch code {\n") + + for _, err := range errors { + fmt.Fprintf(output, "\tcase %d:\n", err.Value) + fmt.Fprintf(output, "\t\treturn \"%s\"\n", err.Name) + } + + output.WriteString("\tdefault:\n") + output.WriteString("\t\treturn \"unknown error\"\n") + output.WriteString("\t}\n") + output.WriteString("}\n\n") +} + +/* +================================================================================ +SECTION 7: MAIN APPLICATION +================================================================================ +The main function orchestrates the entire process: +1. Validate input arguments +2. Fetch WASM from Soroban RPC +3. Extract and parse SEP-48 specifications +4. Analyze contract components +5. Generate Go bindings +6. Save output file +*/ + +func main() { + // Validate command line arguments + if len(os.Args) < 2 { + printUsage() + os.Exit(1) + } + + contractId := os.Args[1] + rpcEndpoint := "https://soroban-testnet.stellar.org" + if len(os.Args) > 2 { + rpcEndpoint = os.Args[2] + } + + // Display startup information + fmt.Printf("šŸš€ SEP-48 Contract Event Parser & Go Binding Generator\n") + fmt.Printf("=====================================================\n") + fmt.Printf("Contract ID: %s\n", contractId) + fmt.Printf("RPC Endpoint: %s\n", rpcEndpoint) + fmt.Printf("Target: Complete Go bindings with event parsing\n") + fmt.Println(strings.Repeat("=", 60)) + + // Step 1: Initialize RPC client and fetch WASM + fmt.Printf("šŸ“” Connecting to Soroban RPC...\n") + client := NewSorobanRPCClient(rpcEndpoint) + + wasmBytes, err := client.FetchContractWasm(contractId) + if err != nil { + fmt.Printf("āŒ Failed to fetch contract WASM: %v\n", err) + os.Exit(1) + } + + // Step 2: Extract SEP-48 specifications from WASM + fmt.Printf("šŸ” Extracting SEP-48 specifications...\n") + specEntries, err := extractContractSpec(wasmBytes) + if err != nil { + fmt.Printf("āŒ Failed to extract contract specifications: %v\n", err) + fmt.Printf(" This contract may not follow SEP-48 standard\n") + os.Exit(1) + } + + // Step 3: Perform detailed contract analysis + fmt.Printf("šŸ”¬ Analyzing contract components...\n") + analysis, err := analyzeContract(contractId, specEntries) + if err != nil { + fmt.Printf("āŒ Contract analysis failed: %v\n", err) + os.Exit(1) + } + + // Step 4: Display comprehensive analysis results + displayAnalysisResults(analysis) + + // Step 5: Generate complete Go bindings + fmt.Printf("šŸ—ļø Generating Go bindings with event parsers...\n") + goCode := generateGoCode(analysis) + + // Step 6: Save generated code to file + outputFile := fmt.Sprintf("contract_%s_bindings.go", strings.ToLower(contractId[:8])) + if err := os.WriteFile(outputFile, []byte(goCode), 0644); err != nil { + fmt.Printf("āŒ Failed to save bindings: %v\n", err) + os.Exit(1) + } + + // Success summary + fmt.Println(strings.Repeat("=", 60)) + fmt.Printf("āœ… SUCCESS: Complete Go bindings generated!\n") + fmt.Printf("šŸ“ Output file: %s\n", outputFile) + fmt.Printf("šŸ“Š Generated code includes:\n") + fmt.Printf(" • %d event parsers with full XDR conversion\n", len(analysis.Events)) + fmt.Printf(" • %d function interfaces\n", len(analysis.Functions)) + fmt.Printf(" • %d type definitions\n", len(analysis.Types)) + fmt.Printf(" • %d error constants\n", len(analysis.Errors)) + + if len(analysis.Events) > 0 { + fmt.Printf("\nšŸ’” Next steps for event monitoring:\n") + fmt.Printf(" 1. Import the generated bindings in your project\n") + fmt.Printf(" 2. Use Soroban RPC getEvents to fetch contract events\n") + fmt.Printf(" 3. Parse events using the generated Parse*Event functions\n") + fmt.Printf(" 4. Use ParseContractEvent() for automatic event type detection\n") + fmt.Printf("\nExample usage:\n") + fmt.Printf(" event, err := ParseContractEvent(contractEventXdr)\n") + fmt.Printf(" if err == nil {\n") + fmt.Printf(" // Handle parsed event\n") + fmt.Printf(" }\n") + } +} + +// printUsage displays help information +func printUsage() { + fmt.Println("SEP-48 Contract Event Parser & Go Binding Generator") + fmt.Println("==================================================") + fmt.Println("This tool analyzes Stellar smart contracts following SEP-48 specification") + fmt.Println("and generates complete Go bindings with event parsing capabilities.") + fmt.Println() + fmt.Println("Usage:") + fmt.Println(" go run main.go [rpc-endpoint]") + fmt.Println() + fmt.Println("Arguments:") + fmt.Println(" contract-id Stellar contract ID (starts with C...)") + fmt.Println(" rpc-endpoint Optional Soroban RPC URL (default: testnet)") + fmt.Println() + fmt.Println("Examples:") + fmt.Println(" # Use testnet (default)") + fmt.Println(" go run main.go CBMWOANWBHW5BYJ6GNACCMK2CQVTE6LUAP3XGIRIQK3NXWVOZLHXDSM3") + fmt.Println() + fmt.Println(" # Use custom RPC endpoint") + fmt.Println(" go run main.go CBMWOANWBHW5BYJ6GNACCMK2CQVTE6LUAP3XGIRIQK3NXWVOZLHXDSM3 https://mainnet.stellar.org") + fmt.Println() + fmt.Println("Output:") + fmt.Println(" - Generated Go bindings file: contract__bindings.go") + fmt.Println(" - Complete event parsers with XDR conversion") + fmt.Println(" - Function interfaces and type definitions") +} + +// displayAnalysisResults shows detailed analysis information +func displayAnalysisResults(analysis *ContractAnalysis) { + fmt.Println(strings.Repeat("=", 60)) + fmt.Printf("šŸ“Š CONTRACT ANALYSIS RESULTS\n") + fmt.Println(strings.Repeat("=", 60)) + + fmt.Printf("Contract ID: %s\n", analysis.ContractID) + fmt.Printf("Functions: %d\n", len(analysis.Functions)) + fmt.Printf("Types: %d\n", len(analysis.Types)) + fmt.Printf("Events: %d\n", len(analysis.Events)) + fmt.Printf("Errors: %d\n", len(analysis.Errors)) + + // Detailed event information + if len(analysis.Events) > 0 { + fmt.Printf("\nšŸ”” EVENT DETAILS:\n") + for i, event := range analysis.Events { + fmt.Printf(" %d. %s\n", i+1, event.Name) + if len(event.PrefixTopics) > 0 { + fmt.Printf(" Signature: %v\n", event.PrefixTopics) + } + fmt.Printf(" Data Format: %s\n", event.DataFormat) + + if len(event.TopicParams) > 0 { + fmt.Printf(" Topic Params: ") + for j, param := range event.TopicParams { + if j > 0 { + fmt.Printf(", ") + } + fmt.Printf("%s:%s", param.Name, param.Type) + } + fmt.Println() + } + + if len(event.DataParams) > 0 { + fmt.Printf(" Data Params: ") + for j, param := range event.DataParams { + if j > 0 { + fmt.Printf(", ") + } + fmt.Printf("%s:%s", param.Name, param.Type) + } + fmt.Println() + } + + if event.Description != "" { + fmt.Printf(" Description: %s\n", event.Description) + } + fmt.Println() + } + } + + // Function summary + if len(analysis.Functions) > 0 { + fmt.Printf("šŸ› ļø FUNCTIONS:\n") + for i, fn := range analysis.Functions { + fmt.Printf(" %d. %s(", i+1, fn.Name) + for j, param := range fn.Inputs { + if j > 0 { + fmt.Printf(", ") + } + fmt.Printf("%s:%s", param.Name, param.Type) + } + fmt.Printf(")") + if len(fn.Outputs) > 0 { + fmt.Printf(" -> %s", fn.Outputs[0].Type) + } + fmt.Println() + } + fmt.Println() + } + + // Type summary + if len(analysis.Types) > 0 { + fmt.Printf("šŸ“‹ TYPES:\n") + for i, typ := range analysis.Types { + fmt.Printf(" %d. %s (%s)\n", i+1, typ.Name, typ.Kind) + } + fmt.Println() + } + + // Error summary + if len(analysis.Errors) > 0 { + fmt.Printf("āš ļø ERRORS:\n") + for i, err := range analysis.Errors { + fmt.Printf(" %d. %s (code: %d)\n", i+1, err.Name, err.Value) + } + fmt.Println() + } +} From f2a3251039148d12113cc0a14d9c88b3344d4d38 Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Mon, 1 Sep 2025 20:01:01 -0700 Subject: [PATCH 2/3] update event parsing code - part 1 --- ingest/tutorial/eg-3/event_parser.go | 277 +++++++++++++-------------- 1 file changed, 128 insertions(+), 149 deletions(-) diff --git a/ingest/tutorial/eg-3/event_parser.go b/ingest/tutorial/eg-3/event_parser.go index 2a1acecd16..3ab9cb1822 100644 --- a/ingest/tutorial/eg-3/event_parser.go +++ b/ingest/tutorial/eg-3/event_parser.go @@ -685,88 +685,64 @@ type XDRValueConverter struct{} func (c *XDRValueConverter) ConvertScValToGoCode(varName, targetType string) string { switch targetType { case "string": - return fmt.Sprintf(` - var %sValue string - if %s.Str() != nil { - %sValue = string(*%s.Str()) - } else if %s.Sym() != nil { - %sValue = string(*%s.Sym()) - } else { - return nil, fmt.Errorf("expected string value for %s") - }`, varName, varName, varName, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetSym() + if !ok { + return nil, fmt.Errorf("invalid event format: expected string value for %s") + } + %sValueConverted := string(%sValue)`, varName, varName, varName, varName, varName) case "bool": - return fmt.Sprintf(` - var %sValue bool - if %s.B() != nil { - %sValue = bool(*%s.B()) - } else { - return nil, fmt.Errorf("expected bool value for %s") - }`, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetB() + if !ok { + return nil, fmt.Errorf("invalid event format: expected bool value for %s") + } + %sValueConverted := bool(%sValue)`, varName, varName, varName, varName, varName) case "int32": - return fmt.Sprintf(` - var %sValue int32 - if %s.I32() != nil { - %sValue = int32(*%s.I32()) - } else { - return nil, fmt.Errorf("expected int32 value for %s") - }`, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetI32() + if !ok { + return nil, fmt.Errorf("invalid event format: expected int32 value for %s") + } + %sValueConverted := int32(%sValue)`, varName, varName, varName, varName, varName) case "uint32": - return fmt.Sprintf(` - var %sValue uint32 - if %s.U32() != nil { - %sValue = uint32(*%s.U32()) - } else { - return nil, fmt.Errorf("expected uint32 value for %s") - }`, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetU32() + if !ok { + return nil, fmt.Errorf("invalid event format: expected uint32 value for %s") + } + %sValueConverted := uint32(%sValue)`, varName, varName, varName, varName, varName) case "int64": - return fmt.Sprintf(` - var %sValue int64 - if %s.I64() != nil { - %sValue = int64(*%s.I64()) - } else { - return nil, fmt.Errorf("expected int64 value for %s") - }`, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetI64() + if !ok { + return nil, fmt.Errorf("invalid event format: expected int64 value for %s") + } + %sValueConverted := int64(%sValue)`, varName, varName, varName, varName, varName) case "uint64": - return fmt.Sprintf(` - var %sValue uint64 - if %s.U64() != nil { - %sValue = uint64(*%s.U64()) - } else { - return nil, fmt.Errorf("expected uint64 value for %s") - }`, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetU64() + if !ok { + return nil, fmt.Errorf("invalid event format: expected uint64 value for %s") + } + %sValueConverted := uint64(%sValue)`, varName, varName, varName, varName, varName) case "*big.Int": - return fmt.Sprintf(` - var %sValue *big.Int - if %s.I128() != nil { - %sValue = new(big.Int) - %sValue.SetBytes((*%s.I128())[:]) - } else if %s.U128() != nil { - %sValue = new(big.Int) - %sValue.SetBytes((*%s.U128())[:]) - } else { - return nil, fmt.Errorf("expected 128-bit int value for %s") - }`, varName, varName, varName, varName, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetI128() + if !ok { + return nil, fmt.Errorf("invalid event format: expected i128 value for %s") + } + %sValueConverted := new(big.Int).SetBytes(%sValue[:])`, varName, varName, varName, varName, varName) case "[]byte": - return fmt.Sprintf(` - var %sValue []byte - if %s.Bytes() != nil { - %sValue = []byte(*%s.Bytes()) - } else { - return nil, fmt.Errorf("expected bytes value for %s") - }`, varName, varName, varName, varName, varName) + return fmt.Sprintf(` %sValue, ok := %s.GetBytes() + if !ok { + return nil, fmt.Errorf("invalid event format: expected bytes value for %s") + } + %sValueConverted := []byte(%sValue)`, varName, varName, varName, varName, varName) default: - return fmt.Sprintf(` - // TODO: Convert %s to %s - // This requires custom conversion logic for user-defined types - %sValue := %s // Placeholder`, varName, targetType, varName, varName) + return fmt.Sprintf(` // TODO: Convert %s to %s using appropriate GetXXX() method + %sValueConverted := %s // Placeholder`, varName, targetType, varName, varName) } } @@ -900,7 +876,6 @@ func generateCompleteEventStruct(output *strings.Builder, event EventSpec) { func generateCompleteEventParser(output *strings.Builder, event EventSpec) { eventName := strings.Title(event.Name) + "Event" expectedTopicCount := len(event.PrefixTopics) + len(event.TopicParams) - converter := &XDRValueConverter{} // Function signature and documentation fmt.Fprintf(output, "// Parse%s parses a '%s' event from Stellar ContractEvent XDR\n", eventName, event.Name) @@ -914,10 +889,10 @@ func generateCompleteEventParser(output *strings.Builder, event EventSpec) { fmt.Fprintf(output, "// Returns: (*%s, error)\n", eventName) fmt.Fprintf(output, "func Parse%s(contractEvent xdr.ContractEvent) (*%s, error) {\n", eventName, eventName) - // Extract topics and data from XDR + // Extract topics and data from XDR (FIXED: Body is a field, not method) output.WriteString("\t// Extract event components from XDR\n") - output.WriteString("\ttopics := contractEvent.Body().V0.Topics\n") - output.WriteString("\tdata := contractEvent.Body().V0.Data\n\n") + output.WriteString("\ttopics := contractEvent.Body.V0.Topics\n") + output.WriteString("\tdata := contractEvent.Body.V0.Data\n\n") // Validate topic count fmt.Fprintf(output, "\t// Validate topic structure\n") @@ -925,30 +900,33 @@ func generateCompleteEventParser(output *strings.Builder, event EventSpec) { fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid '%s' event: expected at least %d topics, got %%d\", len(topics))\n", event.Name, expectedTopicCount) output.WriteString("\t}\n\n") - // Validate prefix topics + // Validate prefix topics using GetSym() if len(event.PrefixTopics) > 0 { output.WriteString("\t// Validate prefix topics (event signature)\n") for i, prefix := range event.PrefixTopics { - fmt.Fprintf(output, "\tif topics[%d].Str() == nil || string(*topics[%d].Str()) != \"%s\" {\n", i, i, prefix) - fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid event signature: expected '%s' at topic[%d]\")\n", prefix, i) + fmt.Fprintf(output, "\ttopic%d, ok%d := topics[%d].GetSym()\n", i, i, i) + fmt.Fprintf(output, "\tif !ok%d {\n", i) + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid event format: topic%d does not exist\")\n", i) output.WriteString("\t}\n") + fmt.Fprintf(output, "\tif string(topic%d) != \"%s\" {\n", i, prefix) + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid event signature: expected '%s' at topic[%d]\")\n", prefix, i) + output.WriteString("\t}\n\n") } - output.WriteString("\n") } - // Validate data format + // Validate data format using GetMap()/GetVec() fmt.Fprintf(output, "\t// Validate data format (expected: %s)\n", event.DataFormat) switch event.DataFormat { case "Map": - output.WriteString("\tif data.Map() == nil {\n") - output.WriteString("\t\treturn nil, fmt.Errorf(\"invalid event data: expected Map format\")\n") - output.WriteString("\t}\n") - output.WriteString("\tdataMap := data.Map()\n\n") + output.WriteString("\tdataMap, ok := data.GetMap()\n") + output.WriteString("\tif !ok {\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"invalid event format: data does not exist\")\n") + output.WriteString("\t}\n\n") case "Vec": - output.WriteString("\tif data.Vec() == nil {\n") - output.WriteString("\t\treturn nil, fmt.Errorf(\"invalid event data: expected Vec format\")\n") - output.WriteString("\t}\n") - output.WriteString("\tdataVec := data.Vec()\n\n") + output.WriteString("\tdataVec, ok := data.GetVec()\n") + output.WriteString("\tif !ok {\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"invalid event format: expected Vec data format\")\n") + output.WriteString("\t}\n\n") case "SingleValue": output.WriteString("\t// Single value data format\n") output.WriteString("\tdataValue := data\n\n") @@ -965,76 +943,75 @@ func generateCompleteEventParser(output *strings.Builder, event EventSpec) { } output.WriteString("\t}\n\n") - // Extract topic parameters with proper XDR conversion + // Extract topic parameters with proper GetXXX() calls if len(event.TopicParams) > 0 { output.WriteString("\t// Extract and convert topic parameters\n") for i, param := range event.TopicParams { topicIndex := len(event.PrefixTopics) + i - paramVarName := fmt.Sprintf("topic%d", topicIndex) fmt.Fprintf(output, "\t// Topic parameter: %s (%s)\n", param.Name, param.Type) - fmt.Fprintf(output, "\t%s := topics[%d]\n", paramVarName, topicIndex) - // Generate type conversion code - conversionCode := converter.ConvertScValToGoCode(paramVarName, param.Type) - output.WriteString(conversionCode) - fmt.Fprintf(output, "\tevent.%s = %sValue\n\n", strings.Title(param.Name), paramVarName) + // Generate proper GetXXX() calls based on type + switch param.Type { + case "string": + fmt.Fprintf(output, "\ttopic%dValue, ok := topics[%d].GetSym()\n", topicIndex, topicIndex) + fmt.Fprintf(output, "\tif !ok {\n") + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid event format: topic%d\")\n", topicIndex) + output.WriteString("\t}\n") + fmt.Fprintf(output, "\tevent.%s = string(topic%dValue)\n\n", strings.Title(param.Name), topicIndex) + + case "uint32": + fmt.Fprintf(output, "\ttopic%dValue, ok := topics[%d].GetU32()\n", topicIndex, topicIndex) + fmt.Fprintf(output, "\tif !ok {\n") + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid event format: expected uint32 value for topic%d\")\n", topicIndex) + output.WriteString("\t}\n") + fmt.Fprintf(output, "\tevent.%s = uint32(topic%dValue)\n\n", strings.Title(param.Name), topicIndex) + + case "int32": + fmt.Fprintf(output, "\ttopic%dValue, ok := topics[%d].GetI32()\n", topicIndex, topicIndex) + fmt.Fprintf(output, "\tif !ok {\n") + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"invalid event format: expected int32 value for topic%d\")\n", topicIndex) + output.WriteString("\t}\n") + fmt.Fprintf(output, "\tevent.%s = int32(topic%dValue)\n\n", strings.Title(param.Name), topicIndex) + + // Add more cases as needed for other types + default: + fmt.Fprintf(output, "\t// TODO: Handle %s type conversion for topic%d\n", param.Type, topicIndex) + fmt.Fprintf(output, "\t// topic%dValue, ok := topics[%d].GetXXX()\n", topicIndex, topicIndex) + fmt.Fprintf(output, "\tevent.%s = topic%dValue // Placeholder\n\n", strings.Title(param.Name), topicIndex) + } } } - // Extract data parameters with proper XDR conversion - if len(event.DataParams) > 0 { - output.WriteString("\t// Extract and convert data parameters\n") - - switch event.DataFormat { - case "Map": - // Map format: extract by key - output.WriteString("\t// Extract parameters from data map\n") - for _, param := range event.DataParams { - fmt.Fprintf(output, "\t// Data parameter: %s (%s)\n", param.Name, param.Type) - fmt.Fprintf(output, "\tif %sVal, exists := (*dataMap)[\"%s\"]; exists {\n", param.Name, param.Name) - - // Generate conversion code - conversionCode := converter.ConvertScValToGoCode(fmt.Sprintf("%sVal", param.Name), param.Type) - indentedCode := strings.ReplaceAll(conversionCode, "\n", "\n\t") - output.WriteString("\t" + indentedCode) - fmt.Fprintf(output, "\t\tevent.%s = %sValValue\n", strings.Title(param.Name), param.Name) - output.WriteString("\t} else {\n") - fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"missing required data parameter: %s\")\n", param.Name) - output.WriteString("\t}\n\n") + // Extract data parameters with proper GetXXX() calls + if len(event.DataParams) > 0 && event.DataFormat == "Map" { + output.WriteString("\t// Extract and convert data parameters from map\n") + for _, param := range event.DataParams { + fmt.Fprintf(output, "\t// Data parameter: %s (%s)\n", param.Name, param.Type) + fmt.Fprintf(output, "\tif %sVal, exists := dataMap[\"%s\"]; exists {\n", param.Name, param.Name) + + // Generate proper GetXXX() calls based on type + switch param.Type { + case "*big.Int": + fmt.Fprintf(output, "\t\t%sValue, ok := %sVal.GetI128()\n", param.Name, param.Name) + fmt.Fprintf(output, "\t\tif !ok {\n") + fmt.Fprintf(output, "\t\t\treturn nil, fmt.Errorf(\"invalid event format: expected i128 value for %s\")\n", param.Name) + output.WriteString("\t\t}\n") + fmt.Fprintf(output, "\t\tevent.%s = new(big.Int).SetBytes(%sValue[:])\n", strings.Title(param.Name), param.Name) + + case "interface{}": + fmt.Fprintf(output, "\t\t// Keep %s as raw ScVal for interface{} type\n", param.Name) + fmt.Fprintf(output, "\t\tevent.%s = %sVal\n", strings.Title(param.Name), param.Name) + + default: + fmt.Fprintf(output, "\t\t// TODO: Convert %sVal to %s\n", param.Name, param.Type) + fmt.Fprintf(output, "\t\t// This requires custom conversion logic for complex types\n") + fmt.Fprintf(output, "\t\tevent.%s = %sVal // Placeholder\n", strings.Title(param.Name), param.Name) } - case "Vec": - // Vec format: extract by index - output.WriteString("\t// Extract parameters from data vector\n") - fmt.Fprintf(output, "\tif len(*dataVec) < %d {\n", len(event.DataParams)) - fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"insufficient data parameters: expected %d, got %%d\", len(*dataVec))\n", len(event.DataParams)) + output.WriteString("\t} else {\n") + fmt.Fprintf(output, "\t\treturn nil, fmt.Errorf(\"missing required data parameter: %s\")\n", param.Name) output.WriteString("\t}\n\n") - - for i, param := range event.DataParams { - fmt.Fprintf(output, "\t// Data parameter[%d]: %s (%s)\n", i, param.Name, param.Type) - paramVarName := fmt.Sprintf("dataParam%d", i) - fmt.Fprintf(output, "\t%s := (*dataVec)[%d]\n", paramVarName, i) - - // Generate conversion code - conversionCode := converter.ConvertScValToGoCode(paramVarName, param.Type) - output.WriteString(conversionCode) - fmt.Fprintf(output, "\tevent.%s = %sValue\n\n", strings.Title(param.Name), paramVarName) - } - - case "SingleValue": - // Single value format: only one data parameter allowed - if len(event.DataParams) > 1 { - output.WriteString("\t// ERROR: SingleValue format supports only one data parameter\n") - } else if len(event.DataParams) == 1 { - param := event.DataParams[0] - fmt.Fprintf(output, "\t// Single data parameter: %s (%s)\n", param.Name, param.Type) - - // Generate conversion code - conversionCode := converter.ConvertScValToGoCode("dataValue", param.Type) - output.WriteString(conversionCode) - fmt.Fprintf(output, "\tevent.%s = dataValueValue\n\n", strings.Title(param.Name)) - } } } @@ -1047,19 +1024,21 @@ func generateEventDispatcher(output *strings.Builder, events []EventSpec) { output.WriteString("// ParseContractEvent attempts to parse any contract event\n") output.WriteString("// Returns the parsed event as an interface{} or an error\n") output.WriteString("func ParseContractEvent(contractEvent xdr.ContractEvent) (interface{}, error) {\n") - output.WriteString("\ttopics := contractEvent.Body().V0.Topics\n") + + // Extract topics using corrected field access + output.WriteString("\t// Extract event components from XDR\n") + output.WriteString("\ttopics := contractEvent.Body.V0.Topics\n") output.WriteString("\tif len(topics) == 0 {\n") output.WriteString("\t\treturn nil, fmt.Errorf(\"event has no topics\")\n") output.WriteString("\t}\n\n") + // Get first topic using GetSym() method output.WriteString("\t// Try to identify event by first topic (event name/prefix)\n") - output.WriteString("\tfirstTopic := topics[0]\n") - output.WriteString("\tvar eventName string\n") - output.WriteString("\tif firstTopic.Str() != nil {\n") - output.WriteString("\t\teventName = string(*firstTopic.Str())\n") - output.WriteString("\t} else {\n") - output.WriteString("\t\treturn nil, fmt.Errorf(\"cannot identify event: first topic is not a string\")\n") - output.WriteString("\t}\n\n") + output.WriteString("\tfirstTopic, ok := topics[0].GetSym()\n") + output.WriteString("\tif !ok {\n") + output.WriteString("\t\treturn nil, fmt.Errorf(\"invalid event format: first topic is not a symbol\")\n") + output.WriteString("\t}\n") + output.WriteString("\teventName := string(firstTopic)\n\n") output.WriteString("\t// Dispatch to appropriate parser based on event signature\n") output.WriteString("\tswitch eventName {\n") @@ -1231,7 +1210,7 @@ func main() { goCode := generateGoCode(analysis) // Step 6: Save generated code to file - outputFile := fmt.Sprintf("contract_%s_bindings.go", strings.ToLower(contractId[:8])) + outputFile := fmt.Sprintf("contract_%s_bindings.go", contractId) if err := os.WriteFile(outputFile, []byte(goCode), 0644); err != nil { fmt.Printf("āŒ Failed to save bindings: %v\n", err) os.Exit(1) From 1829b9125517506ad2a9b93621e6b55801c5196f Mon Sep 17 00:00:00 2001 From: Karthik Iyer Date: Mon, 1 Sep 2025 20:01:22 -0700 Subject: [PATCH 3/3] Commit generated code --- ...BG62RKDZU2DS6BDGALAA6TRRJVOAP2_bindings.go | 169 ++++++++++++++++++ .../eg-3/contract_cchepgth_bindings.go | 46 ++--- 2 files changed, 193 insertions(+), 22 deletions(-) create mode 100644 ingest/tutorial/eg-3/contract_CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2_bindings.go diff --git a/ingest/tutorial/eg-3/contract_CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2_bindings.go b/ingest/tutorial/eg-3/contract_CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2_bindings.go new file mode 100644 index 0000000000..6c36cfefdd --- /dev/null +++ b/ingest/tutorial/eg-3/contract_CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2_bindings.go @@ -0,0 +1,169 @@ +// SEP-48 Contract Bindings +// Auto-generated from Soroban contract specification +// Contract ID: CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2 +// +// This file contains: +// - Event structures and parsers +// - Function interfaces +// - Type definitions +// - Error constants + +package contracts + +import ( + "fmt" + "math/big" + "github.com/stellar/go/xdr" +) + +// Contract metadata +const ContractID = "CCHEPGTHUPDTGPA7B6YLSG5X6PBG62RKDZU2DS6BDGALAA6TRRJVOAP2" + +// ============================================================================ +// CONTRACT EVENTS (Complete SEP-48 Implementation) +// ============================================================================ + +// DefaultEventEvent represents the 'DefaultEvent' contract event +// +// Event Structure: +// - Prefix Topics: [default_event] +// - Data Format: Map +// - Topic Parameters: 2 +// - Data Parameters: 3 +type DefaultEventEvent struct { + // Event metadata (for validation) + EventName string `json:"event_name"` + Prefix0 string `json:"prefix_0"` // Expected: "default_event" + + // Topic parameters (indexed, searchable) + Addr string `json:"addr"` // Topic: string + Num uint32 `json:"num"` // Topic: uint32 + + // Data parameters (event payload) + Bignum *big.Int `json:"bignum"` // Data: *big.Int + Nested []map[string]int64 `json:"nested"` // Data: []map[string]int64 + Any interface{} `json:"any"` // Data: interface{} +} + +// ParseDefaultEventEvent parses a 'DefaultEvent' event from Stellar ContractEvent XDR +// +// This parser validates: +// - Topic count and structure +// - Prefix topic values +// - Data format (Map) +// - Parameter types and conversion +// +// Returns: (*DefaultEventEvent, error) +func ParseDefaultEventEvent(contractEvent xdr.ContractEvent) (*DefaultEventEvent, error) { + // Extract event components from XDR + topics := contractEvent.Body.V0.Topics + data := contractEvent.Body.V0.Data + + // Validate topic structure + if len(topics) < 3 { + return nil, fmt.Errorf("invalid 'DefaultEvent' event: expected at least 3 topics, got %d", len(topics)) + } + + // Validate prefix topics (event signature) + topic0, ok0 := topics[0].GetSym() + if !ok0 { + return nil, fmt.Errorf("invalid event format: topic0 does not exist") + } + if string(topic0) != "default_event" { + return nil, fmt.Errorf("invalid event signature: expected 'default_event' at topic[0]") + } + + // Validate data format (expected: Map) + dataMap, ok := data.GetMap() + if !ok { + return nil, fmt.Errorf("invalid event format: data does not exist") + } + + // Create event instance + event := &DefaultEventEvent{ + EventName: "DefaultEvent", + Prefix0: "default_event", + } + + // Extract and convert topic parameters + // Topic parameter: addr (string) + topic1Value, ok := topics[1].GetSym() + if !ok { + return nil, fmt.Errorf("invalid event format: topic1") + } + event.Addr = string(topic1Value) + + // Topic parameter: num (uint32) + topic2Value, ok := topics[2].GetU32() + if !ok { + return nil, fmt.Errorf("invalid event format: expected uint32 value for topic2") + } + event.Num = uint32(topic2Value) + + // Extract and convert data parameters from map + // Data parameter: bignum (*big.Int) + if bignumVal, exists := dataMap["bignum"]; exists { + bignumValue, ok := bignumVal.GetI128() + if !ok { + return nil, fmt.Errorf("invalid event format: expected i128 value for bignum") + } + event.Bignum = new(big.Int).SetBytes(bignumValue[:]) + } else { + return nil, fmt.Errorf("missing required data parameter: bignum") + } + + // Data parameter: nested ([]map[string]int64) + if nestedVal, exists := dataMap["nested"]; exists { + // TODO: Convert nestedVal to []map[string]int64 + // This requires custom conversion logic for complex types + event.Nested = nestedVal // Placeholder + } else { + return nil, fmt.Errorf("missing required data parameter: nested") + } + + // Data parameter: any (interface{}) + if anyVal, exists := dataMap["any"]; exists { + // Keep any as raw ScVal for interface{} type + event.Any = anyVal + } else { + return nil, fmt.Errorf("missing required data parameter: any") + } + + return event, nil +} + +// ParseContractEvent attempts to parse any contract event +// Returns the parsed event as an interface{} or an error +func ParseContractEvent(contractEvent xdr.ContractEvent) (interface{}, error) { + // Extract event components from XDR + topics := contractEvent.Body.V0.Topics + if len(topics) == 0 { + return nil, fmt.Errorf("event has no topics") + } + + // Try to identify event by first topic (event name/prefix) + firstTopic, ok := topics[0].GetSym() + if !ok { + return nil, fmt.Errorf("invalid event format: first topic is not a symbol") + } + eventName := string(firstTopic) + + // Dispatch to appropriate parser based on event signature + switch eventName { + case "default_event": + return ParseDefaultEventEvent(contractEvent) + default: + return nil, fmt.Errorf("unknown event type: %s", eventName) + } +} + +// ============================================================================ +// CONTRACT FUNCTIONS +// ============================================================================ + +// ContractClient defines the interface for interacting with the contract +type ContractClient interface { + emit() error + +} + diff --git a/ingest/tutorial/eg-3/contract_cchepgth_bindings.go b/ingest/tutorial/eg-3/contract_cchepgth_bindings.go index af9054b6b3..7e0ae87b1d 100644 --- a/ingest/tutorial/eg-3/contract_cchepgth_bindings.go +++ b/ingest/tutorial/eg-3/contract_cchepgth_bindings.go @@ -8,7 +8,7 @@ // - Type definitions // - Error constants -package contracts +package main import ( "fmt" @@ -56,24 +56,29 @@ type DefaultEventEvent struct { // Returns: (*DefaultEventEvent, error) func ParseDefaultEventEvent(contractEvent xdr.ContractEvent) (*DefaultEventEvent, error) { // Extract event components from XDR - topics := contractEvent.Body.V0.Topics - data := contractEvent.Body.V0.Data + var topics []xdr.ScVal + var data xdr.ScVal + topics = contractEvent.Body.V0.Topics + data = contractEvent.Body.V0.Data // Validate topic structure if len(topics) < 3 { return nil, fmt.Errorf("invalid 'DefaultEvent' event: expected at least 3 topics, got %d", len(topics)) } + topic0, ok0 := topics[0].GetSym() + if !ok0 { + return nil, fmt.Errorf("invalid event format: topic0 does not exist") + } // Validate prefix topics (event signature) - if topics[0].Str() == nil || string(*topics[0].Str()) != "default_event" { + if topic0 != "default_event" { return nil, fmt.Errorf("invalid event signature: expected 'default_event' at topic[0]") } - // Validate data format (expected: Map) - if data.Map() == nil { - return nil, fmt.Errorf("invalid event data: expected Map format") + dataMap, ok := data.GetMap() + if !ok { + return nil, fmt.Errorf("invalid event format: data does not exist") } - dataMap := data.Map() // Create event instance event := &DefaultEventEvent{ @@ -85,24 +90,21 @@ func ParseDefaultEventEvent(contractEvent xdr.ContractEvent) (*DefaultEventEvent // Topic parameter: addr (string) topic1 := topics[1] - var topic1Value string - if topic1.Str() != nil { - topic1Value = string(*topic1.Str()) - } else if topic1.Sym() != nil { - topic1Value = string(*topic1.Sym()) - } else { - return nil, fmt.Errorf("expected string value for topic1") - } event.Addr = topic1Value + topic1Value, ok := topic1.GetSym() + if !ok { + return nil, fmt.Errorf("invalid event format: topic1") + } + event.Addr = string(topic1Value) // Topic parameter: num (uint32) topic2 := topics[2] - var topic2Value uint32 - if topic2.U32() != nil { - topic2Value = uint32(*topic2.U32()) - } else { - return nil, fmt.Errorf("expected uint32 value for topic2") - } event.Num = topic2Value + topic2Value, ok := topic2.GetU32() + if !ok { + return nil, fmt.Errorf("invalid event format: expected uint32 value for topic2") + } + event.Num = uint32(topic2Value) + // Extract and convert data parameters // Extract parameters from data map