This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
Lore is a Stellar blockchain token explorer for MTLAP (Persons) and MTLAC (Companies) tokens. It provides a web interface to browse token holders and view detailed account information including balances, metadata, and trustlines.
# Build the binary
go build -o lore ./cmd/lore
# Run tests
go test ./...
# Format and lint
go fmt ./...
go vet ./...Use Docker Compose for local development with fast iteration:
# Quick start: build, start services, sync data
make dev
# After making code changes, rebuild and restart (fast - no container rebuild):
make dev-restart
# View logs
make dev-logs
# Stop everything
make dev-down
# Reset database and re-sync
make db-reset && make devdocker-compose.dev.ymlmounts the local binary into containersmake devandmake dev-restartusebuild-linuxtarget which cross-compiles for Linux- Syncer runs once on startup; restart it manually with
docker compose restart syncer
Docker containers run Linux. The Makefile handles this automatically:
make build— builds for local macOS (use withmake run)make build-linux— builds for Linux/arm64 (used bymake devandmake dev-restart)
Never run go build manually before make dev-restart — it will build a macOS binary that won't work in the Linux container. Always use the Makefile targets.
# Build images and start all services
make prod
# View logs
make prod-logs
# Stop
make prod-down# Start only PostgreSQL
make db
# Run the application
./lore --database-url "postgres://lore:lore@localhost:5432/lore?sslmode=disable" serve
# Run sync
./lore --database-url "postgres://lore:lore@localhost:5432/lore?sslmode=disable" sync
./lore --database-url "..." sync --full # Full resync (truncates tables first)The application follows a layered architecture:
-
Handler Layer (
internal/handler/) - HTTP request handling with Go 1.22+ routing. Usesr.PathValue()for path parameters. Routes:GET /(home),GET /accounts/{id}(detail),GET /accounts/{id}/reputation,GET /transactions/{hash},GET /search,GET /tokens/{issuer}/{code},GET|POST /init/*(forms). Note:GET /matches any path; to test "unregistered routes", use wrong HTTP method. -
Repository Layer (
internal/repository/) - Data access layer for PostgreSQL.AccountRepositoryprovides methods for querying accounts, stats, persons (MTLAP holders), and corporate accounts (MTLAC holders). Uses Squirrel query builder. -
Database Layer (
internal/database/) - PostgreSQL connection pool management via pgx. Includes embedded goose migrations that run automatically on startup. Pool settings: MaxConns=10, MinConns=2. -
Service Layer (
internal/service/) -StellarServicewraps the Stellar Horizon API client. Key methods:GetAccountsWithAsset()for paginated token holder lists,GetAccountDetail()for single account info. Includes utilities for base64 decoding and numbered metadata field parsing. -
Sync Layer (
internal/sync/) - Data synchronization from Stellar Horizon API to PostgreSQL. Fetches MTLAP/MTLAC holders, parses ManageData, calculates delegations, and fetches token prices from SDEX. Uses semaphore + WaitGroup for concurrent processing with 10-worker limit. -
Reputation Layer (
internal/reputation/) - Weighted reputation scoring system.Ratingis a constrained type (A/B/C/D) withIsValid()andValue()methods.Calculatorcomputes weighted scores based on rater portfolio and connections.Graphbuilds 2-level visualization of rating relationships. -
Template Layer (
internal/template/) - Embedded templates using Go'sembedpackage.base.htmlprovides master layout, extended byhome.htmlandaccount.html. Custom functions:add,addFloat(float64 + int),truncate,slice,formatNumber(space-separated thousands),votePower(log10-based: 1-10=1, 11-100=2, 101-1000=3),markdown(renders Markdown with XSS sanitization),searchURL(builds /search URLs with query params). -
Middleware Layer (
internal/middleware/) - HTTP middleware components. Middleware chain (outer to inner): Cache-Control → Rate Limiter → Router.CacheControlsets Cache-Control headers based on content type (static images: 1 year immutable, robots.txt: 1 day, swagger: 1 hour, init forms: 1 hour, API: 1 minute with revalidation, HTML pages: 5 minutes with revalidation, POST/PUT/DELETE: no-store).RateLimiterenforces per-IP rate limits with sliding window algorithm (default: 100 req/min), bypasses static files, supports X-Forwarded-For header, returns HTTP 429 with Retry-After on limit exceeded.ExtractIPextracts client IP from X-Forwarded-For, X-Real-IP, or RemoteAddr (SECURITY: only use behind trusted reverse proxy).
- Template Buffering: Render templates to
bytes.Bufferfirst, write to ResponseWriter only on success. Prevents partial HTML on template errors. - Resource Pooling: Use
sync.Poolfor objects created on hot paths. Handler hasbufferPoolfor bytes.Buffer reuse viagetBuffer()/putBuffer(). API handler has same pattern for JSON encoding. Prevents GC pressure from per-request allocations. - HTTP Client Reuse: NEVER create
&http.Client{}in request handlers or frequently-called functions. StellarService has sharedhttpClientfield with connection pooling (MaxIdleConns: 100, MaxIdleConnsPerHost: 10). Creating clients per-request causes memory/connection leaks. - Buffer Pool Pattern: Handlers use
buf := h.getBuffer(); defer h.putBuffer(buf)for template rendering. API uses same for JSON encoding. Always callbuf.Reset()before returning to pool. - Stellar Metadata: Account data is stored in base64 on Stellar; the service layer decodes transparently
- Stellar SDK Types:
horizon.Balanceembedsbase.Asset, so preferbal.Codeoverbal.Asset.Code(staticcheck QF1008). Importbasepackage when writing tests. - Pagination: Offset-based pagination for database queries, passed as
persons_offsetandcorporate_offsetquery params. Horizon API uses cursor-based pagination for account detail pages. - Numbered Fields: Account metadata like websites use numbered keys (Website0, Website1) parsed and sorted by
parseNumberedDataKeys() - Tag Fields: Account tags use
Tag*prefix keys (TagBelgrade, TagProgrammer) parsed byparseTagKeys(). Value is account ID (ignored for display). - AND Filtering Pattern: For requiring all values match, use
GROUP BY + HAVING COUNT(DISTINCT column) = Nin subqueries. - Configuration: Port via
--port/PORT, Horizon URL via--horizon-url/HORIZON_URL, log level via--log-level/LOG_LEVEL(debug, info, warn, error), database URL via--database-url/DATABASE_URL(required), rate limit via--rate-limit/RATE_LIMIT(default: 100) - Environment Variable Defaults: CRITICAL - Always provide fallback defaults for env vars/CLI flags. IntFlag/StringFlag return zero values when not set. Check for zero/empty and use
config.Default*constants. Example:if rateLimit <= 0 { rateLimit = config.DefaultRateLimit }. Production crashes without defaults. - Logging: Uses
log/slogwith JSON output and source location. Log levels:infofor lifecycle events,errorfor unexpected failures (not expected errors like 404),debugfor troubleshooting - Token Constants: Defined in
internal/config/config.go(MTLAP, MTLAC, issuer address) - Template Inheritance: Each page template must be cloned from base separately (see
template.go). UsingParseFSwith multiple templates defining the same block causes overwrites. - Markdown Rendering: Uses blackfriday for Markdown→HTML and bluemonday for XSS sanitization. Template function:
{{markdown .Field}}. Always sanitize untrusted blockchain data. - CSS Specificity in Templates:
.detail-block-content asets link color globally. Override with specific selectors like.detail-block-content .tag-chipwhen styling links inside detail blocks. - No-JS Design: Avoid page-load animations (like
slide-up) on interactive elements. Causes flashing when user clicks links that reload the page. - Constructor Pattern: Constructors return
(*T, error)with nil validation, not*Tthat silently returns nil on error. - Constrained Type Pattern: For domain values with limited valid states (like
Rating), use a typed string withIsValid()method and constants for valid values. Methods likeValue()can provide numeric conversions. - Transaction Atomicity: Wrap DELETE + INSERT sequences in transactions to prevent partial state on failure.
- Database Migrations: Add new migrations in
internal/database/migrations/with formatNNN_description.sql. Migrations run automatically on startup via goose. - No Foreign Key Constraints: Do not use foreign key constraints in database schema. Data integrity is managed at the application level during sync operations.
- Relation Index Preservation: Relationship indices are stored as strings, not integers, to preserve leading zeros.
PartOf002andPartOf2are distinct keys in the blockchain that must remain distinct in the database. Converting to int would cause both to become2, violating the primary key constraint(source_account_id, target_account_id, relation_type, relation_index). - Squirrel SQL Expressions: Use
sq.Expr("NOW()")for SQL functions, not string literals like"NOW()". String literals are inserted as values, not executed as SQL. - Graceful Degradation: Non-critical data fetches (LP shares, operations) should log errors and continue with nil/empty data, not fail the entire page render.
- Template Block Override Pattern: Use
{{block "name" .}}default{{end}}in base.html for values that child templates can override (e.g., SEO meta tags). Child templates define{{define "name"}}custom value{{end}}. - Static Files: Embedded in
internal/static/using//go:embed. Served viahandler.RegisterStaticRoutes(). Add new static files to embed directive instatic.go. - Cache-Control Strategy: Static images (favicon, og-image) cached for 1 year with immutable flag. robots.txt cached for 1 day. Swagger docs cached for 1 hour. Init forms (GET) cached for 1 hour. API endpoints cached for 1 minute with must-revalidate. HTML pages cached for 5 minutes with must-revalidate. POST/PUT/DELETE requests use no-store. Middleware applies headers before rate limiting to optimize performance.
- Rate Limiting: Per-IP sliding window algorithm with 1-minute window. Default limit: 100 requests/minute (configurable via
--rate-limit/RATE_LIMIT). Static files bypass rate limiting. Returns HTTP 429 with Retry-After header on limit exceeded. Background cleanup goroutine removes expired entries every minute. MUST callClose()on shutdown to prevent goroutine leaks.
- mtla_delegate: General delegation target (account ID that receives delegated votes)
- mtla_c_delegate: Has dual meaning:
"ready"→ account is council-ready (can receive council delegations)- Account ID → delegates council votes to that account
- Council delegation chains: Use
council_delegate_tocolumn, notdelegate_to, for vote calculations
- Init Forms (
internal/service/init.go) - Generate XDR by comparing original vs current form state, producing only the diff as ManageData operations - Use
txnbuild.ManageDatawithValue: nilto delete a key, non-nil to set/update - Always validate account IDs with
keypair.ParseAddress()before building transactions
The Montelibero Blockchainization standard defines relationship types stored as account ManageData entries.
| Tag A (direction) | Tag B (direction) | Meaning |
|---|---|---|
| MyPart (org→person) | PartOf (person→org) | Membership |
| Guardian (guardian→ward) | Ward (ward→guardian) | Guardianship |
| OwnershipFull (corp→owner) | Owner (owner→corp) | 95%+ ownership |
| OwnershipMajority (corp→owner) | OwnerMajority (owner→corp) | 25-95% ownership |
| OwnershipMinority (corp→owner) | OwnerMinority (owner→corp) | <25% ownership |
| Employer (employer→employee) | Employee (employee→employer) | Employment |
Only display when BOTH parties have declared. Hide one-way declarations.
- Spouse, OneFamily, Partnership, Collaboration, FactionMember
- A/B/C/D (credit rating), Sympathy, Love, Divorce, Contractor, Client, WelcomeGuest, RecommendToMTLA
Use github.com/samber/lo for readable, type-safe slice/map operations. Prefer lo helpers over manual loops.
lo.Filter(slice, func(x T, _ int) bool { return condition }) // Filter elements
lo.Map(slice, func(x T, _ int) R { return transform(x) }) // Transform elements
lo.Reduce(slice, func(acc R, x T, _ int) R { ... }, init) // Reduce to single value
lo.ForEach(slice, func(x T, _ int) { ... }) // Iterate with side effects
lo.Uniq(slice) // Remove duplicates
lo.UniqBy(slice, func(x T) K { return key }) // Remove duplicates by key
lo.Compact(slice) // Remove zero values ("", 0, nil)
lo.Flatten(nested) // Flatten nested slices
lo.Chunk(slice, size) // Split into chunks
lo.GroupBy(slice, func(x T) K { return key }) // Group by key -> map[K][]T
lo.KeyBy(slice, func(x T) K { return key }) // Index by key -> map[K]T
lo.Partition(slice, func(x T, _ int) bool { ... }) // Split into [match, nomatch]lo.Find(slice, func(x T) bool { return condition }) // Returns (value, found)
lo.FindOrElse(slice, fallback, func(x T) bool { ... }) // Returns value or fallback
lo.Contains(slice, value) // Check if exists
lo.IndexOf(slice, value) // Find index (-1 if not found)
lo.Every(slice, func(x T, _ int) bool { ... }) // All match predicate
lo.Some(slice, func(x T, _ int) bool { ... }) // Any matches predicatelo.Keys(m) // Get all keys
lo.Values(m) // Get all values
lo.PickBy(m, func(k K, v V) bool { ... }) // Filter map entries
lo.OmitBy(m, func(k K, v V) bool { ... }) // Exclude map entries
lo.MapKeys(m, func(v V, k K) K2 { return newKey }) // Transform keys
lo.MapValues(m, func(v V, k K) V2 { return newValue }) // Transform values
lo.Invert(m) // Swap keys and values
lo.Assign(maps...) // Merge maps (later wins)lo.Must(val, err) // Panic on error, return val
lo.Must0(err) // Panic on error (no return)
lo.Must2(v1, v2, err) // Panic on error, return v1, v2
lo.Coalesce(vals...) // First non-zero value
lo.CoalesceOrEmpty(vals...) // First non-zero or zero value
lo.IsEmpty(val) // Check if zero value
lo.FromPtr(ptr) // Dereference or zero value
lo.ToPtr(val) // Create pointer to value
lo.Ternary(cond, ifTrue, ifFalse) // Inline conditional
lo.If(cond, val).Else(other) // Fluent conditionalimport lop "github.com/samber/lo/parallel"
lop.Map(slice, func(x T, _ int) R { ... }) // Parallel map
lop.ForEach(slice, func(x T, _ int) { ... }) // Parallel iteration
lop.Filter(slice, func(x T, _ int) bool { ... }) // Parallel filterinternal/service/stellar_test.go- utility functions (parseNumberedDataKeys,decodeBase64)internal/sync/*_test.go- sync parsing functions (parseAccountData,parseAssociationTags,getAssetType)internal/handler/*_test.go- HTTP handlers using mockery-generated mocksinternal/reputation/*_test.go- reputation calculator and types- Use table-driven tests with
t.Run()for edge cases
- Check for per-request object creation (HTTP clients, buffers, maps). Use
grep "http\.Client{" "bytes\.Buffer"to find allocations. Look for missingdefer Close(),defer Stop(), or missing cleanup goroutines.
- Config in
.mockery.yaml, regenerate withmockeryorgo generate ./internal/handler/ - When renaming interfaces, update
.mockery.yamland delete old mock files before regenerating - Use
EXPECT()pattern with specific expectations; avoid.Maybe()in favor of isolated sub-tests with fresh mocks
- Commit messages: Use Conventional Commits format (e.g.,
feat:,fix:,refactor:,docs:,chore:) - PR Merge Strategy: Repository only allows rebase merges. Use
gh pr merge --rebase --delete-branch