@across-protocol/sdk 4.3.143 → 4.3.145-alpha.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.
@@ -120,13 +120,25 @@ export declare const getSamplesBetween: (min: number, max: number, size: number)
120
120
  */
121
121
  export declare function delay(seconds: number): Promise<unknown>;
122
122
  /**
123
- * Attempt to retry a function call a number of times with a delay between each attempt
124
- * @param call The function to call
125
- * @param times The number of times to retry
126
- * @param delayS The number of seconds to delay between each attempt
123
+ * Configures {@link retry}. Retries always use exponential backoff
124
+ * (`delaySeconds * 2 ** attempt + random()` seconds) to play nicely with upstream
125
+ * rate-limits; callers that want tighter spacing should lower {@link delaySeconds}.
126
+ */
127
+ export type RetryOptions = {
128
+ /** Maximum number of retry attempts after the initial call (total attempts = retries + 1). Defaults to 2 (3 total tries). */
129
+ retries?: number;
130
+ /** Base delay in seconds for the exponential backoff. Defaults to 1. */
131
+ delaySeconds?: number;
132
+ /** Predicate evaluated against the thrown error to decide whether to retry. Defaults to retrying every error. */
133
+ isRetryable?: (err: unknown) => boolean;
134
+ };
135
+ /**
136
+ * Attempt to retry a function call with exponential backoff and a retryability predicate.
137
+ * @param call The function to call.
138
+ * @param options Retry configuration — see {@link RetryOptions}. All fields are optional; omitted fields inherit the SDK defaults.
127
139
  * @returns The result of the function call.
128
140
  */
