Skip to content

Commit e2b2001

Browse files
fix: retry lighter get tx (#116)
<!--- Provide a general summary of your changes in the Title above --> ## Description <!--- Describe your changes in detail --> ## Related Issue Or Context <!--- If suggesting a new feature or change, please discuss it in an issue first --> <!--- If fixing a bug, there should be an issue describing it with steps to reproduce --> <!--- Otherwise, describe context and motivation for change herre --> Closes: #<issue> ## How Has This Been Tested? Testing details. <!--- Please describe in detail how you tested your changes. --> <!--- Include details of your testing environment, and the tests you ran to --> <!--- see how your change affects other areas of the code, etc. --> ## Types of changes <!--- What types of changes does your code introduce? Put an `x` in all the boxes that apply: --> - [ ] Bug fix (non-breaking change which fixes an issue) - [ ] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to change) - [ ] Documentation ## Checklist: <!--- Go over all the following points, and put an `x` in all the boxes that apply. --> <!--- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have ensured that all acceptance criteria (or expected behavior) from issue are met - [ ] I have updated the documentation locally and in docs. - [ ] I have added tests to cover my changes. - [ ] I have ensured that all the checks are passing and green, I've signed the CLA bot --------- Co-authored-by: GregTheGreek <[email protected]>
1 parent ea49377 commit e2b2001

File tree

3 files changed

+169
-13
lines changed

3 files changed

+169
-13
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ require (
1111
github.com/ethereum/go-ethereum v1.16.2
1212
github.com/golang/mock v1.6.0
1313
github.com/gorilla/mux v1.8.1
14+
github.com/hashicorp/go-retryablehttp v0.7.8
1415
github.com/imdario/mergo v0.3.12
1516
github.com/jellydator/ttlcache/v3 v3.3.0
1617
github.com/libp2p/go-libp2p v0.38.2
@@ -71,7 +72,6 @@ require (
7172
github.com/google/uuid v1.6.0 // indirect
7273
github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect
7374
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
74-
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
7575
github.com/holiman/bloomfilter/v2 v2.0.3 // indirect
7676
github.com/holiman/uint256 v1.3.2 // indirect
7777
github.com/mattn/go-runewidth v0.0.13 // indirect

protocol/lighter/api.go

Lines changed: 64 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,24 @@
11
package lighter
22

33
import (
4+
"bytes"
5+
"context"
46
"encoding/json"
57
"fmt"
68
"io"
79
"net/http"
810
"time"
11+
12+
"github.com/hashicorp/go-retryablehttp"
13+
"github.com/rs/zerolog/log"
914
)
1015

1116
const (
12-
LIGHTER_URL = "https://mainnet.zklighter.elliot.ai/api"
17+
LIGHTER_URL = "https://mainnet.zklighter.elliot.ai/api"
18+
TX_NOT_FOUND_RETRIES = 3
19+
TX_NOT_FOUND_RETRY_WAIT = 500 * time.Millisecond
20+
TX_NOT_FOUND_ERROR_CODE = 21500
21+
TX_FOUND_STATUS_CODE = 200
1322
)
1423

1524
type TxType uint64
@@ -41,8 +50,8 @@ type LighterError struct {
4150
Message string `json:"message"`
4251
}
4352

44-
func (e *LighterError) Error() error {
45-
return fmt.Errorf("lighter error: code %d, message: %s", e.Code, e.Message)
53+
func (e *LighterError) Error() string {
54+
return fmt.Sprintf("lighter error: code %d, message: %s", e.Code, e.Message)
4655
}
4756

4857
func (tx *LighterTx) UnmarshalJSON(data []byte) error {
@@ -54,9 +63,11 @@ func (tx *LighterTx) UnmarshalJSON(data []byte) error {
5463
if tx.Type == TxTypeL2Transfer {
5564
var t *Transfer
5665
if err := json.Unmarshal([]byte(tx.Info), &t); err != nil {
57-
return err
66+
return fmt.Errorf("failed to unmarshal info: %w", err)
5867
}
5968
tx.Transfer = t
69+
} else {
70+
return fmt.Errorf("unsupported transaction type: %d", tx.Type)
6071
}
6172

6273
return nil
@@ -67,13 +78,58 @@ type LighterAPI struct {
6778
}
6879

6980
func NewLighterAPI() *LighterAPI {
81+
retryClient := retryablehttp.NewClient()
82+
retryClient.RetryMax = TX_NOT_FOUND_RETRIES - 1 // RetryMax is a number of retries after an initial attempt
83+
retryClient.RetryWaitMin = TX_NOT_FOUND_RETRY_WAIT
84+
retryClient.RetryWaitMax = TX_NOT_FOUND_RETRY_WAIT
85+
retryClient.CheckRetry = LighterCheckRetry
86+
retryClient.Logger = log.Logger
87+
7088
return &LighterAPI{
71-
HTTPClient: &http.Client{
72-
Timeout: 10 * time.Second,
73-
},
89+
HTTPClient: retryClient.StandardClient(),
7490
}
7591
}
7692

93+
// LighterCheckRetry checks if we should retry the request.
94+
// Retries when: error code is TX_NOT_FOUND_ERROR_CODE (21500) or response has code 200 but missing/empty info.
95+
func LighterCheckRetry(ctx context.Context, resp *http.Response, err error) (bool, error) {
96+
if ctx.Err() != nil {
97+
return false, ctx.Err()
98+
}
99+
100+
if err != nil {
101+
return false, err
102+
}
103+
104+
if resp.StatusCode == http.StatusOK {
105+
body, err := io.ReadAll(resp.Body)
106+
if err != nil {
107+
return false, err
108+
}
109+
resp.Body.Close()
110+
resp.Body = io.NopCloser(bytes.NewReader(body))
111+
112+
// First try to unmarshal as LighterError
113+
e := new(LighterError)
114+
if err := json.Unmarshal(body, e); err == nil && e.Code == TX_NOT_FOUND_ERROR_CODE {
115+
return true, nil
116+
}
117+
118+
// Check if it's a LighterTx with code 200 but missing info
119+
var raw map[string]interface{}
120+
if err := json.Unmarshal(body, &raw); err == nil {
121+
if code, ok := raw["code"].(float64); ok && code == TX_FOUND_STATUS_CODE {
122+
info, hasInfo := raw["info"].(string)
123+
if !hasInfo || info == "" {
124+
return true, nil
125+
}
126+
}
127+
}
128+
}
129+
130+
return false, nil
131+
}
132+
77133
// GetTx fetches transaction from the lighter API
78134
func (a *LighterAPI) GetTx(hash string) (*LighterTx, error) {
79135
url := fmt.Sprintf("%s/v1/tx?by=hash&value=%s", LIGHTER_URL, hash)
@@ -94,11 +150,7 @@ func (a *LighterAPI) GetTx(hash string) (*LighterTx, error) {
94150

95151
s := new(LighterTx)
96152
if err := json.Unmarshal(body, s); err != nil {
97-
e := new(LighterError)
98-
if err := json.Unmarshal(body, e); err != nil {
99-
return nil, fmt.Errorf("failed to unmarshal response body: %s, with error: %w", string(body), err)
100-
}
101-
return nil, e.Error()
153+
return nil, err
102154
}
103155

104156
return s, nil

protocol/lighter/api_test.go

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package lighter_test
33

44
import (
55
"bytes"
6+
"context"
67
"encoding/json"
78
"errors"
89
"fmt"
@@ -113,3 +114,106 @@ func Test_LighterAPI_GetTx(t *testing.T) {
113114
})
114115
}
115116
}
117+
118+
func Test_LighterCheckRetry(t *testing.T) {
119+
tests := []struct {
120+
name string
121+
statusCode int
122+
responseBody string
123+
inputError error
124+
ctxCanceled bool
125+
wantRetry bool
126+
wantErr bool
127+
}{
128+
{
129+
name: "retry on TX_NOT_FOUND_ERROR_CODE",
130+
statusCode: http.StatusOK,
131+
responseBody: `{"code": 21500, "message": "transaction not found"}`,
132+
wantRetry: true,
133+
wantErr: false,
134+
},
135+
{
136+
name: "retry on code 200 with missing info",
137+
statusCode: http.StatusOK,
138+
responseBody: `{"code": 200, "hash": "0xabc", "type": 12}`,
139+
wantRetry: true,
140+
wantErr: false,
141+
},
142+
{
143+
name: "retry on code 200 with empty info",
144+
statusCode: http.StatusOK,
145+
responseBody: `{"code": 200, "hash": "0xabc", "type": 12, "info": ""}`,
146+
wantRetry: true,
147+
wantErr: false,
148+
},
149+
{
150+
name: "no retry on code 200 with valid info",
151+
statusCode: http.StatusOK,
152+
responseBody: `{"code": 200, "hash": "0xabc", "type": 12, "info": "{\"USDCAmount\": 1000}"}`,
153+
wantRetry: false,
154+
wantErr: false,
155+
},
156+
{
157+
name: "no retry on different error code",
158+
statusCode: http.StatusOK,
159+
responseBody: `{"code": 500, "message": "internal server error"}`,
160+
wantRetry: false,
161+
wantErr: false,
162+
},
163+
{
164+
name: "no retry on non-200 status",
165+
statusCode: http.StatusNotFound,
166+
responseBody: `{"error": "not found"}`,
167+
wantRetry: false,
168+
wantErr: false,
169+
},
170+
{
171+
name: "no retry on input error",
172+
inputError: errors.New("connection error"),
173+
wantRetry: false,
174+
wantErr: true,
175+
},
176+
{
177+
name: "error on context cancellation",
178+
ctxCanceled: true,
179+
wantRetry: false,
180+
wantErr: true,
181+
},
182+
}
183+
184+
for _, tc := range tests {
185+
t.Run(tc.name, func(t *testing.T) {
186+
ctx := context.Background()
187+
if tc.ctxCanceled {
188+
var cancel context.CancelFunc
189+
ctx, cancel = context.WithCancel(ctx)
190+
cancel()
191+
}
192+
193+
var resp *http.Response
194+
if tc.statusCode != 0 {
195+
resp = &http.Response{
196+
StatusCode: tc.statusCode,
197+
Body: io.NopCloser(bytes.NewReader([]byte(tc.responseBody))),
198+
Header: make(http.Header),
199+
}
200+
}
201+
202+
shouldRetry, err := lighter.LighterCheckRetry(ctx, resp, tc.inputError)
203+
204+
if tc.wantErr {
205+
if err == nil {
206+
t.Errorf("expected error, got nil")
207+
}
208+
} else {
209+
if err != nil {
210+
t.Errorf("unexpected error: %v", err)
211+
}
212+
}
213+
214+
if shouldRetry != tc.wantRetry {
215+
t.Errorf("expected retry=%v, got retry=%v", tc.wantRetry, shouldRetry)
216+
}
217+
})
218+
}
219+
}

0 commit comments

Comments
 (0)