@across-protocol/sdk 4.3.152 → 4.3.154

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.
Files changed (64) hide show
  1. package/dist/cjs/src/clients/SpokePoolClient/SpokePoolClient.js +1 -4
  2. package/dist/cjs/src/clients/SpokePoolClient/SpokePoolClient.js.map +1 -1
  3. package/dist/cjs/src/coingecko/Coingecko.js +22 -15
  4. package/dist/cjs/src/coingecko/Coingecko.js.map +1 -1
  5. package/dist/cjs/src/coingecko/CoingeckoErrors.d.ts +12 -0
  6. package/dist/cjs/src/coingecko/CoingeckoErrors.js +17 -0
  7. package/dist/cjs/src/coingecko/CoingeckoErrors.js.map +1 -0
  8. package/dist/cjs/src/coingecko/index.d.ts +1 -0
  9. package/dist/cjs/src/coingecko/index.js +1 -0
  10. package/dist/cjs/src/coingecko/index.js.map +1 -1
  11. package/dist/cjs/src/gasPriceOracle/adapters/solana.js +17 -7
  12. package/dist/cjs/src/gasPriceOracle/adapters/solana.js.map +1 -1
  13. package/dist/cjs/src/gasPriceOracle/errors.d.ts +5 -0
  14. package/dist/cjs/src/gasPriceOracle/errors.js +11 -0
  15. package/dist/cjs/src/gasPriceOracle/errors.js.map +1 -0
  16. package/dist/cjs/src/gasPriceOracle/index.d.ts +1 -0
  17. package/dist/cjs/src/gasPriceOracle/index.js +3 -1
  18. package/dist/cjs/src/gasPriceOracle/index.js.map +1 -1
  19. package/dist/cjs/src/utils/DepositUtils.d.ts +4 -2
  20. package/dist/cjs/src/utils/DepositUtils.js +4 -1
  21. package/dist/cjs/src/utils/DepositUtils.js.map +1 -1
  22. package/dist/esm/src/clients/SpokePoolClient/SpokePoolClient.js +1 -4
  23. package/dist/esm/src/clients/SpokePoolClient/SpokePoolClient.js.map +1 -1
  24. package/dist/esm/src/coingecko/Coingecko.js +22 -15
  25. package/dist/esm/src/coingecko/Coingecko.js.map +1 -1
  26. package/dist/esm/src/coingecko/CoingeckoErrors.d.ts +18 -0
  27. package/dist/esm/src/coingecko/CoingeckoErrors.js +13 -0
  28. package/dist/esm/src/coingecko/CoingeckoErrors.js.map +1 -0
  29. package/dist/esm/src/coingecko/index.d.ts +1 -0
  30. package/dist/esm/src/coingecko/index.js +1 -0
  31. package/dist/esm/src/coingecko/index.js.map +1 -1
  32. package/dist/esm/src/gasPriceOracle/adapters/solana.js +25 -9
  33. package/dist/esm/src/gasPriceOracle/adapters/solana.js.map +1 -1
  34. package/dist/esm/src/gasPriceOracle/errors.d.ts +13 -0
  35. package/dist/esm/src/gasPriceOracle/errors.js +15 -0
  36. package/dist/esm/src/gasPriceOracle/errors.js.map +1 -0
  37. package/dist/esm/src/gasPriceOracle/index.d.ts +1 -0
  38. package/dist/esm/src/gasPriceOracle/index.js +1 -0
  39. package/dist/esm/src/gasPriceOracle/index.js.map +1 -1
  40. package/dist/esm/src/utils/DepositUtils.d.ts +6 -3
  41. package/dist/esm/src/utils/DepositUtils.js +6 -2
  42. package/dist/esm/src/utils/DepositUtils.js.map +1 -1
  43. package/dist/types/src/clients/SpokePoolClient/SpokePoolClient.d.ts.map +1 -1
  44. package/dist/types/src/coingecko/Coingecko.d.ts.map +1 -1
  45. package/dist/types/src/coingecko/CoingeckoErrors.d.ts +19 -0
  46. package/dist/types/src/coingecko/CoingeckoErrors.d.ts.map +1 -0
  47. package/dist/types/src/coingecko/index.d.ts +1 -0
  48. package/dist/types/src/coingecko/index.d.ts.map +1 -1
  49. package/dist/types/src/gasPriceOracle/adapters/solana.d.ts.map +1 -1
  50. package/dist/types/src/gasPriceOracle/errors.d.ts +14 -0
  51. package/dist/types/src/gasPriceOracle/errors.d.ts.map +1 -0
  52. package/dist/types/src/gasPriceOracle/index.d.ts +1 -0
  53. package/dist/types/src/gasPriceOracle/index.d.ts.map +1 -1
  54. package/dist/types/src/utils/DepositUtils.d.ts +6 -3
  55. package/dist/types/src/utils/DepositUtils.d.ts.map +1 -1
  56. package/package.json +1 -1
  57. package/src/clients/SpokePoolClient/SpokePoolClient.ts +1 -4
  58. package/src/coingecko/Coingecko.ts +30 -17
  59. package/src/coingecko/CoingeckoErrors.ts +24 -0
  60. package/src/coingecko/index.ts +1 -0
  61. package/src/gasPriceOracle/adapters/solana.ts +31 -11
  62. package/src/gasPriceOracle/errors.ts +14 -0
  63. package/src/gasPriceOracle/index.ts +1 -0
  64. package/src/utils/DepositUtils.ts +10 -3
