// Package node — channel endpoints. // // `/api/channels/:id/members` returns every Ed25519 pubkey registered as a // channel member together with their current X25519 pubkey (from the // identity registry). Clients sealing a message to a channel iterate this // list and call relay.Seal once per recipient — that's the "fan-out" // group-messaging model (R1 in the roadmap). // // Why enrich with X25519 here rather than making the client do it? // - One HTTP round trip vs N. At 10+ members the latency difference is // significant over mobile networks. // - The server already holds the identity state; no extra DB hops. // - Clients get a stable, already-joined view — if a member hasn't // published an X25519 key yet, we return them with `x25519_pub_key=""` // so the caller knows to skip or retry later. package node import ( "fmt" "net/http" "strings" "go-blockchain/blockchain" "go-blockchain/wallet" ) func registerChannelAPI(mux *http.ServeMux, q ExplorerQuery) { // GET /api/channels/{id} → channel metadata // GET /api/channels/{id}/members → enriched member list // // One HandleFunc deals with both by sniffing the path suffix. mux.HandleFunc("/api/channels/", func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } path := strings.TrimPrefix(r.URL.Path, "/api/channels/") path = strings.Trim(path, "/") if path == "" { jsonErr(w, fmt.Errorf("channel id required"), 400) return } switch { case strings.HasSuffix(path, "/members"): id := strings.TrimSuffix(path, "/members") handleChannelMembers(w, q, id) default: handleChannelInfo(w, q, path) } }) } func handleChannelInfo(w http.ResponseWriter, q ExplorerQuery, channelID string) { if q.GetChannel == nil { jsonErr(w, fmt.Errorf("channel queries not configured"), 503) return } ch, err := q.GetChannel(channelID) if err != nil { jsonErr(w, err, 500) return } if ch == nil { jsonErr(w, fmt.Errorf("channel %s not found", channelID), 404) return } jsonOK(w, ch) } func handleChannelMembers(w http.ResponseWriter, q ExplorerQuery, channelID string) { if q.GetChannelMembers == nil { jsonErr(w, fmt.Errorf("channel queries not configured"), 503) return } pubs, err := q.GetChannelMembers(channelID) if err != nil { jsonErr(w, err, 500) return } out := make([]blockchain.ChannelMember, 0, len(pubs)) for _, pub := range pubs { member := blockchain.ChannelMember{ PubKey: pub, Address: wallet.PubKeyToAddress(pub), } // Best-effort X25519 lookup — skip silently on miss so a member // who hasn't published their identity yet doesn't prevent the // whole list from returning. The sender will just skip them on // fan-out and retry later (after that member does register). if q.IdentityInfo != nil { if info, err := q.IdentityInfo(pub); err == nil && info != nil { member.X25519PubKey = info.X25519Pub } } out = append(out, member) } jsonOK(w, map[string]any{ "channel_id": channelID, "count": len(out), "members": out, }) }