@mixerx/oracles 2.0.0 → 3.0.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.
Files changed (51) hide show
  1. package/README.md +31 -19
  2. package/dist/application/ports/ILogger.js +0 -1
  3. package/dist/domain/config/TokenDefaults.js +0 -1
  4. package/dist/domain/exceptions.d.ts +1 -1
  5. package/dist/domain/exceptions.js +0 -1
  6. package/dist/domain/repositories.d.ts +1 -2
  7. package/dist/domain/repositories.js +0 -1
  8. package/dist/factory.d.ts +5 -15
  9. package/dist/factory.js +2 -2
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.js +0 -1
  12. package/dist/infrastructure/blockchain/staticProvider.js +0 -1
  13. package/dist/infrastructure/config/ProtocolTokenConfig.js +5 -5
  14. package/dist/infrastructure/config/gasOracleConfigs.js +0 -1
  15. package/dist/infrastructure/gas/GasPriceOracle.d.ts +5 -18
  16. package/dist/infrastructure/gas/GasPriceOracle.js +51 -45
  17. package/dist/infrastructure/mixerx/MixerXFeeOracle.d.ts +16 -5
  18. package/dist/infrastructure/mixerx/MixerXFeeOracle.js +110 -38
  19. package/dist/infrastructure/multicall/MulticallProvider.d.ts +5 -1
  20. package/dist/infrastructure/multicall/MulticallProvider.js +10 -7
  21. package/dist/infrastructure/token-price/TokenPriceOracle.d.ts +3 -5
  22. package/dist/infrastructure/token-price/TokenPriceOracle.js +11 -10
  23. package/dist/types.d.ts +6 -1
  24. package/dist/types.js +0 -1
  25. package/package.json +62 -56
  26. package/dist/application/ports/ILogger.js.map +0 -1
  27. package/dist/application/ports/index.d.ts +0 -5
  28. package/dist/application/ports/index.js +0 -18
  29. package/dist/application/ports/index.js.map +0 -1
  30. package/dist/domain/config/TokenDefaults.js.map +0 -1
  31. package/dist/domain/config/index.d.ts +0 -1
  32. package/dist/domain/config/index.js +0 -8
  33. package/dist/domain/config/index.js.map +0 -1
  34. package/dist/domain/exceptions.js.map +0 -1
  35. package/dist/domain/repositories.js.map +0 -1
  36. package/dist/factory.js.map +0 -1
  37. package/dist/index.js.map +0 -1
  38. package/dist/infrastructure/blockchain/staticProvider.js.map +0 -1
  39. package/dist/infrastructure/config/ProtocolTokenConfig.js.map +0 -1
  40. package/dist/infrastructure/config/gasOracleConfigs.js.map +0 -1
  41. package/dist/infrastructure/config/index.d.ts +0 -4
  42. package/dist/infrastructure/config/index.js +0 -11
  43. package/dist/infrastructure/config/index.js.map +0 -1
  44. package/dist/infrastructure/gas/GasPriceOracle.js.map +0 -1
  45. package/dist/infrastructure/mixerx/MixerXFeeOracle.js.map +0 -1
  46. package/dist/infrastructure/multicall/MulticallProvider.js.map +0 -1
  47. package/dist/infrastructure/token-price/TokenPriceOracle.js.map +0 -1
  48. package/dist/infrastructure/token-price/index.d.ts +0 -3
  49. package/dist/infrastructure/token-price/index.js +0 -6
  50. package/dist/infrastructure/token-price/index.js.map +0 -1
  51. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -1,8 +1,8 @@
1
1
  # MixerX Oracles
2
2
 
3
- Fail-closed Sepolia pricing and gas policy for MixerX. Version 2.0.0 removes
4
- historical Tornado price fallbacks and rejects a withdrawal quote when its
5
- price is missing, stale or economically unsafe.
3
+ Fail-closed Sepolia pricing and gas policy for MixerX. Version 3.0.0 rejects a
4
+ withdrawal quote when its gas or token price is missing, stale or economically
5
+ unsafe.
6
6
 
7
7
  ## Security contract
8
8
 
@@ -13,8 +13,11 @@ price is missing, stale or economically unsafe.
13
13
  the batch; no historical constant is substituted.
