@gearbox-protocol/sdk 10.4.4 → 10.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -29,11 +29,11 @@ class NoAvailableTransportsError extends import_viem.BaseError {
29
29
  }
30
30
  }
31
31
  class RevolverTransport {
32
- #transports;
33
- #index = 0;
34
32
  #config;
35
- #rotating = false;
36
33
  #requests = /* @__PURE__ */ new WeakMap();
34
+ #selector;
35
+ #rotating = false;
36
+ #isSingle;
37
37
  overrides;
38
38
  /**
39
39
  * Create a new RevolverTransport
@@ -57,9 +57,9 @@ class RevolverTransport {
57
57
  ...config,
58
58
  shouldRetry: config.shouldRetry ?? defaultShouldRetry
59
59
  };
60
- this.#transports = config.providers.map(
60
+ const transports = config.providers.map(
61
61
  ({ url, name, cooldown }) => ({
62
- provider: name,
62
+ name,
63
63
  transport: (0, import_viem.http)(url, {
64
64
  retryCount: config.retryCount,
65
65
  retryDelay: config.retryDelay,
@@ -73,40 +73,45 @@ class RevolverTransport {
73
73
  cooldown: cooldown ?? 0
74
74
  })
75
75
  );
76
- if (this.#transports.length === 0) {
76
+ if (transports.length === 0) {
77
77
  throw new NoAvailableTransportsError();
78
78
  }
79
+ this.#isSingle = transports.length === 1;
80
+ const selectionStrategy = config.selectionStrategy ?? "simple";
81
+ this.#selector = selectionStrategy === "simple" ? new SimpleTransportSelector(transports, config.cooldown) : new OrderedTransportSelector(transports, config.cooldown);
79
82
  }
80
83
  get value() {
81
84
  return {
82
85
  rotate: (reason) => this.rotate(reason),
83
- currentTransportName: () => this.currentTransportName(),
84
- statuses: () => this.statuses()
86
+ currentTransportName: () => this.#selector.transportName(),
87
+ statuses: () => this.#selector.statuses()
85
88
  };
86
89
  }
87
90
  request = async (r) => {
88
- if (this.#transports.length === 1) {
89
- return this.#transport({ ...this.overrides }).request(r);
91
+ if (this.#isSingle) {
92
+ return this.#selector.select()({
93
+ ...this.overrides
94
+ }).request(r);
90
95
  }
91
96
  let error;
92
97
  do {
93
98
  try {
94
- this.#requests.set(r, this.currentTransportName());
95
- const resp = await (0, import_viem.withRetry)(
96
- () => this.#transport({ ...this.overrides }).request(r),
97
- {
98
- delay: this.#config.retryDelay,
99
- retryCount: this.#config.retryCount,
100
- shouldRetry: this.#config.shouldRetry
101
- }
102
- );
99
+ this.#requests.set(r, this.#selector.transportName());
100
+ const transport = this.#selector.select()({
101
+ ...this.overrides
102
+ });
103
+ const resp = await (0, import_viem.withRetry)(() => transport.request(r), {
104
+ delay: this.#config.retryDelay,
105
+ retryCount: this.#config.retryCount,
106
+ shouldRetry: this.#config.shouldRetry
107
+ });
103
108
  this.#requests.delete(r);
104
109
  return resp;
105
110
  } catch (e) {
106
111
  error = error ?? e;
107
112
  if (e instanceof import_viem.RpcError || e instanceof import_viem.HttpRequestError) {
108
113
  const reqTransport = this.#requests.get(r);
109
- if (reqTransport === this.currentTransportName()) {
114
+ if (reqTransport === this.#selector.transportName()) {
110
115
  await this.rotate(e);
111
116
  } else {
112
117
  this.#logger?.debug(
@@ -118,7 +123,7 @@ class RevolverTransport {
118
123
  throw e;
119
124
  }
120
125
  }
121
- } while (this.#hasAvailableTransports);
126
+ } while (this.#selector.canRotate());
122
127
  this.#requests.delete(r);
123
128
  throw new NoAvailableTransportsError(error);
124
129
  };
@@ -140,82 +145,34 @@ class RevolverTransport {
140
145
  * @returns true if rotation was successful
141
146
  */
142
147
  async rotate(reason) {
143
- if (this.#transports.length === 1) {
144
- return true;
145
- }
146
148
  if (this.#rotating) {
147
149
  this.#logger?.debug("already rotating, skipping");
148
- return false;
150
+ return;
149
151
  }
150
152
  this.#rotating = true;
151
- this.#logger?.debug(
152
- {
153
- reason,
154
- current: this.currentTransportName,
155
- total: this.#transports.length
156
- },
157
- "rotating transport"
158
- );
159
- const oldTransportName = this.currentTransportName();
160
- const now = Date.now();
161
- this.#transports[this.#index].cooldown = now + (this.#config.cooldown ?? 6e4);
162
- for (let i = 0; i < this.#transports.length - 1; i++) {
163
- this.#index = (this.#index + 1) % this.#transports.length;
164
- if (this.#transports[this.#index].cooldown < now) {
165
- this.#logger?.info(
166
- {
167
- current: this.currentTransportName,
168
- total: this.#transports.length
169
- },
170
- "switched to next transport"
171
- );
153
+ const from = this.#selector.transportName();
154
+ const success = this.#selector.rotate();
155
+ const to = this.#selector.transportName();
156
+ if (success) {
157
+ if (from !== to) {
158
+ this.#logger?.debug({ from, to, reason }, "transport rotated");
172
159
  try {
173
- await this.#config.onRotateSuccess?.(
174
- oldTransportName,
175
- this.currentTransportName(),
176
- reason
177
- );
160
+ await this.#config.onRotateSuccess?.(from, to, reason);
178
161
  } catch {
179
162
  }
180
- this.#rotating = false;
181
- return true;
182
- } else {
183
- this.#logger?.warn(
184
- {
185
- current: this.currentTransportName,
186
- total: this.#transports.length
187
- },
188
- "transport is still on cooldown"
189
- );
190
163
  }
191
- }
192
- try {
193
- await this.#config.onRotateFailed?.(oldTransportName, reason);
194
- } catch {
164
+ } else {
165
+ this.#logger?.warn({ from, reason }, "transport rotation failed");
166
+ try {
167
+ await this.#config.onRotateFailed?.(from, reason);
168
+ } catch {
169
+ }
195
170
  }
