@mixerx/oracles 2.0.0 → 3.1.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 (52) hide show
  1. package/README.md +124 -106
  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 +1 -2
  13. package/dist/infrastructure/config/ProtocolTokenConfig.js +6 -6
  14. package/dist/infrastructure/config/gasOracleConfigs.d.ts +1 -2
  15. package/dist/infrastructure/config/gasOracleConfigs.js +5 -7
  16. package/dist/infrastructure/gas/GasPriceOracle.d.ts +5 -18
  17. package/dist/infrastructure/gas/GasPriceOracle.js +51 -45
  18. package/dist/infrastructure/mixerx/MixerXFeeOracle.d.ts +16 -5
  19. package/dist/infrastructure/mixerx/MixerXFeeOracle.js +110 -38
  20. package/dist/infrastructure/multicall/MulticallProvider.d.ts +5 -1
  21. package/dist/infrastructure/multicall/MulticallProvider.js +11 -8
  22. package/dist/infrastructure/token-price/TokenPriceOracle.d.ts +3 -5
  23. package/dist/infrastructure/token-price/TokenPriceOracle.js +11 -10
  24. package/dist/types.d.ts +6 -1
  25. package/dist/types.js +0 -1
  26. package/package.json +11 -5
  27. package/dist/application/ports/ILogger.js.map +0 -1
  28. package/dist/application/ports/index.d.ts +0 -5
  29. package/dist/application/ports/index.js +0 -18
  30. package/dist/application/ports/index.js.map +0 -1
  31. package/dist/domain/config/TokenDefaults.js.map +0 -1
  32. package/dist/domain/config/index.d.ts +0 -1
  33. package/dist/domain/config/index.js +0 -8
  34. package/dist/domain/config/index.js.map +0 -1
  35. package/dist/domain/exceptions.js.map +0 -1
  36. package/dist/domain/repositories.js.map +0 -1
  37. package/dist/factory.js.map +0 -1
  38. package/dist/index.js.map +0 -1
  39. package/dist/infrastructure/blockchain/staticProvider.js.map +0 -1
  40. package/dist/infrastructure/config/ProtocolTokenConfig.js.map +0 -1
  41. package/dist/infrastructure/config/gasOracleConfigs.js.map +0 -1
  42. package/dist/infrastructure/config/index.d.ts +0 -4
  43. package/dist/infrastructure/config/index.js +0 -11
  44. package/dist/infrastructure/config/index.js.map +0 -1
  45. package/dist/infrastructure/gas/GasPriceOracle.js.map +0 -1
  46. package/dist/infrastructure/mixerx/MixerXFeeOracle.js.map +0 -1
  47. package/dist/infrastructure/multicall/MulticallProvider.js.map +0 -1
  48. package/dist/infrastructure/token-price/TokenPriceOracle.js.map +0 -1
  49. package/dist/infrastructure/token-price/index.d.ts +0 -3
  50. package/dist/infrastructure/token-price/index.js +0 -6
  51. package/dist/infrastructure/token-price/index.js.map +0 -1
  52. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -1,106 +1,124 @@