@@ -1,6 +1,7 @@
1
1
  import assert from "assert";
2
2
  import { fetchWithTimeout, getCoingeckoTokenIdByAddress, retry } from "../utils";
3
3
  import { Logger } from "../relayFeeCalculator";
4
+ import { CoingeckoPriceNotFoundError } from "./CoingeckoErrors";
4
5
 
5
6
  export function msToS(ms: number) {
6
7
  return Math.floor(ms / 1000);
@@ -206,7 +207,9 @@ export class Coingecko {
206
207
  const _from = msToS(from);
207
208
  const _to = msToS(to);
208
209
  const result = await this.call<HistoricPriceChartData>(
209
- `coins/ethereum/contract/${contract.toLowerCase()}/market_chart/range/?vs_currency=${currency}&from=${_from}&to=${_to}`
210
+ `coins/ethereum/contract/${encodeURIComponent(
211
+ contract.toLowerCase()
212
+ )}/market_chart/range/?vs_currency=${encodeURIComponent(currency)}&from=${_from}&to=${_to}`
210
213
  );
211
214
  // fyi timestamps are returned in ms in contrast to the current price endpoint
212
215
  if (result.prices) return result.prices;
@@ -230,7 +233,7 @@ export class Coingecko {
230
233
  const coingeckoTokenIdentifier = await this.getCoingeckoTokenId(contractAddress, chainId);
231
234
  assert(date, "Requires date string");
232
235
  // Build the path for the Coingecko API request
233
- const url = `coins/${coingeckoTokenIdentifier}/history`;
236
+ const url = `coins/${encodeURIComponent(coingeckoTokenIdentifier)}/history`;
234
237
  // Build the query parameters for the Coingecko API request
235
238
  const queryParams = {
236
239
  date,
@@ -244,7 +247,9 @@ export class Coingecko {
244
247
  }
245
248
 
246
249
  getContractDetails(contract_address: string, platform_id = "ethereum") {
247
- return this.call(`coins/${platform_id}/contract/${contract_address.toLowerCase()}`);
250
+ return this.call(
251
+ `coins/${encodeURIComponent(platform_id)}/contract/${encodeURIComponent(contract_address.toLowerCase())}`
252
+ );
248
253
  }
249
254
 
250
255
  async getCurrentPriceByContract(contractAddress: string, currency = "usd", chainId = 1): Promise<[string, number]> {
@@ -256,7 +261,9 @@ export class Coingecko {
256
261
  tokenPrice = priceCache[contractAddress];
257
262
  }
258
263
 
259
- assert(tokenPrice !== undefined);
264
+ if (tokenPrice === undefined) {
265
+ throw new CoingeckoPriceNotFoundError({ identifier: contractAddress, currency, lookupType: "address" });
266
+ }
260
267
  return [tokenPrice.timestamp.toString(), tokenPrice.price];
261
268
  }
262
269
 
@@ -268,22 +275,25 @@ export class Coingecko {
268
275
  const coingeckoId = await this.getCoingeckoTokenId(contractAddress, chainId);
269
276
  // Build the path for the Coingecko API request
270
277
  const result = await this.call<Record<string, CGTokenPrice>>(
271
- `simple/price?ids=${coingeckoId}&vs_currencies=${currency}&include_last_updated_at=true`
278
+ `simple/price?ids=${encodeURIComponent(coingeckoId)}&vs_currencies=${encodeURIComponent(
279
+ currency
280
+ )}&include_last_updated_at=true`
272
281
  );
273
282
  const cgPrice = result?.[coingeckoId];
274
283
  if (cgPrice === undefined || !cgPrice?.[currency]) {
275
- const errMsg = `No price found for ${coingeckoId}`;
276
284
  this.logger.debug({
277
285
  at: "Coingecko#getCurrentPriceById",
278
- message: errMsg,
286
+ message: `No Coingecko price found for id '${coingeckoId}' in ${currency}`,
279
287
  });
280
- throw new Error(errMsg);
288
+ throw new CoingeckoPriceNotFoundError({ identifier: coingeckoId, currency, lookupType: "id" });
281
289
  } else {
282
290
  this.updatePriceCache(cgPrice, contractAddress, currency, platform_id);
283
291
  }
284
292
  }
285
293
  tokenPrice = priceCache[contractAddress];
286
- assert(tokenPrice !== undefined);
294
+ if (tokenPrice === undefined) {
295
+ throw new CoingeckoPriceNotFoundError({ identifier: contractAddress, currency, lookupType: "address" });
296
+ }
287
297
  return [tokenPrice.timestamp.toString(), tokenPrice.price];
288
298
  }
289
299
 
@@ -291,22 +301,25 @@ export class Coingecko {
291
301
  let tokenPrice = this.getCachedSymbolPrice(symbol, currency);
292
302
  if (tokenPrice === undefined) {
293
303
  const result = await this.call<Record<string, CGTokenPrice>>(
294
- `simple/price?symbols=${symbol}&vs_currencies=${currency}&include_last_updated_at=true`
304
+ `simple/price?symbols=${encodeURIComponent(symbol)}&vs_currencies=${encodeURIComponent(
305
+ currency
306
+ )}&include_last_updated_at=true`
295
307
  );
296
308
  const cgPrice = result?.[symbol.toLowerCase()] || result?.[symbol.toUpperCase()];
297
309
  if (cgPrice === undefined || !cgPrice?.[currency]) {
298
- const errMsg = `Failed to retrieve ${symbol}/${currency} price via Coingecko API`;
299
310
  this.logger.debug({
300
311
  at: "Coingecko#getCurrentPriceBySymbol",
301
- message: errMsg,
312
+ message: `No Coingecko price found for symbol '${symbol}' in ${currency}`,
302
313
  });
303
- throw new Error(errMsg);
314
+ throw new CoingeckoPriceNotFoundError({ identifier: symbol, currency, lookupType: "symbol" });
304
315
  } else {
305
316
  this.updatePriceCacheBySymbol(cgPrice, symbol, currency);
306
317
  }
307
318
  }
308
319
  tokenPrice = this.getCachedSymbolPrice(symbol, currency);
309
- assert(tokenPrice !== undefined);
320
+ if (tokenPrice === undefined) {
321
+ throw new CoingeckoPriceNotFoundError({ identifier: symbol, currency, lookupType: "symbol" });
322
+ }
310
323
  return [tokenPrice.timestamp.toString(), tokenPrice.price];
311
324
  }
312
325
 
@@ -344,9 +357,9 @@ export class Coingecko {
344
357
  try {
345
358
  // Coingecko expects a comma-delimited (%2c) list.
346
359
  result = await this.call(
347
- `simple/token_price/${platform_id}?contract_addresses=${contract_addresses.join(
348
- "%2C"
349
- )}&vs_currencies=${currency}&include_last_updated_at=true`
360
+ `simple/token_price/${encodeURIComponent(platform_id)}?contract_addresses=${contract_addresses
361
+ .map((addr) => encodeURIComponent(addr))
362
+ .join("%2C")}&vs_currencies=${encodeURIComponent(currency)}&include_last_updated_at=true`
350
363
  );
351
364
  } catch (err) {
352
365
  const errMsg = `Failed to retrieve ${platform_id}/${currency} prices (${err})`;
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Thrown when a price lookup against the Coingecko API does not return a
3
+ * usable price for the requested identifier. Callers can use `instanceof` to
4
+ * map this to a 404 / not-found response without needing to string-match
5
+ * the underlying message.
6
+ */
7
+ export type CoingeckoLookupType = "symbol" | "address" | "id";
8
+
9
+ export class CoingeckoPriceNotFoundError extends Error {
10
+ readonly identifier: string;
11
+ readonly currency: string;
12
+ readonly lookupType: CoingeckoLookupType;
13
+
14
+ constructor(args: { identifier: string; currency: string; lookupType: CoingeckoLookupType; cause?: unknown }) {
15
+ super(
16
+ `No Coingecko price found for ${args.lookupType} '${args.identifier}' in ${args.currency}`,
17
+ args.cause !== undefined ? { cause: args.cause } : undefined
18
+ );
19
+ this.name = "CoingeckoPriceNotFoundError";
20
+ this.identifier = args.identifier;
21
+ this.currency = args.currency;
22
+ this.lookupType = args.lookupType;
23
+ }
24
+ }
@@ -1 +1,2 @@
1
1
  export * from "./Coingecko";
2
+ export * from "./CoingeckoErrors";
@@ -1,14 +1,18 @@
1
1
  import { SVMProvider } from "../../arch/svm";
2
- import { toBN, dedupArray } from "../../utils";
2
+ import { BN, toBN, dedupArray } from "../../utils";
3
3
  import { SvmGasPriceEstimate } from "../types";
4
4
  import { GasPriceEstimateOptions } from "../oracle";
5
+ import { SvmGasPriceUnavailableError } from "../errors";
5
6
  import {
6
7
  TransactionMessage,
7
8
  TransactionMessageBytesBase64,
8
9
  TransactionMessageWithFeePayer,
9
10
  compileTransaction,
11
+ setTransactionMessageLifetimeUsingBlockhash,
10
12
  } from "@solana/kit";
11
13
 
14
+ const MAX_BASE_FEE_ATTEMPTS = 2;
15
+
12
16
  /**
13
17
  * @notice Returns result of getFeeForMessage and getRecentPrioritizationFees RPC calls.
14
18
  * @returns GasPriceEstimate
@@ -18,13 +22,8 @@ export async function messageFee(provider: SVMProvider, opts: GasPriceEstimateOp
18
22
 
19
23
  // Cast the opaque unsignedTx type to a solana-kit TransactionMessage with fee payer.
20
24
  const unsignedTx = _unsignedTx as TransactionMessage & TransactionMessageWithFeePayer;
21
- const compiledTransaction = compileTransaction(unsignedTx);
22
25
 
23
- // Get this base fee. This should result in LAMPORTS_PER_SIGNATURE * nSignatures.
24
- const encodedTransactionMessage = Buffer.from(compiledTransaction.messageBytes).toString(
25
- "base64"
26
- ) as TransactionMessageBytesBase64;
27
- const baseFeeResponse = await provider.getFeeForMessage(encodedTransactionMessage).send();
26
+ const baseFee = await getBaseFee(provider, unsignedTx);
28
27
 
29
28
  // Get the priority fee by calling `getRecentPrioritzationFees` on all the addresses in the transaction's instruction array.
30
29
  const instructionAddresses = dedupArray(unsignedTx.instructions.map((instruction) => instruction.programAddress));
@@ -40,8 +39,29 @@ export async function messageFee(provider: SVMProvider, opts: GasPriceEstimateOp
40
39
  const microLamportsPerComputeUnit = toBN(
41
40
  totalPrioritizationFees / BigInt(Math.max(nonzeroPrioritizationFees.length, 1))
42
41
  );
43
- return {
44
- baseFee: toBN(baseFeeResponse!.value!),
45
- microLamportsPerComputeUnit,
46
- };
42
+
43
+ return { baseFee, microLamportsPerComputeUnit };
44
+ }
45
+
46
+ // `getFeeForMessage` returns `{ value: null }` when the cluster has not yet recognised
47
+ // the blockhash referenced by the message. With a load-balanced RPC pool, this happens
48
+ // when the request lands on a node that hasn't seen the blockhash from an earlier
49
+ // `getLatestBlockhash` call. We side-step the race by always refreshing to a `confirmed`
50
+ // blockhash before each attempt — by definition propagated to all healthy nodes — and
51
+ // retrying once if a fee call still comes back null. Fee estimation is never sent
52
+ // on-chain, so blockhash freshness doesn't matter.
53
+ async function getBaseFee(provider: SVMProvider, tx: TransactionMessage & TransactionMessageWithFeePayer): Promise<BN> {
54
+ for (let attempt = 0; attempt < MAX_BASE_FEE_ATTEMPTS; attempt++) {
55
+ const { value: confirmedBlockhash } = await provider.getLatestBlockhash({ commitment: "confirmed" }).send();
56
+ const refreshedTx = setTransactionMessageLifetimeUsingBlockhash(confirmedBlockhash, tx);
57
+ const compiled = compileTransaction(refreshedTx);
58
+ const encoded = Buffer.from(compiled.messageBytes).toString("base64") as TransactionMessageBytesBase64;
59
+ const { value } = await provider.getFeeForMessage(encoded).send();
60
+ if (value !== null && value !== undefined) {
61
+ return toBN(value);
62
+ }
63
+ }
64
+ throw new SvmGasPriceUnavailableError(
65
+ "Solana getFeeForMessage returned null even after retrying with a confirmed blockhash"
66
+ );
47
67
  }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Thrown when the SVM gas-price oracle cannot determine a base fee for the
3
+ * supplied message — most commonly because Solana's `getFeeForMessage` RPC
4
+ * returned `{ value: null }` even after retrying with a confirmed blockhash.
5
+ *
6
+ * Callers can use `instanceof` to map this to a transient upstream-RPC error
7
+ * (e.g. HTTP 502 / 503) rather than treating it as an unhandled exception.
8
+ */
9
+ export class SvmGasPriceUnavailableError extends Error {
10
+ constructor(message: string, opts?: { cause?: unknown }) {
11
+ super(message, opts?.cause !== undefined ? { cause: opts.cause } : undefined);
12
+ this.name = "SvmGasPriceUnavailableError";
13
+ }
14
+ }
@@ -1,2 +1,3 @@
1
1
  export { getGasPriceEstimate, GasPriceEstimateOptions } from "./oracle";
2
2
  export { GasPriceEstimate, EvmGasPriceEstimate, SvmGasPriceEstimate } from "./types";
3
+ export { SvmGasPriceUnavailableError } from "./errors";
@@ -8,6 +8,7 @@ import {
8
8
  Fill,
9
9
  RelayData,
10
10
  SlowFillRequest,
11
+ SpeedUpCommon,
11
12
  ConvertedRelayData,
12
13
  ConvertedFill,
13
14
  } from "../interfaces";
@@ -244,10 +245,16 @@ export function isFillOrSlowFillRequestMessageEmpty(message: string): boolean {
244
245
  /**
245
246
  * Determines if a deposit was updated via a speed-up transaction.
246
247
  * @param deposit Deposit to evaluate.
247
- * @returns True if the deposit was updated, otherwise false.
248
+ * @returns True if the deposit was updated, otherwise false. Narrows the deposit type so callers
249
+ * can safely access updatedRecipient/updatedOutputAmount/updatedMessage/speedUpSignature.
248
250
  */
249
- export function isDepositSpedUp(deposit: Deposit): boolean {
250
- return isDefined(deposit.speedUpSignature) && isDefined(deposit.updatedOutputAmount);
251
+ export function isDepositSpedUp(deposit: Deposit): deposit is Deposit & SpeedUpCommon & { speedUpSignature: string } {
252
+ return (
253
+ isDefined(deposit.speedUpSignature) &&
254
+ isDefined(deposit.updatedOutputAmount) &&
255
+ isDefined(deposit.updatedRecipient) &&
256
+ isDefined(deposit.updatedMessage)
257
+ );
251
258
  }
252
259
 
253
260
  /**