package rest import ( "context" "fmt" "regexp" "strings" "time" ) var ( addressPattern = regexp.MustCompile(`0x[a-fA-F0-9]{40}`) transactionPattern = regexp.MustCompile(`0x[a-fA-F0-9]{64}`) blockRefPattern = regexp.MustCompile(`(?i)\bblock\s+#?(\d+)\b`) ) func (s *Server) buildAIContext(ctx context.Context, query string, pageContext map[string]string) (AIContextEnvelope, []string) { warnings := []string{} envelope := AIContextEnvelope{ ChainID: s.chainID, Explorer: "SolaceScan", PageContext: compactStringMap(pageContext), CapabilityNotice: "This assistant is wired for read-only explorer analysis. It can summarize indexed chain data, liquidity routes, and curated workspace docs, but it does not sign transactions or execute private operations.", } sources := []AIContextSource{ {Type: "system", Label: "Explorer REST backend"}, } if stats, err := s.queryAIStats(ctx); err == nil { envelope.Stats = stats sources = append(sources, AIContextSource{Type: "database", Label: "Explorer indexer database"}) } else if err != nil { warnings = append(warnings, "indexed explorer stats unavailable: "+err.Error()) } if strings.TrimSpace(query) != "" { if txHash := firstRegexMatch(transactionPattern, query); txHash != "" && s.db != nil { if tx, err := s.queryAITransaction(ctx, txHash); err == nil && len(tx) > 0 { envelope.Transaction = tx } else if err != nil { warnings = append(warnings, "transaction context unavailable: "+err.Error()) } } if addr := firstRegexMatch(addressPattern, query); addr != "" && s.db != nil { if addressInfo, err := s.queryAIAddress(ctx, addr); err == nil && len(addressInfo) > 0 { envelope.Address = addressInfo } else if err != nil { warnings = append(warnings, "address context unavailable: "+err.Error()) } } if blockNumber := extractBlockReference(query); blockNumber > 0 && s.db != nil { if block, err := s.queryAIBlock(ctx, blockNumber); err == nil && len(block) > 0 { envelope.Block = block } else if err != nil { warnings = append(warnings, "block context unavailable: "+err.Error()) } } } if routeMatches, routeWarning := s.queryAIRoutes(ctx, query); len(routeMatches) > 0 { envelope.RouteMatches = routeMatches sources = append(sources, AIContextSource{Type: "routes", Label: "Token aggregation live routes", Origin: firstNonEmptyEnv("TOKEN_AGGREGATION_API_BASE", "TOKEN_AGGREGATION_URL", "TOKEN_AGGREGATION_BASE_URL")}) } else if routeWarning != "" { warnings = append(warnings, routeWarning) } if docs, root, docWarning := loadAIDocSnippets(query); len(docs) > 0 { envelope.DocSnippets = docs sources = append(sources, AIContextSource{Type: "docs", Label: "Workspace docs", Origin: root}) } else if docWarning != "" { warnings = append(warnings, docWarning) } envelope.Sources = sources return envelope, uniqueStrings(warnings) } func (s *Server) queryAIStats(ctx context.Context) (map[string]any, error) { if s.db == nil { return nil, fmt.Errorf("database unavailable") } ctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() stats := map[string]any{} var totalBlocks int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM blocks WHERE chain_id = $1`, s.chainID).Scan(&totalBlocks); err == nil { stats["total_blocks"] = totalBlocks } var totalTransactions int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM transactions WHERE chain_id = $1`, s.chainID).Scan(&totalTransactions); err == nil { stats["total_transactions"] = totalTransactions } var totalAddresses int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM ( SELECT from_address AS address FROM transactions WHERE chain_id = $1 AND from_address IS NOT NULL AND from_address <> '' UNION SELECT to_address AS address FROM transactions WHERE chain_id = $1 AND to_address IS NOT NULL AND to_address <> '' ) unique_addresses`, s.chainID).Scan(&totalAddresses); err == nil { stats["total_addresses"] = totalAddresses } var latestBlock int64 if err := s.db.QueryRow(ctx, `SELECT COALESCE(MAX(number), 0) FROM blocks WHERE chain_id = $1`, s.chainID).Scan(&latestBlock); err == nil { stats["latest_block"] = latestBlock } if len(stats) == 0 { var totalBlocks int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM blocks`).Scan(&totalBlocks); err == nil { stats["total_blocks"] = totalBlocks } var totalTransactions int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM transactions`).Scan(&totalTransactions); err == nil { stats["total_transactions"] = totalTransactions } var totalAddresses int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM addresses`).Scan(&totalAddresses); err == nil { stats["total_addresses"] = totalAddresses } var latestBlock int64 if err := s.db.QueryRow(ctx, `SELECT COALESCE(MAX(number), 0) FROM blocks`).Scan(&latestBlock); err == nil { stats["latest_block"] = latestBlock } } if len(stats) == 0 { return nil, fmt.Errorf("no indexed stats available") } return stats, nil } func (s *Server) queryAITransaction(ctx context.Context, hash string) (map[string]any, error) { ctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() query := ` SELECT hash, block_number, from_address, to_address, value, gas_used, gas_price, status, timestamp_iso FROM transactions WHERE chain_id = $1 AND hash = $2 LIMIT 1 ` var txHash, fromAddress, value string var blockNumber int64 var toAddress *string var gasUsed, gasPrice *int64 var status *int64 var timestampISO *string err := s.db.QueryRow(ctx, query, s.chainID, hash).Scan( &txHash, &blockNumber, &fromAddress, &toAddress, &value, &gasUsed, &gasPrice, &status, ×tampISO, ) if err != nil { normalizedHash := normalizeHexString(hash) blockscoutQuery := ` SELECT concat('0x', encode(hash, 'hex')) AS hash, block_number, concat('0x', encode(from_address_hash, 'hex')) AS from_address, CASE WHEN to_address_hash IS NULL THEN NULL ELSE concat('0x', encode(to_address_hash, 'hex')) END AS to_address, COALESCE(value::text, '0') AS value, gas_used, gas_price, status, TO_CHAR(block_timestamp AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS timestamp_iso FROM transactions WHERE hash = decode($1, 'hex') LIMIT 1 ` if fallbackErr := s.db.QueryRow(ctx, blockscoutQuery, normalizedHash).Scan( &txHash, &blockNumber, &fromAddress, &toAddress, &value, &gasUsed, &gasPrice, &status, ×tampISO, ); fallbackErr != nil { return nil, err } } tx := map[string]any{ "hash": txHash, "block_number": blockNumber, "from_address": fromAddress, "value": value, } if toAddress != nil { tx["to_address"] = *toAddress } if gasUsed != nil { tx["gas_used"] = *gasUsed } if gasPrice != nil { tx["gas_price"] = *gasPrice } if status != nil { tx["status"] = *status } if timestampISO != nil { tx["timestamp_iso"] = *timestampISO } return tx, nil } func (s *Server) queryAIAddress(ctx context.Context, address string) (map[string]any, error) { ctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() address = normalizeAddress(address) result := map[string]any{ "address": address, } var txCount int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(*) FROM transactions WHERE chain_id = $1 AND (LOWER(from_address) = $2 OR LOWER(to_address) = $2)`, s.chainID, address).Scan(&txCount); err == nil { result["transaction_count"] = txCount } var tokenCount int64 if err := s.db.QueryRow(ctx, `SELECT COUNT(DISTINCT token_contract) FROM token_transfers WHERE chain_id = $1 AND (LOWER(from_address) = $2 OR LOWER(to_address) = $2)`, s.chainID, address).Scan(&tokenCount); err == nil { result["token_count"] = tokenCount } var recentHashes []string rows, err := s.db.Query(ctx, ` SELECT hash FROM transactions WHERE chain_id = $1 AND (LOWER(from_address) = $2 OR LOWER(to_address) = $2) ORDER BY block_number DESC, transaction_index DESC LIMIT 5 `, s.chainID, address) if err == nil { defer rows.Close() for rows.Next() { var hash string if scanErr := rows.Scan(&hash); scanErr == nil { recentHashes = append(recentHashes, hash) } } } if len(recentHashes) > 0 { result["recent_transactions"] = recentHashes } if len(result) == 1 { normalizedAddress := normalizeHexString(address) var blockscoutTxCount int64 var blockscoutTokenCount int64 blockscoutAddressQuery := ` SELECT COALESCE(transactions_count, 0), COALESCE(token_transfers_count, 0) FROM addresses WHERE hash = decode($1, 'hex') LIMIT 1 ` if err := s.db.QueryRow(ctx, blockscoutAddressQuery, normalizedAddress).Scan(&blockscoutTxCount, &blockscoutTokenCount); err == nil { result["transaction_count"] = blockscoutTxCount result["token_count"] = blockscoutTokenCount } var liveTxCount int64 if err := s.db.QueryRow(ctx, ` SELECT COUNT(*) FROM transactions WHERE from_address_hash = decode($1, 'hex') OR to_address_hash = decode($1, 'hex') `, normalizedAddress).Scan(&liveTxCount); err == nil && liveTxCount > 0 { result["transaction_count"] = liveTxCount } var liveTokenCount int64 if err := s.db.QueryRow(ctx, ` SELECT COUNT(DISTINCT token_contract_address_hash) FROM token_transfers WHERE from_address_hash = decode($1, 'hex') OR to_address_hash = decode($1, 'hex') `, normalizedAddress).Scan(&liveTokenCount); err == nil && liveTokenCount > 0 { result["token_count"] = liveTokenCount } rows, err := s.db.Query(ctx, ` SELECT concat('0x', encode(hash, 'hex')) FROM transactions WHERE from_address_hash = decode($1, 'hex') OR to_address_hash = decode($1, 'hex') ORDER BY block_number DESC, index DESC LIMIT 5 `, normalizedAddress) if err == nil { defer rows.Close() for rows.Next() { var hash string if scanErr := rows.Scan(&hash); scanErr == nil { recentHashes = append(recentHashes, hash) } } } if len(recentHashes) > 0 { result["recent_transactions"] = recentHashes } } if len(result) == 1 { return nil, fmt.Errorf("address not found") } return result, nil } func (s *Server) queryAIBlock(ctx context.Context, blockNumber int64) (map[string]any, error) { ctx, cancel := context.WithTimeout(ctx, 4*time.Second) defer cancel() query := ` SELECT number, hash, parent_hash, transaction_count, gas_used, gas_limit, timestamp_iso FROM blocks WHERE chain_id = $1 AND number = $2 LIMIT 1 ` var number int64 var hash, parentHash string var transactionCount int64 var gasUsed, gasLimit int64 var timestampISO *string err := s.db.QueryRow(ctx, query, s.chainID, blockNumber).Scan(&number, &hash, &parentHash, &transactionCount, &gasUsed, &gasLimit, ×tampISO) if err != nil { blockscoutQuery := ` SELECT number, concat('0x', encode(hash, 'hex')) AS hash, concat('0x', encode(parent_hash, 'hex')) AS parent_hash, (SELECT COUNT(*) FROM transactions WHERE block_number = b.number) AS transaction_count, gas_used, gas_limit, TO_CHAR(timestamp AT TIME ZONE 'UTC', 'YYYY-MM-DD"T"HH24:MI:SS"Z"') AS timestamp_iso FROM blocks b WHERE number = $1 LIMIT 1 ` if fallbackErr := s.db.QueryRow(ctx, blockscoutQuery, blockNumber).Scan(&number, &hash, &parentHash, &transactionCount, &gasUsed, &gasLimit, ×tampISO); fallbackErr != nil { return nil, err } } block := map[string]any{ "number": number, "hash": hash, "parent_hash": parentHash, "transaction_count": transactionCount, "gas_used": gasUsed, "gas_limit": gasLimit, } if timestampISO != nil { block["timestamp_iso"] = *timestampISO } return block, nil } func extractBlockReference(query string) int64 { match := blockRefPattern.FindStringSubmatch(query) if len(match) != 2 { return 0 } var value int64 fmt.Sscan(match[1], &value) return value }