1+ package server
2+
3+ import (
4+ "context"
5+ "encoding/hex"
6+ "encoding/json"
7+ "fmt"
8+ "html/template"
9+ "net/http"
10+ "strconv"
11+ "sync"
12+ "time"
13+
14+ coreda "github.com/evstack/ev-node/core/da"
15+ "github.com/rs/zerolog"
16+ )
17+
18+ // DASubmissionInfo represents information about a DA submission
19+ type DASubmissionInfo struct {
20+ ID string `json:"id"`
21+ Height uint64 `json:"height"`
22+ BlobSize uint64 `json:"blob_size"`
23+ Timestamp time.Time `json:"timestamp"`
24+ GasPrice float64 `json:"gas_price"`
25+ StatusCode string `json:"status_code"`
26+ Message string `json:"message,omitempty"`
27+ NumBlobs int `json:"num_blobs"`
28+ BlobIDs []string `json:"blob_ids,omitempty"`
29+ }
30+
31+ // DAVisualizationServer provides DA layer visualization endpoints
32+ type DAVisualizationServer struct {
33+ da coreda.DA
34+ logger zerolog.Logger
35+ submissions []DASubmissionInfo
36+ mutex sync.RWMutex
37+ }
38+
39+ // NewDAVisualizationServer creates a new DA visualization server
40+ func NewDAVisualizationServer (da coreda.DA , logger zerolog.Logger ) * DAVisualizationServer {
41+ return & DAVisualizationServer {
42+ da : da ,
43+ logger : logger ,
44+ submissions : make ([]DASubmissionInfo , 0 ),
45+ }
46+ }
47+
48+ // RecordSubmission records a DA submission for visualization
49+ func (s * DAVisualizationServer ) RecordSubmission (result * coreda.ResultSubmit , gasPrice float64 , numBlobs int ) {
50+ s .mutex .Lock ()
51+ defer s .mutex .Unlock ()
52+
53+ statusCode := s .getStatusCodeString (result .Code )
54+ blobIDs := make ([]string , len (result .IDs ))
55+ for i , id := range result .IDs {
56+ blobIDs [i ] = hex .EncodeToString (id )
57+ }
58+
59+ submission := DASubmissionInfo {
60+ ID : fmt .Sprintf ("submission_%d_%d" , result .Height , time .Now ().Unix ()),
61+ Height : result .Height ,
62+ BlobSize : result .BlobSize ,
63+ Timestamp : result .Timestamp ,
64+ GasPrice : gasPrice ,
65+ StatusCode : statusCode ,
66+ Message : result .Message ,
67+ NumBlobs : numBlobs ,
68+ BlobIDs : blobIDs ,
69+ }
70+
71+ // Keep only the last 100 submissions to avoid memory growth
72+ s .submissions = append (s .submissions , submission )
73+ if len (s .submissions ) > 100 {
74+ s .submissions = s .submissions [1 :]
75+ }
76+ }
77+
78+ // getStatusCodeString converts status code to human-readable string
79+ func (s * DAVisualizationServer ) getStatusCodeString (code coreda.StatusCode ) string {
80+ switch code {
81+ case coreda .StatusSuccess :
82+ return "Success"
83+ case coreda .StatusNotFound :
84+ return "Not Found"
85+ case coreda .StatusNotIncludedInBlock :
86+ return "Not Included In Block"
87+ case coreda .StatusAlreadyInMempool :
88+ return "Already In Mempool"
89+ case coreda .StatusTooBig :
90+ return "Too Big"
91+ case coreda .StatusContextDeadline :
92+ return "Context Deadline"
93+ case coreda .StatusError :
94+ return "Error"
95+ case coreda .StatusIncorrectAccountSequence :
96+ return "Incorrect Account Sequence"
97+ case coreda .StatusContextCanceled :
98+ return "Context Canceled"
99+ case coreda .StatusHeightFromFuture :
100+ return "Height From Future"
101+ default :
102+ return "Unknown"
103+ }
104+ }
105+
106+ // handleDASubmissions returns JSON list of recent DA submissions
107+ func (s * DAVisualizationServer ) handleDASubmissions (w http.ResponseWriter , r * http.Request ) {
108+ s .mutex .RLock ()
109+ defer s .mutex .RUnlock ()
110+
111+ w .Header ().Set ("Content-Type" , "application/json" )
112+ if err := json .NewEncoder (w ).Encode (map [string ]interface {}{
113+ "submissions" : s .submissions ,
114+ "total" : len (s .submissions ),
115+ }); err != nil {
116+ s .logger .Error ().Err (err ).Msg ("Failed to encode DA submissions response" )
117+ http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
118+ return
119+ }
120+ }
121+
122+ // handleDABlobDetails returns details about a specific blob
123+ func (s * DAVisualizationServer ) handleDABlobDetails (w http.ResponseWriter , r * http.Request ) {
124+ blobID := r .URL .Query ().Get ("id" )
125+ if blobID == "" {
126+ http .Error (w , "Missing blob ID parameter" , http .StatusBadRequest )
127+ return
128+ }
129+
130+ // Decode the hex blob ID
131+ id , err := hex .DecodeString (blobID )
132+ if err != nil {
133+ http .Error (w , "Invalid blob ID format" , http .StatusBadRequest )
134+ return
135+ }
136+
137+ // Try to retrieve blob from DA layer
138+ ctx , cancel := context .WithTimeout (r .Context (), 10 * time .Second )
139+ defer cancel ()
140+
141+ // Extract namespace - using empty namespace for now, could be parameterized
142+ namespace := []byte {}
143+ blobs , err := s .da .Get (ctx , []coreda.ID {id }, namespace )
144+ if err != nil {
145+ s .logger .Error ().Err (err ).Str ("blob_id" , blobID ).Msg ("Failed to retrieve blob from DA" )
146+ http .Error (w , fmt .Sprintf ("Failed to retrieve blob: %v" , err ), http .StatusInternalServerError )
147+ return
148+ }
149+
150+ if len (blobs ) == 0 {
151+ http .Error (w , "Blob not found" , http .StatusNotFound )
152+ return
153+ }
154+
155+ // Parse the blob ID to extract height and commitment
156+ height , commitment , err := coreda .SplitID (id )
157+ if err != nil {
158+ s .logger .Error ().Err (err ).Str ("blob_id" , blobID ).Msg ("Failed to split blob ID" )
159+ }
160+
161+ blob := blobs [0 ]
162+ response := map [string ]interface {}{
163+ "id" : blobID ,
164+ "height" : height ,
165+ "commitment" : hex .EncodeToString (commitment ),
166+ "size" : len (blob ),
167+ "content" : hex .EncodeToString (blob ),
168+ "content_preview" : string (blob [:min (len (blob ), 200 )]), // First 200 bytes as string
169+ }
170+
171+ w .Header ().Set ("Content-Type" , "application/json" )
172+ if err := json .NewEncoder (w ).Encode (response ); err != nil {
173+ s .logger .Error ().Err (err ).Msg ("Failed to encode blob details response" )
174+ http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
175+ }
176+ }
177+
178+ // handleDAVisualizationHTML returns HTML visualization page
179+ func (s * DAVisualizationServer ) handleDAVisualizationHTML (w http.ResponseWriter , r * http.Request ) {
180+ s .mutex .RLock ()
181+ submissions := make ([]DASubmissionInfo , len (s .submissions ))
182+ copy (submissions , s .submissions )
183+ s .mutex .RUnlock ()
184+
185+ // Reverse the slice to show newest first
186+ for i , j := 0 , len (submissions )- 1 ; i < j ; i , j = i + 1 , j - 1 {
187+ submissions [i ], submissions [j ] = submissions [j ], submissions [i ]
188+ }
189+
190+ tmpl := `
191+ <!DOCTYPE html>
192+ <html>
193+ <head>
194+ <title>DA Layer Visualization</title>
195+ <style>
196+ body { font-family: Arial, sans-serif; margin: 20px; }
197+ .header { background-color: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 20px; }
198+ .submission { border: 1px solid #ddd; margin: 10px 0; padding: 15px; border-radius: 5px; }
199+ .success { border-left: 4px solid #4CAF50; }
200+ .error { border-left: 4px solid #f44336; }
201+ .pending { border-left: 4px solid #ff9800; }
202+ .blob-ids { margin-top: 10px; }
203+ .blob-id { background-color: #f0f0f0; padding: 2px 6px; margin: 2px; border-radius: 3px; font-family: monospace; font-size: 12px; }
204+ .meta { color: #666; font-size: 14px; }
205+ table { border-collapse: collapse; width: 100%; }
206+ th, td { border: 1px solid #ddd; padding: 8px; text-align: left; }
207+ th { background-color: #f2f2f2; }
208+ .blob-link { color: #007cba; text-decoration: none; }
209+ .blob-link:hover { text-decoration: underline; }
210+ </style>
211+ </head>
212+ <body>
213+ <div class="header">
214+ <h1>DA Layer Visualization</h1>
215+ <p>Real-time view of blob submissions from the sequencer node to the Data Availability layer.</p>
216+ <p><strong>Total Submissions:</strong> {{len .}} | <strong>Last Update:</strong> {{.LastUpdate}}</p>
217+ </div>
218+
219+ <h2>Recent Submissions</h2>
220+ {{if .}}
221+ <table>
222+ <tr>
223+ <th>Timestamp</th>
224+ <th>Height</th>
225+ <th>Status</th>
226+ <th>Blobs</th>
227+ <th>Size (bytes)</th>
228+ <th>Gas Price</th>
229+ <th>Message</th>
230+ </tr>
231+ {{range .}}
232+ <tr class="{{if eq .StatusCode "Success"}}success{{else if eq .StatusCode "Error"}}error{{else}}pending{{end}}">
233+ <td>{{.Timestamp.Format "15:04:05"}}</td>
234+ <td>{{.Height}}</td>
235+ <td>{{.StatusCode}}</td>
236+ <td>
237+ {{.NumBlobs}}
238+ {{if .BlobIDs}}
239+ <div class="blob-ids">
240+ {{range .BlobIDs}}
241+ <a href="/da/blob?id={{.}}" class="blob-link blob-id">{{slice . 0 8}}...</a>
242+ {{end}}
243+ </div>
244+ {{end}}
245+ </td>
246+ <td>{{.BlobSize}}</td>
247+ <td>{{printf "%.6f" .GasPrice}}</td>
248+ <td>{{.Message}}</td>
249+ </tr>
250+ {{end}}
251+ </table>
252+ {{else}}
253+ <p>No submissions recorded yet.</p>
254+ {{end}}
255+
256+ <div style="margin-top: 30px; padding-top: 20px; border-top: 1px solid #ddd; color: #666;">
257+ <p><strong>API Endpoints:</strong></p>
258+ <ul>
259+ <li><code>GET /da/submissions</code> - JSON list of submissions</li>
260+ <li><code>GET /da/blob?id=<blob_id></code> - Detailed blob information</li>
261+ </ul>
262+ <p><em>Auto-refresh: <span id="countdown">30</span>s</em></p>
263+ </div>
264+
265+ <script>
266+ // Auto-refresh page every 30 seconds
267+ let countdown = 30;
268+ const countdownEl = document.getElementById('countdown');
269+ setInterval(() => {
270+ countdown--;
271+ countdownEl.textContent = countdown;
272+ if (countdown <= 0) {
273+ location.reload();
274+ }
275+ }, 1000);
276+ </script>
277+ </body>
278+ </html>
279+ `
280+
281+ t , err := template .New ("da" ).Funcs (template.FuncMap {
282+ "slice" : func (s string , start , end int ) string {
283+ if end > len (s ) {
284+ end = len (s )
285+ }
286+ return s [start :end ]
287+ },
288+ "len" : func (s []DASubmissionInfo ) int {
289+ return len (s )
290+ },
291+ }).Parse (tmpl )
292+
293+ if err != nil {
294+ s .logger .Error ().Err (err ).Msg ("Failed to parse template" )
295+ http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
296+ return
297+ }
298+
299+ // Create template data with LastUpdate
300+ data := struct {
301+ Submissions []DASubmissionInfo
302+ LastUpdate string
303+ }{
304+ Submissions : submissions ,
305+ LastUpdate : time .Now ().Format ("15:04:05" ),
306+ }
307+
308+ w .Header ().Set ("Content-Type" , "text/html" )
309+ if err := t .Execute (w , data .Submissions ); err != nil {
310+ s .logger .Error ().Err (err ).Msg ("Failed to execute template" )
311+ http .Error (w , "Internal Server Error" , http .StatusInternalServerError )
312+ }
313+ }
314+
315+ // Global DA visualization server instance
316+ var daVisualizationServer * DAVisualizationServer
317+ var daVisualizationMutex sync.Mutex
318+
319+ // SetDAVisualizationServer sets the global DA visualization server instance
320+ func SetDAVisualizationServer (server * DAVisualizationServer ) {
321+ daVisualizationMutex .Lock ()
322+ defer daVisualizationMutex .Unlock ()
323+ daVisualizationServer = server
324+ }
325+
326+ // GetDAVisualizationServer returns the global DA visualization server instance
327+ func GetDAVisualizationServer () * DAVisualizationServer {
328+ daVisualizationMutex .Lock ()
329+ defer daVisualizationMutex .Unlock ()
330+ return daVisualizationServer
331+ }
0 commit comments