14
14
  - Non-native withdrawal fees require a repository price plus its observation
15
15
  timestamp. The default maximum age is two minutes.
16
- - Gas data, service fee, refund, decimals and total fee are bounded before a
17
- result is returned.
16
+ - Gas data, service fee, refund, token decimals and total fee are bounded before
17
+ a result is returned. The default total-fee ceiling matches the contracts at
18
+ 10% of the withdrawal amount.
19
+ - Configurable token-price sanity bands can reject corrupted but fresh cache
20
+ values.
18
21
  - The exact withdrawal gas limit is mandatory input and must come from
19
22
  simulation or `eth_estimateGas`; the package contains no token-specific
20
23
  limits inherited from another protocol.
@@ -29,10 +32,10 @@ do not accept traffic if it fails.
29
32
  ## Installation
30
33
 
31
34
  ```bash
32
- yarn add @mixerx/oracles@2.0.0
35
+ yarn add @mixerx/oracles@3.0.0
33
36
  ```
34
37
 
35
- Node.js 22.12 or newer is required.
38
+ Node.js 24.12.0 or newer is required.
36
39
 
37
40
  ## Usage
38
41
 
@@ -42,18 +45,24 @@ import {
42
45
  type ITokenPriceRepository,
43
46
  } from '@mixerx/oracles';
44
47
 
45
- const priceRepository: ITokenPriceRepository = {
46
- getPrice: async (symbol) => readCachedPrice(symbol),
47
- getLastUpdateTimestamp: async () => readLastPriceUpdate(),
48
- };
48
+ const priceRepository: ITokenPriceRepository = {
49
+ getQuote: async (symbol) => readCachedQuote(symbol),
50
+ };
49
51
 
50
52
  const oracles = createMixerxOraclesModule({
51
53
  chainId: 11155111,
52
54
  rpcUrl: process.env.SEPOLIA_RPC_URL!,
53
55
  relayerFeePercent: 0.004,
54
56
  priceRepository,
55
- maxGasPriceGwei: 100,
56
- maxPriceAgeMs: 120_000,
57
+ maxGasPriceGwei: 100,
58
+ maxPriceAgeMs: 120_000,
59
+ maxTotalFeeBps: 1_000,
60
+ priceBounds: {
61
+ fds: {
62
+ minimumWeiPerToken: minimumAcceptedFdsPrice,
63
+ maximumWeiPerToken: maximumAcceptedFdsPrice,
64
+ },
65
+ },
57
66
  });
58
67
 
59
68
  await oracles.assertReady();
