Skip to content

Commit 1f48397

Browse files
authored
feat: add API for per-account blob feed (#1449)
1 parent 1fd8fde commit 1f48397

File tree

6 files changed

+542
-2
lines changed

6 files changed

+542
-2
lines changed

disperser/dataapi/docs/v2/V2_docs.go

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,76 @@ const docTemplateV2 = `{
1515
"host": "{{.Host}}",
1616
"basePath": "{{.BasePath}}",
1717
"paths": {
18+
"/accounts/{account_id}/blobs": {
19+
"get": {
20+
"produces": [
21+
"application/json"
22+
],
23+
"tags": [
24+
"Accounts"
25+
],
26+
"summary": "Fetch blobs posted by an account in a time window by specific direction",
27+
"parameters": [
28+
{
29+
"type": "string",
30+
"description": "The account ID to fetch blob feed for",
31+
"name": "account_id",
32+
"in": "path",
33+
"required": true
34+
},
35+
{
36+
"type": "string",
37+
"description": "Direction to fetch: 'forward' (oldest to newest, ASC order) or 'backward' (newest to oldest, DESC order) [default: forward]",
38+
"name": "direction",
39+
"in": "query"
40+
},
41+
{
42+
"type": "string",
43+
"description": "Fetch blobs before this time, exclusive (ISO 8601 format, example: 2006-01-02T15:04:05Z) [default: now]",
44+
"name": "before",
45+
"in": "query"
46+
},
47+
{
48+
"type": "string",
49+
"description": "Fetch blobs after this time, exclusive (ISO 8601 format, example: 2006-01-02T15:04:05Z); must be smaller than ` + "`" + `before` + "`" + ` [default: ` + "`" + `before` + "`" + `-1h]",
50+
"name": "after",
51+
"in": "query"
52+
},
53+
{
54+
"type": "integer",
55+
"description": "Maximum number of blobs to return; if limit \u003c= 0 or \u003e1000, it's treated as 1000 [default: 20; max: 1000]",
56+
"name": "limit",
57+
"in": "query"
58+
}
59+
],
60+
"responses": {
61+
"200": {
62+
"description": "OK",
63+
"schema": {
64+
"$ref": "#/definitions/v2.AccountBlobFeedResponse"
65+
}
66+
},
67+
"400": {
68+
"description": "error: Bad request",
69+
"schema": {
70+
"$ref": "#/definitions/v2.ErrorResponse"
71+
}
72+
},
73+
"404": {
74+
"description": "error: Not found",
75+
"schema": {
76+
"$ref": "#/definitions/v2.ErrorResponse"
77+
}
78+
},
79+
"500": {
80+
"description": "error: Server error",
81+
"schema": {
82+
"$ref": "#/definitions/v2.ErrorResponse"
83+
}
84+
}
85+
}
86+
}
87+
},
1888
"/batches/feed": {
1989
"get": {
2090
"produces": [
@@ -1062,6 +1132,20 @@ const docTemplateV2 = `{
10621132
}
10631133
}
10641134
},
1135+
"v2.AccountBlobFeedResponse": {
1136+
"type": "object",
1137+
"properties": {
1138+
"account_id": {
1139+
"type": "string"
1140+
},
1141+
"blobs": {
1142+
"type": "array",
1143+
"items": {
1144+
"$ref": "#/definitions/v2.BlobInfo"
1145+
}
1146+
}
1147+
}
1148+
},
10651149
"v2.AttestationInfo": {
10661150
"type": "object",
10671151
"properties": {

disperser/dataapi/docs/v2/V2_swagger.json

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,76 @@
1212
},
1313
"basePath": "/api/v2",
1414
"paths": {
15+
"/accounts/{account_id}/blobs": {
16+
"get": {
17+
"produces": [
18+
"application/json"
19+
],
20+
"tags": [
21+
"Accounts"
22+
],
23+
"summary": "Fetch blobs posted by an account in a time window by specific direction",
24+
"parameters": [
25+
{
26+
"type": "string",
27+
"description": "The account ID to fetch blob feed for",
28+
"name": "account_id",
29+
"in": "path",
30+
"required": true
31+
},
32+
{
33+
"type": "string",
34+
"description": "Direction to fetch: 'forward' (oldest to newest, ASC order) or 'backward' (newest to oldest, DESC order) [default: forward]",
35+
"name": "direction",
36+
"in": "query"
37+
},
38+
{
39+
"type": "string",
40+
"description": "Fetch blobs before this time, exclusive (ISO 8601 format, example: 2006-01-02T15:04:05Z) [default: now]",
41+
"name": "before",
42+
"in": "query"
43+
},
44+
{
45+
"type": "string",
46+
"description": "Fetch blobs after this time, exclusive (ISO 8601 format, example: 2006-01-02T15:04:05Z); must be smaller than `before` [default: `before`-1h]",
47+
"name": "after",
48+
"in": "query"
49+
},
50+
{
51+
"type": "integer",
52+
"description": "Maximum number of blobs to return; if limit \u003c= 0 or \u003e1000, it's treated as 1000 [default: 20; max: 1000]",
53+
"name": "limit",
54+
"in": "query"
55+
}
56+
],
57+
"responses": {
58+
"200": {
59+
"description": "OK",
60+
"schema": {
61+
"$ref": "#/definitions/v2.AccountBlobFeedResponse"
62+
}
63+
},
64+
"400": {
65+
"description": "error: Bad request",
66+
"schema": {
67+
"$ref": "#/definitions/v2.ErrorResponse"
68+
}
69+
},
70+
"404": {
71+
"description": "error: Not found",
72+
"schema": {
73+
"$ref": "#/definitions/v2.ErrorResponse"
74+
}
75+
},
76+
"500": {
77+
"description": "error: Server error",
78+
"schema": {
79+
"$ref": "#/definitions/v2.ErrorResponse"
80+
}
81+
}
82+
}
83+
}
84+
},
1585
"/batches/feed": {
1686
"get": {
1787
"produces": [
@@ -1059,6 +1129,20 @@
10591129
}
10601130
}
10611131
},
1132+
"v2.AccountBlobFeedResponse": {
1133+
"type": "object",
1134+
"properties": {
1135+
"account_id": {
1136+
"type": "string"
1137+
},
1138+
"blobs": {
1139+
"type": "array",
1140+
"items": {
1141+
"$ref": "#/definitions/v2.BlobInfo"
1142+
}
1143+
}
1144+
}
1145+
},
10621146
"v2.AttestationInfo": {
10631147
"type": "object",
10641148
"properties": {

disperser/dataapi/docs/v2/V2_swagger.yaml

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,15 @@ definitions:
227227
type: number
228228
type: object
229229
type: object
230+
v2.AccountBlobFeedResponse:
231+
properties:
232+
account_id:
233+
type: string
234+
blobs:
235+
items:
236+
$ref: '#/definitions/v2.BlobInfo'
237+
type: array
238+
type: object
230239
v2.AttestationInfo:
231240
properties:
232241
attestation:
@@ -542,6 +551,56 @@ info:
542551
title: EigenDA Data Access API V2
543552
version: "2.0"
544553
paths:
554+
/accounts/{account_id}/blobs:
555+
get:
556+
parameters:
557+
- description: The account ID to fetch blob feed for
558+
in: path
559+
name: account_id
560+
required: true
561+
type: string
562+
- description: 'Direction to fetch: ''forward'' (oldest to newest, ASC order)
563+
or ''backward'' (newest to oldest, DESC order) [default: forward]'
564+
in: query
565+
name: direction
566+
type: string
567+
- description: 'Fetch blobs before this time, exclusive (ISO 8601 format, example:
568+
2006-01-02T15:04:05Z) [default: now]'
569+
in: query
570+
name: before
571+
type: string
572+
- description: 'Fetch blobs after this time, exclusive (ISO 8601 format, example:
573+
2006-01-02T15:04:05Z); must be smaller than `before` [default: `before`-1h]'
574+
in: query
575+
name: after
576+
type: string
577+
- description: 'Maximum number of blobs to return; if limit <= 0 or >1000, it''s
578+
treated as 1000 [default: 20; max: 1000]'
579+
in: query
580+
name: limit
581+
type: integer
582+
produces:
583+
- application/json
584+
responses:
585+
"200":
586+
description: OK
587+
schema:
588+
$ref: '#/definitions/v2.AccountBlobFeedResponse'
589+
"400":
590+
description: 'error: Bad request'
591+
schema:
592+
$ref: '#/definitions/v2.ErrorResponse'
593+
"404":
594+
description: 'error: Not found'
595+
schema:
596+
$ref: '#/definitions/v2.ErrorResponse'
597+
"500":
598+
description: 'error: Server error'
599+
schema:
600+
$ref: '#/definitions/v2.ErrorResponse'
601+
summary: Fetch blobs posted by an account in a time window by specific direction
602+
tags:
603+
- Accounts
545604
/batches/{batch_header_hash}:
546605
get:
547606
parameters:

disperser/dataapi/v2/accounts.go

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
package v2
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"net/http"
7+
"time"
8+
9+
v2 "github.com/Layr-Labs/eigenda/disperser/common/v2"
10+
gethcommon "github.com/ethereum/go-ethereum/common"
11+
"github.com/gin-gonic/gin"
12+
)
13+
14+
// FetchAccountBlobFeed godoc
15+
//
16+
// @Summary Fetch blobs posted by an account in a time window by specific direction
17+
// @Tags Accounts
18+
// @Produce json
19+
// @Param account_id path string true "The account ID to fetch blob feed for"
20+
// @Param direction query string false "Direction to fetch: 'forward' (oldest to newest, ASC order) or 'backward' (newest to oldest, DESC order) [default: forward]"
21+
// @Param before query string false "Fetch blobs before this time, exclusive (ISO 8601 format, example: 2006-01-02T15:04:05Z) [default: now]"
22+
// @Param after query string false "Fetch blobs after this time, exclusive (ISO 8601 format, example: 2006-01-02T15:04:05Z); must be smaller than `before` [default: `before`-1h]"
23+
// @Param limit query int false "Maximum number of blobs to return; if limit <= 0 or >1000, it's treated as 1000 [default: 20; max: 1000]"
24+
// @Success 200 {object} AccountBlobFeedResponse
25+
// @Failure 400 {object} ErrorResponse "error: Bad request"
26+
// @Failure 404 {object} ErrorResponse "error: Not found"
27+
// @Failure 500 {object} ErrorResponse "error: Server error"
28+
// @Router /accounts/{account_id}/blobs [get]
29+
func (s *ServerV2) FetchAccountBlobFeed(c *gin.Context) {
30+
handlerStart := time.Now()
31+
var err error
32+
33+
// Parse account ID
34+
accountStr := c.Param("account_id")
35+
if !gethcommon.IsHexAddress(accountStr) {
36+
s.metrics.IncrementInvalidArgRequestNum("FetchAccountBlobFeed")
37+
invalidParamsErrorResponse(c, errors.New("account id is not valid hex"))
38+
return
39+
}
40+
accountId := gethcommon.HexToAddress(accountStr)
41+
if accountId == (gethcommon.Address{}) {
42+
s.metrics.IncrementInvalidArgRequestNum("FetchAccountBlobFeed")
43+
invalidParamsErrorResponse(c, errors.New("zero account id is not valid"))
44+
return
45+
}
46+
47+
// Parse the feed params
48+
params, err := ParseFeedParams(c, s.metrics, "FetchAccountBlobFeed")
49+
if err != nil {
50+
s.metrics.IncrementInvalidArgRequestNum("FetchAccountBlobFeed")
51+
invalidParamsErrorResponse(c, err)
52+
return
53+
}
54+
55+
var blobs []*v2.BlobMetadata
56+
57+
if params.direction == "forward" {
58+
blobs, err = s.blobMetadataStore.GetBlobMetadataByAccountID(
59+
c.Request.Context(),
60+
accountId,
61+
uint64(params.afterTime.UnixNano()),
62+
uint64(params.beforeTime.UnixNano()),
63+
params.limit,
64+
true, // ascending=true
65+
)
66+
} else {
67+
blobs, err = s.blobMetadataStore.GetBlobMetadataByAccountID(
68+
c.Request.Context(),
69+
accountId,
70+
uint64(params.afterTime.UnixNano()),
71+
uint64(params.beforeTime.UnixNano()),
72+
params.limit,
73+
false, // ascending=false
74+
)
75+
}
76+
77+
if err != nil {
78+
s.metrics.IncrementFailedRequestNum("FetchAccountBlobFeed")
79+
errorResponse(c, fmt.Errorf("failed to fetch blobs from blob metadata store for account (%s): %w", accountId.Hex(), err))
80+
return
81+
}
82+
83+
blobInfo := make([]BlobInfo, len(blobs))
84+
for i := 0; i < len(blobs); i++ {
85+
bk, err := blobs[i].BlobHeader.BlobKey()
86+
if err != nil {
87+
s.metrics.IncrementFailedRequestNum("FetchAccountBlobFeed")
88+
errorResponse(c, fmt.Errorf("blob metadata is malformed and failed to serialize blob key: %w", err))
89+
return
90+
}
91+
blobInfo[i].BlobKey = bk.Hex()
92+
blobInfo[i].BlobMetadata = blobs[i]
93+
}
94+
95+
response := &AccountBlobFeedResponse{
96+
AccountId: accountId.Hex(),
97+
Blobs: blobInfo,
98+
}
99+
100+
s.metrics.IncrementSuccessfulRequestNum("FetchAccountBlobFeed")
101+
s.metrics.ObserveLatency("FetchAccountBlobFeed", time.Since(handlerStart))
102+
c.JSON(http.StatusOK, response)
103+
}

disperser/dataapi/v2/server_v2.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,11 @@ type (
190190
RetrievalStatus string `json:"retrieval_status"`
191191
}
192192

193+
AccountBlobFeedResponse struct {
194+
AccountId string `json:"account_id"`
195+
Blobs []BlobInfo `json:"blobs"`
196+
}
197+
193198
SemverReportResponse struct {
194199
Semver map[string]*semver.SemverMetrics `json:"semver"`
195200
}

0 commit comments

Comments
 (0)