@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.
- package/README.md +124 -106
- package/dist/application/ports/ILogger.js +0 -1
- package/dist/domain/config/TokenDefaults.js +0 -1
- package/dist/domain/exceptions.d.ts +1 -1
- package/dist/domain/exceptions.js +0 -1
- package/dist/domain/repositories.d.ts +1 -2
- package/dist/domain/repositories.js +0 -1
- package/dist/factory.d.ts +5 -15
- package/dist/factory.js +2 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.js +0 -1
- package/dist/infrastructure/blockchain/staticProvider.js +1 -2
- package/dist/infrastructure/config/ProtocolTokenConfig.js +6 -6
- package/dist/infrastructure/config/gasOracleConfigs.d.ts +1 -2
- package/dist/infrastructure/config/gasOracleConfigs.js +5 -7
- package/dist/infrastructure/gas/GasPriceOracle.d.ts +5 -18
- package/dist/infrastructure/gas/GasPriceOracle.js +51 -45
- package/dist/infrastructure/mixerx/MixerXFeeOracle.d.ts +16 -5
- package/dist/infrastructure/mixerx/MixerXFeeOracle.js +110 -38
- package/dist/infrastructure/multicall/MulticallProvider.d.ts +5 -1
- package/dist/infrastructure/multicall/MulticallProvider.js +11 -8
- package/dist/infrastructure/token-price/TokenPriceOracle.d.ts +3 -5
- package/dist/infrastructure/token-price/TokenPriceOracle.js +11 -10
- package/dist/types.d.ts +6 -1
- package/dist/types.js +0 -1
- package/package.json +11 -5
- package/dist/application/ports/ILogger.js.map +0 -1
- package/dist/application/ports/index.d.ts +0 -5
- package/dist/application/ports/index.js +0 -18
- package/dist/application/ports/index.js.map +0 -1
- package/dist/domain/config/TokenDefaults.js.map +0 -1
- package/dist/domain/config/index.d.ts +0 -1
- package/dist/domain/config/index.js +0 -8
- package/dist/domain/config/index.js.map +0 -1
- package/dist/domain/exceptions.js.map +0 -1
- package/dist/domain/repositories.js.map +0 -1
- package/dist/factory.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/infrastructure/blockchain/staticProvider.js.map +0 -1
- package/dist/infrastructure/config/ProtocolTokenConfig.js.map +0 -1
- package/dist/infrastructure/config/gasOracleConfigs.js.map +0 -1
- package/dist/infrastructure/config/index.d.ts +0 -4
- package/dist/infrastructure/config/index.js +0 -11
- package/dist/infrastructure/config/index.js.map +0 -1
- package/dist/infrastructure/gas/GasPriceOracle.js.map +0 -1
- package/dist/infrastructure/mixerx/MixerXFeeOracle.js.map +0 -1
- package/dist/infrastructure/multicall/MulticallProvider.js.map +0 -1
- package/dist/infrastructure/token-price/TokenPriceOracle.js.map +0 -1
- package/dist/infrastructure/token-price/index.d.ts +0 -3
- package/dist/infrastructure/token-price/index.js +0 -6
- package/dist/infrastructure/token-price/index.js.map +0 -1
- package/dist/types.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,106 +1,124 @@
|
|
|
1
|
-
# MixerX Oracles
|
|
2
|
-
|
|
3
|
-
Fail-closed
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
## Security contract
|
|
8
|
-
|
|
9
|
-
- Sepolia (`11155111`)
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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`.
|
|
@@ -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);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import type { TokenPriceQuote } from '../types';
|
|
2
2
|
export interface ITokenPriceRepository {
|
|
3
|
-
|
|
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;
|
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,
|
|
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
|
-
|
|
20
|
-
|
|
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<
|
|
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.
|
|
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.
|
|
10
|
+
(0, gasOracleConfigs_1.assertSupportedChainId)(numericChainId);
|
|
10
11
|
if (!(0, config_1.isSupportedNetId)(numericChainId)) {
|
|
11
|
-
throw new
|
|
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
|
|
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 (
|
|
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
|
|
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,12 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
4
|
-
|
|
3
|
+
exports.assertSupportedChainId = assertSupportedChainId;
|
|
4
|
+
const config_1 = require("@mixerx/config");
|
|
5
5
|
const exceptions_1 = require("../../domain/exceptions");
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
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 &&
|
|
43
|
-
|
|
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:
|
|
53
|
+
maxPriorityFeePerGas: cappedPriorityFeePerGas.toString(),
|
|
50
54
|
};
|
|
51
55
|
}
|
|
52
|
-
if (feeData.gasPrice) {
|
|
53
|
-
|
|
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 &&
|
|
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
|
|
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
|