129
- export declare function retry<T>(call: () => Promise<T>, times: number, delayS: number): Promise<T>;
141
+ export declare function retry<T>(call: () => Promise<T>, options?: RetryOptions): Promise<T>;
130
142
  export type TransactionCostEstimate = {
131
143
  nativeGasCost: BigNumber;
132
144
  tokenGasCost: BigNumber;
@@ -1 +1 @@
1
- {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../../src/utils/common.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"common.d.ts","sourceRoot":"","sources":["../../../../src/utils/common.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@across-protocol/sdk",
3
3
  "author": "UMA Team",
4
- "version": "4.3.143",
4
+ "version": "4.3.145-alpha.0",
5
5
  "license": "AGPL-3.0",
6
6
  "homepage": "https://docs.across.to/reference/sdk",
7
7
  "repository": {
@@ -6,38 +6,105 @@ import winston from "winston";
6
6
  import { ARWEAVE_TAG_APP_NAME, ARWEAVE_TAG_APP_VERSION, DEFAULT_ARWEAVE_STORAGE_ADDRESS } from "../../constants";
7
7
  import { BigNumber, delay, fetchWithTimeout, isDefined, jsonReplacerWithBigNumbers, toBN } from "../../utils";
8
8
 
9
+ export interface ArweaveGatewayConfig {
10
+ host: string;
11
+ protocol?: string;
12
+ port?: number;
13
+ }
14
+
15
+ export const DEFAULT_ARWEAVE_GATEWAYS: ArweaveGatewayConfig[] = [{ host: "arweave.net" }, { host: "ar-io.net" }];
16
+
17
+ interface Gateway {
18
+ client: Arweave;
19
+ url: string;
20
+ }
21
+
9
22
  export class ArweaveClient {
10
- private client: Arweave;
11
- private gatewayUrl: string;
23
+ private gateways: Gateway[];
12
24
 
13
25
  public constructor(
14
26
  private arweaveJWT: JWKInterface,
15
27
  private logger: winston.Logger,
16
- public gatewayURL = "arweave.net",
17
- public protocol = "https",
18
- port = 443,
28
+ gateways: ArweaveGatewayConfig[] = DEFAULT_ARWEAVE_GATEWAYS,
19
29
  private readonly retries = 2,
20
30
  private readonly retryDelaySeconds = 1
21
31
  ) {
22
- this.gatewayUrl = `${protocol}://${gatewayURL}:${port}`;
23
- this.client = new Arweave({
24
- host: gatewayURL,
25
- port,
26
- protocol,
27
- timeout: 20000,
28
- logging: false,
29
- });
30
- this.logger.debug({
31
- at: "ArweaveClient:constructor",
32
- message: "Arweave client initialized",
33
- gateway: this.gatewayUrl,
34
- });
32
+ if (gateways.length === 0) {
33
+ throw new Error("At least one gateway must be provided");
34
+ }
35
35
  if (this.retries < 0) {
36
36
  throw new Error(`retries cannot be < 0 and must be an integer. Currently set to ${this.retries}`);
37
37
  }
38
38
  if (this.retryDelaySeconds < 0) {
39
39
  throw new Error(`delay cannot be < 0. Currently set to ${this.retryDelaySeconds}`);
40
40
  }
41
+ this.gateways = gateways.map(({ host, protocol = "https", port = 443 }) => ({
42
+ client: new Arweave({ host, port, protocol, timeout: 20000, logging: false }),
43
+ url: `${protocol}://${host}:${port}`,
44
+ }));
45
+ this.logger.debug({
46
+ at: "ArweaveClient:constructor",
47
+ message: "Arweave client initialized",
48
+ gateways: this.gateways.map((g) => g.url),
49
+ });
50
+ }
51
+
52
+ /**
53
+ * Races a request across all gateways, returning the first successful response.
54
+ * If all gateways fail, throws an error with details from each gateway.
55
+ */
56
+ private async _raceGateways<T>(label: string, fn: (gw: Gateway) => Promise<T>): Promise<T> {
57
+ try {
58
+ return await Promise.any(this.gateways.map((gw) => this._retryRequest(() => fn(gw), 0)));
59
+ } catch (e) {
60
+ if (e instanceof AggregateError) {
61
+ const details = this.gateways.map((gw, i) => `${gw.url}: ${e.errors[i]}`).join("; ");
62
+ throw new Error(`All Arweave gateways failed for ${label}: ${details}`);
63
+ }
64
+ throw e;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Tries gateways sequentially, returning the first successful response.
70
+ * Used for write operations where we want exactly one successful submission.
71
+ */
72
+ private async _failoverGateways<T>(label: string, fn: (gw: Gateway) => Promise<T>): Promise<T> {
73
+ const errors: Error[] = [];
74
+ for (const gw of this.gateways) {
75
+ try {
76
+ return await this._retryRequest(() => fn(gw), 0);
77
+ } catch (e) {
78
+ errors.push(e as Error);
79
+ this.logger.debug({
80
+ at: "ArweaveClient:failoverGateways",
81
+ message: `Gateway ${gw.url} failed for ${label}, trying next: ${e}`,
82
+ });
83
+ }
84
+ }
85
+ const details = this.gateways.map((gw, i) => `${gw.url}: ${errors[i]}`).join("; ");
86
+ throw new Error(`All Arweave gateways failed for ${label}: ${details}`);
87
+ }
88
+
89
+ private async _retryRequest<T>(request: () => Promise<T>, retryCount: number): Promise<T> {
90
+ try {
91
+ return await request();
92
+ } catch (e) {
93
+ if (retryCount < this.retries) {
94
+ // Implement a slightly aggressive exponential backoff to account for fierce parallelism.
95
+ const baseDelay = this.retryDelaySeconds * Math.pow(2, retryCount);
96
+ const delayS = baseDelay + baseDelay * Math.random();
97
+ this.logger.debug({
98
+ at: "ArweaveClient:retryRequest",
99
+ message: `Arweave request failed, retrying after waiting ${delayS} seconds: ${e}`,
100
+ retryCount,
101
+ });
102
+ await delay(delayS);
103
+ return this._retryRequest(request, retryCount + 1);
104
+ } else {
105
+ throw e;
106
+ }
107
+ }
41
108
  }
42
109
 
43
110
  /**
@@ -47,10 +114,12 @@ export class ArweaveClient {
47
114
  * @param value The value to store
48
115
  * @param topicTag An optional topic tag to add to the transaction
49
116
  * @returns The transaction ID of the stored value
50
- * @
51
117
  */
52
118
  async set(value: Record<string, unknown>, topicTag?: string | undefined): Promise<string | undefined> {
53
- const transaction = await this.client.createTransaction(
119
+ // Get a template client to use for creating a transaction. Since clients are equal up to the gateway URL,
120
+ // it does not matter which gateway is used, so we just choose the first one.
121
+ const templateClient = this.gateways[0].client;
122
+ const transaction = await templateClient.createTransaction(
54
123
  { data: JSON.stringify(value, jsonReplacerWithBigNumbers) },
55
124
  this.arweaveJWT
56
125
  );
@@ -64,29 +133,32 @@ export class ArweaveClient {
64
133
  }
65
134
 
66
135
  // Sign the transaction
67
- await this.client.transactions.sign(transaction, this.arweaveJWT);
136
+ await templateClient.transactions.sign(transaction, this.arweaveJWT);
68
137
  // Send the transaction
69
- const result = await this.client.transactions.post(transaction);
70
138
 
71
- // Ensure that the result is successful
72
- if (result.status !== 200) {
73
- const message = result?.data?.error?.msg ?? "Unknown error";
74
- this.logger.error({
75
- at: "ArweaveClient:set",
76
- message,
77
- result,
78
- txn: transaction.id,
79
- address: await this.getAddress(),
80
- balance: (await this.getBalance()).toString(),
81
- });
82
- throw new Error(message);
83
- } else {
139
+ return await this._failoverGateways("set", async ({ client }) => {
140
+ const result = await client.transactions.post(transaction);
141
+
142
+ // Ensure that the result is successful
143
+ if (result.status !== 200) {
144
+ const message = result?.data?.error?.msg ?? "Unknown error";
145
+ this.logger.error({
146
+ at: "ArweaveClient:set",
147
+ message,
148
+ result,
149
+ txn: transaction.id,
150
+ address: await this.getAddress(),
151
+ balance: (await this.getBalance()).toString(),
152
+ });
153
+ throw new Error(message);
154
+ }
155
+
84
156
  this.logger.debug({
85
157
  at: "ArweaveClient:set",
86
158
  message: `Arweave transaction posted with ${transaction.id}`,
87
159
  });
88
- }
89
- return transaction.id;
160
+ return transaction.id;
161
+ });
90
162
  }
91
163
 
92
164
  /**
@@ -97,15 +169,12 @@ export class ArweaveClient {
97
169
  * @returns The record if it exists, otherwise null
98
170
  */
99
171
  async get<T>(transactionID: string, validator: Struct<T>): Promise<T | null> {
100
- // Resolve the URL of the transaction
101
- const transactionUrl = `${this.gatewayUrl}/${transactionID}`;
102
- // We should query in via Axios directly to the gateway URL. The reasoning behind this is
172
+ // We query via fetchWithTimeout directly to the gateway URL. The reasoning behind this is
103
173
  // that the Arweave SDK's `getData` method is too slow and does not provide a way to set a timeout.
104
174
  // Therefore, something that could take milliseconds to complete could take tens of minutes.
105
- const request = async () => {
106
- return await fetchWithTimeout(transactionUrl, {}, {}, 20_000);
107
- };
108
- const data = await this._retryRequest(request, 0);
175
+ const data = await this._raceGateways("get", async ({ url }) => {
176
+ return await fetchWithTimeout(`${url}/${transactionID}`, {}, {}, 20_000);
177
+ });
109
178
  try {
110
179
  // We should validate the data and perform any logical coercion here.
111
180
  return create(data, validator);
@@ -149,15 +218,15 @@ export class ArweaveClient {
149
218
  ) { edges { node { id } } }
150
219
  }`;
151
220
 
152
- const response = await this._retryRequest(async () => {
153
- const response = await this.client.api.post<{
221
+ const response = await this._raceGateways("getByTopic", async ({ client }) => {
222
+ const response = await client.api.post<{
154
223
  data: { transactions: { edges: { node: { id: string } }[] } };
155
224
  }>("/graphql", { query });
156
225
  if (!response.ok) {
157
226
  throw new Error(`Arweave GraphQL request failed with status ${response.status}`);
158
227
  }
159
228
  return response;
160
- }, 0);
229
+ });
161
230
 
162
231
  const entries = response?.data?.data?.transactions?.edges ?? [];
163
232
  this.logger.debug({
@@ -198,7 +267,9 @@ export class ArweaveClient {
198
267
  * @returns The metadata of the transaction if it exists, otherwise null
199
268
  */
200
269
  async getMetadata(transactionID: string): Promise<Record<string, string> | null> {
201
- const transaction = await this.client.transactions.get(transactionID);
270
+ const transaction = await this._raceGateways("getMetadata", async ({ client }) => {
271
+ return await client.transactions.get(transactionID);
272
+ });
202
273
  if (!isDefined(transaction)) {
203
274
  return null;
204
275
  }
@@ -216,32 +287,12 @@ export class ArweaveClient {
216
287
  }
217
288
 
218
289
  /**
219
- * Returns the address of the signer of the JWT
290
+ * Returns the address of the signer of the JWT. This is a local crypto
291
+ * operation and does not require a network call.
220
292
  * @returns The address of the signer in this client
221
293
  */
222
294
  getAddress(): Promise<string> {
223
- return this.client.wallets.jwkToAddress(this.arweaveJWT);
224
- }
225
-
226
- private async _retryRequest<T>(request: () => Promise<T>, retryCount: number): Promise<T> {
227
- try {
228
- return await request();
229
- } catch (e) {
230
- if (retryCount < this.retries) {
231
- // Implement a slightly aggressive exponential backoff to account for fierce parallelism.
232
- const baseDelay = this.retryDelaySeconds * Math.pow(2, retryCount); // ms; attempt = [0, 1, 2, ...]
233
- const delayS = baseDelay + baseDelay * Math.random();
234
- this.logger.debug({
235
- at: "ArweaveClient:retryRequest",
236
- message: `Arweave request failed, retrying after waiting ${delayS} seconds: ${e}`,
237
- retryCount,
238
- });
239
- await delay(delayS);
240
- return this._retryRequest(request, retryCount + 1);
241
- } else {
242
- throw e;
243
- }
244
- }
295
+ return this.gateways[0].client.wallets.jwkToAddress(this.arweaveJWT);
245
296
  }
246
297
 
247
298
  /**
@@ -250,9 +301,9 @@ export class ArweaveClient {
250
301
  */
251
302
  async getBalance(): Promise<BigNumber> {
252
303
  const address = await this.getAddress();
253
- const request = async () => {
254
- const balanceInFloat = await this.client.wallets.getBalance(address);
255
- // @dev The reason we add in the BN.from into this retry loop is because the client.getBalance call
304
+ return this._raceGateways("getBalance", async ({ client }) => {
305
+ const balanceInFloat = await client.wallets.getBalance(address);
306
+ // @dev The reason we add in the BN.from here is because the client.getBalance call
256
307
  // does not correctly throw an error if the request fails, instead it will return the error string as the
257
308
  // balanceInFloat.
258
309
  // Sometimes the balance is returned in scientific notation, so we need to
@@ -264,7 +315,6 @@ export class ArweaveClient {
264
315
  } else {
265
316
  return BigNumber.from(balanceInFloat);
266
317
  }
267
- };
268
- return await this._retryRequest(request, 0);
318
+ });
269
319
  }
270
320
  }
@@ -401,7 +401,10 @@ export class Coingecko {
401
401
  };
402
402
 
403
403
  // Note: If a pro API key is configured, there is no need to retry as the Pro API will act as the basic's fall back.
404
- return retry(sendRequest, this.apiKey === undefined ? this.numRetries : 0, this.retryDelay);
404
+ return retry(sendRequest, {
405
+ retries: this.apiKey === undefined ? this.numRetries : 0,
406
+ delaySeconds: this.retryDelay,
407
+ });
405
408
  }
406
409
 
407
410
  protected getPriceCache(currency: string, platform_id: string): { [addr: string]: CoinGeckoPrice } {
@@ -221,20 +221,47 @@ export function delay(seconds: number) {
221
221
  }
222
222
 
223
223
  /**
224
- * Attempt to retry a function call a number of times with a delay between each attempt
225
- * @param call The function to call
226
- * @param times The number of times to retry
227
- * @param delayS The number of seconds to delay between each attempt
224
+ * Configures {@link retry}. Retries always use exponential backoff
225
+ * (`delaySeconds * 2 ** attempt + random()` seconds) to play nicely with upstream
226
+ * rate-limits; callers that want tighter spacing should lower {@link delaySeconds}.
227
+ */
228
+ export type RetryOptions = {
229
+ /** Maximum number of retry attempts after the initial call (total attempts = retries + 1). Defaults to 2 (3 total tries). */
230
+ retries?: number;
231
+ /** Base delay in seconds for the exponential backoff. Defaults to 1. */
232
+ delaySeconds?: number;
233
+ /** Predicate evaluated against the thrown error to decide whether to retry. Defaults to retrying every error. */
234
+ isRetryable?: (err: unknown) => boolean;
235
+ };
236
+
237
+ const DEFAULT_RETRY_OPTIONS: Required<RetryOptions> = {
238
+ retries: 2,
239
+ delaySeconds: 1,
240
+ isRetryable: () => true,
241
+ };
242
+
243
+ /**
244
+ * Attempt to retry a function call with exponential backoff and a retryability predicate.
245
+ * @param call The function to call.
246
+ * @param options Retry configuration — see {@link RetryOptions}. All fields are optional; omitted fields inherit the SDK defaults.
228
247
  * @returns The result of the function call.
229
248
  */
230
- export function retry<T>(call: () => Promise<T>, times: number, delayS: number): Promise<T> {
231
- let promiseChain = call();
232
- for (let i = 0; i < times; i++)
233
- promiseChain = promiseChain.catch(async () => {
234
- await delay(delayS);
249
+ export function retry<T>(call: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
250
+ const resolved: Required<RetryOptions> = { ...DEFAULT_RETRY_OPTIONS, ...options };
251
+ const backoffSeconds = (attempt: number): number => resolved.delaySeconds * 2 ** attempt + Math.random();
252
+
253
+ const attempt = async (nAttempts: number): Promise<T> => {
254
+ try {
235
255
  return await call();
236
- });
237
- return promiseChain;
256
+ } catch (err) {
257
+ if (nAttempts >= resolved.retries || !resolved.isRetryable(err)) {
258
+ throw err;
259
+ }
260
+ await delay(backoffSeconds(nAttempts));
261
+ return attempt(nAttempts + 1);
262
+ }
263
+ };
264
+ return attempt(0);
238
265
  }
239
266
 
240
267
  export type TransactionCostEstimate = {