2026-02-10 11:32:49 -08:00
package track2
import (
2026-04-07 23:22:12 -07:00
"encoding/hex"
2026-02-10 11:32:49 -08:00
"encoding/json"
2026-04-07 23:22:12 -07:00
"fmt"
2026-02-10 11:32:49 -08:00
"net/http"
2026-04-07 23:22:12 -07:00
"regexp"
2026-02-10 11:32:49 -08:00
"strconv"
"strings"
2026-04-07 23:22:12 -07:00
"github.com/ethereum/go-ethereum/common"
2026-02-10 11:32:49 -08:00
"github.com/jackc/pgx/v5/pgxpool"
)
2026-04-07 23:22:12 -07:00
var track2HashPattern = regexp . MustCompile ( ` ^0x[0-9a-fA-F] { 64}$ ` )
2026-02-10 11:32:49 -08:00
// Server handles Track 2 endpoints
type Server struct {
db * pgxpool . Pool
chainID int
}
// NewServer creates a new Track 2 server
func NewServer ( db * pgxpool . Pool , chainID int ) * Server {
return & Server {
db : db ,
chainID : chainID ,
}
}
// HandleAddressTransactions handles GET /api/v1/track2/address/:addr/txs
func ( s * Server ) HandleAddressTransactions ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
2026-04-07 23:22:12 -07:00
if ! s . requireDB ( w ) {
return
}
2026-02-10 11:32:49 -08:00
path := strings . TrimPrefix ( r . URL . Path , "/api/v1/track2/address/" )
parts := strings . Split ( path , "/" )
if len ( parts ) < 2 || parts [ 1 ] != "txs" {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid path" )
return
}
2026-04-07 23:22:12 -07:00
address , err := normalizeTrack2Address ( parts [ 0 ] )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
2026-02-10 11:32:49 -08:00
page , _ := strconv . Atoi ( r . URL . Query ( ) . Get ( "page" ) )
if page < 1 {
page = 1
}
limit , _ := strconv . Atoi ( r . URL . Query ( ) . Get ( "limit" ) )
if limit < 1 || limit > 100 {
limit = 20
}
offset := ( page - 1 ) * limit
query := `
SELECT hash , from_address , to_address , value , block_number , timestamp , status
FROM transactions
2026-04-07 23:22:12 -07:00
WHERE chain_id = $ 1 AND ( LOWER ( from_address ) = $ 2 OR LOWER ( to_address ) = $ 2 )
2026-02-10 11:32:49 -08:00
ORDER BY block_number DESC , timestamp DESC
LIMIT $ 3 OFFSET $ 4
`
rows , err := s . db . Query ( r . Context ( ) , query , s . chainID , address , limit , offset )
if err != nil {
writeError ( w , http . StatusInternalServerError , "database_error" , err . Error ( ) )
return
}
defer rows . Close ( )
transactions := [ ] map [ string ] interface { } { }
for rows . Next ( ) {
var hash , from , to , value , status string
var blockNumber int64
var timestamp interface { }
if err := rows . Scan ( & hash , & from , & to , & value , & blockNumber , & timestamp , & status ) ; err != nil {
continue
}
direction := "received"
if strings . ToLower ( from ) == address {
direction = "sent"
}
transactions = append ( transactions , map [ string ] interface { } {
"hash" : hash ,
"from" : from ,
"to" : to ,
"value" : value ,
"block_number" : blockNumber ,
"timestamp" : timestamp ,
"status" : status ,
"direction" : direction ,
} )
}
// Get total count
var total int
2026-04-07 23:22:12 -07:00
countQuery := ` SELECT COUNT(*) FROM transactions WHERE chain_id = $1 AND (LOWER(from_address) = $2 OR LOWER(to_address) = $2) `
2026-02-10 11:32:49 -08:00
s . db . QueryRow ( r . Context ( ) , countQuery , s . chainID , address ) . Scan ( & total )
response := map [ string ] interface { } {
"data" : transactions ,
"pagination" : map [ string ] interface { } {
"page" : page ,
"limit" : limit ,
"total" : total ,
"total_pages" : ( total + limit - 1 ) / limit ,
} ,
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( response )
}
// HandleAddressTokens handles GET /api/v1/track2/address/:addr/tokens
func ( s * Server ) HandleAddressTokens ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
2026-04-07 23:22:12 -07:00
if ! s . requireDB ( w ) {
return
}
2026-02-10 11:32:49 -08:00
path := strings . TrimPrefix ( r . URL . Path , "/api/v1/track2/address/" )
parts := strings . Split ( path , "/" )
if len ( parts ) < 2 || parts [ 1 ] != "tokens" {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid path" )
return
}
2026-04-07 23:22:12 -07:00
address , err := normalizeTrack2Address ( parts [ 0 ] )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
2026-02-10 11:32:49 -08:00
query := `
SELECT token_contract , balance , last_updated_timestamp
FROM token_balances
2026-04-07 23:22:12 -07:00
WHERE LOWER ( address ) = $ 1 AND chain_id = $ 2 AND balance > 0
2026-02-10 11:32:49 -08:00
ORDER BY balance DESC
`
rows , err := s . db . Query ( r . Context ( ) , query , address , s . chainID )
if err != nil {
writeError ( w , http . StatusInternalServerError , "database_error" , err . Error ( ) )
return
}
defer rows . Close ( )
tokens := [ ] map [ string ] interface { } { }
for rows . Next ( ) {
var contract , balance string
var lastUpdated interface { }
if err := rows . Scan ( & contract , & balance , & lastUpdated ) ; err != nil {
continue
}
tokens = append ( tokens , map [ string ] interface { } {
"contract" : contract ,
"balance" : balance ,
2026-04-07 23:22:12 -07:00
"balance_formatted" : nil ,
2026-02-10 11:32:49 -08:00
"last_updated" : lastUpdated ,
} )
}
response := map [ string ] interface { } {
"data" : map [ string ] interface { } {
"address" : address ,
"tokens" : tokens ,
"total_tokens" : len ( tokens ) ,
} ,
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( response )
}
// HandleTokenInfo handles GET /api/v1/track2/token/:contract
func ( s * Server ) HandleTokenInfo ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
2026-04-07 23:22:12 -07:00
if ! s . requireDB ( w ) {
return
}
2026-02-10 11:32:49 -08:00
path := strings . TrimPrefix ( r . URL . Path , "/api/v1/track2/token/" )
2026-04-07 23:22:12 -07:00
contract , err := normalizeTrack2Address ( path )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
2026-02-10 11:32:49 -08:00
// Get token info from token_transfers
query := `
SELECT
2026-04-07 23:22:12 -07:00
(
SELECT COUNT ( * )
FROM (
SELECT from_address AS address
FROM token_transfers
WHERE token_contract = $ 1
AND chain_id = $ 2
AND timestamp >= NOW ( ) - INTERVAL ' 24 hours '
AND from_address IS NOT NULL
AND from_address < > ' '
UNION
SELECT to_address AS address
FROM token_transfers
WHERE token_contract = $ 1
AND chain_id = $ 2
AND timestamp >= NOW ( ) - INTERVAL ' 24 hours '
AND to_address IS NOT NULL
AND to_address < > ' '
) holder_addresses
) as holders ,
2026-02-10 11:32:49 -08:00
COUNT ( * ) as transfers_24h ,
SUM ( value ) as volume_24h
FROM token_transfers
WHERE token_contract = $ 1 AND chain_id = $ 2
AND timestamp >= NOW ( ) - INTERVAL ' 24 hours '
`
var holders , transfers24h int
var volume24h string
2026-04-07 23:22:12 -07:00
err = s . db . QueryRow ( r . Context ( ) , query , contract , s . chainID ) . Scan ( & holders , & transfers24h , & volume24h )
2026-02-10 11:32:49 -08:00
if err != nil {
writeError ( w , http . StatusNotFound , "not_found" , "Token not found" )
return
}
response := map [ string ] interface { } {
"data" : map [ string ] interface { } {
"contract" : contract ,
"holders" : holders ,
"transfers_24h" : transfers24h ,
"volume_24h" : volume24h ,
} ,
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( response )
}
// HandleSearch handles GET /api/v1/track2/search?q=
func ( s * Server ) HandleSearch ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
2026-04-07 23:22:12 -07:00
if ! s . requireDB ( w ) {
return
}
2026-02-10 11:32:49 -08:00
2026-04-07 23:22:12 -07:00
query := strings . TrimSpace ( r . URL . Query ( ) . Get ( "q" ) )
2026-02-10 11:32:49 -08:00
if query == "" {
writeError ( w , http . StatusBadRequest , "bad_request" , "Query parameter 'q' is required" )
return
}
// Try to detect type and search
var result map [ string ] interface { }
// Check if it's a block number
if blockNum , err := strconv . ParseInt ( query , 10 , 64 ) ; err == nil {
var hash string
err := s . db . QueryRow ( r . Context ( ) , ` SELECT hash FROM blocks WHERE chain_id = $1 AND number = $2 ` , s . chainID , blockNum ) . Scan ( & hash )
if err == nil {
result = map [ string ] interface { } {
"type" : "block" ,
"result" : map [ string ] interface { } {
"number" : blockNum ,
"hash" : hash ,
} ,
}
}
2026-04-07 23:22:12 -07:00
} else if track2HashPattern . MatchString ( query ) {
hash , err := normalizeTrack2Hash ( query )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
2026-02-10 11:32:49 -08:00
var txHash string
2026-04-07 23:22:12 -07:00
err = s . db . QueryRow ( r . Context ( ) , ` SELECT hash FROM transactions WHERE chain_id = $1 AND LOWER(hash) = $2 ` , s . chainID , hash ) . Scan ( & txHash )
2026-02-10 11:32:49 -08:00
if err == nil {
result = map [ string ] interface { } {
"type" : "transaction" ,
"result" : map [ string ] interface { } {
"hash" : txHash ,
} ,
}
2026-04-07 23:22:12 -07:00
}
} else if common . IsHexAddress ( query ) {
address , err := normalizeTrack2Address ( query )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
var exists bool
existsQuery := `
SELECT EXISTS (
SELECT 1
FROM addresses
WHERE chain_id = $ 1 AND LOWER ( address ) = $ 2
UNION
SELECT 1
FROM transactions
WHERE chain_id = $ 1 AND ( LOWER ( from_address ) = $ 2 OR LOWER ( to_address ) = $ 2 )
UNION
SELECT 1
FROM token_balances
WHERE chain_id = $ 1 AND LOWER ( address ) = $ 2
)
`
err = s . db . QueryRow ( r . Context ( ) , existsQuery , s . chainID , address ) . Scan ( & exists )
if err == nil && exists {
2026-02-10 11:32:49 -08:00
var balance string
2026-04-07 23:22:12 -07:00
err = s . db . QueryRow ( r . Context ( ) , ` SELECT COALESCE(SUM(balance), '0') FROM token_balances WHERE LOWER(address) = $1 AND chain_id = $2 ` , address , s . chainID ) . Scan ( & balance )
if err != nil {
balance = "0"
}
result = map [ string ] interface { } {
"type" : "address" ,
"result" : map [ string ] interface { } {
"address" : address ,
"balance" : balance ,
} ,
2026-02-10 11:32:49 -08:00
}
}
}
if result == nil {
writeError ( w , http . StatusNotFound , "not_found" , "No results found" )
return
}
response := map [ string ] interface { } {
"data" : result ,
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( response )
}
// HandleInternalTransactions handles GET /api/v1/track2/address/:addr/internal-txs
func ( s * Server ) HandleInternalTransactions ( w http . ResponseWriter , r * http . Request ) {
if r . Method != http . MethodGet {
writeError ( w , http . StatusMethodNotAllowed , "method_not_allowed" , "Method not allowed" )
return
}
2026-04-07 23:22:12 -07:00
if ! s . requireDB ( w ) {
return
}
2026-02-10 11:32:49 -08:00
path := strings . TrimPrefix ( r . URL . Path , "/api/v1/track2/address/" )
parts := strings . Split ( path , "/" )
if len ( parts ) < 2 || parts [ 1 ] != "internal-txs" {
writeError ( w , http . StatusBadRequest , "bad_request" , "Invalid path" )
return
}
2026-04-07 23:22:12 -07:00
address , err := normalizeTrack2Address ( parts [ 0 ] )
if err != nil {
writeError ( w , http . StatusBadRequest , "bad_request" , err . Error ( ) )
return
}
2026-02-10 11:32:49 -08:00
page , _ := strconv . Atoi ( r . URL . Query ( ) . Get ( "page" ) )
if page < 1 {
page = 1
}
limit , _ := strconv . Atoi ( r . URL . Query ( ) . Get ( "limit" ) )
if limit < 1 || limit > 100 {
limit = 20
}
offset := ( page - 1 ) * limit
query := `
SELECT transaction_hash , from_address , to_address , value , block_number , timestamp
FROM internal_transactions
2026-04-07 23:22:12 -07:00
WHERE chain_id = $ 1 AND ( LOWER ( from_address ) = $ 2 OR LOWER ( to_address ) = $ 2 )
2026-02-10 11:32:49 -08:00
ORDER BY block_number DESC , timestamp DESC
LIMIT $ 3 OFFSET $ 4
`
rows , err := s . db . Query ( r . Context ( ) , query , s . chainID , address , limit , offset )
if err != nil {
writeError ( w , http . StatusInternalServerError , "database_error" , err . Error ( ) )
return
}
defer rows . Close ( )
internalTxs := [ ] map [ string ] interface { } { }
for rows . Next ( ) {
var txHash , from , to , value string
var blockNumber int64
var timestamp interface { }
if err := rows . Scan ( & txHash , & from , & to , & value , & blockNumber , & timestamp ) ; err != nil {
continue
}
internalTxs = append ( internalTxs , map [ string ] interface { } {
"transaction_hash" : txHash ,
"from" : from ,
"to" : to ,
"value" : value ,
"block_number" : blockNumber ,
"timestamp" : timestamp ,
} )
}
var total int
2026-04-07 23:22:12 -07:00
countQuery := ` SELECT COUNT(*) FROM internal_transactions WHERE chain_id = $1 AND (LOWER(from_address) = $2 OR LOWER(to_address) = $2) `
2026-02-10 11:32:49 -08:00
s . db . QueryRow ( r . Context ( ) , countQuery , s . chainID , address ) . Scan ( & total )
response := map [ string ] interface { } {
"data" : internalTxs ,
"pagination" : map [ string ] interface { } {
"page" : page ,
"limit" : limit ,
"total" : total ,
"total_pages" : ( total + limit - 1 ) / limit ,
} ,
}
w . Header ( ) . Set ( "Content-Type" , "application/json" )
json . NewEncoder ( w ) . Encode ( response )
}
func writeError ( w http . ResponseWriter , statusCode int , code , message string ) {
w . Header ( ) . Set ( "Content-Type" , "application/json" )
w . WriteHeader ( statusCode )
json . NewEncoder ( w ) . Encode ( map [ string ] interface { } {
"error" : map [ string ] interface { } {
"code" : code ,
"message" : message ,
} ,
} )
}
2026-04-07 23:22:12 -07:00
func ( s * Server ) requireDB ( w http . ResponseWriter ) bool {
if s . db == nil {
writeError ( w , http . StatusServiceUnavailable , "service_unavailable" , "database not configured" )
return false
}
return true
}
func normalizeTrack2Address ( value string ) ( string , error ) {
trimmed := strings . TrimSpace ( value )
if ! common . IsHexAddress ( trimmed ) {
return "" , fmt . Errorf ( "invalid address format" )
}
return strings . ToLower ( common . HexToAddress ( trimmed ) . Hex ( ) ) , nil
}
func normalizeTrack2Hash ( value string ) ( string , error ) {
trimmed := strings . TrimSpace ( value )
if ! track2HashPattern . MatchString ( trimmed ) {
return "" , fmt . Errorf ( "invalid transaction hash" )
}
if _ , err := hex . DecodeString ( trimmed [ 2 : ] ) ; err != nil {
return "" , fmt . Errorf ( "invalid transaction hash" )
}
return strings . ToLower ( trimmed ) , nil
}