@@ -72,8 +81,10 @@ console.log({
72
81
  tokenPriceInEth: fee.tokenPriceInEth,
73
82
  priceSource: fee.priceSource,
74
83
  priceObservedAtMs: fee.priceObservedAtMs,
75
- baseFeePerGas: fee.baseFeePerGas,
76
- });
84
+ baseFeePerGas: fee.baseFeePerGas,
85
+ effectiveGasPriceWei: fee.gasPriceWei,
86
+ gasSource: fee.gasSource,
87
+ });
77
88
  ```
78
89
 
79
90
  `fee_quote` and `fee_validation` remain accepted as input aliases for migration,
@@ -81,10 +92,11 @@ but both normalize to the single `fee` policy in returned data.
81
92
 
82
93
  ## Price-feed operation
83
94
 
84
- `fetchPrices()` reads all configured Sepolia tokens through Multicall3. It
85
- returns `Record<string, string>` only when every token succeeds. The host price
86
- feed must persist both the values and one observation timestamp atomically.
87
- Fee calculation never falls back from that repository to a spot value.
95
+ `fetchPrices()` reads all configured Sepolia tokens through Multicall3 at one
96
+ explicit block. It returns the complete price map together with `blockNumber`
97
+ and `observedAtMs`, and only succeeds when every token succeeds. The host price
98
+ feed must persist each price and its observation metadata atomically. Fee
99
+ calculation never falls back from that repository to a spot value.
88
100
 
89
101
  The default Multicall3 address is
90
102
  `0xcA11bde05977b3631167028862bE2a173976CA11`; an explicit verified address can
@@ -23,4 +23,3 @@ class ConsoleLogger {
23
23
  }
24
24
  }
25
25
  exports.ConsoleLogger = ConsoleLogger;
26
- //# sourceMappingURL=ILogger.js.map
@@ -10,4 +10,3 @@ function getBumpPercent(txType) {
10
10
  const key = txType.trim().toLowerCase();
11
11
  return BUMP_PERCENTAGES.get(key) ?? 10;
12
12
  }
13
- //# sourceMappingURL=TokenDefaults.js.map
@@ -1,4 +1,4 @@
1
- export type OracleErrorCode = 'INVALID_CONFIG' | 'INVALID_INPUT' | 'NETWORK_MISMATCH' | 'RPC_REQUEST_FAILED' | 'TOKEN_PRICE_UNAVAILABLE' | 'TOKEN_PRICE_STALE' | 'ECONOMIC_LIMIT_EXCEEDED';
1
+ export type OracleErrorCode = 'INVALID_CONFIG' | 'INVALID_INPUT' | 'NETWORK_MISMATCH' | 'RPC_REQUEST_FAILED' | 'TOKEN_PRICE_UNAVAILABLE' | 'TOKEN_PRICE_STALE' | 'TOKEN_PRICE_OUT_OF_RANGE' | 'ECONOMIC_LIMIT_EXCEEDED';
2
2
  export declare class OracleError extends Error {
3
3
  readonly code: OracleErrorCode;
4
4
  constructor(code: OracleErrorCode, message: string);
@@ -19,4 +19,3 @@ class TokenPriceError extends OracleError {
19
19
  }
20
20
  }
21
21
  exports.TokenPriceError = TokenPriceError;
22
- //# sourceMappingURL=exceptions.js.map
@@ -1,6 +1,5 @@
1
1
  import type { TokenPriceQuote } from '../types';
2
2
  export interface ITokenPriceRepository {
3
- getPrice(symbol: string): Promise<bigint | null | undefined>;
4
- getLastUpdateTimestamp(symbol: string): Promise<number | null | undefined>;
3
+ getQuote(symbol: string): Promise<TokenPriceQuote | null | undefined>;
5
4
  }
6
5
  export declare function toPriceQuote(price: bigint | null | undefined, observedAtMs: number | null | undefined): TokenPriceQuote | null;
@@ -10,4 +10,3 @@ function toPriceQuote(price, observedAtMs) {
10
10
  }
11
11
  return { priceInEthWei: price, observedAtMs: observedAtMs, source: 'repository' };
12
12
  }
13
- //# sourceMappingURL=repositories.js.map
package/dist/factory.d.ts CHANGED
@@ -2,8 +2,9 @@ import type { ILogger } from './application/ports/ILogger';
2
2
  import type { ITokenPriceRepository } from './domain/repositories';
3
3
  import { GasPriceOracle } from './infrastructure/gas/GasPriceOracle';
4
4
  import { MixerXFeeOracle } from './infrastructure/mixerx/MixerXFeeOracle';
5
+ import type { TokenPriceBounds } from './infrastructure/mixerx/MixerXFeeOracle';
5
6
  import { TokenPriceOracle } from './infrastructure/token-price/TokenPriceOracle';
6
- import type { ChainId, TokenPrices } from './types';
7
+ import type { ChainId, TokenPriceSnapshot } from './types';
7
8
  export interface MixerxOraclesConfig {
8
9
  chainId: ChainId;
9
10
  rpcUrl: string;
@@ -16,25 +17,14 @@ export interface MixerxOraclesConfig {
16
17
  maxGasPriceGwei?: number;
17
18
  maxPriceAgeMs?: number;
18
19
  maxRefundWei?: bigint;
19
- fallbackGasPrices?: {
20
- legacy?: {
21
- instant: number;
22
- fast: number;
23
- standard: number;
24
- low: number;
25
- };
26
- eip1559?: {
27
- baseFee: number;
28
- maxFeePerGas: number;
29
- maxPriorityFeePerGas: number;
30
- };
31
- };
20
+ maxTotalFeeBps?: number;
21
+ priceBounds?: Readonly<Record<string, TokenPriceBounds>>;
32
22
  }
33
23
  export interface IMixerxOraclesModule {
34
24
  gasPriceOracle: GasPriceOracle;
35
25
  tokenPriceOracle: TokenPriceOracle;
36
26
  mixerXFeeOracle: MixerXFeeOracle;
37
- fetchPrices(): Promise<TokenPrices>;
27
+ fetchPrices(): Promise<TokenPriceSnapshot>;
38
28
  assertReady(): Promise<void>;
39
29
  chainId: ChainId;
40
30
  }
package/dist/factory.js CHANGED
@@ -11,7 +11,6 @@ function createMixerxOraclesModule(config) {
11
11
  chainId: config.chainId,
12
12
  rpcUrl: config.rpcUrl,
13
13
  logger,
14
- fallbackGasPrices: config.fallbackGasPrices,
15
14
  rpcTimeoutMs: config.rpcTimeoutMs,
16
15
  maxGasPriceGwei: config.maxGasPriceGwei,
17
16
  });
@@ -28,6 +27,8 @@ function createMixerxOraclesModule(config) {
28
27
  maxGasPriceGwei: config.maxGasPriceGwei,
29
28
  maxPriceAgeMs: config.maxPriceAgeMs,
30
29
  maxRefundWei: config.maxRefundWei,
30
+ maxTotalFeeBps: config.maxTotalFeeBps,
31
+ priceBounds: config.priceBounds,
31
32
  });
32
33
  return {
33
34
  gasPriceOracle,
@@ -38,4 +39,3 @@ function createMixerxOraclesModule(config) {
38
39
  assertReady: () => mixerXFeeOracle.assertReady(),
39
40
  };
40
41
  }
41
- //# sourceMappingURL=factory.js.map
package/dist/index.d.ts CHANGED
@@ -3,4 +3,5 @@ export { OracleError, TokenPriceError } from './domain/exceptions';
3
3
  export type { OracleErrorCode } from './domain/exceptions';
4
4
  export type { ILogger } from './application/ports/ILogger';
5
5
  export type { ITokenPriceRepository } from './domain/repositories';
6
+ export type { TokenPriceQuote, TokenPriceSnapshot } from './types';
6
7
  export type { FeeCalculationResult, MixerXGasPurpose, MixerXFeeCalculationResult, MixerXGasParametersForTxResult, MixerXTxType, WithdrawalFeeInput, } from './infrastructure/mixerx/MixerXFeeOracle';
package/dist/index.js CHANGED
@@ -19,4 +19,3 @@ __exportStar(require("./factory"), exports);
19
19
  var exceptions_1 = require("./domain/exceptions");
20
20
  Object.defineProperty(exports, "OracleError", { enumerable: true, get: function () { return exceptions_1.OracleError; } });
21
21
  Object.defineProperty(exports, "TokenPriceError", { enumerable: true, get: function () { return exceptions_1.TokenPriceError; } });
22
- //# sourceMappingURL=index.js.map
@@ -34,4 +34,3 @@ function validateRpcUrl(rawUrl) {
34
34
  throw new exceptions_1.OracleError('INVALID_CONFIG', 'rpcUrl must use HTTPS and must not contain user-info credentials');
35
35
  }
36
36
  }
37
- //# sourceMappingURL=staticProvider.js.map
@@ -3,12 +3,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.resolveOffchainOracleAddress = resolveOffchainOracleAddress;
4
4
  exports.resolveDefaultTokens = resolveDefaultTokens;
5
5
  const config_1 = require("@mixerx/config");
6
+ const exceptions_1 = require("../../domain/exceptions");
6
7
  const gasOracleConfigs_1 = require("./gasOracleConfigs");
7
8
  function toSupportedChainId(chainId) {
8
9
  const numericChainId = Number(chainId);
9
10
  (0, gasOracleConfigs_1.assertSepoliaChainId)(numericChainId);
10
11
  if (!(0, config_1.isSupportedNetId)(numericChainId)) {
11
- throw new Error(`Unsupported chainId ${chainId} for @mixerx/config`);
12
+ throw new exceptions_1.OracleError('INVALID_CONFIG', `Unsupported chainId ${chainId} for @mixerx/config`);
12
13
  }
13
14
  return numericChainId;
14
15
  }
@@ -17,7 +18,7 @@ function resolveOffchainOracleAddress(chainId) {
17
18
  const network = (0, config_1.getNetworkConfig)(validatedChainId);
18
19
  const address = network?.contracts.offchainOracleAddress;
19
20
  if (!address) {
20
- throw new Error(`Missing offchainOracleAddress in @mixerx/config for chainId ${chainId}`);
21
+ throw new exceptions_1.OracleError('INVALID_CONFIG', `Missing offchainOracleAddress in @mixerx/config for chainId ${chainId}`);
21
22
  }
22
23
  return address;
23
24
  }
@@ -36,7 +37,7 @@ function resolveDefaultTokens(chainId) {
36
37
  }
37
38
  if (networkInstances) {
38
39
  for (const currencyData of Object.values(networkInstances)) {
39
- if (!currencyData.tokenAddress) {
40
+ if (currencyData.assetType !== 'erc20') {
40
41
  continue;
41
42
  }
42
43
  const tokenAddress = currencyData.tokenAddress.toLowerCase();
@@ -49,8 +50,7 @@ function resolveDefaultTokens(chainId) {
49
50
  }
50
51
  const tokens = Array.from(tokenMap.values());
51
52
  if (tokens.length === 0) {
52
- throw new Error(`No default tokens resolved from @mixerx/config for chainId ${chainId}`);
53
+ throw new exceptions_1.OracleError('INVALID_CONFIG', `No default tokens resolved from @mixerx/config for chainId ${chainId}`);
53
54
  }
54
55
  return tokens;
55
56
  }
56
- //# sourceMappingURL=ProtocolTokenConfig.js.map
@@ -9,4 +9,3 @@ function assertSepoliaChainId(chainId) {
9
9
  throw new exceptions_1.OracleError('INVALID_CONFIG', `Unsupported chainId ${String(chainId)}; @mixerx/oracles is Sepolia-only`);
10
10
  }
11
11
  }
12
- //# sourceMappingURL=gasOracleConfigs.js.map
@@ -1,6 +1,8 @@
1
1
  import type { ILogger } from '../../application/ports/ILogger';
2
2
  import type { ChainId } from '../config/gasOracleConfigs';
3
3
  export interface GasPriceParams {
4
+ effectiveGasPriceWei: string;
5
+ gasSource: 'rpc';
4
6
  gasPrice?: string;
5
7
  baseFeePerGas?: string;
6
8
  maxFeePerGas?: string;
@@ -9,7 +11,6 @@ export interface GasPriceParams {
9
11
  export interface GetTxGasParamsInput {
10
12
  isLegacy?: boolean;
11
13
  bumpPercent?: number;
12
- legacySpeed?: 'instant' | 'fast' | 'standard' | 'low';
13
14
  }
14
15
  export interface GasPriceOracleOptions {
15
16
  chainId: ChainId;
@@ -17,35 +18,21 @@ export interface GasPriceOracleOptions {
17
18
  logger?: ILogger;
18
19
  rpcTimeoutMs?: number;
19
20
  maxGasPriceGwei?: number;
20
- fallbackGasPrices?: {
21
- legacy?: {
22
- instant: number;
23
- fast: number;
24
- standard: number;
25
- low: number;
26
- };
27
- eip1559?: {
28
- baseFee: number;
29
- maxFeePerGas: number;
30
- maxPriorityFeePerGas: number;
31
- };
32
- };
33
21
  }
34
22
  export declare class GasPriceOracle {
35
23
  private readonly provider;
36
24
  private readonly logger;
37
25
  private readonly chainId;
38
26
  private readonly maxGasPriceWei;
39
- private readonly fallback;
40
27
  constructor(options: GasPriceOracleOptions);
41
28
  getTxGasParams(input?: GetTxGasParamsInput): Promise<GasPriceParams>;
42
- private fallbackParams;
43
- private validateFallback;
44
29
  private validateGwei;
45
30
  private validateBump;
46
31
  private bump;
47
- private bumpGwei;
48
32
  private toWeiGwei;
49
33
  private cap;
34
+ private assertWithinMaximum;
35
+ private assertPositiveGasPrice;
36
+ private assertValidEip1559FeeData;
50
37
  private min;
51
38
  }
@@ -10,7 +10,6 @@ class GasPriceOracle {
10
10
  logger;
11
11
  chainId;
12
12
  maxGasPriceWei;
13
- fallback;
14
13
  constructor(options) {
15
14
  this.chainId = options.chainId;
16
15
  this.provider = (0, staticProvider_1.createVerifiedJsonRpcProvider)(options.rpcUrl, options.chainId, { batchMaxCount: 1 }, options.rpcTimeoutMs);
@@ -21,15 +20,6 @@ class GasPriceOracle {
21
20
  throw new exceptions_1.OracleError('INVALID_CONFIG', 'maxGasPriceGwei must be greater than 0');
22
21
  }
23
22
  this.maxGasPriceWei = this.toWeiGwei(validatedMaxGasPriceGwei);
24
- this.fallback = {
25
- legacy: options.fallbackGasPrices?.legacy ?? { instant: 35, fast: 25, standard: 20, low: 15 },
26
- eip1559: options.fallbackGasPrices?.eip1559 ?? {
27
- baseFee: 15,
28
- maxFeePerGas: 30,
29
- maxPriorityFeePerGas: 2,
30
- },
31
- };
32
- this.validateFallback();
33
23
  }
34
24
  async getTxGasParams(input = {}) {
35
25
  const bumpPercent = this.validateBump(input.bumpPercent ?? 0);
@@ -39,49 +29,51 @@ class GasPriceOracle {
39
29
  this.provider.getFeeData(),
40
30
  this.provider.getBlock('latest'),
41
31
  ]);
42
- if (!input.isLegacy && feeData.maxFeePerGas && feeData.maxPriorityFeePerGas && block?.baseFeePerGas) {
43
- const baseFeePerGas = this.cap(block.baseFeePerGas);
32
+ if (!input.isLegacy &&
33
+ feeData.maxFeePerGas !== null &&
34
+ feeData.maxPriorityFeePerGas !== null &&
35
+ block?.baseFeePerGas !== null &&
36
+ block?.baseFeePerGas !== undefined) {
37
+ this.assertValidEip1559FeeData(block.baseFeePerGas, feeData.maxFeePerGas, feeData.maxPriorityFeePerGas);
38
+ this.assertWithinMaximum(block.baseFeePerGas, 'base fee');
39
+ const baseFeePerGas = block.baseFeePerGas;
44
40
  const maxPriorityFeePerGas = this.cap(this.bump(feeData.maxPriorityFeePerGas, bumpPercent));
45
41
  const maxFeePerGas = this.cap(this.bump(feeData.maxFeePerGas, bumpPercent));
42
+ if (maxFeePerGas < baseFeePerGas) {
43
+ throw new exceptions_1.OracleError('RPC_REQUEST_FAILED', 'EIP-1559 fee cap is below the current base fee');
44
+ }
45
+ const cappedPriorityFeePerGas = this.min(maxPriorityFeePerGas, maxFeePerGas);
46
+ const effectiveGasPriceWei = this.min(baseFeePerGas + cappedPriorityFeePerGas, maxFeePerGas);
47
+ this.assertPositiveGasPrice(effectiveGasPriceWei);
46
48
  return {
47
49
  baseFeePerGas: baseFeePerGas.toString(),
50
+ effectiveGasPriceWei: effectiveGasPriceWei.toString(),
51
+ gasSource: 'rpc',
48
52
  maxFeePerGas: maxFeePerGas.toString(),
49
- maxPriorityFeePerGas: this.min(maxPriorityFeePerGas, maxFeePerGas).toString(),
53
+ maxPriorityFeePerGas: cappedPriorityFeePerGas.toString(),
50
54
  };
51
55
  }
52
- if (feeData.gasPrice) {
53
- return { gasPrice: this.cap(this.bump(feeData.gasPrice, bumpPercent)).toString() };
56
+ if (feeData.gasPrice !== null) {
57
+ this.assertWithinMaximum(feeData.gasPrice, 'legacy gas price');
58
+ const gasPrice = this.cap(this.bump(feeData.gasPrice, bumpPercent));
59
+ this.assertPositiveGasPrice(gasPrice);
60
+ return {
61
+ effectiveGasPriceWei: gasPrice.toString(),
62
+ gasPrice: gasPrice.toString(),
63
+ gasSource: 'rpc',
64
+ };
54
65
  }
66
+ throw new exceptions_1.OracleError('RPC_REQUEST_FAILED', 'Gas RPC response is incomplete');
55
67
  }
56
68
  catch (error) {
57
- if (error instanceof exceptions_1.OracleError && error.code === 'NETWORK_MISMATCH') {
69
+ if (error instanceof exceptions_1.OracleError &&
70
+ (error.code === 'NETWORK_MISMATCH' ||
71
+ error.code === 'RPC_REQUEST_FAILED' ||
72
+ error.code === 'ECONOMIC_LIMIT_EXCEEDED')) {
58
73
  throw error;
59
74
  }
60
- this.logger.warn('Gas RPC request failed; bounded fallback selected', { code: 'RPC_REQUEST_FAILED' });
61
- }
62
- return this.fallbackParams(input, bumpPercent);
63
- }
64
- fallbackParams(input, bumpPercent) {
65
- if (input.isLegacy) {
66
- const speed = input.legacySpeed ?? 'fast';
67
- const gasPrice = this.cap(this.toWeiGwei(this.bumpGwei(this.fallback.legacy[speed], bumpPercent)));
68
- return { gasPrice: gasPrice.toString() };
69
- }
70
- const baseFeePerGas = this.cap(this.toWeiGwei(this.fallback.eip1559.baseFee));
71
- const maxFeePerGas = this.cap(this.toWeiGwei(this.bumpGwei(this.fallback.eip1559.maxFeePerGas, bumpPercent)));
72
- const priority = this.cap(this.toWeiGwei(this.bumpGwei(this.fallback.eip1559.maxPriorityFeePerGas, bumpPercent)));
73
- return {
74
- baseFeePerGas: baseFeePerGas.toString(),
75
- maxFeePerGas: maxFeePerGas.toString(),
76
- maxPriorityFeePerGas: this.min(priority, maxFeePerGas).toString(),
77
- };
78
- }
79
- validateFallback() {
80
- for (const [name, value] of Object.entries(this.fallback.legacy)) {
81
- this.validateGwei(value, `fallbackGasPrices.legacy.${name}`);
82
- }
83
- for (const [name, value] of Object.entries(this.fallback.eip1559)) {
84
- this.validateGwei(value, `fallbackGasPrices.eip1559.${name}`);
75
+ this.logger.warn('Gas RPC request failed', { code: 'RPC_REQUEST_FAILED' });
76
+ throw new exceptions_1.OracleError('RPC_REQUEST_FAILED', 'Gas RPC request failed');
85
77
  }
86
78
  }
87
79
  validateGwei(value, name) {
@@ -99,18 +91,32 @@ class GasPriceOracle {
99
91
  bump(value, percent) {
100
92
  return percent === 0 ? value : (value * BigInt(100 + percent)) / 100n;
101
93
  }
102
- bumpGwei(gwei, percent) {
103
- return percent === 0 ? gwei : (gwei * (100 + percent)) / 100;
104
- }
105
94
  toWeiGwei(gwei) {
106
95
  return BigInt(Math.round(gwei * 1e9));
107
96
  }
108
97
  cap(value) {
109
98
  return this.min(value, this.maxGasPriceWei);
110
99
  }
100
+ assertWithinMaximum(value, label) {
101
+ if (value > this.maxGasPriceWei) {
102
+ throw new exceptions_1.OracleError('ECONOMIC_LIMIT_EXCEEDED', `${label} exceeds maxGasPriceGwei`);
103
+ }
104
+ }
105
+ assertPositiveGasPrice(value) {
106
+ if (value <= 0n) {
107
+ throw new exceptions_1.OracleError('RPC_REQUEST_FAILED', 'Gas RPC response has no positive economic value');
108
+ }
109
+ }
110
+ assertValidEip1559FeeData(baseFeePerGas, maxFeePerGas, maxPriorityFeePerGas) {
111
+ if (baseFeePerGas < 0n ||
112
+ maxFeePerGas <= 0n ||
113
+ maxPriorityFeePerGas < 0n ||
114
+ maxFeePerGas < maxPriorityFeePerGas) {
115
+ throw new exceptions_1.OracleError('RPC_REQUEST_FAILED', 'EIP-1559 RPC fee data is internally inconsistent');
116
+ }
117
+ }
111
118
  min(left, right) {
112
119
  return left < right ? left : right;
113
120
  }
114
121
  }
115
122
  exports.GasPriceOracle = GasPriceOracle;
116
- //# sourceMappingURL=GasPriceOracle.js.map
@@ -1,7 +1,7 @@
1
1
  import type { ILogger } from '../../application/ports/ILogger';
2
2
  import { type ITokenPriceRepository } from '../../domain/repositories';
3
3
  import { GasPriceOracle, type GasPriceParams } from '../gas/GasPriceOracle';
4
- import type { ChainId } from '../config/gasOracleConfigs';
4
+ import { type ChainId } from '../config/gasOracleConfigs';
5
5
  import { TokenPriceOracle } from '../token-price/TokenPriceOracle';
6
6
  export type MixerXTxType = 'relayer_withdrawal' | 'user_withdrawal' | 'relayer_withdrawal_check_v4';
7
7
  export type MixerXGasPurpose = 'fee' | 'send' | 'replacement';
@@ -20,6 +20,12 @@ export interface MixerXFeeOracleOptions {
20
20
  maxGasPriceGwei?: number;
21
21
  maxPriceAgeMs?: number;
22
22
  maxRefundWei?: bigint;
23
+ maxTotalFeeBps?: number;
24
+ priceBounds?: Readonly<Record<string, TokenPriceBounds>>;
25
+ }
26
+ export interface TokenPriceBounds {
27
+ minimumWeiPerToken: bigint;
28
+ maximumWeiPerToken: bigint;
23
29
  }
24
30
  export interface MixerXWithdrawalFeeInput {
25
31
  currency: string;
@@ -28,6 +34,7 @@ export interface MixerXWithdrawalFeeInput {
28
34
  /** Gas limit obtained from a simulation or eth_estimateGas for this exact transaction. */
29
35
  gasLimit: number;
30
36
  refund?: string;
37
+ refundGasMultiplier?: number;
31
38
  txType?: MixerXTxType;
32
39
  purpose?: 'fee' | LegacyFeePurpose;
33
40
  }
@@ -45,10 +52,12 @@ export interface MixerXFeeCalculationResult {
45
52
  gasCost: bigint;
46
53
  relayerFee: bigint;
47
54
  refundAmount: bigint;
55
+ refundWei: bigint;
48
56
  totalFee: bigint;
49
57
  currency: string;
50
58
  gasLimit: number;
51
59
  gasPriceWei: bigint;
60
+ gasSource: 'rpc';
52
61
  baseFeePerGas?: string;
53
62
  maxFeePerGas?: string;
54
63
  maxPriorityFeePerGas?: string;
@@ -63,7 +72,6 @@ export interface MixerXFeeCalculationResult {
63
72
  export type WithdrawalFeeInput = MixerXWithdrawalFeeInput;
64
73
  export type FeeCalculationResult = MixerXFeeCalculationResult;
65
74
  export declare class MixerXFeeOracle {
66
- private readonly chainId;
67
75
  private readonly relayerFeePartsPerMillion;
68
76
  private readonly gasPriceOracle;
69
77
  private readonly tokenPriceOracle;
@@ -71,18 +79,21 @@ export declare class MixerXFeeOracle {
71
79
  private readonly logger;
72
80
  private readonly maxPriceAgeMs;
73
81
  private readonly maxRefundWei;
74
- private readonly supportedCurrencies;
82
+ private readonly maxTotalFeeBps;
83
+ private readonly currencyDecimals;
84
+ private readonly priceBounds;
75
85
  constructor(options: MixerXFeeOracleOptions);
76
86
  calculateWithdrawalFee(input: MixerXWithdrawalFeeInput): Promise<MixerXFeeCalculationResult>;
77
87
  getGasParametersForTx(input?: MixerXGasParametersForTxInput): Promise<MixerXGasParametersForTxResult>;
78
88
  assertReady(): Promise<void>;
79
- getGasParameters(isLegacy?: boolean): Promise<GasPriceParams>;
80
- getChainId(): ChainId;
81
89
  private validateFeeInput;
82
90
  private parseRefund;
91
+ private resolveRefundWei;
83
92
  private getFreshPrice;
93
+ private validatePriceBounds;
84
94
  private effectiveGasPrice;
85
95
  private assertTotalFee;
86
96
  private normalizePurpose;
97
+ private validateTxType;
87
98
  private result;
88
99
  }