@@ -12,10 +12,17 @@ import (
1212 "time"
1313)
1414
15+ // RealmConfig holds the configurable realm paths
16+ type RealmConfig struct {
17+ UserRealmPath string
18+ RoleRealmPath string
19+ }
20+
1521// QueryClient handles regular GraphQL queries (not subscriptions)
1622type QueryClient struct {
17- url string
18- httpClient * http.Client
23+ url string
24+ httpClient * http.Client
25+ realmConfig RealmConfig
1926}
2027
2128// GraphQLResponse represents a GraphQL response
@@ -35,7 +42,7 @@ type GraphQLError struct {
3542}
3643
3744// NewQueryClient creates a new GraphQL query client
38- func NewQueryClient (url string ) * QueryClient {
45+ func NewQueryClient (url string , realmConfig RealmConfig ) * QueryClient {
3946 // Convert WebSocket URL to HTTP URL if needed
4047 if strings .HasPrefix (url , "wss://" ) {
4148 url = "https://" + url [6 :]
@@ -54,72 +61,153 @@ func NewQueryClient(url string) *QueryClient {
5461 }
5562
5663 return & QueryClient {
57- url : url ,
64+ url : url ,
65+ realmConfig : realmConfig ,
5866 httpClient : & http.Client {
5967 Timeout : 30 * time .Second ,
6068 Transport : transport ,
6169 },
6270 }
6371}
6472
65-
66- // QueryUserEvents queries for all user events (UserLinked and UserUnlinked) in chronological order
67- func ( qc * QueryClient ) QueryUserEvents ( ctx context. Context , afterBlockHeight int64 , afterTxIndex int64 ) ([] Transaction , error ) {
68- // Build the where clause based on position
69- var whereClause string
73+ // buildWhereClause creates the where clause for pagination with proper bounds
74+ func ( qc * QueryClient ) buildWhereClause ( afterBlockHeight int64 , afterTxIndex int64 ) string {
75+ // Use a reasonable upper bound to prevent unbounded queries
76+ upperBound := afterBlockHeight + 1000000
77+
7078 if afterTxIndex > 0 {
7179 // We're resuming from within a block
72- whereClause = fmt .Sprintf (`_or: [{ block_height: { eq: %d } index: { gt: %d } }, { block_height: { gt: %d } }]` , afterBlockHeight , afterTxIndex , afterBlockHeight )
73- } else {
74- // We're starting from the next block
75- whereClause = fmt .Sprintf (`block_height: { gt: %d }` , afterBlockHeight )
80+ return fmt .Sprintf (`_or: [
81+ {
82+ block_height: {
83+ gt: %d
84+ lt: %d
85+ }
86+ },
87+ {
88+ block_height: {
89+ eq: %d
90+ }
91+ index: { gt: %d }
92+ }
93+ ]` , afterBlockHeight , upperBound , afterBlockHeight , afterTxIndex )
7694 }
95+ // We're starting from the next block
96+ return fmt .Sprintf (`_or: [
97+ {
98+ block_height: {
99+ gt: %d
100+ lt: %d
101+ }
102+ },
103+ {
104+ block_height: {
105+ eq: %d
106+ }
107+ index: { gt: 0 }
108+ }
109+ ]` , afterBlockHeight , upperBound , afterBlockHeight )
110+ }
111+
112+ // QueryUserEvents queries for all user events (UserLinked and UserUnlinked) in chronological order
113+ func (qc * QueryClient ) QueryUserEvents (ctx context.Context , afterBlockHeight int64 , afterTxIndex int64 ) ([]Transaction , error ) {
114+ whereClause := qc .buildWhereClause (afterBlockHeight , afterTxIndex )
77115
78116 queryString := fmt .Sprintf (`{
79- "query": "query UserEvents { getTransactions(where: { success: { eq: true } %s response: { events: { GnoEvent: { pkg_path: { eq: \"gno.land/r/linker000/discord/user/v0\" } } } } } order: { heightAndIndex: ASC }) { hash index block_height messages { value { ... on MsgCall { func } } } response { events { ... on GnoEvent { type pkg_path attrs { key value } } } } } }"
80- }` , whereClause )
117+ "query": "query UserEvents {
118+ getTransactions(
119+ where: {
120+ success: { eq: true }
121+ %s
122+ response: {
123+ events: {
124+ GnoEvent: {
125+ pkg_path: { eq: \"%s\" }
126+ }
127+ }
128+ }
129+ }
130+ order: { heightAndIndex: ASC }
131+ ) {
132+ hash
133+ index
134+ block_height
135+ messages {
136+ value {
137+ ... on MsgCall {
138+ func
139+ }
140+ }
141+ }
142+ response {
143+ events {
144+ ... on GnoEvent {
145+ type
146+ pkg_path
147+ attrs {
148+ key
149+ value
150+ }
151+ }
152+ }
153+ }
154+ }
155+ }"
156+ }` , whereClause , qc .realmConfig .UserRealmPath )
81157
82158 return qc .executeQuery (ctx , queryString )
83159}
84160
85161
86162// QueryRoleEvents queries for all role events (RoleLinked and RoleUnlinked) in chronological order
87163func (qc * QueryClient ) QueryRoleEvents (ctx context.Context , afterBlockHeight int64 , afterTxIndex int64 ) ([]Transaction , error ) {
88- // Build the where clause based on position
89- var whereClause string
90- if afterTxIndex > 0 {
91- // We're resuming from within a block
92- whereClause = fmt .Sprintf (`_or: [{ block_height: { eq: %d } index: { gt: %d } }, { block_height: { gt: %d } }]` , afterBlockHeight , afterTxIndex , afterBlockHeight )
93- } else {
94- // We're starting from the next block
95- whereClause = fmt .Sprintf (`block_height: { gt: %d }` , afterBlockHeight )
96- }
164+ whereClause := qc .buildWhereClause (afterBlockHeight , afterTxIndex )
97165
98166 queryString := fmt .Sprintf (`{
99- "query": "query RoleEvents { getTransactions(where: { success: { eq: true } %s response: { events: { GnoEvent: { pkg_path: { eq: \"gno.land/r/linker000/discord/role/v0\" } } } } } order: { heightAndIndex: ASC }) { hash index block_height messages { value { ... on MsgCall { func } } } response { events { ... on GnoEvent { type pkg_path attrs { key value } } } } } }"
100- }` , whereClause )
167+ "query": "query RoleEvents {
168+ getTransactions(
169+ where: {
170+ success: { eq: true }
171+ %s
172+ response: {
173+ events: {
174+ GnoEvent: {
175+ pkg_path: { eq: \"%s\" }
176+ }
177+ }
178+ }
179+ }
180+ order: { heightAndIndex: ASC }
181+ ) {
182+ hash
183+ index
184+ block_height
185+ messages {
186+ value {
187+ ... on MsgCall {
188+ func
189+ }
190+ }
191+ }
192+ response {
193+ events {
194+ ... on GnoEvent {
195+ type
196+ pkg_path
197+ attrs {
198+ key
199+ value
200+ }
201+ }
202+ }
203+ }
204+ }
205+ }"
206+ }` , whereClause , qc .realmConfig .RoleRealmPath )
101207
102208 return qc .executeQuery (ctx , queryString )
103209}
104210
105- // QueryAllEvents queries for all events after a specific block height (for debugging)
106- func (qc * QueryClient ) QueryAllEvents (ctx context.Context , afterBlockHeight int64 , afterTxIndex int64 ) ([]Transaction , error ) {
107- // Build the where clause based on position
108- var whereClause string
109- if afterTxIndex > 0 {
110- // We're resuming from within a block
111- whereClause = fmt .Sprintf (`_or: [{ block_height: { eq: %d } index: { gt: %d } }, { block_height: { gt: %d } }]` , afterBlockHeight , afterTxIndex , afterBlockHeight )
112- } else {
113- // We're starting from the next block
114- whereClause = fmt .Sprintf (`block_height: { gt: %d }` , afterBlockHeight )
115- }
116-
117- queryString := fmt .Sprintf (`{
118- "query": "query AllEvents { getTransactions(where: { success: { eq: true } %s response: { events: { GnoEvent: { pkg_path: { eq: \"gno.land/r/linker000/discord/user/v0\" } } } } } order: { heightAndIndex: ASC }) { hash index block_height messages { value { ... on MsgCall { func } } } response { events { ... on GnoEvent { type pkg_path attrs { key value } } } } } }"
119- }` , whereClause )
120-
121- return qc .executeQuery (ctx , queryString )
122- }
123211
124212// executeQuery executes a GraphQL query and returns parsed transactions
125213func (qc * QueryClient ) executeQuery (ctx context.Context , queryString string ) ([]Transaction , error ) {
@@ -221,7 +309,9 @@ func (qc *QueryClient) QueryLatestBlockHeight(ctx context.Context) (int64, error
221309// queryLatestBlockHeightWithRetry queries the latest block height with retry logic
222310func (qc * QueryClient ) queryLatestBlockHeightWithRetry (ctx context.Context , maxRetries int ) (int64 , error ) {
223311 queryString := `{
224- "query": "query LatestBlock { latestBlockHeight }"
312+ "query": "query LatestBlock {
313+ latestBlockHeight
314+ }"
225315 }`
226316
227317 var lastErr error
0 commit comments