1
- # MixerX Oracles
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.
6
-
7
- ## Security contract
8
-
9
- - Sepolia (`11155111`) is the only accepted chain.
10
- - RPC endpoints must use HTTPS, are time-bounded and are checked against the
11
- real `eth_chainId` response.
12
- - Token calls use Multicall3 atomically at one block. Any failed token rejects
13
- the batch; no historical constant is substituted.
14
- - Non-native withdrawal fees require a repository price plus its observation
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.
18
- - The exact withdrawal gas limit is mandatory input and must come from
19
- simulation or `eth_estimateGas`; the package contains no token-specific
20
- limits inherited from another protocol.
21
- - Logging is disabled by default. Inject an `ILogger` that follows the host
22
- service's privacy policy if operational events are required.
23
-
24
- The Sepolia OffchainOracle deployment must contain valid rates and must be
25
- protected by the protocol's TWAP/deviation policy. The library cannot create or
26
- initialize that on-chain state. Call `assertReady()` during application startup;
27
- do not accept traffic if it fails.
28
-
29
- ## Installation
30
-
31
- ```bash
32
- yarn add @mixerx/oracles@2.0.0
33
- ```
34
-
35
- Node.js 22.12 or newer is required.
36
-
37
- ## Usage
38
-
39
- ```ts
40
- import {
41
- createMixerxOraclesModule,
42
- type ITokenPriceRepository,
43
- } from '@mixerx/oracles';
44
-
45
- const priceRepository: ITokenPriceRepository = {
46
- getPrice: async (symbol) => readCachedPrice(symbol),
47
- getLastUpdateTimestamp: async () => readLastPriceUpdate(),
48
- };
49
-
50
- const oracles = createMixerxOraclesModule({
51
- chainId: 11155111,
52
- rpcUrl: process.env.SEPOLIA_RPC_URL!,
53
- relayerFeePercent: 0.004,
54
- priceRepository,
55
- maxGasPriceGwei: 100,
56
- maxPriceAgeMs: 120_000,
57
- });
58
-
59
- await oracles.assertReady();
60
-
61
- const fee = await oracles.mixerXFeeOracle.calculateWithdrawalFee({
62
- currency: 'fds',
63
- amount: 1_000_000_000_000_000_000n,
64
- decimals: 18,
65
- gasLimit: estimatedWithdrawalGas,
66
- refund: '0',
67
- purpose: 'fee',
68
- });
69
-
70
- console.log({
71
- totalFee: fee.totalFee,
72
- tokenPriceInEth: fee.tokenPriceInEth,
73
- priceSource: fee.priceSource,
74
- priceObservedAtMs: fee.priceObservedAtMs,
75
- baseFeePerGas: fee.baseFeePerGas,
76
- });
77
- ```
78
-
79
- `fee_quote` and `fee_validation` remain accepted as input aliases for migration,
80
- but both normalize to the single `fee` policy in returned data.
81
-
82
- ## Price-feed operation
83
-
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.
88
-
89
- The default Multicall3 address is
90
- `0xcA11bde05977b3631167028862bE2a173976CA11`; an explicit verified address can
91
- be supplied with `multicallAddress`.
92
-
93
- ## Development
94
-
95
- ```bash
96
- yarn install --immutable
97
- yarn validate
98
- yarn npm audit --all --recursive --severity high
99
- ```
100
-
101
- CI executes those gates for every pull request. Published packages pin the
102
- protocol configuration dependency exactly.
103
-
104
- ## License
105
-
106
- GPL-3.0-only. See `LICENSE`.
1
+ # MixerX Oracles
2
+
3
+ Fail-closed Ethereum pricing and gas policy for MixerX. Version 3.1.0 rejects a
4
+ withdrawal quote when its gas or token price is missing, stale or economically
5
+ unsafe.
6
+
7
+ ## Security contract
8
+
9
+ - Ethereum mainnet (`1`) and Sepolia (`11155111`) are accepted, as defined by
10
+ `@mixerx/config`. A fork must report a supported chain ID.
11
+ - RPC endpoints must use HTTPS, are time-bounded and are checked against the
12
+ real `eth_chainId` response.
13
+ - Token calls use Multicall3 atomically at one block. Any failed token rejects
14
+ the batch; no historical constant is substituted.
15
+ - Non-native withdrawal fees require a repository price plus its observation
16
+ timestamp. The default maximum age is two minutes.
17
+ - Gas data, service fee, refund, token decimals and total fee are bounded before
18
+ a result is returned. The default total-fee ceiling matches the contracts at
19
+ 10% of the withdrawal amount.
20
+ - Configurable token-price sanity bands can reject corrupted but fresh cache
21
+ values.
22
+ - The exact withdrawal gas limit is mandatory input and must come from
23
+ simulation or `eth_estimateGas`; the package contains no token-specific
24
+ limits inherited from another protocol.
25
+ - Logging is disabled by default. Inject an `ILogger` that follows the host
26
+ service's privacy policy if operational events are required.
27
+
28
+ The selected network's OffchainOracle deployment must contain valid rates and must be
29
+ protected by the protocol's TWAP/deviation policy. The library cannot create or
30
+ initialize that on-chain state. Call `assertReady()` during application startup;
31
+ do not accept traffic if it fails.
32
+
33
+ Mainnet addresses in Config 1.1.0 are provisional copies of Sepolia. Accepting
34
+ chain ID `1` only enables backend configuration; it does not establish that
35
+ contracts or oracle rates exist. Readiness and pricing still fail when required
36
+ on-chain data is absent or the RPC reports another chain.
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ yarn add @mixerx/oracles@3.1.0
42
+ ```
43
+
44
+ Node.js 24.12.0 or newer is required.
45
+
46
+ ## Usage
47
+
48
+ ```ts
49
+ import {
50
+ createMixerxOraclesModule,
51
+ type ITokenPriceRepository,
52
+ } from '@mixerx/oracles';
53
+
54
+ const priceRepository: ITokenPriceRepository = {
55
+ getQuote: async (symbol) => readCachedQuote(symbol),
56
+ };
57
+
58
+ const oracles = createMixerxOraclesModule({
59
+ chainId: 11155111,
60
+ rpcUrl: process.env.SEPOLIA_RPC_URL!,
61
+ relayerFeePercent: 0.004,
62
+ priceRepository,
63
+ maxGasPriceGwei: 100,
64
+ maxPriceAgeMs: 120_000,
65
+ maxTotalFeeBps: 1_000,
66
+ priceBounds: {
67
+ fds: {
68
+ minimumWeiPerToken: minimumAcceptedFdsPrice,
69
+ maximumWeiPerToken: maximumAcceptedFdsPrice,
70
+ },
71
+ },
72
+ });
73
+
74
+ await oracles.assertReady();
75
+
76
+ const fee = await oracles.mixerXFeeOracle.calculateWithdrawalFee({
77
+ currency: 'fds',
78
+ amount: 1_000_000_000_000_000_000n,
79
+ decimals: 18,
80
+ gasLimit: estimatedWithdrawalGas,
81
+ refund: '0',
82
+ purpose: 'fee',
83
+ });
84
+
85
+ console.log({
86
+ totalFee: fee.totalFee,
87
+ tokenPriceInEth: fee.tokenPriceInEth,
88
+ priceSource: fee.priceSource,
89
+ priceObservedAtMs: fee.priceObservedAtMs,
90
+ baseFeePerGas: fee.baseFeePerGas,
91
+ effectiveGasPriceWei: fee.gasPriceWei,
92
+ gasSource: fee.gasSource,
93
+ });
94
+ ```
95
+
96
+ `fee_quote` and `fee_validation` remain accepted as input aliases for migration,
97
+ but both normalize to the single `fee` policy in returned data.
98
+
99
+ ## Price-feed operation
100
+
101
+ `fetchPrices()` reads all tokens configured for the selected network through Multicall3 at one
102
+ explicit block. It returns the complete price map together with `blockNumber`
103
+ and `observedAtMs`, and only succeeds when every token succeeds. The host price
104
+ feed must persist each price and its observation metadata atomically. Fee
105
+ calculation never falls back from that repository to a spot value.
106
+
107
+ The default Multicall3 address is
108
+ `0xcA11bde05977b3631167028862bE2a173976CA11`; an explicit verified address can
109
+ be supplied with `multicallAddress`.
110
+
111
+ ## Development
112
+
113
+ ```bash
114
+ yarn install --immutable
115
+ yarn validate
116
+ yarn npm audit --all --recursive --severity high
117
+ ```
118
+
119
+ CI executes those gates for every pull request. Published packages pin the
120
+ protocol configuration dependency exactly.
121
+
122
+ ## License
123
+
124
+ GPL-3.0-only. See `LICENSE`.
@@ -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
@@ -7,7 +7,7 @@ const exceptions_1 = require("../../domain/exceptions");
7
7
  const gasOracleConfigs_1 = require("../config/gasOracleConfigs");
8
8
  const DEFAULT_RPC_TIMEOUT_MS = 10_000;
9
9
  function createVerifiedJsonRpcProvider(rpcUrl, chainId, options, timeoutMs = DEFAULT_RPC_TIMEOUT_MS) {
10
- (0, gasOracleConfigs_1.assertSepoliaChainId)(chainId);
10
+ (0, gasOracleConfigs_1.assertSupportedChainId)(chainId);
11
11
  validateRpcUrl(rpcUrl);
12
12
  if (!Number.isInteger(timeoutMs) || timeoutMs < 1_000 || timeoutMs > 60_000) {
13
13
  throw new exceptions_1.OracleError('INVALID_CONFIG', 'rpcTimeoutMs must be an integer between 1000 and 60000');
@@ -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
- (0, gasOracleConfigs_1.assertSepoliaChainId)(numericChainId);
10
+ (0, gasOracleConfigs_1.assertSupportedChainId)(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
@@ -1,3 +1,2 @@
1
1
  export type ChainId = number;
2
- export declare const SEPOLIA_CHAIN_ID = 11155111;
3
- export declare function assertSepoliaChainId(chainId: number): void;
2
+ export declare function assertSupportedChainId(chainId: number): void;
@@ -1,12 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.SEPOLIA_CHAIN_ID = void 0;
4
- exports.assertSepoliaChainId = assertSepoliaChainId;
3
+ exports.assertSupportedChainId = assertSupportedChainId;
4
+ const config_1 = require("@mixerx/config");
5
5
  const exceptions_1 = require("../../domain/exceptions");
6
- exports.SEPOLIA_CHAIN_ID = 11_155_111;
7
- function assertSepoliaChainId(chainId) {
8
- if (chainId !== exports.SEPOLIA_CHAIN_ID) {
9
- throw new exceptions_1.OracleError('INVALID_CONFIG', `Unsupported chainId ${String(chainId)}; @mixerx/oracles is Sepolia-only`);
6
+ function assertSupportedChainId(chainId) {
7
+ if (!(0, config_1.isSupportedNetId)(chainId)) {
8
+ throw new exceptions_1.OracleError('INVALID_CONFIG', `Unsupported chainId ${String(chainId)}; supported networks: ${config_1.SUPPORTED_NET_IDS.join(', ')}`);
10
9
  }
11
10
  }
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