@rhinestone/service-sdk 0.2.17 → 0.2.18

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 CHANGED
@@ -47,26 +47,28 @@ orchestrator **bills** `maxFeePerGas`, but on-chain a transaction only ever pays
47
47
 
48
48
  **Three estimation strategies**, selected per chain:
49
49
 
50
- - **Windowed `eth_feeHistory`** (mainnet, sepolia, plasma) — pools tips across a 20-block
51
- window and prices the base from the *next* block, escalating to a higher reward
50
+ - **Windowed `eth_feeHistory`** (mainnet, sepolia, plasma, bsc) — pools tips across a
51
+ 20-block window and prices the base from the *next* block, escalating to a higher reward
52
52
  percentile when the window's mean `gasUsedRatio` shows congestion. Chains land here
53
- either for a volatile base fee (the L1s) or a broken tip oracle (plasma, whose
54
- `eth_maxPriorityFeePerGas` reports ~3.5x what fills actually clear at).
55
- - **viem's estimate with a tip floor** (base, bsc, polygon, gnosis, hyperEVM, monad) — for
53
+ for a volatile base fee (the L1s), a broken tip oracle (plasma, whose
54
+ `eth_maxPriorityFeePerGas` reports ~3.5x what fills actually clear at), or a floor-pinned
55
+ one (bsc, whose oracle reports the 0.05 gwei chain-min even when blocks are 98% full and
56
+ clearing at 0.1).
57
+ - **viem's estimate with a tip floor** (base, polygon, gnosis, hyperEVM, monad) — for
56
58
  chains whose base fee is stable but whose tip oracle *under*-reports. Polygon and base
57
59
  additionally window the feeHistory tip, since their real tip drifts above a static floor.
58
60
  - **viem's estimate plain** — everything else.
59
61
 
60
62
  On both viem paths a per-chain tip **cap** also applies where the oracle *over*-reports
61
- (optimism, soneium, sonic, avalanche, bsc). Cap and floor can both be set for one chain —
62
- bsc is — in which case the cap applies first and the floor last, so a floor is never
63
- undercut.
63
+ (optimism, soneium, sonic, avalanche). Cap and floor can both be set for one chain, in
64
+ which case the cap applies first and the floor last, so a floor is never undercut. No cap
65
+ applies on the windowed path: a chain moves there precisely so its bid can follow the market.
64
66
 
65
67
  `maxFeePerGas` is rebuilt as `nextBlockBaseFee * multiplier + tip` — the next block's base,
66
68
  not the latest mined one, which is where most of the over-charge disappears. The multiplier
67
69
  is per-chain and each path has its own table: 1.05x by default on the viem paths (overridden
68
70
  for hyperEVM, gnosis, avalanche), and part of the per-chain fee config on the windowed path
69
- (1.30x sepolia, 1.60x mainnet, 2.00x plasma).
71
+ (1.30x sepolia, 1.60x mainnet, 2.00x plasma, 1.00x bsc whose base fee is 0).
70
72
 
71
73
  **`baseFeePerGas` in the result** lets a caller derive the expected price without a second
72
74
  `eth_feeHistory` round trip. It matters because `maxFeePerGas` is a submission *cap* — right
