package node import ( "encoding/base64" "encoding/json" "fmt" "net/http" "strings" "time" "go-blockchain/blockchain" "go-blockchain/relay" ) // RelayConfig holds dependencies for the relay HTTP API. type RelayConfig struct { Mailbox *relay.Mailbox // Send seals a message for recipientX25519PubHex and broadcasts it. // Returns the envelope ID. nil disables POST /relay/send. Send func(recipientPubHex string, msg []byte) (string, error) // Broadcast publishes a pre-sealed Envelope on gossipsub and stores it in the mailbox. // nil disables POST /relay/broadcast. Broadcast func(env *relay.Envelope) error // ContactRequests returns incoming contact records for the given Ed25519 pubkey. ContactRequests func(pubKey string) ([]blockchain.ContactInfo, error) } // registerRelayRoutes wires relay mailbox endpoints onto mux. // // POST /relay/send {recipient_pub, msg_b64} // POST /relay/broadcast {envelope: } // GET /relay/inbox ?pub=[&since=][&limit=N] // GET /relay/inbox/count ?pub= // DELETE /relay/inbox/{envID} ?pub= // GET /relay/contacts ?pub= func registerRelayRoutes(mux *http.ServeMux, rc RelayConfig) { mux.HandleFunc("/relay/send", relaySend(rc)) mux.HandleFunc("/relay/broadcast", relayBroadcast(rc)) mux.HandleFunc("/relay/inbox/count", relayInboxCount(rc)) mux.HandleFunc("/relay/inbox/", relayInboxDelete(rc)) mux.HandleFunc("/relay/inbox", relayInboxList(rc)) mux.HandleFunc("/relay/contacts", relayContacts(rc)) } // relayInboxList handles GET /relay/inbox?pub=[&since=][&limit=N] func relayInboxList(rc RelayConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } pub := r.URL.Query().Get("pub") if pub == "" { jsonErr(w, fmt.Errorf("pub parameter required"), 400) return } since := int64(0) if s := r.URL.Query().Get("since"); s != "" { if v, err := parseInt64(s); err == nil && v > 0 { since = v } } limit := queryIntMin0(r, "limit") if limit == 0 { limit = 50 } envelopes, err := rc.Mailbox.List(pub, since, limit) if err != nil { jsonErr(w, err, 500) return } type item struct { ID string `json:"id"` SenderPub string `json:"sender_pub"` RecipientPub string `json:"recipient_pub"` FeeUT uint64 `json:"fee_ut,omitempty"` SentAt int64 `json:"sent_at"` SentAtHuman string `json:"sent_at_human"` Nonce []byte `json:"nonce"` Ciphertext []byte `json:"ciphertext"` } out := make([]item, 0, len(envelopes)) for _, env := range envelopes { out = append(out, item{ ID: env.ID, SenderPub: env.SenderPub, RecipientPub: env.RecipientPub, FeeUT: env.FeeUT, SentAt: env.SentAt, SentAtHuman: time.Unix(env.SentAt, 0).UTC().Format(time.RFC3339), Nonce: env.Nonce, Ciphertext: env.Ciphertext, }) } hasMore := len(out) == limit jsonOK(w, map[string]any{ "pub": pub, "count": len(out), "has_more": hasMore, "items": out, }) } } // relayInboxDelete handles DELETE /relay/inbox/{envelopeID}?pub= func relayInboxDelete(rc RelayConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodDelete { // Also serve GET /relay/inbox/{id} for convenience (fetch single envelope) if r.Method == http.MethodGet { relayInboxList(rc)(w, r) return } jsonErr(w, fmt.Errorf("method not allowed"), 405) return } envID := strings.TrimPrefix(r.URL.Path, "/relay/inbox/") if envID == "" { jsonErr(w, fmt.Errorf("envelope ID required in path"), 400) return } pub := r.URL.Query().Get("pub") if pub == "" { jsonErr(w, fmt.Errorf("pub parameter required"), 400) return } if err := rc.Mailbox.Delete(pub, envID); err != nil { jsonErr(w, err, 500) return } jsonOK(w, map[string]string{"id": envID, "status": "deleted"}) } } // relayInboxCount handles GET /relay/inbox/count?pub= func relayInboxCount(rc RelayConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { pub := r.URL.Query().Get("pub") if pub == "" { jsonErr(w, fmt.Errorf("pub parameter required"), 400) return } count, err := rc.Mailbox.Count(pub) if err != nil { jsonErr(w, err, 500) return } jsonOK(w, map[string]any{"pub": pub, "count": count}) } } // relaySend handles POST /relay/send // // Request body: // // { // "recipient_pub": "", // "msg_b64": "", // } // // The relay node seals the message using its own X25519 keypair and broadcasts // it on the relay gossipsub topic. No on-chain fee is attached — delivery is // free for light clients using this endpoint. func relaySend(rc RelayConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } if rc.Send == nil { jsonErr(w, fmt.Errorf("relay send not available on this node"), 503) return } var req struct { RecipientPub string `json:"recipient_pub"` MsgB64 string `json:"msg_b64"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonErr(w, fmt.Errorf("invalid JSON: %w", err), 400) return } if req.RecipientPub == "" { jsonErr(w, fmt.Errorf("recipient_pub is required"), 400) return } if req.MsgB64 == "" { jsonErr(w, fmt.Errorf("msg_b64 is required"), 400) return } msg, err := decodeBase64(req.MsgB64) if err != nil { jsonErr(w, fmt.Errorf("msg_b64: %w", err), 400) return } if len(msg) == 0 { jsonErr(w, fmt.Errorf("msg_b64: empty message"), 400) return } envID, err := rc.Send(req.RecipientPub, msg) if err != nil { jsonErr(w, fmt.Errorf("send failed: %w", err), 500) return } jsonOK(w, map[string]string{ "id": envID, "recipient_pub": req.RecipientPub, "status": "sent", }) } } // decodeBase64 accepts both standard and URL-safe base64. func decodeBase64(s string) ([]byte, error) { // Try URL-safe first (no padding required), then standard. if b, err := base64.RawURLEncoding.DecodeString(s); err == nil { return b, nil } return base64.StdEncoding.DecodeString(s) } // relayBroadcast handles POST /relay/broadcast // // Request body: {"envelope": } // // Light clients use this to publish pre-sealed envelopes without a direct // libp2p connection. The relay node stores it in the mailbox and gossips it. func relayBroadcast(rc RelayConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodPost { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } if rc.Broadcast == nil { jsonErr(w, fmt.Errorf("relay broadcast not available on this node"), 503) return } var req struct { Envelope *relay.Envelope `json:"envelope"` } if err := json.NewDecoder(r.Body).Decode(&req); err != nil { jsonErr(w, fmt.Errorf("invalid JSON: %w", err), 400) return } if req.Envelope == nil { jsonErr(w, fmt.Errorf("envelope is required"), 400) return } if req.Envelope.ID == "" { jsonErr(w, fmt.Errorf("envelope.id is required"), 400) return } if len(req.Envelope.Ciphertext) == 0 { jsonErr(w, fmt.Errorf("envelope.ciphertext is required"), 400) return } if err := rc.Broadcast(req.Envelope); err != nil { jsonErr(w, fmt.Errorf("broadcast failed: %w", err), 500) return } jsonOK(w, map[string]string{ "id": req.Envelope.ID, "status": "broadcast", }) } } // relayContacts handles GET /relay/contacts?pub= // // Returns all incoming contact requests for the given Ed25519 public key. func relayContacts(rc RelayConfig) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if r.Method != http.MethodGet { jsonErr(w, fmt.Errorf("method not allowed"), 405) return } if rc.ContactRequests == nil { jsonErr(w, fmt.Errorf("contacts not available on this node"), 503) return } pub := r.URL.Query().Get("pub") if pub == "" { jsonErr(w, fmt.Errorf("pub parameter required"), 400) return } contacts, err := rc.ContactRequests(pub) if err != nil { jsonErr(w, err, 500) return } jsonOK(w, map[string]any{ "pub": pub, "count": len(contacts), "contacts": contacts, }) } } // parseInt64 parses a string as int64. func parseInt64(s string) (int64, error) { var v int64 if err := json.Unmarshal([]byte(s), &v); err != nil { return 0, err } return v, nil }