196
171
  this.#rotating = false;
197
- return false;
198
- }
199
- currentTransportName() {
200
- return this.#transport({}).config.name;
201
- }
202
- statuses() {
203
- const now = Date.now();
204
- return this.#transports.map((t, i) => ({
205
- id: t.transport({}).config.name,
206
- status: t.cooldown < now ? this.#index === i ? "active" : "standby" : "cooldown"
207
- }));
208
172
  }
209
173
  get #logger() {
210
174
  return this.#config.logger;
211
175
  }
212
- get #transport() {
213
- return this.#transports[this.#index].transport;
214
- }
215
- get #hasAvailableTransports() {
216
- const now = Date.now();
217
- return this.#transports.some((t) => t.cooldown < now);
218
- }
219
176
  }
220
177
  const retryCodes = /* @__PURE__ */ new Set([
221
178
  import_viem.InvalidRequestRpcError.code,
@@ -232,6 +189,87 @@ const defaultShouldRetry = ({
232
189
  }
233
190
  return false;
234
191
  };
192
+ class AbstractTransportSelector {
193
+ transports;
194
+ cooldown;
195
+ index = 0;
196
+ constructor(transports, cooldown = 6e4) {
197
+ this.transports = transports;
198
+ this.cooldown = cooldown;
199
+ }
200
+ transportName() {
201
+ return this.transports[this.index].name;
202
+ }
203
+ canRotate() {
204
+ const now = Date.now();
205
+ return this.transports.some((t) => t.cooldown < now);
206
+ }
207
+ statuses() {
208
+ const now = Date.now();
209
+ return this.transports.map((t, i) => ({
210
+ id: t.name,
211
+ status: t.cooldown < now ? this.index === i ? "active" : "standby" : "cooldown"
212
+ }));
213
+ }
214
+ }
215
+ class SimpleTransportSelector extends AbstractTransportSelector {
216
+ /**
217
+ * For simple selector, transport status is not re-evaluated on each request
218
+ * @returns
219
+ */
220
+ select() {
221
+ return this.transports[this.index].transport;
222
+ }
223
+ /**
224
+ * Simply selects next transport that is not in cooldown by checking all transports in cyclic order
225
+ * @returns true if rotation was successful
226
+ */
227
+ rotate() {
228
+ if (this.transports.length === 1) {
229
+ return true;
230
+ }
231
+ const now = Date.now();
232
+ this.transports[this.index].cooldown = now + this.cooldown;
233
+ for (let i = 0; i < this.transports.length - 1; i++) {
234
+ this.index = (this.index + 1) % this.transports.length;
235
+ if (this.transports[this.index].cooldown < now) {
236
+ return true;
237
+ }
238
+ }
239
+ return false;
240
+ }
241
+ }
242
+ class OrderedTransportSelector extends AbstractTransportSelector {
243
+ /**
244
+ * Will select first transport that is not in cooldown
245
+ * @returns
246
+ */
247
+ select() {
248
+ this.#updateIndex(Date.now());
249
+ return this.transports[this.index].transport;
250
+ }
251
+ /**
252
+ * Will put current transport into cooldown and select first available transport that is not in cooldown
253
+ * @returns true if rotation was successful
254
+ */
255
+ rotate() {
256
+ if (this.transports.length === 1) {
257
+ return true;
258
+ }
259
+ const now = Date.now();
260
+ this.transports[this.index].cooldown = now + this.cooldown;
261
+ return this.#updateIndex(now);
262
+ }
263
+ #updateIndex(now) {
264
+ for (let i = 0; i < this.transports.length; i++) {
265
+ if (this.transports[i].cooldown < now) {
266
+ this.index = i;
267
+ return true;
268
+ }
269
+ }
270
+ return false;
271
+ }
272
+ }
235
273
  // Annotate the CommonJS export names for ESM import in node:
236
274
  0 && (module.exports = {
237
275
  NoAvailableTransportsError,
@@ -146,11 +146,6 @@ class RouterV310Contract extends import_AbstractRouterContract.AbstractRouterCon
146
146
  * Implements {@link IRouterContract.findClaimAllRewards}
147
147
  */
148
148
  async findClaimAllRewards(props) {
149
- if (props.calls.length > 0 && !!props.forceCalls) {
150
- return {
151
- calls: [...props.calls]
152
- };
153
- }
154
149
  const tData = props.tokensToClaim.map((a) => ({
155
150
  balance: 0n,
156
151
  claimRewards: true,
@@ -162,6 +157,11 @@ class RouterV310Contract extends import_AbstractRouterContract.AbstractRouterCon
162
157
  props.creditAccount.creditAccount,
163
158
  tData
164
159
  ]);
160
+ if (props.calls.length > 0 && result.length === 0 && !!props.forceCalls) {
161
+ return {
162
+ calls: [...props.calls]
163
+ };
164
+ }
165
165
  return {
166
166
  calls: [...result]
167
167
  };
@@ -16,11 +16,11 @@ class NoAvailableTransportsError extends BaseError {
16
16
  }
17
17
  }
18
18
  class RevolverTransport {
19
- #transports;
20
- #index = 0;
21
19
  #config;
22
- #rotating = false;
23
20
  #requests = /* @__PURE__ */ new WeakMap();
21
+ #selector;
22
+ #rotating = false;
23
+ #isSingle;
24
24
  overrides;
25
25
  /**
26
26
  * Create a new RevolverTransport
@@ -44,9 +44,9 @@ class RevolverTransport {
44
44
  ...config,
45
45
  shouldRetry: config.shouldRetry ?? defaultShouldRetry
46
46
  };
47
- this.#transports = config.providers.map(
47
+ const transports = config.providers.map(
48
48
  ({ url, name, cooldown }) => ({
49
- provider: name,
49
+ name,
50
50
  transport: http(url, {
51
51
  retryCount: config.retryCount,
52
52
  retryDelay: config.retryDelay,
@@ -60,40 +60,45 @@ class RevolverTransport {
60
60
  cooldown: cooldown ?? 0
61
61
  })
62
62
  );
63
- if (this.#transports.length === 0) {
63
+ if (transports.length === 0) {
64
64
  throw new NoAvailableTransportsError();
65
65
  }
66
+ this.#isSingle = transports.length === 1;
67
+ const selectionStrategy = config.selectionStrategy ?? "simple";
68
+ this.#selector = selectionStrategy === "simple" ? new SimpleTransportSelector(transports, config.cooldown) : new OrderedTransportSelector(transports, config.cooldown);
66
69
  }
67
70
  get value() {
68
71
  return {
69
72
  rotate: (reason) => this.rotate(reason),
70
- currentTransportName: () => this.currentTransportName(),
71
- statuses: () => this.statuses()
73
+ currentTransportName: () => this.#selector.transportName(),
74
+ statuses: () => this.#selector.statuses()
72
75
  };
73
76
  }
74
77
  request = async (r) => {
75
- if (this.#transports.length === 1) {
76
- return this.#transport({ ...this.overrides }).request(r);
78
+ if (this.#isSingle) {
79
+ return this.#selector.select()({
80
+ ...this.overrides
81
+ }).request(r);
77
82
  }
78
83
  let error;
79
84
  do {
80
85
  try {
81
- this.#requests.set(r, this.currentTransportName());
82
- const resp = await withRetry(
83
- () => this.#transport({ ...this.overrides }).request(r),
84
- {
85
- delay: this.#config.retryDelay,
86
- retryCount: this.#config.retryCount,
87
- shouldRetry: this.#config.shouldRetry
88
- }
89
- );
86
+ this.#requests.set(r, this.#selector.transportName());
87
+ const transport = this.#selector.select()({
88
+ ...this.overrides
89
+ });
90
+ const resp = await withRetry(() => transport.request(r), {
91
+ delay: this.#config.retryDelay,
92
+ retryCount: this.#config.retryCount,
93
+ shouldRetry: this.#config.shouldRetry
94
+ });
90
95
  this.#requests.delete(r);
91
96
  return resp;
92
97
  } catch (e) {
93
98
  error = error ?? e;
94
99
  if (e instanceof RpcError || e instanceof HttpRequestError) {
95
100
  const reqTransport = this.#requests.get(r);
96
- if (reqTransport === this.currentTransportName()) {
101
+ if (reqTransport === this.#selector.transportName()) {
97
102
  await this.rotate(e);
98
103
  } else {
99
104
  this.#logger?.debug(
@@ -105,7 +110,7 @@ class RevolverTransport {
105
110
  throw e;
106
111
  }
107
112
  }
108
- } while (this.#hasAvailableTransports);
113
+ } while (this.#selector.canRotate());
109
114
  this.#requests.delete(r);
110
115
  throw new NoAvailableTransportsError(error);
111
116
  };
@@ -127,82 +132,34 @@ class RevolverTransport {
127
132
  * @returns true if rotation was successful
128
133
  */
129
134
  async rotate(reason) {
130
- if (this.#transports.length === 1) {
131
- return true;
132
- }
133
135
  if (this.#rotating) {
134
136
  this.#logger?.debug("already rotating, skipping");
135
- return false;
137
+ return;
136
138
  }
137
139
  this.#rotating = true;
138
- this.#logger?.debug(
139
- {
140
- reason,
141
- current: this.currentTransportName,
142
- total: this.#transports.length
143
- },
144
- "rotating transport"
145
- );
146
- const oldTransportName = this.currentTransportName();
147
- const now = Date.now();
148
- this.#transports[this.#index].cooldown = now + (this.#config.cooldown ?? 6e4);
149
- for (let i = 0; i < this.#transports.length - 1; i++) {
150
- this.#index = (this.#index + 1) % this.#transports.length;
151
- if (this.#transports[this.#index].cooldown < now) {
152
- this.#logger?.info(
153
- {
154
- current: this.currentTransportName,
155
- total: this.#transports.length
156
- },
157
- "switched to next transport"
158
- );
140
+ const from = this.#selector.transportName();
141
+ const success = this.#selector.rotate();
142
+ const to = this.#selector.transportName();
143
+ if (success) {
144
+ if (from !== to) {
145
+ this.#logger?.debug({ from, to, reason }, "transport rotated");
159
146
  try {
160
- await this.#config.onRotateSuccess?.(
161
- oldTransportName,
162
- this.currentTransportName(),
163
- reason
164
- );
147
+ await this.#config.onRotateSuccess?.(from, to, reason);
165
148
  } catch {
166
149
  }
167
- this.#rotating = false;
168
- return true;
169
- } else {
170
- this.#logger?.warn(
171
- {
172
- current: this.currentTransportName,
173
- total: this.#transports.length
174
- },
175
- "transport is still on cooldown"
176
- );
177
150
  }
178
- }
179
- try {
180
- await this.#config.onRotateFailed?.(oldTransportName, reason);
181
- } catch {
151
+ } else {
152
+ this.#logger?.warn({ from, reason }, "transport rotation failed");
153
+ try {
154
+ await this.#config.onRotateFailed?.(from, reason);
155
+ } catch {
156
+ }
182
157
  }
183
158
  this.#rotating = false;
184
- return false;
185
- }
186
- currentTransportName() {
187
- return this.#transport({}).config.name;
188
- }
189
- statuses() {
190
- const now = Date.now();
191
- return this.#transports.map((t, i) => ({
192
- id: t.transport({}).config.name,
193
- status: t.cooldown < now ? this.#index === i ? "active" : "standby" : "cooldown"
194
- }));
195
159
  }
