@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.
- package/README.md +31 -19
- 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 +0 -1
- package/dist/infrastructure/config/ProtocolTokenConfig.js +5 -5
- package/dist/infrastructure/config/gasOracleConfigs.js +0 -1
- 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 +10 -7
- 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 +62 -56
- 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,8 +1,8 @@
|
|
|
1
1
|
# MixerX Oracles
|
|
2
2
|
|
|
3
|
-
Fail-closed Sepolia pricing and gas policy for MixerX. Version
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
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@
|
|
35
|
+
yarn add @mixerx/oracles@3.0.0
|
|
33
36
|
```
|
|
34
37
|
|
|
35
|
-
Node.js
|
|
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
|
-
|
|
47
|
-
|
|
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
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|
@@ -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
|
|
@@ -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
|
|
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,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
|
|
@@ -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
|
|
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
|
|
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
|
}
|