@@ -1 +1 @@
1
- {"version":3,"file":"chains.d.ts","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,YAAY,EAAE,MAAM,MAAM,CAAA;AAmB3E,oDAAoD;AAMpD,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,YAAY,EACZ,oBAAoB,GAAG,eAAe,CACvC,GACC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AAEtC,2CAA2C;AAC3C,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8CG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG;IAChD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B,CAAA;AAyWD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAqE3B"}
1
+ {"version":3,"file":"chains.d.ts","sourceRoot":"","sources":["../src/chains.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,gBAAgB,EAAmB,YAAY,EAAE,MAAM,MAAM,CAAA;AAmB3E,oDAAoD;AAMpD,MAAM,MAAM,mBAAmB,GAAG,IAAI,CACpC,YAAY,EACZ,oBAAoB,GAAG,eAAe,CACvC,GACC,OAAO,CAAC,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC,CAAA;AAEtC,2CAA2C;AAC3C,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;;;;;OAOG;IACH,IAAI,CAAC,EAAE,QAAQ,CAAA;IACf;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;OA8CG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAA;CAC/B,CAAA;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,MAAM,gBAAgB,GAAG,gBAAgB,GAAG;IAChD,aAAa,EAAE,MAAM,GAAG,IAAI,CAAA;CAC7B,CAAA;AAqZD;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,mBAAmB,EAC3B,OAAO,EAAE,MAAM,EACf,OAAO,CAAC,EAAE,sBAAsB,GAC/B,OAAO,CAAC,gBAAgB,CAAC,CAqE3B"}
package/dist/chains.js CHANGED
@@ -12,7 +12,12 @@ const viemDefaults_1 = require("./viemDefaults");
12
12
  // made the latest-block-only read jumpy;
13
13
  // - broken-tip-oracle chains (Plasma): eth_maxPriorityFeePerGas over-prices
14
14
  // the tip by orders of magnitude vs the chain's real rewards, and this path
15
- // never consults the oracle — see FEE_CONFIG_OVERRIDES for the specifics.
15
+ // never consults the oracle — see FEE_CONFIG_OVERRIDES for the specifics;
16
+ // - floor-pinned-oracle chains (BSC): the oracle reports the validators'
17
+ // configured minimum no matter how full blocks are, because geth's GPO
18
+ // samples the cheapest tx per block and a few min-price txs squeeze into
19
+ // every block. A static floor+cap around that number can never follow a
20
+ // congested market; only the rewards of included txs can (RHI-6956).
16
21
  // Other chains stay on viem's default (some with a tip-floor overlay, see
17
22
  // TIP_FLOOR_WEI): their base fee is stable or sequencer-pinned, so windowing
18
23
  // adds nothing.
@@ -20,6 +25,7 @@ const HISTORICAL_FEE_CHAINS = new Set([
20
25
  chains_1.sepolia.id,
21
26
  chains_1.mainnet.id,
22
27
  chains_1.plasma.id,
28
+ chains_1.bsc.id,
23
29
  ]);
24
30
  const DEFAULT_FEE_CONFIG = {
25
31
  historyBlocks: 20, // Geth FullNodeGPO window
@@ -140,6 +146,43 @@ const FEE_CONFIG_OVERRIDES = {
140
146
  // every lag from 1 to 50 blocks. The floor only sets what we CHARGE.
141
147
  tipFloorWei: 1400000n,
142
148
  },
149
+ // BSC is on the historical path because its oracle is pinned to the chain-min,
150
+ // not because its base is volatile (base fee is 0, so the tip IS the bid).
151
+ //
152
+ // Incident 2026-09-10 (RHI-6956): blocks ran 97-98% full for ~2h with
153
+ // feeHistory rewards at 0.1 gwei from p10 up, while eth_gasPrice and
154
+ // eth_maxPriorityFeePerGas kept reporting the 0.05 chain-min. The previous
155
+ // viem-path config (static floor 0.05, static cap 0.055 — "realized 0.0542,
156
+ // market p50 0.052", measured 2026-07) could not bid above 0.055 whatever the
157
+ // market did, so every fill sat unmined for 5-8 minutes per nonce and landed
158
+ // 20+ minutes late, reverted. The tip cap does not apply on this path, which
159
+ // is the point: the bid follows the included-tx rewards.
160
+ //
161
+ // Percentiles simulated against live feeHistory at 8 points over the prior
162
+ // week (20-block windows, nearest-rank p50 across the window):
163
+ //
164
+ // window ratio p10/p25 p25/p50 p50/p75
165
+ // crunch (2026-09-10) 0.97 0.100 0.100 0.100
166
+ // onset (~2h earlier) 0.97 0.053 0.073 0.100
167
+ // busy (3 days prior) 0.56 0.050 0.057 0.117
168
+ // calm (2 samples) 0.21 0.050 0.050 0.050-0.051
169
+ //
170
+ // p25/p50 (mainnet's config) reacts at onset and never exceeds the market
171
+ // median; p50/p75 doubles the price on merely-busy days. BSC crosses the 0.5
172
+ // congestion ratio routinely, so the congested column is the common case.
173
+ [chains_1.bsc.id]: {
174
+ // Base fee is 0 on BSC, so this is inert; 100 states that rather than
175
+ // inheriting a headroom figure that means nothing here.
176
+ baseFeeMultiplierPct: 100n,
177
+ tipPercentile: 25,
178
+ congestedTipPercentile: 50,
179
+ // 0.05 gwei — the post-Maxwell chain-min. Carried over from the old static
180
+ // floor (probed 2026-07-13): it guards an upstream that reports 0 rewards,
181
+ // which on a zero-base chain would zero the ENTIRE bid. The shared 0.1 gwei
182
+ // default would double every calm-market BSC quote, where all included txs
183
+ // pay exactly the min and every percentile reads 0.05.
184
+ tipFloorWei: 50000000n,
185
+ },
143
186
  };
144
187
  // Chains where viem's default base-fee estimate is fine but its tip oracle
145
188
  // (eth_maxPriorityFeePerGas) under-prices the priority fee versus the real
@@ -149,7 +192,6 @@ const FEE_CONFIG_OVERRIDES = {
149
192
  // RHI-4468 audit and subsequent fill-tracker probes; each entry dated).
150
193
  const TIP_FLOOR_WEI = {
151
194
  [chains_1.base.id]: 3000000n, // 0.003 gwei — oracle ~0.001 (network ~p25); real fills ~0.003-0.0036 (probed 2026-07-08)
152
- [chains_1.bsc.id]: 50000000n, // 0.05 gwei — post-Maxwell chain-min; oracle intermittently returns 0 through some upstreams, and with BSC's zero base fee that zeroes the ENTIRE bid (probed 2026-07-13)
153
195
  [chains_1.polygon.id]: 45000000000n, // 45 gwei — Bor min 30 gwei x1.5; real p50 tip ~200 gwei
154
196
  // 100 wei. Gnosis operates at a scale where gwei is the wrong unit to think
155
197
  // in: its median base fee is 118 WEI (p10 24, p90 278k), and included-tx tips
@@ -200,8 +242,8 @@ const WINDOWED_TIP_CHAINS = new Set([chains_1.polygon.id, chains_1.base.id]);
200
242
  // above what transactions on that chain actually pay to get included.
201
243
  //
202
244
  // Each value is set from the strongest evidence that chain has. Where we fill
203
- // often enough for our own realized tip to mean something (Optimism, Soneium,
204
- // BSC) the cap sits at that realized price. Where we do not (Sonic, Avalanche:
245
+ // often enough for our own realized tip to mean something (Optimism, Soneium)
246
+ // the cap sits at that realized price. Where we do not (Sonic, Avalanche:
205
247
  // 3-5 fills/wk, too few to be anything but noise — Gnosis' realized price over
206
248
  // 3 fills came out BELOW its own base fee, which is impossible) it is set from
207
249
  // eth_feeHistory rewards, the tips included transactions actually paid, at
@@ -217,7 +259,9 @@ const TIP_CAP_WEI = {
217
259
  [chains_1.soneium.id]: 700000n, // 0.0007 gwei — realized tip 0.000706 across 1198 fills/wk (so this clamps a hair under realized), vs a flat 0.001 oracle quote
218
260
  [chains_1.sonic.id]: 100000n, // 0.0001 gwei — deliberately 5x the 0.00002 market p90, since 3 fills/wk is too thin to trust; still a ~5000x cut from the 0.525 the oracle drove
219
261
  [chains_1.avalanche.id]: 1000000n, // 0.001 gwei — market p75/p90; charged tip was 0.0117
220
- [chains_1.bsc.id]: 55000000n, // 0.055 gwei — realized tip 0.0542, market p50 0.052; the oracle drifts to 0.0629 above the 0.05 chain-min
262
+ // BSC's 0.055 cap was removed 2026-09-10 (RHI-6956): it doubled as the ceiling
263
+ // of what the relayer could bid, and held fills at 0.055 while the market
264
+ // cleared at 0.1. BSC now prices from feeHistory rewards (HISTORICAL_FEE_CHAINS).
221
265
  };
222
266
  const WINDOWED_TIP_HISTORY_BLOCKS = 20;
223
267
  // Stays at p50 for both windowed chains. Polygon looked like a candidate for a
@@ -545,14 +589,16 @@ function recordDegradedEstimate(chainId, reason) {
545
589
  span.addEvent('gas.estimate.degraded', { reason, 'chain.id': chainId });
546
590
  }
547
591
  /**
548
- * Cap for the windowed estimator when eth_feeHistory answered without a base
549
- * fee. Recovers the base implied by viem's own estimate and re-applies this
550
- * chain's multiplier, never returning less than viem's cap or the tip itself.
592
+ * Cap for the windowed estimator when eth_feeHistory could not supply a base
593
+ * fee (answered without one, or failed outright). Recovers the base implied by
594
+ * viem's own estimate and re-applies this chain's multiplier, never returning
595
+ * less than viem's cap or the tip itself.
551
596
  *
552
- * Deliberately does NOT adopt viem's tip — see the call site.
597
+ * Takes viem's `fees` rather than the client so a caller that already read them
598
+ * (the outage path, which also needs the oracle tip) does not read them twice.
599
+ * Deliberately does NOT adopt viem's tip — see the call sites.
553
600
  */
554
- async function viemFallbackCap(client, config, priorityFee) {
555
- const fees = await client.estimateFeesPerGas();
601
+ function viemFallbackCap(fees, client, config, priorityFee) {
556
602
  const oracleTip = fees.maxPriorityFeePerGas;
557
603
  const viemCap = fees.maxFeePerGas + (priorityFee - oracleTip);
558
604
  let cap = viemCap;
@@ -575,12 +621,32 @@ async function estimateFeeHistoryGasFees(client, chainId, config, tipPercentileO
575
621
  // #1:60.000000", verified live 2026-08-10 against publicnode and 1rpc — so the
576
622
  // duplicate form is an RPC error, not a pricing choice.
577
623
  const overridden = tipPercentileOverride !== undefined;
578
- const feeHistory = await client.getFeeHistory({
579
- blockCount: config.historyBlocks,
580
- rewardPercentiles: overridden
581
- ? [tipPercentileOverride]
582
- : [config.tipPercentile, config.congestedTipPercentile],
583
- });
624
+ let feeHistory;
625
+ try {
626
+ feeHistory = await client.getFeeHistory({
627
+ blockCount: config.historyBlocks,
628
+ rewardPercentiles: overridden
629
+ ? [tipPercentileOverride]
630
+ : [config.tipPercentile, config.congestedTipPercentile],
631
+ });
632
+ }
633
+ catch {
634
+ // eth_feeHistory failed outright. A feeHistory outage must never become a
635
+ // quote outage (CLAUDE.md), and until RHI-6956 this path had no fallback —
636
+ // the viem paths always did. There is no window to price from, so the only
637
+ // tip source left is viem's oracle, floored: on BSC that is exactly the
638
+ // pre-RHI-6956 pricing (chain-min), on Plasma it is the ~3.5x over-priced
639
+ // oracle this path exists to avoid — degraded, and recorded as such, but a
640
+ // quote. The base is recovered from viem's cap as in the missing-base case.
641
+ recordDegradedEstimate(chainId, 'feehistory_error');
642
+ const fees = await client.estimateFeesPerGas();
643
+ const priorityFee = (0, tipClamp_1.clampTip)(fees.maxPriorityFeePerGas, config.tipFloorWei, undefined);
644
+ return {
645
+ maxFeePerGas: viemFallbackCap(fees, client, config, priorityFee),
646
+ maxPriorityFeePerGas: priorityFee,
647
+ baseFeePerGas: null,
648
+ };
649
+ }
584
650
  const ratios = feeHistory.gasUsedRatio ?? [];
585
651
  const meanRatio = ratios.length > 0
586
652
  ? ratios.reduce((sum, r) => sum + r, 0) / ratios.length
@@ -613,8 +679,9 @@ async function estimateFeeHistoryGasFees(client, chainId, config, tipPercentileO
613
679
  // carrying the most fills through our pricing. The tip we already computed
614
680
  // is the calibrated one.
615
681
  recordDegradedEstimate(chainId, 'feehistory_missing_base');
682
+ const fees = await client.estimateFeesPerGas();
616
683
  return {
617
- maxFeePerGas: await viemFallbackCap(client, config, priorityFee),
684
+ maxFeePerGas: viemFallbackCap(fees, client, config, priorityFee),
618
685
  maxPriorityFeePerGas: priorityFee,
619
686
  baseFeePerGas: null,
620
687
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rhinestone/service-sdk",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "Shared SDK for Rhinestone services",
5
5
  "author": {
6
6
  "name": "Rhinestone",