196
160
  get #logger() {
197
161
  return this.#config.logger;
198
162
  }
199
- get #transport() {
200
- return this.#transports[this.#index].transport;
201
- }
202
- get #hasAvailableTransports() {
203
- const now = Date.now();
204
- return this.#transports.some((t) => t.cooldown < now);
205
- }
206
163
  }
207
164
  const retryCodes = /* @__PURE__ */ new Set([
208
165
  InvalidRequestRpcError.code,
@@ -219,6 +176,87 @@ const defaultShouldRetry = ({
219
176
  }
220
177
  return false;
221
178
  };
179
+ class AbstractTransportSelector {
180
+ transports;
181
+ cooldown;
182
+ index = 0;
183
+ constructor(transports, cooldown = 6e4) {
184
+ this.transports = transports;
185
+ this.cooldown = cooldown;
186
+ }
187
+ transportName() {
188
+ return this.transports[this.index].name;
189
+ }
190
+ canRotate() {
191
+ const now = Date.now();
192
+ return this.transports.some((t) => t.cooldown < now);
193
+ }
194
+ statuses() {
195
+ const now = Date.now();
196
+ return this.transports.map((t, i) => ({
197
+ id: t.name,
198
+ status: t.cooldown < now ? this.index === i ? "active" : "standby" : "cooldown"
199
+ }));
200
+ }
201
+ }
202
+ class SimpleTransportSelector extends AbstractTransportSelector {
203
+ /**
204
+ * For simple selector, transport status is not re-evaluated on each request
205
+ * @returns
206
+ */
207
+ select() {
208
+ return this.transports[this.index].transport;
209
+ }
210
+ /**
211
+ * Simply selects next transport that is not in cooldown by checking all transports in cyclic order
212
+ * @returns true if rotation was successful
213
+ */
214
+ rotate() {
215
+ if (this.transports.length === 1) {
216
+ return true;
217
+ }
218
+ const now = Date.now();
219
+ this.transports[this.index].cooldown = now + this.cooldown;
220
+ for (let i = 0; i < this.transports.length - 1; i++) {
221
+ this.index = (this.index + 1) % this.transports.length;
222
+ if (this.transports[this.index].cooldown < now) {
223
+ return true;
224
+ }
225
+ }
226
+ return false;
227
+ }
228
+ }
229
+ class OrderedTransportSelector extends AbstractTransportSelector {
230
+ /**
231
+ * Will select first transport that is not in cooldown
232
+ * @returns
233
+ */
234
+ select() {
235
+ this.#updateIndex(Date.now());
236
+ return this.transports[this.index].transport;
237
+ }
238
+ /**
239
+ * Will put current transport into cooldown and select first available transport that is not in cooldown
240
+ * @returns true if rotation was successful
241
+ */
242
+ rotate() {
243
+ if (this.transports.length === 1) {
244
+ return true;
245
+ }
246
+ const now = Date.now();
247
+ this.transports[this.index].cooldown = now + this.cooldown;
248
+ return this.#updateIndex(now);
249
+ }
250
+ #updateIndex(now) {
251
+ for (let i = 0; i < this.transports.length; i++) {
252
+ if (this.transports[i].cooldown < now) {
253
+ this.index = i;
254
+ return true;
255
+ }
256
+ }
257
+ return false;
258
+ }
259
+ }
222
260
  export {
223
261
  NoAvailableTransportsError,
224
262
  RevolverTransport
@@ -123,11 +123,6 @@ class RouterV310Contract extends AbstractRouterContract {
123
123
  * Implements {@link IRouterContract.findClaimAllRewards}
124
124
  */
125
125
  async findClaimAllRewards(props) {
126
- if (props.calls.length > 0 && !!props.forceCalls) {
127
- return {
128
- calls: [...props.calls]
129
- };
130
- }
131
126
  const tData = props.tokensToClaim.map((a) => ({
132
127
  balance: 0n,
133
128
  claimRewards: true,
@@ -139,6 +134,11 @@ class RouterV310Contract extends AbstractRouterContract {
139
134
  props.creditAccount.creditAccount,
140
135
  tData
141
136
  ]);
137
+ if (props.calls.length > 0 && result.length === 0 && !!props.forceCalls) {
138
+ return {
139
+ calls: [...props.calls]
140
+ };
141
+ }
142
142
  return {
143
143
  calls: [...result]
144
144
  };
@@ -12,6 +12,12 @@ export interface ProviderStatus {
12
12
  }
13
13
  type OnRequestFn = (providerName: string, ...args: Parameters<Required<HttpRpcClientOptions>["onRequest"]>) => ReturnType<Required<HttpRpcClientOptions>["onRequest"]>;
14
14
  type OnResponseFn = (providerName: string, ...args: Parameters<Required<HttpRpcClientOptions>["onResponse"]>) => ReturnType<Required<HttpRpcClientOptions>["onResponse"]>;
15
+ /**
16
+ * How to select the next transport
17
+ * - simple: selects next transport after current that is not in cooldown by checking all transports in cyclic order
18
+ * - ordered: will select first available transport that is not in cooldown
19
+ */
20
+ export type SelectionStrategy = "simple" | "ordered";
15
21
  export interface RevolverTransportConfig {
16
22
  network: NetworkType;
17
23
  providers: ProviderConfig[];
@@ -59,17 +65,21 @@ export interface RevolverTransportConfig {
59
65
  * How long, in milliseconds, to wait before try this transport again
60
66
  */
61
67
  cooldown?: number | undefined;
68
+ /**
69
+ * How to select the next transport
70
+ * Defaults to "simple"
71
+ */
72
+ selectionStrategy?: SelectionStrategy;
62
73
  }
63
74
  export declare class NoAvailableTransportsError extends BaseError {
64
75
  constructor(cause?: Error);
65
76
  }
66
77
  export interface RevolverTransportValue {
67
78
  /**
68
- * Manually rotate the transport
79
+ * Manually try to rotate the transport
69
80
  * @param reason
70
- * @returns true if rotation was successful
71
81
  */
72
- rotate: (reason?: BaseError) => Promise<boolean>;
82
+ rotate: (reason?: BaseError) => Promise<void>;
73
83
  /**
74
84
  * Returns the name of the current transport
75
85
  */
@@ -100,8 +110,6 @@ export declare class RevolverTransport implements ReturnType<Transport<"revolver
100
110
  * @param reason
101
111
  * @returns true if rotation was successful
102
112
  */
103
- rotate(reason?: BaseError): Promise<boolean>;
104
- currentTransportName(): string;
105
- statuses(): ProviderStatus[];
113
+ rotate(reason?: BaseError): Promise<void>;
106
114
  }
107
115
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gearbox-protocol/sdk",
3
- "version": "10.4.4",
3
+ "version": "10.5.0",
4
4
  "description": "Gearbox SDK",
5
5
  "license": "MIT",
6
6
  "main": "./dist/cjs/sdk/index.js",